水師提督速成指南:用Keras打造你的AI水軍

景略集智發表於2019-03-03

我們之前在知乎上曾有篇回答,講了美國芝加哥大學的研究人員可以用 AI 為 Yelp 上的餐館和酒店寫虛假評論,讓 AI 客串了一回水軍,而且效果足以以假亂真。

今天我們就分享一下如何用 Keras 打造 AI 水軍,寫出逼真的餐廳評論,速度升職為水師提督。
在閱讀本篇教程之後,你就能學會如何生成一條 5 星的 Yelp 餐廳評論。

下面是 AI 生成的一些評論示例(未編輯狀態):

我吃了牛排、貽貝,還有意式帕瑪森烤雞,全都很好吃,我們還會再來的。

飯菜、服務還有氛圍都很棒,我會給所有的朋友推薦這裡。

很好的氛圍,很棒的飯菜,服務也很好。值得一試!

I had the steak, mussels with a side of chicken parmesan. All were very good. We will be back.

The food, service, atmosphere, and service are excellent. I would recommend it to all my friends

Good atmosphere, amazing food and great service.Service is also pretty good. Give them a try!

下面會教你:

  • 獲取和準備訓練資料。

  • 搭建字元級語言模型。

  • 訓練模型時的 Tips。

  • 生成隨機評論。

在 GPU 上只花幾天就能很容易的訓練出一個模型。幸好,現在有不少預訓練模型權重,所以你也可以直接跳到最後的生成評論部分。

準備資料

我們在 Yelp 官網上,可以免費獲得 json 格式的 Yelp 資料集 資料集

下載資料集並提取資料後,在資料集資料夾中,你會發現兩個需要的檔案:

Review.json

Business.json

這裡提醒一下,這兩個檔案都很大,特別是 review.json,(3.7 G)。

Review.json 檔案的每一行都是一條 json 字串格式的評論,這兩個資料夾中並沒有開始和結束括號 [],因此 json 檔案的內容整體來看並不是一個有效的 json 字串。此外,將整個 review.json 檔案放到記憶體中可能會很困難。因此我們首先用指令碼將它們逐行轉為 CSV 格式的檔案。

python json_converter.py ./dataset/review.json
python json_converter.py ./dataset/business.json
複製程式碼

在這之後,你會發現資料集資料夾中有兩個檔案,它們都是有效的 CSV 檔案,可以用 Pandas 程式庫開啟。
我們接下來會這麼做:只從類別中有“restaurant”標籤的業務中提取 5 星評論文字。

# Read thow two CSV files to pandas dataframes
df_business=pd.read_csv(`../dataset/business.csv`)
df_review=pd.read_csv(`../dataset/review.csv`)
# Filter `Restaurants` businesses
restaurants = df_business[df_business[`categories`].str.contains(`Restaurants`)]
# Filter 5-stars reviews
five_star=df_review[df_review[`stars`]==5]
# merge the reviews with restaurants by key `business_id`
# This keep only 5-star restaurants reviews
combo=pd.merge(restaurants_clean, five_star, on=`business_id`)
# review texts column
rnn_fivestar_reviews_only=combo[[`text`]]
複製程式碼

接下來,我們移除評論中的新行字元和重複的評論。

# remove new line characters
rnn_fivestar_reviews_only=rnn_fivestar_reviews_only.replace({r`
+`: ``}, regex=True)
# remove dupliated reviews
final=rnn_fivestar_reviews_only.drop_duplicates()
複製程式碼

為了給模型展示評論的開始和結尾在哪裡,我們需要為評論文字新增特殊的標記。
那麼最終準備好的評論中會有一行達到預期,如下所示:

鷹嘴豆沙很好吃,也很新鮮!沙拉三明治也超棒。絕對值得再去!店老闆人很好,員工都很和藹。

“Hummus is amazing and fresh! Loved the falafels. I will definitely be back. Great owner, friendly staff”

搭建模型
我們所搭建的模型是一個字元級語言模型,意味著最小的可區分符號為字元。你也可以試試詞彙級模型,也就是輸入為單詞標記。
關於字元級語言模型,有優點也有缺點。

優點:

這樣就不必擔心未知詞彙的問題。
能夠學習較大的詞彙。

缺點:

這樣會造成一個很長的序列,在獲取遠端依賴性方面(語句較早部分對後期部分的影響)不如詞彙級模型。

而且字元級模型在訓練時需要消耗的計算資源也更多。

模型和 lstm_text_generation.py demo code 很像,例外之處是我們會再堆疊幾個迴圈神經網路單元,從而讓隱藏層在輸入層和輸出層之間能儲存更多的資訊。這樣能生成更加真實的 Yelp 評論。

在展示模型的程式碼之前,我們先深究一下堆疊的 RNN 是如何工作的。
你可能在標準的神經網路中見過它(也就是 Keras 中的緻密層,Dense layer)。

第一個層會用輸入 X 計算啟用值 a[1],它會堆疊第二個層來計算出下一個啟用值 a[2]。

水師提督速成指南:用Keras打造你的AI水軍

堆疊的 RNN 有點像標準的神經網路,而且能“及時展開”。

記號 a[I] 表示層 I 的啟用配置,其中 表示時步 t。

水師提督速成指南:用Keras打造你的AI水軍

我們瞅一眼怎麼計算出一個啟用值。

要想計算 a[2]<3>,需要兩個輸入,a[2]<2> 和 a[1]<3>。

g 是啟用函式,wa[2] 和 ba[2] 是層 2 的引數。

水師提督速成指南:用Keras打造你的AI水軍

我們可以看到,要想堆疊 RNN,之前的 RNN 需要將所有的時步 ato 返回到下面的 RNN 中。
Keras 中預設一個 RNN 層,比如 LSTM,只返回最後的時步啟用值 a。為了返回所有時步的啟用值,我們要將 return_sequences 引數設為 true。

這裡說說如何在 Keras 上搭建模型。每個輸入樣本都是一個 60 個字元的獨熱表示(one-hot representation),總共有 95 個可能的字元。

每個輸出就是每個字元的一列 95 個預測概率。

import keras
from keras import layers

model = keras.models.Sequential()
model.add(layers.LSTM(1024, input_shape=(60, 95),return_sequences=True))
model.add(layers.LSTM(1024, input_shape=(60, 95)))
model.add(layers.Dense(95, activation=`softmax`))
複製程式碼

訓練模型

訓練模型的思路很簡單,我們會以輸入/輸出成對地訓練模型。每個輸入是 60 個字元,相應的輸出為緊跟在後面的字元。

在資料準備這一步,我們建立了一列乾淨的 5 星評論文字。總共有 1214016 行評論。為了讓訓練簡單一點,我們只訓練字元長度小於或等於 250 的評論,最後會得到 418955 行評論。

然後我們打亂評論的順序,這樣我們就不會用一行中同一家餐廳的 100 條評論來訓練模型。
我們會將所有的評論讀取為一個長文字字串,然後建立一個 Python 目錄(比如一個雜湊表)將每個字元對映到 0-94(總共 95 個特別字元)之間的一個索引上。

# List of unique characters in the corpus
chars = sorted(list(set(text)))
print(`Unique characters:`, len(chars))
# Dictionary mapping unique characters to their index in `chars`
char_indices = dict((char, chars.index(char)) for char in chars)
複製程式碼

文字庫一共有 72662807 個字元,很難將其作為一個整體處理。因此我們將它拆分為幾個文字塊,每塊有 90k 個字元。

對於拆分後的每個文字塊,我們會生成這部分的每對輸入和輸出。從頭到尾轉變文字塊的指標,如果時步設為 1 的話,每次轉變 1 個字元。

def getDataFromChunk(txtChunk, maxlen=60, step=1):
   sentences = []
   next_chars = []
   for i in range(0, len(txtChunk) - maxlen, step):
       sentences.append(txtChunk[i : i + maxlen])
       next_chars.append(txtChunk[i + maxlen])
   print(`nb sequences:`, len(sentences))
   print(`Vectorization...`)
   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
return [X, y]
複製程式碼

在 GPU(GTX1070)上訓練一個文字塊的話,每個 epoch 耗時 219 秒,那麼訓練完整個文字庫需要花費大約 2 天時間。
72662807 / 90000 * 219 /60 / 60/ 24 = 2.0 days

Keras 的兩個回撥函式 ModelCheckpoint 和 ReduceLROnPlateau 用起來很方便。
ModelCheckpoint 能幫我們儲存每一次優化時的權重。
當損失度量停止下降時,ReduceLROnPlateau 回撥函式會自動減少學習率。它的主要益處是我們不需要再手動調整學習率,主要缺點是它的學習率會一直下降和衰退。

# this saves the weights everytime they improve so you can let it train.  Also learning rate decay
filepath="Feb-22-all-{epoch:02d}-{loss:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor=`loss`, verbose=1, save_best_only=True, mode=`min`)
reduce_lr = ReduceLROnPlateau(monitor=`loss`, factor=0.5,
             patience=1, min_lr=0.00001)
callbacks_list = [checkpoint, reduce_lr]
複製程式碼

將模型訓練 20 個 epoch 的程式碼如下:

for iteration in range(1, 20):
   print(`Iteration`, iteration)
   with open("../dataset/short_reviews_shuffle.txt") as f:
       for chunk in iter(lambda: f.read(90000), ""):
           X, y = getDataFromChunk(chunk)
           model.fit(X, y, batch_size=128, epochs=1, callbacks=callbacks_list)
複製程式碼

如果全部訓練完,大概需要 1 個月的時間。但是對我們來說,訓練上 2 個小時就能生成很不錯的結果。

生成5星評論

有了預訓練模型的權重或者你自己訓練的模型,我們就可以生成有意思的 Yelp 評論了。
思路是這樣:我們用初始 60 個字元作為模型的“種子”,然後讓模型預測緊接下來的字元。

水師提督速成指南:用Keras打造你的AI水軍

“索引抽樣”過程會根據給定預測生成一些隨機性,為最終結果新增一些變體。

如果 temperature 的值很小,它會一直選取有最高預測概率的索引。

def sample(preds, temperature=1.0):
   ```
   Generate some randomness with the given preds
   which is a list of numbers, if the temperature
   is very small, it will always pick the index
   with highest pred value
   ```
   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)
複製程式碼

要想生成 300 個字元,程式碼如下:

# We generate 300 characters
for i in range(300):
   sampled = np.zeros((1, maxlen, len(chars)))
   # Turn each char to char index.
   for t, char in enumerate(generated_text):
       sampled[0, t, char_indices[char]] = 1.
   # Predict next char probabilities
   preds = model.predict(sampled, verbose=0)[0]
   # Add some randomness by sampling given probabilities.
   next_index = sample(preds, temperature)
   # Turn char index to char.
   next_char = chars[next_index]
   # Append char to generated text string
   generated_text += next_char
   # Pop the first char in generated text string.
   generated_text = generated_text[1:]
   # Print the new generated char.
   sys.stdout.write(next_char)
   sys.stdout.flush()
print(generated_text)
複製程式碼

結語

在本文,我們學習瞭如何用 Keras 搭建和訓練一個字元級文字生成模型。本專案的原始碼以及所用的預訓練模型,均可在 GitHub 上獲取獲取

emmmmmm…來都來了,你還可以看看我站其它關於文字處理的教程與文章:

這評論有毒!——文字分類的一般套路

基於TensorFlow用卷積神經網路做文字分類

相關文章