2024年秋季《软件工程原理与实践》实验报告
一、实验内容
1.1 MNIST 数据集分类
1. 数据集加载和准备
-
加载 MNIST 数据集:使用
torchvision.datasets.MNIST
将数据集下载并加载到本地。训练集和测试集分别定义为train_loader
和test_loader
。 -
数据转换和标准化:在数据加载时使用
transforms.Compose
,将图像转换为张量并进行标准化,以提高模型训练效率。 -
展示样本数据:使用 Matplotlib 可视化部分数据集样本,确保数据加载成功且符合预期。
2. 模型构建
2.1. 全连接网络(FC2Layer)
- 定义网络结构:
FC2Layer
类中使用nn.Sequential
将网络层级(包括线性层和激活函数)顺序连接,实现一个简单的三层全连接网络。 - 前向传播函数:
forward
函数使用.view()
函数将输入数据重构为二维张量,适应全连接层输入的格式。 - 参数数量计算:定义
get_n_params
函数计算模型参数,作为后续模型性能比较的参考指标。
2.2. 卷积神经网络(CNN)
- 定义卷积层与全连接层:在
CNN
类中定义两个卷积层、池化层以及线性层。卷积层提取特征,池化层缩小特征图尺寸。 - 前向传播函数:在
forward
函数中按照卷积层、池化层、全连接层的顺序对输入数据进行传递和变换。 - 卷积网络的优势:卷积层能够利用局部性和图像的平移不变性特点,从而提升图像特征提取能力。
3. 训练与测试函数
-
训练函数
train
:模型进入训练模式,按批次获取数据并传入模型,通过计算损失和反向传播优化模型参数。每 100 个批次打印损失。 -
测试函数
test
:模型进入评估模式,对测试集进行前向传播计算损失,并记录正确预测的数量以计算准确率。# 训练函数 def train(model): model.train() # 主里从train_loader里,64个样本一个batch为单位提取样本进行训练 for batch_idx, (data, target) in enumerate(train_loader): # 把数据送到GPU中 data, target = data.to(device), target.to(device) optimizer.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() optimizer.step() if batch_idx % 100 == 0: print('Train: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( batch_idx * len(data), len(train_loader.dataset), 100. * batch_idx / len(train_loader), loss.item())) def test(model): model.eval() test_loss = 0 correct = 0 for data, target in test_loader: # 把数据送到GPU中 data, target = data.to(device), target.to(device) # 把数据送入模型,得到预测结果 output = model(data) # 计算本次batch的损失,并加到 test_loss 中 test_loss += F.nll_loss(output, target, reduction='sum').item() # get the index of the max log-probability,最后一层输出10个数, # 值最大的那个即对应着分类结果,然后把分类结果保存在 pred 里 pred = output.data.max(1, keepdim=True)[1] # 将 pred 与 target 相比,得到正确预测结果的数量,并加到 correct 中 # 这里需要注意一下 view_as ,意思是把 target 变成维度和 pred 一样的意思 correct += pred.eq(target.data.view_as(pred)).cpu().sum().item() test_loss /= len(test_loader.dataset) accuracy = 100. * correct / len(test_loader.dataset) print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format( test_loss, correct, len(test_loader.dataset), accuracy))
4. 训练与评估
4.1. 全连接网络训练与评估
-
定义模型与优化器:实例化
FC2Layer
并设置优化器为SGD
,使用学习率 0.01 和动量 0.5。 -
模型训练与测试:调用
train
和test
函数,打印模型参数数量和测试结果。 -
观察结果:全连接网络在准确性上略显不足为86%。
4.2. 卷积神经网络训练与评估
-
定义模型与优化器:实例化
CNN
并设置相同的优化器。 -
模型训练与测试:调用
train
和test
函数,观察卷积神经网络在测试集上的损失和准确率。 -
观察结果:卷积神经网络在相同参数数量下表现明显优于全连接网络为95%。
对比解读:通过上面的测试结果,可以发现,含有相同参数的 CNN 效果要明显优于 简单的全连接网络,是因为 CNN 能够更好的挖掘图像中的信息,主要通过两个手段:
- 卷积:Locality and stationarity in images
- 池化:Builds in some translation invariance
5. 像素打乱后的训练与测试
- 像素打乱实验:使用
torch.randperm
随机打乱图像像素顺序,并可视化展示打乱前后的图像。 - 重新定义训练和测试函数:在
train_perm
和test_perm
函数中对数据进行像素打乱,测试模型对无序数据的适应能力。
5.1. 全连接网络的打乱效果
- 训练与测试:全连接网络对打乱的图像仍能取得较好的性能,这说明全连接网络并不依赖像素之间的空间位置。
5.2. 卷积神经网络的打乱效果
- 训练与测试:卷积神经网络在打乱像素后的性能明显下降,表明卷积操作在空间结构一致的图像上更有效,打乱后的图像失去了局部性信息,导致卷积层无法有效提取特征。
解读
卷积神经网络的优势在于其空间不变性特征,可以很好地提取图像的空间信息,而全连接网络更依赖于整体特征而非空间关系。因此,卷积网络在像素无序的情况下,效果大打折扣。这次实验中也验证了CNN的局部特征提取能力,使其在结构化的图像分类任务上有显著优势,而全连接网络则因其结构的全局性特点,在不同图像结构上的表现差异较小。
1.2 CIFAR10 数据集分类
1. 数据加载与准备
- 加载 CIFAR-10 数据集:使用
torchvision.datasets.CIFAR10
下载并加载训练和测试集。 - 数据标准化:通过
transforms.Normalize
方法将图像数据标准化至[-1, 1]
范围内,方便网络训练。 - 定义 DataLoader:训练集设定为
shuffle=True
增加数据多样性,测试集设定为shuffle=False
保持数据一致性。 - 数据可视化:利用 Matplotlib 和
torchvision.utils.make_grid
展示部分样本,以检查数据加载是否正确。
9002d67c60150e1cddd36db3a9d8403.png&pos_id=img-cCci6rkV-1728441045486)
2. 构建卷积神经网络
2.1. 定义网络结构
- 网络架构:
- 第一个卷积层
conv1
:输入 3 个通道,输出 6 个特征图,卷积核大小为 5x5。 - 第一个池化层
pool
:最大池化操作,核大小为 2x2,步长为 2。 - 第二个卷积层
conv2
:输入 6 个通道,输出 16 个特征图,卷积核大小为 5x5。 - 三个全连接层
fc1
,fc2
,fc3
:用于将卷积层提取的特征转换成类别预测。
- 第一个卷积层
- 激活函数:ReLU 函数作为非线性激活函数。
- 前向传播:定义
forward
方法描述数据流向,将输入数据通过卷积、池化、全连接层的顺序依次处理。
2.2. 初始化模型与优化器
- 模型移动至 GPU:如果可用,将模型移动至 GPU 加快运算速度。
- 损失函数:交叉熵损失函数
CrossEntropyLoss
用于多分类任务。 - 优化器:Adam 优化器以学习率 0.001 优化网络参数。
3. 网络训练与测试
3.1. 训练过程
- 迭代多轮训练:在每一轮训练中,对所有训练数据分批次输入网络。
- 前向传播:计算预测值,并通过损失函数计算损失值。
- 反向传播与优化:通过反向传播计算梯度,并更新网络参数。
- 输出训练状态:每 100 个批次输出一次当前的损失,方便观察收敛情况。
3.2. 测试网络性能
- 从测试集中获取样本:取出一批测试样本进行可视化展示。
- 预测标签:通过模型预测样本标签,并与真实标签对比,显示模型的预测结果。
- 计算准确率:遍历整个测试集,计算模型的总体准确率。
4. 实验结果与解读
- 训练过程观察:随着训练轮次增加,损失值逐步下降,说明网络逐渐学习到数据特征。
- 测试样本预测:从展示结果中可以看到,模型在部分类别上预测较准确,但对部分样本预测错误。
- 准确率分析:最终模型在 CIFAR-10 测试集上的准确率为 64%。虽然这是一个基本的卷积网络模型,且准确率较基础,但已表明模型能有效识别 CIFAR-10 数据集的一部分特征。
解读:
这个卷积神经网络架构较为简单,只包含了两个卷积层和三个全连接层。对于 CIFAR-10 这种包含细节较多的图像集,它的特征提取能力较弱,导致分类准确率相对较低。如果要提升模型性能,可以考虑:
- 增加卷积层与特征图数量:更多的卷积层和特征图可以提取更高层次的特征信息。
- 使用更复杂的架构:例如引入 ResNet、VGG 等架构。
1.3 使⽤ VGG16 对 CIFAR10 分类
1. 数据加载与准备
- 数据预处理:
- 训练集转换:使用
RandomCrop
和RandomHorizontalFlip
进行数据增强,有助于提升模型的泛化能力。 - 归一化:将图像归一化至均值和标准差分别为
(0.4914, 0.4822, 0.4465)
和(0.2023, 0.1994, 0.2010)
,以加速模型训练。
- 训练集转换:使用
- 数据加载:通过
DataLoader
实例化训练集和测试集。训练集batch_size=128
并设置shuffle=True
;测试集同样使用批量大小 128,shuffle=False
保持数据顺序。 - 类别定义:CIFAR-10 数据集包含 10 个类别(如飞机、汽车、鸟等),方便后续测试和分析。
2. 构建 VGG16 网络
2.1. 定义 VGG 网络结构
- VGG16 模型:
- VGG16 使用多层卷积层提取图像特征,每次卷积后进行
Max Pooling
操作,缩小特征图尺寸。 - 配置列表
cfg
定义了模型的层级和过滤器数量,例如:第一个64
表示使用 64 个卷积核进行卷积,'M'
表示池化层。 _make_layers
方法根据cfg
配置自动生成模型结构,并添加了 Batch Normalization 层,改善训练稳定性。
- VGG16 使用多层卷积层提取图像特征,每次卷积后进行
- 全连接层:包含一个分类器,将卷积层输出的数据展平为二维,通过全连接层进行分类。
2.2. 初始化模型与优化器
- 损失函数:使用
CrossEntropyLoss
处理多分类任务。 - 优化器:使用
Adam
优化器,以学习率 0.001 更新网络权重。
3. 网络训练
-
训练过程:模型训练 10 轮,每轮对所有训练数据进行批次训练。
-
前向传播、反向传播与参数更新:计算损失,反向传播梯度,更新模型参数。
-
输出训练信息:每 100 个批次输出当前损失,跟踪模型的收敛情况。
4. 模型测试与性能评估
- 准确率计算:遍历测试集,计算网络在 10,000 张测试图像上的准确率。
- 结果展示:训练完成后,模型在测试集上的准确率为 84.06%,显著优于之前使用简单卷积神经网络得到的 64%。
解读
VGG16 网络通过多层卷积层与池化层的堆叠,形成了深层次网络结构,极大地增强了特征提取能力,从而使模型能在 CIFAR-10 这样的复杂数据集上获得较高的准确率。具体优势如下:
- 深度结构:VGG16 通过增加网络深度,捕捉更多的图像细节特征。
- 数据增强:数据增强技术在小型数据集上显著提高模型的泛化能力。
- 特征提取能力:逐层的卷积和池化操作有效提取不同尺度的特征信息,增强了对图像局部细节的捕获能力。
二、问题总结与体会
2.1 实验中遇到的问题及解决方法
**1. 问题:**1.2 CIFAR10 数据集分类实.验
**解决:**在 PyTorch 1.5及以上版本中,DataLoaderter已被弃用。应该使用DataLoader的迭代器方法来代替,即不再使用.next()方法。所以,你可以尝试使用以下代码:
dataiter =iter(test loader)
images,labels =next(dataiter)
因此将iter(trainloader).next
改为next(iter(trainoader))
即可成功运行
**2. 问题:**1.3 使⽤ VGG16 对 CIFAR10 分类,代码中的两矩阵不符合矩阵相乘的要求
**解决:**修改后一个矩阵为512*10
2.2 问题总结与解答
1. dataloader ⾥⾯ shuffle 取不同值有什么区别?
在 DataLoader
中,shuffle
参数控制是否在每个 epoch 之前将数据集打乱。代码中,我们对训练数据集设置 shuffle=True
,而测试数据集使用 shuffle=False
。
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)
- 训练集:
shuffle=True
在每个 epoch 开始时随机打乱数据。这样,模型在训练时不会学到数据的固定顺序,有助于提升模型的泛化能力,尤其是在小数据集上。 - 测试集:
shuffle=False
保持测试数据的顺序一致,这样可以确保每次评估的结果可重复。
2. transform ⾥,取了不同值,这个有什么区别?
transforms
用于数据预处理与增强操作。在 CIFAR-10 实验中,我们为训练集和测试集分别设置了不同的 transform:
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
- 训练集的 transform:使用了
RandomCrop
和RandomHorizontalFlip
进行数据增强,增加了数据的多样性,帮助模型在训练过程中学习到更多样的特征,提高其在真实场景中的表现。 - 测试集的 transform:仅进行了标准化操作,保持图像分布一致,以确保评估的稳定性。
不同的 transforms 组合会影响数据分布,从而影响模型的训练与测试表现。
3. epoch 和 batch 的区别?
- Epoch 表示模型遍历整个数据集一次。代码中通常使用
for epoch in range(10)
来进行多轮训练,每轮训练都包含多个 batch。 - Batch 表示在每个迭代中使用的数据子集。例如,在代码中,
batch_size=64
表示每个 batch 包含 64 个样本,训练集中包含多个 batch。
在模型训练代码中,这样的结构示例:
for epoch in range(10):
for i, (inputs, labels) in enumerate(trainloader):
# 进行训练
此结构表示在每个 epoch 中,模型会逐个 batch 获取数据进行训练,从而实现批量更新模型参数。
在大数据集上,分批训练(mini-batch training)是一种常用的策略。这样不仅可以有效利用内存资源,还可以使模型更快地收敛。模型参数的更新是在每个 batch 后进行的,而整个 epoch 表示一次完整的数据遍历。
4.1x1的卷积和 FC 有什么区别?主要起什么作⽤?
- 1x1 卷积:1x1 卷积只作用于通道维度,不会改变特征图的空间尺寸。它能在不改变空间信息的情况下对通道进行加权,常用于:
- 降低通道数,以减少计算量。
- 改变特征维度,为后续网络层的输入提供更合适的通道数。
- 全连接层 (FC):全连接层会将特征展平成一维,并作用于整个特征空间。常用于分类器部分,将卷积层提取的特征转换为类别预测。
1x1 卷积层具有局部性和位置敏感性,而全连接层更适合对全局特征进行分类。
5. residual leanring 为什么能够提升准确率?
残差学习(Residual Learning)通过引入“残差连接”(即跳跃连接),将前一层的输出直接添加到后续层的输入。这样做有以下优点:
- 避免梯度消失:残差连接提供了较短的梯度传播路径,使得梯度可以直接传回前几层,有助于解决深层网络中的梯度消失问题。
- 加速训练:通过残差连接,网络可以更快地收敛,同时提升准确率。
- 增强特征组合:跳跃连接能够帮助模型学习更丰富的特征组合,从而提升模型的表达能力和泛化性能。
6. 代码练习⼆⾥,⽹络和1989年 Lecun 提出的 LeNet 有什么区别?
LeNet 是一个经典的 CNN 模型,适用于小型图像数据集。相比之下,代码练习二中的网络做了扩展:
- 更多卷积层:代码练习二的网络使用了更多卷积层,能够捕捉更复杂的特征,这在处理 CIFAR-10 这种更复杂的数据集时十分重要。
- 激活函数不同:LeNet 使用 Sigmoid 或 Tanh 函数,而现代网络(包括代码中实现的网络)使用 ReLU 激活函数,能有效解决梯度消失问题。
- 池化方式:LeNet 使用平均池化,而练习二中的网络使用了最大池化(
MaxPool2d
),更有助于保留特征中的显著信息。
7. 代码练习⼆⾥,卷积以后feature map 尺⼨会变⼩,如何应⽤ Residual Learning?
在代码练习二中,卷积操作会使特征图的尺寸缩小,如果要结合残差学习(Residual Learning),需要确保残差连接的输入和输出的维度一致,否则两者无法相加。
1. 使用 1x1 卷积调整尺寸
当卷积层的输出特征图尺寸小于输入特征图时,可以使用 1x1 卷积 来调整残差连接的通道数或空间尺寸,使得两者匹配。1x1 卷积层不会改变空间信息,但可以改变通道数量以便在特征图维度上进行相加。
例如,如果卷积层输出尺寸为 16x16x64,但输入的尺寸为 32x32x64,以下代码可以调整输入以匹配输出的特征图尺寸:
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
# 用 1x1 卷积匹配尺寸和通道数
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# 将调整后的输入与输出相加
out += self.shortcut(x)
out = F.relu(out)
return out
解释:如果步长(stride
)不是 1,或者输入和输出通道数不同,使用 1x1 卷积调整输入特征图的尺寸与通道数,以匹配卷积输出,确保维度一致后再相加。
2. 增加步长实现下采样
在 ResNet 中,某些残差块使用卷积层的步长为 2 实现空间下采样,同时在残差连接上使用 1x1 卷积进行下采样:
# 使用步长为2的卷积进行下采样
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=2, padding=1)
在这种方式下,主路径中的卷积层会将特征图尺寸缩小一半(例如,从 32x32 到 16x16),而残差连接中的 1x1 卷积也会相应地调整输入特征图尺寸,使它们可以进行相加。
3. 直接使用零填充
如果卷积层缩小了特征图的尺寸,可以在残差连接上直接填充零,匹配两者的尺寸。这种方法简单有效,但较少使用,因为直接通过卷积调整维度更加灵活且高效。
例如,可以将输入 x
的尺寸填充至 out
的尺寸,然后相加:
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
if self.shortcut:
x = self.shortcut(x)
else:
# 直接填充零,使输入尺寸与输出尺寸相同
x = F.pad(x, (0, 0, 0, 0, 0, out.size(1) - x.size(1)))
out += x
out = F.relu(out)
return out
解释:这种方法填充后的残差连接直接与输出特征图相加。
4. 使用恒等映射进行尺寸不变的残差连接
在部分残差块中,可以保持输入和输出特征图的尺寸相同,此时直接通过恒等映射进行残差连接。典型的实现如下:
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# 恒等映射直接相加
out += x
out = F.relu(out)
return out
解释:这里 x
与 out
的尺寸相同,直接相加。这种残差块的优势在于其计算效率和灵活性,可以有效提升模型深度。
8. 有什么⽅法可以进⼀步提升准确率?
实验中,模型在 CIFAR-10 上取得了良好的表现,但可以进一步提升:
- 增加网络深度:例如使用 ResNet 或更深的 VGG 模型,能够学习到更丰富的特征。
- 数据增强:可以增加更多的增强操作,例如随机旋转、色彩抖动等。
- 使用预训练模型:将预训练的 VGG 或 ResNet 模型迁移到 CIFAR-10 任务中,这样可以利用在大数据集上学习到的特征。
- 调参:尝试不同的学习率、优化器或批量大小。Adam、RMSProp 等优化器在许多任务中表现优异,可以加快收敛速度。
通过这些方法可以有效提高模型的准确性,并增强其在不同数据集上的泛化能力。
2.3 个人体会感悟
在完成这些实验后,我深刻感受到数据预处理和数据增强对模型训练的重要性。通过标准化、随机裁剪和翻转等技术,不仅有效提升了模型的泛化能力,还在小数据集上减少了过拟合的可能性,尤其是在 CIFAR-10 数据集上,数据增强对模型表现的提升尤为显著。这些预处理操作让我意识到,在训练深度学习模型时,数据准备是至关重要的一环。
此外,通过构建和对比不同深度的网络,我进一步理解了网络结构的复杂度如何影响模型性能。相比于简单的 LeNet,VGG 网络这种更深层的结构能更充分地提取特征,在更复杂的分类任务上表现得更优越。而在了解残差学习后,我认识到残差连接的引入,不仅解决了深层网络的梯度消失问题,还提升了网络的可训练性,使得深层网络变得更加稳定。
通过这次实验,我也认识到 1x1 卷积在网络结构中的灵活应用,以及优化器和超参数对模型训练结果的影响。像 Adam 优化器在模型的收敛速度上表现得非常好,这让我深刻理解到,除了网络架构,选择合适的优化器和参数调优同样能带来显著的效果。总的来说,这次实验让我更全面地了解了卷积神经网络的各个组成部分,也让我对未来深度学习方向的学习充满了信心和期待。