本節介紹使用飛槳快速實現“手寫數字識別”的建模方法。
與“房價預測”的案例類似,我們以同樣的標准結構實現“手寫數字識別”的建模。在后續的課程中,該標准結構會反復出現,逐漸加深我們對深度學習模型的理解。深度學習模型的標准結構分如下五個步驟:
- 數據處理:讀取數據和預處理操作。
- 模型設計:搭建神經網絡結構。
- 訓練配置:配置優化器、學習率、訓練參數。
- 訓練過程:循環調用訓練過程,循環執行“前向計算 + 損失函數 + 反向傳播”。
- 保存模型並測試:將訓練好的模型保存並評估測試。
下面我們使用飛槳框架,按照五個步驟寫“手寫數字識別”模型,體會下使用飛槳框架的感覺。
在數據處理前,首先要加載飛槳平台、與“手寫數字識別”模型相關類庫,代碼如下:
1 #加載飛槳和相關類庫 2 import paddle 3 import paddle.fluid as fluid 4 from paddle.fluid.dygraph.nn import FC 5 import numpy as np 6 import os 7 from PIL import Image
1. 數據處理
飛槳提供了多個封裝好的數據集API,覆蓋計算機視覺、自然語言處理、推薦系統等多個領域,可以幫助我們快速完成機器學習任務。比如,在“手寫數字識別”模型中,我們可以通過調用paddle.dataset.mnist的train函數和test函數,直接獲取處理好的MNIST訓練集和測試集。
定義數據讀取器
用戶可以通過如下代碼定義數據讀取器:
1 # 如果~/.cache/paddle/dataset/mnist/目錄下沒有MNIST數據,API會自動將MINST數據下載到該文件夾下 2 # 設置數據讀取器,讀取MNIST數據訓練集 3 trainset = paddle.dataset.mnist.train() 4 testset = paddle.dataset.mnist.test() 5 # 包裝數據讀取器,每次讀取的數據數量設置為batch_size=8 6 train_reader = paddle.batch(trainset, batch_size=8) 7 test_reader = paddle.batch(testset,batch_size=8)
讀取數據,並打印觀察
paddle.batch函數將MNIST數據集拆分成多個批次,我們可以用下面的代碼讀取第一個批次的數據內容(因為for循環結尾處有一個break,運行一個循環后就立即退出),並觀察數據結果。
1 # 以迭代的形式讀取數據 2 for batch_id, data in enumerate(train_reader()): 3 # 獲得圖像數據,並轉為float32類型的數組 4 img_data = np.array([x[0] for x in data]).astype('float32') 5 # 獲得圖像標簽數據,並轉為float32類型的數組 6 label_data = np.array([x[1] for x in data]).astype('float32') 7 # 打印數據形狀 8 #print("圖像數據形狀和對應數據為:", img_data.shape, img_data[0]) 9 print("圖像數據形狀和對應數據為:", img_data.shape) 10 print("圖像標簽形狀和對應數據為:", label_data.shape, label_data[0]) 11 break 12 13 print("\n打印第一個batch的第一個圖像,對應標簽數字為{}".format(label_data[0])) 14 # 顯示第一batch的第一個圖像(可以在程序任一個地方引用庫) 15 import matplotlib.pyplot as plt 16 #img=np.array(img_data[0]) #這兩句話效果相同 17 img = np.array(img_data[0]+1)*127.5 18 img = np.reshape(img, [28, 28]).astype(np.uint8) 19 20 plt.figure("Image") # 圖像窗口名稱 21 plt.imshow(img) 22 plt.axis('on') # 關掉坐標軸為 off 23 plt.title('image') # 圖像題目 24 plt.show()
圖像數據形狀和對應數據為: (8, 784) 圖像標簽形狀和對應數據為: (8,) 5.0 打印第一個batch的第一個圖像,對應標簽數字為5.0
上面得到的數據img_data[0]輸出的是一個矩陣,每個數據值在【-1,1】之間。
從代碼的輸出來看,我們從數據加載器train_loader()中讀取一次數據,可以得到形狀為 (8, 784)的圖像數據和形狀為(8,)的標簽數據。其中,8與設置的batch大小對應,784為mnist數據集中每個圖像的像素數量(28*28)。
另外,從打印的圖像數據來看,圖像數據的范圍是[-1, 1],表明這是已經完成圖像歸一化后的圖像數據,且背景部分的值是-1。我們可以將圖像數據反歸一化(就是+1,再乘以127),並使用matplotlib工具包將其顯示出來。顯示的數字是5,和對應標簽數字一致。
說明:飛槳將維度是28*28的手寫數字數據圖像轉成向量形式存儲,因此,使用飛槳數據讀取到的手寫數字圖像是長度為784(28*28)的向量。
2. 模型設計
在“房價預測”深度學習任務中,我們使用了單層且沒有非線性變換的模型,取得了理想的預測效果。在“手寫數字識別”中,我們依然使用這個模型預測輸入的圖形數字值。其中,模型的輸入為784維(28*28)數據,輸出為1維數據,如圖1所示。

1 # 定義mnist數據識別網絡結構,同房價預測網絡 2 class MNIST(fluid.dygraph.Layer): 3 def __init__(self, name_scope): 4 super(MNIST, self).__init__(name_scope) 5 name_scope = self.full_name() 6 # 定義一層全連接層,輸出維度是1,激活函數為None,即不使用激活函數 7 #hidden1=fluid.layers.fc(input=name_scope,size=100,act='relu') 8 #hidden2=FC(input=hidden1,size=100,act='relu') 9 self.fc = FC(name_scope, size=1, act='relu') 10 11 # 定義網絡結構的前向計算過程 12 def forward(self, inputs): 13 outputs = self.fc(inputs) 14 return outputs
3. 訓練配置
訓練配置負責神經網絡訓練前的准備,包括:
- 聲明定義好的模型。
- 加載訓練數據和測試數據。
- 設置優化算法和學習率,本次實驗優化算法使用隨機梯度下降SGD,學習率使用 0.01。
1 # 定義飛槳動態圖工作環境 2 with fluid.dygraph.guard(): 3 # 聲明網絡結構 4 model = MNIST("mnist") 5 # 啟動訓練模式 6 model.train() 7 # 定義數據讀取函數,數據讀取batch_size設置為16 8 train_loader = paddle.batch(paddle.dataset.mnist.train(), batch_size=16) 9 # 定義優化器,使用隨機梯度下降SGD優化器,學習率設置為0.001 10 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001)
上面中train()函數沒有發現在MNIST類中有定義,很是奇怪,需要找到train()函數的原始定義。
4. 訓練過程
完成訓練配置后,可啟動訓練過程。采用二層循環嵌套方式:
- 內層循環負責整個數據集的一次遍歷,遍歷數據集采用分批次(batch)方式。
- 外層循環定義遍歷數據集的次數,本次訓練中外層循環10次,通過參數EPOCH_NUM設置。
1 # 通過with語句創建一個dygraph運行的context, 2 # 動態圖下的一些操作需要在guard下進行 3 with fluid.dygraph.guard(): 4 model = MNIST("mnist") 5 model.train() 6 train_loader = paddle.batch(paddle.dataset.mnist.train(), batch_size=16) 7 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001) 8 EPOCH_NUM = 10 9 for epoch_id in range(EPOCH_NUM): 10 for batch_id, data in enumerate(train_loader()): 11 #准備數據,格式需要轉換成符合框架要求的 12 image_data = np.array([x[0] for x in data]).astype('float32') 13 label_data = np.array([x[1] for x in data]).astype('float32').reshape(-1, 1) 14 # 將數據轉為飛槳動態圖格式 15 image = fluid.dygraph.to_variable(image_data) 16 label = fluid.dygraph.to_variable(label_data) 17 18 #前向計算的過程 19 predict = model(image) 20 21 #計算損失,取一個批次樣本損失的平均值 22 loss = fluid.layers.square_error_cost(predict, label) 23 avg_loss = fluid.layers.mean(loss) 24 25 #每訓練了1000批次的數據,打印下當前Loss的情況 26 if batch_id !=0 and batch_id % 1000 == 0: 27 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 28 29 #后向傳播,更新參數的過程 30 avg_loss.backward() 31 optimizer.minimize(avg_loss) 32 model.clear_gradients() 33 34 # 保存模型 35 fluid.save_dygraph(model.state_dict(), 'mnist')
epoch: 0, batch: 1000, loss is: [1.8736392] epoch: 0, batch: 2000, loss is: [4.0054626] epoch: 0, batch: 3000, loss is: [3.7705934] epoch: 1, batch: 1000, loss is: [1.8645047] epoch: 1, batch: 2000, loss is: [3.8951108] epoch: 1, batch: 3000, loss is: [3.5067868] epoch: 2, batch: 1000, loss is: [1.8366505] epoch: 2, batch: 2000, loss is: [3.778401] epoch: 2, batch: 3000, loss is: [3.4168165] epoch: 3, batch: 1000, loss is: [1.8329564] epoch: 3, batch: 2000, loss is: [3.7081861] epoch: 3, batch: 3000, loss is: [3.3437557] epoch: 4, batch: 1000, loss is: [1.8373424] epoch: 4, batch: 2000, loss is: [3.6615422] epoch: 4, batch: 3000, loss is: [3.2822015] epoch: 5, batch: 1000, loss is: [1.8455799] epoch: 5, batch: 2000, loss is: [3.6457105] epoch: 5, batch: 3000, loss is: [3.2266264] epoch: 6, batch: 1000, loss is: [1.8546844] epoch: 6, batch: 2000, loss is: [3.6325989] epoch: 6, batch: 3000, loss is: [3.1794245] epoch: 7, batch: 1000, loss is: [1.863614] epoch: 7, batch: 2000, loss is: [3.6269343] epoch: 7, batch: 3000, loss is: [3.129442] epoch: 8, batch: 1000, loss is: [1.8726021] epoch: 8, batch: 2000, loss is: [3.6225967] epoch: 8, batch: 3000, loss is: [3.0918531] epoch: 9, batch: 1000, loss is: [1.880715] epoch: 9, batch: 2000, loss is: [3.6216624] epoch: 9, batch: 3000, loss is: [3.0604477]
上面為訓練的結果輸出。
同樣的問題:predict = model(image)這句中,沒有調用model的forward()函數就完成了前向計算,也理解不了。
通過觀察訓練過程中損失所發生的變化,可以發現雖然損失整體上在降低,但到訓練的最后一輪,損失函數值依然較高。可以猜測,“手寫數字識別”完全復用“房價預測”的代碼,訓練效果並不好。接下來我們通過模型測試,獲取模型訓練的真實效果。
5. 模型測試
模型測試的主要目的是驗證訓練好的模型是否能正確識別出數字。測試模型包括以下三步:
- 從'./demo/example_0.jpg'目錄下讀取樣例圖片。
- 加載模型並將模型的狀態設置為校驗狀態(eval),顯式告訴框架我們接下來只會使用前向計算的流程,不會計算梯度和梯度反向傳播,這將減少內存的消耗。
- 將測試樣本傳入模型,獲取預測結果,取整后作為預測標簽輸出。
1 # 導入圖像讀取第三方庫 2 import matplotlib.image as mpimg 3 import matplotlib.pyplot as plt 4 # 讀取圖像 5 example = mpimg.imread('./work/example_0.png') 6 # 顯示圖像 7 plt.imshow(example)

1 # 讀取一張本地的樣例圖片,轉變成模型輸入的格式 2 def load_image(img_path): 3 # 從img_path中讀取圖像,並轉為灰度圖 4 im = Image.open(img_path).convert('L') 5 print(np.array(im)) 6 im = im.resize((28, 28), Image.ANTIALIAS) 7 im = np.array(im).reshape(1, -1).astype(np.float32) 8 # 圖像歸一化,保持和數據集的數據范圍一致 9 im = 2 - im / 127.5 10 return im 11 12 # 定義預測過程 13 with fluid.dygraph.guard(): 14 model = MNIST("mnist") 15 params_file_path = 'mnist' 16 img_path = './work/example_0.png' 17 # 加載模型參數 18 model_dict, _ = fluid.load_dygraph("mnist") 19 model.load_dict(model_dict) 20 21 model.eval() 22 tensor_img = load_image(img_path) 23 result = model(fluid.dygraph.to_variable(tensor_img)) 24 #預測輸出取整,即為預測的數字 25 print("本次預測的數字是", result.numpy().astype('int32'))
[[255 255 255 ... 255 255 255] [255 255 255 ... 255 255 255] [255 255 255 ... 255 255 255] ... [255 255 255 ... 255 255 255] [255 255 255 ... 255 255 255] [255 255 255 ... 255 255 255]]
本次預測的數字是 [[1]]
model.load_dict()是加載模型,model.eval()是去掉訓練過程(也就是沒有了反向計算過程),只是測試。
如上可見,模型錯誤預測樣例圖片中的數字是1,實際應該預測的結果是0。但如果我們嘗試更多的樣本,會發現很多數字圖片識別結果是錯誤的,完全復用房價預測的實驗並不適用於手寫數字識別任務,接下來我們會對該實驗進行逐一改進,直到獲得令人滿意的結果。
