深度学习已经成为了人工智能各类研究领域的主流技术,python则是其中的通用编程语言。世界上一群顶尖的深度学习研究领域科学家和工程师推出了适应于各类实际问题的网络模型和编程框架,并将其开源让更多的人受益,也有许多的公司也因此推出相关的智能产品。目前深度学习框架中最令人熟知的包括caffe、tensorflow、pytorch、keras、mxnet、cntk等,国内百度公司paddlepaddle、旷世科技的MegEngine、华为的MindSpore。各类学习资源视频也很齐全,本文将聚焦于keras和pytorch框架入门案例--基于Lenet-5卷积神经网络实现mnist手写数字识别。
一、Lenet-5网络模型
Lenet-5可以说是计算机视觉领域内卷积神经网络开山之祖,也是深度学习三巨头之一Lecun教授提出的一个经典的深度学习网络模型,如下为该模型的基本架构图:
如搭积木一样,该网络共包括这样几个层:
输入原始图 --> 第一次(卷积+池化) --> 第二次(卷积+池化) --> 第一个全连接层 --> 第二个全连接层 --> 输出层
下面对这几个层稍加详细的解释一下:
原始图:如上图所示,手写的A字母。很明显,就是一个32x32的灰度图像。
第一次卷积:卷积就是利用一个小的二维矩阵与原图进行点乘操作,通过卷积可以得到与卷积核相似度最大的特征,进而形成特征图。在第一次卷积时,采用了6个卷积核对原图进行处理,每个卷积核尺寸为5x5,卷积步长为1。处理后共得到6个特征图,不过由于没有对卷积边界进行补零处理,因此第一个卷积后特征图尺寸变成了28x28。此时将这6个特征图堆叠起来,形成一个6x28x28的特征体。
第一次池化:这是一次下采样过程,可以简单理解为对特征体抽稀处理,当然处理的时候并不是直接抽稀,而是采用了一些技巧方法。网络中选用了最大池化方法,分析特征体中每个特征图上2x2单元格内(共4个像素点)哪个像素值最大,然后保留该单元格。处理的时候也是滑动时窗,步长为1,这样从左上角到右下角对每个特征图分析一次,处理后特征图大小就变成了14x14,依然是6张特征图,因此此时特征体尺寸变成了6x14x14。
第二次卷积:经过第一次卷积和池化处理后,原始图像中的特征已经得到了抽象表示。在第二次卷积时,采用了16个卷积核,每个卷积核尺寸还是5x5,卷积步长为1。此时是在6x14x14的特征体上进行的卷积,卷积后共得到16张特征图,此时每张特征图的尺寸变成了10x10。将这16张特征图堆叠处理,形成一个16x10x10的特征体。
第二次池化:与第一次池化一样,下采样操作主要是为了减少参数量和防止过拟合。此时池化操作同样采用最大池化策略,在上述16x10x10的特征体上进行池化操作,最终每个特征图的尺寸缩小一半,变成了5x5,此时特征体尺寸为16x5x5。
上述卷积和池化可以认为是一个图像特征抽取过程,处理结束后得到一个参数量较为合适的特征体,如上第二次池化后得到了16个5x5的小尺寸图。如果将这个特征体上每个像素点当作一个参数的话,总共就有16x5x5=400个像素点。可以将16个5x5的二维矩阵全部按序号拉平降为一维,此时就形成了1个包括有400个像素点的向量。接下来网络就采用这400个像素点当作神经网络的输入节点,正式进入神经网络的结构:输入层、隐层、输出层。
第一个全连接层:其输入层为上述的400个像素点,所以输入有400个节点。网络中隐层设置为120个,其输出就为下一个全连接层的节点。
第二个全连接层:其输入为第一个全连接层的输出,隐层设置为84个节点,其输出对应最后的10个类别节点。
也就是神经网络结构部分,输入为卷积操作后的特征体拉平后的像素向量,隐层包括两个,第一个节点为120个,第二个隐层节点为84个,输出层为10个,这10个输出层对应手写数字中的(0-9)共10个类别。
在实现lenet-5网络处理中,作者在卷积部分的激活函数选择了sigmoid函数,神经网络隐层部分选择tanh激活函数,最后输出层使用了gaussian连接。上述对网络结构中使用权值和偏置参数的分析并不十分仔细,尤其是参数部分,作者在卷积和BP网络部分都加了偏置项,使得最后的参数量非常大。读者有兴趣可以仔细阅读原文。
这篇文章是1998年发表的,受限于当时的计算资源条件,作者还采用了一些其他的优化策略。当时间来到了10多年后,GPU计算的盛行,使得原有的lenet-5那些计算都不是问题了。所有如今看到的许多案例中,都已经对原有的lenet-5网络模型进行了改进和优化处理。
二、Keras框架实现lenet-5模型
下面我们以keras来实现lenet-5网络完成数字识别任务为例,说明整个过程。
第一步,导入相关库和mnist数据集。
由于目前keras已经属于tensorflow的高阶API,所以只要安装好tensorflow就可以使用keras。同时在案例中还包括numpy、matplotlib、pandas等基础库。
mnist数据集也集成到了keras的datasets模块中,导入后就可以下载到本地。
import keras
import cv2 as cv
import pandas as pd
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout
import matplotlib.pyplot as plt
第二步,取出mnist数据集中的训练集和测试集,了解数据
(x_train,train_label),(x_test,test_label) = mnist.load_data()
调用数据对象的shape属性查看维度:
print(x_train.shape,x_test.shape)
print(train_label.shape,test_label.shape)
输出结果为:
(60000, 28, 28) (10000, 28, 28)
(60000,) (10000,)
所以训练集中有6万个28x28图像矩阵,6万个标签(说明属于哪个数字);测试集为1万个28x28的图像矩阵,1万个标签。
可以采用matplotlib绘制一下训练集中的第一个28x28的图像:
def pltshow(img):
plt.imshow(img,cmap='binary')
plt.show()
pltshow(x_train[0])
第三步,对数据集进行预处理,包括reshape和normalization操作,对标签采用one-hot编码处理。
x_train = x_train.reshape(x_train.shape[0],28,28,1).astype('float32') #reshape操作
x_test = x_test.reshape(x_test.shape[0],28,28,1).astype('float32')
x_train_norm = x_train/255 #归一化处理
x_test_norm = x_test/255
train_label_onehot = np_utils.to_categorical(train_label) #对标签进行one-hot编码
test_label_onehot = np_utils.to_categorical(test_label)
我们可以查看一下编码后的标签形式:
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
一共10个数,在标签对应索引位置设置为1,其余设置为0,所以上述编码结果对应实际数字为5。
第四步,构建模型
这里采用前面介绍的序列化方式,实现模型层的堆叠构建。这里的模型层与第一部分分析的lenet-5结构一致,但策略有区别。
首先来看卷积部分,两层卷积+池化。很显然,这里采用了padding补全策略,让输出图像大小保持不变。
model = Sequential()
#1.第一个卷积层:输入图像为28x28x1,卷积核5x5,共6个卷积核,边界策略padding设置为same保持不变,激活函数采用relu
model.add(Conv2D(filters=6,
kernel_size=(5,5),
padding='same',
input_shape=(28,28,1),
activation='relu'))
#2.第一个池化层,大小为2x2
model.add(MaxPooling2D(pool_size=(2,2)))
#3.第二个卷积层:卷积核5x5,共16个卷积核,边界策略padding设置为same保持不变,激活函数采用relu
model.add(Conv2D(filters=16,
kernel_size=(5,5),
padding='same',
activation='relu'))
#4.第二个池化层,大小为2x2
model.add(MaxPooling2D(pool_size=(2,2)))
接下来是神经网络部分,这里在对特征体拉平处理之前先进行了dropout策略,使得某些神经元保持抑制状态,优化网络。
#5.采用dropout策略
model.add(Dropout(0.25))
#6.拉平特征体成一维向量
model.add(Flatten())
#7.搭建神经网络第一个隐层,120个节点,激活函数使用relu
model.add(Dense(120,activation='relu'))
#8.搭建神经网络第二个隐层,84个节点,激活函数使用relu
model.add(Dense(84,activation='relu'))
model.add(Dropout(0.25))
#9.搭建神经网络输出层,10个节点,激活函数采用softmax
model.add(Dense(10,activation='softmax'))
模型构建完成后,可以使用model.summary()来查看:
print(model.summary())
终端输出:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 28, 28, 6) 156
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 14, 14, 6) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 14, 14, 16) 2416
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 7, 7, 16) 0
_________________________________________________________________
dropout (Dropout) (None, 7, 7, 16) 0
_________________________________________________________________
flatten (Flatten) (None, 784) 0
_________________________________________________________________
dense (Dense) (None, 120) 94200
_________________________________________________________________
dense_1 (Dense) (None, 84) 10164
_________________________________________________________________
dropout_1 (Dropout) (None, 84) 0
_________________________________________________________________
dense_2 (Dense) (None, 10) 850
=================================================================
Total params: 107,786
Trainable params: 107,786
Non-trainable params: 0
第五步,编译模型
这里直接使用model的compile方法,设定loss计算方法,优化方法等。
#10.编译模型
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
第六步,训练模型
此时训练时也采用了如今的常用策略,如交叉验证比例、批处理方法等。
#11.训练模型
model.fit(x=x_train_norm,
y=train_label_onehot ,
validation_split=0.2,
epochs=10, #训练次数为10次
batch_size=300,verbose=2)
执行后会计算loss和精度:
-------------------------------------------------------------------------------------------
Epoch 1/10
160/160 - 11s - loss: 0.6368 - accuracy: 0.7997 - val_loss: 0.1361 - val_accuracy: 0.9612
Epoch 2/10
160/160 - 11s - loss: 0.1735 - accuracy: 0.9468 - val_loss: 0.0915 - val_accuracy: 0.9727
Epoch 3/10
160/160 - 11s - loss: 0.1166 - accuracy: 0.9641 - val_loss: 0.0655 - val_accuracy: 0.9809
Epoch 4/10
160/160 - 12s - loss: 0.0895 - accuracy: 0.9723 - val_loss: 0.0565 - val_accuracy: 0.9834
Epoch 5/10
160/160 - 11s - loss: 0.0766 - accuracy: 0.9764 - val_loss: 0.0498 - val_accuracy: 0.9856
Epoch 6/10
160/160 - 11s - loss: 0.0676 - accuracy: 0.9788 - val_loss: 0.0468 - val_accuracy: 0.9869
Epoch 7/10
160/160 - 12s - loss: 0.0578 - accuracy: 0.9819 - val_loss: 0.0426 - val_accuracy: 0.9872
Epoch 8/10
160/160 - 12s - loss: 0.0521 - accuracy: 0.9840 - val_loss: 0.0380 - val_accuracy: 0.9884
Epoch 9/10
160/160 - 12s - loss: 0.0464 - accuracy: 0.9855 - val_loss: 0.0412 - val_accuracy: 0.9883
Epoch 10/10
160/160 - 12s - loss: 0.0442 - accuracy: 0.9861 - val_loss: 0.0421 - val_accuracy: 0.9868
经过10次训练模型精度可以达到98.7%左右。
第七步,使用模型进行预测
可以使用model的predict方法,或者predict_classes方法来获得预测结果:
#模型预测
print("预测结果:",model.predict_classes(x_test_norm[:10]))
print("实际标签:",test_label[:10])
打印结果如下:
预测结果: [7 2 1 0 4 1 4 9 5 9]
实际标签: [7 2 1 0 4 1 4 9 5 9]
很显然预测非常准确了。与1998年发表的lenet-5模型结构对比,如今在使用keras等深度学习框架时可以使用许多有效的策略优化方法,例如dropout、批归一化、交叉验证、relu激活函数等,使得精度得到更好的保障。lenet-5奠定了卷积神经网络的基本骨架,后续的许多网络模型均是以它为蓝本:先卷积后神经网络,不过在网络的模型构建、学习训练部分等发展了更深更好的架构和策略,如Alexnet、VGG-16/19、ZFnet、Googlenet、Resnet等。
三、pytorch框架实现lenet-5模型
pytorch在研究领域使用非常多,属于facebook开源出来的深度学习框架。不仅能够实现强大的GPU加速,同时还支持动态神经网络。pytorch安装过程也较为简单,可以直接使用pip安装,也可以按照许多文献中所说的使用conda安装。由于深度学习一般都需要算力支持,因此建议最好配备GPU显卡。当然小数据集使用CPU计算也可以。
下面以mnist数据集为例,入门一下pytorch深度学习框架使用的基本过程。
第一步,导入相关库和mnist数据集。
由于本文第一个框架为keras,里面就有mnist数据集。在使用pytorch时可以直接拿来使用,同时pytorch库中也有一些经典数据集,就包括mnist数据集。这里的相关库就是pytorch和numpy等。
#1.配置库
import torch
from torch import nn,optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms
上述模块中DataLoader是一个包装类,用来将数据包装为Dataset类,然后传入DataLoader中,我们再使用DataLoader这个类来更加快捷的对数据进行操作。 具体来说,就是将数据集包装成特定的格式,便于处理和引用。torchvision.transforms是pytorch中的图像预处理模块,包含了很多种对图像数据进行变换的函数,这些都是在进行图像数据读入步骤中必不可少的。datasets顾名思义是一系列数据集,可以通过相应的命令加载诸如MNIST等的数据集。 下面就使用datasets模块下载mnist数据集到本地,并转换为dataloader:
#2.下载数据集到本地
train_dataset = torchvision.datasets.MNIST(root='data', #存放目录为本地data文件夹下
train=True, #训练集
transform=transforms.ToTensor(), #转换为tensor
download=True) #同意下载到本地目录下
test_dataset = torchvision.datasets.MNIST(root='data',
train=False, #测试集
transform=transforms.ToTensor(),
download=True)
train_loader = DataLoader(train_dataset,batch_size=100,shuffle=True)
test_loader = DataLoader(test_dataset,batch_size=100,shuffle=False)
第二步,查看mnist数据集
由于DataLoader的策略是分批加载,每批100组数据,共600批次。 每批数据组由100张原始图像和100个标签对构成。
print("训练集数据组总数:",len(train_loader))
#训练集中包括imgs和labels
img_data,label_data = [],[]
for imgs,labels in train_loader:
img_data.append(imgs)
label_data.append(labels)
#查看原始图像信息
print("查看第一批图像数据维度:",img_data[0].shape)
print("第一批里第一张原始图数据维度:",img_data[0][0].shape)
print("第一批里第一个标签数据:",label_data[0][0])
#绘制第一批第一张图像
img = img_data[0][0].reshape(28,28)
plt.imshow(img,cmap='gray')
plt.title("the first picture,label is {}".format(label_data[0][0]))
plt.show()
运行后终端输出:
第三步,构建Lenet-5网络模型
lenet-5网络结构较为清晰,不过由于数据输入尺寸现在为28x28,因此有些参数需要稍加修改。lenet-5包括两个卷积层和两个池化层、两个全连接层。
#3.创建lenet5模型
class Lenet(nn.Module):
def __init__(self):
super(Lenet, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5, padding=2) #第一个卷积层参数,增加补齐边界
self.conv2 = nn.Conv2d(6, 16, 5, 1) #第二个卷积层参数
self.fc1 = nn.Linear(5 * 5 * 16, 120) #第一个全连接层参数
self.fc2 = nn.Linear(120, 84) #第二个全连接层参数
self.fc3 = nn.Linear(84, 10) #输出层参数
def forward(self, x):
x = F.relu(self.conv1(x)) #第一次卷积处理
x = F.max_pool2d(x, 2, 2) #第一次池化
x = F.relu(self.conv2(x)) #第二次卷积处理
x = F.max_pool2d(x, 2, 2) #第二次池化
x = x.view(x.size()[0],-1) #展平处理
x = F.relu(self.fc1(x)) #第一次全连接处理
x = F.relu(self.fc2(x)) #第二次全连接处理
x = self.fc3(x) #第三次全连接处理
return F.log_softmax(x, dim=1) #输出结果
定义好模型后,可以直接打印一下网络结构:
net = Lenet()
print(net)
第四步,定义优化器和损失函数计算方法。如采用交叉熵loss计算和adam优化器:
#4.定义模型优化方法和loss计算
model = Lenet()
cost = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
第五步,使用训练集对模型进行训练
这里可以定义一个train函数,由于pytorch具有自动求导机制,因此对于模型的训练部分,只需要关注前向计算,对于反向传播直接使用误差的backward方法。使得整个训练过程非常简洁:
#5.训练集训练模型
def train(epoch,model,traindata,optimizer,criterion):
'''
:param model:网络模型
:param data: 输入处理训练集
:param optim: 优化器选择
:param criterion: 计算loss方法
:return:
'''
model.train() #对模型进行训练
for index, (data, target) in enumerate(traindata):
#前向计算过程
optimizer.zero_grad() # 梯度先清零
output = model(data)
loss = criterion(output, target)
#反向梯度优化过程
loss.backward() # 误差反向传播计算
optimizer.step() # 更新梯度
#输出训练过程中的loss和数据量
if index % 100 == 0:
# 保存训练模型
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, index * len(data), len(train_loader),
100. * index / len(train_loader), loss.item()))
定义好训练函数后,需要传入相应参数就可以开始运行,其中的epoch参数为迭代次数。例如测试一下当迭代次数取1时:
train(epoch=1,model=net,traindata=train_loader,optimizer=optimizer,criterion=criterion)
运行结果为:
可以看到,训练1次,loss是在逐渐减小的。但明显数值比较大,需要迭代多次时才能降低。例如我们将训练次数设置为50次时,通过多次训练后,loss就变得比较小了。
如果训练集上精度达到一定标准,可以将网络模型保存下来,备后续的预测使用。
#设定训练迭代次数为20次
for epoch in range(1,20):
train(epoch=epoch,model=net,traindata=train_loader,optimizer=optimizer,criterion=criterion)
print("training process end....")
torch.save(net, 'ownmodel.pkl') #将模型保存下来到本地
第六步,对测试集进行模型测试
与训练过程稍微不同的是,在测试集只需要正常运行前向过程,不需要反向传播误差实现权值参数优化。如下:
#6.模型测试
def validate(model,testdata,criterion):
'''
:param model: 网络模型
:param data: 验证集
:param criterion: 计算loss方法
:return:
'''
model.eval()
running_loss = 0 #loss初始值为0
correct = 0 #准确率初始值为0
for data,target in testdata:
y_estimate = model(data)
loss = criterion(y_estimate,target)
running_loss+=loss.item()*data.size(0)
pred = y_estimate.data.max(1,keepdim=True)[1]
correct += pred.eq(target.data.view_as(pred)).cpu().sum()
epoch_loss = running_loss / len(testdata) #计算当前次数里平均loss
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
epoch_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
此时将训练完成的模型加载进来,然后执行如下代码:
model = torch.load('ownmodel.pkl')
validate(model=model,testdata=test_loader,criterion=criterion)
可以获取整个测试集的预测精度:
Test set: Average loss: 432.9472, Accuracy: 9887/10000 (98.87%)
下面选择测试集中的小部分数据进行模型验证
#7.选择小批数据预测,每批次4组数据
data_loader_test = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size = 4,
shuffle = True)
#取出其中第一组数据,返回图像数据和标签
X_test, y_test = next(iter(data_loader_test))
#加载保存好的网络模型
model = torch.load('ownmodel.pkl')
#将图像数据传入模型中进行预测
y_estimate = model(X_test)
#将预测tensor向量转化为ndarry
pred_y = torch.max(y_estimate, 1)[1].data.numpy()
#输出结果
print("该组图像预测数字为:",pred_y)
print("该组图像实际数字为:",y_test.numpy())
执行后终端输出:
至此,利用pytorch来实现CNN卷积神经网络入门过程就结束了。对比keras的高级抽象封装模块,pytorch相对更灵活一些。这些深度学习框架对于卷积层、池化层、激活函数、全连接层等都进行了很好的封装,所以学习起来也相对容易,反正在数据预处理部分显得有挑战一些。
上述两种框架入门mnist手写数字识别代码下载地址为: https://gitee.com/caoln2003/deeplearning
下载后可以直接运行获得结果。如果有什么疑问或者表述不清楚的地方,欢迎下方留言交流