如何用 Python 和迴圈神經網路做中文文字分類?

本文為你展示,如何使用 fasttext 詞嵌入預訓練模型和迴圈神經網路(RNN), 在 Keras 深度學習框架上對中文評論資訊進行情感分類。
疑問
回顧一下,之前咱們講了很多關於中文文字分類的內容。
你現在應該已經知道如何對中文文字進行分詞了。
你也已經學習過,如何利用經典的機器學習方法,對分詞後的中文文字,做分類。
你還學習過,如何用詞嵌入預訓練模型,以向量,而不是一個簡單的索引數值,來代表詞語,從而讓中文詞語的表徵包含語義級別的資訊。
但是,好像還差了點兒什麼。
對,基於 深度學習 的中文文字分類方法,老師是不是忘了講?
其實沒有。
我一直惦記著,把這個重要的知識點,給你詳細講解一下。但是之前這裡面一直有一條鴻溝,那就是迴圈神經網路(Recurrent Neural Network, RNN)。
如果你不知道 RNN 是怎麼回事兒,你就很難理解文字作為序列,是如何被深度學習模型來處理的。
好在,我已經為你做了視訊教程,用手繪的方式,給你講了這一部分。

既然現在這道鴻溝,已被跨越了。本文咱們就來嘗試,把之前學過的知識點整合在一起,用 Python 和 Keras 深度學習框架,對中文文字嘗試分類。
環境
為了對比的便捷,咱們這次用的,還是《 ofollow,noindex" target="_blank">如何用Python和機器學習訓練中文文字情感分類模型? 》一文中採用過的某商戶的點評資料。
我把它放在了一個 github repo 中,供你使用。
請點選 這個連結 ,訪問咱們的程式碼和資料。

我們的資料就是其中的 dianping.csv
。你可以點選它,看看內容。

每一行是一條評論。評論內容和情感間,用逗號分隔。
1 代表正向情感,0 代表負面情感。
注意,請使用 Google Chrome 瀏覽器來完成以下操作。因為你需要安裝一個瀏覽器外掛外掛,叫做 Colaboratory ,它是 Google 自家的外掛,只能在 Chrome 瀏覽器中,才能執行。
點選這個連結,安裝外掛。

把它新增到 Google Chrome 之後,你會在瀏覽器的擴充套件工具欄裡面,看見下圖中間的圖示:

回到本範例的 github repo 主頁面 ,開啟其中的 demo.ipynb
檔案。

然後,點選剛剛安裝的 Colaboratory 擴充套件圖示。Google Chrome 會自動幫你開啟 Google Colab,並且裝載這個 ipynb 檔案。

點選選單欄裡面的“程式碼執行程式”,選擇“更改執行時型別”。

在出現的對話方塊中,確認選項如下圖所示。

點選“儲存”即可。
下面,你就可以依次執行每一個程式碼段落了。
注意第一次執行的時候,可能會有警告提示。

出現上面這個警告的時候,點選“仍然執行”就可以繼續了。
環境準備好了,下面我們來一步步執行程式碼。
預處理
首先,我們準備好 Pandas ,用來讀取資料。
import pandas as pd
我們從前文介紹的 github repo 裡面,下載程式碼和資料。
!git clone https://github.com/wshuyi/demo-chinese-text-classification-lstm-keras.git

下面,我們呼叫 pathlib 模組,以便使用路徑資訊。
from pathlib import Path
我們定義自己要使用的程式碼和資料資料夾。
mypath = Path("demo-chinese-text-classification-lstm-keras")
下面,從這個資料夾裡,把資料檔案開啟。
df = pd.read_csv(mypath/'dianping.csv')
看看頭幾行資料:
df.head()

讀取正確,下面我們來進行分詞。
我們先把結巴分詞安裝上。
!pip install jieba

安裝好之後,匯入分詞模組。
import jieba
對每一條評論,都進行切分:
df['text'] = df.comment.apply(lambda x: " ".join(jieba.cut(x)))
因為一共只有2000條資料,所以應該很快完成。
Building prefix dict from the default dictionary ... Dumping model to file cache /tmp/jieba.cache Loading model cost 1.089 seconds. Prefix dict has been built succesfully.
再看看此時的前幾行資料。
df.head()

如圖所示, text
一欄下面,就是對應的分詞之後的評論。
我們捨棄掉原始評論文字,只保留目前的分詞結果,以及對應的情感標記。
df = df[['text', 'sentiment']]
看看前幾行:
df.head()

好了,下面我們讀入一些 Keras 和 Numpy 模組,為後面的預處理做準備:
from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences import numpy as np
系統提示我們,使用的後端框架,是 Tensorflow 。
Using TensorFlow backend.
下面我們要設定一下,每一條評論,保留多少個單詞。當然,這裡實際上是指包括標點符號在內的“記號”(token)數量。我們決定保留 100 個。
然後我們指定,全域性字典裡面,一共保留多少個單詞。我們設定為 10000 個。
maxlen = 100 max_words = 10000
下面的幾條語句,會自動幫助我們,把分詞之後的評論資訊,轉換成為一系列的數字組成的序列。
tokenizer = Tokenizer(num_words=max_words) tokenizer.fit_on_texts(df.text) sequences = tokenizer.texts_to_sequences(df.text)
看看轉換後的資料型別。
type(sequences) list
可見, sequences
是列表型別。
我們看看第一條資料是什麼。
sequences[:1]

評論語句中的每一個記號,都被轉換成為了對應的序號。
但是這裡有個問題——評論句子有長有短,其中包含的記號個數不同啊。
我們驗證一下,只看前面5句。
for sequence in sequences[:5]: print(len(sequence)) 150 12 16 57 253
果然,不僅長短不一,而且有的還比我們想要的記號數量多。
沒關係,用 pad_sequences
方法裁長補短,我們讓它統一化:
data = pad_sequences(sequences, maxlen=maxlen)
再看看這次的資料:
data array([[2,1,74, ..., 4471,864,4], [0,0,0, ...,9,52,6], [0,0,0, ...,1, 3154,6], ..., [0,0,0, ..., 2840,1, 2240], [0,0,0, ...,19,44,196], [0,0,0, ...,533,42,6]], dtype=int32)
那些長句子,被剪裁了;短句子,被從頭補充了若干個 0 。
同時,我們還希望知道,這些序號分別代表什麼單詞,所以我們把這個索引儲存下來。
word_index = tokenizer.word_index
看看索引的型別。
type(word_index) dict
沒錯,它是個字典(dict)。列印看看。
print(word_index)

好了,中文評論資料,已經被我們處理成一系列長度為 100 ,其中都是序號的序列了。下面我們要把對應的情感標記,儲存到 labels
中。
labels = np.array(df.sentiment)
看一下其內容:
labels array([0, 1, 0, ..., 0, 1, 1])
好了,總體資料都已經備妥了。下面我們來劃分一下訓練集和驗證集。
我們採用的,是把序號隨機化,但保持資料和標記之間的一致性。
indices = np.arange(data.shape[0]) np.random.shuffle(indices) data = data[indices] labels = labels[indices]
看看此時的標記:
labels array([0, 1, 1, ..., 0, 1, 1])
注意順序已經發生了改變。
我們希望,訓練集佔 80% ,驗證集佔 20%。根據總數,計算一下兩者的實際個數:
training_samples = int(len(indices) * .8) validation_samples = len(indices) - training_samples
其中訓練集包含多少資料?
training_samples 1600
驗證集呢?
validation_samples 400
下面,我們正式劃分資料。
X_train = data[:training_samples] y_train = labels[:training_samples] X_valid = data[training_samples: training_samples + validation_samples] y_valid = labels[training_samples: training_samples + validation_samples]
看看訓練集的輸入資料:
X_train array([[0,0,0, ...,963,4,322], [0,0,0, ..., 1485,79,22], [1,26,305, ...,289,3,71], ..., [0,0,0, ...,365,810,3], [0,0,0, ...,1,162, 1727], [ 141,5,237, ...,450,254,4]], dtype=int32)
好了,至此預處理部分,就算完成了。
詞嵌入
下面,我們安裝 gensim 軟體包,以便使用 Facebook 提供的 fasttext 詞嵌入預訓練模型。
!pip install gensim

讀入載入工具:
from gensim.models import KeyedVectors
然後我們需要把 github repo 中下載來的詞嵌入預訓練模型壓縮資料解壓。
myzip = mypath / 'zh.zip' !unzip $myzip Archive:demo-chinese-text-classification-lstm-keras/zh.zip inflating: zh.vec
好了,讀入詞嵌入預訓練模型資料。
zh_model = KeyedVectors.load_word2vec_format('zh.vec')
看看其中的第一個向量是什麼:
zh_model.vectors[0]

這麼長的向量,對應的記號是什麼呢?
看看前五個詞彙:
list(iter(zh_model.vocab))[:5] ['的', '</s>', '在', '是', '年']
原來,剛才這個向量,對應的是標記“的”。
向量裡,到底有多少個數字?
len(zh_model[next(iter(zh_model.vocab))]) 300
我們把這個向量長度,進行儲存。
embedding_dim = len(zh_model[next(iter(zh_model.vocab))])
然後,以我們最大化標記個數,以及每個標記對應向量長度,建立一個隨機矩陣。
embedding_matrix = np.random.rand(max_words, embedding_dim)
看看它的內容:
embedding_matrix

因為這種隨機矩陣,預設都是從0到1的實數。
然而,我們剛才已經看過了“的”的向量表示,

請注意,其中的數字在 -1 到 1 的範圍中間。為了讓我們隨機產生的向量,跟它類似,我們把矩陣進行一下數學轉換:
embedding_matrix = (embedding_matrix - 0.5) * 2 embedding_matrix

這樣看起來就好多了。
我們嘗試,對某個特定標記,讀取預訓練的向量結果:
zh_model.get_vector('的')

但是注意,如果標記在預訓練過程中沒有出現,會如何呢?
試試輸入我的名字:
zh_model.get_vector("王樹義")

不好意思,因為我的名字,在 fasttext 做預訓練的時候沒有出現,所以會報錯。
因此,在我們構建適合自己任務的詞嵌入層的時候,也需要注意那些沒有被訓練過的詞彙。
這裡我們判斷一下,如果無法獲得對應的詞向量,我們就乾脆跳過,使用預設的隨機向量。
for word, i in word_index.items(): if i < max_words: try: embedding_vector = zh_model.get_vector(word) embedding_matrix[i] = embedding_vector except: pass
這也是為什麼,我們前面儘量把二者的分佈調整成一致。
看看我們產生的詞嵌入矩陣:
embedding_matrix

模型
詞嵌入準備好了,下面我們就要搭建模型了。
from keras.models import Sequential from keras.layers import Embedding, Flatten, Dense, LSTM units = 32 model = Sequential() model.add(Embedding(max_words, embedding_dim)) model.add(LSTM(units)) model.add(Dense(1, activation='sigmoid')) model.summary()

注意這裡的模型,是最簡單的順序模型,對應的模型圖如下:

如圖所示,我們輸入資料通過詞嵌入層,從序號轉化成為向量,然後經過 LSTM (RNN 的一個變種)層,依次處理,最後產生一個32位的輸出,代表這句評論的特徵。
這個特徵,通過一個普通神經網路層,然後採用 Sigmoid 函式,輸出為一個0到1中間的數值。

這樣,我們就可以通過數值與 0 和 1 中哪個更加接近,進行分類判斷。
但是這裡注意,搭建的神經網路裡,Embedding 只是一個隨機初始化的層次。我們需要把剛剛構建的詞嵌入矩陣匯入。
model.layers[0].set_weights([embedding_matrix]) model.layers[0].trainable = False
這裡,我們希望保留好不容易獲得的單詞預訓練結果,所以在後面的訓練中,我們 不希望 對這一層進行訓練。
因為是二元分類,因此我們設定了損失函式為 binary_crossentropy
。
我們訓練模型,儲存輸出為 history
,並且把最終的模型儲存為 mymodel.h5
。
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc']) history = model.fit(X_train, y_train, epochs=10, batch_size=32, validation_data=(X_valid, y_valid)) model.save("mymodel.h5")
執行上面程式碼段,模型就在認認真真訓練了。

結果如上圖所示。
討論
對於這個模型的分類效果,你滿意嗎?
如果單看最終的結果,訓練集準確率超過 90%, 驗證集準確率也超過 80%,好像還不錯嘛。
但是,我看到這樣的資料時,會有些擔心。
我們把那些數值,用視覺化的方法,顯示一下:
import matplotlib.pyplot as plt acc = history.history['acc'] val_acc = history.history['val_acc'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(acc) + 1) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.show()

上圖是準確率曲線。虛線是訓練集,實線是驗證集。我們看到,訓練集一路走高,但是驗證集在波動。雖然最後一步剛好是最高點。
看下面的圖,會更加清晰。

上圖是損失數值對比。我們可以看到,訓練集上,損失數值一路向下,但是,從第2個 epoch 開始,驗證集的損失數值,就沒有保持連貫的顯著下降趨勢。二者發生背離。
這意味著什麼?
這就是深度學習中,最常見,也是最惱人的問題——過擬合(overfitting)。
《 如何用機器學習處理二元分類任務? 》一文中,我曾經就這個問題,為你做過詳細的介紹。這裡不贅述了。
但是,我希望你能夠理解它出現的原因——相對於你目前使用的迴圈神經網路結構,你的資料量太小了。
深度學習,對於資料數量和質量的需求,都很高。
有沒有辦法,可以讓你不需要這麼多的資料,也能避免過擬合,取得更好的訓練結果呢?
這個問題的答案,我在《 如何用 Python 和深度遷移學習做文字分類? 》一文中已經為你介紹過,如果你忘記了,請複習一下。
小結
本文,我們探討了如何用迴圈神經網路處理中文文字分類問題。讀過本文並且實踐之後,你應該已經能夠把下列內容融會貫通了:
- 文字預處理
- 詞嵌入矩陣構建
- 迴圈神經網路模型搭建
- 訓練效果評估
希望這份教程,可以在你的科研和工作中,幫上一些忙。
祝(深度)學習愉快!
喜歡請點贊和打賞。還可以微信關注和置頂我的公眾號 “玉樹芝蘭”(nkwangshuyi) 。
如果你對 Python 與資料科學感興趣,不妨閱讀我的系列教程索引貼《 如何高效入門資料科學? 》,裡面還有更多的有趣問題及解法。