用自己的風格教AI說話,語言生成模型可以這樣學
- 2019 年 10 月 6 日
- 筆記
選自 towardsdatascience
作者:Maël Fabie
機器之心編譯
參與:Panda
很多研究者和開發者都認為,初學神經網路時,最好的方法莫過於先自己動手訓練一個模型。機器之心也曾推薦過很多不同開發者寫的上手教程。本文同樣是其中之一,數據科學家 Maël Fabien 介紹了如何使用自己的部落格文章訓練一個和自己風格一樣的簡單語言生成模型。
在過去幾個月的課程中,我在我的個人部落格上寫了 100 多篇文章。數量還是很可觀的。然後我有了一個想法:
訓練一個說話方式與我類似的語言生成模型。
更具體而言,是書寫風格像我。這種方式能完美地闡釋語言生成的主要概念、使用 Keras 的實現以及我的模型的局限性。
本文的完整程式碼見這個程式碼庫:
https://github.com/maelfabien/Machine_Learning_Tutorials
在我們開始之前,我推薦一個我發現的很有用的 Kaggle Kernel 資源,可以幫助理解語言生成演算法的結構:https://www.kaggle.com/shivamb/beginners-guide-to-text-generation-using-lstms
語言生成
自然語言生成(NLG)是一個以生成有意義的自然語言為目標的領域。
大多數情況下,內容是以單個詞的序列的形式生成的。這是一個很寬泛的思想,大致工作方式如下:
- 訓練一個模型來預測一個序列的下一個詞
- 為訓練好的模型提供一個輸入
- 迭代 N 次,使其生成後面的 N 個詞

序列預測過程
1.創建數據集
第一步是構建一個數據集,讓我們之後可以基於其構建網路,因此這個數據集需要構建成可被該網路理解的形式。首先導入以下軟體包:
a.載入數據
我寫的每篇文章的文件頭都使用了以下模板:

這是我們通常不希望出現在我們的最終數據集中的內容。我們想要關注的是文本本身。
每一篇文章都是一個單獨的 Markdown 文件。文件頭基本上包含的是標題、標題圖片等資訊。
首先,我們需要定位到包含文章的文件夾。在我的目錄中,這個文件夾名為「maelfabien.github.io」。
b. 句子 token 化
然後,打開每篇文章,將每篇文章的內容都附加到一個列表中。但是,因為我們的目標是生成句子,而非整篇文章,所以我們需要將每篇文章都分割成句子列表,並將每個句子附加到列表「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 個訓練句子。到目前為止的過程如下:

句子分割
c. 創建 n-gram
然後,創建一起出現的詞的 n-gram。為了實現這一目標,我們需要:
- 在語料庫上使用一個 token 化程式,為每個 token 都關聯一個索引
- 將語料庫中的每個句子都分解為一個 token 序列
- 將一起出現的 token 序列保存起來
下圖展示了這個過程:

創建 N-gram
我們來實現它吧。我們首先需要使用 token 化程式:
tokenizer = Tokenizer() tokenizer.fit_on_texts(all_sentences) total_words = len(tokenizer.word_index) + 1
變數 total_words 包含使用過的不同詞的總數。這裡是 8976。然後,對於每個句子,獲取對應的 token 並生成 n-gram:
token_list 變數包含以 token 序列形式存在的句子:
[656, 6, 3, 2284, 6, 3, 86, 1283, 640, 1193, 319] [33, 6, 3345, 1007, 7, 388, 5, 2128, 1194, 62, 2731] [7, 17, 152, 97, 1165, 1, 762, 1095, 1343, 4, 656]
然後,n_gram_sequences 創建 n-gram。它從前兩個詞開始,然後逐漸添加詞:
[656, 6] [656, 6, 3] [656, 6, 3, 2284] [656, 6, 3, 2284, 6] [656, 6, 3, 2284, 6, 3] ...
d. 填充
現在我們面臨著這樣一個問題:並非所有序列都一樣長!我們如何解決這個問題呢?
我們將使用填充(padding)。填充是在變數 input_sequences 的每一行之前添加 0 構成的序列,這樣每一行的長度便與最長行一樣了。

填充的圖示
為了將所有句子都填充到句子的最大長度,我們必須先找到最長的句子:
max_sequence_len = max([len(x) for x in input_sequences])
我的情況是最大序列長度為 792。好吧,對於單句話來說,這一句確實相當長!因為我的部落格包含一些程式碼和教程,所以我估計這一句實際上是 Python 程式碼。我們繪製一個序列長度的直方圖來看看:

序列長度
確實僅有非常少的樣本的單個序列超過 200 個詞。那麼將最大序列長度設置為 200 如何?
max_sequence_len = 200 input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre'))
這會返回類似這樣的結果:
array([[ 0, 0, 0, ..., 0, 656, 6], [ 0, 0, 0, ..., 656, 6, 3], [ 0, 0, 0, ..., 6, 3, 2284], ...,
e. 分割 X 和 y
現在我們有固定長度的數組了,其中大多數在實際的序列之前都填充了 0。那麼,我們如何將其轉換成一個訓練集?我們需要分割 X 和 y!要記住,我們的目標是預測序列的下一個詞。因此,我們必須將最新的 token 之外的所有 token 都視為 X,而將那個最新的 token 視為 y。

分割 X 和 y
用 Python 執行這個操作非常簡單:
X, y = input_sequences[:,:-1],input_sequences[:,-1]
現在我們可以把這個問題視為一個多類分類任務。首先,我們必須對 y 進行 one-hot 編碼,得到一個稀疏矩陣,該矩陣在對應於該 token 的一列包含一個 1,其它地方則都是 0。

在 Python 中,使用 Keras Utils 的 to_categorial:
y = ku.to_categorical(y, num_classes=total_words)
現在,X 的形狀為 (164496, 199),y 的形狀為 (164496, 8976)。
現在我們有大約 165000 個訓練樣本。X 的列寬為 199,因為其對應於我們允許的最長序列長度(200-1,減去的 1 是要預測的標籤)。y 有 8976 列,對應於辭彙表所有詞的一個稀疏矩陣。現在,數據集就準備好了!
2. 構建模型
我們將使用長短期記憶網路(LSTM)。LSTM 有一個重要優勢,即能夠理解在整個序列上的依賴情況,因此,句子的起始部分可能會影響到所要預測的第 15 個詞。另一方面,循環神經網路(RNN)僅涉及對網路之前狀態的依賴,且僅有前一個詞有助於預測下一個詞。如果選用 RNN,我們很快就會失去上下文語境,因此選擇 LSTM 似乎是正確的。
a. 模型架構
因為訓練需要非常非常非常非常非常的時間(不是開玩笑),所以我們就創建一個簡單的「1 嵌入層+1 LSTM 層+1 密集層」的網路:
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()
首先,我們添加一個嵌入層。我們將其傳遞給一個有 100 個神經元的 LSTM,添加一個 dropout 來控制神經元共適應(neuron co-adaptation),最後添加一個密集層(dense layer)收尾。注意,我們僅在最後一層上應用一個 softmax 激活函數,以獲得輸出屬於每個類別的概率。這裡使用的損失是類別交叉熵,因為這是一個多類分類問題。
下面匯總了該模型的情況:

模型情況總覽
b. 訓練模型
現在我們終於準備好訓練模型了!
model.fit(X, y, batch_size=256, epochs=100, verbose=True)
然後模型的訓練就開始了:
Epoch 1/10 164496/164496 [==============================] - 471s 3ms/step - loss: 7.0687 Epoch 2/10 73216/164496 [============>.................] - ETA: 5:12 - loss: 7.0513
在一個 CPU 上,單個 epoch 耗時大約 8 分鐘。在 GPU 上(比如 Colab),你應該修改所使用的 Keras LSTM 網路,因為它不能被用在 GPU 上。你需要的是這個:
# Modify Import from keras.layers import Embedding, LSTM, Dense, Dropout, CuDNNLSTM # In the Moddel ... model.add(CuDNNLSTM(100)) ...
我在訓練幾步之後就會停一下,以便取樣預測結果,以及根據交叉熵的不同值來控制模型的品質。
下面是我觀察到的結果:

3. 生成句子
讀到這裡,下一步就可以預料了:生成新句子!要生成新句子,我們需要將同樣的變換應用到輸入文本上。我們構建一個循環來迭代生成下一個詞一定次數:
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
當損失大約為 3.1 時,下面是使用「Google」作為輸入而生成的句子:
Google is a large amount of data produced worldwide
這沒什麼真正的含義,但它成功地將 Google 與大量數據的概念關聯到了一起。這是非常了不起的,因為這隻依靠詞的共現,並沒有整合任何語法概念。
如果模型的訓練時間更長一些,將損失降到了 2.5,那麼給定輸入「Random Forest」,會得到:
Random Forest is a fully managed service distributed designed to support a large amount of startups vision infrastructure
同樣,生成的東西沒什麼意義,但其語法結構是相當正確的。
損失在大約 50 epoch 後就收斂了,且從未低於 2.5。
我認為這是由於這裡開發的方法的局限性:
- 模型仍然非常簡單
- 訓練數據沒有理應的那樣整潔
- 數據量非常有限
話雖如此,我認為結果還是挺有意思的,比如訓練好的模型可以輕鬆地部署到 Flask WebApp 上。
總結
我希望這篇文章是有用的。我嘗試闡釋了語言生成的主要概念、難題和局限。相比於本文中討論的方法,更大型的網路和更好的數據肯定有助於改善結果。
原文鏈接:https://towardsdatascience.com/i-trained-a-network-to-speak-like-me-9552c16e2396
本文為機器之心編譯,轉載請聯繫本公眾號獲得授權。
✄————————————————
加入機器之心(全職記者 / 實習生):[email protected]
投稿或尋求報道:[email protected]
廣告 & 商務合作:[email protected]