《从锅炉工到AI专家(8)》中咱们介绍了一个“图片风格迁移”的例子。由于所引用的做品中使用了TensorFlow 1.x的代码,算法也相对复杂,因此文中没有仔细介绍风格迁移的原理。
今天在TensorFlow 2.0的帮助,和新算法思想的优化下,实现一样功能的代码量大幅减小,结构也愈加清晰。因此今天就来说讲这个话题。python
“风格迁移”指的是将艺术做品的笔触、技法等表现出来的视觉效果,应用在普通照片上,使得所生成的图片,相似使用一样笔触、技法所绘制完成,但内容跟照片相同的“伪画做”。
在神经网络机器学习的帮助下,生成图片的观赏性很是高,远非早期传统方法获得的图片可比。
这里重贴一遍前文中的例图,让咱们有一个更直观的感觉。
首先是一张原程序做者的的自拍照:
接着不陌生,著名大做《星空》:
(请将以上两图保存至工做目录,不要修改文件名,咱们稍晚的代码中会用到。)
两张图片通过程序处理后,会获得一幅新的图片:
即便用《星空》风格模仿的手绘做品《黄粱一梦》:)算法
风格迁移原理基于论文《A Neural Algorithm of Artistic Style》。
虽然论文中并无明说,但采用卷积神经网络作图像的风格迁移应当属于一个实验科学的成果而非单纯的理论研究。
咱们再引用一张前系列讲解CNN时候的图片:
一张图片数据所造成的矩阵,在通过卷积网络的时候,图像中边缘等视觉特征会被放大、强化,从而造成一种特殊的输出。一般咱们只关心数据结果,并无把这些数据还原为图片来观察。而论文做者不只这样作了,恐怕还进行了大量的实验。
这些神经网络中间结果图片具备如此典型的特征,能够脱离出主题内容而成为单纯风格的描述。被敏锐的做者抓住深刻研究也就不奇怪了。
最终研究成果确立了卷积神经网络进行图片迁移的两大基础算法:数组
本系列文章都是尽力不出现数学公式,用代码讲原理。
在《从锅炉工到AI专家(8)》引用的代码中,除了构建神经网络、训练,主要工做是在损失函数下降到满意程度以后,使用网络中间层的输出结果计算、组合成目标图片。原文中对这部分的流程也作了简介。
新的代码来自TensorFlow官方文档。除了程序升级为TensorFlow 2.0原生代码。在图片的产生上也作了大幅创新:使用照片图片训练神经网络,每一阶梯的训练结果,不该用回神经网络(网络的权重参数一直固定锁死的),而把训练结果应用到图片自己。在下一次的训练循环中,使用新的图片再次计算损失值。这样,当损失值最小的时候,训练图片自己就已是符合咱们要求的生成图片。固然本质上,跟前一种方法同样的。但感受上,结构清晰了不少。这个过程对比起来,大量节省了图片生成的计算。固然,主要缘由仍是TensorFlow 2.0内置的tf.linalg.einsum方法强大好用。网络
在特征层的定义上,照片内容的描述使用vgg-19网络的第5部分的第2层卷积输出结果。艺术图片风格特征的描述使用了5个层,分别是vgg-19网络的第1至第5部分第1个网络层的输出结果。在程序中,能够这样描述:app
# 定义最能表明内容特征的网络层 content_layers = ['block5_conv2'] # 定义最能表明风格特征的网络层 style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
网络层的名称来自于vgg-19网络定义完成后,各层的名称。可使用以下代码获得全部层的名称:机器学习
... # 创建无需分类结果的vgg网络 vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') # 显示vgg中全部层的名称 print() for layer in vgg.layers: print(layer.name) ...
一般的模型训练,都是使用代价函数比较网络输出结果,和目标标注值的差别,使得差别逐渐缩小。
本例的训练目标比较复杂,能够描述为两条:函数
虽然这个代价函数略微复杂,不过比VAE的代价函数仍是简单多了:)工具
程序中的注释很是详细。跟之前的程序有一点区别,就是直接使用TensorFlow内置方法读取了图片文件,而后调用jpg解码还原为矩阵。
不过TensorFlow内置的将图像0-255整数值转换为浮点数的过程,会自动将数值变为0-1的浮点小数。
这个过程其实对咱们画蛇添足,由于咱们后续的不少计算都须要转换回0-255。性能
#!/usr/bin/env python3 from __future__ import absolute_import, division, print_function, unicode_literals import tensorflow as tf import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np import time import functools import time from PIL import Image # 设置绘图窗口参数,用于图片显示 mpl.rcParams['figure.figsize'] = (13, 10) mpl.rcParams['axes.grid'] = False # 获取下载后本地图片的路径,content_path是真实照片,style_path是艺术品风格图片 content_path = "1-content.jpg" style_path = "1-style.jpg" # 读取一张图片,并作预处理 def load_img(path_to_img): max_dim = 512 # 读取二进制文件 img = tf.io.read_file(path_to_img) # 作JPEG解码,这时候获得宽x高x色深矩阵,数字0-255 img = tf.image.decode_jpeg(img) # 类型从int转换到32位浮点,数值范围0-1 img = tf.image.convert_image_dtype(img, tf.float32) # 减掉最后色深一维,获取到的至关于图片尺寸(整数),转为浮点 shape = tf.cast(tf.shape(img)[:-1], tf.float32) # 获取图片长端 long = max(shape) # 以长端为比例缩放,让图片成为512x??? scale = max_dim/long new_shape = tf.cast(shape*scale, tf.int32) # 实际缩放图片 img = tf.image.resize(img, new_shape) # 再扩展一维,成为图片数字中的一张图片(1,长,宽,色深) img = img[tf.newaxis, :] return img # 读入两张图片 content_image = load_img(content_path) style_image = load_img(style_path) ############################################################ # 定义最能表明内容特征的网络层 content_layers = ['block5_conv2'] # 定义最能表明风格特征的网络层 style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] # 神经网络层的数量 num_content_layers = len(content_layers) num_style_layers = len(style_layers) # 定义一个工具函数,帮助创建获得特定中间层输出结果的新模型 def vgg_layers(layer_names): """ Creates a vgg model that returns a list of intermediate output values.""" # 定义使用ImageNet数据训练的vgg19网络 vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') # 已经通过了训练,因此锁定各项参数避免再次训练 vgg.trainable = False # 获取所需层的输出结果 outputs = [vgg.get_layer(name).output for name in layer_names] # 最终返回结果是一个模型,输入是图片,输出为所需的中间层输出 model = tf.keras.Model([vgg.input], outputs) return model # 定义函数计算风格矩阵,这实际是由抽取出来的5个网络层的输出计算得来的 def gram_matrix(input_tensor): result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor) input_shape = tf.shape(input_tensor) num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32) return result/(num_locations) # 自定义keras模型 class StyleContentModel(tf.keras.models.Model): def __init__(self, style_layers, content_layers): super(StyleContentModel, self).__init__() # 本身的vgg模型,包含上面所列的风格抽取层和内容抽取层 self.vgg = vgg_layers(style_layers + content_layers) self.style_layers = style_layers self.content_layers = content_layers self.num_style_layers = len(style_layers) # vgg各层参数锁定再也不参数训练 self.vgg.trainable = False def call(self, input): # 输入的图片是0-1范围浮点,转换到0-255以符合vgg要求 input = input*255.0 # 对输入图片数据作预处理 preprocessed_input = tf.keras.applications.vgg19.preprocess_input(input) # 获取风格层和内容层输出 outputs = self.vgg(preprocessed_input) # 输出实际是一个数组,拆分为风格输出和内容输出 style_outputs, content_outputs = ( outputs[:self.num_style_layers], outputs[self.num_style_layers:]) # 计算风格矩阵 style_outputs = [gram_matrix(style_output) for style_output in style_outputs] # 转换为字典 content_dict = {content_name: value for content_name, value in zip(self.content_layers, content_outputs)} # 转换为字典 style_dict = {style_name: value for style_name, value in zip(self.style_layers, style_outputs)} # 返回内容和风格结果 return {'content': content_dict, 'style': style_dict} # 使用自定义模型创建一个抽取器 extractor = StyleContentModel(style_layers, content_layers) # 设定风格特征的目标,即最终生成的图片,但愿风格上尽可能接近风格图片 style_targets = extractor(style_image)['style'] # 设定内容特征的目标,即最终生成的图片,但愿内容上尽可能接近内容图片 content_targets = extractor(content_image)['content'] # 内容图片转换为张量 image = tf.Variable(content_image) # 截取0-1的浮点数,超范围部分被截取 def clip_0_1(image): return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0) # 优化器 opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1) # 预约义风格和内容在最终结果中的权重值,用于在损失函数中计算总损失值 style_weight = 1e-2 content_weight = 1e4 # 损失函数 def style_content_loss(outputs): style_outputs = outputs['style'] content_outputs = outputs['content'] # 风格损失值,就是计算方差 style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()]) # 权重值平均到每层,计算整体风格损失值 style_loss *= style_weight/num_style_layers # 内容损失值,也是计算方差 content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()]) content_loss *= content_weight/num_content_layers # 总损失值 loss = style_loss+content_loss return loss ################################################################ # 一次训练 @tf.function() def train_step(image): with tf.GradientTape() as tape: # 抽取风格层、内容层输出 outputs = extractor(image) # 计算损失值 loss = style_content_loss(outputs) # 梯度降低 grad = tape.gradient(loss, image) # 应用计算后的新参数,注意这个新值不是应用到网络 # 做为训练完成的vgg网络,其参数前面已经设定不可更改 # 这个参数实际将应用于原图 # 以求取,新图片通过网络后,损失值最小 opt.apply_gradients([(grad, image)]) # 更新图片,用新图片进行下次训练迭代 image.assign(clip_0_1(image)) start = time.time() epochs = 10 steps_per_epoch = 100 step = 0 for n in range(epochs): for m in range(steps_per_epoch): step += 1 train_step(image) print(".", end='') print("") # 每100次迭代显示一次图片 # imshow(image.read_value()) # plt.title("Train step: {}".format(step)) # plt.show() end = time.time() print("Total time: {:.1f}".format(end-start)) ######################################## #保存结果图片 file_name = 'newart1.png' mpl.image.imsave(file_name, image[0])
程序的输出结果以下图:
看起来基本达到了设计要求,不过再仔细观察,彷佛效果虽然都有了,但画面看上去有一点不干净,有不少小的噪点甚至有了干涉纹。
这是由于,在照片原图和艺术做品原图中,确定自然就存在有噪点以及图片中自己应当有的小而频繁的花纹。这些内容在经过卷积增强后,两幅照片再叠加,这些噪声就被强化了,从而在生成的图片中体现的很是明显。
这个问题若是在传统算法中可使用高通滤波。在卷积神经网络中则更容易,是统计整体变分损失值(Total Variation Loss),在代价函数中,让这个损失值降到最小,就抑制了这种噪点的产生。也至关于神经网络具备了降噪的效果。
变分损失是计算图片中,在X方向及Y方向,相邻像素的差值。若是像素差异不大,那差确定很小甚至趋近于0。若是差异大,固然差值就大。
请使用下面的代码,替换上面程序中训练的部分:学习
################################################### # 计算x方向及y方向相邻像素差值,若是有高频花纹,这个值确定会高, # 由于相邻点相同差值接近0,区别越大,差值固然越大 def high_pass_x_y(image): x_var = image[:, :, 1:, :] - image[:, :, :-1, :] y_var = image[:, 1:, :, :] - image[:, :-1, :, :] return x_var, y_var # 计算整体变分损失 def total_variation_loss(image): x_deltas, y_deltas = high_pass_x_y(image) return tf.reduce_mean(x_deltas**2)+tf.reduce_mean(y_deltas**2) # 整体变分损失值在损失值中所占权重 total_variation_weight = 1e8 # 一次训练 @tf.function() def train_step(image): with tf.GradientTape() as tape: # 抽取风格层、内容层输出 outputs = extractor(image) # 计算损失值 loss = style_content_loss(outputs) loss += total_variation_weight*total_variation_loss(image) # 梯度降低 grad = tape.gradient(loss, image) # 应用计算后的新参数,注意这个新值不是应用到网络 # 做为训练完成的vgg网络,其参数前面已经设定不可更改 # 这个参数实际将应用于原图 # 以求取,新图片通过网络后,损失值最小 opt.apply_gradients([(grad, image)]) # 更新图片,用新图片进行下次训练迭代 image.assign(clip_0_1(image)) # 内容图片做为逐步迭代生成的新图片,一开始固然是原图,这里是转换为张量 image = tf.Variable(content_image) start = time.time() # 迭代10次,每次100步训练 epochs = 10 steps = 100 step = 0 for n in range(epochs): for m in range(steps): step += 1 train_step(image) print(".", end='') print("") end = time.time() print("Total time: {:.1f}".format(end-start)) #保存结果图片 file_name = 'newart1.png' mpl.image.imsave(file_name, image[0])
再次执行,所获得的输出图片以下:
效果不错吧?能够换上本身的照片还有本身心仪的艺术做品来试试。
程序中限制了图片宽、高最大值是512,若是设备性能比较好,或者有更大尺寸的需求,能够修改程序中的常量。
(待续...)