TensorFlow练手项目三:使用VGG19迁移学习实现图像风格迁移

使用VGG19迁移学习实现图像风格迁移


2020.3.15 更新:

使用Python 3.7 + TensorFlow 2.0的实现:

有趣的深度学习——使用TensorFlow 2.0实现图片神经风格迁移 (https://blog.csdn.net/aaronjny/article/details/104879258)


转载请注明出处:https://blog.csdn.net/aaronjny/article/details/79681080


一直想要做个图像风格迁移来玩玩的,感觉还是蛮有意思的。

所谓图像风格迁移,即给定内容图片A,风格图片B,能够生成一张具有A图片内容和B图片风格的图片C。

比如说,我们可以使用梵高先生的名画《星夜》 作为风格图片,来与其他图片生成具有《星夜》风格新图片。emmm,夭寿啦,机器帮你画世界名画啦。。。

举两个生成的例子:

均使用《星夜》作为风格图片(可以替换,我以《星夜》为例):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PBbLpmXy-1584256961127)(https://raw.githubusercontent.com/AaronJny/nerual_style_change/master/sample/input_style_1.jpg)]

示例1:

网络上找到的一张风景图片。

内容图片:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hBlBxLwz-1584256961128)(https://raw.githubusercontent.com/AaronJny/nerual_style_change/master/sample/input_content_1.jpg)]

生成图片:

生成图片1

生成图片的尺寸比较小,没办法,我的显卡太差了,尺寸大一点的话显卡内存不足。

示例2:

嗷嗷嗷,狼人嚎叫~

内容图片:

内容图片2

生成图片:

生成图片2

效果还凑合吧,可以接受。

下面记录实现过程。


一.获取预训练的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结构图

这个代码里主要是定义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语法= =)

L i = 1 2 ∗ M ∗ N ∑ i j ( X i j − P i j ) 2 L_{i}=\frac{1}{2*M*N}\sum_{ij}^{}(X_{ij}-P_{ij})^2 Li=2MN1ij(XijPij)2

其中,X是噪声图片的特征矩阵,P是内容图片的特征矩阵。M是P的长*宽,N是信道数。

最终的内容损失为,每一层的内容损失加权和,再对层数取平均。

4.将风格图片输入网络,计算风格图片在网络指定层(比如[‘conv1_1’,‘conv2_1’,‘conv3_1’,‘conv4_1’])上的输出值。

5.计算风格损失。我们使用风格图像在指定层上的特征矩阵的GRAM矩阵来衡量其风格,风格损失可以定义为风格图像和噪音图像特征矩阵的格莱姆矩阵的差值的L2范数。

对于每一层的风格损失函数:

L i = 1 4 ∗ M 2 ∗ N 2 ∑ i j ( G i j − A i j ) 2 L_{i}=\frac{1}{4*M^2*N^2}\sum_{ij}(G_{ij}-A_{ij})^2 Li=4M2N21ij(GijAij)2

其中M是特征矩阵的长*宽,N是特征矩阵的信道数。G为噪音图像特征的Gram矩阵,A为风格图片特征的GRAM矩阵。

最终的风格损失为,每一层的风格损失加权和,再对层数取平均。

6.最终用于训练的损失函数为内容损失和风格损失的加权和。

L t o t a l = α L c o n t e n t + β L s t y l e L_{total}=\alpha L_{content}+\beta L_{style} Ltotal=αLcontent+βLstyle

7.当训练开始时,我们根据内容图片和噪声,生成一张噪声图片。并将噪声图片喂给网络,计算loss,再根据loss调整噪声图片。将调整后的图片喂给网络,重新计算loss,再调整,再计算…直到达到指定迭代次数,此时,噪声图片已兼具内容图片的内容和风格图片的风格,进行保存即可。

具体代码如下:

train.py

# -*- coding: utf-8 -*-
# @Time    : 18-3-23 下午12:22
# @Author  : AaronJny
# @Email   : Aaron__7@163.com
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()

四、一些配置信息

settings.py

# -*- coding: utf-8 -*-
# @Time    : 18-3-23 下午12:22
# @Author  : AaronJny
# @Email   : Aaron__7@163.com


# 内容图片路径
CONTENT_IMAGE = 'images/content.jpg'
# 风格图片路径
STYLE_IMAGE = 'images/style.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

更多

以上即是全部内容,感觉还是挺有意思的。

附上项目GitHub地址:https://github.com/AaronJny/nerual_style_change

学识浅薄,文中如有纰漏错误之处,还请诸位大佬斧正,万分感谢!

参考

[1]A Neural Algorithm of Artistic Style

[2]深度学习实战(一)快速理解实现风格迁移

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页