【深度學習】卷積網絡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的網絡結構比較簡單,適合於深度學習入門。其分爲兩個模塊:卷積層模塊和全連接層模塊。

  • 卷積層模塊:卷積層模塊的基本單位是卷積層後接池化層,卷積層用於識別圖像的位置特徵,池化層用於降低卷積層對位置的敏感度。卷積層模塊由兩個這樣的基本單位堆疊而成。在卷積層中,使用555*5卷積窗口,並在輸出上使用‘sigmoid’函數。第一個卷積層的輸出通道數爲6,第二個卷積層的輸出通道數增加至16,這樣做得目的是爲了使兩個卷積層的參數尺寸類似,因爲第一個卷積層的輸入比第二個卷積層的輸入數據高和寬都大。在池化層中,池化窗口尺寸爲222*2,且步幅爲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 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章