本文為你展示,如何使用 fasttext 詞嵌入預訓練模型和循環(huán)神經(jīng)網(wǎng)絡(RNN), 在 Keras 深度學習框架上對中文評論信息進行情感分類。
疑問
回顧一下,之前咱們講了很多關于中文文本分類的內(nèi)容。
你現(xiàn)在應該已經(jīng)知道如何對中文文本進行分詞了。
你也已經(jīng)學習過,如何利用經(jīng)典的機器學習方法,對分詞后的中文文本,做分類。
你還學習過,如何用詞嵌入預訓練模型,以向量,而不是一個簡單的索引數(shù)值,來代表詞語,從而讓中文詞語的表征包含語義級別的信息。
但是,好像還差了點兒什么。
對,基于深度學習的中文文本分類方法,老師是不是忘了講?
其實沒有。
我一直惦記著,把這個重要的知識點,給你詳細講解一下。但是之前這里面一直有一條鴻溝,那就是循環(huán)神經(jīng)網(wǎng)絡(Recurrent Neural Network, RNN)。
如果你不知道 RNN 是怎么回事兒,你就很難理解文本作為序列,是如何被深度學習模型來處理的。
好在,我已經(jīng)為你做了視頻教程,用手繪的方式,給你講了這一部分。
既然現(xiàn)在這道鴻溝,已被跨越了。本文咱們就來嘗試,把之前學過的知識點整合在一起,用 Python 和 Keras 深度學習框架,對中文文本嘗試分類。
環(huán)境
為了對比的便捷,咱們這次用的,還是《如何用Python和機器學習訓練中文文本情感分類模型?》一文中采用過的某商戶的點評數(shù)據(jù)。
我把它放在了一個 github repo 中,供你使用。
請點擊這個鏈接,訪問咱們的代碼和數(shù)據(jù)。
我們的數(shù)據(jù)就是其中的 dianping.csv
。你可以點擊它,看看內(nèi)容。
每一行是一條評論。評論內(nèi)容和情感間,用逗號分隔。
1 代表正向情感,0 代表負面情感。
注意,請使用 Google Chrome 瀏覽器來完成以下操作。因為你需要安裝一個瀏覽器插件插件,叫做 Colaboratory ,它是 Google 自家的插件,只能在 Chrome 瀏覽器中,才能運行。
點擊這個鏈接,安裝插件。
把它添加到 Google Chrome 之后,你會在瀏覽器的擴展工具欄里面,看見下圖中間的圖標:
回到本范例的github repo 主頁面,打開其中的 demo.ipynb
文件。
然后,點擊剛剛安裝的 Colaboratory 擴展圖標。Google Chrome 會自動幫你開啟 Google Colab,并且裝載這個 ipynb 文件。
點擊菜單欄里面的“代碼執(zhí)行程序”,選擇“更改運行時類型”。
在出現(xiàn)的對話框中,確認選項如下圖所示。
點擊“保存”即可。
下面,你就可以依次執(zhí)行每一個代碼段落了。
注意第一次執(zhí)行的時候,可能會有警告提示。
出現(xiàn)上面這個警告的時候,點擊“仍然運行”就可以繼續(xù)了。
環(huán)境準備好了,下面我們來一步步運行代碼。
預處理
首先,我們準備好 Pandas ,用來讀取數(shù)據(jù)。
import pandas as pd
我們從前文介紹的github repo里面,下載代碼和數(shù)據(jù)。
!git clone https://github.com/wshuyi/demo-chinese-text-classification-lstm-keras.git
下面,我們調(diào)用 pathlib 模塊,以便使用路徑信息。
from pathlib import Path
我們定義自己要使用的代碼和數(shù)據(jù)文件夾。
mypath = Path("demo-chinese-text-classification-lstm-keras")
下面,從這個文件夾里,把數(shù)據(jù)文件打開。
df = pd.read_csv(mypath/'dianping.csv')
看看頭幾行數(shù)據(jù):
df.head()
讀取正確,下面我們來進行分詞。
我們先把結(jié)巴分詞安裝上。
!pip install jieba
安裝好之后,導入分詞模塊。
import jieba
對每一條評論,都進行切分:
df['text'] = df.comment.apply(lambda x: " ".join(jieba.cut(x)))
因為一共只有2000條數(shù)據(jù),所以應該很快完成。
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.
再看看此時的前幾行數(shù)據(jù)。
df.head()
如圖所示,text
一欄下面,就是對應的分詞之后的評論。
我們舍棄掉原始評論文本,只保留目前的分詞結(jié)果,以及對應的情感標記。
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
系統(tǒng)提示我們,使用的后端框架,是 Tensorflow 。
Using TensorFlow backend.
下面我們要設置一下,每一條評論,保留多少個單詞。當然,這里實際上是指包括標點符號在內(nèi)的“記號”(token)數(shù)量。我們決定保留 100 個。
然后我們指定,全局字典里面,一共保留多少個單詞。我們設置為 10000 個。
maxlen = 100
max_words = 10000
下面的幾條語句,會自動幫助我們,把分詞之后的評論信息,轉(zhuǎn)換成為一系列的數(shù)字組成的序列。
tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(df.text)
sequences = tokenizer.texts_to_sequences(df.text)
看看轉(zhuǎn)換后的數(shù)據(jù)類型。
type(sequences)
list
可見, sequences
是列表類型。
我們看看第一條數(shù)據(jù)是什么。
sequences[:1]
評論語句中的每一個記號,都被轉(zhuǎn)換成為了對應的序號。
但是這里有個問題——評論句子有長有短,其中包含的記號個數(shù)不同啊。
我們驗證一下,只看前面5句。
for sequence in sequences[:5]:
print(len(sequence))
150
12
16
57
253
果然,不僅長短不一,而且有的還比我們想要的記號數(shù)量多。
沒關系,用 pad_sequences
方法裁長補短,我們讓它統(tǒng)一化:
data = pad_sequences(sequences, maxlen=maxlen)
再看看這次的數(shù)據(jù):
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)
好了,中文評論數(shù)據(jù),已經(jīng)被我們處理成一系列長度為 100 ,其中都是序號的序列了。下面我們要把對應的情感標記,存儲到 labels
中。
labels = np.array(df.sentiment)
看一下其內(nèi)容:
labels
array([0, 1, 0, ..., 0, 1, 1])
好了,總體數(shù)據(jù)都已經(jīng)備妥了。下面我們來劃分一下訓練集和驗證集。
我們采用的,是把序號隨機化,但保持數(shù)據(jù)和標記之間的一致性。
indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]
看看此時的標記:
labels
array([0, 1, 1, ..., 0, 1, 1])
注意順序已經(jīng)發(fā)生了改變。
我們希望,訓練集占 80% ,驗證集占 20%。根據(jù)總數(shù),計算一下兩者的實際個數(shù):
training_samples = int(len(indices) * .8)
validation_samples = len(indices) - training_samples
其中訓練集包含多少數(shù)據(jù)?
training_samples
1600
驗證集呢?
validation_samples
400
下面,我們正式劃分數(shù)據(jù)。
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]
看看訓練集的輸入數(shù)據(jù):
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 中下載來的詞嵌入預訓練模型壓縮數(shù)據(jù)解壓。
myzip = mypath / 'zh.zip'
!unzip $myzip
Archive: demo-chinese-text-classification-lstm-keras/zh.zip
inflating: zh.vec
好了,讀入詞嵌入預訓練模型數(shù)據(jù)。
zh_model = KeyedVectors.load_word2vec_format('zh.vec')
看看其中的第一個向量是什么:
zh_model.vectors[0]
這么長的向量,對應的記號是什么呢?
看看前五個詞匯:
list(iter(zh_model.vocab))[:5]
['的', '</s>', '在', '是', '年']
原來,剛才這個向量,對應的是標記“的”。
向量里,到底有多少個數(shù)字?
len(zh_model[next(iter(zh_model.vocab))])
300
我們把這個向量長度,進行保存。
embedding_dim = len(zh_model[next(iter(zh_model.vocab))])
然后,以我們最大化標記個數(shù),以及每個標記對應向量長度,建立一個隨機矩陣。
embedding_matrix = np.random.rand(max_words, embedding_dim)
看看它的內(nèi)容:
embedding_matrix
因為這種隨機矩陣,默認都是從0到1的實數(shù)。
然而,我們剛才已經(jīng)看過了“的”的向量表示,
請注意,其中的數(shù)字在 -1 到 1 的范圍中間。為了讓我們隨機產(chǎn)生的向量,跟它類似,我們把矩陣進行一下數(shù)學轉(zhuǎn)換:
embedding_matrix = (embedding_matrix - 0.5) * 2
embedding_matrix
這樣看起來就好多了。
我們嘗試,對某個特定標記,讀取預訓練的向量結(jié)果:
zh_model.get_vector('的')
但是注意,如果標記在預訓練過程中沒有出現(xiàn),會如何呢?
試試輸入我的名字:
zh_model.get_vector("王樹義")
不好意思,因為我的名字,在 fasttext 做預訓練的時候沒有出現(xiàn),所以會報錯。
因此,在我們構(gòu)建適合自己任務的詞嵌入層的時候,也需要注意那些沒有被訓練過的詞匯。
這里我們判斷一下,如果無法獲得對應的詞向量,我們就干脆跳過,使用默認的隨機向量。
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
這也是為什么,我們前面盡量把二者的分布調(diào)整成一致。
看看我們產(chǎn)生的詞嵌入矩陣:
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()
注意這里的模型,是最簡單的順序模型,對應的模型圖如下:
如圖所示,我們輸入數(shù)據(jù)通過詞嵌入層,從序號轉(zhuǎn)化成為向量,然后經(jīng)過 LSTM (RNN 的一個變種)層,依次處理,最后產(chǎn)生一個32位的輸出,代表這句評論的特征。
這個特征,通過一個普通神經(jīng)網(wǎng)絡層,然后采用 Sigmoid 函數(shù),輸出為一個0到1中間的數(shù)值。
這樣,我們就可以通過數(shù)值與 0 和 1 中哪個更加接近,進行分類判斷。
但是這里注意,搭建的神經(jīng)網(wǎng)絡里,Embedding 只是一個隨機初始化的層次。我們需要把剛剛構(gòu)建的詞嵌入矩陣導入。
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False
這里,我們希望保留好不容易獲得的單詞預訓練結(jié)果,所以在后面的訓練中,我們不希望對這一層進行訓練。
因為是二元分類,因此我們設定了損失函數(shù)為 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")
執(zhí)行上面代碼段,模型就在認認真真訓練了。
結(jié)果如上圖所示。
討論
對于這個模型的分類效果,你滿意嗎?
如果單看最終的結(jié)果,訓練集準確率超過 90%, 驗證集準確率也超過 80%,好像還不錯嘛。
但是,我看到這樣的數(shù)據(jù)時,會有些擔心。
我們把那些數(shù)值,用可視化的方法,顯示一下:
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()
上圖是準確率曲線。虛線是訓練集,實線是驗證集。我們看到,訓練集一路走高,但是驗證集在波動。雖然最后一步剛好是最高點。
看下面的圖,會更加清晰。
上圖是損失數(shù)值對比。我們可以看到,訓練集上,損失數(shù)值一路向下,但是,從第2個 epoch 開始,驗證集的損失數(shù)值,就沒有保持連貫的顯著下降趨勢。二者發(fā)生背離。
這意味著什么?
這就是深度學習中,最常見,也是最惱人的問題——過擬合(overfitting)。
《如何用機器學習處理二元分類任務?》一文中,我曾經(jīng)就這個問題,為你做過詳細的介紹。這里不贅述了。
但是,我希望你能夠理解它出現(xiàn)的原因——相對于你目前使用的循環(huán)神經(jīng)網(wǎng)絡結(jié)構(gòu),你的數(shù)據(jù)量太小了。
深度學習,對于數(shù)據(jù)數(shù)量和質(zhì)量的需求,都很高。
有沒有辦法,可以讓你不需要這么多的數(shù)據(jù),也能避免過擬合,取得更好的訓練結(jié)果呢?
這個問題的答案,我在《如何用 Python 和深度遷移學習做文本分類?》一文中已經(jīng)為你介紹過,如果你忘記了,請復習一下。
小結(jié)
本文,我們探討了如何用循環(huán)神經(jīng)網(wǎng)絡處理中文文本分類問題。讀過本文并且實踐之后,你應該已經(jīng)能夠把下列內(nèi)容融會貫通了:
- 文本預處理
- 詞嵌入矩陣構(gòu)建
- 循環(huán)神經(jīng)網(wǎng)絡模型搭建
- 訓練效果評估
希望這份教程,可以在你的科研和工作中,幫上一些忙。
祝(深度)學習愉快!
喜歡請點贊和打賞。還可以微信關注和置頂我的公眾號“玉樹芝蘭”(nkwangshuyi)。
如果你對 Python 與數(shù)據(jù)科學感興趣,不妨閱讀我的系列教程索引貼《如何高效入門數(shù)據(jù)科學?》,里面還有更多的有趣問題及解法。