在当今人工智能飞速发展的时代,图像分类作为计算机视觉领域的核心任务之一,具有广泛的应用前景,如安防监控、医学影像分析、自动驾驶等。深度学习技术的崛起为图像分类带来了革命性的突破,而PyTorch作为一个强大且灵活的深度学习框架,以其动态计算图、易于调试和丰富的工具库等优点,受到了广大科研人员和开发者的青睐。本次任务就聚焦于基于PyTorch深度学习框架,在CIFAR - 10数据集上开展图像分类实践。
CIFAR - 10数据集是图像分类研究领域的经典基准数据集,它由加拿大先进研究院(CIFAR)收集整理。这个数据集包含了10个不同类别的60000张32x32彩色图像,每一个类别都有6000张图像。这10个类别分别是飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。由于其规模适中、类别丰富且图像尺寸统一,非常适合用于图像分类算法的研究和实践。通过在这个数据集上进行实验,可以快速验证不同模型的性能和效果,为进一步的研究和应用提供有力的支持。
本次实践的重点在于从简单的卷积神经网络(SimpleCNN)逐步过渡到较为复杂的残差网络(ResNet18)。SimpleCNN作为一种基础的卷积神经网络,结构相对简单,易于理解和实现,适合初学者入门学习。而ResNet18则是一种深度残差网络,它通过引入残差块有效地解决了深度神经网络训练中的梯度消失问题,能够构建更深层次的网络结构,从而在图像分类任务中取得更好的性能。通过对这两种不同类型的网络进行实践和比较,我们可以更深入地理解卷积神经网络的原理和不同网络结构的优缺点,为今后在更复杂的任务中选择合适的模型奠定基础。
实践步骤
1. 环境准备与数据加载
在进行深度学习实践之前,环境准备是至关重要的第一步。环境的稳定性和正确性直接影响到后续代码的运行和实验结果的准确性。本次实践主要基于Python语言,需要安装一系列必要的库,其中最核心的是PyTorch和torchvision。
PyTorch是一个开源的深度学习框架,它提供了丰富的张量操作和自动求导功能,使得我们可以方便地构建和训练神经网络。torchvision则是PyTorch的一个扩展库,它包含了许多常用的计算机视觉数据集、模型和图像变换工具,为我们处理图像数据提供了极大的便利。
为了安装这些库,我们可以使用Python的包管理工具pip或者conda。以pip为例,我们可以在命令行中输入以下命令进行安装:
pip install torch torchvision
安装完成后,我们就可以在Python代码中导入这些库进行后续的操作。
接下来就是加载CIFAR - 10数据集并进行预处理。数据预处理是深度学习中非常重要的一个环节,它可以提高模型的训练效率和性能。在这个任务中,我们使用torchvision.transforms模块来定义一系列的图像变换操作。具体来说,我们使用了两个主要的变换:transforms.ToTensor()和transforms.Normalize()。
transforms.ToTensor()的作用是将PIL图像或者numpy数组转换为PyTorch的张量(Tensor)。在深度学习中,模型通常需要输入张量形式的数据,因此这个变换是必不可少的。transforms.Normalize()则是对图像进行归一化处理,它将图像的每个像素值减去均值并除以标准差,使得图像数据的分布更加均匀,有助于模型的收敛。我们将均值和标准差都设置为(0.5, 0.5, 0.5),这是一种常见的归一化方式。
下面是具体的代码实现:
import torch
import torchvision
import torchvision.transforms as transforms
# 数据预处理
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 加载训练集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
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=4,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
在这段代码中,我们首先定义了一个transform对象,它是一个由多个变换组成的序列。然后使用torchvision.datasets.CIFAR10类来加载CIFAR - 10数据集,通过设置train=True和train=False来分别加载训练集和测试集。同时,我们使用torch.utils.data.DataLoader类来创建数据加载器,它可以将数据集按照指定的批次大小进行分批加载,并且支持多线程并行加载,提高了数据加载的效率。最后,我们定义了一个包含10个类别名称的元组classes,方便后续对分类结果进行解读。
2. 构建SimpleCNN模型
卷积神经网络(Convolutional Neural Network,简称CNN)是专门为处理具有网格结构数据(如图像)而设计的一种深度学习模型。它通过卷积层、池化层和全连接层等组件的组合,能够自动提取图像的特征,并进行分类预测。SimpleCNN作为一种简单的卷积神经网络,是理解CNN基本原理的良好起点。
SimpleCNN主要包含卷积层、池化层和全连接层。卷积层是CNN的核心组件之一,它通过卷积核在图像上滑动进行卷积操作,提取图像的局部特征。每个卷积核可以看作是一个小的滤波器,它能够检测图像中的特定模式,如边缘、纹理等。在SimpleCNN中,我们使用了两个卷积层:self.conv1和self.conv2。self.conv1的输入通道数为3,这是因为CIFAR - 10数据集中的图像是彩色图像,有红、绿、蓝三个通道;输出通道数为6,意味着它会学习6个不同的卷积核;卷积核的大小为5x5。self.conv2的输入通道数为6,与self.conv1的输出通道数相匹配,输出通道数为16,卷积核大小同样为5x5。
池化层的作用是对卷积层的输出进行下采样,减少数据的维度,同时保留重要的特征信息。在SimpleCNN中,我们使用了最大池化层(MaxPool2d),它会在每个2x2的区域中选择最大值作为该区域的输出。这样可以有效地降低特征图的尺寸,减少计算量,并且增强模型的鲁棒性。
全连接层则负责将卷积层和池化层提取的特征进行整合,并输出最终的分类结果。在SimpleCNN中,我们使用了三个全连接层:self.fc1、self.fc2和self.fc3。self.fc1的输入维度为16 * 5 * 5,这是经过卷积和池化操作后特征图的维度;输出维度为120。self.fc2的输入维度为120,输出维度为84。self.fc3的输入维度为84,输出维度为10,这与CIFAR - 10数据集中的类别数相匹配。
下面是SimpleCNN模型的代码实现:
import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, 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
net = SimpleCNN()
在这段代码中,我们定义了一个名为SimpleCNN的类,它继承自nn.Module。在类的构造函数__init__中,我们初始化了所有的层。在forward方法中,我们定义了模型的前向传播过程。首先,输入图像x经过第一个卷积层self.conv1,然后通过ReLU激活函数进行非线性变换,再经过最大池化层self.pool。接着,经过第二个卷积层self.conv2、ReLU激活函数和最大池化层。之后,将特征图展平为一维向量,通过三个全连接层进行处理,最终输出分类结果。最后,我们创建了一个SimpleCNN的实例net。
3. 训练SimpleCNN模型
在构建好模型之后,我们需要对其进行训练,使其能够学习到数据中的特征和模式,从而在图像分类任务中取得良好的性能。训练模型的过程主要包括定义损失函数、选择优化器和进行迭代训练。
损失函数的作用是衡量模型的预测结果与真实标签之间的差异。在分类任务中,常用的损失函数是交叉熵损失(CrossEntropyLoss)。交叉熵损失能够很好地反映模型分类的准确性,当模型的预测结果与真实标签越接近时,损失值越小。在代码中,我们使用nn.CrossEntropyLoss()来定义损失函数。
优化器的作用是根据损失函数的值来更新模型的参数,使得损失函数的值逐渐减小。在深度学习中,随机梯度下降(Stochastic Gradient Descent,简称SGD)是一种常用的优化算法。它通过计算损失函数对模型参数的梯度,并沿着梯度的反方向更新参数。在代码中,我们使用optim.SGD来定义优化器,设置学习率(lr)为0.001,动量(momentum)为0.9。学习率控制了每次参数更新的步长,动量则可以加速收敛过程,避免陷入局部最优解。
下面是训练SimpleCNN模型的代码实现:
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
for epoch in range(2): # 训练2个epoch
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 2000 == 1999: # 每2000个batch打印一次损失
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
running_loss = 0.0
print('Finished Training')
在这段代码中,我们首先定义了损失函数criterion和优化器optimizer。然后使用一个外层的for循环来控制训练的轮数(epoch),这里我们设置为2个epoch,即模型会对整个训练集进行2次遍历。在内层的for循环中,我们从数据加载器trainloader中依次取出一个批次的数据(inputs和labels)。在每次迭代之前,我们需要调用optimizer.zero_grad()来清空优化器中的梯度信息,因为PyTorch会自动累积梯度。接着,将输入数据传入模型net中,得到预测输出outputs。然后使用损失函数criterion计算预测输出与真实标签之间的损失值loss。调用loss.backward()进行反向传播,计算损失函数对模型参数的梯度。最后,调用optimizer.step()根据梯度信息更新模型的参数。
在训练过程中,我们使用running_loss来记录每2000个批次的累计损失值,并在每2000个批次结束时打印平均损失值,这样可以让我们实时了解模型的训练情况。当所有的epoch训练完成后,我们打印出“Finished Training”表示训练结束。
4. 测试SimpleCNN模型
训练好的模型需要在测试集上进行评估,以检验其在未见过的数据上的性能。在测试过程中,我们不需要进行反向传播和参数更新,因此可以使用torch.no_grad()上下文管理器来关闭梯度计算,这样可以节省内存和计算资源。
下面是测试SimpleCNN模型的代码实现:
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total} %')
在这段代码中,我们首先初始化了两个变量correct和total,分别用于记录正确预测的样本数和总的样本数。然后使用with torch.no_grad()上下文管理器来关闭梯度计算。在for循环中,我们从测试数据加载器testloader中依次取出一个批次的数据(images和labels)。将图像数据传入模型net中,得到预测输出outputs。使用torch.max()函数找出每个样本预测结果中概率最大的类别索引,即预测的类别predicted。接着,更新total和correct的值,total加上当前批次的样本数,correct加上预测正确的样本数。
最后,我们计算模型在测试集上的准确率,即正确预测的样本数除以总的样本数,并将结果乘以100转换为百分比形式,然后打印出准确率。通过这个准确率,我们可以直观地了解模型在图像分类任务中的性能。
5. 构建ResNet18模型
随着深度学习的发展,人们发现增加神经网络的深度可以提高模型的性能。然而,当网络深度增加到一定程度时,会出现梯度消失或梯度爆炸的问题,导致模型难以训练。为了解决这个问题,何恺明等人在2015年提出了残差网络(Residual Network,简称ResNet)。
ResNet的核心思想是引入残差块(Residual Block),通过跳跃连接(Skip Connection)将输入直接加到卷积层的输出上。这样,在反向传播过程中,梯度可以直接通过跳跃连接传递,避免了梯度消失的问题。ResNet18是ResNet家族中的一个经典模型,它包含18层卷积层和全连接层。
在PyTorch中,我们可以使用torchvision.models模块来快速构建ResNet18模型。代码如下:
import torchvision.models as models
resnet18 = models.resnet18(pretrained=False)
# 修改输入和输出层以适应CIFAR - 10数据集
resnet18.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
resnet18.fc = nn.Linear(512, 10)
在这段代码中,我们首先使用models.resnet18(pretrained=False)来创建一个未预训练的ResNet18模型。由于CIFAR - 10数据集中的图像尺寸为32x32,而原始的ResNet18模型是为更大尺寸的图像设计的,因此我们需要对模型的输入层和输出层进行修改。具体来说,我们将输入层的卷积核大小修改为3x3,步长为1,填充为1,以适应32x32的图像。同时,将输出层的全连接层修改为输入维度为512,输出维度为10,以匹配CIFAR - 10数据集中的类别数。
6. 训练和测试ResNet18模型
训练和测试ResNet18模型的方法与SimpleCNN类似。我们同样需要定义损失函数和优化器,然后进行迭代训练和测试。
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(resnet18.parameters(), lr=0.001, momentum=0.9)
# 训练ResNet18
for epoch in range(2):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = resnet18(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 2000 == 1999:
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
running_loss = 0.0
print('Finished Training ResNet18')
# 测试ResNet18
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = resnet18(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy of ResNet18 on the 10000 test images: {100 * correct / total} %')
7. 模型训练过程可视化与结果分析
在深度学习项目中,可视化是理解模型行为和性能的关键手段。我们提供了三个核心可视化功能,帮助您更直观地评估模型表现。
a.训练历史可视化
plot_training_history
函数绘制了训练过程中的损失和准确率曲线:
def plot_training_history(history, save_path=None):
"""绘制训练历史(损失和准确率)"""
plt.figure(figsize=(12, 4))
# 损失曲线
plt.subplot(1, 2, 1)
plt.plot(history['train_loss'], 'b-', label='训练损失')
plt.plot(history['val_loss'], 'r-', label='验证损失')
# 准确率曲线
plt.subplot(1, 2, 2)
plt.plot(history['train_acc'], 'b-', label='训练准确率')
plt.plot(history['val_acc'], 'r-', label='验证准确率')
典型输出显示:
-
训练/验证损失应呈下降趋势
-
准确率应逐步提升
-
若出现明显差距可能预示过拟合
b.预测结果展示
visualize_predictions
展示模型在测试集上的预测示例:
def visualize_predictions(model, test_loader, class_names, num_samples=10):
"""可视化模型预测结果"""
images, labels = next(iter(test_loader))
outputs = model(images)
_, predicted = torch.max(outputs, 1)
# 显示图像与预测标签
for i in range(num_samples):
plt.subplot(2, 5, i+1)
plt.imshow(images[i].permute(1,2,0))
color = "green" if predicted[i]==labels[i] else "red"
plt.title(f"真实: {class_names[labels[i]]}\n预测: {class_names[predicted[i]]}", color=color)
绿色标题表示正确预测,红色表示错误预测。
例如:
c.错误分析
plot_misclassified
专门可视化分类错误的样本:
def plot_misclassified(model, test_loader, class_names, num_samples=10):
"""收集错误分类样本"""
misclassified = []
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs, 1)
mask = predicted != labels
misclassified.extend(zip(images[mask], labels[mask], predicted[mask]))
# 显示错误样本
for i, (img, label, pred) in enumerate(misclassified[:num_samples]):
plt.subplot(2, 5, i+1)
plt.imshow(img.permute(1,2,0))
plt.title(f"真实: {class_names[label]}\n预测: {class_names[pred]}", color="red")
这些可视化工具可以帮助我们判断以下内容:
-
监控训练过程是否正常
-
发现模型容易混淆的类别
-
评估是否需要调整模型结构或超参数
d.完整训练流程实现
训练脚本train.py
提供了端到端的模型训练解决方案:
# 配置关键参数
DATA_PATH = "C:/Users/31146/Desktop/cifar-10"
BATCH_SIZE = 64
EPOCHS = 10
# 获取数据加载器
train_loader, test_loader = get_cifar10_loaders(
data_path=DATA_PATH,
batch_size=BATCH_SIZE
)
# 初始化模型
model = SimpleCNN() # 或 ResNet18()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
# 设置优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
# 训练循环
for epoch in range(EPOCHS):
model.train()
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 每个epoch后在测试集上验证
model.eval()
with torch.no_grad():
correct = 0
total = 0
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / total
print(f'Epoch {epoch+1}, 准确率: {accuracy:.2f}%')
# 保存模型
torch.save(model.state_dict(), 'model.pth')
模型 | 训练准确率 | 测试准确率 | 参数量 | 训练时间(10 epochs) |
---|---|---|---|---|
SimpleCNN | 62.3% | 58.7% | 0.38M | 12 分钟 |
ResNet18 | 92.1% | 89.4% | 11.2M | 28 分钟 |
e.性能差异分析
模型 | 训练准确率 | 测试准确率 | 参数量 | 训练时间(10 epochs) |
---|---|---|---|---|
SimpleCNN | 62.3% | 58.7% | 0.38M | 12 分钟 |
ResNet18 | 92.1% | 89.4% | 11.2M | 28 分钟 |
ResNet18的优势 :
1.残差连接有效缓解梯度消失问题,允许构建更深网络
2.通过批量归一化(BatchNorm)加速收敛
3.在小样本场景下(CIFAR-10仅6万张图片)仍能保持较高泛化能力
SimpleCNN的局限性:
1.浅层网络难以捕捉复杂特征模式
2.易出现过拟合(训练/测试差距达3.6%)
3.对图像旋转/尺度变化敏感
8.可视化与结果简要分析
以下图片均为此次实验所输出
.
├── data_loader.py # 数据加载模块
├── models/
│ ├── simple_cnn.py # SimpleCNN实现
│ └── resnet.py # ResNet18实现
├── train.py # 训练脚本
├── utils/
│ └── visualization.py # 可视化工具
├── SimpleCNN_history.json # 训练历史记录
└── visualizations/ # 可视化结果目录 (本次整体实验框架)
.
训练过程分析:
从训练过程输出看,随着训练轮次(Epoch)增加,训练损失持续下降,训练准确率稳步上升,如Epoch 1训练损失为0.9226,训练准确率47.73% ,到Epoch 10训练损失降至0.3992 ,训练准确率达85.94%,说明模型在训练集上不断学习优化,性能逐步提升。
可视化结果分析:
损失曲线:训练损失持续降低且幅度较大,验证损失先降后升。前期两者差距缩小,后期差距拉大,表明模型在训练初期能有效拟合数据,但后期出现过拟合倾向,对新数据(验证集)泛化能力变弱。
准确率曲线:训练准确率持续上升,验证准确率前期上升,后期增长缓慢甚至停滞,同样体现模型在训练集上表现越来越好,但在验证集上泛化性能受限,过拟合影响了其对未知数据的分类能力。
综合结论:整体而言,模型在训练集上表现出良好的学习能力,但存在过拟合问题,泛化到验证集时性能提升受限。后续可通过调整超参数(如学习率、正则化参数)、数据增强等方式来改善模型泛化能力,缓解过拟合现象。
结语:通过以上步骤,我们完成了从SimpleCNN到ResNet18在CIFAR - 10数据集上的图像分类实践。SimpleCNN结构简单,训练速度快,但分类准确率相对较低;ResNet18结构复杂,训练时间长,但由于其残差块的设计,能够有效解决梯度消失问题,分类准确率通常较高。在实际应用中,可以根据具体需求选择合适的模型。欢迎各位小伙伴在评论区进行讨论,谢谢大家。