从神经网络原理到 CIFAR-10 图片分类实战
一、项目简介
本项目使用 PyTorch 搭建了一个简单的卷积神经网络 CNN,用来完成 CIFAR-10 图片分类任务。
CIFAR-10 是一个经典图像分类数据集,一共有 10 个类别:
| 类别 | 英文名 |
|---|---|
| 飞机 | airplane |
| 汽车 | automobile |
| 鸟 | bird |
| 猫 | cat |
| 鹿 | deer |
| 狗 | dog |
| 青蛙 | frog |
| 马 | horse |
| 船 | ship |
| 卡车 | truck |
每张图片大小为 32 × 32,并且是 RGB 彩色图片,所以输入模型时可以理解成:
3 × 32 × 32
其中 3 表示 RGB 三个颜色通道。
本项目的完整流程是:
加载数据集 -> 数据预处理 -> 建立 CNN 网络 -> 声明损失函数和优化器 -> 训练模型 -> 测试模型 -> 可视化结果
二、什么是神经网络
神经网络可以简单理解为一个“可以学习规律的函数”。
普通程序通常需要人手动写规则,例如:
如果有尖耳朵、胡须、圆脸,那么可能是猫
但图片太复杂了,猫、狗、车、飞机在不同角度、颜色、背景下都会变化,很难靠人工写规则判断。
神经网络的做法是:
给模型大量图片和正确答案,让模型自己从数据中学习规律
比如我们给模型很多猫的图片和标签 cat,很多狗的图片和标签 dog,模型就会逐渐学会区分猫和狗。
三、神经网络的基本训练流程
训练神经网络主要包含四个核心步骤:
前向传播 -> 计算损失 -> 反向传播 -> 更新参数
可以把它理解成一次学习过程:
| 步骤 | 作用 |
|---|---|
| 前向传播 | 模型根据输入图片给出预测结果 |
| 计算损失 | 判断预测结果和真实答案差多少 |
| 反向传播 | 计算每个参数应该怎么调整 |
| 更新参数 | 优化器根据梯度修改模型参数 |
这个过程会重复很多次,模型的预测能力就会逐渐提升。
四、什么是前向传播
前向传播就是图片从输入端进入模型,一层一层经过神经网络,最后得到预测结果的过程。
在本项目中,一张 CIFAR-10 图片进入模型后,大致流程如下:
原始图片
-> 卷积层提取特征
-> 激活函数增强表达能力
-> 池化层压缩尺寸
-> Dropout 减少过拟合
-> 全连接层进行分类
-> 输出 10 个类别分数
项目中的前向传播函数非常简洁:
def forward(self, x):
x = self.features(x)
return self.classifier(x)
这里的 x 就是输入图片。它先经过 features 提取特征,再经过 classifier 输出分类结果。
模型最后不是直接输出“猫”或者“狗”,而是输出 10 个类别对应的分数。哪个类别分数最高,模型就认为图片属于哪个类别。
例如模型可能输出:
| 类别 | 分数 |
|---|---|
| cat | 2.8 |
| dog | 1.6 |
| truck | -0.4 |
| ship | 0.2 |
如果 cat 的分数最高,那么模型就预测这张图片是猫。
五、本项目 CNN 网络结构
本项目使用的是一个简单 CNN 网络,主要分成两部分:
features:负责提取图片特征
classifier:负责根据特征进行分类
也可以理解为:
卷积部分负责“看图”
全连接部分负责“做判断”
项目中模型的主体写法如下,只保留关键结构:
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256),
nn.ReLU(inplace=True),
nn.Linear(256, num_classes),
)
这段代码体现了 CNN 的基本思想:前半部分不断用卷积层提取图像特征,后半部分用全连接层完成分类。
1. 特征提取部分 features
特征提取部分主要由卷积层、归一化层、激活函数、池化层和 Dropout 组成。
整体结构可以概括为三组:
| 组别 | 通道变化 | 图片尺寸变化 | 作用 |
|---|---|---|---|
| 第一组 | 3 -> 32 | 32×32 -> 16×16 | 提取基础特征 |
| 第二组 | 32 -> 64 | 16×16 -> 8×8 | 提取更复杂的局部特征 |
| 第三组 | 64 -> 128 | 8×8 -> 4×4 | 提取更高级的图像特征 |
图片在卷积部分中的变化大致是:
3 × 32 × 32
-> 32 × 32 × 32
-> 32 × 16 × 16
-> 64 × 16 × 16
-> 64 × 8 × 8
-> 128 × 8 × 8
-> 128 × 4 × 4
可以看到,随着网络变深:
图片尺寸越来越小
特征通道越来越多
这说明模型正在把原始图片逐渐压缩成更抽象的特征。
2. 分类部分 classifier
卷积部分输出的是:
128 × 4 × 4
这个结果还不能直接分类,所以需要先展开成一维向量:
128 × 4 × 4 = 2048
然后通过全连接层进行分类:
2048 个特征 -> 256 个特征 -> 10 个类别分数
最后的 10 对应 CIFAR-10 的 10 个类别。
六、每种网络层的作用
1. Conv2d 卷积层
卷积层是 CNN 的核心。
它的作用是从图片中提取局部特征,例如:
边缘
纹理
颜色块
轮廓
局部形状
在本项目中,卷积层一共有 5 个,是网络中最重要的特征提取部分。
2. BatchNorm2d 批归一化层
BatchNorm2d 的作用是让训练过程更加稳定。
可以简单理解为:它会调整每一批数据的分布,让模型训练时不容易大幅波动。
3. ReLU 激活函数
ReLU 是激活函数,它会把负数变成 0,正数保持不变。
输入:[-2, 0.5, 3, -1]
输出:[0, 0.5, 3, 0]
它的作用是给网络加入非线性能力。如果没有激活函数,网络即使堆很多层,表达能力也会很有限。
这里要注意:ReLU 不是代码注释,也不是“解释上一层”,它本身就是网络中真实执行的一步操作。
4. MaxPool2d 池化层
池化层的作用是缩小特征图尺寸。
例如:
32 × 32 -> 16 × 16
16 × 16 -> 8 × 8
8 × 8 -> 4 × 4
这样可以减少计算量,同时保留比较明显的特征。
5. Dropout 随机失活
Dropout 会在训练时随机丢弃一部分神经元输出。
例如:
Dropout(0.30)
表示训练时随机丢弃 30% 的输出。
它的作用是防止模型过度依赖某些特征,从而减少过拟合。
七、这个网络到底有几层
如果只数主要可学习层,本项目网络有:
5 个卷积层
2 个全连接层
所以可以说:
这是一个 5 层卷积 + 2 层全连接的简单 CNN 网络。
如果把 BatchNorm、ReLU、池化、Dropout、Flatten 都算进去,层数会更多。但在介绍网络结构时,通常重点说卷积层和全连接层。
八、损失函数是什么
模型训练时需要知道自己预测得好不好。衡量预测错误程度的函数,就叫损失函数。
本项目使用的是:
CrossEntropyLoss
它是多分类任务中最常用的损失函数。
在项目中声明损失函数的代码是:
criterion = nn.CrossEntropyLoss()
比如真实答案是 cat,但是模型认为 dog 的分数最高,那么损失就会比较大。
训练的目标就是:
让损失函数的值越来越小
损失越小,说明模型预测结果和真实标签越接近。
九、反向传播是什么
前向传播之后,模型得到了预测结果。接着损失函数会告诉模型“错得有多离谱”。
但模型还需要知道:
到底应该修改哪些参数?
每个参数应该往哪个方向改?
应该改多少?
这就是反向传播要做的事情。
反向传播会从损失函数开始,沿着网络反方向计算每个参数对错误的影响,这个影响就叫梯度。
可以简单理解成:
反向传播 = 给每个参数计算修改建议
十、优化器是什么
反向传播只负责计算梯度,也就是告诉模型“应该怎么改”。
真正修改参数的是优化器。
本项目使用的是:
Adam 优化器
对应代码如下:
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
优化器会根据梯度调整模型参数,让下一次预测更接近正确答案。
其中学习率 learning rate 控制每次参数更新的步子大小。
学习率太大,模型可能训练不稳定;学习率太小,模型训练会很慢。
本项目使用:
学习率 = 0.001
十一、一次训练循环发生了什么
训练时,模型会一批一批读取图片。每一批图片都会经历下面的过程:
1. 读取一批图片和标签
2. 把图片和标签放到 GPU
3. 前向传播,得到预测结果
4. 计算损失
5. 清空旧梯度
6. 反向传播,计算新梯度
7. 优化器更新参数
8. 统计 loss 和 accuracy
在代码中,对应的核心逻辑是:
清空梯度 -> 前向传播 -> 计算损失 -> 反向传播 -> 更新参数
这就是神经网络训练中最核心的一套流程。
对应到项目代码,就是下面这几行:
optimizer.zero_grad()
logits = model(images)
loss = criterion(logits, labels)
loss.backward()
optimizer.step()
逐行理解:
| 代码 | 作用 |
|---|---|
optimizer.zero_grad() |
清空上一轮梯度 |
logits = model(images) |
前向传播,得到预测分数 |
loss = criterion(logits, labels) |
计算预测和真实标签之间的损失 |
loss.backward() |
反向传播,计算梯度 |
optimizer.step() |
优化器更新模型参数 |
十二、训练模式和评估模式的区别
训练时使用:
model.train()
评估时使用:
model.eval()
原因是有些层在训练和测试时行为不同,例如:
| 层 | 训练时 | 评估时 |
|---|---|---|
| Dropout | 随机丢弃部分输出 | 不再随机丢弃 |
| BatchNorm | 使用当前 batch 统计信息 | 使用训练得到的统计信息 |
所以训练和评估时要切换模式。
另外,评估时不需要反向传播,因为我们只是测试模型效果,不需要更新参数。
十三、为什么要画训练曲线
训练曲线可以帮助我们判断模型是否真的在学习。
一般会观察:
train_loss
test_loss
train_acc
test_acc
正常情况下:
loss 会逐渐下降
accuracy 会逐渐上升
如果训练准确率很高,但测试准确率很低,说明模型可能过拟合。
十四、混淆矩阵有什么用
准确率只能告诉我们模型整体对了多少,但不能告诉我们具体错在哪里。
混淆矩阵可以显示模型把哪些类别认错了。
例如:
cat 被预测成 dog
automobile 被预测成 truck
deer 被预测成 horse
这些类别本身外观比较相近,所以容易混淆。
本项目会生成混淆矩阵:
image/confusion_matrix.png
十五、结合本项目总结
本项目实现了一个完整的 CIFAR-10 图片分类流程:
1. 使用 torchvision 自动下载 CIFAR-10 数据集
2. 使用 transforms 做图片预处理和数据增强
3. 使用 CNN 提取图片特征
4. 使用全连接层输出 10 类分类结果
5. 使用 CrossEntropyLoss 计算分类损失
6. 使用 Adam 优化器更新参数
7. 使用 GPU 加速训练
8. 输出训练曲线、混淆矩阵和预测结果
项目中的 CNN 网络虽然不算复杂,但已经包含了深度学习图像分类任务中最核心的部分:
卷积特征提取
非线性激活
池化降采样
Dropout 防止过拟合
全连接分类
前向传播
反向传播
参数更新
通过这个项目,可以比较完整地理解一个神经网络是如何从图片输入开始,经过层层计算,最终输出分类结果,并通过训练不断提升准确率的。
十六、个人理解
一开始看神经网络代码时,最容易困惑的是每一层到底在做什么。
比如:
Conv2d 后面接 BatchNorm2d、ReLU、MaxPool2d、Dropout
可以这样记:
Conv2d:提取特征
BatchNorm2d:稳定训练
ReLU:增加非线性
MaxPool2d:缩小尺寸
Dropout:减少过拟合
Linear:根据特征分类
神经网络训练也可以用一句话概括:
前向传播负责预测,损失函数衡量错误,反向传播计算梯度,优化器更新参数。
结合 CIFAR-10 项目后,这些概念就不再抽象。图片从 3 × 32 × 32 输入网络,经过卷积层逐渐变成 128 × 4 × 4 的高级特征,最后通过全连接层输出 10 个类别分数。训练过程中,模型会根据预测错误不断调整参数,最终学会区分不同类别的图片。
更多推荐


所有评论(0)