**
**
摘 要 神经风格迁移是一种优化技术,用于将两个图像(一个内容图像和一个风格参考图像)混合在一起,使输出的噪声图像看起来像内容图像,
但是使用了风格参考图像的风格。 这是通过优化输出图像以匹配内容图像的内容统计数据和风格参考图像的风格统计数据来实现的。
这些统计数据可以使用卷积网络从图像中提取。
本次论文采用Tensorflow2.0实现简单的图片风格迁移,采用官方VGG19训练模型,对内容、风格图片进行训练,通过增加训练次数与噪声图片生成次数达到减小图片内容损失以及风格损失的目的;实验表明通过增加训练次数与噪声图片生成次数可以明显的减小图片的内容以及风格损失。
关键词:神经网络;风格迁移;卷积网络;VGG19;Tensorflow2.0
**
**
图1-1 风格图片以及迁移前后对比图
图1-2 训练次数与融合次数不同对比图
**
**
图2.3.2—1 修改requirements.txt文件
图2.3.2—2 修改相关参数
图2.3.2—3 风格迁移后的输出图片
**
**
由于python以及相关附件包版本不同,部分相关函数的调用方法有所改变。以至于在初步运行Demo时总是报错,其中报错最多的为找不到某一个附件包内所涉及的函数调用。通过在CSDN网站内大量查询解决办法,最终确定为版本不兼容,更新项目所涉及相关附件包后得以解决。
其中在正式确定本次项目Demo前,在运行其它类似项目时遇到以下问题:
Tensorflow与keras版本有相关对应关系[1]。
例如keras2.3.1对应tensorflow2.1、2.0.1.15.
Tensorflow与python版本有对应关系。[2]
例如windows环境下tensorflow1.6.0对应python3.5-3.6
Pytorch和Torchvision以及python版本有严格对应关系[3]。
例如torch1.4.0对应torchvision0.5.0,python2.7或者python3.5—3.8。
图3.2—1 修改噪声图片输出大小
原项目中[5]重点提到了以梵高名画《星空》作为风格图片进行相关的图像风格融合,且风格与内容的损失比较少。但该项目对于其它风格的图像迁移也有着不错的表现,例如赛博朋克风格、素描风格等,其中赛博朋克风格在建筑上的表现更佳,对于其它类型的图片表现不够理想,素描风格的整体表现都不错。
该项目所训练的模型不止能对于绘画内的风格迁移,摄影类也能很好的进行风格迁移,但对于摄影类(即真实类)的风格、内容损失较大,能够明显的感受到有很大的突兀感,不够真实。其绘画(素描)类的风格迁移表现不够理想,突兀感很强烈,风格损失过大。
import typing
import tensorflow as tf
import settings
def get_vgg19_model(layers):
"""
创建并初始化vgg19模型
:return:
"""
# 加载imagenet上预训练的vgg19
vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
# 提取需要被用到的vgg的层的output
outputs = [vgg.get_layer(layer).output for layer in layers]
# 使用outputs创建新的模型
model = tf.keras.Model([vgg.input, ], outputs)
# 锁死参数,不进行训练
model.trainable = False
return model
class NeuralStyleTransferModel(tf.keras.Model):
def __init__(self, content_layers: typing.Dict[str, float] = settings.CONTENT_LAYERS,
style_layers: typing.Dict[str, float] = settings.STYLE_LAYERS):
super(NeuralStyleTransferModel, self).__init__()
# 内容特征层字典 Dict[层名,加权系数]
self.content_layers = content_layers
# 风格特征层
self.style_layers = style_layers
# 提取需要用到的所有vgg层
layers = list(self.content_layers.keys()) + list(self.style_layers.keys())
# 创建layer_name到output索引的映射
self.outputs_index_map = dict(zip(layers, range(len(layers))))
# 创建并初始化vgg网络
self.vgg = get_vgg19_model(layers)
def call(self, inputs, training=None, mask=None):
outputs = self.vgg(inputs)
# 分离内容特征层和风格特征层的输出,方便后续计算 typing.List[outputs,加权系数]
content_outputs = []
for layer, factor in self.content_layers.items():
content_outputs.append((outputs[self.outputs_index_map[layer]][0], factor))
style_outputs = []
for layer, factor in self.style_layers.items():
style_outputs.append((outputs[self.outputs_index_map[layer]][0], factor))
# 以字典的形式返回输出
return {'content': content_outputs, 'style': style_outputs}
from PIL import Image
# 内容特征层及loss加权系数
CONTENT_LAYERS = {'block4_conv2': 0.5, 'block5_conv2': 0.5}
# 风格特征层及loss加权系数
STYLE_LAYERS = {'block1_conv1': 0.2, 'block2_conv1': 0.2, 'block3_conv1': 0.2, 'block4_conv1': 0.2,
'block5_conv1': 0.2}
# 内容图片路径
CONTENT_IMAGE_PATH = './images/test_0.jpg'
# 风格图片路径
STYLE_IMAGE_PATH = './images/style_2.jpeg'
# 生成图片的保存目录
OUTPUT_DIR = './output/'
# 内容loss总加权系数
CONTENT_LOSS_FACTOR = 1
# 风格loss总加权系数
STYLE_LOSS_FACTOR = 100
# 加载内容图片
img_pillow = Image.open(CONTENT_IMAGE_PATH)
# 图片宽度
#WIDTH = 450
WIDTH = img_pillow.width
# 图片高度
#HEIGHT = 300
HEIGHT = img_pillow.height
# 训练epoch数
EPOCHS = 1
# 每个epoch训练多少次
STEPS_PER_EPOCH = 100
# 学习率
LEARNING_RATE = 0.03
import tensorflow as tf
import settings
# 我们准备使用经典网络在imagenet数据集上的与训练权重,所以归一化时也要使用imagenet的平均值和标准差
image_mean = tf.constant([0.485, 0.456, 0.406])
image_std = tf.constant([0.299, 0.224, 0.225])
def normalization(x):
"""
对输入图片x进行归一化,返回归一化的值
"""
return (x - image_mean) / image_std
def load_images(image_path, width=settings.WIDTH, height=settings.HEIGHT):
"""
加载并处理图片
:param image_path: 图片路径
:param width: 图片宽度
:param height: 图片长度
:return: 一个张量
"""
# 加载文件
x = tf.io.read_file(image_path)
# 解码图片
x = tf.image.decode_jpeg(x, channels=3)
# 修改图片大小
x = tf.image.resize(x, [height, width])
x = x / 255.
# 归一化
x = normalization(x)
x = tf.reshape(x, [1, height, width, 3])
# 返回结果
return x
def save_image(image, filename):
x = tf.reshape(image, image.shape[1:])
x = x * image_std + image_mean
x = x * 255.
x = tf.cast(x, tf.int32)
x = tf.clip_by_value(x, 0, 255)
x = tf.cast(x, tf.uint8)
x = tf.image.encode_jpeg(x)
tf.io.write_file(filename, x)
import os
import numpy as np
from tqdm import tqdm
import tensorflow as tf
from model import NeuralStyleTransferModel
import settings
import utils
# 创建模型
model = NeuralStyleTransferModel()
# 加载内容图片
content_image = utils.load_images(settings.CONTENT_IMAGE_PATH)
# 风格图片
style_image = utils.load_images(settings.STYLE_IMAGE_PATH)
# 计算出目标内容图片的内容特征备用
target_content_features = model([content_image, ])['content']
# 计算目标风格图片的风格特征
target_style_features = model([style_image, ])['style']
M = settings.WIDTH * settings.HEIGHT
N = 3
def _compute_content_loss(noise_features, target_features):
"""
计算指定层上两个特征之间的内容loss
:param noise_features: 噪声图片在指定层的特征
:param target_features: 内容图片在指定层的特征
"""
content_loss = tf.reduce_sum(tf.square(noise_features - target_features))
# 计算系数
x = 2. * M * N
content_loss = content_loss / x
return content_loss
def compute_content_loss(noise_content_features):
"""
计算并当前图片的内容loss
:param noise_content_features: 噪声图片的内容特征
"""
# 初始化内容损失
content_losses = []
# 加权计算内容损失
for (noise_feature, factor), (target_feature, _) in zip(noise_content_features, target_content_features):
layer_content_loss = _compute_content_loss(noise_feature, target_feature)
content_losses.append(layer_content_loss * factor)
return tf.reduce_sum(content_losses)
def gram_matrix(feature):
"""
计算给定特征的格拉姆矩阵
"""
# 先交换维度,把channel维度提到最前面
x = tf.transpose(feature, perm=[2, 0, 1])
# reshape,压缩成2d
x = tf.reshape(x, (x.shape[0], -1))
# 计算x和x的逆的乘积
return x @ tf.transpose(x)
def _compute_style_loss(noise_feature, target_feature):
"""
计算指定层上两个特征之间的风格loss
:param noise_feature: 噪声图片在指定层的特征
:param target_feature: 风格图片在指定层的特征
"""
noise_gram_matrix = gram_matrix(noise_feature)
style_gram_matrix = gram_matrix(target_feature)
style_loss = tf.reduce_sum(tf.square(noise_gram_matrix - style_gram_matrix))
# 计算系数
x = 4. * (M ** 2) * (N ** 2)
return style_loss / x
def compute_style_loss(noise_style_features):
"""
计算并返回图片的风格loss
:param noise_style_features: 噪声图片的风格特征
"""
style_losses = []
for (noise_feature, factor), (target_feature, _) in zip(noise_style_features, target_style_features):
layer_style_loss = _compute_style_loss(noise_feature, target_feature)
style_losses.append(layer_style_loss * factor)
return tf.reduce_sum(style_losses)
def total_loss(noise_features):
"""
计算总损失
:param noise_features: 噪声图片特征数据
"""
content_loss = compute_content_loss(noise_features['content'])
style_loss = compute_style_loss(noise_features['style'])
return content_loss * settings.CONTENT_LOSS_FACTOR + style_loss * settings.STYLE_LOSS_FACTOR
# 使用Adma优化器
optimizer = tf.keras.optimizers.Adam(settings.LEARNING_RATE)
# 基于内容图片随机生成一张噪声图片
noise_image = tf.Variable((content_image + np.random.uniform(-0.2, 0.2, (1, settings.HEIGHT, settings.WIDTH, 3))) / 2)
# 使用tf.function加速训练
@tf.function
def train_one_step():
"""
一次迭代过程
"""
# 求loss
with tf.GradientTape() as tape:
noise_outputs = model(noise_image)
loss = total_loss(noise_outputs)
# 求梯度
grad = tape.gradient(loss, noise_image)
# 梯度下降,更新噪声图片
optimizer.apply_gradients([(grad, noise_image)])
return loss
# 创建保存生成图片的文件夹
if not os.path.exists(settings.OUTPUT_DIR):
os.mkdir(settings.OUTPUT_DIR)
# 共训练settings.EPOCHS个epochs
for epoch in range(settings.EPOCHS):
# 使用tqdm提示训练进度
with tqdm(total=settings.STEPS_PER_EPOCH, desc='Epoch {}/{}'.format(epoch + 1, settings.EPOCHS)) as pbar:
# 每个epoch训练settings.STEPS_PER_EPOCH次
for step in range(settings.STEPS_PER_EPOCH):
_loss = train_one_step()
pbar.set_postfix({'loss': '%.4f' % float(_loss)})
pbar.update(1)
# 每个epoch保存一次图片
utils.save_image(noise_image, '{}/{}.jpg'.format(settings.OUTPUT_DIR, epoch + 1))
tensorflow==2.0.0
numpy==1.19.5
tqdm==4.62.3
转载请注明来源