摘要
本周根据上一周学习的CNN基础知识进行了简单的代码实践,基于Pytorch框架使用卷积神经网络实现了一个简单的手写数字识别。通过这个实例,我加深了对卷积神经网络的理解,并且在这个过程中也更加熟悉了Pytorch的各个功能和Python的语法。
1.手写数字识别
CNN的实现主要包括以下步骤:
- 数据加载与预处理
- 模型搭建
- 定义损失函数、优化器
- 模型训练
- 模型测试
1.1.导入相关库
import numpy as np
import torch
import torchvision
from torch import nn # 神经网络模块
from torchvision import datasets, transforms, utils
from PIL import Image # 图像处理
import matplotlib.pyplot as plt
import torch.optim as optim # PyTorch中的优化器模块
1.2.数据加载和预处理
# 定义超参数
batch_size = 128 # 每个批次(batch)的样本数
# 对输入的数据进行标准化处理
# 神经网络需要的输入数据必须是张量格式,并且需要进行归一化处理,以提高模型的训练效果。
# 输入数据进行标准化处理可以提高模型的鲁棒性和稳定性,减少模型训练过程中的梯度爆炸和消失问题。
transform = transforms.Compose([ # 将多个数据转换操作组合在一起,形成一个序列化的数据预处理流程,大括号[]内是一个包含多个转换对象的列表
transforms.ToTensor(), # 将图像数据转换为 PyTorch 中的张量(tensor)格式,并将像素值缩放到 0-1 的范围内
transforms.Normalize(mean=[0.5], std=[0.5])]) # 将图像像素值进行标准化处理,使其均值为 0,标准差为 1
# 加载MNIST数据集
train_dataset = torchvision.datasets.MNIST(root='./data', # 指定数据集的存储根目录
train=True, # True/False: 分别加载训练集或测试集
transform=transform, # 应用之前定义的数据预处理变换
download=True) # 如果数据集不在指定的目录中,则自动下载
test_dataset = torchvision.datasets.MNIST(root='./data',
train=False,
transform=transform,
download=True)
# 创建数据加载器(用于将数据按batch size分批次放进模型进行训练)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True, # 装载过程中随机乱序
num_workers=2) # 表示2个子进程加载数据
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False,
num_workers=2)
检查数据集的数量是否正确:
print(len(train_dataset))
print(len(test_dataset))
检查批次数是否正确:
# batch=128
# train_loader=60000/128 = 469 个batch
# test_loader=10000/128=79 个batch
print(len(train_loader))
print(len(test_loader))
# 预览前五个数据
for i in range(5):
oneimg, label = train_dataset[i]
grid = utils.make_grid(oneimg) # 将单个图像数据oneimg转换成一个网格形式,以便于在一行中显示多个图像
grid = grid.numpy().transpose(1,2,0)
std = [0.5]
mean = [0.5]
grid = grid * std + mean # 应用逆标准化操作,即将图像数据从标准化后的形式转换回原始形式,以便于正确显示
# 可视化图像
plt.subplot(1, 5, i+1)
plt.imshow(grid)
plt.axis('off')
plt.show()
Pytorch中图像张量格式是 (channel, height, width),即通道维度在第一个维度。 torchvision.transforms.ToTensor() 函数会将 PIL 图像对象转换为 PyTorch 张量,并将通道维度放在第一个维度。因此,当我们使用 ToTensor() 函数加载图像数据时,得到的 PyTorch 张量的格式就是 (channel, height, width)。代码中的oneimg.numpy().transpose(1,2,0) 就是将 PyTorch 张量 oneimg 转换为 NumPy 数组,然后通过 transpose 函数将图像数组中的通道维度从第一个维度(channel-first)调整为最后一个维度(channel-last),即将 (channel, height, width) 调整为 (height, width, channel),以便于 Matplotlib 库正确处理通道信息。
1.3.模型搭建
卷积神经网络结构如下所示:
不同层参数如下所示:
卷积层: Connv2d
- in_channels ——输入数据的通道数目
- out_channels ——卷积产生的通道数目
- kernel_size ——卷积核的尺寸
- stride——步长
- padding——输入数据的边缘填充0的层数
池化层: MaxPool2d
- kernel_siez ——池化核大小
- stride——步长
- padding——输入数据的边缘填充0的层数
全连接层: Linear
- in_features:输入特征数
- out_features:输出特征数
在卷积神经网络(CNN)中,使用适当的填充(padding)可以让输出特征图的尺寸与输入尺寸保持一致。当使用填充时,通常的目标是保持输出特征图的尺寸与输入图像的尺寸相同,这样可以方便地处理不同大小的输入,并保持网络结构的一致性。
公式计算
假设输入图像的尺寸为
H
×
W
H×W
H×W,卷积核的大小为
K
K
K,步长为
S
S
S,填充为
P
P
P,则输出特征图的尺寸,可以通过以下公式计算得出:
O
=
H
+
2
P
−
K
S
\begin{align} O= \frac{H+2P-K }{S}\end{align}
O=SH+2P−K
对于一个28x28的输入图像,如果使用5x5的卷积核,并且想要保持输出特征图的尺寸与输入相同,我们可以按以下步骤计算所需的填充量:
- 确定卷积核大小: K = 5 K=5 K=5
- 确定步长: S = 1 S=1 S=1(通常情况下,如果不需要减小输出尺寸,步长设置为1)
- 确定输入尺寸: H = W = 28 H=W=28 H=W=28
- 计算填充量:为了保持输出特征图的尺寸与输入相同,我们需要在每个维度上添加 ⌊ k − 1 2 ⌋ = ⌊ 5 − 1 2 ⌋ = 2 \lfloor \frac{k-1}{2}\rfloor=\lfloor \frac{5-1}{2}\rfloor=2 ⌊2k−1⌋=⌊25−1⌋=2个像素的填充。
示例计算
输入尺寸:28x28
卷积核大小:5x5
步长:1
填充量:2
根据公式计算输出尺寸:
O
=
28
+
2
×
2
−
5
1
=
28
\begin{align} O= \frac{28+2×2-5 }{1}=28\end{align}
O=128+2×2−5=28
因此,输出特征图的尺寸也为28x28,与输入尺寸相同。
class CNN(nn.Module):
# 定义网络结构
def __init__(self):
super(CNN, self).__init__()
# 图片是灰度图片,只有一个通道
self.conv1 = nn.Conv2d(in_channels=1, out_channels=16,
kernel_size=5, stride=1, padding=2)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=16, out_channels=32,
kernel_size=5, stride=1, padding=2)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
self.fc1 = nn.Linear(in_features=7 * 7 * 32, out_features=256)
self.relu3 = nn.ReLU()
self.fc2 = nn.Linear(in_features=256, out_features=10)
# 定义前向传播过程的计算函数
def forward(self, x):
# 第一层卷积、激活函数和池化
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
# 第二层卷积、激活函数和池化
x = self.conv2(x)
x = self.relu2(x)
x = self.pool2(x)
# 将数据平展成一维
x = x.view(-1, 7 * 7 * 32)
# 第一层全连接层
x = self.fc1(x)
x = self.relu3(x)
# 第二层全连接层
x = self.fc2(x)
return x
nn.Module是PyTorch中的一个基类,用于构建各种神经网络层和模型,class CNN(nn.Module)表明定义类继承自nn.Module。
1.4.定义损失函数和优化函数
learning_rate = 0.001 # 学习率
# 定义损失函数,计算模型的输出与目标标签之间的交叉熵损失
criterion = nn.CrossEntropyLoss()
# 训练过程通常采用反向传播来更新模型参数,这里使用的是SDG(随机梯度下降)优化器
# momentum 表示动量因子,可以加速优化过程并提高模型的泛化性能。
optimizer = optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9) # net是实例化后的模型
#也可以选择Adam优化方法
# optimizer = torch.optim.Adam(net.parameters(),lr=1e-2)
1.5.模型训练
model = CNN() # 实例化CNN模型
num_epochs = 10 # 定义迭代次数
train_accs = [] # 画图所需数据
train_loss = [] # 画图所需数据
# 如果可用的话使用 GPU 进行训练,否则使用 CPU 进行训练。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 将神经网络模型 net 移动到指定的设备上。
model = model.to(device)
total_step = len(train_loader)
for epoch in range(num_epochs):
for i, (images,labels) in enumerate(train_loader):
images=images.to(device)
labels=labels.to(device)
optimizer.zero_grad() # 清空上一个batch的梯度信息
# 将输入数据 inputs 喂入神经网络模型 net 中进行前向计算,得到模型的输出结果 outputs。
outputs=model(images)
# 使用交叉熵损失函数 criterion 计算模型输出 outputs 与标签数据 labels 之间的损失值 loss。
loss=criterion(outputs,labels)
# 使用反向传播算法计算模型参数的梯度信息,并使用优化器 optimizer 对模型参数进行更新。
loss.backward()
# 更新梯度
optimizer.step()
# 以下为画图所需数据
correct = 0
total = 0
total += labels.size(0)
_, predicted = torch.max(outputs.data, 1)
correct += (predicted == labels).sum().item()
train_loss.append(loss.item())
train_accs.append(100 * correct / total)
# 输出训练结果
if (i+1) % 100 == 0:
print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch+1, num_epochs, i+1, total_step, loss.item()))
print('Finished Training')
上述代码在run的时候遇到了一个错误:
错误信息提示在尝试启动新的进程前,当前进程尚未完成其引导阶段(bootstrap phase)。这是因为在Windows系统上使用多进程(multiprocessing)时的一个常见问题。Python的multiprocessing模块在Windows上不使用fork机制(与Unix/Linux不同),因此需要确保主模块的导入行为正确处理。解决方法是在脚本的主入口点添加如下代码段:
if __name__ == '__main__':
# 在这里放置你的主程序逻辑,比如创建dataloader和训练模型
拓展DLC:Python中的「if name == “main“」: 使用详解
确保所有涉及创建子进程的操作都包裹在这个条件语句内。这可以防止在被子进程导入时尝试再次执行这些操作,从而避免错误。添加条件语句之后模型可以正常训练了:
1.6.模型保存
# 模型保存
PATH = './mnist_net.pth'
torch.save(model.state_dict(), PATH)
1.7.模型测试
# 测试CNN模型
with torch.no_grad(): # 进行评测的时候网络不更新梯度
correct = 0
total = 0
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device) # 确保predicted和labels都在同一设备上
outputs = model(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: {} %'.format(100 * correct / total))
结果如下,准确率高达98%,如果还想继续提高模型准确率,可以调整迭代次数、学习率等参数或者修改CNN网络结构实现。
1.8.模型评估
def draw_train_process(title, iters, costs, accs, label_cost, lable_acc):
plt.title(title, fontsize=24)
plt.xlabel("iter", fontsize=20)
plt.ylabel("acc(\%)", fontsize=20)
plt.plot(iters, costs, color='red', label=label_cost)
plt.plot(iters, accs, color='green', label=lable_acc)
plt.legend()
plt.grid()
plt.show()
可视化检验一个批次测试数据的准确性:
# 将 test_loader 转换为一个可迭代对象 dataiter
dataiter = iter(test_loader)
# 使用 next(dataiter) 获取 test_loader 中的下一个 batch 的图像数据和标签数据
images, labels = next(dataiter)
# print images
test_img = utils.make_grid(images)
test_img = test_img.numpy().transpose(1, 2, 0)
std = [0.5]
mean = [0.5]
test_img = test_img * std + 0.5
plt.imshow(test_img)
plt.axis('off')
plt.show()
plt.savefig('./mnist_net.png')
#print('GroundTruth: ', ' '.join('%d' % labels[j] for j in range(128)))
print('GroundTruth: ')
for j in range(128):
print('%d' % labels[j], end=' ')
if (j + 1) % 8 == 0: # 每8个标签后换行
print('\n', end='')
else:
print(' ', end='')
print() # 打印最后一个换行符,以确保输出整洁
总结
本周代码实践深入理解了卷积神经网络的搭建流程,在遇到问题时可以灵活处理并解决问题,实现了一个简单的手写数字识别。在这个过程中我也学习到了pytorch各个主要模块的功能作用,体会到了pytorch框架的强大之处。在卷积计算部分也学会了如何计算输出尺寸以及为了保持输入输出的一致性对于padding要如何处理。总的来说,实践环节对于学习的帮助是巨大的,不仅要注重理论学习更要注重实践学习。