Deep learning with Python 學習筆記(10)

生成式深度學習

機器學習模型可以對圖像、音樂和故事的統計潛在空間(latent space)進行學習,而後從這個空間中採樣(sample),創造出與模型在訓練數據中所見到的藝術做品具備類似特徵的新做品html

使用 LSTM 生成文本

生成序列數據python

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

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

使用語言模型逐個字符生成文本的過程
數組

採樣策略網絡

生成文本時,如何選擇下一個字符相當重要。一種簡單的方法是貪婪採樣(greedy sampling),就是始終選擇可能性最大的下一個字符。但這種方法會獲得重複的、可預測的字符串,看起來不像是連貫的語言。一種更有趣的方法是作出稍顯意外的選擇:在採樣過程當中引入隨機性,即從下一個字符的機率分佈中進行採樣。這叫做隨機採樣(stochastic sampling,stochasticity 在這個領域中就是「隨機」的意思)。在這種狀況下,根據模型結果,若是下一個字符是 e 的機率爲0.3,那麼你會有 30% 的機率選擇它架構

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

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

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 層(或自定義層)中

Deep learning with Python 學習筆記(11)
Deep learning with Python 學習筆記(9)

相關文章
相關標籤/搜索