| 姓名和学号 | zgx,****** |
|---|---|
| 本实验属于哪门课程 | 中国海洋大学25秋《软件工程原理与实践》 |
| 实验名称 | 实验3:卷积神经网络 |
一、实验内容
本次实验中的三个实验均来自以下链接:
实验三:https://oucaigroup.feishu.cn/wiki/ZZHiwlpZJiLuNCkpL5ScHeCTnqg
实验四:https://oucaigroup.feishu.cn/wiki/TWFdw1ecwiBILikSJrecvMX4nOf
实验五:https://oucaigroup.feishu.cn/wiki/UJ1xwmjVziixTckP1zFcgNgcnqc
1. 实验三:MNIST数据集分类
1.1 基础准备
这部分代码主要是在为后面的神经网络训练做准备,导入所需要的库、定义模型参数统计函数、设置运行设备三部分。
1.2 加载数据(MNIST)
1.2.1 MNIST 数据集并预处理
设置图片的大小为28*28, 并设置输出类别数10(0-9), 并加载训练集和测试集。
对图片进行预处理, transforms.ToTensor()将图片转换成张量,并将像素值缩放到0~1, 通过transforms.Normalize()对像素进行标准化,并用transforms.Compose()将上述操作组合起来依次执行。
1.2.2 显示数据集中的部分图像
取出 MNIST 训练集中的前 20 张图片,按 4 行 5 列显示出来。
1.3 创建网络
1.3.1 构建神经网络
这部分代码定义了两种模型:
全连接神经网络首先将每张 28×28 的图片展开为包含 784 个像素值的一维向量,然后依次通过两个隐藏层和一个输出层,引入ReLU函数进行非线性变换,最后通过LogSoftmax输出各类别的对数概率。
卷积神经网络直接接收二维图像,通过两次卷积提取图像中的笔画、边缘等局部特征,并在每次卷积后使用 ReLU 激活和最大池化。将大小从 28×28 逐步缩小为 4×4,再将特征展开,送入两个全连接层完成分类。
这部分主要是为后续训练和比较分类做准备。
1.3.2 定义训练和测试函数
对每批数据依次执行梯度清零、前向传播、损失计算、反向传播和参数更新。损失计算采用负对数似然损失,主要是用于衡量预测结果与真实标签之间的差距。
每隔 100 个批次输出一次训练进度和当前批次的损失,这样也能方便观察结果。
最后是进行测试,选择每张图片输出值最大的类别作为预测标签,将预测标签与真实标签进行比较,累计预测正确的数量,同时累加各批次的损失。计算平均损失和分类准确率。
1.4 进行训练
1.4.1 在小型全连接网络上训练
先设置隐藏层有8个神经元,随后调用FC2Layer创建全连接模型。
开始进行训练。
其中的optim.SGD()是带动量的随机梯度下降优化器。有动量可以使参数更新时结合之前积累的更新方向,可以更好的减少来回震荡。
1.4.2 在卷积神经网络上训练
定义两个卷积层各输出 6 个特征通道,利用CNN()创建卷积神经网络,也同样为CNN设计带动量的随机梯度下降优化器。最后输出结果
通过以上对比,可以发现含有相同参数的CNN效果要明显优于简单的全连接网络。
主要是因为CNN中的“卷积”和“池化”可以更好的挖掘图像中的信息,帮助神经网络更精准的识别。
1.5 打乱像素顺序后再次在两个网络上训练与测试
如果把图像中的像素打乱顺序,“卷积”和“池化”会难以发挥作用。再次实验观察两个神经网络上的结果如何。
1.5.1 随机打乱像素后图像的形态
把像素打乱就是:把图片中的28\×28=784 个像素随机打乱,然后组成的图像。
1.5.2 重新定义训练与测试函数
与之前的测试函数没多大区别,只是对数据进行了打乱顺序操作。
1.5.3 在小型全连接网络上训练
1.5.3 在卷积神经网络上训练
通过两次实验来看,全连接网络的性能基本没有发生变化,但是卷积神经网络的性能明显的下降。这主要因为卷积神经网络的核心“卷积”和“池化”需要利用像素之间的局部关系,打乱之后这些像素关系就无法得到利用了,也大大影响了卷积神经网络的性能。
2. 实验四:CIFAR10 数据集分类
2.1 基础准备
这部分代码完成了模型训练前的数据准备工作。
先导入所需的库,并根据 GPU 是否可用选择计算设备。
随后,将图片转换成张量,把像素值调整到 −1 到 1 的范围,再分别加载 CIFAR-10 的训练集和测试集,使用 DataLoader 按批次读取数据。
训练时打乱样本顺序,测试时保持原有顺序。最后定义类别名称,方便查看预测结果。
展示CIFAR10 数据集里面的一些照片:
这里的迭代器没有 .next() 方法,需要使用 Python 内置的 next() 函数获取一批数据。
将原语句改成:images, labels = next(iter(trainloader))即可
2.2 定义网络、损失函数和优化器
这部分代码搭建了一个包含两个卷积层、两次最大池化和三个全连接层的分类网络。
卷积层负责提取图片特征,池化层负责缩小特征图、减少计算量,全连接层则综合这些特征,输出十个类别的得分。图片经过卷积和池化后,每张得到 16 张大小为 5×5 的特征图,需要先展开成 400 个特征值,再送入全连接层。
最后,代码将模型放到指定设备上,并设置交叉熵损失函数和 Adam 优化器,为后续计算损失和更新参数做好准备。
2.3 训练网络
这部分代码实现了模型的训练过程,共训练 10 轮,每轮按批次读取训练图片和真实标签。
每个批次先清空之前的梯度,再将图片输入模型,计算预测结果与真实标签之间的损失,随后通过反向传播计算梯度,由优化器更新模型参数。
反向传播负责计算梯度,而优化器负责真正调整参数,两者共同完成模型的学习。训练过程中还会定期输出当前批次的损失,方便观察训练进度和损失变化。
2.4 训练结果
从测试集中取出8张图片:
这部分也会报错,解决方法同2.1中的图片展示方法
把图片输入模型,看看CNN把这些图片识别成了什么:
可以看出有部分图片识别错了。
网络在整个数据集上的表现:
3. 实验五:VGG16对CIFAR10分类
3.1 定义dataloader
这部分代码在原有的数据加载流程中加入了数据增强,并分别设置训练集和测试集的预处理方式。训练图片经过随机裁剪和水平翻转后,同一张图片在不同轮次中可能呈现不同的位置和朝向,有助于模型适应图片的变化。
测试图片只进行张量转换和标准化,保证评估过程稳定。同时,代码调整了各通道的标准化参数,并将训练和测试的批次大小都设为 128。
也就是 transform 负责处理单张图片,而 DataLoader 负责把图片和标签组织成批次,两者共同为模型提供训练和测试数据。
3.2 VGG网络定义
模型实现代码:
这部分代码通过配置列表搭建了一个 VGG 风格的卷积神经网络,列表中的数字表示卷积层的输出通道数,M 表示最大池化。
相比之前的简单网络,它增加了卷积层和通道数,并在每个卷积层后加入批量归一化和 ReLU,以提取更丰富的特征并帮助稳定训练。
图片经过五次池化后,由 32×32 缩小到 1×1,最终得到 512 个特征,再通过全连接层输出十个类别的得分。
代码中需要修改一下语句:self.features = self._make_layers(self.cfg),否则会导致变量名不一致。
代码中需要修改一下语句:self.classifier = nn.Linear(512, 10)(将2048改为512),适配后续的代码参数,否则无法运行成功。
初始化一些参数:
使用交叉熵损失函数衡量预测结果与真实标签之间的差距,再设置学习率为 0.001 的 Adam 优化器,用于后续更新模型参数。
对比之下损失函数和优化方式是保持不变的。
3.3 网络训练
训练的代码和之前完全一样:
3.4 测试验证准确率
测试代码也与之前完全一样:
根据结果可以发现:一个简化版的VGG网络就可以将准确率从63%大幅度提升到82.79%
二、问题总结与体会
1. DataLoader 中,shuffle 取不同值有什么区别?
shuffle决定读取样本时是否打乱顺序。
shuffle=True:每轮训练重新打乱样本顺序,使不同 batch 的样本组合发生变化,减少固定排列顺序对训练的影响。shuffle=False:按数据集原有顺序读取,每轮的样本顺序基本一致。
训练集通常设为True,测试集通常设为False。训练时打乱数据有助于模型学习;测试时不更新参数,一般不需要打乱,也方便对应预测结果和原始样本。
2. transform 中,取不同值有什么区别?
Normalize(mean, std)是按通道调整像素数值,改变输入数据的中心位置和数值尺度。
其计算公式为:
x′=x−meanstd x'=\frac{x-\text{mean}}{\text{std}}x′=stdx−mean
例如,Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))中,两组数分别对应 RGB 三个通道的均值参数和标准差参数。输入已经在[0,1]时,变换后落在[-1,1]。
这不代表处理后的实际均值一定为 0、标准差一定为 1。只有使用数据本身的相应统计量,才有这样的标准化效果。
3. epoch 和 batch 的区别是什么?
- batch:一次送入模型的一批样本。
- batch size:这一批包含多少个样本。
- epoch:整个训练集被完整训练一遍。
例如,训练集有 50,000 张图片,batch_size=100,那么:
1 个 epoch=500 个 batch 1\text{ 个 epoch}=500\text{ 个 batch}1个epoch=500个batch
通常每处理一个 batch,就进行一次前向传播、损失计算、反向传播和参数更新。因此,训练 10 个 epoch,通常会更新参数 5,000 次。
4. 1×1 卷积和 FC 有什么区别?主要起什么作用?
1×1 卷积虽然只覆盖一个空间位置,但会使用该位置的所有输入通道,对这些通道进行加权组合。
例如,输入为64×32×32,使用 16 个 1×1 卷积核,步长为 1、不填充时,输出为:
64×32×32→16×32×32 64\times32\times32\rightarrow16\times32\times3264×32×32→16×32×32
空间尺寸不变,通道数由 64 变成 16。
| 对比项 | 1×1 卷积 | 展平后的全连接层 FC |
|---|---|---|
| 处理范围 | 每个空间位置上的所有通道 | 展平后的全部输入特征 |
| 空间结构 | 保留二维位置结构 | 通常输出一个特征向量 |
| 参数共享 | 不同空间位置使用同一组权重 | 不同输入位置通常使用不同权重 |
| 常见作用 | 通道融合、升降维、残差分支对齐 | 综合全部特征,输出分类得分 |
1×1 卷积主要有三个用途:
- 调整通道数,使特征符合后续网络的输入要求。
- 融合不同通道的信息,学习新的特征组合。
- 先降低通道数,再进行较大的卷积,减少后续计算量。
它本身仍是线性变换,接上 ReLU 等激活函数后,才能引入非线性。
5. Residual Learning 为什么能够提升准确率?
普通网络直接学习目标映射 H(x),残差学习则让网络学习:
F(x)=H(x)−x F(x)=H(x)-xF(x)=H(x)−x
最后通过跳跃连接得到:
y=F(x)+x y=F(x)+xy=F(x)+x
也就是保留输入特征,再学习需要补充或修正的部分。
它主要解决深层网络难以训练的问题:
- 保留原有信息。输入可以通过跳跃连接直接传到后面,不必完全依赖多层变换。
- 方便梯度传播。反向传播多了一条直接路径,有助于减轻深层网络的梯度传播困难。
- 缓解网络退化。如果新增的层暂时学不到有效改进,可以让残差接近 0,使整个模块接近恒等映射,避免新增层必须重新学会“保留输入”。
因此,残差结构使更深的网络更容易优化,从而有机会学到更有效的特征并提高准确率。
6. 代码练习二中的网络,与 LeNet 有什么区别?
代码练习二采用的是 LeNet 风格的卷积神经网络,由两层卷积、两次最大池化和三层全连接组成,用于 CIFAR-10 图像分类。
输入图片为三通道的 32×32 彩色图像,数据在网络中的变化如下,表中省略 batch 维度:
| 网络操作 | 输出形状 |
|---|---|
| 输入图片 | 3×32×32 |
| 第一层 5×5 卷积,接 ReLU | 6×28×28 |
| 第一次 2×2 最大池化 | 6×14×14 |
| 第二层 5×5 卷积,接 ReLU | 16×10×10$ |
| 第二次 2×2 最大池化 | 16×5×516×5×516×5×5 |
| 展平 | 400 |
| 第一层全连接,接 ReLU | 120 |
| 第二层全连接,接 ReLU | 84 |
| 第三层全连接 | 10 |
两者的共同点是采用局部连接和卷积权重共享来提取图像特征,再通过后续网络完成分类。它们都能直接从图像中学习特征,不需要手工设计特征。
主要区别如下:
激活函数不同。
早期网络使用 sigmoid 类的平滑饱和激活函数,本实验在两层卷积和前两层全连接之后使用 ReLU。ReLU 计算简单,正值区域不会因激活函数饱和而使梯度趋近于零,有利于训练。下采样方式不同。
本实验明确使用MaxPool2d(2, 2),每次取局部区域的最大值,使特征图的高、宽减半。1989 年网络的下采样由其局部连接和步长设计实现,不能直接理解为本代码中的独立最大池化层。具体层结构和参数配置不同。
本实验的卷积通道数为3→6→16,全连接层为400→120→84→10。它沿用了 LeNet 系列的设计思路,但不是对 1989 年网络结构的原样复现。训练配置不同。
本实验使用交叉熵损失和 Adam 优化器,学习率为0.001。最后一层直接输出 10 个类别的原始得分,由CrossEntropyLoss完成相应计算,因此不需要在网络末尾额外添加 Softmax。
7. 卷积后 feature map 尺寸变小,如何应用 Residual Learning?
关键要求是:参与相加的两个张量必须具有相同的形状,包括通道数、高和宽。
尺寸一致时,可以直接相加:
y=F(x)+x y=F(x)+xy=F(x)+x
尺寸不一致时,需要先调整捷径分支:
y=F(x)+P(x) y=F(x)+P(x)y=F(x)+P(x)
其中,P(x) 是用于匹配形状的变换。
| 情况 | 处理方法 |
|---|---|
| 高、宽不变,通道数改变 | 用 1×1 卷积调整通道数 |
| 高、宽缩小,例如减半 | 用带步长的 1×1 卷积,同时调整空间尺寸和通道数 |
因未填充卷积而缩小,如32→28 | 可修改主分支的 padding 保持尺寸,或对捷径分支进行匹配的裁剪等处理 |
例如,主分支把输入从64×32×32变成128×16×16,捷径分支可以使用nn.Conv2d(64, 128, kernel_size=1, stride=2)。
这样两条分支的输出都是128×16×16,可以逐元素相加。
8. 有什么方法可以进一步提升准确率?
可以从以下方面改进:
| 方法 | 具体做法与目的 |
|---|---|
| 数据增强 | 训练时加入适度的随机裁剪、水平翻转,减少对固定图像形式的依赖 |
| 调整学习率 | 前期使用合适的学习率,后期逐步降低,让参数调整更细 |
| 调整训练轮数 | 训练不足时增加 epoch;出现过拟合时不再单纯延长训练 |
| 改进网络结构 | 适当增加卷积通道或深度,引入残差连接,提高特征提取能力 |
| 加入 BatchNorm | 改善网络的训练稳定性 |
| 控制过拟合 | 使用权重衰减、适当的 Dropout 或早停 |
| 检查数据处理 | 确保标签正确、标准化一致,测试时调用model.eval() |
首先应同时观察训练集和验证集的损失、准确率,再决定调整方向:
- 训练集和验证集都表现不好:重点检查是否训练不足、学习率不合适,或模型能力不足。
- 训练集很好,验证集明显较差:重点考虑过拟合,增加数据增强和正则化。
- 训练过程中损失反复大幅波动:优先检查学习率和输入数据。
心得体会
通过本次实验,我学会了在 Colab 中使用 PyTorch 完成 MNIST 和 CIFAR-10 图像分类,熟悉了数据预处理、模型搭建、训练和测试的基本过程,也弄清了卷积层、池化层和全连接层各自的作用,对图片如何经过网络得到分类结果有了更具体的认识。
通过简单卷积网络和 VGG 网络的对比,我进一步理解了不同网络结构在特征提取方式上的差异。同时也认识到,图像分类效果不仅与网络结构有关,数据增强、学习率和训练轮数等设置也会影响结果,不能只靠增加网络层数来提高准确率。
这次实验让我逐渐能够把代码中的操作与所学原理联系起来,对卷积神经网络有了更清楚的理解,也学会了在遇到维度不匹配等问题时,沿着数据在网络中的变化过程查找原因。