21个项目玩转深度学习:基于TensorFlow的实践详解
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

4.2 TensorFlow中的Deep Dream模型实践本节的代码参考了TensorFlow源码中的示例程序https://github.com/tensorflow/tensorflow/tree/master_/tensorflow/examples/tutorials/deepdream,并做了适当修改。

4.2.1 导入Inception模型

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

首先导入要用到的一些基本模块(语句from__future__import print_function是为了在python2、python3中可以使用互相兼容的print函数):

    # coding:utf-8
    # 导入要用到的基本模块
    from__future__import print_function
    import numpy as np
    import tensorflow as tf

再创建基本的图和会话:

    # 创建图和会话
    graph=tf.Graph()
    sess=tf.InteractiveSession(graph=graph)

以上都是一些基本的准备工作,下面开始真正地导入Inception模型。TensorFlow为提供了一种特殊的以“.pb”为扩展名的文件,可以事先将模型导入到pb文件中,再在需要的时候导出。对于Inception模型,对应的pb文件为tensorflow_inception_graph.pb网站https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip提供了tensorflow_inception_graph.pb文件的下载。本书也在chapter_4_data/里向读者提供了这个文件。该文件需要放到项目根目录中。

使用下面的程序就可以把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)打印出所有层的名称。

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

4.2.2 生成原始的Deep Dream图像

本节对应的程序为gen_naive.py,它可以生成原始的Deep Dream图片。gen_naive.py的开头同样是导入Inception模型,这与第4.2.1节中一致,不再赘述。除了导入Inception模型外,还定义了一个保存图片的函数,它可以把一个numpy.ndarray保存成文件的形式。(保存图像其实在第1.1.2节中做过,使用的方法和本节也是一样的):

    def savearray(img_array, img_name):
      scipy.misc.toimage(img_array).save(img_name)
      print('img saved: %s' % img_name)

程序的主要部分如下:

    # 定义卷积层、通道数,并取出对应的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_output。在第4.2.1节中,已经知道它的格式为(? , ? , ? , 144)。这里任意选择一个通道进行最大化,如设定channel=139,最后调用渲染函数render_naive的时候传递layer_output[:, :, :, 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_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即可。

运行gen_naive.py会得到类似下面的输出:

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

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

图4-3 运行gen_naive.py生成的图像

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

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

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

本节对应的程序是gen_multiscale.py,它可以生成更大尺寸的Deep Dream图像。这个程序中的函数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是用来生成大尺寸图像的函数。相比第4.2.2节的gen_naive函数,它又多出了两个参数octave_n和octave_scale。先生成小尺寸的图像,然后调用resize_ratio将小尺寸图像放大octave_scale倍,再使用放大后的图像作为初值进行计算。这样的放大一共会进行octave_n-1次。换句话说,octave_n越大,最后生成的图像就会越大,默认的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)

运行gen_multiscale.py后,会生成一张multiscale.jpg图像,如图4-4所示。

图4-4 运行gen_multiscale.py生成的更大尺度的图像

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

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

在第4.2.3节中,学习了如何将生成大尺寸的图像。在本节中,将关注点转移到图像本身的“质量”上。对应的参考代码是gen_lapnorm.py。

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

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

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

图4-5 图像的拉普拉斯金字塔分解

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

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

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

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

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

    def tffunc(*argtypes):
      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函数,它的功能是将一个对Tensor定义的函数转换成一个正常的对numpy.ndarray定义的函数。上面定义的lap_normalize的输入参数是一个Tensor,而输出也是一个Tensor,利用tffunc函数可以将它变成一个输入ndarray类型,输出也是ndarray类型的函数。读者可能需要一定的Python基础才能理解tffunc函数的定义,初学者如果弄不明白可以跳过这个部分,只需要知道它的大致功能即可。

生成图像的render_lapnorm函数和第4.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)

使用python gen_lapnorm.py命令运行gen_lapnorm.py后,就可以在当前目录下生成图像lapnorm.jpg,如图4-6所示。

图4-6 拉普拉斯金字塔标准化后得到的DeepDream图片(详见彩插)

与第4.2.3节对比,本节确实在一定程度上提高了生成图像的质量。也可以更清楚地看到这个卷积层中的第139个通道学习到的图像特征。读者可以尝试不同的通道,如channel=99时,可以生成图4-7a所示的图像。卷积层mixed4d_3x3_bottleneck_pre_relu一共具有144个通道,因此0~143通道中的任何channel值都是有效的。除了对单独的通道进行生成外,还可以对多个通道进行组合。如使用render_lapnorm(layer_output[:, :, :, 139]+layer_output[:, :, :, 99], img_noise, iter_n=20),就可以组合第139个通道和第99个通道,生成图4-7b所示的图片。读者可以自由尝试更多组合。

图4-7 使用不同通道数运行gen_lapnorm.py生成的图像

4.2.5 最终的Deep Dream模型

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

    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')

    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, iter_n=150)

读入图像‘test.jpg',并将它作为起点,传递给函数render_deepdream。为了保证图像生成的质量,render_deepdream对图像也进行高频低频的分解。分解的方法是直接缩小原图像,就得到低频成分lo,其中缩放图像使用的函数是resize,它的参数hw是一个元组(tuple),用(h, w)的形式表示缩放后图像的高和宽。

在生成图像的时候,从低频的图像开始。低频的图像实际上就是缩小后的图像,经过一定次数的迭代后,将它放大再加上原先的高频成分。计算梯度的方法同样使用的是calc_grad_tiled方法。

运行程序gen_deepdream.py,就可以得到最终的Deep Dream图片了。如图4-8a所示为原始的test.jpg图片,图4-8b所示为生成的Deep Dream图片。

图4-8 运行gen_deepdream.py生成的图片

利用下面的代码可以生成非常著名的含有动物的Deep Dream图片,此时优化的目标是mixed4c的全体输出。

      name='mixed4c'
      layer_output=graph.get_tensor_by_name("import/%s:0" % name)
      render_deepdream(tf.square(layer_output), img0)

生成效果如图4-9所示。

图4-9 带有背景的DeepDream的图片(详见彩插)

读者可以自行尝试不同的背景图像,不同的通道数,不同的输出层,就可以得到各种各样的生成图像。