Pytorch学习笔记-第八章
Pytorch学习笔记-第八章风格迁移
-
utils
-
- Gram矩阵
-
PackerVGG
-
transformer
-
main
-
- 训练
- 生成图片
记录一下个人学习和使用Pytorch中的一些问题。强烈推荐 《深度学习框架PyTorch:入门与实战》.写的非常好而且作者也十分用心,大家都可以看一看,本文为学习第八章风格迁移的学习笔记。
主要分析实现代码里面main,transformer,PackedVGG,utils这4个代码文件完成整个项目模型结构定义,训练及生成,还有输出展示的整个过程。
utils
这是项目中用到的一些额外工具包,对visdom封装更方便显示多个点以及网格显示图片这个老朋友就不多说了。此外还有一个图像里面常用的BN操作(注意,这里的BN时IBN既每个样本一个标准化,而不是整批的标准化,而且BN的均值标准差不是一般的0.5时专人计算过的imagenet上的均值和标准差)的函数以及获取封装的从路径获取风格图片的函数,还有一个在这种风格迁移任务里用到的计算Gram矩阵的函数。
Gram矩阵
Gram是用来描述图像整体风格特征的一种矩阵。假设输入的图像尺寸是BxCxHxW,那么得到的Gram矩阵的尺寸是BxCxC既只于输入的通道数有关。矩阵取值由如下公式计算

其中F_{ik}代表第i个输入通道(实际上输入的是多个特征图,所以就是第i个特征图)的第k个像素,同时可以发现该计算和位置信息无关,也就是说原始图片随机打散后生成新的特征图重新计算Gram矩阵也是一致的。说明Gram提取是整体风格而不是结构信息。代码实现计算Gram矩阵如下。
def gram_matrix(y):
"""
Input shape: b,c,h,w
Output shape: 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)
#batch矩阵乘法
return gram
AI写代码
其中用到了一个小技巧,对于CxHxW的输入来说如果我们把它调整为CxHW的二维矩阵,这个二维矩阵与自己的转置矩阵相乘的结果就是Gram矩阵。
PackerVGG
该文件主要是封装了一个预训练好了的VGG16网络,是用来提取我们想要的风格的特征的。看它的前向传播函数,最后返回的不是一般意义上最后分类结果,可是把VGG16前4个卷积块的输出(高层次特征图)当作结果返回。我们计算风格loss也是在VGG输出的特征图上计算,看生成图片和原始风格图片高层特征是否相近。
class Vgg16(torch.nn.Module):
def __init__(self):
super(Vgg16, self).__init__()
#自带的vgg16由两个nn.Sequential,一个用来提取特征的feature,一个分类的classifier,我们这里用featurn即可
features = list(vgg16(pretrained=True).features)[:23]
# the 3rd, 8th, 15th and 22nd layer of \
# self.features are: 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}:
#前4个卷积块的输出
results.append(x)
vgg_outputs = namedtuple("VggOutputs", ['relu1_2', 'relu2_2', 'relu3_3', 'relu4_3'])
#返回一个命名元组,可以通过obj.value的方式访问元素
return vgg_outputs(*results)
AI写代码
transformer
迁移部分的网络结构如下

对着结构图可以很清晰的看懂代码文件,要注意的地方也有几点:
- 没有采用一般的用0补齐图片边缘,而是采用边缘反射的像素补齐
reflection_padding = int(np.floor(kernel_size / 2))
self.reflection_pad = nn.ReflectionPad2d(reflection_padding)
AI写代码
- 上采样不是直接用的反卷积,而是Upsample再卷积来扩大尺寸。
class UpsampleConvLayer(nn.Module):
"""UpsampleConvLayer
instead of ConvTranspose2d, we do UpSample + Conv2d
see ref for why.
ref: http://distill.pub/2016/deconv-checkerboard/
"""
def __init__(self, in_channels, out_channels, kernel_size, stride, upsample=None):
super(UpsampleConvLayer, self).__init__()
self.upsample = upsample
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):
x_in = x
if self.upsample:
#插值上采样,默认使用最近邻
x_in = t.nn.functional.interpolate(x_in, scale_factor=self.upsample)
out = self.reflection_pad(x_in)
out = self.conv2d(out)
return out
AI写代码
- 没有全连接层,全是卷积,所以不要在意图片尺寸,都可以输入。
main
该文件除了一些设置的参数之外,就是训练和结果生成函数了。
训练
前面的说明已经铺垫好了,现在要来正式训练一下网络了,整体训练过程如下。

整个网络架构如下图。

需要注意的是(3)中计算风格loss,是所有风格图片和生成图片的Gram矩阵的均方误差之和。
生成图片
直接读取模型参数,然后读取原始图片送入模型保存输出结果即可。这个内容实现的网络叫做Fast Neural Style,正是因为它存在一个训练好的模型,可以快速完成风格迁移;而最初Neural Style文章里,类似GAN的操作是不同调整输出的,虽然输出的结果更好了,但是相当耗时间。
