接前一篇的內容:自己動手寫深度神經網路框架(一);然後先附上完整實現

leeroee/CNN?

github.com
圖標

之前介紹到了Softmax函數:

softmaxleft(x_{i}
ight)=frac{e^{x_{i}}}{sum_{j} e^{x_{j}}}

可以看到,它接受一個向量(或者一組變數)作為輸入,每個變數指數化後除以所有指數化變數之和,(順便說一下,sigmoid函數就是其輸入數為2時的特例),有點類似於對輸入進行歸一化,事實上它就叫做歸一化指數函數

為了更直觀的理解先來看一個栗子,現有一個向量如下

x=left[ egin{array}{l}{2} \ {1} \ {5}end{array}
ight]

將其作為softamax的輸入,先對其進行指數化:

e^{x}=left[ egin{array}{l}{e^{x_{1}}} \ {e^{x_{2}}} \ {e^{x_{3}}}end{array}
ight]=left[ egin{array}{c}{e^{2}} \ {e^{1}} \ {e^{5}}end{array}
ight]

最後就是:

softmax(x)=left[ egin{array}{c}{frac{e^{2}}{e^{2}+e^{1}+e^{5}}} \ {frac{e^{1}}{e^{2}+e^{1}+e^{5}}} \ {frac{e^{5}}{e^{2}+e^{1}+e^{5}}}end{array}
ight]=left[ egin{array}{l}{0.0466} \ {0.0171} \ {0.9362}end{array}
ight]

得到的結果之和為1,每一個分量表示其概率,例如,如果是手寫數字識別的話,輸入數據經過若干個層的計算後投入到softmax中,得到一個長度為10(從0到9)的向量,每個分量的值表示個所要識別的數字為該數字的概率,取概率最高的作為結果

基本上的原理和功能都說明白了,不過在實際計算過程中有事會遇到一些問題,如溢出

從上面的式子中可以看出,如果輸入的x有某個值非常的的話,再對其進行指數化的話,就可能因為數字過大而導致溢出,所以在進行計算之前需要讀輸入x進行一些處理,將其每個分量都減去最大的那個值,用在上面的情況就是:

x=left[ egin{array}{l}{2-5} \ {1-5} \ {5-5}end{array}
ight]=left[ egin{array}{l}{-3} \ {-4} \ {0}end{array}
ight]

softmax(x)=left[ egin{array}{c}{frac{e^{-3}}{e^{-3}+e^{-4}+e^{0}}} \ {frac{e^{-4}}{e^{-2}+e^{-4}+e^{0}}} \ {frac{e^{0}}{e^{-3}+e^{-4}+e^{0}}}end{array}
ight]=left[ egin{array}{l}{0.0466} \ {0.0171} \ {0.9362}end{array}
ight]

可以看到,這樣並不會改變最終的結果

線性層和激活層的forward都說完了,接下來就是損失函數了

首先,為什麼要有損失函數?

在有監督的訓練中,將訓練樣本的數據投入到網路中,得到一個輸出結果,同時你有該樣本的真值;例如數字識別,將一個數字的圖片輸入到網路中,得到網路的輸出結果(即網路判斷這是數字幾),然後將這個判斷的數字和真實值進行比較,根據結果進而調整網路的權重,以期提升網路針對這一任務的表現

如何進行比較呢?這就需要用到損失函數了,例如均方誤差損失(MSELoss):

M S E L o s s=frac{1}{n} sum_{i}left(y_{i}-a_{i}
ight)^{2}

這只是多種誤差函數的一種,不同的任務會有不同的損失函數,例如上述的softmax,它用作多分類任務的輸出,而該類任務的損失函數叫做交叉熵損失(CrossEntropyLoss):

Loss=-sum_{i} y_{i} ln a_{i}

關於交叉熵更多詳細的內容可以去百度查一下,這裡只要知道它合適作為分類任務的損失函數即可

有了損失函數,就可以衡量網路對給定樣本的輸出值與樣本的真值之間的差距,所以只要朝著減小這個差距的方向去改變網路的權重,就能夠使網路的輸出越來越接近真值,而這個改變權重的方法即梯度下降

什麼是梯度?

在此之前,我想應該都了解導數是什麼吧,進一步的,先說一下偏導數是什麼

簡單的栗子,一個二元(或多元)函數 y=fleft( x_1,x_2 
ight) ,如果要求y關於變數x1(或者x2)的偏導數 frac{partial y}{partial x_{1}} (或 frac{partial y}{partial x_{2}} ):則把另一個變數當做常數處理,然後求y關於該變數的導數即可

理解偏導數之後,梯度簡單的來說就是,有一個函數: y=fleft( x 
ight)

其中x是一個向量: x=left[ egin{array}{llll}{x_{1}} & {x_{2}} & {cdots} & {x_{n}}end{array}
ight]

那麼該函數關於x的梯度 
abla_{x} f(x) (或 
abla_{x} y )就是由n個偏導數組成的向量


abla_{x} y=left[ egin{array}{lll}{frac{partial y}{partial x_{1}}} & {frac{partial y}{partial x_{2}}} & {cdots} & {frac{partial y}{partial x_{n}}}end{array}
ight]

而梯度下降說白了就是讓梯度減小,即讓各個分量的偏導減小

圖片來自網路

如上圖,xy軸是網路的參數(權重w和偏置b),z軸是損失函數的值,也可以是成本函數(它們有一些區別,以後會講,不過這裡先把它們看做一回事)

那麼梯度下降就是求得損失函數關於w(和b)的梯度 
abla_{W} L o s s (為方便下面直接寫作 
abla W ),然後對W進行更新,進而降低Loss的值:

W = W - eta 
abla W eta 是學習率,一個實數,自己調整

所以要降低損失,關鍵就在於如何求得W的梯度了;問題是,怎麼求?

從上面損失函數的定義可以看到,和W並沒有什麼關係,直接求根本就沒地方下手

但是直接的關係沒有,間接的影響還是有的,網路的輸出就是根據W和b算出來的,所以根據鏈式法則,即可求得我們想要的梯度


abla_{W} L o s s=frac{partial L o ss}{partial W} =frac{partial L o ss}{partial a} frac{partial a}{partial W}

如果一個網路有n層的話,那麼要求的第1層的參數的梯度那就是:


abla_{W^1} L o s s=frac{partial L o s s}{partial W^{1}} =frac{partial L o s s}{partial a^{n}} frac{partial a^{n}}{partial a^{n-1}} dots frac{partial a^{1}}{partial W^1}

但是,並不是每一層的梯度都要完完全全的算這麼一遍的

上面的 a^1a^2 …… a^n 就是forward中各層計算的輸出值,同時也是下一層的輸入值

所以,各層只需要根據上一層傳回來的梯度,本層有參數需要更新就更新,然後計算出本層的分量與之相乘傳給下一層就行了

就上面的栗子而言,損失函數這一級只需要負責計算 frac{partial L o s s}{partial a^{n}} 並返回就可以了

然後是第n層,它得到了 frac{partial L o s s}{partial a^{n}} ,如果該層有參數 W^n ,算出 frac{partial a_n}{partial W^{n}} 即可得到 
abla_{W^n} L o s s

如果沒有參數,那麼那隻需要算出 frac{partial a^{n}}{partial a^{n-1}} ,然後得到 frac{partial Loss}{partial a^{n}}frac{partial a^{n}}{partial a^{n-1}} 將其返回就行了

到了n-1層,它得到的就是 frac{partial Loss}{partial a^{n}}frac{partial a^{n}}{partial a^{n-1}} ,也只需要負責自己這一層的部分就行了,有參數更新參數,沒參數就計算梯度返回

所以,到了第一層的話,它得到的肯定是 frac{partial L o s s}{partial a^{n}} frac{partial a^{n}}{partial a^{n-1}} dots frac{partial a^{2}}{partial a^{1}} ,也只需要計算自己這一層的部分就行了

以上,就是反向傳播的過程了

大致的流程清楚了,那麼接下來就是對梯度進行推導了

損失函數關於輸出值的梯度,就用交叉熵損失函數來做實例吧,畢竟後面用的比較多,弄明白計算原理還是比較好,而且前面提到的MSELoss也很簡單,感興趣的可以自己推導一下

因為梯度是一個向量


abla_{a} L o s s=left[ egin{array}{lll}{frac{partial L o s s}{partial a_{1}}} & {frac{partial L o s s}{partial a_{2}}} & {cdots} & {frac{partial L o s s}{partial a_{n}}}end{array}
ight]

所以先求它的各個分量

frac{partial L o s s}{partial a_{1}}=frac{partial}{partial a_{1}}left(-sum_{i} y_{i} ln a_{i}
ight)

=-frac{partial y_{1} ln a_{1}}{partial a_{1}} 這一步看不明白的可以看看偏導數的定義

-y_{1} frac{partial ln a_{1}}{partial a_{1}}=-frac{y_{1}}{a_{1}}

同理

frac{partial L o s s}{partial a_{n}}=-frac{y_{n}}{a_{n}}

所以


abla_{a} L o s s=left[-frac{y_{1}}{a_{1}}-frac{y_{2}}{a_{2}} cdots-frac{y_{n}}{a_{n}}
ight]

如果寫成代碼就是:

def gradient(output, target):
return -target / output # target就是上面公式中的y

接下來是是softmax層的推導,先回顧下softmax函數的公式:

a_{i}=frac{e^{x_{i}}}{sum_{n} e^{x_{n}}}

然後其backward中的需要計算的就是:

frac{partial a}{partial x}=left[frac{partial a}{partial x_{1}} frac{partial a}{partial x_{2}} cdots frac{partial a}{partial x_{n}}
ight]

這個就比較坑了,因為a也是個向量,所以向量對向量求導結果就是一個矩陣(也叫Jacobian矩陣)

因為a也是向量,所以

frac{partial a}{partial x_{1}}=left[frac{partial a_{1}}{partial x_{1}} frac{partial a_{2}}{partial x_{1}} cdots frac{partial a_{n}}{partial x_{1}}
ight]^{T}

首先求分量,然後總結規律(前方數學公式預警,不感興趣可以直接跳到後面看結論)

frac{partial a_{1}}{partial x_{1}}=frac{partial}{partial x_{1}}left(frac{e^{x_{1}}}{sum_{n} e^{x_{n}}}
ight)

可以看到,分子很分母都和 x_1 有關聯,所以設分子分母分別是x_1的函數:

uleft(x_{1}
ight)=e^{x_{1}}

vleft(x_{1}
ight)=sum_{n} e^{x_{n}}

所以之前的式子就變成了一個複合函數求導,根據公式

frac{partial a_{1}}{partial x_{1}}=left(frac{u}{v}
ight)^{prime}

=frac{u^{prime} v-v^{prime} u}{v^{2}}

分別求得:

u^{prime}=u^{prime}left(x_{1}
ight)=e^{x_{1}}

v^{prime}=v^{prime}left(x_{1}
ight)=e^{x_{1}}

代入得到:

frac{u^{prime} v-v^{prime} u}{v^{2}}=frac{e^{x_{1}}}{sum_{n} e^{x_{n}}}left(1-frac{e^{x_{1}}}{sum_{n} e^{x_{n}}}
ight)=a_{1}left(1-a_{1}
ight)

然後是:

frac{partial a_{2}}{partial x_{1}}=frac{partial}{partial x_{1}}left(frac{e^{x_{2}}}{sum_{n} e^{x_{n}}}
ight)

因為這裡只有分母是和 x_1 有關,分子可以當做一個常量,所以:

frac{partial a_{2}}{partial x_{1}}=-frac{e^{x_{2}}}{left(sum_{n} e^{x_{n}}
ight)^{2}} frac{partial sum_{n} e^{x_{n}}}{partial x_{1}}

=-frac{e^{x_{2}}}{sum_{n} e^{x_{n}}} frac{e^{x_{1}}}{sum_{n} e^{x_{n}}}

=-a_{1} a_{2}

於是可以總結得到:

frac{partial a}{partial x_{1}}=left[a_{1}left(1-a_{1}
ight) quad-a_{1} a_{2} quad cdots quad-a_{1} a_{n}
ight]^{T}

frac{partial a}{partial x_{2}}=left[-a_{2} a_{1} quad a_{2}left(1-a_{2}
ight) cdots-a_{2} a_{n}
ight]^{T}

……

frac{partial a}{partial x_{n}}=left[ egin{array}{lll}{-a_{n} a_{1}} & {-a_{n} a_{2}} & {dots} & {a_{n}left(1-a_{n}
ight) ]^{T}}end{array}
ight.

最後,將這些列向量組成一個矩陣,就得到了softmax中backward的計算結果

frac{partial a}{partial x}=left[ egin{array}{cccc}{a_{1}left(1-a_{1}
ight)} & {-a_{2} a_{1}} & {cdots} & {-a_{n} a_{1}} \ {-a_{1} a_{2}} & {a_{2}left(1-a_{2}
ight)} & {cdots} & {-a_{n} a_{2}} \ {vdots} & {vdots} & {ddots} & {vdots} \ {-a_{1} a_{n}} & {-a_{2} a_{n}} & {cdots} & {a_{n}left(1-a_{n}
ight)}end{array}
ight]

看起來有點複雜,而且寫成代碼的話好像沒有隻有一行的解決辦法

但是,真正使用的時候並不用這麼複雜,因為Softmax和CrossEntropyLoss經常是結合在一起使用的,而且Softmax層本身又沒有參數,所以可以將兩者的backward放在一起

frac{partial Loss }{partial x}=left[frac{partial L}{partial a_{1}} frac{partial a_{1}}{partial x_{1}} frac{partial L}{partial a_{2}} frac{partial a_{2}}{partial x_{2}} cdots frac{partial L}{partial a_{n}} frac{partial a_{n}}{partial x_{n}}
ight]

frac{partial L}{partial a_{1}} frac{partial a_{1}}{partial x_{1}}=-y_{1}+y_{1} a_{1}+y_{2} a_{1}+cdots+y_{n} a_{1}

=-y_{1}+a_{1} sum y_{n}

因為在多分類任務中,訓練樣本的真值只有1個為真,其餘都為假,例如數字識別的話,如果訓練樣本的是數字3,那麼其真值就是:

y=left[ egin{array}{ccccccccc}{0} & {0} & {0} & {1} & {0} & {0} & {0} & {0} & {0} & {0}end{array}
ight]

所以  sum y_{n}=1

frac{partial L}{partial a_{1}} frac{partial a_{1}}{partial x_{1}}=a_1-y_1

相應的

frac{partial L}{partial a_{n}} frac{partial a_{n}}{partial x_{n}}=a_n-y_n

所以如果是訓練多分類的任務的話,一般就不在最後一層加上Softmax,而是直接將Linear層的輸出作為網路的輸出,然後和真值一起代入到交叉熵損失中,這樣在反向傳播時計算量就小得多

class CrossEntropyLoss(object):
def __init__(self):
self.classifier = Softmax()

def __call__(self, a, y, requires_acc=True):
self.a = self.classifier.forward(a)
self.y = y
batch_size = len(y)
loss = np.log(self.a) * y
loss = loss.sum() / batch_size
if requires_acc:
acc = np.count_nonzero(np.argmax(self.a, axis=-1) == np.argmax(y, axis=-1)) / batch_size
return acc, -loss
return -loss

def gradient(self):
return self.a - self.y

上面是計算交叉熵損失的實現,其自帶了一個Softmax作為分類器,在計算損失時先將output和target保存,這樣在計算gradient時一行代碼就能解決問題,另外計算損失時加了一個參數,除了返回損失值之外,還能根據需要計算網路輸出的正確率


推薦閱讀:
相关文章