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。

# 創建圖和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)。

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

  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的實踐詳解