近年来,由深度学习所引领的人工智能技术浪潮,开始越来越广泛地应用到社会各个领域。这其中,人工智能与艺术的交叉碰撞,不仅在相关的技术领域和艺术领域引起了高度关注。以相关技术为基础而开发的各种图像处理软件和滤镜应用更是一推出就立刻吸引了海量用户,风头一时无两。在这各种神奇的背后,最核心的就是基于深度学习的图像风格迁移(style transfer)。本博文就是介绍一个这样酷炫的深度学习应用:风格迁移。
基于神经网络的风格迁移算法 “A Neural Algorithm of Artistic Style” 最早由 Gatys 等人在 2015 年提出,随后发表在 CVPR 2016 上。斯坦福大学的 Justin Johnson(cs231n 课程的主讲人之一)给出了 Torch 实现 neural-style。除此之外,这篇文章的作者另外还建立了一个在线艺术风格迁移的网站,deepart.io。在介绍主要内容之前,先直观看下什么是艺术风格迁移,如图 1 所示,给定内容图像(第一行左边图像)以及风格图像(左下角图像)可以生成特定风格下的混合图像。网络多次运算后,人眼很难判断出该图像是否为梵高或者毕加索的真迹。
艺术风格迁移的核心思想就是,可以从一副图像中提取出“风格 style”(比如梵高的夜空风格)以及“内容 content”(比如你能在画中看出河边有匹马)。你可以告诉机器,把 A 用 B 的风格再画一遍。但是怎么用数学语言具体定义风格和内容呢?在这里通过引入一个 VGG19 深度网络来具体阐述相关的含义。
VGG 是 ImageNet2014 年的图像识别大赛识别定位组的冠军,几种不同深度的 VGG 网络结构如图 2 所示(如果对该网络的具体细节比较感兴趣,可以阅读相关论文)。那么问题来了,这种网络既然是用来识别和定位的,和要定义的“风格”和“内容”好像没有什么直接的联系,那这个网络到底是怎么去定义的呢?
当我们把一张图片输入到 VGG 网络中,会在开始处变成一系列向量(每个像素上包含红绿蓝三值,代表了图像长什么样)。而在网络的每层中,我们能得到中间向量,比如 conv3_ 1,conv4_ 2(分别代表第三个卷积层的第一个 feature map 和第四个卷积层的第二个 feature map),但是它们并没有内在的含义。不过,大家想一下,其实对于一个训练好的网络,比如 VGG19,其参数已经确定,通过该参数计算得出的中间向量就可以代表该图像本身,这样就可以定义某一个卷积层中的某个 feature map 作为该图像的内容(content),比如上面说的 conv4_ 2。当然了,这里也不一定非得是 conv4_ 2,也可以是 conv6_ 1,可以根据自己的网络结构进行调整,不过拿不同的feature map作为图像的内容对结果会有影响。最后,在这里总结一下,图像的内容可以简单的认为是通过某个训练好的网络(比如 VGG19)进行计算后,某个卷积层中的某个 feature map(比如 conv4_2)。
给定一张图片 p 和训练好的卷积网络(比如 VGG19),那么在每个卷积层中可以得到多个 feature map,个数取决于每层滤波器的个数 。我们把每个 feature map 向量化,得到大小为 的向量。把每一层的 个特征向量保存到矩阵 中,其元素 表示第l层的第i个滤波器在位置j上的激活响应。现在指定某一层 l 的特征,希望生成一张新的图片 x,使 x 在该层的特征 等于原特征表示 (内容匹配)。定义损失函数如下:
注意:这里是拿某一个卷积层(比如 conv3)中所有的 feature map 作为“内容”,并和新的图像 在同一卷积层中的所有 feature map 进行比较,然后进行平方差求和。不过,这和上面说的某一个卷积层中的某一个 feature map 作为“内容”好像不一样,其实是这样,作为图像的内容,可以是某一层的全部 feature map,也可以是某一个 feature map,这取决于你想要得到的结果。拿特定层的所有 feature map 作为内容,这样更加准确,但同时也增加了计算量,会使训练速度变慢;拿其中一个 feature map 作为内容,可以加快训练速度,但是内容保真度不能得到保证。
图像的内容在上面已经定义好了,相对比较简单,而定义“风格”就有点复杂了。在这里不是随便选取某一层中的某个 feature map 作为“风格”层,而是把一层中的所有特征图都拿来(每一层基本上都有大量的 feature map,这取决于上一层卷积操作的卷积核数量),然后对这些 feature map 两两作内积,求 Gram 矩阵,这个 Gram 矩阵就是图像的风格。那么什么是 Gram 矩阵呢?在这里我不会做太多数学上的描述,尽量以比较易懂的语言来介绍。Gram 矩阵包含了图片的纹理信息以及颜色信息,其定义如下:
其中: 是l层中feature map i和j的内积,k是feature map中的对应元素,对于某一个层来说,feature map i 和j的内积其实就是Gram矩阵中第i行第j列的元素值。下面来定义每层风格的损失函数:
其中: 为该层的feature map数量, 为每个 feature up 的尺寸(比如 3x3,则对应为 9)。遍历该层中的个 feature map(i和j可以相同),这样就可以得到一个 Gram 矩阵。注意,这里只是某一层中定义的“风格”,为了达到更好的效果,需要对多个卷积层定义“风格”,最终的风格损失函数为:
其中: 为每个层“风格”所对应的权重,具体数值要根据实际情况来定,当然也可以设置每层的数值相等,最终和为 1。
上面我们已经定义好了图像的内容和风格以及对应的损失函数,那么最终的损失函数该怎么定义呢?定义如下式所示:
其中 和 分别代表内容和风格损失的权重,其和为1。如果需要在合成图像中突出内容图像,则给予 较大权值;如果需要突出风格图像,则给予 较大权值。
其实很简单,这里以白噪声图像作为输入,除此之外,还需要一幅内容图像和一幅风格图像。内容和风格可以由训练好的某种网络来提取,比如 VGG19,当然也可以是 Reset。有了白噪声图像,还需要搭建一个用来生成合成图像的卷积神经网络,该网络可以根据需要自定义,既可以是较深的网络,也可以是较浅的网络,不过要保证输入是一定维度的白噪声数据(或图像),输出为合成的图像数据。注意,这里的网络在设计时,某些层的维度要和提取网络的相同(比如使用 VGG 的 conv3 层作为内容层,那么该网络在对应层生成的数据大小应与 conv3 的相同),这样才能满足损失函数的计算要求,训练结构如图 3 所示:
首先介绍下自己的实验环境,8G 内存+256G SSD+GeForce 920MX+cuda8.0+cudnn6.1+Anaconda3+tensorflow-gpu,关于 cuda 和 cudnn 的安装配置在这里就不多说了,网上很多教程,下面来看下代码吧!
%导入几个需要用到的库 import tensorflow as tf import numpy as np import scipy.io import scipy.misc import os %设置图像的路径以及需要调整到的宽度值和高度值 IMAGE_W = 800 IMAGE_H = 600 CONTENT_IMG = './images/Taipei101.jpg' STYLE_IMG = './images/StarryNight.jpg' OUTOUT_DIR = './results' OUTPUT_IMG = 'results.png' VGG_MODEL = 'imagenet-vgg-verydeep-19.mat' %这里采用在imagenet中训练好的VGG19模型,需要下载 INI_NOISE_RATIO = 0.7 %对输入图像噪声干扰的比例 STYLE_STRENGTH = 500 %风格强度 ITERATION = 5000 %迭代次数
%对VGG19网络定义内容层和风格层 CONTENT_LAYERS =[('conv4_2',1.)] STYLE_LAYERS=[('conv1_1',1.),('conv2_1',1.),('conv3_1',1.),('conv4_1',1.),('conv5_1',1.)] MEAN_VALUES = np.array([123, 117, 104]).reshape((1,1,1,3)) %定义建立池化层和卷积层网络的函数 def build_net(ntype, nin, nwb=None): if ntype == 'conv': return tf.nn.relu(tf.nn.conv2d(nin, nwb[0], strides=[1, 1, 1, 1], padding='SAME')+ nwb[1]) elif ntype == 'pool': return tf.nn.avg_pool(nin, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') %获取VGG19模型参数的函数 def get_weight_bias(vgg_layers, i,): weights = vgg_layers[i][0][0][0][0][0] weights = tf.constant(weights) bias = vgg_layers[i][0][0][0][0][1] bias = tf.constant(np.reshape(bias, (bias.size))) return weights, bias %建立训练网络模型 def build_vgg19(path): net = {} vgg_rawnet = scipy.io.loadmat(path) vgg_layers = vgg_rawnet['layers'][0] net['input'] = tf.Variable(np.zeros((1, IMAGE_H, IMAGE_W, 3)).astype('float32')) net['conv1_1'] = build_net('conv',net['input'],get_weight_bias(vgg_layers,0)) net['conv1_2'] = build_net('conv',net['conv1_1'],get_weight_bias(vgg_layers,2)) net['pool1'] = build_net('pool',net['conv1_2']) net['conv2_1'] = build_net('conv',net['pool1'],get_weight_bias(vgg_layers,5)) net['conv2_2'] = build_net('conv',net['conv2_1'],get_weight_bias(vgg_layers,7)) net['pool2'] = build_net('pool',net['conv2_2']) net['conv3_1'] = build_net('conv',net['pool2'],get_weight_bias(vgg_layers,10)) net['conv3_2'] = build_net('conv',net['conv3_1'],get_weight_bias(vgg_layers,12)) net['conv3_3'] = build_net('conv',net['conv3_2'],get_weight_bias(vgg_layers,14)) net['conv3_4'] = build_net('conv',net['conv3_3'],get_weight_bias(vgg_layers,16)) net['pool3'] = build_net('pool',net['conv3_4']) net['conv4_1'] = build_net('conv',net['pool3'],get_weight_bias(vgg_layers,19)) net['conv4_2'] = build_net('conv',net['conv4_1'],get_weight_bias(vgg_layers,21)) net['conv4_3'] = build_net('conv',net['conv4_2'],get_weight_bias(vgg_layers,23)) net['conv4_4'] = build_net('conv',net['conv4_3'],get_weight_bias(vgg_layers,25)) net['pool4'] = build_net('pool',net['conv4_4']) net['conv5_1'] = build_net('conv',net['pool4'],get_weight_bias(vgg_layers,28)) net['conv5_2'] = build_net('conv',net['conv5_1'],get_weight_bias(vgg_layers,30)) net['conv5_3'] = build_net('conv',net['conv5_2'],get_weight_bias(vgg_layers,32)) net['conv5_4'] = build_net('conv',net['conv5_3'],get_weight_bias(vgg_layers,34)) net['pool5'] = build_net('pool',net['conv5_4']) return net %定义内容损失,算法上面已经讲过 def build_content_loss(p, x): M = p.shape[1]*p.shape[2] N = p.shape[3] loss = (1./(2* N**0.5 * M**0.5 )) * tf.reduce_sum(tf.pow((x - p),2)) return loss %定义Gram矩阵,针对网络中的图像 def gram_matrix(x, area, depth): x1 = tf.reshape(x,(area,depth)) g = tf.matmul(tf.transpose(x1), x1) return g %定义Gram矩阵,针对风格图像 def gram_matrix_val(x, area, depth): x1 = x.reshape(area,depth) g = np.dot(x1.T, x1) return g %建立风格损失,算法上面已经介绍过 def build_style_loss(a, x): M = a.shape[1]*a.shape[2] N = a.shape[3] A = gram_matrix_val(a, M, N ) G = gram_matrix(x, M, N ) loss = (1./(4 * N**2 * M**2)) * tf.reduce_sum(tf.pow((G - A),2)) return loss %读取图像的函数 def read_image(path): image = scipy.misc.imread(path) image = scipy.misc.imresize(image,(IMAGE_H,IMAGE_W)) image = image[np.newaxis,:,:,:] image = image - MEAN_VALUES return image %保存图像的函数 def write_image(path, image): image = image + MEAN_VALUES image = image[0] image = np.clip(image, 0, 255).astype('uint8') scipy.misc.imsave(path, image) %主函数 def main(): net = build_vgg19(VGG_MODEL) sess = tf.Session() sess.run(tf.initialize_all_variables()) noise_img = np.random.uniform(-20, 20, (1, IMAGE_H, IMAGE_W, 3)).astype('float32') content_img = read_image(CONTENT_IMG) style_img = read_image(STYLE_IMG) %这里通过迁移学习,提取VGG19中的参数来搭建新的网络,而且新的网络也是提取网络 sess.run([net['input'].assign(content_img)]) cost_content = sum(map(lambda l,: l[1]*build_content_loss(sess.run(net[l[0]]) , net[l[0]]) , CONTENT_LAYERS)) sess.run([net['input'].assign(style_img)]) cost_style = sum(map(lambda l: l[1]*build_style_loss(sess.run(net[l[0]]) , net[l[0]]) , STYLE_LAYERS)) %这里是把风格作为首要优化的对象 cost_total = cost_content + STYLE_STRENGTH * cost_style optimizer = tf.train.AdamOptimizer(2.0) train = optimizer.minimize(cost_total) sess.run(tf.initialize_all_variables()) sess.run(net['input'].assign( INI_NOISE_RATIO* noise_img + (1.-INI_NOISE_RATIO) * content_img)) %构建输入图像,其中也有一部分内容图像 if not os.path.exists(OUTOUT_DIR): os.mkdir(OUTOUT_DIR) %注意,如果笔记本性能不是太好,建议把迭代次数调小点 for i in range(ITERATION): sess.run(train) if i%100 ==0: result_img = sess.run(net['input']) print(sess.run(cost_total)) write_image(os.path.join(OUTOUT_DIR,'%s.png'%(str(i).zfill(4))),result_img) write_image(os.path.join(OUTOUT_DIR,OUTPUT_IMG),result_img) if __name__ == '__main__': main()
源代码链接: