本教程源代碼目錄在book/recognize_digits, 初次使用請參考PaddlePaddle安裝教程。
當(dāng)我們學(xué)習(xí)編程的時候,編寫的第一個程序一般是實現(xiàn)打印"Hello World"。而機器學(xué)習(xí)(或深度學(xué)習(xí))的入門教程,一般都是 MNIST 數(shù)據(jù)庫上的手寫識別問題。原因是手寫識別屬于典型的圖像分類問題,比較簡單,同時MNIST數(shù)據(jù)集也很完備。MNIST數(shù)據(jù)集作為一個簡單的計算機視覺數(shù)據(jù)集,包含一系列如圖1所示的手寫數(shù)字圖片和對應(yīng)的標(biāo)簽。圖片是28x28的像素矩陣,標(biāo)簽則對應(yīng)著0~9的10個數(shù)字。每張圖片都經(jīng)過了大小歸一化和居中處理。
http://wiki.jikexueyuan.com/project/deep-learning/images/02-01.png" alt="png" />
圖1. MNIST圖片示例
MNIST數(shù)據(jù)集是從 NIST 的Special Database 3(SD-3)和Special Database 1(SD-1)構(gòu)建而來。由于SD-3是由美國人口調(diào)查局的員工進行標(biāo)注,SD-1是由美國高中生進行標(biāo)注,因此SD-3比SD-1更干凈也更容易識別。Yann LeCun等人從SD-1和SD-3中各取一半作為MNIST的訓(xùn)練集(60000條數(shù)據(jù))和測試集(10000條數(shù)據(jù)),其中訓(xùn)練集來自250位不同的標(biāo)注員,此外還保證了訓(xùn)練集和測試集的標(biāo)注員是不完全相同的。
Yann LeCun早先在手寫字符識別上做了很多研究,并在研究過程中提出了卷積神經(jīng)網(wǎng)絡(luò)(Convolutional Neural Network),大幅度地提高了手寫字符的識別能力,也因此成為了深度學(xué)習(xí)領(lǐng)域的奠基人之一。如今的深度學(xué)習(xí)領(lǐng)域,卷積神經(jīng)網(wǎng)絡(luò)占據(jù)了至關(guān)重要的地位,從最早Yann LeCun提出的簡單LeNet,到如今ImageNet大賽上的優(yōu)勝模型VGGNet、GoogLeNet、ResNet等(請參見圖像分類 教程),人們在圖像分類領(lǐng)域,利用卷積神經(jīng)網(wǎng)絡(luò)得到了一系列驚人的結(jié)果。
有很多算法在MNIST上進行實驗。1998年,LeCun分別用單層線性分類器、多層感知器(Multilayer Perceptron, MLP)和多層卷積神經(jīng)網(wǎng)絡(luò)LeNet進行實驗,使得測試集上的誤差不斷下降(從12%下降到0.7%)[1]。此后,科學(xué)家們又基于K近鄰(K-Nearest Neighbors)算法[2]、支持向量機(SVM)[3]、神經(jīng)網(wǎng)絡(luò)[4-7]和Boosting方法[8]等做了大量實驗,并采用多種預(yù)處理方法(如去除歪曲、去噪、模糊等)來提高識別的準(zhǔn)確率。
本教程中,我們從簡單的模型Softmax回歸開始,帶大家入門手寫字符識別,并逐步進行模型優(yōu)化。
基于MNIST數(shù)據(jù)訓(xùn)練一個分類器,在介紹本教程使用的三個基本圖像分類網(wǎng)絡(luò)前,我們先給出一些定義:
$X$是輸入:MNIST圖片是$28\times28$ 的二維圖像,為了進行計算,我們將其轉(zhuǎn)化為$784$維向量,即
$X=\left ( x_0, x_1, \dots, x_{783} \right )$。
$Y$是輸出:分類器的輸出是10類數(shù)字(0-9),即$Y=\left ( y_0, y_1, \dots, y_9 \right )$,每一維$y_i$代表圖片分類為第$i$類數(shù)字的概率。
$L$是圖片的真實標(biāo)簽:$L=\left ( l_0, l_1, \dots, l_9 \right )$也是10維,但只有一維為1,其他都為0。
最簡單的Softmax回歸模型是先將輸入層經(jīng)過一個全連接層得到的特征,然后直接通過softmax 函數(shù)進行多分類[9]。
輸入層的數(shù)據(jù)$X$傳到輸出層,在激活操作之前,會乘以相應(yīng)的權(quán)重 $W$ ,并加上偏置變量 $b$ ,具體如下:
$$ y_i = \text{softmax}(\sum_j W_{i,j}x_j + b_i) $$
其中 $ \text{softmax}(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}} $
對于有 $N$ 個類別的多分類問題,指定 $N$ 個輸出節(jié)點,$N$ 維結(jié)果向量經(jīng)過softmax將歸一化為 $N$ 個[0,1]范圍內(nèi)的實數(shù)值,分別表示該樣本屬于這 $N$ 個類別的概率。此處的 $y_i$ 即對應(yīng)該圖片為數(shù)字 $i$ 的預(yù)測概率。
在分類問題中,我們一般采用交叉熵代價損失函數(shù)(cross entropy),公式如下:
$$ \text{crossentropy}(label, y) = -\sum_i label_ilog(y_i) $$
圖2為softmax回歸的網(wǎng)絡(luò)圖,圖中權(quán)重用藍線表示、偏置用紅線表示、+1代表偏置參數(shù)的系數(shù)為1。
http://wiki.jikexueyuan.com/project/deep-learning/images/02-02.png" alt="png" />
圖2. softmax回歸網(wǎng)絡(luò)結(jié)構(gòu)圖
Softmax回歸模型采用了最簡單的兩層神經(jīng)網(wǎng)絡(luò),即只有輸入層和輸出層,因此其擬合能力有限。為了達到更好的識別效果,我們考慮在輸入層和輸出層中間加上若干個隱藏層[10]。
1.經(jīng)過第一個隱藏層,可以得到 $ H_1 = \phi(W_1X + b_1) $,其中$\phi$代表激活函數(shù),常見的有sigmoid、tanh或ReLU等函數(shù)。
2.經(jīng)過第二個隱藏層,可以得到 $ H_2 = \phi(W_2H_1 + b_2) $。
3.最后,再經(jīng)過輸出層,得到的$Y=\text{softmax}(W_3H_2 + b_3)$,即為最后的分類結(jié)果向量。
圖3為多層感知器的網(wǎng)絡(luò)結(jié)構(gòu)圖,圖中權(quán)重用藍線表示、偏置用紅線表示、+1代表偏置參數(shù)的系數(shù)為1。
http://wiki.jikexueyuan.com/project/deep-learning/images/02-03.png" alt="png" />
圖3. 多層感知器網(wǎng)絡(luò)結(jié)構(gòu)圖
在多層感知器模型中,將圖像展開成一維向量輸入到網(wǎng)絡(luò)中,忽略了圖像的位置和結(jié)構(gòu)信息,而卷積神經(jīng)網(wǎng)絡(luò)能夠更好的利用圖像的結(jié)構(gòu)信息。LeNet-5是一個較簡單的卷積神經(jīng)網(wǎng)絡(luò)。圖4顯示了其結(jié)構(gòu):輸入的二維圖像,先經(jīng)過兩次卷積層到池化層,再經(jīng)過全連接層,最后使用softmax分類作為輸出層。下面我們主要介紹卷積層和池化層。
http://wiki.jikexueyuan.com/project/deep-learning/images/02-04.png" alt="png" />
圖4. LeNet-5卷積神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)
卷積層是卷積神經(jīng)網(wǎng)絡(luò)的核心基石。在圖像識別里我們提到的卷積是二維卷積,即離散二維濾波器(也稱作卷積核)與二維圖像做卷積操作,簡單的講是二維濾波器滑動到二維圖像上所有位置,并在每個位置上與該像素點及其領(lǐng)域像素點做內(nèi)積。卷積操作被廣泛應(yīng)用與圖像處理領(lǐng)域,不同卷積核可以提取不同的特征,例如邊沿、線性、角等特征。在深層卷積神經(jīng)網(wǎng)絡(luò)中,通過卷積操作可以提取出圖像低級到復(fù)雜的特征。
http://wiki.jikexueyuan.com/project/deep-learning/images/02-05.png" alt="png" />
圖5. 卷積層圖片
圖5給出一個卷積計算過程的示例圖,輸入圖像大小為$H=5,W=5,D=3$,即$5 \times 5$大小的3通道(RGB,也稱作深度)彩色圖像。這個示例圖中包含兩(用$K$表示)組卷積核,即圖中濾波器$W_0$和$W_1$。在卷積計算中,通常對不同的輸入通道采用不同的卷積核,如圖示例中每組卷積核包含($D=3)$個$3 \times 3$(用$F \times F$表示)大小的卷積核。另外,這個示例中卷積核在圖像的水平方向($W$方向)和垂直方向($H$方向)的滑動步長為2(用$S$表示);對輸入圖像周圍各填充1(用$P$表示)個0,即圖中輸入層原始數(shù)據(jù)為藍色部分,灰色部分是進行了大小為1的擴展,用0來進行擴展。經(jīng)過卷積操作得到輸出為$3 \times 3 \times 2$(用$H_{o} \times W_{o} \times K$表示)大小的特征圖,即$3 \times 3$大小的2通道特征圖,其中$H_o$計算公式為:$H_o = (H - F + 2 \times P)/S + 1$,$W_o$同理。 而輸出特征圖中的每個像素,是每組濾波器與輸入圖像每個特征圖的內(nèi)積再求和,再加上偏置$b_o$,偏置通常對于每個輸出特征圖是共享的。輸出特征圖$o[:,:,0]$中的最后一個$-2$計算如圖5右下角公式所示。
在卷積操作中卷積核是可學(xué)習(xí)的參數(shù),經(jīng)過上面示例介紹,每層卷積的參數(shù)大小為$D \times F \times F \times K$。在多層感知器模型中,神經(jīng)元通常是全部連接,參數(shù)較多。而卷積層的參數(shù)較少,這也是由卷積層的主要特性即局部連接和共享權(quán)重所決定。
局部連接:每個神經(jīng)元僅與輸入神經(jīng)元的一塊區(qū)域連接,這塊局部區(qū)域稱作感受野(receptive field)。在圖像卷積操作中,即神經(jīng)元在空間維度(spatial dimension,即上圖示例H和W所在的平面)是局部連接,但在深度上是全部連接。對于二維圖像本身而言,也是局部像素關(guān)聯(lián)較強。這種局部連接保證了學(xué)習(xí)后的過濾器能夠?qū)τ诰植康妮斎胩卣饔凶顝姷捻憫?yīng)。局部連接的思想,也是受啟發(fā)于生物學(xué)里面的視覺系統(tǒng)結(jié)構(gòu),視覺皮層的神經(jīng)元就是局部接受信息的。
權(quán)重共享:計算同一個深度切片的神經(jīng)元時采用的濾波器是共享的。例如圖4中計算$o[:,:,0]$的每個每個神經(jīng)元的濾波器均相同,都為$W_0$,這樣可以很大程度上減少參數(shù)。共享權(quán)重在一定程度上講是有意義的,例如圖片的底層邊緣特征與特征在圖中的具體位置無關(guān)。但是在一些場景中是無意的,比如輸入的圖片是人臉,眼睛和頭發(fā)位于不同的位置,希望在不同的位置學(xué)到不同的特征 (參考斯坦福大學(xué)公開課)。請注意權(quán)重只是對于同一深度切片的神經(jīng)元是共享的,在卷積層,通常采用多組卷積核提取不同特征,即對應(yīng)不同深度切片的特征,不同深度切片的神經(jīng)元權(quán)重是不共享。另外,偏重對同一深度切片的所有神經(jīng)元都是共享的。
通過介紹卷積計算過程及其特性,可以看出卷積是線性操作,并具有平移不變性(shift-invariant),平移不變性即在圖像每個位置執(zhí)行相同的操作。卷積層的局部連接和權(quán)重共享使得需要學(xué)習(xí)的參數(shù)大大減小,這樣也有利于訓(xùn)練較大卷積神經(jīng)網(wǎng)絡(luò)。
http://wiki.jikexueyuan.com/project/deep-learning/images/02-06.png" alt="png" />
圖6. 池化層圖片
池化是非線性下采樣的一種形式,主要作用是通過減少網(wǎng)絡(luò)的參數(shù)來減小計算量,并且能夠在一定程度上控制過擬合。通常在卷積層的后面會加上一個池化層。池化包括最大池化、平均池化等。其中最大池化是用不重疊的矩形框?qū)⑤斎雽臃殖刹煌膮^(qū)域,對于每個矩形框的數(shù)取最大值作為輸出層,如圖6所示。
更詳細的關(guān)于卷積神經(jīng)網(wǎng)絡(luò)的具體知識可以參考斯坦福大學(xué)公開課和圖像分類教程。
sigmoid激活函數(shù): $ f(x) = sigmoid(x) = \frac{1}{1+e^{-x}} $
tanh激活函數(shù): $ f(x) = tanh(x) = \frac{e^x-e^{-x}}{e^x+e^{-x}} $
實際上,tanh函數(shù)只是規(guī)模變化的sigmoid函數(shù),將sigmoid函數(shù)值放大2倍之后再向下平移1個單位:tanh(x) = 2sigmoid(2x) - 1 。
ReLU激活函數(shù): $ f(x) = max(0, x) $
更詳細的介紹請參考維基百科激活函數(shù)。
PaddlePaddle在API中提供了自動加載MNIST數(shù)據(jù)的模塊paddle.dataset.mnist。加載后的數(shù)據(jù)位于/home/username/.cache/paddle/dataset/mnist下:
| 文件名稱 | 說明 |
|---|---|
| train-images-idx3-ubyte | 訓(xùn)練數(shù)據(jù)圖片,60,000條數(shù)據(jù) |
| train-labels-idx1-ubyte | 訓(xùn)練數(shù)據(jù)標(biāo)簽,60,000條數(shù)據(jù) |
| t10k-images-idx3-ubyte | 測試數(shù)據(jù)圖片,10,000條數(shù)據(jù) |
| t10k-labels-idx1-ubyte | 測試數(shù)據(jù)標(biāo)簽,10,000條數(shù)據(jù) |
首先,加載PaddlePaddle的V2 api包。
import gzip
import paddle.v2 as paddle
其次,定義三個不同的分類器:
Softmax回歸:只通過一層簡單的以softmax為激活函數(shù)的全連接層,就可以得到分類的結(jié)果。
def softmax_regression(img):
predict = paddle.layer.fc(input=img,
size=10,
act=paddle.activation.Softmax())
return predict
多層感知器:下面代碼實現(xiàn)了一個含有兩個隱藏層(即全連接層)的多層感知器。其中兩個隱藏層的激活函數(shù)均采用ReLU,輸出層的激活函數(shù)用Softmax。
def multilayer_perceptron(img):
# 第一個全連接層,激活函數(shù)為ReLU
hidden1 = paddle.layer.fc(input=img, size=128, act=paddle.activation.Relu())
# 第二個全連接層,激活函數(shù)為ReLU
hidden2 = paddle.layer.fc(input=hidden1,
size=64,
act=paddle.activation.Relu())
# 以softmax為激活函數(shù)的全連接輸出層,輸出層的大小必須為數(shù)字的個數(shù)10
predict = paddle.layer.fc(input=hidden2,
size=10,
act=paddle.activation.Softmax())
return predict
卷積神經(jīng)網(wǎng)絡(luò)LeNet-5: 輸入的二維圖像,首先經(jīng)過兩次卷積層到池化層,再經(jīng)過全連接層,最后使用以softmax為激活函數(shù)的全連接層作為輸出層。
def convolutional_neural_network(img):
# 第一個卷積-池化層
conv_pool_1 = paddle.networks.simple_img_conv_pool(
input=img,
filter_size=5,
num_filters=20,
num_channel=1,
pool_size=2,
pool_stride=2,
act=paddle.activation.Relu())
# 第二個卷積-池化層
conv_pool_2 = paddle.networks.simple_img_conv_pool(
input=conv_pool_1,
filter_size=5,
num_filters=50,
num_channel=20,
pool_size=2,
pool_stride=2,
act=paddle.activation.Relu())
# 以softmax為激活函數(shù)的全連接輸出層,輸出層的大小必須為數(shù)字的個數(shù)10
predict = paddle.layer.fc(input=conv_pool_2,
size=10,
act=paddle.activation.Softmax())
return predict
接著,通過layer.data調(diào)用來獲取數(shù)據(jù),然后調(diào)用分類器(這里我們提供了三個不同的分類器)得到分類結(jié)果。訓(xùn)練時,對該結(jié)果計算其損失函數(shù),分類問題常常選擇交叉熵損失函數(shù)。
# 該模型運行在單個CPU上
paddle.init(use_gpu=False, trainer_count=1)
images = paddle.layer.data(
name='pixel', type=paddle.data_type.dense_vector(784))
label = paddle.layer.data(
name='label', type=paddle.data_type.integer_value(10))
# predict = softmax_regression(images) # Softmax回歸
# predict = multilayer_perceptron(images) #多層感知器
predict = convolutional_neural_network(images) #LeNet5卷積神經(jīng)網(wǎng)絡(luò)
cost = paddle.layer.classification_cost(input=predict, label=label)
然后,指定訓(xùn)練相關(guān)的參數(shù)。
訓(xùn)練方法(optimizer): 代表訓(xùn)練過程在更新權(quán)重時采用動量優(yōu)化器 Momentum ,其中參數(shù)0.9代表動量優(yōu)化每次保持前一次速度的0.9倍。
訓(xùn)練速度(learning_rate): 迭代的速度,與網(wǎng)絡(luò)的訓(xùn)練收斂速度有關(guān)系。
正則化(regularization): 是防止網(wǎng)絡(luò)過擬合的一種手段,此處采用L2正則化。
parameters = paddle.parameters.create(cost)
optimizer = paddle.optimizer.Momentum(
learning_rate=0.1 / 128.0,
momentum=0.9,
regularization=paddle.optimizer.L2Regularization(rate=0.0005 * 128))
trainer = paddle.trainer.SGD(cost=cost,
parameters=parameters,
update_equation=optimizer)
下一步,我們開始訓(xùn)練過程。paddle.dataset.movielens.train()和paddle.dataset.movielens.test()分別做訓(xùn)練和測試數(shù)據(jù)集。這兩個函數(shù)各自返回一個reader——PaddlePaddle中的reader是一個Python函數(shù),每次調(diào)用的時候返回一個Python yield generator。
下面shuffle是一個reader decorator,它接受一個reader A,返回另一個reader B —— reader B 每次讀入buffer_size條訓(xùn)練數(shù)據(jù)到一個buffer里,然后隨機打亂其順序,并且逐條輸出。
batch是一個特殊的decorator,它的輸入是一個reader,輸出是一個batched reader —— 在PaddlePaddle里,一個reader每次yield一條訓(xùn)練數(shù)據(jù),而一個batched reader每次yield一個minibatch。
event_handler_plot可以用來在訓(xùn)練過程中畫圖如下:
http://wiki.jikexueyuan.com/project/deep-learning/images/02-07.png" alt="png" />
from paddle.v2.plot import Ploter
train_title = "Train cost"
test_title = "Test cost"
cost_ploter = Ploter(train_title, test_title)
step = 0
# event_handler to plot a figure
def event_handler_plot(event):
global step
if isinstance(event, paddle.event.EndIteration):
if step % 100 == 0:
cost_ploter.append(train_title, step, event.cost)
cost_ploter.plot()
step += 1
if isinstance(event, paddle.event.EndPass):
# save parameters
with gzip.open('params_pass_%d.tar.gz' % event.pass_id, 'w') as f:
parameters.to_tar(f)
result = trainer.test(reader=paddle.batch(
paddle.dataset.mnist.test(), batch_size=128))
cost_ploter.append(test_title, step, result.cost)
event_handler 用來在訓(xùn)練過程中輸出訓(xùn)練結(jié)果
lists = []
def event_handler(event):
if isinstance(event, paddle.event.EndIteration):
if event.batch_id % 100 == 0:
print "Pass %d, Batch %d, Cost %f, %s" % (
event.pass_id, event.batch_id, event.cost, event.metrics)
if isinstance(event, paddle.event.EndPass):
# save parameters
with gzip.open('params_pass_%d.tar.gz' % event.pass_id, 'w') as f:
parameters.to_tar(f)
result = trainer.test(reader=paddle.batch(
paddle.dataset.mnist.test(), batch_size=128))
print "Test with Pass %d, Cost %f, %s\n" % (
event.pass_id, result.cost, result.metrics)
lists.append((event.pass_id, result.cost,
result.metrics['classification_error_evaluator']))
trainer.train(
reader=paddle.batch(
paddle.reader.shuffle(
paddle.dataset.mnist.train(), buf_size=8192),
batch_size=128),
event_handler=event_handler_plot,
num_passes=5)
訓(xùn)練過程是完全自動的,event_handler里打印的日志類似如下所示:
# Pass 0, Batch 0, Cost 2.780790, {'classification_error_evaluator': 0.9453125}
# Pass 0, Batch 100, Cost 0.635356, {'classification_error_evaluator': 0.2109375}
# Pass 0, Batch 200, Cost 0.326094, {'classification_error_evaluator': 0.1328125}
# Pass 0, Batch 300, Cost 0.361920, {'classification_error_evaluator': 0.1015625}
# Pass 0, Batch 400, Cost 0.410101, {'classification_error_evaluator': 0.125}
# Test with Pass 0, Cost 0.326659, {'classification_error_evaluator': 0.09470000118017197}
訓(xùn)練之后,檢查模型的預(yù)測準(zhǔn)確度。用 MNIST 訓(xùn)練的時候,一般 softmax回歸模型的分類準(zhǔn)確率為約為 92.34%,多層感知器為97.66%,卷積神經(jīng)網(wǎng)絡(luò)可以達到 99.20%。
可以使用訓(xùn)練好的模型對手寫體數(shù)字圖片進行分類,下面程序展示了如何使用paddle.infer接口進行推斷。
from PIL import Image
import numpy as np
import os
def load_image(file):
im = Image.open(file).convert('L')
im = im.resize((28, 28), Image.ANTIALIAS)
im = np.array(im).astype(np.float32).flatten()
im = im / 255.0
return im
test_data = []
cur_dir = os.path.dirname(os.path.realpath(__file__))
test_data.append((load_image(cur_dir + '/image/infer_3.png'),))
probs = paddle.infer(
output_layer=predict, parameters=parameters, input=test_data)
lab = np.argsort(-probs) # probs and lab are the results of one batch data
print "Label of image/infer_3.png is: %d" % lab[0][0]
本教程的softmax回歸、多層感知器和卷積神經(jīng)網(wǎng)絡(luò)是最基礎(chǔ)的深度學(xué)習(xí)模型,后續(xù)章節(jié)中復(fù)雜的神經(jīng)網(wǎng)絡(luò)都是從它們衍生出來的,因此這幾個模型對之后的學(xué)習(xí)大有裨益。同時,我們也觀察到從最簡單的softmax回歸變換到稍復(fù)雜的卷積神經(jīng)網(wǎng)絡(luò)的時候,MNIST數(shù)據(jù)集上的識別準(zhǔn)確率有了大幅度的提升,原因是卷積層具有局部連接和共享權(quán)重的特性。在之后學(xué)習(xí)新模型的時候,希望大家也要深入到新模型相比原模型帶來效果提升的關(guān)鍵之處。此外,本教程還介紹了PaddlePaddle模型搭建的基本流程,從dataprovider的編寫、網(wǎng)絡(luò)層的構(gòu)建,到最后的訓(xùn)練和預(yù)測。對這個流程熟悉以后,大家就可以用自己的數(shù)據(jù),定義自己的網(wǎng)絡(luò)模型,并完成自己的訓(xùn)練和預(yù)測任務(wù)了。

本教程 由 PaddlePaddle 創(chuàng)作,采用 知識共享 署名-相同方式共享 4.0 國際 許可協(xié)議進行許可。