神经风格迁移是一种优化技术,用于将两个图像——一个内容图像和一个风格参考图像(如著名画家的一个作品)——混合在一起,使输出的图像看起来像内容图像, 但是用了风格参考图像的风格。
这是通过优化输出图像以匹配内容图像的内容统计数据和风格参考图像的风格统计数据来实现的。 这些统计数据可以使用卷积网络从图像中提取。
导入和配置模块
下载图像并选择风格图像和内容图像:
定义一个加载图像的函数,并将其最大尺寸限制为 512 像素。
创建一个简单的函数来显示图像:
本教程演示了原始的风格迁移算法。其将图像内容优化为特定风格。在进入细节之前,让我们看一下 TensorFlow Hub 模块如何快速风格迁移:
使用模型的中间层来获取图像的内容和风格表示。 从网络的输入层开始,前几个层的激励响应表示边缘和纹理等低级 feature (特征)。 随着层数加深,最后几层代表更高级的 feature (特征)——实体的部分,如轮子或眼睛。 在此教程中,我们使用的是 VGG19 网络结构,这是一个已经预训练好的图像分类网络。 这些中间层是从图像中定义内容和风格的表示所必需的。 对于一个输入图像,我们尝试匹配这些中间层的相应风格和内容目标的表示。
加载 VGG19 并在我们的图像上测试它以确保正常运行:
现在,加载没有分类部分的 VGG19 ,并列出各层的名称:
运行结果如下:
从网络中选择中间层的输出以表示图像的风格和内容:
用于表示风格和内容的中间层
那么,为什么我们预训练的图像分类网络中的这些中间层的输出允许我们定义风格和内容的表示?
从高层理解,为了使网络能够实现图像分类(该网络已被训练过),它必须理解图像。 这需要将原始图像作为输入像素并构建内部表示,这个内部表示将原始图像像素转换为对图像中存在的 feature (特征)的复杂理解。
这也是卷积神经网络能够很好地推广的一个原因:它们能够捕获不变性并定义类别(例如猫与狗)之间的 feature (特征),这些 feature (特征)与背景噪声和其他干扰无关。 因此,将原始图像传递到模型输入和分类标签输出之间的某处的这一过程,可以视作复杂的 feature (特征)提取器。通过这些模型的中间层,我们就可以描述输入图像的内容和风格。
使用tf.keras.applications中的网络可以让我们非常方便的利用Keras的功能接口提取中间层的值。
在使用功能接口定义模型时,我们需要指定输入和输出:
model = Model(inputs, outputs)
以下函数构建了一个 VGG19 模型,该模型返回一个中间层输出的列表:
然后建立模型:
运行结果如下:
图像的内容由中间 feature maps (特征图)的值表示。
这可以使用tf.linalg.einsum函数来实现:
构建一个返回风格和内容张量的模型。
在图像上调用此模型,可以返回 style_layers 的 gram 矩阵(风格)和 content_layers 的内容:
运行结果如下:
使用此风格和内容提取器,我们现在可以实现风格传输算法。我们通过计算每个图像的输出和目标的均方误差来做到这一点,然后取这些损失值的加权和。
设置风格和内容的目标值:
定义一个 tf.Variable 来表示要优化的图像。 为了快速实现这一点,使用内容图像对其进行初始化( tf.Variable 必须与内容图像的形状相同)
由于这是一个浮点图像,因此我们定义一个函数来保持像素值在 0 和 1 之间:
创建一个 optimizer 。 本教程推荐 LBFGS,但 Adam 也可以正常工作:
为了优化它,我们使用两个损失的加权组合来获得总损失:
使用 tf.GradientTape 来更新图像。
现在,我们运行几个步来测试一下:
此实现只是一个基础版本,它的一个缺点是它会产生大量的高频误差。 我们可以直接通过正则化图像的高频分量来减少这些高频误差。 在风格转移中,这通常被称为总变分损失:
而且,本质上高频分量是一个边缘检测器。 我们可以从 Sobel 边缘检测器获得类似的输出,例如:
运行结果如下:
以上说明了总变分损失的用途。但是无需自己实现,因为 TensorFlow 包含了一个标准实现:
运行结果如下:
选择 total_variation_loss 的权重:
现在,将它加入 train_step 函数中:
重新初始化优化的变量:
并进行优化:
最后,保存结果: