之前用numpy完整的把CNN的整個過程都實現了一遍,這次上手一個比較有意思的項目:圖片的風格遷移,在弄明白原理和計算過程之後還是用numpy完整的實現一遍

這裡不對CNN的一些基本概念做解釋,如果對CNN不太熟悉的話,可以看一遍以前寫的一篇筆記,Numpy實現卷積神經網路識別MNIST,而且後面用到的卷積網路的代碼也是復用的這裡面的。

風格遷移,簡單的說就是提取一個圖片的風格(該圖片稱為風格圖片),如下面這張圖片:

風格圖片

然後將其投入到你的內容圖片中:

內容圖片

最後,得到一個基於內容圖片融合風格圖片的風格的生成圖片:

生成圖片

下面來看看具體的實現過程。

之前接觸到的神經網路,無論是DNN,還是CNN,都涉及到兩個大點,誤差的反向傳播,和參數的梯度下降。隨著訓練輪數的增加,各層的參數都在沿著梯度下降的方向更新,一步一步到誤差最小(局部)的點。在這裡,無論里訓練幾千幾萬次,變化的永遠是各層的參數,訓練數據,也就是輸入數據,本身在訓練過程中是不會被改變的

而風格遷移的實現中恰恰相反,網路是早就已經訓練好的,在生成圖片的過程中,各層的參數不會改變,改變的是輸入數據

上圖是一個神經網路的正、反向過程,其中輸入層沒有畫進來,但是可以看到,在反向傳播的過程里,誤差傳到最後一層後依然可以繼續往上,傳給輸入層,輸入層接收到傳來的誤差之後,一樣可以使用梯度下降演算法來對輸入數據進行更新。

在弄清楚風格遷移和普通的CNN的區別之後整個過程就好解釋了:

首先生成一個隨機圖片,大小和風格圖片、內容圖片一樣,例如,400x400x3(RGB三通道)

然後在網路中選擇一個中間層,分別將內容圖片,風格圖片,生成的隨機圖片作為輸入數據投入到網路中進行一遍forward過程,得到對應的激活值(也就是之前選的那一層的輸出值),分別記做 A_{content} (簡寫為 A_c )、 A_{style} ( A_s )、 A_{generated} ( A_g )。接著定義兩個損失函數

內容損失函數: L_{content}left( A_c,A_g 
ight) ,表示生成圖片和內容圖片直接的誤差

風格損失函數: L_{style}left( A_s,A_g 
ight) ,表示生成圖片和風格圖片之間的誤差

具體定義及求導後面再說,將這兩個損失加權求和,得到一個總的損失:

Lossleft( G 
ight)=alpha L_cleft( A_c,Ag 
ight)+eta L_sleft( A_s,Ag 
ight) alpha,eta 是兩個超參數

然後按照正常的反向傳播過程,層層加碼,最後得到生成圖片的梯度: frac{partial Lossleft( G 
ight)}{partial G}

最後按照梯度下降演算法(可以使用Adam)對生成圖片進行更新,以上有幾點需要注意的是:反向傳播的過程中沒有計算各層參數的梯度,生成圖片和風格圖片的激活值只需要計算一遍就可以了,一直反覆進行計算激活值更新梯度的是生成圖片。而且這裡只選定了一個中間層的輸出,所以直接可以把後面的層都截斷,將該層作為輸出層即可。

通過一次次的訓練,隨機生成的圖片會沿著梯度下降的方向更新,得到上面的結果,也可以通過調整alpha,eta的值,使生成的圖片更加偏向內容圖片或者風格圖片:

這是我用不同參數調整出來與上面不同的結果,雖然有點感覺整毀容了

接下來看兩個損失函數的具體定義和求導過程:

L_cleft( A_c,A_g 
ight)=frac{1}{2H	imes W	imes C} |A_c-Ag|_{2}=frac{1}{2H	imes W	imes C} sum_{i}^{H}{sum_{j}^{W}{sum_{k}^{C}{left( a_{i,j,k}^c - a_{i,j,k}^g 
ight)^{2}}}}

def MSELoss(output, target):
return np.sum(np.square(target - output))

content_loss = MSELoss(A_g, A_c) / (2*H*W*C)

H,W,C是激活值的維度大小,其實就也就普通的均方誤差損失函數(MSELoss)。所以直接貼結果了:

frac{partial L_cleft( A_c,A_g 
ight)}{partial A_g}=frac{1}{H	imes W 	imes C}left( A_g - A_c 
ight)

grad_content_loss = (A_g - A_c) / (H*W*C)

然後是風格損失函數,不過在此之前,需要先介紹一個叫做格拉姆矩陣的概念:

Gram格拉姆矩陣在風格遷移中的應用,然後是公式和代碼:

Gramleft( A 
ight)_{k,k}=sum_{i}^{H}{sum_{j}^{W}{a_{i,j,k}a_{i,j,k}}}

def Gram(A):
return np.tensordot(A, A, [(0,1), (0,1)])

G_s = Gram(A_s)
G_g = Gram(A_g)

而風格損失函數就是風格圖片和生成圖片的格拉姆矩陣的均方誤差:

L_sleft( A_s,A_g 
ight)=frac{1}{left( 2H	imes W 	imes C 
ight)^2}| Gramleft( A_s 
ight)-Gramleft( A_g 
ight) |_2

style_loss = MSELoss(Gram(A_g), Gram(A_s)) / (2*H*W*C)**2

然後就是求風格損失對於生成圖片激活值的梯度了: frac{partial L_sleft( A_s,A_g 
ight)}{partial A_g}

顯然,這是一個複合函數: frac{partial L_s left(A_{s}-A_{g}
ight)}{partial A_{g}}=frac{partial L_sleft(A_{s}-A_{g}
ight)}{partial Gramleft(A_{g}
ight)} frac{partial Gramleft(A_{g}
ight)}{partial A_{g}}

第一部分就比較簡單了,根據損失函數的定義:

frac{partial L_sleft(A_{s}-A_{g}
ight)}{partial Gramleft(A_{g}
ight)}=frac{1}{2left( H	imes W	imes C 
ight)^2}left( Gramleft( A_g 
ight) - Gramleft( A_s 
ight) 
ight)

剩下的稍微麻煩一點,先看一個例子:

Gram(A)=left[ egin{array}{lll}{G_{1,1}} & {G_{1,2}} & {G_{1,3}} \ {G_{2,1}} & {G_{2,2}} & {G_{2,3}} \ {G_{3,1}} & {G_{3,2}} & {G_{3,3}}end{array}
ight]

再回過頭看格拉姆矩陣的公式:

G_{k,k}=sum_{i}^{H}{sum_{j}^{W}{a_{i,j,k}a_{i,j,k}}}

所以,對於生成圖片激活值( A_g 是三維數據)的某一具體值 a_{i,j,k} 的梯度:

frac{partial G_{k, k}}{partial a_{i, j, k}}=a_{i, j, k}

就上面的例子而已, a_{i,j,1} 的值會影響到 left[ egin{array}{lll}{G_{11}} & {G_{12}} & {G_{13}}end{array}
ight]left[ egin{array}{l}{G_{11}} \ {G_{21}} \ {G_{31}}end{array}
ight] 的值,所以就需要用到全導數公式:

frac{partial L_sleft(A_{s},A_{g}
ight)}{partial a_{i, j, 1}}=sum_{k} frac{partial L_sleft(A_{s},A_{g}
ight)}{partial G_{1,k}^g} frac{partial G_{1,k}^g}{partial a_{i, j, 1}}+sum_{k} frac{partial L_sleft(A_{s},A_{g}
ight)}{partial G_{k,1}^g} frac{partial G_{k,1}^g}{partial a_{i, j, 1}}

=2sum_{k} frac{partial L_sleft(A_{s},A_{g}
ight)}{partial G_{1,k}^g} frac{partial G_{1,k}^g}{partial a_{i, j, 1}} (因為格拉姆矩陣是對稱的)

=2sum_{k} frac{1}{2(H 	imes W 	imes C)^{2}}left(G_{1, k}^{g}-G_{1, k}^{s}
ight)a_{i, j, k}

=frac{1}{(H 	imes W 	imes C)^{2}} sum_{k} a_{i, j, k}left(G_{1, k}^{g}-G_{1, k}^{S}
ight)

以上是公式,雖然有點點複雜,但是用代碼解決也就是一行的事:

grad_style_loss = np.tensordot(A_g, Gram(A_g) - Gram(A_s), (2, 1)) / (H*W*C)**2

然後將兩個損失的梯度按權重相加:

frac{partial Loss(G)}{partial A_{g}}=alpha frac{partial L_{c}left(A_{c}, A_{g}
ight)}{partial A_{g}}+eta frac{partial L_{s}left(A_{s}, A_{g}
ight)}{partial A_{g}}

grad_loss = alpha * grad_content_loss + beta * grad_style_loss

以上,就是兩個損失函數的推導全部過程了,現在你只需要找一個訓練好的卷積網路,以及2張圖片,就可以生成圖片了,我用的是VGG-16網路,以下是我自己的完整實現代碼,包括從VGG-16摳出來的各層卷積核參數,你可以自己做調整:

leeroee/CNN?

github.com
圖標

最後,做一個改進就是,在上面的實現中,內容損失和風格損失採用的都是同一層的輸出值做參數,論文中採用了各個不同層的風格損失,這樣的話效果更好一些

這裡有一個問題就是,如果使用多個層的風格損失,不同層的損失梯度如何往回傳,下面來看一個例子,設輸入是為 x,現有三個函數其關係如下:

a=aleft( x 
ight),b=bleft( a 
ight),c=cleft( b 
ight)

以及損失函數: Lc=Lossleft( c 
ight),Lb=Lossleft( b 
ight),L=Lb+Lc ,求輸入對於總體損失 Lc 的梯度

frac{partial L}{partial x}=frac{partial Lb}{partial x}+frac{partial Lc}{partial x}

= left( frac{partial Lb}{partial b} +frac{partial Lc}{partial c} frac{partial c}{partial b} 
ight) frac{partial b}{partial a} frac{partial a}{partial x}

經過上面的推導,可以知道,對於不同層的損失,可以先將後面的損失往回傳,然後到了下一個層的損失直接加上去,一起往回傳就可以了

可以想像一開始只有一個層的損失的實現是一架飛機,從起點到終點是直達的,中間沒有其他人上來(也上不來)

現在使用多層損失的實現變成了一列火車,從起點到終點,你有哪些站點(使用哪些層的損失)就會在哪裡停,然後到終點站後全部出來。

推薦閱讀:

相关文章