點擊上圖,立即開啟AI急速修煉
作者:Charlotte 高級算法工程師 ,博客專家;
擅長用通俗易懂的方式講解深度學習和機器學習算法,熟悉Tensorflow,PaddlePaddle等深度學習框架,負責過多個機器學習落地項目,如垃圾評論自動過濾,用戶分級精準營銷,分布式深度學習平臺搭建等,都取了的不錯的效果。
博客專欄:https://www.cnblogs.com/charlotte77/
前文傳送門:
【好書推薦&學習階段】三個月教你從零入門深度學習
【深度學習系列】PaddlePaddle之手寫數字識別
【深度學習系列】卷積神經網絡CNN原理詳解(一)——基本原理
上篇文章中我們講解了卷積神經網絡的基本原理,包括幾個基本層的定義、運算規則等。本文主要寫卷積神經網絡如何進行一次完整的訓練,包括前向傳播和反向傳播,並自己手寫一個卷積神經網絡。如果不了解基本原理的,可以先看看上篇文章:【深度學習系列】卷積神經網絡CNN原理詳解(一)——基本原理
卷積神經網絡的前向傳播
首先我們來看一個最簡單的卷積神經網絡:
1.輸入層---->卷積層
以上一節的例子為例,輸入是一個4*4 的image,經過兩個2*2的卷積核進行卷積運算後,變成兩個3*3的feature_map
以卷積核filter1為例(stride = 1 ):
計算第一個卷積層神經元o11的輸入:
神經元o11的輸出:(此處使用Relu激活函數)
其他神經元計算方式相同
2.卷積層---->池化層
計算池化層m11 的輸入(取窗口為 2 * 2),池化層沒有激活函數
3.池化層---->全連接層
池化層的輸出到flatten層把所有元素「拍平」,然後到全連接層。
4.全連接層---->輸出層
全連接層到輸出層就是正常的神經元與神經元之間的鄰接相連,通過softmax函數計算後輸出到output,得到不同類別的概率值,輸出概率值最大的即為該圖片的類別。
卷積神經網絡的反向傳播
傳統的神經網絡是全連接形式的,如果進行反向傳播,只需要由下一層對前一層不斷的求偏導,即求鏈式偏導就可以求出每一層的誤差敏感項,然後求出權重和偏置項的梯度,即可更新權重。而卷積神經網絡有兩個特殊的層:卷積層和池化層。池化層輸出時不需要經過激活函數,是一個滑動窗口的最大值,一個常數,那麼它的偏導是1。池化層相當於對上層圖片做了一個壓縮,這個反向求誤差敏感項時與傳統的反向傳播方式不同。從卷積後的feature_map反向傳播到前一層時,由於前向傳播時是通過卷積核做卷積運算得到的feature_map,所以反向傳播與傳統的也不一樣,需要更新卷積核的參數。下面我們介紹一下池化層和卷積層是如何做反向傳播的。
在介紹之前,首先回顧一下傳統的反向傳播方法:
1.通過前向傳播計算每一層的輸入值neti,j (如卷積後的feature_map的第一個神經元的輸入:neti11)
2.反向傳播計算每個神經元的誤差項,其中E為損失函數計算得到的總體誤差,可以用平方差,交叉熵等表示。
3.計算每個神經元權重wi,j的梯度,
4.更新權重 wi,j=wi,j−λ⋅ηi,j(其中λλ為學習率)
卷積層的反向傳播
由前向傳播可得:
neti11表示上一層的輸入,
首先計算卷積的上一層的第一個元素
(注意這裡是
註:原來這裡寫的是計算輸入層的誤差項是不準確的,這裡的
表示的是卷積層的上一層即可。
先計算
此處我們並不清楚 怎麼算,那可以先把input層通過卷積核做完卷積運算後的輸出feature_map寫出來:
然後依次對輸入元素ii,j求偏導
i11的偏導:
i12的偏導:
i13的偏導:
i21的偏導:
i22的偏導:
觀察一下上面幾個式子的規律,歸納一下,可以得到如下表達式:
圖中的卷積核進行了180°翻轉,與這一層的誤差敏感項矩陣deltai,j)周圍補零後的矩陣做卷積運算後,就可以得到,即
第一項求完後,我們來求第二項
此時我們的誤差敏感矩陣就求完了,得到誤差敏感矩陣後,即可求權重的梯度。
由於上面已經寫出了卷積層的輸入neto11與權重hi,j之間的表達式, 所以可以直接求出:
推論出權重的梯度:
偏置項的梯度:
可以看出,偏置項的偏導等於這一層所有誤差敏感項之和。得到了權重和偏置項的梯度後,就可以根據梯度下降法更新權重和梯度了。
池化層的反向傳播
池化層的反向傳播就比較好求了,看著下面的圖,左邊是上一層的輸出,也就是卷積層的輸出feature_map,右邊是池化層的輸入,還是先根據前向傳播,把式子都寫出來,方便計算:
假設上一層這個滑動窗口的最大值是outo11
1.定義一個卷積層
首先我們通過ConvLayer來實現一個卷積層,定義卷積層的超參數
class ConvLayer(object): ''' 參數含義: input_width:輸入圖片尺寸——寬度 input_height:輸入圖片尺寸——長度 channel_number:通道數,彩色為3,灰色為1 filter_width:卷積核的寬 filter_height:卷積核的長 filter_number:卷積核數量 zero_padding:補零長度 stride:步長 activator:激活函數 learning_rate:學習率 ''' def __init__(self, input_width, input_height, channel_number, filter_width, filter_height, filter_number, zero_padding, stride, activator, learning_rate): self.input_width = input_width self.input_height = input_height self.channel_number = channel_number self.filter_width = filter_width self.filter_height = filter_height self.filter_number = filter_number self.zero_padding = zero_padding self.stride = stride self.output_width = \ ConvLayer.calculate_output_size( self.input_width, filter_width, zero_padding, stride) self.output_height = \ ConvLayer.calculate_output_size( self.input_height, filter_height, zero_padding, stride) self.output_array = np.zeros((self.filter_number, self.output_height, self.output_width)) self.filters = [] for i in range(filter_number): self.filters.append(Filter(filter_width, filter_height, self.channel_number)) self.activator = activator self.learning_rate = learning_rate
其中calculate_output_size用來計算通過卷積運算後輸出的feature_map大小
@staticmethod def calculate_output_size(input_size, filter_size, zero_padding, stride): return (input_size - filter_size + 2 * zero_padding) / stride + 1
2.構造一個激活函數
此處用的是RELU激活函數,因此我們在activators.py裡定義,forward是前向計算,backforward是計算公式的導數:
class ReluActivator(object): def forward(self, weighted_input): #return weighted_input return max(0, weighted_input) def backward(self, output): return 1 if output > 0 else 0
其他常見的激活函數我們也可以放到activators裡,如sigmoid函數,我們可以做如下定義:
class SigmoidActivator(object): def forward(self, weighted_input): return 1.0 / (1.0 + np.exp(-weighted_input)) #the partial of sigmoid def backward(self, output): return output * (1 - output)
如果我們需要自動以其他的激活函數,都可以在activator.py定義一個類即可。
3.定義一個類,保存卷積層的參數和梯度
class Filter(object): def __init__(self, width, height, depth): #初始權重 self.weights = np.random.uniform(-1e-4, 1e-4, (depth, height, width)) #初始偏置 self.bias = 0 self.weights_grad = np.zeros( self.weights.shape) self.bias_grad = 0 def __repr__(self): return 'filter weights:\n%s\nbias:\n%s' % ( repr(self.weights), repr(self.bias)) def get_weights(self): return self.weights def get_bias(self): return self.bias def update(self, learning_rate): self.weights -= learning_rate * self.weights_grad self.bias -= learning_rate * self.bias_grad
4.卷積層的前向傳播
1).獲取卷積區域
# 獲取卷積區域def get_patch(input_array, i, j, filter_width, filter_height, stride): ''' 從輸入數組中獲取本次卷積的區域, 自動適配輸入為2D和3D的情況 ''' start_i = i * stride start_j = j * stride if input_array.ndim == 2: input_array_conv = input_array[ start_i : start_i + filter_height, start_j : start_j + filter_width] print "input_array_conv:",input_array_conv return input_array_conv elif input_array.ndim == 3: input_array_conv = input_array[:, start_i : start_i + filter_height, start_j : start_j + filter_width] print "input_array_conv:",input_array_conv return input_array_conv
2).進行卷積運算
def conv(input_array, kernel_array, output_array, stride, bias): ''' 計算卷積,自動適配輸入為2D和3D的情況 ''' channel_number = input_array.ndim output_width = output_array.shape[1] output_height = output_array.shape[0] kernel_width = kernel_array.shape[-1] kernel_height = kernel_array.shape[-2] for i in range(output_height): for j in range(output_width): output_array[i][j] = ( get_patch(input_array, i, j, kernel_width, kernel_height, stride) * kernel_array ).sum() + bias
3).增加zero_padding
#增加Zero paddingdef padding(input_array, zp): ''' 為數組增加Zero padding,自動適配輸入為2D和3D的情況 ''' if zp == 0: return input_array else: if input_array.ndim == 3: input_width = input_array.shape[2] input_height = input_array.shape[1] input_depth = input_array.shape[0] padded_array = np.zeros(( input_depth, input_height + 2 * zp, input_width + 2 * zp)) padded_array[:, zp : zp + input_height, zp : zp + input_width] = input_array return padded_array elif input_array.ndim == 2: input_width = input_array.shape[1] input_height = input_array.shape[0] padded_array = np.zeros(( input_height + 2 * zp, input_width + 2 * zp)) padded_array[zp : zp + input_height, zp : zp + input_width] = input_array return padded_array
4).進行前向傳播
def forward(self, input_array): ''' 計算卷積層的輸出 輸出結果保存在self.output_array ''' self.input_array = input_array self.padded_input_array = padding(input_array, self.zero_padding) for f in range(self.filter_number): filter = self.filters[f] conv(self.padded_input_array, filter.get_weights(), self.output_array[f], self.stride, filter.get_bias()) element_wise_op(self.output_array, self.activator.forward)
其中element_wise_op函數是將每個組的元素對應相乘
# 對numpy數組進行element wise操作,將矩陣中的每個元素對應相乘def element_wise_op(array, op): for i in np.nditer(array, op_flags=['readwrite']): i[...] = op(i)
5.卷積層的反向傳播
1).將誤差傳遞到上一層
def bp_sensitivity_map(self, sensitivity_array, activator): ''' 計算傳遞到上一層的sensitivity map sensitivity_array: 本層的sensitivity map activator: 上一層的激活函數 ''' # 處理卷積步長,對原始sensitivity map進行擴展 expanded_array = self.expand_sensitivity_map( sensitivity_array) # full卷積,對sensitivitiy map進行zero padding # 雖然原始輸入的zero padding單元也會獲得殘差 # 但這個殘差不需要繼續向上傳遞,因此就不計算了 expanded_width = expanded_array.shape[2] zp = (self.input_width + self.filter_width - 1 - expanded_width) / 2 padded_array = padding(expanded_array, zp) # 初始化delta_array,用於保存傳遞到上一層的 # sensitivity map self.delta_array = self.create_delta_array() # 對於具有多個filter的卷積層來說,最終傳遞到上一層的 # sensitivity map相當於所有的filter的 # sensitivity map之和 for f in range(self.filter_number): filter = self.filters[f] # 將filter權重翻轉180度 flipped_weights = np.array(map( lambda i: np.rot90(i, 2), filter.get_weights())) # 計算與一個filter對應的delta_array delta_array = self.create_delta_array() for d in range(delta_array.shape[0]): conv(padded_array[f], flipped_weights[d], delta_array[d], 1, 0) self.delta_array += delta_array # 將計算結果與激活函數的偏導數做element-wise乘法操作 derivative_array = np.array(self.input_array) element_wise_op(derivative_array, activator.backward) self.delta_array *= derivative_array
2).保存傳遞到上一層的sensitivity map的數組
def create_delta_array(self): return np.zeros((self.channel_number, self.input_height, self.input_width))
3).計算代碼梯度
def bp_gradient(self, sensitivity_array): # 處理卷積步長,對原始sensitivity map進行擴展 expanded_array = self.expand_sensitivity_map( sensitivity_array) for f in range(self.filter_number): # 計算每個權重的梯度 filter = self.filters[f] for d in range(filter.weights.shape[0]): conv(self.padded_input_array[d], expanded_array[f], filter.weights_grad[d], 1, 0) # 計算偏置項的梯度 filter.bias_grad = expanded_array[f].sum()
4).按照梯度下降法更新參數
def update(self): ''' 按照梯度下降,更新權重 ''' for filter in self.filters: filter.update(self.learning_rate)
6.MaxPooling層的訓練
1).定義MaxPooling類
class MaxPoolingLayer(object): def __init__(self, input_width, input_height, channel_number, filter_width, filter_height, stride): self.input_width = input_width self.input_height = input_height self.channel_number = channel_number self.filter_width = filter_width self.filter_height = filter_height self.stride = stride self.output_width = (input_width - filter_width) / self.stride + 1 self.output_height = (input_height - filter_height) / self.stride + 1 self.output_array = np.zeros((self.channel_number, self.output_height, self.output_width))
2).前向傳播計算
# 前向傳播 def forward(self, input_array): for d in range(self.channel_number): for i in range(self.output_height): for j in range(self.output_width): self.output_array[d,i,j] = ( get_patch(input_array[d], i, j, self.filter_width, self.filter_height, self.stride).max())
3).反向傳播計算
#反向傳播 def backward(self, input_array, sensitivity_array): self.delta_array = np.zeros(input_array.shape) for d in range(self.channel_number): for i in range(self.output_height): for j in range(self.output_width): patch_array = get_patch( input_array[d], i, j, self.filter_width, self.filter_height, self.stride) k, l = get_max_index(patch_array) self.delta_array[d, i * self.stride + k, j * self.stride + l] = \ sensitivity_array[d,i,j]
完整代碼請見:cnn.py (https://github.com/huxiaoman7/PaddlePaddle_code/blob/master/1.mnist/cnn.py)
#coding:utf-8'''Created by huxiaoman 2017.11.22'''import numpy as npfrom activators import ReluActivator,IdentityActivatorclass ConvLayer(object): def __init__(self,input_width,input_weight, channel_number,filter_width, filter_height,filter_number, zero_padding,stride,activator, learning_rate): self.input_width = input_width self.input_height = input_height self.channel_number = channel_number self.filter_width = filter_width self.filter_height = filter_height self.filter_number = filter_number self.zero_padding = zero_padding self.stride = stride #此處可以加上stride_x, stride_y self.output_width = ConvLayer.calculate_output_size( self.input_width,filter_width,zero_padding, stride) self.output_height = ConvLayer.calculate_output_size( self.input_height,filter_height,zero_padding, stride) self.output_array = np.zeros((self.filter_number, self.output_height,self.output_width)) self.filters = [] for i in range(filter_number): self.filters.append(Filter(filter_width, filter_height,self.channel_number)) self.activator = activator self.learning_rate = learning_rate def forward(self,input_array): ''' 計算卷積層的輸出 輸出結果保存在self.output_array ''' self.input_array = input_array self.padded_input_array = padding(input_array, self.zero_padding) for i in range(self.filter_number): filter = self.filters[f] conv(self.padded_input_array, filter.get_weights(), self.output_array[f], self.stride, filter.get_bias()) element_wise_op(self.output_array, self.activator.forward)def get_batch(input_array, i, j, filter_width,filter_height,stride): ''' 從輸入數組中獲取本次卷積的區域, 自動適配輸入為2D和3D的情況 ''' start_i = i * stride start_j = j * stride if input_array.ndim == 2: return input_array[ start_i : start_i + filter_height, start_j : start_j + filter_width] elif input_array.ndim == 3: return input_array[ start_i : start_i + filter_height, start_j : start_j + filter_width]# 獲取一個2D區域的最大值所在的索引def get_max_index(array): max_i = 0 max_j = 0 max_value = array[0,0] for i in range(array.shape[0]): for j in range(array.shape[1]): if array[i,j] > max_value: max_value = array[i,j] max_i, max_j = i, j return max_i, max_jdef conv(input_array,kernal_array, output_array,stride,bias): ''' 計算卷積,自動適配輸入2D,3D的情況 ''' channel_number = input_array.ndim output_width = output_array.shape[1] output_height = output_array.shape[0] kernel_width = kernel_array.shape[-1] kernel_height = kernel_array.shape[-2] for i in range(output_height): for j in range(output_width): output_array[i][j] = ( get_patch(input_array, i, j, kernel_width, kernel_height,stride) * kernel_array).sum() +biasdef element_wise_op(array, op): for i in np.nditer(array, op_flags = ['readwrite']): i[...] = op(i)class ReluActivators(object): def forward(self, weighted_input): # Relu計算公式 = max(0,input) return max(0, weighted_input) def backward(self,output): return 1 if output > 0 else 0class SigmoidActivator(object): def forward(self,weighted_input): return 1 / (1 + math.exp(- weighted_input)) def backward(self,output): return output * (1 - output)
最後,我們用之前的4 * 4的image數據檢驗一下通過一次卷積神經網絡進行前向傳播和反向傳播後的輸出結果:
def init_test(): a = np.array( [[[0,1,1,0,2], [2,2,2,2,1], [1,0,0,2,0], [0,1,1,0,0], [1,2,0,0,2]], [[1,0,2,2,0], [0,0,0,2,0], [1,2,1,2,1], [1,0,0,0,0], [1,2,1,1,1]], [[2,1,2,0,0], [1,0,0,1,0], [0,2,1,0,1], [0,1,2,2,2], [2,1,0,0,1]]]) b = np.array( [[[0,1,1], [2,2,2], [1,0,0]], [[1,0,2], [0,0,0], [1,2,1]]]) cl = ConvLayer(5,5,3,3,3,2,1,2,IdentityActivator(),0.001) cl.filters[0].weights = np.array( [[[-1,1,0], [0,1,0], [0,1,1]], [[-1,-1,0], [0,0,0], [0,-1,0]], [[0,0,-1], [0,1,0], [1,-1,-1]]], dtype=np.float64) cl.filters[0].bias=1 cl.filters[1].weights = np.array( [[[1,1,-1], [-1,-1,1], [0,-1,1]], [[0,1,0], [-1,0,-1], [-1,1,0]], [[-1,0,0], [-1,0,1], [-1,0,0]]], dtype=np.float64) return a, b, cl
運行一下:
def test(): a, b, cl = init_test() cl.forward(a) print "前向傳播結果:", cl.output_array cl.backward(a, b, IdentityActivator()) cl.update() print "反向傳播後更新得到的filter1:",cl.filters[0] print "反向傳播後更新得到的filter2:",cl.filters[1]if __name__ == "__main__": test()
運行結果:
前向傳播結果: [[[ 6. 7. 5.] [ 3. -1. -1.] [ 2. -1. 4.]] [[ 2. -5. -8.] [ 1. -4. -4.] [ 0. -5. -5.]]]反向傳播後更新得到的filter1: filter weights:array([[[-1.008, 0.99 , -0.009], [-0.005, 0.994, -0.006], [-0.006, 0.995, 0.996]], [[-1.004, -1.001, -0.004], [-0.01 , -0.009, -0.012], [-0.002, -1.002, -0.002]], [[-0.002, -0.002, -1.003], [-0.005, 0.992, -0.005], [ 0.993, -1.008, -1.007]]])bias:0.99099999999999999反向傳播後更新得到的filter2: filter weights:array([[[ 9.98000000e-01, 9.98000000e-01, -1.00100000e+00], [ -1.00400000e+00, -1.00700000e+00, 9.97000000e-01], [ -4.00000000e-03, -1.00400000e+00, 9.98000000e-01]], [[ 0.00000000e+00, 9.99000000e-01, 0.00000000e+00], [ -1.00900000e+00, -5.00000000e-03, -1.00400000e+00], [ -1.00400000e+00, 1.00000000e+00, 0.00000000e+00]], [[ -1.00400000e+00, -6.00000000e-03, -5.00000000e-03], [ -1.00200000e+00, -5.00000000e-03, 9.98000000e-01], [ -1.00200000e+00, -1.00000000e-03, 0.00000000e+00]]])bias:-0.0070000000000000001
PaddlePaddle卷積神經網絡源碼解析
卷積層
在上篇文章中,我們對paddlepaddle實現卷積神經網絡的的函數簡單介紹了一下。在手寫數字識別中,我們設計CNN的網絡結構時,調用了一個函數simple_img_conv_pool(上篇文章的連結已失效,因為已經把framework--->fluid,更新速度太快了 = =)使用方式如下:
conv_pool_1 = paddle.networks.simple_img_conv_pool( input=img, filter_size=5, num_filters=20, num_channel=1, pool_size=2, pool_stride=2, act=paddle.activation.Relu())
這個函數把卷積層和池化層兩個部分封裝在一起,只用調用一個函數就可以搞定,非常方便。如果只需要單獨使用卷積層,可以調用這個函數img_conv_layer,使用方式如下:
conv = img_conv_layer(input=data, filter_size=1, filter_size_y=1, num_channels=8, num_filters=16, stride=1, bias_attr=False, act=ReluActivation())
我們來看一下這個函數具體有哪些參數(注釋寫明了參數的含義和怎麼使用)
def img_conv_layer(input, filter_size, num_filters, name=None, num_channels=None, act=None, groups=1, stride=1, padding=0, dilation=1, bias_attr=None, param_attr=None, shared_biases=True, layer_attr=None, filter_size_y=None, stride_y=None, padding_y=None, dilation_y=None, trans=False, layer_type=None): """ 適合圖像的卷積層。Paddle可以支持正方形和長方形兩種圖片尺寸的輸入 也可適用於圖像的反卷積(Convolutional Transpose,即deconv)。 同樣可支持正方形和長方形兩種尺寸輸入。 num_channel:輸入圖片的通道數。可以是1或者3,或者是上一層的通道數(卷積核數目 * 組的數量) 每一個組都會處理圖片的一些通道。舉個例子,如果一個輸入如偏的num_channel是256,設置4個group, 32個卷積核,那麼會創建32*4 = 128個卷積核來處理輸入圖片。通道會被分成四塊,32個卷積核會先 處理64(256/4=64)個通道。剩下的卷積核組會處理剩下的通道。 name:層的名字。可選,自定義。 type:basestring input:這個層的輸入 type:LayerOutPut filter_size:卷積核的x維,可以理解為width。 如果是正方形,可以直接輸入一個元祖組表示圖片的尺寸 type:int/ tuple/ list filter_size_y:卷積核的y維,可以理解為height。 PaddlePaddle支持長方形的圖片尺寸,所以卷積核的尺寸為(filter_size,filter_size_y) type:int/ None act: 激活函數類型。默認選Relu type:BaseActivation groups:卷積核的組數量 type:int stride: 水平方向的滑動步長。或者世界輸入一個元祖,代表水平數值滑動步長相同。 type:int/ tuple/ list stride_y:垂直滑動步長。 type:int padding: 補零的水平維度,也可以直接輸入一個元祖,水平和垂直方向上補零的維度相同。 type:int/ tuple/ list padding_y:垂直方向補零的維度 type:int dilation:水平方向的擴展維度。同樣可以輸入一個元祖表示水平和初值上擴展維度相同 :type:int/ tuple/ list dilation_y:垂直方向的擴展維度 type:int bias_attr:偏置屬性 False:不定義bias True:bias初始化為0 type: ParameterAttribute/ None/ bool/ Any num_channel:輸入圖片的通道channel。如果設置為None,自動生成為上層輸出的通道數 type: int param_attr:卷積參數屬性。設置為None表示默認屬性 param_attr:ParameterAttribute shared_bias:設置偏置項是否會在卷積核中共享 type:bool layer_attr: Layer的 Extra Attribute type:ExtraLayerAttribute param trans:如果是convTransLayer,設置為True,如果是convlayer設置為conv type:bool layer_type:明確layer_type,默認為None。 如果trans= True,必須是exconvt或者cudnn_convt,否則的話要麼是exconv,要麼是cudnn_conv ps:如果是默認的話,paddle會自動選擇適合cpu的ExpandConvLayer和適合GPU的CudnnConvLayer 當然,我們自己也可以明確選擇哪種類型 type:string return:LayerOutput object rtype:LayerOutput """def img_conv_layer(input, filter_size, num_filters, name=None, num_channels=None, act=None, groups=1, stride=1, padding=0, dilation=1, bias_attr=None, param_attr=None, shared_biases=True, layer_attr=None, filter_size_y=None, stride_y=None, padding_y=None, dilation_y=None, trans=False, layer_type=None): if num_channels is None: assert input.num_filters is not None num_channels = input.num_filters if filter_size_y is None: if isinstance(filter_size, collections.Sequence): assert len(filter_size) == 2 filter_size, filter_size_y = filter_size else: filter_size_y = filter_size if stride_y is None: if isinstance(stride, collections.Sequence): assert len(stride) == 2 stride, stride_y = stride else: stride_y = stride if padding_y is None: if isinstance(padding, collections.Sequence): assert len(padding) == 2 padding, padding_y = padding else: padding_y = padding if dilation_y is None: if isinstance(dilation, collections.Sequence): assert len(dilation) == 2 dilation, dilation_y = dilation else: dilation_y = dilation if param_attr.attr.get('initial_smart'): # special initial for conv layers. init_w = (2.0 / (filter_size**2 * num_channels))**0.5 param_attr.attr["initial_mean"] = 0.0 param_attr.attr["initial_std"] = init_w param_attr.attr["initial_strategy"] = 0 param_attr.attr["initial_smart"] = False if layer_type: if dilation > 1 or dilation_y > 1: assert layer_type in [ "cudnn_conv", "cudnn_convt", "exconv", "exconvt" ] if trans: assert layer_type in ["exconvt", "cudnn_convt"] else: assert layer_type in ["exconv", "cudnn_conv"] lt = layer_type else: lt = LayerType.CONVTRANS_LAYER if trans else LayerType.CONV_LAYER l = Layer( name=name, inputs=Input( input.name, conv=Conv( filter_size=filter_size, padding=padding, dilation=dilation, stride=stride, channels=num_channels, groups=groups, filter_size_y=filter_size_y, padding_y=padding_y, dilation_y=dilation_y, stride_y=stride_y), **param_attr.attr), active_type=act.name, num_filters=num_filters, bias=ParamAttr.to_bias(bias_attr), shared_biases=shared_biases, type=lt, **ExtraLayerAttribute.to_kwargs(layer_attr)) return LayerOutput( name, lt, parents=[input], activation=act, num_filters=num_filters, size=l.config.size)
我們了解這些參數的含義後,對比我們之前自己手寫的CNN,可以看出paddlepaddle有幾個優點:
在我們自己寫的CNN中,只支持正方形的圖片長度,如果是長方形會報錯。滑動步長,補零的維度等也只支持水平和垂直方向上的維度相同。了解卷積層的參數含義後,我們來看一下底層的源碼是如何實現的:ConvBaseLayer.py 有興趣的同學可以在這個連結下看看底層是如何用C++寫的ConvLayer
池化層同理,可以按照之前的思路分析,有興趣的可以一直順延看到底層的實現,下次有機會再詳細分析。(佔坑明天補一下tensorflow的源碼實現)
總結
本文主要講解了卷積神經網絡中反向傳播的一些技巧,包括卷積層和池化層的反向傳播與傳統的反向傳播的區別,並實現了一個完整的CNN,後續大家可以自己修改一些代碼,譬如當水平滑動長度與垂直滑動長度不同時需要怎麼調整等等,最後研究了一下paddlepaddle中CNN中的卷積層的實現過程,對比自己寫的CNN,總結了4個優點,底層是C++實現的,有興趣的可以自己再去深入研究。寫的比較粗糙,如果有問題歡迎留言:)
參考文章:
1.https://www.cnblogs.com/pinard/p/6494810.html
2.https://www.zybuluo.com/hanbingtao/note/476663
想從零開始學人工智慧?
下圖掃碼了解本文作者胡老師的系列課程吧!
限時優惠價299!