[关闭]
@CelesteGG 2019-01-16T13:32:23.000000Z 字数 11218 阅读 2161

卷积神经网络实现图像风格迁移

卷积神经网络 风格迁移 TennsorFlow VGG-19


本文是一篇学习报告,主要内容是实践操作了通过使用训练好的VGG-19神经网络算法模型,借助流行的TensorFlow框架来实现将一张图像的典型风格迁移到另一张图像上面去,其产出效果类似于Prisma。

文章将分为以下几个部分


算法简介

风格迁移
风格迁移是将一幅图片的内容和另一幅艺术图片的风格结合,生成一张艺术化的图片的过程。输入是一张内容图和一张风格图,输出是风格化的结果。

神经风格迁移的两大关键分别是风格表征和图像重建。

卷积神经网络VGG19
VGG19 模型图
VGGNet是牛津大学视觉几何团队(Visual Geometry Group, VGG)开发的一组卷积神经网络算法,VGGNet构筑中仅使用3×3的卷积核并保持卷积层中输出特征图尺寸不变,通道数加倍,池化层中输出的特征图尺寸减半,简化了神经网络的拓扑结构并取得了良好效果。同时VGGNet将卷积层提升到卷积块的概念:卷积块有2~3个卷积层构成,使网络有更大感受野的同时能降低网络参数,同时多次使用ReLu激活函数有更多的线性变换,学习能力更强。

模型思路
给定一张风格图像 a 和一张普通图像 p,风格图像经过 VGG 的时候在每个卷积层会得到很多 feature maps,这些 feature maps 组成一个集合 A,同样的普通图像 p 通过 VGG 的时候也会得到很多 feature maps,这些 feature maps 组成一个集合 P,然后生成一张随机噪声图像 x, 随机噪声图像 x 通过 VGG 的时候也会生成很多 feature maps,这些 feature maps 构成集合 G 和 F,并分别对应集合 A 和 P,最终的优化函数是希望调整 x ,让随机噪声图像 x 最后看起来既保持普通图像 p 的内容,又有一定的风格图像 a 的风格。
vgg风格迁移模型

算法实现

  1. 首先,我们使用VGG中的一些层的输出来表示图片的内容特征和风格特征。

  2. 将内容图片输入网络,计算内容图片在网络指定层([‘conv4_2’])上的输出值。

  3. 计算内容的损失。我们这样定义内容损失:内容图片在指定层上提取出的特征矩阵,与噪声图片在对应层上的特征矩阵的差值的L2范数。即求两两之间的像素差值的平方.
    最小化内容图像以及混合图像激活特征的差距,使得混合图像和内容图像的轮廓相似。
    对应每一层的内容损失函数:


    其中,F是噪声图片的特征矩阵,P是内容图片的特征矩阵。M是P的长*宽,N是信道数。

    最终的内容损失为,每一层的内容损失加权和\omega_l。

  4. 将风格图片输入网络,计算风格图片在网络指定层([('conv1_1',1.),('conv2_1',1.),('conv3_1',1.),('conv4_1',1.),('conv5_1',1.)])上的输出值。

  5. 计算风格的损失。我们使用风格图像在指定层上的特征矩阵的GRAM矩阵来衡量其风格,风格损失可以定义为风格图像和噪音图像特征矩阵的格莱姆矩阵的差值的L2范数。
    格拉姆矩阵本质上是风格层中特征激活向量的点乘矩阵。如果格拉姆矩阵中的一个元素值接近于0,则意味着给定层的特征没有被激活。因此,最小化损失函数,我们可以将风格图片的纹理和特征迁移到混合图像上。
    对于每一层的风格损失函数:


其中M是特征矩阵的长*宽,N是特征矩阵的信道数。G为噪音图像特征的Gram矩阵,A为风格图片特征的GRAM矩阵。
最终的风格损失为

每一层的风格损失加权和,是l层的权重。

6.最终用于训练的损失函数为内容损失和风格损失的加权和。


7. 当训练开始时,我们根据内容图片和噪声,生成一张噪声图片。并将噪声图片输入网络,计算loss,再根据loss调整噪声图片。将调整后的图片重新输入给网络,重新计算loss,经过反复迭代,最终得到一张混合效果的风格迁移图片。


环境配置

笔者代码基础薄弱,并且是python语言的门外汉,为了搭建这个机深度学习所需环境走了很多的弯路,最终选择的是一种配置起来比较轻松的运行环境。所以先从环境的搭建写起,整理一下本文所需学习效果
- 模型优化


算法简介

卷积神经网络


环境配置

笔者代码基础薄弱,并且是python语言的门外汉,为了搭建这个机深度学习所需环境走了很多的弯路。所以我先整理一下我的使用环境,以及顺利配置好所需要的环境的一种成功的流程。

运行配置:

环境搭建:

(在我本身的环境搭建过程中,绝大部分细节我都没有截图,于是一些常见错误也没有保留下来,但是一些重点会在文中赘述)

此次,由于会使用anancoda直接搭建环境,对原电脑配置的python不做要求。
先直接在[anaconda官网下载链接][31]上下载对应电脑操作系统版本的Anaconda安装包,安装包较大。官网下载软件流程较为简单,可以通过该链接 来学习。
为了系统使用有规律且易于配置环境,建议将整个Anaconda安装在默认安装目录,例如我的电脑是AUSU的笔记本,其安装路径是"C:\Users\asus\Anaconda3",安装完成后会,开始菜单就会显示我们的整个Anaconda目录下的软件。

安装以后我们可以直接启动Anaconda Promot,然后进行下一步配置。

首先创建一个虚拟环境,可以命名为tensorflow 。

conda create -n tensorflow

系统显示已经建立好名为tensorflow的虚拟环境,就可以激活该虚拟环境

creative tensorflow

之后的操作都在该虚拟环境中。
在该tensorflow下建立的虚拟环境会存储在Anaconda3安装路径下的envs文件夹中。(例如C:\Users\asus\Anaconda3\envs\tensorflow)
因为我们需要在其后用pip获取TensorFlow库,官网下载非常慢,我们先为它配置tensorflow的清华镜像源。

conda config --add channels     https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --set show_channel_urls yes

使用的是python3.5,直接在该环境下输入

python=3.5

anaconda会自动获取官网对应版本python3.5并进行下载安装,下载过程中会有键入y继续,然后显示安装完成。

其后下载TensorFlow库

pip install --upgrade --ignore-installed tensorflow

这一步中我的运行在下载中报错,报错提示说因为pip的版本过低,此时只要更新pip

pip install --upgrade pip

接着再次进行前面一步下载TensorFlow库步骤。
待tensorflow下载完成以后,根据实际代码需求需要将它配置给python的其他编辑器,比如PyCharm,Jupiter Notebook,Spyder等,
可以直接使用

ipython
spyer

等命令,此时就会在tensorflow虚拟环境下打开它们。比如我在调开的Spyder中,验证TensorFlow库是否已经配置给编译环境,键入

import tensorflow as tf
hello = tf.constant("Hello Tensorflow!")
sess = tf.Session()
print (sess.run(hello))
sess.close()

其后输出

b'Hello Tensorflow!'

证明TensorFlow库已经能够实现调用。

在菜单栏中也会加载出Spyder(tensorflow)。于是从菜单栏的按钮,我们可以快捷进入在相同虚拟环境下的Spyder编辑器,后期的程序处理都可以直接选择这个入口。

不过PyCharm的配置,需要主动在编辑器中选择在虚拟环境目录下的python3.5并加载相应库,没有最终使用PyCharm,使用可以自行参考[PyCharm配置][42]。

紧接着剩下的numpy、scipy等库,我们都可以通过Anaconda的主界面来安装,非常方便。

最后,需要将网络上训练好的VGG-19模型下载下来放入正确位置。[VGG-19下载链接][53]
由于我使用Spyder编译器,为了实现模型调用,我们需要将 模型文件放入主目录下的(C:\Users\asus.spyder-py3)中。

到此,我们的环境配置基本结束!


程序实现

导入用到的函数库

  1. import tensorflow as tf
  2. import numpy as np
  3. import scipy.io
  4. import scipy.misc
  5. import os

设置图像大小,因为已经提前定义,所以在放置图片时需要首先处理好图片的尺寸为像素600*800

  1. IMAGE_W = 800
  2. IMAGE_H = 600

下面分别载入内容图片,风格图片,已经生成的结果图片。文件夹images是在主目录下的(C:\Users\asus.spyder-py3)中,其中放入图片都已经分割成600*800的格式了

  1. CONTENT_IMG = './images/NPUac.jpg'
  2. STYLE_IMG = './images/MONET1.jpg'
  3. OUTOUT_DIR = './results'
  4. OUTPUT_IMG = 'results.png'
  5. VGG_MODEL = 'imagenet-vgg-verydeep-19.mat'

定义随机噪声与内容图像的比例

  1. INI_NOISE_RATIO = 0.7

定义内容图像和风格图像的权重为1:500

  1. CONTENT_STRENGTH = 1
  2. STYLE_STRENGTH = 500

定义训练的迭代次数

  1. ITERATION = 300

匹配内容和风格图像在卷积神经网络中的层次

  1. CONTENT_LAYERS =[('conv4_2',1.)]
  2. STYLE_LAYERS=[('conv1_1',1.),('conv2_1',1.),('conv3_1',1.),('conv4_1',1.),('conv5_1',1.)]

定义图片均值

  1. ```
  2. MEAN_VALUES = np.array([123, 117, 104]).reshape((1,1,1,3))
  3. 定义网络
  4. ```python
  5. def build_net(n_type, n_in, n_wb=None):
  6. if n_type == 'conv':
  7. return tf.nn.relu(tf.nn.conv2d(n_in, n_wb[0], strides=[1, 1, 1, 1], padding='SAME')+ n_wb[1])
  8. elif n_type == 'pool':
  9. return tf.nn.avg_pool(n_in, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
  10. 定义并获取获取VGG-19模型训练好的权重值weights和偏差值bias
  11. <div class="md-section-divider"></div>
  12. ```python
  13. def get_weight_bias(vgg_layers, i,):
  14. weights = vgg_layers[i][0][0][0][0][0]
  15. weights = tf.constant(weights)
  16. bias = vgg_layers[i][0][0][0][0][64]
  17. bias = tf.constant(np.reshape(bias, (bias.size)))
  18. return weights, bias

根据VGG19的网络,重建卷积神经网络模型

  1. def build_vgg19(path):
  2. net = {}
  3. vgg_rawnet = scipy.io.loadmat(path)
  4. vgg_layers = vgg_rawnet['layers'][0]
  5. net['input'] = tf.Variable(np.zeros((1, IMAGE_H, IMAGE_W, 3)).astype('float32'))
  6. net['conv1_1'] = build_net('conv',net['input'],get_weight_bias(vgg_layers,0))
  7. net['conv1_2'] = build_net('conv',net['conv1_1'],get_weight_bias(vgg_layers,2))
  8. net['pool1'] = build_net('pool',net['conv1_2'])
  9. net['conv2_1'] = build_net('conv',net['pool1'],get_weight_bias(vgg_layers,5))
  10. net['conv2_2'] = build_net('conv',net['conv2_1'],get_weight_bias(vgg_layers,7))
  11. net['pool2'] = build_net('pool',net['conv2_2'])
  12. net['conv3_1'] = build_net('conv',net['pool2'],get_weight_bias(vgg_layers,10))
  13. net['conv3_2'] = build_net('conv',net['conv3_1'],get_weight_bias(vgg_layers,12))
  14. net['conv3_3'] = build_net('conv',net['conv3_2'],get_weight_bias(vgg_layers,14))
  15. net['conv3_4'] = build_net('conv',net['conv3_3'],get_weight_bias(vgg_layers,16))
  16. net['pool3'] = build_net('pool',net['conv3_4'])
  17. net['conv4_1'] = build_net('conv',net['pool3'],get_weight_bias(vgg_layers,19))
  18. net['conv4_2'] = build_net('conv',net['conv4_1'],get_weight_bias(vgg_layers,21))
  19. net['conv4_3'] = build_net('conv',net['conv4_2'],get_weight_bias(vgg_layers,23))
  20. net['conv4_4'] = build_net('conv',net['conv4_3'],get_weight_bias(vgg_layers,25))
  21. net['pool4'] = build_net('pool',net['conv4_4'])
  22. net['conv5_1'] = build_net('conv',net['pool4'],get_weight_bias(vgg_layers,28))
  23. net['conv5_2'] = build_net('conv',net['conv5_1'],get_weight_bias(vgg_layers,30))
  24. net['conv5_3'] = build_net('conv',net['conv5_2'],get_weight_bias(vgg_layers,32))
  25. net['conv5_4'] = build_net('conv',net['conv5_3'],get_weight_bias(vgg_layers,34))
  26. net['pool5'] = build_net('pool',net['conv5_4'])
  27. return net

创建内容图像的损失函数。损失函数选用的是均方误差模型,最小化内容图像以及混合图像激活特征的差距,使得混合图像和内容图像的轮廓相似。
普通图像p的feature maps集合为P, 随机噪声图像x的feature maps集合为F

  1. def build_content_loss(p, x):
  2. print(type(p),np.shape(p))
  3. # net['conv4_2']-- <class 'numpy.ndarray'> (1, 75, 100, 512)
  4. print(type(x),np.shape(x))
  5. # net['conv4_2']-- <class 'tensorflow.python.framework.ops.Tensor'> (1, 75, 100, 512)
  6. M = p.shape[1]*p.shape[2]
  7. N = p.shape[3]
  8. loss = (1./(2* N**0.5 * M**0.5 )) * tf.reduce_sum(tf.pow((x - p),2))
  9. return loss

风格图像损失函数的定义,同样使用均方误差模型。计算风格输出层张量的格拉姆矩阵,格拉姆矩阵本质上是风格层中特征激活向量的点乘矩阵。如果格拉姆矩阵中的一个元素值接近于0,则意味着给定层的特征没有被激活。因此,最小化损失函数,我们可以将风格图片的纹理和特征迁移到混合图像上。
风格图像a的feature maps集合A, 随机噪声图像x的feature maps集合G

  1. def build_style_loss(a, x):
  2. def _gram_matrix(x, area, depth):
  3. x1 = tf.reshape(x,(area,depth))
  4. g = tf.matmul(tf.transpose(x1), x1)
  5. return g
  6. def _gram_matrix_val(x, area, depth):
  7. x1 = x.reshape(area,depth)
  8. g = np.dot(x1.T, x1)
  9. return g
  10. M = a.shape[1]*a.shape[2]
  11. N = a.shape[3]
  12. G = _gram_matrix(x, M, N)
  13. A = _gram_matrix_val(a, M, N)
  14. loss = (1./(4 * N**2 * M**2)) * tf.reduce_sum(tf.pow((G - A),2))
  15. return loss

使用scipy.misc库,对读写图像函数进行定义

  1. def read_image(path):
  2. image = scipy.misc.imread(path)
  3. image = scipy.misc.imresize(image,(IMAGE_H,IMAGE_W))
  4. image = image[np.newaxis,:,:,:]
  5. image = image - MEAN_VALUES
  6. return image
  7. def write_image(path, image):
  8. image = image + MEAN_VALUES
  9. image = image[0]
  10. image = np.clip(image, 0, 255).astype('uint8')
  11. scipy.misc.imsave(path, image)

模型训练的主函数如下

  1. def main():
  2. net = build_vgg19(VGG_MODEL)
  3. sess = tf.Session()
  4. sess.run(tf.global_variables_initializer())
  5. #写入内容图片和风格图片,用随机函数生成随机噪声图片
  6. noise_img = np.random.uniform(-20, 20, (1, IMAGE_H, IMAGE_W, 3)).astype('float32')
  7. content_img = read_image(CONTENT_IMG)
  8. style_img = read_image(STYLE_IMG)
  9. #build_content_loss()的第一个参数由content_img获得;第二个参数由噪声图像x计算而得,x在迭代中更新
  10. sess.run([net['input'].assign(content_img)])
  11. cost_content = sum(map(lambda lay: lay[1]*build_content_loss(sess.run(net[lay[0]]), net[lay[0]]), CONTENT_LAYERS))
  12. #build_style_loss()的第一个参数由style_img获得;STYLE_LAYERS包含5层,权重自定义,这里lay[1]都是1
  13. sess.run([net['input'].assign(style_img)])
  14. cost_style = sum(map(lambda lay: lay[1]*build_style_loss(sess.run(net[lay[0]]), net[lay[0]]), STYLE_LAYERS))
  15. #定义混合图片的混合计算式
  16. cost_total = cost_content + STYLE_STRENGTH * cost_style
  17. #选用Adam算法的优化器
  18. optimizer = tf.train.AdamOptimizer(2.0)
  19. #定义自动训练
  20. train = optimizer.minimize(cost_total)
  21. sess.run(tf.global_variables_initializer())
  22. sess.run(net['input'].assign( INI_NOISE_RATIO * noise_img + (1.-INI_NOISE_RATIO) * content_img))
  23. #net['input']是一个tf变量由于tf.Variable()从而每次迭代更新;而vgg19中的weights和bias都是tf常量不会被更新
  24. if not os.path.exists(OUTOUT_DIR):
  25. os.mkdir(OUTOUT_DIR)
  26. #定义训练的迭代以及图像保存的周期,迭代次数在前面设置,图片生成png格式
  27. for i in range(ITERATION):
  28. sess.run(train)
  29. if i%15==0:
  30. result_img = sess.run(net['input'])
  31. print(sess.run(cost_total))
  32. write_image(os.path.join(OUTOUT_DIR, '%s.png' % (str(i).zfill(4))), result_img)
  33. if __name__ == '__main__':
  34. main()

根据我们的上述代码,模型正常运行时,会在每次迭代15次(预设值)后,输出cost_total的计算值,并在主目录的result文件夹下生成一个png图片。
![运行结果][75]


实现效果

上述代码中,写入了具体的加载图片,是一个对莫奈的一张风景画的风格进行学习,并将其迁移到一张西工大的风景图片上的实现。
风格图片
莫奈风景1
内容图片
npuac
学习生成的过程图片
过程1
过程主图2
最终300次迭代后的效果
![此处输入图片的描述300次 ][12是一个对莫奈的一张风景画的风格进行学习,并将其迁移到一张西工大的风景图片上的实现。
风格图片
此处输入图片的描述
内容图片
此处输入图片的描述
学习生成的过程图片
此处输入图片的描述
此处输入图片的描述
最终300次迭代后的效果
此处输入图片的描述

除此之外我还做了多组不同艺术风格风格、不同迭代次数、不同图像对象的风格迁移训练尝试:


1.艺术风格:凡高《starrynight》迁移对象:《长安游泳馆》迭代次数:500

2.艺术风格:凡高《starrynight》迁移对象:《npu启真湖》迭代次数:100
( 梵高的绘画风格中,特有卷曲的排布的线条和星月夜的用色被这一模型学习的效果非常的好。)

3.艺术风格:毕加索抽象风格 迁移对象:《长安游泳馆》迭代次数:300

4.艺术风格:毕加索抽象风格 迁移对象:《npu启真湖》迭代次数:1050
(毕加索这一幅抽象画,主要风格表现在线条上:以整体的细致纹理来表现面积为主。迭代训练300次以后的图片,其效果并不佳,尝试加大训练次数以后,其线性特征在局部不断强化,表现为原有线条的加深和图片明度、对比度的提高,但是呈现出来的整体风格也比较勉强,没有出现大面积较长而规律排布的细线条特征。)

5.艺术风格:毕加索印象《PARTY》迁移对象:《长安游泳馆》迭代次数:100

6.艺术风格:毕加索印象《PARTY》迁移对象:《npu启真湖》迭代次数:500
(下面每三张图为一组,一共六组,分别对应图1.内容图片。图2.合成图片。图3.风格图片)



divstyle=float:none;clear:both;

divstyle=float:none;clear:both;










效果分析

进阶试探

图像风格迁移在某些部分已经具备了比较成熟的效果,现在已经有可以直接对图片进行风格滤镜的手机软件譬如Prisma,所以可见图像风格迁移生成的部分特征已经可以直接抽取以后叠加在内容图像上。


本文网页版链接
本文用图专用网址


参考文献

Johnson J, Alahi A, Fei-Fei L. Perceptual losses for real-time style transfer and super-resolution[C]//European Conference on Computer Vision. Springer International Publishing, 2016: 694-711.
Gatys L A, Ecker A S, Bethge M. A neural algorithm of artistic style[J]. arXiv preprint arXiv:1508.06576, 2015
Chuan Li, Michael Wand; Combining Markov Random Fields and Convolutional Neural Networks for Image Synthesis,The IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2016, pp. 2479-2486
https://github.com/ckmarkoh/neuralart_tensorflow

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注