深度学习第六篇--- Pytorch 卷积神经网络-手写数字识别

卷积神经网络是深度学习中的一个里程碑式的技术,有了这个技术,才会让计算机有能力理解图片和视频信息,才会有计算机视觉的众多应用。 本文讨论卷积神经网络模型(CNN)的Hello World。前面讨论的是一个二分类问题,本文讨论多分类问题。每张图片是一个28*28的灰度图片,所以本文的任务是给出一张图片,能识别这个图片是0-9数字中的哪一个。不过在此之前,还得学习一下卷积神经网络的基础知识。

1 卷积神经网络基础


之前我们学习的案例,对于模型的输入都是向量,但是当输入是一个图片的时候该怎么做呢?最直接的方式是把图片的像素点按照行列拉成一个长长的向量。这样就可以采用之前的方式来训练模型。但是如上图所示,一个“猫”的照片是10001000,由于是彩色的,具有R,G,B三个通道,那么输入的数据大小就是100010003,如果一个神经网络第一层有1000个神经元,那么总的参数量为1000100031000 = 3 * 109个参数,这对硬件资源提出了太高的要求。

传统的方式,进行可以先对上面的图片进行模糊化处理,这个是怎么做到的呢?


RGB可以看出3个2维矩阵,在模糊化的过程中,需要用上面的这个33矩阵,称之为核-kernel,核的大小为33,被称为kernel-size,这个核和原来矩阵作用的过程称为卷积。这个kernel就类似全连接层中的Weights一样,所以卷积核里的数值,也是通过反向传播的方法学习到的。

1.1 卷积层

卷积的运算规则:卷积核在输入矩阵中上下滑动,然后和对应的元素相乘求和。


如上图,卷积核大小是3x3的,也就是说其卷积核每次覆盖原图像的9个像素,行和列都滑动了3次,一共滑动3x3=9次,得到了一个3*3的二维矩阵。卷积核在矩阵横向或者纵向一次移动的大小叫做步长(stride),步长可以为1,2,3,4。我们直接看一下代码。

import numpy as np
import torch
from torch import nn
from torch.autograd import Variable
import torch.nn.functional as F
from PIL import Image
import matplotlib.pyplot as plt

if __name__ == "__main__":
   # 使用convert('L')读入一个灰度图片
   image = Image.open('../digital_recognition/digital_data/cat.png').convert('L')
   # 将图片转成矩阵
   image = np.array(image, dtype='float32')
   # 将图片显示出来
   plt.imshow(image.astype('uint8'), cmap='gray')
   print("finish")

我们对它进行一下卷积操作,需要用到Conv2d,参数解释详见官方文档。https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html#torch.nn.Conv2d

torch.nn.Conv2d(
   in_channels, #输入特征矩阵的深度即channel,比如输入一张RGB彩色图像,那in_channels = 3;
   out_channels, #代表卷积核的个数,使用n个卷积核输出的特征矩阵深度即channel就是n;
   kernel_size, #卷积核大小
   stride=1, # 卷积的步长
   padding=0, #卷积核对图像四周的填充边界
   dilation=1, 
   groups=1, 
   bias=True, 
   padding_mode='zeros', 
   device=None, 
   dtype=None
)
# 将图片矩阵转换成pytorch tensor,并适配卷积的输入的要求
image = torch.from_numpy(image.reshape(1,1,image.shape[0],image.shape[1]))

# 构建一个卷积,输入和输出通道都是1(因为是黑白的),卷积核大小是3,
conv = nn.Conv2d(1,1,3,bias=False)
# 构建卷积核
sobel_kernel = np.array([[-1,-1,-1],[-1,-8,-1],[-1,-1,-1]],dtype='float32')
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3))
# 给卷积的kernel赋值
conv.weight.data = torch.from_numpy(sobel_kernel)
edge = conv(Variable(image))
# 将输出转成图片的格式
edge = edge.data.squeeze().numpy()
plt.imshow(edge,cmap='gray')
print("finish")

卷积操作图片结果如下:


下面表格列举了其他卷积核的效果


卷积核里面的数字现在是固定的,只能表现图像的某些特性,那么我们可以不固定卷积核里面的数值,然后通过监督学习的方式去自动学习他,“这个可以学习的卷积操作”就是构成卷积神经网络里面最重要的概念。

1.2 池化层

池化是一个对输入进行下采样的操作,能快速减少输入大小,从而减少神经网络后面的参数量,便于训练模型。相对于卷积的下采样,有不需要参数的优点(没有卷积核参数)。一般有两种池化方式:
● 最大值池化层(max pooling)
● 平均值池化层(average pooling)

下面图是一个最大值池化层,每种颜色的矩阵取一个最大值构成右边的图。


我们从代码来看一下:

#池化核大小是2,移动步长是2
max_pool = nn.MaxPool2d(2, 2)
print('before max pool, image shape:{} * {}'.format(image.shape[2], image.shape[3]))

image = max_pool(Variable(image))

image = image.data.squeeze().numpy()
plt.imshow(image,cmap='gray')
print('before max pool, image shape:{} * {}'.format(image.shape[0], image.shape[1]))
print("max_pool finish")
image shape= (886, 878)
before max pool, image shape:886 * 878
before max pool, image shape:443 * 439

和原图对比,内容没有变化但是尺寸发生了变化。说明池化只能改变图片的大小,不会影响图片的内容。

1.3 总结

理解了卷积层和池化层,那么卷积神经网络就是卷积层+池化层作用神经网络的隐藏层反复出现的多层神经网络结构,如下图所示。


我们分析一下它的层级结构:
● 原始的输入是一张图片,可以是彩色的,也可以是灰度的或黑白的。这里假设是只有一个通道的图片,目的是识别0~9的手写体数字;
● 第一层卷积,我们使用了4个卷积核,得到了4张feature map;激活函数层没有单独画出来,这里我们紧接着卷积操作使用了Relu激活函数;
● 第二层是池化,使用了Max Pooling方式,把图片的高宽各缩小一倍,但仍然是4个feature map;
● 第三层卷积,我们使用了4x6个卷积核,其中4对应着输入通道,6对应着输出通道,从而得到了6张feature map,当然也使用了Relu激活函数;
● 第四层再次做一次池化,现在得到的图片尺寸只是原始尺寸的四分之一左右;
● 第五层把第四层的6个图片展平成一维,成为一个全连接层;
● 第六层再接一个小一些的全连接层;
● 最后接一个softmax函数,判别10个分类,这个后面实战会在介绍一下。
所以,在一个典型的卷积神经网络中,会至少包含以下几个层:
● 卷积层
● 激活函数层
● 池化层
● 全连接分类层

2 手写数字识别实战

2.1 ReLU激活函数

上面了解了卷积神经网络的基础知识,这里在介绍一个新的激活函数,ReLU函数(Rectified Linear Unit)。



ReLU和Sigmoid函数是常用的激活函数,它们在神经网络中起到非线性映射的作用,下面是它们的优缺点对比:
ReLU函数优点:
● 计算简单,只需要判断输入是否大于零。
● 解决了sigmoid函数的梯度消失问题,能更好地应对梯度下降算法。
● 可以使一部分神经元的输出为零,从而实现稀疏性,减少模型的复杂度。
ReLU函数缺点:
● ReLU函数在输入小于零时,梯度为零,导致神经元无法更新权重,称为“神经元死亡”问题。
● 对于输入小于零的情况,ReLU函数不是严格的非线性函数,可能导致模型的表达能力受限。

Sigmoid函数优点:
● Sigmoid函数的输出范围在(0,1)之间,可以将输出解释为概率。
● Sigmoid函数是严格的非线性函数,具有较强的表达能力。
● 可以将Sigmoid函数的输出直接作为分类器的输出,适用于二分类问题。
Sigmoid函数缺点:
● Sigmoid函数存在梯度饱和问题,当输入的绝对值很大时,梯度接近于零,导致梯度下降算法收敛缓慢。
● Sigmoid函数的计算量较大,使用指数运算,计算时间较长。

我们从https://playground.tensorflow.org/体验一下ReLu的性能。Sigmod需要1150个Epoch才能收敛,而ReLu只需要23个Epoch就收敛了。

2.2 交叉熵损失函数

2.3 读取数据集

def load_dataset():
   train_data = mnist.MNIST('.data', train=True, download=True)
   test_data = mnist.MNIST('.data', train=False, download=True)
   return train_data, test_data


if __name__ == '__main__':
   train_data, test_data = load_dataset()
   data, label = train_data[0]
   print("data", data)
   print("label", label)

   image = np.array(data, dtype='float32')
   print("image shape =", image.shape)
   print("image =", image)
   # 将图片显示出来
   plt.imshow(image.astype('uint8'), cmap='gray')
   print("finish")

首次执行的时候,需要下载。


输出:

label 5
image shape = (28, 28)
image = [[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   3.  18.
  18.  18. 126. 136. 175.  26. 166. 255. 247. 127.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.  30.  36.  94. 154. 170. 253.
 253. 253. 253. 253. 225. 172. 253. 242. 195.  64.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.  49. 238. 253. 253. 253. 253. 253.
 253. 253. 253. 251.  93.  82.  82.  56.  39.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.  18. 219. 253. 253. 253. 253. 253.
 198. 182. 247. 241.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.  80. 156. 107. 253. 253. 205.
  11.   0.  43. 154.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.  14.   1. 154. 253.  90.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0. 139. 253. 190.
   2.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.  11. 190. 253.
  70.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.  35. 241.
 225. 160. 108.   1.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.  81.
 240. 253. 253. 119.  25.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
  45. 186. 253. 253. 150.  27.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.  16.  93. 252. 253. 187.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0. 249. 253. 249.  64.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
  46. 130. 183. 253. 253. 207.   2.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.  39. 148.
 229. 253. 253. 253. 250. 182.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.  24. 114. 221. 253.
 253. 253. 253. 201.  78.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.  23.  66. 213. 253. 253. 253.
 253. 198.  81.   2.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.  18. 171. 219. 253. 253. 253. 253. 195.
  80.   9.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.  55. 172. 226. 253. 253. 253. 253. 244. 133.  11.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0. 136. 253. 253. 253. 212. 135. 132.  16.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]]

2.4 定义模型


class CNN(nn.Module):  # 我们建立的CNN继承nn.Module这个模块
   def __init__(self):
       super(CNN, self).__init__()
       # 建立第一个卷积(Conv2d)-> 激活函数(ReLU)->池化(MaxPooling)
       self.conv1 = nn.Sequential(
           # 第一个卷积con2d
           nn.Conv2d(  # 输入图像大小(1,28,28)
               in_channels=1,  # 输入图片的高度,因为minist数据集是灰度图像只有一个通道
               out_channels=16,  # n_filters 卷积核的高度
               kernel_size=5,  # filter size 卷积核的大小 也就是长x宽=5x5
               stride=1,  # 步长
               padding=2,  # 想要con2d输出的图片长宽不变,就进行补零操作 padding = (kernel_size-1)/2
           ),  # 输出图像大小(16,28,28)
           # 激活函数
           nn.ReLU(),
           # 池化,下采样
           nn.MaxPool2d(kernel_size=2),  # 在2x2空间下采样
           # 输出图像大小(16,14,14)
       )
       # 建立第二个卷积(Conv2d)-> 激励函数(ReLU)->池化(MaxPooling)
       self.conv2 = nn.Sequential(
           # 输入图像大小(16,14,14)
           nn.Conv2d(  # 也可以直接简化写成nn.Conv2d(16,32,5,1,2)
               in_channels=16,
               out_channels=32,
               kernel_size=5,
               stride=1,
               padding=2
           ),
           # 输出图像大小 (32,14,14)
           nn.ReLU(),
           nn.MaxPool2d(2),
           # 输出图像大小(32,7,7)
       )
       # 建立全卷积连接层
       self.out = nn.Linear(32 * 7 * 7, 10)  # 输出是10个类

   # 下面定义x的传播路线
   def forward(self, x):
       x = self.conv1(x)  # x先通过conv1
       x = self.conv2(x)  # 再通过conv2
       # 把每一个批次的每一个输入都拉成一个维度,即(batch_size,32*7*7)
       # 因为pytorch里特征的形式是[bs,channel,h,w],所以x.size(0)就是batchsize
       x = x.view(x.size(0), -1)  # view就是把x弄成batchsize行个tensor
       output = self.out(x)
       return output

2.5 训练模型

# 超参数
EPOCH = 10 
BATCH_SIZE = 50
LR = 0.001  # 学习率
DOWNLOAD_MNIST = True  # 表示还没有下载数据集,如果数据集下载好了就写False

if __name__ == '__main__':
   # 训练集
   train_data = torchvision.datasets.MNIST(
       root='./data/',  # 保存或提取的位置  会放在当前文件夹中
       train=True,  # true说明是用于训练的数据,false说明是用于测试的数据
       transform=torchvision.transforms.ToTensor(),  # 转换PIL.Image or numpy.ndarray
       download=DOWNLOAD_MNIST,  # 已经下载了就不需要下载了
   )

   # 测试集
   test_data = torchvision.datasets.MNIST(
       root='./data/',
       train=False
   )
   # 加载数据
   train_loader = Data.DataLoader(
       dataset=train_data,
       batch_size=BATCH_SIZE,
       shuffle=True  # 是否打乱数据,一般都打乱
   )

   # 图像的pixel本来是0到255之间,除以255对图像进行归一化使取值范围在(0,1)
   # torch.unsqueeze(a) 是用来对数据维度进行扩充,这样shape就从(x,28,28)->(x,1,28,28)
   test_x = torch.unsqueeze(test_data.train_data, dim=1).type(torch.FloatTensor) / 255

   test_y = test_data.test_labels
   cnn = CNN()
   print(cnn)

   # 优化器选择Adam
   optimizer = torch.optim.Adam(cnn.parameters(), lr=LR)
   # 损失函数
   loss_func = nn.CrossEntropyLoss() 
   # 开始训练
   for epoch in range(EPOCH):
       for step, (b_x, b_y) in enumerate(train_loader):  # 分配batch data
           output = cnn(b_x)  # 先将数据放到cnn中计算output
           loss = loss_func(output, b_y)  # 输出和真实标签的loss,二者位置不可颠倒
           optimizer.zero_grad()  # 清除之前学到的梯度的参数
           loss.backward()  # 反向传播,计算梯度
           optimizer.step()  # 应用梯度
           if step % 100 == 0:
               test_output = cnn(test_x)
               pred_y = torch.max(test_output, 1)[1].data.numpy()
               accuracy = float((pred_y == test_y.data.numpy()).astype(int).sum()) / float(test_y.size(0))
               print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy)

   # 保存模型
   torch.save(cnn.state_dict(), 'cnn2.pkl')

跑了7个Epoch之后,模型的准确率已经到99%。

Epoch:  0 | train loss: 2.3034 | test accuracy: 0.14
Epoch:  0 | train loss: 0.5552 | test accuracy: 0.89
Epoch:  0 | train loss: 0.1276 | test accuracy: 0.95
Epoch:  0 | train loss: 0.0556 | test accuracy: 0.96
.....
Epoch:  7 | train loss: 0.0666 | test accuracy: 0.99
Epoch:  7 | train loss: 0.0071 | test accuracy: 0.99
Epoch:  7 | train loss: 0.0054 | test accuracy: 0.99

2.6 模型验证

cnn.load_state_dict(torch.load('cnn2.pkl'))
cnn.eval()

test_output = cnn(test_x)
pred_y = torch.max(test_output, 1)[1].data.numpy()
print(pred_y, 'prediction number')
print(test_y.numpy(), 'real number')
# 检查元素是否相等并统计不相等的个数
unequal_count = sum(pred_y[i] != test_y[i] for i in range(len(pred_y)))

# 计算不相等的比例
equal_ratio = 1 - unequal_count / len(pred_y)
# 输出结果
print("总个数:{},不相等的个数{}".format(len(pred_y), unequal_count))
print("准确率:", equal_ratio.item())
[7 2 1 ... 4 5 6] prediction number
[7 2 1 ... 4 5 6] real number
总个数:10000,不相等的个数165
准确率: 0.9835000038146973
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容