《Python深度學習》讀書筆記(下)

2021-02-15 九號線
接著寫《Python深度學習》第六~第八章讀書筆記。書中有很多概念簡略帶過,但我覺得對於理解卷積網絡很有幫助,所以部分內容我會根據自己的理解展開講。


第六章 深度學習用於文本和序列

本章將介紹使用深度學習模型處理文本(可以將其理解為單詞序列或字符序列)、時間序列以及一般的序列數據。用於處理序列的兩種基本的深度學習算法分別是循環神經網絡(recurrentneural network)和一維卷積神經網絡(1D convnet),後者是上一章介紹的二維卷積神經網絡的一維版本。文本是最常用的序列數據之一,可以理解為字符序列或單詞序列,最常見的是單詞級處理。深度學習序列處理模型可以根據文本生成基本形式的自然語言理解,並可用於文檔分類、情感分析等應用。但本章的這些深度學習模型都沒有像人類一樣真正地理解文本,而只是映射出書面語言的統計結構,但這足以解決許多簡單的文本任務。深度學習用於自然語言處理是將模式識別應用於單詞、句子和段落,這與計算機視覺是將模式識別應用於像素大致相同。首先,深度學習模型不會接收原始文本作為輸入,它只能處理數值張量。所以先要將文本轉換為數值張量(即文本向量化vectorize),比如:(1)將文本分割為單詞,並將每個單詞轉換為一個向量;(2)將文本分割為字符,並將每個字符轉換為一個向量;

將文本分解成的單元(單詞、字符或n-gram)叫作標記(token),將文本分解成標記的過程叫作分詞(tokenization)。所有文本向量化過程都是應用某種分詞方案,然後將數值向量生成的標記相關聯。本文介紹兩種主要方法:對標記做one-hot 編碼(one-hotencoding)與標記嵌入(token embedding,通常只用於單詞叫作詞嵌入(word embedding)。

單詞和字符的one-hot 編碼

在第3章的IMDB個例子中,已經用過這種方法(單詞級)。它將每個單詞與一個唯一的整數索引相關聯,然後將這個整數索i轉換為長度為N 的二進位向量(N是詞表大小),這個向量只有第i個元素是1,其餘元素都為0。當然也可以進行字符級的one-hot 編碼。

Keras的內置函數Tokenizer可以對原始文本數據進行單詞級或字符級的one-hot 編碼,它們實現了許多重要的特性,比如從字符串中去除特殊字符、只考慮數據集中前N個最常見的單詞(這是一種常用的限制,以避免處理非常大的輸入向量空間)。

from keras.preprocessing.text import Tokenizer
samples = ['The cat sat on the mat.', 'The dog ate my homework.']
#創建一個分詞器tokenizer,設置為只考慮前1000個最常見的單詞
tokenizer = Tokenizer(num_words=1000)
tokenizer.fit_on_texts(samples)
#將字符串轉換為整數索引組成的列表
sequences = tokenizer.texts_to_sequences(samples)
#或者直接得到one-hot二進位表示
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')
#獲取單詞字典
word_index = tokenizer.word_index

one-hot編碼的一種變體是所謂的one-hot散列技巧(one-hot hashing trick),如果詞表中唯一標記的數量太大而無法直接處理,就可以使用這種技巧。這種方法沒有為每個單詞顯式分配一個索引並將這些索引保存在一個字典中,而是將單詞散列編碼為固定長度的向量,通常用一個非常簡單的散列函數來實現。(其實跟one-hot編碼大同小異)。

使用詞嵌入

將原始文本轉換為數組張量的第二個方法是詞嵌入(word embedding),也叫詞向量(word vector)。one-hot 編碼得到的向量是二進位的、稀疏的(絕大部分元素都是0)、維度很高的(維度大小等於詞表中的單詞個數),而詞嵌入是低維的浮點數向量(即密集向量,與稀疏向量相對)。與one-hot 編碼得到的詞向量不同,詞嵌入是從數據中學習得到的一種表示。詞向量維度一般是256、512或1024(處理非常大的詞表時)。與此相對,onehot編碼的詞向量維度通常為20000或更高(對應包含20 000個單詞的詞表)。因此詞向量可以將更多的信息塞入更低的維度中(不同的深淺、不同的位置表示不同的單詞)。

(1)在完成主任務(比如文檔分類或情感預測)的同時學習詞嵌入。在這種情況下,一開始是隨機的詞向量,然後對這些詞向量進行學習,其學習方式與學習神經網絡的權重相同。

詞向量之間的幾何關係應該表示這些詞之間的語義關係。詞嵌入的作用應該是將人類的語言映射到幾何空間中。例如,在一個合理的嵌入空間中,同義詞應該被嵌入到相似的詞向量中,一般來說,任意兩個詞向量之間的幾何距離(比如L2 距離)應該和這兩個詞的語義距離有關(表示不同事物的詞被嵌入到相隔很遠的點,而相關的詞則更加靠近)。除了距離,你可能還希望嵌入空間中的特定方向也是有意義的。為了更清楚地說明這一點,如下圖二維平面上四個詞分別是cat(貓)、dog(狗)、wolf(狼)和tiger(虎)這些詞之間的某些語義關係可以被編碼為幾何變換:例如從cat到tiger的向量與從dog到wolf的向量相等,都可以被解釋為「從寵物到野生動物」向量。同樣,從dog到cat的向量與從wolf到tiger的向量也相等,它可以被解釋為「從犬科到貓科」向量。

有沒有一個理想的詞嵌入空間可以完美地映射人類語言,並可用於所有自然語言處理任務?可能有,但我們尚未發現。此外,也不存在人類語言(human language)這種東西。世界上有許多種不同的語言,而且它們不是同構的,因為語言是特定文化和特定環境的反射。但從更實際的角度來說,一個好的詞嵌入空間在很大程度上取決於你的任務。英語電影評論情感分析模型的完美詞嵌入空間,可能不同於英語法律文檔分類模型的完美詞嵌入空間,因為某些語義關係的重要性因任務而異。因此,合理的做法是對每個新任務都學習一個新的嵌入空間。Keras的Embedding層可以使這一過程變得更簡單,我們要做的就是學習輸入單詞的表達權重矩陣,Embedding通常有3個輸入:單詞數量、每個單詞的嵌入緯度、截取每句評論(每個序列)的前N個單詞。

from keras.datasets import imdb
from keras import preprocessing
max_features = 10000
maxlen = 20
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
#截取每句話的前20個單詞
x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)


from keras.models import Sequential
from keras.layers import Flatten, Dense, Embedding
model = Sequential()
#這裡是1000即最大單詞索引數目,8是需要將每個單詞嵌入的維度,截取每句話的前maxlen的單詞
model.add(Embedding(10000, 8, input_length=maxlen))
#將三維的嵌入張量展平成形狀為(samples, maxlen * 8) 的二維張量,然後輸入密基連接層
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,epochs=10,batch_size=32,validation_split=0.2)

該模型得到的驗證精度約為76%,考慮到僅查看每條評論的前20個單詞,這個結果還是相當不錯的。但請注意,僅僅將嵌入序列展開並在上面訓練一個Dense 層,會導致模型對輸入序列中的每個單詞單獨處理,而沒有考慮單詞之間的關係和句子結構(舉個例子,這個模型可能會將thismovie is a bomb和this movie is the bomb兩條都歸為負面評論)。更好的做法是在嵌入序列上添加循環層或一維卷積層,將每個序列作為整體來學習特徵。 

(2)採用預訓練的詞嵌入。在不同於待解決問題的機器學習任務上預計算好詞嵌入(通常是更複雜的文本分析任務),然後將其加載到模型中。這些詞嵌入叫作預訓練詞嵌入(pretrained word embedding)。在自然語言處理中使用預訓練的詞嵌入,其背後的原理與在圖像分類中使用預訓練的卷積神經網絡是一樣的:沒有足夠的數據來自己學習真正強大的特徵,而你需要的特徵應該是非常通用的,比如常見的視覺特徵或語義特徵。這種情況下重複使用在其他問題上學到的特徵是合理的。預訓練的詞嵌入有很多,可以下載後載入Embedding層中,這裡將使用GloVe詞嵌入資料庫。下面將使用詞嵌入的方法,從頭開始訓練一個文本分析模型,用的是IMDB原始文本數據而不是使用Keras內置的已經預先分詞的IMDB 數據。首先從http://mng.bz/0tIo下載原始IMDB 數據集並解壓,保存在名為「aclImdb」的目錄中,讀取的每條原始評論保存在texts列表中。

#讀取原始數據,texts列表的每一個元素都是一句評論
import os
imdb_dir = 'C:\\Users\\user\\PycharmProjects\\test_project\\aclImdb'
train_dir = os.path.join(imdb_dir, 'train')
labels = []
texts = []
for label_type in ['neg', 'pos']:
dir_name = os.path.join(train_dir, label_type)
for fname in os.listdir(dir_name):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname),encoding='UTF-8')
texts.append(f.read())
f.close()
if label_type == 'neg':
labels.append(0)
else:
labels.append(1)

然後利用Keras的內置函數Tokenizer對文本進行分詞,並將其劃分為訓練集和驗證集。因為預訓練的詞嵌入對訓練數據很少的問題特別有用,所以我們將訓練數據限定為前200 個樣本,即用前200個樣本學習模型對電影評論進行分類。

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np
#讀取每句評論的前100個單詞
maxlen = 100
#用100句評論訓練模型
training_samples = 200
#用前10000句評論驗證模型
validation_samples = 10000

max_words = 10000

#分詞器擁有10000個單詞

tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts)
#sequences的每個元素是每句話的單詞索引序列
sequences = tokenizer.texts_to_sequences(texts)
#獲得詞典索引
word_index = tokenizer.word_index
#截取評論的前100個單詞,並且轉換為數組
data = pad_sequences(sequences, maxlen=maxlen)
#將標籤數組化
labels = np.asarray(labels)
#將數據劃分為訓練集和驗證集,但首先要打亂數據,因為一開始數據中的樣本是
#排好序的(所有負面評論都在前面,然後是所有正面評論)
indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]
x_train = data[:training_samples]
y_train = labels[:training_samples]
x_val = data[training_samples: training_samples + validation_samples]
y_val = labels[training_samples: training_samples + validation_samples]

接著下載GloVe詞嵌入。打開https://nlp.stanford.edu/projects/glove,下載2014 年英文維基百科的預計算嵌入。這是一個822MB的壓縮文件,文件名是glove.6B.zip,裡面包含400000個單詞(或標記)的100維嵌入向量。解壓文件,我們需要用其中的」glove.6B.100d.txt「文件,該文件的每一行是一個單詞和它對應的詞嵌入向量,結果保存在embeddings_index詞典中。

glove_dir = "C:\\Users\\user\\PycharmProjects\\test_project\\glove.6B"
embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'),encoding='UTF-8')
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs
f.close()

接著需要構建一個可以加載到Embedding層中的嵌入矩陣。它必須是一個形狀為(max_words, embedding_dim) 的矩陣,對於單詞索引(在分詞時構建)中索引為i的單詞,這個矩陣的元素i就是這個單詞對應的embedding_dim維(這裡是100緯)向量embedding_vector。

max_words=10000
embedding_dim = 100
embedding_matrix = np.zeros((max_words, embedding_dim))
for word, i in word_index.items():
if i < max_words:
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector

定義模型並加載Embedding的權重,最後訓練模型

from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense
model_text = Sequential()
model_text.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model_text.add(Flatten())
model_text.add(Dense(32, activation='relu'))
model_text.add(Dense(1, activation='sigmoid'))
#將準備好的GloVe矩陣embedding_vector加載到Embedding層,即模型的第一層
#同時設定為不可更改(訓練)
model_text.layers[0].set_weights([embedding_matrix])
model_text.layers[0].trainable = False
model_text.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
#保存模型
model_text.save_weights('pre_trained_glove_model.h5')
history = model_text.fit(x_train, y_train,epochs=10,batch_size=32,
validation_data=(x_val, y_val))

從模型的訓練、驗證損失及精度來看,模型很快就開始過擬合,考慮到訓練樣本很少(200個),這一點也不奇怪。出於同樣的原因,驗證精度的波動很大,但似乎達到了接近60%。

本書目前為止接觸的所有神經網絡(比如密集連接網絡和卷積神經網絡)都有一個主要特點,那就是它們都沒有記憶。它們單獨處理每個輸入,在輸入與輸入之間沒有保存任何狀態。對於這樣的網絡,要想處理數據點的序列或時間序列,你需要向網絡同時展示整個序列,即將序列轉換成單個數據點。例如,你在IMDB 示例中就是這麼做的:將全部電影評論轉換為一個大向量,然後一次性處理。這種網絡叫作前饋網絡(feedforward network)。而循環神經網絡(RNN,recurrent neural network)處理序列的方式是:遍歷所有序列元素,並保存一個狀態(state),其中包含與已查看內容相關的信息。實際上,RNN是一類具有內部環的神經網絡。在處理兩個不同的獨立序列(比如兩條不同的IMDB評論)之間,RNN 狀態會被重置,因此,你仍可以將一個序列看作單個數據點,即網絡的單個輸入。但數據點不再是在單個步驟中進行處理,相反,網絡內部會對序列元素進行遍歷。

RNN本質是一個for 循環,它重複使用循環前一次迭代的計算結果。RNN的特徵在於其時間步函數,比如下面這個函數,state_t來自於t-1的輸出:
output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)

如果輸入inputs 是(timesteps, input_features)形勢的,最終輸出也是一個形狀為(timesteps, output_features) 的二維張量,其中每個時間步t是循環在t 時刻的輸出,其包含輸入序列中時間步0~t的信息,即關於全部過去的信息。因此在多數情況下,循環網絡只需要最後一個輸出(循環結束時的output_t),因為它已經包含了整個序列的信息。

Keras中的簡單循環層

Keras的SimpleRNN層可以實現簡單的RNN網絡,它接收形狀為(batch_size,timesteps,input_features) 的輸入。與Keras 中的所有循環層一樣,SimpleRNN可以在兩種不同的模式下運行:一種是返回每個時間步連續輸出的完整序列,即形狀為(batch_size, timesteps, output_features)的三維張量;另一種是只返回每個輸入序列的最終輸出,即形狀為(batch_size, output_features) 的二維張量。這兩種模式由return_sequences 這個構造函數參數來控制。來看一個使用SimpleRNN的簡單例子:

from keras.models import Sequential
from keras.layers import Embedding, SimpleRNN
model = Sequential()
model.add(Embedding(10000, 32))
#返回最後一個時間步輸出
model.add(SimpleRNN(32))
#返回整個時間序列
model.add(SimpleRNN(32, return_sequences=True))

將這個模型應用於之前的IMDB電影評論分類問題。首先,對數據進行預處理。

#IMDB
from keras.datasets import imdb
from keras.preprocessing import sequence
max_features = 10000
maxlen = 500
batch_size = 32
#讀取數據
(input_train, y_train), (input_test, y_test) = imdb.load_data(num_words=max_features)
#截斷評論,取前500個單詞
input_train = sequence.pad_sequences(input_train, maxlen=maxlen)
input_test = sequence.pad_sequences(input_test, maxlen=maxlen)

from keras.layers import Dense
model_rnn = Sequential()
model_rnn.add(Embedding(max_features, 32))
model_rnn.add(SimpleRNN(32))
model_rnn.add(Dense(1, activation='sigmoid'))
model_rnn.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history_rnn = model_rnn.fit(input_train, y_train,epochs=10,batch_size=128,validation_split=0.2)

第三章處理這個數據集的簡單方法得到的測試精度是88%。與這個基準相比,這個小型循環網絡的表現並不好(驗證精度只有85%)。問題的部分原因在於輸入只考慮了前500個單詞而不是整個序列,因此RN 獲得的信息比第三章的模型更少。另一部分原因在於SimpleRNN不擅長處理長序列,比如文本。

SimpleRNN的最大問題是,在時刻t,理論上來說,它應該能夠記住許多時間步之前見過的信息,但實際上它是不可能學到這種長期依賴的。其原因在於梯度消失問題(vanishing gradient problem),這一效應類似於在層數較多的非循環網絡(即前饋網絡)中觀察到的效應:隨著層數的增加,網絡最終變得無法訓練。關於梯度消失,我這裡補充解釋下:首先為了擬合非線性函數,需要向神經網絡中引入非線性變換。其次神經網絡主要的訓練方法是BP算法,BP算法的基礎是導數的鏈式法則,也就是多個導數的乘積。以sigmoid 激活函數為例,其導數的取值範圍在0~0.25之間。

當層數很多時,多個導數的連乘會導致最終結果很小甚至趨於0,從而使得W_new=W_old- step * gradient中的gradient過小,基本上無法引起參數的擾動,也就是沒有將loss的信息傳遞到淺層網絡,導致網絡無法進一步訓練學習。為了解決梯度消失問題,LSTM層和GRU層應運而生。先來看LSTM層。其背後的長短期記憶(LSTM,long short-term memory)算法由Hochreiter和Schmidhuber 在1997 年開發。LSTM層是SimpleRNN層的一種變體,它增加了一種攜帶信息跨越多個時間步的方法。假設有一條傳送帶,其運行方向平行於你所處理的序列。序列中的信息可以在任意位置跳上傳送帶,然後被傳送到更晚的時間步。這實際上就是LSTM的原理:它保存信息以便後面使用,從而防止較早期的信號在處理過程中逐漸消失,算法流程如下:

為了更清晰地說明其運作流程,我截取一張CSDN的圖如下。LSTM算法相對傳統RNN主要增加了三個門運算,從左到右依次為遺忘門、輸入門、輸出門。遺忘門用來計算哪些信息需要忘記,通過sigmoid處理後為0到1的值,1表示全部保留,0表示全部忘記;輸入門用來計算哪些信息保存到狀態單元中,分兩部分,第一部確定當前輸入有多少是需要保存到單元狀態的。第二部分將當前輸入產生的新信息來添加到單元狀態中。結合這兩部分來創建一個新記憶;輸出門通過sigmoid函數計算需要輸出哪些信息,再乘以當前單元狀態通過tanh函數的值得到輸出。(進一步深入理解LSTM可以參考這篇文章:http://colah.github.io/posts/2015-08-Understanding-LSTMs/ )

來看一個更實際的問題:使用LSTM層來創建一個模型,然後在IMDB 數據上訓練模型。這個網絡與前面介紹的SimpleRNN 網絡類似。只需指定LSTM 層的輸出維度,其他所有參數(有很多)都使用Keras默認值即可。

from keras.models import Sequential
from keras.layers import LSTM
from keras.layers import Embedding,Dense
model_lstm = Sequential()
model_lstm.add(Embedding(max_features, 32))
model_lstm.add(LSTM(32))
model_lstm.add(Dense(1, activation='sigmoid'))
model_lstm.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])

history_lstm = model_lstm.fit(input_train, y_train,epochs=10,batch_size=128,

               validation_split=0.2)

這次驗證精度達到了89%。還不錯,肯定比SimpleRNN網絡好多了,這主要是因為LSTM 受梯度消失問題的影響要小得多。這個結果也比第3章的全連接網絡略好,雖然使用的數據量比第3章要少。此處在500 個時間步之後將序列截斷,而在第3章是讀取整個序列。但似乎LSTMde表現並沒有很突出,因為適用於評論分析全局的長期性結構(這正是LSTM所擅長的)對情感分析問題幫助不大。對於情感分析問題,觀察每條評論中出現了哪些詞及其出現頻率就可以很好地解決,即第3章全連接方法的做法。但遇到更加困難的自然語言處理問題,特別是問答和機器翻譯,這時LSTM優勢就明顯了。本部分內容將介紹提高循環神經網絡的性能和泛化能力的三種高級技巧:(1)循環dropout(recurrent dropout)。在循環層中使用dropout來降低過擬合。(2) 堆疊循環層(stacking recurrent layers)。這會提高網絡的表示能力(代價是更高的計算負荷)。(3) 雙向循環層(bidirectional recurrent layer)。將相同的信息以不同的方式(逆向)呈現給循環網絡,可以提高精度並緩解遺忘問題。

這裡將會用到一個天氣時間序列數據集(下載地址:https://s3.amazonaws.com/keras-datasets/jena_climate_2009_2016.csv.zip)。它由德國耶拿的馬克思• 普朗克生物地球化學研究所的氣象站記錄。該數據集中每10分鐘記錄14個不同的量(比如氣溫、氣壓、溼度、風向等),涵蓋2009—2016年的數據。這個數據集非常適合用來學習處理數值型時間序列,我們將會用它來構建模型,輸入最近的一些數據(幾天的數據點),然後預測24 小時之後的氣溫。

先將原始數據轉換為numpy數組表示的形式,每一行代表一個時間步(每10分鐘為一步)。簡單繪製了溫度序列圖,可以清楚地看到溫度每年的周期性變化。

#觀察耶拿天氣數據集的數據
import os
import numpy as np
data_dir = "C:\\Users\\user\\PycharmProjects\\test_project\\jena_climate"
fname = os.path.join(data_dir, 'jena_climate_2009_2016.csv')
f = open(fname)
data = f.read()
f.close()
lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]
#將數據轉換為numpy數組
float_data = np.zeros((len(lines), len(header) - 1))
for i, line in enumerate(lines):
values = [float(x) for x in line.split(',')[1:]]
float_data[i, :] = values
#繪製溫度(第一列)
from matplotlib import pyplot as plt
temp = float_data[:, 1]
plt.plot(range(len(temp)), temp)

該任務的目的是:給定過去lookback個時間步之內的數據,預測delay個時間步之後的溫度。

lookback =1440:給定過去10天內的觀測數據(一個數據代表10分鐘);

steps = 6:觀測數據的採樣頻率是每6個數據(即小時)一個數據點;

delay = 144:目標是未來24 小時即1天之後的數據。

之前數組float_data包含了全部數據,我們將使用前200000個時間步作為訓練數據,因為數據量綱不同,先對這部分數據計算平均值和標準差。

#先將數據標準化
mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std

對於每個時間節點,我們將提取之前1440/6即240個觀測數據作為一組輸入,預測未來24小時即1天之後的溫度。因此這裡會有很多重複數據,為了避免數據冗餘,這裡編寫一個Python生成器,以當前的浮點數數組作為輸入,並從最近的數據中生成數據批量,同時生成未來的目標溫度。它返回一個元組(samples, targets),其中samples是(batch_size,lookback/steps即1440/6,features即14)的三維數組,targets是(batch_size,)的一維數組。

#生成時間序列樣本及其目標的生成器
def generator(data, lookback, delay, min_index, max_index,shuffle=False, batch_size=128, step=6):
if max_index is None:
max_index = len(data) - delay - 1
i = min_index + lookback
while 1:
#是否隨機打亂取數據
if shuffle:
rows = np.random.randint(min_index + lookback, max_index, size=batch_size)
else:
if i + batch_size >= max_index:
i = min_index + lookback
rows = np.arange(i, min(i + batch_size, max_index))
i += len(rows)
samples = np.zeros((len(rows), lookback // step,data.shape[-1]))
targets = np.zeros((len(rows),))
for j, row in enumerate(rows):
indices = range(rows[j] - lookback, rows[j], step)
samples[j] = data[indices]
targets[j] = data[rows[j] + delay][1]
yield samples, targets

下面使用這個抽象的generator函數來實例化三個生成器:一個用於訓練,一個用於驗證,還有一個用於測試。每個生成器分別讀取原始數據的不同時間段:訓練生成器讀取前200000個時間步,驗證生成器讀取隨後的100 000 個時間步,測試生成器讀取剩下的時間步。

#準備訓練生成器、驗證生成器和測試生成器
lookback = 1440
step = 6
delay = 144
batch_size = 128
train_gen = generator(float_data,lookback=lookback,delay=delay,min_index=0,

max_index=200000,shuffle=True,step=step,batch_size=batch_size)

val_gen = generator(float_data,lookback=lookback,delay=delay,min_index=200001,

max_index=300000,step=step,batch_size=batch_size)

test_gen = generator(float_data,lookback=lookback,delay=delay,min_index=300001,

max_index=None,step=step,batch_size=batch_size)

val_steps = (300000 - 200001 - lookback) //batch_size
test_steps = (len(float_data) - 300001 - lookback) //batch_size

為了比較各種深度學習模型的優劣,我們先設計一個簡單的比較基準。我們可以合理地假設,溫度時間序列是連續的(明天的溫度很可能接近今天的溫度),並且具有每天的周期性變化。因此,一種基於常識的方法就是始終預測24小時後的溫度等於現在的溫度,並使用平均絕對誤差(MAE)指標來評估這種方法。

def evaluate_naive_method():
batch_maes = []
for step in range(val_steps):
samples, targets = next(val_gen)
#以每個樣本的組後一組時間的溫度作為預測溫度
preds = samples[:, -1, 1]
mae = np.mean(np.abs(preds - targets))
#求平均MAE
batch_maes.append(mae)
evaluate_naive_method()

得到的平均MAE為0.29,因為數據經過標準化,即0.29個標準差。這個平均絕對誤差還是相當大的,接下來的任務是利用深度學習模型來改進結果。首先我們使用最簡單的密集連接模型,先將數據展平然後通過兩個Dense 層並運行,對於回歸問題最後一個Dense層無需使用激活函數。我們使用MAE作為損失。評估數據和評估指標都與常識方法完全相同,所以可以直接比較兩種方法的結果。

#訓練並評估一個密集連接模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
#先將時間序列展平,注意input_shape的0緯為lookback/steps
model.add(layers.Flatten(input_shape=(lookback // step, float_data.shape[-1])))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,steps_per_epoch=500,epochs=20,
validation_data=val_gen,validation_steps=val_steps)

部分驗證損失接近不包含學習的基準方法,但這個結果並不可靠。事實證明,超越簡單基準並不容易,常識中包含了大量有價值的信息,而機器學習模型並不知道這些信息。

如果從數據到目標之間存在一個簡單且表現良好的模型(即基於常識的基準方法),那為什麼訓練模型沒有找到這個模型並進一步改進呢?原因在於,這個簡單的解決方案並不是訓練過程所要尋找的目標。我們在模型空間(即假設空間)中搜索解決方案,這個模型空間是具有我們所定義的架構的所有兩層網絡組成的空間。這些網絡已經相當複雜了。如果你在一個複雜模型的空間中尋找解決方案,那麼可能無法學到簡單且性能良好的基準方法,雖然技術上來說它屬於假設空間的一部分。
通常來說,這對機器學習是一個非常重要的限制:如果學習算法沒有被硬編碼要求去尋找特定類型的簡單模型,那麼有時候參數學習是無法找到簡單問題的簡單解決方案的。第一個全連接方法的效果並不好,但這並不意味著機器學習不適用於這個問題。前一個方法首先將時間序列展平,這從輸入數據中刪除了時間的概念。而原始數據是一個序列,其中因果關係和順序都很重要。接著我們將使用Chung等人在2014年開發的GRU 層a,而不是上一節介紹的LSTM 層。門控循環單元(GRU,gated recurrent unit)層的工作原理與LSTM相同。但它做了一些簡化,因此運行的計算代價更低(雖然表示能力可能不如LSTM)

#訓練並評估一個基於GRU 的模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.GRU(32, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,steps_per_epoch=500,epochs=20,
validation_data=val_gen,validation_steps=val_steps)

新的驗證MAE最低約為0.265,遠優於基於常識的基準方法,也證明了循環網絡與序列展平的密集網絡相比在時間序列任務上的優勢。

使用循環dropout 來降低過擬合    

上圖訓練LOSS和驗證LOSS在第五輪後開始反向,模型存在明顯的過擬合。第三章曾經學過dropout方法,即將某一層的輸入單元隨機設為0,其目的是打破該層訓練數據中的偶然相關性。

但循環網絡中如何正確地使用dropout並不是一個簡單的問題。人們早就知道,在循環層前面應用dropout,這種正則化會妨礙學習過程,而不是有所幫助。2015 年,在關於貝葉斯深度學習的博士論文《Uncertainty in deep learning》中,Yarin Gal確定了在循環網絡中使用dropout 的正確方法:對每個時間步應該使用相同的dropout掩碼(dropoutmask,相同模式的捨棄單元),而不是讓dropout掩碼隨著時間步的增加而隨機變化。此外,為了對GRU、LSTM等循環層得到的表示做正則化,應該將不隨時間變化的dropout掩碼應用於層的內部循環激活(叫作循環dropout掩碼)。對每個時間步使用相同的dropout掩碼,可以讓網絡沿著時間正確地傳播其學習誤差,而隨時間隨機變化的dropout掩碼則會破壞這個誤差信號,並且不利於學習過程。

Keras的每個循環層都有兩個與dropout相關的參數:一個是dropout,它是一個浮點數,指定該層輸入單元的dropout 比率;另一個是recurrent_dropout,指定循環單元的dropout 比率(注意這是兩個概念)。通過向GRU層中添加dropout和循環dropout,看一下這麼做對過擬合的影響。因為使用dropout正則化的網絡總是需要更長的時間才能完全收斂,所以網絡訓練輪次增加為原來的2倍。

#訓練並評估一個使用dropout正則化的基於GRU循環層的模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.GRU(32,dropout=0.2,recurrent_dropout=0.2,
input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,steps_per_epoch=500,epochs=40,
validation_data=val_gen,validation_steps=val_steps)

從驗證損失來看,前30輪不再過擬合。不過,雖然評估分數更加穩定,但最佳分數並沒有比之前低很多。

模型不再過擬合,但似乎遇到了性能瓶頸,所以我們應該考慮增加網絡容量。增加網絡容量的通常做法是增加每層單元數或增加層數。循環層堆疊(recurrent layer stacking)是構建更加強大的循環網絡的經典方法,例如,目前谷歌翻譯算法就是7 個大型LSTM層的堆疊——這個架構很大。

在Keras中逐個堆疊循環層,所有中間層都應該返回完整的輸出序列,即一個形狀為(samples,timesteps,features)的3D張量,而不是只返回最後一個時間步的輸出。這可以通過指定return_sequences=True來實現。

#訓練並評估一個使用dropout正則化的堆疊GRU模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.GRU(32,dropout=0.1,recurrent_dropout=0.5,return_sequences=True,
input_shape=(None, float_data.shape[-1])))
model.add(layers.GRU(64, activation='relu',dropout=0.1,recurrent_dropout=0.5))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,steps_per_epoch=500,epochs=40,
validation_data=val_gen,validation_steps=val_steps)

驗證損失如下圖,可以看到,添加一層的確對結果有所改進,但並不顯著。我們可以得出兩個結論:因為過擬合仍然不是很嚴重,所以可以放心地增大每層的大小,以進一步改進驗證損失,但這麼做的計算成本很高;添加一層後模型並沒有顯著改進,所以你可能發現,提高網絡能力的回報在逐漸減小。

最後介紹一種叫雙向RNN(bidirectional RNN)的方法。雙向RNN是一種常見的RNN 變體,它在某些任務上的性能比普通RNN更好。RNN特別依賴於順序或時間,RNN按順序處理輸入序列的時間步,而打亂時間步或反轉時間步會完全改變RNN從序列中學到的表示。正是由於這個原因,如果順序對問題很重要(比如溫度預測問題),RNN的表現會很好。雙向RNN利用了RNN的順序敏感性:它包含兩個普通RNN,比如你已經學過的GRU層或LSTM層,每個RNN分別沿一個方向對輸入序列進行處理(時間正序和時間逆序),然後將它們的表示合併在一起。通過沿這兩個方向處理序列,雙向RNN能夠捕捉到可能被單向RNN忽略的模式。本節的RNN層都是按時間正序處理序列(更早的時間步在前),這可能是一個隨意的決定。如果RNN按時間逆序處理輸入序列(更晚的時間步在前),能否表現得足夠好呢?我們在嘗試一下這種方法,看一下會發生什麼。只需要編寫一個數據生成器的變體,將輸入序列沿著時間維度反轉(即將最後一行代碼替換為yield samples[:, ::-1, :], targets)。本節一開始用了一個單GRU層的網絡,我們訓練一個與之相同的網絡。

從結果來看,逆序GRU的效果甚至比基於常識的基準方法還要差很多,這說明在本例中,按時間正序處理對成功解決問題很重要。這合乎常理:GRU層通常更善於記住最近的數據,而不是久遠的數據,與更早的數據點相比,更靠後的天氣數據點對問題自然具有更高的預測能力(這也是基於常識的基準方法非常強大的原因)。因此,溫度預測問題按時間正序的模型必然會優於時間逆序的模型。對許多其他問題(包括自然語言)而言,情況並不是這樣:直覺上來看,一個單詞對理解句子的重要性通常並不取決於它在句子中的位置。我們嘗試對第二節中,使用LSTM方法預測IMDB 數據的例子做拓展,使用逆序序列作為輸入。

#獲取逆序序列
x_train = [x[::-1] for x in x_train]
x_test = [x[::-1] for x in x_test]

該結果的性能與正序LSTM幾乎相同。值得注意的是,在這樣一個文本數據集上,逆序處理的效果與正序處理一樣好,這證實了一個假設:雖然單詞順序對理解語言很重要,但使用哪種順序並不重要。雙向RNN 正是利用這個想法來提高正序RNN的性能。它從兩個方向查看數據從而得到更加豐富的表示,並捕捉到僅使用正序RNN時可能忽略的一些模式,即在逆序序列上訓練的RNN學到的表示不同於在原始序列上學到的表示。

在Keras中將一個雙向RNN實例化需要使用Bidirectional層,它的第一個參數是一個循環層實例。Bidirectional對這個循環層創建了第二個單獨實例,然後使用一個實例按正序處理輸入序列,另一個實例按逆序處理輸入序列。我們在溫度預測任務中嘗試使用這一方法。

#訓練一個雙向GRU
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
#Bidirectional對一個GRU層雙向實例化
model.add(layers.Bidirectional(layers.GRU(32), input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,steps_per_epoch=500,epochs=40,
validation_data=val_gen,validation_steps=val_steps)

這個模型的表現與普通單層GRU差不多一樣好。其原因很容易理解:所有的預測能力肯定都來自於正序的那一半網絡,因為逆序的那一半在時間序列任務上的表現非常糟糕。

(1)如果序列數據的整體順序很重要,那麼最好使用循環網絡來處理。時間序列通常都是這樣,最近的數據可能比久遠的數據包含更多的信息量;(2)如果整體順序沒有意義,那麼一維卷積神經網絡可以實現同樣好的效果,而且計算代價更小。文本數據通常都是這樣,在句首發現關鍵詞和在句尾發現關鍵詞一樣都很有意義。本章介紹Keras函數式API、Keras回調函數和TensorBoard可視以及其他幾種高級用法,包括批標準化、深度可分離卷積、超參數優化和模型集成。之前介紹的所有神經網絡都是用Sequential模型實現的。Sequential模型假設網絡只有一個輸入和一個輸出,且網絡是層的線性堆疊,如下圖:

然而有些網絡需要多個獨立的輸入、有些網絡則需要多個輸出,而有些網絡在層與層之間具有內部分支,這使得網絡看起來像是層構成的圖(graph),而不是層的線性堆疊。例如,有些任務需要多模態(multimodal)輸入。這些任務合併來自不同輸入源的數據,並使用不同類型的神經層處理不同類型的數據。假設有一個深度學習模型,試圖利用下列輸入來預測一件二手衣服最可能的市場價格:用戶提供的元數據(比如商品品牌、已使用年限等)、用戶提供的文本描述與商品照片。如果你只有元數據,那麼可以使用one-hot編碼,然後用密集連接網絡來預測價格。如果你只有文本描述,那麼可以使用循環神經網絡或一維卷積神經網絡。如果你只有圖像,那麼可以使用二維卷積神經網絡。怎麼才能同時使用這三種數據呢?一種樸素的方法是訓練三個獨立的模型,然後對三者的預測做加權平均。但這種方法可能不是最優的,因為模型提取的信息可能存在冗餘。更好的方法是使用一個可以同時查看所有可用的輸入模態的模型,從而聯合學習一個更加精確的數據模型。

同樣,有些任務需要預測輸入數據的多個目標屬性。給定一部小說的文本,你可能希望將它按類別自動分類(比如愛情小說或驚悚小說),同時還希望預測其大致的寫作日期。當然可以訓練兩個獨立的模型:一個用於劃分類別,一個用於預測日期。但由於這些屬性並不是統計無關的,你可以構建一個更好的模型,用這個模型來學習同時預測類別和日期。這種聯合模型將有兩個輸出。因為類別和日期之間具有相關性,所以知道小說的寫作日期有助於模型在小說類別空間中學到豐富而又準確的表示,反之亦然。

此外,許多最新開發的神經架構要求非線性的網絡拓撲結構,即網絡結構為有向無環圖。比如,Inception 系列網絡(由Google 的Szegedy 等人開發)a依賴於Inception模塊,其輸入被多個並行的卷積分支所處理,然後將這些分支的輸出合併為單個張量

最近還有一種趨勢是向模型中添加殘差連接(residual connection),它最早出現於ResNet 系列網絡(由微軟的何愷明等人開發)。殘差連接是將前面的輸出張量與後面的輸出張量相加,從而將前面的表示重新注入下遊數據流中,這有助於防止信息處理流程中的信息損失。

這時就要用到Keras的函數API(functional API)。使用函數式API,你可以直接操作張量,也可以把層當作函數來使用,接收張量並返回張量。首先來看一個最簡單的示例,展示一個簡單的Sequential模型以及對應的函數式API實現,兩種方法是等價的。

from keras.models import Sequential, Model
from keras import layers
from keras import Input
#Sequential實現方式
seq_model = Sequential()
seq_model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
seq_model.add(layers.Dense(32, activation='relu'))
seq_model.add(layers.Dense(10, activation='softmax'))

#對應的函數式API實現方式,每一層都可以所謂下一層的輸入
input_tensor = Input(shape=(64,))
x = layers.Dense(32, activation='relu')(input_tensor)
x = layers.Dense(32, activation='relu')(x)
output_tensor = layers.Dense(10, activation='softmax')(x)
model = Model(input_tensor, output_tensor)

Keras會在後臺檢索從input_tensor到output_tensor所包含的每一層,並將這些層組合成一個類圖的數據結構,即一個Model。當然,這種方法有效的原因在於,output_tensor是通過對input_tensor進行多次變換得到的。但如果用不相關的輸入和輸出來構建一個模型那麼會報錯,模型的編譯、訓練跟Sequential模型相同。

model.compile(optimizer='rmsprop', loss='categorical_crossentropy',metrics=['acc'])
import numpy as np
x_train = np.random.random((1000, 64))
y_train = np.random.random((1000, 10))
model.fit(x_train, y_train, epochs=10, batch_size=128)
score = model.evaluate(x_train, y_train)answer_vocabulary_size = 500

來看一個典型的輸入問答模型:一個自然語言描述的問題和一個文本片段(比如新聞文章),後者提供用於回答問題的信息。然後模型要生成一個回答,在最簡單的情況下,這個回答只包含一個詞,可以通過對某個預定義的詞表做softmax得到。

#用函數式API實現雙輸入問答模型
from keras.models import Model
from keras import layers
from keras import Input
text_vocabulary_size = 10000
question_vocabulary_size = 10000
answer_vocabulary_size = 500
#設置層名為text
text_input = Input(shape=(None,), dtype='int32', name='text')
embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_input)
encoded_text = layers.LSTM(32)(embedded_text)
#設置層名為question
question_input = Input(shape=(None,),dtype='int32',name='question')
embedded_question = layers.Embedding(question_vocabulary_size, 32)(question_input)
encoded_question = layers.LSTM(16)(embedded_question)
#根據最後一個維度axis=-1(通常是特徵緯度)合併兩個輸入
concatenated = layers.concatenate([encoded_text, encoded_question],axis=-1)
answer = layers.Dense(answer_vocabulary_size,activation='softmax')(concatenated)
model = Model([text_input, question_input], answer)

#編譯模型,這個跟Sequential方法
model.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['acc'])
import numpy as np
num_samples = 1000
max_length = 100
#生成樣本大小一樣的text、question、answers
text = np.random.randint(1, text_vocabulary_size,size=(num_samples, max_length))
question = np.random.randint(1, question_vocabulary_size,size=(num_samples, max_length))
answers = np.random.randint(answer_vocabulary_size, size=(num_samples))
#將答案編碼
answers = keras.utils.to_categorical(answers, answer_vocabulary_size)
#訓練模型
model.fit([text, question], answers, epochs=10, batch_size=128)
model.fit({'text': text, 'question': question}, answers,epochs=10, batch_size=128)

注意 ,最後模型的兩個訓練語句是等價的,前一個使用列表形式,後一個使用字典形式,但這種方法只有對輸入進行命名之後才能用。

 (2)多輸出模型

利用相同的方法還可以使用函數式API 來構建具有多個輸出(或多頭)的模型。一個簡單的例子就是一個網絡試圖同時預測數據的不同性質,比如輸入某個匿名人士的一系列社交媒體發帖,然後嘗試預測那個人的屬性,比如年齡、性別和收入水平。

訓練多輸出模型需要能夠對網絡的各個頭指定不同的損失函數,例如年齡預測是標量回歸任務,收入預測是多分類任務,性別預測是二分類任務,三者需要不同的訓練過程。但是,梯度下降要求將一個標量最小化,所以為了能夠訓練模型,我們必須將這些損失合併為單個標量。合併不同損失最簡單的方法就是對所有損失求和。在Keras中,你可以在編譯時使用損失組成的列表或字典來為不同輸出指定不同損失,然後將得到的損失值相加得到一個全局損失,並在訓練過程中將這個損失最小化。同樣不同的任務使用的損失函數也不同,上述三個任務的損失函數分別為:'mse', 'categorical_crossentropy', 'binary_crossentropy'。

#用函數式API實現一個三輸出模型
from keras import layers
from keras import Input
from keras.models import Model
vocabulary_size = 50000
num_income_groups = 10
posts_input = Input(shape=(None,), dtype='int32', name='posts')
embedded_posts = layers.Embedding(256, vocabulary_size)(posts_input)
#對於分本分析,這裡用了一維卷積網絡
x = layers.Conv1D(128, 5, activation='relu')(embedded_posts)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
#GlobalMaxPooling1D將輸出變成二維張量
x = layers.GlobalMaxPooling1D()(x)
x = layers.Dense(128, activation='relu')(x)
#age預測就是一個整數,直接輸出線性結果就行
age_prediction = layers.Dense(1, name='age')(x)
#收入一共有10種,所以用softmax激活
income_prediction = layers.Dense(num_income_groups,activation='softmax',name='income')(x)
#性別有兩種,所以用sigmoid激活
gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)
#一個輸入,三個輸出

model = Model(posts_input,[age_prediction, income_prediction, gender_prediction])

#模型編譯
model.compile(optimizer='rmsprop',loss=['mse', 'categorical_crossentropy',
'binary_crossentropy'])
model.compile(optimizer='rmsprop',loss={'age': 'mse','income': 'categorical_crossentropy',
'gender': 'binary_crossentropy'})

這裡需要注意的是:嚴重不平衡的損失貢獻會導致模型表示針對單個損失值最大的任務優先進行優化,而不考慮其他任務的優化。為了解決這個問題,需要為每個損失值對最終損失的貢獻分配不同大小的重要性。如果不同的損失值具有不同的取值範圍,那麼這一方法尤其有用。比如用於年齡回歸任務的均方誤差(MSE)損失值通常在3~5 左右,而用於性別分類任務的交叉熵損失值可能低至0.1。在這種情況下,為了平衡不同損失的貢獻,我們可以讓交叉熵損失的權重取10,而MSE 損失的權重取0.5。

#模型編譯,並加權重,兩種表達等價
model.compile(optimizer='rmsprop',
loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'],loss_weights=[0.25, 1., 10.])
model.compile(optimizer='rmsprop',loss={'age': 'mse','income': 'categorical_crossentropy',
'gender': 'binary_crossentropy'},
loss_weights={'age': 0.25,'income': 1.,'gender': 10.})

#訓練模型,同樣兩種表達等價

model.fit(posts, [age_targets, income_targets, gender_targets],epochs=10, batch_size=64)
model.fit(posts, {'age': age_targets,'income': income_targets,
'gender': gender_targets},epochs=10, batch_size=64)

(3)層組成的有向無環圖

利用函數式API還可以實現具有複雜的內部拓撲結構的網絡。Keras中的神經網絡可以是層組成的任意有向無環圖(directed acyclicgraph)。無環(acyclic)詞很重要,即這些圖不能有循環,張量x不能成為生成x的某一層的輸入。唯一允許的處理循環(即循環連接)是循環層的內部循環。一些常見神經網絡組件都以圖的形式實現。兩個著名的組件是Inception 模塊和殘差連接,下面來看一下如何用Keras實現這二者。Inception由Google的Christian Szegedy及其同事在2013—2014年開發,它是模塊的堆疊,這些模塊本身看起來像是小型的獨立網絡,被分為多個並行分支。Inception模塊最基本的形式包含3~4 個分支,首先是一個1×1 的卷積,然後是一個3×3 的卷積,最後將所得到的特徵連接在一起。這種設置有助於網絡分別學習空間特徵和逐通道的特徵,這比聯合學習這兩種特徵更加有效。Inception模塊也可能具有更複雜的形式,通常會包含池化運算、不同尺寸的空間卷積(比如在某些分支上使用5×5 的卷積代替3×3 的卷積)和不包含空間卷積的分支(只有一個1×1 卷積),如下圖的Inception V3

使用函數式API就可以實現上圖中的模塊,其代碼如下所示。這個例子假設我們有一個四維輸入張量x。注意,每個分支都有相同的步幅值(strides=2),這對於保持所有分支輸出具有相同的尺寸是很有必要的,這樣最後才能將它們連接在一起。

#KerasInception實現模塊

from keras import layers

#注意這裡二維卷積層的第二個參數同之前的(1,1)或(3,3),表示1*1或3*3卷積核

branch_a = layers.Conv2D(128, 1,activation='relu', strides=2)(x)

branch_b = layers.Conv2D(128, 1, activation='relu')(x)
branch_b = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_b)

branch_c = layers.AveragePooling2D(3, strides=2)(x)
branch_c = layers.Conv2D(128, 3, activation='relu')(branch_c)

branch_d = layers.Conv2D(128, 1, activation='relu')(x)
branch_d = layers.Conv2D(128, 3, activation='relu')(branch_d)
branch_d = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_d)

#同樣沿著最後一個軸合併
output = layers.concatenate([branch_a, branch_b, branch_c, branch_d], axis=-1)

nception V3架構內置於keras.applications.inception_v3.InceptionV3中,其中包括在ImageNet 數據集上預訓練得到的權重。與其密切相關的另一個模型是Xception,它也是Keras.applications模塊的一部分。Xception代表極端Inception(extreme inception),它是一種卷積神經網絡架構,其靈感可能來自於Inception。Xception將分別進行通道特徵學習與空間特徵學習的想法推向邏輯上的極端,並將Inception模塊替換為深度可分離卷積,其中包括一個逐深度卷積(即一個空間卷積,分別對每個輸入通道進行處理)和之後一個逐點卷積(即1×1 卷積)。這個深度可分離卷積實際上是Inception 模塊的一種極端形式,其空間特徵和通道特徵被完全分離。Xception的參數個數與Inception V3大致相同,但因為它對模型參數的使用更加高效,所以在ImageNet 以及其他大規模數據集上的運行性能更好,精度也更高。這裡有必要拓展說一下深度可分離卷積和逐點卷積(即1×1 卷積),書上沒有詳細介紹,詳細技術細節可參考《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》。上中圖(a)代表標準卷積。假設輸入特徵圖尺寸為  ,卷積核尺寸為 ,輸出特徵圖尺寸為 ,則標準卷積層的參數量為 。圖(b)代表深度卷積,圖(c)代表逐點卷積,兩者合起來就是深度可分離卷積。即深度可分離卷積=深度卷積+逐點卷積。深度卷積負責濾波,尺寸為(DK,DK,1),每層輸入通道分配一個權重矩陣共M個。逐點卷積負責轉換通道,尺寸為(1,1,M),每層輸出通道分配一個共N個,作用在深度卷積的輸出特徵映射上。因此深度卷積參數量為,逐點卷積參數量為 ,所以深度可分離卷積參數量是標準卷積的 ,參數數量減少很多!!!再來看一張書中解釋深度可分離卷積的圖。如果假設輸入中的空間位置高度相關,但不同的通道之間相對獨立,那麼這麼做是很有意義的。如前所述它需要的參數要少很多,計算量也更小,因此可以得到更小、更快的模型,往往能夠使用更少的數據學到更好的表示,從而得到性能更好的模型。

一句話總結就是,深度可分離卷積將空間特徵學習(只考慮高度*寬度,每一層單獨學習)和通道特徵學習(所有層一起學,每次只學習一個像素點)分開,最後再連接。

b、殘差連接

殘差連接是讓前面某層的輸出作為後面某層的輸入,從而在序列網絡中有效地創造了一條捷徑。殘差連接解決了困擾所有大規模深度學習模型的兩個共性問題:梯度消失和表示瓶頸。通常來說,向任何多於10 層的模型中添加殘差連接都可能會有所幫助。

前面層的輸出沒有與後面層的激活連接在一起,而是與後面層的激活相加(這裡假設兩個激活的形狀相同)。如果輸出特徵圖的尺寸相同,在Keras中實現殘差連接的方法如下,假設有一個四維輸入張量x

#輸出特徵圖相同

from keras import layers

x = ...
#padding='same'為了輸出特徵圖和輸入特徵圖空間尺寸相同
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.add([y, x])

如果輸出特徵圖的尺寸不同,需要調整後再連接。同樣假設有一個四維輸入張量x。注意由於採用步幅為2(strides=2)的MaxPooling2D後y的輸出特徵圖尺寸縮小了一半,所以x也要做同樣的處理。

#輸出特徵圖不相同
from keras import layers
x = ...
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.MaxPooling2D(2, strides=2)(y)
residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)
y = layers.add([y, residual])

c、共享層權重

函數式API還能夠允許多次重複使用一個層實例。如果你對一個層實例調用兩次,而不是每次調用都實例化一個新層,那麼每次調用可以重複使用相同的權重。這樣就可以構建具有共享分支的模型,即幾個分支全都共享相同的知識並執行相同的運算。舉個例子,假設一個模型想要評估兩個句子之間的語義相似度。這個模型有兩個輸入(需要比較的兩個句子),並輸出一個範圍在0~1 的分數,0 表示兩個句子毫不相關,1 表示兩個句子完全相同或只是換一種表述。在這種設置下,兩個輸入句子是可以互換的,因為語義相似度是一種對稱關係,A相對於B的相似度等於B相對於A的相似度。因此沒必要學習兩個單獨的模型,相反只需要用一個LSTM層來處理兩個句子,這個LSTM層的表示(即它的權重)是同時基於兩個輸入來學習的。我們將其稱為連體LSTM(Siamese LSTM)或共享LSTM(shared LSTM)模型。

#共享LSTM
from keras import layers
from keras import Input
from keras.models import Model
#實例化一個LSTM層
lstm = layers.LSTM(32)
left_input = Input(shape=(None, 128))
left_output = lstm(left_input)
right_input = Input(shape=(None, 128))

right_output = lstm(right_input)

#基於最後一個維度合併兩個LSTM輸出

merged = layers.concatenate([left_output, right_output], axis=-1)
#基於合併的兩層LSTM構建一個分類器
predictions = layers.Dense(1, activation='sigmoid')(merged)
model = Model([left_input, right_input], predictions)
model.fit([left_data, right_data], targets)


2、使用Keras回調函數和TensorBoard來檢查並監控深度學習模型

本節介紹可以實時監控訓練過程的方法

之前所有例子都採用這樣一種策略:訓練足夠多的輪次,這時模型已經開始過擬合,然後根據運行結果來確定訓練所需要的正確輪數,然後使用這個最佳輪數從頭開始再啟動一次新的訓練。當然,這種方法很浪費時間。

處理這個問題的更好方法是,當觀測到驗證損失不再改善時就停止訓練。這可以使用Keras回調函數來實現。回調函數(callback)是在調用fit函數時傳入模型的一個對象(即實現特定方法的類實例),它在訓練過程中的不同時間點都會被模型調用。它可以訪問關於模型狀態與性能的所有可用數據,還可以採取行動:中斷訓練、保存模型、加載一組不同的權重或改變模型的狀態。

keras.callbacks 模塊包含許多內置的回調函數,下面列出了其中一些,但還有很多沒有列出來:模型檢查點(model checkpointing):在訓練過程中的不同時間點保存模型的當前權重,對應keras.callbacks.ModelCheckpoint;提前終止(early stopping):如果驗證損失不再改善,則中斷訓練(當然,同時保存在訓練過程中得到的最佳模型),對應keras.callbacks.EarlyStopping;在訓練過程中動態調節某些參數值:比如優化器的學習率,對應keras.callbacks.LearningRateScheduler或keras.callbacks.ReduceLROnPlateau;在訓練過程中記錄訓練指標和驗證指標,或將模型學到的表示可視化(這些表示也在不斷更新),對應keras.callbacks.CSVLogger,Keras 進度條就是一個回調函數!如果監控的目標指標在設定的輪數內不再改善,可以用EarlyStopping回調函數來中斷訓練。比如,這個回調函數可以在剛開始過擬合的時候就中斷訓練,從而避免用更少的輪次重新訓練模型。這個回調函數通常與ModelCheckpoint結合使用,後者可以在訓練過程中持續不斷地保存模型(你也可以選擇只保存目前的最佳模型,即一輪結束後具有最佳性能的模型)。

#ModelCheckpoint與EarlyStopping回調函數
import keras

#回調函數列表,可以傳入任意個數的回調函數

callbacks_list = [

#監控模型的驗證精度,且精度在多於一輪的時間(即兩輪)內不再改善,中斷訓練

keras.callbacks.EarlyStopping(monitor='acc',patience=1),

#如果val_loss沒有改善,那麼不需要保存新模型文件

keras.callbacks.ModelCheckpoint(filepath='my_model.h5',monitor='val_loss',save_best_only=True)

]

model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
#由於回調函數要監控驗證損失和驗證精度,所以在調用fit時需要傳入validation_data
model.fit(x, y,epochs=10,batch_size=32,callbacks=callbacks_list,validation_data=(x_val, y_val))

訓練過程中,可以使用ReduceLROnPlateau回調函數降低(或提高)學習率。在訓練過程中如果出現了損失平臺(loss plateau),那麼增大或減小學習率都是跳出局部最小值的有效策略。

#ReduceLROnPlateau回調函數
#如果驗證損失在10輪內都沒有改善,觸發這個回調函數將學習率除以10
callbacks_list = [keras.callbacks.ReduceLROnPlateau(monitor='val_loss'factor=0.1,patience=10)]
#因為回調函數要監控驗證損失,所以調用fit時需傳入validation_data
model.fit(x, y,epochs=10,batch_size=32,callbacks=callbacks_list,validation_data=(x_val, y_val))

如果需要在訓練過程中採取特定行動,而這項行動又沒有包含在內置回調函數中,那麼可以編寫自己的回調函數。實現方式是創建keras.callbacks.Callback 類的子類。這種情況比較少,具體可以網上找教程不再贅述。

(2)TensorBoard:TensorFlow的可視化框架

取得實驗進展是一個反覆迭代的過程(或循環):首先你有一個想法,並將其表述為一個實驗,用於驗證你的想法是正確;接著運行這個實驗,並處理其生成的信息。這又激發了你的下一個想法。在這個循環中實驗的迭代次數越多,你的想法也就變得越來越精確、越來越強大。

本節介紹TensorBoard的是一款內置於TensorFlow中的基於瀏覽器的可視化工具。注意,只有當Keras使用TensorFlow後端時,這一方法才能用於Keras 模型。TensorBoard的主要用途是在訓練過程中以可視化的方法監控模型內部發生的一切。如果你監控了除模型最終損失之外的更多信息,那麼可以更清楚地了解模型做了什麼、沒做什麼,並且能夠更快地取得進展。TensorBoard具有下列巧妙的功能,都在瀏覽器中實現:在開始使用TensorBoard之前需要創建一個目錄,用於保存它生成的日誌文件,比如'my_log_dir',然後需要在啟動TensorBoard,命令行進入tensorboard所在文件夾,鍵入」tensorboard.exe --logdir=【你的日誌文件路徑】「。下面是一個例子。

#使用了TensorBoard的文本分類模型
import keras
from keras import layers
from keras.datasets import imdb
from keras.preprocessing import sequence
max_features = 2000
max_len = 500
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
x_train = sequence.pad_sequences(x_train, maxlen=max_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_len)
model = keras.models.Sequential()
model.add(layers.Embedding(max_features, 128,input_length=max_len,name='embed'))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))
model.summary()
model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])

#注意記錄日誌的文件夾是'my_log_dir'
callbacks = [keras.callbacks.TensorBoard(log_dir='my_log_dir',
histogram_freq=1,embeddings_freq=1,)]
history = model.fit(x_train, y_train,epochs=20,batch_size=128,
validation_split=0.2,callbacks=callbacks)

然後用瀏覽器打開http://localhost:6006,並查看模型的訓練過程。除了訓練指標和驗證指標的實時圖表之外,你還可以訪問HISTOGRAMS標籤頁,並查看美觀的直方圖可視化,直方圖中是每層的激活值。EMBEDDINGS標籤頁讓你可以查看輸入詞表中2000 個單詞的嵌入位置和空間關係,它們都是由第一個Embedding層學到的。因為嵌入空間是128 維的,所以TensorBoard會使用你選擇的降維算法自動將其降至二維或三維。

此外GRAPHS標籤頁顯示的是Keras模型背後的底層TensorFlow運算圖的交互式可視化。可見圖中的內容比之前想像的要多很多。在Keras中定義模型時可能看起來很簡單,只是幾個基本層的堆疊;但在底層,你需要構建相當複雜的圖結構。其中許多內容都與梯度下降過程有關。你所見到的內容與你所操作的內容之間存在這種複雜度差異,這正是你選擇使用Keras來構建模型、而不是使用原始TensorFlow 從頭開始定義所有內容的主要動機。

(1)高級架構模式之標準化

之前的示例都是在將數據輸入模型之前對數據做標準化。但在網絡的每一次變換之後都應該考慮數據標準化。即使輸入Dense或Conv2D網絡的數據均值為0、方差為1,也沒有理由假定網絡輸出的數據也是這樣。批標準化(batch normalization)是Ioffe 和Szegedy在2015年提出的一種層的類型(在Keras中是BatchNormalization),即使在訓練過程中均值和方差隨時間發生變化,它也可以適應性地將數據標準化。其工作原理是:訓練過程中在內部保存已讀取每批數據均值和方差的指數移動平均值。批標準化的主要效果是它有助於梯度傳播(這一點和殘差連接很像),因此允許更深(層次更多的)的網絡。對於有些特別深的網絡,只有包含多個BatchNormalization層時才能進行訓練。例如,BatchNormalization廣泛用於Keras內置的許多高級卷積神經網絡架構,比如ResNet50、Inception V3 和Xception,使用起來也很簡單:

#BatchNormalization層通常在卷積層或密集連接層之後使用,對輸出標準化
conv_model.add(layers.Conv2D(32, 3, activation='relu'))
conv_model.add(layers.BatchNormalization())
dense_model.add(layers.Dense(32, activation='relu'))
dense_model.add(layers.BatchNormalization())

BatchNormalization層接收一個axis參數,它指定應該對哪個特徵軸做標準化。這個參數的默認值是-1,即輸入張量的最後一個軸(一般對應特徵緯度,跟之前連接不同層時一樣)。對於Dense層、Conv1D層、RNN 層和將data_format 設為"channels_last"(通道在後)的Conv2D 層,這個默認值都是正確的。但有少數人使用將data_format 設為"channels_first"(通道在前)的Conv2D 層,這時特徵軸是編號為1的軸(0軸一般是samples軸),因此BatchNormalization的axis參數應該相應地設為1。

(2)超參數優化

構建深度學習模型時,你必須做出許多看似隨意的決定:應該堆疊多少層?每層應該包含多少個單元或過濾器?激活應該使用relu還是其他函數?在某一層之後是否應該使用BatchNormalization?應該使用多大的dropout 比率?還有很多。這些在架構層面的參數叫作超參數(hyperparameter),以便將其與模型參數(通過反向傳播優化進行訓練)區分開來。

在實踐中,經驗豐富的工程師和研究人員會培養出直覺,能夠判斷上述選擇哪些可行、哪些不可行。但更一般地,需要首先制定一個原則,系統性地自動探索可能的超參數組合空間。需要搜索架構空間,並根據經驗找到性能最佳的架構。這正是超參數自動優化領域的內容。這個領域是一個完整的研究領域,超參數優化的過程通常如下所示。c、將模型在訓練數據上擬合,並衡量其在驗證數據上的最終性能;這個過程的關鍵在於,給定許多組超參數,使用驗證性能的歷史來選擇下一組需要評估的超參數。有多種不同的技術可供選擇:貝葉斯優化、遺傳算法、簡單隨機搜索、網格法等等。通常情況下,隨機搜索(隨機選擇需要評估的超參數,並重複這一過程)就是最好的解決方案,雖然這也是最簡單的解決方案。但有一種工具確實比隨機搜索更好,它就是Hyperopt。它是一個用於超參數優化的Python庫,其內部使用Parzen估計器的樹來預測哪組超參數可能會得到好的結果。另一個叫作Hyperas的庫將Hyperopt與Keras 模型集成在一起,可以試試。在進行大規模超參數自動優化時,有一個重要的問題需要牢記,那就是驗證集過擬合。因為通常是使用驗證數據計算出一個信號,然後根據這個信號更新超參數,所以你實際上是在驗證數據上訓練超參數,很快會對驗證數據過擬合。

(3)模型集成

想要在一項任務上獲得最佳結果,另一種強大的技術是模型集成(model ensembling)。集成是指將一系列不同模型的預測結果匯集到一起,從而得到更好的預測結果。模型集成依賴於這樣的假設,即對於獨立訓練的不同良好模型,它們表現良好可能是因為不同的原因:每個模型都從略有不同的角度觀察數據來做出預測,得到了「真相」的一部分,但不是全部真相。以分類問題為例。想要將一組分類器的預測結果匯集在一起[即分類器集成(ensemblethe classifiers)],最簡單的方法就是將它們的預測結果取均值作為預測結果,加權方式有等權或根據性能加權

preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)
#等權平均
final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_d)
#根據每個模型性能好壞,加權平均
final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d

想要保證集成方法有效,關鍵在於這組分類器的多樣性(diversity)。多樣性就是力量。用機器學習的術語來說,如果所有模型的偏差都在同一個方向上,那麼集成也會保留同樣的偏差。如果各個模型的偏差在不同方向上,那麼這些偏差會彼此抵消,集成結果會更加穩定、更加準確。因此集成的模型應該儘可能好且儘可能不同,這通常意味著使用非常不同的架構,甚至使用不同類型的機器學習方法。作者發現有一種方法在實踐中非常有效(但這一方法還沒有推廣到所有問題領域),就是將基於樹的方法(比如隨機森林或梯度提升樹)和深度神經網絡進行集成

之前所有內容都是根據已有信息對做出預測或判斷,第八章將從各個角度探索深度學習在增強藝術創作方面的可能性。書中介紹了幾個例子,包括序列數據生成(可用於生成文本或音樂)、DeepDream以及使用變分自編碼器和生成式對抗網絡進行圖像生成。

用深度學習生成序列數據的通用方法,就是使用前面的標記作為輸入,訓練一個網絡(通常是循環神經網絡或卷積神經網絡)來預測序列中接下來的一個或多個標記。例如,給定輸入the cat is on the ma,訓練網絡來預測目標為t,即下一個字符。與前面處理文本數據時一樣,標記(token)通常是單詞或字符,給定前面的標記,能夠對下一個標記的概率進行建模的任何網絡都叫作語言模型(language model)。語言模型能夠捕捉到語言的潛在空間(latent space),即語言的統計結構。一旦訓練好了這樣一個語言模型,就可以從中採樣(sample,即生成新序列)。向模型中輸入一個初始文本字符串,要求模型生成下一個字符或下一個單詞(甚至可以同時生成多個標記),然後將生成的輸出添加到輸入數據中,並多次重複這一過程。在本例中,我們將會用到一個LSTM層,向其輸入從文本語料中提取的N 個字符組成的字符串,然後訓練模型來生成第N+1 個字符。模型的輸出是對所有可能的字符做softmax,得到下一個字符的概率分布,然後取概率最大的字符作為輸出。生成文本時,如何選擇下一個字符至關重要。一種簡單的方法是貪婪採樣(greedy sampling),就是始終選擇可能性最大的下一個字符。但這種方法會得到重複的、可預測的字符串,看起來不像是連貫的語言。一種更有趣的方法是做出稍顯意外的選擇:在採樣過程中引入隨機性,即從下一個字符的概率分布中進行採樣。這叫作隨機採樣。在這種情況下,如果下一個字符是e的概率為0.3,那麼你會有30%的概率選擇它(注意,貪婪採樣也可以被看作從一個概率分布中進行採樣,即某個字符的概率為1,其他所有字符的概率都是0)。從模型的softmax 輸出中進行概率採樣是一種很巧妙的方法,它甚至可以在某些時候採樣到不常見的字符,從而生成看起來更加有趣的句子,而且有時會得到訓練數據中沒有的、聽起來像是真實存在的新單詞,從而表現出創造性。但這種方法有一個問題,就是它在採樣過程中無法控制隨機性的大小。為什麼需要有一定的隨機性?考慮一個極端的例子——純隨機採樣,即從均勻概率分布中抽取下一個字符,其中每個字符的概率相同。這種方案具有最大的隨機性,換句話說,這種概率分布具有最大的熵(熵是資訊理論中度量不確定性的單位 )。當然它不會生成任何有趣的內容。再來看另一個極端——貪婪採樣。貪婪採樣也不會生成任何有趣的內容,它沒有任何隨機性,即相應的概率分布具有最小的熵。從「真實」概率分布(即模型softmax 函數輸出的分布)中進行採樣,是這兩個極端之間的中間點。但是,還有許多其他中間點具有更大或更小的熵,更小的熵可以讓生成的序列具有更加可預測的結構(因此可能看起來更真實),而更大的熵會得到更加出人意料且更有創造性的序列。

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

#對於不同的softmax溫度,對概率分布進行重新加權
import numpy as np
def reweight_distribution(original_distribution, temperature=0.5):
distribution = np.log(original_distribution) / temperature
distribution = np.exp(distribution)
return distribution / np.sum(distribution)

distribution為重新加權後的結果,distribution的求和可能不再等於1,因此需要將它除以求和,以得到新的分布。更高的溫度得到的是熵更大的採樣分布,會生成更加出人意料、更加無結構的生成數據;而更低的溫度對應更小的隨機性以及更加可預測的生成數據。


下面用Keras來實現這些想法。首先需要可用於學習語言模型的大量文本數據。本例將使用尼採的一些作品,他是19 世紀末期的德國哲學家,這些作品已經被翻譯成英文。因此,我們要學習的語言模型將是針對於尼採的寫作風格和主題的模型,而不是關於英語的通用模型。

import keras
import numpy as np
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的序列(這些序列之間存在部分重疊),對它們進行one-hot編碼,然後將其打包成形狀為(sequences, maxlen, unique_characters) 的三維Numpy數組x。同時,還需要準備一個數組y,其中包含對應的目標,即在每一個所提取的序列之後出現的下一個字符。

#將字符序列向量化
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])

#text中出現的字符組成非重複的有序列表
chars = sorted(list(set(text)))
#將字符映射為它在列表chars中的索引,後面將用字典方式得到索引
char_indices = dict((char, chars.index(char)) for char in chars)
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,然後是一個Dense 分類器和對所有可能字符的softmax激活。由於目標是經過one-hot編碼的,所以訓練模型需要使用categorical_crossentropy作為損失。

#構建模型
from keras import layers
from keras.models import Sequential
from keras import optimizers
model = Sequential()
model.add(layers.LSTM(128, input_shape=(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer=optimizers.RMSprop(lr=0.01))

給定一個訓練好的模型和一個種子文本片段,我們可以通過重複以下操作來生成新的文本:a、給定目前已生成的文本,從模型中得到下一個字符的概率分布。c、根據重新加權後的分布對下一個字符進行隨機採樣。下列代碼將對模型得到的原始概率分布進行重新加權,並從中抽取概率最大字符的索引(即採樣函數sampling function)。

#給定模型預測,採樣下一個字符的函數
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)
#返回的是概率最高字符對應的index
return np.argmax(probas)

最後,下面這個循環將反覆訓練並生成文本。在每輪過後都使用一系列不同的溫度值來生成文本。這樣我們可以看到隨著模型收斂,生成的文本如何變化,以及溫度對採樣策略的影響。

#文本生成循環
import random
import sys
#一共訓練60輪,每一輪模型首先在x、y上訓練模型
for epoch in range(1, 60):
print('epoch', epoch)
model.fit(x, y, batch_size=128, epochs=1)
#隨機抽取一段長度為maxlen的文本
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)
#預測後續400個新字符
for i in range(400):
sampled = np.zeros((1, maxlen, len(chars)))
#對隨機生成的字符進行one-hot編碼
for t, char in enumerate(generated_text):
sampled[0, t, char_indices[char]] = 1.
#根據隨機生成的文本,預測下一個字符的可能性,verbose=0表示不展示訓練過程
preds = model.predict(sampled, verbose=0)[0]
#返回概率最高字符對應的index
next_index = sample(preds, temperature)
next_char = chars[next_index]
#將生成的新字符添加到文本末,並重新截取長度為maxlen的文本
generated_text += next_char
generated_text = generated_text[1:]
sys.stdout.write(next_char)

第60輪時,模型已幾乎完全收斂,文本看起來更加連貫,結果如下所示(說實話我也看不懂):

temperature=0.2的結果

cheerfulness, friendliness and kindness of a heart are the sense of thespirit is a man with the sense of the sense of the world of the self-end and self-concerning the subjection of the strengthorixes--the subjection of the subjection of the subjection of the self-concerning the feelings in the superiority in the subjection of the subjection of the spirit isn't to be a man of the sense of the subjection and said to the strength of the sense of the

temperature=0.5的結果

cheerfulness, friendliness and kindness of a heart are the part of the soul who have been the art of the philosophers, and which the one won't say, which is it the higher the and with religion of the frences.the life of the spirit among the most continuess of the strengther of the sense the conscience of men of precisely before enough presumption, and can mankind, and something the conceptions, the subjection of the sense and suffering and the 

temperature=1.0的結果。

cheerfulness, friendliness and kindness of a heart are spiritual by the ciuture for the entalled is, he astraged, or errors to our you idstood--and it needs,to think by spars to whole the amvives of the newoatly, prefectly raals! it was name, for example but voludd atu-especity"--or rank onee, or even all "solett increessic of the world and implussional tragedy experience, transf, or insiderar,--must hast if desires of the strubction is be stronges可見,較小的溫度值會得到極端重複和可預測的文本,但局部結構是非常真實的,特別是所有單詞都是真正的英文單詞。隨著溫度值越來越大接近1,生成的文本也變得更有趣、更出人意料,甚至更有創造性,它有時會創造出全新的單詞,聽起來有幾分可信(比如eterned 和troveration)。毫無疑問,在這個特定的設置下,0.5 的溫度值生成的文本最為有趣,可以要嘗試多種採樣策略!在學到的結構與隨機性之間,巧妙的平衡能夠讓生成的序列非常有趣。但是,不要期待能夠生成任何有意義的文本,除非是很偶然的情況。你所做的只是從一個統計模型中對數據進行採樣,這個模型是關於字符先後順序的模型。神經風格遷移(neuralstyle transfer)由Leon Gatys等人於2015 年夏天提出。自首次提出以來,神經風格遷移算法已經做了許多改進,並衍生出許多變體,而且還成功轉化成許多智慧型手機圖片應用。本節將重點介紹原始論文中描述的方法。神經風格遷移是指將參考圖像的風格應用於目標圖像,同時保留目標圖像的內容,如下圖:

風格(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),從而實現我們定義的風格遷移。Gatys等人發現深度卷積神經網絡能夠從數學上定義style和content兩個函數。首先,網絡更靠底部的層激活包含關於圖像的局部信息,而更靠近頂部(指靠近最後輸出)的層則包含更加全局、更加抽象的信息(如鼻子、眼睛等)。因此內容是更加全局和抽象的,我們認為它能夠被卷積神經網絡更靠頂部的層的表示所捕捉到。於是內容損失的定義就是兩個激活之間的L2範數,一個激活是預訓練的卷積神經網絡更靠頂部的某層在目標圖像上的激活,另一個激活是同一層在生成圖像上的激活。風格損失則使用了卷積神經網絡的多個層。我們想要捉到卷積神經網絡在風格參考圖像的所有空間尺度上提取的外觀,而不僅僅是在單一尺度上。對於風格損失,Gatys等人使用了層激活的格拉姆矩陣(Gram matrix),即某一層輸出特徵圖的內積。這個內積可以被理解成表示該層特徵之間相互關係的映射。這些特徵相互關係抓住了在特定空間尺度下模式的統計規律,它對應於這個尺度上找到的紋理的外觀。因此,風格損失的目的是在風格參考圖像與生成圖像之間,在不同層的激活內保存相似的內部相互關係。神經風格遷移可以用任何預訓練卷積神經網絡來實現。這裡使用Gatys等人所使用的VGG19網絡,神經風格遷移的一般過程如下:a、創建一個網絡,它能夠同時計算風格參考圖像、目標圖像和生成圖像的VGG19 層激活;b、使用這三張圖像上計算的層激活來定義之前所述的損失函數,為了實現風格遷移,需要將這個損失函數最小化;c、設置梯度下降過程,更新生成圖像以使這個損失函數最小化。首先來定義風格參考圖像和目標圖像的路徑,為了確保處理後的圖像具有相似的尺寸(如果圖像尺寸差異很大,會使得風格遷移變得更加困難),需要將所有圖像的高度調整為400 像素。

#用Keras實現神經風格遷移
from keras.preprocessing.image import load_img, img_to_array
target_image_path = 'img\\target.jpg'
style_reference_image_path = 'img\\style.jpg'
width, height = load_img(target_image_path).size
#按比例調整圖像高度為400像素
img_height = 400
img_width = int(width * img_height / height)

定義一些輔助函數,用於對進出VGG19卷積神經網絡的圖像進行加載、預處理和後處理。

import numpy as np
from keras.applications import vgg19
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
#解碼函數,vgg19.preprocess_input會對圖像編碼,SO最後輸出時要解碼
def deprocess_image(x):
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779

x[:, :, 2] += 123.68

#vgg19.preprocess_input將圖像通道有RGB改為BGR,所以要改回來

x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x

下面構建VGG19網絡。它接收三張圖像的批量作為輸入,三張圖像分別是目標圖像、風格參考圖像和一個用於保存生成圖像的佔位符(注意順序為0、1、2)。佔位符是一個符號張量,它的值由外部Numpy 張量提供,所包含的值會隨著時間而改變。風格參考圖像和目標圖像都是不變的,因此使用K.constant來定義。

#加載預訓練的VGG19網絡,並將其應用於三張圖像
from keras import backend as K
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)

下面定義內容損失,它要保證目標圖像和生成圖像在VGG19卷積神經網絡的頂層具有相似的結果。以及風格損失,它使用一個輔助函數來計算輸入矩陣的格拉姆矩陣,即原始特徵矩陣中相互關係的映射。

#定義內容損失
def content_loss(base, combination):
return K.sum(K.square(combination - base))
#定義gram矩陣,用於計算風格損失
def gram_matrix(x):
#先將通道緯度至於0第一軸,然後平鋪數據,進而計算對應特徵向量的點積
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):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_height * img_width
#channels和size用於調整數據量綱
return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))

除了這兩個損失分量,還要添加個總變差損失(total variation loss),它對生成的組合圖像的像素進行操作。它促使生成圖像具有空間連續性,從而避免結果過度像素化,你可以將其理解為正則化損失,實際就是生成圖片height緯度像素的差分。

#總變差損失,其實就是height緯度的差分
def total_variation_loss(x):
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))

最後將三個損失加權求和,可以看到style的權重最大。

#定義需要最小化的最終損失
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
#內容層損失來自block5_conv2
content_layer = 'block5_conv2'
#風格層損失來自下列5層 
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]

#第0和2個樣本是目標圖像和生成圖像

target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss += content_weight * content_loss(target_image_features,ombination_features)
for layer_name in style_layers:

  layer_features = outputs_dict[layer_name]

  #第1和2個樣本是風格圖像和生成圖像

  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)

接著再設置梯度下降過程。在Gatys等人最初的論文中使用L-BFGS算法進行最優化,所以這裡也將使用這種方法。L-BFGS算法內置於SciPy中,但SciPy實現有兩個小小的限制:a、它需要將損失函數值和梯度值作為兩個單獨的函數傳入b、它只能應用於展平的向量,而我們的數據是三維圖像數組這裡創建一個名為Evaluator的Python類,它可以同時計算損失值和梯度值(以函數形式返回),在第一次調用時會返回損失值,同時緩存梯度值用於下一次調用。最後實例化一個Evaluator類。

#K.gradients函數用於求loss相對combination_image的梯度
grads = K.gradients(loss, combination_image)[0]
#function只是將輸入、輸出結合,當給定輸入列表時,它將遵循從輸入到輸出的計算流程
#在這種情況下,給定單個輸入列表,它返回損失和梯度的列表
fetch_loss_and_grads = K.function([combination_image], [loss, grads])
#定義一個類可以同時反饋loss和grads函數
class Evaluator(object):
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 = Evaluator()

最後使用SciPy的L-BFGS算法來運行梯度下降過程,算法每一次迭代時都會保存當前的生成圖像(例子中1次迭代表示1個梯度下降步驟)。

from scipy.optimize import fmin_l_bfgs_b
from scipy.misc import imsave
import time
result_prefix = 'my_result'
iterations = 20
x = preprocess_image(target_image_path)
x = x.flatten()
#fmin_l_bfgs_b需要輸入平鋪的數據,以及loss和grads函數
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))

請記住,這種技術所實現的僅僅是一種形式的改變圖像紋理,或者叫紋理遷移。如果風格參考圖像具有明顯的紋理結構且高度自相似,並且內容目標不需要高層次細節就能夠被識別,那麼這種方法的效果最好。如下圖:但它通常無法實現比較抽象的遷移,比如將一幅肖像的風格遷移到另一幅中。這種算法更接近於經典的信號處理,而不是更接近於人工智慧,因此不要指望它能實現魔法般的效果。

終於寫完了。本書是基於Keras的深度學習應用,重點在應用,所以很多理論細節只是略微帶過,但其實這些細節對於理解深度網絡很有幫助,比如bias和variance的權衡、常見幾個優化算法SGD、RMSProp、Adam優缺點及比較、表示瓶頸和梯度消失(爆炸)問題、池化運算的作用、深度可分離卷積的工作原理、1*1卷積核的作用、LSTM算法三個門的具體含義和作用等等,書中都沒有詳細介紹,需要參考其他深度學習的教材了

相關焦點

  • 《TensorFlow實戰Google深度學習框架》讀書筆記
    本文大致脈絡:讀書筆記的自我說明對讀書筆記的摘要具體章節的摘要:第一章 深度學習簡介第二章 TensorFlow環境搭建第三章 TensorFlow入門第四章 深層神經網絡 第五章 MNIST 數字識別問題 第六章 圖像識別和卷積神經網絡 第七章 圖像數據處理 第八章 循環神經網絡
  • 資源|用Python和NumPy學習《深度學習》中的線性代數基礎
    本文系巴黎高等師範學院在讀博士 Hadrien Jean 的一篇基礎學習博客,其目的是幫助初學者/高級初學者基於深度學習和機器學習來掌握線性代數的概念。掌握這些技能可以提高你理解和應用各種數據科學算法的能力。
  • 深度學習筆記15:ubuntu16.04 下深度學習開發環境搭建與配置
    作者:魯偉一個數據科學踐行者的學習日記。
  • 學習筆記,從NumPy到Scrapy,學習Python不能錯過這些庫
    在網絡上看到幾位前輩寫了關於python深度學習庫的文章,對於小小白來說,因為我剛開始學python,我得承認自己看完後依然覺得雲裡霧裡的,不知道這些庫到底對我有什麼用處。所以我到網絡上搜集補充關於這些庫的說明內容,感覺在這個整理資料的過程中,對於這些python程序庫了解了更多,以下是我整理的學習筆記。
  • 深度學習筆記4:深度神經網絡的正則化
    作者:魯偉 一個數據科學踐行者的學習日記。
  • 下載量過百萬的吳恩達機器學習和深度學習筆記更新了!(附PDF下載)
    今天,我把吳恩達機器學習和深度學習課程筆記都更新了,並提供下載,這兩本筆記非常適合機器學習和深度學習入門。
  • 最新(2019)斯坦福CS224n深度學習自然語言處理課程(視頻+筆記+2017年合集)
    為方便大家學習,本文也收錄了 CS224n:深度學習的自然語言處理(2017年冬季)系列課程和相關筆記:AI項目體驗地址 https://loveai.tech閱讀過本文的人還看了以下:分享《深度學習入門:基於Python的理論與實現》高清中文版PDF+原始碼《21個項目玩轉深度學習:基於TensorFlow的實踐詳解》完整版PDF+附書代碼
  • 「python學習手冊-筆記」003.數值類型
    003.數值類型本系列文章是我個人學習《python學習手冊(第五版)》的學習筆記,其中大部分內容為該書的總結和個人理解,小部分內容為相關知識點的擴展。非商業用途轉載請註明作者和出處;商業用途請聯繫本人(gaoyang1019@hotmail.com)獲取許可。
  • python深度學習---帶你從入門到精通
    深度學習在搜索技術,數據挖掘,機器學習,機器翻譯,自然語言處理,多媒體學習,語音,推薦和個性化技術,以及其他相關領域都取得了很多成果。深度學習使機器模仿視聽和思考等人類的活動,解決了很多複雜的模式識別難題,使得人工智慧相關技術取得了很大進步。
  • 《深度閱讀》:運用這7個技巧做筆記,積累屬於自己的讀書經驗
    深度閱讀者,以追求閱讀質量為目的,注重知識的轉化與價值體現,善於做讀書筆記。徐捷在《深度閱讀》第八章「筆記:積累屬於自己的經驗」中講述到做筆記的一些技巧,以及做筆記需站立的立場、所持的態度。單純的摘抄形式不是做筆記的真實要求,閱讀者自身的邏輯思維才是關鍵。閱讀完一本書,最大的收穫應該是經思考後將書中知識轉化成為自身知識的那一部分,也就是讀書筆記中評論與思考的那一部分。
  • 【教程】AlphaGo Zero 核心技術 - David Silver深度強化學習課程中文學習筆記
    Alpha Zero的背後核心技術是深度強化學習,為此,專知有幸邀請到葉強博士根據DeepMind AlphaGo的主要研究人員David Silver《深度強化學習》視頻公開課進行創作的中文學習筆記,在專知發布推薦給大家!
  • 學習方法 如何做「讀書筆記」收穫更大?
    看書時有做讀書筆記的習慣嗎?你在平時閱讀過程中,有沒有使用好的筆記方法讓書本發揮更大的價值呢?今天向各位推薦幾招:直接在書上做讀書的批註會非常直觀。勾畫你有感觸的語句,直接批註上你的想法和體悟,這樣下次再翻看書籍時,非常容易識別書中精華和重點,也可以基於上次的閱讀體會提出更深入的見解或引入更多的視角。在用這個方法讀書時,有個很重要的前提是賦予符號固定的含義。
  • 適合新手的 python pandas 學習筆記(2)
    回顧一下昨天的學習筆記在適合新手的 python pandas 學習筆記(1)中,準備工作已經完成。
  • 李宏毅機器學習完整筆記正式發布
    《LeeML-Notes》李宏毅機器學習筆記3.《LeeML-Notes》學習筆記框架4.筆記內容細節展示a. 對梯度下降概念的解析b. 為什麼需要做特徵縮放c. 隱形馬爾科夫鏈的應用5.代碼呈現a. 回歸分析b.
  • 如何做好讀書筆記和學習筆記?
    相信很多人都會有這樣的困惑,就是自己在參加完一場講座或者學習完一項技能功課時,回顧下來,好像自己從中的收穫並不多。這其中很大一部分原因就是,你不會做筆記,記錄重點。不管是參加多麼優秀的培訓、講座,或者學習技能,如果你不會做筆記,或者你的筆記無法再現當時的學習內容,那麼你的學習是浪費時間,而且你吸收的知識也很容易忘記,到最後是白學。要如何做好讀書筆記呢?下面幾點能夠幫助你一、學習過程中最重要的是什麼?
  • 讀書筆記大全800字-讀書筆記摘抄
    新東方網整理中外名著讀書筆記大全,將好的讀書筆記摘抄,大家可以參考範文,總結讀書筆記格式,了解讀書筆記怎麼寫。更多內容推薦: 讀書筆記大全:中外名著讀書筆記摘抄1031篇1101篇讀書筆記大全告訴你讀書筆記該怎麼寫
  • 【微筆記】houdini使用python創建城市教程筆記I
    昨天小編分享了houdini使用python製作城市的教程,講解得非常不錯,不過個人建議大家至少有一些python基礎和houdini python結合基礎學習會比較好,因為課程內容量比較大。小編也在同步學習,今天把第一部分的筆記1-8課 python基礎介紹內容筆記整理分享出來。在CG獵人vip群裡的同學可以獲得pdf版本,作為小福利吧。
  • 斯坦福CS231N課程學習筆記(一).課程簡介與準備
    為了強制自己學習,強化學習效果,將學習中的筆記整理出來,與大家一起分享,也希望藉此與同在學習這門課程、以及其他計算機視覺的學習者、研究者一起探討和進步。        本人此前沒有接觸過這一領域,IT從業以來多以工程為主,少有接觸學術和算法研究,所以學習筆記也會因為本人理解能力原因,存在謬誤,懇請閱讀者指正。
  • 從零開始深度學習Pytorch筆記(12)—— nn.Module
    從零開始深度學習Pytorch筆記(1)——安裝Pytorch從零開始深度學習Pytorch
  • python學習筆記:條件語句IF
    實例flag = Falsename = 'Clancey'if name == 'python': # 判斷變量是否為 python print('welcome boss') # 並輸出歡迎信息else: print(name) # 條件不成立時輸出變量名稱