神經網路學習筆記7
C7 卷積神經網路
CNN中新增了Convolution層和Pooling層,其連線順序是:Convolution-Relu-Pooling,靠近輸出層使用Affine-ReLU組合
①全連線:相鄰層的所有神經元之間都有連線
②卷積層:以多維的資料形式接收輸入資料,其輸入輸出資料稱為“特徵圖”
卷積運算:相當於影象處理中的濾波器運算;對於輸入資料,以一定間隔滑動濾波器視窗進行運算,將各個位置上過濾的元素和輸入的對應元素相乘,再求和,把結果儲存到輸入的對應位置(乘積累加運算)
偏置:在應用了濾波器的資料上加上偏置
填充:向資料周圍填入固定資料,主要是為了調整輸出的大小。如果每次進行卷積運算都會縮小空間,則在某個時刻輸出大小就可能變為1,導致無法再應用卷積運算。為避免這樣的情況,就要使用填充。
假設輸入大小為(H,W),濾波器大小為(FH,FW),輸出大小為(OH,OW),填充為P,步幅為S
輸出大小計算公式:
OH=(H+2P-FH)/S+1
OW=(W+2P-FW)/S+1
多維資料的卷積運算:對於多通道方向資料,當通道方向上有多個特徵圖時,會按照通道進行輸入資料和濾波器的卷積運算,並將結果相加從而得到輸出。輸入資料和濾波器的通道數要設定為相同的值。
批處理:對神經網路的處理中進行輸入資料的打包處理,能夠實現處理的高效化和學習時對mini-batch的對應。
在卷積運算中同樣對應批處理,需要將各層間傳遞的資料儲存為4維資料,按照(batch_num,channel,height,width)順序儲存資料。批處理將n次處理彙總成1次進行。
池化層特徵:沒有要學習的引數,通道數不發生變化,對於微小的位置變化具有魯棒性。
卷積層與池化層實現
1、四維陣列:對於CNN中各層間傳遞的四維資料,使用python實現
x=np.random.rand(10,1,28,28)
訪問第一個資料:x[0]
x[0].shape #(1,28,28)
2、基於im2col的展開
如果老老實實地實現卷積運算,估計要重複好幾層的for語句,這樣的實現比較麻煩。而且NumPy中存在使用for語句後處理變慢的缺點。使用im2col這個不便利的函式進行簡單實現。
im2col函式可以將輸入資料展開以是和濾波器,對三維的輸入資料應用im2col後,資料轉換為二維矩陣
使用im2col展開資料後,就只需將卷積層的濾波器縱向展開為1列,並且計算2個矩陣的乘積即可。與全連線層Affine層進行處理基本相同。
※卷積層實現
初始化方法將濾波器(權重)、偏置、步幅、填充作為引數接收。濾波器是(FN,C,FH,FW)的4維形狀。
使用im2col展開輸入資料,並用reshape將濾波器展開為二維陣列。然後計算展開後的矩陣的乘積。
其中,通過使用reshape(FN,-1),將引數指定為-1,reshape函式會自動計算-1維度上的元素個數,以使得多維陣列元素個數前後一致
transpose:更改多維陣列的軸的順序,將輸出大小轉換為合適的形狀。
卷積層進行反向轉播時,必須進行im2col的逆處理
class Convolution: def __init__(self, W, b, stride=1, pad=0): self.W = W self.b = b self.stride = stride self.pad = pad # 中間資料(backward時使用) self.x = None self.col = None self.col_W = None # 權重和偏置引數的梯度 self.dW = None self.db = None def forward(self, x): FN, C, FH, FW = self.W.shape N, C, H, W = x.shape out_h = 1 + int((H + 2*self.pad - FH) / self.stride) out_w = 1 + int((W + 2*self.pad - FW) / self.stride) col = im2col(x, FH, FW, self.stride, self.pad) col_W = self.W.reshape(FN, -1).T out = np.dot(col, col_W) + self.b out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) self.x = x self.col = col self.col_W = col_W return out def backward(self, dout): FN, C, FH, FW = self.W.shape dout = dout.transpose(0,2,3,1).reshape(-1, FN) self.db = np.sum(dout, axis=0) self.dW = np.dot(self.col.T, dout) self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW) dcol = np.dot(dout, self.col_W.T) dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad) return dx
※池化層實現
一樣使用im2col展開輸入資料,不過池化層中通道方向是獨立的,池化的應用區域按照通道單獨展開。
進行階段:
①展開輸入資料,②求各行的最大值,③轉換為合適的輸出大小
class Pooling: def __init__(self, pool_h, pool_w, stride=1, pad=0): self.pool_h = pool_h self.pool_w = pool_w self.stride = stride self.pad = pad self.x = None self.arg_max = None def forward(self, x): N, C, H, W = x.shape out_h = int(1 + (H - self.pool_h) / self.stride) out_w = int(1 + (W - self.pool_w) / self.stride) col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) col = col.reshape(-1, self.pool_h*self.pool_w) arg_max = np.argmax(col, axis=1) out = np.max(col, axis=1) out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2) self.x = x self.arg_max = arg_max return out def backward(self, dout): dout = dout.transpose(0, 2, 3, 1) pool_size = self.pool_h * self.pool_w dmax = np.zeros((dout.size, pool_size)) dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten() dmax = dmax.reshape(dout.shape + (pool_size,)) dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1) dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad) return dx
CNN的實現:
引數解釋
①input_dim:輸入資料的維度(通道,高,長)
②conv_param:卷積層的超引數(字典),字典的關鍵字如下:
filter_num:濾波器的數量
filter_size:濾波器的大小
stride:步幅,
pad:填充
③hidden_size:隱藏層(全連線)的神經元數量
④output_size:輸出層(全連線)的神經元數量
⑤weight_int_std:初始化時權重的標準差
將由初始化引數傳入的卷積層出號引數從字典中取了出來,計算卷積層輸出大小,接下來是權重引數的初始化部分。
學習所需的引數是第一層的卷積層和剩餘兩個全連線的權重和偏置。這些引數儲存在例項變數中params字典中。將第一層的卷積層權重設為關鍵字w1,偏置設定為關鍵字b1,分別使用關鍵字w2,b2和關鍵字w3,b3儲存第二個和第三個全連線的權重與偏置。
然後按照順序生成必要的層,向有序字典(orderedDict)的layers中新增層,只有最後的softmaxWithLoss層被新增到別的變數lastLayer中。
推理:predict方法,從頭開始依次呼叫已經新增的層,將結果傳遞給下一個層。
在求損失函式函式的loss方法中,除了使用predict方法進行的forward處理以外,還會繼續進行forward處理,知道到達最後的softmaxWithLoss層
接下來是基於誤差反差傳播法求梯度的程式碼實現
引數的梯度通過誤差反向傳播法(反向傳播)求出,通過把正向傳播和反向傳播組裝在一起完成。因為已經再各層中正確實現了正向傳播和反向傳播功能,這裡只需要以合適的順序呼叫即可。最後把各個權重引數梯度儲存到grads字典中。
# coding: utf-8 import sys, os sys.path.append(os.pardir) # 為了匯入父目錄的檔案而進行的設定 import pickle import numpy as np from collections import OrderedDict from common.layers import * from common.gradient import numerical_gradient class SimpleConvNet: """簡單的ConvNet conv - relu - pool - affine - relu - affine - softmax Parameters ---------- input_size : 輸入大小(MNIST的情況下為784) hidden_size_list : 隱藏層的神經元數量的列表(e.g. [100, 100, 100]) output_size : 輸出大小(MNIST的情況下為10) activation : 'relu' or 'sigmoid' weight_init_std : 指定權重的標準差(e.g. 0.01) 指定'relu'或'he'的情況下設定“He的初始值” 指定'sigmoid'或'xavier'的情況下設定“Xavier的初始值” """ def __init__(self, input_dim=(1, 28, 28), conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size=100, output_size=10, weight_init_std=0.01): filter_num = conv_param['filter_num'] filter_size = conv_param['filter_size'] filter_pad = conv_param['pad'] filter_stride = conv_param['stride'] input_size = input_dim[1] conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1 pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2)) # 初始化權重 self.params = {} self.params['W1'] = weight_init_std * \ np.random.randn(filter_num, input_dim[0], filter_size, filter_size) self.params['b1'] = np.zeros(filter_num) self.params['W2'] = weight_init_std * \ np.random.randn(pool_output_size, hidden_size) self.params['b2'] = np.zeros(hidden_size) self.params['W3'] = weight_init_std * \ np.random.randn(hidden_size, output_size) self.params['b3'] = np.zeros(output_size) # 生成層 self.layers = OrderedDict() self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'], conv_param['stride'], conv_param['pad']) self.layers['Relu1'] = Relu() self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2) self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2']) self.layers['Relu2'] = Relu() self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3']) self.last_layer = SoftmaxWithLoss() def predict(self, x): for layer in self.layers.values(): x = layer.forward(x) return x def loss(self, x, t): """求損失函式 引數x是輸入資料、t是教師標籤 """ y = self.predict(x) return self.last_layer.forward(y, t) def accuracy(self, x, t, batch_size=100): if t.ndim != 1 : t = np.argmax(t, axis=1) acc = 0.0 for i in range(int(x.shape[0] / batch_size)): tx = x[i*batch_size:(i+1)*batch_size] tt = t[i*batch_size:(i+1)*batch_size] y = self.predict(tx) y = np.argmax(y, axis=1) acc += np.sum(y == tt) return acc / x.shape[0] def numerical_gradient(self, x, t): """求梯度(數值微分) Parameters ---------- x : 輸入資料 t : 教師標籤 Returns ------- 具有各層的梯度的字典變數 grads['W1']、grads['W2']、...是各層的權重 grads['b1']、grads['b2']、...是各層的偏置 """ loss_w = lambda w: self.loss(x, t) grads = {} for idx in (1, 2, 3): grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)]) grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)]) return grads def gradient(self, x, t): """求梯度(誤差反向傳播法) Parameters ---------- x : 輸入資料 t : 教師標籤 Returns ------- 具有各層的梯度的字典變數 grads['W1']、grads['W2']、...是各層的權重 grads['b1']、grads['b2']、...是各層的偏置 """ # forward self.loss(x, t) # backward dout = 1 dout = self.last_layer.backward(dout) layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) # 設定 grads = {} grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db return grads def save_params(self, file_name="params.pkl"): params = {} for key, val in self.params.items(): params[key] = val with open(file_name, 'wb') as f: pickle.dump(params, f) def load_params(self, file_name="params.pkl"): with open(file_name, 'rb') as f: params = pickle.load(f) for key, val in params.items(): self.params[key] = val for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']): self.layers[key].W = self.params['W' + str(i+1)] self.layers[key].b = self.params['b' + str(i+1)]
CNN的視覺化
學習前的濾波器是隨機進行初始化的 所以在黑白的濃淡上沒有規律可循,但學習後的濾波器變成了有規律的影象。對於有規律的濾波器是在觀察邊緣和斑塊,會對圖中對應方向的邊緣有響應。
卷積層的濾波器會提取邊緣或者斑塊等原始資訊,剛才實現的CNN會將這些原始資訊傳遞給後面的層
具有代表性的CNN
1、LeNet
具有連續的卷積層和池化層,最後經過全連線層輸出結果。使用sigmoid函式,原始的LeNet中使用子取樣縮小中間資料的大小。
2、AlexNet
有多個卷積層和池化層,最後經由全連線層輸出結果。啟用函式使用ReLU,使用進行區域性正規化的LRN,使用Dropout層