在本教程中,我們將逐步介紹如何使用MNIST數據集構建手寫的數字分類器.對于一個剛剛接觸深度學習的朋友來說,這個練習可以說是“Hello World”.
Note:This blog without all the resources that are outside the original,reproduced please indicate the source.
官網:mxnet.incubator.apache.org,文中如有任何錯漏的地方請諒解并指出,不勝感激.
MNIST
是一個廣泛使用的數據集,用于手寫數字分類任務。它由70,000個標記為28x28像素的手寫數字的灰度圖像組成。數據集被分成60,000個訓練圖像和10,000個測試圖像。它總共有10個類(每個10個數字對應一個)。目前的任務是用6萬張訓練圖像訓練一個模型,然后測試其在10,000個測試圖像上的分類精度。
前提
完成教程,您需要:
- ?安裝好MXNet 0.10 以上版本
- 安裝python以及Jupyter Notebook.
$ pip install requests jupyter
加載數據
在我們定義模型之前,讓我們先獲取MNIST數據集。
下面的源代碼下載并將圖像和相應的標簽載入內存。
import mxnet as mx
mnist = mx.test_utils.get_mnist()
在運行上述源代碼之后,整個MNIST數據集應該被完全加載到內存中。注意,對于大型數據集,預先加載整個數據集是不可行的,就像我們在這里做的那樣
。我們需要的是一種機制,讓我們可以快速有效地從源頭直接流數據。MXNet
數據迭代器通過提供精確的數據來補償這里。數據迭代器是我們將輸入數據輸入到MXNet
訓練算法中的一種機制,它們對于初始化和使用非常簡單,并且對速度進行了優化。在訓練過程中,我們通常會小批量的訓練樣本,并且在整個訓練周期中,我們會最后多次處理每個訓練實例。在本教程中,我們將配置數據迭代器來以100為批次的方式提供示例。請記住,每個示例都是一個28x28灰度圖像和相應的標簽。
圖像批次通常用一個4-D(?四維)數組表示(行如:batch_size
,num_channels
,width
,height
)。對于MNIST
數據集,由于圖像是灰度的,所以只有一個顏色通道。另外,因為圖像是28x28像素,所以每個圖像的寬度和高度等于28。因此,輸入應該是這樣:(batch_size,1,28,28
)。另一個重點關注的是輸入樣本的順序。當提供訓練樣本時,我們不能連續地給樣本提供相同的標簽,這是非常重要的。這樣做會減緩訓練。數據迭代器通過隨機打亂輸入來解決這個問題。注意,我們只需要打亂訓練數據。這個順序對測試數據沒有影響。
下面的源代碼初始化了MNIST
數據集的數據迭代器。請注意,我們初始化了兩個迭代器:一個用于訓練數據,一個用于測試數據。
batch_size = 100
train_iter = mx.io.NDArrayIter(mnist['train_data'], mnist['train_label'], batch_size, shuffle=True)
val_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
訓練
我們將介紹幾個執行手寫數字識別任務的方法。第一種方法利用傳統的深度神經網絡結構,稱為多層感知器(MLP)。我們將討論它的缺點,并以此為動機引入第二種更高級的方法,稱為卷積神經網絡(CNN),它已經被證明能夠很好地處理圖像分類任務。
多層感知器
第一個方法是利用多層感知器來解決這個問題。我們將使用MXNet的符號接口定義MLP。我們首先為輸入數據創建一個place holder變量。在使用MLP時,我們需要將28x28的圖像壓縮成一個平面一維結構的784(28 * 28)原始像素值。在平坦的矢量中,像素值的順序并不重要,只要我們在所有圖像上都是一致的。
data = mx.sym.var('data')
# Flatten the data from 4-D shape into 2-D (batch_size, num_channel*width*height)
data = mx.sym.flatten(data=data)
您可能會有疑問,我們是否通過扁平化來丟棄有價值的信息。這確實是事實,在保留輸入形狀的問題上我們會在討論卷積神經網絡的時候進行詳細的介紹。現在,我們將繼續使用扁平的圖像。
MLP
包含幾個完全連接層。一個完全連接層或簡稱FC
層,是層中的每個神經元與前一層中的每個神經元相連接的地方。從線性代數的角度來看,FC
層對n x m
輸入矩陣x進行仿射變換,并輸出n x k
大小的矩陣Y
,其中k
為FC
層中的神經元數。k
也被稱為隱藏的大小。輸出Y是根據等式Y = WX + b
計算的,FC
層有兩個可學習的參數,即mxk權重矩陣W和mx1偏差向量b
。
在MLP中,大多數FC
層的輸出都被輸入到一個激活函數中,適用于非線性元素。這一步是至關重要的,它給神經網絡提供了分類輸入的能力,而這些輸入不是線性可分的。激活函數的常見選擇是sigmoid
、tanh
和recUNK linear unit(ReLU)
。在這個示例中,我們將使用具有多個理想屬性的ReLU激活函數,通常他被認為是默認選項。
下面的代碼聲明了兩個完全連接層,每個層有128個和64個神經元。此外,這些FC
層被夾在ReLU
激活層之間,每個層負責執行在FC
層輸出上執行元素的ReLU
轉換。
# The first fully-connected layer and the corresponding activation function
fc1 = mx.sym.FullyConnected(data=data, num_hidden=128)
act1 = mx.sym.Activation(data=fc1, act_type="relu")
# The second fully-connected layer and the corresponding activation function
fc2 = mx.sym.FullyConnected(data=act1, num_hidden = 64)
act2 = mx.sym.Activation(data=fc2, act_type="relu")
最后一個完全連接的層通常有其隱藏的大小,等于數據集中的輸出類的數量。這個層的激活函數將是softmax
函數。Softmax
層將其輸入映射為每一類輸出的概率分數。在訓練階段,一個損失函數計算網絡預測的概率分布(softmax output
)與標簽給出的真實概率分布之間的交叉熵。
下面的源代碼聲明了最終全連接的10級的層。順便說一下,10是數字的總數。該層的輸出被輸入到一個軟maxoutput
層,在一個過程中執行軟max
和交叉熵損失計算。請注意,損失計算只在訓練期間發生。
# MNIST has 10 classes
fc3 = mx.sym.FullyConnected(data=act2, num_hidden=10)
# Softmax with cross entropy loss
mlp = mx.sym.SoftmaxOutput(data=fc3, name='softmax')
現在,數據迭代器和神經網絡都被定義了,我們可以開始訓練了。在這里,我們將使用MXNet
中的模塊特性,它為在預定義網絡上運行訓練和推理提供高級抽象。模塊API
允許用戶指定適當的參數來控制培訓的進展。
下面的源代碼初始化一個模塊來訓練我們上面定義的MLP
網絡。對于我們的培訓,我們將使用隨機梯度下降(SGD
)優化器。特別地,我們將使用迷你批處理SGD
。標準的SGD
進程一次只處理一個示例。在實踐中,這是非常緩慢的,一個可以通過小批量的例子來加快進程。在這種情況下,我們的批量大小為100,這是一個合理的選擇。我們在這里選擇的另一個參數是學習速率,它控制優化器獲取解決方案所需的步驟大小。我們會選擇一個0.1的學習率,也是一個合理的選擇。諸如批量大小和學習速率等設置通常被稱為超參數。我們給予他們的價值觀會對他們的訓練有很大的影響。為了本教程的目的,我們將從一些合理和安全的值開始。在其他教程中,我們將討論如何為最佳模型性能尋找超參數的組合。
通常情況下,一個運行訓練直到收斂,這意味著我們從訓練數據中學習了一組很好的模型參數(權重+偏差)。為了本教程的目的,我們將運行10次訓練并停止。?一次訓練是整個列車數據的一個完整的傳遞。
import logging
logging.getLogger().setLevel(logging.DEBUG) # logging to stdout
# create a trainable module on CPU
mlp_model = mx.mod.Module(symbol=mlp, context=mx.cpu())
mlp_model.fit(train_iter, # train data
eval_data=val_iter, # validation data
optimizer='sgd', # use SGD to train
optimizer_params={'learning_rate':0.1}, # use fixed learning rate
eval_metric='acc', # report accuracy during training
batch_end_callback = mx.callback.Speedometer(batch_size, 100), # output progress for each 100 data batches
num_epoch=10) # train for at most 10 dataset passes
預測
在上述培訓完成后,我們可以通過對測試數據的預測來評估培訓的模型。下面的源代碼計算每個測試圖像的預測概率得分。prob[i][j]
是第i個測試圖像包含j - th
輸出類的概率。
test_iter = mx.io.NDArrayIter(mnist['test_data'], None, batch_size)
prob = mlp_model.predict(test_iter)
assert prob.shape == (10000, 10)
由于數據集也有所有的測試圖像的標簽,我們可以計算精度指標如下:
test_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
# predict accuracy of mlp
acc = mx.metric.Accuracy()
mlp_model.score(test_iter, acc)
print(acc)
assert acc.get()[1] > 0.96
如果一切順利,我們應該看到一個大約0.96的精度值,這意味著我們能夠準確地預測96%的測試圖像中的數字。這是一個很好的結果。但正如我們將在本教程的下一部分中看到的,我們可以做得更好。
卷積神經網絡
上文中,我們簡要介紹了MLP
的一個缺點,我們說到我們需要丟棄輸入圖像的原始形狀,并將它作為一個矢量來壓平,然后我們可以把它作為輸入到MLP
的第一個完全連接的層。這是一個很重要的問題,因為我們沒有利用圖像中的像素在水平和垂直軸上具有自然空間相關的事實。一個卷積神經網絡(CNN
)旨在通過使用更結構化的權重表示來解決這個問題。它沒有把圖像壓平,而是做一個簡單的矩陣乘法,而是使用一個或多個卷積層,每個層在輸入圖像上執行二維的卷積。
單個卷積層由一個或多個過濾器組成,每個過濾器都扮演特性檢測器的角色。在訓練過程中,CNN
學習了這些過濾器的適當表示(參數)。類似于MLP
,卷積層的輸出通過應用非線性轉換。除了卷積層之外,CNN
的另一個關鍵方面是匯聚層。一個匯聚層可以使CNN的平移不變:即使在向左/右/向上移動一些像素時,數字仍然保持不變。池層將n個x m補丁減少為單個值,以使網絡對空間位置不敏感。在CNN
的每一個conv
(+激活)層之后都要包含池層。
下面的代碼定義了一個稱為LeNet
的卷積神經網絡架構。LeNet
是一個很受歡迎的網絡,它可以很好地處理數字分類任務。我們將使用與原來的LeNet
實現稍微不同的版本,用tanh激活來代替神經元的sigmoid
激活
data = mx.sym.var('data')
# first conv layer
conv1 = mx.sym.Convolution(data=data, kernel=(5,5), num_filter=20)
tanh1 = mx.sym.Activation(data=conv1, act_type="tanh")
pool1 = mx.sym.Pooling(data=tanh1, pool_type="max", kernel=(2,2), stride=(2,2))
# second conv layer
conv2 = mx.sym.Convolution(data=pool1, kernel=(5,5), num_filter=50)
tanh2 = mx.sym.Activation(data=conv2, act_type="tanh")
pool2 = mx.sym.Pooling(data=tanh2, pool_type="max", kernel=(2,2), stride=(2,2))
# first fullc layer
flatten = mx.sym.flatten(data=pool2)
fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=500)
tanh3 = mx.sym.Activation(data=fc1, act_type="tanh")
# second fullc
fc2 = mx.sym.FullyConnected(data=tanh3, num_hidden=10)
# softmax loss
lenet = mx.sym.SoftmaxOutput(data=fc2, name='softmax')
現在我們用同樣的超參數來訓練LeNet
。注意,如果GPU
可用,我們建議使用它。這大大加快了計算速度,因為LeNet
比之前的多層感知器更加復雜和計算更密集。為此,我們只需要將mx. cpu()
改為mx.gpu()
,而MXNet
則負責其余部分。就像以前一樣,我們將在10次訓練之后停止訓練。
# create a trainable module on GPU 0
lenet_model = mx.mod.Module(symbol=lenet, context=mx.cpu())
# train with the same
lenet_model.fit(train_iter,
eval_data=val_iter,
optimizer='sgd',
optimizer_params={'learning_rate':0.1},
eval_metric='acc',
batch_end_callback = mx.callback.Speedometer(batch_size, 100),
num_epoch=10)
預測
最后,我們將使用經過訓練的LeNet模型來生成對測試數據的預測。
test_iter = mx.io.NDArrayIter(mnist['test_data'], None, batch_size)
prob = lenet_model.predict(test_iter)
test_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
# predict accuracy for lenet
acc = mx.metric.Accuracy()
lenet_model.score(test_iter, acc)
print(acc)
assert acc.get()[1] > 0.98
如果一切順利,我們應該會看到使用LeNet
的預測更準確。在CNN
,我們應該能夠正確預測98%的測試圖像。
小結
在本教程中,我們學習了如何使用MXNet
來解決一個標準的計算機視覺問題:將手寫數字的圖像分類。您已經了解了如何快速、輕松地構建、訓練和評估諸如MLP
和CNN
等模型,并使用MXNet
。