Deep Dream 是谷歌公司在 2015 年公布的一项有趣的技术. 在训练好的卷积神经网络中, 只需要设定几个参数, 就可以通过这项技术生成一张图像.
本文章的代码和图片都放在我的 GitHub 上, 想实现本文代码的同学建议大家可以先把代码 Download 下来, 再参考本文的解释, 理解起来会更加方便.
疑问:
卷积层究竟学习到了什么内容?
卷积层的参数代表的意义是什么?
浅层的卷积和深层的卷积学习到的内容有哪些区别?
设输入网络的图形为 x, 网络输出的各个类别的概率为 $t$(1000 维的向量, 代表了 1000 种类别的概率), 我们以 t[100]的某一类别为优化目标, 不断地让神经网络去调整输入图像 x 的像素值, 让输出 t[100]尽可能的大, 最后得到下图图像.
极大化某一类概率得到的图片
卷积的一个通道就可以代表一种学习到的 "信息" . 以某一个通道的平均值作为优化目标, 就可以弄清楚这个通道究竟学习到了什么, 这也是 Deep Dream 的基本原理. 在下面的的小节中, 会以程序的形式, 更详细地介绍如何生成并优化 Deep Dream 图像.
TensorFlow 中的 Deep Dream 模型
导入 Inception 模型
原始的 Deep Dream 模型只需要优化 ImageNet 模型卷积层某个通道的激活值就可以了, 为此, 应该先在 TensorFlow 导入一个 ImageNet 图像识别模型. 这里以 Inception 模型为例进行介绍, 对应程序的文件名为 load_inception.py.
以下是真正导入 Inception 模型. TensorFlow 为提供了一种特殊的以 ".pb" 为扩展名的文件, 可以事先将模型导入到 pb 文件中, 再在需要的时候导出. 对于 Inception 模型, 对应的 pb 文件为 tensorflow_inception_graph.pb.
- InceptionV1 model
- InceptionV3 model
- # 创建图和 Session
- graph = tf.Graph()
- sess = tf.InteractiveSession(graph=graph)
- # 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(tf.float32, name='input')
- imagenet_mean = 117.0 # 图片像素值的 均值
- # 输入图像需要经过处理才能送入网络中
- # expand_dims 是加一维, 从 [height, width, channel] 变成[1, height, width, channel]
- # 因为 Inception 模型输入格式是(batch, height, width,channel).
- t_preprocessed = tf.expand_dims(t_input - imagenet_mean, 0)
- # 将数据导入模型
- 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)) # Number of layers 59
- # 特别地, 输出 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())))
- # shape of mixed4d_3x3_bottleneck_pre_relu: (?, ?, ?, 144)
- # 因为不清楚输入图像的个数以及大小, 所以前三维的值是不确定的, 显示为问号
生成原始的 Deep Dream 图像
我们定义一个保存图像的函数, 以便我们把模型输出的数据保存为图像.
- def savearray(img_array, img_name):
- """把 numpy.ndarray 保存图片"""
- scipy.misc.toimage(img_array).save(img_name)
- print('img saved: %s' % img_name)
输入图像, 生成某一通道图像
- # 定义卷积层, 通道数, 并取出对应的 tensor
- name = 'mixed4d_3x3_bottleneck_pre_relu'
- layer_output = graph.get_tensor_by_name("import/%s:0" % name) # 该层输出为(? , ?, ? , 144)
- # 因此 channel 可以取 0~143 中的任何一个整数值
- channel = 139
- # 定义原始的图像噪声 作为初始的图像优化起点
- img_noise = np.random.uniform(size=(224, 224, 3)) + 100.0
- # 调用 render_naive 函数渲染
- render_naive(layer_output[:, :, :, channel], img_noise, iter_n=20)
计算梯度, 不断迭代渲染初始图片
- def render_naive(t_obj, img0, iter_n=20, step=1.0):
- """ 通过调整输入图像 t_input, 来让优化目标 t_score 尽可能的大
- :param t_obj: 卷积层某个通道的值
- :param img0: 初始化噪声图像
- :param iter_n: 迭代数
- :param step: 学习率
- """
- # t_score 是优化目标. 它是 t_obj 的平均值
- # t_score 越大, 就说明神经网络卷积层对应通道的平均激活越大
- 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')
经过 20 次迭代后, 会把图像保存为 naive.jpg,
确实可以通过最大化某一通道的平均值得到一些有意义的图像! 此处图像的生成效果还不太好,
生产更大尺寸的 Deep Dream 图像
首先尝试生成更大尺寸的图像, 在上面生成图像的尺寸是(224, 224, 3), 这正是传递的 img_noise 的大小. 如果传递更大的 img_noise, 就可以生成更大的图片.
产生问题: 会占用更大的内存(或显存), 若想生成特别大的图片, 就会因为内存不足而导致渲染失败.
解决办法: 把图片分成几个部分, 每次只对图片的一个部分做优化, 这样每次优化时只会消耗固定大小的内存.
- def calc_grad_tiled(img, t_grad, tile_size=512):
- """ 可以对任意大小的图像计算梯度
- :param img: 初始化噪声图片
- :param t_grad: 优化目标 (score) 对输入图片的梯度
- :param tile_size: 每次只对 tile_size*tile_size 大小的图像计算梯度, 避免内存问题
- :return: 返回梯度更新后的图像
- """
- sz = tile_size # 512
- h, w = img.shape[:2]
- # 防止在 tile 的边缘产生边缘效应对图片进行整体移动
- # 产生两个 (0,sz] 之间均匀分布的整数值
- sx, sy = np.random.randint(sz, size=2)
- # 先在水平方向滚动 sx 个位置, 再在垂直方向上滚动 sy 个位置
- img_shift = np.roll(np.roll(img, sx, 1), sy, 0)
- grad = np.zeros_like(img)
- # x, y 是开始位置的像素
- 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)
在实际工程中, 为了加快图像的收敛速度, 采用先生成小尺寸, 再将图片放大的方法
- def resize_ratio(img, ratio):
- """将图片 img 放大 ratio 倍"""
- min = img.min() # 图片的最小值
- max = img.max() # 图片的最大值
- img = (img - min) / (max - min) * 255 # 归一化
- # 把输出缩放为 0~255 之间的数
- print("魔", img.shape)
- img = np.float32(scipy.misc.imresize(img, ratio))
- print("鬼", img.shape)
- 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):
- """ 生成更大尺寸的图像
- :param t_obj: 卷积层某个通道的值
- :param img0: 初始化噪声图像
- :param iter_n: 迭代数
- :param step: 学习率
- :param octave_n: 放大一共会进行 octave_n-1 次
- :param octave_scale: 图片放大倍数, 大于 1 的 "浮点数" 则会变成原来的倍数! 整数会变成百分比
- :return:
- """
- # 同样定义目标和梯度
- t_score = tf.reduce_mean(t_obj) # 定义优化目标
- t_grad = tf.gradients(t_score, t_input)[0] # 计算 t_score 对 t_input 的梯度
- img = img0.copy()
- print("原始尺寸",img.shape)
- for octave in range(octave_n):
- if octave> 0:
- # 将小图片放大 octave_scale 倍
- # 共放大 octave_n - 1 次
- print("前", img.shape)
- img = resize_ratio(img, octave_scale)
- print("后", img.shape)
- for i in range(iter_n):
- # 调用 calc_grad_tiled 计算任意大小图像的梯度
- g = calc_grad_tiled(img, t_grad) # 对图像计算梯度
- g /= g.std() + 1e-8
- img += g * step
- savearray(img, 'multiscale.jpg')
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)
此时可以看到, 卷积层 "mixed4d_3x3_bottleneck_pre_rel" 的第 139 个通道实际上就是学习到了某种花朵的特征, 如果输入这种花朵的图像, 它的激活值就会达到最大. 大家还可以调整 octave_n 为更大的值, 就可以生成更大的图像. 不管最终图像的尺寸是多大, 始终只会对 512 * 512 像素的图像计算梯度, 因此内存始终是够用的. 如果在读者的环境中, 计算 512 * 512 的图像的梯度会造成内存问题, 可以将函数中 tile_size 修改为更小的值.
生成更高质量的 Deep Dream 图像
我们将关注点转移到 "质量" 上, 上一节生成的图像在细节部分变化还比较剧烈, 而希望图像整体的风格应该比较 "柔和".
在图像处理算法中, 有高频成分和低频成分的概念:
高频成分: 图像中灰度, 颜色, 明度变化比较剧烈的地方, 如边缘, 细节部分
低频成分: 图像变化不大的地方, 如大块色块, 整体风格
上图生成的高频成分太多, 而我们希望图像的低频成分应该多一些, 这样生成的图像才会更加 "柔和".
解决方法:
对高频成分加入损失. 这样图像在生成的时候就因为新加入损失的作用而发生改变. 但加入损失会导致计算量和收敛步数的增加.
放大低频的梯度. 之前生成图像时, 使用的梯度是统一的. 如果可以对梯度作分解, 将之分为 "高频梯度"" 低频梯度 ", 再人为地去放大" 低频梯度 ", 就可以得到较为柔和的图像了.
拉普拉斯金字塔 (LaplacianPyramid) 对图像进行分解. 这种算法可以把图片分解为多层, 底层的 level1,level2 对应图像的高频成分, 上层的 level3,level4 对应图像的低频成分.
我们可以对梯度也做拉普拉斯金字塔分解. 分解之后, 对高频的梯度和低频的梯度都做标准化, 可以让梯度的低频成分和高频成分差不多, 表现在图像上就会增加图像的低频成分, 从而提高生成图像的质量. 通常称这种方法为拉普拉斯金字塔梯度标准化(Laplacian Pyramid GradientNormalization).
下面是拉普拉斯金字塔梯度标准化实现的代码, 代码我已经详细注释, 实现流程
首先将原始图片分解成 n-1 个高频成分, 和 1 个低频成分
然后对每层都进行标准化
将标准化后的高频成分和低频成分相加
- k = np.float32([1, 4, 6, 4, 1])
- k = np.outer(k, k) # 计算两个向量的外积(5, 5)
- k5x5 = k[:, :, None, None] / k.sum() * np.eye(3, dtype=np.float32) # (5, 5, 3, 3)
- # 这个函数将图像分为低频成分和高频成分
- def lap_split(img):
- with tf.name_scope('split'):
- # 做过一次卷积相当于一次 "平滑", 因此 lo 为低频成分
- # filter=k5x5=[filter_height, filter_width, in_channels, out_channels]
- lo = tf.nn.conv2d(img, k5x5, [1, 2, 2, 1], 'SAME')
- # 低频成分放缩到原始图像一样大小
- # value,filter,output_shape,strides
- lo2 = tf.nn.conv2d_transpose(lo, k5x5 * 4, tf.shape(img), [1, 2, 2, 1])
- # 用原始图像 img 减去 lo2, 就得到高频成分 hi
- 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'):
- # value,filter,output_shape,strides
- # 卷积后变成低频, 转置卷积将低频还原成图片的高频
- 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)))
- # 返回的是 a, b 之间的最大值
- 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 函数: 可以把图像分解为高频成分和低频成分. 其中对原始图像做一次卷积就得到低频成分 lo. 这里的卷积起到的作用就是 "平滑", 以提取到图片中变化不大的部分. 得到低频成分后, 使用转置卷积将低频成分缩放到原图一样的大小 lo2, 再用原图 img 减去 lo2 就可以得到高频成分了.
lap_split_n 函数: 它将图像分成 n 层的拉普拉斯金字塔, 每次都调用 lap_split 对当前图像进行分解, 分解得到的高频成分就保存到金字塔 levels 中, 而低频成分则留待下一次分解.
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):
- """
- :param t_obj: 目标分数, 某一通道的输出值 layer_output[:,:,:,channel] (?, ?, ?, 144)
- :param img0: 输入图片, 噪声图像 size=(224, 224, 3)
- :param iter_n: 迭代次数
- :param step: 学习率
- """
- t_score = tf.reduce_mean(t_obj) # 定义优化目标
- t_grad = tf.gradients(t_score, t_input)[0] # 定义梯度
- # 将 lap_normalize 转换为正常函数, partial: 冻结函数一个参数
- 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 类型的函数.
最终生成图像的代码也与之前类似, 只需要调用 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)
与上节对比, 本节确实在一定程度上提高了生成图像的质量. 也可以更清楚地看到这个卷积层中的第 139 个通道学习到的图像特征. 大家可以尝试不同的通道.
最终的 Deep Dream 模型
前面我们分别介绍了如何通过极大化卷积层某个通道的平均值来生成图像, 并学习了如何生成大尺寸和更高质量的图像. 最终的 Deep Dream 模型还需要对图片添加一个背景.
其实之前是从 image_noise 开始优化图像的, 现在使用一张背景图像作为起点对图像进行优化就可以了.
- def resize(img, hw):
- # 参数 hw 是一个元组 (tuple), 用(h, w) 的形式表示缩放后图像的高和宽.
- 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
- ef 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
- 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):
- # 0 1 2 3
- 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, 'deepdream1.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)
这里改了 3 个部分, 读入图像'test.jpg', 并将它作为起点, 传递给函数 render_deepdream. 为了保证图像生成的质量, render_deepdream 对图像也进行高频低频的分解. 分解的方法是直接缩小原图像, 就得到低频成分 lo, 其中缩放图像使用的函数是 resize, 它的参数 hw 是一个元组 (tuple), 用(h, w) 的形式表示缩放后图像的高和宽.
在生成图像的时候, 从低频的图像开始. 低频的图像实际上就是缩小后的图像, 经过一定次数的迭代后, 将它放大再加上原先的高频成分. 计算梯度的方法同样使用的是 calc_grad_tiled 方法.
左图为原始的图片, 右图为生成的 Deep Dream 图片
利用下面的代码可以生成非常著名的含有动物的 DeepDream 图片, 此时优化的目标是 mixed4c 的全体输出.
- name = "mixed4c"
- layer_optput = graph.get_tensor_by_name("import/%s:0" % name)
- render_deepdream(tf.square(layer_optput), img0)
大家可以自行尝试不同的背景图像, 不同的通道数, 不同的输出层, 就可以得到各种各样的生成图像.
参考
21 个项目玩转深度学习: 基于 TensorFlow 的实践详解 https://book.douban.com/subject/30179607/
来源: https://www.cnblogs.com/LXP-Never/p/12691957.html