每周一個機器學習小項目002-卷積網路實現與圖片

每周一個機器學習小項目002-卷積網路實現與圖片

來自專欄 神經網路學習與TensorFlow實踐

1. 推薦閱讀

推薦閱讀時間:42min

推薦閱讀文章:Lecun Y, Bottou L, Bengio Y, et al. Gradient-based learning applied to document recognition[J]. Proceedings of the IEEE, 1998, 86(11):2278-2324.

2. 軟體環境

  • python3
  • numpy
  • matplotlib

3. 前置基礎

  • 導數
  • 矩陣運算

4. 數據描述

  • 數據來源:Yann LeCun文章
  • 數據下載:yann.lecun.com/exdb/mni
  • 數據描述:MNIST(Mixed National Institute of Standards and Technology database)是一個計算機視覺數據集,它包含70000張手寫數字的灰度圖片,其中每一張圖片包含 28 X 28 個像素點。保存形式為長度為784的向量。

5. 理論部分

對於所有的深度神經網路層都分為向前傳播與反向傳播過程。卷積神經網路的一般組件包括:卷積層、池化層、全鏈接層。下面這三個組件的向前、向後傳播過程進行描述:

5.1 卷積層向前傳播

首先需要明確輸入的形式,對於處理圖像問題來說,卷積層輸入形式為[BATCHSIZE, Height, Weight, Chanel],每個[Height, Weight]稱之為一個特徵圖。很多人都喜歡叫的「卷積」神經網路,在實現過程之中只是一個互相關操作,互相關的矩陣稱之為卷積核,其形式為[KernelSize, KernelSize, OldChanel, NewChanel],Kernelsize為卷積核心大小,大的卷積核心可以處理更大範圍的圖像,但是更加難以訓練:

egin{matrix} x^{l+1}_{buvk} = sum_{p=1}^{m} sum_{q=1}^{m} sum_r x^l_{b,i+p,j+q,r} W_{pqrk}\ i=ucdot stride\ j=vcdot stride end{matrix} (1.1)

1.1式描述了卷積過程,W為卷積核心,x^l為輸入,m為卷積核心大小。stride為滑動互相關過程中卷積核心每次移動多少步。

5.2 池化層向前傳播

池化實際上是一個降採樣的過程,說直白一點就是將一個將一個圖像的解析度降低一些。這可以使得需要處理的圖像大小顯著減少,但是更重要的目的在於增加感受野

名詞-感受野:某一層輸出神經元所對應的所有輸入神經元個數。舉個例子,1.1式中m=5則感受野為5,以相同的參數(m=5,stride=1),再次疊加一層此時輸出層的感受野為9。

池化層可以通過在卷積層中選取一個大於1的stride來完成相同的效果。這裡用最大池化(maxpool)作為示例:

egin{matrix} x^{l+1}_{buvk} = max(x^l{b,i:i+p,j:j+q,1:r})\ i=ucdot stride\ j=vcdot stride end{matrix} (1.2)

5.3 卷積層反向傳播

卷積層誤差反向傳播過程:

egin{matrix} e^{l}_{bijr}=frac{partial loss}{partial x^{l}_{bijr}}\ =frac{partial loss}{partial x^{l+1}_{buvk}}frac{partial x^{l+1}_{buvk}}{partial x^{l}_{bijr}}=e^{l-1}_{buvk}cdot (sum_{a+p=i}^{} sum_{b+q=j}^{} sum_r x^l_{b,a+p,b+q,k} W_{pqrk})\ i=ucdot stride\ j=vcdot stride end{matrix} (1.3)

卷積層可訓練參數:

egin{matrix} frac{partial loss}{partial W_{mnqr}}=sum_b sum_{c-i=m} sum_{d-j=m} sum_r e^{l-1}_{buvr}x^{l}_{b,c,d,q} \ i=ucdot stride\ j=vcdot stride end{matrix} (1.4)

5.4 池化層反向傳播

池化層反向傳播過程之中只需知道具體哪一個最大值即可,誤差選擇取得極大值的神經元傳播,其他輸出反向傳播誤差為0。

5.5 展開層反向傳播

卷積神經網路與全鏈接網路之間需要一個展開層過渡,使得矩陣符合卷積、全鏈接的輸入與輸出。因此誤差傳播過程僅為對矩陣進行的變換。

5.6 偏置項導數可訓練參數導數與誤差傳播

加入偏置項與全鏈接類似,此步之中僅需計算導數即可:

frac{partial loss}{partial b_i}=sum_b sum_p sum_q e^l_{bpqi}

6. 代碼部分

6.1 結構分析

可以看到將神經網路拆分成幾個計算層後,每一層都有兩個導數需要計算:反向傳播誤差與訓練參數的導數。每一層又分為兩個部分,第一個部分用於計算正向傳播過程,第二個部分用於計算反向傳播過程。導數與誤差均在反向傳播過程之中計算,相比於全鏈接實踐添加了卷積層:

6.2 卷積層

def _conv2d(self, inputs, filters, par): stride, padding = par B, H, W, C = np.shape(inputs) K, K, C, C2 = np.shape(filters) if padding == "SAME": H2 = int((H-0.1)//stride + 1) W2 = int((W-0.1)//stride + 1) pad_h_2 = K + (H2 - 1) * stride - H pad_w_2 = K + (W2 - 1) * stride - W pad_h_left = int(pad_h_2//2) pad_h_right = int(pad_h_2 - pad_h_left) pad_w_left = int(pad_w_2//2) pad_w_right = int(pad_w_2 - pad_w_left) X = np.pad(inputs, ((0, 0), (pad_h_left, pad_h_right), (pad_w_left, pad_w_right), (0, 0)), constant, constant_values=0) elif padding == "VALID": H2 = int((H - K)//stride + 1) W2 = int((W - K)//stride + 1) X = inputs else: raise "parameter error" out = np.zeros([B, H2, W2, C2]) for itr1 in range(B): for itr2 in range(H2): for itr3 in range(W2): for itrc in range(C2): itrh = itr2 * stride itrw = itr3 * stride out[itr1, itr2, itr3, itrc] = np.sum(X[itr1, itrh:itrh+K, itrw:itrw+K, :] * filters[:,:,:,itrc]) return out def _d_conv2d(self, in_error, n_layer, layer_par=None): stride, padding = self.layer[n_layer][1] inputs = self.outputs[n_layer] filters = self.value[n_layer] B, H, W, C = np.shape(inputs) K, K, C, C2 = np.shape(filters) if padding == "SAME": H2 = int((H-0.1)//stride + 1) W2 = int((W-0.1)//stride + 1) pad_h_2 = K + (H2 - 1) * stride - H pad_w_2 = K + (W2 - 1) * stride - W pad_h_left = int(pad_h_2//2) pad_h_right = int(pad_h_2 - pad_h_left) pad_w_left = int(pad_w_2//2) pad_w_right = int(pad_w_2 - pad_w_left) X = np.pad(inputs, ((0, 0), (pad_h_left, pad_h_right), (pad_w_left, pad_w_right), (0, 0)), constant, constant_values=0) elif padding == "VALID": H2 = int((H - K)//stride + 1) W2 = int((W - K)//stride + 1) X = inputs else: raise "parameter error" error = np.zeros_like(X) for itr1 in range(B): for itr2 in range(H2): for itr3 in range(W2): for itrc in range(C2): itrh = itr2 * stride itrw = itr3 * stride error[itr1, itrh:itrh+K, itrw:itrw+K, :] += in_error[itr1, itr2, itr3, itrc] * filters[:,:,:,itrc] self.d_value[n_layer] = np.zeros_like(self.value[n_layer]) for itr1 in range(B): for itr2 in range(H2): for itr3 in range(W2): for itrc in range(C2): itrh = itr2 * stride itrw = itr3 * stride self.d_value[n_layer][:, :, :, itrc] += in_error[itr1, itr2, itr3, itrc] * X[itr1, itrh:itrh+K, itrw:itrw+K, :] return error[:, pad_h_left:-pad_h_right, pad_w_left:-pad_w_right, :] def conv2d(self, filters, stride, padding="SAME"): self.value.append(filters) self.d_value.append(np.zeros_like(filters)) self.layer.append((self._conv2d, (stride, padding), self._d_conv2d, None)) self.layer_name.append("conv2d")

6.3 偏置項修改

def _bias_add(self, inputs, b, *args, **kw): return inputs + b def _d_bias_add(self, in_error, n_layer, *args, **kw): shape = np.shape(in_error) dv = [] if len(shape) == 2: self.d_value[n_layer] = np.sum(in_error, axis=0) else: dv = np.array([np.sum(in_error[:, :, :, itr]) for itr in range(shape[-1])]) self.d_value[n_layer] = np.squeeze(np.array(dv)) return in_error def bias_add(self, bias, *args, **kw): self.value.append(bias) self.d_value.append(np.zeros_like(bias)) self.layer.append((self._bias_add, None, self._d_bias_add, None)) self.layer_name.append("bias_add")

6.4 展開層

def _flatten(self, X, *args, **kw): B = np.shape(X)[0] return np.reshape(X, [B, -1]) def _d_flatten(self, in_error, n_layer, layer_par): shape = np.shape(self.outputs[n_layer]) return np.reshape(in_error, shape) def flatten(self): self.value.append([]) self.d_value.append([]) self.layer.append((self._flatten, None, self._d_flatten, None)) self.layer_name.append("flatten")

6.5 最大池化層

為了簡便,使用二範數作為loss函數:

def _maxpool(self, X, _, stride, *args, **kw): B, H, W, C = np.shape(X) X_new = np.reshape(X, [B, H//stride, stride, W//stride, stride, C]) return np.max(X_new, axis=(2, 4)) def _d_maxpool(self, in_error, n_layer, layer_par): stride = layer_par X = self.outputs[n_layer] Y = self.outputs[n_layer + 1] expand_y = np.repeat(np.repeat(Y, stride, axis=1), stride, axis=2) expand_e = np.repeat(np.repeat(in_error, stride, axis=1), stride, axis=2) return expand_e * (expand_y == X) def maxpool(self, stride, *args, **kw): self.value.append([]) self.d_value.append([]) self.layer.append((self._maxpool, stride, self._d_maxpool, stride)) self.layer_name.append("maxpool")

6.6 修正線性激活函數

def _relu(self, X, *args, **kw): return (X + np.abs(X))/2. def _d_relu(self, in_error, n_layer, layer_par): X = self.outputs[n_layer] drelu = np.zeros_like(X) drelu[X>0] = 1 return in_error * drelu def relu(self): self.value.append([]) self.d_value.append([]) self.layer.append((self._relu, None, self._d_relu, None)) self.layer_name.append("relu")

6.7 代碼其他部分

代碼與全鏈接層共享代碼,因此不單獨列出。

7. 程序運行

程序運行過程,首先是對網路進行描述:

# 初始化值cw1 = np.random.uniform(-0.1, 0.1, [5, 5, 1, 32])cb1 = np.zeros([32])fw1 = np.random.uniform(-0.1, 0.1, [7 * 7 * 32, 10])fb1 = np.zeros([10])# 建立模型mtd = NN()mtd.conv2d(cw1, 1)mtd.bias_add(cb1)mtd.relu()mtd.maxpool(4)mtd.flatten()mtd.matmul(fw1)mtd.bias_add(fb1)mtd.sigmoid()mtd.loss_square()# 訓練for itr in range(500): ... mtd.fit(inx, iny)

7.1 運行結果

類中加入語句

def model(self): for idx, itr in enumerate(self.layer_name): print("Layer %d: %s"%(idx, itr))

用於輸出模型:

Layer 0: conv2d

Layer 1: bias_add

Layer 2: relu

Layer 3: maxpool

Layer 4: flatten

Layer 5: matmul

Layer 6: bias_add

Layer 7: sigmoid

Layer 8: loss

迭代100次後精度92%。

接下來需要修改什麼

  • 模型太簡單
  • 損失函數並不合適
  • 訓練過程迭代緩慢
  • 其他梯度迭代方法比如Adam引入

推薦閱讀:

過擬合與正則化
深入機器學習系列4-KMeans
Kaggle項目--Titanic生存率預測
機器學習入門筆記4

TAG:機器學習 | 神經網路 | 深度學習DeepLearning |