【深度学习】卷积网络LeNet及其MXNet实现

LeNet网络概述

笔者在【深度学习】多层感知机(二)MXNet实现双层感知机一文中使用单隐藏层感知机模型对MNIST数据集中的手写数字图像进行了分类。MNIST数据集中每张图像尺寸都是28*28像素,将其按行展开将得到一个长度为784的向量,这也正是上述感知机模型全连接隐藏层的输入。在使用‘ReLU’激活函数情况下的准确率大约在97.8%,使用‘sigmoid’激活函数的情况下准确率大约在95.7%。

使用全连接层有如下的局限性1

  1. 图像在同一列的邻近像素在这个展开向量中距离较远,它们构成的特征(或者说模式)难以被模型识别;
  2. 对于大尺寸的输入图像,使用全连接层容易造成模型过大。打个比方,输入是3通道1000*1000像素图像,即使全连接层节点个数为256,这层的模型参数有3*1000*1000*256,将占用大约3GB的内存或者显存空间,带来了过高的模型复杂度和存储开销。

卷积层可以有效解决这两个问题:一方面卷积层保留输入形状,使得图像的像素在高和宽两个方向上的相关性均可以被识别;另一方面,卷积层通过滑动窗口将同一个卷积核与不同位置的输入重复计算,从而避免了过大的参数尺寸。

本文展示一个早期用于识别手写数字图像的卷积神经网络LeNet 2,介绍其网络结构,使用MXNet深度学习框架进行复现,并在MNIST数据集上进行实验。

LeNet网络结构

LeNet的网络结构比较简单,适合于深度学习入门。其分为两个模块:卷积层模块和全连接层模块。

  • 卷积层模块:卷积层模块的基本单位是卷积层后接池化层,卷积层用于识别图像的位置特征,池化层用于降低卷积层对位置的敏感度。卷积层模块由两个这样的基本单位堆叠而成。在卷积层中,使用 5 ∗ 5 5*5 55卷积窗口,并在输出上使用‘sigmoid’函数。第一个卷积层的输出通道数为6,第二个卷积层的输出通道数增加至16,这样做得目的是为了使两个卷积层的参数尺寸类似,因为第一个卷积层的输入比第二个卷积层的输入数据高和宽都大。在池化层中,池化窗口尺寸为 2 ∗ 2 2*2 22,且步幅为2,故而池化窗口在输入上每次滑动后覆盖的区域并不重合。
  • 全连接层模块:全连层模块由三个全连接层构成,输出个数分别为120,84和10,其中10为数据集中的类别数目。

在这里插入图片描述
LeNet中数据的正向传播如下图所示3
在这里插入图片描述
输入数据如下图所示(以上图中数字8为例):
在这里插入图片描述
这里有一个值得注意的问题,卷积层模块输出数据的形状为(样本数,通道数,高,宽),全连接层模块进行处理前,将会对每个样本进行变平(flatten)操作,即将每个样本的像素按行展开,这样将得到一个二维的输入,其中第一维表示样本,第二维是每个样本变平后的向量表示,且向量的长度为通道数*高*宽。在MXNet中,将自动执行这一步的变平操作。利用MXNet定义模型如下:

from mxnet.gluon import nn

lenet = nn.Sequential()
lenet.add(nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid'),  # 卷积层模块,输出(批量大小,通道数,高,宽)
          nn.MaxPool2D(pool_size=2, strides=2),  # 这里使用最大池化层
          nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'),
          nn.MaxPool2D(pool_size=2, strides=2),
          nn.Dense(units=120, activation='sigmoid'),  # 全连接层,Dense会自动执行flatten操作,变为(批量大小,通道数*高*宽)
          nn.Dense(units=84, activation='sigmoid'),
          nn.Dense(units=10))

我们人为设置一个随机输入数据,并编写实验代码,看看LeNet每一层的输出数据形状(维度):

from mxnet.gluon import nn
from mxnet import nd

# 模型的定义及初始化
lenet = nn.Sequential()
lenet.add(nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid'),  # 卷积层模块,输出(批量大小,通道数,高,宽)
          nn.MaxPool2D(pool_size=2, strides=2),
          nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'),
          nn.MaxPool2D(pool_size=2, strides=2),
          nn.Dense(units=120, activation='sigmoid'),  # 全连接层,Dense会自动执行flatten操作,变为(批量大小,通道数*高*宽)
          nn.Dense(units=84, activation='sigmoid'),
          nn.Dense(units=10))
lenet.initialize()
x = nd.random.uniform(shape=(1, 1, 28, 28))
for layer in lenet:
    x = layer(x)
    print(layer.name, 'output shape: ', x.shape)

可以得到输出如下,含义(样本数,通道数,高,宽):
在这里插入图片描述

MNIST数据集实验

相比于单隐藏层MLP模型,LeNet的计算要复杂得多,所以这里的实验使用GPU来进行计算。需要注意的是,MXNet中,必须保证模型和数据同在内存或者显存中,计算才能正常进行。也就是说,在这里的实验中,我们载入数据后,训练和测试时需要将数据从内存拷贝到显存中。使用MXNet提供的as_in_context函数即可,其他MXNet用法可参考MXNet官方文档或者笔者之前的文章。

编写代码进行实验之前,请确保已经正确安装了MXNet的GPU环境,通过nvidia-smi查看gpu信息,记下空闲gpu的device_id,代码中将会用到,用于告诉MXNet用哪一块gpu进行运算。

环境搭建可参考笔者之前的文章:

实验代码

# coding=utf-8
# author: BebDong
# 2019/1/21
# LeNet:早期用于识别手写数字图像的卷积神经网络,论文的第一作者Yann LeCun,故而得名LeNet
# 通过Sequential来实现
# 利用GPU实现计算,务必保证模型和数据同在显存中

import mxnet as mx
from mxnet.gluon import nn, data as gdata, loss as gloss
from mxnet import init, gluon, autograd, nd

from matplotlib import pyplot as plt
from IPython import display
import pylab

import sys
import time

# 数据读取并形成批量,注意这里数据是保存在内存中,训练时需要将数据复制到显存
batch_size = 256  # 批量大小
transformer = gdata.vision.transforms.ToTensor()
mnist_train = gdata.vision.MNIST(train=True)
mnist_test = gdata.vision.MNIST(train=False)
num_workers = 0 if sys.platform.startswith('win32') else 4  # 非windows系统多线程加速数据读取
train_iter = gdata.DataLoader(mnist_train.transform_first(transformer), batch_size, shuffle=True,
                              num_workers=num_workers)
test_iter = gdata.DataLoader(mnist_test.transform_first(transformer), batch_size, shuffle=False,
                             num_workers=num_workers)

# 模型的定义及初始化
lenet = nn.Sequential()
lenet.add(nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid'),  # 卷积层模块,输出(批量大小,通道数,高,宽)
          nn.MaxPool2D(pool_size=2, strides=2),
          nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'),
          nn.MaxPool2D(pool_size=2, strides=2),
          nn.Dense(units=120, activation='sigmoid'),  # 全连接层,Dense会自动执行flatten操作,变为(批量大小,通道数*高*宽)
          nn.Dense(units=84, activation='sigmoid'),
          nn.Dense(units=10))
gpu_id = 0  # 通过nvidia-smi查看空闲GPU
lenet.initialize(ctx=mx.gpu(gpu_id), init=init.Xavier())  # 模型保存在显存中

# 模型训练
lr = 0.1  # 学习率
epochs = 100  # 训练次数
trainer = gluon.Trainer(lenet.collect_params(), optimizer='sgd', optimizer_params={'learning_rate': lr})
loss = gloss.SoftmaxCrossEntropyLoss()
train_acc_array, test_acc_array = [], []  # 记录训练过程中的数据,作图
for epoch in range(epochs):
    train_los_sum, train_acc_sum = 0.0, 0.0  # 每个epoch的损失和准确率
    epoch_start = time.time()  # epoch开始的时间
    for X, y in train_iter:
        X, y = X.as_in_context(mx.gpu(gpu_id)), y.as_in_context(mx.gpu(gpu_id))  # 将数据复制到GPU中
        with autograd.record():
            y_hat = lenet(X)
            los = loss(y_hat, y)
        los.backward()
        trainer.step(batch_size)
        train_los_sum += los.mean().asscalar()  # 计算训练的损失
        train_acc_sum += (y_hat.argmax(axis=1) == y.astype('float32')).mean().asscalar()  # 计算训练的准确率
    test_acc_sum = nd.array([0], ctx=mx.gpu(gpu_id))  # 计算模型此时的测试准确率
    for features, labels in test_iter:
        features, labels = features.as_in_context(mx.gpu(gpu_id)), labels.as_in_context(mx.gpu(gpu_id))
        test_acc_sum += (lenet(features).argmax(axis=1) == labels.astype('float32')).mean()
    test_acc = test_acc_sum.asscalar() / len(test_iter)
    print('epoch %d, time %.1f sec, loss %.4f, train acc %.4f, test acc %.4f' %
          (epoch + 1, time.time() - epoch_start, train_los_sum / len(train_iter), train_acc_sum / len(train_iter),
           test_acc))
    train_acc_array.append(train_acc_sum / len(train_iter))  # 记录训练过程中的数据
    test_acc_array.append(test_acc)

# 作图
display.set_matplotlib_formats('svg')  # 矢量图
plt.rcParams['figure.figsize'] = (3.5, 2.5)  # 图片尺寸
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.semilogy(range(1, epochs + 1), train_acc_array)
plt.semilogy(range(1, epochs + 1), test_acc_array, linestyle=":")
plt.legend(['train accuracy', 'test accuracy'])
pylab.show()

结果讨论

笔者设置的学习率为0.1,训练100次,得到的结果如下所示:
在这里插入图片描述
每个epoch的准确率如下,可以看到最后测试准确率约为98.4%,这相比【深度学习】多层感知机(二)MXNet实现双层感知机一文中同样使用’sigmoid’激活函数单隐藏层感知机的95.7%要好很多了。有兴趣的筒子还可以试试’ReLU’激活函数,还可以改变网络的超参数看看实验结果的变化(比如学习率、训练次数、卷积层卷积核尺寸、各层通道数目等等)。
在这里插入图片描述

FashionMNIST数据集实验

代码逻辑和主体代码完全同上MNIST数据集实验,仅需要更改数据读取部分即可:

mnist_train = gdata.vision.FashionMNIST(train=True)
mnist_test = gdata.vision.FashionMNIST(train=False)

设置参数:lr=0.3epochs=200,可以得到如下结果,准确率91%左右,这比【深度学习】Softmax回归(三)MXNet深度学习框架实现一文中使用Softmax回归所得结果85%又有了不错的提升。(当然,所有这些结果都没有经过大量的参数调优)
在这里插入图片描述
我们作出准确率随着epoch的变化曲线图:
在这里插入图片描述

参考文献


  1. Aston Zhang, Mu Li et al. Hands on Deep Learning[EB/OL]. http://zh.d2l.ai/chapter_convolutional-neural-networks/lenet.html ↩︎

  2. Lecun Y L , Bottou L , Bengio Y , et al. Gradient-Based Learning Applied to Document Recognition[J]. Proceedings of the IEEE, 1998, 86(11):2278-2324. ↩︎

  3. yangyang688. LeNet论文+tensorflow代码实现[EB/OL]. https://blog.csdn.net/yangyang688/article/details/82884336 ↩︎

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值