1. 程式人生 > >[Deep-Learning-with-Python] 文字序列中的深度學習

[Deep-Learning-with-Python] 文字序列中的深度學習

  • 將文字資料處理成有用的資料表示
  • 迴圈神經網路
  • 使用1D卷積處理序列資料

深度學習模型可以處理文字序列、時間序列、一般性序列資料等等。處理序列資料的兩個基本深度學習演算法是迴圈神經網路和1D卷積(2D卷積的一維模式)。

文字資料

文字是最廣泛的序列資料形式。可以理解為一系列字元或一系列單詞,但最經常處理的是單詞層面。自然語言處理的深度學習是應用在單詞、句子或段落上的模式識別;就像計算機視覺是應用在畫素上的模式識別。

就像其他神經網路一樣,深度學習模型不能直接處理原始文字:只能處理數值型張量。文字向量化是指將文字轉換成數值型張量的過程。有多種處理方式:
- 將文字分割成單詞,將每個單詞轉換成一個向量;
- 將文字分割成字元,將每個字元轉換成一個向量;
- 抽取單詞或字元的n-grams,將每個n-grams轉換成一個向量;n-grams是多個連續單詞或字元的重疊組。

總的來說,可以文字分解的基本的不同單元(單詞,字元或n元語法)稱為標記,將文字分解為這樣的標記的過程稱為標記化tokenization。文字向量化過程:對文字使用標記模式,將數值向量和生成的token聯絡起來。這些向量打包成序列張量,送到深度學習網路中。將向量和token對應方式有多種,比如one-hot encoding for tokens和token embedding(word embedding).

單詞、字元的one-hot編碼

將token向量化最常見、最基本的方法是one-hot編碼。
單詞級別one-hot編碼

import numpy as np

samples = ['The cat sat on the mat.'
, 'The dog ate my homework.'] token_index = {}#對應關係token-ids for sample in samples: for word in sample.split(): if word not in token_index: #字典中不存在token時,新增token token_index[word] = len(token_index)+1 #不使用0 max_length = 10#處理單句的最大長度 results = np.zeros(shape=(len(samples),max_length,max(token_index.values())+1
))#所有樣本向量化儲存結果 for i, sample in enumerate(samples): for j, word in list(enumerate(sample.split()))[:max_length]:#如果句子長度過長,截斷;j句子第j單詞 index = token_index.get(word)#字典中位置 results[i,j,index] = 1.

字元級one-hot編碼

import string

samples = ['The cat sat on the mat.', 'The dog ate my homework.']
characters = string.printable#包含所有可列印字元的字串
token_index = dict(zip(characters,range(1,len(characters)+1)))#id-character;1計數

max_length = 50#處理單個句子的字元最大長度
results = np.zeros(shape=(len(samples),max_length,max(token_index.keys())+1))

for i, sample in enumerate(samples):
    for j, character in enumerate(sample):#將句子看做字元的集合,而不是單詞
        index = token_index.get(character)
        results[i,j,index] = 1.

Keras內建有文字單詞級和字符集one-hot編碼函式,從原始文字資料開始處理。推薦使用這些函式,因為它們考慮了許多重要的特性,比如忽略字串中的個別特殊字元,只考慮資料集中最常見的N個單詞(避免處理非常大的輸入向量空間)。
Keras內建函式的單詞級one-hot編碼

from keras.preprocessing.text import Tokenizer

samples=['The cat sat on the mat.','The dog ate my homework.']

tokenizer = Tokenizer(num_words=1000)#考慮1000個最常見的單詞
tokenizer.fit_on_texts(samples)#生成word index

sequences = tokenizer.texts_to_sequences(samples)#將文字轉換成下標列表

one_hot_results = tokenizer.texts_to_matrix(samples,mode='binary')#文字直接轉換為one-hot編碼,向量

word_index= tokenizer.word_index#學到的word index對應關係
print('Found %s unique tokens.' % len(word_index))

單熱編碼的變體是單熱雜湊編碼—當詞彙表中的唯一token數量太大而無法明確處理時,可以使用該技巧。可以將單詞雜湊為固定大小的向量,而不是為每個單詞顯式分配索引並在字典中保留這些索引的引用。這通常使用非常輕量級的雜湊函式來完成。這種方法的主要優點是它不需要維護一個明確的單詞索引,這可以節省記憶體並允許資料的線上編碼(可以在看到所有可用資料之前立即生成token向量)。這種方法的一個缺點是它容易受到雜湊衝突的影響:兩個不同的詞可能最終會有相同的雜湊值,隨後任何檢視這些雜湊值的機器學習模型都無法區分這些詞。當雜湊空間的維度遠大於被雜湊的唯一token的總數時,雜湊衝突的可能性降低。

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

dimensionality = 1000#hash空間維度
max_length = 10#處理單個句子長度

results = np.zeros((len(samples), max_length, dimensionality))
for i, sample in enumerate(samples):
    for j, word in list(enumerate(sample.split()))[:max_length]:
        index = abs(hash(word)) % dimensionality
        results[i, j, index] = 1.

word embeddings

將向量與單詞相關聯的另一種流行且有效的方法是使用密集單詞向量,也稱為詞嵌入。通過單熱編碼獲得的向量是二進位制的,稀疏的(主要由零組成),並且具有非常高的維度(與詞彙表中的單詞數相同的維度),詞嵌入是低維浮點向量(即密集向量,與稀疏向量相反).與通過單熱編碼獲得的單詞向量不同,詞嵌入是從資料中學習的。在處理非常大的詞彙表時,通常會看到256維,512維或1,024維的單詞嵌入。另一方面,單熱編碼字通常導致向量維度是20000或更大(在這種情況下捕獲20000token的詞彙標)。因此,詞嵌入將更多資訊打包到更少的維度中。

詞嵌入有兩種獲得方式:
- 學習詞嵌入和關注的主要任務(例如文件分類或情緒預測)聯合起來。在此設定中,從隨機單詞向量開始,然後以與神經網路權重相同的方式學習單詞向量;
- 載入到模型詞嵌入中,這些詞是使用不同的機器學習任務預先計算出來的,而不是正在嘗試解決的任務。這些被稱為預訓練詞嵌入。

通過Embedding網路層學習詞嵌入向量

將密集向量與單詞相關聯的最簡單方法是隨機選擇向量。這種方法的問題在於產生的嵌入空間沒有結構:例如,accurate和exact的單詞最終可能會有完全不同的嵌入,即使它們在大多數句子中都是可互換的。深度神經網路難以理解這種嘈雜的非結構化嵌入空間。
更抽象的說,詞向量之間的幾何關係應該反映這些單詞之間的語義關係。詞嵌入意味著將自然語言對映到幾何空間中。比如,在適合的嵌入空間中,希望將同義詞嵌入到相似的單詞向量中;一般來說,期望任意兩個單詞向量之間的幾何距離(例如L2距離)與相關單詞之間的語義距離相關(意思不同的單詞嵌入在遠離彼此相關,而相關的詞更接近)。除了距離之外,可能希望嵌入空間中的特定方向有意義。
是否有一些理想的單詞嵌入空間可以完美地對映人類語言,並且可以用於任何自然語言處理任務?可能,但尚未計算任何型別的東西。此外,沒有人類語言這樣的東西—有許多不同的語言,它們不是同構的,因為語言是特定文化和特定語境的反映。但更務實的是,良好的詞彙嵌入空間在很大程度上取決於你的任務:英語電影評論情感分析模型的完美詞彙嵌入空間可能與英語法律的文件分類模型的完美嵌入空間有所不同,因為某些語義關係的重要性因任務而異。

因此,在每個新任務中學習新的嵌入空間是合理的。幸運的是,反向傳播使這很容易,而Keras使它變得更加容易。它是關於學習圖層的權重:Embedding嵌入圖層。

from keras.layers import Embedding

embedding_layer = Embedding(1000,64)#嵌入層需要至少兩個引數:tokens個數eg1000,嵌入層維度eg64

Embedding嵌入層最好的理解方法是看成一個字典:將整數下標(代表一個某個單詞)對映到一個稠密向量上。它將整數作為輸入,它在內部字典中查詢這些整數,並返回相關的向量。

Embedding網路層接收一個2D整數張量為輸入,形狀(samples,sequence_length),其中每個實體是整數的序列。它可以嵌入可變長度的序列:例如,可以在前面的示例批次中輸入嵌入層,其中包含形狀(32,10)(32個序列長度為10的批次)或(64,15)(64個序列長度15的批次)。但是,批處理中的所有序列必須具有相同的長度(因為需要將它們打包到單個張量中),因此比其他序列短的序列應該用零填充,並且應該截斷更長的序列。
網路層返回一個3D浮點型別張量,形狀(samples, sequence_length, embedding_dimensionality).這樣的3D張量可以用RNN或1D卷積層處理。
當例項化一個Embedding網路層時,權重(內部字典的token向量)和其他網路層類似,隨機初始化。在訓練過程中,這些詞向量通過反向傳播逐漸改動,將空間結構化為下游模型可以利用的東西。一旦完全訓練,嵌入空間將顯示許多結構 —一種專門針對正在訓練模型的特定問題的結構。
在IMDB電影評論語義分析任務上,應用詞嵌入。首先,在電影評論中取最常見的10000個單詞,然後將每條評論長度限制為20個單詞。網路將會學習到10000個單詞的8維詞嵌入空間,將每個輸入的整數序列(2D)轉換成嵌入層序列(3D浮點張量),平鋪成2D張量,新增一個Dense層做分類。

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)#資料為整數列表

x_train = preprocessing.sequence.pad_sequences(x_train,maxlen=maxlen)#轉換為張量,(samples,maxlen)
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)

使用Embedding層分類

from keras.models import Sequential
from keras.layers import Flatten,Dense

model = Sequential()
model.add(Embedding(10000,8,input_length=maxlen))
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個單詞,這個結果也可以接受。注意僅僅將embedded嵌入序列平鋪,然後在單層全連線網路上訓練,導致模型將輸入序列的每個單詞分割開來看,沒有考慮句子的結構以及單詞之間的關係。最好在嵌入序列的頂部新增迴圈層或1D卷積層,以學習將每個序列作為一個整體考慮在內的特徵。

使用預訓練詞嵌入

有時,只有很少的訓練資料,無法單獨使用資料來學習特定的任務的詞嵌入,怎麼辦?可以從預先計算的嵌入空間中載入嵌入向量,而不是學習想要解決的問題的詞嵌入向量,這些嵌入空間是高度結構化的並且展示了有用的屬性 - 它捕獲了語言結構的一般方面。在自然語言處理中使用預訓練單詞嵌入的基本原理與在影象分類中使用預訓練的卷積網路大致相同:沒有足夠的資料可用於自己學習真正有用的特徵,但期望獲得所需的特徵相當通用—即常見的視覺特徵或語義特徵。在這種情況下,重用在不同問題上學習的特徵是有意義的。

這樣的詞嵌入通常使用詞出現統計(關於在句子或文件中共同出現的詞的觀察),使用各種技術來計算,一些涉及神經網路,一些不涉及。Bengio等人最初探討了以無人監督的方式計算密集,低維度的文字嵌入空間的想法。在21世紀初期,釋出了最著名和最成功的詞彙嵌入方案之後:Word2vec演算法,它開始在研究和行業中廣泛應用,由Tomas Mikolov於2013年在谷歌開發. Word2vec維度捕獲具體語義屬性,例如性別
可以在Keras嵌入層中下載和使用各種預嵌入的字嵌入資料庫。 Word2vec就是其中之一。另一種流行的稱為全球向量詞表示GloVe,由斯坦福大學的研究人員於2014年開發。該嵌入技術基於對詞共現統計矩陣進行因式分解,已經為數以百萬計的英語token提供了預先計算的嵌入,這些嵌入是從維基百科資料和通用爬網資料中獲得的。

整合:原始文字到詞嵌入

下載IMDB原始資料集
地址,解壓縮。

處理原始資料

import os

imdb_dir = './imdb'#資料集地址
train_dir = os.path.join(imdb,'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))
            texts.append(f.read())#讀取文字內容
            f.close()
            if label_type == 'neg':#儲存標籤
                labels.append(0)
            else:
                labels.append(1)

資料分詞tokenizing
文字向量化,劃分訓練集和驗證集。因為預訓練的單詞嵌入對於幾乎沒有可用訓練資料的問題特別有用(否則,任務特定的嵌入表現可能超過它們),將新增限制:將訓練資料限制為前200個樣本。因此,在查看了200個示例之後,對電影評論進行分類。

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np

maxlen = 100#單個句子最大長度
training_samples = 200#訓練集資料量
validation_samples = 10000#驗證集資料量
max_words = 10000#字典長度

tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts)#生成tokens字典
sequences = tokenizer.texts_to_sequences(texts)#將多個文件轉換為字典對應下標的list表示,shape為(文件數,每條文件的長度)
word_index = tokenizer.word_index#word-id字典
print('Found %s unique tokens' % len(word_index))

data = pad_sequences(sequences, maxlen=maxlen)#將每個序列padding成相同長度

labels = np.asarray(labels)
print('Shape of data tensor:',data.shape)
print('Shape of label tensor:', labels.shape)

indices = np.arange(data.shape[0])#打亂shuffle
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+validaion_samples]

下載GLOVE詞嵌入向量
地址;2014英語維基百科,822MB zip檔案,名字‘glove.6B.zip’,包括100維的嵌入向量,40萬個單詞。
預處理嵌入向量
讀取txt檔案,構建一個對映關係:單詞–詞向量。

glove_dir = './glove.6B'

embeddings_index = {}
f = open(os.path.join(glove_dir,'glove.6B.100d.txt'))
for line in f:
    values = line.split()
    word = values[0]
    coefs = np.asarray(values[1:],dtype='float32')
    embeddings_index[word] = coefs

f.close()

print('Found %s word vectors.' % len(embeddings_index))

之後,生成一個嵌入矩陣,載入到Embedding網路層中,形狀(max_words,embedding_dims),其中其中每個條目i包含參考詞索引(在tokenization期間構建)中索引i的單詞的embedding_dim維的向量。請注意,索引0不應代表任何單詞或標記 - 它是佔位符。
預處理GloVe詞向量

embedding_dims = 100

embedding_matrix = np.zeros((max_words,embedding_dims))
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

模型定義

from keras.models import Sequential
from keras.layers import Embedding,Flatten,Dense

model=Sequential() 

model.add(Embedding(max_words, embedding_dim, input_length=maxlen)) #字典長度,輸出維度,句子長度
model.add(Flatten())
model.add(Dense(32,activation='relu')) 
model.add(Dense(1,activation='sigmoid'))

載入預訓練的詞向量到Embedding網路層中

model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False

“Freeze”凍住網路層–不可訓練。

模型訓練驗證

model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
history = model.fit(x_train, y_train,epochs=10,batch_size=32, 
            validation_data=(x_val, y_val))

model.save_weights('pre_trained_glove_model.h5')

模型訓練集和驗證集上的準確率、損失值變化

該模型很快就開始過度擬合—訓練樣本數量很少。出於同樣的原因,驗證準確性具有很大的差異。

請注意,結果可能會有所不同:因為訓練樣本很少,效能很大程度上取決於選擇的200個樣本—而且是隨意選擇。
也可以訓練相同的模型,而無需載入預訓練的單詞嵌入,也不凍結嵌入層。在這種情況下,您將學習輸入tokens的特定於任務的嵌入,當大量資料可用時,這通常比預訓練的詞嵌入更強大。

不用預訓練詞嵌入訓練相同的網路模型

from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense

model = Sequential()

model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
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_data=(x_val, y_val))

訓練集和驗證集準確率、損失值變化

驗證準確性在50%內停滯。因此,在這種情況下,預訓練的單詞嵌入優於共同學習的嵌入。

測試集上評估

test_dir = os.path.join(imdb_dir, 'test')#處理測試資料

labels = []
texts = []

for label_type in ['neg', 'pos']:
    dir_name = os.path.join(test_dir, label_type)
    for fname in sorted(os.listdir(dir_name)):
        if fname[-4:] == '.txt':
            f = open(os.path.join(dir_name, fname))
            texts.append(f.read())
            f.close()
            if label_type == 'neg':
                labels.append(0)
            else:
                labels.append(1)

sequences = tokenizer.texts_to_sequences(texts)
x_test = pad_sequences(sequences, maxlen=maxlen)
y_test = np.asarray(labels)

model.load_weights('pre_trained_glove_model.h5')
model.evaluate(x_test, y_test)#準確率在56%左右。

小結

  • 將原始資料轉換成網路可以處理的張量;
  • 在Keras模型中使用Embedding網路層;
  • 在自然語言處理的小資料集問題上使用預訓練的詞向量提高模型準確率。

迴圈神經網路Recurrent neural networks[RNN]

到目前為止,所見過的所有神經網路的一個主要特徵,例如全連線的網路和卷積網路,就是它們沒有“記憶能力”。顯示給它們的每個輸入都是獨立處理的,輸入之間沒有任何狀態。使用此類網路,為了處理序列或時間序列的資料,必須立即向網路顯示整個序列:將其轉換為單個數據點。例如,在IMDB示例中所做的:整個電影評論被轉換為單個大型向量並一次處理。這種網路稱為前饋網路。

相比之下,當你正在閱讀現在的句子時,你正在逐字處理它 - 或者更確切地說,通過眼睛掃視 - 同時記住之前的事物;這使你能夠流暢地表達這句話所傳達的意義。生物智慧逐步處理資訊,同時保持其處理內部模型,根據過去的資訊構建,並隨著新資訊的不斷更新而不斷更新。

遞迴神經網路(RNN)採用相同的原理,儘管是極其簡化的版本:它通過迭代序列元素並維持包含與迄今為止所見內容相關資訊的狀態來處理序列。 實際上,RNN是一種具有內部迴圈的神經網路. 在處理兩個不同的獨立序列(例如兩個不同的IMDB評論)之間重置RNN的狀態,因此仍然將一個序列視為單個數據點:網路的單個輸入。 更改的是,資料點不再在一個步驟中處理;相反,網路內部迴圈遍歷序列元素

為了使這些迴圈loop和狀態state的概念清晰,用Numpy實現一個小的RNN的前向傳遞。該RNN將一系列向量作為輸入,您將其編碼為2D張量大小(timesteps, input_features)。它在時間步長上迴圈,並且在每個時間步長,它在t處考慮其當前狀態,在t處考慮輸入,形狀(input_features, ),並將它們組合起來以獲得t處的輸出。然後,將設定下一步的狀態為此前一個輸出。對於第一個時間步,未定義前一個輸出;因此,目前沒有狀態。所以,把狀態初始化為零向量稱為網路的初始狀態。

虛擬碼
V1

state_t = 0 #初始t:0
for input_t in input_sequence:#序列元素迭代
    output_t = f(input_t,state_t)#輸出為當前輸入和當前狀態相關
    state_t = output_t#下一刻的狀態為上一刻狀態的輸出

可以具體化函式f:將輸入和狀態轉換為輸出—引數化為兩個矩陣W和U以及偏置向量。類似於前饋網路中全連線層操作的轉換。

V2

state_t = 0
for input_t in input_sequences:
    output_t = activation(dot(W,input_t)+dot(U,state_t)+b)
    state_t = output_t

V3

import numpy as np

timesteps = 100
input_features = 32
output_features = 64

inputs = np.zeros((timesteps,input_features))
state_t = np.zeros((output_features,))

W = np.random.random((output_features,input_features))
U = np.random.random((output_features,output_features))
b = np.random.random((output_features,))

successive_outputs = []
for input_t in inputs:
    output_t = np.tanh(np.dot(W,input_t)+np.dot(U,state_t)+b)
    successive_outputs.append(output_t)
    state_t = output_t

final_output_sequence = np.concatenate(successive_outputs,axis=0)

總之,RNN是一個for迴圈,它重用迴圈的前一次迭代期間計算的結果,僅此而已。當然,適合這個定義有許多不同的RNN - 這個例子就是其中之一最簡單的RNN。RNN的特徵在於它們的階躍函式,例如在這種情況下的以下函式:
outputt=np.tanh(np.dot(W,inputt)+np.dot(U,statet)+b)

注意:在該示例中,最終輸出是2D張量的形狀(timesteps,output_features),其中每個時間步長是時間t處的迴圈的輸出結果。輸出張量中的每個時間步t包含關於輸入序列中的時間步長0到t的資訊 - 關於整個過去。因此,在許多情況下,不需要這個完整的輸出序列;你只需要最後一個輸出(迴圈結束時的output_t),因為它已經包含有關整個序列的資訊。

Keras 迴圈網路層

上面numpy編碼實現的是Keras網路層—SimpleRNN網路層:

from keras.layers import SimpleRNN

有一個小區別是:SimpleRNN網路層處理序列小批量,而不是一個簡單的numpy序列,意味著輸入形狀為(batch_size, timesteps, input_features),不是(timesteps, input_features).
和Keras的其他迴圈網路類似,SimpleRNN有兩種執行方式:返回每個時間步的輸出結果序列集,3D張量,形狀(batch_size, timesteps, output_features);返回每個輸入序列的最終輸出結果,2D張量,形狀(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))

返回全部的狀態序列state:

model = Sequential()
model.add(Embedding(10000,32))
model.add(SimpleRNN(32,return_sequences=True))

有時候,需要將幾個迴圈網路層依次相連,增加網路模型的特徵表示能力。同時,為了返回所有的輸出序列,必須獲得所有的中間網路層結果。

model = Sequential()
model.add(Embedding(10000,32))
model.add(SimpleRNN(32,return_sequences=True))
model.add(SimpleRNN(32,return_sequences=True))
model.add(SimpleRNN(32,return_sequences=True))
model.add(SimpleRNN(32))

使用迴圈網路處理IMDB資料集。
資料集處理

from keras.datasets import imdb
from keras.preprocessing import sequence

max_fetaures = 10000
maxlen = 500
batch_size = 32
print("Loading data...")
(x_train,y_train),(x_test,y_test)=imdb.load_data(num_words=max_features)
print(len(x_train),'train sequences')
print(len(x_test),'test sequences')

print('Pad sequences (sample x time)')
x_train = sequence.pad_sequences(x_train,maxlen=maxlen)
x_test = sequence.pad_sequences(x_test,maxlen=maxlen)

print('x_train shape:', x_train.shape)
print('x_test shape:',x_test.shape)

模型訓練

from keras.layers import SimpleRNN

model = Sequential()
model.add(Embedding(max_features,32))
model.add(SimpleRNN(32))
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=128,validation_split=0.2)

訓練接、驗證集上準確率、損失值變化

簡單的SimpleRNN驗證集上準確率最高85%左右,主要問題在於輸入序列只考慮前500個單詞,而不是整個完整序列。SimpleRNN不擅長處理長序列,如文字。常用其他迴圈網路處理。

LSTM和GRU網路層

SimpleRNN並不是Keras唯一的迴圈網路層,還有LSTM和GRU。實際應用時,通常不使用SimpleRNN,因為SimpleRNN過於簡單,無法實際使用。SimpleRNN有一個主要問題:雖然它理論上應該能夠在時間t保留有關輸入的資訊[這些資訊在很多時間之前看到],但在實踐中,這種長期依賴性是不可能學習到的。 這是由於梯度消失問題,類似於非迴圈網路(前饋網路)所觀察到的:當不斷向網路新增層時,網路最終變得無法處理。LSTM和GRU層旨在解決梯度消失問題。

LSTM,Long Short-Term Memory,SimpleRNN的變種:它增加了一種跨多個時間步攜帶資訊的方法。 想象一下,傳送帶與正在處理的序列平行執行。序列中的資訊可以在任何時候跳到傳送帶上,運輸到稍後的時間步,並在需要時完好無損地跳下。這基本上就是LSTM所做的事情:它為以後儲存資訊,從而防止舊訊號在處理過程中逐漸消失。

為了詳細瞭解這一點,讓我們從SimpleRNN單元格開始。因為有很多權重矩陣,所以用單詞o(Wo和Uo)索引單元格中用於輸出的W和U矩陣。
SimpleRNN

在此圖片中新增一個跨時間步長傳輸資訊的附加資料流。不同的時間步長Ct各不相同,其中C代表Carry。此資訊將對單元格產生以下影響:它將與輸入連線和迴圈連線相結合(通過全連線轉換:帶有權重矩陣的點積,然後是偏置加法和啟用函式),它將影響被髮送到下一個時間步的狀態(通過啟用函式和乘法運算)。從概念上講,資訊資料流是一種調製下一個輸出和下一個狀態的方法。
LSTM
微妙之處:計算Ct資料流的下一個值的方式。涉及三種不同的轉變。這三種都具有SimpleRNN單元的形式:

y = activation(dot(state_t,U)+dot(input_t,W)+b)

但三種轉換方式都有自己的權重矩陣,用i, f, k 對三種方式索引。

output_t=activation(dot(state_t,Uo)+dot(input_t,Wo)+dot(C_t,Vo)+bo)

i_t = activation(dot(state_t, Ui) + dot(input_t, Wi) + bi)
f_t = activation(dot(state_t, Uf) + dot(input_t, Wf) + bf)
k_t = activation(dot(state_t, Uk) + dot(input_t, Wk) + bk)

通過組合i_t,f_t和k_t獲得新的carray狀態(next c_t)。

c_t+1 = i_t * k_t + c_t * f_t

如果想直覺性地瞭解,可以解釋每個操作的意圖。例如,可以說乘以c_t和f_t是故意忘記carry資料流中無關資訊的一種方法;同時,i_t和k_t提供有關當前的資訊,用新資訊更新carry軌道。但是這些解釋並沒有多大意義,因為這些操作實際上做的是由引數化的權重決定的;並且權重以端到端的方式學習,從每輪訓練開始—不可能將這個或那個操作歸功於特定目的。RNN單元格的規範確定了假設空間—在訓練期間搜尋良好模型配置的空間 - 但它不能確定單元格的作用;這取決於單元格權重。(如全連線網路確定假設空間,全連線權重係數決定每次轉換操作)。具有不同重量的相同單元可以做非常不同的事情。因此,構成RNN單元的操作組合可以更好地解釋為對空間搜尋的一組約束,而不是工程意義上的設計

對於研究人員來說,‘ 如何實現RNN單元的問題’似乎選擇約束方式, 最好留給優化演算法(如遺傳演算法或強化學習過程),而不是人類工程師。在未來,這就是構建網路的方式。總之,不需要了解LSTM單元的特定架構。LSTM單元的作用:允許以後重新注入過去的資訊,從而解決消失梯度問題。

LSTM例子

IMDB資料集上使用LSTM.網路模型和SimpleRNN架構類似。設定LSTM網路層輸出維度,其他為預設設定。Keras預設引數設定,不需要微調即可取得很好的效果。

from keras.layers import LSTM

model = Sequential()
model.add(Embedding(10000,32))
model.add(LSTM(32))
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=128,validation_split=0.2)

訓練集、驗證集損失值、準確率變化

驗證集上準確率在89%左右。比SimpleRNN結果好很多,因為梯度消失問題對LSTM影響很小。但是這種結果對於這種計算密集型方法並不具有開創性。為什麼LSTM表現不佳?一個原因是沒有
調整超引數,例如嵌入維度或LSTM輸出維度。另一種可能是缺乏正則化。但主要原因是分析評論的長期結構(LSTM擅長什麼)對情緒分析問題沒有幫助。通過檢視每個評論中出現的單詞以及頻率,可以很好地解決這樣一個基本問題。這就是第一個全連線的方法。
但是有更難的自然語言處理問題在那裡,LSTM的優勢將變得明顯:特別是問答和機器翻譯

小結

  • RNN結構,如何工作?
  • LSTM
  • Keras LSTM處理序列資料

迴圈神經網路的高階應用

  • 迴圈網路Dropout:緩解過擬合
  • stacking 迴圈網路:增加模型特徵表示能力;
  • 雙向迴圈網路:以不同的方式向迴圈網路提供相同的資訊,提高準確性並減少遺忘。

溫度預測問題

到目前為止,所涵蓋的唯一序列資料是文字資料,例如IMDB資料集和路透社資料集。但是,除了語言處理之外,序列資料還存在於更多問題中。比如德國耶拿馬克斯普朗克生物地球化學研究所氣象站記錄的天氣時間序列資料集

在該資料集中,記錄幾年內每10分鐘14種不同的度量(例如空氣溫度,大氣壓,溼度,風向等)。原始資料可以追溯到2003年,但這個例子僅限於2009 - 2016年的資料。該資料集非常適合學習使用數值時間序列。使用它來構建一個模型,該模型將最近的一些資料作為輸入過去(幾天的資料點)並預測未來24小時的氣溫。

下載地址

觀察資料

import os

data_dir = '/home/gao/datasets/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:]

print(header)
print(len(lines))

一共有420551條記錄,每行是一個時間步:日期和14個和天氣相關的記錄值。headers:

["Date Time",
"p (mbar)",
"T (degC)",
"Tpot (K)",
"Tdew (degC)",
"rh (%)",
"VPmax (mbar)",
"VPact (mbar)",
"VPdef (mbar)",
"sh (g/kg)",
"H2OC (mmol/mol)",
"rho (g/m**3)",
"wv (m/s)",
"max. wv (m/s)",
"wd (deg)"]

將資料轉換成Numpy陣列:

import numpy as np

float_data = np.zeros((len(lines),len(headers)-1))
for i,line in enumerate(lines):
    values = [float(x) for x in line.split(',')[1:]]
    float_data[i,:] = values

每10分鐘一條記錄,1天144條資料。畫圖顯示前10天溫度變化情況。

from matplotlib.pyplot as plt

temp = float_data[:, 1]
plt.plot(range(1440), temp[:1440])

如果在過去幾個月的資料中嘗試預測下個月的平均溫度,由於資料的年度可靠週期性,問題將很容易。但是,在幾天的時間內檢視資料,溫度看起來更加混亂。這個時間序列是否可以在日常範圍內預測?

準備資料

問題的確切表述如下:給定的資料可以追溯到回溯時間步長(時間步長為10分鐘)並按步驟時間步長取樣,能預測延遲時間步長的溫度嗎?
- lookback:720,檢視過去5天資料;
- steps:6,每小時進行一次資料取樣;
- delay:144,將來24小時的預測。

開始之前需要:
1. 將資料預處理為神經網路可以處理的格式。資料已經是數字,因此不需要進行任何向量化。但是資料中的每個時間序列都有不同的取值範圍(例如,溫度通常介於-20和+30之間,但是以mbar為單位測量的大氣壓力大約為1,000)。 獨立標準化每個時間序列,以便它們都以相似的比例獲取小值。
2. 編寫一個Python生成器,它接收當前浮點資料陣列,並從最近的過去產生批量資料,以及將來的目標溫度。因為資料集中的樣本是高度冗餘的(樣本N和樣本N + 1將具有共同的大多數時間步長,明確分配每個樣本將是浪費的。相反,您將使用原始資料動態生成樣本。

通過減去每個時間序列的平均值併除以標準差來預處理資料。將使用前200,000個步驟作為訓練資料,因此僅計算此部分資料的平均值和標準差。
資料標準化

mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std

資料生成器生成元組形式,(samples,targets),samples是輸入資料的一個批量,targets是對應的溫度標籤陣列。引數:
- data:原始浮點陣列資料;
- lookback:輸入資料檢視的歷史資料長度;
- delay:預測將來資料的長度;
- min_index和max_index:資料陣列中的索引,用於分隔要繪製的時間步長timesteps,對於保留一部分資料以進行驗證以及另一部分用於測試非常有用;
- shuffle:是否打亂順序;
- batch_size:批量容量大小;
- step: 用於對資料進行取樣的時間段(以時間步長為單位)。將其設定為6,以便每小時繪製一個數據點。

資料生成器

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):
            indeices = range(rows[j]-lookback,rows[j],step)
            samples[j] = data[indices]
            targets[j] = data[rows[j]+delay][1]
        yield samples, targets

使用generator生成器生成訓練集、驗證集和測試集。訓練集在前200000條資料上,驗證集在之後的100000條資料上,測試集在剩下資料集上。
準備訓練集、驗證集和測試集

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)
test_steps = (len(float_data) - 300001 - lookback)

常識性的非機器學習baseline

在開始使用黑盒深度學習模型來解決溫度預測問題之前,先嚐試一種簡單的常識性方法。它將作為一個完整性檢查,它將建立一個你必須擊敗的baseline,以證明更先進的機器學習模型的用處。當你正在接近尚未知解決方案的新問題時,這些常識baseline會很有用。一個典型的例子是不平衡的分類任務,其中一些類比其他類更常見。如果資料集包含90%的A類例項和10%B類例項,則採用常識方法分類任務是在呈現新樣本時始終預測“A”。這樣的分類器總體上是90%準確的,因此任何基於學習的方法都應該超過這個90%的分數以證明有用性。有時,這些基本baseline可能難以擊敗。

在這種情況下,可以安全地假設溫度時間序列是連續的(明天的溫度可能接近今天的溫度)以及具有每日週期的週期性。因此,常識性的方法是始終預測從現在起24小時的溫度將等於現在的溫度。
使用平均絕對誤差(MAE)度量來評估