Python深度学习之DeepDream

Deep Learning with Python

这篇文章是我学习《Deep Learning with Python》(第二版,François Chollet 著) 时写的系列笔记之一。文章的内容是从 Jupyter notebooks 转成 Markdown 的,你可以去 GitHub 或 Gitee 找到原始的 .ipynb 笔记本。

你可以去这个网站在线阅读这本书的正版原文(英文)。这本书的作者也给出了配套的 Jupyter notebooks。

本文为 第8章 生成式深度学习 (Chapter 8. Generative deep learning) 的笔记之一。

[TOC]

8.2 DeepDream

DeepDream

DeepDream 是一种让机器修改图像技术,它利用卷积神经网络,可以作出很迷幻的图像:

DeepDream 输出图像示例

由于 DeepDream 用的 CNN 是在 ImageNet 上训练的,而 ImageNet 里有大量的动物图片,所以,DeepDream 生成的图像里有很多动物、动物的一部分的伪影。

DeepDream 使用的算法与卷积神经网络过滤器可视化技术很类似。回忆一下,卷积神经网络过滤器可视化所做的操作是:反向运行卷积神经网络,从一张带随机噪点的空白图像输入,做梯度上升,使某过滤器激活最大化。

DeepDream 与过滤器可视化的主要区别在于:

  • 在 DeepDream 中,我们尝试将所有层的激活最大化,而不只是某一层,这样,会有大量的可视化特征混合在一起——生成更加迷幻的图像。
  • 从一张现有的图像开始,而不是从随机噪点的输入开始,这样生成的图像中会包含输入图像中已存在的视觉模式,并以将其中的一些元素扭曲——生成更加迷幻的图像。
  • 输入图像会在不同的尺度上进行处理——这些尺度称为「八度」(octave),这可以提高输出的质量。

用 Keras 实现 DeepDream

首先,我们需要选定一个在 ImageNet 上预训练的卷积神经网络:VGG16、Inception、ResNet50 等等都可以。实践证明,Inception 可以生成的比较好,所以这里我们使用 Keras 内置的 Inception V3 模型。

1
2
3
4
5
6
# 不使用及时执行模式
# See https://github.com/tensorflow/tensorflow/issues/33135

import tensorflow as tf

tf.compat.v1.disable_eager_execution()
1
2
3
4
5
6
7
8
# 加载预训练的 Inception V3 模型

from tensorflow.keras.applications import inception_v3
from tensorflow.keras import backend as K

K.set_learning_phase(0)

model = inception_v3.InceptionV3(weights='imagenet', include_top=False)
WARNING:tensorflow:From <ipython-input-3-aa9be14f7cc2>:6: set_learning_phase (from tensorflow.python.keras.backend) is deprecated and will be removed after 2020-10-11.
Instructions for updating:
Simply pass a True/False value to the `training` argument of the `__call__` method of your layer or model.

接下来定义损失——需要用梯度上升最大化的量。DeepDream 中,我们要同时将多个层的所有过滤器激活最大化。具体的实现是:对一组靠近顶部的层激活的 L2 范数进行加权求和,然后求出这个值的最大化。层的选择、以及权重分配对生成的结果影响很大:

  • 靠近底部的层会生成基本的几何图案;
  • 靠近顶部的层会生成能够看出某些物品的图像(ImageNet 中的图案,比如鸟或狗)

这里就随便选几个了:

1
2
3
4
5
# 输出 Inception V3 模型的结构,在其中选择层

import tensorflow as tf

tf.keras.utils.plot_model(model)
png
1
2
3
4
5
6
7
8
# DeepDream 配置

layer_contributions = {
'mixed4': 0.0,
'mixed5': 3.0,
'mixed6': 2.0,
'mixed7': 1.5,
}

求对于这些被选中层的损失:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义需要最大化的损失

layer_dict = dict([(layer.name, layer) for layer in model.layers])

loss = K.variable(0.)

for layer_name in layer_contributions:
coeff = layer_contributions[layer_name]
activation = layer_dict[layer_name].output

scaling = K.prod(K.cast(K.shape(activation), 'float32'))
# loss += coeff * K.sum(K.square(activation[:, 2: -2, :])) / scaling
# 应该用下面的代码👇。参考:https://github.com/fchollet/deep-learning-with-python-notebooks/issues/43
loss = loss + coeff * K.sum(K.square(activation[:, 2: -2, :])) / scaling

对损失做梯度上升:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 梯度上升过程

dream = model.input

grads = K.gradients(loss, dream)[0]

grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)

outputs = [loss, grads]
fetch_loss_and_grads = K.function([dream], outputs)

def eval_loss_and_grads(x):
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1]
return loss_value, grad_values

def gradient_ascent(x, iterations, step, max_loss=None):
for i in range(iterations):
loss_value, grad_values = eval_loss_and_grads(x)
if max_loss is not None and loss_value > max_loss:
break
print(f' loss value at {i}: {loss_value}')
x += step * grad_values
return x

最后,实现 DeepDream 算法:先定义一个尺度(scales,也叫做八度,octaves)列表,里面包含要处理的图像。后一个尺度的图像比前一个放大一定的倍数。DeepDream 要按照这个列表从最小到最大,在当前尺度运行梯度上升,然后将得到的图像放大。放大之后,图像会变得模糊,所以还要将丢失的细节重新注入到图像中。

DeepDream 过程:空间处理尺度的连续放大(八度)与放大时重新注入细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 辅助函数

import scipy
import imageio

from tensorflow.keras.preprocessing import image

def resize_img(img, size):
img = np.copy(img)
factors = (1,
float(size[0]) / img.shape[1],
float(size[1]) / img.shape[2],
1)
return scipy.ndimage.zoom(img, factors, order=1)

def save_img(img, fname):
pil_img = deprocess_image(np.copy(img))
# scipy.misc.imsave(fname, pil_img)
imageio.imsave(fname, pil_img)

def preprocess_image(image_path):
'''
用于打开图像,改变图像大小,并将图像格式转换为 Inception V3 模型能够处理的张量
'''
img = image.load_img(image_path)
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = inception_v3.preprocess_input(img)
return img

def deprocess_image(x):
'''
将一个张量转换为有效图像
'''
if K.image_data_format() == 'channels_first':
x = x.reshape((3, x.shape[2], x.shape[3]))
x = x.transpose((1, 2, 0))
else:
x = x.reshape((x.shape[1], x.shape[2], 3))

x /= 2.
x += 0.5
x *= 255.
x = np.clip(x, 0, 255).astype('uint8')
return x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 在多个连续尺度上运行梯度上升

import numpy as np

step = 0.01 # 梯度上升的步长
num_octave = 3 # 尺度个数
octave_scale = 1.4 # 两个尺度的大小比例
iterations = 20 # 在每个尺度上运行梯度上升的步数

max_loss = 10. # 损失上升的太大时中止梯度上升,以免得到丑陋的伪影

base_image_path = './img.png'

img = preprocess_image(base_image_path)

original_shape = img.shape[1:3]
successive_shapes = [original_shape]
for i in range(1, num_octave):
shape = tuple([dim // (octave_scale ** i)
for dim in original_shape])
successive_shapes.append(shape)
successive_shapes = successive_shapes[::-1]

original_img = np.copy(img)
shrunk_original_img = resize_img(img, successive_shapes[0])

for shape in successive_shapes:
print('Processing image shape', shape)
img = resize_img(img, shape)
img = gradient_ascent(img,
iterations=iterations,
step=step,
max_loss=max_loss)
upscaled_shrunk_original_img = resize_img(shrunk_original_img, shape)
same_size_original = resize_img(original_img, shape)
lost_detail = same_size_original - upscaled_shrunk_original_img

img += lost_detail
shrunk_original_img = resize_img(original_img, shape)
save_img(img, fname=f'dream_at_scale_{shape}.png')

save_img(img, fname='final_dream.png')
Processing image shape (510.0, 510.0)
   loss value at 0: 0.7559041976928711
   loss value at 1: 0.8942177295684814
   ...
   loss value at 18: 6.890047073364258
   loss value at 19: 7.1789984703063965
Processing image shape (714.0, 714.0)
   loss value at 0: 1.6531521081924438
   loss value at 1: 2.5666146278381348
   ...
   loss value at 18: 9.60411548614502
   loss value at 19: 9.890054702758789
Processing image shape (1000, 1000)
   loss value at 0: 1.6969424486160278
   loss value at 1: 2.754427909851074
   ...
   loss value at 17: 9.593267440795898
   loss value at 18: 9.928627014160156

结果:

DeepDream结果

可以看到 DeepDream 在 final_dream 图片里,画上了几只狗🐶。

注:由于 Inception V3 原始训练的尺寸的缘故,这里实现的 DeepDream 实现在尺寸介于 300×300 和 400×400 之间的图像上能够得到更好的结果,不过这不是严格的限制,任何尺寸的都是可以的。


注:使用 TensorfFlow 2 的 eager 模式,也可以很容易地实现一个更好的 DeepDream,详见这篇 TensorFlow 的官方教程。