神经风格迁移:使用 tf.keras 和 Eager Execution 利用深度学习创作艺术
2018 年 8 月 3 日
作者:Raymond Yuan,软件工程实习生

在本 教程 中,我们将学习如何使用深度学习将图像以另一种图像的风格进行合成(是否曾经希望你能像毕加索或梵高一样作画?)。这被称为 **神经风格迁移**!这是一种在 Leon A. Gatys 的论文,一种艺术风格的神经算法 中概述的技术,非常值得一读,你一定要去看看。

神经风格迁移是一种优化技术,用于将三张图像(一张 **内容** 图像、一张 **风格参考** 图像(例如著名画家的作品)以及你想要进行风格化的 **输入** 图像)融合在一起,使输入图像看起来像内容图像,但以风格图像的风格“绘制”。

例如,让我们取一张海龟的图像和葛饰北斋的《神奈川冲浪里》:
P. Lindgren 拍摄的绿海龟图像,来自 维基百科
现在,如果北斋决定将浪潮的纹理或风格添加到海龟图像中,会是什么样子?就像这样?
output image
这是魔法还是深度学习?幸运的是,这与魔法无关:风格迁移是一种有趣且引人入胜的技术,它展示了神经网络的能力和内部表示。

神经风格迁移的原理是定义两个距离函数,一个描述两张图像内容的差异,Lcontent,另一个描述两张图像在风格方面的差异,Lstyle。然后,给定三张图像,一张目标风格图像、一张目标内容图像以及输入图像(用内容图像初始化),我们尝试将输入图像进行变换,以最小化内容图像与其内容距离以及风格图像与其风格距离。

总而言之,我们将使用基本输入图像、我们想要匹配的内容图像以及我们想要匹配的风格图像。我们将通过最小化内容和风格距离(损失)来变换基本输入图像,从而创建一张与内容图像的内容相匹配且与风格图像的风格相匹配的图像。

将涵盖的具体概念

在此过程中,我们将获得实践经验并对以下概念形成直觉
  • **Eager Execution** — 使用 TensorFlow 的命令式编程环境,该环境会立即评估操作
  • 详细了解 Eager Execution
  • 实际操作(许多教程都可以在 Colaboratory 中运行)
  • **使用 函数式 API 定义模型** — 我们将构建模型的一个子集,该子集将使用函数式 API 为我们提供访问必要中间激活的权限
  • **利用预训练模型的特征图** — 学习如何使用预训练模型及其特征图
  • **创建自定义训练循环** — 我们将检查如何设置优化器以针对输入参数最小化给定损失

我们将遵循执行风格迁移的一般步骤

  1. 可视化数据
  2. 基本预处理/准备数据
  3. 设置损失函数
  4. 创建模型
  5. 针对损失函数进行优化
目标受众: 本文面向熟悉基本机器学习概念的中级用户。为了充分利用本文,你应该预计时间: 60 分钟

代码

你可以在此 链接 中找到本文的完整代码。如果你想逐步执行此示例,可以在此 链接 中找到 colab。

实施

我们将从启用 Eager Execution 开始。Eager Execution 允许我们以最清晰、最易读的方式完成此技术。
tf.enable_eager_execution()
print("Eager execution: {}".format(tf.executing_eagerly()))
 
Here are the content and style images we will use: 
plt.figure(figsize=(10,10))

content = load_img(content_path).astype('uint8')
style = load_img(style_path)

plt.subplot(1, 2, 1)
imshow(content, 'Content Image')

plt.subplot(1, 2, 2)
imshow(style, 'Style Image')
plt.show()
P .Lindgren 拍摄的绿海龟图像 - 来自 维基百科 和葛饰北斋的《神奈川冲浪里》图像 - 公有领域

定义内容和风格表示

为了获取图像的内容和风格表示,我们将查看模型中的一些中间层。中间层表示特征图,这些特征图随着你深入网络而变得越来越高级。在本例中,我们使用 VGG19 网络架构,这是一种预训练的图像分类网络。这些中间层对于定义图像的内容和风格表示是必要的。对于输入图像,我们将尝试在这些中间层匹配相应的风格和内容目标表示。

为什么是中间层?

你可能想知道,我们预训练的图像分类网络中的这些中间输出为什么允许我们定义风格和内容表示。从高层次来看,这种现象可以用以下事实来解释:为了使网络能够执行图像分类(我们的网络经过训练可以执行该操作),它必须理解图像。这涉及将原始图像作为输入像素,并通过将原始图像像素转换为对图像中存在的特征的复杂理解的变换来构建内部表示。这也是卷积神经网络能够很好地泛化的部分原因:它们能够捕获类中与背景噪声和其他干扰无关的恒定性和定义特征(例如,猫与狗)。因此,在将原始图像馈入和输出分类标签之间,模型充当复杂的特征提取器;因此,通过访问中间层,我们能够描述输入图像的内容和风格。

具体来说,我们将从网络中提取这些中间层
# Content layer where will pull our feature maps
content_layers = ['block5_conv2'] 

# Style layer we are interested in
style_layers = ['block1_conv1',
                'block2_conv1',
                'block3_conv1', 
                'block4_conv1', 
   'block5_conv1'
               ]

num_content_layers = len(content_layers)
num_style_layers = len(style_layers)

模型

在本例中,我们加载 VGG19,并将我们的输入张量馈入模型。这将允许我们提取内容、风格和生成图像的特征图(以及随后的内容和风格表示)。

我们使用 VGG19,如论文中所建议。此外,由于 VGG19 是一个相对简单的模型(与 ResNet、Inception 等相比),特征图实际上在风格迁移中效果更好。

为了访问与我们的风格和内容特征图相对应的中间层,我们使用 Keras 的 **函数式 API** 来定义具有所需输出激活的模型,从而获取相应的输出。

使用函数式 API,定义模型只需定义输入和输出:model = Model(inputs, outputs)
def get_model():
  """ Creates our model with access to intermediate layers. 
  
  This function will load the VGG19 model and access the intermediate layers. 
  These layers will then be used to create a new model that will take input image
  and return the outputs from these intermediate layers from the VGG model. 
  
  Returns:
    returns a keras model that takes image inputs and outputs the style and 
      content intermediate layers. 
  """
  # Load our model. We load pretrained VGG, trained on imagenet data (weights=’imagenet’)
  vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet')
  vgg.trainable = False
  # Get output layers corresponding to style and content layers 
  style_outputs = [vgg.get_layer(name).output for name in style_layers]
  content_outputs = [vgg.get_layer(name).output for name in content_layers]
  model_outputs = style_outputs + content_outputs
  # Build model 
  return models.Model(vgg.input, model_outputs)
在上面的代码段中,我们将加载我们的预训练图像分类网络。然后,我们获取之前定义的感兴趣的层。然后,我们通过将模型的输入设置为图像,并将输出设置为风格和内容层的输出,来定义一个模型。换句话说,我们创建了一个模型,该模型将接收输入图像,并输出内容和风格中间层!

定义和创建我们的损失函数(内容和风格距离)

内容损失

我们的内容损失定义实际上非常简单。我们将向网络传递目标内容图像和我们的基本输入图像。这将返回我们模型的中间层输出(来自上面定义的层)。然后,我们只需取这两个图像的两个中间表示之间的欧几里得距离。更正式地说,内容损失是一个函数,它描述了我们的输入图像 x 和我们的内容图像 p 的内容距离。令 Cₙₙ 为一个预训练的深度卷积神经网络。同样,在本例中,我们使用 VGG19。令 X 为任何图像,则 Cₙₙ(x) 是由 X 馈送的网络。令 Fˡᵢⱼ(x)∈ Cₙₙ(x) 和 Pˡᵢⱼ(x) ∈ Cₙₙ(x) 描述网络分别由 x 和 p 作为输入在层 l 的中间特征表示。然后,我们将内容距离(损失)正式描述为
我们以通常的方式执行反向传播,以便最小化这种内容损失。因此,我们改变初始图像,直到它在某个层(在 content_layer 中定义)中生成与原始内容图像相似的响应。

这可以非常简单地实现。同样,它将接收作为输入的网络在层 L 的特征图,该网络由 x(我们的输入图像)和 p(我们的内容图像)馈送,并返回内容距离。
def get_content_loss(base_content, target):
  return tf.reduce_mean(tf.square(base_content - target))

风格损失

计算风格损失更复杂一些,但遵循相同的原理,这次我们将我们的基本输入图像和风格图像馈入网络。但是,我们没有比较基本输入图像和风格图像的原始中间输出,而是比较这两个输出的格拉姆矩阵。

从数学上讲,我们将基本输入图像 x 和风格图像 a 的风格损失描述为这些图像的风格表示(格拉姆矩阵)之间的距离。我们将图像的风格表示描述为由格拉姆矩阵 Gˡ 给出的不同滤波器响应之间的相关性,其中 Gˡᵢⱼ 是层 l 中向量化特征图 i 和 j 之间的内积。我们可以看到,在给定图像的特征图上生成的 Gˡᵢⱼ 表示特征图 i 和 j 之间的相关性。

为了生成我们基础输入图像的样式,我们从内容图像开始进行梯度下降,将其转换为与原始图像样式表示相匹配的图像。我们通过最小化样式图像和输入图像的特征相关图之间的均方距离来实现。每一层对总样式损失的贡献由以下公式描述:
其中 Gˡᵢⱼ 和 Aˡᵢⱼ 分别是输入图像 x 和样式图像 a 在第 l 层的样式表示。Nl 表示特征图的数量,每个特征图的大小为 Ml=高度∗宽度。因此,每一层上的总样式损失为:
其中我们用一些因子 wl 来权衡每一层损失的贡献。在本例中,我们对每一层的权重相同:
这可以通过以下方式简单实现:
def gram_matrix(input_tensor):
  # We make the image channels first 
  channels = int(input_tensor.shape[-1])
  a = tf.reshape(input_tensor, [-1, channels])
  n = tf.shape(a)[0]
  gram = tf.matmul(a, a, transpose_a=True)
  return gram / tf.cast(n, tf.float32)
 
def get_style_loss(base_style, gram_target):
  """Expects two images of dimension h, w, c"""
  # height, width, num filters of each layer
  height, width, channels = base_style.get_shape().as_list()
  gram_style = gram_matrix(base_style)
  
  return tf.reduce_mean(tf.square(gram_style - gram_target))

运行梯度下降

如果您不熟悉梯度下降/反向传播或需要复习,您应该查看此 资源。 在这种情况下,我们使用 Adam 优化器来最小化我们的损失。我们迭代地更新输出图像,使其最小化我们的损失:我们不更新与我们网络相关的权重,而是训练我们的输入图像以最小化损失。为了做到这一点,我们必须知道如何计算损失和梯度。请注意,L-BFGS 优化器(如果您熟悉此算法,建议使用)在本教程中未被使用,因为本教程的主要动机是说明热心执行的最佳实践。通过使用 Adam,我们可以演示带有自定义训练循环的 autograd/梯度带功能。

计算损失和梯度

我们将定义一个小的辅助函数,它将加载我们的内容和样式图像,并将它们前馈到我们的网络中,然后从我们的模型输出内容和样式特征表示。
def get_feature_representations(model, content_path, style_path):
  """Helper function to compute our content and style feature representations.
 
  This function will simply load and preprocess both the content and style 
  images from their path. Then it will feed them through the network to obtain
  the outputs of the intermediate layers. 
  
  Arguments:
    model: The model that we are using.
    content_path: The path to the content image.
    style_path: The path to the style image
    
  Returns:
    returns the style features and the content features. 
  """
  # Load our images in 
  content_image = load_and_process_img(content_path)
  style_image = load_and_process_img(style_path)
  
  # batch compute content and style features
  stack_images = np.concatenate([style_image, content_image], axis=0)
  model_outputs = model(stack_images)
  
  # Get the style and content feature representations from our model  
  style_features = [style_layer[0] for style_layer in model_outputs[:num_style_layers]]
  content_features = [content_layer[1] for content_layer in model_outputs[num_style_layers:]]
  return style_features, content_features
在这里我们使用 tf.GradientTape 来计算梯度。它使我们能够利用自动微分,通过跟踪操作来计算以后的梯度。它记录前向传递过程中的操作,然后能够计算损失函数相对于输入图像的梯度,用于反向传递。
def compute_loss(model, loss_weights, init_image, gram_style_features, content_features):
  """This function will compute the loss total loss.
  
  Arguments:
    model: The model that will give us access to the intermediate layers
    loss_weights: The weights of each contribution of each loss function. 
      (style weight, content weight, and total variation weight)
    init_image: Our initial base image. This image is what we are updating with 
      our optimization process. We apply the gradients wrt the loss we are 
      calculating to this image.
    gram_style_features: Precomputed gram matrices corresponding to the 
      defined style layers of interest.
    content_features: Precomputed outputs from defined content layers of 
      interest.
      
  Returns:
    returns the total loss, style loss, content loss, and total variational loss
  """
  style_weight, content_weight, total_variation_weight = loss_weights
  
  # Feed our init image through our model. This will give us the content and 
  # style representations at our desired layers. Since we're using eager
  # our model is callable just like any other function!
  model_outputs = model(init_image)
  
  style_output_features = model_outputs[:num_style_layers]
  content_output_features = model_outputs[num_style_layers:]
  
  style_score = 0
  content_score = 0

  # Accumulate style losses from all layers
  # Here, we equally weight each contribution of each loss layer
  weight_per_style_layer = 1.0 / float(num_style_layers)
  for target_style, comb_style in zip(gram_style_features, style_output_features):
    style_score += weight_per_style_layer * get_style_loss(comb_style[0], target_style)
    
  # Accumulate content losses from all layers 
  weight_per_content_layer = 1.0 / float(num_content_layers)
  for target_content, comb_content in zip(content_features, content_output_features):
    content_score += weight_per_content_layer* get_content_loss(comb_content[0], target_content)
  
  style_score *= style_weight
  content_score *= content_weight
  total_variation_score = total_variation_weight * total_variation_loss(init_image)

  # Get total loss
  loss = style_score + content_score + total_variation_score 
  return loss, style_score, content_score, total_variation_score
然后计算梯度就很简单了:
def compute_grads(cfg):
  with tf.GradientTape() as tape: 
    all_loss = compute_loss(**cfg)
  # Compute gradients wrt input image
  total_loss = all_loss[0]
  return tape.gradient(total_loss, cfg['init_image']), all_loss

应用并运行样式迁移过程

而要真正执行样式迁移:
def run_style_transfer(content_path, 
                       style_path,
                       num_iterations=1000,
                       content_weight=1e3, 
                       style_weight = 1e-2): 
  display_num = 100
  # We don't need to (or want to) train any layers of our model, so we set their trainability
  # to false. 
  model = get_model() 
  for layer in model.layers:
    layer.trainable = False
  
  # Get the style and content feature representations (from our specified intermediate layers) 
  style_features, content_features = get_feature_representations(model, content_path, style_path)
  gram_style_features = [gram_matrix(style_feature) for style_feature in style_features]
  
  # Set initial image
  init_image = load_and_process_img(content_path)
  init_image = tfe.Variable(init_image, dtype=tf.float32)
  # Create our optimizer
  opt = tf.train.AdamOptimizer(learning_rate=10.0)

  # For displaying intermediate images 
  iter_count = 1
  
  # Store our best result
  best_loss, best_img = float('inf'), None
  
  # Create a nice config 
  loss_weights = (style_weight, content_weight)
  cfg = {
      'model': model,
      'loss_weights': loss_weights,
      'init_image': init_image,
      'gram_style_features': gram_style_features,
      'content_features': content_features
  }
    
  # For displaying
  plt.figure(figsize=(15, 15))
  num_rows = (num_iterations / display_num) // 5
  start_time = time.time()
  global_start = time.time()
  
  norm_means = np.array([103.939, 116.779, 123.68])
  min_vals = -norm_means
  max_vals = 255 - norm_means   
  for i in range(num_iterations):
    grads, all_loss = compute_grads(cfg)
    loss, style_score, content_score = all_loss
    # grads, _ = tf.clip_by_global_norm(grads, 5.0)
    opt.apply_gradients([(grads, init_image)])
    clipped = tf.clip_by_value(init_image, min_vals, max_vals)
    init_image.assign(clipped)
    end_time = time.time() 
    
    if loss < best_loss:
      # Update best loss and best image from total loss. 
      best_loss = loss
      best_img = init_image.numpy()

    if i % display_num == 0:
      print('Iteration: {}'.format(i))        
      print('Total loss: {:.4e}, ' 
            'style loss: {:.4e}, '
            'content loss: {:.4e}, '
            'time: {:.4f}s'.format(loss, style_score, content_score, time.time() - start_time))
      start_time = time.time()
      
      # Display intermediate images
      if iter_count > num_rows * 5: continue 
      plt.subplot(num_rows, 5, iter_count)
      # Use the .numpy() method to get the concrete numpy array
      plot_img = init_image.numpy()
      plot_img = deprocess_img(plot_img)
      plt.imshow(plot_img)
      plt.title('Iteration {}'.format(i + 1))

      iter_count += 1
  print('Total time: {:.4f}s'.format(time.time() - global_start))
      
  return best_img, best_loss 
就是这样!

让我们在海龟图像和葛饰北斋的《神奈川冲浪里》上运行它:
best, best_loss = run_style_transfer(content_path, 
                                     style_path,
                                     verbose=True,
                                     show_intermediates=True)
绿海龟图像 由 P.Lindgren [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)],来自维基百科公共资源
观察随时间推移的迭代过程:
以下是一些神经风格迁移可以做到的其他酷炫示例。看看吧!
图宾根图像 - 照片作者:Andreas Praefcke [GFDL (http://www.gnu.org/copyleft/fdl.html) 或 CC BY 3.0 (https://creativecommons.org/licenses/by/3.0)],来自 维基百科公共资源 和 梵高《星空》图像,公有领域
图宾根图像 - 照片作者:Andreas Praefcke [GFDL (http://www.gnu.org/copyleft/fdl.html) 或 CC BY 3.0 (https://creativecommons.org/licenses/by/3.0)],来自 维基百科公共资源 和 堪丁斯基《构成第七号》图像,公有领域
图宾根图像 - 照片作者:Andreas Praefcke [GFDL (http://www.gnu.org/copyleft/fdl.html) 或 CC BY 3.0 (https://creativecommons.org/licenses/by/3.0)],来自 维基百科公共资源 和 NASA、ESA 和哈勃遗产团队的《创生之柱》图像,公有领域
尝试使用您自己的图像!

关键要点

我们涵盖的内容

  • 我们构建了几个不同的损失函数,并使用反向传播来转换我们的输入图像以最小化这些损失。
  • 为了做到这一点,我们加载了一个 预训练模型 并使用它学习的特征图来描述我们图像的内容和样式表示。
  • 我们的主要损失函数主要计算这些不同表示之间的距离。
  • 我们使用自定义模型和 热心执行 来实现这一点。
  • 我们使用函数式 API 构建了我们的自定义模型。
  • 热心执行使我们能够动态地使用张量,使用自然的 Python 控制流。
  • 我们直接操作张量,这使得调试和使用张量变得更加容易。
我们使用 tf.gradient 迭代地更新我们的图像,应用我们的优化器更新规则。优化器最小化了给定损失相对于我们的输入图像。
下一篇文章
Neural Style Transfer: Creating Art with Deep Learning using tf.keras and eager execution

作者:Raymond Yuan,软件工程实习生

在这个 教程 中,我们将学习如何使用深度学习以另一种图像的风格合成图像(您是否曾经希望能够像毕加索或梵高一样绘画?)。这被称为 神经风格迁移!这是一种由 Leon A. Gatys 的论文《艺术风格的神经算法》 中概述的技术,这是一篇非常棒的读物,你一定要看看它…