本章我們將介紹一個酷炫的深度學(xué)習(xí)應(yīng)用——風(fēng)格遷移(Style Transfer)。近年來,由深度學(xué)習(xí)引領(lǐng)的人工智能技術(shù)浪潮越來越廣泛地應(yīng)用到社會各個領(lǐng)域播歼。這其中,手機(jī)應(yīng)用Prisma掰读,嘗試為用戶的照片生成名畫效果秘狞,一經(jīng)推出就吸引了海量用戶叭莫,登頂App Store下載排行榜。這神奇背后的核心技術(shù)就是基于深度學(xué)習(xí)的圖像風(fēng)格遷移烁试。
風(fēng)格遷移又稱風(fēng)格轉(zhuǎn)換食寡,直觀點的類比就是給輸入的圖像加個濾鏡,但是又不同于傳統(tǒng)濾鏡廓潜。風(fēng)格遷移基于人工智能,每個風(fēng)格都是由真正的藝術(shù)家作品訓(xùn)練善榛、創(chuàng)作而成辩蛋。只需要給定原始圖片,并選擇藝術(shù)家的風(fēng)格圖片移盆,就能把原始圖片轉(zhuǎn)化成具有相應(yīng)藝術(shù)家風(fēng)格的圖片悼院。如下圖所示,給定一張風(fēng)格圖片(左上角咒循,手繪糖果圖)和一張內(nèi)容圖片(右上角据途,斯坦福校園圖),神經(jīng)網(wǎng)絡(luò)能夠生成手繪風(fēng)格的斯坦福校園圖(下圖)叙甸。
本章我們將一起學(xué)習(xí)風(fēng)格遷移的原理颖医,并利用PyTorch從頭實現(xiàn)一個風(fēng)格遷移的神經(jīng)網(wǎng)絡(luò),來看看人工智能與藝術(shù)的交叉碰撞會產(chǎn)生什么樣的有趣結(jié)果裆蒸。
8.1 風(fēng)格遷移原理介紹
風(fēng)格遷移中有兩類圖片熔萧,一類是風(fēng)格圖片,通常是一些藝術(shù)家的作品僚祷,比較經(jīng)典的有梵高的《星月夜》《向日葵》佛致,畢加索的《A muse》,莫奈的《印象-日出》辙谜,日本浮世繪的《神奈川沖浪里》等俺榆,這些圖片往往具有比較明顯的藝術(shù)家風(fēng)格,包括色彩装哆、線條罐脊、輪廓等;另一類是內(nèi)容圖片蜕琴,這些圖片通常來自現(xiàn)實世界中爹殊,例如用戶個人攝影。利用風(fēng)格遷移能夠?qū)?nèi)容圖片轉(zhuǎn)換成具有藝術(shù)家風(fēng)格的圖片奸绷。
2015年梗夸,來自德國圖賓根大學(xué)(University of Tubingen)Bethge實驗室的三位研究院萊昂-蓋提斯(Leon Gatys)、亞歷山大-昂抛恚克(Alexander Ecker)和馬蒂亞斯-貝特格(Matthias Bethge)研發(fā)了一種算法反症,模擬人類視覺的處理方式辛块,通過訓(xùn)練多層卷積神經(jīng)網(wǎng)絡(luò)(CNN),讓計算機(jī)識別并學(xué)會了梵高的“風(fēng)格”铅碍,然后將任何一張普通的照片變成梵高的《星空》润绵。2015年,他們的發(fā)現(xiàn)被整理成兩篇論文《A Neural Algorithm of Artistic Style》和《Texture Synthesis Using Convolutional Neural Networks》胞谈,引起了學(xué)術(shù)界和工業(yè)界的極大興趣尘盼。
Gatys等人提出的方法稱為Neural Style,然而他們的做法在實現(xiàn)上過于復(fù)雜烦绳,每次進(jìn)行風(fēng)格遷移都需要幾十分鐘甚至幾個小時的訓(xùn)練卿捎。斯坦福博士生Justin Johnson于2016年在ECCV上發(fā)表論文《Perceptual Losses for Real-Time Style Transfer and Super-Resolution》,提出了一種快速實現(xiàn)風(fēng)格遷移的算法径密,這種方法通常被稱為Fast Neural Style午阵。當(dāng)用Fast Neural Style訓(xùn)練好某一個風(fēng)格的模型之后,通常只需要GPU運行幾秒享扔,就能生成對應(yīng)的風(fēng)格遷移結(jié)果底桂。本章中介紹的主要是基于Justin Johnson的Fast Neural Style方法,即快速風(fēng)格遷移惧眠。
Fast Neural Style和Neural Style主要有以下兩點區(qū)別籽懦。
(1)Fast Neural Style針對每一個風(fēng)格圖片訓(xùn)練一個模型(在GPU上運行大概4個小時),而后可以反復(fù)利用氛魁,進(jìn)行快速風(fēng)格遷移(幾秒到20秒)猫十。Neural Style不需要專門訓(xùn)練模型,只需要從噪聲中不斷地調(diào)整圖像的像素值呆盖,直到最后得到結(jié)果拖云,速度較慢,需要十幾分鐘到幾十分鐘不等应又。
(2)普遍認(rèn)為Neural Style生成的圖片效果會比Fast Neural Style的效果好宙项。
關(guān)于Neural Style的實現(xiàn),可以參考PyTorch官方的Tutorial中的教程株扛,實現(xiàn)也比較簡單尤筐。本節(jié)主要介紹Fast Neural Style的實現(xiàn)。
要產(chǎn)生效果逼真的風(fēng)格遷移的圖片有兩個要求洞就。一是要生成的圖片在內(nèi)容盆繁、細(xì)節(jié)上盡可能地與輸入的內(nèi)容圖片相似;二是要生成的圖片在風(fēng)格上盡可能地與風(fēng)格圖片相似旬蟋。相應(yīng)地油昂,我們定義兩個損失content loss和style loss,分別用來衡量上述兩個指標(biāo)。
圖像的內(nèi)容和風(fēng)格含義廣泛冕碟,并且沒有嚴(yán)格統(tǒng)一的數(shù)學(xué)定義拦惋,具有很大程度上的主觀性,因此很難表示安寺。content loss比較常用的做法是采用逐像素計算差值厕妖,又稱pixel-wise loss,追求生成的圖片和原始圖片逐像素的差值盡可能小挑庶。這種做法有諸多不合理的地方言秸,Justin在論文中提出了一種更好的計算content loss的方法:perceptual loss。不同于pixel-wise loss計算像素層面的差異迎捺,perceptual loss計算的是圖像在更高語義層次上的差異举畸,論文中使用預(yù)訓(xùn)練好的神經(jīng)網(wǎng)絡(luò)的高層輸入作為圖片的知覺特征,進(jìn)而計算二者的差異值作為perceptual loss破加。
深度學(xué)習(xí)之所以被稱為“深度”,就在于它采用了深層的網(wǎng)絡(luò)結(jié)構(gòu)雹嗦,網(wǎng)絡(luò)的不同層學(xué)到的是圖像不同層面的特征信息范舀。深度學(xué)習(xí)網(wǎng)絡(luò)的輸入是像素信息,也可以認(rèn)為是點了罪,研究表明锭环,幾乎所有神經(jīng)網(wǎng)絡(luò)的第一層學(xué)習(xí)到的都是關(guān)于線條和顏色的信息,直觀理解就是像素組成色彩泊藕,點組成線辅辩,這與人眼的感知特征十分相像。再往上娃圆,神經(jīng)網(wǎng)絡(luò)開始關(guān)注一些復(fù)雜的特征玫锋,例如拐角或者某些特殊的形狀,這些特征可以看成是低層次的特征組合讼呢。隨著深度的加深撩鹿,神經(jīng)網(wǎng)絡(luò)關(guān)注的信息逐漸抽象,例如有些卷積核關(guān)注的是這張圖中有個鼻子悦屏,或者是圖中有張人臉节沦,以及對象之間的空間關(guān)系,例如鼻子在人臉的中間等础爬。
在進(jìn)行風(fēng)格遷移時甫贯,我們并不要求生成圖片的像素和原始圖片中的每一個像素都一樣,我們追求的是生成圖片和原始圖片具有相同的特征:例如原圖中有只貓看蚜,我們希望風(fēng)格遷移之后的圖片依舊有貓叫搁。圖片中“有貓”這個概念不就是我們分類問題最后一層的輸出嗎?但最后一層的特征對我們來說抽象程度太高,因為我們不僅希望圖片中有只貓常熙,還希望保存這只貓的部分細(xì)節(jié)信息纬乍,例如它的形狀、動作等信息裸卫,這些信息相對來說沒有那么高的層次仿贬。因此我們使用中間某些層的特征作為目標(biāo),希望原圖像和風(fēng)格遷移的結(jié)果在這些層輸出的結(jié)果盡可能相似墓贿,即將圖片在深度模型的中間某些層的輸出作為圖像的知覺特征茧泪。
我們一般使用Gram矩陣來表示圖像的風(fēng)格特征。對于每一張圖片聋袋,卷積層的輸出形狀為队伟,是卷積核的通道數(shù),一般稱為有個卷積核幽勒,每個卷積核學(xué)習(xí)圖像的不同特征嗜侮。每一個卷積核輸出的代表這張圖像的一個feature map,可以認(rèn)為是一張?zhí)厥獾膱D像——原始彩色圖像可以看作RGB三個feature map拼接組合成的特殊feature maps啥容。通過計算每個feature map之間的相似性锈颗,我們可以得到圖像的風(fēng)格特征。對于一個的feature maps 咪惠,Gram Matrix的形狀為击吱,其第、個元素的計算方式定義如下:
其中代表第個feature map的第個像素點遥昧。關(guān)于Gram Matrix覆醇,以下三點值得注意:
- Gram Matrix的計算采用了累加的形式,拋棄了空間信息炭臭。一張圖片的像素隨機(jī)打亂之后計算得到的Gram Matrix和原始Gram Matrix一樣永脓。所以可以認(rèn)為Gram Matrix拋棄了元素之間的空間信息。
- Gram Matrix的結(jié)果與feature maps 的尺度無關(guān)鞋仍,只與通道數(shù)有關(guān)憨奸。無論、的大小如何凿试,最后Gram Matrix的形狀都是排宰。
- 對于一個的feature maps,可以通過調(diào)整形狀和矩陣乘法快速計算它的Gram Matrix那婉,即先將調(diào)整為的二維矩陣板甘,然后再計算,結(jié)果計算Gram Matrix详炬。
下圖展現(xiàn)了Gram Matrix的特點:注重風(fēng)格紋理等特征盐类,忽略空間信息寞奸。圖中第一行是輸入的原圖片,經(jīng)過神經(jīng)網(wǎng)絡(luò)計算出不同層的Gram Matrix在跳,然后嘗試從這些層的Gram Matrix恢復(fù)出原圖枪萄,換一種角度說,我們可以認(rèn)為每一列的圖像的Gram Matrix值都很接近猫妙。我們可以明顯地看出瓷翻,無論恢復(fù)的圖像清晰度如何,圖像的空間信息在計算Gram Matrix時都被舍棄割坠,但是紋理齐帚、色彩等風(fēng)格信息被保存下來。
實踐證明利用Gram Matrix表征圖像的風(fēng)格特征在風(fēng)格遷移彼哼、紋理合成等任務(wù)中的表現(xiàn)十分出眾对妄。
總結(jié)如下。
- 神經(jīng)網(wǎng)絡(luò)的高層輸出可以作為圖像的知覺特征描述敢朱。
- 神經(jīng)網(wǎng)絡(luò)的高層輸出的Gram Matrix可以作為圖像的風(fēng)格特征描述剪菱。
- 風(fēng)格遷移的目標(biāo)是使生成圖片和原圖片的知覺特征盡可能相似,并且和風(fēng)格圖片的風(fēng)格特征盡可能地相似拴签。
在最初的Neural Style論文中孝常,隨機(jī)初始化目標(biāo)圖片為噪聲,然后利用梯度下降法調(diào)整圖片篓吁,使目標(biāo)圖片和風(fēng)格圖片的風(fēng)格特征(即Gram Matrix)盡可能地相似茫因,和原圖片的知覺特征也盡可能地相似蚪拦。這種做法生成的圖片效果很好杖剪,但其十分耗時!每次都需要從一個噪聲開始調(diào)整圖片驰贷,直到得到最終的目標(biāo)圖片盛嘿,在GPU上完成一次風(fēng)格遷移需要十幾分鐘甚至數(shù)小時。
2016年Justin Johnson提出了一種快速風(fēng)格遷移算法括袒,這種算法被稱為Fast Neural Style或Fast Style Transfer次兆。與Neural Style相比,F(xiàn)ast Neural Style專門設(shè)計了一個網(wǎng)絡(luò)用來進(jìn)行風(fēng)格遷移锹锰,輸入原圖片芥炭,網(wǎng)絡(luò)自動生成目標(biāo)圖片。這個網(wǎng)絡(luò)需要針對每一種風(fēng)格圖片馴良一個相對應(yīng)的風(fēng)格網(wǎng)絡(luò)恃慧,但是一旦訓(xùn)練完成园蝠,便只需要20秒甚至更短的時間就能完成一次風(fēng)格遷移。
Fast Neural Style的網(wǎng)絡(luò)結(jié)構(gòu)如下所示痢士。是輸入圖像彪薛,在風(fēng)格遷移任務(wù)中,是風(fēng)格圖片,Image Transform Net 是我們設(shè)計的風(fēng)格遷移網(wǎng)絡(luò)善延,針對輸入的圖像少态,能夠返回一張新的圖像。在圖像內(nèi)容上與相似易遣,但在風(fēng)格上與相似彼妻。損失網(wǎng)絡(luò)(Loss Network)不用訓(xùn)練,只是用來計算知覺特征和風(fēng)格特征训挡。在論文中損失網(wǎng)絡(luò)采用在ImageNet上預(yù)訓(xùn)練好的VGG-16澳骤。
VGG-16的網(wǎng)絡(luò)結(jié)構(gòu)如下所示(第D列)。網(wǎng)絡(luò)從左到右有5個卷積塊澜薄,兩個卷積塊之間通過MaxPooling層區(qū)分为肮。每個卷積塊有2~3個卷積層,每一個卷積層后面都跟著一個ReLU激活層肤京。上面的relu2_2表示第2個卷積塊的第2個卷積層的激活層(ReLU)輸出颊艳。
Fast Neural Style的訓(xùn)練步驟如下。
(1)輸入一張圖片到中得到結(jié)果忘分。
(2)將和(其實就是)輸入到loss network(VGG-16)中棋枕,計算它在relu3_3的輸出,并計算它們之間的均方誤差作為content loss妒峦。
(3)將和(風(fēng)格圖片)輸入到loss network中重斑,計算它在relu1_2、relu2_2肯骇、relu3_3和relu4_3的輸出窥浪,再計算它們的Gram Matrix的均方誤差作為style loss。
(4)兩個損失相加笛丙,并反向傳播漾脂。更新參數(shù),固定loss network不動胚鸯。
(5)跳回第一步骨稿,繼續(xù)訓(xùn)練。
在講解如何用PyTorch實現(xiàn)風(fēng)格遷移之前姜钳,我們先來了解全卷積網(wǎng)絡(luò)的結(jié)構(gòu)坦冠。我們知道風(fēng)格遷移網(wǎng)絡(luò)的輸入是圖片,輸出也是圖片哥桥,對這種網(wǎng)絡(luò)我們一般實現(xiàn)為一個全部都是卷積層而沒有全連接層的網(wǎng)絡(luò)結(jié)構(gòu)辙浑。對于卷積層,當(dāng)輸入feature maps(或者圖片)的尺寸為泰讽,卷積核有個例衍,卷積核尺寸為昔期,padding大小為,步長為時佛玄,輸出feature maps的形狀為硼一,其中
舉例來說,如果我們輸入的圖片尺寸是3 x 256 x 256梦抢,第一層卷積的卷積核大小為3般贼,padding為1,步長為2奥吩,通道數(shù)為128哼蛆,那么輸出的feature maps形狀,按照上述公式計算的結(jié)果就是:
所以最后的輸出是霞赫,即尺寸縮小一倍腮介,通道數(shù)增加。如果把步長由2改成1端衰,則輸出的形狀就是128 x 256 x 256叠洗,即尺寸不變,只是通道數(shù)增加旅东。
除卷積層之外灭抑,還有一種叫做轉(zhuǎn)置卷積層(Transposed Convolution),也有人稱之為反卷積(DeConvolution)抵代,它可以看成是卷積操作的逆運算腾节。對于卷積操作,當(dāng)步長大于1時荤牍,執(zhí)行的是類似于上采樣的操作案腺。全卷積網(wǎng)絡(luò)的一個重要優(yōu)勢在于對輸入的尺寸沒有要求,這樣在進(jìn)行風(fēng)格遷移時就能夠接受不同分辨率的圖片参淫。
論文中提到的風(fēng)格遷移結(jié)構(gòu)全部由卷積層救湖、Batch Normalization和激活層組成愧杯,不包含全連接層涎才。在這里我們不使用Batch Normalization,取而代之的是Instance Normalization力九。Instance Normalization和Batch Normalization的唯一區(qū)別就在于InstanceNorm只對每一個樣本求均值和方差耍铜,而BatchNorm則會對一個batch中所有的樣本求均值。例如對于一個的tensor跌前,在Batch Normalization中計算均值時棕兼,就會計算個數(shù)的均值,共有個均值抵乓;而Instance Normalization會計算個數(shù)的均值伴挚,即共有個均值靶衍。在Dmitry Ulyanov的論文《Instance Normalization:The Missing Ingredient for Fast Stylization》中提到過,用InstanceNorm代替BatchNorm能顯著地提升風(fēng)格遷移的效果茎芋。
8.2 用PyTorch實現(xiàn)風(fēng)格遷移
本章所有代碼和圖片數(shù)據(jù):百度網(wǎng)盤下載颅眶,提取碼:nyf9。
我們先來看看本次實驗的文件組織:
checkpoints/
data/coco/
main.py
PackedVGG.py
style.jpg
transformer_net.py
utils.py
上述各個文件的主要內(nèi)容和作用如下田弥。
- checkpoints/:用來保存模型涛酗。
- data/:用于保存數(shù)據(jù),可以把數(shù)據(jù)直接保存于coco文件夾下偷厦。
- main.py:主函數(shù),包括訓(xùn)練和測試。
- PackedVGG.py:預(yù)訓(xùn)練好的VGG-16窟却,為了提取中間層的輸出软驰,所以做了一些修改簡化。
- transformer_net.py:風(fēng)格遷移網(wǎng)絡(luò)请唱。輸入一張圖片枯途,輸出一張圖片。
- utils.py:工具集合籍滴,主要是可視化工具visdom的封裝和計算Gram Matrix等酪夷。
首先來看看如何使用預(yù)訓(xùn)練的VGG,這部分代碼保存在PackedVGG.py中孽惰。在torchvision的倉庫中有預(yù)訓(xùn)練好的VGG-16網(wǎng)絡(luò)晚岭,使用十分方便,但在風(fēng)格遷移網(wǎng)絡(luò)中勋功,我們需要獲得中間層的輸出坦报,因此需要修改網(wǎng)絡(luò)的前向傳播過程,將相應(yīng)層的輸出保存下來狂鞋。同時有很層不再需要片择,可刪除以節(jié)省內(nèi)存占用。實現(xiàn)的代碼如下骚揍。
# coding:utf8
import torch
import torch.nn as nn
from torchvision.models import vgg16
from collections import namedtuple
class Vgg16(torch.nn.Module):
def __init__(self):
super(Vgg16, self).__init__()
features = list(vgg16(pretrained=True).features)[:23]
# features的第3字管,8,15信不,22層分別是: relu1_2,relu2_2,relu3_3,relu4_3
self.features = nn.ModuleList(features).eval()
def forward(self, x):
results = []
for ii, model in enumerate(self.features):
x = model(x)
if ii in {3, 8, 15, 22}:
results.append(x)
vgg_outputs = namedtuple("VggOutputs", ['relu1_2', 'relu2_2', 'relu3_3', 'relu4_3'])
return vgg_outputs(*results)
在torchvision中嘲叔,VGG的實現(xiàn)由兩個nn.Sequential對象組成,第一個是features抽活,包含卷積硫戈、激活和MaxPool等層,用來提取圖片特征下硕,另一個是classifier丁逝,包含全連接層等汁胆,用來分類。用以通過vgg.features直接獲得對應(yīng)的nn.Sequential對象霜幼。這樣在前向傳播時沦泌,當(dāng)計算完指定層的輸出后,將結(jié)果保存于一個list中辛掠,然后再使用namedtuple進(jìn)行名稱綁定谢谦,這樣可以通過output.relu1_2訪問第一個元素,更為方便和直觀萝衩。當(dāng)然也可以利用layer.register_forward_hook的方式獲取相應(yīng)層的輸出回挽,但是在本例中相對比較麻煩。
接下來要實現(xiàn)的是風(fēng)格遷移網(wǎng)絡(luò)猩谊,其代碼在transformer_net.py中千劈,實現(xiàn)時參考了PyTorch的官方示例,其網(wǎng)絡(luò)結(jié)構(gòu)如下圖所示牌捷。
圖中(b)是網(wǎng)絡(luò)的總體結(jié)構(gòu)墙牌,左邊(d)是一個殘差單元的結(jié)構(gòu)圖,右邊(c)和(d)分別是下采樣和上采樣單元的結(jié)構(gòu)圖暗甥。網(wǎng)絡(luò)結(jié)構(gòu)總結(jié)起來有以下幾個特點喜滨。
- 先下采樣,然后上采樣撤防,這種做法使計算量變小虽风,詳細(xì)說明請參考論文。
- 使用殘差結(jié)構(gòu)使網(wǎng)絡(luò)變深寄月。
- 邊緣補(bǔ)齊的方式不再是傳統(tǒng)的補(bǔ)0辜膝,而是采用一種被稱為Reflection Pad的補(bǔ)齊策略:上下左右反射邊緣的像素進(jìn)行補(bǔ)齊。
- 上采樣不再是使用傳統(tǒng)的ConvTranspose2d漾肮,而是先用Upsample厂抖,然后用Conv2d,這種做法能避免Checkboard Artifacts現(xiàn)象克懊。
- Batch Normalization全部改成Instance Normalization忱辅。
- 網(wǎng)絡(luò)中沒有全連接層,線性 操作是卷積保檐。因此對輸入和輸出圖片的尺寸沒有要求耕蝉,這里我們輸入和輸出圖片的尺寸都是3 * 256 * 256(其他尺寸的一樣可以)崔梗。
在第6章中我們提到過夜只,對于常見的網(wǎng)絡(luò)結(jié)構(gòu),可以實現(xiàn)為nn.Module對象蒜魄,作為一個特殊的層扔亥。在本例中场躯,Conv、UpConv和Residual Block都可以如此實現(xiàn)旅挤。
例如Conv踢关,可以實現(xiàn)為:
class ConvLayer(nn.Module):
"""
add ReflectionPad for Conv
默認(rèn)的卷積的padding操作是補(bǔ)0,這里使用邊界反射填充
"""
def __init__(self, in_channels, out_channels, kernel_size, stride):
super(ConvLayer, self).__init__()
reflection_padding = int(np.floor(kernel_size / 2))
self.reflection_pad = nn.ReflectionPad2d(reflection_padding)
self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size, stride)
def forward(self, x):
out = self.reflection_pad(x)
out = self.conv2d(out)
return out
UpConv和Residual Block的實現(xiàn)也是類似的粘茄,這里不再細(xì)說签舞,具體內(nèi)容請看本章配套代碼。
主模型主要包含三個部分:下采樣的卷積層柒瓣、深度殘差層和上采樣的卷積層儒搭。實現(xiàn)時充分利用了nn.Sequential,避免在forward中重復(fù)寫代碼芙贫,其代碼如下搂鲫。
class TransformerNet(nn.Module):
def __init__(self):
super(TransformerNet, self).__init__()
# 下卷積層
self.initial_layers = nn.Sequential(
ConvLayer(3, 32, kernel_size=9, stride=1),
nn.InstanceNorm2d(32, affine=True),
nn.ReLU(True),
ConvLayer(32, 64, kernel_size=3, stride=2),
nn.InstanceNorm2d(64, affine=True),
nn.ReLU(True),
ConvLayer(64, 128, kernel_size=3, stride=2),
nn.InstanceNorm2d(128, affine=True),
nn.ReLU(True),
)
# Residual layers(殘差層)
self.res_layers = nn.Sequential(
ResidualBlock(128),
ResidualBlock(128),
ResidualBlock(128),
ResidualBlock(128),
ResidualBlock(128)
)
# Upsampling Layers(上卷積層)
self.upsample_layers = nn.Sequential(
UpsampleConvLayer(128, 64, kernel_size=3, stride=1, upsample=2),
nn.InstanceNorm2d(64, affine=True),
nn.ReLU(True),
UpsampleConvLayer(64, 32, kernel_size=3, stride=1, upsample=2),
nn.InstanceNorm2d(32, affine=True),
nn.ReLU(True),
ConvLayer(32, 3, kernel_size=9, stride=1)
)
def forward(self, x):
x = self.initial_layers(x)
x = self.res_layers(x)
x = self.upsample_layers(x)
return x
搭建完網(wǎng)絡(luò)之后,我們還要實現(xiàn)一些工具函數(shù)磺平,這部分的代碼保存于utils.py中魂仍。代碼如下:
# coding:utf8
from itertools import chain
import visdom
import torch as t
import time
import torchvision as tv
import numpy as np
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
def gram_matrix(y):
"""
輸入 b,c,h,w
輸出 b,c,c
"""
(b, ch, h, w) = y.size()
features = y.view(b, ch, w * h)
features_t = features.transpose(1, 2)
gram = features.bmm(features_t) / (ch * h * w)
return gram
class Visualizer():
"""
封裝了visdom的基本操作,但是你仍然可以通過`self.vis.function`
調(diào)用原生的visdom接口
"""
def __init__(self, env='default', **kwargs):
import visdom
self.vis = visdom.Visdom(env=env, use_incoming_socket=False, **kwargs)
# 畫的第幾個數(shù)拣挪,相當(dāng)于橫座標(biāo)
# 保存(’loss',23) 即loss的第23個點
self.index = {}
self.log_text = ''
def reinit(self, env='default', **kwargs):
"""
修改visdom的配置
"""
self.vis = visdom.Visdom(env=env,use_incoming_socket=False, **kwargs)
return self
def plot_many(self, d):
"""
一次plot多個
@params d: dict (name,value) i.e. ('loss',0.11)
"""
for k, v in d.items():
self.plot(k, v)
def img_many(self, d):
for k, v in d.items():
self.img(k, v)
def plot(self, name, y):
"""
self.plot('loss',1.00)
"""
x = self.index.get(name, 0)
self.vis.line(Y=np.array([y]), X=np.array([x]),
win=name,
opts=dict(title=name),
update=None if x == 0 else 'append'
)
self.index[name] = x + 1
def img(self, name, img_):
"""
self.img('input_img',t.Tensor(64,64))
"""
if len(img_.size()) < 3:
img_ = img_.cpu().unsqueeze(0)
self.vis.image(img_.cpu(),
win=name,
opts=dict(title=name)
)
def img_grid_many(self, d):
for k, v in d.items():
self.img_grid(k, v)
def img_grid(self, name, input_3d):
"""
一個batch的圖片轉(zhuǎn)成一個網(wǎng)格圖擦酌,i.e. input(36,64菠劝,64)
會變成 6*6 的網(wǎng)格圖仑氛,每個格子大小64*64
"""
self.img(name, tv.utils.make_grid(
input_3d.cpu()[0].unsqueeze(1).clamp(max=1, min=0)))
def log(self, info, win='log_text'):
"""
self.log({'loss':1,'lr':0.0001})
"""
self.log_text += ('[{time}] {info} <br>'.format(
time=time.strftime('%m%d_%H%M%S'),
info=info))
self.vis.text(self.log_text, win=win)
def __getattr__(self, name):
return getattr(self.vis, name)
def get_style_data(path):
"""
加載風(fēng)格圖片,
輸入: path闸英, 文件路徑
返回: 形狀 1*c*h*w锯岖, 分布 -2~2
"""
style_transform = tv.transforms.Compose([
tv.transforms.ToTensor(),
tv.transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
])
style_image = tv.datasets.folder.default_loader(path)
style_tensor = style_transform(style_image)
return style_tensor.unsqueeze(0)
def normalize_batch(batch):
"""
輸入: b,ch,h,w 0~255
輸出: b,ch,h,w -2~2
"""
mean = batch.data.new(IMAGENET_MEAN).view(1, -1, 1, 1)
std = batch.data.new(IMAGENET_STD).view(1, -1, 1, 1)
mean = (mean.expand_as(batch.data))
std = (std.expand_as(batch.data))
return (batch / 255.0 - mean) / std
可以看出utils.py中主要包含以下三個功能。
- 計算Gram Matrix:利用矩陣轉(zhuǎn)置的乘法即可實現(xiàn)甫何,但是這里我們要對batch中的每一個樣本計算Gram Matrix出吹,因此用的是tensor.bmm(tensor2),而不是tensor.mm(tensor2)辙喂。
- 獲取圖片風(fēng)格:根據(jù)文件名讀取圖片捶牢,并將它轉(zhuǎn)化成tensor。這里圖片的均值和標(biāo)準(zhǔn)差不是0.5和0.5巍耗,而是使用他人專門計算的ImageNet上所有圖片的均值和標(biāo)準(zhǔn)差秋麸,更符合真實世界的圖片的分布。人們發(fā)現(xiàn)按照這個數(shù)值處理的圖片比簡單地使用0.5作為均值和標(biāo)準(zhǔn)差效果更好炬太。
- 將分布在0 ~ 255的圖片進(jìn)行標(biāo)準(zhǔn)化灸蟆,和上述對風(fēng)格圖片的處理類似,需要注意亲族,這里是針對Variable對象的處理炒考。
除此之外可缚,還有對visdom操作的封裝,這里不再展示斋枢。
當(dāng)我們將上述網(wǎng)絡(luò)定義和工具函數(shù)寫完之后帘靡,就可以開始訓(xùn)練網(wǎng)絡(luò)了。首先來看看在config.py中都有哪些可以配置的參數(shù)瓤帚。
class Config(object):
image_size = 256 # 圖片大小
batch_size = 8
data_root = 'data/' # 數(shù)據(jù)集存放路徑:data/coco/a.jpg
num_workers = 4 # 多線程加載數(shù)據(jù)
use_gpu = True # 使用GPU
style_path = 'style.jpg' # 風(fēng)格圖片存放路徑
lr = 1e-3 # 學(xué)習(xí)率
env = 'neural-style' # visdom env
plot_every = 10 # 每10個batch可視化一次
epoches = 2 # 訓(xùn)練epoch
content_weight = 1e5 # content_loss 的權(quán)重
style_weight = 1e10 # style_loss的權(quán)重
model_path = None # 預(yù)訓(xùn)練模型的路徑
debug_file = 'debug/debug.txt' # touch $debug_fie 進(jìn)入調(diào)試模式
content_path = 'input.png' # 需要進(jìn)行分割遷移的圖片
result_path = 'output.png' # 風(fēng)格遷移結(jié)果的保存路徑
論文中訓(xùn)練用的圖片是MS COCO數(shù)據(jù)集描姚,讀者可以從官網(wǎng)下載。其訓(xùn)練集中包含超過8萬張圖片戈次,超過13GB轰胁。筆者認(rèn)為COCO的數(shù)據(jù)比ImageNet的數(shù)據(jù)更復(fù)雜,更像是日常生活的照片朝扼。讀者如果有ImageNet的圖片赃阀,也一樣可以用。獲取數(shù)據(jù)之后擎颖,將數(shù)據(jù)解壓到data/coco/文件夾下榛斯,或放到任意位置,然后在訓(xùn)練時指定對應(yīng)路徑搂捧。
我們可以在main.py中直接利用ImageFolder和DataLoader加載數(shù)據(jù)驮俗。
# 數(shù)據(jù)加載
transfroms = tv.transforms.Compose([
tv.transforms.Resize(opt.image_size),
tv.transforms.CenterCrop(opt.image_size),
tv.transforms.ToTensor(),
tv.transforms.Lambda(lambda x: x * 255)
])
dataset = tv.datasets.ImageFolder(opt.data_root, transfroms)
dataloader = data.DataLoader(dataset, opt.batch_size)
# 轉(zhuǎn)換網(wǎng)絡(luò)
transformer = TransformerNet()
if opt.model_path:
transformer.load_state_dict(t.load(opt.model_path, map_location=lambda _s, _: _s))
transformer.to(device)
# 損失網(wǎng)絡(luò) Vgg16
vgg = Vgg16().eval()
vgg.to(device)
for param in vgg.parameters():
param.requires_grad = False
# 優(yōu)化器
optimizer = t.optim.Adam(transformer.parameters(), opt.lr)
# 獲取風(fēng)格圖片的數(shù)據(jù)
style = utils.get_style_data(opt.style_path)
vis.img('style', (style.data[0] * 0.225 + 0.45).clamp(min=0, max=1))
style = style.to(device)
# 風(fēng)格圖片的gram矩陣
with t.no_grad():
features_style = vgg(style)
gram_style = [utils.gram_matrix(y) for y in features_style]
# 損失統(tǒng)計
style_meter = tnt.meter.AverageValueMeter()
content_meter = tnt.meter.AverageValueMeter()
訓(xùn)練步驟在8.1節(jié)已經(jīng)講過,按照訓(xùn)練步驟允跑,很容易寫出如下訓(xùn)練代碼:
for epoch in range(opt.epoches):
content_meter.reset()
style_meter.reset()
for ii, (x, _) in tqdm.tqdm(enumerate(dataloader)):
# 訓(xùn)練
optimizer.zero_grad()
x = x.to(device)
y = transformer(x)
y = utils.normalize_batch(y)
x = utils.normalize_batch(x)
features_y = vgg(y)
features_x = vgg(x)
# content loss
content_loss = opt.content_weight * F.mse_loss(features_y.relu2_2, features_x.relu2_2)
# style loss
style_loss = 0.
for ft_y, gm_s in zip(features_y, gram_style):
gram_y = utils.gram_matrix(ft_y)
style_loss += F.mse_loss(gram_y, gm_s.expand_as(gram_y))
style_loss *= opt.style_weight
total_loss = content_loss + style_loss
total_loss.backward()
optimizer.step()
完整的代碼請查看本書的配套源碼王凑。這個程序中容易讓人混淆的是圖片的尺度,有時是0 ~ 1聋丝,有時是-2 ~ 2索烹,還有時是0 ~ 255,統(tǒng)一說明如下弱睦。
- 圖片每個像素的取值范圍為0 ~ 255百姓。
- 調(diào)用torchvision的transforms.ToTensor()操作,像素會被轉(zhuǎn)換到0 ~ 1况木。
- 這時如果進(jìn)行標(biāo)準(zhǔn)化(減去均值垒拢、除以標(biāo)準(zhǔn)差),均值和標(biāo)準(zhǔn)差均為0.5火惊,那么標(biāo)準(zhǔn)化之后圖片的分布就是-1 ~ 1求类,但在本次實驗中使用的均值和標(biāo)準(zhǔn)差不是0.5,而是[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]屹耐,這時在ImageNet 100萬張圖片上計算得到的圖片的均值和標(biāo)準(zhǔn)差尸疆,可以估算得知這時圖片的分布范圍大概在和之間。盡管這時它的分布在-2.1 ~ 2.7,但是它的均值接近0仓技,標(biāo)準(zhǔn)差接近1鸵贬,采用ImageNet圖片的均值和標(biāo)準(zhǔn)差作為標(biāo)準(zhǔn)化參數(shù)的目的是圖像的各個像素的分布接近標(biāo)準(zhǔn)分布俗他。
- VGG-16網(wǎng)絡(luò)的輸入圖像數(shù)值大小為使用ImageNet均值和標(biāo)準(zhǔn)差進(jìn)行標(biāo)準(zhǔn)化之后的圖片數(shù)據(jù)脖捻,即-2.1 ~ 2.7。
- TransformerNet網(wǎng)絡(luò)的輸入圖片的像素值是0 ~ 255兆衅,輸出的像素值也希望是0 ~ 255地沮,但是由于沒有做特殊處理,所以可能出現(xiàn)小于0和大于255的像素羡亩。
- 使用visdom images進(jìn)行可視化和使用torchvision.utils.save_image保存圖片時摩疑,希望tensor的數(shù)據(jù)位于0 ~ 1。
當(dāng)我們掌握了上述內(nèi)容后畏铆,就不難理解為什么在代碼中時不時地出現(xiàn)各種尺度變換(乘以標(biāo)準(zhǔn)差雷袋、加上均值)和截斷操作。尺度變換是為了從一個尺度變成另一個尺度辞居,截斷是為了確保數(shù)值在一定范圍之內(nèi)(0 ~ 1或者0 ~ 255)楷怒。
除了訓(xùn)練模型,我們還希望能加載預(yù)訓(xùn)練好的模型對指定的圖片進(jìn)行風(fēng)格遷移的操作瓦灶,這部分的代碼實現(xiàn)如下鸠删。
@t.no_grad()
def stylize(**kwargs):
opt = Config()
for k_, v_ in kwargs.items():
setattr(opt, k_, v_)
device=t.device('cuda') if opt.use_gpu else t.device('cpu')
# 圖片處理
content_image = tv.datasets.folder.default_loader(opt.content_path)
content_transform = tv.transforms.Compose([
tv.transforms.ToTensor(),
tv.transforms.Lambda(lambda x: x.mul(255))
])
content_image = content_transform(content_image)
content_image = content_image.unsqueeze(0).to(device).detach()
# 模型
style_model = TransformerNet().eval()
style_model.load_state_dict(t.load(opt.model_path, map_location=lambda _s, _: _s))
style_model.to(device)
# 風(fēng)格遷移與保存
output = style_model(content_image)
output_data = output.cpu().data[0]
tv.utils.save_image(((output_data / 255)).clamp(min=0, max=1), opt.result_path)
這樣,我們就可以通過命令行的方式訓(xùn)練贼陶,或者加載預(yù)訓(xùn)練好的模型進(jìn)行風(fēng)格遷移刃泡。
# 訓(xùn)練,使用GPU碉怔,數(shù)據(jù)存放于data/下的一個文件夾中
python main.py train \
--use-gpu \
--data-root=data \
--batch-size=2
# 風(fēng)格遷移烘贴,不使用GPU
python main.py stylize \
--model-path='transformer.pth' \
--content-path='amber.jpg' \
--result-path='output.png' \
--use-gpu=False
8.3 實驗結(jié)果分析
在本例中我們只訓(xùn)練了一個風(fēng)格的模型,風(fēng)格圖片如下圖所示撮胧。
風(fēng)格遷移的結(jié)果所下圖所示庙楚。上面一行是原始圖片,下面一行是風(fēng)格遷移的結(jié)果趴樱。
隨書附帶代碼中帶有這個預(yù)訓(xùn)練的模型馒闷,讀者可以用其他圖片查看風(fēng)格遷移的效果,圖片分辨率越高叁征,風(fēng)格遷移的效果越好纳账。另外讀者也可以通過指定--style-path=my_style.png訓(xùn)練不同風(fēng)格的遷移圖片。
例如:我們有一張高清美女圖片如下捺疼,使用自己訓(xùn)練出來的模型遷移風(fēng)格疏虫,效果如下。
python main.py stylize \
--model-path='checkpoints/1_style.pth' \
--content-path='amber.jpg' \
--result-path='output.png' \
--use-gpu=False
除了風(fēng)格遷移,類似的應(yīng)用還有Google DeepDream卧秘,可以輸入一張圖片生成神經(jīng)網(wǎng)絡(luò)眼中的這張圖片的樣子呢袱,網(wǎng)絡(luò)越深,生成的圖片包含的奇幻東西越多翅敌,效果如下所示羞福。
2017年有兩個比較吸引人的風(fēng)格遷移項目,一個是來自Adobe的圖片風(fēng)格深度遷移(Deep Photo Style Transfer)蚯涮,這是由康奈爾大學(xué)的中國留學(xué)生和Adobe公司的工程師共同合作的一個新項目治专,通過深度學(xué)習(xí)的圖片處理方法,直接提取了參考圖片的風(fēng)格遭顶,并轉(zhuǎn)換為相對應(yīng)的濾鏡张峰,其效果如下圖所示,最左邊是原圖棒旗,中間是目標(biāo)風(fēng)格圖片喘批,最右邊是將中間的風(fēng)格遷移到左邊的結(jié)果。
另一個項目是來自UC Berkeley的CycleGAN铣揉,它能夠勝任任何的圖像轉(zhuǎn)換和圖像翻譯任務(wù)饶深,在風(fēng)格遷移上的效果尤其令人矚目。CycleGAN的網(wǎng)絡(luò)結(jié)構(gòu)和Fast Neural Style的transformer類似老速,但它采用了GAN的訓(xùn)練方式粥喜,能夠?qū)崿F(xiàn)風(fēng)格的雙向轉(zhuǎn)換,更加通用橘券。
在本章额湘,我們學(xué)會了如何實現(xiàn)一個深度學(xué)習(xí)中很酷的應(yīng)用:風(fēng)格遷移。不僅講解了它的原理旁舰、風(fēng)格損失和內(nèi)容損失的實現(xiàn)锋华,還用PyTorch實現(xiàn)了相應(yīng)的代碼。