风格迁移
风格迁移算法经历多次定义和更新,现在应用在许多智能手机APP上。
风格迁移在保留目标图片内容的基础上,将图片风格引用在目标图片上。
风格本质上是指在各种空间尺度上图像中的纹理,颜色和视觉图案;内容是图像的高级宏观结构。
实现风格迁移背后的关键概念与所有深度学习算法的核心相同:定义了一个损失函数来指定想要实现的目标,并最大限度地减少这种损失。
知道自己想要实现的目标:在采用参考图像的样式的同时保留原始图像的内容。如果我们能够在数学上定义内容和样式,那么最小化的适当损失函数将是以下内容:
loss = distance(style(reference_image) - style(generated_image)) +
distance(content(original_image) - content(generated_image))
distance是一个如L2范数的函数,content计算图片内容表示的函数;style计算图片风格表示的函数。最小化损失函数导致style(generated_image)和引用图片style(reference_image)尽可能接近,而content(generated_image))与内容图片content(original_image)尽可能接近,最终达到风格迁移的目标。
内容损失函数
我们已经知道网络模型前几层的激活函数值表示图片的局部信息,高层网络激活值包括全局性、抽象性的特征信息。换言之,卷积网的不同层的激活值提供了在不同空间尺度上图像内容的分解。因此,期望通过convnet中上层的表示捕获更全局和抽象的图像内容。
内容损失函数的另一种选择是在目标图像上计算的预训练的网络中的上层的激活与生成的图像上计算的相同层的激活之间的L2范数。这保证从上层看生成的图像看起来与原始目标图像类似。假设卷积网的上层看到的是输入图像的内容,那么这就是保存图像内容的一种方式。
风格损失函数
内容损失函数仅使用单个上层,但是Gatys定义的风格损失函数使用多个convnet层:尝试捕获由convnet提取的所有空间比例的样式参考图像的外观,而不仅仅是单个比例。对于风格的损失,Gatys使用图层激活的Gram矩阵:给定图层的要素图的内积。该内积可以理解为表示层的特征之间的相关性的图。这些特征相关性捕获特定空间尺度的模式的统计数据,其在经验上对应于在该尺度下找到的纹理的外观。
因此,风格损失旨在在风格参考图像和生成的图像之间保持不同层的激活内的类似内部相关性。反过来,这保证了在不同空间尺度上找到的纹理在样式参考图像和生成的图像中看起来相似。
可以使用预训练好的网络模型定义损失函数:
- 通过在目标内容图像和生成的图像之间保持类似的高级图层激活来保留内容。卷积网应该“看到”目标图像和生成的图像包含相同的内容;
- 通过在低级图层和高级图层的激活中保持类似的相关性来保留样式。特征相关性捕获纹理:生成的图像和样式参考图像应在不同的空间尺度共享相同的纹理。
Keras实现
使用VGG19网络模型实现风格迁移。流程:
- 设置一个网络,同时为风格参考图像,目标图像和生成图像计算VGG19图层激活函数值;
- 使用在这三个图像上计算的图层激活值来定义前面描述的损失函数,可以将其最小化以实现风格迁移;
- 设置梯度下降过程以最小化此损失函数。
定义风格图片、目标图片路径地址;为了确保两张处理图片尺寸相同(尺寸不同会增加处理难度),对两张图片进行resize操作,大小为400px。
定义初始变量
from keras.preprocessing.image import load_img,img_to_array
target_image_path = 'img/protrait.jpg'
style_reference_image_path = 'img/transfer_style_reference.jpg'
width, height = load_img(target_image_path).size
img_height = 400
img_width = int(width*img_height/height)
定义辅助函数方便加载、预处理、后期处理VGG19卷积网络接收和产生的图片。
辅助函数
import numpy as np
from keras.applications import vgg19
def preprocess_image(image_path):
img = load_img(image_path,target_size=(img_height,img_width))
img = img_to_array(img)
img = np.expand_dims(img,axis=0)
img = vgg19.preprocess_input(img)
return img
def deprocess_image(x):
x[:,:,0] += 103.939#zero-centering 0中心化:减去均值
x[:,:,1] += 116.779
x[:,:,2] += 123.68
x = x[:,:,::-1]#BGR---> RGB
x = np.clip(x,0,255).astype('uint8')
return x
设置vgg19网络。以风格图片、目标图片、生成图片的placeholder三张图片的batch作为输入。
加载预训练VGG19模型,应用
from keras import backend as K
target_image = K.constant(preprocess_image(target_image_path))
style_reference_image=K.constant(preprocess_image(style_reference_image_path))
combination_image = K.placeholder((1,img_height,img_width,3))#包含生成图片
input_tensor = K.concatenate([target_image,style_reference_image,
combination_image],axis=0)#形成输入张量
model = vgg19.VGG19(input_tensor=input_tensor,weights='imagenet',include_top=False)
print("Model loaded.")
定义内容损失,确保目标图片和生成图片在VGG19卷积网络的上层网络中相似。
内容损失
def content_loss(base,combination):
return K.sum(K.square(combination-base))
定义风格损失。使用辅助函数计算Gram矩阵:在原始特征矩阵中找到的相关图。
风格损失
def gram_matrix(x):
features = K.batch_flatten(K.permute_dimensions(x,(2,0,1)))
gram = K.dot(features,K.transpose(features))
return gram
def style_loss(style,combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_height*img_width
return K.sum(K.square(S-C)) / (4.*(channels ** 2) * (size ** 2))
除了这两种损失函数外,加第三种:总变异损失,其对所生成的组合图像的像素进行操作。它鼓励生成的图像中的空间连续性,从而避免过度像素化的结果。可以将其解释为正则化损失。
变异损失
def total_variation_loss(x):
a = K.square(
x[:,:img_height-1,:img_width-1,:]-
x[:,1:,:img_width-1,:]
)
b = K.square(
x[:,img_height-1,:img_width-1,:] -
x[:,:img_height-1,1:,:]
)
return K.sum(K.pow(a+b, 1.25))
最小化损失函数是三种损失函数的加权平均。为了计算内容损失,只需要使用一个上层网络--block5_conv2网络层;计算风格损失,需要使用多个网络层:从底层网络到高层网络。最后加上变异损失。
依赖于使用风格图片和内容图片,可能需要微调content_weight系数(内容损失对全部损失的贡献程度)。大content_weight意味着目标内容在生成图片中更容易识别。
定义最终损失函数
output_dict = dict([(layer.name,layer.output) for layer in model.layers])
content_layer = 'block5_conv2'
style_layers = ['block1_conv1','block2_conv1',
'block3_conv1','block4_conv1','block5_conv1']
total_variation_weight = 1e-4
style_weight = 1.
content_weight = 0.025
loss = K.variable(0.)#最终损失值
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss+=content_weight*content_loss(target_image_features,combination_features)#加内容损失
for layer_name in style_layers:#加风格损失
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss += (style_weight / len(style_layers)) * sl
#加变异损失,得到最终损失函数值
loss += total_variation_weight * total_variation_loss(combination_image)
最后,使用梯度下降算法。Gatys论文中使用L-BFGS算法。L-BFGS算法包含在SciPy包中,在SciPy实现中有两个限制:
- 要求的损失函数值、梯度函数值作为两个独立的函数传递;
- 必须flat展开向量,而图片数组是3D。
单独计算损失函数的值和梯度的值是低效的,因为这样做会导致两者之间的大量冗余计算;这个过程几乎是共同计算过程的两倍。要绕过这个,将设置一个名为Evaluator的Python类,它同时计算损失值和梯度值,在第一次调用时返回损失值,并缓存下一次调用的梯度。
梯度更新
grads = K.gradients(loss, combination_image)[0]
fetch_loss_and_grads = K.function([combination_image], [loss, grads])
class Evaluator(object):
def __init__(self):
self.loss_value = None
self.grads_values = None
def loss(self, x):
assert self.loss_value is None
x = x.reshape((1, img_height, img_width, 3))
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1].flatten().astype('float64')
self.loss_value = loss_value
self.grad_values = grad_values
return self.loss_value
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
self.loss_value = None
self.grad_values = None
return grad_values
evaluator = Evaluator()
最终,使用SciPy的L-BFGS算法运行梯度下降,每次迭代过程中保存当前生成的图片。
风格迁移循环
from scipy.optimize import fmin_l_bfgs_b
from scipy.misc import imsave
import time
result_prefix = 'my_result'
iterations = 20
x = preprocess_image(target_image_path)#目标图片路径
x = x.flatten()#展开,应用l-bfgs
for i in range(iterations):
print('Start of iteration', i)
start_time = time.time()
#在生成图片上运行L-BFGS优化;注意传递计算损失和梯度值必须为两个不同函数作为参数
x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x,
fprime=evaluator.grads,maxfun=20)
print('Current loss value:', min_val)
img = x.copy().reshape((img_height, img_width, 3))
img = deprocess_image(img)
fname = result_prefix + '_at_iteration_%d.png' % i
imsave(fname, img)
print('Image saved as', fname)
end_time = time.time()
print('Iteration %d completed in %ds' % (i, end_time - start_time))
请记住,这种技术所实现的仅仅是图像重新构造或纹理转移的一种形式。它最适用于具有强烈纹理和高度自相似性的样式参考图像,并且内容目标不需要高级别的细节以便可识别。它通常无法实现相当抽象的功能,例如将一幅肖像的风格转移到另一幅肖像。该算法更接近经典信号处理而不是AI。
另外,请注意运行此风格迁移算法很慢。但是,由设置操作的转换非常简单,只要有适当的训练数据,它就可以通过一个小型,快速的前馈卷积网络学习。因此,可以通过首先花费大量计算周期来生成固定样式参考图像的输入输出训练示例,使用概述的方法,然后训练一个简单的convnet来学习这种特定于样式的转换,从而实现快速样式转换。一旦完成,对给定图像进行风格化是即时的:它只是这个小小的一个前向传递。
小结
- 风格迁移包括创建新图像,该图像保留目标图像的内容,同时还捕获参考图像的样式;
- 内容可以通过卷积网络的高层网络捕获;
- 风格通过卷积网络的不同网络层激活函数的内部相关性计算;
- 因此,深度学习允许将风格迁移表达为使用由预训练的convnet对定义的损失进行优化的过程。