做資訊網(wǎng)站要什么手續(xù)公司怎么推廣網(wǎng)絡營銷
CNN簡介與實現(xiàn)
- 導語
- 整體結構
- 卷積層
- 卷積
- 填充
- 步幅
- 三維卷積
- 立體化
- 批處理
- 實現(xiàn)
- 池化層
- 特點
- 實現(xiàn)
- CNN實現(xiàn)
- 可視化
- 總結
- 參考文獻
導語
CNN全稱卷積神經(jīng)網(wǎng)絡,可謂聲名遠揚,被用于生活中的各個領域,也是最好理解的神經(jīng)網(wǎng)絡結構之一。
整體結構
相較于先前的神經(jīng)網(wǎng)絡,CNN出現(xiàn)了卷積層和池化層的概念,基本的組成模塊是“卷積-ReLU-池化”,并且,在靠近輸出或最后輸出時時仍會采用“Affine-ReLU”、"Affine-ReLU"的組合,書上給出的示例圖如下:
卷積層
在思考為什么要用卷積層之前,我們可以先來看看卷積層之前的全連接層有什么局限性,全連接層通常要求輸入是一個一維的數(shù)組,即使原始數(shù)據(jù)是更高維的數(shù)據(jù),如高、長、通道的三維圖像,這個時候,使用全連接層,原始數(shù)據(jù)中的幾何信息、點之間的相對位置等空間信息就都被清除了,這些信息其實很重要,因為點與點之間在高維空間的關聯(lián)性是比一維更強的。
相比之下,卷積層就考慮到了這些空間信息,當輸入為圖像時,卷積層會以三維數(shù)據(jù)的形式接受輸入數(shù)據(jù),并且輸出也是三維數(shù)據(jù)。
CNN中卷積層的輸入輸出數(shù)據(jù)被稱作特征圖,輸入叫輸入特征圖,輸出叫輸出特征圖。
卷積
卷積是卷積層的運算,類似與圖像中的濾波器處理,具體做法如圖(圖源自網(wǎng)絡,侵刪):
此圖省略了卷積核,只給出了輸入和結果,以該圖為例,輸入是一個4×4的矩陣,在矩陣上存在一個3×3的滑動窗口,窗口每次移動一個單位,每次對窗口內的矩陣A進行一次權重累和,具體的權重為同等大小的卷積核矩陣,具體的例子如下, 36 = 1 × 1 + 2 × 1 + 0 × 3 + 4 × 0 + 5 × 2 + 6 × 0 + 7 × 1 + 8 × 2 + 1 × 1 36=1×1+2×1+0×3+4×0+5×2+6×0+7×1+8×2+1×1 36=1×1+2×1+0×3+4×0+5×2+6×0+7×1+8×2+1×1。
與全連接層一樣,CNN中也存在偏置,對于算出的結果矩陣,對矩陣中的所有元素可以加上一個相同的偏置值。
填充
在進行卷積前,有時候要把數(shù)據(jù)拓寬,例如把4×4拓成6×6,如何拓寬呢很簡單,把不夠的部分都設置為同一個值就可以(一般是0或者1),具體操作如圖(圖源網(wǎng)絡,侵刪):
這種做法,就叫做填充,使用填充主要是為了調整輸出大小,在使用卷積核運算的時候,如果不進行填充,卷積的結果勢必會在整體上變小(如4×4變成2×2),多次使用后,最后的結果就可能只有一個1,因此使用填充來避免這種情況的發(fā)生。
步幅
步幅很容易理解,就是滑動窗口的每次的移動距離,像下面這張圖,就是步幅為2時候的卷積(圖源網(wǎng)絡,侵刪):
可以看到,增大步幅會使得輸出變小,加上填充會變大,這個時候就可以根據(jù)兩者關系列出卷積輸出結果的公式了。
書上的描述如下(值除不盡四舍五入):
三維卷積
在現(xiàn)實使用中,CNN的輸入并不是一個單純的二維矩陣,輸入的圖像時一個帶有高、寬、通道的具體的特征圖,以RGB為例,RGB圖像是三通道,如果對RGB圖像進行卷積,那么就要對圖像上的每一個通道都使用一個卷積核,通道方向有多個特征圖時,需要按照通道方向進行輸入數(shù)據(jù)和濾波器的卷積運算,并將結果累和,生成一個新的二維矩陣。
立體化
當我們把輸入和輸出推向更一般的適用情況,多通道輸入數(shù)據(jù)使用對應的多通道核,最后輸出一張單個圖,書上的例子如下,其中C為通道數(shù)、H為高度、W為長度。
如果要再通道方向上也擁有多個卷積運算的輸出,就需要使用多個濾波器(權重),書上的圖如下:
如果再考慮上偏置,書上給出的圖如下:
批處理
通常,為了加快效率,神經(jīng)網(wǎng)絡會將輸入數(shù)據(jù)進行一批批的打包,一次性處理一堆數(shù)據(jù),為了處理一批數(shù)據(jù),需要在上一張圖的基礎上加上批次,書上給出的圖如下:
數(shù)據(jù)作為4維數(shù)據(jù)在各層之間傳遞,批處理將N次處理匯總成了1次進行。
實現(xiàn)
如果直接實現(xiàn)卷積運算,利用for循環(huán),效率其實是不高的,況且python給出了更好的選擇:im2col函數(shù)。
im2col將輸入數(shù)據(jù)展開來適合卷積核的計算,書上給出的圖如下:
這里更詳細的解釋一下,輸入的是一個三維的數(shù)據(jù),把每一面(二維)從左到右,從上到下,拉成一個一維的數(shù)組,然后把每個通道的一維數(shù)組拼起來,形成一個二維的矩陣,如果是多批次,就把這些矩陣首尾相連,形成一個更大的二維矩陣即可。
實際的卷積運算中,卷積核的應用區(qū)域幾乎彼此重疊,因此,在使用im2col之后,展開的元素個數(shù)會多于原來的輸入元素個數(shù),所以會消耗更多的內存。
書上給出了用im2col進行卷積的流程:
還需要明晰的一點是,im2col的使用并不會損失原數(shù)據(jù)在空間上的信息,它只是為了方便進行矩陣對數(shù)據(jù)進行了一些處理,并且在最后恢復了原來的數(shù)據(jù)模式。
書上給出了im2col和基于im2col實現(xiàn)的卷積層代碼如下:
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):#輸入,高,長,步幅,填充N, C, H, W = input_data.shapeout_h = (H + 2*pad - filter_h)//stride + 1#根據(jù)步長和高度計算輸出的長高out_w = (W + 2*pad - filter_w)//stride + 1img = 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))#設置一個空的拉伸之后的二維數(shù)組for y in range(filter_h):y_max = y + stride*out_hfor x in range(filter_w):x_max = x + stride*out_wcol[:, :, 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 colClass Convolution:def __init__(self,W,b,stride=1,pad=0):#初始化賦值self.W=Wself.b=bself.stride=strideself.pad=paddef forward(self,x):FN,C,FH,FW=self.W.shapeN,C,H,W=x.shapeout_h=int(1+(H+2*self.pad-FH)/self.stride)#獲得填充和卷積之后的規(guī)模out_w=int(1+(W+2*self.pad-FW)/self.stride)col=im2col(x,FH,FW,self.stride,self.pad)#拉伸#卷積層反向傳播的時候,需要進行im2col的逆處理col_W=self.W.reshape(FN,-1).T#把卷積核展開out=np.dot(col,col_W)+self.bout=out.reshape(N,out_h,out_w,-1).transpose(0,3,1,2)#更改軸的順序,NHWC變成NCHWreturn outdef backward(self, dout):FN, C, FH, FW = self.W.shapedout = 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
池化層
簡單來說,卷積是使用卷積核計算對應區(qū)域的乘積和,池化層是選取對應區(qū)域的最大值(也有其他的池化,比如平均值池化,指的是取對應區(qū)域的平均值作為輸出),書上給出的例子如下:
特點
池化層的操作很簡單,不需要像卷積層那樣學習卷積核的參數(shù),只需要提取最值或平均即可;其次,池化層的計算是按照通道獨立進行的,輸入和輸出的通道數(shù)不會變化;最后,池化層對輸入數(shù)據(jù)的微小偏差具有魯棒性(例如目標區(qū)域的非最大值有變化,并不會影響池化層最后的輸出)。
實現(xiàn)
池化層也是用im2col展開,但展開時在通道方向上是獨立的,書上給的圖示如下:
書上的實現(xiàn)代碼如下:
class Pooling:def __init__(self, pool_h, pool_w, stride=1, pad=0):#初始化self.pool_h = pool_hself.pool_w = pool_wself.stride = strideself.pad = padself.x = Noneself.arg_max = Nonedef forward(self, x):#推理函數(shù)N, C, H, W = x.shapeout_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)#還原成數(shù)據(jù)self.x = xself.arg_max = arg_maxreturn outdef backward(self, dout):#反向傳播dout = dout.transpose(0, 2, 3, 1)pool_size = self.pool_h * self.pool_wdmax = 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實現(xiàn)
將已經(jīng)實現(xiàn)的各個層進行組合,就可以實現(xiàn)一個簡單的CNN,書上給出了一個簡單CNN的具體代碼實現(xiàn),具體圖如下:
書上加上注釋的代碼如下:
class SimpleConvNet: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):#輸入大小,卷積核數(shù)量,卷積核大小,填充,步幅,隱藏層神經(jīng)元數(shù)量,輸出大小,初始權重標準差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 + 1pool_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()#損失函數(shù)def predict(self, x):#預測值for layer in self.layers.values():x = layer.forward(x)return xdef loss(self, 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.0for 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):#求梯度,用數(shù)值微分方法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 gradsdef gradient(self, x, t):#誤差反向傳播求梯度# forwardself.loss(x, t)# backwarddout = 1dout = 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'].dbgrads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].dbgrads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].dbreturn grads
訓練所需要的時間相較于先前的方法比較久,但是得到的結果識別率更高,具體訓練結果如下:
可視化
“卷積”是一種數(shù)學運算,邏輯上其實很難理解到它的用處,因此,書上給出了對卷積作用更加直接的展現(xiàn)方式,以上一部分學習前和學習后的卷積核為例,各個卷積核的權重圖如下:
學習前:
學習后:
可以明顯的看到,學習前雜亂無章的權重矩陣,在學習后變得有跡可循,明顯有些區(qū)域的權重更深一些,那么,這些權重更大的部分對應的目標究竟是什么呢?
書上給出了答案:這些卷積核在學習邊緣(顏色變化的分界線)和斑塊(局部的塊狀區(qū)域),例如黑白分界線,可以根據(jù)手寫數(shù)字識別的例子想象,手寫的數(shù)字是黑色,背景是白色,那么卷積核的目標就是使得模型對黑色的部分更加敏感,權重更大。
上述的結果是只進行了一次卷積得到的,隨著層次的加深,提取的信息也會越來越抽象,在深度學習中,最開始層會對簡單的邊緣有響應,接下來是對紋理,在接下來是對更復雜的性質,隨著層次遞增,模型的目標會從簡單的形狀進化到更高級的信息。
總結
本章詳細介紹了CNN的構造,對卷積層、池化層進行了從零開始的實現(xiàn),但是對反向傳播的部分只給出了代碼實現(xiàn)。最重要的還是對im2col的理解,明白了im2col的原理,卷積層、池化層乃至反向傳播的實現(xiàn),這些問題就迎刃而解了。
基于最基本的CNN,后續(xù)還有更多功能強大,網(wǎng)絡結構更深的CNN網(wǎng)絡,如LeNet(激活函數(shù)為sigmod,使用子采樣縮小中間數(shù)據(jù)大小,而不是卷積、池化)還有AlexNet(多個卷積層和池化層,激活函數(shù)為sigmod,使用進行局部正規(guī)化的LRN層,使用Dropout)等。
參考文獻
- 【Pytorch實現(xiàn)】——深入理解im2col(詳細圖解)
- 12張動圖幫你看懂卷積神經(jīng)網(wǎng)絡到底是什么
- 《深度學習入門——基于Python的理論與實現(xiàn)》