本教程由深度學習中文社區(Studydl.com)持續發布與更新, 教程中完整代碼已上傳至github上, 可關注我百家號後發送消息"CNN代碼", 獲得地址.
前言
Numpy是一個非常好用的python科學計算的庫,CNN是現在視覺領域深度學習的基礎之一。雖然好的框架很多,不過自己用Numpy實現一個可以使用的CNN的模型有利於初學者加深對CNN的理解。
後面我們將通過一系列文章介紹如何用Numpy從零實現一個可以訓練的CNN簡易網絡,同時對深度學習(CNN)的相關基礎知識進行一些複習,也希望能夠給正在入門的同學一些簡單的歸納。
在這一系列的文章中,我們主要需要做以下一些工作:
Numpy實現如下圖中所示的基本的CNN網絡組件:conv, pooling, relu, fullyconnect, softmax
用實現的基本組件搭建可以訓練的網絡,完成mnist的訓練與測試,畫出一個下圖的訓練曲線從貫序連接的層模型到計算圖模型的引入,完成自動求值,求導等功能常見的初始化方法,激活函數,優化方法,Loss函數的實現與比較,例如relu系與sigmoid系的比較,sgd,momentum,Adam的比較等等常見的trick的實現,例如dropout, batchnorm, residual
絕大部分的同學入門深度學習,第一個接觸的應該就是LeNet,我們也將以此為例子介紹卷積神經網絡的基本組件。參考上面的的網絡結構圖,它包含了卷積層(Convolutions),池化層(pooling), 全連接層(Full connection)。
卷積層(Convolutions)
卷積:如果你有圖像處理的基礎,對於卷積操作我想你一定不會陌生。在傳統的圖像處理中,卷積操作多用來進行濾波,銳化或者邊緣檢測啥的。我們可以認為卷積是利用某些設計好的參數組合(卷積核)去提取圖像空域上相鄰的信息。
在二維圖像上,卷積操作一方面可以高效地按照我們的需求提取圖像的鄰域信息,在全局上又有著非常好的平移等變性,簡單就是說你將輸入圖像的某一部分移動到另外一部分,在輸出圖像上也會有著相應的移動。比如下圖是花書裡用來闡述卷積在特徵提取的效率優勢上的一張圖片,我們用來演示卷積的效果。假設我們把狗狗的鼻子移動到左上角,那麼相應輸出裡面的狗鼻子也會出現在左上角。(照片來源Paula Goodfellow)
卷積神經網絡之所以非常適合處理視覺問題,卷積的平移等變性是一大功臣。卷積神經網絡中的卷積層可以理解成是在二維圖像卷積的基礎上增廣而來。在二維圖像卷積操作裡,對於一個固定的卷積核,它能夠提取輸入圖像的某種特徵,但在實際的視覺問題裡,某一個尺度下的某種空間特徵不足以解決我們的需求。所以我們需要改進原始的卷積操作使得可以提取不同尺度下的不同特徵。
一次操作(一層)中使用多個卷積核得到該尺度下的多張特徵映射多層(次)提取不同尺度下的不同特徵信息
由於第一點改進,自然而然,即使第一張圖片輸入只有一個通道,後面其他層的輸入都是多通道。所以對應的我們的卷積核也是多通道。即輸入圖像和卷積核都添加了channel這個維度,那麼卷積層中的卷積操作變為了如下的定義:
到這裡,我們大概可以總結出一個卷積層的前向計算(Forward)和他的功能。當然正如我們前面提到過,需要讀者懂一點BP算法,因為即使我們知道如何通過卷積層提取輸入圖像的特徵,卷積層依舊無法正常的工作,因為卷積操作最關鍵的部分卷積核的數值沒有被確定下來。BP算法就是告訴我們,如何通過監督學習的方法來優化我們的卷積核的數值,使得我們能夠找到在對應任務下表現最好的卷積核(特徵),當然這個說法不是很準確。所以在我們實現的卷積層的類中,還會包含一個Backward方法,用於反向傳播求導。考慮到一次篇幅不要太長這一部分的原理和實現將放在下一篇文章裡。
在這篇接下來的部分裡我們就將逐一用Numpy實現一個可以運行的Conv2D類以及Forward方法。
Show me your code
PS:文章看到的版本是不基於graph模型的,Conv2D直接繼承自Object,所有的數據和操作都是裸露的,單純為了實現功能。github上面這一部分代碼已經不用了,放在layers文件夾下。
初始化
根據上面的公式,我們知道實現一個卷積前向計算的操作,我們需要知道以下信息:
輸入數據的shape = [N,W,H,C] N=Batchsize/W=width/H=height/C=channels卷積核的尺寸ksize ,個數output_channels, kernel shape [output_channels,k,k,C]卷積的步長,基本默認為1.卷積的方法,VALID or SAME,即是否通過padding保持輸出圖像與輸入圖像的大小不變
實際上還需要知道核參數的初始化方法
class Conv2D(object):def __init__(self, shape, output_channels, ksize=3, stride=1, method='VALID'):self.input_shape = shapeself.output_channels = output_channelsself.input_channels = shape[-1]self.batchsize = shape[0]self.stride = strideself.ksize = ksizeself.method = methodweights_scale = math.sqrt(ksize*ksize*self.input_channels/2)self.weights = np.random.standard_normal((ksize, ksize, self.input_channels, self.output_channels)) / weights_scaleself.bias = np.random.standard_normal(self.output_channels) / weights_scale
我們通過初始化函數確定上面提到的所需參數,在第一次聲明的時候完成了對該層的構建。如下:
conv1 = Conv2D([batch_size, 28, 28, 1], 12, 5, 1)
這個conv1的實例就代表了該層,自然也會包含該層所需要的參數,例如kernel weights, kernel bias,我們用 np.random.standard_noral(kernel_shape)生成對應的kernel weights和kernel bias。因為 np.random.standard_noral()生成的mean=0,stdev=1的隨機Numpy數組,這裡我們後面除以相應的weights_scale(msra方法)去控制一下初始化生成的weights的stdev,好的初始化可以加速收斂。
下面這一部分是反向傳播中用到的,這篇文章中暫時不會用到,self.eta用於儲存backward傳回來的
他與該層的out是一個同樣維度的數組.
這裡我們就可以看到method時如何控制輸出數據的形狀的。「SAME」就表示添加padding使得輸出長寬不變。self.w_gradient,self.b_gradient則分別用於儲存backward計算過後得到的該次的
if method == 'VALID':self.eta = np.zeros((shape[0], (shape[1] - ksize ) / self.stride + 1, (shape[1] - ksize ) / self.stride + 1,self.output_channels))if method == 'SAME':self.eta = np.zeros((shape[0], shape[1]/self.stride, shape[2]/self.stride,self.output_channels))self.w_gradient = np.zeros(self.weights.shape)self.b_gradient = np.zeros(self.bias.shape)self.output_shape = self.eta.shape
前向計算(forward)
如何實現卷積層前向計算,是一個非常老生常談的問題,賈揚清大神對這個問題解釋的比較清楚,可以參考查詢下在Caffe中如何計算卷積
這裡主要使用的就是im2col優化方法:通過將圖像展開,使得卷積運算可以變成兩個矩陣乘法
詳情參見論文 High Performance Convolutional Neural Networks for Document Processing
我們的forward方法的實現基本就是實現了上圖,主要分為以下四個步驟:
完整的代碼如下
def forward(self, x):col_weights = self.weights.reshape([-1, self.output_channels])if self.method == 'SAME':x = np.pad(x, ((0, 0), (self.ksize / 2, self.ksize / 2), (self.ksize / 2, self.ksize / 2), (0, 0)),'constant', constant_values=0)self.col_image = []conv_out = np.zeros(self.eta.shape)for i in range(self.batchsize):img_i = x[i][np.newaxis, :]self.col_image_i = im2col(img_i, self.ksize, self.stride)conv_out[i] = np.reshape(np.dot(self.col_image_i, col_weights) + self.bias, self.eta[0].shape)self.col_image.append(self.col_image_i)self.col_image = np.array(self.col_image)return conv_out
首先我們將卷積層的參數weights通過ndarray自帶的reshape方法reshape到上圖中Kernal Matrix的形狀。根據self.method,選擇是否對輸入的數據進行padding,這裡我們調用 np.pad()方法,對我們的輸入數據四維ndarray的第二維和第三維分別padding上與卷積核大小相匹配的0元素。聲明一個list用於存儲轉換為column的image,在backward中我們還會用到。對於batch中的每一個數據,分別調用im2col方法,將該數據轉化為上圖中的Input features(Matrix), 然後調用 np.dot()完成矩陣乘法得到Output features(Matrix), reshape輸出的shape,填充到輸出數據中。
im2col的代碼如下:
def im2col(image, ksize, stride):# image is a 4d tensor([batchsize, width ,height, channel])image_col = []for i in range(0, image.shape[1] - ksize + 1, stride):for j in range(0, image.shape[2] - ksize + 1, stride):col = image[:, i:i + ksize, j:j + ksize, :].reshape([-1])image_col.append(col)image_col = np.array(image_col)return image_col
到這裡我們就基本實現了簡單的卷積層的forward方法,你可以通過輸入圖片,指定weights的值,進行簡單的運算,測試是否能夠正確的進行前向的計算,完整的代碼可以去我的github直接看最新版,雖然有很較大的出入,但一樣是非常容易理解的。我們也能看出,之所以選用python+Numpy實現,是因為大多數需要用到的方法,例如reshape,pad,dot,都已經在Numpy中實現好了,同時,也很方便我們實時的去檢查。
第一篇文章裡就只介紹到這裡。整篇文章基本上是以圖像的視角,介紹了如何利用Numpy實現CNN中的卷積層的前向計算。這只是CNN精彩的地方,但不是最關鍵的地方,下一篇中我們將先介紹BP算法以及從機器學習相關視角來看待CNN,然後實現卷積層裡面的backward()與apply_gradient()。教程中完整代碼已上傳至github上, 可關注我百家號後發送私信"CNN代碼", 獲得地址.
TensorFlow入門系列教程文章地址:
只需以下兩步就可獲取到零基礎入門教教程啦:
回到文章最上方點擊關注按鈕關注我的百家號,方便接收最新教程.點擊標題下方的頭像位置, 就可進入我的主頁, 裡面就有我之前發布的TensorFlow入門系列教程啦.
目前已發布的TensorFlow入門教程列表: