Deep Dream是谷歌公司在2015年公布的一项有趣的技术。在训练好的卷积神经网络中,只须要设定几个参数,就能够经过这项技术生成一张图像。git
本文章的代码和图片都放在个人github上,想实现本文代码的同窗建议你们能够先把代码Download下来,再参考本文的解释,理解起来会更加方便。github
疑问:算法
设输入网络的图形为x,网络输出的各个类别的几率为$t$(1000维的向量,表明了1000种类别的几率),咱们以t[100]的某一类别为优化目标,不断地让神经网络去调整输入图像x的像素值,让输出t[100]尽量的大,最后获得下图图像。api
极大化某一类几率获得的图片网络
卷积的一个通道就能够表明一种学习到的“信息” 。以某一个通道的平均值做为优化目标,就能够弄清楚这个通道究竟学习到了什么,这也是Deep Dream 的基本原理。在下面的的小节中, 会以程序的形式,更详细地介绍如何生成并优化Deep Dream 图像。session
原始的Deep Dream模型只须要优化ImageNet模型卷积层某个通道的激活值就能够了,为此,应该先在TensorFlow导入一个ImageNet图像识别模型。这里以Inception模型为例进行介绍,对应程序的文件名为load_inception.py。app
如下是真正导入Inception模型。TensorFlow为提供了一种特殊的以“.pb”为扩展名的文件,能够事先将模型导入到pb文件中,再在须要的时候导出。对于Inception模型,对应的pb文件为tensorflow_inception_graph.pb。dom
# 建立图和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})
导入模型后,找出模型中全部的卷积层,并尝试输出某个卷积层的形状:ide
# 找到全部卷积层 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) # 由于不清楚输入图像的个数以及大小,因此前三维的值是不肯定的,显示为问号
咱们定义一个保存图像的函数,以便咱们把模型输出的数据保存为图像。函数
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,
确实能够经过最大化某一通道的平均值获得一些有意义的图像!此处图像的生成效果还不太好,
首先尝试生成更大尺寸的图像,在上面生成图像的尺寸是(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修改成更小的值。
咱们将关注点转移到“质量”上,上一节生成的图像在细节部分变化还比较剧烈,而但愿图像总体的风格应该比较“柔和”。
在图像处理算法中,有高频成分和低频成分的概念:
上图生成的高频成分太多,而咱们但愿图像的低频成分应该多一些,这样生成的图像才会更加“柔和”。
解决方法:
拉普拉斯金字塔(LaplacianPyramid)对图像进行分解。这种算法能够把图片分解为多层,底层的level一、level2对应图像的高频成分,上层的level三、level4对应图像的低频成分。
咱们能够对梯度也作拉普拉斯金字塔分解。分解以后,对高频的梯度和低频的梯度都作标准化,可让梯度的低频成分和高频成分差很少,表如今图像上就会增长图像的低频成分,从而提升生成图像的质量。一般称这种方法为拉普拉斯金字塔梯度标准化(Laplacian Pyramid GradientNormalization)。
下面是拉普拉斯金字塔梯度标准化实现的代码,代码我已经详细注释,实现流程
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, :, :, :]
函数解释:
有了拉普拉斯金字塔标准化的函数后,就能够写出生成图像的代码:
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模型还须要对图片添加一个背景。
其实以前是从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方法。
左图为原始的test.jpg图片,右图为生成的Deep Dream图片
利用下面的代码能够生成很是著名的含有动物的DeepDream图片,此时优化的目标是mixed4c的全体输出。
name = "mixed4c" layer_optput = graph.get_tensor_by_name("import/%s:0" % name) render_deepdream(tf.square(layer_optput), img0)
你们能够自行尝试不一样的背景图像,不一样的通道数,不一样的输出层,就能够获得各类各样的生成图像。