资讯动态

TensorFlow Keras模型构建:Sequential、Functional与Subclassing选型指南

发布时间:2026/10/27 5:58:28 来源:尧图企业网站定制
1. 为什么要在TensorFlow生态里聊Keras模型种类做深度学习项目这些年我见过太多人卡在同一个地方环境装好了数据也清洗完了打开编辑器准备写模型结果面对Sequential、Functional、Model Subclassing这几种写法直接懵了。网上教程各写各的有的全程Sequential一把梭有的上来就class MyModel(tf.keras.Model)新手根本分不清什么时候该用哪种。这篇内容就是想把这件事彻底讲透——Keras模型种类到底有哪几种、每种背后的设计逻辑是什么、在真实项目里怎么选、踩过哪些坑。先把范围说清楚。这里讨论的是TensorFlow 2.x内置的tf.keras不是独立版的Keras虽然API高度一致但版本对应关系容易出问题后面会专门讲。Keras作为TensorFlow的官方高阶API核心价值在于把模型构建这件事抽象成了几种不同粒度的方式从搭积木到自己造积木都有覆盖。适合谁看如果你已经能跑通一个MNIST分类但面对稍微复杂一点的结构多输入、多输出、共享层、残差连接就不知道怎么组织代码那这篇就是写给你的。如果你是完全零基础建议先把Dense、Conv2D这些层的基本用法过一遍再回来不然会有点吃力。我个人的判断是模型种类的选择本质上是在开发效率和表达自由度之间做权衡。Sequential最快但最死板Subclassing最灵活但最容易写出隐蔽bug。中间还有一个Functional API是我在实际项目里用得最多的——大约七成场景它都能覆盖剩下三成才需要动Subclassing。下面按这个逻辑线展开每种都配上可直接跑的案例和参数说明。2. 三种模型构建方式的核心差异与选型逻辑2.1 Sequential线性堆叠的傻瓜相机Sequential是Keras里最古老的模型容器本质就是一个有序的层列表。你往里面add层它就按顺序串起来数据从第一层流到最后一层中间没有任何分叉。用生活化的类比它就像一列火车车厢一节接一节你只能决定挂哪些车厢、什么顺序但不能让某节车厢同时通向两个方向。它的优势非常明确——代码量最少、可读性最高、调试最直观。一个典型的三层全连接网络import tensorflow as tf model tf.keras.Sequential([ tf.keras.layers.Dense(128, activationrelu, input_shape(784,)), tf.keras.layers.Dropout(0.3), tf.keras.layers.Dense(64, activationrelu), tf.keras.layers.Dense(10, activationsoftmax) ]) model.summary()model.summary()会打印出每层的输出形状和参数量这是排查维度错误的第一道防线。我习惯在每次改完结构后都跑一遍比等到fit报错再回头找要省事得多。但Sequential的局限也很硬它不支持任何非线性拓扑。什么叫非线性拓扑多输入比如同时吃图像和文本、多输出比如一个网络同时做分类和回归、层共享同一个Dense层被调用两次、跳跃连接ResNet那种x f(x)——这些它统统做不了。早期有人用Sequential硬凑残差连接写法极其别扭后来官方也不推荐了。注意Sequential里第一个层必须指定input_shape或者用Input层显式声明否则summary()和权重初始化都会出问题。这个input_shape不包含batch维度比如输入是(32, 784)的批次这里写(784,)。2.2 Functional API有向无环图的乐高说明书Functional API是我最推荐新手优先掌握的。它的核心思想是把层当作函数来调用用张量的流动关系描述整个计算图。你定义输入张量然后一层层调用过去最后用Model(inputs..., outputs...)把入口和出口包起来。这个过程中间可以任意分叉、合并、复用。举个多输入的典型场景——假设你要做一个模型同时接收用户画像特征数值向量和用户行为序列时间序列最后融合输出一个评分from tensorflow.keras import layers, Model, Input # 分支一数值特征 num_input Input(shape(20,), namenum_features) x1 layers.Dense(32, activationrelu)(num_input) x1 layers.Dense(16, activationrelu)(x1) # 分支二序列特征 seq_input Input(shape(50, 8), nameseq_features) x2 layers.LSTM(32)(seq_input) # 融合 merged layers.concatenate([x1, x2]) output layers.Dense(1, activationsigmoid, namescore)(merged) model Model(inputs[num_input, seq_input], outputsoutput)这段代码里num_input和seq_input是两条独立的输入路径各自经过不同的层处理后用concatenate合并。这种结构用Sequential是绝对写不出来的。而且Functional模型天然支持model.summary()、plot_model可视化、save/load完整保存工程化程度很高。Functional API的关键约束是必须是有向无环图DAG。也就是说数据流不能成环。如果你需要循环结构比如某些自定义的RNN变体、或者带状态迭代的逻辑那就得往下看Subclassing了。另外Functional里所有层都是声明式的你没法在call里写if/else这种动态控制流——虽然可以用tf.cond之类的算子绕但代码会变得很难看。2.3 Model Subclassing完全自定义的手工打造Subclassing就是继承tf.keras.Model在__init__里定义层在call里定义前向传播。它给你的是Python级别的完全控制权——你可以写任意控制流、任意循环、任意条件分支只要最终返回张量就行。class CustomModel(tf.keras.Model): def __init__(self, hidden_units): super().__init__() self.dense1 layers.Dense(hidden_units, activationrelu) self.dense2 layers.Dense(hidden_units // 2, activationrelu) self.out layers.Dense(1) def call(self, inputs, trainingFalse): x self.dense1(inputs) if training: x tf.nn.dropout(x, rate0.3) x self.dense2(x) return self.out(x)注意call里的training参数——这是Subclassing的一个经典细节。Dropout、BatchNormalization这些层在训练和推理时行为不同Functional和Sequential会自动处理这个开关但Subclassing里你必须手动把training传下去否则推理时也会随机丢神经元结果完全不可复现。这个坑我见过至少三个人踩过而且症状很隐蔽训练集指标正常验证集指标莫名其妙偏低查半天以为是过拟合。Subclassing的代价是可解释性和可序列化性下降。model.summary()对Subclassing模型往往打印不出完整的层结构因为层是在call里动态调用的Keras静态分析不出来plot_model也可能失败。保存模型时推荐用model.save_weights()而不是model.save()后者对自定义模型的支持一直不太稳定。2.4 三种方式的选型对照表维度SequentialFunctional APIModel Subclassing拓扑表达能力仅线性有向无环图任意含循环代码量最少中等最多调试难度低中高summary()完整性完整完整部分缺失模型保存含结构支持支持不推荐动态控制流不支持不支持支持典型场景简单分类/回归多输入输出、残差自定义训练循环、复杂逻辑我的实际经验是先用Functional把结构画出来只有当Functional确实表达不了时才上Subclassing。很多新手一上来就Subclassing结果连基本的维度对齐都调半天完全没必要。反过来如果你发现自己在Functional里疯狂用Lambda层塞逻辑那说明该换Subclassing了——Lambda层在保存和跨版本加载时问题很多能不用就不用。3. 核心细节拆解每种模型背后的关键机制3.1 层的权重共享与命名机制不管用哪种方式Keras的层都有一个容易被忽视的特性同一个层实例被多次调用时权重是共享的。这在Functional里做孪生网络Siamese Network时特别有用shared_dense layers.Dense(64, activationrelu) input_a Input(shape(128,)) input_b Input(shape(128,)) out_a shared_dense(input_a) out_b shared_dense(input_b) distance layers.Lambda(lambda x: tf.abs(x[0] - x[1]))([out_a, out_b])这里shared_dense只初始化了一份权重两个分支共用。如果你想要两份独立的权重就必须创建两个Dense实例。这个区别在对比学习、度量学习里非常关键搞错了模型就学不到东西。层的命名也值得说一句。默认情况下Keras会自动生成名字dense_1、dense_2...但一旦模型复杂起来summary()里全是这种名字根本没法看。我的习惯是每个关键层都显式命名比如nameuser_embedding、namefusion_concat。这样调试时能直接定位保存的权重文件里键名也可读。Subclassing里更要注意__init__里定义的层属性名最好和用途对应别用self.fc1、self.fc2这种过两周自己都忘了哪个是干嘛的。3.2 输入形状声明与batch维度的坑Input(shape...)和Dense(input_shape...)里的shape都不包含batch维度。这是新手最高频的错误来源。比如你有一批数据形状是(1000, 28, 28, 1)1000张28x28单通道图那么Input(shape(28, 28, 1))是对的写成(1000, 28, 28, 1)就会在fit时报维度不匹配。更隐蔽的是变长序列的情况。处理文本或时间序列时序列长度往往不固定。这时候Input(shape(None, 128))里的None表示该维度可变配合Masking层或padding使用。但要注意不是所有层都支持可变长度——Dense可以Flatten不行它需要固定形状LSTM默认支持但需要return_sequences配合。我见过有人在Flatten前面接变长输入报错信息极其晦涩查了半天才发现是这里的问题。提示写完模型先跑一次model.summary()重点看每层的Output Shape。如果某一层的输出形状出现?或者和预期不符基本就是输入声明或层参数的问题这时候改比训练到一半再改要省太多时间。3.3 激活函数与输出层的搭配原则模型种类的选择不直接影响激活函数但不同结构对输出层的要求不一样这里顺带说清楚。多分类用softmax输出维度等于类别数二分类用sigmoid输出维度1回归用线性不写激活或activationNone。这是基本盘但多输出模型里容易乱比如一个模型同时做三分类和回归那分类分支用softmax3维回归分支用线性1维两个输出分别算lossmodel.compile里loss要传一个字典或列表对应起来。还有一个细节softmax和from_logits的关系。如果你在输出层用了softmax那loss用CategoricalCrossentropy()默认from_logitsFalse如果输出层不加激活输出logits那loss要用CategoricalCrossentropy(from_logitsTrue)。后者在数值稳定性上更好因为softmax和交叉熵的计算可以融合避免中间结果溢出。我现在的习惯是输出层不加softmaxloss里开from_logitsTrue实测在类别数很多时比如上千类稳定性提升明显。3.4 模型编译时的优化器与学习率设置model.compile这一步看似简单但参数选错会让好模型也训不出来。优化器方面Adam是默认首选学习率1e-3起步。但如果你用的是BatchNormalization较多的深层网络Adam的默认学习率可能偏大我一般会降到1e-4甚至3e-5。反过来如果网络很浅两三层1e-3完全够用。学习率调度也值得提一句。固定学习率在训练后期往往收敛不充分用tf.keras.callbacks.ReduceLROnPlateau做自适应衰减是很实用的做法lr_scheduler tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 )patience3表示验证loss连续3个epoch不降就砍半。这个参数别设太小比如1否则学习率掉得太快模型还没充分探索就停滞了。我一般设3到5之间看数据集大小。4. 完整实操从零构建一个多输入模型并训练4.1 场景设定与数据准备光讲概念不够直接上一个能跑的完整案例。假设我们要做一个用户流失预测模型输入包含用户的静态画像年龄、消费额等20维数值特征和最近30天的行为序列每天8维行为统计输出用户是否会流失二分类。这个场景天然是多输入正好用Functional API演示。数据我用随机生成的方式模拟方便你直接复制运行import numpy as np import tensorflow as tf from tensorflow.keras import layers, Model, Input np.random.seed(42) tf.random.set_seed(42) # 模拟数据800个样本 n_samples 800 num_feat np.random.randn(n_samples, 20).astype(float32) seq_feat np.random.randn(n_samples, 30, 8).astype(float32) labels np.random.randint(0, 2, size(n_samples, 1)).astype(float32) # 划分训练/验证 split int(n_samples * 0.8) train_data ([num_feat[:split], seq_feat[:split]], labels[:split]) val_data ([num_feat[split:], seq_feat[split:]], labels[split:])这里train_data是一个元组第一个元素是输入列表对应两个Input第二个是标签。fit的时候直接传这个元组就行Keras会自动按顺序对应。4.2 模型结构搭建与参数说明# 分支一静态特征 num_input Input(shape(20,), namestatic_features) x1 layers.Dense(64, activationrelu, namestatic_fc1)(num_input) x1 layers.BatchNormalization(namestatic_bn)(x1) x1 layers.Dropout(0.3, namestatic_drop)(x1) x1 layers.Dense(32, activationrelu, namestatic_fc2)(x1) # 分支二行为序列 seq_input Input(shape(30, 8), namebehavior_seq) x2 layers.LSTM(48, return_sequencesFalse, nameseq_lstm)(seq_input) x2 layers.Dense(32, activationrelu, nameseq_fc)(x2) # 融合与输出 merged layers.concatenate([x1, x2], namefusion) z layers.Dense(32, activationrelu, namefusion_fc)(merged) z layers.Dropout(0.2, namefusion_drop)(z) output layers.Dense(1, activationsigmoid, nameprediction)(z) model Model(inputs[num_input, seq_input], outputsoutput, namechurn_model) model.summary()几个参数的选择理由LSTM(48)的48是试出来的——太小16欠拟合太大128在800样本上直接过拟合48是个平衡点。BatchNormalization放在第一个Dense之后是因为静态特征数值范围差异大归一化能加速收敛。Dropout(0.3)和Dropout(0.2)的差异是因为融合层之后信息更浓缩丢太多会损失关键信号。summary()输出里你会看到Total params和Trainable params如果两者差距很大说明有大量冻结层这里没有如果Trainable params是0那说明所有层都被冻了肯定是哪里写错了。4.3 编译、训练与回调配置model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losstf.keras.losses.BinaryCrossentropy(), metrics[accuracy, tf.keras.metrics.AUC(nameauc)] ) callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_auc, patience5, modemax, restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ), tf.keras.callbacks.ModelCheckpoint( best_churn.weights.h5, monitorval_auc, modemax, save_best_onlyTrue, save_weights_onlyTrue ) ] history model.fit( train_data[0], train_data[1], validation_dataval_data, epochs50, batch_size32, callbackscallbacks, verbose1 )EarlyStopping监控val_auc而不是val_loss是因为类别不平衡时loss的绝对值参考意义有限AUC更能反映排序质量。restore_best_weightsTrue很关键——不加的话训练结束后模型停在最后一个epoch可能已经过拟合了。ModelCheckpoint的文件名用.weights.h5后缀这是TF 2.x推荐的新格式老的.h5整模型保存方式对自定义层支持不好。4.4 模型评估与推理训练完用model.evaluate看最终指标然后单独做推理test_loss, test_acc, test_auc model.evaluate(val_data[0], val_data[1]) print(floss{test_loss:.4f}, acc{test_acc:.4f}, auc{test_auc:.4f}) # 单样本推理 sample_num num_feat[split:split1] sample_seq seq_feat[split:split1] pred model.predict([sample_num, sample_seq]) print(f流失概率: {pred[0][0]:.4f})注意predict的输入也要是列表形式顺序和Model(inputs[...])一致。如果顺序搞反了形状对不上会直接报错但如果两个输入形状恰好兼容比如都是二维就可能静默出错——结果莫名其妙但没报错。所以输入顺序一定要和定义时严格对应我习惯在变量名里带上num/seq前缀来防呆。5. 常见问题与排查技巧实录5.1 维度不匹配类报错速查维度问题是Keras报错的重灾区症状五花八门但根因就那么几类。整理成表方便对照报错关键词常见原因排查动作expected shape(None, X)输入shape声明少了/多了维度检查Input(shape)是否含batch维Dimensions must be equalconcatenate/add的两路形状不一致打印两路Output Shape对比Input 0 is incompatiblefit传入的数据形状和模型输入不符用np.shape()确认数据维度Cannot convert to tensor数据类型不是float32astype(float32)转换我踩过最坑的一次是concatenate两路特征一路是(None, 32)另一路因为LSTM没设return_sequencesFalse变成了(None, 30, 48)三维和二维拼接直接报错。报错信息里只说了维度不匹配没告诉你哪一路错了得自己summary()逐层看。所以每加一个分支就summary()一次比全部写完再查要高效得多。5.2 训练不收敛的排查顺序模型结构没问题但loss不降按这个顺序查学习率先试1e-4和1e-2两个极端如果都不动大概率不是学习率问题。数据标签打印几个样本的输入和标签确认没有错位。我见过标签和特征整体错开一位的模型怎么训都是随机水平。损失函数二分类用了CategoricalCrossentropy、多分类用了BinaryCrossentropy这种低级错误实际发生率不低。激活函数输出层该用sigmoid的用了relu输出全是非负二分类直接废掉。梯度消失深层网络没有BatchNormalization或残差连接前几层梯度接近0。加BN或换LeakyReLU试试。注意BatchNormalization在batch_size很小时比如小于8统计量不稳定反而拖累训练。小batch场景建议用LayerNormalization或者干脆不用。5.3 模型保存与加载的版本兼容问题model.save(model.h5)这种整模型保存方式在TF 2.x里逐渐被弃用官方推荐SavedModel格式model.save(model_dir)不带后缀或只存权重。整模型保存的问题在于自定义层、Lambda层、Subclassing模型在加载时经常报Unknown layer。我的做法是结构代码和权重分离——结构用代码定义好权重用save_weights/load_weights存取。这样跨版本迁移时只要结构代码不变权重就能复用。如果非要用整模型保存加载时记得传custom_objectsmodel tf.keras.models.load_model( model.h5, custom_objects{CustomLayer: CustomLayer} )但Lambda层里的lambda函数没法这样传所以再次强调生产代码里尽量别用Lambda层用自定义层或者Subclassing替代。5.4 显存溢出与batch_size调优ResourceExhaustedError: OOM是GPU训练的高频问题。根因是显存不够但解法不只是调小batch_size。按优先级先降batch_size减半试再检查是否有不必要的中间张量被保留比如在call里存了self.cache x最后考虑用tf.keras.mixed_precision混合精度训练。混合精度在支持Tensor Core的GPU上能省近一半显存代价是数值精度略降对大多数任务影响可忽略tf.keras.mixed_precision.set_global_policy(mixed_float16)注意输出层要强制float32layers.Dense(1, dtypefloat32)否则softmax/sigmoid在float16下容易溢出。6. 我个人的选型心得与几个实用建议聊了这么多最后说点纯个人经验。新手阶段别纠结用哪种模型种类先把Functional API练熟——它能覆盖你80%的需求而且写出来的代码结构清晰、好调试、好保存。等你遇到Functional确实表达不了的场景比如需要根据输入动态决定走哪个分支再学Subclassing那时候你对层的理解也到位了学起来很快。另一个建议是养成写model.summary()和画结构图的习惯。tf.keras.utils.plot_model(model, show_shapesTrue)能把整个计算图可视化出来多输入多输出的结构一眼就能看清哪路接哪路。我每次搭完复杂模型都会画一张比盯着代码脑补靠谱得多。还有个小技巧调试阶段把batch_size设小比如8epochs设少比如2先确认整个流程能跑通、loss能降再放大参数正式训练。这样每次改结构验证只要几十秒比一上来就跑完整训练再发现问题要省太多时间。踩过的坑告诉我深度学习项目里最贵的成本不是算力是等待——等一个跑了两小时才发现维度写错的训练那种感觉真的很难受。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑