Image Style Transfer

这次笔者主要对基于tensorflow框架的代码进行理解分析:
《A Neural Algorithm of Artistic Style》
《Perceptual Losses for Real-Time Style Transfer and Super-Resolution》
《Texture Networks: Feed-forward Synthesis of Textures and Stylized Images》

==================读书笔记=======================
风格转移,输出的图片C = A(content) + B(style),就是将图片B的风格转移到图片C中,同时保留图片A的内容。这看起来像是PS就能实现的功能,直接将两个图层叠加融合即可。但其实不然,保留图片A的内容是所有CNN网络都可以实现的,但style是更为抽象的特征(直观上来看),是否可以通过更深层的卷积网络就能得到style的feature,这是一个值得探讨的问题。(当然,作者已经帮我们印证了这个答案,TUT)
所以说,这是一项非常有意思的工作,应该跟之前红极一时的prisma软件的工作原理差不多。

风格转移的两种方法

  1. 基于图片迭代的描述性神经网络
    这一方法会从随机噪声开始,通过反向传播迭代更新(尚未知晓的)风格化图像。图像迭代的目标是最小化总损失,这样风格化后的图像就能同时将内容图像的内容与风格图像的风格匹配起来。
    需要指定输入图片与风格图片,然后进行足够多的迭代才能有比较好的效果,每一次运行都是重新训练一个模型,且不可复用。
    论文中还提及,现有的描述性神经网络主要基于最大均值差(MMD)和基于马尔科夫随机场(MRF)两种方法来做。笔者数学不太好,对这两种方法并不是很了解,但个人感觉应该是用来刻画输出图片与内容图片、风格图片之间分布差异的方法
  2. 基于模型迭代的生成式神经网络
    这种方法更像是为了解决“基于图片迭代的描述性神经网络”在风格转移中效率过低而存在的,也被成为“快速”神经风格迁移,主要解决了前者的计算速度和计算成本问题。
    核心是先训练保存一些用户常用的“风格”参数。用户输入需要转换风格的图片A到网络中,再指定网络调用B风格的参数,输出的图片C其实只是网络最后一层的输出,中间不会涉及过多的参数调整及优化。
    与第一种方法对比,基于模型迭代的生成式神经网络更像是一个网络的“test”部分,其只是输出结果,做部分参数的调整,但优劣性不予保证;基于图片迭代的描述性神经网络就是一个网络的“train”部分,其需要进行多次的权重调整及优化,才能使得初始化的噪声图片有比较好的输出。

两种方法各有优劣,应用场景也是不同的:
第一种方法根据任意风格图片转换任意图片,甚至可以进行多种风格的混合(只要输入多张图片作为风格list);但速度是个硬伤,实验室Tesla K80 的GPU,居然花了40min才完成一张图片1000次的迭代。适用于用户愿意等待,且服务器有很强的计算能力能应付多个用户的并发操作。
第二种方法则是考虑了效率,输出一张效果不错的图片,只花了不到10s;但却失去了灵活性,用户只能从现有的“滤镜”(模型)中转换自己的图片。这更适用于客户端离线转换。


基于图片迭代的源码解析code

整体思路
源码架构.png
  1. 共用一个卷积网络(VGG-19),但上方示意图显示的是对风格图片的“style”特征进行提取,下方示意图显示的是对内容图片的“content”进行提取;这里有一个很有意思的问题,同一个网络,为什么能实现对风格图片的“style”特征提取,又能提取内容图片的“content”特征?
    答案是不同的layer。CNN网络的优势在于不同的layer有不同的含义,使用更高层的layer在理论上可以表达更抽象的特征,具体使用哪一层的特征表达,后面会再进行分析。
  2. 随机初始化一张噪声图片x,与内容图片p和风格图片a三者同时输入网络中训练,定义总的损失函数是关于x的非线性函数。目标是使x在内容上与p相近,在风格上与a相近。怎么实现?
    定义总的损失函数是关于x的,Total_loss = centent_loss + style_loss ;将这个loss最小化,利用梯度下降的方式,就能实现这个目标。
Centent Loss

content loss 计算公式

Fijl:第l层第i个filter上位置j处的激活值
Fl:随机噪声x在第l层的响应值
Pl:内容图片p在第l层的响应值
Lcontent :两者的误差平方和

利用CONTENT_LAYERS('relu4_2','relu5_2')作为“content”的layer表达

g = tf.Graph()
with g.as_default(), g.device('/cpu:0'), tf.Session() as sess:
    image = tf.placeholder('float', shape=shape)
    net = vgg.net_preloaded(vgg_weights, image, pooling)
    content_pre = np.array([vgg.preprocess(content, vgg_mean_pixel)])
    for layer in CONTENT_LAYERS:
        content_features[layer] = net[layer].eval(feed_dict={image: content_pre})

content loss 计算

    content_loss = 0
    content_losses = []
    for content_layer in CONTENT_LAYERS:
        content_losses.append(content_layers_weights[content_layer] * content_weight * (2 * tf.nn.l2_loss(
                net[content_layer] - content_features[content_layer]) /
                content_features[content_layer].size))
    content_loss += reduce(tf.add, content_losses)
Style Loss

Gijl:随机噪声x在第l层的响应值
Al:风格图片a在第l层的响应值
El:单层的“style”损失
Lstyle :所有层的“style”损失和

利用STYLE_LAYERS('relu1_1','relu2_1','relu3_1','relu4_1','relu5_1')作为“style”的layer表达。
可以看到,“Style”使用到的layer其实是要比“content”更低的。
或许可以这样解释,正如笔者上方强调的那样,“Style”从直观上来看应该是更为抽象的特征,应该采用更高的layer才能很好刻画出来;但其实“Style”在某种程度上来说其实是纹理信息,采用较为浅的layer才能保留这些有用的细节信息,从而忽略整体信息。(这里还有待考虑,代码体现出来的采用更浅的layer,但"style"应该属于更抽象的特征才对)

for i in range(len(styles)):
    g = tf.Graph()
    with g.as_default(), g.device('/cpu:0'), tf.Session() as sess:
        image = tf.placeholder('float', shape=style_shapes[i])
        net = vgg.net_preloaded(vgg_weights, image, pooling)
        style_pre = np.array([vgg.preprocess(styles[i], vgg_mean_pixel)])
        for layer in STYLE_LAYERS:
            features = net[layer].eval(feed_dict={image: style_pre})
            features = np.reshape(features, (-1, features.shape[3]))
            gram = np.matmul(features.T, features) / features.size
            style_features[i][layer] = gram

style loss 计算

    style_loss = 0
    for i in range(len(styles)):
        style_losses = []
        for style_layer in STYLE_LAYERS:
            layer = net[style_layer]
            _, height, width, number = map(lambda i: i.value, layer.get_shape())
            size = height * width * number
            feats = tf.reshape(layer, (-1, number))
            gram = tf.matmul(tf.transpose(feats), feats) / size
            style_gram = style_features[i][style_layer]
            style_losses.append(style_layers_weights[style_layer] * 2 * tf.nn.l2_loss(gram - style_gram) / style_gram.size)
        style_loss += style_weight * style_blend_weights[i] * reduce(tf.add, style_losses)

基于模型迭代的源码解析code

直接执行代码下的eval.py 文件,指定调用的风格,就可以生成风格转换后的图片,这就没什么要分析的必要了。不过基于模型迭代其实也是可以训练自己的风格网络的,主要在train.py文件中。具体的代码讲解再后续更新,笔者训练自己的风格网络的时候报错了。
不过大致的思想应该是这样的:

基于模型迭代的生成式神经网络示意图
  1. 采用VGG-16的网络,网络架构以及总损失函数与基于图片迭代的描述性神经网络相似
  2. 不再基于噪声生成新图片,而是直接基于输入的内容图片直接转换成特定的纹理风格
  3. loss network 的参数不再更新,只是基于图片做一个前馈计算,作者的解释是“Image Classification的pretrained的卷积模型已经很好的学习了perceptual和semantic information(场景和语义信息)”,有点“迁移学习”的意思

效果图

原图

wave.jpg

Oil painting

后记

相对来说这是篇比较仓促的笔记,简单调试了下代码就开始动笔写了,可能某些细节问题没有注意到,后续有时间会持续更新。不得不说,有这样的idea,并且实现了出来,真的非常了不起。
但通过上面的几张测试图片,我们也可以发现,这样的处理方法确实可以针对图片进行风格转换,但这转化的是“画”,并非图片,这样说的原因是转化过程中出现“局部扭曲”的现象非常明显,没有人会认为这是一张真实的照片,而且关键是没办法做到对特定部位的转换。

References

《Neural Style Transfer: A Review》
神经风格迁移研究概述:从当前研究到未来方向
TensorFlow之深入理解Fast Neural Style

推荐阅读更多精彩内容