StyleTransfer-Pytorch.ipynb
这里用的是cs231n2020的代码,因为有些pytorch的用法和以前的不一样了。
注意:StyleTransfer-Pytorch.ipynb和style_transfer_pytorch.py都有设置运算类型(CPU或GPU)的地方,需要两处同时更改。
预处理
接下来会实现"Image Style Transfer Using Convolutional Neural Networks" (Gatys et al., CVPR 2015)里的风格迁移。
要实现风格迁移,首先在深度网络的特征空间中建立一个与每张图像的内容和风格相匹配的损失函数,然后对图像本身的像素进行梯度下降。
提取特征所用的神经网络是 SqueezeNet(一个在ImageNet上预训练好的轻量级网络),实际上可以使用任一神经网络进行特征提取,但是SqueezeNet相对较小并且比较有效。
下图是风格迁移的结果:
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as T
import PIL
import numpy as np
import matplotlib.pyplot as plt
from cs231n.image_utils import SQUEEZENET_MEAN, SQUEEZENET_STD
%matplotlib inline
# 自动加载其他文件里的模型、函数
%load_ext autoreload
%autoreload 2
加载’style-transfer-checks.npz’
from cs231n.style_transfer_pytorch import preprocess, deprocess, rescale, rel_error, features_from_img
# 如果'style-transfer-checks.npz'在同级目录下
CHECKS_PATH = 'style-transfer-checks.npz'
# 否则
#CHECKS_PATH = '/content/drive/My Drive/{}/{}'.format(FOLDERNAME, 'style-transfer-checks.npz')
assert CHECKS_PATH is not None, "[!] Choose path to style-transfer-checks.npz"
# 存放图片的位置
STYLES_FOLDER = CHECKS_PATH.replace('style-transfer-checks.npz', 'styles')
# 加载'style-transfer-checks.npz'
answers = dict(np.load(CHECKS_PATH))
# 如果只有cpu就用这行代码
#dtype = torch.FloatTensor
dtype = torch.cuda.FloatTensor
# 加载预训练的SqueezeNet模型
cnn = torchvision.models.squeezenet1_1(pretrained=True).features
cnn.type(dtype)
# 不需要训练SqueezeNet模型,所以不需要计算SqueezeNet参数的梯度
for param in cnn.parameters():
param.requires_grad = False
计算损失
风格迁移会生成一张图片,这张图片保持一张输入图片的内容,却和另一张输入图片的风格相似。所以风格迁移的损失函数是由内容损失、风格损失和正则化损失构成的,是三者的加权求和。
内容损失
内容损失衡量了生成图像的特征和输入图片特征的差异(神经网络在进行分类时,考虑了图像的内容,所以神经网络的特征可以在一定程度上表现两张图片的内容是否相似)。
我们只关心网络中特定层(比如说,第ℓ层)的特征(𝐴ℓ∈ℝ1×𝐶ℓ×𝐻ℓ×𝑊ℓ),其中𝐶ℓ是第ℓ层的通道数,𝐻ℓ和𝑊ℓ分别是高和宽。
通过𝑀ℓ=𝐻ℓ×𝑊ℓ变换特征图的维度, 𝐹ℓ∈ℝ𝐶ℓ×𝑀ℓ是生成图片的特征,𝑃ℓ∈ℝ𝐶ℓ×𝑀ℓ是输入(内容)图片的特征,𝑤𝑐是内容损失的权重。
内容损失的公式如下:
cs231n/style_transfer_pytorch.py中实现content_loss()后运行下面的代码,应该得到小于0.001的相对误差。
from cs231n.style_transfer_pytorch import content_loss, extract_features, features_from_img
def content_loss_test(correct):
content_image = '%s/tubingen.jpg' % (STYLES_FOLDER)
# 用于调整图片的大小,维持比例的同时,讲短边缩小为image_size个像素
image_size = 192
# 只关心第3层的特征
content_layer = 3
content_weight = 6e-2
# 获取SqueezeNet每一层的特征,content_img_var是预处理过的特征,用于生成bad_img
c_feats, content_img_var = features_from_img(content_image, image_size, cnn)
bad_img = torch.zeros(*content_img_var.data.size()).type(dtype)
# 提取另一张图片的特征
feats = extract_features(bad_img, cnn)
# 计算两个(SqueezeNet第3层)特征的内容损失
student_output = content_loss(content_weight, c_feats[content_layer], feats[content_layer]).cpu().data.numpy()
error = rel_error(correct, student_output)
print('Maximum error is {}'.format(error))
content_loss_test(answers['cl_out'])
# Maximum error is 0.00017621209553908557
风格损失
我们希望生成图片与输入(风格)图片的特征统计数据相近,可以用协方差矩阵相似来表现这种相近。但是协方差矩阵计算比较麻烦,所以使用格拉姆矩阵来代替协方差矩阵(它比较容易计算,并且在实践中有比较好的结果)。
对于特定的层ℓ,计算每个通道特征之间的格拉姆矩阵𝐺。假设神经网络第ℓ层的特征为𝐹ℓ∈ℝ𝐶ℓ×𝑀ℓ,有𝐶ℓ个通道,那么格拉姆矩阵的大小为(𝐶ℓ,𝐶ℓ)。
格拉姆矩阵的计算公式如下:
假设𝐺ℓ是生成图片的格拉姆矩阵,𝐴ℓ是输入(风格)图片的格拉姆矩阵,𝑤ℓ是第ℓ层风格损失的权重,那么第ℓ层的风格损失为:
在实际应用中,一般计算多层风格损失,而不是一层。所以,最终的风格损失要进行多层求和:
测试格拉姆矩阵是否正确
from cs231n.style_transfer_pytorch import gram_matrix
def gram_matrix_test(correct):
style_image = '%s/starry_night.jpg' % (STYLES_FOLDER)
style_size = 192
feats, _ = features_from_img(style_image, style_size, cnn)
# 计算神经网络第五层的格拉姆矩阵
student_output = gram_matrix(feats[5].clone()).cpu().data.numpy()
error = rel_error(correct, student_output)
print('Maximum error is {}'.format(error))
gram_matrix_test(answers['gm_out'])
# Maximum error is 0.0004066229157615453
测试风格损失