文章目录
- Deep Dream
- 风格迁移
- 参考资料
Deep Dream
DeepDream 是一项将神经网络学习模式予以可视化展现的实验。与孩子们观察云朵并尝试解释随机形状相类似,DeepDream 会过度解释并增强其在图像中看到的图案。
DeepDream为了说明CNN学习到的各特征的意义,将采用放大处理的方式。具体来说就是使用梯度上升的方法可视化网络每一层的特征,即用一张噪声图像输入网络,反向更新的时候不更新网络权重,而是更新初始图像的像素值,以这种“训练图像”的方式可视化网络。DeepDream正是以此为基础。
DeepDream如何放大图像特征?这里我们先看一个简单实例。比如:有一个网络学习了分类猫和狗的任务,给这个网络一张云的图像,这朵云可能比较像狗,那么机器提取的特征可能也会像狗。假设对应一个特征最后输入概率为[0.6, 0.4], 0.6表示为狗的概率, 0.4表示为猫的概率,那么采用L2范数可以很好达到放大特征的效果。对于这样一个特征,L2 =〖x1〗2+〖x2〗2,若x1越大,x2越小,则L2越大,所以只需要最大化L2就能保证当x1>x2的时候,迭代的轮数越多x1越大,x2越小,所以图像就会越来越像狗。每次迭代相当于计算L2范数,然后用梯度上升的方法调整图像。优化的就不再是优化权重参数,而是特征值或像素点,因此,构建损失函数时,不使用通常的交叉熵,而是最大化特征值的L2范数。使图片经过网络之后提取的特征更像网络隐含的特征。
使用基本图像,它输入到预训练的CNN。 然后,正向传播到特定层。为了更好理解该层学到了什么,我们需要最大化通过该层激活值。以该层输出为梯度,然后在输入图像上完成渐变上升,以最大化该层的激活值。不过,光这样做并不能产生好的图像。为了提高训练质量,需要使用一些技术使得到的图像更好。可以进行高斯模糊以使图像更平滑,使用多尺度(又称为八度)的图片进行计算。先连续缩小输入图像,然后,再逐步放大,并将结果合并为一个图像输出。
首先使用预训练模型 InceptionV3
对图像特征进行提取,其中 mixed
表示的是 InceptionV3
中的 mixed
层的特征值;
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
layer_coeff = {
"mixed4": 1.0,
"mixed5": 1.5,
"mixed6": 2.0,
"mixed7": 2.5,
}
model = tf.keras.applications.inception_v3.InceptionV3(weights="imagenet", include_top=False)
outputs_dict = dict([(layer.name, layer.output) for layer in [model.get_layer(name) for name in layer_coeff.keys()]])
feature_extractor = tf.keras.Model(inputs=model.inputs, outputs=outputs_dict)
计算损失:
def compute_loss(input_image):
features = feature_extractor(input_image)
loss_list = []
for name in features.keys():
coeff = layer_settings[name]
activation = features[name]
# 通过仅在损失中包含非边界像素来避免边界伪影
scaling = tf.reduce_prod(tf.cast(tf.shape(activation), "float32"))
loss_list.append(coeff * tf.reduce_sum(tf.square(activation[:, 2:-2, 2:-2, :])) / scaling)
return tf.reduce_sum(loss_list)
定义训练函数:
@tf.function
def train_step(img, learning_rate=1e-1):
with tf.GradientTape() as tape:
tape.watch(img)
loss = compute_loss(img)
grads = tape.gradient(loss, img)
grads /= tf.math.reduce_std(grads)
img += learning_rate * grads
img = tf.clip_by_value(img, -1, 1)
return loss, img
def train_loop(img, iterations, learning_rate=1e-1, max_loss=None):
for i in range(iterations):
loss, img = gradient_ascent_step(img, learning_rate)
if max_loss is not None and loss > max_loss:
break
return img
定义超参数:
# 缩放次数 多尺度次数 也即八度 每一次缩放 octave_scale
num_octave = 1
# 缩放倍数
octave_scale = 1.4
# train_loop 训练迭代次数
iterations = 80
# 最大损失
max_loss = 15
# 学习率
learning_rate = 1e-2
如下便是多尺度缩放的训练过程:
定义数据:
img = preprocess_image('./dog.jpg')
plt.imshow(deprocess(img[0]))
开始训练:
original_img = preprocess_image('./dog.jpg')
original_shape = original_img.shape[1:3]
successive_shapes = [original_shape]
for i in range(1, num_octave):
shape = tuple([int(dim / (octave_scale ** i)) for dim in original_shape])
successive_shapes.append(shape)
successive_shapes = successive_shapes[::-1]
shrunk_original_img = tf.image.resize(original_img, successive_shapes[0])
img = tf.identity(original_img) # Make a copy
for i, shape in enumerate(successive_shapes):
print("Processing octave %d with shape %s" % (i, shape))
img = tf.image.resize(img, shape)
img = train_loop(img, iterations=iterations, learning_rate=learning_rate, max_loss=max_loss)
upscaled_shrunk_original_img = tf.image.resize(shrunk_original_img, shape)
same_size_original = tf.image.resize(original_img, shape)
lost_detail = same_size_original - upscaled_shrunk_original_img
img += lost_detail
shrunk_original_img = tf.image.resize(original_img, shape)
tf.keras.preprocessing.image.save_img('./dream-' + "dog.jpg", deprocess(img[0]))
总的来说,Deep Dream 相当于训练可视化,其不对参数进行梯度更新,而是对图像进行梯度更新,通过梯度上升让图像能够最大程度的激活目标层的输出结果;其模型实际意义不强,有稍微的模型解释性;
风格迁移
风格迁移的本质和 Deep Dream
是一样的,其主要还是因为风格转换涉及到的样本数量太少,基本就是两张图片之间进行转化,因此对参数进行梯度更新是不现实的,我们只能利用预训练模型,提取图片特征然后定义特征之间的损失进而进行操作;实现风格迁移的核心思想就是定义损失函数。
风格迁移的损失函数由内容损失和风格损失组成,这里用
O
i
m
a
g
e
O_{image}
Oimage 表示原图,
R
i
m
a
g
e
R_{image}
Rimage 表示风格图,
G
i
m
a
g
e
G_{image}
Gimage 表示生成图,那么损失如下:
L
=
d
i
s
t
a
n
c
e
(
s
t
y
l
e
(
R
i
m
a
g
e
)
−
s
t
y
l
e
(
G
i
m
a
g
e
)
)
+
d
i
s
t
a
n
c
e
(
c
o
n
t
e
n
t
(
O
i
m
a
g
e
)
−
c
o
n
t
e
n
t
(
G
i
m
a
g
e
)
)
\mathcal{L} = distance(style(R_{image}) - style(G_{image})) + distance(content(O_{image}) - content(G_{image}))
L=distance(style(Rimage)−style(Gimage))+distance(content(Oimage)−content(Gimage))
卷积神经网络不同层学到的图像特征是不一样的,靠近输入端的卷积层学到的是图像比较具体,局部的特征,如位置,形状,颜色,纹理等。靠近输出端的卷积层学到的是图像更全面,更抽象的特征,但会丢失图像的一些详细信息;
风格损失
风格损失是利用 Gram矩阵
来计算的,Gram矩阵
将图像的通道作为一个维度,将图像的宽和高合并作为一个维度,得到
X
X
X 的尺寸为
[
c
h
a
n
n
e
l
,
w
∗
h
]
[channel, w*h]
[channel,w∗h],然后计算
X
⋅
X
T
X \cdot X^T
X⋅XT ,用该值来衡量风格;
@tf.function
def gram_matrix(image):
image = tf.transpose(image, (2, 0, 1))
image = tf.reshape(image, [tf.shape(image)[0], -1])
gram = tf.matmul(image, image, transpose_b=True)
return gram
@tf.function
def compute_style_loss(r_image, g_image):
r_w, r_h, r_c = tf.shape(r_image)
g_w, g_h, g_c = tf.shape(g_image)
r_gram = gram_matrix(r_image)
g_gram = gram_matrix(g_image)
style_loss = tf.reduce_sum(tf.square(r_gram - g_gram))/ (4 * (r_c * g_c) * (r_w * r_h * g_w * g_h))
内容损失
内容损失很简单,也就是生成图像和原来图像之间的区别;
@tf.function
def compute_content_loss(o_image, g_image):
return tf.reduce_sum(tf.square(o_image - g_image))
这里不需要放缩是因为没有像风格损失一样经历过 Gram矩阵
计算,这就导致原本的内容并没有经过扩大,不过后面同样会给内容损失和风格损失分配权重;
总损失
总损失让生成的图像具有连续性,不要这里一块那里一块;
def compute_variation_loss(x):
a = tf.square(x[:, :tf.shape(x)[1]-1, :tf.shape(x)[2]-1, :] - x[:, 1:, :tf.shape(x)[2]-1, :])
b = tf.square(x[:, :tf.shape(x)[1]-1, :tf.shape(x)[2]-1, :] - x[:, :tf.shape(x)[1]-1, 1:, :])
return tf.reduce_sum(tf.pow(a+b, 1.25))
这里还是以上面的小狗图片作为原图片,风格图片采取梵高的星空图片;
首先导入预训练模型 VGG19
,以及图像处理函数 preprocess_image
deprocess_image
;
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
def preprocess_image(image_path):
img = tf.keras.preprocessing.image.load_img(image_path, target_size=(400, 600))
img = tf.keras.preprocessing.image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = tf.keras.applications.vgg19.preprocess_input(img)
return tf.convert_to_tensor(img)
def deprocess_image(x):
x = x.reshape((400, 600, 3))
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype("uint8")
return x
# 用于风格损失的网络层列表
style_layer_names = [
"block1_conv1",
"block2_conv1",
"block3_conv1",
"block4_conv1",
"block5_conv1",
]
# 用于内容损失的网络层
content_layer_names = [
"block5_conv2",
]
model = tf.keras.applications.vgg19.VGG19(weights="imagenet", include_top=False)
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers if layer.name in style_layer_names + content_layer_names])
feature_extractor = tf.keras.Model(inputs=model.inputs, outputs=outputs_dict)
定义三个损失:compute_style_loss
compute_content_loss
compute_variation_loss
def gram_matrix(image):
image = tf.transpose(image, (2, 0, 1))
image = tf.reshape(image, [tf.shape(image)[0], -1])
gram = tf.matmul(image, image, transpose_b=True)
return gram
def compute_style_loss(r_image, g_image):
r_w, r_h, r_c = tf.cast(tf.shape(r_image)[0], tf.float32), tf.cast(tf.shape(r_image)[1], tf.float32), tf.cast(tf.shape(r_image)[2], tf.float32)
g_w, g_h, g_c = tf.cast(tf.shape(g_image)[0], tf.float32), tf.cast(tf.shape(g_image)[1], tf.float32), tf.cast(tf.shape(g_image)[2], tf.float32)
r_gram = gram_matrix(r_image)
g_gram = gram_matrix(g_image)
style_loss = tf.reduce_sum(tf.square(r_gram - g_gram))/ (4 * (r_c * g_c) * (r_w * r_h * g_w * g_h))
return style_loss
def compute_content_loss(o_image, g_image):
return tf.reduce_sum(tf.square(o_image - g_image))
def compute_variation_loss(x):
a = tf.square(x[:, :tf.shape(x)[1]-1, :tf.shape(x)[2]-1, :] - x[:, 1:, :tf.shape(x)[2]-1, :])
b = tf.square(x[:, :tf.shape(x)[1]-1, :tf.shape(x)[2]-1, :] - x[:, :tf.shape(x)[1]-1, 1:, :])
return tf.reduce_sum(tf.pow(a+b, 1.25))
定义损失比例以及总损失计算函数 compute_loss
total_weight = 1e-6
style_weight = 1e-6
content_weight = 2.5e-8
def compute_loss(o_image, r_image, g_image):
X = tf.concat([o_image, r_image, g_image], axis=0)
features = feature_extractor(X)
loss_list = []
for content_layer_name in content_layer_names:
temp = features[content_layer_name]
o_image_ = temp[0,:,:,:]
g_image_ = temp[2,:,:,:]
loss = compute_content_loss(o_image_, g_image_)
loss_list.append(loss*content_weight/len(content_layer_names))
for style_layer_name in style_layer_names:
temp = features[style_layer_name]
r_image_ = temp[1,:,:,:]
g_image_ = temp[2,:,:,:]
loss = compute_style_loss(r_image_, g_image_)
loss_list.append(loss*style_weight/len(style_layer_names))
loss = compute_variation_loss(g_image)
loss_list.append(loss*total_weight)
return tf.reduce_sum(loss_list)
定义优化器,图片以及开始训练:
o_image = preprocess_image('./dog.jpg')
r_image = preprocess_image('./start-night.png')
g_image = tf.Variable(o_image)
optimizer = tf.keras.optimizers.Adam(learning_rate=1)
def train_step():
with tf.GradientTape() as tape:
loss = compute_loss(o_image, r_image, g_image)
grads = tape.gradient(loss, g_image)
optimizer.apply_gradients([(grads, g_image)])
return loss
for epoch in range(100):
plt.imshow(deprocess_image(g_image.numpy()))
plt.axis('off')
plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
plt.show()
tf.print(train_step())
最后将 生成的图片 转化为 GIF
;
import imageio
from PIL import Image
import os
import numpy as np
# 这里与 for epoch in range(100): 中的图片名称对应 image_at_epoch_{:04d}.png
converted_images = [np.array(Image.open(item)) for item in [file for file in os.listdir('./') if file.startswith('image')]]
imageio.mimsave("animation.gif", converted_images, fps=15)
得到如下结果:
参考资料
DeepDream | TensorFlow Core (google.cn)
【数学-20】格拉姆矩阵(Gram matrix)详细解读 - 知乎 (zhihu.com)