人人都能看懂的「风格迁移」简介 ~ Neural Style Transfer
基于 CNN 的图像风格迁移
~什么是风格迁移~
我们都知道,每一幅画,都可以看成「内容」与「画风」的组合。
比如名画《呐喊》画了一个张着嘴巴的人,这是一种表现主义的画风。
还有梵高这幅《星夜》,非常有个人风格的一幅夜景。
再比如这幅画,一个二次元画风的少女。
最后展示的是一个帅哥,这是一张写实的照片。
所谓风格迁移,就是把一张图片的风格,嵌入到另一张图片的内容里,形成一张新的图片:
如上图所示,左上角的A是一幅真实的照片,BCD分别是把其他几幅画作的风格迁移到原图中形成的新图片。
究竟是什么技术能够实现这么神奇的「风格迁移」效果呢?别急,让我们从几个简单的例子慢慢学起。
~复制一幅图片~
如果你想复制一幅图片,你会怎么做?
在Windows上,你可以打开画图软件,点击左上角的选择框,把要复制的图片框起来。Ctrl+C、Ctrl+V,就能轻松完成图像复制。
但是,我觉得的这种方法太简单了,不能体现出我们这些学过数学的人的智慧。我打算用一个更高端的方法。
我把复制图像的任务,看成一个数学上的优化问题。已知源图像S
,我要生成一个目标图像T
,使得二者均方误差MSE(S-T)
最小。这样,一个生成图像的问题,就变成求最优的T
的优化问题。
对于这个问题,我们可以随机初始化一张图像T
,然后对上面那个优化目标做梯度下降。几轮下来,我们就能求出最优的T
——一幅和源图像S
一模一样的目标图像。
这段逻辑可以PyTorch实现:
假设我们通过read_image
函数读取了一个图片img
,且把图片预处理成了[1, 3, H, W]
的格式。
source_img = read_image('dldemos/StyleTransfer/picasso.jpg')
我们可以随机初始化一个[1, 3, H, W]
大小的图片。由于这张图片是我们的优化对象,所以我们令input_img.requires_grad_(True)
,这样这张图片就可以被PyTorch自动优化了。
input_img = torch.randn(1, 3, *img_size)
input_img.requires_grad_(True)
之后,我们使用PyTorch的优化器LBFGS
,并按照优化器的要求传入被优化参数。(这是这篇论文的作者推荐的优化器~)
optimizer = optim.LBFGS([input_img])
一切变量准备就绪后,我们可以执行梯度下降了:
steps = 0
while steps <= 10:
def closure():
global steps
optimizer.zero_grad()
loss = F.mse_loss(input_img, source_img)
loss.backward()
steps += 1
if steps % 5 == 0:
print(f"Step {steps}:")
print(f"Loss: {loss}")
return loss
optimizer.step(closure)
这段代码有一点要注意:由于LBFGS
执行上的特殊性,我们要把执行梯度下降的代码封装成一个闭包(closure,即一个临时定义的函数),并把这个闭包传给optimizer.step
。
执行上面的代码进行梯度下降后,这个优化问题很快就能得到收敛。优化结束后,假设我们写好了一个后处理图片的函数save_image
,我们可以这样保存它:
save_image(input_img, 'work_dirs/output.jpg')
理论上,这幅图片会和我们的源图像img
一模一样。
大家看到这里,肯定一肚子疑惑:为什么要用这么复杂的方式去复制图像啊?就好像告诉你x=2,拿优化算法求和x完全相等的y一样。这不直接令y=2就行了吗?别急,让我们再看下去。
~拟合神经网络的输出~
刚才我们求解目标图像T
的过程,其实可以看成是拟合T
的某项特征与S
的特征的过程。只不过,我们使用的是像素值这个最基本的特征。假如我们去拟合更特别的一些特征,会发生什么事呢?
Gatys 等科学家发现,如果用预训练VGG模型不同层的卷积输出作为拟合特征,则可以拟合出不同的图像:
如果你对预训练VGG模型不熟,也不用担心。VGG是一个包含很多卷积层的神经网络模型。所谓预训练VGG模型,就是在图像分类数据集上训练过的VGG模型。经过了预训练后,VGG模型的各个卷积层都能提取出图像的一些特征,尽管这些特征是我们人类无法理解的。
上图中,越靠右边的图像,是用越深的卷积层特征进行特征拟合恢复出来的图像。从这些图像恢复结果可以看出,更深的特征只会保留图像的内容(形状),而难以保留图像的纹理(天空的颜色、房子的颜色)。
看到这,大家可能有一些疑惑:这些图片具体是怎么拟合出来的呢?让我们和刚刚一样,详细地看一看这一图像生成过程。
假设我们想生成上面的图c,即第三个卷积层的拟合结果。我们已经得到了模型model_conv123
,其包含了预训练VGG里的前三个卷积层。我们可以设立以下的优化目标:
source_feature = model_conv123(source_img)
input_feature = model_conv123(input_img)
# minimize MSE(source_feature, input_feature)
在实现时,我们只要稍微修改一下开始的代码即可。
首先,我们可以预处理出源图像的特征。注意,这里我们要用source_feature.detach()
来把source_feature
从计算图中取出,防止源图像被PyTorch自动更新。
source_img = read_image('dldemos/StyleTransfer/picasso.jpg')
source_feature = model_conv123(source_img).detach()
之后,我们可以用类似的方法做梯度下降:
steps = 0
while steps <= 50:
def closure():
global steps
optimizer.zero_grad()
input_feature = model_conv123(input_img)
loss = F.mse_loss(input_feature, source_feature)
loss.backward()
steps += 1
if steps % 5 == 0:
print(f"Step {steps}:")
print(f"Loss: {loss}")
return loss
optimizer.step(closure)
看到没,我们刚刚这种利用优化问题生成目标图像的方法并不愚蠢,只是一开始大材小用了而已。通过这种方法,我们可以生成一幅拟合了源图像在神经网络中的深层特征的目标图像。那么,怎么利用这种方法完成风格迁移呢?
~风格+内容=风格迁移~
Gatys 等科学家发现,不仅是卷积结果可以当作拟合特征,VGG的一些其他中间结果也可以作为拟合特征。受到之前用CNN做纹理生成的工作[2]的启发,他们发现用卷积结果的Gram矩阵作为拟合特征可以得到另一种图像生成效果:
上图中,右边a-e是用VGG不同卷积结果的Gram矩阵作为拟合特征,得到的对左图的拟合图像。可以看出,用这种特征来拟合的话,生成图像会失去原图的内容(比如星星和物体的位置完全变了),但是会保持图像的整体风格。
这里稍微提一下Gram矩阵的计算方法。Gram矩阵定义在两个特征的矩阵F_1, F_2 上。其中,每个特征矩阵F 是VGG某层的卷积输出张量F_conv(shape: [n, h, w]) reshape成一个矩阵F (shape: [n, h * w]) 的结果。Gram矩阵,就是两个特征矩阵F_1, F_2 的内积,即F_1 每个通道的特征向量和F_2 每个通道的特征向量的相似度构成的矩阵。我们这里假设F_1=F_2 ,即对某个卷积特征自身生成Gram矩阵。这段逻辑用代码实现如下:
def gram(x: torch.Tensor):
# x 是VGG卷积层的输出张量
n, c, h, w = x.shape
features = x.reshape(n * c, h * w)
features = torch.mm(features, features.T)
return features
其实,我们不必纠结于Gram矩阵的实际意义,以及为什么要使用Gram矩阵。后续研究证明,还有其他类似的特征也能达到和Gram矩阵一样的效果。我们只需要知道,通过拟合和卷积结果相关的特征,我们能得到保持源图像风格的拟合图像。
看到这里,大家或许已经明白风格迁移是怎么实现的了。风格迁移,其实就是既拟合一幅图像的内容,又去拟合另一幅图像的风格。我们把前一幅图像叫做内容图像,后一幅图像叫做风格图像。
我们在上一节知道了如何拟合内容,这一节知道了怎么去拟合风格。要把二者结合起来,只要令我们的优化目标既包含和内容图像的内容误差,又包含和风格图像的风格误差。在原论文中,这些误差是这样表达的:
上面第一行公式表达的是内容误差,第二行公式表达的是风格误差。
第一行公式中, , 分别是生成图像的卷积特征和源图像的卷积特征。
第二行公式中, 是生成图像的卷积特征, 是 的Gram矩阵, 是源图像卷积特征的Gram矩阵, 表示第 层的风格误差。在论文中,总风格误差是某几层风格误差的加权和,其中权重为 。事实上,不仅总风格误差可以用多层风格误差的加权和表示,总内容误差也可以用多层内容误差的加权和表示。只是在原论文中,只使用了一层的内容误差。
第三行中, 分别是内容误差的权重和风格误差的权重。实际上,我们只用考虑 的比值即可。如果 较大,则说明优化内容的权重更大,生成出来的图像更靠近内容图像。反之亦然。
只要用这个误差去替换我们刚刚代码实现中的误差,就可以完成图像的风格迁移了,听起来是不是十分简单?但是,用PyTorch实现风格迁移时还要考虑不少细节。在下篇文章中,我会对风格迁移的实现代码做一些讲解。
~思考~
其实这篇文章是比较早期的用神经网络做风格迁移的工作。在近两年里,肯定有许多试图改进此方法的研究。时至今日,再去深究这篇文章里的一些细节(为什么用Gram矩阵,应该用VGG的哪些层做拟合)已经意义不大了。我们应该关注的是这篇文章的主要思想。
这篇文章对我的最大启发是:神经网络不仅可以用于在大批数据集上训练,完成一项通用的任务,还可以经过预训练,当作一个特征提取器,为其他任务提供额外的信息。同样,要记住神经网络只是优化任务的一项特例,我们完全可以把梯度下降法用于普通的优化任务中。在这种利用了神经网络的参数,而不去更新神经网络参数的优化任务中,梯度下降法也是适用的。
此外,这篇文章中提到的「风格」也是很有趣的一项属性。这篇文章算是首次利用了神经网络中的信息,用于提取内容、风格等图像属性。这种提取属性(尤其是提取风格)的想法被运用到了很多的后续研究中,比如大名鼎鼎的StyleGAN。
长期以来,人们总是把神经网络当成黑盒。但是,这篇文章给了我们一个掀开黑盒的思路:通过拟合神经网络中卷积核的特征,我们能够窥见神经网络每一层保留了哪些信息。相信在之后的研究中,人们能够更细致地去研究神经网络的内在原理。
参考文献
[1] Gatys L A, Ecker A S, Bethge M. Image style transfer using convolutional neural networks[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2016: 2414-2423.
[2] Gatys L, Ecker A S, Bethge M. Texture synthesis using convolutional neural networks[J]. Advances in neural information processing systems, 2015, 28.
彩蛋
在理解了风格迁移是在做什么后,我就立刻想到:可不可以用风格迁移,把照片渲染成二次元风格呢?
在我实现完代码后,立马去尝试了一下。诶,好像这个算法还真有点东西:
究竟最后能生成怎样的图片呢?欢迎阅读下一篇文章——用PyTorch实现神经网络风格迁移。