訂閱
糾錯
加入自媒體

使用Numpy從頭構(gòu)建卷積神經(jīng)網(wǎng)絡(luò)

使用該網(wǎng)絡(luò)對手寫數(shù)字進(jìn)行分類。所獲得的結(jié)果不是最先進(jìn)的水平,但仍然令人滿意,F(xiàn)在想更進(jìn)一步,我們的目標(biāo)是開發(fā)一個(gè)僅使用Numpy的卷積神經(jīng)網(wǎng)絡(luò)(CNN)。

這項(xiàng)任務(wù)背后的動機(jī)與創(chuàng)建全連接的網(wǎng)絡(luò)的動機(jī)相同:盡管Python深度學(xué)習(xí)庫是強(qiáng)大的工具,但它阻止從業(yè)者理解底層正在發(fā)生的事情。對于CNNs來說,這一點(diǎn)尤其正確,因?yàn)樵撨^程不如經(jīng)典深度網(wǎng)絡(luò)執(zhí)行的過程直觀。

解決這一問題的唯一辦法是嘗試自己實(shí)現(xiàn)這些網(wǎng)絡(luò)。

打算將本文作為一個(gè)實(shí)踐教程,而不是一個(gè)全面指導(dǎo)CNNs運(yùn)作原則的教程。因此,理論部分很窄,主要用于對實(shí)踐部分的理解。

對于需要更好地理解卷積網(wǎng)絡(luò)工作原理的讀者,留下了一些很好的資源。image.png

什么是卷積神經(jīng)網(wǎng)絡(luò)?

卷積神經(jīng)網(wǎng)絡(luò)使用特殊的結(jié)構(gòu)和操作,使其非常適合圖像相關(guān)任務(wù),如圖像分類、對象定位、圖像分割等。它們大致模擬了人類的視覺皮層,每個(gè)生物神經(jīng)元只對視野的一小部分做出反應(yīng)。此外,高級神經(jīng)元對其他低級神經(jīng)元的輸出做出反應(yīng)[1]。

正如我在上一篇文章中所展示的,即使是經(jīng)典的神經(jīng)網(wǎng)絡(luò)也可以用于圖像分類等任務(wù)。問題是,它們僅適用于小尺寸圖像,并且在應(yīng)用于中型或大型圖像時(shí)效率極低。原因是經(jīng)典神經(jīng)網(wǎng)絡(luò)需要大量的參數(shù)。

例如,200x200像素的圖像具有40'000個(gè)像素,如果網(wǎng)絡(luò)的第一層具有1'000個(gè)單位,則僅第一層的權(quán)重為4000萬。由于CNN實(shí)現(xiàn)了部分連接的層和權(quán)重共享,這一問題得到了高度緩解。

卷積神經(jīng)網(wǎng)絡(luò)的主要組成部分包括:

· 卷積層

· 池化層

卷積層

卷積層由一組濾波器(也稱為核)組成,當(dāng)應(yīng)用于層的輸入時(shí),對原始圖像進(jìn)行某種修改。濾波器是一種矩陣,其元素值定義了對原始圖像執(zhí)行的修改類型。類似以下的3x3內(nèi)核具有突出顯示圖像中垂直邊的效果:

不同的是,該核突出了水平邊:

核中元素的值不是手動選擇的,而是網(wǎng)絡(luò)在訓(xùn)練期間學(xué)習(xí)的參數(shù)。

卷積的作用是隔離圖像中存在的不同特征。Dense層稍后使用這些功能。

池化層

池化層非常簡單。池化層的任務(wù)是收縮輸入圖像,以減少網(wǎng)絡(luò)的計(jì)算負(fù)載和內(nèi)存消耗。事實(shí)上,減少圖像尺寸意味著減少參數(shù)的數(shù)量。

池化層所做的是使用核(通常為2x2維)并將輸入圖像的一部分聚合為單個(gè)值。例如,2x2最大池核獲取輸入圖像的4個(gè)像素,并僅返回具有最大值的像素。

Python實(shí)現(xiàn)

此GitHub存儲庫中提供了所有代碼。image.png

這個(gè)實(shí)現(xiàn)背后的想法是創(chuàng)建表示卷積和最大池層的Python類。此外,由于該代碼后來被應(yīng)用于MNIST分類問題,我為softmax層創(chuàng)建了一個(gè)類。

每個(gè)類都包含實(shí)現(xiàn)正向傳播和反向傳播的方法。

這些層隨后被連接在一個(gè)列表中,以生成實(shí)際的CNN。

卷積層實(shí)現(xiàn)

class ConvolutionLayer:

   def __init__(self, kernel_num, kernel_size):

       self.kernel_num = kernel_num

       self.kernel_size = kernel_size        

       self.kernels = np.random.randn(kernel_num, kernel_size, kernel_size) / (kernel_size**2)

   def patches_generator(self, image):

       image_h(yuǎn), image_w = image.shape

       self.image = image

       for h in range(image_h(yuǎn)-self.kernel_size+1):

           for w in range(image_w-self.kernel_size+1):

               patch = image[h:(h+self.kernel_size), w:(w+self.kernel_size)]

               yield patch, h, w
   

   def forward_prop(self, image):

       image_h(yuǎn), image_w = image.shape

       convolution_output = np.zeros((image_h(yuǎn)-self.kernel_size+1, image_w-self.kernel_size+1, self.kernel_num))

       for patch, h, w in self.patches_generator(image):

           convolution_output[h,w] = np.sum(patch*self.kernels, axis=(1,2))

       return convolution_output
   

   def back_prop(self, dE_dY, alpha):

       dE_dk = np.zeros(self.kernels.shape)

       for patch, h, w in self.patches_generator(self.image):

           for f in range(self.kernel_num):

               dE_dk[f] += patch * dE_dY[h, w, f]

       self.kernels -= alpha*dE_dk

       return dE_dk

構(gòu)造器將卷積層的核數(shù)及其大小作為輸入。我假設(shè)只使用大小為kernel_size x kernel_size的平方核。

在第5行中,我生成隨機(jī)濾波器(kernel_num、kernel_size、kernel_size),并將每個(gè)元素除以核大小的平方進(jìn)行歸一化。

patches_generator()方法是一個(gè)生成器。它產(chǎn)生切片。

forward_prop()方法對上述方法生成的每個(gè)切片進(jìn)行卷積。

最后,back_prop()方法負(fù)責(zé)計(jì)算損失函數(shù)相對于層的每個(gè)權(quán)重的梯度,并相應(yīng)地更新權(quán)重值。注意,這里提到的損失函數(shù)不是網(wǎng)絡(luò)的全局損失。相反,它是由最大池層傳遞給前一卷積層的損失函數(shù)。

為了顯示這個(gè)類的實(shí)際效果,我用32個(gè)3x3濾波器實(shí)例化了一個(gè)卷積層對象,并將正向傳播方法應(yīng)用于圖像。輸出包含32個(gè)稍小的圖像。

原始輸入圖像的大小為28x28像素,如下所示:

在應(yīng)用卷積層的前向傳播方法后,我獲得了32幅尺寸為26x26的圖像。這里我繪制了其中一幅:

如你所見,圖像稍小,手寫數(shù)字變得不那么清晰?紤]到這個(gè)操作是由一個(gè)填充了隨機(jī)值的濾波器執(zhí)行的,所以它并不代表經(jīng)過訓(xùn)練的CNN實(shí)際執(zhí)行的操作。

盡管如此,你可以得到這樣的想法,即這些卷積提供了較小的圖像,其中對象特征被隔離。

最大池層實(shí)現(xiàn)

class MaxPoolingLayer:

   def __init__(self, kernel_size):

       self.kernel_size = kernel_size

   def patches_generator(self, image):

       output_h(yuǎn) = image.shape[0] // self.kernel_size

       output_w = image.shape[1] // self.kernel_size

       self.image = image


       for h in range(output_h(yuǎn)):

           for w in range(output_w):

               patch = image[(h*self.kernel_size):(h*self.kernel_size+self.kernel_size), (w*self.kernel_size):(w*self.kernel_size+self.kernel_size)]

               yield patch, h, w

   def forward_prop(self, image):

       image_h(yuǎn), image_w, num_kernels = image.shape

       max_pooling_output = np.zeros((image_h(yuǎn)//self.kernel_size, image_w//self.kernel_size, num_kernels))

       for patch, h, w in self.patches_generator(image):

           max_pooling_output[h,w] = np.a(chǎn)max(patch, axis=(0,1))

       return max_pooling_output

   def back_prop(self, dE_dY):

       dE_dk = np.zeros(self.image.shape)

       for patch,h,w in self.patches_generator(self.image):

           image_h(yuǎn), image_w, num_kernels = patch.shape

           max_val = np.a(chǎn)max(patch, axis=(0,1))


           for idx_h(yuǎn) in range(image_h(yuǎn)):

               for idx_w in range(image_w):

                   for idx_k in range(num_kernels):

                       if patch[idx_h(yuǎn),idx_w,idx_k] == max_val[idx_k]:

                           dE_dk[h*self.kernel_size+idx_h(yuǎn), w*self.kernel_size+idx_w, idx_k] = dE_dY[h,w,idx_k]

           return dE_dk

構(gòu)造函數(shù)方法只分配核大小值。以下方法與卷積層的方法類似,主要區(qū)別在于反向傳播函數(shù)不更新任何權(quán)重。事實(shí)上,池化層不依賴于權(quán)重來執(zhí)行。

Sigmoid層實(shí)現(xiàn)

class SoftmaxLayer:

   def __init__(self, input_units, output_units):

       self.weight = np.random.randn(input_units, output_units)/input_units

       self.bias = np.zeros(output_units)

   def forward_prop(self, image):

       self.original_shape = image.shape

       image_flattened = image.flatten()

       self.flattened_input = image_flattened

       first_output = np.dot(image_flattened, self.weight) + self.bias

       self.output = first_output

       softmax_output = np.exp(first_output) / np.sum(np.exp(first_output), axis=0)

       return softmax_output

   def back_prop(self, dE_dY, alpha):

       for i, gradient in enumerate(dE_dY):

           if gradient == 0:

               continue

           transformation_eq = np.exp(self.output)

           S_total = np.sum(transformation_eq)

           dY_dZ = -transformation_eq[i]*transformation_eq / (S_total**2)

           dY_dZ[i] = transformation_eq[i]*(S_total - transformation_eq[i]) / (S_total**2)

           dZ_dw = self.flattened_input

           dZ_db = 1

           dZ_dX = self.weight

           dE_dZ = gradient * dY_dZ


           dE_dw = dZ_dw[np.newaxis].T @ dE_dZ[np.newaxis]

           dE_db = dE_dZ * dZ_db

           dE_dX = dZ_dX @ dE_dZ

           self.weight -= alpha*dE_dw

           self.bias -= alpha*dE_db

           return dE_dX.reshape(self.original_shape)

softmax層使最大池提供的輸出體積變平,并輸出10個(gè)值。它們可以被解釋為與數(shù)字0–9相對應(yīng)的圖像的概率。

結(jié)論

你可以克隆包含代碼的GitHub存儲庫并使用main.py腳本。該網(wǎng)絡(luò)一開始沒有達(dá)到最先進(jìn)的性能,但在幾個(gè)epoch后達(dá)到96%的準(zhǔn)確率。

參考引用image.png

       原文標(biāo)題 : 使用Numpy從頭構(gòu)建卷積神經(jīng)網(wǎng)絡(luò)

聲明: 本文由入駐維科號的作者撰寫,觀點(diǎn)僅代表作者本人,不代表OFweek立場。如有侵權(quán)或其他問題,請聯(lián)系舉報(bào)。

發(fā)表評論

0條評論,0人參與

請輸入評論內(nèi)容...

請輸入評論/評論長度6~500個(gè)字

您提交的評論過于頻繁,請輸入驗(yàn)證碼繼續(xù)

  • 看不清,點(diǎn)擊換一張  刷新

暫無評論

暫無評論

人工智能 獵頭職位 更多
掃碼關(guān)注公眾號
OFweek人工智能網(wǎng)
獲取更多精彩內(nèi)容
文章糾錯
x
*文字標(biāo)題:
*糾錯內(nèi)容:
聯(lián)系郵箱:
*驗(yàn) 證 碼:

粵公網(wǎng)安備 44030502002758號