这次来用PyTorch实现一下CNN卷积神经网络, 数据我们采用 MNIST 这个手写数字识别的数据库, 完成一个多分类任务(判断是哪个数字)

不清楚PyTorch基本用法请移步 https://anti-entrophic.github.io/posts/10010.html

最简单的CNN的结构是 “->卷积层->激活函数->池化层->线性层”,这里先简单介绍一下,后面会配合代码详细描述。

卷积层目标就是训练若干个卷积核,期望这些卷积核能够学到图像的某些特征。图像的各个通道会通过各个卷积核,得到卷积操作后的结果,然后经过ReLU激活函数。

池化层就如下图,目的是为了给图像降维,减少参数,并且期望能够捕捉一些关键特征,忽略不重要的细节

最后压缩维度后经过一个线性层,得到最终结果的概率分布,然后利用交叉熵损失函数来进行优化。

更详细的:https://zhuanlan.zhihu.com/p/630695553

我们可以很方便的通过 torchvision 这个包下载到 MNIST 这个数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
import torch.nn as nn
import torch.utils.data as Data
import torch.optim as optim
import torchvision

# 获取数据集
train_data = torchvision.datasets.MNIST(
root = './MNIST/',
train = True,
transform = torchvision.transforms.ToTensor(),
download = True
)

test_data = torchvision.datasets.MNIST(root='./MNIST/', train=False)

这样下载下来一个是训练集,一个是验证集。并且下载下来就是 torch.utils.data.Dataset 类,可以很好地适配 PyTorch 中常用的 Dataloader

1
2
3
4
5
train_loader = Data.DataLoader(
dataset = train_data,
batch_size = 50,
shuffle = True
)

Dataloader 可以很方便地完成将数据组成batch,随机取样等操作。

可以简单看一下 MNIST 这个数据集,每张图片的大小都是 28*28,训练样本有60000个,测试样本有10000个

1
2
3
4
5
print(train_data.data.shape)
print(test_data.data.shape)
# -----output-----
# torch.Size([60000, 28, 28])
# torch.Size([10000, 28, 28])

卷积层的输入是三维的,第一维是图像的通道数。

这个 nn.Conv2din_channels就是输入图像的通道数,灰度图像就是1,RGB图像就是3。output_channels就是卷积核数,也是输出图像的通道数。

如果in_channels是3的话,那对每个卷积核,都是对3个通道各自卷积,然后加起来,会得到16个卷积后的通道,最后合在一起。

kernel_size 就是卷积核的大小,stride 是卷积核移动的步长,padding 是周围补0,控制卷积后图像的大小。

ReLU() 就激活一下,不过我有点疑惑的是,卷积操作完之后,会不会某些点的intensity超过255?因为这在图像中应该是不可能的情况,但是好像直接就没有处理;小于0的话经过ReLU()可以调回来。

nn.MaxPool2d 就是一个池化层,如文章开头图片所示,取2x2格中的最大值。

最终第一层的维度变化为 (batch_size, 1, 28, 28) -> (batch_size, 16, 14, 14)

第二层的维度变化为 (batch_size, 16, 14, 14) -> (batch_size, 32, 7, 7)

线性层的维度变化为 -> (batch, 3277) -> (batch, 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__() # 在Python3中,不再需要显式地传递参数,会自动地识别当前类及其继承的父类,所以写super().__init__()更好

self.convl = nn.Sequential(
nn.Conv2d(
in_channels=1,
out_channels=16,
kernel_size=5,
stride=1,
padding=2
),
nn.ReLU(),
nn.MaxPool2d(
kernel_size=2
)
)

self.conv2 = nn.Sequential(
nn.Conv2d(
in_channels=16,
out_channels=32,
kernel_size=5,
stride=1,
padding=2
),
nn.ReLU(),
nn.MaxPool2d(
kernel_size=2
)
)

self.out = nn.Linear(32*7*7, 10)

def forward(self, x):
x = self.convl(x) # (batch_size, 1, 28, 28) -> (batch_size, 16, 14, 14)
x = self.conv2(x) # (batch_size, 16, 14, 14) -> (batch_size, 32, 7, 7)
x = x.view(x.shape[0], -1) # (batch, 32*7*7)
x = self.out(x) # (batch, 10)
return x

对于分类的概率分布,损失函数用交叉熵损失,原因我在其它文章中也提到过,详细链接:https://zhuanlan.zhihu.com/p/115277553

优化器没有用SGD而是Adam,不知道具体会有什么差异

只训练一个epoch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
model = CNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

for epoch in range(1):
for step,(batch_x,batch_y) in enumerate(train_loader):
pred_y = model(batch_x)
loss = criterion(pred_y, batch_y)

if (step + 1) % 200 == 0:
print('Step:', '%04d' % (step + 1), 'cost =', '{:.6f}'.format(loss))

optimizer.zero_grad()
loss.backward()
optimizer.step()

看一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 这步unsqueeze让test_data从(10000,28,28)->(10000,1,28,28),适配CNN的输入
test_x = torch.unsqueeze(test_data.data,dim=1).float()[:2000]
# 取出前2000个验证样本的标签
test_y = test_data.targets[:2000]

# 简单试一下前20个的输出结果
test_output = model(test_x[:20])
# 这里,test_output的维度是(20,10),torch.max会返回两个值,一个是value,一个是index,index就代表着分类为哪个数字
# torch.max(_, 1) 表示沿着test_output的第1维也就是10这一维去找最大值,找的就是每一个概率分布中的最大值
pred_y = torch.max(test_output, 1)[1].numpy()
print(pred_y, 'prediction number')
print(test_y[:20].numpy(),'real number')

# -----output-----
# [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5 9 7 3 4] prediction number
# [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5 9 7 3 4] real number

只是最简单的CNN吧,不过结果确实挺好,网络就真的学到特征了。也没去试过换一下优化器啊激活函数会有什么效果,只是学一下基础知识顺便学习PyTorch用法吧