深度学习丨基于MNIST的MLP手写体识别实践

为了巩固阶段性学习成果,尝试应用MLP进行实践。本次使用MNIST数据集,通过一个使用MLP训练一个手写体数字识别模型。

获取数据

本次仍然采用MNIST数据集中的数据,数据获取方式与softmax一节中相似。不同之处在于,这里从训练集中划分了20%作为验证集,用于在最终测试前对模型进行调整。

1
2
3
4
5
6
7
8
#下载数据
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform = transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform = transform)
#划分训练集和验证集
train_size = int(0.8*len(train_dataset))
val_size = len(train_dataset)-train_size
train_dataset,val_dataset=torch.utils.data.random_split(train_dataset,[train_size,val_size])

接着将训练集、验证集、测试集分别打乱并按64的批量存入迭代器中。

1
2
3
4
batch_size = 64
train_loader=DataLoader(train_dataset, batch_size, shuffle=True)
val_loader=DataLoader(val_dataset, batch_size, shuffle=True)
test_loader=DataLoader(test_dataset, batch_size, shuffle=True)

定义模型

由于数据集较简单,事实上很简单的模型就能取得较高的精确度。一开始仅仅使用三个全连接层并直接使用ReLU激活函数就能在验证集上达到97%的精确度。后续引入了批量归一化层,并尝试着加大深度、调整宽度,但最终也只能达到98%的精确度。一者因为精确度已经较高,二者因为MLP的局限,再做过多调整意义已经不大。以下是经过调整的模型代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28*28,1024)
self.bn1 = nn.BatchNorm1d(1024)#批量归一化层
self.fc2 = nn.Linear(1024,512)
self.bn2 = nn.BatchNorm1d(512)
self.fc3 = nn.Linear(512,256)
self.bn3 = nn.BatchNorm1d(256)
self.fc4 = nn.Linear(256,10)
self.relu=nn.ReLU()
def forward(self,x):
x = x.view(x.shape[0],-1)#将x展平
x = self.relu(self.bn1(self.fc1(x)))
x = self.relu(self.bn2(self.fc2(x)))
x = self.relu(self.bn3(self.fc3(x)))
return self.fc4(x)

model = MLP()
#定义损失函数和优化器
criterion = nn.CrossEntropyLoss()#会自动softmax并计算交叉熵
optimzer = optim.Adam(model.parameters(),lr=0.001)#Adam是比sgd更加先进的优化器,对学习率较不敏感且收敛更快

训练模型

共进行10轮训练,每轮训练完成后使用验证集对模型进行检验,看是否过拟合,从而对超参数进行调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
num_epoches = 10#训练轮数
epoch_losses = []#用于记录各轮训练损失,供可视化使用
val_losses = []#记录各轮验证损失
for epoch in range(num_epoches):
model.train()#声明训练模式
epoch_loss=0.0#该轮训练损失
for features, labels in train_loader:
optimzer.zero_grad()
outputs = model(features)
loss = criterion(outputs,labels)
loss.backward()
optimzer.step()
epoch_loss += loss.item()
avg_epoch_loss = epoch_loss/len(train_loader)#平均损失
epoch_losses.append(avg_epoch_loss)
#验证模型
model.eval()#切换评估模型
val_loss=0.0
with torch.no_grad():#不更新梯度
for features,labels in val_loader:
outputs = model(features)
val_loss += criterion(outputs,labels).item()
avg_val_loss=val_loss/len(val_loader)
val_losses.append(avg_val_loss)

将各轮损失进行可视化:

损失变化

可以看到第四轮开始就出现过拟合现象了,可以根据损失的变化调整训练轮数。由于模型拟合已较好,经过调整实际变化不大。

测试

确定模型后,就可以使用测试集对模型效果进行测试。调整后的模型在测试集上精确度达到了98.08%。

1
2
3
4
5
6
7
8
9
10
11
model.eval()
with torch.no_grad():
correct = 0
total = 0
for features, labels in test_loader:
outputs = model(features)
_,predicted = torch.max(outputs.data,1)#1表示从每行中选最大的,即预测值
total += labels.size(0)#labels.size(0)返回张量labels第一个维度的大小。通常,这用于获取张量第一个轴上的样本数或元素数。
correct += (predicted==labels).sum().item()#预测正确的数量
accuracy=correct/total#精确度
print(f"{accuracy*100:.2f}%")

我们还可以输出几个样本来观察预测是否正确。

部分预测结果