风格迁移试玩

人工智能160

风格迁移

*
- 图像风格迁移原理
-
+ 内容损失函数
+ 风格损失函数
- 现成工具:tensorflow hub
- 手工实现风格迁移
-
+ 我们对风格有失恭敬

神经风格转换是深度学习领域中一个很有趣的技术。它可以改变图像的风格。

如下图所示,根据一张内容图片和一张风格图片,生成一张新图片,这张图片结合了第一张图像的内容和第二张图像的风格。

风格迁移试玩
风格迁移试玩

风格迁移试玩

; 图像风格迁移原理

我们用卷积神经网络来实现风格迁移,需要准备俩张图:

  • 输入图:内容图C C C
  • 输入图:风格图S S S

目标,是将内容图(真人)变成某种风格(二次元)的内容图(二次元风格的真人)。

  • 生成图:某种风格的内容图G G G

在机器学习、深度学习里,只需要知道数据格式、损失函数,一个问题就变成了优化算法,不断迭代去调参即可。

风格转换网络的损失函数,是由俩个子损失函数组成的。

  • J c ( C , G ) J_{c}(C,~G)J c ​(C ,G ):计算内容图片、生成图片之间的损失,即内容损失函数
  • J s ( S , G ) J_{s}(S,~G)J s ​(S ,G ):计算风格图片、生成图片之间的损失,即风格损失函数
  • J ( G ) = J c ( C , G ) + J s ( S , G ) J(G)=J_{c}(C,~G)+J_{s}(S,~G)J (G )=J c ​(C ,G )+J s ​(S ,G ),风格转化损失函数 = 内容损失函数 + 风格损失函数

当然,这样写俩者就是等比例的,我们想实现不同的风格效果,需要添加俩个参数 α 、 β \alpha、\beta α、β,用于调整内容与风格之间的占比:

  • J ( G ) = α ∗ J c ( C , G ) + β ∗ J s ( S , G ) J(G)=\alphaJ_{c}(C,~G)+\betaJ_{s}(S,~G)J (G )=α∗J c ​(C ,G )+β∗J s ​(S ,G )
  • α \alpha α 大,侧重于生成内容
  • β \beta β 大,侧重于生成风格

清楚损失函数就可以训练神经网络,过程模拟如下。

输入内容图片(编号1)、风格图片(编号2)图片:
风格迁移试玩
最开始,随机初始化生成图(编号3)。

通过损失函数计算得出,当前生成图与内容图、风格图之间的损失都很大。

梯度下降算法开始最小化代价函数 J ( G ) J(G)J (G ),逐步处理像素,这样慢慢得到一个生成图片(编号4、5),越来越像用风格图片的风格画出来的内容图片(编号6)。

内容损失函数

不同层的神经网络学到的特征不同,学习过程从点 -> 线 -> 面,前面的是抽象的,后面的是具象的。

而风格转换既不能太抽象,也不能太具象,所以计算内容损失函数用中间层的神经网络。

内容损失函数,如何计算呢?

  • 计算内容图像与生成图像在某一层的激活值的差异程度。

假设是第 u 层的激活值计算损失:

-求俩个激活值向量的差异: ∣ ∣ a [ u ] ( C ) − a [ u ] ( G ) ∣ ∣ 2 || a^{u} - a^{u} ||^{2}∣∣a u −a u ∣∣2

  • a [ u ] ( C ) a^{u}a u :内容图像第 u 层激活值
  • [ u ] ( G ) ^{u}u :生成图像第 u 层激活值

如果结果小,说明生成图像在内容上与内容图像很相似。

  • 内容损失函数:J c o n t e n t u ( C , G ) = 1 ( 2 n h [ u ] n w [ u ] n c [ u ] ) 2 ∑ j = 1 n h [ u ] ∑ j = 1 n w [ u ] ( a [ u ] ( C ) − a [ u ] ( G ) ) 2 J^{u}{content}(C,~G)=\frac{1}{(2n{h}^{[u]}n_{w}^{[u]}n_{c}^{[u]})^{2}}\sum\limits_{j=1}^{n_{h}^{[u]}}\sum\limits_{j=1}^{n_{w}^{[u]}}(a^{u} - a^{u})^{2}J co n t e n t u ​(C ,G )=(2 n h [u ]​n w [u ]​n c [u ]​)2 1 ​j =1 ∑n h [u ]​​j =1 ∑n w [u ]​​(a u −a u )2

风格损失函数

在风格损失函数之前,风格上什么?

  • 从创作角度来看,一种带有综合性的总体特点,包含笔触、纹理、用色等等,比如一些画派追求眼睛所见的真实,对颜色、光影的掌控无敌的好。一些画派凭主观印象来画,对空间、设计的掌控无敌的好。
  • 从神经网络的角度来说,风格是激活值矩阵中不同深度的互相关系,是 通道的不同相同位置的激活值之间的关系。

风格迁移试玩
conv1_1 为例,共包含 64 个通道,这 64 个通道可以类比成 64 个人对一幅画的不同理解。

  • 一些人喜欢眼睛所见之真实
  • 一些人喜欢抽象代表的内涵

这 64 个人之间理解的差异,可用 64 个通道的互相关(喜好差异)表示:

  • 如果输入的图片的风格和生成图片一样,互相关的结果也就是相同的
  • 如果输入的图片的风格和生成图片不一样,互相关的结果也就是差异化的

怎么计算呢?

  • 第 1、2 个通道的第 1 个激活值相乘,第 1、2 个通道的第 2 个激活值相乘,······, 相乘后的结果相加,结果就是俩个通道的风格关系。

通过这样一一相乘再相加(内积),会得到一个新矩阵。

生成图像的风格矩阵,用 G 表示:

  • G k k ′ [ u ] ( s ) = ∑ i = 1 n h [ u ] ∑ j = 1 n w [ u ] a i j k [ u ] ( s ) a i j k ′ [ u ] ( s ) G_{kk'}^{u}=\sum\limits_{i=1}^{n_{h}^{[u]}}\sum\limits_{j=1}^{n_{w}^{[u]}}a_{ij_{k}^{u}}a_{ij_{k'}^{u}}G k k ′u ​=i =1 ∑n h [u ]​​j =1 ∑n w [u ]​​a i j k u ​​a i j k ′u ​​

解释:

  • u u u:第 u 层,每个卷积层都有自己对应的风格矩阵
  • k 、 k ′ k、k'k 、k ′:第 k、k' 通道之间的风格关系
  • S S S:风格矩阵是关于风格图像
  • n h n_{h}n h ​:激活矩阵的高
  • n w n_{w}n w ​:激活矩阵的宽

同理,生成矩阵图像的风格矩阵(三个 s 改成 G 即可):

  • G k k ′ [ u ] ( G ) = ∑ i = 1 n h [ u ] ∑ j = 1 n w [ u ] a i j k [ u ] ( G ) a i j k ′ [ u ] ( G ) G_{kk'}^{u}=\sum\limits_{i=1}^{n_{h}^{[u]}}\sum\limits_{j=1}^{n_{w}^{[u]}}a_{ij_{k}^{u}}a_{ij_{k'}^{u}}G k k ′u ​=i =1 ∑n h [u ]​​j =1 ∑n w [u ]​​a i j k u ​​a i j k ′u ​​

风格损失函数:

  • J s t y l e [ u ] ( S , G ) = 1 ( 2 n h [ u ] n w [ u ] n c [ u ] ) 2 ∑ k ∑ k ′ ( G k k ′ [ u ] ( s ) − G k k ′ [ u ] ( G ) ) J_{style}^{[u]}(S,~G)=\frac{1}{(2n_{h}^{[u]}n_{w}^{[u]}n_{c}^{[u]})^{2}}\sum\limits_{k}\sum\limits_{k'}(G_{kk'}^{u}-G_{kk'}^{u})J s t y l e [u ]​(S ,G )=(2 n h [u ]​n w [u ]​n c [u ]​)2 1 ​k ∑​k ′∑​(G k k ′u ​−G k k ′u ​)

风格图像的风格矩阵 - 生成图像的风格矩阵,如果风格差异大,那相减的结果就很大,就是损失很大。

那神经网络就会改变参数让损失变小,让俩张图像的风格越来越靠近。

除此之外,因为每层都有风格损失函数,我们引入一个超参数 λ \lambda λ,控制对不同卷积层的重视程度,或是低层次风格特征(纹理、边缘),或是高层次风格特征(复杂对象)。

完整的风格损失函数:

  • J s t y l e ( S , G ) = ∑ 1 λ u ⋅ J s t y l e [ u ] ( S , G ) J_{style}(S,~G)=\sum\limits_{1}\lambda^{u}·J_{style}^{[u]}(S,~G)J s t y l e ​(S ,G )=1 ∑​λu ⋅J s t y l e [u ]​(S ,G )

; 现成工具:tensorflow hub

在 tensorflow hub 中已经有现成的风格转换模型可以被免费调用了。

除了风格转换模型外,hub 中还包含了很多常见的模型,很强大很可怕!!

我们将下面俩张图合成吧。
风格迁移试玩
风格迁移试玩

以下代码,除了图片路径需要修改,其他都是通用的:

content_path = tf.keras.utils.get_file('ebcf732904a54911be5967c5b072a8e4.jpeg', 'https://img-blog.csdnimg.cn/ebcf732904a54911be5967c5b072a8e4.jpg')

style_path = tf.keras.utils.get_file('b275d4b95c33488e93a829bb1e7da6c9.jpeg', 'https://img-blog.csdnimg.cn/b275d4b95c33488e93a829bb1e7da6c9.jpg')

import os
import tensorflow as tf
os.environ['TFHUB_MODEL_LOAD_FORMAT'] = 'COMPRESSED'
import IPython.display as display
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (12, 12)
mpl.rcParams['axes.grid'] = False

import numpy as np
import PIL.Image
import time
import functools

def load_img(path_to_img):
  max_dim = 512
  img = tf.io.read_file(path_to_img)
  img = tf.image.decode_image(img, channels=3)
  img = tf.image.convert_image_dtype(img, tf.float32)

  shape = tf.cast(tf.shape(img)[:-1], tf.float32)
  long_dim = max(shape)
  scale = max_dim / long_dim
  new_shape = tf.cast(shape * scale, tf.int32)
  img = tf.image.resize(img, new_shape)
  img = img[tf.newaxis, :]
  return img

def imshow(image, title=None):
  if len(image.shape) > 3:
    image = tf.squeeze(image, axis=0)

  plt.imshow(image)
  if title:
    plt.title(title)

content_path = tf.keras.utils.get_file('ebcf732904a54911be5967c5b072a8e4.jpeg', 'https://img-blog.csdnimg.cn/ebcf732904a54911be5967c5b072a8e4.jpg')
style_path = tf.keras.utils.get_file('b275d4b95c33488e93a829bb1e7da6c9.jpeg', 'https://img-blog.csdnimg.cn/b275d4b95c33488e93a829bb1e7da6c9.jpg')

content_image = load_img(content_path)
style_image = load_img(style_path)
plt.subplot(1, 2, 1)
imshow(content_image, 'Content Image')
plt.subplot(1, 2, 2)
imshow(style_image, 'Style Image')

def tensor_to_image(tensor):
  tensor = tensor * 255
  tensor = np.array(tensor, dtype=np.uint8)
  if np.ndim(tensor) > 3:
    assert tensor.shape[0] == 1
    tensor = tensor[0]
  return PIL.Image.fromarray(tensor)

import tensorflow_hub as hub
hub_model = hub.load('https://hub.tensorflow.google.cn/google/magenta/arbitrary-image-stylization-v1-256/2')
stylized_image = hub_model(tf.constant(content_image), tf.constant(style_image))[0]
tensor_to_image(stylized_image)

输出:
风格迁移试玩

手工实现风格迁移

迁移学习其实就是利用已经训练好的模型来实现另一个任务,我们借用一个训练好了的 VGG-19 模型。

vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')

content_layers = ['block5_conv2']

style_layers = ['block1_conv1',
                'block2_conv1',
                'block3_conv1',
                'block4_conv1',
                'block5_conv1']

num_content_layers = len(content_layers)
num_style_layers = len(style_layers)

def vgg_layers(layer_names):
  vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
  vgg.trainable = False

  outputs = [vgg.get_layer(name).output for name in layer_names]

  model = tf.keras.Model([vgg.input], outputs)
  return model

def gram_matrix(input_tensor):
  result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
  input_shape = tf.shape(input_tensor)
  num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
  return result/(num_locations)

class StyleContentModel(tf.keras.models.Model):
  def __init__(self, style_layers, content_layers):
    super(StyleContentModel, self).__init__()
    self.vgg = vgg_layers(style_layers + content_layers)
    self.style_layers = style_layers
    self.content_layers = content_layers
    self.num_style_layers = len(style_layers)
    self.vgg.trainable = False

  def call(self, inputs):
    inputs = inputs*255.0
    preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs)
    outputs = self.vgg(preprocessed_input)
    style_outputs, content_outputs = (outputs[:self.num_style_layers],
                                      outputs[self.num_style_layers:])

    style_outputs = [gram_matrix(style_output)
                     for style_output in style_outputs]

    content_dict = {content_name: value
                    for content_name, value
                    in zip(self.content_layers, content_outputs)}

    style_dict = {style_name: value
                  for style_name, value
                  in zip(self.style_layers, style_outputs)}

    return {'content': content_dict, 'style': style_dict}

extractor = StyleContentModel(style_layers, content_layers)
style_targets = extractor(style_image)['style']

content_targets = extractor(content_image)['content']

image = tf.Variable(content_image)

def clip_0_1(image):
  return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0)

opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1)
style_weight = 1e-2

content_weight = 1e4

def style_content_loss(outputs):
    style_outputs = outputs['style']
    content_outputs = outputs['content']

    style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
                           for name in style_outputs.keys()])

    style_loss *= style_weight / num_style_layers

    content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
                             for name in content_outputs.keys()])
    content_loss *= content_weight / num_content_layers
    loss = style_loss + content_loss
    return loss

@tf.function()
def train_step(image):

  with tf.GradientTape() as tape:
    outputs = extractor(image)
    loss = style_content_loss(outputs)

  grad = tape.gradient(loss, image)

  opt.apply_gradients([(grad, image)])
  image.assign(clip_0_1(image))

import time
start = time.time()

epochs = 10
steps_per_epoch = 100

train_step(image)
train_step(image)
train_step(image)
tensor_to_image(image)

'''
真正训练的话,是要很多步的。会花很长时间,以下代码电脑配置不好的可能要花几个小时
step = 0
for n in range(epochs):
  for m in range(steps_per_epoch):
    step += 1
    train_step(image)
    print(".", end='', flush=True)
  display.clear_output(wait=True)
  display.display(tensor_to_image(image))
  print("Train step: {}".format(step))

end = time.time()
print("Total time: {:.1f}".format(end-start))
'''

输出:

风格迁移试玩
从结果可以看出,图片已经有了一点点风格图片的感觉了。

我们对风格有失恭敬

风格,是一种非常人性化的东西,它的反义词是机械化。

同样一个笑话,或者一句特别经典的话,奥巴马说一遍可能效果就非常好,而你如果接下来照着他学一遍,那就完全不好使 —— 你就是机械化的模仿,你没有自己的个人风格。

说服别人,不能用写学术论文的方法,期待用一大堆数字图表去碾压别人,那样别人只会反感,当你是个机器人。

没人愿意听机器人的,人们喜欢有风格的人。

我喜欢你的风格 — 这简直就是对人最高级的评价。

得有自己的风格,甚至哲学。

任何时候都要真诚,不要模仿任何人,永远做最真实的自己 — 而且你也不必为此道歉。

如果你的真实自我是一个很怪异的人,那你就做这样一个很怪异的人。

我所喜欢的风格 — 惜字如金,一语惊人。

能打动别人,说服别人,的确是个本事。但是我们周围人写的文章里诗歌实在太多,中文世界里有太多感情充沛气势磅礴,而又言之无物的东西。

含金量高的书,第一言之有物,传达了独特的思想或感受,第二文字凝练,赋予了这些思想或感受以最简洁的形式。

所谓文字凝练,倒不在于刻意少写,而在于不管写多写少,都力求货真价实(站得住脚,而不是好看)。

这一要求见之于修辞,就是剪除一切可有可无的词句,达于文风的简洁。

由于惜墨如金,所以果然就落笔成金,字字都掷地有声。

Original: https://blog.csdn.net/qq_41739364/article/details/120620705
Author: Debroon
Title: 风格迁移试玩



相关阅读

Title: 简洁优美的深度学习包-bert4keras

在鹅厂实习阶段,follow苏神(科学空间)的博客,启发了idea,成功改进了线上的一款模型。想法产出和实验进展很大一部分得益于苏神设计的bert4keras,清晰轻量、基于keras,可以很简洁的实现bert,同时附上了很多易读的example,对nlp新手及其友好!本文推荐几篇基于bert4keras的项目,均来自苏神,对新手入门bert比较合适~

  • tokenizer:分词器,主要方法:encode,decode。
  • build_transformer_model:建立bert模型,建议看源码,可以加载多种权重和模型结构(如unilm)。
import numpy as np
from bert4keras.models import build_transformer_model
from bert4keras.tokenizers import Tokenizer
from bert4keras.snippets import to_array

config_path = '/root/kg/bert/chinese_L-12_H-768_A-12/bert_config.json'
checkpoint_path = '/root/kg/bert/chinese_L-12_H-768_A-12/bert_model.ckpt'
dict_path = '/root/kg/bert/chinese_L-12_H-768_A-12/vocab.txt'

tokenizer = Tokenizer(dict_path, do_lower_case=True)
model = build_transformer_model(
    config_path=config_path, checkpoint_path=checkpoint_path, with_mlm=True
)

token_ids, segment_ids = tokenizer.encode(u'科学技术是第一生产力')

token_ids[3] = token_ids[4] = tokenizer._token_mask_id
token_ids, segment_ids = to_array([token_ids], [segment_ids])

probas = model.predict([token_ids, segment_ids])[0]
print(tokenizer.decode(probas[3:5].argmax(axis=1)))

  • 句子1和句子2拼接在一起输入bert。
  • bert模型的pooler输出经dropout和mlp投影到2维空间,做分类问题。
  • 最终整个模型是一个标准的keras model。
class data_generator(DataGenerator):
    """数据生成器
"""
    def __iter__(self, random=False):
        batch_token_ids, batch_segment_ids, batch_labels = [], [], []
        for is_end, (text1, text2, label) in self.sample(random):
            token_ids, segment_ids = tokenizer.encode(
                text1, text2, maxlen=maxlen
            )
            batch_token_ids.append(token_ids)
            batch_segment_ids.append(segment_ids)
            batch_labels.append([label])
            if len(batch_token_ids) == self.batch_size or is_end:
                batch_token_ids = sequence_padding(batch_token_ids)
                batch_segment_ids = sequence_padding(batch_segment_ids)
                batch_labels = sequence_padding(batch_labels)
                yield [batch_token_ids, batch_segment_ids], batch_labels
                batch_token_ids, batch_segment_ids, batch_labels = [], [], []

bert = build_transformer_model(
    config_path=config_path,
    checkpoint_path=checkpoint_path,
    with_pool=True,
    return_keras_model=False,
)

output = Dropout(rate=0.1)(bert.model.output)
output = Dense(
    units=2, activation='softmax', kernel_initializer=bert.initializer
)(output)

model = keras.models.Model(bert.model.input, output)
model = build_transformer_model(
    config_path,
    checkpoint_path,
    application='unilm',
    keep_tokens=keep_tokens,
)

NLG任务的loss是交叉熵,示例中的实现很美观:

  • CrossEntropy类继承Loss类,重写compute_loss。
  • 将参与计算loss的变量过一遍CrossEntropy,这个过程中loss会被计算,具体阅读Loss类源码。
  • 最终整个模型是一个标准的keras model。
class CrossEntropy(Loss):
    """交叉熵作为loss,并mask掉输入部分
"""
    def compute_loss(self, inputs, mask=None):
        y_true, y_mask, y_pred = inputs
        y_true = y_true[:, 1:]
        y_mask = y_mask[:, 1:]
        y_pred = y_pred[:, :-1]
        loss = K.sparse_categorical_crossentropy(y_true, y_pred)
        loss = K.sum(loss * y_mask) / K.sum(y_mask)
        return loss

model = build_transformer_model(
    config_path,
    checkpoint_path,
    application='unilm',
    keep_tokens=keep_tokens,
)

output = CrossEntropy(2)(model.inputs + model.outputs)

model = Model(model.inputs, output)
model.compile(optimizer=Adam(1e-5))
model.summary()

预测阶段自回归解码,继承AutoRegressiveDecoder类可以很容易实现beam_search。

项目地址:SimBert
融合了unilm和对比学习,data generator和loss类的设计很巧妙,值得仔细阅读,建议看不懂的地方打开jupyter对着一行一行print来理解。

bert4keras项目的优点:

  • build_transformer_model一句代码构建bert模型,一个参数即可切换为unilm结构。
  • 继承Loss类,重写compute_loss方法,很容易计算loss。
  • 深度基于keras,训练、保存和keras一致。
  • 丰富的example!苏神的前沿算法研究也会附上bert4keras实现。

Original: https://blog.csdn.net/weixin_44597588/article/details/123910248
Author: 一只用R的浣熊
Title: 简洁优美的深度学习包-bert4keras