集智专栏
资源加载中,请稍后...
集智专栏

如何用Keras打造出“风格迁移”的AI艺术作品

集智小编

过去几年,卷积神经网络(CNN)成为一种前沿的计算机视觉工具,在业界和学界广泛应用。除了人脸识别和无人驾驶领域,CNN 这几年还在艺术领域广受欢迎,其中衍生出一个代表性技术就是“风格迁移”,根据这项技术诞生了很多美图应用,比如 2016 年大火的 Prisma APP。

“风格迁移”是展示神经网络强大能力的一个很有趣的途径。2015 年,德国和美国的一组研究人员发布了一篇论文《A Neural Algorithm of Artistic Style》

详细讨论了深度卷积神经网络如何区分照片中的“内容”和“风格”。论文作者展示了 CNN如何能够将一张照片的艺术风格应用在另一张照片上,生成一张全新的令人眼前一亮的照片。而且他们的方法不需要训练一个新的神经网络,使用来自 ImageNet 这类数据集中的预训练权重就有很好的效果。

在本文,我(作者 Walid Ahmad——译者注)会展示如何用流行的 Python 程序库 Keras 创作“风格迁移”的 AI 作品,整体思路和上面这篇论文的方法一致。本文的全部代码点击这里获取。

使用两张基本的图像素材,我们就能创造出下面这样的 AI 艺术作品:

我们要解决的这个问题是现在有了两张基本图像素材,我们想把它们“合并”在一起。其中一张照片的内容我们希望能够保留,我们把这张照片称为 p。在我举的这个例子中,我从谷歌上随便搜了一张可爱的猫咪照片:

另一张基本图像的艺术风格我们希望能够保留,我们称它为 a。我选了一张巴洛克风格的著名照片:《Violin on Palette》。

enter_image_description_hereenter_image_description_here

最后,我们会得到一张生成照片 x,并用随机的颜色数值将它初始化。随着我们最小化内容和风格的损失函数,这张照片会随之不断变化。

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
## 为1)内容图像 2)风格图像和3)生成图像指定路径
##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

cImPath = './data/base_images/cat.jpg'
sImPath = './data/base_images/violin_and_palette.jpg'
genImOutputPath = './results/output.jpg'

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
## 图像处理
##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
from keras import backend as K
from keras.applications.vgg16 import preprocess_input
from keras.preprocessing.image import load_img, img_to_array

targetHeight = 512
targetWidth = 512
targetSize = (targetHeight, targetWidth)

cImage = load_img(path=cImPath, target_size=targetSize)
cImArr = img_to_array(cImage)
cImArr = K.variable(preprocess_input(np.expand_dims(cImArr, axis=0)), dtype='float32')

sImage = load_img(path=sImPath, target_size=targetSize)
sImArr = img_to_array(sImage)
sImArr = K.variable(preprocess_input(np.expand_dims(sImArr, axis=0)), dtype='float32')

gIm0 = np.random.randint(256, size=(targetWidth, targetHeight, 3)).astype('float64')
gIm0 = preprocess_input(np.expand_dims(gIm0, axis=0))
gImPlaceholder = K.placeholder(shape=(1, targetWidth, targetHeight, 3))

注意,我们这里为了后面的优化,将glm0初始化为 float64。而且为了避免GPU的内存错误,我们将cImArr和slmArr保持为float32.

内容损失

内容损失的目标是确保生成的照片x仍能保留内容照片p的“全局”风格。比如,在我们的这个例子中,我们希望最终生成的图像能看起来还是照片p中的猫咪。这意味着,猫咪的脸、耳朵、眼睛等这些都是可以识别出的。要想达到这个目标,内容损失函数会分别在给定层L中定义为p和x的特征表示之间的均方误差。内容损失函数为:

在这里, F和P是两个矩阵,包含N个行和M个列 N是给定层L中的过滤器数量,M是给定层I的特征图谱(高度乘以宽度)中空间元素的数量 F包含给定层L中X的特征表示 P包含给定层L中p的特征表示

ef get_feature_reps(x, layer_names, model):
    """
    Get feature representations of input x for one or more layers in a given model.
    """
    featMatrices = []
    for ln in layer_names:
        selectedLayer = model.get_layer(ln)
        featRaw = selectedLayer.output
        featRawShape = K.shape(featRaw).eval(session=tf_session)
        N_l = featRawShape[-1]
        M_l = featRawShape[1]*featRawShape[2]
        featMatrix = K.reshape(featRaw, (M_l, N_l))
        featMatrix = K.transpose(featMatrix)
        featMatrices.append(featMatrix)
    return featMatrices

def get_content_loss(F, P):
    cLoss = 0.5*K.sum(K.square(F - P))
    return cLoss

风格损失

风格损失需要保存风格照片a的风格特征。论文作者并未利用特征表示之间的不同,而是利用选定层中的格拉姆矩阵的不同之处,其中格拉姆矩阵定义如下:

格莱姆矩阵格莱姆矩阵

格拉姆矩阵是一个正方矩阵,包含层级L中每个矢量过滤器(vectorized filter)之间的点积。因此该矩阵可以看作层级L中过滤器的一个非规整矩阵。

def get_Gram_matrix(F):
    G = K.dot(F, K.transpose(F))
    return G

那么我们可以将给定层L中的风格损失函数定义为:

给定层的风格损失给定层的风格损失

其中A是风格照片a的格拉姆矩阵,G为生成照片x的格拉姆矩阵。 在大多数卷积神经网络中如VGG,提升层(ascending layer)的感受野(receptive field)会越来越大。随着感受野不断变大,输入图像的更大规模的特征也得以保存下来。正因如此,我们应该选择多个层级用于“风格迁移”,将局部和全局的风格质量进行合并。为了让这些层之间连接顺畅,我们可以为每个层赋予一个权重w,将整个风格损失函数定义为:

风格损失函数风格损失函数

def get_style_loss(ws, Gs, As):
    sLoss = K.variable(0.)
    for w, G, A in zip(ws, Gs, As):
        M_l = K.int_shape(G)[1]
        N_l = K.int_shape(G)[0]
        G_gram = get_Gram_matrix(G)
        A_gram = get_Gram_matrix(A)
        sLoss+= w*0.25*K.sum(K.square(G_gram - A_gram))/ (N_l**2 * M_l**2)
    return sLoss

整合两个函数

最后,我们只需分别为内容损失函数和风格损失函数赋予加权系数,然后大功告成!

整体损失函数整体损失函数

终于得到一个整洁优美的函数公式,能让我们利用⍺和 ß在生成照片上调整内容照片和风格照片两者的相对影响。根据那篇论文的建议以及我自己的经验,让⍺= 1 ,ß = 10,000 效果会很好。

def get_total_loss(gImPlaceholder, alpha=1.0, beta=10000.0):
    F = get_feature_reps(gImPlaceholder, layer_names=[cLayerName], model=gModel)[0]
    Gs = get_feature_reps(gImPlaceholder, layer_names=sLayerNames, model=gModel)
    contentLoss = get_content_loss(F, P)
    styleLoss = get_style_loss(ws, Gs, As)
    totalLoss = alpha*contentLoss + beta*styleLoss
    return totalLoss

模型应用详情

要想开始改变我们的生成图像以最小化损失函数,我们必须用scipy和Keras后端再定义两个函数。首先,用一个函数计算整体损失,其次,用另一个函数计算梯度。两者计算后得到的结果会分别作为目标函数和梯度函数输入到Scipy优化函数中。在这里,我们使用L-BFGS算法(limited-memory BFGS)。

对于每张内容照片和风格照片,我们会提取特征表示,用来构建P和A(对于每个选中的风格层),然后为风格层赋给相同的权重。在实际操作中,通常用L-BFGS算法进行超过500次迭代后,产生的结果就比较可信了。

def calculate_loss(gImArr):
  """
  Calculate total loss using K.function
  """
    if gImArr.shape != (1, targetWidth, targetWidth, 3):
        gImArr = gImArr.reshape((1, targetWidth, targetHeight, 3))
    loss_fcn = K.function([gModel.input], [get_total_loss(gModel.input)])
    return loss_fcn([gImArr])[0].astype('float64')

def get_grad(gImArr):
  """
  Calculate the gradient of the loss function with respect to the generated image
  """
    if gImArr.shape != (1, targetWidth, targetHeight, 3):
        gImArr = gImArr.reshape((1, targetWidth, targetHeight, 3))
    grad_fcn = K.function([gModel.input], 
                          K.gradients(get_total_loss(gModel.input), [gModel.input]))
    grad = grad_fcn([gImArr])[0].flatten().astype('float64')
    return grad

from keras.applications import VGG16
from scipy.optimize import fmin_l_bfgs_b

tf_session = K.get_session()
cModel = VGG16(include_top=False, weights='imagenet', input_tensor=cImArr)
sModel = VGG16(include_top=False, weights='imagenet', input_tensor=sImArr)
gModel = VGG16(include_top=False, weights='imagenet', input_tensor=gImPlaceholder)
cLayerName = 'block4_conv2'
sLayerNames = [
                'block1_conv1',
                'block2_conv1',
                'block3_conv1',
                'block4_conv1',
                ]

P = get_feature_reps(x=cImArr, layer_names=[cLayerName], model=cModel)[0]
As = get_feature_reps(x=sImArr, layer_names=sLayerNames, model=sModel)
ws = np.ones(len(sLayerNames))/float(len(sLayerNames))

iterations = 600
x_val = gIm0.flatten()
xopt, f_val, info= fmin_l_bfgs_b(calculate_loss, x_val, fprime=get_grad,
                            maxiter=iterations, disp=True)

虽然过程有点慢,但能保证效果···

所选优化步骤所选优化步骤

我们开始看见若隐若现地出现一个立体主义画派版的小猫咪!等算法再迭代上几次后:

有立体感的猫咪有立体感的猫咪

我们可以根据猫咪原图的大小对照片略作修改,将两张图并列在一起。很容易看到猫咪的主要特征,比如眼睛、鼻子和爪爪都维持在原来的状态。不过,为了匹配照片风格,它们都被扁平化了,而且棱角分明——但这正是我们想要的结果啊!

我们用同样的方法可是试试其他照片。比如我从谷歌上找了一张建筑图,然后选了梵高的名画《罗纳河上的星夜》:

总结

在本文我们探究了如何用Keras应用“风格迁移”技术,不过我们还可以做很多工作,创造出更加迷人的作品:

  • 尝试用不同的权重:不同的照片混合可能需要调整风格损失权重w或不断优化⍺和 ß的值。例如,在有些例子中,ß/⍺的比例值为10⁵ 效果会更好。

  • 尝试用更多的风格层级:这会消耗更多的计算资源,但能够更顺畅地对风格进行迁移。你可以试试VGG19,而不是VGG16,或者将不同的神经网络架构结合在一起。

  • 尝试用多张内容照片和风格照片:你可以为损失函数增加几张风格照片,混合多张照片或多种艺术风格。增加内容照片或许会带来更有意思的艺术效果。

  • 增加总变分去噪方法:如果你仔细看看上面我得到的照片,你会发现上面有些颗粒状图案——小小的颜色旋涡。用神经网络处理照片通常都会有这个问题,其中一个原因就是照片的有损压缩被带进了特征图谱里。添加总变分去噪可以有效减轻这个问题,点击查看这一步的代码:代码

下面是本文参考的一些资料,大家可以去看一看:

Experiments with style transfer

A Neural Algorithm of Artistic Style

您也许喜欢这些文章

集智专栏

机器学习文章 Top 10(2017年9月),OpenAI竟然才排第7

发表至业界新闻
本月我们浏览了近1400+篇机器学习文章,从中遴选出Top 10(中奖率0.7%)。 关键词:StarCraft II, Dota 2, 目标检测, 语音识别, Siri, 增强学习, 神经网络, TensorFlow
集智专栏

这评论有毒!——文本分类的一般套路

发表至数据科学
文本分类,是各种数据挖掘比赛中比较常见的题,今天我们打算分享了一个评论多分类比赛的一般套路。数据集来源于kaggle比赛:Toxic Comment Classification Challenge,这份代码得分约在0.043分左右。代码基于Keras编写,同时提供了方便的数据集下载渠道。
集智专栏

审片员也快失业了,Google推出视频自动标签挑战赛

发表至业界新闻
视频分析和分类是目前机器学习领域中的一个热门方向。因为视频资料所蕴含的信息丰富、涵盖面广,因此如果能够很好的得到一个视频分类模型,人类在数据科学方面的发展就可以迈出坚实的一步。Google最新在Kaggle竞赛平台上推出的一个YouTube视频自动标签挑战就是在这个方向上的一次很有意义的尝试。

文章评论(0)