0%

手写数字识别

中山大学计算机学院

人工智能

本科生实验报告

(2022学年春季学期)

课程名称:Artificial Intelligence

一、实验题目

用卷积神经网络(CNN)实现手写数字识别,数据集为MNIST,网络结构自行设计,CNN原理可看理论课件ML23-4 P30-53

二、实验内容

1.算法原理

​ 卷积网络在本质上是一种输入到输出的映射,它能够学习大量的输入与输出之间的映射关系,而不需要任何输入和输出之间的精确的数学表达式,只要用已知的模式对卷积网络加以训练,网络就具有输入输出对之间的映射能力。

​ 卷积神经网络具有表征学习能力,能够按其阶层结构对输入信息进行平移不变分类,可以进行监督学习和非监督学习,其隐含层内的卷积核参数共享和层间连接的稀疏性使得卷积神经网络能够以较小的计算量对格点化特征。

2.关键代码展示

其实这个实验我在上个学期自己有看一本书然后写过一次,所以直接在我之前写的代码中加了一些修改以符合我们这次实验的要求。

1
2
3
4
5
6
7
8
9
10
data_train = datasets.MNIST(root="./data",
transform=transform,
train=True,
download=True,
)

data_test = datasets.MNIST(root="./data",
transform=transform,
train=False,
)

上面是数据集(训练集和测试集)的获取代码,我们通过torchvision里的datasets类直接把数据集下载到我们的文件夹中。train为 True 代表载入训练部分,train为 False 代表载入测试集部分。

1
2
3
transform = transforms.Compose([transforms.ToTensor(),
transforms.Lambda(lambda x: x.repeat(3, 1, 1)),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])])

上面是对数据进行变换操作的代码,因为原本的数据是不能被直接处理的,我们通过transforms.Compose对数据变化操作进行组合,我们先将数据转换成张量,再对数据进行维度变化,最后进行正则化。

1
2
3
4
5
6
7
8
9
data_loader_train = torch.utils.data.DataLoader(dataset=data_train,
batch_size=64,
shuffle=True,
)

data_loader_test = torch.utils.data.DataLoader(dataset=data_test,
batch_size=4,
shuffle=True,
)

上面是数据装载部分,我们在装载过程中进行图片打包处理后才发给我们的模型进行训练,我们通过DataLoader类进行实现,batch_size确认每个包的大小,并通过shuffle来打乱图片顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(stride=2, kernel_size=2))

self.dense = torch.nn.Sequential(
torch.nn.Linear(14 * 14 * 128, 1024),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(1024, 10))

def forward(self, x):
x = self.conv1(x)
x = x.view(-1, 14 * 14 * 128)
x = self.dense(x)
return x

上面的代码是CNN网络模型搭建部分,我们首先用 conv1 进行卷积处理,实际上的实现我们先通过①一个Conv2d进行卷积、②通过ReLU达到非线性、③再通过Conv2d进行卷积、④再通过ReLU达到非线性、⑤2*2最大池化。

随后我们为了之后投入全连接层利用 view 实现参数扁平化。

最后我们再使用 dense 通入全连接层,全连接层实际上有两个全连接层组成,我们在第一个全连接层后还添加了ReLU,并且使用了Dropout的方法,以0.5概率将部分参数归0,从而防止过拟合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
model = Model()
cost = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())

n_epochs = 4

train_losses = []
train_accuracies = []
test_accuracies = []

for epoch in range(n_epochs):
running_loss = 0.0
running_correct = 0
print("Epoch {}/{}".format(epoch, n_epochs))
print("-" * 10)

for data in data_loader_train:
X_train, y_train = data
X_train, y_train = Variable(X_train), Variable(y_train)
outputs = model(X_train)
_, pred = torch.max(outputs.data, 1)
optimizer.zero_grad()
loss = cost(outputs, y_train)

loss.backward()
optimizer.step()
running_loss += loss.data
running_correct += torch.sum(pred == y_train.data)

testing_correct = 0
for data in data_loader_test:
X_test, y_test = data
X_test, y_test = Variable(X_test), Variable(y_test)
outputs = model(X_test)
_, pred = torch.max(outputs.data, 1)
testing_correct += torch.sum(pred == y_test.data)
print("Loss is:{:.4f},Train Accuracy is:{:.4f}%,Test Accuracy is:{:.4f}".format
(running_loss / len(data_train), 100 * running_correct / len(data_train),
100 * testing_correct / len(data_test)))

train_losses.append(running_loss / len(data_train))
train_accuracies.append((running_correct / len(data_train)).item())
test_accuracy = (testing_correct / len(data_test)).item()
test_accuracies.append(test_accuracy)

上面的代码是我们的训练和测试部分。

model使用我们刚刚设计的 Model 类,cost采用交叉熵损失,优化采用Adam自适应优化。

训练部分:

我们定义训练4轮,在每轮中我们将每个训练集数据包中的数据拿出来并且转换为Variable类型以便自动求导,随后我们将X_train放到网络中得到它的预测输出,之后我们通过 _, pred = torch.max(outputs.data, 1) 得到网络预测数字,将梯度重新置0后我们得到交叉熵损失,用交叉熵损失我们计算梯度并对参数进行更新。随后更新本轮损失和正确数。

测试部分:

和训练部分类似,不过不需要进行梯度更新。

训练部分和测试部分后,我们输出本轮的损失、正确率有关信息,并且把这些信息传入设计的几个输出列表中,便于后续图像绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 画出损失变化曲线
plt.figure()
plt.plot(range(1, n_epochs+1), train_losses)
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.show()

# 画出训练集准确率和测试集准确率变化曲线
plt.figure()
plt.plot(range(1, n_epochs+1), train_accuracies, label='Train')
plt.plot(range(1, n_epochs+1), test_accuracies, label='Test')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.title('Training and Testing Accuracies')
plt.legend()
plt.show()

上面的代码用于图像绘制,第一张图用于绘制Loss曲线,第二张图用于绘制Acc变化曲线,只需要利用我们刚刚每轮更新的列表即可。

三、实验结果及分析

1.实验结果展示示例

(1) 测试集准确率(Acc)90%以上

1
2
3
4
5
6
7
8
9
10
11
12
Epoch 0/4
----------
Loss is:0.0020,Train Accuracy is:96.2233%,Test Accuracy is:98.2500
Epoch 1/4
----------
Loss is:0.0007,Train Accuracy is:98.6600%,Test Accuracy is:98.6700
Epoch 2/4
----------
Loss is:0.0005,Train Accuracy is:99.0833%,Test Accuracy is:98.5800
Epoch 3/4
----------
Loss is:0.0003,Train Accuracy is:99.3233%,Test Accuracy is:98.3800

可以看到训练的损失稳定的下降,训练的准确率稳定的上升,但是测试准确率先上升然后处于波动,不过主要是第一轮训练其实就达到了很好的效果的原因,而测试集又独立于训练集所以测试集才会产生波动。

(2) 画出训练过程中Loss和Acc的变化曲线图

总共4个 Epoch:

image-20230530194005901

image-20230530194028600

四、思考题

本次实验中无思考题。

五、参考资料

① week 15 CNN.pptx —— Our Class

② ML23-4.pptx —— Our Class

③ 《深度学习之PyTorch实战计算机视觉》 —— 唐进民