cnn风格迁移_图像风格迁移详解
前⾔
Leon A.Gatys是最早使⽤CNN做图像风格迁移的先驱之⼀,这篇⽂章还有另外⼀个版本[2],应该是它投到CVPR之前的预印版,两篇⽂章内容基本相同。
我们知道在训练CNN分类器时,接近输⼊层的Feature Map包含更多的图像的纹理等细节信息,⽽接近输出层的Feature Map则包含更多的内容信息。这个特征的原理可以通过我们在残差⽹络中介绍的数据处理不等式(DPI)解释:越接近输⼊层的Feature Map经过的处理(卷积和池化)越少,则这时候损失的图像信息还不会很多。随着⽹络层数的加深,图像经过的处理也会增多,根据DPI中每次处理信息会减少的原理,靠后的Feature Map则包含的输⼊图像的信息是不会多余其之前的Feature Map的;同理当我们使⽤标签值进⾏参数更新时,越接近损失层的Feature Map则会包含越多的图像标签(内容)信息,越远则包含越少的内容信息。这篇论⽂正是利⽤了CNN的天然特征实现的图像风格迁移的。
具体的讲,当我们要在图⽚
(content)的内容之上应⽤图⽚
(style)的风格时,我们会使⽤梯度下降等算法更新⽬标图像
(target)的内容,使其在较浅的层有和图⽚
类似的响应值,同时在较深的层和
也有类似的响应,这样就保证了
有类似的风格⽽且和
有类似的内容,这样⽣成的图⽚
就是我们要得到的风格迁移的图⽚。如图1所⽰。
在Keras官⽅源码中,作者提供了神经风格迁移的源码,这⾥对算法的讲解将结合源码进⾏分析。
图1:图像风格迁移效果图
1. Image Style Transfer(IST)算法详解
1.1 算法概览
IST的原理基于上⾯提到的⽹络的不同层会响应不同的类型特征的特点实现的。给定⼀个训练好的⽹络,
源码中使⽤的是VGG19 [3],下⾯是源码第142-143⾏,因此在运⾏该源码时如果你之前没有下载过训练好的VGG19模型⽂件,第⼀次运⾏会有下载该⽂件的过程,⽂件名为'vgg19_weights_tf_dim_ordering_tf_kernels_notop.h5'。
142 model = vgg19.VGG19(input_tensor=input_tensor,
143                    weights='imagenet', include_top=False)
论⽂中有两点在源码中并没有体现,⼀个是对权值进⾏了归⼀化,使⽤的⽅法是我们之前介绍的Weight Normalization[4],另外⼀个是使⽤平均池化代替最⼤池化,使⽤了这两点的话会有更快的收敛速度。
图2有三个部分,最左侧的输⼊是风格图⽚
,将其输⼊到训练好的VGG19中,会得到⼀批它对应的Feature Map;最右侧则是内容图⽚
,它也会输⼊到这个⽹络中得到它对应的Feature Map;中间是⽬标图⽚
,它的初始值是⽩噪⾳图⽚,它的值会通过SGD进⾏更新,SGD的损失函数时通过
在这个⽹络中得到的Feature Map和
的Feature Map以及
的Feature Map计算得到的。图2中所有的细节会在后⾯的章节中进⾏介绍。
图2:图像风格迁移算法流程图
传统的深度学习⽅法是根据输⼊数据更新⽹络的权值。⽽IST的算法是固定⽹络的参数,更新输⼊的数据。固定权值更新数据还有⼏个经典案例,例如材质学习[5],卷积核可视化等。
1.2 内容表⽰
内容表⽰是图2中右侧的两个分⽀所⽰的过程。我们先看最右侧,
输⼊VGG19中,我们提取其在第四个block中第⼆层的Feature Map,表⽰为conv4_2(源码中提取的是conv5_2)。假设其层数为
是Feature Map的数量,也就是通道数,
是Feature Map的像素点的个数。那么我们得到Feature Map
可以表⽰为
则是第
层的第
个Feature Map在位置
处的像素点的值。根据同样的定义,我们可以得到
在conv4_2处的Feature Map
如果
⾮常接近,那么我们可以认为
在内容上⽐较接近,因为越接近输出的层包含有越多的内容信息。这⾥我们可以定义IST的内容损失函数为:
下⾯我们来看⼀下源码,上⾯142⾏的input_tensor的是由
⼀次拼接⽽成的,见136-138⾏。
136 input_tensor = K.concatenate([base_image,
137                              style_reference_image,
138                              combination_image], axis=0)
通过对142⾏的model的遍历我们可以得到每⼀层的Feature Map的名字以及内容,然后将其保存在字典中,见147⾏。
147 outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
这样我们可以根据关键字提取我们想要的Feature Map,例如我们提取两个图像在conv5_2处的Feature Map
(源码中的
base_image_features)和
源码中的
combination_features),然后使⽤这两个Feature Map计算损失值,见208-212⾏:
208 layer_features = outputs_dict['block5_conv2']
209 base_image_features = layer_features[0, :, :, :]
210 combination_features = layer_features[2, :, :, :]
211 loss += content_weight * content_loss(base_image_features,
212                                      combination_features)
上式中的content_weight是内容损失函数的⽐重,源码中给出的值是0.025,内容损失函数的定义见185-186⾏:
185 def content_loss(base, combination):
186    return K.sum(K.square(combination - base))
有了损失函数的定义之后,我们便可以根据损失函数的值计算其关于
的梯度值,从⽽实现从后向前的梯度更新。
如果损失函数只包含内容损失,当模型收敛时,我们得到的
应该⾮常接近
的内容。但是它很难还原到和
⼀模⼀样,因为即使损失值为0时,我们得到的
值也有多种的形式。
为什么说
具有
的内容呢,因为当
经过VGG19的处理后,它的conv5_2层的输出了
⼏乎⼀样,⽽较深的层具有较⾼的内容信息,这也就说明了
具有⾮常类似的内容信息。
1.3 风格表⽰
风格表⽰的计算过程是图2的左侧和中间两个分⽀。和计算
相同,我们将
输⼊到模型中便可得到它对应的Feature Map
。不同于内容表⽰的直接运算,风格表⽰使⽤的是Feature Map展开成1维向量的Gram矩阵的形式。使⽤Gram矩阵的原因是因为考虑到纹理特征是和图像的具体位置没有关系的,所以通过打乱纹理的位置信息来保证这个特征,Gram矩阵的定义如下:
另外⼀点和内容表⽰不同的是,风格表⽰使⽤了每个block的第⼀个卷积来计算损失函数,作者认为这
种⽅式得到的纹理特征更为光滑,因为仅仅使⽤底层Feature Map得到的图像较为精细但是⽐较粗糙,⽽⾼层得到的图像则含有更多的内容信息,损失了⼀些纹理信息,但他的材质更为光滑。所以,综合了所有层的样式表⽰的损失函数为:
其中
的Gram矩阵
的Gram矩阵
的均⽅误差:
它关于
的梯度的计算⽅式为:
上⾯的更新同样使⽤SGD。
下⾯我们继续来学习源码,从源码的214-223⾏我们可以看出样式表⽰使⽤了5个block的Feature Map:
214 feature_layers = ['block1_conv1', 'block2_conv1',
215                  'block3_conv1', 'block4_conv1',
216                  'block5_conv1']
217 for layer_name in feature_layers:
218    layer_features = outputs_dict[layer_name]
219    style_reference_features = layer_features[1, :, :, :]
220    combination_features = layer_features[2, :, :, :]
221    sl = style_loss(style_reference_features, combination_features)
222    loss += (style_weight / len(feature_layers)) * sl
223 loss += total_variation_weight * total_variation_loss(combination_image)
从上⾯的代码中我们可以看出,样式表⽰使⽤了feature_layers中所包含的Feature Map,并且最后loss的计算把它们进⾏了相加。第221⾏的style_loss的定义见源码的171-178⾏:
171 def style_loss(style, combination):
172    assert K.ndim(style) == 3
173    assert K.ndim(combination) == 3
174    S = gram_matrix(style)
175    C = gram_matrix(combination)
176    channels = 3
177    size = img_nrows * img_ncols
178    return K.sum(K.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))
从174-175⾏我们可以看出损失函数的计算使⽤的是两个Feature Map的Gram矩阵,Gram矩阵的定义见155-162⾏:
155 def gram_matrix(x):
156    assert K.ndim(x) == 3
157    if K.image_data_format() == 'channels_first':
158        features = K.batch_flatten(x)
159    else:
160        features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
161    gram = K.dot(features, K.transpose(features))
流程图转换为ns图
162    return gram
第158或者160⾏的batch_flatten验证了Feature Map要先展开成向量,第161⾏则是Gram矩阵的计算公式。
还有⼀些超餐在配置⽂件中进⾏了指定,style_weight和total_variation_weight的默认值都是1。
1.4 风格迁移
明⽩了如何计算内容损失函数
和风格损失函数
之后,整个风格迁移任务的损失函数就是两个损失值得加权和:
其中
就是我们在1.2节和1.3节介绍的
content_weight和 total_variation_weight。通过调整这两个超参数的值我们可以设置⽣成的图像更偏向于
的内容还是
的风格。
的值⽤来更新输⼊图像
的内容,作者推荐使⽤L-BFGS更新梯度。
另外对于
的初始化,论⽂中推荐使⽤⽩噪⾳进⾏初始化,这样虽然计算的时间要更长⼀些,但是得到的图像的样式具有更强的随机性。⽽论⽂使⽤
的是使⽤
初始化
,这样得到的⽣成图像更加稳定。
下⾯继续学习这⼀部分的源码。在第287-288⾏的fmin_l_bfgs_b说明了计算梯度使⽤了L-BFGS算法,它是scipy提供:
287 x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x.flatten(),
288                                  ads, maxfun=20)
fmin_l_bfgs_b是scipy包中⼀个函数。第⼀个参数是定义的损失函数,第⼆个参数是输⼊数据,fprime通常⽤于计算第⼀个损失函数的梯度,maxfun是函数执⾏的次数。它的第⼀个返回值是更新之后的x的值,这⾥使⽤了递归的⽅式反复更新x,第⼆个返回值是损失值。
其中x的初始化使⽤的是内容图⽚
:
282 x = preprocess_image(base_image_path)
287⾏的损失函数定义在264-269⾏:
264 def loss(self, x):
265    assert self.loss_value is None
266    loss_value, grad_values = eval_loss_and_grads(x)
267    self.loss_value = loss_value
268    ad_values = grad_values
269    return self.loss_value
其中最重要的函数是eval_loss_and_grads()函数,它定义在了237-248⾏:
237 def eval_loss_and_grads(x):
238    if K.image_data_format() == 'channels_first':
239        x = x.reshape((1, 3, img_nrows, img_ncols))
240    else:
241        x = x.reshape((1, img_nrows, img_ncols, 3))
242    outs = f_outputs([x])
243    loss_value = outs[0]
244    if len(outs[1:]) == 1:
245        grad_values = outs[1].flatten().astype('float64')
246    else:
247        grad_values = np.array(outs[1:]).flatten().astype('float64')
248    return loss_value, grad_values
其中f_outputs()是实例化的Keras函数,作⽤是使⽤梯度更新
的内容,见226-234⾏:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。