0
| 本文作者: skura | 2019-09-05 17:55 |

AI 開發(fā)者按,本文的作者是數(shù)據(jù)科學(xué)家 Ma?l Fabien。在過去的幾個(gè)月里,他在個(gè)人博客上寫了 100 多篇文章。這個(gè)內(nèi)容量相當(dāng)可觀。他突然想到一個(gè)主意:訓(xùn)練一個(gè)能像他一樣說話的語言生成模型。
為此,他寫了一篇文章分享了生成一個(gè)像人一樣說話的神經(jīng)網(wǎng)絡(luò)模型的過程和相關(guān)代碼,他的文章內(nèi)容如下:
我想訓(xùn)練一個(gè)能像我一樣說話的語言生成模型,或者更具體地說,一個(gè)可以像我一樣寫作的模型。它可以完美的說明語言生成的主要概念、使用 keras 實(shí)現(xiàn)語言生成模型,以及我的模型的局限性。
本文的全部代碼都可以在這個(gè) repo 中找到:
在我們開始之前,我想分享這個(gè)意外發(fā)現(xiàn)的資源—— Kaggle Kernel ,它對理解語言生成算法結(jié)構(gòu)來說是非常有用資源。
語言生成
自然語言生成的目的是生成有意義的自然語言。
大多數(shù)情況下,內(nèi)容是作為單個(gè)單詞的序列生成的。總的來說,它的工作原理如下:
你訓(xùn)練一個(gè)模型來預(yù)測序列中的下一個(gè)單詞
你給經(jīng)過訓(xùn)練的模型一個(gè)輸入
重復(fù)上面的步驟 n 次,生成接下來的 n 個(gè)單詞

序列預(yù)測的過程
1.創(chuàng)建數(shù)據(jù)集
第一步是構(gòu)建一個(gè)數(shù)據(jù)集,以便我們稍后將要構(gòu)建的網(wǎng)絡(luò)可以理解這個(gè)數(shù)據(jù)集。首先導(dǎo)入以下包:
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Embedding, LSTM, Dense, Dropout
from keras.preprocessing.text import Tokenizer
from keras.callbacks import EarlyStopping
from keras.models import Sequential
import keras.utils as ku
import pandas as pd
import numpy as np
import string, os
a.加載數(shù)據(jù)
我寫的每一篇文章的標(biāo)題都遵循這個(gè)模板:

這是我們通常不希望在最終數(shù)據(jù)集中包含的內(nèi)容類型。相反,我們將關(guān)注文本本身。
所有文章都寫在一個(gè)單獨(dú)的 Markdown 文件中。標(biāo)題基本上包含了標(biāo)題、圖片標(biāo)題等信息。

首先,我們需要指向包含文章的文件夾,在我的目錄中,名為「maelfabien.github.io」。
B.句子標(biāo)記
然后,打開每一篇文章,并將每一篇文章的內(nèi)容添加到列表中。但是,由于我們的目標(biāo)是生成句子,而不是生成整篇文章,因此我們將把每一篇文章拆分成一個(gè)句子列表,并將每個(gè)句子附加到「all_sentences」列表中:
all_sentences= []
for file in glob.glob("*.md"):
f = open(file,'r')
txt = f.read().replace("\n", " ")
try:
sent_text = nltk.sent_tokenize(''.join(txt.split("---")[2]).strip())
for k in sent_text :
all_sentences.append(k)
except :
pass
總的來說,我們有超過 6800 個(gè)訓(xùn)練的句子。目前的過程如下:

句子拆分
c. N-gram 創(chuàng)建
然后,我的想法是根據(jù)一起出現(xiàn)的單詞創(chuàng)建 N-grams。為此,我們需要:
在語料庫上安裝一個(gè)標(biāo)記器,將索引與每個(gè)標(biāo)記相關(guān)聯(lián)
把語料庫中的每個(gè)句子分解成一系列的標(biāo)記
存儲一起發(fā)生的標(biāo)記序列
可通過下圖來理解這個(gè)過程:

N-gram 創(chuàng)建
接下來,讓我們來實(shí)現(xiàn)它。我們首先需要安裝標(biāo)記器:
tokenizer = Tokenizer()
tokenizer.fit_on_texts(all_sentences)
total_words = len(tokenizer.word_index) + 1
變量「total_words」包含已使用的不同單詞的總數(shù),這里的數(shù)值是 8976。然后,對于每個(gè)句子,獲取相應(yīng)的標(biāo)記并生成 N-grams:
input_sequences = []
# For each sentence
for sent in all_sentences:
# Get the corresponding token
token_list = tokenizer.texts_to_sequences([sent])[0]
# Create the corresponding n-grams
for i in range(1, len(token_list)):
n_gram_sequence = token_list[:i+1]
input_sequences.append(n_gram_sequence)
「token_list」變量將語句作為標(biāo)記序列包含:

然后,'n_gram_sequences' 序列創(chuàng)建 n-grams。它從前兩個(gè)單詞開始,然后逐漸添加單詞:

d.Padding
我們現(xiàn)在面臨的問題是:不是所有的序列都有相同的長度!那么,如何解決這個(gè)問題?
我們將使用 Padding。Padding 在變量“input_sequences”的每一行之前添加 0 序列,這樣每一行的長度就與最長的行的長度相同了。

Padding 的解釋
為了將所有句子填充到句子的最大長度,我們必須首先找到最長的句子:
max_sequence_len = max([len(x) for x in input_sequences])
在我的例子里面它等于 792。好吧,對單個(gè)句子來說它已經(jīng)夠大了!由于我的博客包含了一些代碼和教程,我希望這一句話是由 python 代碼編寫的。讓我們繪制序列長度的直方圖:
import matplotlib.pyplot as plt
plt.figure(figsize=(12,8))
plt.hist([len(x) for x in input_sequences], bins=50)
plt.axvline(max_sequence_len, c="r")
plt.title("Sequence Length")
plt.show()

序列長度
在單個(gè)句子中,很少有例子會超過 200 個(gè)單詞。如果把最大序列長度設(shè)為 200 會如何?
max_sequence_len = 200
input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre'))
其輸出結(jié)果為:

e.拆分 X 和 Y
現(xiàn)在我們有了固定長度的數(shù)組,其中大多數(shù)在實(shí)際序列之前填充了 0。好吧,我們怎么把它變成一個(gè)訓(xùn)練集?我們需要拆分 X 和 Y!記住,我們的目標(biāo)是預(yù)測序列中的下一個(gè)單詞。因此,我們必須將除最后一個(gè)標(biāo)記外的所有標(biāo)記作為 X,并將最后一個(gè)標(biāo)記作為 Y。

拆分 X 和 Y
在 python 中,它就和下面的語句一樣簡單:
X, y = input_sequences[:,:-1],input_sequences[:,-1]
我們現(xiàn)在將這個(gè)問題看作一個(gè)多分類任務(wù)。像往常一樣,我們必須首先對 y 進(jìn)行 one-hot 編碼,以獲得一個(gè)稀疏矩陣,該矩陣在對應(yīng)于該標(biāo)記的列中包含 1,在其他位置包含 0:

在 python 中,使用「keras utils to_categorical」:
y = ku.to_categorical(y, num_classes=total_words)
X 的 shape 是 (164496, 199),Y 的 shape 是 (164496, 8976)。
我們有大約 165000 個(gè)訓(xùn)練樣本。X 是 199 列寬,因?yàn)樗鼘?yīng)于我們允許的最長序列(200-1,要預(yù)測的標(biāo)簽)。Y 有 8976 列,對應(yīng)于所有詞匯的稀疏矩陣。數(shù)據(jù)集現(xiàn)在準(zhǔn)備好了!
2.建立模型
我們將使用 Long Short-Term Memory networks (LSTM)。LSTM 的一個(gè)重要優(yōu)點(diǎn)是能夠理解對整個(gè)序列的依賴性,因此,句子的開頭可能會對要預(yù)測的第 15 個(gè)單詞也產(chǎn)生影響。另一方面,遞歸神經(jīng)網(wǎng)絡(luò)(RNNs)只意味著依賴于網(wǎng)絡(luò)的前一個(gè)狀態(tài),只有前一個(gè)詞才能幫助預(yù)測下一個(gè)狀態(tài)。如果選擇 RNN,我們很快就會錯過上下文,因此,LSTM 應(yīng)該是目前的最佳選擇。
a.模型架構(gòu)
由于訓(xùn)練可以非常(非常)(非常)(非常)(非常)(不開玩笑)長,我們將構(gòu)建一個(gè)簡單的 1 Embedding + 1 LSTM 層 + 1 密集網(wǎng)絡(luò):
def create_model(max_sequence_len, total_words):
input_len = max_sequence_len - 1
model = Sequential()
# Add Input Embedding Layer
model.add(Embedding(total_words, 10, input_length=input_len))
# Add Hidden Layer 1 - LSTM Layer
model.add(LSTM(100))
model.add(Dropout(0.1))
# Add Output Layer
model.add(Dense(total_words, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')
return model
model = create_model(max_sequence_len, total_words)
model.summary()
首先,我們添加一個(gè) embedding 層。我們將其傳遞到一個(gè)有 100 個(gè)神經(jīng)元的 LSTM 中,添加一個(gè) dropout 來控制神經(jīng)元共適應(yīng),最后是一個(gè)稠密層。注意,我們在最后一層應(yīng)用一個(gè) softmax 激活函數(shù)來獲得輸出屬于每個(gè)類的概率。由于損失是一個(gè)多分類問題,因此使用的損失是分類交叉熵。
模型大體情況如下:

模型概覽
b.訓(xùn)練模型
我們終于可以開始訓(xùn)練模型啦!
model.fit(X, y, batch_size=256, epochs=100, verbose=True)
然后模型的訓(xùn)練就開始啦:

在 CPU上,一個(gè) epoch 大約需要 8 分鐘。在 GPU 上(例如在 Colab 中),你應(yīng)該修改使用的 Keras LSTM 網(wǎng)絡(luò),因?yàn)樗荒茉?GPU 上使用。相反,你需要:
# Modify Import
from keras.layers import Embedding, LSTM, Dense, Dropout, CuDNNLSTM
# In the Moddel
...
model.add(CuDNNLSTM(100))
...
我傾向于在幾個(gè)步驟中停止訓(xùn)練,以便進(jìn)行樣本預(yù)測,并在給定交叉熵的幾個(gè)值時(shí)控制模型的質(zhì)量。
以下是我的結(jié)果:

3.生成序列
如果你讀到這里,接下來就是你所期望的了:生成新的句子!要生成句子,我們需要對輸入文本應(yīng)用相同的轉(zhuǎn)換。我們將構(gòu)建一個(gè)循環(huán),在給定的迭代次數(shù)內(nèi)生成下一個(gè)單詞:
input_txt = "Machine"
for _ in range(10):
# Get tokens
token_list = tokenizer.texts_to_sequences([input_txt])[0] # Pad the sequence
token_list = pad_sequences([token_list], maxlen=max_sequence_len-1, padding='pre') # Predict the class
predicted = model.predict_classes(token_list, verbose=0)
output_word = ""
# Get the corresponding work
for word,index in tokenizer.word_index.items():
if index == predicted:
output_word = word
break
input_txt += " "+output_word
當(dāng)損失在 3.1 左右時(shí),以「google」作為輸入生成的句子如下:
Google is a large amount of data produced worldwide!
這并沒有什么實(shí)際意義,但它成功地將谷歌與大數(shù)據(jù)的概念聯(lián)系起來。這是相當(dāng)令人印象深刻的,因?yàn)樗鼉H僅依賴于單詞的共現(xiàn),而沒有整合任何語法概念。
如果我們在訓(xùn)練中稍等一段時(shí)間,讓損失減少到 2.5,并給它輸入「Random Forest」:
Random Forest is a fully managed service distributed designed to support a large amount of startups vision infrastructure。
同樣,生成的內(nèi)容沒有意義,但語法結(jié)構(gòu)相當(dāng)正確。
損失在大約 50 個(gè) epoch 后開始分化,并從未低于 2.5。
我想我們已經(jīng)達(dá)到了這個(gè)方法的極限:
模型仍然很簡單
訓(xùn)練數(shù)據(jù)不夠清晰
數(shù)據(jù)量非常有限
也就是說,我發(fā)現(xiàn)結(jié)果非常有趣,例如,經(jīng)過訓(xùn)練的模型可以很容易地部署在 Flask WebApp 上。
結(jié)論
我希望這篇文章對你有用。我試圖說明語言生成的主要概念、挑戰(zhàn)和限制。當(dāng)然,與本文討論的方法相比,更大的網(wǎng)絡(luò)和更好的數(shù)據(jù)無疑是改進(jìn)的源泉。
資料來源:
Kaggle Kernel : https://www.kaggle.com/shivamb/beginners-guide-to-text-generation-using-lstms
via:https://towardsdatascience.com/i-trained-a-network-to-speak-like-me-9552c16e2396
雷鋒網(wǎng)雷鋒網(wǎng)雷鋒網(wǎng)
雷峰網(wǎng)版權(quán)文章,未經(jīng)授權(quán)禁止轉(zhuǎn)載。詳情見轉(zhuǎn)載須知。