使用Pytorch实现风格迁移(Neural-Transfer)
共 9642字,需浏览 20分钟
·
2021-08-26 22:10
# 前言
本文主要向大家分享一个小编刚刚学习的神经网络应用的实例:风格迁移(Neural-Transfer)。这是一个由 Leon A. Gatys,Alexander S. Ecker和Matthias Bethge提出的算法。通过这个算法,我们可以用一种新的风格对指定图片进行重构,更通俗一点即:风格图片+内容图片=输出图片,即:
![1b8b55026a80114182cb346df880b34d.webp](https://filescdn.proginn.com/2675c57ab93174204b4c53961ff54dff/1b8b55026a80114182cb346df880b34d.webp)
如上图所示,神经风格迁移可以将内容图像的内容、风格图像的风格混合在一起,使得输出的图片看起来像内容图像,但采用了风格图像的风格。这就生成了一个带有《星空》风格的风景图,是不是很有趣呢?下面让我们一起来看看它是怎么实现的吧!
01
基本原理
原理很简单:我们定义了两个距离,一个为内容D_c和一个用于样式D_s。D_c 测量两个图像之间的内容有多大不同,而 D_s衡量两个图像之间风格的差异。然后,我们生成第三张图片,并利用D_c和D_s来优化这张图片,使其与内容图片的内容差别和风格图片的风格差别最小化。当然,我们的第一步是利用卷积神经网络提取出图片的特征,如图所示:
![fdbb81bc6e29eef018d7eb7a07e7b4f6.webp](https://filescdn.proginn.com/e65a8852a640df0e3f7082ecaba70068/fdbb81bc6e29eef018d7eb7a07e7b4f6.webp)
02
读入图片
现在我们导入content和style的原图,我们需要使用PIL中的Image来读取内存中的图片(PS:opencv也可以,但PIL的效果更好一点),再用torchvision中的transforms将读入图片转化为tensor以便之后的操作。另外,我们还需要定义一个用于输出图像的函数,以便输出最终所得到的图片,该函数会将tensor转化为图像。
代码如下:
# load_img模块
import PIL.Image as Image
import torch
import torchvision.transforms as transforms
img_size = 512 if torch.cuda.is_available() else 128#根据设备选择改变后项数大小
def load_img(img_path):#图像读入
img = Image.open(img_path).convert('RGB')#将图像读入并转换成RGB形式
img = img.resize(img_size, img_size)#调整读入图像像素大小
img = transforms.ToTensor()(img)#将图像转化为tensor
img = img.unsqueeze(0)#在0维上增加一个维度
return img
def show_img(img):#图像输出
img = img.squeeze(0)#将多余的0维通道删去
img = transforms.ToPILImage()(img)#将tensor转化为图像
img.show()
03
损失函数
下一步是定义我们的损失函数,为了实现神经风格迁移,我们需要定义一个关于生成图像(Generated image)G的损失函数,用于评价生成图像的好坏。通过最小化损失函数的方式,来生成所要的图像。损失函数需要分成两部分,一个是内容损失函数,它是关于生成图像G与内容图像C的函数,用于衡量生成图像与内容图像在内容上有多相似;一个是风格损失函数,关于生成图像G与风格图像S的函数,用于衡量生成图像与风格图像在风格上的相似度。最后需要用两个超参数α和β来确定两个函数之间的权重,我们的总损失是它们两个的加权和,即
![6e725c728f49b46590cca4b882d9efbb.webp](https://filescdn.proginn.com/f7ebfbb15cb9223a0ec0897f08f80d08/6e725c728f49b46590cca4b882d9efbb.webp)
那么我们的关键就在于先单独求出内容损失和风格损失,计算它们的损失也很简单,首先我们看一下内容损失,我们使用最简单的均方误差:
![38d13a6256b00ee6ff879099bf7893e9.webp](https://filescdn.proginn.com/7111bb769816a3d013ad774296576f2b/38d13a6256b00ee6ff879099bf7893e9.webp)
上式是内容损失函数的定义。其中l代表第l层的特征表示,p是原始内容图片特征图,x是生成图片特征图。公式的含义就是对于每一层,原始图片的特征图(feature map)和生成图片的特征图的一一对应做差值平方和。
然后我们需要对损失梯度下降求导来优化参数
![f6cdc0f113a33e0f92e17136b01c9d03.webp](https://filescdn.proginn.com/fc744a5e8ec9e98bfb94c3e8dca9e33c/f6cdc0f113a33e0f92e17136b01c9d03.webp)
对于风格损失我们还需要引入Gram矩阵来帮助我们表示图像的风格特征,我们读入图像卷积层的输出形状为C × H × W ,C是卷积核的通道数,每个卷积核学习图像不同特征,每个卷积核输出H × W 代表这张图像的一个feature map,读入RGB图像的三色通道相当于三个feature map,我们用Gram矩阵来计算feature map间的相似性,得到图像的风格特征。
关于Gram矩阵
由于计算风格损失需要Gram矩阵,所以我们先来了解一下它吧。
Gram矩阵的定义:
![74b6d00840c2676952e501e0d6e674bf.webp](https://filescdn.proginn.com/67a6688cd349959b37ea55064a5c69a6/74b6d00840c2676952e501e0d6e674bf.webp)
Gram矩阵计算公式:
![0c94fb619d981e95d6628b8cfb173fff.webp](https://filescdn.proginn.com/fb30c9091890295ca8e87987b77f1b19/0c94fb619d981e95d6628b8cfb173fff.webp)
F表示生成图像的feature map。上面式子的含义:第l层的Gram矩阵第i行,第j列的数值等于把生成图像在第l层的第i个feature map与第j个feature map分别拉成一维后相乘求和,即Gram矩阵中的每个值都是每个通道 i 的feature map与每个通道 j 的feature map的内积,由于内积可以判断两个向量之间的夹角和方向关系(如:若a·b>0,则二者方向相同,夹角在0°到90°之间),所以Gram矩阵中的值可以反映出两个feature map之间的某种关系。
Gram矩阵可以看是feature之间的偏心协方差矩阵,feature map中的每个数字都来自于一个特定滤波器在特定位置的卷积,因此每个数字代表一个特征的强度,而Gram计算的实际上是两两特征之间的相关性。Gram的对角线元素提供了不同特征图各自的信息,而其余元素提供了不同特征图之间的相关信息,因此,Gram有助于把握整个图像的大体风格。有了表示风格的Gram矩阵,要度量两个图像风格的差异,只需比较他们Gram矩阵的差异即可。
然后我们就可以利用Gram矩阵提取的风格特征计算损失了,这里我们仍然使用均方误差,并进行归一化操作。
![cd10988698e88f0f8a8078933fe34d7b.webp](https://filescdn.proginn.com/9b953b758ab3ef3195d9f9c6e77e6b92/cd10988698e88f0f8a8078933fe34d7b.webp)
上面是第l层的风格损失函数,N是指生成图feature map数量,M是图片宽乘高,G是生成图像的Gram矩阵,A是风格图像的Gram矩阵。
然后是梯度下降:
![3c38fbd05a35cf431215a92f9536033a.webp](https://filescdn.proginn.com/82bd54fef27fea191a93e3ba292c4b33/3c38fbd05a35cf431215a92f9536033a.webp)
最终的风格损失函数为每一层的风格损失加权求和可得。式中,a是指风格图像,x是指生成图像,w是指权重:
![0f0947e6d69fb4d23b56b492b2233e9a.webp](https://filescdn.proginn.com/4fa750a84efff7c89dd264a9ad071a58/0f0947e6d69fb4d23b56b492b2233e9a.webp)
下面我们看看如何用代码实现它:
# loss板块
import torch.nn as nn
import torch
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,这是为了动态计算梯度,否则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_graph=True):#反向求导
self.loss.backward(retain_graph=retain_graph)
return self.loss
class Gram(nn.Module):#定义Gram矩阵
def __init__(self):
super(Gram, self).__init__()
def forward(self, input):#向前计算Gram矩阵
a, b, c, d = input.size()#a为批量大小,b为feature map的数量,c*d为feature map的大小
feature = input.view(a * b, c * d)
gram = torch.mm(feature, feature.t())
gram /= (a * b * c * d)
return gram
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_graph=True):
self.loss.backward(retain_graph=retain_graph)
return self.loss
04
模型构建
接下来就该构建我们的模型啦!虽然我们用的是已经预训练好的vgg框架,但我们还需要对它做一些“改造”:把我们之前构造好的损失函数加进去。毕竟“白嫖”也是有限度的嘛!话不多说,上代码!
# build_model模块
import torch.nn as nn
import torch
import torchvision.models as models
import loss # 指的是上文中已经写好的loss模块
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")#选择运行设备,如果你的电脑有gpu就在gpu上运行,否则在cpu上运行
vgg = models.vgg19(pretrained=True).features.to(device)#这里我们使用预训练好的vgg19模型
'''所需的深度层来计算风格/内容损失:'''
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,按顺序放入layer
model = model.to(device)
gram = loss.Gram().to(device)
'''把vgg19中的layer、content_loss以及style_loss按顺序加入到model中:'''
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
05
执行
一切工作准备就绪,我们就可以开始定义我们的run_code模块了,经历过这么多操作终于走到了最后一步,是不是有点小激动呢?我们的执行模块也很简单,先定义一个LBFGS优化器,然后就可以开始我们一次一次的训练了。这里我们定义每训练50次输出一次我们的损失来评估学习效果。
# run_code模块
import torch.nn as nn
import torch.optim as optim
from build_model import get_style_model_and_loss
def get_input_param_optimier(input_img):
"""input_img is a Variable"""
input_param = nn.Parameter(input_img.data)#获取参数
optimizer = optim.LBFGS([input_param])#用LBFGS优化参数
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:#每隔50次输出一次loss
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.item(), content_score.item()))
print()
return style_score + content_score
optimizer.step(closure)
input_param.data.clamp_(0, 1)#再次修正
return input_param.data
06
运行
最最最最最激动人心的时刻就要到了!敲了这么多代码,现在我们就可以开始验证我们的成果了!现在我们只需要挑选一张中意的图片,再为它寻找一个你想要的风格图片,只需短短几分钟(甚至几十秒),你,就创造出了属于自己的艺术画作(其实一般)!!!
# start模块
from torch.autograd import Variable
from torchvision import transforms
import torch
from run_code import run_style_transfer
from load_img import load_img, show_img
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
style_img = load_img('./picture/style1.png')#风格图片地址
style_img = Variable(style_img).to(device)
content_img = load_img('./picture/content5.png')#内容图片地址
content_img = Variable(content_img).to(device)
input_img = content_img.clone()
out = run_style_transfer(content_img, style_img, input_img, num_epoches=200)#进行200次训练
save_pic = transforms.ToPILImage()(out.cpu().squeeze(0))
save_pic.save('./picture/result4.png')#选择你要保存的地址
save_pic.show()
下面我们做几组演示:
输入:
![d4721719664f1b011a4eda42bb9baf49.webp](https://filescdn.proginn.com/78e44dcf5c8dcb462470757b5e51f058/d4721719664f1b011a4eda42bb9baf49.webp)
![f55990613a25a4e9e23929c2619469cf.webp](https://filescdn.proginn.com/3fcebad93982e585b721f48e5982a0d5/f55990613a25a4e9e23929c2619469cf.webp)
输出:
![9b0022904fa8b3be3519b3dbddc9d1cb.webp](https://filescdn.proginn.com/400527782c4807bf16b757667ce027fe/9b0022904fa8b3be3519b3dbddc9d1cb.webp)
输入:
![95a55b228cbafedef011fd153d9dd5bf.webp](https://filescdn.proginn.com/cfaec14c2a74a2abaef6237b64c4695e/95a55b228cbafedef011fd153d9dd5bf.webp)
![d3af3adde5a57c1f60aa4a9c2707422a.webp](https://filescdn.proginn.com/a726cdc57b0f027d0c40943caf2d2e1d/d3af3adde5a57c1f60aa4a9c2707422a.webp)
输出:
![68976b34d427f1ef90b3967512ad7001.webp](https://filescdn.proginn.com/30938dfe45826ed175397dee14988e11/68976b34d427f1ef90b3967512ad7001.webp)
最后我们试一试人像:
![bd8f1ee43fc08da7fc256a2521f1199a.webp](https://filescdn.proginn.com/f2a1dbea4b142c9872034c439091a5b9/bd8f1ee43fc08da7fc256a2521f1199a.webp)
![c00e5c82ce1a03a9f6b3b124e21d013a.webp](https://filescdn.proginn.com/ebee5ec1815c5fa02f6ab205f85aa837/c00e5c82ce1a03a9f6b3b124e21d013a.webp)
输出:
![4b30b09bf3da50041410a95e21311ce1.webp](https://filescdn.proginn.com/39af75fc6e362bee373fbdc259ebdeb1/4b30b09bf3da50041410a95e21311ce1.webp)
下面是其中一组loss的输出:
Opimizing...
run [50]
Style Loss: 0.7299 Content Loss: 3.7480
run [100]
Style Loss: 0.3702 Content Loss: 3.3560
run [150]
Style Loss: 0.2747 Content Loss: 3.1843
run [200]
Style Loss: 0.2058 Content Loss: 3.0981
我们可以看出内容损失还是挺大的,损失的大小不仅跟我们的模型有关,我们选择的图片也会对损失有很大的影响,不得不说有的图片特征确实难以捕获,小编曾经还有过训练1000次仍然有40多损失的惨痛经历,这种情况就不是单纯增加训练次数就能解决的了,我们需要更强大的模型去捕获信息,大家也可以自己试试改进一下模型,也许会有意外的收获呦!
reference
1] Gatys L A, Ecker A S, Bethge M. A neural algorithm of artistic style[J]. arXiv preprint arXiv:1508.06576, 2015.
2] https://pytorch.org/tutorials/advanced/neural_style_tutorial.html#importing-packages-and-selecting-a-device
1
END
1
文案&排版:王心怡(华中科技大学管理学院本科一年级)
潘云飞(华中科技大学管理学院本科一年级)
指导老师:秦虎老师(华中科技大学管理学院)
审稿:张宇(华中科技大学管理学院本科二年级)
如对文中内容有疑问,欢迎交流。PS:部分资料来自网络。
如有需求,可以联系:
秦虎老师(华中科技大学管理学院:tigerqin1980@qq.com)
王心怡(华中科技大学管理学院本科一年级:3348619379@qq.com)
潘云飞(华中科技大学管理学院本科一年级:1401914932@qq.com)
张宇(华中科技大学管理学院本科二年级:8611452@qq.com)