背景介紹
不知道大家是否用過prisma宾符,就算沒有用過桌粉,也一定看見別人用過這個軟件宣脉,下面是一張這個軟件得到的一個效果圖
官方宣傳的賣點(diǎn)是一秒鐘讓你的作品擁有名家風(fēng)格,什么畢加索跌宛,梵高,都不在話下积仗。通過這個效果再將你的照片發(fā)到朋友圈疆拘,是不是效果爆棚,簡直是各種裝逼界的一股清流寂曹,秒殺各種修圖ps好嗎哎迄。而且可以完美的掩飾掉一些瑕疵,又比ps更自然隆圆,更有逼格漱挚,是不是很棒。
這個軟件將使用的方法發(fā)了一篇論文渺氧,并且這個軟件在發(fā)布的時候就取得了上千萬的融資旨涝,是不是瞬間感覺現(xiàn)在學(xué)習(xí)了知識也能成為千萬富豪了。如今在這個高速發(fā)展的時代侣背,知識付費(fèi)的時代確實(shí)已經(jīng)到來了白华,所以我們現(xiàn)在努力學(xué)習(xí)各種知識就是在賺錢啊哩治,有木有。這樣大家的學(xué)習(xí)的時候就能夠有著更大的動力了衬鱼。
這篇論文感興趣的同學(xué)可以去查看一下业筏,里面主要涉及的是卷積神經(jīng)網(wǎng)絡(luò)。今天這篇文章要做的是什么呢鸟赫?我們希望自己能夠簡單的實(shí)現(xiàn)這個風(fēng)格遷移算法蒜胖,并且用自己的算法來得到新的風(fēng)格圖片。一想到我們放到朋友圈的照片是自己寫的算法來實(shí)現(xiàn)的就感覺成就感爆棚抛蚤,有沒有台谢。
環(huán)境配置
廢話不多說,我們先來看看需要的基本配置岁经。首先需要python環(huán)境朋沮,建議使用anaconda;然后我們使用的深度學(xué)習(xí)框架是pytorch缀壤,當(dāng)然你也可以用tensorflow樊拓,具體框架的介紹可以去看看之前寫的文章,需要安裝pytorch和torchvision塘慕,這里查看安裝幫助筋夏;同時需要一些其他的包,如果缺什么就pip安裝就好图呢。
這篇文章主要參考于pytorch的官方tutorial条篷,感興趣的同學(xué)可以直接移步至官方教程的地方,這篇文章我會說一些自己的理解蛤织,代碼部分基本都是參考這個教程赴叹,但是我會做一些說明,力求更加清楚指蚜。
原理分析
其實(shí)要實(shí)現(xiàn)的東西很清晰乞巧,就是需要將兩張圖片融合在一起,這個時候就需要定義怎么才算融合在一起姚炕。首先需要的就是內(nèi)容上是相近的摊欠,然后風(fēng)格上是相似的。這樣來我們就知道我們需要做的事情是什么了柱宦,我們需要計算融合圖片和內(nèi)容圖片的相似度些椒,或者說差異性,然后盡可能降低這個差異性掸刊;同時我們也需要計算融合圖片和風(fēng)格圖片在風(fēng)格上的差異性免糕,然后也降低這個差異性就可以了。這樣我們就能夠量化我們的目標(biāo)了。
對于內(nèi)容的差異性我們該如何定義呢石窑?其實(shí)我們能夠很簡答的想到就是兩張圖片每個像素點(diǎn)進(jìn)行比較牌芋,也就是求一下差,因?yàn)楹唵蔚挠嬎闼麄冎g的差會有正負(fù)松逊,所以我們可以加一個平方躺屁,使得差全部是正的,也可以加絕對值经宏,但是數(shù)學(xué)上絕對值會破壞函數(shù)的可微性犀暑,所以大家都用平方,這個地方不理解也沒關(guān)系烁兰,記住普遍都是使用平方就行了耐亏。
對于風(fēng)格的差異性我們該如何定義呢?這才是一個難點(diǎn)沪斟。這也是這篇文章提出的創(chuàng)新點(diǎn)广辰,引入了Gram矩陣計算風(fēng)格的差異。我盡量不使用數(shù)學(xué)的語言來解釋主之,而使用通俗的語言择吊。
首先需要的預(yù)先知識是卷積網(wǎng)絡(luò)的知識,這里不細(xì)講了杀餐,不了解的同學(xué)可以看之前的卷積網(wǎng)絡(luò)的文章干发。我們知道一張圖片通過卷積網(wǎng)絡(luò)之后可以的到一個特征圖朱巨,Gram矩陣就是在這個特征圖上面定義出來的史翘。每個特征圖的大小一般是 MxNxC 或者是 CxMxN 這種大小,這里C表示的時候厚度冀续,放在前面和后面都可以琼讽,MxN 表示的是一個矩陣的大小,其實(shí)就是有 C 個 MxN 這樣的矩陣疊在一起洪唐。
Gram矩陣是如何定義的呢钻蹬?首先Gram矩陣的大小是有特征圖的厚度決定的,等于 CxC凭需,那么每一個Gram矩陣的元素问欠,也就是 Gram(i, j) 等于多少呢?先把特征圖中第 i 層和第 j 層取出來粒蜈,這樣就得到了兩個 MxN的矩陣顺献,然后將這兩個矩陣對應(yīng)元素相乘然后求和就得到了 Gram(i, j),同理 Gram 的所有元素都可以通過這個方式得到枯怖。這樣 Gram 中每個元素都可以表示兩層特征圖的一種組合注整,就可以定義為它的風(fēng)格。
然后風(fēng)格的差異就是兩幅圖的 Gram 矩陣的差異,就像內(nèi)容的差異的計算方法一樣肿轨,計算一下這兩個矩陣的差就可以量化風(fēng)格的差異寿冕。
實(shí)現(xiàn)
以下的內(nèi)容都是用pytorch實(shí)現(xiàn)的,如果對pytorch不熟悉的同學(xué)可以看一下我之前的pytorch介紹文章椒袍,看看官方教程驼唱,如果不想了解pytorch的同學(xué)可以用自己熟悉的框架實(shí)現(xiàn)這個算法,理論部分前面已經(jīng)講完了驹暑。
內(nèi)容差異的loss定義
class Content_Loss(nn.Module):
def __init__(self, target, weight):
super(Content_Loss, self).__init__()
self.weight = weight
self.target = target.detach() * self.weight
# 必須要用detach來分離出target曙蒸,這時候target不再是一個Variable,這是為了動態(tài)計算梯度岗钩,否則forward會出錯纽窟,不能向前傳播
self.criterion = nn.MSELoss()
def forward(self, input):
self.loss = self.criterion(input * self.weight, self.target)
out = input.clone()
return out
def backward(self, retain_variabels=True):
self.loss.backward(retain_variables=retain_variabels)
return self.loss
其中有個變量weight,這個是表示權(quán)重兼吓,內(nèi)容和風(fēng)格你可以選擇一個權(quán)重臂港,比如你想風(fēng)格上更像,內(nèi)容上多一點(diǎn)差別沒關(guān)系视搏,那么內(nèi)容的權(quán)重你可以定義小一點(diǎn)审孽,風(fēng)格的權(quán)重可以定義大一點(diǎn);反之你可以把風(fēng)格的權(quán)重定義小一點(diǎn)浑娜,內(nèi)容的權(quán)重定義大一點(diǎn)佑力。
風(fēng)格差異的loss定義
Gram 矩陣的定義
class Gram(nn.Module):
def __init__(self):
super(Gram, self).__init__()
def forward(self, input):
a, b, c, d = input.size()
feature = input.view(a * b, c * d)
gram = torch.mm(feature, feature.t())
gram /= (a * b * c * d)
return gram
style loss定義
class Style_Loss(nn.Module):
def __init__(self, target, weight):
super(Style_Loss, self).__init__()
self.weight = weight
self.target = target.detach() * self.weight
self.gram = Gram()
self.criterion = nn.MSELoss()
def forward(self, input):
G = self.gram(input) * self.weight
self.loss = self.criterion(G, self.target)
out = input.clone()
return out
def backward(self, retain_variabels=True):
self.loss.backward(retain_variables=retain_variabels)
return self.loss
建立模型
使用19層的 vgg 作為提取特征的卷積網(wǎng)絡(luò),并且定義哪幾層為需要的特征筋遭。
vgg = models.vgg19(pretrained=True).features
vgg = vgg.cuda()
content_layers_default = ['conv_4']
style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']
def get_style_model_and_loss(style_img, content_img, cnn=vgg,
style_weight=1000,
content_weight=1,
content_layers=content_layers_default,
style_layers=style_layers_default):
content_loss_list = []
style_loss_list = []
model = nn.Sequential()
model = model.cuda()
gram = loss.Gram()
gram = gram.cuda()
i = 1
for layer in cnn:
if isinstance(layer, nn.Conv2d):
name = 'conv_' + str(i)
model.add_module(name, layer)
if name in content_layers_default:
target = model(content_img)
content_loss = loss.Content_Loss(target, content_weight)
model.add_module('content_loss_' + str(i), content_loss)
content_loss_list.append(content_loss)
if name in style_layers_default:
target = model(style_img)
target = gram(target)
style_loss = loss.Style_Loss(target, style_weight)
model.add_module('style_loss_' + str(i), style_loss)
style_loss_list.append(style_loss)
i += 1
if isinstance(layer, nn.MaxPool2d):
name = 'pool_' + str(i)
model.add_module(name, layer)
if isinstance(layer, nn.ReLU):
name = 'relu' + str(i)
model.add_module(name, layer)
return model, style_loss_list, content_loss_list
訓(xùn)練模型
def get_input_param_optimier(input_img):
"""
input_img is a Variable
"""
input_param = nn.Parameter(input_img.data)
optimizer = optim.LBFGS([input_param])
return input_param, optimizer
def run_style_transfer(content_img, style_img, input_img,
num_epoches=300):
print('Building the style transfer model..')
model, style_loss_list, content_loss_list = get_style_model_and_loss(
style_img, content_img
)
input_param, optimizer = get_input_param_optimier(input_img)
print('Opimizing...')
epoch = [0]
while epoch[0] < num_epoches:
def closure():
input_param.data.clamp_(0, 1)
model(input_param)
style_score = 0
content_score = 0
optimizer.zero_grad()
for sl in style_loss_list:
style_score += sl.backward()
for cl in content_loss_list:
content_score += cl.backward()
epoch[0] += 1
if epoch[0] % 50 == 0:
print('run {}'.format(epoch))
print('Style Loss: {:.4f} Content Loss: {:.4f}'.format(
style_score.data[0], content_score.data[0]
))
print()
return style_score + content_score
optimizer.step(closure)
input_param.data.clamp_(0, 1)
return input_param.data
需要特別注意的是這個模型里面參數(shù)不再是網(wǎng)絡(luò)里面的參數(shù)打颤,因?yàn)榫W(wǎng)絡(luò)使用的是已經(jīng)預(yù)訓(xùn)練好的 vgg 網(wǎng)絡(luò),這個算法里面的參數(shù)是合成圖片里面的每個像素點(diǎn)漓滔,我們可以將內(nèi)容圖片直接 copy 成合成圖片编饺,然后訓(xùn)練使得他的風(fēng)格和我們的風(fēng)格圖片相似,同時也可以隨機(jī)化一張圖片作為合成圖片响驴,然后訓(xùn)練他使得他與內(nèi)容圖片以及風(fēng)格圖片具有相似性透且。
實(shí)驗(yàn)結(jié)果
我們使用的風(fēng)格圖片為
內(nèi)容圖片為
得到的合成效果為
結(jié)語
通過這篇文章,我們利用pytorch實(shí)現(xiàn)了基本的風(fēng)格轉(zhuǎn)移算法豁鲤,得到的效果也是滿意的秽誊,所以我們可以把自己的圖片通過這個算法做一個風(fēng)格轉(zhuǎn)移,實(shí)現(xiàn)你想要的作品的風(fēng)格琳骡,逼格滿滿锅论,大家學(xué)習(xí)之后肯定會有特別大的成就感,在完成項目的同時也學(xué)習(xí)到了新的知識日熬,同時也會對這個產(chǎn)生更濃厚的感興趣棍厌,興趣才是各種的動力肾胯,比任何雞湯都有用,希望大家都能夠找到自己的興趣耘纱,熱愛自己所做的事敬肚。
本文代碼已經(jīng)上傳到了github上
歡迎查看我的知乎專欄,深度煉丹
歡迎訪問我的博客