Deep Dream模型

Deep Dream 是Google 公司在2015 年公布的一顶有趣的技术。在训练好的卷积神经网络中,只需要设定几个参数,就可以通过这项技术生成一张图像。生成出的图像不仅令人印象深刻,而且还能帮助我们理解卷积神经网络背后的运行机制。本章介绍Deep Dream 的基本原理,并使用TensorFlow实现Deep Dream 生成模型。

1. Deep Dream 的技术原理

在卷积网络中,输入一般是一张图像,中间层是若干卷积运算,输出是图像的类别。在训练阶段,会使用大量的训练图片计算梯度,网络根据梯度不断地调整和学习最佳的参数。对此,通常会再一些疑问,例如:

  • 卷积层究竟学到了什么内容?
  • 卷积层的参数代表的意义是什么?
  • 浅层的卷积层和深层的卷积层学到的内容有哪些区别?

Deep Dream 可以解答上述问题。

设输入网络的图像为x ,网络输出的各个类别的概率为t(如ImageNet为1000种分类,在这种情况下,t 是一个1000 维的向量,代表了1 000 种类别的概率),以香蕉类别为例,假设宫对应的概率输出值t[100],换句话说, t[100]代表了神经网络认为一张图片是香蕉的概率。设定
t[100]为优化目标,不断地让神经网络去调整输入图像x的像素值,让输出t[100]尽可能的大,最后得到如图4-1所示的图像。
这里写图片描述

在图4-1中,左边是输入x 的初始图像,只是一些随机的噪声,经过神
经网络不断地调整,得到极大化t[100]对应的圄像。这就是在神经网络“眼中” 最具备香蕉特点的图像。在图中可以很明显地观察到香蕉基本的颜色和形状特征。

图4-2展示了更多类别物体对应的特征,第一行分别是大玲羊、量杯、
蚂蚁、海星,第二行分别是小丑鱼、香蕉、降落伞、蝶、钉。
这里写图片描述

通过图4-2 可以理解最后的类别概率代表怎样的含义, 但我们还想弄清楚神经网络中间的卷积层究竟学到了什么。真实使用的方法是类似的, 只需要最大化卷积层某一通道的输出就可以。同样设输入图像为x ,中间某个卷积层的输出是y。y 的形状应该是h*w吨, 真中h 为y 的高度, w 为y 的宽度, c 则代表“通道数” 。原始图像高R 、G 、B 三个通道, 而在大多数卷积层中,通道数都远远不止3 个。卷积的一个通道就可以代表一种学习到的“信息” 。以某一个通道的平均值作为优化目标,就可以弄清楚这个通道究竟学习到了什么,这也是Deep Dream 的基本原理。在下面的的小节中, 会以程序的形式,更详细地介绍如何生成并优化Deep Dream 图像。

2. TensorFlow中的Deep Dream模型实践

2.1 导入Inception模型

原始的Deep Dream模型只需要优化ImageNet 模型卷积层某个通道的激活值就可以了,为此,应该先在TensorFlow 导入一个ImageNet 图像识别模型。这里以Inception 模型为例进行介绍, 对应程序的文件名为load_ inception. py 。

首先导入要用到的一些基本模块(语句from future import
print_ function 是为了在python2 、python3 中可以使用E侣兼容的print 函数):[本小节代码参考]了TensorFlow源码中的示例程序(https%20:%20//github%20.%20com/tensorfl%20ow/%20tensorflow/tree/master%20/tensorflow/examples/tutorials/deepdream)

# 导入要用到的基本模块。
from __future__ import print_function
import numpy as np
import tensorflow as tf
# 创建图和Session
graph = tf.Graph()
sess = tf.InteractiveSession(graph=graph)

以上都是一些基本的准备工作,下面开始真正地导入Inception模型。TensorFlow提供了一种特殊的以“.pb”为扩展名的文件,可以事先将模型导入到pb文件中,再在需要的时候导出。对于Inception模型,对应的pb文件为tensorflow_inception_graph.pb。

使用下面的程序就可以把Inception 模型导入TensorFlow 中:

# tensorflow_inception_graph.pb文件中,既存储了inception的网络结构也存储了对应的数据
# 使用下面的语句将之导入
model_fn = 'tensorflow_inception_graph.pb'
with tf.gfile.FastGFile(model_fn, 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
# 定义t_input为我们输入的图像
t_input = tf.placeholder(np.float32, name='input')
imagenet_mean = 117.0
# 输入图像需要经过处理才能送入网络中
# expand_dims是加一维,从[height, width, channel]变成[1, height, width, channel]
# t_input - imagenet_mean是减去一个均值
t_preprocessed = tf.expand_dims(t_input - imagenet_mean, 0)
tf.import_graph_def(graph_def, {'input': t_preprocessed})

在导入的时候需要给网络指定一个输入图像。为此,设置一个占位符
t_input ,在后面的程序中,就会把图像数据传递给t_input 。需要注意的是,使用的图像数据通常的格式为(height, width, channel),其中height为图像的像素高度,width为图像的像素宽度, channel为图像的通道数。一般使用的是RGB图像,因此图像的通道数channel就等于3 。虽然图像的格式是(height,width, channel),但是Inception 模型需要的输入格式却是(batch, height, width,channel)。这是因为恪式(height, width, channel)只能表示一张图片,但在训练神经网络时往往需要同时送入多张图片,因此在前面加了一维,让输入图像的格式变为(batch, height, width, channel) 。在此,尽管一次只需要输入一张
图像,但同样需要把输入数据变为(batch, height, width, channel)自由形式,不过batch 此时等于1 。为此,使用tf.expand_ dims 函数, 它就会在原始的输入前增加一维。

另一个需要注意的地方是,还需要为图像减去一个像素均值。这是由于
在训练Inception 模型的时候,已经做了减去均值的预处理,因此应该使用同样的预处理方法,才能保持输入的一致。此处使用的Inception 模型减去的是一个固定的均值117,所以在程序中也定义了imagenet_ mean= 117,并用t_input 减去imagenet_mean 。

经过减去均值、添加维度两个预处理后,得到真正送入网络的输入图像
t_preprocessed ,下面使用tf.import_graph_ def(graph def, {’input’:
t_preprocessed})就可以导入模型了。

导入模型后,找出模型中所有的卷积层,并尝试输出某个卷积层的形状:

# 找到所有卷积层
layers = [op.name for op in graph.get_operations() if op.type == 'Conv2D' and 'import/' in op.name]

# 输出卷积层层数
print('Number of layers', len(layers))

# 特别地,输出mixed4d_3x3_bottleneck_pre_relu的形状
name = 'mixed4d_3x3_bottleneck_pre_relu'
print('shape of %s: %s' % (name, str(graph.get_tensor_by_name('import/' + name + ':0').get_shape())))

运行代码后,会输出共高59 个卷积层。59 实际是layers 这个列表的沃
度,我们可以自行使用print(layers)打印出所有层的名称。

特别地,尝试输出一个卷积层、iixed4d_3x3 bottleneck_pre_relu ”的形状,输出的结果应该是(?, ?, ?, 144) 。事实上,卷积层的格式一般是(batch,height, width, channel),因为此时还不清楚输入图像的个数以及大小,所以前三维的值是不确定的,显示为问号。最后,channel 的值是固定的,一共有144个通道。除了mixed4d_3x3 _bottleneck _pre_relu 卷积层外,我们还可以根据print(layers)的结果,尝试打印出其他卷积层的形状。下面就以
mixed4d_3x3 _bottleneck_pre_relu 卷积层为例3 最大化宫某一个通道的平均值,以达到生成图像的目的。

2.2 生成原始的Deep Dream图像

程序的主要部分如下:

# 定义卷积层、通道数,并取出对应的tensor
name = 'mixed4d_3x3_bottleneck_pre_relu'
channel = 139
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[:, :, :, channel], img_noise, iter_n=20)

首先取出对应名称’mixed4d_3x3_bottleneck_pre_relu’的卷积层输出layer_out。在2.1小节中,我们已经知道了它的格式为(?,?,?,144)。我们任意选择一个通道进行最大化,如设定channel=139,最后调用渲染函数render_naive的时候传递layer_out[:,:,:,channel]即可。总通道数是144,channel可以取0~143中的任何一个整数值,这里只是以139通道为例。另外,还定义了一个图像躁声img_noise ,它是一个形状为(224, 224, 3 )的张量,表示初始的图像优化起点。渲染函数render_naive 细节如下.

def render_naive(t_obj, img0, iter_n=20, step=1.0):
    # t_score是优化目标。它是t_obj的平均值
    # 结合调用处看,实际上就是layer_output[:, :, :, channel]的平均值
    t_score = tf.reduce_mean(t_obj)
    # 计算t_score对t_input的梯度
    t_grad = tf.gradients(t_score, t_input)[0]

    # 创建新图
    img = img0.copy()
    for i in range(iter_n):
        # 在sess中计算梯度,以及当前的score
        g, score = sess.run([t_grad, t_score], {t_input: img})
        # 对img应用梯度。step可以看做“学习率”
        g /= g.std() + 1e-8
        img += g * step
        print('score(mean)=%f' % (score))
    # 保存图片
    savearray(img, 'naive.jpg')

下面仔细介绍这个函数是怎样工作的。函数的参数t_obj实际上就是layer_output[:,:,:,channel],也就是说卷积层某个通道值。又定义了t_score = tf.reduce_mean(t_obj),意即t_score 是t_obj 的平均值。t_score越大,就说明神经网络卷积层对应通道的平均激活越大。本小节的目标就是通过调整输入图像t_input,来让t_score尽可能的大。为此使用梯度下降法,定义梯度# 计算t_score对t_input的梯度
t_grad = tf.gradients(t_score, t_input)[0],在后面的程序中,会把计算得到的梯度应用到输入图像上。

img0对应了初始图像。之前传递的初始图像是一个随机的躁声图像
image_noise 。在render_naive 中,先通过img = img0.copy()复制一个新图像,这样可以避免影响原先图像的值。在新图向上,迭代iter_n步。每一步都将梯度应用到图像img上。计算梯度的语句为:g, score = sess.run([t_grad, t_score], {t_input: img})。g对应梯度t grad 的值,而score 对应t_score 的值。得到梯度后, 对梯度做一个简单的正规化处理,然后就将官应用到图片上:img += g * step。step 可以看作“学习率里取默认的step =1即可。

运行程序会得到如下中间结果:

score (mean) = -19.889559
score (mean) = -29.800030
score (mean) = 17.490173
score (mean) = 98.266052
score (mean) = 63.729172
score (mean) = 216.509613
score (mean) = 278.762970

这就说明score ( 也就是卷积层对应通道的平均值)确实是按期望逐渐
增大的。在经过20 次迭代后,会把圄像保存为naive.j pg ,如国4-3 所示。
这里写图片描述

确实可以通过最大化某一通道的平均值得到一些有意义的图像!此处图
像的生成效果还不太好,在下面的几节中,会开始逐步提高生成图片的质量,生成更加精美的Deep Dream 图片。

2.3 生成更大尺寸的Deep Dream 图像

首先尝试生成更大尺寸的图像。在2.2节中,生成图像的尺寸是(224,224,3),这正是传递的img_noise的大小。如果传递更大的img_noise,就可以生成更大的图片。但是这样会有一个潜在的问题:要生成的图像越大,就会占用越大的内存或显存,若想生成特别大的图片,就回应为内存或显存不足导致渲染失败。如何解决这个问题呢?其实方法很简单:每次不对整张图片做优化,而是把图片分成几个部分,每次只对某中的一个部分做优化,这样每次优化时只会消耗固定大小的内存。

定义函数calc_grad_tiled 可以对任意大小的圄像计算梯度,它的代码如下:

def calc_grad_tiled(img, t_grad, tile_size=512):
    # 每次只对tile_size×tile_size大小的图像计算梯度,避免内存问题
    sz = tile_size
    h, w = img.shape[:2]
    # img_shift:先在行上做整体移动,再在列上做整体移动
    # 防止在tile的边缘产生边缘效应
    sx, sy = np.random.randint(sz, size=2)
    img_shift = np.roll(np.roll(img, sx, 1), sy, 0)
    grad = np.zeros_like(img)
    # y, x是开始位置的像素
    for y in range(0, max(h - sz // 2, sz), sz):
        for x in range(0, max(w - sz // 2, sz), sz):
            # 每次对sub计算梯度。sub的大小是tile_size×tile_size
            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
    # 使用np.roll移动回去
    return np.roll(np.roll(grad, -sx, 1), -sy, 0)

尽管原始图像img 可能很大,但此函数只对tile_size×tile_size大小的图像计算梯度,因此计算只会消耗固定的内存,不会发生内存耗尽的问题。默认取tile size=512 。

如果直接计算梯度,在每个512 * 512 块的边缘,可能会发生比较明显的“边缘效应”,影响图片美观。改进后的做法是生成两个随机数sx,sy,使用np.roll(np.roll(img, sx, 1), sy, 0)对图片做“整体移动”,这样原先在图象边缘的像素就会被移动到图象中间,从而避免边缘效应。我们可以查看np.roll 函数的文挡,详细地了解如何整体移动图像的像素,此处不再赘述。

有了calc_ grad_ tiled函数,可以对任意大小的图像计算梯度了。在实际工程中, 为了加快图像的收敛速度,采用先生成小尺寸,再将图片放大的方法,请参考下面的代码:

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 render_multiscale(t_obj, img0, iter_n=10, step=1.0, octave_n=3, octave_scale=1.4):
    # 同样定义目标和梯度
    t_score = tf.reduce_mean(t_obj)
    t_grad = tf.gradients(t_score, t_input)[0]

    img = img0.copy()
    for octave in range(octave_n):
        if octave > 0:
            # 每次将将图片放大octave_scale倍
            # 共放大octave_n - 1 次
            img = resize_ratio(img, octave_scale)
        for i in range(iter_n):
            # 调用calc_grad_tiled计算任意大小图像的梯度
            g = calc_grad_tiled(img, t_grad)
            g /= g.std() + 1e-8
            img += g * step
            print('.', end=' ')
    savearray(img, 'multiscale.jpg')

resize_ratio函数的功能是将图片img放大ratio倍。因此,在其内使用的函数是scipy.misc.imresize 。但scipy.misc.imresize 会自动把输出缩放为0 ~ 255 之间的数,这可能和原先的像素值的范围不符,影响收敛。因此,resize_ratio函数先确定原先像素的范围,计算img的最大值和最小值,使用scipy.misc.imresize后,再将像素值缩放回去。

render_multiscale 是用来生成大尺寸图像的函数。相比上节的函数,它又多出了两个参数octave_n和octave_scale。先生成小尺寸的图像,然后调用resize_ratio将小尺寸图像放大octave_scale倍,再使用放大后的图像做为初始值进行计算。这样的放大一共会进行octave_n - 1 次。
换句话说, octave_n 越大,最后生成的图像就会越大3 默认的octave _n=3 。

有了上面两个函数后,生成图像就很简单了,直接调用这些函数即可,程序如下:

if __name__ == '__main__':
    name = 'mixed4d_3x3_bottleneck_pre_relu'
    channel = 139
    img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0
    layer_output = graph.get_tensor_by_name("import/%s:0" % name)
    render_multiscale(layer_output[:, :, :, channel], img_noise, iter_n=20)

这里写图片描述
此时可以看到,卷积层“mixed4d_3x3 _bottleneck _prerel”的第139个通道实际上就是学习到了某种花朵的特征,如果输入这种花朵的图像,它的激活值就会达到最大。我们还可以调整octave_n为更大的值,就可以生成更大的图像。不管最终图像的尺寸是多大,失踪只会对512*512像素的图像计算梯度,因此内存始终是够用的。如果计算512*512的图像的梯度会造成内存问题,可以将2.2节的calc_grad_tiled函数中tile_size 修改为更小的值。

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

在2.3节中,我们了解了如何生成更大磁村的图像。在本节中,我们将关点转移到图像本身的“质量”上。

在2.3节中,生成的图像在细节部分变化还比较剧烈,而希望图像整体风格应该比较“柔和”。在图像处理算法中,有高频成分和低频成分的概念。简单的来讲,所谓高频成分,是指图像中灰度、颜色、明度变化比较剧烈的地方,如边缘、细节部分。而低频成分是指图像变化不大的地方,如大块色块、整体风格。第2.3节中生成的高频成分太多,而希望图像的低频成分应该多一些,这样生成的图像才会更加“柔和”。

如何让图像具有更多的低频成分而不是高频成分?一种方法是针对高频成分加入损失,这样图像在生成的时候就会因为加入损失的作用而发生改变。但加入损失会导致计算量和收敛步数的增大。此处采用另外一种方法:放大低频的梯度之前生成图像时,使用的梯度是统一的。如果可以对梯度做分解,将之分为“高频梯度”和“低频梯度”,再人为地去放大“低频的梯度”,就可以得到较为柔和的图像了。

在具体实践上,使用拉普拉斯金字塔(Laplacian Pyramid)对图像进行分解。这种算法可以把图片分解为多层,如图4-5所示。底层的levell 、level2
就对应图像的高频成分,而上层的level3 、level4 对应图像的低频成分。可以对梯度也作这样的分解。分解之后,对高频的梯度和低频的梯度都做标准化,可以让梯度的低频成分和高频成分差不多,表现在图像上就会增加图像的低频成分,从而提高生成图像的质量。通常称这种方法为拉普拉斯金字塔
梯度标准化( Laplacian Pyramid Gradient Normalization )。
这里写图片描述

拉普拉斯金字塔梯度标准化实现的代码如下:

k = np.float32([1, 4, 6, 4, 1])
k = np.outer(k, k)
k5x5 = k[:, :, None, None] / k.sum() * np.eye(3, dtype=np.float32)

# 这个函数将图像分为低频和高频成分
def lap_split(img):
    with tf.name_scope('split'):
        # 做过一次卷积相当于一次“平滑”,因此lo为低频成分
        lo = tf.nn.conv2d(img, k5x5, [1, 2, 2, 1], 'SAME')
        # 低频成分放缩到原始图像一样大小得到lo2,再用原始图像img减去lo2,就得到高频成分hi
        lo2 = tf.nn.conv2d_transpose(lo, k5x5 * 4, tf.shape(img), [1, 2, 2, 1])
        hi = img - lo2
    return lo, hi

# 这个函数将图像img分成n层拉普拉斯金字塔
def lap_split_n(img, n):
    levels = []
    for i in range(n):
        # 调用lap_split将图像分为低频和高频部分
        # 高频部分保存到levels中
        # 低频部分再继续分解
        img, hi = lap_split(img)
        levels.append(hi)
    levels.append(img)
    return levels[::-1]
# 将拉普拉斯金字塔还原到原始图像
def lap_merge(levels):
    img = levels[0]
    for hi in levels[1:]:
        with tf.name_scope('merge'):
            img = tf.nn.conv2d_transpose(img, k5x5 * 4, tf.shape(hi), [1, 2, 2, 1]) + hi
    return img


# 对img做标准化。
def normalize_std(img, eps=1e-10):
    with tf.name_scope('normalize'):
        std = tf.sqrt(tf.reduce_mean(tf.square(img)))
        return img / tf.maximum(std, eps)

# 拉普拉斯金字塔标准化
def lap_normalize(img, scale_n=4):
    img = tf.expand_dims(img, 0)
    tlevels = lap_split_n(img, scale_n)
    # 每一层都做一次normalize_std
    tlevels = list(map(normalize_std, tlevels))
    out = lap_merge(tlevels)
    return out[0, :, :, :]

先来看lap_split 和lap_split_n 。lap_split 可以把图像分解为高频成分和低频成分。冥中对原始图像做一次卷积就得到低频成分lo 。这里的卷积起到的作用就是“平滑”,以提取到图片中变化不大的部分。得到低频成分后,使用转置卷积将低频成分缩放到原图一样的大小102 ,再用原国img减去lo2就可以得到高频成分了。再来看函数lap_split_n, 它将图像分成n层的拉普拉斯金字塔,每次都调用lap_split 对当前图像进行分解,分解得到的高频成分就保存到金字塔levels中, 而低频成分则留待下一次分解。

lap_merge函数和normalize_std 函数比较简单。lap_merge函数的功能就是将一个分解好的拉普拉斯金字塔还原成原始图像,而normalize_std则是对图像进行标准化。

最后, lap_normalize就是将输入图像分解为拉普拉斯金字塔,然后调用
normalize_std对每一层进行标准化,输出为融合后的结果。

有了拉普拉斯金字塔标准化的函数后,就可以写出生成图像的代码:

def tffunc(*argtypes):
    # 将一个对Tensor定义的函数转换成一个证对numpy.ndarray定义的函数
    placeholders = list(map(tf.placeholder, argtypes))
    def wrap(f):
        out = f(*placeholders)
        def wrapper(*args, **kw):
            return out.eval(dict(zip(placeholders, args)), session=kw.get('session'))
        return wrapper
    return wrap


def render_lapnorm(t_obj, img0,
                   iter_n=10, step=1.0, octave_n=3, octave_scale=1.4, lap_n=4):
    # 同样定义目标和梯度
    t_score = tf.reduce_mean(t_obj)
    t_grad = tf.gradients(t_score, t_input)[0]
    # 将lap_normalize转换为正常函数
    lap_norm_func = tffunc(np.float32)(partial(lap_normalize, scale_n=lap_n))

    img = img0.copy()
    for octave in range(octave_n):
        if octave > 0:
            img = resize_ratio(img, octave_scale)
        for i in range(iter_n):
            g = calc_grad_tiled(img, t_grad)
            # 唯一的区别在于我们使用lap_norm_func来标准化g!
            g = lap_norm_func(g)
            img += g * step
            print('.', end=' ')
    savearray(img, 'lapnorm.jpg')

这里再一个tffunc 函数, 它d的功能是将一个对Tensor定义的函数转换成
一个正常的对numpy.ndarray 定义的函数。上面定义的lap_normalize的输入
参数是一个Tensor,而输出也是一个Tensor,利用tffunc函数可以将它变成
一个输入ndarray类型,输出也是ndarray类型的函数。这可能需要一定的
Python基础才能理解tffunc函数的定义, 初学者如果弄不明白可以跳过这个
部分, 只需要知道它的大致功能即可。

生成图像的render_lapnorm 函数和第2.3节中对应的render_multiscale
基本相同。唯一的区别在于对梯度g应用了拉普拉斯标准化函数
lap_ norm_ func 。最终生成图像的代码也与之前类似,只需要调用
render_ lapnorm 函数即可,有了以上函数,运行下面主程序:

if __name__ == '__main__':
    name = 'mixed4d_3x3_bottleneck_pre_relu'
    channel = 139
    img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0
    layer_output = graph.get_tensor_by_name("import/%s:0" % name)
    render_lapnorm(layer_output[:, :, :, channel], img_noise, iter_n=20)

这里写图片描述
与2.3节对比,本节确实在一定程度上提高了生成图像的质量。也可以更清楚地看到这个卷积层中的第139个通道学习到的图像特征。我们也可以尝试不同的通道,如channel=100时,可以生成如下图:
这里写图片描述

卷积层mixed4d_ 3x3 _bottleneck _pre re Lu 一共具有144 个通道,因此0~ 143通道中的任何channel值都是高效的。除了对单独的通道进行生成外,还可以对多个通道进行组合。如使用render_lapnorm(layer_output[:, :, :, 139]+layer_output[:, :, :, 100], img_noise, iter_n=20)
就可以生成如下图:
这里写图片描述

2.5 最终的Deep Dream模型

前面已经介绍了如何通过极大化卷积层某个通道的平均值来生成图像,
并学习了如何生成更大尺寸和更高质量的图像。最终的Deep Dream 模型还
需要对图片添加一个背景。具体应该怎么做呢?真实,之前是从image _noi se开始优化图像的,现在使用一张背景图像作为起点对图像进行优化就可以了。具体的代码如下:

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

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
    # 同样将图像进行金字塔分解
    # 此时提取高频、低频的方法比较简单。直接缩放就可以
    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))
            print('.', end=' ')

    img = img.clip(0, 255)
    savearray(img, 'deepdream.jpg')

读入图像‘test.jpg’,并将它作为起点,传递给函数render_deepdream为了保证图像生成的质量, render_deepdream 对图像也进行高频低频的分解。

分解的方法是直接缩小原因像,就得到低频成分lo ,真中缩放图像使用的函
数是resize ,它的参数hw 是一个元组( tuple ),用(h , w)的形式表示缩放后图像的高和宽。在生成图像的时候,从低频的图像开始。低频的图像实际上就是缩小后的图像,经过一定次数的迭代后,将它放大再加上原先的高频成分。计算梯度的方法同样使用的是calc_grad_tiled 方法。

运行如下主程序:

if __name__ == '__main__':
    img0 = PIL.Image.open('test.jpg')
    img0 = np.float32(img0)

    name = 'mixed4d_3x3_bottleneck_pre_relu'
    channel = 139
    layer_output = graph.get_tensor_by_name("import/%s:0" % name)
    render_deepdream(layer_output[:, :, :, channel], img0)

得到如下对比图:
这里写图片描述

利用下面的代码可以生成非常著名的含有动物的Deep Dream图片,此时的优化的目标是mixed4c 的全体输出。生成效果图如下(好有魔性):
这里写图片描述

我们还可以尝试不同的背景图像,不同的通道数, 不同的输出层,就可以得到各种各样的生成图像。

3. 总结

在这篇文章中,我们首先学习了Deep Dream 模型的基本原理,以及如何使用TensorFlow生成最原始的Deep Dream 图片,接着学习了如何生成更大尺寸、更高质量的图片,最后完成了一个最终版的Deep Dream模型。这个项目不仅非常高趣,而且还高助于理解卷积神经网络学习到的内部特征。

猜你喜欢

转载自blog.csdn.net/czp_374/article/details/81138934