CS20 08. 风格迁移

CS20的第二次作业是使用TensorFlow实现风格迁移(style transfer)。对于这个名词,了解深度学习的人应该不会太陌生:简单来说,神经网络接收两张图片,其中一个称为“风格图片”,主要学习其中的画风;另一个称为“内容图片”,网络将其渲染为学习到的风格。本次课的大部分内容是在讨论作业,没有讲义,所以笔记可能会简单一些

原始胶片参看 https://docs.google.com/presentation/d/1ftgals7pXNOoNoWe0E9PO27miOpXbHrQIXyBm0YOiyc/edit 。无特别说明,笔记中所有代码来自胶片

TFRecord

本讲首先介绍了一些关于TFRecord的内容。它是一种TensorFlow推荐的数据格式,是tf.train.Exampleprotobuf对象序列化之后的二进制结果(这里所谓的tf.train.Example个人感觉并不简单的是一个例子,而是以例子的形式,以protobuf为载体,规定了一种针对输入数据的数据格式。在官网上它直接被指向了一个protobuf文件)。使用二进制文件的好处是可以更好利用磁盘缓存,数据流动起来比较快,而且可以处理不同类型的文件

要将数据转换为TFRecord格式,需要一个tf.python_io.TFRecordWriter对象。示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Step 1: create a writer to write tfrecord to that file
writer = tf.python_io.TFRecordWriter(out_file)

# Step 2: get serialized shape and values of the image
shape, binary_image = get_image_binary(image_file)

# Step 3: create a tf.train.Features object
features = tf.train.Features(feature={'label': _int64_feature(label),
'shape': _bytes_feature(shape),
'image': _bytes_feature(binary_image)})

# Step 4: create a sample containing of features defined above
sample = tf.train.Example(features=features)

# Step 5: write the sample to the tfrecord file
writer.write(sample.SerializeToString())
writer.close()

从第8到10行代码可以看出,其核心思想是将所有不同的数据类型都转换成byte string。具体实现为

1
2
3
4
5
def _int64_feature(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

def _bytes_feature(value):
return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

读取TFRecord文件需要使用tf.data.TFRecordDataset对象,并且自己定义一个解析函数,将其解析(还原)为各个特征,例如

1
2
3
4
5
6
7
8
9
10
11
12
def _parse_function(tfrecord_serialized):
features={'label': tf.FixedLenFeature([], tf.int64),
'shape': tf.FixedLenFeature([], tf.string),
'image': tf.FixedLenFeature([], tf.string)}

parsed_features = tf.parse_single_example(tfrecord_serialized, features)

return parsed_features['label'], parsed_features['shape'], parsed_features['image']


dataset = tf.data.TFRecordDataset(tfrecord_files)
dataset = dataset.map(_parse_function)

风格迁移

理论

风格迁移的目标可以简单说为,要生成一张新的图片,使得其内容接近“内容图片”的内容,风格接近“风格图片”的风格。既然有两种图片,那么就会有相应的两种损失函数(这也就是问题的关键点):“内容损失”和“风格损失”。从直观上来讲,“内容损失”衡量生成图片的内容和“内容图片”的内容之间的差距,而“风格损失”定义类似

接下来的问题就是,对于一张图片,怎么提取内容,又怎么提取风格?前面提到的卷积神经网络CNN一般都有很多层,实际上,每一层都是一个提取特定特征的函数,只不过我们不知道具体能学到什么特征。但是从宏观来看,底层(靠近输入的层)倾向提取和内容相关的特征,高层(靠近输出的层)倾向提取和风格相关的特征。因此,损失函数可以进一步理解为,“内容损失”衡量的是生成图片“内容层”特征映射与“内容图片”之间的差别,“风格损失”定义类似,不过比较的是生成图片“风格层”特征映射的Gram矩阵和“风格图片”的Gram矩阵之间的差别。其中Gram矩阵是某个矩阵与其转置的乘积。按照一篇博客的讲解,由于矩阵中每一列都有每一行做内积,可以把这个过程看做是将原始图片的空间信息分布化,因此得到的矩阵会得到图片的非局部特征,包括纹理、形状等等,这些就是图片的风格

特征提取可以使用预训练好的网络来做,例如VGG、AlexNet、GoogleNet等等。按照原始论文(Image Style Transfer Using Convolutional Neural Networks, Gatys et al., CVPR 2016, pp. 2414-2423)的定义,输入图像\(\vec{x}\)在CNN的每一层都会被对应的滤波器(卷积核)编码,假设在第\(l\)层有\(N_l\)个卷积核,每个卷积核大小都是\(M_l \times M_l\)的,那么第\(l\)层编码的信息会存在矩阵\(F^l \in \mathcal{R}^{N_l \times M_l}\)中,其中\(F_{ij}^l\)是层\(l\)位置\(j\)被第\(i\)个卷积核激活的值。令\(\vec{p}\)为原始内容图像,\(\vec{x}\)为生成图像,\(P^l\)\(F^l\)为各自在第\(l\)层得到的特征表示,则“内容损失”可以定义为 \[ \mathcal{L}_{\rm content}(\vec{p}, \vec{x}, l) = \frac{1}{2}\sum_{i, j}(F_{ij}^l - P_{ij}^l)^2 \] "内容损失"对层\(l\)的激活值的偏导数为 \[ \frac{\partial \mathcal{L}_{\rm content}}{\partial F_{ij}^l} = \begin{cases}(F^l-P^l)_{ij} & {\rm if\ }F_{ij}^l > 0 \\ 0 & {\rm if\ }F_{ij}^l < 0 \end{cases} \] 因此\(\vec{x}\)的梯度可以使用反向传播得出,经过反向传播可以将原始随机生成的图像一点一点调整为和\(\vec{p}\)内容相近的图像。所以这里要优化的是生成的图像,而不是网络的权重!

为了获得图像的风格表示,原始论文使用前面提到的Gram矩阵,通过特征相关性来达到目的。更形式化地,对第\(l\)层,假设有\(N_l\)个卷积核,那么对应的Gram矩阵\(G^l \in \mathcal{R}^{N_l \times N_l}\)的每个元素是特征映射向量化后的内积 \[ G_{ij}^l = \sum_kF_{ik}^lF_{jk}^l \]\(\vec{a}\)为原始风格图像,\(\vec{x}\)为生成图像,\(A^l\)\(G^l\)为各自在第\(l\)层得到的风格表示,则层\(l\)的”风格损失“可以定义为 \[ E_l = \frac{1}{4N_l^2M_l^2}\sum_{i,j}(G_{ij}^l - A_{ij}^l)^2 \] 总的风格损失为 \[ \mathcal{L}_{\rm style}(\vec{a}, \vec{x}) = \sum_{l=0}^Lw_lE_l \] 其中\(w_l\)是每层对总损失值贡献的权重。相应的,也可以计算该损失值对第\(l\)层激活值的偏导 \[ \frac{\partial E_l}{\partial F_{ij}^l} = \begin{cases}\frac{1}{N_l^2M_l^2}((F^l)^\mathsf{T}(G^l-A^l))_{ji} & {\rm if\ }F_{ij}^l > 0 \\ 0 & {\rm if\ }F_{ij}^l < 0\end{cases} \] 为了将画\(\vec{a}\)的风格迁移到照片\(\vec{p}\)上,需要综合考虑上述两种损失值,因此要最小化的总损失函数为 \[ \mathcal{L}_{\rm total}(\vec{p}, \vec{a}, \vec{x}) = \alpha \mathcal{L}_{\rm content}(\vec{p}, \vec{x}) + \beta \mathcal{L}_{\rm style}(\vec{a}, \vec{x}) \]

对作业的一点贴士

  • 训练的是输入而非权重
  • 使用共享变量避免创建很多相同的子图
  • 使用预训练的网络(这里给的是VGG-19)。不过numpy的权重和偏置必须转化成TF的张量。以及,一定要设成不可训练的!
坚持原创技术分享,您的支持将鼓励我继续创作!