CNN反向传播卷积核翻转
前言
前面煞費苦心地嚴格按照上下標證明BP,主要就是為了鍛煉自己的證明時候的嚴謹性,那么這里也嚴格按照上下標的計算方法推導為何卷積的反向傳播需要將卷積核旋轉180°
粗略證明
回顧一下BP的第l層第i個偏置的更新
這里面的 δl+1j其實是誤差值對第 l+1層的第 j個單元的偏置的導數,也就是下一層偏置的梯度,從而構成遞歸形式的求解,逼格叫法就是鏈式求導。
再來看CNN,粗略地說,每個卷積核K對應的局部輸入
X與輸出值y可以構成一個簡單的BP網絡,即
X=[x11x21x12x22]K=[k11k21k12k22]
zy=conv(K,X)+θ=k11x11+k12x12+k21x21+k22x22+θ=σ(z)
這里強調一下,其實正常的卷積是這樣的
但是為了證明方便,我們就不做這種運算了,直接當做相干來做,但是面試什么的人家問卷積,一定要知道相干不是卷積。
假設X的每個單元對應偏置是bij,這樣我們就可以套用BP的那個偏置更新式子去求解?E?bij,第一項δl+1j這一項不管了,鏈式求解它,后面再說它的邊界,即全連接部分的計算方法;第二項Wl+1ji代表連接y與第(i,j)位置的輸入神經元的權重kij; 最后一項是當前層輸入值函數的導數,比如當函數是sigmoid的時候σ′(z)=z(1?z),所以
看到這里如果你沒有疑問,那就是你卷積的前向操作沒學好了。如果學過多通道卷積,肯定會問“同一塊特征圖的所有單元應該共享偏置啊,為什么這里特征圖的每個神經元都有一個偏置?”這個問題一般的解決方法是對同一塊特征圖所求得的偏置向量求和作為當前特征圖需要更新的偏置量,詳細后面看代碼實現。
關鍵的一步來了,如何使用卷積來實現這個式子的三個乘法,其實主要體現在如何使用卷積來實現δl+1×kij,使得計算的結果大小剛好是X這么大的維度。如何將圖形越卷積維度越大?擯棄CNN中的卷積,單考驗你對卷積在圖像處理中的操作及其作用,如果想不出來個一二三,建議學習一下那本綠皮的《數字圖像處理》,作者好像叫岡薩雷斯,如果喜歡編程,可以看很多視覺庫中的卷積操作,比如matlab中關于卷積的三種處理,詳見此博客,我們使用full convolution的卷積操作,通過對特征圖邊緣填充零使其維度變大,然后再執行卷積即可。
針對上例,方法是
[δl+1k11δl+1k21δl+1k12δl+1k22]=conv??????0000δl+10000???,[k22k12k21k11]???=conv??????0000δl+10000???,rot(K,180°)???
這便說明了,卷積里面反向傳播為什么翻轉卷積核?這個證明就是原因。
代碼實現
在matlab的deeplearning toolbox中,將sigmoid作為激活函數,因而實際的當前層的偏置計算方法為:下一層的偏置矩陣先做補零擴充,然后與卷積核的180°翻轉做卷積,得到的矩陣與當前層的神經元對應元素做乘法即可,還有一些其它技巧依據代碼做補充。
網絡預設
先看看如何設計網絡,其實主要就是看每層權重和偏置的維度罷了:
池化:
if strcmp(net.layers{l}.type, 's')mapsize = mapsize / net.layers{l}.scale;assert(all(floor(mapsize)==mapsize), ['Layer ' num2str(l) ' size must be integer. Actual: ' num2str(mapsize)]);for j = 1 : inputmapsnet.layers{l}.b{j} = 0;end end當前層是池化層的時候,沒權重,且偏置為0,主要是因為池化操作只是簡單的下采樣操作,用于降低卷積后的特征圖的維度,不需要使用權重和偏置做運算。
卷積:
if strcmp(net.layers{l}.type, 'c')mapsize = mapsize - net.layers{l}.kernelsize + 1;fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2;for j = 1 : net.layers{l}.outputmaps % output mapfan_in = inputmaps * net.layers{l}.kernelsize ^ 2;for i = 1 : inputmaps % input mapnet.layers{l}.k{i}{j} = (rand(net.layers{l}.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out));endnet.layers{l}.b{j} = 0;endinputmaps = net.layers{l}.outputmaps; end這里注意看一下,針對第l個卷積操作連接第i個輸入特征圖和第j個輸出特征圖的卷積核,使用fan_in,fan_out準則初始化,其實這就間接告訴我們,兩層特征圖之間的卷積參數量為上一層特征圖個數×下一層特征圖個數(卷積核個數)×卷積核高×卷積核寬;針對第l層的第l次卷積操作的第j個輸出特征圖的共享偏置值設置為0。
前向運算
多通道卷積
我發現很多新手童鞋不知道多通道卷積到底是何種蛇皮操作,只要你記住,經過卷積后得到的特征圖個數等于卷積核個數就行了,切記不是卷積核個數乘以輸入特征圖個數。具體操作是先使用每個卷積核對所有特征圖卷積一遍,然后再加和,比如第二個特征圖的值的計算方法就是上一層的第1個特征圖與第2個卷積核卷積+上一層的第2個特征圖與第2個卷積核卷積+?,一直加到最后一個即可。
for j = 1 : net.layers{l}.outputmaps % for each output map% create temp output mapz = zeros(size(net.layers{l - 1}.a{1}) - [net.layers{l}.kernelsize - 1 net.layers{l}.kernelsize - 1 0]);for i = 1 : inputmaps % for each input map% convolve with corresponding kernel and add to temp output mapz = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid');end% add bias, pass through nonlinearitynet.layers{l}.a{j} = sigm(z + net.layers{l}.b{j}); end池化
采用均值池化,對上一層的輸出特征圖對應區域單元求加和平均,可以采用值為1的卷積核,大小由設定的池化區域決定
for j = 1 : inputmapsz = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid'); % !! replace with variablenet.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :); end全連接
經過多次卷積、池化操作后,為了做分類,我們把最后一層所有特征圖拉長并拼接成一個向量,連接標簽向量,這就叫全連接。
for j = 1 : numel(net.layers{n}.a)sa = size(net.layers{n}.a{j});net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))]; end最后計算輸出
net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));反向傳播
全連接部分
反向傳播必然是先計算全連接部分的誤差值,然后向前推到由特征圖拉長的向量的每個神經元的偏置,這也就是上面提到的為什么沒有共享偏置的根源,因為在反向傳播的第一層便對每個神經元單獨求解偏置更新量了。想想之前關于BP總結的那個式子Δc×W×B×(1?B),便可以得到誤差對于全連接層偏置的偏導數了。
% error net.e = net.o - y; % loss function net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);%% backprop deltas net.od = net.e .* (net.o .* (1 - net.o)); % output delta △c net.fvd = (net.ffW' * net.od); % feature vector delta △c X B if strcmp(net.layers{n}.type, 'c') % only conv layers has sigm functionnet.fvd = net.fvd .* (net.fv .* (1 - net.fv)); end然后再把全連接層的列向量分離成最后一層特征圖大小的塊,因為它本來就是由最后一層拉長的,很容易進行反操作還原回去。
sa = size(net.layers{n}.a{1}); fvnum = sa(1) * sa(2); for j = 1 : numel(net.layers{n}.a)net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3)); end現在技巧來了,我們知道池化層其實就是對卷積層的元素進行簡單的處理,比如每塊加和求均值,那么我們就可以粗略得將其還原回去,下述代碼就是當當前層由池化操作得來的時候,將第此層的偏置更新量擴大成上一層的輸入特征圖大小:
expand(net.layers{l + 1}.d{j}, [net.layers{l + 1}.scale net.layers{l + 1}.scale 1]) / net.layers{l + 1}.scale ^ 2然后有一個蛇皮操作就是,理論部分不是說過要計算σ(xij)么,換成sigmoid就是xij(1?xij),他這里提前進行了下一層偏置更新量與當前層值函數導數的乘積:
net.layers{l}.d{j} = net.layers{l}.a{j} .* (1 - net.layers{l}.a{j}) .* (expand(net.layers{l + 1}.d{j}, [net.layers{l + 1}.scale net.layers{l + 1}.scale 1]) / net.layers{l + 1}.scale ^ 2);回顧一下我們的偏置導數計算公式:
這一行代碼直接就完成了δl+1×σ′(xij)的操作,接下來直接乘以卷積核即可,注意是填充以后與原來卷積核的翻轉180°做卷積操作
z = z + convn(net.layers{l + 1}.d{j}, rot180(net.layers{l + 1}.k{i}{j}), 'full');因為是批量更新,所以需要對所有的偏置向量除以樣本總數
for l = 2 : nif strcmp(net.layers{l}.type, 'c')for j = 1 : numel(net.layers{l}.a)for i = 1 : numel(net.layers{l - 1}.a)net.layers{l}.dk{i}{j} = convn(flipall(net.layers{l - 1}.a{i}), net.layers{l}.d{j}, 'valid') / size(net.layers{l}.d{j}, 3);endnet.layers{l}.db{j} = sum(net.layers{l}.d{j}(:)) / size(net.layers{l}.d{j}, 3);endendend這里蘊含了利用偏置更新量計算權值更新量的操作,這個與BP一毛一樣,就是偏置更新量乘以前一層的單元值即可。
還有就是最后說的由于共享卷積核,所以同一卷積核的偏置更新量也應該一樣,直接求均值就行
net.dffW = net.od * (net.fv)' / size(net.od, 2); net.dffb = mean(net.od, 2);總結
以上是生活随笔為你收集整理的CNN反向传播卷积核翻转的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BP推导——续
- 下一篇: 【音频处理】离散傅里叶变换