TensorFLow深度学习实战(11)——风格迁移详解

发布于:2025-03-13 ⋅ 阅读:(12) ⋅ 点赞:(0)

0. 前言

风格迁移是用于训练神经网络创作艺术作品的深度学习技术,同时也是一种有趣的神经网络应用,提供了一种用于深入理解神经网络的方式。在本节中,我们将学习神经风格迁移算法。在神经风格迁移中,我们需要一个内容图像和一个风格图像,我们的目标是保持内容图像的同时融和风格图像中的风格样式,以组合这两个图像生成全新图像。

1. 风格迁移原理

当我们观察一幅画作时,我们通常会关注两种元素:画作本身(比如一只宠物或者一幅风景)以及艺术家内在的风格。很难具体定义风格,但我们知道毕加索、梵高等艺术家都有自己的风格。假设我们将梵高的一幅画作交给神经网络,让神经网络以毕加索的风格重新绘制,或者将照片交给神经网络,让它以梵高或毕加索(或其他任何艺术家)的风格重新绘制照片,这就是风格迁移的概念。
接下来,我们正式地定义风格迁移的过程,风格迁移是生成一幅图像 x x x,既保留了源内容图像 p p p 的内容,又采用了源风格图像 a a a 的风格。因此,直观地说,我们需要两个损失函数:一个损失函数测量两幅图像内容的差异 L c o n t e n t L_{content} Lcontent,另一个损失函数测量两幅图像风格的差异 L s t y l e L_{style} Lstyle。然后,风格迁移可以看作是一个优化问题,试图最小化这两个损失函数。 接下来,我们使用预训练的网络来实现风格迁移,使用 VGG19 提取有效表示图像的特征。首先定义用于训练网络的两个函数:内容损失和风格损失。

风格迁移

1.1 内容损失

给定两幅图像, p p p 为内容图像, x x x 为生成的风格图像,定义内容损失为在 VGG19 网络(网络接收这两幅图像作为输入)的某一层 l l l 定义的特征空间中的距离。换句话说,两幅图像由预训练的 VGG19 提取的特征表示,这些特征将图像投影到一个特征内容空间中,在这个空间中可以方便地计算内容损失:
L c o n t e n t l ( p , x ) = ∑ i , j ( F i j l ( x ) − P i j l ( p ) ) 2 L_{content}^l(p,x)=\sum_{i,j}(F_{ij}^l(x)-P_{ij}^l(p))^2 Lcontentl(p,x)=i,j(Fijl(x)Pijl(p))2
为了生成优秀的图像,我们需要确保生成图像的内容与输入内容图像的内容相似(即距离较小),通过标准反向传播最小化内容损失:

def get_content_loss(base_content, target):
    return tf.reduce_mean(tf.square(base_content - target))

1.2 风格损失

VGG19 较高层的特征用作内容表示,可以将这些特征视为卷积核激活。为了表示风格,我们使用 gram 矩阵 G G G (定义为向量 v v v 的矩阵 v T v v^Tv vTv),gram 矩阵表示了不同卷积核激活之间的相关性矩阵。每层对总风格损失的贡献定义为:
E l = 1 4 N l 2 M l 2 ∑ i , j ( G i j l − A i j l ) 2 E_l=\frac 1{4N_l^2M_l^2}\sum_{i,j}(G_{ij}^l-A_{ij}^l)^2 El=4Nl2Ml21i,j(GijlAijl)2
其中 G i j l G_{ij}^l Gijl 是生成的风格图像 x x xgram 矩阵, A i j l A_{ij}^l Aijl 是样式图像a的格拉姆矩阵, N l N_l Nl 是特征图的数量,每个特征图的大小是 M l M_l Ml。格拉姆矩阵可以将图像投影到风格空间中。此外,使用来自多个 VGG19 层的特征相关性,因为我们希望考虑多尺度信息和更鲁棒的风格表示。总风格损失是各层风格损失的加权和:
L s t y l e ( a , x ) = ∑ l ∈ L w l E l L_{style}(a,x)=\sum_{l\in L}w_lE_l Lstyle(a,x)=lLwlEl
因此,关键思想是在内容图像上执行梯度下降,使其风格与风格图像相似:

def gram_matrix(input_tensor):
    # 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):
    # 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))

简言之,风格迁移首先使用 VGG19 作为特征提取器,然后定义两个适当的损失函数以最小化,一个用于风格,另一个用于内容。

2. 模型分析

在了解了神经风格迁移的基本原理之后,我们继续对模型的运行流程进行分析,主要通过以下步骤实现神经风格迁移:

  • 通过预训练的模型处理图像,并在预定义的网络层上提取图像特征
    • 将内容图像输入到预训练模型中,并在预定义的内容网络层上提取图像特征
    • 计算内容损失
    • 将风格图像输入到预训练模型中,并在预定义的风格网络层上提取图像特征,然后计算风格图像的 gram 矩阵值
  • 将生成图像传递给风格图像所经过的同样的网络层,并计算对应的 gram 矩阵值
  • 提取两个图像的 gram 矩阵值的平方差,得到的结果即为风格损失
  • 总损失为风格损失和内容损失的加权平均值
  • 根据损失修改输入图像,得到令总损失最小的图像即为最终的图像

3. 使用 TensorFlow 实现神经风格迁移

了解了模型原理和运算流程后,在本节中,我们利用 Keras 实现神经风格迁移。

(1) 导入所需要的库,以及用于神经风格迁移的内容图像和风格图像:

from tensorflow.keras.applications import vgg19
from tensorflow.keras import backend as K
import numpy as np
import cv2
from PIL import Image
from matplotlib import pyplot as plt
import tensorflow as tf

style_img = cv2.imread('Vincent_van_Gogh_779.jpg')
style_img = cv2.cvtColor(style_img, cv2.COLOR_BGR2RGB)
style_shape = style_img.shape

content_img = cv2.imread('3.jpeg')
content_img = cv2.cvtColor(content_img, cv2.COLOR_BGR2RGB)
content_shape = content_img.shape

(2) 为了便于理解神经风格迁移算法的效果,在生成图像前先查看风格和内容图像:

plt.subplot(211)
# 为了进行对比,将风格图像进行缩放,以与内容图像具有相同的尺寸
plt.imshow(cv2.resize(style_img, (content_shape[1], content_shape[0])))
plt.title('Style image')
plt.axis('off')
plt.subplot(212)
plt.imshow(content_img)
plt.title('Content image')
plt.axis('off')
plt.show()

风格图像与内容图像

(3) 初始化 VGG19 模型,以便获取输入图像在网络层中的特征输出:

vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')

(4) 定义图像预处理函数,并加载图像:

def tensor_to_image(tensor):
    tensor = tensor*255
    tensor = np.array(tensor, dtype=np.uint8)
    if np.ndim(tensor)>3:
        assert tensor.shape[0] == 1
        tensor = tensor[0]
    return Image.fromarray(tensor)

def load_img(path_to_img):
    max_dim = 512
    img = tf.io.read_file(path_to_img)
    img = tf.image.decode_image(img, channels=3)
    img = tf.image.convert_image_dtype(img, tf.float32)

    shape = tf.cast(tf.shape(img)[:-1], tf.float32)
    long_dim = max(shape)
    scale = max_dim / long_dim

    new_shape = tf.cast(shape * scale, tf.int32)

    img = tf.image.resize(img, new_shape)
    img = img[tf.newaxis, :]
    return img

def imshow(image, title=None):
    if len(image.shape) > 3:
        image = tf.squeeze(image, axis=0)

    plt.imshow(image)
    if title:
        plt.title(title)

content_path = '3.jpeg'
style_path = 'Vincent_van_Gogh_779.jpg'
content_image = load_img(content_path)
style_image = load_img(style_path)

(5) 接下来,定义需要提取用于计算内容和风格损失的神经网络图层:

content_layers = ['block5_conv2'] 

style_layers = ['block1_conv1',
                'block2_conv1',
                'block3_conv1', 
                'block4_conv1', 
                'block5_conv1']

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

(6) 构建了一个 VGG19 模型,该模型返回一个中间层输出的列表,并构建模型:

def vgg_layers(layer_names):
    """ Creates a VGG model that returns a list of intermediate output values."""
    vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
    vgg.trainable = False

    outputs = [vgg.get_layer(name).output for name in layer_names]

    model = tf.keras.Model([vgg.input], outputs)
    return model

style_extractor = vgg_layers(style_layers)
style_outputs = style_extractor(style_image*255)

(7) 定义函数 gram_matrix,用于计算输入的 gram 矩阵:

def gram_matrix(input_tensor):
    result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
    input_shape = tf.shape(input_tensor)
    num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
    return result/(num_locations)

(8) 构建一个返回风格和内容张量的模型,在图像上调用此模型,可以返回 style_layersgram 矩阵和 content_layers 的内容:

class StyleContentModel(tf.keras.models.Model):
    def __init__(self, style_layers, content_layers):
        super(StyleContentModel, self).__init__()
        self.vgg = vgg_layers(style_layers + content_layers)
        self.style_layers = style_layers
        self.content_layers = content_layers
        self.num_style_layers = len(style_layers)
        self.vgg.trainable = False

    def call(self, inputs):
        "Expects float input in [0,1]"
        inputs = inputs*255.0
        preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs)
        outputs = self.vgg(preprocessed_input)
        style_outputs, content_outputs = (outputs[:self.num_style_layers],
                                        outputs[self.num_style_layers:])

        style_outputs = [gram_matrix(style_output)
                        for style_output in style_outputs]

        content_dict = {content_name: value
                        for content_name, value
                        in zip(self.content_layers, content_outputs)}

        style_dict = {style_name: value
                    for style_name, value
                    in zip(self.style_layers, style_outputs)}

        return {'content': content_dict, 'style': style_dict}

extractor = StyleContentModel(style_layers, content_layers)
results = extractor(tf.constant(content_image))

(9) 设置风格和内容的目标值,定义一个 tf.Variable 表示要优化的图像,并创建优化器:

style_targets = extractor(style_image)['style']
content_targets = extractor(content_image)['content']
image = tf.Variable(content_image)

def clip_0_1(image):
    return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0)

opt = tf.keras.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1)

(9) 使用内容损失和风格损失的加权组合获得总损失:

style_weight=1e-2
content_weight=1e4

def style_content_loss(outputs):
    style_outputs = outputs['style']
    content_outputs = outputs['content']
    style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) 
                           for name in style_outputs.keys()])
    style_loss *= style_weight / num_style_layers

    content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) 
                             for name in content_outputs.keys()])
    content_loss *= content_weight / num_content_layers
    loss = style_loss + content_loss
    return loss

(10) 使用 tf.GradientTape 更新图像:

@tf.function()
def train_step(image):
    with tf.GradientTape() as tape:
        outputs = extractor(image)
        loss = style_content_loss(outputs)

    grad = tape.gradient(loss, image)
    opt.apply_gradients([(grad, image)])
    image.assign(clip_0_1(image))

for i in range(15):
    train_step(image)
tensor_to_image(image)

使用以上代码生成图像,得到内容图像和风格图像的融合后的风格迁移图片:

结果图像

可以通过选择不同的神经网络层来计算内容和风格损失,并为不同网络层分配不同的权重系数,观察生成图像的差别。

小结

使用风格迁移算法生成图像的核心思想是通过获取损失和梯度变化值以生成风格迁移图像,将内容图像和风格参考图像混合在一起。在本节中,首先介绍了神经风格迁移的核心思想与风格迁移图像的生成流程,然后利用 TensorFlow 从零开始实现了风格迁移算法,可以通过修改模型中的超参数来生成不同观感的图像。

系列链接

TensorFlow深度学习实战(1)——神经网络与模型训练过程详解
TensorFlow深度学习实战(2)——使用TensorFlow构建神经网络
TensorFlow深度学习实战(3)——深度学习中常用激活函数详解
TensorFlow深度学习实战(4)——正则化技术详解
TensorFlow深度学习实战(5)——神经网络性能优化技术详解
TensorFlow深度学习实战(6)——回归分析详解
TensorFlow深度学习实战(7)——分类任务详解
TensorFlow深度学习实战(8)——卷积神经网络
TensorFlow深度学习实战(9)——构建VGG模型实现图像分类
TensorFlow深度学习实战(10)——迁移学习详解