手寫數字識別[paddle框架]:2.網路結構
網路結構
0.概述
前幾節我們嘗試使用與房價預測相同的簡單神經網路解決手寫數字識別問題,但是效果並不理想。原因是手寫數字識別的輸入是28 × 28的畫素值,輸出是0-9的數字標籤,而線性迴歸模型無法捕捉二維影象資料中蘊含的複雜資訊,如 圖1 所示。無論是牛頓第二定律任務,還是房價預測任務,輸入特徵和輸出預測值之間的關係均可以使用“直線”刻畫(使用線性方程來表達)。但手寫數字識別任務的輸入畫素和輸出數字標籤之間的關係顯然不是線性的,甚至這個關係複雜到我們靠人腦難以直觀理解的程度。
圖1:數字識別任務的輸入和輸入不是線性關係
因此,我們需要嘗試使用其他更復雜、更強大的網路來構建手寫數字識別任務,觀察一下訓練效果,即將“橫縱式”教學法從橫向展開,如 圖2
經典的多層全連線神經網路
和卷積神經網路
。
圖2:“橫縱式”教學法 — 網路結構優化
1.全連線神經網路與卷積神經網路
1.1經典的全連線神經網路
經典的全連線神經網路來包含四層網路:輸入層、兩個隱含層和輸出層,將手寫數字識別任務通過全連線神經網路表示,如 圖3 所示。
圖3:手寫數字識別任務的全連線神經網路結構
- 輸入層:將資料輸入給神經網路。在該任務中,輸入層的尺度為28×28的畫素值。
- 隱含層:增加網路深度和複雜度,隱含層的節點數是可以調整的,節點數越多,神經網路表示能力越強,引數量也會增加。在該任務中,中間的兩個隱含層為10×10的結構,通常隱含層會比輸入層的尺寸小,以便對關鍵資訊做抽象,啟用函式使用常見的sigmoid函式。
- 輸出層:輸出網路計算結果,輸出層的節點數是固定的。如果是迴歸問題,節點數量為需要回歸的數字數量。如果是分類問題,則是分類標籤的數量。在該任務中,模型的輸出是迴歸一個數字,輸出層的尺寸為1。
說明:
隱含層引入非線性啟用函式sigmoid是為了增加神經網路的非線效能力。
舉例來說,如果一個神經網路採用線性變換,有四個輸入\(x_1\)~\(x_4\),一個輸出\(y\)。假設第一層的變換是\(z_1=x_1-x_2\)和\(z_2=x_3+x_4\),第二層的變換是\(y=z_1+z_2\),則將兩層的變換展開後得到\(y=x_1-x_2+x_3+x_4\)。也就是說,無論中間累積了多少層線性變換,原始輸入和最終輸出之間依然是線性關係。
Sigmoid是早期神經網路模型中常見的非線性變換函式,公式為\(Sigmoid(x) = \frac {1}{1-e^{-x}}\),通過如下程式碼,繪製出Sigmoid的函式曲線。如圖4所示。
圖4:sigmoid函式曲線
- 輸入層的尺度為28×28,但批次計算的時候會統一加1個維度(大小為batchsize)。
- 中間的兩個隱含層為10×10的結構,啟用函式使用常見的sigmoid函式。
- 與房價預測模型一樣,模型的輸出是迴歸一個數字,輸出層的尺寸設定成1。
下述程式碼為經典全連線神經網路的實現。完成網路結構定義後,即可訓練神經網路。
# 多層全連線神經網路實現
class MNIST(fluid.dygraph.Layer):
def __init__(self):
super(MNIST, self).__init__()
# 定義兩層全連線隱含層,輸出維度是10,啟用函式為sigmoid
self.fc1 = Linear(input_dim=784, output_dim=10, act='sigmoid') # 隱含層節點為10,可根據任務調整
self.fc2 = Linear(input_dim=10, output_dim=10, act='sigmoid')
# 定義一層全連線輸出層,輸出維度是1,不使用啟用函式
self.fc3 = Linear(input_dim=10, output_dim=1, act=None)
# 定義網路的前向計算
def forward(self, inputs, label=None):
inputs = fluid.layers.reshape(inputs, [inputs.shape[0], 784])
outputs1 = self.fc1(inputs)
outputs2 = self.fc2(outputs1)
outputs_final = self.fc3(outputs2)
return outputs_final
1.2卷積神經網路
雖然使用經典的全連線神經網路可以提升一定的準確率,但對於計算機視覺問題,效果最好的模型仍然是卷積神經網路。卷積神經網路針對視覺問題的特點進行了網路結構優化,更適合處理視覺問題。
卷積神經網路由多個卷積層和池化層組成,如 圖4 所示。卷積層負責對輸入進行掃描以生成更抽象的特徵表示,池化層對這些特徵表示進行過濾,保留最關鍵的特徵資訊。
圖4:在處理計算機視覺任務中大放異彩的卷積神經網路
卷積層(convolutional layer)、池化層(pooling layer)、步長(stride)、邊界填充(pad)
說明:
本節只簡單介紹用卷積神經網路實現手寫數字識別任務,以及它帶來的效果提升。讀者可以將卷積神經網路先簡單的理解成是一種比經典的全連線神經網路更強大的模型即可,更詳細的原理和實現在接下來的第四章-計算機視覺-卷積神經網路基礎中講述。
卷積網路定義
兩層卷積和池化的神經網路實現如下所示。
# 多層卷積神經網路實現
class MNIST(fluid.dygraph.Layer):
def __init__(self):
super(MNIST, self).__init__()
# 定義卷積層,輸出特徵通道num_filters設定為20,卷積核的大小filter_size為5,卷積步長stride=1,padding=2
# 啟用函式使用relu
self.conv1 = Conv2D(num_channels=1, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
# 定義池化層,池化核pool_size=2,池化步長為2,選擇最大池化方式
self.pool1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
# 定義卷積層,輸出特徵通道num_filters設定為20,卷積核的大小filter_size為5,卷積步長stride=1,padding=2
self.conv2 = Conv2D(num_channels=20, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
# 定義池化層,池化核pool_size=2,池化步長為2,選擇最大池化方式
self.pool2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
# 定義一層全連線層,輸出維度是1,不使用啟用函式
self.fc = Linear(input_dim=980, output_dim=1, act=None)
# 定義網路前向計算過程,卷積後緊接著使用池化層,最後使用全連線層計算最終輸出
def forward(self, inputs):
x = self.conv1(inputs)
x = self.pool1(x)
x = self.conv2(x)
x = self.pool2(x)
x = fluid.layers.reshape(x, [x.shape[0], -1])
x = self.fc(x)
return x
模型訓練定義
配置模型訓練函式:EPOCH=20
,batch=100
訓練定義好的卷積網路,程式碼如下:
#網路結構部分之後的程式碼,保持不變
with fluid.dygraph.guard():
model = MNIST()
model.train()
#呼叫載入資料的函式
train_loader = load_data('train')
optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01, parameter_list=model.parameters())
EPOCH_NUM = 20
for epoch_id in range(EPOCH_NUM):
for batch_id, data in enumerate(train_loader()):
#準備資料
image_data, label_data = data
image = fluid.dygraph.to_variable(image_data)
label = fluid.dygraph.to_variable(label_data)
#前向計算的過程
predict = model(image)
#計算損失,取一個批次樣本損失的平均值
loss = fluid.layers.square_error_cost(predict, label)
avg_loss = fluid.layers.mean(loss)
#每訓練了100批次的資料,列印下當前Loss的情況
if batch_id % 200 == 0:
print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
#後向傳播,更新引數的過程
avg_loss.backward()
optimizer.minimize(avg_loss)
model.clear_gradients()
#儲存模型引數
fluid.save_dygraph(model.state_dict(), 'mnist')
訓練輸出節選:
...
...
epoch: 17, batch: 0, loss is: [1.3615658]
epoch: 17, batch: 200, loss is: [1.3933234]
epoch: 17, batch: 400, loss is: [1.0364356]
epoch: 18, batch: 0, loss is: [1.3520143]
epoch: 18, batch: 200, loss is: [1.0927333]
epoch: 18, batch: 400, loss is: [1.2897038]
epoch: 19, batch: 0, loss is: [1.2680488]
epoch: 19, batch: 200, loss is: [1.3827461]
epoch: 19, batch: 400, loss is: [0.90689504]
比較經典全連線神經網路和卷積神經網路的損失變化,可以發現卷積神經網路的損失值下降更快,且最終的損失值更小。
模型測試
利用ACC
來分別評估全連線網路與卷積神經網路模型效能。
評估函式程式碼如下:
# 封裝評估模型函式
def eval_model(model_class, model_file, data):
# 計算準確率
def calculate_ACC(pres, labels):
assert pres.shape == labels.shape,'pres.shape:{} != labels.shape:{} is required when calculat ACC'.format(pres.shape, labels.shape)
count = 0
for i in pres.astype('int32') == labels:
if i:
count += 1
acc = count / len(labels)
return acc
# 定義飛漿動態圖工作環境
imgs, labels = data
with fluid.dygraph.guard():
model = model_class()
model_dict, _ = fluid.load_dygraph(model_file)
# 載入模型引數
model.load_dict(model_dict)
# 設定模型工作模式,灌入測試資料
model.eval()
results = model(fluid.dygraph.to_variable(imgs))
# print('predict results shape:{}'.format(results.numpy().shape))
model_acc = calculate_ACC(results.numpy(), labels)
# print('Acc of model is:{}%'.format(model_acc*100))
return model_acc
# 開始評估
eval_loader = load_data('eval')
acc_list = []
for id, data in enumerate(eval_loader()):
# print('eval images shape is:{}\neval labels shape is:{}'.format(data[0].shape, data[1].shape))
acc_list.append(eval_model(MNIST, 'mnist', data))
# 計算訓練20個epoch後模型的預測準確率
print('Acc of model is:{}%'.format(np.mean(acc_list)*100))
輸出:
loading mnist dataset from ./work/mnist.json.gz ......
Acc of model is:36.669999999999995%
從最終結果來看效果也不是很理想,這是由於影象識別屬於分類問題,目前網路輸出為迴歸後的一個實數。且訓練中使用的均方差損失函式,計算輸出含義是:輸出與實際標籤間的差距,並不適用與分類問題。
因此為了提升準確率,需要選擇合適的損失函式。下節內容即為損失函式的優化。