TensorFlow 2.x 工程实践:从计算图、tf.data到SavedModel与TFLite部署
1. 为什么“tensorflow”这个标题值得单独拎出来聊“tensorflow”这个词放在今天的技术语境里已经不只是某个库的名字了。它更像是一个入口——一个把数学表达、硬件调度、生产部署串起来的工程体系。很多人第一次接触它是在教程里跑MNIST手写数字识别几行代码就能出结果感觉不过如此但真正把它放进项目里才会发现坑不在模型本身而在数据管道、显存管理、版本兼容、部署形态这些“脏活累活”上。我写这篇东西的出发点很简单网上关于TensorFlow的入门内容已经足够多了但大多数停留在“怎么调用API”的层面很少有人把“为什么这么设计”“什么场景该选哪条路”“踩过的坑长什么样”讲透。这篇博文面向的是已经能跑通基础示例、准备把模型往真实业务里塞的开发者也适合那些用PyTorch顺手、但需要维护TensorFlow存量项目的同学。核心关键词会围绕计算图、Keras、tf.data、SavedModel、TFLite、分布式策略这些实际工作中绕不开的点展开。需要先说明一点TensorFlow的版本差异极大。1.x和2.x几乎是两个物种2.x内部不同小版本之间也有行为变化。我下面讲的内容默认以2.x为主线涉及1.x的地方会单独标注。如果你手上是更早的代码建议先做迁移评估别硬扛。2. 整体设计思路它到底想解决什么问题2.1 从“定义计算”到“端到端平台”的定位演变TensorFlow最早的核心卖点其实是“数据流图”。你把运算写成节点和边框架负责调度这样同一份计算描述可以放到CPU、GPU甚至分布式集群上跑。这个抽象在1.x时代非常明显tf.Session、tf.placeholder、tf.Graph是绕不开的三件套。它的好处是图一旦构建好优化空间大、跨设备部署方便坏处是调试困难写起来像在写另一种语言。2.x引入Eager Execution作为默认模式之后写起来跟普通Python几乎一样tf.Tensor可以直接打印值条件判断和循环也能用Python原生语法。这看起来是向PyTorch靠拢但底层并没有放弃图的能力——tf.function可以把Python函数追踪成图兼顾开发效率和执行性能。这个“动态优先、静态可选”的设计是理解现代TensorFlow的关键。我个人的判断是如果你的项目需要从训练到移动端、浏览器、服务端全链路覆盖TensorFlow的生态完整度仍然很有优势如果你只做研究原型、追求极致的调试体验那另一套方案可能更顺手。选型没有绝对对错关键是看你的交付形态。2.2 分层架构Keras、核心算子与运行时TensorFlow的代码组织大致可以分成几层。最上层是Keras它现在是官方推荐的高层API负责模型定义、训练循环、回调这些。中间层是核心算子库比如tf.linalg、tf.nn、tf.image提供细粒度控制。最下层是运行时包括设备管理、内存分配、图优化、分布式通信。这种分层带来的直接好处是你可以用Keras快速搭原型遇到Keras不支持的算子时下沉到核心层再不行还可以写自定义Op。但分层也意味着抽象泄漏——高层API报错时错误信息可能来自底层运行时排查起来需要你对整体结构有概念。提示遇到Keras报错先别急着改模型很多时候问题出在输入数据的shape或dtype上往上游查一层往往更快。2.3 为什么它强调“生产部署”而不是只做训练很多框架的训练体验很好但把模型搬到线上时要重新写一遍推理代码。TensorFlow从早期就在推SavedModel这个统一格式训练完导出成一个目录里面包含计算图和权重服务端、移动端、浏览器都能加载。这个设计思路是“一次训练多处部署”代价是导出环节有学习成本签名、输入输出名称、动态shape这些都需要配置清楚。我见过不少项目在训练阶段很顺利卡在导出上花了两三天。所以我的建议是从项目第一天就把导出和加载推理纳入流程别等到最后再补。3. 核心细节解析那些文档里不会重点讲的部分3.1 张量与自动微分一切运算的基础tf.Tensor是不可变对象这一点和NumPy数组不同。你没法对某个元素直接赋值所有“修改”本质上都是创建新张量。这个设计是为了让框架能追踪计算依赖从而做自动微分。tf.GradientTape是2.x里求梯度的核心工具它记录前向过程中的操作反向时自动计算梯度。用GradientTape时有几个细节容易翻车。第一默认它只追踪tf.Variable和可训练张量常量不会被记录第二tape.gradient()调用一次后默认释放资源如果要多次求导需要设persistentTrue第三在tf.function内部使用GradientTape时追踪行为会受图模式影响调试信息不如Eager模式直观。import tensorflow as tf x tf.Variable(3.0) with tf.GradientTape() as tape: y x ** 2 2 * x dy_dx tape.gradient(y, x) print(dy_dx.numpy()) # 8.0这段代码看起来简单但如果你把x换成普通常量gradient会返回None。这个坑我在早期项目里踩过排查了半天才发现是变量类型的问题。3.2 tf.data数据管道的性能决定训练上限模型再快数据喂不上就是白搭。tf.data.Dataset是官方推荐的数据输入方式它的核心优势是能把预处理和训练重叠起来——GPU在算当前batch时CPU已经在准备下一个batch。这个流水线效果靠prefetch、map的并行调用、cache这些操作组合实现。一个典型的管道长这样从文件列表创建Dataset用map做解码和增强shuffle打乱batch组批最后prefetch预取。顺序很重要shuffle要在batch之前否则打乱的是batch内部而不是样本之间。map里的函数如果包含大量计算记得设num_parallel_callstf.data.AUTOTUNE让框架自己决定并行度。dataset tf.data.Dataset.from_tensor_slices(file_paths) dataset dataset.map(load_and_preprocess, num_parallel_callstf.data.AUTOTUNE) dataset dataset.shuffle(buffer_size1000) dataset dataset.batch(32) dataset dataset.prefetch(tf.data.AUTOTUNE)buffer_size的设置也有讲究。太小打乱不充分太大占内存。经验值是数据集样本量的十分之一到全部如果内存够就直接用全量。另外cache要慎用它会把数据缓存在内存或磁盘小数据集很香大数据集可能直接把内存撑爆。3.3 Keras的三种建模方式与选择依据Keras提供Sequential、Functional、Subclassing三种建模方式。Sequential适合线性堆叠几层就写完Functional用函数式API能处理多输入多输出、共享层、残差连接这些结构Subclassing继承tf.keras.Model在call方法里写前向逻辑灵活性最高但可读性和可导出性稍差。我的选择习惯是能用Functional就不用Subclassing因为Functional的模型结构是显式声明的导出SavedModel时更稳定可视化也方便。Subclassing适合研究场景比如需要动态控制流或自定义训练逻辑。Sequential则适合快速验证想法但一旦结构复杂起来就该换。注意Subclassing模型在保存时如果call方法里有Python副作用加载后行为可能不一致导出前务必做一次加载验证。3.4 混合精度与显存优化训练大模型时显存经常是瓶颈。混合精度训练是一个性价比很高的手段前向和反向用float16计算权重更新用float32保留精度。TensorFlow通过tf.keras.mixed_precision提供支持配合LossScaleOptimizer防止梯度下溢。开启方式不复杂但有几个前提GPU要支持float16加速较新的架构基本都支持损失函数和优化器要按规范配置某些算子在float16下数值不稳定需要强制用float32。实测下来混合精度通常能省30%到50%显存速度也有提升但精度损失需要根据任务评估。policy tf.keras.mixed_precision.Policy(mixed_float16) tf.keras.mixed_precision.set_global_policy(policy) model build_model() optimizer tf.keras.optimizers.Adam() optimizer tf.keras.mixed_precision.LossScaleOptimizer(optimizer)这段配置里LossScaleOptimizer是必须的否则float16的梯度很容易变成0。我见过有人只设了policy没包优化器训练loss直接不动排查很久才定位到。4. 实操过程从训练到部署的完整链路4.1 环境准备与版本锁定TensorFlow的版本兼容性是出了名的麻烦。CUDA、cuDNN、Python、TensorFlow四者之间有严格的对应关系装错一个版本就是一堆报错。我的做法是用虚拟环境隔离然后用pip安装指定版本装完立刻跑一段验证代码确认GPU可用。python -m venv tf_env source tf_env/bin/activate pip install tensorflow2.15.0import tensorflow as tf print(tf.__version__) print(tf.config.list_physical_devices(GPU))如果GPU列表为空先检查驱动版本再检查CUDA路径。别急着卸载重装很多时候只是环境变量没配好。另外建议把依赖写进requirements.txt并锁定小版本团队协作时能省掉大量“在我机器上是好的”问题。4.2 数据准备与增强策略数据质量决定模型上限。图像任务里增强是提升泛化能力的常规手段。Keras提供了ImageDataGenerator但更推荐用tf.keras.layers里的预处理层比如RandomFlip、RandomRotation、RandomZoom它们能直接嵌进模型导出时一起带走推理阶段自动跳过。data_augmentation tf.keras.Sequential([ tf.keras.layers.RandomFlip(horizontal), tf.keras.layers.RandomRotation(0.1), tf.keras.layers.RandomZoom(0.1), ])把增强层放进模型的好处是部署时不用额外写预处理代码坏处是模型文件会稍大一点。这个取舍我觉得值得因为线上预处理和训练预处理不一致是极常见的翻车原因。4.3 模型构建、训练与回调配置训练循环用model.fit就够了关键是回调的配置。ModelCheckpoint保存最优权重EarlyStopping防止过拟合ReduceLROnPlateau在指标停滞时降学习率TensorBoard记录曲线。这几个回调组合起来能覆盖大部分训练需求。callbacks [ tf.keras.callbacks.ModelCheckpoint(best.keras, save_best_onlyTrue), tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience3), tf.keras.callbacks.TensorBoard(log_dir./logs), ] model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy]) model.fit(train_ds, validation_dataval_ds, epochs50, callbackscallbacks)patience的设置要看数据集大小和训练轮次。小数据集可以设小一点比如3大数据集设5到10更稳妥。restore_best_weightsTrue很重要否则EarlyStopping触发后模型停在最后一轮而不是最优轮。4.4 导出SavedModel与签名配置训练完的模型要导出成SavedModel才能被其他语言或平台加载。Keras的model.save默认就是SavedModel格式2.x后期也支持.keras格式。导出时要确认输入输出的签名特别是动态batch维度。tf.function(input_signature[tf.TensorSpec([None, 224, 224, 3], tf.float32)]) def serving_fn(inputs): return {outputs: model(inputs, trainingFalse)} tf.saved_model.save(model, saved_model_dir, signatures{serving_default: serving_fn})input_signature里的None表示batch维度可变这样服务端可以处理任意batch大小。如果不指定导出后可能被固定成训练时的batch线上请求数量不匹配就报错。这个细节我在一个服务化项目里踩过当时请求单条数据一直失败最后发现是签名写死了batch32。4.5 移动端与边缘部署TFLite转换移动端部署要把模型转成TFLite格式体积更小、延迟更低。转换时可以做量化把float32权重压成int8模型体积能缩小到四分之一左右速度也有提升代价是精度可能下降一两个点。converter tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) converter.optimizations [tf.lite.Optimize.DEFAULT] tflite_model converter.convert() with open(model.tflite, wb) as f: f.write(tflite_model)量化分动态范围量化和全整型量化。动态范围量化最简单不需要代表性数据集全整型量化需要提供一批校准数据精度更可控但流程更复杂。如果目标设备没有GPU全整型量化往往能带来明显的速度收益。提示转换后一定要用TFLite解释器跑一遍验证对比原模型和量化模型的输出差异别只看文件大小。5. 常见问题与排查技巧实录5.1 显存不足OOM的排查顺序OOM是训练阶段最高频的问题。排查顺序我一般是这样先看batch size是不是太大试着减半再看模型参数量和输入分辨率这两者直接决定激活值大小然后检查是否有不必要的张量被保留比如在GradientTape里累积了中间结果最后考虑混合精度或梯度累积。梯度累积是个实用技巧用小batch跑多次累积梯度后再更新一次权重效果接近大batch但显存占用低。实现上就是在GradientTape里累加梯度到一定步数再apply。5.2 训练loss不下降或震荡loss不动的原因很多按概率排序学习率太大或太小、数据标签没对齐、损失函数选错、梯度消失或爆炸。我的排查习惯是先拿一个极小数据集比如几十条过拟合如果loss能降到接近0说明模型和代码没问题问题在数据或超参如果连小数据集都过拟合不了那就是代码有bug。学习率可以用学习率扫描找合适区间或者直接用ReduceLROnPlateau让框架自己调。标签对齐问题在分类任务里很常见特别是用ImageDataGenerator的flow_from_directory时类别索引和标签映射容易搞混建议打印几个batch的标签确认。5.3 版本兼容与API变更TensorFlow 2.x内部API变动频繁比如tf.keras.preprocessing下的很多工具被标记为deprecated推荐用tf.keras.utils或tf.keras.layers替代。升级版本时建议先看release notes重点看breaking changes部分。常见旧API推荐替代备注tf.placeholdertf.keras.Input1.x到2.x的核心变化tf.SessionEager模式直接执行图模式用tf.functiontf.layers.densetf.keras.layers.Dense高层API统一model.fit_generatormodel.fit直接传Datasettf.image.resize_imagestf.image.resize参数命名调整这张表里的替换不是简单的改名行为可能有细微差异迁移后要重新验证。5.4 分布式训练的配置要点多卡训练用tf.distribute.MirroredStrategy单机多卡场景基本够用。关键是把模型构建、编译、fit都放在strategy.scope()里否则变量不会正确分布。strategy tf.distribute.MirroredStrategy() with strategy.scope(): model build_model() model.compile(optimizeradam, losssparse_categorical_crossentropy) model.fit(train_ds, epochs10)batch size要按卡数放大比如单卡用32四卡就用128这样每张卡的实际batch不变。学习率也可以相应调整但要看具体优化器。分布式训练报错时先确认所有卡都能被识别再检查scope范围是否覆盖了所有变量创建。5.5 模型导出后加载失败的典型原因导出成功不代表加载成功。常见问题包括自定义层没有注册、签名名称不匹配、输入dtype不一致、依赖了训练时的Python对象。自定义层要用tf.keras.utils.register_keras_serializable装饰否则加载时会找不到类定义。tf.keras.utils.register_keras_serializable() class MyLayer(tf.keras.layers.Layer): def call(self, inputs): return inputs * 2加载时用tf.keras.models.load_model如果报未知层错误检查装饰器是否加上、模块是否被导入。这个坑在跨团队交付模型时特别常见建议导出后在一个干净环境里做加载测试。6. 一些实际项目里的取舍经验做了一段时间TensorFlow项目后我越来越觉得框架本身不是难点难的是把训练、评估、导出、服务这条链路打通并保持稳定。有几个习惯我觉得挺值第一从项目开始就写导出和推理的测试用例别等到交付前才补第二数据管道和模型代码分开管理数据出问题时不至于牵连模型第三版本锁定要彻底包括Python、TensorFlow、CUDA和所有第三方库。还有一个容易被忽视的点是日志和指标。训练时的loss曲线、学习率变化、梯度范数这些信息出问题时就是排查线索。TensorBoard虽然老派但胜在稳定建议一直开着。线上服务则要记录输入分布和输出分布模型漂移往往先从数据分布变化开始。最后分享一个小技巧如果你不确定某个算子在不同设备上的行为写一个最小复现脚本分别在CPU和GPU上跑一遍对比输出。很多“玄学”问题其实是数值精度或设备差异导致的最小复现能帮你快速定位。这个习惯帮我省下了不少瞎猜的时间。

相关新闻

手写轻量级资源内联工具:把静态资源自动组装进HTML

手写轻量级资源内联工具:把静态资源自动组装进HTML

先从我手头一个真实的项目说起吧。前段时间我在负责一个活动页的优化,页面不大,但静态资源特别多,十几个小图标、几段内联样式、一两个首屏脚本,单独发请求的话,移动端首屏时间很难看。当时的第一反应是引入打包工具&a…

2026/10/14 4:18:50 阅读更多 →
AnyPS5跨平台串流实战:从架构设计到延迟优化的完整指南

AnyPS5跨平台串流实战:从架构设计到延迟优化的完整指南

1. 从“AnyPS5”这个标题说起:一个跨平台串流工具的设计思路第一次看到“AnyPS5”这个标题,我脑子里蹦出来的第一个念头是:这大概率是一个围绕主机游戏串流展开的项目。为什么这么说?因为“Any”这个前缀在技术圈里几乎已经成了一…

2026/10/14 4:18:49 阅读更多 →
AI工程师核心能力拆解:从零实现、数据敏感度与实验管理实战路径

AI工程师核心能力拆解:从零实现、数据敏感度与实验管理实战路径

1. 这个项目到底在解决什么问题第一次看到“andrej-karpathy-skills”这个标题,很多人会以为这是某个开源代码库或者工具包。实际上,它更像是一份能力清单——把一位顶级AI研究者兼教育者身上那些可迁移、可练习、可复用的技能拆解出来,形成一…

2026/10/14 4:17:49 阅读更多 →

最新新闻

Cocos Creator 水波纹效果:Shader 扰动与交互涟漪实现

Cocos Creator 水波纹效果:Shader 扰动与交互涟漪实现

简介:这份资源是面向Cocos Creator开发者的一套水波纹特效项目源码,适合具备一定Shader与脚本基础、希望在游戏中实现动态水面、魔法或雨滴等视觉效果的开发者参考。压缩包共3个文件,以inscode工程配置、html页面与gitignore为主,…

2026/10/14 5:11:01 阅读更多 →
pyKT知识追踪复现框架:统一评测与训练流水线实践

pyKT知识追踪复现框架:统一评测与训练流水线实践

1. 从零散脚本到统一评测:为什么需要一套知识追踪复现框架做知识追踪方向的研究,最让人头疼的往往不是模型本身,而是复现环节。我见过太多这样的情况:论文里报告了一个漂亮的AUC,你兴冲冲地照着描述搭好网络结构&#…

2026/10/14 5:11:01 阅读更多 →
跌倒检测数据集zip文件实战:从解压到baseline的避坑指南

跌倒检测数据集zip文件实战:从解压到baseline的避坑指南

简介:跌倒检测数据集以zip压缩包形式提供,面向计算机视觉、模式识别与机器学习方向的研究者及算法开发者,用于训练和验证跌倒行为识别模型,尤其适合关注老年人健康监护与公共安全场景的智能系统研发。压缩包共收录2000个文件&…

2026/10/14 5:11:01 阅读更多 →
矩阵90度顺时针旋转的原地实现原理与工程实践

矩阵90度顺时针旋转的原地实现原理与工程实践

1. 项目概述:一道被低估的矩阵操作题,藏着工程思维的底层逻辑“牛客网-旋转矩阵/C翻转【简洁实现】”——光看标题,你可能以为这只是算法刷题平台里一道平平无奇的二维数组练习题。但在我带过的几十个模拟项目X训练营中,这道题是淘…

2026/10/14 5:11:01 阅读更多 →
AI芯片软硬件协同设计:从架构到调优的工程实践

AI芯片软硬件协同设计:从架构到调优的工程实践

1. AI芯片软硬件协同设计的核心逻辑1.1 为什么软硬件必须一起设计很多刚接触AI芯片的朋友会有一个误区,觉得芯片设计就是硬件工程师的事,软件团队等流片回来再适配就行。这个思路在传统通用CPU时代勉强能跑通,但在AI芯片领域基本等于自杀。我…

2026/10/14 5:11:01 阅读更多 →
Go语言流程控制详解:for循环、switch与defer的工程实践

Go语言流程控制详解:for循环、switch与defer的工程实践

如果你写过一段时间的 C 或者 Java,刚转 Go 时,流程控制这块第一感觉多半是:怎么这么少?没有 while,没有 do-while,switch 还自带 break,甚至 for 循环都能当 while 用。我一开始也挺不适应&…

2026/10/14 5:10:01 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →