资讯详情 Keras模型构建三范式:Sequential、Functional与Subclassing详解
📅 2026/10/27 14:16:39
Keras 这东西刚接触深度学习那会儿我把它当成高级封装随手用直到有一次要复现一篇论文里的多输入融合结构才发现自己对它到底有几种模型组织方式根本没搞清楚——Sequential 一把梭写惯了遇到需要共享层、需要多输出、需要中途分叉的场景就卡壳。后来我把 Keras 的模型构建方式系统梳理了一遍从最朴素的顺序堆叠到函数式图结构再到子类化才算真正把模型种类这件事吃透。这篇就围绕 Keras 里几种主流的模型组织形态展开讲清楚它们各自解决什么问题、底层是怎么组织的、什么场景该选哪种以及我在实际项目里踩过的那些坑。不管你是刚学完 TensorFlow 基础想往上走还是已经能跑通 Demo 但一遇到复杂结构就发怵下面这些内容应该都能对上你的需求。1. 先把模型种类这件事的边界划清楚很多人一看到Keras 模型种类这几个字第一反应是不就是 Sequential 和 Functional 两种吗。这个理解不算错但太窄了。Keras 里所谓模型种类本质上是在回答一个问题你用什么方式描述一张计算图。描述方式不同能表达的拓扑结构就不同调试体验、可复用性、导出能力也跟着变。1.1 三种描述范式而不是三个并列的类从组织形态上看Keras 构建模型有三条主线顺序式Sequential层像排队一样首尾相接只有一个输入一个输出中间不允许分叉。函数式Functional把层当成函数调用输入张量进、输出张量出可以任意分叉、合并、共享最终用Model(inputs, outputs)把整张图包起来。子类化Subclassing继承tf.keras.Model在__init__里定义层在call里写前向逻辑前向过程就是普通 Python 代码。这三者不是互斥的三个类而是三种抽象层级。Sequential 其实是 Functional 的一个特例单链图Functional 是声明式地画图Subclassing 是命令式地写图。理解这个层级关系比记住三个 API 名字重要得多。1.2 为什么这个区分值得单独拎出来讲我见过太多项目一开始图省事用 Sequential模型写到一半发现要做残差连接只能推倒重来改成 Functional也见过有人一上来就 Subclassing结果模型结构没法用model.summary()直观看到调结构调到怀疑人生。选错描述范式的代价往往不是多写几行代码而是整个调试和迭代节奏被打乱。提示判断该用哪种先问自己两个问题——模型有没有分叉或合并需不需要在训练中途访问中间层输出前者决定 Sequential 够不够用后者决定 Functional 和 Subclassing 的取舍。下面这张表是我自己总结的快速对照先给个全局印象后面逐项展开。维度SequentialFunctionalSubclassing拓扑表达能力单链任意有向无环图任意含动态控制流结构可视化好好差需额外处理多输入多输出不支持原生支持原生支持层共享不方便自然支持自然支持调试友好度高高中适合场景快速原型、简单堆叠大多数生产模型研究型、动态结构2. Sequential最省心但它的省心有明确代价Sequential 是绝大多数人写的第一个 Keras 模型。它的 API 极简model.add()一层层往上摞就行读起来像一份清单。但正因为太顺手很多人没意识到它到底限制了什么。2.1 它到底限制在哪单输入单输出 线性拓扑Sequential 的本质是一条线性链第 n 层的输出只能是第 n1 层的输入不允许某一层的输出同时喂给两个下游层也不允许两个分支在后面汇合。这意味着残差连接ResNet 那种x f(x)、Inception 那种多分支并行、注意力机制里 query/key/value 的分头处理统统写不了。有人会问那我能不能在 Sequential 里塞一个自定义层把分叉逻辑藏在层内部可以但这等于你把图结构藏进了黑盒summary()看不出来可视化也画不出来本质上已经放弃了 Sequential 的透明性优势不如直接上 Functional。2.2 一个能直接跑的 Sequential 骨架import tensorflow as tf from tensorflow.keras import layers model tf.keras.Sequential([ layers.Input(shape(784,)), layers.Dense(256, activationrelu), layers.Dropout(0.3), layers.Dense(128, activationrelu), layers.Dense(10, activationsoftmax) ], namemlp_baseline) model.compile( optimizertf.keras.optimizers.Adam(1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) model.summary()这里有个细节值得说第一层用layers.Input显式声明输入形状而不是靠input_shape参数。前者是现在推荐的做法因为它让模型在构建时就确定了完整的形状信息summary()能直接列出每层的输出维度也方便后续保存和加载。老代码里常见的Dense(256, input_shape(784,))写法虽然还能用但会在某些导出场景下带来形状推断的麻烦。2.3 Sequential 真正好用的地方别因为上面那些限制就小看它。做基线实验、跑通数据管道、验证一个想法是否值得深入Sequential 的效率无可替代。我个人的习惯是任何新任务先用 Sequential 搭一个最朴素的 MLP 或小型 CNN把数据加载、损失函数、评估指标这条链路跑通确认没有数据层面的低级错误再考虑换更复杂的结构。这个先跑通再优化的顺序能帮你把模型结构问题和数据管道问题分开排查省下大量时间。3. Functional API把模型当成一张可以任意连线的图Functional 是我在真实项目里用得最多的一种。它的核心心智模型很简单层是可调用的函数张量是流动的数据你把输入张量喂进去拿到输出张量最后声明从这些输入到这些输出就是我的模型。3.1 为什么它比 Sequential 表达力强这么多关键在于 Functional 里每一层的调用都会返回一个新的张量你可以把这个张量存下来、复用、和别的张量做运算。这就打开了三扇门分叉一个张量可以同时喂给多个层。合并多个张量的输出可以用Concatenate、Add、Multiply等层合并。共享同一个层对象可以被调用多次权重自动共享。这三件事组合起来几乎能表达所有常见的前馈结构。残差块、多尺度特征融合、双塔召回模型、多任务学习里的共享底层全都是这套机制的排列组合。3.2 手写一个带残差和多输出的结构下面这个例子同时演示了分叉、合并和多输出是我用来讲 Functional 的经典骨架from tensorflow.keras import layers, Model inputs layers.Input(shape(128,), namefeatures) # 主干 x layers.Dense(64, activationrelu)(inputs) x layers.Dense(64, activationrelu)(x) # 残差把输入投影到同维度后相加 shortcut layers.Dense(64)(inputs) x layers.Add()([x, shortcut]) x layers.Activation(relu)(x) # 分叉出两个任务头 cls_out layers.Dense(1, activationsigmoid, namecls)(x) reg_out layers.Dense(1, namereg)(x) model Model(inputsinputs, outputs[cls_out, reg_out], namemulti_task) model.compile( optimizeradam, loss{cls: binary_crossentropy, reg: mse}, loss_weights{cls: 1.0, reg: 0.5} )注意shortcut那一步因为主干的输出维度是 64输入是 128直接相加维度对不上所以要先过一个Dense(64)把维度对齐。这是写残差结构时最容易翻车的地方——Add要求所有输入张量形状完全一致维度不匹配会直接报错。很多人第一次写残差就在这里卡住以为是 API 用错了其实是形状没对齐。3.3 多输出模型的损失配置逻辑上面compile里用了字典形式的 loss 和loss_weights这里展开说一下为什么这么配。多输出模型训练时每个输出头都有自己的损失总损失是各头损失的加权和。loss_weights控制各任务的相对重要性比如分类任务更重要就给它大一点的权重。如果不指定默认各头等权。实际调参时我的经验是先让各头等权跑一轮看各头 loss 的量级。如果某个头的 loss 天然就比另一个大一个数量级比如回归的 MSE 和分类的交叉熵量纲不同等权会导致梯度被大 loss 那头主导小 loss 那头学不动。这时候要么调loss_weights要么对各头的 loss 做归一化。这个坑在多任务学习里极其常见光看总 loss 下降就以为训练正常其实某个任务早就躺平了。3.4 Functional 的调试优势中间层随时可取Functional 还有一个被低估的好处因为整张图是显式声明的你可以随时构造一个截断模型来拿中间层输出。比如想看第 3 层的特征直接Model(inputsmodel.input, outputsmodel.layers[2].output)就行。这在做特征可视化、迁移学习、模型蒸馏时特别方便。Subclassing 就没这么轻松因为它的前向逻辑是写在call里的 Python 代码没有显式的图结构可供截取。4. Subclassing当前向逻辑复杂到图描述不够用时Subclassing 是三种方式里最自由的也是最容易被误用的。它的写法是继承tf.keras.Model在__init__里把层建好在call里写前向计算。前向过程就是普通 Python可以有 if、for、甚至递归。4.1 它解决的是图描述不了的问题什么时候 Functional 不够用典型场景是前向逻辑依赖运行时数据。比如根据输入长度动态决定循环次数某些序列模型。前向过程中包含条件分支不同样本走不同路径。需要在前向里做复杂的张量操作用现成的层拼不出来。这些情况下Functional 的声明式画图就力不从心了因为图必须在构建时静态确定。Subclassing 把前向变成命令式代码运行时才决定怎么算灵活性拉满。class GatedBlock(tf.keras.Model): def __init__(self, units): super().__init__() self.dense_a layers.Dense(units, activationrelu) self.dense_b layers.Dense(units, activationsigmoid) self.proj layers.Dense(units) def call(self, inputs, trainingNone): a self.dense_a(inputs) gate self.dense_b(inputs) return self.proj(a * gate)这个门控块用 Functional 也能写Multiply层但一旦逻辑再复杂一点比如门控值要根据 batch 统计量动态调整Functional 就难写了Subclassing 则很自然。4.2 Subclassing 的代价你失去了什么自由是有代价的而且这个代价在项目后期会集中爆发summary()不完整因为图没显式构建summary()往往只能列出子层看不到完整的形状流转甚至有时直接报错。保存和加载变麻烦Functional 模型可以只存结构不存权重Subclassing 模型通常需要保存完整对象跨版本加载容易出问题。可视化困难没有静态图画不出结构图。形状推断不友好某些依赖静态形状的优化和检查会失效。我的实际体会是除非确实需要动态控制流否则优先 Functional。Subclassing 更适合研究阶段快速验证一个非标准结构等结构定型后如果可能再改写成 Functional 以便部署和维护。这个先子类化验证、再函数式固化的路径在不少团队里是默认做法。4.3 一个容易忽略的细节call里的training参数Subclassing 写call时签名里最好带上trainingNone并在用到 Dropout、BatchNorm 这类训练/推理行为不同的层时把它传下去。因为 Keras 在训练和推理时会通过这个参数控制层的行为如果你在call里自己调用了这些层却没传training它们可能一直按训练模式跑导致推理结果异常。这个 bug 很隐蔽因为训练时一切正常只有推理或评估时才暴露。5. 三种方式怎么选一张决策路径加几条硬经验讲完三种方式回到最实际的问题拿到一个新任务怎么快速决定用哪种。我给一个自己常用的决策顺序再补几条踩坑换来的硬经验。5.1 决策顺序从简到繁够用就停先问拓扑模型是不是一条链是直接用 Sequential别想太多。有分叉/合并/多输入输出是上 Functional。绝大多数生产模型到这一步就够了。前向逻辑依赖运行时数据、需要动态控制流是才考虑 Subclassing。混合使用Functional 里可以嵌 Subclassing 写的自定义层Subclassing 里也可以用 Functional 构建的子模块。不必非此即彼。第 4 点值得强调。实际项目里最常见的不是纯 Functional或纯 Subclassing而是用 Functional 搭主干把复杂的自定义块用 Subclassing 封装成层。这样既保留了主图的可视化和可保存性又在局部获得了灵活性。5.2 几条用血换来的经验经验一形状对齐是 Functional 报错的头号来源。Add、Concatenate、Multiply这些合并层对输入形状有严格要求Add要求完全一致Concatenate要求除拼接轴外其他维度一致。写合并前先在纸上或注释里把各分支的输出形状标出来能省掉大量试错。经验二给每个有意义的层和输出起名字。namecls、namereg这种命名在多输出模型里直接决定你compile时字典的 key 怎么写也决定保存后加载时能不能对上。默认名字如dense_3在模型一改结构就全乱后期维护成本极高。经验三Sequential 转 Functional 的成本比想象中低。如果一开始用 Sequential后来发现要加分叉不用推倒重来。把 Sequential 里的层逐个拿出来用函数式重新连一遍即可层对象可以复用。我一般会保留原来的层定义只改连接方式这样权重初始化逻辑不变对比实验更干净。经验四Subclassing 模型保存优先用SavedModel格式而非 H5。H5 对子类化模型的支持一直不太稳尤其是带自定义层的时候。用model.save(path, save_formattf)存成 SavedModel 目录加载时用tf.keras.models.load_model并显式传入custom_objects成功率最高。经验五多输出模型的评估指标要分头看。model.evaluate返回的是一个列表顺序和你outputs列表一致。别只看第一个值就下结论我见过有人把回归头的 MSE 当成分类准确率看了半天纳闷为什么准确率是几百。6. 把三种模型放进同一条训练链路里验证光讲概念容易飘我习惯用一个统一的小任务把三种写法都跑一遍确认自己对它们的理解没有偏差。这个任务用同一个简单的表格数据分别用 Sequential、Functional、Subclassing 实现结构等价的模型对比参数量、训练曲线和保存加载行为。6.1 结构等价的三份实现# 方式一Sequential seq_model tf.keras.Sequential([ layers.Input(shape(32,)), layers.Dense(64, activationrelu), layers.Dense(1) ]) # 方式二Functional inp layers.Input(shape(32,)) h layers.Dense(64, activationrelu)(inp) out layers.Dense(1)(h) func_model Model(inp, out) # 方式三Subclassing class SubModel(tf.keras.Model): def __init__(self): super().__init__() self.d1 layers.Dense(64, activationrelu) self.d2 layers.Dense(1) def call(self, x): return self.d2(self.d1(x)) sub_model SubModel() sub_model.build((None, 32)) # 触发权重创建三份实现的参数量应该完全一致32×6464 加 64×11。跑一遍count_params()对比如果对不上说明某处结构写错了。这个自检动作我强烈建议养成习惯尤其是从论文复现结构时参数量对不上往往意味着某层维度或连接方式搞错了。6.2 保存加载行为的差异实测这一步最能体现三种方式的区别。Sequential 和 Functional 可以直接model.save(m.keras)再load_model无损恢复Subclassing 如果不调用build或没有跑过一次前向直接保存可能因为权重未创建而失败。实测下来Subclassing 模型在保存前务必先build或跑一个 dummy batch否则加载回来的模型可能是空的。这个细节文档里提得不显眼但实际踩到会浪费不少时间。6.3 训练曲线对比的意义结构等价的三份实现在相同数据、相同优化器、相同随机种子下训练曲线应该高度接近。如果某一份明显偏离通常不是模型种类本身的问题而是某处实现细节不一致——比如 Subclassing 里忘了传training导致 Dropout 行为不同或者 Functional 里某层默认参数和 Sequential 不一致。用这种方式做交叉验证能帮你把模型种类和实现细节这两类问题彻底分开。7. 几个高频疑问的直给回答最后整理几个我被问得最多、也是自己当初最困惑的问题直接给结论。Sequential 能不能加自定义层能只要这个层是单输入单输出的model.add(MyLayer(...))就行。但如果自定义层内部有分叉Sequential 的透明性就没了建议换 Functional。Functional 模型能不能像 Subclassing 一样写循环不能。Functional 的图在构建时静态确定循环必须在构建时展开比如用 Python for 循环生成多个层再连起来不能依赖运行时数据决定循环次数。需要运行时循环就上 Subclassing。三种方式能不能互相转换Sequential 和 Functional 之间转换很容易层对象复用即可。Subclassing 转 Functional 需要把call里的逻辑手动改写成函数式连接工作量取决于前向复杂度动态控制流部分往往改不了只能保留为自定义层。生产部署选哪种优先 Functional。它的静态图对导出、优化、跨平台推理最友好。Subclassing 模型在导出时经常需要额外处理除非部署环境明确支持否则不建议作为首选。学习顺序建议先把 Sequential 用熟理解层、张量、编译训练这条主线再啃 Functional重点练分叉、合并、多输出最后看 Subclassing理解它解决的是哪类 Functional 解决不了的问题。这个顺序符合抽象层级递进跳步容易夹生。把这几块吃透之后再回头看 TensorFlow 官方那些案例你会发现它们无非是这三种范式的不同组合。真正决定你能不能复现一个复杂模型的不是记住了多少 API而是拿到结构图时能立刻判断这该用哪种方式描述、哪里会形状不匹配、保存加载要注意什么。这些判断力只能靠把三种方式都亲手跑一遍、踩几个坑才能长出来。