語言模型 N-gram 模型
一般自然語言處理的傳統方法是將句子處理爲一個詞袋模型(Bag-of-Words,BoW),而不考慮每個詞的順序,比如用樸素貝葉斯算法進行垃圾郵件識別或者文本分類。
語言模型中的 N-gram 就是一種考慮句子中詞與詞之間的順序的模型。
N-gram 模型是一種語言模型(Language Model,LM),是一個基於概率的判別模型,它的輸入是一句話(詞的順序序列),輸出是這句話的概率,即這些詞的聯合概率(Joint Probability)。
使用 N-gram 語言模型思想,一般是需要知道當前詞以及前面的詞,因爲一個句子中每個詞的出現並不是獨立的。比如,如果第一個詞是“空氣”,接下來的詞是“很”,那麼下一個詞很大概率會是“新鮮”。類似於我們人的聯想,N-gram 模型知道的信息越多,得到的結果也越準確。
在前面課程中講解的文本分類中,我們曾用到基於 sklearn 的詞袋模型,嘗試加入抽取 2-gram
和 3-gram
的統計特徵,把詞庫的量放大,獲得更強的特徵。
通過 ngram_range 參數來控制,代碼如下:
from sklearn.feature_extraction.text import CountVectorizer
vec = CountVectorizer(
analyzer='word', # tokenise by character ngrams
ngram_range=(1,4), # use ngrams of size 1 and 2
max_features=20000, # keep the most common 1000 ngrams
)
N-gram 模型,在自然語言處理中主要應用在如詞性標註、垃圾短信分類、分詞器、機器翻譯和語音識別、語音識別等領域。
然而 N-gram 模型並不是完美的,它存在如下優缺點:
-
優點:包含了前 N-1 個詞所能提供的全部信息,這些詞對於當前詞的出現概率具有很強的約束力;
-
缺點:需要很大規模的訓練文本來確定模型的參數,當 N 很大時,模型的參數空間過大。所以常見的 N 值一般爲1,2,3等。還有因數據稀疏而導致的數據平滑問題,解決方法主要是拉普拉斯平滑和內插與回溯。
所以,根據 N-gram 的優缺點,它的進化版 NNLM(Neural Network based Language Model)誕生了。
NNLM 由 Bengio 在2003年提出,它是一個很簡單的模型,由四層組成,輸入層、嵌入層、隱層和輸出層,模型結構如下圖(來自百度圖片):
NNLM 接收的輸入是長度爲 N 的詞序列,輸出是下一個詞的類別。首先,輸入是詞序列的 index 序列,例如詞“我”在字典(大小爲|V|)中的 index 是10,詞“是”的 index 是23, “小明”的 index 是65,則句子“我是小明”的 index 序列就是 10、 23、65。嵌入層(Embedding)是一個大小爲 |V|×K
的矩陣,從中取出第10、23、65行向量拼成 3×K 的矩陣就是 Embedding 層的輸出了。隱層接受拼接後的 Embedding 層輸出作爲輸入,以 tanh 爲激活函數,最後送入帶 softmax 的輸出層,輸出概率。
NNLM 最大的缺點就是參數多,訓練慢,要求輸入定長 N 這一點很不靈活,同時不能利用完整的歷史信息。
因此,針對 NNLM 存在的問題,Mikolov 在2010年提出了 RNNLM,有興趣可以閱讀相關論文,其結構實際上是用 RNN 代替 NNLM 裏的隱層,這樣做的好處,包括減少模型參數、提高訓練速度、接受任意長度輸入、利用完整的歷史信息。同時,RNN 的引入意味着可以使用 RNN 的其他變體,像 LSTM、BLSTM、GRU 等等,從而在序列建模上進行更多更豐富的優化。
以上,從詞袋模型說起,引出語言模型 N-gram 以及其優化模型 NNLM 和 RNNLM,後續內容從 RNN 說起,來看看其變種 LSTM 和 GRU 模型如何處理類似序列數據。
RNN 以及變種 LSTM 和 GRU 原理
RNN 爲序列數據而生
RNN 稱爲循環神經網路,因爲這種網絡有“記憶性”,主要應用在自然語言處理(NLP)和語音領域。RNN 具體的表現形式爲網絡會對前面的信息進行記憶並應用於當前輸出的計算中,即隱藏層之間的節點不再無連接而是有連接的,並且隱藏層的輸入不僅包括輸入層的輸出還包括上一時刻隱藏層的輸出。
理論上,RNN 能夠對任何長度的序列數據進行處理,但由於該網絡結構存在“梯度消失”問題,所以在實際應用中,解決梯度消失的方法有:梯度裁剪(Clipping Gradient)和 LSTM(Long Short-Term Memory
)。
下圖是一個簡單的 RNN 經典結構:
RNN 包含輸入單元(Input Units),輸入集標記爲 \{x_0,x_1,...,x_t,x_t...\}{x0,x1,...,xt,xt...};輸出單元(Output Units)的輸出集則被標記爲 \{y_0,y_1,...,y_t,...\}{y0,y1,...,yt,...};RNN 還包含隱藏單元(Hidden Units),我們將其輸出集標記爲 \{h_0,h_1,...,h_t,...\}{h0,h1,...,ht,...},這些隱藏單元完成了最爲主要的工作。
LSTM 結構
LSTM 在1997年由“Hochreiter & Schmidhuber”提出,目前已經成爲 RNN 中的標準形式,用來解決上面提到的 RNN 模型存在“長期依賴”的問題。
LSTM 通過三個“門”結構來控制不同時刻的狀態和輸出。所謂的“門”結構就是使用了 Sigmoid 激活函數的全連接神經網絡和一個按位做乘法的操作,Sigmoid 激活函數會輸出一個0~1之間的數值,這個數值代表當前有多少信息能通過“門”,0表示任何信息都無法通過,1表示全部信息都可以通過。其中,“遺忘門”和“輸入門”是 LSTM 單元結構的核心。下面我們來詳細分析下三種“門”結構。
-
遺忘門,用來讓 LSTM“忘記”之前沒有用的信息。它會根據當前時刻節點的輸入 X_tXt、上一時刻節點的狀態 Ct−1Ct−1 和上一時刻節點的輸出 h_{t-1}ht−1 來決定哪些信息將被遺忘。
-
輸入門,LSTM 來決定當前輸入數據中哪些信息將被留下來。在 LSTM 使用遺忘門“忘記”部分信息後需要從當前的輸入留下最新的記憶。輸入門會根據當前時刻節點的輸入 X_tXt、上一時刻節點的狀態 C_{t-1}Ct−1 和上一時刻節點的輸出 h_{t-1}ht−1 來決定哪些信息將進入當前時刻節點的狀態 C_tCt,模型需要記憶這個最新的信息。
-
輸出門,LSTM 在得到最新節點狀態 C_tCt 後,結合上一時刻節點的輸出 h_{t-1}ht−1 和當前時刻節點的輸入 X_tXt 來決定當前時刻節點的輸出。
GRU 結構
GRU(Gated Recurrent Unit)是2014年提出來的新的 RNN 架構,它是簡化版的 LSTM。下面是 LSTM 和 GRU 的結構比較圖(來自於網絡):
在超參數均調優的前提下,效果和 LSTM 差不多,但是參數少了1/3,不容易過擬合。如果發現 LSTM 訓練出來的模型過擬合比較嚴重,可以試試 GRU。
實戰基於 Keras 的 LSTM 和 GRU 文本分類
整個過程包括:
- 語料加載
- 分詞和去停用詞
- 數據預處理
- 使用 LSTM 分類
- 使用 GRU 分類
使用 Keras 來快速構建和訓練模型
第一步,引入數據處理庫,停用詞和語料加載:
#引入包
import random
import jieba
import pandas as pd
#加載停用詞
stopwords=pd.read_csv('stopwords.txt',index_col=False,quoting=3,sep="\t",names=['stopword'], encoding='utf-8')
stopwords=stopwords['stopword'].values
#加載語料
laogong_df = pd.read_csv('beilaogongda.csv', encoding='utf-8', sep=',')
laopo_df = pd.read_csv('beilaogongda.csv', encoding='utf-8', sep=',')
erzi_df = pd.read_csv('beierzida.csv', encoding='utf-8', sep=',')
nver_df = pd.read_csv('beinverda.csv', encoding='utf-8', sep=',')
#刪除語料的nan行
laogong_df.dropna(inplace=True)
laopo_df.dropna(inplace=True)
erzi_df.dropna(inplace=True)
nver_df.dropna(inplace=True)
#轉換
laogong = laogong_df.segment.values.tolist()
laopo = laopo_df.segment.values.tolist()
erzi = erzi_df.segment.values.tolist()
nver = nver_df.segment.values.tolist()
第二步,分詞和去停用詞:
#定義分詞和打標籤函數preprocess_text
#參數content_lines即爲上面轉換的list
#參數sentences是定義的空list,用來儲存打標籤之後的數據
#參數category 是類型標籤
def preprocess_text(content_lines, sentences, category):
for line in content_lines:
try:
segs=jieba.lcut(line)
segs = [v for v in segs if not str(v).isdigit()]#去數字
segs = list(filter(lambda x:x.strip(), segs)) #去左右空格
segs = list(filter(lambda x:len(x)>1, segs))#長度爲1的字符
segs = list(filter(lambda x:x not in stopwords, segs)) #去掉停用詞
sentences.append((" ".join(segs), category))# 打標籤
except Exception:
print(line)
continue
#調用函數、生成訓練數據
sentences = []
preprocess_text(laogong, sentences,0)
preprocess_text(laopo, sentences, 1)
preprocess_text(erzi, sentences, 2)
preprocess_text(nver, sentences, 3)
第三步,先打散數據,使數據分佈均勻,然後獲取特徵和標籤列表:
#打散數據,生成更可靠的訓練集
random.shuffle(sentences)
#控制檯輸出前10條數據,觀察一下
for sentence in sentences[:10]:
print(sentence[0], sentence[1])
#所有特徵和對應標籤
all_texts = [ sentence[0] for sentence in sentences]
all_labels = [ sentence[1] for sentence in sentences]
第四步,使用 LSTM 對數據進行分類:
#引入需要的模塊
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
from keras.layers import Dense, Input, Flatten, Dropout
from keras.layers import LSTM, Embedding,GRU
from keras.models import Sequential
#預定義變量
MAX_SEQUENCE_LENGTH = 100 #最大序列長度
EMBEDDING_DIM = 200 #embdding 維度
VALIDATION_SPLIT = 0.16 #驗證集比例
TEST_SPLIT = 0.2 #測試集比例
#keras的sequence模塊文本序列填充
tokenizer = Tokenizer()
tokenizer.fit_on_texts(all_texts)
sequences = tokenizer.texts_to_sequences(all_texts)
word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))
data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH)
labels = to_categorical(np.asarray(all_labels))
print('Shape of data tensor:', data.shape)
print('Shape of label tensor:', labels.shape)
#數據切分
p1 = int(len(data)*(1-VALIDATION_SPLIT-TEST_SPLIT))
p2 = int(len(data)*(1-TEST_SPLIT))
x_train = data[:p1]
y_train = labels[:p1]
x_val = data[p1:p2]
y_val = labels[p1:p2]
x_test = data[p2:]
y_test = labels[p2:]
#LSTM訓練模型
model = Sequential()
model.add(Embedding(len(word_index) + 1, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH))
model.add(LSTM(200, dropout=0.2, recurrent_dropout=0.2))
model.add(Dropout(0.2))
model.add(Dense(64, activation='relu'))
model.add(Dense(labels.shape[1], activation='softmax'))
model.summary()
#模型編譯
model.compile(loss='categorical_crossentropy',
optimizer='rmsprop',
metrics=['acc'])
print(model.metrics_names)
model.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=10, batch_size=128)
model.save('lstm.h5')
#模型評估
print(model.evaluate(x_test, y_test))
第五步,使用 GRU 進行文本分類,上面就是完整的使用 LSTM 進行 文本分類,如果使用 GRU 只需要改變模型訓練部分:
model = Sequential()
model.add(Embedding(len(word_index) + 1, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH))
model.add(GRU(200, dropout=0.2, recurrent_dropout=0.2))
model.add(Dropout(0.2))
model.add(Dense(64, activation='relu'))
model.add(Dense(labels.shape[1], activation='softmax'))
model.summary()
model.compile(loss='categorical_crossentropy',
optimizer='rmsprop',
metrics=['acc'])
print(model.metrics_names)
model.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=10, batch_size=128)
model.save('lstm.h5')
print(model.evaluate(x_test, y_test))
zzzz