神经风格迁移是生成式深度学习中较为奇妙的项目之一,可以通过卷积神经网络模型中某一层(或多层)所学习到的输出(学习到的特征)进行整合实现。如果想更深入的了解卷积神经网络每层的输出的内容,可以看看我的上一篇博客(内容通俗易懂)“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)_新手村霸的博客-CSDN博客。接下来就让我们走进卷积神经网络的艺术世界吧
目录
2.1创建一个网络,他能同时计算(获得)风格参考图像, 目标图像,生成图像的VGG19对应的层激活:
2.1.3加载预训练的VGG19模型,输入三张准备好的图片:
2.2我们用(1)中获得的激活来定义损失函数(损失函数的大致内容就和上面描述的一样)。我们的目标就是将其最小化:
神经风格迁移:将参考图像的风格应用于目标图像,同时保留目标图像的内容。简单的一句话,进行这篇博客的总结。如下图所示(相信我,你做出来的会好看很多哈哈哈):
生成的图片(最右侧)中保留了第一张图片的内容和第二张图片的风格。在这里的风格所指的就是图片中的纹理、颜色、和视觉图案,内容则是指图像中的高级宏伟结构。接下来就让我们看看如何通过卷积神经网络实现这一奇妙的创作吧。
1.了解构造损失函数的思路(就等于了解了大致流程):
其实神经风格迁移的背后的相关概念和深度学习的核心算法是一样的,我们需要定义一个损失函数,让后将其最小化(通过梯度下降的方式,如果想深入了解梯度下降可以参考我的这篇博客梯度下降法原理解析(大白话+公式推理)_新手村霸的博客-CSDN博客)。我们的目标是保存原始图像的内容和参考风格图像中的风格。那么通过目标我们就能得出如下损失函数的基本概念:
loss = distance(style(reference_image) - style(generated_image)) +
distance(content(original_image) - content(generated_image))
简单解释就是 损失 = 风格(参考风格图片—生成图片) + 内容(原始图片— 生成图片)。
1.1内容损失:
我们知道在网络中更靠近顶层的输出(激活)包含图像更加全局、更加抽象的信息(如羽毛,眼睛,房屋等)。这也就对应上了原始图片上的内容信息。因此,内容损失很好的选择就是原始图像和生成图像在网络中较为顶层的输出(激活)的L2范数(也就是用原始图像在较为顶层中的输出和生成图像在较为顶层中的输出进行比较)。
1.2 风格损失:
相反,由纹理、颜色、视觉图案所表示的风格就在网络中较为底层的输出中。但是风格在网络中分布的较为广泛(风格是多尺度的),我们单单选择一层是不够的(内容损失就可以选择一层在稍微顶层的层能实现)。对于风格损失,在这里我们将使用层激活的格拉姆矩阵,即特征图的内积(如果不了解也没关系,后面代码会用大白话解析,这里混个耳熟即可)。这个内积可以被理解为一层中特征图之间的映射关系,也就是抓住了图片特征的规律(就是我们想索取的风格),我们可以在它身上找到我们想要的纹理外观。
阶段总结:
在网络的较顶层中找到要保留的原始图片中的内容,在较低层和较高层(主要是低层)找寻我们要保留的风格图像中的风格。
2用Keras实现神经风格迁移:
我们使用VGG19(notop版)来简单实现神经风格迁移的操作。基本思路如下,这也是接下来文章的主要结构:
(1)创建一个网络,他能同时计算(获得)风格参考图像, 目标图像,生成图像的VGG19对应的层激活。
(2)我们用(1)中获得的激活来定义损失函数(损失函数的大致内容就和上面描述的一样)。我们的目标就是将其最小化。
(3)设置梯度下降来将损失函数最小化。
2.1创建一个网络,他能同时计算(获得)风格参考图像, 目标图像,生成图像的VGG19对应的层激活:
2.1.1定义初始化变量:
代码如下:
from keras.preprocessing.image import load_img, img_to_array
target_image_path = '/media/hjl/Ubuntu 20.0/zzzz.jpg' # 原始图像路径(内容来源)
style_reference_image_path = '/media/hjl/Ubuntu 20.0/lll.jpg' # 参考风格图像
width, height = load_img(target_image_path).size
img_height = 400 # 固定(统一)图像高
img_width = int(width * img_height / height) # 通过固定的高按比例缩放宽
2.1.2两个处理数据的辅助函数:
在导入VGG19网络之前我们来定义一些辅助函数,用于对输入VGG19网络的图片数据作预处理(图像变张量),和VGG19输出的数据进行后处理(张量变图像)代码如下:
import numpy as np
from keras.applications import vgg19
# 将图像转化为VGG19可以识别的数据张量:
def preprocess_image(image_path):
# 导入图片
img = load_img(image_path, target_size=(img_height, img_width))
# 将图片变为张量数据
img = img_to_array(img)
# 因为Keras中VGG19处理图片数据是4维张量,所以我们要多加一个维度在最前面,也就是输入数据的批量
img = np.expand_dims(img,axis=0)
# 这里经过两个处理,1.将数据的RGB转变为BGR。2.每一层减去像素(BGR)自身的平均数mean(为了方便处理)
img = vgg19.preprocess_input(img)
return img
# 将输出数据转化为图片(和上面的操作相反即可)
def deprocess_image(x):
# 以下三步是分别加回像素(BGR)的平均值
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
# 这一步就是将BGR转换回RGB
x = x[:, :, ::-1]
# 控制数值在0-255之间
x = np.clip(x, 0, 255).astype('uint8')
return x
2.1.3加载预训练的VGG19模型,输入三张准备好的图片:
这里先做个简单的说明,因为输入的目标图像(内容来源)和风格参考图像(风格来源)在网络中就像常量一样,我们只负责提取,不用对他们作出改变。所以存放在K.constant()中,而生成的图片(艺术照)则是在梯度下降的过程中不断改善,是变化的。所以我们将它存放在K.placeholder()中(一个可变的生成占位符)。代码如下:
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))
# 我们将三组张量组成的批量放在一起输入VGG19网络,注意放入的顺序,后面会用到哈。
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.')
这样我们就完成了风格迁移的第一大步啦!都是比较常规的操作。
2.2我们用(1)中获得的激活来定义损失函数(损失函数的大致内容就和上面描述的一样)。我们的目标就是将其最小化:
2.2.1内容损失:
这种方式只是一种输入损失函数比较好的表示方式(不用纠结为什么这么做,是比较通用的表示方式)。
def content_loss(base, combination):
return K.sum(K.square(combination - base)) # 将张量对应的元素求平方再加起来
2.2.2风格损失:
这里就会用到刚才提到的格拉姆矩阵了,这里提前解释一下哈(大白话), 先了解以下这三个函数(很好理解)
(1)K.permute_dimensions(x, (2, 0, 1)):假设x是一个张量,那么(2, 0,1)对应的就是这个张量要改变成的索引位置,例如:x的张量形状为(14,14,512)就变为(512, 14, 14),将索引为2的放到最前面。
(2)K.batch_flatten(x):这里就是将x扁平化的操作,将x拍扁成2阶张量。比如x的形状为(512,14,14),就变成(512,14*14)。
(3)K.transpose(x):求x张量的转置
如果了解了上面的函数,就知道格拉姆矩阵的求法啦~,接下来上代码:
# 这个就是格拉姆矩阵的求法啦,是不是看完上面的函数就明白啦
def gram_maxtrix(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_maxtrix(style)
# 生成艺术照的格拉姆矩阵
C = gram_maxtrix(combination)
# RGB三色通道
channels = 3
size = img_height * img_width
# 这个返回值是这篇论文内结论,没有给出推倒,有可能是论文作者的经验或者尝试得到的最佳风格损失返回值
return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))
2.2.3总变差损失(选作):
这个损失在之前没有提到,因为这个损失的目的是促使生成的艺术照更具有空间连续性(也就是更丝滑),算是锦上添花的操作吧。可以简单了解一下哈,也不复杂,一张辅助图片就看懂啦。
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))
也就是将输入的张量x 进行上图的运算。这里其实就是相邻像素之间的相减发现了吗?只要将这个损失减小,那么相邻两个像素之间的变化就减小,那么也就变化的不会那么突兀啦。
2.2.4将所有的损失合并:
代码比较长,但是很好理解。我会每一步都做解析的。
# 首先将每一层的名字和对应的输出组成字典的形式
outputs_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为0
loss = K.variable(0.)
# 以下几行代码是将内容损失添加到loss之中
layer_features = outputs_dict[content_layer]
# 这里的[0, :, :, :]就是之前上文说要注意放入输入数据顺序的地方,0号索引对应的就是target_image的内容
target_image_features = layer_features[0, :, :, :]
# 同理
combination_features = layer_features[2, :, :, :]
loss = loss + content_weight * content_loss(target_image_features, combination_features)
# 接下来几行代码就是将风格损失放入loss中,过程和上面的内容损失是一样的
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 = loss + (style_weight / len(style_layers)) * sl
# 这里是将总变差损失放入loss中
loss = loss + total_variation_loss(combination_image) * total_variation_weight
终于完成第二大步啦各位,希望各位能耐心看。通过代码解读后是很好理解的啦。
2.3设置梯度下降来将损失函数最小化(快要结束啦):
这里我们使用L-BFGS算法进行优化(论文中就是用这个)。但是它有两个小小的约束:
(1)它需要将损失函数值和梯度值作为两个单独的函数传入(但是按照Keras习惯性操作是K.function(x,[loss, grads])。)
(2)它只能用于展平的向量,但是我们的数据是三维的图像数组。
这两个问题我们逐一解决。
(1)用flatten()函数展平输入数据解决第一个问题啦。
(2)我们将自己创建一个python类,取名为Evaluator。这个类能实现同时计算loss和grad,然后就能一起传入L-BFGS算法进行优化啦
接下来我们创建自己的Evaluator类,代码如下:
import tensorflow as tf
# 这行代码是因为我直接使用gradients函数会报错,如果各位也会再加上吧
tf.compat.v1.disable_eager_execution()
# 创建grads梯度值
grads = K.gradients(loss, combination_image)[0]
# 将损失值和梯度值相关联并创建函数(输入combination_image,返回loss, grads)
fetch_loss_and_grads = K.function([combination_image], [loss, grads])
# 创建类
class Evaluator(object):
def __init__(self):
self.loss_value = None
self. grad_values = None
# 计算loss和grad
def loss(self, x):
assert self.loss_value is None
# 先将展平的数据还原,用于计算loss和grads
x = x.reshape((1, img_height, img_width, 3))
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
#将grad展平,因为输入数据x是三维所以梯度也是三维的,要展平才能输入L-BFGS
grad_values = outs[1].flatten().astype('float64')
self.loss_value = loss_value
self.grad_values = grad_values
return self.loss_value
# 这里是缓存这一轮的grad,下面使用时就明白了
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
# 这里改为none说明只记录当前轮次
self.loss_value = None
self.grad_values = None
return grad_values
#创建实例
evaluator = Evaluator()
最后一步啦,写主程序,就是之前的代码进行拼接。也是体现流程的地方啦
2.4主程序,总流程(拼接代码,没有难点):
这里我们每轮迭代会进行20次梯度上升,所以最后会生成20张艺术照。
# 导入L-BFGS
from scipy.optimize import fmin_l_bfgs_b
# 这里我使用SciPy会报错,所以我使用这个库
import imageio
import time
# 这个是生成艺术照的名字的开头
result_prefix = 'My_result'
# 进行20次梯度上升
iterations = 20
# 图片预处理,这个就是之前创建的辅助函数之一啦
x = preprocess_image(target_image_path)
# 展平,为了输入L-BFGS中
x = x.flatten()
for i in range(iterations):
print('Start of iterations', i)
start_time = time.time()
# 这里就用到我们创建的类啦,看到这里也就明白为什么要缓存grad了
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 + '%d_.png' % i
imageio.imwrite(fname, img)
end_time = time.time()
print(i, end_time - start_time)
好啦,到这里本篇博客就结束啦。希望看到这里的你能亲自动手实验一下哈。图片选的好得到的艺术照片也就越好看哈哈哈。你可以尝试改变我选取的风格的网络对应的层,也就是style_layers,会得到不同的效果。这里比较建议使用GPU(CPU的话大概要1个小时了都,生成20张图片的话)。
希望这篇博客能给你带来收获哈,期待你的一键三连哈~