python手寫神經網絡之用im2col實現卷積層、池化層

簡介

im2col就是img to colomn主,要是把圖像轉成column,原因和用途也很清晰,CNN中數據是四維的,並且有滑動窗口的存在,如果用for循環,計算效率不敢看。

 

那麼原理也很簡單,展開、複製、向量化。

 

但是從示意圖到實現,還是有一個地方比較繞,所以我一下也沒想到實現,還是看着參考代碼分析了一下才明白。

 

示意圖:

其實示意圖可以寫兩種,不帶batch的,和帶batch的,前者更簡單,但是隻算一個循序漸進的推導過程,無論如何最終都要用帶batch的版本,所以我就直接帶batch畫展開圖了。(前者的一個好處是圖不復雜,方便再一次展開觀察廣播過程,比如把filter複製N次等操作,所以兩種最好都畫一下,因爲廣播操作交給numpy去做了,所以帶batch的版本就隱藏廣播細節了,不然沒法畫)

其實宏觀的我這樣畫應該很直觀了,就是把圖片按一個滑窗一個行來轉換,batch反應在行數上,其實不增加理解的難度。

filter每個作爲一列,最後行列直接矩陣乘法,每個行和每個列都有交集,對應地,每個具體的窗口數據和每個具體的filter都應該有交集。

 

 

這是具體實現的shape操作,col有6個維度,這裏可能是稍微有那麼點抽象的,但是核心思路就一樣,實現圖一的展開效果,具體怎麼抽象,其實都只是實現細節,不過爲了便於理解這個細節,我還是按照這個過程畫了圖四。

 

 

 

圖三:代碼部分:

for循環略微抽象!

抽象的點主要在哪?在於,7*7的輸入,5*5的kernel,3*3的輸出。不是以窗口視角通過3*3的循環提取了9次5*5的塊。而是提取了25次3*3的塊。

其實很好理解,其實就是摳出來了(5,5)份的(3,3)個對應位置的數值,仔細看y_max=y+stride*out_h和y:y_max:stride,這兩個細節,證明每次的切塊都不是一個原圖中的實際(緊密形狀)的切塊,而是有跳躍的。

直覺:我想要3*3個5*5的切塊,結果,得到5*5個3*3的切塊,每一個3*3的切塊,包含了全部9個輸出各自的一個組成部分(二十五個組成部分之一,如果你要算上bias,相當於輸出的二十六個組成部分之一)。這種多維空間的思考很看幾何直覺的,如果實在不理解,可以多畫圖,多用變量描述,對於我,到這一步已經很容易理解了,所以不再過多的展開,過於繁瑣。建議實在不行就跳過(我在後邊已經做了我認爲更符合直覺的實現方案,供參考)不理解也沒關係,爲什麼有這麼“抽象”的一步,主要就是爲了下邊的reshape能夠按意願去操作(這操作其實是和圖一計算的shape呼應的),最後,transpose之後就符合直覺了!

 

 

圖四:專門針對循環內部的那個用OH和OW“滑窗”的操作做的等價轉換。

儘量用比較直覺的形式去表達,但是因爲再畫就太大了,所以點到爲止,可以看到,5*5個3*3的切片,都拿出切塊中的第九個點,就組成了原始的第九個窗口(對應第九個輸出所對應的輸入)

 

那麼問題來了,爲什麼不能寫一個符合直覺的循環呢?爲什麼不直接用滑窗的形式去提取?並且也省略了transpose那一步。因爲效率?transpose操作本身不花時間嗎?是和內存排列有關,這種切法效率更高?

 

所以我怎麼可能不去嘗試呢(實現我個人的im2col)?

一方面是探究如果不這樣是不是就不能實現,另一方面也爲了給大家展示一個符合直覺的過程!

(專門去搜了一下,網上關於im2col的中文內容,大多是複製了代碼,加了解釋,目前沒看到有像我一樣換方法實現的,所以至少作爲一個探索吧)

下面是我自己的實現——以滑窗方塊視角去操作,二維上(實際是四維的,有N和C),每次循環一個5*5的方塊,一共3*3次循環(size按之前的假定):

注意一致性,col的定義變了,循環要變,reshape也要按照圖一的目的做相應改變(transpose不能完全省略,還是要改變channel的位置的)。

 

 

至少從實現結果上,應該是一致的,那麼效率呢,直覺上,他的切塊比我還跳躍,也不太像是根據內存去跳的(顯然不是),所以至少他沒理由比我快吧。

 

 

下面計算一下時間,反而我的實現還更快一些!!

 

 

雖然自己做的更改看起來是能達到同樣效果,並且效率還更高,不過保留其他可能存在的侷限性,比如反向操作col2img的難度(col2img作爲一個函數,input肯定是固定的,output既然也一樣,那麼好像col2img哪怕共享同一種實現,也是不干擾的。)

 

另一方面,我好像也不能完全說第一個參考實現不符合直覺,可能是視角不同,但是我感覺那種略抽象。他是以一個卷積核元素爲單位去看的。for循環內,一次一個卷積核元素和它所有對應的滑動輸入。另一個角度看,也許他爲了反向傳播更符合直覺?並且兩者更一致!!

 

另外一個就是整體賦值的自動匹配問題

col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col[:,:,y,x,:,:] = img[:,:,y_start:y_start + filter_h, x_start:x_start+filter_w]

切片的形狀分別是(N,C,3,3)和(N,C,5,5),col定義中,前兩者是NC,賦值中,固定了y,x,剩下那(3,3)和(5,5)就自動匹配上了。

 

 

 

隨便給出im2col兩個例子:

其實很直觀,batch是疊加在外邊的,等於“行”數shape[0]翻倍。

shape[1]很好計算,只和filter size和channel數量有關

 

 

最後是把im2col應用到卷積網絡,實現一個卷積層。

前向傳播:

主要就是注意im2col的輸出形狀,結合圖二完成相應轉換和dot操作,然後就是四維形狀的還原(圖二中的N*OH*OW是關鍵,現在reshape就按這個,先摺疊成四維,最後再把channel移到axis=2)

注意自定義的b的形狀,如果寫錯,經過廣播,out就錯了!一個輸出通道只有一個b,也就是按下邊的FN通用就可以了。

 x是隨機的三維圖像,7*7像素,完成一個簡單的卷積層和前向傳播。

 

反向傳播相關

首先看一下網絡部分,不考慮col2im的話,宏觀看,其實很簡單,就當一個FC層去看,幾乎一樣,除了一些shape關係,要和前向傳播對應,我也畫了圖。結合圖一圖二的話,會很簡單。

 

 

同樣的,col2im也會繞一些,建議畫圖理解,或者跳過此部分。其實繞的也就是6維數組的操作,整體是很簡單的,建立空的img,pad,把梯度從col再逐漸放回到img中,(結果img其實是dimg,或者叫dx)

 

 

接下來是池化層:

如果只是單純看一下《深度學習入門》,大概看一下內容,就會覺得“你說的都對”,但是當我思考細節的時候,因爲共用了im2col接口,那麼im2col接口是按卷積核切片來做的操作(channel並不是一個獨立層級,而是以切塊的形式和每個窗口綁定,也就是C*FH*FW),怎麼會是圖中那樣呢——整個C(1)裏的所有FH*FW整齊排列(一共OH*OW行),然後再下一個C(2)?仔細一推,發現書裏有一個小bug!!(也可能他爲了便於讀者理解吧,im2col和池化層代碼並非作者原創,但是可能不便於他表達,所以就改了,至少圖片和代碼確實是不一致的,但是兩種過程是可以等價的,只要軸擼的清楚,怎麼操作都行)

C在最後,就註定不可能出現圖中的形狀!圖中的形狀,C的邏輯維度要比OH和OW高!

 

 

那麼根據我對代碼的理解,實際流程是這樣的(圖中上半部分,而書中應該對應圖中下半部分,書中的圖忽略了N,我帶上了,其實在最外圍,不影響,變量太多,不知道怎麼表達合適了,核心思路大概是這樣。)

如果想要書中那種效果,其實需要reshape+transpose+reshape,略微繁瑣,但是隻要搞清楚軸的關係,其實也不復雜!

 

 

下面用代碼驗證一下:

手動生成通道1的數據,後邊的被遮擋了,就隨便複製幾個。觀察通道1即可。

 

在np.max之前的這一步,和書中的圖示不匹配,通道1的四個輸出並沒有連續分佈,而是被分割開,證明了我前邊說的

 

這類bug在深度學習中太常見了,經常有論文的描述和代碼不一致,比如按論文的操作(記得是ResNet和RCNNs之類的,讀者廣泛地存在困惑),224*224的圖最後得到7*7的結果那裏就很困惑,其實前邊有一層padding,把圖變成230*230了,所以還是要多研究代碼,看論文千遍不如過代碼一遍。

 

 

最後是pooling的bp過程:

代碼和示意圖有些不同(代碼是新建的一個dmax,而非在原數據上擴充維度),意思是一樣的,總之,可以參考圖一的流程和圖二的關鍵節點形狀。核心思路也是一樣的,從(N,C,H,W)的形狀變成平鋪,(BP過程)再通過col2im恢復到img形狀。

 

 

發佈了183 篇原創文章 · 獲贊 376 · 訪問量 116萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章