卷积神经网络:(三)风格迁移——代码部分
引言
本文是在第一步配置完环境后基础上运行的。使用的为系统直装的python环境(在anaconda环境下一样适用,后面注意的点会提示的。)。
友情提示:风格迁移跑的时间会很长。有点耐心哦。
若想查看环境配置步骤,请点击https://blog.csdn.net/weixin_41108515/article/details/103636284,
想知道原理,请点击https://blog.csdn.net/weixin_41108515/article/details/103650964
转载请注明出处:https://blog.csdn.net/weixin_41108515/article/details/103651784
这里引用的是:
https://blog.csdn.net/aaronjny/article/details/79681080
http://zh.gluon.ai/chapter_computer-vision/neural-style.html
这两篇都非常详细,并且经调试可以使用,但是第二个并未使用tensorflow。
以及理论帮助的一篇https://juejin.im/post/5d29e818e51d454f73356de0
第一部分 :简介
主要操作以这部分为主,这篇引用的是tensorflow练手项目三。可以通过点击查看,代码也是所有我调试过的里面较简洁的一个,能够实现基本功能。这里只是我添加了一些了解,以及操作步骤,便于新手理解。
所谓风格迁移就是两张图片,你有你的风格,我有我的内容。你用你的油画
风格将我的内容进行绘画一遍。这里使用的是style里面的图片(painting.jpg),数据集将存放在百度网盘。
content文件选取的是qd.jpg,在content文件夹下。
最终实现效果如下。
第二部分 :操作
1.获取模型
VGG是Visual Geometry Group 这个实验室发明的,VGG是在2014年的 ILSVRC localization and classification 两个问题上分别取得了第一名和第二名的网络架构,是一个具有里程碑意义的CNN架构,其中最令人震惊的就是它的深度,这里使用的VGG19,有19层之多。VGG19包含了19个隐藏层(16个卷积层和3个全连接层)。VGG网络的结构非常一致,从头到尾全部使用的是3x3的卷积和2x2的max pooling。
选择使用VGG是为了将深度卷积神经网络的训练从对数据集特征的一步步抽取的过程,从简单的特征,到复杂的特征的模式转为直接使用已经训练好的模型进行特征抽取。在imagenet数据集上训练好的模型上,直接抽取其他图像的特征,虽说这样的效果往往没有在新数据上重新训练的效果好,但能够节省大量的训练时间,在特定情况下非常有用。
CNN在图片处理上表现良好,VGG19提出后,也被用在图像处理上。我这里要用到的VGG19模型就是在imagenet数据集上预训练的模型。
注: 预训练好的VGG19模型可以从http://www.vlfeat.org/matconvnet/models/beta16/imagenet-vgg-verydeep-19.mat下载,下载较慢的话,网盘https://pan.baidu.com/s/1uFinsEArbrgYRc2FWY9zVw:。
2.模型修改
这里是指从预训练的VGG模型中,获取卷积层部分的参数,用于构建我们自己的模型。VGG19中的全连接层舍弃掉,这一部分对提取图像特征基本无用。VGG19模型中权重由ImageNet训练而来,全部是作为常量使用的,这些参数是不会再被训练的,在反向传播的过程中也不会改变。
现在知道图片的内容表示和风格表示在卷积神经网络中是可分离的。也就是说,我们可以独立地操纵这两种表示来产生新的有感知意义上的图片。
风格迁移图片是通过寻找一个同时匹配照片内容和对应的艺术风格的图片的方法而生成的。这些合成图片在保留原始照片的全局布置的同时,继承了各种艺术图片的不同艺术风格。风格表示是一个多层次的表达,包括了神经网络结构的多个层次。当风格表示只包含了少量的低层结构,(简单理解为训练模型次数少,模型特征不够强势)风格的就变得更加局部化,产生不同的视觉效果。当风格表示由网络的高层结构表示时,图像的结构会在更大的范围内和这种风格匹配(特征强势,会改变整个图的风格),产生别样的感觉。
理论上简单理解了,开始操作。
这里建立py文件 models.py,下面内容我会写在注释里。
# 导入必须的包
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模型
# 这里装的是misc,安装opencv的也亦可以使用opencv等其他方法
vgg = scipy.io.loadmat(settings.VGG_MODEL_PATH)
vgg_layers = vgg['layers'][0]
net = {}
# 使用预训练的模型参数构建vgg网络的卷积层和池化层
# 全连接层不需要
# 注意,除了input之外,这里参数都为常量,不训练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)
3.模型训练
但是实际上,图片的内容和风格是不能够被完全分离的。当我们合成图片时,我们通常找不出一张能够匹配某个图片内容和另一种图片风格的图片。在我们合成图片的过程中,我们需要最小化的损失函数包含内容和风格,但它们是分开的。因此,我们需要平滑地调整内容和风格的权重比例。当损失函数分配在内容和风格的权重不同时,合成产生的图片效果也完全不一样。我们需要适当地调整内容表示和风格表示的权重比来产生具有视觉感染力的图片。是否能够找到合适的权重比是能否产生令人满意的图片的关键因素。
就是将输入层的Variable训练到满意的比例,最开始输入一张噪音图片,然后不断地根据内容loss和风格loss对其进行调整,直到一定次数后,该图片兼具了风格图片的风格以及内容图片的内容。当训练结束时,输入层的参数就是我们生成的图片。
这里建立py文件 train.py,下面内容我会写在注释里。
# -*- 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()
4.系统文件配置这里建立py文件 setting.py
# -*- coding: utf-8 -*-
# 内容图片路径
CONTENT_IMAGE = 'content/qd.jpg' # 路径/图片 自己在工程文件夹下建立。
# 风格图片路径
STYLE_IMAGE = 'style/painting.jpg' # 路径/图片 自己在工程文件夹下建立。
# 输出图片路径
OUTPUT_IMAGE = 'output/output' # 路径/图片开始名 自己在工程文件夹下建立。
# 预训练的vgg模型路径
VGG_MODEL_PATH = '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 # 这里推荐几百次就行,确实时间太长。