神经风格迁移(neural style transfer)它由 Leon Gatys 等人于 2015 年夏天提出[“A neural algorithm of artistic style”]
'''
#成功转化成许多智能手机图片应用
#神经风格迁移是指将参考图像的风格应用于目标图像,同时保留目标图像的内容
实现风格迁移背后的关键概念与所有深度学习算法的核心思想是一样的:
定义一个损失函 数来指定想要实现的目标,然后将这个损失最小化。你知道想要实现的目标是什么,就是保存原始图像的内容,同时采用参考图像的风格。如果我们能够在数学上给出内容和风格的定义, 那么就有一个适当的损失函数(如下所示),我们将对其进行最小化。
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(generated_image),从而实现我们定义的风格迁移
'''
#内容损失
'''
cnn更靠底部的层激活包含关于图像的局部信息,而更靠近顶部的层则包含更加全局、更加抽象的信息。
因此,图像的内容是更加全局和抽象的,我们认为它能够被卷积神经网络更 靠顶部的层的表示所捕捉到。
内容损失的一个很好的候选者就是两个激活之间的 L2范数,一个激活是预训练的卷积神经网络更靠顶部的某层在目标图像上计算得到的激活,另一个激活是同一层在生成图像上计算得到的激活
在更靠顶部的层看来,生成图像与原始目标图像看起来很相似。假设卷积神经网络更靠顶部的层看到的就是输入图像的内容,那么这种方法可以保存图像内容。
'''
#风格损失
'''
Gatys 等人定义的风格损失则使用了卷积神经网络的多个层。
我们想要捉到卷积神经网络在风格参考图像的所有空间尺度上提取的外观,而不仅仅是在单一尺度上。对于风格损失,Gatys 等人使用了层激活的格拉姆矩阵(Gram matrix), 即某一层特征图的内积。这个内积可以被理解成表示该层特征之间相互关系的映射。
风格损失的目的是在风格参考图像与生成图像之间,在不同的层激活内保存相似的内部相互关系。反过来,这保证了在风格参考图像与生成图像之间,不同空间尺度找到的纹理看起来都很相似。
'''
'''
简而言之,你可以使用预训练的卷积神经网络来定义一个具有以下特点的损失。
1.在目标内容图像和生成图像之间保持相似的较高层激活,从而能够保留内容。卷积神经网络应该能够“看到”目标图像和生成图像包含相同的内容。
2.在较低层和较高层的激活中保持类似的相互关系(correlation),从而能够保留风格。特征相互关系捕捉到的是纹理(texture),生成图像和风格参考图像在不同的空间尺度上应该具有相同的纹理。
'''
#keras实现神经风格迁移
'''
#神经风格迁移可以用任何预训练卷积神经网络来实现。我们这里将使用 Gatys 等人所使用的 VGG19 网络
神经风格迁移的一般过程如下。
(1) 创建一个网络,它能够同时计算风格参考图像、目标图像和生成图像的 VGG19 层激活。
(2) 使用这三张图像上计算的层激活来定义之前所述的损失函数,为了实现风格迁移,需要将这个损失函数最小化。
(3) 设置梯度下降过程来将这个损失函数最小化
'''
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pylab
from pandas import DataFrame, Series
from keras import models, layers, optimizers, losses, metrics
from keras.utils.np_utils import to_categorical
plt.rcParams['font.sans-serif'] = ['SimHei'] #指定默认字体
plt.rcParams['axes.unicode_minus'] = False #解决保存图像是负号'-'显示为方块的问题
#为了确保处理后的图像具有相似的尺寸 (如果图像尺寸差异很大,会使得风格迁移变得更加困难),稍后需要将所有图像的高度调整为400像素
from keras.preprocessing.image import load_img,img_to_array
target_image_path='datasets/lufei.jpeg'
style_reference_image_path='datasets/timg.jpg'
width,height=load_img(target_image_path).size
img_height=400
img_width=int(width*img_height/height)
from keras.applications import vgg19
def preprocess_image(image_path):#处理图像,转化为vgg19接受的输入张量
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
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
# vgg19.preprocess_input的作用是减去ImageNet的平均像素值,使其中心为0。这里相当于vgg19.preprocess_input的逆操作
x = x[:, :, ::-1]#将图像由BGR格式转换为RGB格式。这也是vgg19.preprocess_input 逆操作的一部分
x = np.clip(x, 0, 255).astype('uint8')#裁剪x中元素到指定范围
return x
#加载预训练的VGG19网络。并将其应用于三张图像
'''
它接收三张图像的批量作为输入,三张图像分别是风格参考图像、目标图像和一个用于保存生成图像的占位符。占位符是一个符号张量,它的值由外部Numpy张量提供。风格
参考图像和目标图像都是不变的,因此使用 K.constant 来定义,但生成图像的占位符所包含的值会随着时间而改变。
'''
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)#利用三张图像组成的批量作为输入来构建 VGG19 网络。加载模型将使用预训练的ImageNet权重
model.summary()
#定义内容损失
def content_loss(base,combination):
return K.sum(K.square(combination-base))
#定义风格损失
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表示目标内容更容易在生成图像中被识别出来。
'''
#定义需要最小化的最终损失
outputs_dict=dict([(layer.name,layer.output) for layer in model.layers])#将层的名称映射为激活张量的字典
content_layer='block5_conv2'#用于内容损失的层,top层
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 实现有两个小小的限制。
1.它需要将损失函数值和梯度值作为两个单独的函数传入。
2.它只能应用于展平的向量,而我们的数据是三维图像数组。
分别计算损失函数值和梯度值是很低效的,这里创建一个名为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()
#风格迁移循环
from scipy.optimize import fmin_l_bfgs_b
from scipy.misc import imsave
import time
result_prefix='datasets/outputs/my_result'
iterations=20
x=preprocess_image(target_image_path)#初始状态目标图像
x=x.flatten()#图像展平,scipy.optimize.fmin_l_bfgs_b 只能处理展平的向量
for i in range(iterations):
print('Start of iteration', i)
start_time = time.time()
x,min_val,info=fmin_l_bfgs_b(evaluator.loss,x,fprime=evaluator.grads,maxfun=iterations)#对生成图像的像素运行L-BFGS 最优化,以将神经风格损失最小化。【必须将计算损失的函数和计算梯度的函数作为两个单独的参数传入】
print('Current loss value:', min_val)
img=x.copy().reshape((img_height,img_width,3))#复原梯度优化后的图像x
img=deprocess_image(img)#chnnels为GBR的张量转换为RGB图像,然后保存
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))
原来的两张图(下载自百度)
第19次迭代效果图
'''
它通常无法实现比较抽象的迁移,比如将一幅肖像的风格迁移到另一幅中。这种算法更接近于经典的信号处理,而不是更接近于人工智能,因此不要指望它能实现魔法般的效果。
此外还请注意,这个风格迁移算法的运行速度很慢。但这种方法实现的变换足够简单,只要有适量的训练数据,一个小型的快速前馈卷积神经网络就可以学会这种变换。
因此,实现快速风格迁移的方法是,首先利用这里介绍的方法,花费大量的计算时间对一张固定的风格参考图像生成许多输入 - 输出训练样例,然后训练一个简单的卷积神经网络来学习这个特定风格的变换。
一旦完成之后,对一张图像进行风格迁移是非常快的,只是这个小型卷积神经网络的一次前向传递而已。
'''