9.4 Deep Dream图像生成

> 生成原始Deep Dream图像——单通道

现在尝试通过单通道的特征生成Deep Dream图像。

首先,定义卷积层和通道数。然后,按照指定的卷积层和通道数,利用get_tensor_by_name()函数取出对应的张量。这个卷积层有144个通道,可以任意选择一个通道进行最大化,比如这里设定channel=139,之后在调用渲染函数render_naive()时,传递layer_output里面的指定通道就可以了。总的通道数是144,所以channel可以取0-143之间的任意整数。

另外,还定义了图像噪声,它的形是(224,224,3),它表示了初始化图像的优化起点。为什么是(224,224,3)呢?因为Inception模型的单张输入图像的尺寸就是(224,224,3),分别代表图像的像素高、像素宽以及通道数,3代表它是三通道的彩色图像。

经过渲染函数render_naive()对定义的图像噪声进行优化渲染后,需要对图像进行保存和显示。

通过单通道特征生成Deep Dream图像,关键有这几步:

1. 取出指定的卷积层、通道数并取出对应的tensor

2. 把它传递到渲染函数中

3. 保存并显示

它的核心就在于渲染函数。

> 渲染函数

渲染函数的参数t_obj是layer_output里的指定通道,即它是卷积层某个通道的激活值。

在下面又定义了一个t_score,它是t_obj的平均值。为什么要对它求平均值呢?因为除了利用单通道的卷积特征来生成Deep Dream图像,还可能会利用多通道、甚至全通道的卷积特征来生成Deep Dream图像,所以需要对它的激活值平均化。t_score越大,神经网络的卷积层对应通道的平均激活值越大。

我们的目标就是通过调整t_input,使得t_score尽可能大。因此,对这个最大化问题,可以通过梯度下降的方法最优化这个目标函数。定义梯度t_grad来计算t_score对t_input的梯度。在后面的程序中,会把计算得到的梯度应用到输入图像上。

img0对应了初始的图像。在这个案例中,传递的初始图像是一个随机的噪声图像——img_noise。通过img0.copy()复制得到一个新图像,这样可以避免影响原先图像的值,在新的图像上操作就可以了。

在多次迭代的过程中,每一次都将梯度应用到图像img上。通过sess.run()的方式把张量t_grad和t_score的值求出来,得到梯度的值后,对它进行归一化处理,就可以应用到图片上了。通过控制每次迭代的步长来控制学习的速度,step默认为1.0。

完成迭代优化后,需要对图片进行保存,这里调用了savearray()函数,把一个数组保存成图像文件。

运行之后可以看到目标函数值的变化趋势,可以看出score(卷积层对应的通道的平均值)确实是按照期望逐渐增加的。通过“mixed4d_3x3_bottleneck_pre_relu”卷积层的第140个通道,得到了下面这张图像:

这是经过20次迭代之后所保存的图像。

mixed4d是Inception里的第四个卷积模块,如果用更浅层的卷积层,比如第三个卷积模块中的某一个卷积层,它的结果又会怎么样呢?

如果想用较低层单通道卷积特征生成DeepDream图像,只需要重新指定卷积层及其通道数就可以了,其他的定义都是一样的。

通过“mixed3a_3x3_bottleneck_pre_relu”卷积层的第87个通道,生成了下图:

除了对较低层的卷积特征进行可视化,若还想知道高层的卷积特征是什么样的,也可以通过同样的方法对它进行可视化。

这里把原先的卷积层换成较高的卷积层,然后在一定范围内去取它的通道数,其他的定义都是一样的。

通过“'mixed5b_5x5_pre_relu”卷积层的第119个通道,生成了下图:

这三个卷积层生成的DeepDream图像有什么不一样呢?通过这三个实验可以得到一个结论:通过最大化某一通道的平均值能够得到有意义的图像,它都有一定的规律或是纹理。

从第三个卷积模块到第四个卷积模块到第五个卷积模块,随着卷积层越来越高,它抽取的特征也越来越抽象。刚开始是简单的、规律的、重复的一些纹理,比如一开始像个圆圈,然后是类似花的形状,但到了第五个卷积模块它所提取的特征已经很抽象了。

目前,只是通过卷积层的单通道特征来生成DeepDream图像。接下来,将利用所有通道的卷积特征来生成DeepDream图像。

> 生成原始Deep Dream图像——所有通道

利用所有通道跟利用单通道有什么不同呢?

定义卷积层、噪声图像都是一样的,调用函数和保存图像也都是一样的,不一样的仅仅是调用render_naive()函数时传递的layer_output不一样。 在利用单通道的特征的时候,是“layer_output[:, :, :, channel]”的形式。指定了通道,表示特定通道的特征。若不指定特定通道,layer_output表示的是利用所有通道特征。

这就是利用“mixed4e_5x5_bottleneck_pre_relu”卷积层所有的特征所生成的图像:

通过对比可以发现:同样的卷积层,利用所有的通道所生成的图像比用单通道时所生成的图像更加抽象。

> 以背景图像为起点生成Deep Dream图像

刚刚已经介绍了如何通过极大化卷积层的某个通道的平均值来生成Deep Dream图像,但最终的Deep Dream模型还需要对图片添加背景,然后生成类似下面的图像,这才是最终的Deep Dream图像。

这是一张非常著名的含有动物的Deep Dream图像。

其实在日常生活中,也有可能接触到Deep Dream图像,只是并不知道它是Deep Dream图像罢了。

比如在这本很有名的深度学习的专著中,它的封面就是一张Deep Dream图像。

如何添加一个背景图像来生成最终的Deep Dream图像呢?之前介绍的例子,都是以噪声图像作为优化的起点的。如果使用一张背景图像来作为起点对图像进行优化,就可以生成带有背景的Deep Dream图像了。

它们的代码只有红框里面的部分和之前的不同,其他部分都是一样的:

用随机噪音图像作为起始点时,用的是随机数,这个随机数的尺寸是224×224×3,然后再加了一个值为100的扰动。现在用一张背景图像来作为起点,然后对它进行优化,所以读入的是一张图像而不是噪音图像,把它传递给render_naive()函数。

可以看到,图像的纹理和颜色都已经发生了变化,但这个效果并不理想,所以接下来需要提高生成图像的质量。

> 提高生成图像的质量

同样的,先定义目标函数t_score,然后定义梯度t_grad,因为每一步都要将梯度应用到图像上,再然后通过copy来避免操作影响到原图像。所增加的地方就是后面这两段代码:第一段代码是将图像进行金字塔分解,第二段代码有关图像的生成。

> 图像的拉普拉斯金字塔分解

在图像算法中,有高频成分和低频成分的概念。简单的说,高频成分就是图像中灰度、颜色、明度变化比较大的地方,比如图像的边缘和细节部分。低频成分就是图像中变化不大的地方,比如大块色块、整体风格。因此,对于刚刚生成的Deep Dream图像,它的高频成分太多,颜色、明度变化都比较剧烈。 而理想是图像的低频成分更多一些,这样生成的图像才能够更加柔和。

使用拉普拉斯金字塔来对图像进行分解,可以让图像具有更多的低频成分。简单的说,就是把图片分成多层,下面的level1、level2对应的就是图像的高频成分,上面的level3、level4对应的就是图像的相对低频的成分,level0是原图像。

下面是代码实现:

render_deepdream是对之前的渲染函数的改进。它引入了两个新的参数,第一个参数octave_n表示金字塔的层数,刚刚金字塔的示意图有四层,所以这里为4。第二个参数octave_scale指的是层与层之间的倍数,乘以这个倍数,图像就放大;除以这个倍数,图像就缩小。

接下来对图像进行金字塔分解,把它分解成高频成分和低频成分。首先通过shape获得图像尺寸,hw是一个元组,表示缩放后图像的高和宽。然后对图像进行分解,通过除以octave_scale来得到较小的图像,然后通过resize()函数把它的尺寸进行重设,重设成和img一样大小的图像,从而得到低频成分lo,由于图像由高频成分和低频成分构成,所以用原图减去低频成分,就可以得到高频成分了。

注意:不能直接用img减去低频成分lo得到高频成分。因为这时带有低频成分的图像,它的尺寸是原来图像缩小了1.4倍之后的大小,两幅大小不一的图像是不能相减的,要把低频图像重新调整成和原图一样大的图像,然后用原图减去低频成分,从而得到高频成分。

最后通过octaves.append(hi)将高频成分保存在金字塔中,并通过for循环将低频成分留作下一次分解。

利用图像金字塔将图像分解成高频和低频成分之后,我们要对图像进行生成。

> 图像生成

生成图像的时候,是从最低频开始的,这个图可以更加直观地描述这个过程:

在图像分解时,是从金字塔底层开始的,比如从level0(原图)开始,分解出level1高频成分,把当时的低频成分留作level2,然后再把level2分解成高频成分和更低频成分的level3,逐步分解,来得到更加低频的信息。

在图像生成时,是从金字塔顶层开始的。首先生成低频的图像,把低频成分(level4)放大成跟level3一样的尺寸后,加到level3,得到比较高频的成分,然后再把合成之后的level3’放大,加到level2,逐层相加,得到最后的最大尺寸图像,即合成图像。可以通过下面代码来实现:

注意:在图像生成时,调用了calc_grad_tiled()函数,这个函数是自定义的,它是为了避免发生内存耗尽的问题。

在前面生成图像的时候,尺寸是224×224×3,正是之前传递的img_noise噪音图像的大小。如果传递更大的img_noise就会生成更大的图片,但这样有一个潜在的问题:如果要生成的图片很大,它会占用很大的内存。当生成的图片特别大时,有可能因为内存不足导致渲染失败,这时就需要calc_grad_tiled()函数。

它的思路其实很简单,每次并不对整张图像做优化,而是把一张大的图像分解成几张小的图像,每次只对一张小图像进行优化。当固定了小图像的尺寸时,每次优化只会消耗固定大小的内存。

这个函数的输入参数是需要分解的图像(img)、梯度(t_grad)以及分解出来的小图像尺寸(title_size),比如这里为512。

img_shift是先在行上做整体移动,再在列上做整体移动。移动是通过np.roll()函数实现的。这里有一个sx和sy,它们是两个随机数。使用随机数后,在移动时它的起点不是每次都固定在图片左上角,滑动窗口的边缘位置也就不同了。原先在图像边缘的像素会被移动到图像的中间,来避免边缘效应。

接下来看一下生成更高质量的Deep Dream图像的程序整体结构。

首先导入库和Inception模型:

然后定义相关的函数:

包括保存图像、图像放大、调整图像尺寸、计算一小块图像上梯度的函数以及改进之后的渲染函数。

然后通过指定卷积层、传递作为起始点的图像,并且调用渲染函数来生成改进的Deep Dream图像:

得到的最终的图像如下图所示:

在这个图像上,可以看到多种动物的特征以及它们的一些器官,比如眼睛、鼻子等等。

大家可以通过尝试不同的背景图像、通道数、卷积层得到各种各样的生成图像。

> 示例代码

> 导入Inception模型

导入库.py
from __future__ import print_function
import os
from io import BytesIO
import numpy as np
from functools import partial
import PIL.Image 
import scipy.misc
import tensorflow as tf
创建图和会话.py
graph = tf.Graph()
sess = tf.InteractiveSession(graph=graph)
导入模型.py
model_fn = 'tensorflow_inception_graph.pb'#导入Inception网络
# tensorflow_inception_graph.pb文件的下载:
# https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip

with tf.gfile.FastGFile(model_fn, 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    
# 定义输入图像的占位符
t_input = tf.placeholder(np.float32, name='input')

#图像预处理——减均值
imagenet_mean = 117.0 #在训练Inception模型时做了减均值预处理,此处也需减同样的均值以保持一致

#图像预处理——增加维度
# 图像数据格式一般是(height,width,channels),为同时将多张图片输入网络而在前面增加一维
# 变为(batch,height,width,channel)
t_preprocessed = tf.expand_dims(t_input - imagenet_mean, 0) 

# 导入模型并将经预处理的图像送入网络中
tf.import_graph_def(graph_def, {'input': t_preprocessed})
找出卷积层.py
layers = [op.name for op in graph.get_operations() if op.type == 'Conv2D']
# 输出卷积层层数
print('Number of layers', len(layers))

# 输出所有卷积层名称
print(layers)

# 还可输出指定卷积层的参数
name1 = 'mixed4d_3x3_bottleneck_pre_relu'
print('shape of %s: %s' % (name1, str(graph.get_tensor_by_name('import/' + name1 + ':0').get_shape())))

name2 = 'mixed4e_5x5_bottleneck_pre_relu'
print('shape of %s: %s' % (name2, str(graph.get_tensor_by_name('import/' + name2 + ':0').get_shape())))

> 生成原始的Deep Dream图像

定义相关函数.py
# 把一个numpy.ndarray保存成图像文件
def savearray(img_array, img_name):
    scipy.misc.toimage(img_array).save(img_name)
    print('img saved: %s' % img_name)

# 渲染函数
def render_naive(t_obj, img0, iter_n=20, step=1.0):
    # t_obj:是layer_output[:, :, :, channel],即卷积层某个通道的值
    # img0:初始图像(噪声图像)
    # iter_n:迭代次数
    # step:用于控制每次迭代步长,可以看作学习率   

    t_score = tf.reduce_mean(t_obj)
    # t_score是t_obj的平均值
    # 由于我们的目标是调整输入图像使卷积层激活值尽可能大
    # 即最大化t_score
    # 为达到此目标,可使用梯度下降
    # 计算t_score对t_input的梯度
    t_grad = tf.gradients(t_score, t_input)[0]
    
    img = img0.copy()#复制新图像可避免影响原图像的值
    for i in range(iter_n):
        # 在sess中计算梯度,以及当前的t_score
        g, score = sess.run([t_grad, t_score], {t_input: img})
        # 对img应用梯度
        # 首先对梯度进行归一化处理
        g /= g.std() + 1e-8
        # 将正规化处理后的梯度应用在图像上,step用于控制每次迭代步长,此处为1.0
        img += g * step
        #print('score(mean)=%f' % (score))
        print('iter:%d' %(i+1), 'score(mean)=%f' % score)
    # 保存图片
    savearray(img, 'naive_deepdream.jpg')

> 通过单通道特征生成DeepDream图像

# 定义卷积层、通道数,并取出对应的tensor
name = 'mixed4d_3x3_bottleneck_pre_relu'# (?, ?, ?, 144)
channel = 139 
# 'mixed4d_3x3_bottleneck_pre_relu'共144个通道
# 此处可选任意通道(0~143之间任意整数)进行最大化

layer_output = graph.get_tensor_by_name("import/%s:0" % name)
# layer_output[:, :, :, channel]即可表示该卷积层的第140个通道

# 定义图像噪声
img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0

# 调用render_naive函数渲染
render_naive(layer_output[:, :, :, channel], img_noise, iter_n=20)

# 保存并显示图片
im = PIL.Image.open('naive_deepdream.jpg')
im.show()
im.save('naive_single_chn.jpg')

> 利用较低层单通道卷积特征生成DeepDream图像

# 定义卷积层、通道数,并取出对应的tensor
name3 = 'mixed3a_3x3_bottleneck_pre_relu'
layer_output = graph.get_tensor_by_name("import/%s:0" % name3)
print('shape of %s: %s' % (name3, str(graph.get_tensor_by_name('import/' + name3 + ':0').get_shape())))

# 定义噪声图像
img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0

# 调用render_naive函数渲染
channel = 86 # (?, ?, ?, 96)
render_naive(layer_output[:, :, :, channel], img_noise, iter_n=20)

# 保存并显示图片
im = PIL.Image.open('naive_deepdream.jpg')
im.show()
im.save('shallow_single_chn.jpg')

> 利用较高层单通道卷积特征生成DeepDream图像

# 定义卷积层、通道数,并取出对应的tensor
name4 = 'mixed5b_5x5_pre_relu'
layer_output = graph.get_tensor_by_name("import/%s:0" % name4)
print('shape of %s: %s' % (name4, str(graph.get_tensor_by_name('import/' + name4 + ':0').get_shape())))

# 定义噪声图像
img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0

# 调用render_naive函数渲染
channel =118 # (?, ?, ?, 128)
render_naive(layer_output[:, :, :, channel], img_noise, iter_n=20)

# 保存并显示图片
im = PIL.Image.open('naive_deepdream.jpg')
im.show()
im.save('deep_single_chn.jpg')

> 通过组合多个通道特征生成DeepDream图像

# 定义卷积层、通道数,并取出对应的tensor
name1 = 'mixed4d_3x3_bottleneck_pre_relu' #(?, ?, ?, 144)
name2= 'mixed4e_5x5_bottleneck_pre_relu' # (?, ?, ?, 32)
channel1 = 139 #因为共144通道,此处可选择0~143之间任意整数
channel2 = 28 # 因为共32通道,此处可选择0~31之间任意整数

layer_output1= graph.get_tensor_by_name("import/%s:0" % name1)
layer_output2= graph.get_tensor_by_name("import/%s:0" % name2)

# 定义噪声图像
img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0

# 调用render_naive函数渲染
render_naive(layer_output1[:, :, :, channel1]+layer_output2[:, :, :, channel2], img_noise, iter_n=20)

# 保存并显示图片
im = PIL.Image.open('naive_deepdream.jpg')
im.show()
im.save('multi_chn.jpg')

> 利用所有通道特征生成DeepDream图像

# 定义卷积层,并取出对应的tensor
name = 'mixed4d_3x3_bottleneck_pre_relu' 
layer_output= graph.get_tensor_by_name("import/%s:0" % name)

# 定义噪声图像
img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0

# 调用render_naive函数渲染
render_naive(layer_output, img_noise, iter_n=20) # 不指定特定通道,即表示利用所有通道特征
# 单通道时:layer_output[:, :, :, channel]

# 保存并显示图片
im = PIL.Image.open('naive_deepdream.jpg')
#im = PIL.Image.open('deepdream.jpg')
im.show()
im.save('all_chn.jpg')

> 利用背景图像生成DeepDream图像

# 定义卷积层、并取出对应的tensor
name = 'mixed4c'
layer_output= graph.get_tensor_by_name("import/%s:0" % name)
print(layer_output)

# 用一张背景图像(而不是随机噪音图像)作为起点对图像进行优化
img_test=PIL.Image.open('mountain.jpg') # img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0

# 调用render_naive函数渲染
render_naive(layer_output, img_noise, iter_n=100) # 不指定特定通道,即表示利用所有通道特征

# 保存并显示图片
im = PIL.Image.open('deepdream.jpg')
im.show()
im.save('mountain_naive.jpg')

> 生成更高质量的Deep Dream图像

导入库与Inception模型.py
from __future__ import print_function
import os
from io import BytesIO
import numpy as np
from functools import partial
import PIL.Image
import scipy.misc
import tensorflow as tf


graph = tf.Graph()
model_fn = 'tensorflow_inception_graph.pb'
sess = tf.InteractiveSession(graph=graph)
with tf.gfile.FastGFile(model_fn, 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
t_input = tf.placeholder(np.float32, name='input')  
imagenet_mean = 117.0
t_preprocessed = tf.expand_dims(t_input - imagenet_mean, 0)
tf.import_graph_def(graph_def, {'input': t_preprocessed})
定义相关函数.py
# 保存图像
def savearray(img_array, img_name):
    scipy.misc.toimage(img_array).save(img_name)
    print('img saved: %s' % img_name)

# 将图像放大ratio倍
def resize_ratio(img, ratio):
    min = img.min()
    max = img.max()
    img = (img - min) / (max - min) * 255
    img = np.float32(scipy.misc.imresize(img, ratio))
    img = img / 255 * (max - min) + min
    return img

# 调整图像尺寸
def resize(img, hw):
    min = img.min()
    max = img.max()
    img = (img - min) / (max - min) * 255
    img = np.float32(scipy.misc.imresize(img, hw))
    img = img / 255 * (max - min) + min
    return img

# 原始图像尺寸可能很大,从而导致内存耗尽问题 
# 每次只对 tile_size * tile_size 大小的图像计算梯度,避免内存问题
def calc_grad_tiled(img, t_grad, tile_size=512):
    sz = tile_size
    h, w = img.shape[:2]
    sx, sy = np.random.randint(sz, size=2)
    img_shift = np.roll(np.roll(img, sx, 1), sy, 0)  # 先在行上做整体移动,再在列上做整体移动
    grad = np.zeros_like(img)
    for y in range(0, max(h - sz // 2, sz), sz):
        for x in range(0, max(w - sz // 2, sz), sz):
            sub = img_shift[y:y + sz, x:x + sz]
            g = sess.run(t_grad, {t_input: sub})
            grad[y:y + sz, x:x + sz] = g
    return np.roll(np.roll(grad, -sx, 1), -sy, 0)

def render_deepdream(t_obj, img0,
                     iter_n=10, step=1.5, octave_n=4, octave_scale=1.4):
    t_score = tf.reduce_mean(t_obj)
    t_grad = tf.gradients(t_score, t_input)[0]
    img = img0.copy()
    
    # 将图像进行金字塔分解
    # 从而分为高频、低频部分
    octaves = []
    for i in range(octave_n - 1):
        hw = img.shape[:2]
        lo = resize(img, np.int32(np.float32(hw) / octave_scale))
        hi = img - resize(lo, hw)
        img = lo
        octaves.append(hi)

    # 首先生成低频的图像,再依次放大并加上高频
    for octave in range(octave_n):
        if octave > 0:
            hi = octaves[-octave]
            img = resize(img, hi.shape[:2]) + hi
        for i in range(iter_n):
            g = calc_grad_tiled(img, t_grad)
            img += g * (step / (np.abs(g).mean() + 1e-7))
           
    img = img.clip(0, 255)
    savearray(img, 'mountain_deepdream.jpg')
    im = PIL.Image.open('mountain_deepdream.jpg').show()
生成以背景图像作为起点的DeepDream图像.py
name = 'mixed4c'
layer_output = graph.get_tensor_by_name("import/%s:0" % name)

img0 = PIL.Image.open('mountain.jpg')
img0 = np.float32(img0)
render_deepdream(tf.square(layer_output), img0)    

Last updated