Deep Dream模型與實現
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方法。
左圖為原始的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的實踐