Tensorflow化骨绵掌第6式-使用VGG19进行图片风格迁移
经过前面的Tensorflow的学习知识,我们了解许多基本的操作,从基本的数据收集、数据集制作、如何读取图片数据、如何处理数据、神经网络Lenet-5、AlexNet、VGG16等的构建、如何建立图片队列等。然后还做了一些简单的应用,比如识别手写数据集、识别花卉图片、识别CIFAR数据集图片等,这些都是应用在图像分类上。这期我们学习一下如何利用VGG19进行风格迁移,也是处理另外一种图像的方式。下图是进行风格迁移的原图和效果图。
在学习神经风格迁移,我们可以先去阅读论文进行深入的了解,在进行总结它的具体用了什么方法、具体的操作是怎样应用的。论文下载地址:https://arxiv.org/pdf/1508.06576v1.pdf
1、神经风格迁移的介绍
深度学习可以捕获一个图像的内容并将其与另一个图像的风格相结合,这种技术叫做神经风格迁移。但是,神经风格迁移是如何运作的呢?在这篇文章中,我们将研究神经风格迁移(NST)的基本机制。
我们可以看到,生成的图像具有内容图像的内容和风格图像的风格。可以看出,仅通过重叠图像不能获得上述结果。我们是如何确保生成的图像具有内容图像的内容和风格图像的风格呢?
2、卷积神经网络得到的是什么
现在,在第1层使用32个filters ,网络可以捕捉简单的模式,比如直线或水平线,这对我们可能没有意义,但对网络非常重要,慢慢地,当我们到第2层,它有64个filters ,网络开始捕捉越来越复杂的特征,它可能是一张狗的脸或一辆车的*。这种捕获不同的简单特征和复杂特征称为特征表示。
这里需要注意的是,卷积神经网络(CNN)并不知道图像是什么,但他们学会了编码特定图像所代表的内容。卷积神经网络的这种编码特性可以帮助我们实现神经风格迁移。
对于内容重建来说,用了原始网络的五个卷积层,‘conv1_1’ (a), ‘conv2_1’ (b), ‘conv3_1’ ©, ‘conv4_1’ (d) and ‘conv5_1’ (e),即图下方中的a、b、c、d、e。VGG 网络主要用来做内容识别,在实践中作者发现,使用前三层a、b、c已经能够达到比较好的内容重建工作,d、e两层保留了一些比较高层的特征,丢失了一些细节。
VGG19是Google DeepMind发表在ICLR 2015上的论文《VERY DEEP CONVOLUTIONAL NETWORK SFOR LARGE-SCALE IMAGE RECOGNITION》中提出的一种DCNN结构。
众所周知,CNN在图片处理上表现良好,VGG19提出后,也被用在图像处理上。我这里要用到的VGG19模型就是在imagenet数据集上预训练的模型。
一般认为,深度卷积神经网络的训练是对数据集特征的一步步抽取的过程,从简单的特征,到复杂的特征。
训练好的模型学习到的是对图像特征的抽取方法,所以在imagenet数据集上训练好的模型理论上来说,也可以直接用于抽取其他图像的特征,这也是迁移学习的基础。自然,这样的效果往往没有在新数据上重新训练的效果好,但能够节省大量的训练时间,在特定情况下非常有用。
对于风格重建,用了卷积层的不同子集:
‘conv1_1’ (a),
‘conv1_1’ and ‘conv2_1’ (b),
‘conv1_1’, ‘conv2_1’ and ‘conv3_1’ ©,
‘conv1_1’, ‘conv2_1’ , ‘conv3_1’and ‘conv4_1’ (d),
‘conv1_1’, ‘conv2_1’ , ‘conv3_1’, ‘conv4_1’and ‘conv5_1’ (e)
3、卷积神经网络如何用于捕获图像的内容和风格
VGG19网络用于神经风格迁移。VGG-19是一个卷积神经网络,可以对ImageNet数据集中的一百多万个图像进行训练。该网络深度为19层,并在数百万张图像上进行了训练。因此,它能够检测图像中的高级特征。现在,CNN的这种“编码性质”是神经风格迁移的关键。首先,我们初始化一个噪声图像,它将成为我们的输出图像(G)。然后,我们计算该图像与网络中特定层(VGG网络)的内容和风格图像的相似程度。由于我们希望输出图像(G)应该具有内容图像(C)的内容和风格图像(S)的风格,因此我们计算生成的图像(G)的损失,即到相应的内容(C)和风格( S)图像的损失。有了上述直觉,让我们将内容损失和风格损失定义为随机生成的噪声图像。
4、内容损失计算
计算内容损失意味着随机生成的噪声图像(G)与内容图像©的相似性。为了计算内容损失:
假设我们在一个预训练网络(VGG网络)中选择一个隐藏层(L)来计算损失。因此,设P和F为原始图像和生成的图像。其中,F[l]和P[l]分别为第l层图像的特征表示。现在,内容损失定义如下:
5、风格损失计算
在计算风格损失之前,让我们看看“ 图像风格 ”的含义或我们如何捕获图像风格。
这张图片显示了特定选定层的不同通道或特征映射或filters。现在,为了捕捉图像的风格,我们将计算这些filters之间的“相关性”,也就是这些特征映射的相似性。但是相关性是什么意思呢?
让我们借助一个例子来理解它:
上图中的前两个通道是红色和黄色。假设红色通道捕获了一些简单的特征(比如垂直线),如果这两个通道是相关的,那么当图像中有一条垂直线被红色通道检测到时,第二个通道就会产生黄色的效果。
现在,让我们看看数学上是如何计算这些相关性的。为了计算不同filters或信道之间的相关性,我们计算两个filters激活向量之间的点积。由此获得的矩阵称为Gram矩阵。但是我们如何知道它们是否相关呢?
如果两个filters激活之间的点积大,则说两个通道是相关的,如果它很小则是不相关的。以数学方式:
风格图像的Gram矩阵(S):
这里k '和k '表示层l的不同filters或通道。我们将其称为Gkk ’ [l][S]。
现在,我们可以定义风格损失:
风格与生成图像的成本函数是风格图像的Gram矩阵与生成图像的Gram矩阵之差的平方。
6、风格迁移的总损失函数
总是内容和风格图像的成本之和。在数学上,它可以表示为:
您可能已经注意到上述等式中的Alpha和beta。它们分别用于衡量内容成本和风格成本。通常,它们在生成的输出图像中定义每个成本的权重。
一旦计算出损失,就可以使用反向传播使这种损失最小化,反向传播又将我们随机生成的图像优化为有意义的艺术品。
7、神经风格迁移操作过程
(1)使用VGG中一些层用来表示图片的内容特征和风格特征。
# 定义计算内容损失的vgg层名称及对应权重的列表
CONTENT_LOSS_LAYERS = [('conv4_2', 0.5),('conv5_2',0.5)]
# 定义计算风格损失的vgg层名称及对应权重的列表
STYLE_LOSS_LAYERS = [('conv1_1', 0.2), ('conv2_1', 0.2), ('conv3_1', 0.2), ('conv4_1', 0.2), ('conv5_1', 0.2)]
(2)将内容图片输入网络,计算内容图片在网络指定层上输出的值。
(3)计算内容损失。
(4)将风格图片输入网络,计算风格图片在网络指定层上输出的值。
(5)计算风格损失。
(6)计算训练的总损失。
(7)当训练开始时,我们根据内容图片和噪声,生成一张噪声图片。并将噪声图片喂给网络,计算loss,再根据loss调整噪声图片。将调整后的图片喂给网络,重新计算loss,再调整,再计算…直到达到指定迭代次数,此时,噪声图片已兼具内容图片的内容和风格图片的风格,进行保存即可。
8、神经风格迁移编程代码
(1)切记,一定得先下载VGG19已训练好的模型。
地址:www.vlfeat.org/matconvnet/models/beta16/imagenet-vgg-verydeep-19.mat
(2)编写网络参数设定
setting.py
#coding:utf-8
# 内容图片路径
CONTENT_IMAGE = 'D:/biancheng/pythonCODE/PythonLearn/DeepLearning/images/bijiasuo.jpg'
# 风格图片路径
STYLE_IMAGE = 'D:/biancheng/pythonCODE/PythonLearn/DeepLearning/images/style.jpg'
# 输出图片路径
OUTPUT_IMAGE = 'D:/biancheng/pythonCODE/PythonLearn/DeepLearning/output/out'
# 预训练的vgg模型路径
VGG_MODEL_PATH = 'D:/biancheng/pythonCODE/PythonLearn/DeepLearning/imagenet-vgg-verydeep-19.mat'
# 图片宽度
IMAGE_WIDTH = 450
# 图片高度
IMAGE_HEIGHT = 300
# 定义计算内容损失的vgg层名称及对应权重的列表
CONTENT_LOSS_LAYERS = [('conv4_2', 0.5),('conv5_2',0.5)]
# 定义计算风格损失的vgg层名称及对应权重的列表
STYLE_LOSS_LAYERS = [('conv1_1', 0.2), ('conv2_1', 0.2), ('conv3_1', 0.2), ('conv4_1', 0.2), ('conv5_1', 0.2)]
# 噪音比率
NOISE = 0.5
# 图片RGB均值
IMAGE_MEAN_VALUE = [128.0, 128.0, 128.0]
# 内容损失权重
ALPHA = 1
# 风格损失权重
BETA = 500
# 训练次数
TRAIN_STEPS = 3000
(3)导入图片,编写网络结构代码
models.py
#coding:utf-8
import tensorflow as tf
import numpy as np
import settings
import scipy.io
import scipy.misc
class Model(object):
def __init__(self, content_path, style_path):
self.content = self.loadimg(content_path) # 加载内容图片
self.style = self.loadimg(style_path) # 加载风格图片
self.random_img = self.get_random_img() # 生成噪音内容图片
self.net = self.vggnet() # 建立vgg网络
def vggnet(self):
# 读取预训练的vgg模型
vgg = scipy.io.loadmat(settings.VGG_MODEL_PATH)
vgg_layers = vgg['layers'][0]
net = {}
# 使用预训练的模型参数构建vgg网络的卷积层和池化层
# 全连接层不需要
# 注意,除了input之外,这里参数都为constant,即常量
# 和平时不同,我们并不训练vgg的参数,它们保持不变
# 需要进行训练的是input,它即是我们最终生成的图像
net['input'] = tf.Variable(np.zeros([1, settings.IMAGE_HEIGHT, settings.IMAGE_WIDTH, 3]), dtype=tf.float32)
# 参数对应的层数可以参考vgg模型图
net['conv1_1'] = self.conv_relu(net['input'], self.get_wb(vgg_layers, 0))
net['conv1_2'] = self.conv_relu(net['conv1_1'], self.get_wb(vgg_layers, 2))
net['pool1'] = self.pool(net['conv1_2'])
net['conv2_1'] = self.conv_relu(net['pool1'], self.get_wb(vgg_layers, 5))
net['conv2_2'] = self.conv_relu(net['conv2_1'], self.get_wb(vgg_layers, 7))
net['pool2'] = self.pool(net['conv2_2'])
net['conv3_1'] = self.conv_relu(net['pool2'], self.get_wb(vgg_layers, 10))
net['conv3_2'] = self.conv_relu(net['conv3_1'], self.get_wb(vgg_layers, 12))
net['conv3_3'] = self.conv_relu(net['conv3_2'], self.get_wb(vgg_layers, 14))
net['conv3_4'] = self.conv_relu(net['conv3_3'], self.get_wb(vgg_layers, 16))
net['pool3'] = self.pool(net['conv3_4'])
net['conv4_1'] = self.conv_relu(net['pool3'], self.get_wb(vgg_layers, 19))
net['conv4_2'] = self.conv_relu(net['conv4_1'], self.get_wb(vgg_layers, 21))
net['conv4_3'] = self.conv_relu(net['conv4_2'], self.get_wb(vgg_layers, 23))
net['conv4_4'] = self.conv_relu(net['conv4_3'], self.get_wb(vgg_layers, 25))
net['pool4'] = self.pool(net['conv4_4'])
net['conv5_1'] = self.conv_relu(net['pool4'], self.get_wb(vgg_layers, 28))
net['conv5_2'] = self.conv_relu(net['conv5_1'], self.get_wb(vgg_layers, 30))
net['conv5_3'] = self.conv_relu(net['conv5_2'], self.get_wb(vgg_layers, 32))
net['conv5_4'] = self.conv_relu(net['conv5_3'], self.get_wb(vgg_layers, 34))
net['pool5'] = self.pool(net['conv5_4'])
return net
def conv_relu(self, input, wb):
"""
进行先卷积、后relu的运算
:param input: 输入层
:param wb: wb[0],wb[1] == w,b
:return: relu后的结果
"""
conv = tf.nn.conv2d(input, wb[0], strides=[1, 1, 1, 1], padding='SAME')
relu = tf.nn.relu(conv + wb[1])
return relu
def pool(self, input):
"""
进行max_pool操作
:param input: 输入层
:return: 池化后的结果
"""
return tf.nn.max_pool(input, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
def get_wb(self, layers, i):
"""
从预训练好的vgg模型中读取参数
:param layers: 训练好的vgg模型
:param i: vgg指定层数
:return: 该层的w,b
"""
w = tf.constant(layers[i][0][0][0][0][0])
bias = layers[i][0][0][0][0][1]
b = tf.constant(np.reshape(bias, (bias.size)))
return w, b
def get_random_img(self):
"""
根据噪音和内容图片,生成一张随机图片
:return:
"""
noise_image = np.random.uniform(-20, 20, [1, settings.IMAGE_HEIGHT, settings.IMAGE_WIDTH, 3])
random_img = noise_image * settings.NOISE + self.content * (1 - settings.NOISE)
return random_img
def loadimg(self, path):
"""
加载一张图片,将其转化为符合要求的格式
:param path:
:return:
"""
# 读取图片
image = scipy.misc.imread(path)
# 重新设定图片大小
image = scipy.misc.imresize(image, [settings.IMAGE_HEIGHT, settings.IMAGE_WIDTH])
# 改变数组形状,其实就是把它变成一个batch_size=1的batch
image = np.reshape(image, (1, settings.IMAGE_HEIGHT, settings.IMAGE_WIDTH, 3))
# 减去均值,使其数据分布接近0
image = image - settings.IMAGE_MEAN_VALUE
return image
if __name__ == '__main__':
Model(settings.CONTENT_IMAGE, settings.STYLE_IMAGE)
(4)开始训练,进行风格迁移
# -*- coding: utf-8 -*-
import tensorflow as tf
import settings
import models
import numpy as np
import scipy.misc
def loss(sess, model):
"""
定义模型的损失函数
:param sess: tf session
:param model: 神经网络模型
:return: 内容损失和风格损失的加权和损失
"""
# 先计算内容损失函数
# 获取定义内容损失的vgg层名称列表及权重
content_layers = settings.CONTENT_LOSS_LAYERS
# 将内容图片作为输入,方便后面提取内容图片在各层中的特征矩阵
sess.run(tf.assign(model.net['input'], model.content))
# 内容损失累加量
content_loss = 0.0
# 逐个取出衡量内容损失的vgg层名称及对应权重
for layer_name, weight in content_layers:
# 提取内容图片在layer_name层中的特征矩阵
p = sess.run(model.net[layer_name])
# 提取噪音图片在layer_name层中的特征矩阵
x = model.net[layer_name]
# 长x宽
M = p.shape[1] * p.shape[2]
# 信道数
N = p.shape[3]
# 根据公式计算损失,并进行累加
content_loss += (1.0 / (2 * M * N)) * tf.reduce_sum(tf.pow(p - x, 2)) * weight
# 将损失对层数取平均
content_loss /= len(content_layers)
# 再计算风格损失函数
style_layers = settings.STYLE_LOSS_LAYERS
# 将风格图片作为输入,方便后面提取风格图片在各层中的特征矩阵
sess.run(tf.assign(model.net['input'], model.style))
# 风格损失累加量
style_loss = 0.0
# 逐个取出衡量风格损失的vgg层名称及对应权重
for layer_name, weight in style_layers:
# 提取风格图片在layer_name层中的特征矩阵
a = sess.run(model.net[layer_name])
# 提取噪音图片在layer_name层中的特征矩阵
x = model.net[layer_name]
# 长x宽
M = a.shape[1] * a.shape[2]
# 信道数
N = a.shape[3]
# 求风格图片特征的gram矩阵
A = gram(a, M, N)
# 求噪音图片特征的gram矩阵
G = gram(x, M, N)
# 根据公式计算损失,并进行累加
style_loss += (1.0 / (4 * M * M * N * N)) * tf.reduce_sum(tf.pow(G - A, 2)) * weight
# 将损失对层数取平均
style_loss /= len(style_layers)
# 将内容损失和风格损失加权求和,构成总损失函数
loss = settings.ALPHA * content_loss + settings.BETA * style_loss
return loss
def gram(x, size, deep):
"""
创建给定矩阵的格莱姆矩阵,用来衡量风格
:param x:给定矩阵
:param size:矩阵的行数与列数的乘积
:param deep:矩阵信道数
:return:格莱姆矩阵
"""
# 改变shape为(size,deep)
x = tf.reshape(x, (size, deep))
# 求xTx
g = tf.matmul(tf.transpose(x), x)
return g
def train():
# 创建一个模型
model = models.Model(settings.CONTENT_IMAGE, settings.STYLE_IMAGE)
# 创建session
with tf.Session() as sess:
# 全局初始化
sess.run(tf.global_variables_initializer())
# 定义损失函数
cost = loss(sess, model)
# 创建优化器
optimizer = tf.train.AdamOptimizer(1.0).minimize(cost)
# 再初始化一次(主要针对于第一次初始化后又定义的运算,不然可能会报错)
sess.run(tf.global_variables_initializer())
# 使用噪声图片进行训练
sess.run(tf.assign(model.net['input'], model.random_img))
# 迭代指定次数
for step in range(settings.TRAIN_STEPS):
# 进行一次反向传播
sess.run(optimizer)
# 每隔一定次数,输出一下进度,并保存当前训练结果
if step % 50 == 0:
print('step {} is down.'.format(step))
# 取出input的内容,这是生成的图片
img = sess.run(model.net['input'])
# 训练过程是减去均值的,这里要加上
img += settings.IMAGE_MEAN_VALUE
# 这里是一个batch_size=1的batch,所以img[0]才是图片内容
img = img[0]
# 将像素值限定在0-255,并转为整型
img = np.clip(img, 0, 255).astype(np.uint8)
# 保存图片
scipy.misc.imsave('{}-{}.jpg'.format(settings.OUTPUT_IMAGE,step), img)
# 保存最终训练结果
img = sess.run(model.net['input'])
img += settings.IMAGE_MEAN_VALUE
img = img[0]
img = np.clip(img, 0, 255).astype(np.uint8)
scipy.misc.imsave('{}.jpg'.format(settings.OUTPUT_IMAGE), img)
if __name__ == '__main__':
train()
训练图如下所示:
输入一张图片:
输出
参考博客、文献:
1、https://baijiahao.baidu.com/s?id=1628984110488737495&wfr=spider&for=pc
2、https://blog.csdn.net/a595130080/article/details/79182843
3、https://blog.csdn.net/qq_37172182/article/details/88135181
4、https://blog.csdn.net/aaronjny/article/details/79681080
5、《VERY DEEP CONVOLUTIONAL NETWORK SFOR LARGE-SCALE IMAGE RECOGNITION》
作业:
1、写神经风格迁移
2、
3、
4、
本期学习到此结束,欢迎各位看官关注、批评指正。