Pytorch实现图像风格迁移
Posted 城南皮卡丘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Pytorch实现图像风格迁移相关的知识,希望对你有一定的参考价值。
图像风格迁移是图像纹理迁移研究的进一步拓展,可以理解为针对一张风格图像和一张内容图像,通过将风格图像的风格添加到内容图像上,从而对内容图像进行进一步创作,获得具有不同风格的目标图像。基于深度学习网络的图像风格迁移主要有三种类型,分别为固定风格固定内容的风格迁移、固定风格任意内容的快速风格迁移和任意风格任意内容的极速风格迁移。
图像风格迁移主要任务是将图像的风格迁移到内容图像上,使得内容图像也具有一定的风格。其中风格图像通常可以是艺术家的一些作品,如画家梵高的《向日葵》《星月夜》,日本浮世绘的《神奈川冲浪里》等经典的画作,这些图像通常包含一些经典的艺术家风格。风格图像也可以是经典的具有特色的照片,如夕阳下的照片、城市的夜景等,图像具有鲜明色彩图像。而内容图像则通常来自现实世界,可以是自拍照、户外摄影等。利用图像风格迁移则可以将内容图像处理为想要的风格。
1.固定风格固定内容的普通风格迁移
固定风格固定内容的风格迁移方法,也可以称为普通图像风格迁移方法,也是最早的基于深度卷积神经网络的图像风格迁移方法。针对每张固定内容图像和风格图像,普通图像风格迁移方法都需要重新经过长时间的训练,这是最慢的方法,也是最经典的方法。固定风格固定内容的风格迁移方法思路很简单,就是把图片当作可以训练的变量,通过不断优化图片的像素值,降低其与内容图片的内容差异,并降低其与风格图片的风格差异,通过对卷积网络的多次迭代训练,能够生成一幅具有特定风格的图像,并且内容与内容图片的内容一致,生成图片风格与风格图片的风格一致。
上图是论文《Image Style Transfer Using Convolutional Neural Networks》中,提到的基于VGG16网络中卷积层的图像风格迁移流程。在图中左边的图像 为输入的风格图像,右边的图像 为输入的内容图像。中间的图像 则是表示由随机噪声生成的图像风格迁移后的图像。表示图像的内容损失, 表示图像的风格损失, 和 分别表示内容损失权重和风格损失权重。
针对深度卷积神经网络的研究发现,使用较深层次的卷积计算得到的特征映射能够较好地表示图像的内容,而较浅层次的卷积计算得到的特征映射能够较好地表示图像的风格。基于这样的思想就可以通过不同卷积层的特征映射来度量目标图像在风格上和风格图像的差异,以及在内容上和内容图像的差异。
两个图像的内容相似性度量主要是通过度量两张图像在通过VGG16的卷积计算后,在conv4_2层上特征映射的相似性,作为图像的内容损失,内容损失函数如下所示:
式中,l 表示特征映射的层数; F 和 P 分别是目标图像和内容图像在对应卷积层输出的特征映射。
图像风格的损失并不是直接通过特征映射进行比较的,而是通过计算Gram矩阵先计算出图像的风格,再进行比较图像的风格损失。计算特征映射的Gram矩阵则是先将其特征映射变换为一个列向量,而Gram矩阵则使用这个列向量乘以其转置获得,Gram矩阵可以更好地表示图像的风格。所以输入风格图像 和目标图像 ,使用 和 分别表示它们在 l 层特征映射的风格表示(计算得到的Gram矩阵),那么图像的风格损失可以通过下面的方式进行计算:
式中, 是每个层的风格损失的权重; 和 对应着特征映射的高和宽。针对固定图像固定风格的图像风格迁移,使用PyTorch很容易实现。后续小节将介绍如何使用PyTorch进行固定图像固定风格的图像风格迁移。
2.固定风格任意内容的快速风格迁移
固定风格任意内容的快速风格迁移,是在固定风格固定内容的图像风格迁移的基础上,做出的一些必要改进,即在普通图像风格迁移的基础上,添加一个可供训练的图像转换网络。针对一种风格图像进行训练后,可以将任意输入图像非常迅速地进行图像迁移学习,让该图像具有学习好的图像风格。其深度网络的框架如下:
上图来自论文《Perceptual Losses for Real-Time Style Transfer and Super-Resolution》图示可以看作两个部分,一部分是通过输入图像 x 经过图像转换网络 ,得到网络的输出 ,这部分是普通风格迁移图像框架中没有的部分,普通图像风格迁移的输入图像是随机噪声,而快速风格迁移的输入是一张图像经过转换网络 的输出;另一部分是使用VGG16网络中的相关卷积层去度量一张图像的内容损失和风格损失。
在图像转换网络( Image Transform Net)部分,可以分为3个阶段,分别是图像降维部分、残差连接部分和图像升维部分。
(1)图像降维部分:主要通过3个卷积层来完成,将图像的尺寸从256×256逐渐缩小到原来的1/4,即64×64,并且将通道数逐渐从3个增加到128个特征映射。
(2)残差连接部分:该部分是通过连接5个残差块,对图像进行学习,该结构用于学习如何在原图上添加少量内容,改变原图的风格。其中每个残差连接的结构如图所示:
(3)图像升维部分:该部分主要输出5个残差单元,通过3个卷积层的操作,逐渐将其通道数从128缩小到3,每个特征映射的尺寸从64×64放大到256 ×256,也可以使用转置卷积来完成网络的升维部分。
1.准备VGG19网络
从torchvision的models模块中导入预训练好的VGG19网络,预训练好的网络是在ImageNet数据集上进行训练的,所以使用时会非常方便。因为VGG19网络的作用是计算对应图像在网络中一些层输出的特征映射,在计算过程中,不需要更新VGG19的参数权重,所以导入VGG19网络后,需要将其中的权重冻结,程序如下所示:
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
import torch
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
import torch.optim as optim
import requests
from torchvision import models
from torchvision import transforms
import time
import hiddenlayer as hl
from skimage.io import imread
vgg19=models.vgg19(pretrained=True)
vgg=vgg19.features
for param in vgg.parameters():
param.requires_grad_(False)
在上面的程序中,使用models.vgg19(pretrained=True)读取已经预训练的VGG19网络,参数pretrained=True表示读取的网络是已经预训练过的。预训练过的网络可以直接进行相关的应用。因不需要网络中的分类器相关的层,所以使用vgg19.features即可获取网络中由卷积和池化组成的特征提取层。在冻结网络的权重时,通过一个循环来遍历网络中所有可以训练的权重,然后通过requires_grad_(False)方法,设置权重在接下来的计算中不更新梯度,即权重不更新。
准备好VGG19网络之后,还需要对输入的图像进行相关的处理,因为网络可以接受任意尺寸的输入图像(图像的尺寸不宜过小,预防深层的卷积操作后没有特征映射输出,或特征映射尺寸太小),但是图像的尺寸越大,在进行风格迁移时,需要进行的计算量就会越多,速度就会越慢,所以需要保持图像有合适的尺寸。虽然图像风格迁移时可以使用任意尺寸的图像,而且输入的风格图像的尺寸和内容图像的尺寸大小也可以不相同(在实际应用中为了方便,通常会将风格图像的尺寸和内容图像的尺寸设置为相同),但目标图像尺寸和内容图像的尺寸需要相同,这样才能计算和比较内容损失的大小。下面定义load_image()函数用于读取图像,在读取图像的同时,控制图像的尺寸大小,程序如下所示:
def load_image(img_path,max_size=400,shape=None):
image=Image.open(img_path).convert('RGB')
#如果图片尺寸过大,就对图像进行尺寸变换
if max(image.size) > max_size:
size=max_size
else:
size=max(image.size)
#如果指定了图像的尺寸,就像图像转化为shape指定的尺寸
if shape is not None:
size=shape
#使用transform将图像转化为张量,并进行标准化
in_transform=transforms.Compose([
transforms.Resize(size),#图像尺寸变换,图像的短边匹配size
transforms.ToTensor(),#数组转化为张量
#图像进行标准化
transforms.Normalize((0.485,0.456,0.406),(0.229,0.224,0.225))
])
image=in_transform(image)[:3,:,:].unsqueeze(dim=0)
return image
load_image()函数有三个参数,第一个参数是输入需要读取图像的路径img_path,第二个参数和第三个参数用于控制图像的大小。如果指定了max_size参数,在读取图像时,若图像的尺寸过大,图像会进行相应的缩小,如果指定了图像的尺寸( shape参数),则将图像转化为shape指定的大小。读取图像后,为了方便通过卷积网络计算相关的特征输出,使用transforms的相关转换操作,对图像进行预处理,最后将输出一个可以使用的四维张量image。上述读取后的图像并不能通过matplotlib库直接进行可视化,需要定义一个im_convert()函数,该函数可以将一张图像的四维张量转化为一个可以使用matplotlib库可视化的三维数组,程序如下所示:
def im_convert(tensor):
"""
将[1,c,h,w]维度的张量转化为[h,w,c]的数组
因为张量进行了标准化,所以要进行标准化逆变换
:param tensor:
:return:
"""
image=tensor.data.numpy().squeeze()#去除batch维度数据
image=image.transpose(1,2,0)#置换数组的维度[c,h,w]->[h,w,c]
#进行标准化的逆操作
image=image * np.array((0.229,0.224,0.225))+ np.array((0.485,0.456,0.406))
image=image.clip(0,1)#将图像的取值剪切到0-1
return image
下面的程序将读取需要使用的风格图像和内容图像,并将它们可视化。
content=load_image(r"C:\\Users\\zex\\Desktop\\sky.jpg",max_size=400)
print("content shape:",content.shape)
#根据内容图像的宽高来设置风格图像的宽高
style=load_image(r"C:\\Users\\zex\\Desktop\\fangao.png",shape=content.shape[-2:])
print("style shape:",style.shape)
#可视化图像,可视化内容图像和风格图像
fig,(ax1,ax2)=plt.subplots(1,2,figsize=(12,5))
ax1.imshow(im_convert(content))
ax1.set_title("content")
ax2.imshow(im_convert(style))
ax2.set_title("style")
plt.show()
运行上述程序得到如下所示的输出和如下图所示的图像。为了保证两张图像具有相同的大小,在程序中通过内容图像的尺寸来定义风格图像的尺寸。
2.图像的输出特征和Gram矩阵的计算
为了更方便获取图像在VGG19网络指定层上的特征映射输出,定义一个get_features()函数,程序如下所示:
def get_features(image,model,layer=None):
"""
将一张图像image在一个网络model中进行前向传播计算,并获取指定层layer中特征映射输出
:param image:
:param model:
:param layer:
:return:
"""
#将PyTorch的VGGNet的完整映射层名称与论文中的名称相对应
#layers参数指定:需要用于图像的内容和样式表示的图层
##如果layers没有指定、就使用默认的层
if layer is None:
layers=
'0':'conv1_1',
'5':'conv2_1',
'10':'conv3_1',
'19':'conv4_1',
'21':'conv4_2',
'28':'conv5_1'
features=#获取的每层特征保存到字典中
x=image#需要获取的特征图像
#model._modules是一个字典,保存着网络model每层信息
for name,layer in model._modules.items():
#从第一层开始获取图像的特征
x=layer(x)
#如果是layers参数指定的特征,那就保存到features中
if name in layers:
features[layers[name]]=x
return features
get_features()函数通过输人参数图像(image),使用网络(model)和指定的层参数( layers),输出图像在指定网络层上的特征映射,并将输出的结果保存在一个字典中,如指定VGG19网络,但不指定layers参数,默认情况下会输出VGG19网络的conv1_1、conv2_1、conv3_1、conv4_1、conv4_2、conv5_1层的特征映射。比较两个图像是否具有相同的风格时,可以使用Gram矩阵来评价。我们定义函数gram_matrix()对一张图像的特征映射输出计算Gram矩阵。
def gram_matrix(tensor):
"""
计算指定向量的Gram matrix,该矩阵表示图像的风格特征
格拉姆矩阵最终能够在保证内容的情况下,进行风格传输
tensor:是一张图像前向计算后的一层特征映射
:param tensor:
:return:
"""
#获得tensor的batch_size,depth,height,width
_,d,w,h=tensor.size()
#改变矩阵的维度为(深度,高*宽)
tensor=tensor.view(d,h*w)
#计算gram matrix
gram=torch.mm(tensor,tensor.t())
return gram
在上面定义的gram_matrix()函数是计算一张图像Gram矩阵,针对输入的四维特征映射,将其每一个特征映射设置为一个向量,得到一个行为d(特征映射数量),列为h* w(每个特征映射的像素数量)的矩阵,该矩阵乘以其转置即可得到需要的Gram矩阵。
在定义好两个辅助函数后,下面针对内容图像和风格图像计算特征输出,并且计算风格图像在每个特征输出上的Gram矩阵,程序如下所示:
#计算在第一次训练之前内容特征和风格特征,使用get_features函数
content_features=get_features(content,vgg)
#计算风格图像的风格表示
style_features=get_features(style,vgg)
#为风格图像的风格表示计算每层的格拉姆矩阵,使用字典保存
style_grams=layer: gram_matrix(style_features[layer]) for layer in style_features
#使用内容图像的副本创建一个目标图像,训练时对目标图像进行调整
target=content.clone().requires_grad_(True)
3.进行图像风格迁移
在相关准备工作做好之后,下面就可以使用相关图像和网络进行图像风格迁移的学习,为了训练效果,在计算风格时,针对不同层的风格特征映射Gram矩阵,定义不同大小的权重,此处使用style_weights字典法完成,并且针对最终的损失,内容损失权重α和风格损失权重β分别定义为1和1×,程序如下所示:
style_weights='conv1_1':1.,
'conv2_1':0.75,
'conv3_1':0.2,
'conv4_1':0.2,
'conv5_2':0.2
alpha=1
beta=1e6
content_weight=alpha
style_weight=beta
需要注意的是,在style_weights中没有定义conv4_2层的Gram权重,这是因为该层的特征映射用于度量图像内容的相似性。
定义好权重参数后,下面使用Adam优化器进行训练,其中学习率为0.0003,并且为了监督网络在训练过程中的结果,每间隔1000次迭代输出目标图像的可视化情况,用于观察,并将迭代过程中每次相关损失值保存在列表中。用于优化目标图像的程序如下所示:
show_every=1000#每迭代1000次输出一个中间结果
#将损失保存
total_loss_all=[]
content_loss_all=[]
style_loss_all=[]
#使用Adam优化器
optimizer=optim.Adam([target],lr=0.0003)
steps=5000#优化时迭代的次数
t0=time.time()#记录需要的时间
for i in range(steps):
#获取目标图像的特征
target_features=get_features(target,vgg)
#计算内容损失
content_loss=torch.mean((target_features["conv4_2"] - content_features["conv4_2"])**2)
#计算风格损失,并且初始化为0
style_loss=0
#将每层的gram_matrix损失相加
for layer in style_weights:
#计算要生成的图像风格表示
target_feature = target_features[layer]
target_gram=gram_matrix(target_feature)
_,d,h,w=target_feature.shape
#获取风格图像在每层的风格的gram_matrix
style_gram=style_grams[layer]
#计算要生成图像的风格和风格图像的风格之间的差距,每层都有一个权重
layer_style_loss=style_weights[layer] * torch.mean((target_gram-style_gram)**2)
#累加计算风格差异损失
style_loss +=layer_style_loss/(d*h*w)
#计算一次迭代的总的损失,即内容损失和风格损失的加权和
total_loss=content_weight * content_loss + style_weight * style_loss
#保留三种损失大小
content_loss_all.append(content_loss.item())
style_loss_all.append(style_loss.item())
total_loss_all.append(total_loss.item())
#更新需要生成的目标图像
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
#输出每show_every次迭代后的生成图像
if i % show_every==0:
print('Total loss:',total_loss.item())
print('Use time:',(time.time()-t0)/3600,'hour')
newIm=im_convert(target)
plt.imshow(newIm)
plt.title("Iteration:"+str(i)+'times')
plt.show()
result=Image.fromarray((newIm * 255).astype(np.uint8))
result.save('C:\\\\Users\\\\zex\\\\Desktop\\\\' +str(i)+'.bmp')
在上面的程序中还需要注意以下几点:
(1)优化器的使用方式为optim.Adam([target], lr=0.0003),表明在优化器中,最终要优化的参数是目标图像的像素值,不会优化VGG网络中的权重等参数。
(2)获取目标图像在相关层的特征输出时使用get_features(target, vgg)函数,并且因为内容图像的特征映射在conv4_2层,所以内容损失计算时,需提取指定层的输出,即使用target_features['conv4_2']获得目标图像的内容表示,以及使用content__features['conv4_2']获得内容图像的内容表示。
(3)由于图像的风格表示的损失是通过多个层来表示,所以需要通过for循环来逐层计算相关的Gram矩阵和风格损失。
(4)最终的损失是风格损失和内容损失的加权和。
(5)为了观察和保留图像风格在迁移过程中的结果,将图像每间隔1000次迭代计算后的结果进行可视化并保存到指定的文件中。
由于以上程序训练时间十分漫长,这也是普通图像风格迁移方法的最大缺点,因此最终结果这里就不展示了。下一节课介绍快速图像风格迁移方法。
PyTorch-11 进行神经风格迁移neural style tutorial
要查看带有配图的文章内容,请前往 http://studyai.com/pytorch-1.4/advanced/neural_style_tutorial.html
本教程介绍了如何实现由Leon A.Gatys开发的 Neural-Style algorithm 。 Neural-Style, 或 Neural-Transfer, 允许你对一幅图像采取一种新的艺术风格的形象和再现。 该算法接受输入图像(input image)、 内容图像(content-image)和风格图像(style-image)三种图像,并对输入进行修改, 使之与内容图像的内容和风格图像的艺术风格相似。 content1
底层原理
原理很简单:我们定义了两个距离,一个用于内容(DC ) ,一个用于样式(DS)。 DC 测量两个图像之间的内容有多不同,而 DS
测量两个图像之间的风格有多不同。 然后,我们接受第三个图像作为输入,并转换它,以最小化它与内容图像的内容距离和 与样式图像的风格距离。现在我们可以导入必要的包并开始 neural transfer。 导入包和选择设备
下面所列出的包都是实现 neural transfer 时所用到的包。
torch, torch.nn, numpy (用PyTorch神经网络不可缺少的软件包)
torch.optim (高效的梯度下降算法优化包)
PIL, PIL.Image, matplotlib.pyplot (加载和展示图像的包)
torchvision.transforms (把 PIL 图像转换为tensors)
torchvision.models (训练 和 加载 预训练的模型)
copy (深度拷贝模型; system package)
from __future__ import print_function
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from PIL import Image
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
import torchvision.models as models
import copy
接下来,我们需要选择在哪个设备上运行网络,并导入内容和样式图像。 在大型图像上运行neural transfer算法需要花费更长的时间,并且在GPU上运行的速度要快得多。 我们可以使用 torch.cuda.is_available() 来检测是否有可用的GPU。 接下来,我们将 torch.device 设置为在整个教程中使用。 此外,.to(device) 方法用于将张量或模块移动到所需的设备。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
加载图像
现在我们将导入样式图像和内容图像。原始PIL图像的值介于0到255之间,但是当转换为torch tensors时, 它们的值被转换为0和1之间。图像也需要调整大小以具有相同的尺寸。需要注意的一个重要细节是, torch library中的神经网络的张量值从0到1变化。如果您试图向网络提供取值为0到255的张量图像, 那么激活的特征映射将无法感觉到预期的内容和样式。 然而,来自Caffe库的预训练网络被训练成0到255的张量图像.
Note
这里是本教程用到的两张图片的下载地址: picasso.jpg 和 dancing.jpg. 下载这两张图片然后将它们放到你当前工作目录中名称为 images 的文件夹中。
# 输出图像的期望尺寸
imsize = 512 if torch.cuda.is_available() else 128 # 如果没有GPU的话,就把尺寸搞小点儿
loader = transforms.Compose([
transforms.Resize(imsize), # 缩放导入的图像
transforms.ToTensor()]) # 把它转换成 torch tensor
def image_loader(image_name):
image = Image.open(image_name)
# 虚拟的 batch 维 ,为了满足网络输入对纬度的要求
image = loader(image).unsqueeze(0)
return image.to(device, torch.float)
style_img = image_loader("./data/images/neural-style/picasso.jpg")
content_img = image_loader("./data/images/neural-style/dancing.jpg")
assert style_img.size() == content_img.size(), \\
"we need to import style and content images of the same size"
现在,让我们通过将图像的副本转换为PIL格式并使用 plt.imshow 显示副本 来创建一个显示图像的函数。我们将尝试显示内容图像和样式图像, 以确保它们被正确导入。
unloader = transforms.ToPILImage() # 再次转换为 PIL image
plt.ion()
def imshow(tensor, title=None):
image = tensor.cpu().clone() # we clone the tensor to not do changes on it
image = image.squeeze(0) # remove the fake batch dimension
image = unloader(image)
plt.imshow(image)
if title is not None:
plt.title(title)
plt.pause(0.001) # pause a bit so that plots are updated
plt.figure()
imshow(style_img, title='Style Image')
plt.figure()
imshow(content_img, title='Content Image')
损失函数
内容损失(Content Loss)
内容损失是一个函数,它表示了一个单独层的加权内容距离。该函数接收处理输入 X 的网络的 层 L 的特征图 FXL ,返回输入图像 X 和 内容图像 C 。 内容图像的特征图(FCL)必须已知以便能够计算 内容距离。我们将这个函数实现为一个 torch module ,它有一个构造器 接受 FCL 作为输入。 该距离 ∥FXL−FCL∥2
是两个特征图集合之间的平均平方误差,可以使用 nn.MSELoss 来计算。
我们将把这个内容损失module直接加到计算内容距离的卷积层后面。在这种方式下,每次网络接到一张输入图像, 内容损失将会在需要的层被计算出来,并且因为 auto grad, 所有的梯度将会被计算。 现在, 为了使得内容损失层变得透明,我们需要定义一个 forward 方法 来计算内容损失然后返回该层的输入。 计算出的损失被保存为module的参数。
class ContentLoss(nn.Module):
def __init__(self, target,):
super(ContentLoss, self).__init__()
# we 'detach' the target content from the tree used
# to dynamically compute the gradient: this is a stated value,
# not a variable. Otherwise the forward method of the criterion
# will throw an error.
self.target = target.detach()
def forward(self, input):
self.loss = F.mse_loss(input, self.target)
return input
Note
重要细节: 虽然这个模块名为 ContentLoss,但它不是一个真正的PyTorch损失函数。 如果要将内容损失定义为PyTorch损失函数,则必须创建PyTorch自动梯度函数,以便在 backward 方法中手动计算/实现梯度。
风格损失(Style Loss)
The style loss module 的实现与 content loss module 的实现类似。 它在网络中作为一个透明的层去计算该层的风格损失,我们需要计算 gram 矩阵 GXL 。 gram 矩阵是给定矩阵和该矩阵的转置相乘的结果。在这个应用中,给定的矩阵是 层 L 的特征图 FXL 的 reshaped 版本。 FXL 被 reshape 来形成 F^XL, 一个 KxN 矩阵, 其中 K 是层 L 的特征图的数量,而 N 是任意向量化的特征图 FkXL 的长度。比如说,F^XL 的第一行对应于第一个 向量化的特征图 F1XL
。
最终, gram 矩阵必须通过将每个元素除以矩阵中的元素总数来标准化。这种归一化是为了抵消 具有大 N 维数的 F^XL
矩阵在gram矩阵中产生较大值的事实。 这种特别大的值将会引起前面的层(在池化层之前的层)在梯度下降过程中施加重要的影响。 Style features 倾向于在网络中更深的层,所以这个归一化步骤极其重要。
def gram_matrix(input):
a, b, c, d = input.size() # a=batch size(=1)
# b=number of feature maps
# (c,d)=dimensions of a f. map (N=c*d)
features = input.view(a * b, c * d) # resise F_XL into \\hat F_XL
G = torch.mm(features, features.t()) # compute the gram product
# we 'normalize' the values of the gram matrix
# by dividing by the number of element in each feature maps.
return G.div(a * b * c * d)
现在,style loss module 看起来与content loss module 几乎完全一样。 使用 GXL 和 GSL
之间的均方误差计算style distance。
class StyleLoss(nn.Module):
def __init__(self, target_feature):
super(StyleLoss, self).__init__()
self.target = gram_matrix(target_feature).detach()
def forward(self, input):
G = gram_matrix(input)
self.loss = F.mse_loss(G, self.target)
return input
导入模型
现在我们需要引进一个预先训练过的神经网络。我们将使用19层VGG网络,就像论文中使用的那样。
PyTorch实现的VGG是一个模块(module),分为两个子 Sequential 模块: features (包含卷积层和池化层)和 classifier (包含完全连接的层)。 我们将使用features
module,因为我们需要 个别卷积层的输出 以测量内容损失和风格损失。 有些层在训练过程中的行为与评估不同,因此我们必须使用 .eval() 将网络设置为评估模式。
cnn = models.vgg19(pretrained=True).features.to(device).eval()
另外, VGG 网络 是在每个通道被均值为 mean=[0.485, 0.456, 0.406] 和 std=[0.229, 0.224, 0.225] 所规范化的图像上训练的。 我们将使用它们归一化图像,然后把归一化图像送给网络处理。
cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).to(device)
cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).to(device)
# 创建一个module去归一化输入图像,以便我们可以简单滴将它们送给 nn.Sequential 。
class Normalization(nn.Module):
def __init__(self, mean, std):
super(Normalization, self).__init__()
# .view the mean and std to make them [C x 1 x 1] so that they can
# directly work with image Tensor of shape [B x C x H x W].
# B is batch size. C is number of channels. H is height and W is width.
self.mean = torch.tensor(mean).view(-1, 1, 1)
self.std = torch.tensor(std).view(-1, 1, 1)
def forward(self, img):
# normalize img
return (img - self.mean) / self.std
Sequential module 包含一个由child modules构成的有序的list。 比如, vgg19.features 包含一个序列 (Conv2d, ReLU, MaxPool2d, Conv2d, ReLU…) aligned in the right order of depth. 我们需要在他们检测到的卷积层之后立即添加内容损失层和风格损失层。 为此,我们必须创建一个内容损失模块和风格损失模块被正确插入的 新 Sequential 模块。
# 计算 style/content losses 所需要的深度的层:
content_layers_default = ['conv_4']
style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']
def get_style_model_and_losses(cnn, normalization_mean, normalization_std,
style_img, content_img,
content_layers=content_layers_default,
style_layers=style_layers_default):
cnn = copy.deepcopy(cnn)
# normalization module
normalization = Normalization(normalization_mean, normalization_std).to(device)
# just in order to have an iterable access to or list of content/syle
# losses
content_losses = []
style_losses = []
# assuming that cnn is a nn.Sequential, so we make a new nn.Sequential
# to put in modules that are supposed to be activated sequentially
model = nn.Sequential(normalization)
i = 0 # increment every time we see a conv
for layer in cnn.children():
if isinstance(layer, nn.Conv2d):
i += 1
name = 'conv_'.format(i)
elif isinstance(layer, nn.ReLU):
name = 'relu_'.format(i)
# The in-place version doesn't play very nicely with the ContentLoss
# and StyleLoss we insert below. So we replace with out-of-place
# ones here.
layer = nn.ReLU(inplace=False)
elif isinstance(layer, nn.MaxPool2d):
name = 'pool_'.format(i)
elif isinstance(layer, nn.BatchNorm2d):
name = 'bn_'.format(i)
else:
raise RuntimeError('Unrecognized layer: '.format(layer.__class__.__name__))
model.add_module(name, layer)
if name in content_layers:
# add content loss:
target = model(content_img).detach()
content_loss = ContentLoss(target)
model.add_module("content_loss_".format(i), content_loss)
content_losses.append(content_loss)
if name in style_layers:
# add style loss:
target_feature = model(style_img).detach()
style_loss = StyleLoss(target_feature)
model.add_module("style_loss_".format(i), style_loss)
style_losses.append(style_loss)
# now we trim off the layers after the last content and style losses
for i in range(len(model) - 1, -1, -1):
if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss):
break
model = model[:(i + 1)]
return model, style_losses, content_losses
接下来我们选择输入图像。 你可以使用内容图像的副本或者一副白噪声图像 作为输入图像。
input_img = content_img.clone()
# 如果你想使用白噪声,就去掉下面这行代码的注释:
# input_img = torch.randn(content_img.data.size(), device=device)
# 把原始输入图像加入到 figure 中:
plt.figure()
imshow(input_img, title='Input Image')
梯度下降
该算法的作者 建议, 我们使用 L-BFGS 算法来运行梯度下降。不像训练一个网络,我们想要训练的是输入图像以便最小化 content/style losses。 我们将创建一个 PyTorch L-BFGS 优化器 optim.LBFGS 并把我们的图像传递给它作为要被优化的张量。
def get_input_optimizer(input_img):
# this line to show that input is a parameter that requires a gradient
optimizer = optim.LBFGS([input_img.requires_grad_()])
return optimizer
最后,我们必须定义一个执行neural transfer的函数。对于网络的每一次迭代, 它都得到一个更新的输入,并计算新的损失。 我们将运行每个loss module的 backward 方法来动态地计算它们的梯度。 优化器需要一个 “closure” 函数,它重新评估模块并返回损失。
我们还有最后一个制约因素要解决。网络可以尝试优化输入值,其值超过图像的0到1张量范围。 我们可以通过每次网络运行时将输入值修正为0到1来解决这个问题。
def run_style_transfer(cnn, normalization_mean, normalization_std,
content_img, style_img, input_img, num_steps=300,
style_weight=1000000, content_weight=1):
"""Run the style transfer."""
print('Building the style transfer model..')
model, style_losses, content_losses = get_style_model_and_losses(cnn,
normalization_mean, normalization_std, style_img, content_img)
optimizer = get_input_optimizer(input_img)
print('Optimizing..')
run = [0]
while run[0] <= num_steps:
def closure():
# correct the values of updated input image
input_img.data.clamp_(0, 1)
optimizer.zero_grad()
model(input_img)
style_score = 0
content_score = 0
for sl in style_losses:
style_score += sl.loss
for cl in content_losses:
content_score += cl.loss
style_score *= style_weight
content_score *= content_weight
loss = style_score + content_score
loss.backward()
run[0] += 1
if run[0] % 50 == 0:
print("run :".format(run))
print('Style Loss : :4f Content Loss: :4f'.format(
style_score.item(), content_score.item()))
print()
return style_score + content_score
optimizer.step(closure)
# a last correction...
input_img.data.clamp_(0, 1)
return input_img
最后, 我们可以运行算法
output = run_style_transfer(cnn, cnn_normalization_mean, cnn_normalization_std,
content_img, style_img, input_img)
plt.figure()
imshow(output, title='Output Image')
# sphinx_gallery_thumbnail_number = 4
plt.ioff()
plt.show()
以上是关于Pytorch实现图像风格迁移的主要内容,如果未能解决你的问题,请参考以下文章
PyTorch实战用PyTorch实现基于神经网络的图像风格迁移