TensorFlow练手项目三:使用VGG19迁移学习实现图像风格迁移
撰写于 2018 年 3 月 24 日,由我的 csdn blog 个上迁移而来。
当时的开发环境为:
- python 2.7
- tensorflow==1.4
使用 VGG19 迁移学习实现图像风格迁移
转载请注明出处:https://blog.csdn.net/aaronjny/article/details/79681080
一直想要做个图像风格迁移来玩玩的,感觉还是蛮有意思的。
所谓图像风格迁移,即给定内容图片 A,风格图片 B,能够生成一张具有 A 图片内容和 B 图片风格的图片 C。
比如说,我们可以使用梵高先生的名画《星夜》 作为风格图片,来与其他图片生成具有《星夜》风格新图片。emmm,夭寿啦,机器帮你画世界名画啦。。。
举两个生成的例子:
均使用《星夜》作为风格图片(可以替换,我以《星夜》为例):
[外链图片转存失败(img-my3dpb4D-1567749275391)(https://raw.githubusercontent.com/AaronJny/nerual_style_change/master/sample/input_style_1.jpg)]
示例 1:
网络上找到的一张风景图片。
内容图片:
[外链图片转存失败(img-bMUHRDKs-1567749275391)(https://raw.githubusercontent.com/AaronJny/nerual_style_change/master/sample/input_content_1.jpg)]
生成图片:
生成图片的尺寸比较小,没办法,我的显卡太差了,尺寸大一点的话显卡内存不足。
示例 2:
嗷嗷嗷,狼人嚎叫 ~
内容图片:
[外链图片转存失败(img-xBUkv1Q0-1567749275393)(https://raw.githubusercontent.com/AaronJny/nerual_style_change/master/sample/input_content_2.jpg)]
生成图片:
效果还凑合吧,可以接受。
下面记录实现过程。
一。获取预训练的 vgg19 模型
VGG19 是 Google DeepMind 发表在 ICLR 2015 上的论文《VERY DEEP CONVOLUTIONAL NETWORK SFOR LARGE-SCALE IMAGE RECOGNITION》中提出的一种 DCNN 结构。
众所周知,CNN 在图片处理上表现良好,VGG19 提出后,也被用在图像处理上。我这里要用到的 VGG19 模型就是在 imagenet 数据集上预训练的模型。
一般认为,深度卷积神经网络的训练是对数据集特征的一步步抽取的过程,从简单的特征,到复杂的特征。
训练好的模型学习到的是对图像特征的抽取方法,所以在 imagenet 数据集上训练好的模型理论上来说,也可以直接用于抽取其他图像的特征,这也是迁移学习的基础。自然,这样的效果往往没有在新数据上重新训练的效果好,但能够节省大量的训练时间,在特定情况下非常有用。
预训练好的 VGG19 模型可以从这里下载,模型大小 500M+。
二。模型编写
这里的模型基本上就是 VGG19 模型,只是稍微做了一些修改。
我们要从预训练的模型中,获取卷积层部分的参数,用于构建我们自己的模型。VGG19 中的全连接层舍弃掉,这一部分对提取图像特征基本无用。
要注意的是,我这里提取出来的 VGG 参数全部是作为 constant(即常量)使用的,也就是说,这些参数是不会再被训练的,在反向传播的过程中也不会改变。
另外,输入层要设置为 Variable,我们要训练的就是这个。最开始输入一张噪音图片,然后不断地根据内容 loss 和风格 loss 对其进行调整,直到一定次数后,该图片兼具了风格图片的风格以及内容图片的内容。当训练结束时,输入层的参数就是我们生成的图片。
附一张 VGG 结构图:
这个代码里主要是定义 VGG,至于 LOSS 在训练过程中进行说明。
models.py
# -*- coding: utf-8 -*-
# @Time : 18-3-23 下午12:20
# @Author : AaronJny
# @Email : Aaron__7@163.com
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)
三。模型训练
这里简述一下训练的思路:
1.首先,我们使用 VGG 中的一些层的输出来表示图片的内容特征和风格特征。比如,我使用['conv4_2','conv5_2']表示内容特征,使用['conv1_1','conv2_1','conv3_1','conv4_1']表示风格特征。
# 定义计算内容损失的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.将内容图片输入网络,计算内容图片在网络指定层(比如['conv4_2','conv5_2'])上的输出值。
3.计算内容损失。我们可以这样定义内容损失:内容图片在指定层上提取出的特征矩阵,与噪声图片在对应层上的特征矩阵的差值的 L2 范数。即求两两之间的像素差值的平方。
对应每一层的内容损失函数:
(话说为了写这个公式,我还跑去现学了 latex 语法 = =)
标题:TensorFlow练手项目三:使用VGG19迁移学习实现图像风格迁移
作者:AaronJny
地址:https://aaronjny.com/articles/2019/09/06/1567749415828.html