1. 程式人生 > >python Deep learning 學習筆記(10)

python Deep learning 學習筆記(10)

並且 () 想要 res valid else 示例 variable enc

生成式深度學習

機器學習模型能夠對圖像、音樂和故事的統計潛在空間(latent space)進行學習,然後從這個空間中采樣(sample),創造出與模型在訓練數據中所見到的藝術作品具有相似特征的新作品

使用 LSTM 生成文本

生成序列數據

用深度學習生成序列數據的通用方法,就是使用前面的標記作為輸入,訓練一個網絡(通常是循環神經網絡或卷積神經網絡)來預測序列中接下來的一個或多個標記。例如,給定輸入the cat is on the ma,訓練網絡來預測目標 t,即下一個字符。與前面處理文本數據時一樣,標記(token)通常是單詞或字符,給定前面的標記,能夠對下一個標記的概率進行建模的任何網絡都叫作語言模型(language model)。語言模型能夠捕捉到語言的潛在空間(latent space),即語言的統計結構

一旦訓練好了這樣一個語言模型,就可以從中采樣(sample,即生成新序列)。向模型中輸入一個初始文本字符串[即條件數據(conditioning data)],要求模型生成下一個字符或下一個單詞(甚至可以同時生成多個標記),然後將生成的輸出添加到輸入數據中,並多次重復這一過程。這個循環可以生成任意長度的序列,這些序列反映了模型訓練數據的結構,它們與人類書寫的句子幾乎相同

使用語言模型逐個字符生成文本的過程
技術分享圖片

采樣策略

生成文本時,如何選擇下一個字符至關重要。一種簡單的方法是貪婪采樣(greedy sampling),就是始終選擇可能性最大的下一個字符。但這種方法會得到重復的、可預測的字符串,看起來不像是連貫的語言。一種更有趣的方法是做出稍顯意外的選擇:在采樣過程中引入隨機性,即從下一個字符的概率分布中進行采樣。這叫作隨機采樣(stochastic sampling,stochasticity 在這個領域中就是“隨機”的意思)。在這種情況下,根據模型結果,如果下一個字符是 e 的概率為0.3,那麽你會有 30% 的概率選擇它

從模型的 softmax 輸出中進行概率采樣是一種很巧妙的方法,它甚至可以在某些時候采樣到不常見的字符,從而生成看起來更加有趣的句子,而且有時會得到訓練數據中沒有的、聽起來像是真實存在的新單詞,從而表現出創造性。但這種方法有一個問題,就是它在采樣過程中無法控制隨機性的大小

為了在采樣過程中控制隨機性的大小,我們引入一個叫作 softmax 溫度(softmax temperature)的參數,用於表示采樣概率分布的熵,即表示所選擇的下一個字符會有多麽出人意料或多麽可預測。給定一個 temperature 值,將按照下列方法對原始概率分布(即模型的 softmax 輸出)進行重新加權,計算得到一個新的概率分布

import numpy as np
def reweight_distribution(original_distribution, temperature=0.5): 
    #original_distribution 是概率值組成的一維 Numpy 數組,這些概率值之和必須等於 1。temperature 是一個因子,用於定量描述輸出分布的熵
    distribution = np.log(original_distribution) / temperature
    distribution = np.exp(distribution)
    return distribution / np.sum(distribution)

更高的溫度得到的是熵更大的采樣分布,會生成更加出人意料、更加無結構的生成數據,而更低的溫度對應更小的隨機性,以及更加可預測的生成數據

對同一個概率分布進行不同的重新加權。更低的溫度 = 更確定,更高的溫度 = 更隨機
技術分享圖片

實現字符級的 LSTM 文本生成

首先下載語料,並將其轉換為小寫。接下來,我們要提取長度為 maxlen 的序列(這些序列之間存在部分重疊),對它們進行one-hot 編碼,然後將其打包成形狀為 (sequences, maxlen, unique_characters) 的三維Numpy 數組。與此同時,還需要準備一個數組 y,其中包含對應的目標,即在每一個所提取的序列之後出現的字符 ,下一步,構建網絡。最後訓練語言模型並從中采樣

給定一個訓練好的模型和一個種子文本片段,我們可以通過重復以下操作來生成新的文本

  1. 給定目前已生成的文本,從模型中得到下一個字符的概率分布
  2. 根據某個溫度對分布進行重新加權
  3. 根據重新加權後的分布對下一個字符進行隨機采樣
  4. 將新字符添加到文本末尾

demo

import keras
import numpy as np
from keras import layers
import random
import sys


path = keras.utils.get_file(‘nietzsche.txt‘, origin=‘https://s3.amazonaws.com/text-datasets/nietzsche.txt‘)
# 將語料轉為小寫
text = open(path).read().lower()
print(‘Corpus length:‘, len(text))

maxlen = 60
step = 3
sentences = []
next_chars = []
for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_chars.append(text[i + maxlen])
print(‘Number of sequences:‘, len(sentences))
# 語料中唯一字符組成的列表
chars = sorted(list(set(text)))
print(‘Unique characters:‘, len(chars))
# 將唯一字符映射為它在列表 chars 中的索引
char_indices = dict((char, chars.index(char)) for char in chars)

print(‘Vectorization...‘)
# 將字符 one-hot 編碼為二進制數組
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
    for t, char in enumerate(sentence):
        x[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1

# 用於預測下一個字符的單層 LSTM 模型
model = keras.models.Sequential()
model.add(layers.LSTM(128, input_shape=(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation=‘softmax‘))

optimizer = keras.optimizers.RMSprop(lr=0.01)
model.compile(loss=‘categorical_crossentropy‘, optimizer=optimizer)


# 模型預測,采樣下一個字符的函數
def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype(‘float64‘)
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)


for epoch in range(1, 41):
    print(‘epoch‘, epoch)
    model.fit(x, y, batch_size=128, epochs=1)
    start_index = random.randint(0, len(text) - maxlen - 1)
    generated_text = text[start_index: start_index + maxlen]
    print(‘--- Generating with seed: "‘ + generated_text + ‘"‘)
    for temperature in [0.2, 0.5, 1.0, 1.2]:
        print(‘------ temperature:‘, temperature)
        sys.stdout.write(generated_text)
        for i in range(400):
            sampled = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(generated_text):
                sampled[0, t, char_indices[char]] = 1.
        preds = model.predict(sampled, verbose=0)[0]
        next_index = sample(preds, temperature)
        next_char = chars[next_index]
        generated_text += next_char
        generated_text = generated_text[1:]
        sys.stdout.write(next_char)

結果
技術分享圖片

由訓練結果可以看出,,較小的溫度值會得到極端重復和可預測的文本,但局部結構是非常真實的,特別是所有單詞都是真正的英文單詞(單詞就是字符的局部模式)。隨著溫度值越來越大,生成的文本也變得更有趣、更出人意料,甚至更有創造性,它有時會創造出全新的單詞,聽起來有幾分可信。對於較大的溫度值,局部模式開始分解,大部分單詞看起來像是半隨機的字符串。毫無疑問,在這個特定的設置下,0.5 的溫度值生成的文本最為有趣。一定要嘗試多種采樣策略!在學到的結構與隨機性之間,巧妙的平衡能夠讓生成的序列非常有趣

利用更多的數據訓練一個更大的模型,並且訓練時間更長,生成的樣本會更連貫、更真實。但是,不要期待能夠生成任何有意義的文本,除非是很偶然的情況。你所做的只是從一個統計模型中對數據進行采樣,這個模型是關於字符先後順序的模型

DeepDream

DeepDream 是一種藝術性的圖像修改技術,它用到了卷積神經網絡學到的表示。DeepDream 算法與的卷積神經網絡過濾器可視化技術幾乎相同,都是反向運行一個卷積神經網絡:對卷積神經網絡的輸入做梯度上升,以便將卷積神經網絡靠頂部的某一層的某個過濾器激活最大化。DeepDream 使用了相同的想法,但有以下這幾個簡單的區別

  1. 使用 DeepDream,我們嘗試將所有層的激活最大化,而不是將某一層的激活最大化,因此需要同時將大量特征的可視化混合在一起
  2. 不是從空白的、略微帶有噪聲的輸入開始,而是從現有的圖像開始,因此所產生的效果能夠抓住已經存在的視覺模式,並以某種藝術性的方式將圖像元素扭曲
  3. 輸入圖像是在不同的尺度上[叫作八度(octave)]進行處理的,這可以提高可視化的質量

DeepDream 過程:空間處理尺度的連續放大(八度)與放大時重新註入細節
技術分享圖片

對於每個連續的尺度,從最小到最大,我們都需要在當前尺度運行梯度上升,以便將之前定義的損失最大化。每次運行完梯度上升之後,將得到的圖像放大 40%。在每次連續的放大之後(圖像會變得模糊或像素化),為避免丟失大量圖像細節,我們可以使用一個簡單的技巧:每次放大之後,將丟失的細節重新註入到圖像中。這種方法是可行的,因為我們知道原始圖像放大到這個尺寸應該是什麽樣子。給定一個較小的圖像尺寸 S 和一個較大的圖像尺寸 L,你可以計算將原始圖像大小調整為 L 與將原始圖像大小調整為 S 之間的區別,這個區別可以定量描述從 S 到 L 的細節損失

我們可以選擇任意卷積神經網絡來實現 DeepDream, 不過卷積神經網絡會影響可視化的效果,因為不同的卷積神經網絡架構會學到不同的特征。接下來將使用 Keras 內置的 Inception V3模型來夠生成漂亮的 DeepDream 圖像

步驟如下

  1. 加載預訓練的 Inception V3 模型
  2. 計算損失(loss),即在梯度上升過程中需要最大化的量

    將多個層的所有過濾器的激活同時最大化。具體來說,就是對一組靠近頂部的層激活的 L2 範數進行加權求和,然後將其最大化。選擇哪些層(以及它們對最終損失的貢獻)對生成的可視化結果具有很大影響,所以我們希望讓這些參數變得易於配置。更靠近底部的層生成的是幾何圖案,而更靠近頂部的層生成的則是從中能夠看出某些 ImageNet 類別(比如鳥或狗)的圖案

  3. (2)設置 DeepDream 配置
  4. (2)定義需要最大化的損失
  5. 設置梯度上升過程
  6. 在多個連續尺度上運行梯度上升

demo

from keras.applications import inception_v3
from keras import backend as K
import numpy as np
import scipy
from keras.preprocessing import image


def resize_img(img, size):
    img = np.copy(img)
    factors = (1, float(size[0]) / img.shape[1], float(size[1]) / img.shape[2], 1)
    return scipy.ndimage.zoom(img, factors, order=1)


def save_img(img, fname):
    pil_img = deprocess_image(np.copy(img))
    scipy.misc.imsave(fname, pil_img)


def preprocess_image(image_path):
    img = image.load_img(image_path)
    img = image.img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = inception_v3.preprocess_input(img)
    return img


def deprocess_image(x):
    if K.image_data_format() == ‘channels_first‘:
        x = x.reshape((3, x.shape[2], x.shape[3]))
        x = x.transpose((1, 2, 0))
    else:
        x = x.reshape((x.shape[1], x.shape[2], 3))
    x /= 2.
    x += 0.5
    x *= 255.
    x = np.clip(x, 0, 255).astype(‘uint8‘)
    return x


# 這個命令會禁用所有與訓練有關的操作
K.set_learning_phase(0)
# 構建不包括全連接層的 Inception V3網絡。使用預訓練的 ImageNet 權重來加載模型
model = inception_v3.InceptionV3(weights=‘imagenet‘, include_top=False)

# 將層的名稱映射為一個系數,這個系數定量表示該層激活對你要最大化的損失的貢獻大小
layer_contributions = {
    ‘mixed2‘: 0.2,
    ‘mixed3‘: 3.,
    ‘mixed4‘: 2.,
    ‘mixed5‘: 1.5,
}

# 定義最大化的損失
layer_dict = dict([(layer.name, layer) for layer in model.layers])
loss = K.variable(0.)
for layer_name in layer_contributions:
    coeff = layer_contributions[layer_name]
    activation = layer_dict[layer_name].output
    scaling = K.prod(K.cast(K.shape(activation), ‘float32‘))
    # 將該層特征的L2範數添加到loss中。為了避免出現邊界偽影,損失中僅包含非邊界的像素
    loss += coeff * K.sum(K.square(activation[:, 2: -2, 2: -2, :])) / scaling

# 梯度上升過程
# 這個張量用於保存生成的圖像,即夢境圖像
dream = model.input
# 計算損失相對於夢境圖像的梯度
grads = K.gradients(loss, dream)[0]
# 將梯度標準化
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)
outputs = [loss, grads]
# 給定一張輸出圖像,設置一個 Keras 函數來獲取損失值和梯度值
fetch_loss_and_grads = K.function([dream], outputs)


def eval_loss_and_grads(x):
    outs = fetch_loss_and_grads([x])
    loss_value = outs[0]
    grad_values = outs[1]
    return loss_value, grad_values


# 運行 iterations次梯度上升
def gradient_ascent(x, iterations, step, max_loss=None):
    for i in range(iterations):
        loss_value, grad_values = eval_loss_and_grads(x)
        if max_loss is not None and loss_value > max_loss:
            break
        print(‘...Loss value at‘, i, ‘:‘, loss_value)
        x += step * grad_values
    return x


#  在多個連續尺度上運行梯度上升
# 梯度上升的步長
step = 0.01
# 運行梯度上升的尺度個數
num_octave = 3
# 兩個尺度之間的大小比例
octave_scale = 1.4
iterations = 20
# 如果損失增大到大於 10,我們要中斷梯度上升過程,以避免得到醜陋的偽影
max_loss = 10.
base_image_path = ‘img_url‘
img = preprocess_image(base_image_path)
original_shape = img.shape[1:3]
successive_shapes = [original_shape]
for i in range(1, num_octave):
    # 一個由形狀元組組成的列表,它定義了運行梯度上升的不同尺度
    shape = tuple([int(dim / (octave_scale ** i)) for dim in original_shape])
    successive_shapes.append(shape)
    # 將形狀列表反轉,變為升序
    successive_shapes = successive_shapes[::-1]
    original_img = np.copy(img)
    # 將圖像 Numpy 數組的大小縮放到最小尺寸
    shrunk_original_img = resize_img(img, successive_shapes[0])
    for shape in successive_shapes:
        print(‘Processing image shape‘, shape)
        # 將夢境圖像放大
        img = resize_img(img, shape)
        # 運行梯度上升,改變夢境圖像
        img = gradient_ascent(img, iterations=iterations,
                              step=step, max_loss=max_loss)
        # 將原始圖像的較小版本放大,它會變得像素化
        upscaled_shrunk_original_img = resize_img(shrunk_original_img, shape)
        # 在這個尺寸上計算原始圖像的高質量版本
        same_size_original = resize_img(original_img, shape)
        lost_detail = same_size_original - upscaled_shrunk_original_img
        # 將丟失的細節重新註入到夢境圖像中
        img += lost_detail
        shrunk_original_img = resize_img(original_img, shape)
        save_img(img, fname=‘dream_at_scale_‘ + str(shape) + ‘.png‘)
    save_img(img, fname=‘final_dream.png‘)

原圖
技術分享圖片

dreamImage
技術分享圖片

神經風格遷移

神經風格遷移是指將參考圖像的風格應用於目標圖像,同時保留目標圖像的內容
技術分享圖片

風格(style)是指圖像中不同空間尺度的紋理、顏色和視覺圖案,內容(content)是指圖像的高級宏觀結構

實現風格遷移背後的關鍵概念與所有深度學習算法的核心思想是一樣的:定義一個損失函數來指定想要實現的目標,然後將這個損失最小化。你知道想要實現的目標是什麽,就是保存原始圖像的內容,同時采用參考圖像的風格。如果我們能夠在數學上給出內容和風格的定義,那麽就有一個適當的損失函數,我們將對其進行最小化

loss = distance(style(reference_image) - style(generated_image)) + distance(content(original_image) - content(generated_image))

這裏的 distance 是一個範數函數,比如 L2 範數;content 是一個函數,輸入一張圖像,並計算出其內容的表示;style 是一個函數,輸入一張圖像,並計算出其風格的表示。將這個損失最小化,會使得 style(generated_image) 接近於 style(reference_image)、content(generated_image) 接近於 content(generated_image),從而實現我們定義的風格遷移

深度卷積神經網絡能夠從數學上定義 style 和 content 兩個函數

內容損失

網絡更靠底部的層激活包含關於圖像的局部信息,而更靠近頂部的層則包含更加全局、更加抽象的信息。卷積神經網絡不同層的激活用另一種方式提供了圖像內容在不同空間尺度上的分解。因此,圖像的內容是更加全局和抽象的,我們認為它能夠被卷積神經網絡更靠頂部的層的表示所捕捉到

因此,內容損失的一個很好的候選者就是兩個激活之間的 L2 範數,一個激活是預訓練的卷積神經網絡更靠頂部的某層在目標圖像上計算得到的激活,另一個激活是同一層在生成圖像上計算得到的激活。這可以保證,在更靠頂部的層看來,生成圖像與原始目標圖像看起來很相似

風格損失

內容損失只使用了一個更靠頂部的層,但 Gatys 等人定義的風格損失則使用了卷積神經網絡的多個層。我們想要捉到卷積神經網絡在風格參考圖像的所有空間尺度上提取的外觀,而不僅僅是在單一尺度上。對於風格損失,Gatys 等人使用了層激活的格拉姆矩陣(Gram matrix),即某一層特征圖的內積。這個內積可以被理解成表示該層特征之間相互關系的映射。這些特征相互關系抓住了在特定空間尺度下模式的統計規律,從經驗上來看,它對應於這個尺度上找到的紋理的外觀

因此,風格損失的目的是在風格參考圖像與生成圖像之間,在不同的層激活內保存相似的內部相互關系。反過來,這保證了在風格參考圖像與生成圖像之間,不同空間尺度找到的紋理看起來都很相似

最終,你可以使用預訓練的卷積神經網絡來定義一個具有以下特點的損失

  1. 在目標內容圖像和生成圖像之間保持相似的較高層激活,從而能夠保留內容。卷積神經網絡應該能夠“看到”目標圖像和生成圖像包含相同的內容
  2. 在較低層和較高層的激活中保持類似的相互關系(correlation),從而能夠保留風格。特征相互關系捕捉到的是紋理(texture),生成圖像和風格參考圖像在不同的空間尺度上應該具有相同的紋理

用 Keras 實現神經風格遷移

神經風格遷移可以用任何預訓練卷積神經網絡來實現。神經風格遷移的一般過程如下

  1. 創建一個網絡,它能夠同時計算風格參考圖像、目標圖像和生成圖像的 VGG19 層激活
  2. 使用這三張圖像上計算的層激活來定義之前所述的損失函數,為了實現風格遷移,需要將這個損失函數最小化
  3. 設置梯度下降過程來將這個損失函數最小化

demo

from keras.preprocessing.image import load_img, img_to_array
import numpy as np
from keras.applications import vgg19
from keras import backend as K
from scipy.optimize import fmin_l_bfgs_b
from scipy.misc import imsave
import time


def preprocess_image(image_path):
    img = load_img(image_path, target_size=(img_height, img_width))
    img = img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = vgg19.preprocess_input(img)
    return img


def deprocess_image(x):
    # vgg19.preprocess_input 的作用是減去 ImageNet 的平均像素值,
    # 使其中心為 0。這裏相當於 vgg19.preprocess_input 的逆操作
    x[:, :, 0] += 103.939
    x[:, :, 1] += 116.779
    x[:, :, 2] += 123.68
    # 將圖像由 BGR 格式轉換為 RGB 格式。這也是
    # vgg19.preprocess_input 逆操作的一部分
    x = x[:, :, ::-1]
    x = np.clip(x, 0, 255).astype(‘uint8‘)
    return x


target_image_path = ‘cat.jpg‘
style_reference_image_path = ‘style.png‘

# 設置生成圖像的尺寸
width, height = load_img(target_image_path).size
img_height = 400
img_width = int(width * img_height / height)
# 加載預訓練的 VGG19 網絡,並將其應用於三張圖像
target_image = K.constant(preprocess_image(target_image_path))
style_reference_image = K.constant(preprocess_image(style_reference_image_path))
# 占位符用於保存生成圖像
combination_image = K.placeholder((1, img_height, img_width, 3))
# 將三張圖像合並為一個批量
input_tensor = K.concatenate([target_image, style_reference_image, combination_image], axis=0)
model = vgg19.VGG19(input_tensor=input_tensor, weights=‘imagenet‘, include_top=False)
print(‘Model loaded.‘)


def content_loss(base, combination):
    """
    內容損失
    :param base:
    :param combination:
    :return:
    """
    return K.sum(K.square(combination - base))


def gram_matrix(x):
    features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
    gram = K.dot(features, K.transpose(features))
    return gram


def style_loss(style, combination):
    """
    風格損失
    :param style:
    :param combination:
    :return:
    """
    S = gram_matrix(style)
    C = gram_matrix(combination)
    channels = 3
    size = img_height * img_width
    return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))


def total_variation_loss(x):
    """
    總變差損失
    對生成的組合圖像的像素進行操作,
    促使生成圖像具有空間連續性,從而避免結果過度像素化
    也可以簡單理解為正則化損失
    :param x:
    :return:
    """
    a = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, 1:, :img_width - 1, :])
    b = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, :img_height - 1, 1:, :])
    return K.sum(K.pow(a + b, 1.25))


# 定義最小化的最終損失
# 將層的名稱映射為激活張量的字典
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
content_layer = ‘block5_conv2‘
style_layers = [‘block1_conv1‘, ‘block2_conv1‘, ‘block3_conv1‘, ‘block4_conv1‘, ‘block5_conv1‘]
total_variation_weight = 1e-4
style_weight = 1.
content_weight = 0.025
loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss += content_weight * content_loss(target_image_features, combination_features)
for layer_name in style_layers:
    layer_features = outputs_dict[layer_name]
    style_reference_features = layer_features[1, :, :, :]
    combination_features = layer_features[2, :, :, :]
    sl = style_loss(style_reference_features, combination_features)
    loss += (style_weight / len(style_layers)) * sl
loss += total_variation_weight * total_variation_loss(combination_image)

# 使用 L-BFGS 算法進行優化,設置梯度下降過程
# 獲取損失相對於生成圖像的梯度
grads = K.gradients(loss, combination_image)[0]
# 用於獲取當前損失值和當前梯度值的函數
fetch_loss_and_grads = K.function([combination_image], [loss, grads])


class Evaluator(object):
    """
    這個類將 fetch_loss_and_grads 包
    裝起來,讓你可以利用兩個單獨的方法
    調用來獲取損失和梯度,這是我們要使
    用的 SciPy 優化器所要求的
    """
    def __init__(self):
        self.loss_value = None
        self.grads_values = None

    def loss(self, x):
        assert self.loss_value is None
        x = x.reshape((1, img_height, img_width, 3))
        outs = fetch_loss_and_grads([x])
        loss_value = outs[0]
        grad_values = outs[1].flatten().astype(‘float64‘)
        self.loss_value = loss_value
        self.grad_values = grad_values
        return self.loss_value

    def grads(self, x):
        assert self.loss_value is not None
        grad_values = np.copy(self.grad_values)
        self.loss_value = None
        self.grad_values = None
        return grad_values


evaluator = Evaluator()

# 使用 SciPy 的 L-BFGS 算法來運行梯度上升過程
# 風格遷移循環
result_prefix = ‘my_result‘
iterations = 20
x = preprocess_image(target_image_path)
# 將圖像展平,因為 scipy.optimize.fmin_l_bfgs_b 只能處理展平的向量
x = x.flatten()
for i in range(iterations):
    print(‘Start of iteration‘, i)
    start_time = time.time()
    x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=20)
    print(‘Current loss value:‘, min_val)
    # 保存當前的生成圖像
    img = x.copy().reshape((img_height, img_width, 3))
    img = deprocess_image(img)
    fname = result_prefix + ‘_at_iteration_%d.png‘ % i
    imsave(fname, img)
    print(‘Image saved as‘, fname)
    end_time = time.time()
    print(‘Iteration %d completed in %ds‘ % (i, end_time - start_time))

以上技術所實現的僅僅是一種形式的改變圖像紋理,或者叫紋理遷移。如果風格參考圖像具有明顯的紋理結構且高度自相似,並且內容目標不需要高層次細節就能夠被識別,那麽這種方法的效果最好。它通常無法實現比較抽象的遷移,比如將一幅肖像的風格遷移到另一幅中

上面這個風格遷移算法的運行速度很慢。但這種方法實現的變換足夠簡單,只要有適量的訓練數據,一個小型的快速前饋卷積神經網絡就可以學會這種變換。因此,實現快速風格遷移的方法是,首先利用這裏介紹的方法,花費大量的計算時間對一張固定的風格參考圖像生成許多輸入 - 輸出訓練樣例,然後訓練一個簡單的卷積神經網絡來學習這個特定風格的變換。一旦完成之後,對一張圖像進行風格遷移是非常快的,只是這個小型卷積神經網絡的一次前向傳遞而已

用變分自編碼器生成圖像

從圖像的潛在空間中采樣,並創建全新圖像或編輯現有圖像,這是目前最流行也是最成功的創造性人工智能應用。該領域的兩種主要技術分別為變分自編碼器(VAE,variational autoencoder)生成式對抗網絡(GAN,generative adversarial network)

從圖像的潛在空間中采樣

圖像生成的關鍵思想就是找到一個低維的表示潛在空間(latent space,也是一個向量空間),其中任意點都可以被映射為一張逼真的圖像。能夠實現這種映射的模塊,即以潛在點作為輸入並輸出一張圖像(像素網格),叫作生成器(generator,對於 GAN 而言)或解碼器(decoder,對於 VAE 而言)。一旦找到了這樣的潛在空間,就可以從中有意地或隨機地對點進行采樣,並將其映射到圖像空間,從而生成前所未見的圖像

生成圖像過程示例
技術分享圖片

想要學習圖像表示的這種潛在空間,GAN 和 VAE 是兩種不同的策略。VAE 非常適合用於學習具有良好結構的潛在空間,其中特定方向表示數據中有意義的變化軸。GAN 生成的圖像可能非常逼真,但它的潛在空間可能沒有良好結構,也沒有足夠的連續性

VAE 生成的人臉連續空間
技術分享圖片

概念向量(concept vector):給定一個表示的潛在空間或一個嵌入空間,空間中的特定方向可能表示原始數據中有趣的變化軸

變分自編碼器是一種生成式模型,特別適用於利用概念向量進行圖像編輯的任務。它是一種現代化的自編碼器,將深度學習的想法與貝葉斯推斷結合在一起。自編碼器是一種網絡類型,其目的是將輸入編碼到低維潛在空間,然後再解碼回來

經典的圖像自編碼器接收一張圖像,通過一個編碼器模塊將其映射到潛在向量空間,然後再通過一個解碼器模塊將其解碼為與原始圖像具有相同尺寸的輸出。然後,使用與輸入圖像相同的圖像作為目標數據來訓練這個自編碼器,也就是說,自編碼器學習對原始輸入進行重新構建。通過對代碼(編碼器的輸出)施加各種限制,我們可以讓自編碼器學到比較有趣的數據潛在表示。最常見的情況是將代碼限制為低維的並且是稀疏的(即大部分元素為 0),在這種情況下,編碼器的作用是將輸入數據壓縮為更少二進制位的信息

自編碼器模型表示
技術分享圖片

這種自編碼器不會得到特別有用或具有良好結構的潛在空間。它們也沒有對數據做多少壓縮。但是,VAE 向自編碼器添加了一點統計魔法,迫使其學習連續的、高度結構化的潛在空間。這使得 VAE 已成為圖像生成的強大工具

VAE 不是將輸入圖像壓縮成潛在空間中的固定編碼,而是將圖像轉換為統計分布的參數,即平均值和方差。本質上來說,這意味著我們假設輸入圖像是由統計過程生成的,在編碼和解碼過程中應該考慮這一過程的隨機性。然後,VAE 使用平均值和方差這兩個參數來從分布中隨機采樣一個元素,並將這個元素解碼到原始輸入。這個過程的隨機性提高了其穩健性,並迫使潛在空間的任何位置都對應有意義的表示,即潛在空間采樣的每個點都能解碼為有效的輸出

VAE模型表示
技術分享圖片

VAE 的工作原理

  1. 一個編碼器模塊將輸入樣本 input_img 轉換為表示潛在空間中的兩個參數 z_mean 和 z_log_variance
  2. 我們假定潛在正態分布能夠生成輸入圖像,並從這個分布中隨機采樣一個點 z:z = z_mean + exp(z_log_variance) * epsilon,其中 epsilon 是取值很小的隨機張量
  3. 一個解碼器模塊將潛在空間的這個點映射回原始輸入圖像

因為 epsilon 是隨機的,所以這個過程可以確保,與 input_img 編碼的潛在位置(即z-mean)靠近的每個點都能被解碼為與 input_img 類似的圖像,從而迫使潛在空間能夠連續地有意義。潛在空間中任意兩個相鄰的點都會被解碼為高度相似的圖像。連續性以及潛在空間的低維度,將迫使潛在空間中的每個方向都表示數據中一個有意義的變化軸,這使得潛在空間具有非常良好的結構,因此非常適合通過概念向量來進行操作

VAE 的參數通過兩個損失函數來進行訓練:一個是重構損失(reconstruction loss),它迫使解碼後的樣本匹配初始輸入;另一個是正則化損失(regularization loss),它有助於學習具有良好結構的潛在空間,並可以降低在訓練數據上的過擬合

demo

import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np
from keras.datasets import mnist
import matplotlib.pyplot as plt
from scipy.stats import norm


class CustomVariationalLayer(keras.layers.Layer):
    """
    用於計算 VAE 損失的自定義層
    """
    def vae_loss(self, x, z_decoded):
        x = K.flatten(x)
        z_decoded = K.flatten(z_decoded)
        xent_loss = keras.metrics.binary_crossentropy(x, z_decoded)
        kl_loss = -5e-4 * K.mean(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
        return K.mean(xent_loss + kl_loss)

    def call(self, inputs):
        x = inputs[0]
        z_decoded = inputs[1]
        loss = self.vae_loss(x, z_decoded)
        self.add_loss(loss, inputs=inputs)
        return x


def sampling(args):
    """
     潛在空間采樣的函數
    :param args:
    :return:
    """
    z_mean, z_log_var = args
    epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim),
    mean=0., stddev=1.)
    return z_mean + K.exp(z_log_var) * epsilon


# 網絡
img_shape = (28, 28, 1)
batch_size = 16
latent_dim = 2
input_img = keras.Input(shape=img_shape)
x = layers.Conv2D(32, 3, padding=‘same‘, activation=‘relu‘)(input_img)
x = layers.Conv2D(64, 3, padding=‘same‘, activation=‘relu‘, strides=(2, 2))(x)
x = layers.Conv2D(64, 3, padding=‘same‘, activation=‘relu‘)(x)
x = layers.Conv2D(64, 3, padding=‘same‘, activation=‘relu‘)(x)
shape_before_flattening = K.int_shape(x)
x = layers.Flatten()(x)
x = layers.Dense(32, activation=‘relu‘)(x)
z_mean = layers.Dense(latent_dim)(x)
z_log_var = layers.Dense(latent_dim)(x)


# z_mean 和 z_log_var 是統計分布的參數,假設這個分布能夠生成 input_img
# 接下來的代碼將使用 z_mean 和 z_log_var 來生成一個潛在空間點 z
z = layers.Lambda(sampling)([z_mean, z_log_var])
# VAE 解碼器網絡,將潛在空間點映射為圖像
decoder_input = layers.Input(K.int_shape(z)[1:])
# 對輸入進行上采樣
x = layers.Dense(np.prod(shape_before_flattening[1:]), activation=‘relu‘)(decoder_input)
# 將 z 轉換為特征圖,使其形狀與編碼器模型最後一個 Flatten 層之前的特征圖的形狀相同
x = layers.Reshape(shape_before_flattening[1:])(x)
# 使用一個 Conv2DTranspose 層和一個Conv2D 層,將 z 解碼為與原始輸入圖像具有相同尺寸的特征圖
x = layers.Conv2DTranspose(32, 3, padding=‘same‘, activation=‘relu‘, strides=(2, 2))(x)
x = layers.Conv2D(1, 3, padding=‘same‘, activation=‘sigmoid‘)(x)
# 將解碼器模型實例化,它將 decoder_input轉換為解碼後的圖像
decoder = Model(decoder_input, x)
# 將實例應用於 z,以得到解碼後的 z
z_decoded = decoder(z)

y = CustomVariationalLayer()([input_img, z_decoded])

# 訓練 VAE
vae = Model(input_img, y)
vae.compile(optimizer=‘rmsprop‘, loss=None)
vae.summary()
(x_train, _), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype(‘float32‘) / 255.
x_train = x_train.reshape(x_train.shape + (1,))
x_test = x_test.astype(‘float32‘) / 255.
x_test = x_test.reshape(x_test.shape + (1,))
vae.fit(x=x_train, y=None, shuffle=True, epochs=10, batch_size=batch_size, validation_data=(x_test, None))

# 從二維潛在空間中采樣一組點的網格,並將其解碼為圖像
n = 15
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))

for i, yi in enumerate(grid_x):
    for j, xi in enumerate(grid_y):
        z_sample = np.array([[xi, yi]])
        z_sample = np.tile(z_sample, batch_size).reshape(batch_size, 2)
        x_decoded = decoder.predict(z_sample, batch_size=batch_size)
        digit = x_decoded[0].reshape(digit_size, digit_size)
        figure[i * digit_size: (i + 1) * digit_size, j * digit_size: (j + 1) * digit_size] = digit
plt.figure(figsize=(10, 10))
plt.imshow(figure, cmap=‘Greys_r‘)
plt.show()

結果
技術分享圖片

VAE 得到的是高度結構化的、連續的潛在表示。因此,它在潛在空間中進行各種圖像編輯的效果很好,比如換臉、將皺眉臉換成微笑臉等。它制作基於潛在空間的動畫效果也很好,比如沿著潛在空間的一個橫截面移動,從而以連續的方式顯示從一張起始圖像緩慢變化為不同圖像的效果
?GAN 可以生成逼真的單幅圖像,但得到的潛在空間可能沒有良好的結構,也沒有很好的連續性

生成式對抗網絡(GAN,generative adversarial network)能夠迫使生成圖像與真實圖像在統計上幾乎無法區分,從而生成相當逼真的合成圖像

GAN 的工作原理:一個偽造者網絡和一個專家網絡,二者訓練的目的都是為了打敗彼此。因此,GAN 由以下兩部分組成
生成器網絡(generator network):它以一個隨機向量(潛在空間中的一個隨機點)作為輸入,並將其解碼為一張合成圖像
判別器網絡(discriminator network)或對手(adversary):以一張圖像(真實的或合成的均可)作為輸入,並預測該圖像是來自訓練集還是由生成器網絡創建

訓練生成器網絡的目的是使其能夠欺騙判別器網絡,因此隨著訓練的進行,它能夠逐漸生成越來越逼真的圖像,即看起來與真實圖像無法區分的人造圖像,以至於判別器網絡無法區分二者。與此同時,判別器也在不斷適應生成器逐漸提高的能力,為生成圖像的真實性設置了很高的標準。一旦訓練結束,生成器就能夠將其輸入空間中的任何點轉換為一張可信圖像。與 VAE 不同,這個潛在空間無法保證具有有意義的結構,而且它還是不連續的

GAN示意
技術分享圖片

GAN系統的優化最小值是不固定的。通常來說,梯度下降是沿著靜態的損失地形滾下山坡。但對於 GAN 而言,每下山一步,都會對整個地形造成一點改變。它是一個動態的系統,其最優化過程尋找的不是一個最小值,而是兩股力量之間的平衡。因此,GAN 的訓練極其困難,想要讓 GAN 正常運行,需要對模型架構和訓練參數進行大量的仔細調整

GAN 的簡要實現流程

  1. generator網絡將形狀為(latent_dim,)的向量映射到形狀為(32, 32, 3)的圖像
  2. discriminator 網絡將形狀為 (32, 32, 3) 的圖像映射到一個二進制分數,用於評估圖像為真的概率
  3. gan 網絡將 generator 網絡和 discriminator 網絡連接在一起:gan(x) = discriminator(generator(x))。生成器將潛在空間向量解碼為圖像,判別器對這些圖像的真實性進行評估,因此這個 gan 網絡是將這些潛在向量映射到判別器的評估結果
  4. 我們使用帶有“真”/“假”標簽的真假圖像樣本來訓練判別器,就和訓練普通的圖像分類模型一樣
  5. 為了訓練生成器,我們要使用 gan 模型的損失相對於生成器權重的梯度。這意味著,在每一步都要移動生成器的權重,其移動方向是讓判別器更有可能將生成器解碼的圖像劃分為“真”。換句話說,我們訓練生成器來欺騙判別器

實現GAN的一些技巧

  1. 使用 tanh 作為生成器最後一層的激活,而不用 sigmoid,後者在其他類型的模型中更加常見
  2. 使用正態分布(高斯分布)對潛在空間中的點進行采樣,而不用均勻分布。隨機性能夠提高穩健性。訓練GAN得到的是一個動態平衡,所以GAN可能以各種方式“卡住”。在訓練過程中引入隨機性有助於防止出現這種情況。我們通過兩種方式引入隨機性:一種是在判別器中使用 dropout,另一種是向判別器的標簽添加隨機噪聲
  3. 稀疏的梯度會妨礙 GAN 的訓練。在深度學習中,稀疏性通常是我們需要的屬性,但在GAN 中並非如此。有兩件事情可能導致梯度稀疏:最大池化運算和 ReLU 激活。推薦使用步進卷積代替最大池化來進行下采樣,還推薦使用 LeakyReLU 層來代替 ReLU 激活。LeakyReLU 和 ReLU 類似,但它允許較小的負數激活值,從而放寬了稀疏性限制
  4. 在生成的圖像中,經常會見到棋盤狀偽影,這是由生成器中像素空間的不均勻覆蓋導致的。為了解決這個問題,每當在生成器和判別器中都使用步進的 Conv2DTranpose或 Conv2D 時,使用的內核大小要能夠被步幅大小整除

GAN訓練循環的大致流程

  1. 從潛在空間中抽取隨機的點(隨機噪聲)
  2. 利用這個隨機噪聲用 generator 生成圖像
  3. 將生成圖像與真實圖像混合
  4. 使用這些混合後的圖像以及相應的標簽(真實圖像為“真”,生成圖像為“假”)來訓練discriminator
  5. 在潛在空間中隨機抽取新的點
  6. 使用這些隨機向量以及全部是“真實圖像”的標簽來訓練 gan。這會更新生成器的權重(只更新生成器的權重,因為判別器在 gan 中被凍結),其更新方向是使得判別器能夠將生成圖像預測為“真實圖像”。這個過程是訓練生成器去欺騙判別器

demo

import keras
from keras import layers
import numpy as np
import os
from keras.preprocessing import image


# 生成器
latent_dim = 32
height = 32
width = 32
channels = 3
generator_input = keras.Input(shape=(latent_dim,))
x = layers.Dense(128 * 16 * 16)(generator_input)
x = layers.LeakyReLU()(x)
x = layers.Reshape((16, 16, 128))(x)
x = layers.Conv2D(256, 5, padding=‘same‘)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2DTranspose(256, 4, strides=2, padding=‘same‘)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(256, 5, padding=‘same‘)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(256, 5, padding=‘same‘)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(channels, 7, activation=‘tanh‘, padding=‘same‘)(x)
generator = keras.models.Model(generator_input, x)
generator.summary()

# 判別器
discriminator_input = layers.Input(shape=(height, width, channels))
x = layers.Conv2D(128, 3)(discriminator_input)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Flatten()(x)
x = layers.Dropout(0.4)(x)
x = layers.Dense(1, activation=‘sigmoid‘)(x)
discriminator = keras.models.Model(discriminator_input, x)
discriminator.summary()
discriminator_optimizer = keras.optimizers.RMSprop(lr=0.0008, clipvalue=1.0, decay=1e-8)
discriminator.compile(optimizer=discriminator_optimizer, loss=‘binary_crossentropy‘)

# 對抗網絡,將生成器和判別器連接在一起
discriminator.trainable = False
gan_input = keras.Input(shape=(latent_dim,))
gan_output = discriminator(generator(gan_input))
gan = keras.models.Model(gan_input, gan_output)
gan_optimizer = keras.optimizers.RMSprop(lr=0.0004, clipvalue=1.0, decay=1e-8)
gan.compile(optimizer=gan_optimizer, loss=‘binary_crossentropy‘)

# 實現 GAN 的訓練
(x_train, y_train), (_, _) = keras.datasets.cifar10.load_data()
x_train = x_train[y_train.flatten() == 6]
x_train = x_train.reshape((x_train.shape[0],) + (height, width, channels)).astype(‘float32‘) / 255
iterations = 10000
batch_size = 20
save_dir = ‘your_dir‘
start = 0
for step in range(iterations):
    random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))
    generated_images = generator.predict(random_latent_vectors)
    stop = start + batch_size
    real_images = x_train[start: stop]
    combined_images = np.concatenate([generated_images, real_images])
    labels = np.concatenate([np.ones((batch_size, 1)),
    np.zeros((batch_size, 1))])
    labels += 0.05 * np.random.random(labels.shape)
    d_loss = discriminator.train_on_batch(combined_images, labels)
    random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))
    misleading_targets = np.zeros((batch_size, 1))
    a_loss = gan.train_on_batch(random_latent_vectors, misleading_targets)
    start += batch_size
    if start > len(x_train) - batch_size:
        start = 0
    if step % 100 == 0:
        gan.save_weights(‘gan.h5‘)
        print(‘discriminator loss:‘, d_loss)
        print(‘adversarial loss:‘, a_loss)
        img = image.array_to_img(generated_images[0] * 255., scale=False)
        img.save(os.path.join(save_dir, ‘generated_frog‘ + str(step) + ‘.png‘))
        img = image.array_to_img(real_images[0] * 255., scale=False)
        img.save(os.path.join(save_dir, ‘real_frog‘ + str(step) + ‘.png‘))

GAN 由一個生成器網絡和一個判別器網絡組成。判別器的訓練目的是能夠區分生成器的輸出與來自訓練集的真實圖像,生成器的訓練目的是欺騙判別器。值得註意的是,生成器從未直接見過訓練集中的圖像,它所知道的關於數據的信息都來自於判別器

註:

在 Keras 中,任何對象都應該是一個層,所以如果代碼不是內置層的一部分,我們應該將其包裝到一個 Lambda 層(或自定義層)中

python Deep learning 學習筆記(9)

python Deep learning 學習筆記(10)