Deep Dream模型與實現

  Deep Dream是谷歌公司在2015年公佈的一項有趣的技術。在訓練好的卷積神經網絡中,只須要設定幾個參數,就能夠經過這項技術生成一張圖像。git

  本文章的代碼和圖片都放在個人github上,想實現本文代碼的同窗建議你們能夠先把代碼Download下來,再參考本文的解釋,理解起來會更加方便。github

疑問:算法

  • 卷積層究竟學習到了什麼內容?
  • 卷積層的參數表明的意義是什麼?
  • 淺層的卷積和深層的卷積學習到的內容有哪些區別?

  設輸入網絡的圖形爲x,網絡輸出的各個類別的機率爲$t$(1000維的向量,表明了1000種類別的機率),咱們以t[100]的某一類別爲優化目標,不斷地讓神經網絡去調整輸入圖像x的像素值,讓輸出t[100]儘量的大,最後獲得下圖圖像。api

極大化某一類機率獲得的圖片網絡

  卷積的一個通道就能夠表明一種學習到的「信息」 。以某一個通道的平均值做爲優化目標,就能夠弄清楚這個通道究竟學習到了什麼,這也是Deep Dream 的基本原理。在下面的的小節中, 會以程序的形式,更詳細地介紹如何生成並優化Deep Dream 圖像。session

TensorFlow中的Deep Dream模型

導入Inception模型

  原始的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)
# 由於不清楚輸入圖像的個數以及大小,因此前三維的值是不肯定的,顯示爲問號

生成原始的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)對圖像進行分解。這種算法能夠把圖片分解爲多層,底層的level一、level2對應圖像的高頻成分,上層的level三、level4對應圖像的低頻成分

  咱們能夠對梯度也作拉普拉斯金字塔分解。分解以後,對高頻的梯度和低頻的梯度都作標準化,可讓梯度的低頻成分和高頻成分差很少,表如今圖像上就會增長圖像的低頻成分,從而提升生成圖像的質量。一般稱這種方法爲拉普拉斯金字塔梯度標準化(Laplacian Pyramid GradientNormalization)。

  下面是拉普拉斯金字塔梯度標準化實現的代碼,代碼我已經詳細註釋,實現流程

  1. 首先將原始圖片分解成n-1個高頻成分,和1個低頻成分
  2. 而後對每層都進行標準化
  3. 將標準化後的高頻成分和低頻成分相加
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的實踐詳解

相關文章
相關標籤/搜索