前面使用與房價預測相同的簡單神經網絡解決手寫數字識別問題,效果並不理想,原因有兩點:
- 輸入數據類型不同。房價預測的輸入為離散一維數據。房價預測使用全連接神經網絡無法學習到圖像二維數據中的空間信息。
- 模型復雜度不夠。因為手寫數字識別任務涉及到圖像信號,比房價預測任務更加復雜,模型的復雜度也會影響最終的效果,理論上復雜的模型能夠表示更復雜的轉換關系(從輸入到輸出)。
本節介紹兩種常見的網絡結構,全連接神經網絡和卷積神經網絡,觀測卷積網絡能否提升手寫數字識別的訓練效果。
在開始介紹網絡結構前,復用上一節的數據處理代碼,代碼如下。
1 #數據處理部分之前的代碼,保持不變 2 import os 3 import random 4 import paddle 5 import paddle.fluid as fluid 6 from paddle.fluid.dygraph.nn import Conv2D, Pool2D, FC 7 import numpy as np 8 import matplotlib.pyplot as plt 9 from PIL import Image 10 11 import gzip 12 import json 13 14 # 定義數據集讀取器 15 def load_data(mode='train'): 16 17 # 數據文件 18 datafile = './work/mnist.json.gz' 19 print('loading mnist dataset from {} ......'.format(datafile)) 20 data = json.load(gzip.open(datafile)) 21 train_set, val_set, eval_set = data 22 23 # 數據集相關參數,圖片高度IMG_ROWS, 圖片寬度IMG_COLS 24 IMG_ROWS = 28 25 IMG_COLS = 28 26 27 if mode == 'train': 28 imgs = train_set[0] 29 labels = train_set[1] 30 elif mode == 'valid': 31 imgs = val_set[0] 32 labels = val_set[1] 33 elif mode == 'eval': 34 imgs = eval_set[0] 35 labels = eval_set[1] 36 37 imgs_length = len(imgs) 38 39 assert len(imgs) == len(labels), \ 40 "length of train_imgs({}) should be the same as train_labels({})".format( 41 len(imgs), len(labels)) 42 43 index_list = list(range(imgs_length)) 44 45 # 讀入數據時用到的batchsize 46 BATCHSIZE = 100 47 48 # 定義數據生成器 49 def data_generator(): 50 if mode == 'train': 51 random.shuffle(index_list) 52 imgs_list = [] 53 labels_list = [] 54 for i in index_list: 55 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') 56 label = np.reshape(labels[i], [1]).astype('float32') 57 imgs_list.append(img) 58 labels_list.append(label) 59 if len(imgs_list) == BATCHSIZE: 60 yield np.array(imgs_list), np.array(labels_list) 61 imgs_list = [] 62 labels_list = [] 63 64 # 如果剩余數據的數目小於BATCHSIZE, 65 # 則剩余數據一起構成一個大小為len(imgs_list)的mini-batch 66 if len(imgs_list) > 0: 67 yield np.array(imgs_list), np.array(labels_list) 68 69 return data_generator
數據處理部分結構不變。
1. 經典全連接神經網絡
經典的全連接神經網絡含有四層網絡:兩個隱含層,輸入層和輸出層。
- 輸入層。准備數據,輸入給神經網絡。
- 隱含層。增加網絡深度和復雜度,隱含層的節點數是可以調整的。節點數越多,神經網絡表示能力越強,但同時參數量也會增加。
- 輸出層。輸出網絡計算結果,輸出層的節點數是固定的,比如手寫數字識別有0-9十個數字,輸出標簽有10個,所以輸出層必須是10個節點。
說明:
隱含層引入非線性激活函數sigmoid是為了增加神經網絡的非線性能力,舉例來說,一個神經網絡有四個輸入x1~x4,一個輸出y,采用線性變換。假設第一層的變換是z1=x1-x2和z2=x3+x4,第二層的變換是y=z1+z2。將兩層的變換展開后得到 y=x1-x2+x3+x4。原始輸入和最終輸出之間依然是線性關系,無論中間累積了多少層線性變換均是如此。
Sigmoid的函數曲線如下圖所示,是早期神經網絡模型中常見的非線性變換函數。
1 def sigmoid(x): 2 # 直接返回sigmoid函數 3 return 1. / (1. + np.exp(-x)) 4 5 # param:起點,終點,間距 6 x = np.arange(-8, 8, 0.2) 7 y = sigmoid(x) 8 plt.plot(x, y) 9 plt.show()

下述代碼為經典全連接神經網絡的實現。完成網絡結構定義后,即可訓練神經網絡。
1 # 多層全連接神經網絡實現 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 # 定義兩層全連接隱含層,輸出維度是10,激活函數為sigmoid 7 self.fc1 = FC(name_scope, size=10, act='sigmoid') # 隱含層節點為10,可根據任務調整 8 self.fc2 = FC(name_scope, size=10, act='sigmoid') 9 # 定義一層全連接輸出層,輸出維度是1,不使用激活函數 10 self.fc3 = FC(name_scope, size=1, act=None) 11 12 # 定義網絡的前向計算 13 def forward(self, inputs, label=None): 14 outputs1 = self.fc1(inputs) 15 outputs2 = self.fc2(outputs1) 16 outputs_final = self.fc3(outputs2) 17 return outputs_final
訓練部分與原來的一樣,保持不變。
1 #網絡結構部分之后的代碼,保持不變 2 with fluid.dygraph.guard(): 3 model = MNIST("mnist") 4 model.train() 5 #調用加載數據的函數,獲得MNIST訓練數據集 6 train_loader = load_data('train') 7 # 使用SGD優化器,learning_rate設置為0.01 8 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) 9 # 訓練5輪 10 EPOCH_NUM = 5 11 for epoch_id in range(EPOCH_NUM): 12 for batch_id, data in enumerate(train_loader()): 13 #准備數據 14 image_data, label_data = data 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 #每訓練了200批次的數據,打印下當前Loss的情況 26 if batch_id % 200 == 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')
同樣的疑問,前向計算過程是這樣完成的: predict=model(image),沒有調用forward函數?
下面為訓練后的輸出結果:
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [22.33305] epoch: 0, batch: 200, loss is: [4.7322707] epoch: 0, batch: 400, loss is: [3.3345733] epoch: 1, batch: 0, loss is: [3.40572] epoch: 1, batch: 200, loss is: [3.1861389] epoch: 1, batch: 400, loss is: [3.113542] epoch: 2, batch: 0, loss is: [3.364307] epoch: 2, batch: 200, loss is: [3.6445987] epoch: 2, batch: 400, loss is: [2.8337402] epoch: 3, batch: 0, loss is: [2.29281] epoch: 3, batch: 200, loss is: [2.0806828] epoch: 3, batch: 400, loss is: [1.7476844] epoch: 4, batch: 0, loss is: [2.8141792] epoch: 4, batch: 200, loss is: [2.8806489] epoch: 4, batch: 400, loss is: [2.480993]
總的來講,loss有下降趨勢。
2.卷積神經網絡
雖然使用經典的神經網絡可以達到一定的准確率,但對於計算機視覺問題,效果最好的模型是卷積神經網絡。卷積神經網絡針對視覺問題的特點優化了網絡結構,更適合處理視覺問題。卷積神經網絡的原理會在第四章詳述,在這里只展示如何將模型替換成卷積神經網絡,以及它帶來的效果提升。
卷積神經網絡由多個卷積層和池化層組合而成,卷積層負責對輸入進行掃描以生成更抽象的特征表示,而池化層則對這些特征表示進行過濾,保留最關鍵的信息。下面是卷積神經網絡的實現,卷積和池化函數會在第四章詳述,這里僅理解成一種比經典神經網絡更強大的模型即可。
1 # 多層卷積神經網絡實現 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 # 定義卷積層,輸出特征通道num_filters設置為20,卷積核的大小filter_size為5,卷積步長stride=1,padding=2 7 # 激活函數使用relu 8 self.conv1 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 9 # 定義池化層,池化核pool_size=2,池化步長為2,選擇最大池化方式 10 self.pool1 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 11 # 定義卷積層,輸出特征通道num_filters設置為20,卷積核的大小filter_size為5,卷積步長stride=1,padding=2 12 self.conv2 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 13 # 定義池化層,池化核pool_size=2,池化步長為2,選擇最大池化方式 14 self.pool2 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 15 # 定義一層全連接層,輸出維度是1,不使用激活函數 16 self.fc = FC(name_scope, size=1, act=None) 17 18 # 定義網絡前向計算過程,卷積后緊接着使用池化層,最后使用全連接層計算最終輸出 19 def forward(self, inputs): 20 x = self.conv1(inputs) 21 x = self.pool1(x) 22 x = self.conv2(x) 23 x = self.pool2(x) 24 x = self.fc(x) 25 return x
接下來訓練定義好的卷積神經網絡,比較兩個模型的損失變化(經典神經網絡和卷積神經網絡),可以發現卷積神經網絡的損失值下降更快,且最終的損失值更小。
1 #網絡結構部分之后的代碼,保持不變 2 with fluid.dygraph.guard(): 3 model = MNIST("mnist") 4 model.train() 5 #調用加載數據的函數 6 train_loader = load_data('train') 7 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) 8 EPOCH_NUM = 5 9 for epoch_id in range(EPOCH_NUM): 10 for batch_id, data in enumerate(train_loader()): 11 #准備數據 12 image_data, label_data = data 13 image = fluid.dygraph.to_variable(image_data) 14 label = fluid.dygraph.to_variable(label_data) 15 16 #前向計算的過程 17 predict = model(image) 18 19 #計算損失,取一個批次樣本損失的平均值 20 loss = fluid.layers.square_error_cost(predict, label) 21 avg_loss = fluid.layers.mean(loss) 22 23 #每訓練了100批次的數據,打印下當前Loss的情況 24 if batch_id % 200 == 0: 25 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 26 27 #后向傳播,更新參數的過程 28 avg_loss.backward() 29 optimizer.minimize(avg_loss) 30 model.clear_gradients() 31 32 #保存模型參數 33 fluid.save_dygraph(model.state_dict(), 'mnist')
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [26.78181] epoch: 0, batch: 200, loss is: [9.082592] epoch: 0, batch: 400, loss is: [4.95523] epoch: 1, batch: 0, loss is: [3.5062227] epoch: 1, batch: 200, loss is: [2.9835818] epoch: 1, batch: 400, loss is: [3.0593839] epoch: 2, batch: 0, loss is: [2.8837323] epoch: 2, batch: 200, loss is: [2.5961332] epoch: 2, batch: 400, loss is: [1.5832586] epoch: 3, batch: 0, loss is: [1.398091] epoch: 3, batch: 200, loss is: [1.9496399] epoch: 3, batch: 400, loss is: [0.97344035] epoch: 4, batch: 0, loss is: [1.6800342] epoch: 4, batch: 200, loss is: [1.0535694] epoch: 4, batch: 400, loss is: [1.2734855]
訓練過程的代碼與上面的全連接神經網絡的相同。從數據來看,卷積神經網絡loss下降更快。
