一、这个项目在做什么

一句话概括:教会计算机看图识物

具体来说,我们拿到了一个叫 CIFAR-100 的图片数据集——6 万张 32×32 像素的小图片,涵盖苹果、汽车、老虎、向日葵等 100 种类别。我们的任务是训练神经网络,让它看到一张图后能判断"这是什么"。

听起来简单,但 32×32 像素非常小(大约指甲盖大小的缩略图),而且 100 个类别里有很多很相近的东西(比如不同品种的花、不同类型的车),所以分类难度其实不低。

为了搞清楚"什么样的网络 + 什么样的训练方式效果最好",我们设计了一组系统的对比实验:两种网络结构(MLP 和 CNN),两种优化器(SGD 和 Adam),三个学习率(0.1、0.01、0.001),交叉组合一共 12 组实验,一键运行,自动对比。

项目见:
https://github.com/david-gao1/883AI/tree/main/%E7%A0%94%E7%A9%B6%E7%94%9F%E4%BD%9C%E4%B8%9A/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0


二、两种网络:MLP 和 CNN

MLP——最朴素的做法

MLP(多层感知机)是最基础的神经网络。它处理图片的方式非常粗暴:把一张 32×32×3 的彩色图片直接"拍平"成一个长度为 3072 的数字串,然后送进两层全连接网络,最后输出 100 个分数,分数最高的那个类别就是答案。

MLP 的问题在于,它把图片拍平的一瞬间,所有空间位置信息就丢了——左上角的像素和右下角的像素在向量里变成了地位平等的数字,网络不知道它们谁和谁是邻居。一只猫的眼睛和耳朵明明挨着,拍平后网络完全感知不到这个关系。

但 MLP 并不是没有价值。它是一个重要的基线——先用最朴素的方式跑一遍,再用更高级的方式跑一遍,两相对比才能清楚地看出改进有多大。

CNN——像人眼一样看图

CNN(卷积神经网络)的设计哲学直接模仿了人眼的工作方式。你看一张猫的照片,大脑不会一个像素一个像素地扫描,而是先看局部——这里有尖尖的轮廓(耳朵)、这里有圆形高光(眼睛)、这里有柔和纹理(毛发),然后把这些局部特征组合起来得出"这是一只猫"的结论。

CNN 做的事情一模一样:

  1. 用一个 3×3 的小窗口(卷积核)在图片上滑动,每滑一步就计算出窗口覆盖区域的一个"特征值"。不同的卷积核能检测不同的模式——有的专门找水平边缘,有的专门找垂直边缘,有的对颜色变化敏感。这些卷积核不是人工设计的,而是网络在训练过程中自己学出来的。

  2. 卷积扫描完一遍后,用池化操作把特征图缩小——每 2×2 的区域取最大值,尺寸减半。这样做既减少了计算量,又让网络的"视野"变大了:同样的 3×3 卷积核,在缩小后的特征图上实际覆盖的原图区域更大。

  3. 重复几轮"卷积 + 池化",特征从低级(边缘、色块)逐渐变成高级(纹理、部件、物体轮廓)。一张 32×32 的图片经过三轮处理后变成了 4×4,空间尺寸缩了 8 倍,但通道数从 3 涨到了 256——图变小了,每个位置蕴含的"语义信息"却更丰富了。

  4. 最后把这些高级特征拍平,送进全连接层做最终分类。

本项目的 CNN 采用了经典的 VGG 风格设计:三个阶段,每个阶段包含两层 3×3 卷积(通道数依次为 64、128、256),中间穿插批归一化(稳定训练)、ReLU 激活(引入非线性)、最大池化(缩小尺寸)和 Dropout(防过拟合)。麻雀虽小,五脏俱全——理解了这个结构,再去看 ResNet、EfficientNet 这些更复杂的网络,就只是在这些基础组件上做加法。


三、训练一个神经网络意味着什么

神经网络刚创建时,所有参数(卷积核里的数字、全连接层的权重)都是随机的,它对图片的分类完全靠蒙。训练的过程就是不断地"看题 → 答题 → 对答案 → 改参数",循环往复直到它学会为止。

每一轮(epoch)的训练流程是这样的:

第一步:前向传播。 把一批 128 张图片送入网络,网络层层计算,最后输出每张图对 100 个类别的得分。

第二步:算损失。 用交叉熵损失函数衡量"预测和真实答案差多远"。如果网络给正确类别打了最高分,损失就小;如果正确类别的分数被其他类别压过去了,损失就大。

第三步:反向传播。 从损失出发,沿着网络的计算链路从后往前追溯,算出每个参数对这次错误"贡献"了多少——这就是梯度。梯度告诉你:如果你把这个参数往某个方向调一点点,损失会变小还是变大。

第四步:更新参数。 优化器根据梯度调整参数:往损失变小的方向走一步。步子多大由学习率决定。

把训练集里的 45000 张图全部过一遍,就算完成了一个 epoch。本项目默认训练 10 个 epoch。

训练过程中还有一个重要的细节:每个 epoch 结束后,我们会在验证集(5000 张图)上评估模型的表现,记录验证准确率最高的那一轮模型参数。为什么不直接用最后一轮的参数?因为最后一轮不一定最好——可能第 7 轮时验证准确率最高,之后模型反而开始"死记硬背"训练数据了(过拟合)。保留验证集上最好的那一轮,就是一种简单有效的防过拟合策略。


四、优化器和学习率——训练效果的决定性因素

SGD 和 Adam 的区别

优化器决定了"怎么根据梯度调参数"。可以用一个下山的比喻来理解:你站在山上,想尽快走到最低点(损失最小的地方)。

SGD(随机梯度下降 + 动量) 的做法是:每一步沿着当前最陡的下坡方向走一步,步长就是学习率。加上动量后,它还会"记住"之前的行进方向——就像一个滚下山的球有惯性,不容易被小坑小洼拦住。SGD 的所有参数共用一个学习率,所以对学习率的选择很敏感:太大会震荡飞出去,太小会爬得很慢。但如果调好了,SGD 训出来的模型泛化性能往往不错。

Adam(自适应矩估计) 则更聪明:它给每个参数单独维护一份"档案",记录这个参数最近的梯度大小和波动程度。梯度一直很稳定的参数,它就大胆地迈大步;梯度忽大忽小的参数,它就谨慎地迈小步。这种"自适应学习率"的特性让 Adam 几乎不需要精心调参,开箱即用,收敛通常也更快。代价是每个参数多了两倍的存储开销。

学习率的影响

学习率控制的是每步走多远。本项目试了三个值:

  • 0.1(太大):相当于下山时跳着走,很可能一脚踩过头跳到对面山坡上。实验中 Adam + 0.1 的组合直接完全发散,准确率降到了 1%——等于在 100 个类别里瞎猜。
  • 0.001(太小):相当于一毫米一毫米地挪,10 个 epoch 还没走到好的位置。SGD + 0.001 的收敛速度明显慢于 SGD + 0.01。
  • 0.01(适中):正常步伐走路,稳定下山。实验中 SGD + 0.01 是 CNN 的最佳组合,Adam 的最佳学习率则更小一些(0.001),这恰好印证了 Adam 的自适应步长本身就偏大,需要更小的初始学习率来配合。

实验结果充分说明了一个道理:学习率是最敏感的超参数。一个不合适的学习率可以直接让训练崩掉,而优化器的选择反倒是次要的。


五、防止过拟合的三道防线

神经网络有很强的"记忆力",如果不加约束,它很容易把训练数据的细枝末节都背下来(包括噪声和异常),到了新数据上就抓瞎——这就是过拟合。本项目用了三种手段来对抗它:

第一道:数据增强。 训练时,每次读取一张图片都随机做两件事:四周补 4 像素后随机裁剪回 32×32(相当于随机平移),以及 50% 概率水平翻转。这让模型每次"看到"同一张图的不同变体,等价于人为增加了数据量,减轻了死记硬背的可能。

第二道:Dropout。 训练时随机关掉 30% 的神经元(CNN 里是关掉整个通道),迫使网络不能依赖任何一个特定的神经元来做决策。每一步训练的其实是一个"残缺版"的网络,最终效果相当于集成了指数级多个小网络,泛化能力天然更强。测试时所有神经元都打开,PyTorch 会自动处理这个切换。

第三道:权重衰减。 两个优化器都加了 weight_decay=1e-4,每次更新参数时都把权重往零的方向拉一点点。这防止参数长得过大,让网络保持"简洁"。

此外,批归一化(BatchNorm)本身也有轻微的正则化效果——因为每个 batch 的统计量有随机性,等于给训练加了一点噪声。


六、实验结果:CNN 对 MLP 的碾压

12 组实验全部跑完后,结果非常清晰:

CNN 全面优于 MLP。 在最佳配置下(SGD + lr=0.01),CNN 测试准确率 38.69%,MLP 只有 20.45%——将近一倍的差距。在所有对应配置下,CNN 都比 MLP 好,没有例外。这个结果并不意外:CNN 保留了图片的空间结构,能看到"哪些像素是邻居",而 MLP 一拍平就把这些信息全丢了。

SGD 比 Adam 更稳。 在较宽的学习率范围内(0.001~0.01),SGD 都能有效训练,而 Adam 只在 lr=0.001 时可用,lr 稍大就崩掉。SGD 是图像分类里的经典选择,配合动量和合理的学习率就能出好结果。

10 个 epoch 远远不够。 最好的模型(CNN + SGD + lr=0.01)到第 10 轮时验证准确率还在持续上升,loss 还在持续下降,完全没有收敛。如果训练 50~100 个 epoch,配合学习率衰减策略,准确率预计还能大幅提升。

学习率 0.1 对 Adam 是灾难。 Adam 自适应步长本身就偏大,再给一个大学习率,参数更新就完全失控了。CNN + Adam + lr=0.1 和 MLP + Adam + lr=0.1 的测试准确率都掉到了 1%,和瞎猜一样。

从训练曲线上也能清楚地看到这些规律:好的配置下 loss 稳定下降、accuracy 持续上升,差的配置下 loss 高位震荡、accuracy 趴在底部不动。


七、工程层面:怎么把实验跑起来

好的实验不只是算法好,还需要工程设计能让实验"跑得顺、管得住、扩展得了"。

文件组织

整个项目分成几个清晰的模块:

  • src/models.py 只管"网络长什么样"——定义 MLP 和 CNN 两个类,以及一个根据名字创建模型的工厂函数。将来要加新模型(比如 ResNet),只在这个文件里加一个类和一行注册代码就行。
  • src/trainer.py 管"怎么训练"——数据加载、优化器创建、训练循环、画图、保存结果,全在这里。
  • train.py 是总指挥——它自己不实现任何逻辑,只负责解析命令行参数、生成实验组合、依次调用上述函数。
  • summarize_results.py 是报告员——训练完成后读取所有实验的 JSON 结果,生成一份 Markdown 汇总报告。

这种"调度层 / 逻辑层 / 模型层"分开的设计,让任何一处改动都只影响一个文件,不会牵连其他部分。

一键跑完 12 组实验

所有超参数都通过命令行控制。默认配置就是 2 模型 × 2 优化器 × 3 学习率 = 12 组,一行命令跑完:

uv run train.py --epochs 10

如果只想快速验证代码能不能跑通,可以用 5% 的数据跑 1 轮:

uv run train.py --epochs 1 --subset-ratio 0.05

每组实验的结果独立存放在各自的目录下(模型参数、训练曲线、预测可视化、指标 JSON),最后汇总成一张 CSV 排名表。

可复现性

固定随机种子(42)保证了同样的代码 + 同样的参数永远得到同样的结果。设备自动选择(Mac GPU → NVIDIA GPU → CPU)让代码在任何机器上都能跑。依赖用 pyproject.toml + uv.lock 管理,uv sync 一行命令安装好所有库。


八、核心概念速查

概念 一句话解释
卷积 用小窗口在图片上滑动,提取局部特征
池化 每个小区域取最大值,缩小图片保留关键信息
ReLU 负数变 0,给网络引入非线性——没有它再多层也等于一层
BatchNorm 校准每层输出的分布,让训练更稳定更快
Dropout 训练时随机关掉一部分神经元,防止网络死记硬背
全连接层 每个输入都和每个输出相连,用于最终分类
交叉熵损失 衡量"你给正确类别打了多高的分",分数越高损失越小
前向传播 数据从输入到输出依次经过各层的计算
反向传播 从损失出发,逆序算出每个参数的梯度
梯度 告诉你"参数往哪个方向调能让损失变小"
学习率 每次调参走多大一步
epoch 完整遍历训练集一次
batch 一次送入网络的一小批图片(128 张)
过拟合 训练集表现好、新数据表现差——"背答案"了
数据增强 对训练图片做随机变换,增加多样性

九、总结与展望

这个项目虽然只用了最基础的 MLP 和 CNN,但完整覆盖了深度学习图像分类的核心链路:数据加载与预处理 → 模型设计 → 损失函数选择 → 优化器配置 → 训练循环 → 结果评估与可视化。12 组对比实验清晰地展示了三个关键结论:CNN 比 MLP 强在哪里(空间特征提取)、学习率为什么是最敏感的超参数(太大发散太小龟速)、SGD 和 Adam 各自的脾气(SGD 稳但需调参,Adam 方便但对大学习率敏感)。

从工程角度看,项目采用了模块化设计、命令行参数控制、笛卡尔积实验遍历、独立目录隔离结果、自动生成报告等实践,使得整个实验流程可复现、可扩展、易维护。

如果继续改进,有几个明确的方向:增加训练轮数配合学习率衰减(余弦退火或阶梯衰减)、引入更深的网络结构(ResNet 的残差连接可以让训练几百层成为可能)、加强数据增强策略(Mixup、CutMix)、使用学习率预热(Warmup)缓解初始阶段的不稳定。但即使是当前这个版本,它已经把深度学习图像分类的核心知识和工程实践都串起来了——理解了这些,再往更深更广的方向走,都会顺畅很多。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐