Python深度學習入門筆記 4 CNN
卷積神經網路
這一部分的主題是卷積神經網路CNN,
網路結構
CNN和之前介紹的神經網路一樣,也是一層一層構建,不過,CNN中新出現了卷積層(Convolution層)和池化層(Pooling層),
在之前介紹的神經網路中,相鄰層的所有神經元之間都有連接,這稱為全連接(fully-connected),下圖是基于全連接層(Affine層)的網路的例子:

而CNN的結構通常是:

在CNN中,層的連接順序是“Convolution-ReLU-(Pooling)”(Pooling 層有時會被省略),這可以理解為之前的“Affine-ReLU”連接被替換成了“Convolution-ReLU-(Pooling)”連接,不過,靠近輸出層的部分還是使用了之前的“Affine-ReLU”,輸出層也使用的是“Affine-Softmax”
卷積層
全連接層有什么問題?
全連接層存在一個問題:資料的形狀被“忽視”了,例如,輸入圖片時,通常的圖片資料都是“高、長、通道“方向上的三維資料,但是,向全連接層輸入時,需要將3維資料拉平為1維資料,
在影像的這個三維資料中,存在著重要的空間資訊,比如,空間上鄰近的像素為相似的值、RBG的各個通道之間分別有密切的關聯性、相距較遠的像素之間沒有什么關聯等,3維形狀中可能隱藏有值得提取的本質模式,因為全連接層會忽視形狀,將全部的輸入資料作為相同的神經元(同一維度的神經元)處理,所以無法利用與形狀相關的資訊,
而卷積層可以保持形狀不變,當輸入資料是影像時,卷積層會以 3 維資料的形式接收輸入資料,并同樣以3 維資料的形式輸出至下一層,因此,在CNN中,可以(有可能)正確理解影像等具有形狀的資料,
在CNN 中,有時將卷積層的輸入輸出資料稱為特征圖(feature map)如,輸入特征圖,輸出特征圖,后續“輸入輸出資料”和“特征圖”將表示同樣的意思,
關于后續會涉及一些數字影像處理中的名詞和知識,我之前的分享中有詳細的整理,數字影像處理筆記,
傳統數字影像處理和基于深度學習的影像處理之間最主要的差別在于:傳統數字影像處理中用到的濾波器是固定的,比較通用的,或者是根據經驗研究已經確定好的;而基于深度學習可以“學習出”針對當前場景最合適的濾波器,除此之外,對于影像的處理操作,二者是沒有什么差別的,
簡化一下就如下圖所示:

卷積運算
卷積層進行的處理就是卷積運算,卷積運算相當于影像處理中的“濾波器運算”,“濾波器”也叫作“核”,
一個輸入資料大小是(4, 4),濾波器大小是(3, 3),最終輸出大小的(2, 2)的例子:

對于輸入資料,卷積運算以一定間隔滑動濾波器的視窗并應用,將各個位置上濾波器的元素和輸入的對應元素相乘,然后再求和(有時將這個計算稱為乘積累加運算),然后,將這個結果保存到輸出的對應位置,將這個程序在所有位置都進行一遍,就可以得到卷積運算的輸出,

CNN中,濾波器的引數就對應之前的權重,并且,CNN中也存在偏置,偏置通常只有1個,這個值會被加到應用了濾波器的所有元素上,

填充
可以看到的是,經過濾波之后,影像比之前“少了一圈”,那是因為影像最外層的元素沒有完整的四周元素與濾波器對應相乘,我們為了是邊緣元素不會丟失,就采用了填充,
在進行卷積層的處理之前,有時要向輸入資料的周圍填入固定的資料(比
如0等),這稱為填充(padding),

卷積運算的填充處理:向輸入資料的周圍填入0(圖中用虛線表示填充,0沒有顯示出來)
步幅
應用濾波器的位置間隔稱為步幅(stride),如果將步幅設為2,則如下圖所示,應用濾波器的視窗的間隔變為2個元素,

綜上,增大步幅后,輸出大小會變小,而增大填充后,輸出大小會變大,那么如何通過二者與輸入大小計算出輸出大小呢?
假設輸入大小為(H, W),濾波器大小為(FH, FW),輸出大小為(OH, OW),填充為P,步幅為S,那么輸入大小就是:

注意式子中的除法,當輸出大小無法除盡時(結果是小數時),需要采取報錯等對策,
3維資料的卷積運算
我們來看一下對加上了通道方向的3維資料進行卷積運算的例子,

注意:在3維資料的卷積運算中,濾波器的通道數只能設定為和輸入資料的通道數相同的值,
到這里,我們又發現一個問題:三維資料經過一個濾波器濾波后變成了二維資料,

怎么解決呢?**用多個濾波器(權重)**就可以了,

通過應用FN個濾波器,輸出特征圖也生成了FN個,如果將這FN個特征圖匯集在一起,就得到了形狀為(FN, OH, OW)的方塊,將這個方塊傳給下一層,就是CNN的處理流,
每個通道有一個偏置,因此,偏置的形狀是 (FN, 1, 1),

批處理
神經網路的處理中進行了將輸入資料打包的批處理,如果我們希望卷積運算也同樣對應批處理,就需要將在各層間傳遞的資料保存為4維資料(一批三維資料),具體地講,就是按(batch_num, channel, height, width)的順序保存資料,

池化層
池化是縮小高、長方向上的空間的運算,在影像識別領域,主要使用Max池化,也就是說選取一個目標區域里的最大值,
下面例子是按步幅2進行2 × 2的Max池化時的處理順序:

池化層的特征:
- 沒有要學習的引數,它的作業很簡單只是從目標區域中取最大值(或者平均值),所以不存在要學習的引數,
- 通道數不發生變化,每個通道單獨池化,
- 對微小的位置變化具有魯棒性(健壯),
卷積層和池化層的實作
我們將用Python來實作這兩個層,不過在開始之前我們還要解決一些小問題,
基于im2col的展開
CNN中處理的是4維資料,因此卷積運算的實作看上去會很復雜,但是通過使用im2col,問題就會變得很簡單,
如果老老實實地實作卷積運算,估計要重復好幾層的for陳述句,這樣的實作有點麻煩,而且,NumPy中存在使用for陳述句后處理變慢的缺點(NumPy中,訪問元素時最好不要用for陳述句)
im2col(image to column)是一個函式,將輸入資料展開以適合濾波器(權重),

在上圖中,為了便于觀察,將步幅設定得很大,以使濾波器的應用區域不重疊,而在實際的卷積運算中,濾波器的應用區域幾乎都是重疊的,在濾波器的應用區域重疊的情況下,使用im2col展開后,展開后的元素個數會多于原方塊的元素個數,因此,使用im2col的實作存在比普通的實作消耗更多記憶體的缺點,但是,匯總成一個大的矩陣進行計算,對計算機的計算頗有益處,
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
"""
Parameters
----------
input_data : 由(資料量, 通道, 高, 長)的4維陣列構成的輸入資料
filter_h : 濾波器的高
filter_w : 濾波器的長
stride : 步幅
pad : 填充
Returns
-------
col : 2維陣列
"""
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
使用im2col展開輸入資料后,之后就只需將卷積層的濾波器(權重)縱向展開為1列,并計算2個矩陣的乘積即可,

可以看到,展開之后正好可以利用矩陣的乘法進行計算,
卷積層的實作
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 = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (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)
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
這里要注意,通過在reshape時指定為-1,reshape函式會自動計算-1維度上的元素個數,以使多維陣列的元素個數前后一致,而transpose會更改多維陣列的軸的順序,例如transpose(0, 3, 1, 2)就是將原來0,1,2,3位置的軸分別換到輸入引數的位置,

以上就是卷積層的forward處理的實作,至于卷積層的反向傳播的代碼,用到了col2im,就是im2col的逆程序,代碼如下:
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
"""
Parameters
----------
col :
input_shape : 輸入資料的形狀(例:(10, 1, 28, 28))
filter_h :
filter_w
stride
pad
Returns
-------
"""
N, C, H, W = input_shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, pad:H + pad, pad:W + pad]
池化層的實作
池化的應用區域按通道單獨展開,

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)
最大值的計算可以使用 NumPy 的np.max方法,np.max可以指定axis引數,并在這個引數指定的各個軸方向上求最大值,比如,如果寫成np.max(x, axis=1),就可以在輸入x的第1維的各個軸方向上求最大值,
通過將輸入資料展開為容易進行池化的形狀,后面的實作就會變得非常簡單.
CNN的實作
將要實作的網路的構成是“Convolution-ReLU-Pooling-Affine-
ReLU-Affine-Softmax”,我們將它實作為名為SimpleConvNet的類
class SimpleConvNet:
"""
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層的卷積層的權重,權重的元素是實數,但是在影像的顯示上,統一將最小值顯示為黑色(0),最大值顯示為白色(255):

學習前的濾波器是隨機進行初始化的,所以在黑白的濃淡上沒有規律可循,但學習后的濾波器變成了有規律的影像,我們發現,通過學習,濾波器被更新成了有規律的濾波器,
如果要問圖中右邊學習過的有規律的濾波器在“觀察”什么,答案就是它在觀察邊緣(顏色變化的分界線)和斑塊(區域的塊狀區域)等,

在傳統數字影像處理中,檢測邊緣的濾波器一般都是固定的,如梯度算子,高斯-拉普拉斯算子等,而神將網路可以根據訓練影像自己學習出邊緣的規律,產生合適的濾波器,
基于分層結構的資訊提取
根據深度學習的可視化相關的研究,隨著層次加深,提取的資訊(正確地講,是反映強烈的神經元)也越來越抽象,

CNN的卷積層中提取的資訊,第1層的神經元對邊緣或斑塊有回應,第3層對紋
理有回應,第5層對物體部件有回應,最后的全連接層對物體的類別(狗或車)有
回應,
如果堆疊了多層卷積層,則隨著層次加深,提取的資訊也愈加復雜、抽象,這是深度學習中很有意思的一個地方,隨著層次加深,神經元從簡單的形狀向“高級”資訊變化,換句話說,就像我們理解東西的“含義”一樣,回應的物件在逐漸變化,
具有代表性的CNN
LeNet

LeNet是最早的CNN,和“現在的CNN”相比,LeNet有幾個不同點:
- 第一個不同點在于激活函式,LeNet 中使用sigmoid 函式,而現在的 CNN中主要使用 ReLU 函式,
- 原始的LeNet中使用子采樣(subsampling)縮小中間資料的大小,而現在的CNN中Max池化是主流,
AlexNet
AlexNet是引發深度學習熱潮的導火線,不過它的網路結構和LeNet基本上沒有什么不同,

AlexNet疊有多個卷積層和池化層,最后經由全連接層輸出結果,在結構上AlexNet和LeNet沒有大的不同,但有以下幾點差異:
- 激活函式使用ReLU,
- 使用進行區域正規化的LRN(Local Response Normalization)層
著層次加深,神經元從簡單的形狀向“高級”資訊變化,換句話說,就像我們理解東西的“含義”一樣,回應的物件在逐漸變化,
具有代表性的CNN
LeNet
[外鏈圖片轉存中…(img-2DFXyUwG-1642312849109)]
LeNet是最早的CNN,和“現在的CNN”相比,LeNet有幾個不同點:
- 第一個不同點在于激活函式,LeNet 中使用sigmoid 函式,而現在的 CNN中主要使用 ReLU 函式,
- 原始的LeNet中使用子采樣(subsampling)縮小中間資料的大小,而現在的CNN中Max池化是主流,
AlexNet
AlexNet是引發深度學習熱潮的導火線,不過它的網路結構和LeNet基本上沒有什么不同,
[外鏈圖片轉存中…(img-YSH2M5DR-1642312849110)]
AlexNet疊有多個卷積層和池化層,最后經由全連接層輸出結果,在結構上AlexNet和LeNet沒有大的不同,但有以下幾點差異:
- 激活函式使用ReLU,
- 使用進行區域正規化的LRN(Local Response Normalization)層
- 使用Dropout(在學習的程序中隨機洗掉神經元),
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/413228.html
標籤:AI
上一篇:Python深度學習入門筆記 5
下一篇:油田生產資料選取問題4
