首页 / 资讯中心 / 文章详情

TensorFlow 2.0 Keras实战:从模型搭建到训练评估指南

TensorFlow 2.0 Keras实战:从模型搭建到训练评估指南 ★ FEATURED ARTICLE
说到TensorFlow 2.0的Keras实战我最想先聊一个词心态。很多朋友刚接触深度学习一上来就被各种模型架构、论文术语劝退觉得自己“没那个天赋”。但如果你只是想把模型跑起来、跑明白、再改一改Keras几乎是目前最友善的入口。这也是我写这个系列的原因——上一篇聊了TensorFlow 2.0的整体变化和安装这一篇就集中火力攻Keras实战把“从搭模型到训练评估”这条必经之路讲透。这一篇适合谁有Python基础但没怎么碰过深度学习的人调过别人的代码但不知道每一步在干嘛的人以及想系统搞懂Keras建模方式、训练细节和踩坑点的进阶学习者。我会从Keras设计背后的逻辑讲起再给你三套可以直接照抄的实战代码最后把训练过程中最常见的报错和坑一次性列清楚。读完之后你至少能做到不查资料搭出一个像样的分类模型并且知道每行代码为什么要这样写。1. Keras实战前必须搞懂的三个底层问题1.1 为什么TensorFlow 2.0把Keras“扶正”了先把我个人对Keras的看法放在前面Keras不是“玩具框架”不是只能跑跑MNIST的demo工具。TensorFlow 2.0把Keras作为官方高阶API这件事本身就说明官方对“易用性”的重视已经到了很高的程度。TensorFlow 1.x时代最劝退的是什么是Session、Graph、placeholder那一套。你要先定义一个计算图再开一个会话去执行它中间隔着一层“图模式”的思维负担。写出来的代码又长又绕调试还特别费劲。Keras的核心贡献是把这套东西全部封掉了你像叠积木一样把层堆起来调用fit就开始训练再也没有“先构图再执行”的割裂感。用一个不严谨但好懂的类比TensorFlow 1.x像用汇编语言写程序每一步都要手动管寄存器Keras像写高级语言底层再怎么复杂你手里拿的是好用的工具。TensorFlow 2.0默认采用eager execution即训即得配合Keras这套API整个开发体验从“写框架代码”变成了“写业务逻辑”。从实操角度看这意味着你不需要再手动管理全局变量和计算图上下文模型可以像普通Python对象一样被调用、打印、检查单步调试、print输出张量形状都变得非常简单很多人在2.0刚出的时候还在犹豫要不要从1.x迁移我的建议非常直接如果是从零开始学直接用2.x不要回头碰1.x的写法。Keras的教程资料这两年已经非常充足你踩过的坑大部分人都踩过。1.2 三种建模方式什么场景用什么Keras在TensorFlow 2.0里提供了三种建模方式我分别说说它们的定位和取舍这比你直接上手写代码更重要。Sequential序列式层按顺序堆叠一个接一个没有分支。适合90%的入门场景——线性模型、简单CNN、简单MLP。优点是写法极简几乎不需要动脑缺点是表达能力有限一旦出现多输入、多输出、跳跃连接它就无能为力。Functional函数式通过显式定义输入张量、输出张量来组装模型。这个是我日常用得最多的方式比Sequential多写不了几行代码但灵活度高出好几个档次。多输入、多输出、共享层、残差连接都能表达。比如我要把文本特征和数值特征拼在一起做一个模型就必须用函数式。Model子类化继承式通过继承tf.keras.Model在call方法里自定义前向计算逻辑。这是最灵活的方式适合做研究、做实验的场景可以自由控制张量在层之间的流转方式。缺点是代码量偏大新手容易把自己绕晕调试成本相对高一些。我给新手的建议是第一遍学老老实实用Sequential把全集跑通第二遍开始尽量换成函数式。你终归要面对复杂模型早一点接触函数式的写法后面看各种开源项目源码会轻松很多。Model子类化可以等对前向传播有了直觉之后再学不必急。1.3 张量形状机器学习里的“坐标系”Keras实战里最常遇到的报错就是shape对不上。与其等报错再猜不如先建立一个关键直觉。深度学习中张量的shape阅读顺序是从后往前。一个形状为(batch, 32, 32, 3)的四维张量最后一位是通道数3RGB倒数第二位和第三位是图像的高和宽最前面是batch size。对于序列数据形状通常是(batch, seq_len, feature_dim)意思是一批数据里有batch个样本每个样本有seq_len个时间步每个时间步用feature_dim维向量表示。为什么这个直觉重要因为你在写模型的时候需要时刻知道自己手里张量的形状。Keras里层与层之间会自动连但输入层的input_shape必须你自己定。很多报错“expected ndim3, found shape(None, 4)”都是因为你在输入层少算了一个维度。一个实用技巧拿到一批数据第一件事不是写模型而是打印x.shape和y.shape把形状记在脑子里。这个习惯能帮你省掉至少一半的shape报错。2. 环境准备与第一个能跑的模型2.1 版本选择与安装避坑TensorFlow和Python版本、CUDA之间有一条微妙的“锁链关系”装错了轻则警告重则直接跑不起来。我个人的建议是用较新的稳定版本而不是最新发布版本。原因很简单——生态里的配套库往往滞后太新容易碰上兼容性bug。安装方面普通学习者用CPU环境跑中小型数据集完全够用没必要一上来就折腾GPU。先用CPU把流程跑通理解模型怎么训练再考虑加GPU加速。如果你确实要配GPU环境记住先确认显卡驱动版本和CUDA版本的匹配关系再装TensorFlow顺序反了你大概率要返工。还有一个容易忽略的点安装TensorFlow时尽量用独立的虚拟环境不要直接在系统默认的Python环境里装。这个习惯能避免极其痛苦的环境冲突问题。具体步骤不展开装好之后你可以用一行代码验证是否成功import tensorflow as tf print(tf.__version__) print(tf.config.list_physical_devices(GPU))如果输出了版本号说明基础环境就绪。GPU那行如果是空列表也没关系说明你用的是CPU环境。2.2 十分钟搭一个图像分类模型直接抄这个我见过太多教程只讲MNIST手写数字。这次我换个更有区分度的数据集Fashion MNIST它是服装图片分类比MNIST难度高一些也更接近真实场景。完整代码如下你可以直接复制运行import tensorflow as tf from tensorflow.keras import layers # 加载数据 (x_train, y_train), (x_test, y_test) tf.keras.datasets.fashion_mnist.load_data() # 数据预处理归一化到0-1区间 x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0 # 搭建模型 model tf.keras.Sequential([ layers.Flatten(input_shape(28, 28)), layers.Dense(128, activationrelu), layers.Dropout(0.3), layers.Dense(10, activationsoftmax) ]) # 编译模型 model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) # 训练 history model.fit( x_train, y_train, batch_size64, epochs15, validation_data(x_test, y_test) )这段代码里有一个特别值得展开的地方Flatten层。输入shape是(28, 28)这是二维的灰度图像但Dense层只能接收一维向量所以Flatten的作用就是把28×28的矩阵“拉平”成784维的向量。这一步不做模型根本跑不起来。训练完成后验证集准确率通常能达到85%左右。这个数字对新手来说应该是个不错的起点但实际上这个模型还有很大的提升空间——如果你换成卷积神经网络准确率能轻松超过90%。这个差距就是模型表达能力的直观体现。2.3 fit的背后训练循环里到底发生了什么新手最容易把model.fit当成黑盒。其实fit做的事情非常简单核心就是一个嵌套循环外层循环遍历epochs每轮就是把全部数据过一遍内层循环按batch_size把数据切成小块每小块执行一次“前向传播→计算损失→反向传播→更新参数”前向传播是数据从输入层流到输出层得到预测结果损失函数计算预测值与真实值的差距反向传播把这个差距“传导”到每一个参数告诉它们该往哪个方向调优化器根据这个信息更新参数。这就是“学习”的本质。很多人不知道fit返回的history对象是宝。它记录了每个epoch的训练损失和验证损失你能从中直观看到模型的学习过程。想判断过拟合最简单的方法就是把history里的训练损失和验证损失画成曲线对比——训练损失一路下降但验证损失开始上升说明模型已经在死记硬背训练集了。我强烈建议新手养成一个习惯每次训练完都打印一下history的内容哪怕只看最后几行。这个微小的习惯能帮你快速建立对训练过程的直觉。3. 三个实战案例把Keras用熟用透3.1 函数式API实战文本情感分类模型为什么要单独挑函数式API来讲因为一旦你开始解决真实问题几乎不可避免会遇到“多个输入”或“多个输出”的场景。我拿一个简化版的情感分类任务演示数据就用一个简化的文本样例集合重点是让你看懂函数式的写法。import tensorflow as tf from tensorflow.keras import layers # 假设vocab_size5000, max_len100 inputs tf.keras.Input(shape(100,), nametext_input) x layers.Embedding(input_dim5000, output_dim128)(inputs) x layers.Bidirectional(layers.LSTM(64, return_sequencesTrue))(x) x layers.GlobalMaxPooling1D()(x) x layers.Dense(64, activationrelu)(x) outputs layers.Dense(1, activationsigmoid, nameoutput)(x) model tf.keras.Model(inputsinputs, outputsoutputs) model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] )这里有两个关键层需要理解Embedding层把每个词的编号映射成一个128维的稠密向量这是文本模型的第一步“翻译”工作Bidirectional LSTM层双向读取序列既能看前文又能看后文在情感分析这种任务里天然合适。GlobalMaxPooling1D的作用是把整个序列的信息压缩成单个向量——对情感分类来说提取“最突出的特征”往往比平均更有用。跟Sequential的写法对比一下你会发现区别就在Input和Model两层。Input显式声明了输入形状Model把输入和输出张量绑在一起。中间层的写法完全一样所以学习成本其实很低。3.2 从全连接到卷积图像分类进阶Fashion MNIST用全连接网络能到85%但到了CIFAR-10这种32×32的彩色图片数据集上全连接网络就真的不够看了。CIFAR-10是10类彩色物体的公开数据集类别包括猫、狗、飞机、汽车等图片分辨率小、噪声大对模型能力的要求明显更高。看看卷积网络怎么做model tf.keras.Sequential([ layers.Conv2D(32, (3, 3), paddingsame, activationrelu, input_shape(32, 32, 3)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dropout(0.4), layers.Dense(256, activationrelu), layers.Dense(10, activationsoftmax) ])这个结构很经典遵循一个规律通道数逐层递增32→64→128特征图尺寸逐层递减32×32→16×16→8×8→4×4。通道数增加意味着提取的特征越来越抽象尺寸减小意味着空间位置信息被逐步压缩为语义信息。关于参数量的计算我顺手算一个简单的例子第一个Conv2D层输入是32×32×3输出32个通道卷积核3×3。参数数量就是3×3×3×32再加上32个偏置等于896。这个数字背后代表的信息是这一层只需896个参数就能在整张图上提取32种不同的局部特征模式。对比全连接层动辄几万甚至几十万的参数量卷积层的参数效率优势非常明显。这个模型在CIFAR-10上不加数据增强大概能到70%左右。虽然离当前的前沿精度还差得远但作为亲手搭出来的模型这个数字会让你对模型能力有一个真实的感知。3.3 模型子类化打破固有结构的限制函数式API已经够灵活了但还有一种场景它表达不了前向传播过程中需要动态变化的逻辑。比如我要写一个模型当输入长度大于某个阈值时走一条分支否则走另一条分支。这种条件控制流只有子类化能做到。class MyModel(tf.keras.Model): def __init__(self): super().__init__() self.dense1 layers.Dense(128, activationrelu) self.dense2 layers.Dense(64, activationrelu) self.out layers.Dense(1, activationsigmoid) def call(self, inputs): x self.dense1(inputs) x self.dense2(x) return self.out(x) model MyModel()这段代码的重点是模型的结构在__init__里定义前向传播的逻辑在call里写。你可以print中间变量可以用if可以用循环可以调用NumPy函数——因为整个计算就是一段普通的Python函数。子类化最大的坑也在这一点上因为你完全自由所以很容易写出“结构对但训练效果差”的代码。常见问题是忘了调用父类的__init__或者call方法里重复创建层。记住层只能在__init__里创建一次call里只能使用不能创建。否则每次前向传播都会新建一组参数模型永远无法收敛。4. 让模型训练更稳更快的实用技巧4.1 回调函数三板斧训练辅助的正确姿势训练模型不是傻等epoch跑完就行的。真正高效的做法是用回调函数在训练过程中自动干预。我日常必用的三个是EarlyStopping、ModelCheckpoint、ReduceLROnPlateau。EarlyStopping的作用是监控验证集指标连续多少轮不提升就自动停止训练防止浪费时间。ModelCheckpoint自动把最优模型保存到文件防止训练跑了半天最后发现最佳权重重现在某个中间epoch。ReduceLROnPlateau在验证指标停滞时自动降低学习率让训练继续稳定下降。组合使用效果更好callbacks [ tf.keras.callbacks.EarlyStopping(patience3, restore_best_weightsTrue), tf.keras.callbacks.ModelCheckpoint(best_model.keras, save_best_onlyTrue), tf.keras.callbacks.ReduceLROnPlateau(patience2, factor0.5) ] model.fit(x_train, y_train, validation_data(x_test, y_test), epochs50, callbackscallbacks)有个细节要提醒EarlyStopping的restore_best_weights参数很多人不知道它是什么意思。如果不设置成True训练停止时保留的是最后一轮权重而不是验证集指标最好的那一轮权重。设置成True之后训练结束时自动回滚到最佳权重这个习惯能避免很多无意义的损失。4.2 数据增强用一张图变出很多张图模型过拟合的根本原因是训练数据不够多。数据增强的思路很朴素对原始图片做随机小变换比如平移、翻转、旋转、缩放让模型看到更多“变体”从而学到更本质的特征。data_augmentation tf.keras.Sequential([ layers.RandomFlip(horizontal), layers.RandomRotation(0.1), layers.RandomZoom(0.1), ])然后训练时在模型第一层接上data_augmentation即可。注意一点数据增强通常在训练集上做验证集和测试集上不要做增强否则验证集就不“真实”了。我踩过的一个坑是过度增强反而让模型欠拟合。增强力度设得太大图片被扭曲得面目全非模型根本学不到有效特征。从适度的增强开始观察训练损失和验证损失的差距再逐步调整力度。4.3 迁移学习用好预训练模型如果你要解决的图像分类问题数据量不大从头训练一个深层CNN往往不划算。这时候迁移学习是最优雅的方案思路是直接用在大规模数据集上预训练好的模型把前面那些学到的通用特征提取能力搬过来只替换最后面的分类层。最简明的迁移学习流程base_model tf.keras.applications.EfficientNetB0( include_topFalse, weightsimagenet, input_shape(224, 224, 3) ) base_model.trainable False inputs base_model.input x base_model(inputs, trainingFalse) x layers.GlobalAveragePooling2D()(x) x layers.Dropout(0.2)(x) outputs layers.Dense(10, activationsoftmax)(x) model tf.keras.Model(inputs, outputs) model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])然后你只需要把数据resize到224×224就能用非常少的epoch达到还不错的精度。接下来如果你想进一步提精度可以解冻部分base_model的层用很小的学习率做“微调”。解冻的关键是学习率要调到非常小比如1e-5避免破坏已经学好的特征。这里有个特别容易被忽略的细节包含BatchNormalization的预训练模型在冻结阶段调用时要传trainingFalse。不传的话batch norm层会按训练模式运行用当前batch的统计量更新自身这会导致特征提取层的输出不稳定模型表现反而变差。这个坑我亲眼见过好几个同学踩过。5. 常见报错与排查心得实录5.1 维度不匹配新手的第一大坑维度报错是Keras实战中出现频率最高的错误。常见的是straightforward的size对不上比如“Input 0 of layer dense is incompatible with the layer”后面会跟着expected shape和found shape。排查思路就三步第一步打印x.shape和y.shape第二步逐层检查模型的输出形状——可以用model.layers[i].output_shape打印每一层的输出维度第三步注意损失函数的选择。这里有个初学者几乎必踩的坑如果把标签做成了one-hot编码损失函数要用categorical_crossentropy如果标签还是整数编码就要用sparse_categorical_crossentropy。这两个名字就差一个sparse但一旦用错模型还能训练只是损失曲线看起来很奇怪。排查方法很简单打印y_train.shape如果是二维且每行只有一个1说明是one-hot如果是一维整数数组就用带sparse的版本。5.2 显存不足和训练太慢怎么办训练速度慢第一反应不是换机器而是检查你的batch_size是不是设置得太小。GPU环境下显存利用率不充分是一个非常常见的问题。适当调大batch_size比如从32调到128训练速度往往能提升一倍以上。代价是模型收敛的稳定性会稍微变差需要配合调小学习率。遇到显存不足OOM的时候我建议确定性调优的优先级是这样的先调小batch_size这是最直接有效的然后尝试降低输入图片的分辨率很多任务256降到224完全够用最后再考虑使用混合精度训练。不要一上来就换硬件很多时候问题出在代码策略上。还有一个容易被忽略的点不要太依赖model.fit默认的加载方式。数据量大的时候把数据封装成tf.data.Dataset并做prefetch缓存能有效避免“数据读取成为训练瓶颈”的问题。具体来说就是在fit前加一行train_dataset.prefetch(tf.data.AUTOTUNE)。5.3 过拟合的识别与应对清单过拟合的表现大家都背过训练损失持续下降验证损失先降后升。但实际训练中很多人到了epoch 10才看出来白白浪费了时间。我的建议是第一轮训练就把epoch设大一点比如50强制开启EarlyStopping。这样你不需要时刻盯着训练过程模型自己会在合适的时候停下来。如果发现过拟合已经发生按下面的优先级依次处理增加数据增强力度图像任务加大Dropout比例使用权重衰减L2正则化减少模型容量降低层数或神经元数找更多数据很多人最后一个才想到这个其实应该最早考虑最后再提醒一件事如果你发现模型训练了十几个epoch损失纹丝不动大概率不是模型结构问题而是数据预处理出了问题。最常见的检查点是归一化是否做了、标签是否配对正确、输入顺序是否被打乱。先检查数据再检查模型这个习惯能救你无数次。我个人在实际训练中的体会是Keras这套API的门槛已经降到了“有耐心就能上手”的程度真正拉开差距的是你对张量形状的敏感度、对训练信号的理解深度、以及处理异常情况的经验。这篇写的内容基本都是新手进阶路上绕不开的关键节点后面如果你想继续深入建议往这三个方向走一个是自己动手实现一个自定义Loss函数搞清楚梯度是怎么传播的一个是去读几个成熟项目的源码看看别人怎么组织数据管道和模型结构还有一个是自己折腾一个端到端的小项目从数据分析到模型部署全部走一遍。任何一项实践都比刷十篇教程管用。
阅读完成 · 觉得有帮助?
咨询建站