如何用 Python 和循環(huán)神經(jīng)網(wǎng)絡做中文文本分類?

image

本文為你展示,如何使用 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)為你做了視頻教程,用手繪的方式,給你講了這一部分。

image

既然現(xiàn)在這道鴻溝,已被跨越了。本文咱們就來嘗試,把之前學過的知識點整合在一起,用 Python 和 Keras 深度學習框架,對中文文本嘗試分類。

環(huán)境

為了對比的便捷,咱們這次用的,還是《如何用Python和機器學習訓練中文文本情感分類模型?》一文中采用過的某商戶的點評數(shù)據(jù)。

我把它放在了一個 github repo 中,供你使用。

請點擊這個鏈接,訪問咱們的代碼和數(shù)據(jù)。

image

我們的數(shù)據(jù)就是其中的 dianping.csv 。你可以點擊它,看看內(nèi)容。

image

每一行是一條評論。評論內(nèi)容和情感間,用逗號分隔。

1 代表正向情感,0 代表負面情感。

注意,請使用 Google Chrome 瀏覽器來完成以下操作。因為你需要安裝一個瀏覽器插件插件,叫做 Colaboratory ,它是 Google 自家的插件,只能在 Chrome 瀏覽器中,才能運行。

點擊這個鏈接,安裝插件。

image

把它添加到 Google Chrome 之后,你會在瀏覽器的擴展工具欄里面,看見下圖中間的圖標:

image

回到本范例的github repo 主頁面,打開其中的 demo.ipynb 文件。

image

然后,點擊剛剛安裝的 Colaboratory 擴展圖標。Google Chrome 會自動幫你開啟 Google Colab,并且裝載這個 ipynb 文件。

image

點擊菜單欄里面的“代碼執(zhí)行程序”,選擇“更改運行時類型”。

image

在出現(xiàn)的對話框中,確認選項如下圖所示。

image

點擊“保存”即可。

下面,你就可以依次執(zhí)行每一個代碼段落了。

注意第一次執(zhí)行的時候,可能會有警告提示。

image

出現(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
image

下面,我們調(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()
image

讀取正確,下面我們來進行分詞。

我們先把結(jié)巴分詞安裝上。

!pip install jieba
image

安裝好之后,導入分詞模塊。

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()
image

如圖所示,text 一欄下面,就是對應的分詞之后的評論。

我們舍棄掉原始評論文本,只保留目前的分詞結(jié)果,以及對應的情感標記。

df = df[['text', 'sentiment']]

看看前幾行:

df.head()
image

好了,下面我們讀入一些 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]
image

評論語句中的每一個記號,都被轉(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)
image

好了,中文評論數(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
image

讀入加載工具:

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]
image

這么長的向量,對應的記號是什么呢?

看看前五個詞匯:

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
image

因為這種隨機矩陣,默認都是從0到1的實數(shù)。

然而,我們剛才已經(jīng)看過了“的”的向量表示,

image

請注意,其中的數(shù)字在 -1 到 1 的范圍中間。為了讓我們隨機產(chǎn)生的向量,跟它類似,我們把矩陣進行一下數(shù)學轉(zhuǎn)換:

embedding_matrix = (embedding_matrix - 0.5) * 2
embedding_matrix
image

這樣看起來就好多了。

我們嘗試,對某個特定標記,讀取預訓練的向量結(jié)果:

zh_model.get_vector('的')
image

但是注意,如果標記在預訓練過程中沒有出現(xiàn),會如何呢?

試試輸入我的名字:

zh_model.get_vector("王樹義")
image

不好意思,因為我的名字,在 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
image

模型

詞嵌入準備好了,下面我們就要搭建模型了。

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()
image

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

image

如圖所示,我們輸入數(shù)據(jù)通過詞嵌入層,從序號轉(zhuǎn)化成為向量,然后經(jīng)過 LSTM (RNN 的一個變種)層,依次處理,最后產(chǎn)生一個32位的輸出,代表這句評論的特征。

這個特征,通過一個普通神經(jīng)網(wǎng)絡層,然后采用 Sigmoid 函數(shù),輸出為一個0到1中間的數(shù)值。

image

這樣,我們就可以通過數(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í)行上面代碼段,模型就在認認真真訓練了。

image

結(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()
image

上圖是準確率曲線。虛線是訓練集,實線是驗證集。我們看到,訓練集一路走高,但是驗證集在波動。雖然最后一步剛好是最高點。

看下面的圖,會更加清晰。

image

上圖是損失數(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ù)科學?》,里面還有更多的有趣問題及解法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內(nèi)容