实战MNIST手写数字识别

前言

读取MNIST数据集实现手写数字的识别

测试代码

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import torch
import numpy
import torchvision
from torch.autograd import Variable
import torch.utils.data.dataloader as Data
import matplotlib.pyplot as plt

train_data = torchvision.datasets.MNIST(
'./mnist',
train=True,
transform=torchvision.transforms.ToTensor(),
download=True)
test_data = torchvision.datasets.MNIST(
'./mnist', train=False, transform=torchvision.transforms.ToTensor())
print("train_data:", train_data.data.size())
print("train_labels:", train_data.targets.size())
print("test_data:", test_data.data.size())

train_loader = Data.DataLoader(dataset=train_data, batch_size=64, shuffle=True)
test_loader = Data.DataLoader(dataset=test_data, batch_size=64)

class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(1, 32, 3, 1, 1), torch.nn.ReLU(),
torch.nn.MaxPool2d(2))
self.conv2 = torch.nn.Sequential(
torch.nn.Conv2d(32, 64, 3, 1, 1), torch.nn.ReLU(),
torch.nn.MaxPool2d(2))
self.conv3 = torch.nn.Sequential(
torch.nn.Conv2d(64, 64, 3, 1, 1), torch.nn.ReLU(),
torch.nn.MaxPool2d(2))
self.dense = torch.nn.Sequential(
torch.nn.Linear(64 * 3 * 3, 128), torch.nn.ReLU(),
torch.nn.Linear(128, 10))

def forward(self, x):
conv1_out = self.conv1(x)
conv2_out = self.conv2(conv1_out)
conv3_out = self.conv3(conv2_out)
res = conv3_out.view(conv3_out.size(0), -1)
out = self.dense(res)
return out

model = Net()

optimizer = torch.optim.Adam(model.parameters())
loss_func = torch.nn.CrossEntropyLoss()

for epoch in range(1):
print('epoch {}'.format(epoch + 1))
train_loss = 0.
train_acc = 0.
for batch_x, batch_y in train_loader:
batch_x, batch_y = Variable(batch_x), Variable(batch_y)
out = model(batch_x)
loss = loss_func(out, batch_y)
train_loss += loss.item()
pred = torch.max(out, 1)[1]
train_correct = (pred == batch_y).sum()
train_acc += train_correct.item()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('Train Loss: {:.6f}, Acc: {:.6f}'.format(
train_loss / (len(train_data)), train_acc / (len(train_data))))
model.eval()
eval_loss = 0.
eval_acc = 0.
for batch_x, batch_y in test_loader:
with torch.no_grad():
batch_x = Variable(batch_x)
with torch.no_grad():
batch_y = Variable(batch_y)
out = model(batch_x)
loss = loss_func(out, batch_y)
eval_loss += loss.item()
pred = torch.max(out, 1)[1]
num_correct = (pred == batch_y).sum()
eval_acc += num_correct.item()
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(test_data)),
eval_acc / (len(test_data))))

data_loader_test = torch.utils.data.DataLoader(
dataset=test_data, batch_size=4, shuffle=True)
x_test, y_test = next(iter(data_loader_test))
inputs = Variable(x_test)
pred = model(inputs)
_, pred = torch.max(pred, 1)

print('Predict Label is:', [i for i in pred.data])
print('Real Label is:', [i for i in y_test])

img = torchvision.utils.make_grid(x_test)
img = img.numpy().transpose(1, 2, 0)

plt.imshow(img)
plt.show()

测试结果

1
2
3
4
5
6
7
8
train_data: torch.Size([60000, 28, 28])
train_labels: torch.Size([60000])
test_data: torch.Size([10000, 28, 28])
epoch 1
Train Loss: 0.003245, Acc: 0.934217
Test Loss: 0.001147, Acc: 0.973900
Predict Label is: [tensor(0), tensor(2), tensor(9), tensor(4)]
Real Label is: [tensor(0), tensor(2), tensor(9), tensor(4)]

测试图

代码分析

实质上网络并不复杂(那么,为什么咕咕咕?)具体是由三个卷积层和一个全连接层组成,由于数据集被分成了一个个包,每个包的格式为[64, 1, 28, 28],64是包含了64张图,1代表通道,MNIST的图像都是单通道黑白图,28是图像的长和宽。
下面,我先介绍一下网络里的相关函数,再进行分析:

class torch.nn.Conv2d()

二维卷积函数
in_channels(int) – 输入信号的通道
out_channels(int) – 卷积产生的通道
kernel_size(int or tuple) - 卷积核的尺寸
stride(int or tuple, optional) - 卷积步长
padding(int or tuple, optional) - 输入的每一条边补充0的层数
常见的是这前五个参数,输入信号的通道和卷积产生的通道决定了要产生几个卷积核,kernel_size和stride是卷积核的参数,padding是由于在卷积后周围一圈的像素点会因为没有卷积而消失,因此需要人为的补充一圈像素,保持图像尺寸不变。

torch.nn.ReLU()

激活函数,本质是max(0, x),可以看出目的是实现完全抑制负数,成比例促进正数,是一种对神经元的模仿。

class torch.nn.MaxPool2d()

二维池化函数
kernel_size(int or tuple) - max pooling的窗口大小
stride(int or tuple, optional) - max pooling的窗口移动的步长。默认值是kernel_size
padding(int or tuple, optional) - 输入的每一条边补充0的层数
最大池化比较好理解,在池化过程中保留最大值,可以说是在放大需要特征,消除不必要特征。

网络分析

conv1,conv2,conv3都是由一个卷积函数,一个激励函数以及一个池化函数组成,数据集在进入conv1时的格式为[1, 28, 28],卷积后变为[32, 28, 28],池化后变为[32, 14, 14],
同理,在经过conv2层后格式为[64, 7, 7],在经过conv3层后变为[64, 3, 3];而dense层则是全连接层,目的是为了进行线性变换,使最后产生的值只有10个,这十个值可以说是模拟出来的概率,即针对0-9每一个数字的概率,值越大就代表是这个对应数字的可能性越大。
这点怎么看呢?
在训练过程中有一行代码是实现训练值和真实值的比较:

1
pred = torch.max(out, 1)[1]

torch.max()

第一个参数是输入Tensor,第二个参数是选择行和列,0代表比较每一列的最大值,1代表比较每一行的最大值;而max返回两个值,第一个是最大值,第二个如果选择比较行最大值,那么就返回最大值所在列,如果选择比较列最大值,那么就返回最大值所在行。后面可以指定单独返回哪个值,[0]返回最大值,[1]返回位置。
代码展示的就是比较行最大值,并返回最大值所在列,我打印一些测试值数据和经过max之后的数据出来:
测试值数据:

1
2
3
4
5
6
tensor([[ -4.3027, -0.9863, 2.6202, 1.2236, -5.9825, -2.1526, -15.2012,
12.2280, -2.6344, 3.1522],
[ 2.3235, 2.6725, 13.7567, 2.4374, -7.5948, -7.6910, -2.6478,
-6.4028, 4.3079, -6.4800],
[ -2.2610, 8.7104, -0.7275, -2.9841, 1.5691, -2.3972, -1.0289,
-0.6307, 0.1610, -2.7338]])

max之后的数据:
1
tensor([7, 2, 1])

可以看到测试值数据的最大值正好出现在7,2,1的位置。

深度学习真有趣

(那你为什么咕咕咕?)