直接开始,不绕圈子。这篇想聊的是“基于Tensorflow和Keras实现卷积神经网络CNN”这个项目。我前后手写了不止一次CNN的代码,也帮别人调过不少,这个标题看着基础,但里面真正值得讲的细节远不止“调用几个层”那么简单。如果只是照抄示例代码,跑通一次训练就结束,那你大概率什么都没学会。本文会从设计思路、代码落地、参数选择、问题排查一路讲到底,目标是让你不仅能跑通一个CNN,还能说清楚每一步为什么这么写、遇到问题怎么查。
这篇文章适合正在学深度学习的人、准备做图像分类相关项目但还没系统写过CNN的人,以及那些“能跑通但一换数据集就懵”的朋友。我尽量按一个真实项目的推进顺序来讲,不卖弄术语,但该严谨的地方也不会含糊。
1. 项目整体设计与拆解
1.1 为什么是TensorFlow加Keras,而不是别的组合
很多初学者会在PyTorch和TensorFlow之间纠结。我的看法是:如果你只是想快速验证想法、做一个能出结果的图像分类项目,TensorFlow 2.x的Keras高层接口是目前上手成本最低的方案之一。它把数据管道、模型搭建、训练循环、评估流程几乎全部封装成了“填参数”的级别,你不需要自己写反向传播,不需要手动管理多个GPU的调度细节,也不需要为了一个卷积层去查一堆底层API文档。
这个项目选Keras的原因是它提供了一种Sequential式的建模方式。对于CNN这种“一层接一层”堆叠的网络,Sequential模型能让人直观地看到信息流:输入图片经过卷积、激活、池化、再卷积、展平、全连接、输出分类。整个流程就像流水线一样清晰,很合适做教学演示和快速原型。
但如果你的需求是研究新型网络结构、处理非规则输入或多模态数据,那PyTorch的动态图机制会更灵活。这个项目没有用PyTorch,不是因为它不好,而是我们的目标是“用最小的认知成本把CNN跑通,并且能可视化每一步的效果”。工具选型本身没有绝对优劣,只有适不适合当前场景。
1.2 这个项目要解决的三个核心问题
我把整个项目的需求拆成了三块,这也是所有CNN项目绕不开的问题,只是这个项目把范围缩小了:
第一,图像数据从文件到张量的全流程处理。一张图片在磁盘上是JPEG或PNG文件,在程序里是三维数组(高x宽x通道),进网络之前还需要变成四维张量(批次x高x宽x通道),并且要做归一化。很多人在这里栽跟头,不是模型写得不对,是数据的shape不匹配。
第二,CNN本身的架构设计。卷积核数量怎么设、池化窗口多大、全连接层为什么放最后、Dropout放在哪里,这些不是拍脑袋定的,背后有对特征提取层数、参数规模、过拟合风险的综合考量。
第三,训练过程的有效管理。包括数据集的随机打乱、验证集的划分、损失函数和优化器的选择、训练轮数(epoch)的控制、训练曲线的解读。这些是模型能不能“收敛”的关键。
这个项目选了一个公开的图像数据集作为实验对象。输入是固定尺寸的图片,输出是对应的类别标签。整个过程从零开始,不加载任何预训练权重,完全靠数据驱动训练一个小型CNN。这样你能清楚地看到:每一层卷积提取到了什么特征,池化做了什么,全连接层如何把特征映射到类别,准确率是怎么一步步涨上去的。
1.3 为什么选择构造小型CNN,而不是直接加载预训练模型
我在帮别人看代码时经常遇到一种情况:项目需求明明只有十个类别、数据量也就几万张,上来就直接加载ResNet之类的大型预训练模型。这种方案当然也能跑,但有一个致命问题——你无法真正理解网络在做什么,因为你跳过了所有设计过程。
这个项目刻意选择了从头构造一个小型CNN,同时在文章里也没有使用任何预训练权重。原因有三点:
- 小模型的训练速度快,迭代周期短,适合观察每一次参数调整带来的效果变化。
- 参数量小,过拟合问题更明显,正好用来展示Dropout、数据增强这些正则化手段的实际作用。
- 结构简单透明,每一层的输出可以很直观地可视化,真正看到卷积核学习到了边缘、纹理等信息。
等你把小型CNN从数据到训练整个流程吃透了,再去迁移学习、微调大型模型,就是顺水推舟的事。基础不牢,后面全是窟窿。
2. 工具版本选择与代码结构设计
2.1 版本锁定:TensorFlow 2.x是底线
这个项目必须建立在TensorFlow 2.x之上,这是硬性前提。1.x版本的占位符(placeholder)、会话(Session)机制已经过时,不建议任何人再学。Keras自2.3之后整合进了TensorFlow,所以你不需要单独安装Keras包,直接用tensorflow.keras即可。
我自己实测过几套版本组合,比较稳的搭配是:Python 3.8-3.10、TensorFlow 2.8-2.15、NumPy 1.21-1.24。为什么特意提NumPy版本?因为TensorFlow和NumPy之间版本兼容性特别敏感,新版NumPy(比如1.26以上)在某些老版本的TensorFlow下可能会报编译错误。卡住的人十有八九是环境问题,不是代码问题。
提示:如果你的机器有独立显卡,建议安装带GPU支持的TensorFlow版本。CUDA和cuDNN的版本匹配是另一个容易踩坑的地方,建议直接去TensorFlow官方文档里对照对应关系表,不要自己猜。
2.2 代码文件的组织方式
这个项目我建议按下面这个结构组织,虽然简单,但比把几百行代码塞在一个文件里要合理得多:
cnn_project/ ├── config.py # 全局参数统一维护 ├── data_preprocess.py # 数据加载与预处理 ├── model.py # CNN模型结构定义 ├── train.py # 训练主流程 ├── evaluate.py # 模型评估与预测 └── utils.py # 辅助函数:绘图、日志等可能有人觉得做个小项目没必要分文件,但我的经验是:你这次可能只是跑通,但过两周你可能想把数据增强加上,或者把模型换成ResNet,如果所有代码都堆在一个文件里,改动起来牵一发动全身。配置文件单独抽出来,模型结构和训练逻辑分开,是一个性价比非常高的习惯。
3. 数据预处理:模型喂什么比模型怎么写更关键
3.1 数据到底要处理成什么样子
CNN不会直接吃图片文件。它吃的是“数字张量”——就是一堆数字排成的多维数组。一张28x28像素的灰度图片,在计算机里就是28x28的矩阵,每个元素是0到255之间的整数,代表该像素的亮度。如果是彩色图片,就是28x28x3的三维数组,最后一个维度是RGB三个通道。
在送入模型之前,必须做三件基础的事情:
第一,归一化。把像素值从0-255缩放到0-1,方法就是直接除以255。这一步极其重要。如果不做归一化,数值范围太大,梯度更新会非常不稳定,模型的收敛很慢,甚至在训练初期就loss爆掉变成NaN。这就像量身高时用纳米做单位,数字大得离谱,计算起来毫无意义。
第二,reshape成四维张量。TensorFlow的卷积层要求输入是(batch_size, height, width, channels)的形式。训练时你不可能把整个数据集一次性塞进内存,所以按批次送入,比如每次取32张图片,那输入就是(32, 28, 28, 1)。如果你的图片是彩色三通道,最后一维就是3。灰度图之所以是1,就是告诉模型“这不是彩色图”。
第三,数据打乱。训练时每个批次的数据要从整个数据集中随机抽取,不能让模型按顺序学习。否则模型会学到数据集的排列顺序,而不是真正区分图像内容。Keras的fit函数里有shuffle=True参数,默认就是开着的,但你如果自己写训练循环,一定不能忽略。
3.2 数据增强:把一份数据用出五份的数据量
如果数据集不大,一个非常有效的技巧是数据增强。简单说,就是对原始图片做随机变换,让模型看到“看起来不同但本质相同”的样本。常用的包括随机水平翻转、小幅度的旋转、平移、缩放、亮度调整等。
数据增强的意义在于:它能极大缓解过拟合。模型见过各种变形的猫的照片后,再看到一张角度稍微倾斜的猫,它依然能认出是猫。无论训练集还是测试集,这些变换只应用在训练阶段。测试集必须保持原始数据不变,否则评估结果就不公平。
TensorFlow的ImageDataGenerator已经封装好了这些操作,用起来非常方便。当然,这个项目的实验数据集已经做了很多预处理,比如调整到固定尺寸、数值范围归一化,自带数据增强与否,要看具体数据集的复杂度来定,不要无脑加。
4. CNN模型搭建:动手实现卷积神经网络
4.1 核心组件的角色认知:卷积、池化、激活、全连接
在写代码之前,很有必要把CNN的几个核心组件的角色讲透,因为它们就像流水线上的不同工序,各管一段。
卷积层是特征提取器。你可以把卷积核想象成一个小手电筒的透镜,在整张图上按步长滑动,每次照到一小块区域,计算这块区域和透镜内数字的点积,得到一个数值。所有位置都滑完后,得到一张“特征图”。不同的卷积核关注不同的模式:有的识别横线,有的识别竖线,有的识别颜色块。浅层的卷积核关注边缘和纹理,深层卷积核在浅层特征基础上组合出更抽象的部位信息,比如眼睛、车轮等。
池化层是降采样器。它的作用是压缩特征图的尺寸,减少参数数量,同时保留主要特征。最常用的是最大池化(Max Pooling),取2x2窗口内最大的数值作为输出。形象地说,它像你把一张高清大图缩小成缩略图,信息量减少了一些,但主要轮廓还在。池化的另一个好处是让模型对轻微的平移、缩放不那么敏感——猫在图片里向左移了三个像素,池化后特征图基本不变。
激活函数是非线性变换。如果没有激活函数,再怎么堆叠卷积层,本质上还是线性运算,根本学不了复杂模式。ReLU是当前最主流的选择,公式是max(0, x),负值直接置零,正值保留。它的计算极快,又能缓解梯度消失问题。
全连接层是分类器。卷积和池化提取到的是特征张量,要在末尾做分类,就需要把这些特征展平成一维向量,送入全连接层。最后一层的神经元数量等于类别数,输出每个类别的得分。使用Softmax激活后,得分变成概率分布——每个类别的预测概率加总为1。
4.2 模型结构的参数设计与代码实现
下面给出这个项目使用的CNN结构。它是一个经典的小型网络,可以在CPU上十几分钟内跑完一轮完整的训练,同时又能保证足够的特征表达能力。
输入层:(28, 28, 1)——灰度图,单通道。
第一个卷积块:Conv2D(32, (3,3), activation='relu'),之后接MaxPooling2D((2,2))。
第二个卷积块:Conv2D(64, (3,3), activation='relu'),之后接MaxPooling2D((2,2))。
第三个卷积块:Conv2D(128, (3,3), activation='relu'),之后接MaxPooling2D((2,2))。
展平层:Flatten()。
全连接层:Dense(128, activation='relu'),之后接Dropout(0.5)。
输出层:Dense(类别数, activation='softmax')。
为什么第一个卷积块是32个卷积核,而不是64或16?这不是拍脑袋。第一层卷积直接从原始像素提取特征,主要学习边缘、颜色梯度,一个特征图可能就够用了。32个核意味着能学到32种不同的基础特征,对大多数小数据集够用。更关键的是,第一层卷积核数量决定了输入到第二层的通道数。如果一开始就上128个核,参数规模会急剧膨胀,而训练数据不够多时,这些多余的参数只会导致过拟合。
为什么第二层翻倍到64?因为第二层是在第一层特征的基础上组合更复杂的模式。特征图的尺寸减半,但通道数翻倍,这在参数总量上是一个平衡的设计——用更多的通道来表达更丰富的组合特征,同时空间维度降低避免计算量爆炸。第三层128同理。
为什么每层卷积后只做一次池化而不是每个卷积都池化?池化在降维的同时也在丢失空间信息。两个卷积层之间会保留更多的空间精确位置,这在分类任务中有帮助。过度池化会导致特征图太小,深层卷积无处提取特征。我见过不少新手在每个Conv2D后面都跟一个池化层,结果网络很快就把特征图缩到了1x1,后续层完全失去意义。
Dropout放在全连接层后面是有讲究的。全连接层的参数量是所有层里最大的,最容易过拟合,Dropout的作用就是在训练时随机让一部分神经元失活(输出置零),强迫网络不要依赖某一个特定的神经元,而是让多个神经元协同工作。放在卷积层后面也有用,但效果没有全连接层那么显著,而且在训练早期会拖慢收敛速度。
对应的Keras代码如下:
import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_shape=(28, 28, 1), num_classes=10): model = models.Sequential([ layers.Input(shape=input_shape), layers.Conv2D(32, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), layers.Dense(num_classes, activation='softmax') ]) return modelpadding='same'的作用是保持卷积输出的空间尺寸不变。因为3x3卷积核在滑动时,边缘像素被访问的次数少于中间像素,不做填充的话,特征图尺寸会逐层缩小,而且边缘信息丢失比较严重。same会在输入周围补零,让输出尺寸等于输入尺寸除以步长。这里用same是为了让特征图尺寸只受池化层控制,便于计算中间层尺寸的变化。
4.3 模型参数量到底怎么算
很多初学者对“卷积层参数量”没有概念,我建议你自己算一次。以第一个卷积块为例:输入是1个通道,输出32个通道,卷积核是3x3,那么参数量是1 * 32 * 3 * 3 = 288,还要加上每个输出通道一个偏置项,就是32 + 288 = 320。别小看这320个参数,它意味着一块3x3大小的视野在一次滑动中要计算9个数和一个偏置的乘积和。几十万个参数在网络深层累积起来,就是模型的内存占用和计算量的来源。
你可以用model.summary()直接查看每一层参数量和输出尺寸,这个输出是排查shape问题的最佳工具。如果在某个地方报维度不匹配,先运行summary()看每一层输出是什么,就能很快定位是展平后的长度不对,还是全连接层的输入维度算错了。
5. 训练配置与关键参数选择
5.1 损失函数和优化器:这是模型的“方向盘”和“油门”
分类问题用的是交叉熵损失函数。它的含义是:衡量模型输出的概率分布和真实标签的概率分布之间的差异。模型越接近正确标签,损失值越小。Keras里,整数标签对应SparseCategoricalCrossentropy,独热编码标签对应CategoricalCrossentropy。初学者经常把这两者搞混,记住一句话:如果y_train是整数数组,用Sparse那个;如果y_train是0/1矩阵(独热),用不带Sparse的那个。这个项目的数据集标签是整数形式,所以代码里用的是SparseCategoricalCrossentropy。
优化器选择了Adam。它是目前最常用的自适应学习率优化器,核心思想是:每个参数都有自己的学习率,更新幅度根据历史梯度的变化自动调整。它比传统的SGD更稳,不需要花大量精力调学习率,默认为0.001已经能跑得很好。在后续调参时可以尝试SGD加动量,但第一版直接用Adam,因为它能让你很快看到“学得动”的效果。
5.2 训练轮数、批次大小与验证集划分
训练轮数epochs就是整个数据集被完整过几遍。设得太少,模型欠拟合,还没学会就停了。设得太多,模型过拟合,开始死记硬背训练集,测试集的表现反而下降。这个项目建议先设15轮,每轮结束后打印验证集准确率,用曲线来判断有没有继续训练的价值,而不是一开始就拉满50轮干等。
批次大小batch_size影响训练稳定性和速度。批次太大,一次吃太多数据,每个参数更新方向更稳,但训练变慢;批次太小,梯度噪声大,训练震荡,但每次更新快。这个项目选128,是理论上“较小数据集+小型CNN”的一个比较稳妥的折中。用小数据集时可以适当调小到64,观察loss曲线的稳定性。
验证集不能和训练集重叠。如果在训练集上评估模型,指标会虚高,因为这相当于开卷考试。这个项目直接利用数据集自带的测试集划分,训练集负责学,测试集负责考,互不干扰。这是一种比较合理的评估方式。
5.3 模型编译与训练代码
model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) history = model.fit( x_train, y_train, batch_size=128, epochs=15, validation_data=(x_test, y_test) )fit返回的history对象记录每一轮训练和验证的loss与准确率,这是之后绘制训练曲线的数据来源。训练能跑通,只是第一步。第二步是分析这些曲线,判断模型是否过拟合、欠拟合、学习率是否合适。
学习率衰减是一个很实用但经常被忽略的技巧。训练初期模型离最优点很远,可以用较大的学习率快速下降。随着训练推进,梯度变小,损失函数曲面变得平坦,这时如果不降低学习率,参数会在最优点附近来回震荡,loss死活下不去。Keras里有一个回调函数ReduceLROnPlateau可以自动监控验证集loss,连续几轮不下降时自动把学习率乘一个factor,比如0.5。使用回调之后,训练后期曲线会明显更稳。
from tensorflow.keras.callbacks import ReduceLROnPlateau reduce_lr = ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6 )5.4 训练曲线怎么看
训练结束后,把历史记录里的loss和val_loss画出来,这是评估模型最直观的方法。一段正常的训练曲线应该是:训练loss逐渐下降并趋于平缓,验证loss先下降后也趋于平缓。假如验证loss在某个点之后反弹上升,而训练loss还在下降,这就说明过拟合了——模型开始记答案而不是找规律。假如两条曲线都还在一路猛跌,说明15轮不够,可以接着训练。
准确率曲线也是同理。看训练集和验证集准确率的差距,如果在5到10个百分点以上,基本可以断定过拟合需要加正则化手段。最常见的操作就是调整Dropout的比例,或者引入数据增强。这比在模型结构上加层、加神经元更有效。
6. 模型评估与预测:怎么验证项目“真的做完了”
6.1 评估和预测,是Step的必选项
训练完模型不评估,这个项目等于白做。用model.evaluate()在测试集上跑一遍,得到loss和准确率,是你对外宣称“模型达到多少准确率”的唯一依据。注意这里用的是没有参与训练的数据,才有说服力。
test_loss, test_acc = model.evaluate(x_test, y_test) print(f'测试集准确率: {test_acc:.4f}')预测部分可以用model.predict(),输入一批图片数据,输出的是每个类别的概率分布,取argmax就是预测类别。对于随机选几张测试图片,打印出“真实标签 vs 预测标签”的对比,可以让项目结果很直观地呈现在眼前。
6.2 可视化:看卷积层到底学到了什么
这个环节有一定的进阶意味,但我强烈建议动手做一次。取测试集中的某一张图片,经过模型第一层卷积,把输出的32张特征图绘制出来。你会发现,这些特征图看起来像原图的“轮廓滤镜版”,有的特征图响应横线,有的响应竖线,有的响应边缘。当你想验证一个CNN是不是真的在“看”图片,这一步已经是很好的实证。
更高阶的做法是用GradCAM生成热力图,显示模型分类时重点关注图片的哪些区域。在图像领域做诊断时,这个工具能很直观地告诉你:模型是学到了关键特征,还是靠背景信息“蒙对”了。这对于后续调优模型的方向非常有帮助。
7. 常见问题与项目避坑技巧实录
7.1 loss不下降、为NaN、shape报错
我见过最多的问题,是把loss不下降归咎于模型结构,结果最后发现是数据压根没喂对。所以,按现象分类排查是最高效的方式。
loss一直是初始值附近:先检查数据归一化和打乱,再看学习率是否设置得太低。如果训练第一个epoch结束时loss没比随机猜测好多少,说明模型在“乱学”,优先调整数据预处理,其次才是网络结构。
loss变成NaN:通常是数值溢出。常见原因包括学习率过大导致梯度爆炸,或者数据里混入了异常值。此时降低学习率到0.0001,再检查数据是否存在除以0的情况,基本能解决。
shape报错:ValueError: Input 0 of layer "conv2d" is incompatible with the layer。这类错误几乎都是输入张量的维度不对。你只需要打印一下x_train.shape,对照build_cnn里设置的input_shape,多半会发现自己把(28, 28)传进去而漏了通道维1。卷积层要求的(h, w, c)中,c不能为空,即使灰度图也要写成1。
7.2 显存不足、训练速度慢的排查
GPU训练时爆显存,最直接的做法是先降低批次大小,从128降到64甚至是32。如果还爆,检查是否有其他程序占用显存。TensorFlow默认申请几乎全部可用显存,可以通过tf.config设置按需增长,避免启动即占满。
训练速度慢,要先搞清楚瓶颈在数据读取还是计算。把数据预读取和训练放在流水线上并行,Keras的DATSetsAPI可以帮你做。不要一开始就把锅甩给GPU性能,很多时候是CPU在数据加载环节拖后腿。
7.3 过拟合:小项目中最常见的模型问题
过拟合的典型表现是训练集准确率接近100%,验证集准确率却卡在80%左右。如果你用的数据集本身类别多且样本不均衡,这个现象会更明显。最直接的缓解手段包括:增加Dropout比例、加入数据增强、减少网络层数或神经元数量、加入L2正则化。不要只调一个参数,项目里建议逐步叠加验证,修改一处跑一轮,观察效果,而不是同时改动所有参数,那样出了问题也无法定位是哪一步的锅。
7.4 问题排查速查表
| 现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| loss保持高位不降 | 数据未归一化/学习率过低 | 打印x_train范围,确认归一化 |
| loss为NaN | 学习率过大/数据异常 | 调低学习率至1e-4,检查数据 |
| 验证集准确率远低于训练集 | 过拟合 | 加Dropout比例/加数据增强 |
| 训练极其缓慢 | 批次过小/数据加载瓶颈 | 调整batch_size,检查数据管道 |
| 维度报错 | input_shape写错 | 执行model.summary()定位 |
| GPU显存不足 | 批次过大/多程序占用 | 降低batch_size,设置显存按需增长 |
8. 亲手跑通之后,怎么进一步做项目扩展
如果你按照上面的流程完整走了一遍,你已经具备了基本的深度学习项目能力。但这个项目能扩展的方向还很多,这里列出四个简单但非常有效的切入点,选择一个自己喜欢的继续往下做,收获会翻倍。
8.1 加入BatchNormalization层
批归一化(Batch Normalization)是近年来很实用的技术,核心作用是在每一层激活之前把数据重新拉到均值为0、方差为1的分布,从而稳定训练。它最大的实际收益是:你可以用更大的学习率训练,收敛速度有明显提升,而且对参数的初始值不那么敏感。在Keras里加实现很简单,把它放在卷积层和激活函数之间即可。一个值得做的实验是:训练一个加了BN的CNN,再训练一个没加BN的CNN,比较最终收敛精度和训练时间。这是理解BN价值的最好方式。
8.2 引入数据增强后重新训练
把训练数据做一些随机旋转、移位、缩放和水平翻转,重新训练同一个模型。你会发现验证集准确率往往有提升。数据增强等于告诉模型“任务比你想的更复杂”,天然带有正则化的作用。注意图片增强的参数不能太狠——一张猫的照片旋转45度还能认出是猫,但旋转170度就变成“倒挂的猫”,如果增强过度,会反而损害训练效果。
8.3 把小型CNN换成经典网络结构
当你能熟练调整参数后,我建议试一下把模型主体替换成已有的经典结构,比如去掉自定义的卷积块,改用残差结构(比如一个简易版ResNet)。这种结构能训练更深的网络而不退化,是通向更高级项目的必经之路。对比一下在不改变训练设置的情况下,残差网络是否比原来的普通结构效果更好,亲手做一次,你对深度学习话题的讨论会更有底气。
8.4 用GradCAM可视化模型注意力
你可能只关心分类准确率,但实际项目中,模型的可解释性往往更重要。比如在医学辅助诊断场景,你不仅要告诉医生“这张图有病灶”,还得告诉他“模型是依据哪些区域判断的”。GradCAM能生成一张热力图,标出模型在分类时关注的区域。当你发现自己模型分类准确率很高但热力图每次都聚焦在背景上,这通常意味着模型学到了你不希望它学到的东西,典型的就是数据本身的偏好痕迹,校正方式和数据增强就派上用场了。
9. 一点自己动手之后的心得
亲手写完这个项目后,最大的感受是:深度学习的门槛不在于“调用框架”,而在于“观察模型行为并做出正确判断”的能力。框架代码大家都会写,难的是当准确率不涨时,你能从数据、结构、训练策略三个维度去定位问题。这份经验只能从一次次的实操和踩坑里积累。
我在实际调试中经常看到有人把模型的每个小细节都改一遍,十来个参数同时调,结果模型一团糟,根本不知道是哪一步起的作用。正确做法是每次只改一个变量,保住一个基线,让结果可对比。这个项目也一样,你完全可以跑一个基础版,再逐步叠加数据增强、Dropout、学习率衰减,每一步都记录心得,之后回头整理这些曲线你就有了第一手经验。
最后分享一个小技巧:训练过程中把每一轮的验证集准确率记录下来,写完脚本后仔细观察曲线变化的拐点,很多结构问题并非需要训练完才能发现。第一轮结束时准确率还不到随机水平,说明任务难度太高或者模型容量严重不足,早停重来,别干等。这些经验别从别人博客里抄,直接动手,自己起码跑一遍完整的流程,整套东西才会真正长在你身上。