姓名和学号? | |
---|---|
本实验属于哪门课程? | 中国海洋大学24秋《软件工程原理与实践》 |
实验名称? | 实验3:卷积神经网络 |
一、实验操作
训练和测试全连接神经网络(FNN)和卷积神经网络(CNN)模型来分类MNIST手写数字的项目。
1. 数据预处理
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=True, download=True,
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=64, shuffle=True)
这里使用了PyTorch的DataLoader
来加载MNIST数据集,并对数据进行标准化。Normalize((0.1307,), (0.3081,))
是基于MNIST数据集的均值和标准差进行的标准化操作。
2. 全连接神经网络(FNN)
class FC2Layer(nn.Module):
def __init__(self, input_size, n_hidden, output_size):
super(FC2Layer, self).__init__()
self.network = nn.Sequential(
nn.Linear(input_size, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, output_size),
nn.LogSoftmax(dim=1)
)
def forward(self, x):
x = x.view(-1, self.input_size)
return self.network(x)
这个类定义了一个简单的两层全连接神经网络。输入是展平后的28x28像素的MNIST图片,输出是10个类别(0-9的数字)。ReLU
用于激活,LogSoftmax
用于输出分类概率。
3. 卷积神经网络(CNN)
class CNN(nn.Module):
def __init__(self, input_size, n_feature, output_size):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=n_feature, kernel_size=5)
self.conv2 = nn.Conv2d(n_feature, n_feature, kernel_size=5)
self.fc1 = nn.Linear(n_feature*4*4, 50)
self.fc2 = nn.Linear(50, 10)
这个CNN网络有两个卷积层(conv1
和conv2
),池化层使用最大池化(F.max_pool2d
),最后是两层全连接层。输入图片被压缩到更小的特征图,并经过卷积处理得到更高层次的特征表示。
4. 训练和测试
训练和测试函数中分别使用了train
和test
函数:
-
train
函数负责每个epoch的模型训练过程,包括损失计算和参数更新。 -
test
函数用于评估模型在测试集上的表现,并计算平均损失和分类准确率。
5. 像素顺序打乱
perm = torch.randperm(784)
def perm_pixel(data, perm):
data_new = data.view(-1, 28*28)
data_new = data_new[:, perm]
data_new = data_new.view(-1, 1, 28, 28)
return data_new
在训练和测试的最后,增加了一个打乱像素顺序的操作,用perm_pixel
函数对输入图片的像素顺序进行随机打乱,来观察神经网络是否能够在像素顺序不同的情况下仍然学习到有效的特征。
PyTorch实现的卷积神经网络(CNN)来处理CIFAR-10数据集的图像分类任务。
1. 数据预处理
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
CIFAR-10数据集中的图像是RGB图像,每个通道的像素值在[0,1]范围内。通过Normalize
操作将其标准化为均值为0、标准差为0.5的范围,便于训练过程中的梯度计算。
2. 数据加载
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=8, shuffle=False, num_workers=2)
通过torchvision.datasets.CIFAR10
下载并加载CIFAR-10数据集,分别为训练集和测试集设置DataLoader
。在训练集上,shuffle=True
用于随机打乱样本,增加训练的多样性;测试集则不打乱样本顺序。
3. CNN模型定义
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
这是一个典型的卷积神经网络,结构包含:
- 两个卷积层(
conv1
和conv2
),每个卷积层后接ReLU激活函数和最大池化层(pool
)来降低特征图的尺寸。 - 三个全连接层,用于将卷积层提取的特征映射到分类空间,输出10个类别的概率。
4. 训练过程
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
for epoch in range(10):
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
训练过程中使用了交叉熵损失函数CrossEntropyLoss
,优化器选择了Adam
。在每个epoch中,网络会对每个batch的输入进行前向传播、计算损失、反向传播梯度,并通过优化器更新参数。
5. 测试与评估
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
在测试阶段,模型对测试集的图像进行预测,计算模型在10000张测试图像上的分类准确率。
6. 可视化与结果展示
images, labels = next(iter(testloader))
imshow(torchvision.utils.make_grid(images))
outputs = net(images.to(device))
_, predicted = torch.max(outputs, 1)
for j in range(8):
print(classes[predicted[j]])
该部分使用matplotlib
对一组测试图像及其预测标签进行可视化,方便直观观察模型的预测结果。
VGG卷积神经网络(CNN)用于CIFAR-10数据集的图像分类任务。
1. 数据预处理
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))])
这里为训练集和测试集定义了不同的预处理方法。训练集采用了数据增强(随机裁剪和随机水平翻转)来增加数据的多样性,防止模型过拟合。所有数据均经过标准化处理,使用CIFAR-10数据集的统计参数进行归一化。
2. 数据加载
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)
通过torchvision.datasets.CIFAR10
加载训练和测试数据集,DataLoader
用于按批次提取数据以提高训练和测试的效率。训练集中的shuffle=True
确保每个epoch的数据顺序是随机的,以避免模型过拟合特定的样本顺序。
3. VGG网络结构
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
self.cfg = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']
self.features = self._make_layers(self.cfg)
self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out
该网络基于VGG的设计,cfg
(配置)列表定义了网络的结构:
M
代表最大池化层,用于降低空间维度。- 数字代表每个卷积层的输出通道数。
_make_layers
函数构建了卷积层和池化层的序列,卷积层使用了3x3的核大小,并进行了批归一化(BatchNorm),使训练过程更加稳定。
最后的全连接层(classifier
)将卷积层的输出特征映射到10个类别。
4. 训练过程
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
for epoch in range(10): # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
训练过程采用交叉熵损失函数(CrossEntropyLoss
)和Adam优化器进行参数优化。每个epoch遍历训练集,计算损失并反向传播,更新模型参数。
5. 测试与评估
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %.2f %%' % (
100 * correct / total))
在测试阶段,模型对所有测试数据进行预测,统计模型的分类准确率。通过torch.max
函数从输出中找到预测的类别,与真实标签进行比较,计算正确预测的数量,最终输出模型在10000张测试图像上的准确率。
问题总结
1. DataLoader 里面 shuffle
取不同值有什么区别?
shuffle=True
:表示在每个 epoch 开始时,数据集会被随机打乱,这样每次训练时的样本顺序都不同。这种方式可以防止模型因为样本顺序导致的学习偏差,提高模型的泛化能力。shuffle=False
:表示数据集的样本顺序不会被打乱,数据以原始顺序进行训练。通常在测试集或验证集使用shuffle=False
,以确保模型的评估结果是稳定的。
使用场景:
- 在训练阶段通常使用
shuffle=True
来提升模型的泛化能力。 - 在验证和测试阶段通常使用
shuffle=False
,以便与之前的结果进行可重复的对比。
2. Transform 里取了不同值,这个有什么区别?
transform
是 PyTorch 中用来对数据进行预处理和增强的模块。不同的transform
会对输入数据进行不同的处理,影响模型输入的数据分布。
**常见 transform
**:
transforms.ToTensor()
:将 PIL 图片或 NumPy 数组转化为 PyTorch 的张量,并归一化到 [0,1] 范围。transforms.Normalize(mean, std)
:使用给定的均值和标准差对张量进行归一化(标准化),可以让数据的分布更加适合模型的学习。transforms.RandomCrop
/transforms.RandomResizedCrop
:随机裁剪图片,可以增加数据多样性,防止模型过拟合。transforms.RandomHorizontalFlip
:随机水平翻转图片,也是一种数据增强方式。
3.Epoch 和 Batch 的区别?
-
Epoch:一个 epoch 是指模型对整个训练数据集进行一次完整的训练。一个 epoch 通常包含多个 batch。
-
Batch:一个 batch 是指模型一次前向传播和反向传播所用的样本数量。将训练集分为若干个 batch 可以减少内存占用,并加快训练速度。
-
Epoch
和Batch
的区别及公式在深度学习中,
Epoch
和Batch
是用于描述模型训练过程的两个重要概念:-
Epoch
:表示所有训练数据通过神经网络一次的过程。通常需要多次epoch
来训练模型,以便它逐渐收敛。 -
Batch
:是指将训练数据划分为多个小组,每组称为一个batch
。每个batch
会进行一次前向传播和反向传播。使用batch
训练可以降低每次梯度更新的计算开销,并帮助模型更快地收敛。
在训练模型时,一个
epoch
经过的batch
数量可以用以下公式表示:batches_per_epoch = ⌊ total_samples batch_size ⌋ \text{batches\_per\_epoch} = \left\lfloor \frac{\text{total\_samples}}{\text{batch\_size}} \right\rfloor batches_per_epoch=⌊batch_sizetotal_samples⌋
-
如果总样本数为 1000,
batch size
为 100,则一个epoch
包含 10 个batch
。# 计算每个 epoch 包含的 batch 数量 total_samples = 1000 batch_size = 100 batches_per_epoch = total_samples // batch_size print(f"每个 epoch 包含的 batch 数量: {batches_per_epoch}")
-
区别总结:
- 在一个 epoch 中,模型会使用所有的 batch 进行训练。对于数据集有 1000 个样本,batch size 为 100 时,一个 epoch 包含 10 个 batch。
- Epoch 是一个全局的概念,表示训练了多少轮;Batch 是一个局部的概念,表示模型一次处理的数据量。
4. 1x1 的卷积和 FC 有什么区别?主要起什么作用?
-
1x1 卷积:
- 1x1 卷积是一种特殊的卷积操作,它的卷积核大小为 1x1。它可以看作是一个逐点卷积,即仅在每个像素点上进行线性变换。
- 主要作用是:减少通道数(降维)、增加通道数(升维),或者对每个像素点进行特征组合。
- 它不会改变 feature map 的宽度和高度,只会改变通道数量。
-
全连接层(Fully Connected, FC):
- 全连接层将所有输入特征连接到每个神经元,相当于对输入的线性变换,并输出一个向量。
- 它会打破输入特征的空间结构,将特征全部展开成一个一维向量。
区别:
-
1x1 卷积保留了输入特征的空间结构(宽度和高度),而 FC 层会将输入拉平,丢失空间信息。
-
1x1 卷积主要用于特征组合和通道变换,而 FC 层通常用于分类任务的最后几层。
1x1 卷积与全连接层 (FC) 的作用及实例
-
1x1 卷积公式:
1x1 卷积在空间维度上不做任何操作,只是对每个输入像素进行通道上的线性组合。假设输入为 ( H \times W \times C_{\text{in}} ),输出为 ( H \times W \times C_{\text{out}} ),那么 1x1 卷积的计算公式为: -
Output ( h , w , c ) = ∑ c ′ = 1 C in Input ( h , w , c ′ ) ⋅ Weight ( 1 , 1 , c ′ , c ) \text{Output}(h, w, c) = \sum_{c' = 1}^{C_{\text{in}}} \text{Input}(h, w, c') \cdot \text{Weight}(1, 1, c', c) Output(h,w,c)=c′=1∑CinInput(h,w,c′)⋅Weight(1,1,c′,c)
-
1x1 卷积实例:
import torch import torch.nn as nn # 定义输入 input_tensor = torch.randn(1, 3, 4, 4) # 形状为 (batch_size, channels, height, width) print(f"Input shape: {input_tensor.shape}") # 定义 1x1 卷积 conv1x1 = nn.Conv2d(in_channels=3, out_channels=5, kernel_size=1) output_tensor = conv1x1(input_tensor) print(f"Output shape after 1x1 convolution: {output_tensor.shape}")
- 全连接层实例:
fc = nn.Linear(3 * 4 * 4, 5) # 全连接层 input_flatten = input_tensor.view(1, -1) # 将输入拉平 output_fc = fc(input_flatten) print(f"Output shape after fully connected layer: {output_fc.shape}")
-
5. Residual Learning 为什么能够提升准确率?
-
残差学习(Residual Learning) 是由 ResNet 提出的,它通过在模型中引入残差块(residual block)来缓解深度网络训练中的梯度消失和梯度爆炸问题。
-
当网络层数加深时,梯度在反向传播时会变得非常小或非常大,导致网络难以训练。残差学习通过引入跳跃连接(skip connection),让输入直接与输出相加,从而形成残差映射(residual mapping),使得模型能够学习残差(即预测值与输入的差值)。
-
这种结构可以更容易地进行梯度传播,并且在理论上能够让模型更好地拟合目标函数,从而提升模型的准确率。
残差学习公式及实例
- 残差学习公式:
在残差学习中,假设输入为 ( x ),学习的目标是残差 ( F(x) ),则残差块的输出可以表示为:
y = F ( x ) + x y = F(x) + x y=F(x)+x
其中,( F(x) ) 通常表示一个卷积层或多个卷积层的组合。通过引入残差连接,网络更容易学习到恒等映射,解决了深层神经网络中的梯度消失问题。
- 残差学习代码实例:
import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_channels): super(ResidualBlock, self).__init__() self.conv1 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(in_channels) self.relu = nn.ReLU() self.conv2 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(in_channels) def forward(self, x): residual = x # 保存输入 out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) out += residual # 加上残差 out = self.relu(out) return out # 测试残差块 input_tensor = torch.randn(1, 3, 32, 32) # 假设输入为 (batch_size, channels, height, width) res_block = ResidualBlock(in_channels=3) output_tensor = res_block(input_tensor) print(f"Residual Block Output shape: {output_tensor.shape}")
说明
- 残差连接 (Skip connection) 的主要作用是将输入直接与输出相加,缓解梯度消失问题,使得网络在更深的层数上也能保持良好的性能。
- 该代码实例中定义了一个简单的残差块
ResidualBlock
,通过两个卷积层来构建 ( F(x) ),并且通过残差连接将输入 ( x ) 和 ( F(x) ) 相加,最终输出 ( y = F(x) + x )。
在卷积神经网络(如 ResNet)中,残差学习极大地提高了深层网络的性能和训练效率。
- 残差学习公式:
6. 代码练习二里,网络和1989年 LeCun 提出的 LeNet 有什么区别?
LeNet-5 是 1989 年由 LeCun 提出的经典卷积神经网络架构,主要用于手写数字识别。LeNet-5 和现代的卷积神经网络在以下方面有显著区别:
-
网络层次:
- LeNet-5 的层次结构比较简单,通常包含 2 个卷积层、2 个池化层、2 个全连接层。
- 现代 CNN 结构(VGG)则包含更多的卷积层和更复杂的结构(如残差连接、瓶颈结构等)。
-
激活函数:
- LeNet-5 使用的是 Sigmoid 或 Tanh 激活函数。
- 现代 CNN 通常使用 ReLU 激活函数,因为它更容易处理梯度消失问题。
-
参数量与计算量:
- LeNet-5 的参数量和计算量较少,适用于小型数据集和低分辨率图像。
- 现代 CNN 通过增加卷积层和引入更多参数量来处理更复杂的任务。
7. 代码练习二里,卷积以后 feature map 尺寸会变小,如何应用 Residual Learning?
- 当卷积操作导致特征图(feature map)尺寸变小时,可以采用以下几种方式应用 Residual Learning:
- 使用 1x1 卷积进行维度匹配:在残差连接中使用 1x1 卷积核,将输入的特征图维度调整为与输出相同的维度,从而可以相加。
- 使用
stride
为 2 的卷积进行降采样:在残差连接的分支上采用 stride 为 2 的卷积来实现降采样,使得输入特征图和输出特征图具有相同的尺寸。 - 直接填充 0:对于一些简单情况,可以直接在较小特征图的旁边填充 0,使得输入特征图与输出特征图的大小相同。
8. 有什么方法可以进一步提升准确率?
提升模型准确率的方法有很多,具体要看模型的结构和数据特点。以下是一些常见的方法:
-
数据增强:通过增加数据的多样性来提升模型的泛化能力,如随机裁剪、旋转、翻转等。
-
模型架构改进:引入更深或更复杂的网络结构(如使用 ResNet、DenseNet、EfficientNet)。
-
使用预训练模型:使用在大规模数据集(如 ImageNet)上预训练的模型,并在自己的数据集上进行微调。
-
引入正则化技术:使用 Dropout、L2 正则化等方法防止模型过拟合。
-
使用更好的优化器:如 AdamW、Ranger、SGD with Momentum 等优化算法可以更快地收敛并提升模型性能。
提升模型准确率的实例
- 数据增强(Data Augmentation):
transform = transforms.Compose([ transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(30), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ])
- 调整学习率(Learning Rate Scheduling):
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # 每 10 个 epoch 学习率缩放 0.1 倍
- 引入正则化(Regularization):
# 在卷积层后或全连接层后添加 Dropout dropout = nn.Dropout(p=0.5) # 50% 的概率随机丢弃神经元
- 使用预训练模型(Pretrained Models):
from torchvision import models # 使用在 ImageNet 上预训练的 ResNet50 模型 model = models.resnet50(pretrained=True) # 将最后一个全连接层替换为自己数据集所需的类别数 model.fc = nn.Linear(2048, num_classes)
总结与心得体会:
通过一系列实验,我深入学习了深度学习模型在图像分类任务中的应用,特别是在处理MNIST和CIFAR-10数据集时,全连接神经网络(FNN)、卷积神经网络(CNN)以及VGG等网络结构的设计与性能对比让我更好地理解了卷积在提取图像空间特征上的优势。通过使用PyTorch进行数据加载、模型构建、训练和测试,我体会到了模型优化的细节,以及数据增强在提高模型泛化能力方面的重要性。同时,随机打乱像素顺序的实验让我认识到神经网络在学习数据特征时的依赖性,并了解了如何调整超参数和设计数据预处理策略来影响模型性能。整个过程不仅提升了我对深度学习模型工作原理的理解,还加深了我对现代深度学习框架高效训练与评估的实际操作经验。