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
# 創建圖和Sessiongraph = 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)
輸入影像,生成某一通道影像
# 定義卷積層、通道數,并取出對應的tensorname = '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 imgdef 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 wrapdef 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 imgef 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)

大家可以自行嘗試不同的背景影像,不同的通道數,不同的輸出層,就可以得到各種各樣的生成影像,
參考
21個專案玩轉深度學習:基于TensorFlow的實踐詳解
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/36687.html
標籤:其他
上一篇:matlab2014a安裝問題
