本文整理自筆者年前在知乎上的一個回答:

大數據輿情情感分析

,如何提取情感並使用什麼樣的工具?(貼情感標籤)

1、我將數據篩選預處理好,然後分好詞。2、是不是接下來應該與與情感辭彙本庫對照,生成結合詞頻和情感詞庫的情感關鍵詞庫。3、將信息與情感關鍵詞庫進行比對,對信息加以情感標記。4、我想問實現前三步,需要什麼工具的什麼功能呢?據說用spss和武漢大學的ROST WordParser。該如何使用呢?zhihu.com/question/3147

情感分析說白了,就是一個文本(多)分類問題,我看一般的情感分析都是2類(正負面)或者3類(正面、中性和負面)。其實,這種粒度是遠遠不夠的。本著「Talk is cheap, show you my code」的原則,我不扯鹹淡,直接上代碼給出解決方案(而且是經過真實文本數據驗證了的:我用一個14個分類的例子來講講各類文本分類模型---從傳統的機器學習文本分類模型到現今流行的基於深度學習

的文本分類模型,最後給出一個超NB的模型集成,效果最優。

**************************************前方高能****************************************

在這篇文章中,筆者將討論自然語言處理中文本分類的相關問題,將使用一個復旦大學開源的文本分類語料庫,對文本分類的一般流程和常用模型進行探討。

首先,筆者會創建一個非常基礎的初始模型,然後基於此使用不同的特徵進行改進。

接下來,筆者還將討論如何使用深度神經網路來解決NLP問題,並在文章末尾以一般關於集成的一些想法結束這篇文章。

本文覆蓋的NLP方法有:

  • TF-IDF
  • Count Features
  • Logistic Regression
  • Naive Bayes
  • SVM
  • Xgboost
  • Grid Search
  • Word Vectors
  • Dense Network
  • LSTM/BiLSTM
  • GRU
  • Ensembling

NOTE: 筆者並不能保證你學習了本文之後就能在NLP相關比賽中獲得非常高的分數。但是,如果你正確地「喫透」它,並根據實際情況適時作出一些調整,你可以獲得非常高的分數。

廢話不多說,先導入一些我將要使用的重要python模塊。

import pandas as pd
import numpy as np
import xgboost as xgb
from tqdm import tqdm
from sklearn.svm import SVC
from keras.models import Sequential
from keras.layers.recurrent import LSTM, GRU
from keras.layers.core import Dense, Activation, Dropout
from keras.layers.embeddings import Embedding
from keras.layers.normalization import BatchNormalization
from keras.utils import np_utils
from sklearn import preprocessing, decomposition, model_selection, metrics, pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from keras.layers import GlobalMaxPooling1D, Conv1D, MaxPooling1D, Flatten, Bidirectional, SpatialDropout1D
from keras.preprocessing import sequence, text
from keras.callbacks import EarlyStopping
from nltk import word_tokenize

接下來,載入並檢視數據集。

data=pd.read_excel(/home/kesci/input/Chinese_NLP6474/復旦大學中文文本分類語料.xlsx,sheet1)
data.head()

數據樣例

data.info()

數據概況

對文本數據的正文欄位進行分詞,這裡是在Linux上運行的,可以開啟jieba的並行分詞模式,分詞速度是平常的好多倍,具體看你的CPU核心數。

import jieba
jieba.enable_parallel(16) #並行分詞開啟
data[文本分詞] = data[正文].apply(lambda i:jieba.cut(i) )
data[文本分詞] =[ .join(i) for i in data[文本分詞]]

使用jieba進行分詞,簡單示範而已,後期需要優化

值得注意的是,分詞是任何中文文本分類的起點,分詞的質量會直接影響到後面的模型效果。在這裡,作為演示,筆者有點偷懶,其實你還可以:

  • 設置可靠的自定義詞典,以便分詞更精準;
  • 採用分詞效果更好的分詞器,如pyltp、THULAC、Hanlp等;
  • 編寫預處理類,就像下面要談到的數字特徵歸一化,去掉文本中的#@¥%……&等等。

data.分類.unique()

文本類別

data.head()

分詞後的數據構成

這是一個典型的文本多分類問題,需要將文本劃分到給定的14個主題上。

針對該問題,筆者採用了kaggle上通用的 Multi-Class Log-Loss 作為評測指標(Evaluation Metric).

def multiclass_logloss(actual, predicted, eps=1e-15):
"""對數損失度量(Logarithmic Loss Metric)的多分類版本。
:param actual: 包含actual target classes的數組
:param predicted: 分類預測結果矩陣, 每個類別都有一個概率
"""
# Convert actual to a binary array if its not already:
if len(actual.shape) == 1:
actual2 = np.zeros((actual.shape[0], predicted.shape[1]))
for i, val in enumerate(actual):
actual2[i, val] = 1
actual = actual2

clip = np.clip(predicted, eps, 1 - eps)
rows = actual.shape[0]
vsota = np.sum(actual * np.log(clip))
return -1.0 / rows * vsota

接下來用scikit-learn中的Label Encoder將文本標籤(Text Label)轉化為數字(Integer)

lbl_enc = preprocessing.LabelEncoder()
y = lbl_enc.fit_transform(data.分類.values)

在進一步研究之前,我們必須將數據分成訓練和驗證集。 我們可以使用scikit-learn的model_selection模塊中的train_test_split來完成它。

xtrain, xvalid, ytrain, yvalid = train_test_split(data.文本分詞.values, y,
stratify=y,
random_state=42,
test_size=0.1, shuffle=True)

print (xtrain.shape)
print (xvalid.shape)

(8324,) (925,)

構建基礎模型(Basic Models)

讓我們先創建一個非常基礎的模型。

這個非常基礎的模型(very first model)基於 TF-IDF (Term Frequency - Inverse Document Frequency)+邏輯斯底回歸(Logistic Regression)。

筆者將scikit-learn中的TfidfVectorizer類稍稍改寫下,以便將文本中的數字特徵統一表示成"#NUMBER",達到一定的降噪效果。

def number_normalizer(tokens):
""" 將所有數字標記映射為一個佔位符(Placeholder)。
對於許多實際應用場景來說,以數字開頭的tokens不是很有用,
但這樣tokens的存在也有一定相關性。 通過將所有數字都表示成同一個符號,可以達到降維的目的。
"""
return ("#NUMBER" if token[0].isdigit() else token for token in tokens)

class NumberNormalizingVectorizer(TfidfVectorizer):
def build_tokenizer(self):
tokenize = super(NumberNormalizingVectorizer, self).build_tokenizer()
return lambda doc: list(number_normalizer(tokenize(doc)))

利用剛才創建的Number Normalizing Vectorizer類來提取文本特徵,注意裡面各類參數的含義,自己去sklearn官方網站找教程看。

stwlist=[line.strip() for line in open(/home/gaochangkuan/input/stopwords7085/停用辭彙總.txt,
r,encoding=utf-8).readlines()]
tfv = NumberNormalizingVectorizer(min_df=3,
max_df=0.5,
max_features=None,
ngram_range=(1, 2),
use_idf=True,
smooth_idf=True,
stop_words = stwlist)

使用TF-IDF來擬合訓練集和測試集(半監督學習):

tfv.fit(list(xtrain) + list(xvalid))
xtrain_tfv = tfv.transform(xtrain)
xvalid_tfv = tfv.transform(xvalid)

利用提取的TFIDF特徵來擬合一個簡單的Logistic Regression。

clf = LogisticRegression(C=1.0,solver=lbfgs,multi_class=multinomial)
clf.fit(xtrain_tfv, ytrain)
predictions = clf.predict_proba(xvalid_tfv)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.627

做完第一個基礎模型後,得出的 multiclass logloss 是0.627.

但筆者「貪婪」,想要獲得更好的分數。 基於相同模型採用不同的特徵,再看看結果如何。

我們也可以使用辭彙計數(Word Counts)作為功能,而不是使用TF-IDF。 這可以使用scikit-learn中的CountVectorizer輕鬆完成。

ctv = CountVectorizer(min_df=3,
max_df=0.5,
ngram_range=(1,2),
stop_words = stwlist)

使用Count Vectorizer來擬合訓練集和測試集(半監督學習)

ctv.fit(list(xtrain) + list(xvalid))
xtrain_ctv = ctv.transform(xtrain)
xvalid_ctv = ctv.transform(xvalid)

利用提取的word counts特徵來擬合一個簡單的Logistic Regression .

clf = LogisticRegression(C=1.0,solver=lbfgs,multi_class=multinomial)
clf.fit(xtrain_ctv, ytrain)
predictions = clf.predict_proba(xvalid_ctv)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.732

比之前者,效果略差。。。

接下來,讓我們嘗試一個非常簡單的模型- 樸素貝葉斯,它在以前是非常有名的。 讓我們看看當我們在這個數據集上使用樸素貝葉時會發生什麼:

利用提取的TFIDF特徵來擬合Naive Bayes:

clf = MultinomialNB()
clf.fit(xtrain_tfv, ytrain)

predictions = clf.predict_proba(xvalid_tfv)
print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.982

效果更差。。。。。。 樸素貝葉斯模型的表現不怎麼樣! 但基於辭彙計數的邏輯回歸的效果仍然很棒! 當我們在基於辭彙計數的基礎上使用樸素貝葉斯模型時會發生什麼?

利用提取的word counts特徵來擬合Naive Bayes:

clf = MultinomialNB()
clf.fit(xtrain_ctv, ytrain)
predictions = clf.predict_proba(xvalid_ctv)
print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 3.780

。。。。。。

這次效果感覺不咋樣。 傳統文本分類演算法裏還有一個名叫支持向量機(SVM)。 SVM曾是很多機器學習愛好者的「最愛」。 因此,我們必須在此數據集上嘗試SVM。 由於SVM需要花費大量時間,因此在應用SVM之前,我們將使用奇異值分解(Singular Value Decomposition )來減少TF-IDF中的特徵數量。 同時,在使用SVM之前,我們還需要將數據標準化(Standardize Data )。

使用SVD進行降維,components設為120,對於SVM來說,SVD的components的合適調整區間一般為120~200 。

svd = decomposition.TruncatedSVD(n_components=120)
svd.fit(xtrain_tfv)
xtrain_svd = svd.transform(xtrain_tfv)
xvalid_svd = svd.transform(xvalid_tfv)

對從SVD獲得的數據進行縮放:

scl = preprocessing.StandardScaler()
scl.fit(xtrain_svd)
xtrain_svd_scl = scl.transform(xtrain_svd)
xvalid_svd_scl = scl.transform(xvalid_svd)

現在是時候應用SVM模型進行文本分類了。 在運行以下單元格後,你可以去喝杯茶了---因為這將耗費大量的時間...

調用下SVM模型:

clf = SVC(C=1.0, probability=True) # since we need probabilities
clf.fit(xtrain_svd_scl, ytrain)
predictions = clf.predict_proba(xvalid_svd_scl)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

llogloss: 0.347

看起來,SVM在這些數據上表現還行! 在採用更高級的演算法前,讓我們再試試Kaggle上應用最流行的演算法:xgboost!

基於tf-idf特徵,使用xgboost

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8,
subsample=0.8, nthread=10, learning_rate=0.1)
clf.fit(xtrain_tfv.tocsc(), ytrain)
predictions = clf.predict_proba(xvalid_tfv.tocsc())

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.182

效果不錯,比SVM還牛呢!

基於word counts特徵,使用xgboost:

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8,
subsample=0.8, nthread=10, learning_rate=0.1)
clf.fit(xtrain_ctv.tocsc(), ytrain)
predictions = clf.predict_proba(xvalid_ctv.tocsc())

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.154

基於tf-idf的svd特徵,使用xgboost

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8,
subsample=0.8, nthread=10, learning_rate=0.1)
clf.fit(xtrain_svd, ytrain)
predictions = clf.predict_proba(xvalid_svd)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.394

再對經過數據標準化(Scaling)的tf-idf-svd特徵使用xgboost

clf = xgb.XGBClassifier(nthread=10)
clf.fit(xtrain_svd_scl, ytrain)
predictions = clf.predict_proba(xvalid_svd_scl)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.373

XGBoost的效果似乎挺棒的! 但我覺得還可以進一步優化,因為我還沒有做過任何超參數優化。 我很懶,所以我會告訴你該怎麼做,你可以自己做!)。 這將在下一節中討論:

網格搜索(Grid Search)

網格搜索是一種超參數優化的技巧。 如果知道這個技巧,你可以通過獲取最優的參數組合來產生良好的文本分類效果。

在本節中,我將討論使用基於邏輯回歸模型的網格搜索。

在開始網格搜索之前,我們需要創建一個評分函數,這可以通過scikit-learn的make_scorer函數完成的。

mll_scorer = metrics.make_scorer(multiclass_logloss,
greater_is_better=False, needs_proba=True)

接下來,我們需要一個pipeline。 為了演示,我將使用由SVD(進行特徵縮放)和邏輯回歸模型組成的pipeline。

#SVD初始化
svd = TruncatedSVD()

# Standard Scaler初始化
scl = preprocessing.StandardScaler()

# 再一次使用Logistic Regression
lr_model = LogisticRegression()

# 創建pipeline
clf = pipeline.Pipeline([(svd, svd),
(scl, scl),
(lr, lr_model)])

接下來我們需要一個參數網格(A Grid of Parameters):

param_grid = {svd__n_components : [120, 180],
lr__C: [0.1, 1.0, 10],
lr__penalty: [l1, l2]}

因此,對於SVD,我們評估120和180個分量(Components),對於邏輯回歸,我們評估三個不同的學習率C值,其中懲罰函數為l1和l2。 現在,我們可以開始對這些參數進行網格搜索咯。

網格搜索模型(Grid Search Model)初始化:

model = GridSearchCV(estimator=clf, param_grid=param_grid, scoring=mll_scorer,
verbose=10, n_jobs=-1, iid=True, refit=True, cv=2)

擬合網格搜索模型:

model.fit(xtrain_tfv, ytrain) #為了減少計算量,這裡我們僅使用xtrain
print("Best score: %0.3f" % model.best_score_)
print("Best parameters set:")
best_parameters = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print(" %s: %r" % (param_name, best_parameters[param_name]))

logloss: 0.377

nb_model = MultinomialNB()

創建pipeline :

clf = pipeline.Pipeline([(nb, nb_model)])

搜索參數設置:

param_grid = {nb__alpha: [0.001, 0.01, 0.1, 1, 10, 100]}

網格搜索模型(Grid Search Model)初始化:

model = GridSearchCV(estimator=clf, param_grid=param_grid, scoring=mll_scorer,
verbose=10, n_jobs=-1, iid=True, refit=True, cv=2)

擬合網格搜索模型:

model.fit(xtrain_tfv, ytrain) # 為了減少計算量,這裡我們僅使用xtrain
print("Best score: %0.3f" % model.best_score_)
print("Best parameters set:")
best_parameters = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print(" %s: %r" % (param_name, best_parameters[param_name]))

自從2013年穀歌的Tomas Mikolov團隊發明瞭word2vec以後,word2vec就成為了處理NLP問題的標配。word2vec訓練向量空間模型的速度比以往的方法都快。許多新興的詞嵌入基於人工神經網路,而不是過去的n元語法模型和非監督式學習。

接下來,讓我們來深入研究一下如何使用word2vec來進行NLP文本分類。

基於word2vec的詞嵌入

在不深入細節的情況下,筆者將解釋如何創建語句向量(Sentence Vectors),以及如何基於它們在其上創建機器學習模型。鄙人是GloVe向量,word2vec和fasttext的粉絲(但平時還是用word2vec較多)。在這篇文章中,筆者使用的文本分類模型是基於Word2vec詞向量模型(100維)。

訓練word2vec詞向量:

import gensim
model = gensim.models.Word2Vec(X, size=100)

X是經分詞後的文本構成的list,也就是tokens的列表的列表。

注意,Word2Vec還有3個值得關注的參數,iter是模型訓練時迭代的次數,假如參與訓練的文本量較少,就需要把這個參數調大一些;sg是模型訓練演算法的類別,1 代表 skip-gram,;0代表 CBOW;window控制窗口,它指當前詞和預測詞之間的最大距離,如果設得較小,那麼模型學習到的是辭彙間的功能性特徵(詞性相異),如果設置得較大,會學習到辭彙之間的相似性特徵(詞性相同)的大小,假如語料夠多,筆者一般會設置得大一些,8~10。

embeddings_index = dict(zip(model.wv.index2word, model.wv.syn0))

print(Found %s word vectors. % len(embeddings_index))

Found 56,000word vectors

該函數會將語句轉化為一個標準化的向量(Normalized Vector):

def sent2vec(s):
words = str(s).lower()
words = word_tokenize(words)
words = [w for w in words if not w in stop_words]
words = [w for w in words if w.isalpha()]
M = []
for w in words:
try:
M.append(embeddings_index[w])
except:
continue
M = np.array(M)
v = M.sum(axis=0)
if type(v) != np.ndarray:
return np.zeros(300)
return v / np.sqrt((v ** 2).sum())

對訓練集和驗證集使用上述函數,進行文本向量化處理

xtrain_w2v = [sent2vec(x) for x in tqdm(xtrain)]
xvalid_w2v = [sent2vec(x) for x in tqdm(xvalid)]

xtrain_w2v = np.array(xtrain_w2v)
xvalid_w2v = np.array(xvalid_w2v)

讓我們看看xgboost在Glove詞向量特徵的表現如何:

基於word2vec特徵使用XGB文本分類器:

clf = xgb.XGBClassifier(nthread=10, silent=False)
clf.fit(xtrain_w2v, ytrain)
predictions = clf.predict_proba(xvalid_w2v)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.389

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8,
subsample=0.8, nthread=10, learning_rate=0.1, silent=False)
clf.fit(xtrain_w2v, ytrain)
predictions = clf.predict_proba(xvalid_w2v)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.122

我們可以看到,簡單的對參數進行微調,就提高基於word2vec詞向量特徵的xgboost得分! 相信我,你還可以從中繼續「壓榨」出更優秀的表現!

深度學習(Deep Learning)

這是一個深度學習大行其道的時代!它的存在使大家擺脫了文本特徵手動抽取的麻煩,文本分類問題也在它的指引下獲得突飛猛進的發展!

在這裡,我們將在word2vec功能上訓練LSTM和簡單的全連接網路(Dense Network)。讓我們先從全連接網路開始~

在使用神經網路前,先對數據進行縮放:

scl = preprocessing.StandardScaler()
xtrain_w2v_scl = scl.fit_transform(xtrain_w2v)
xvalid_w2v_scl = scl.transform(xvalid_w2v)

對標籤進行binarize處理

ytrain_enc = np_utils.to_categorical(ytrain)
yvalid_enc = np_utils.to_categorical(yvalid)

創建1個3層的序列神經網路(Sequential Neural Net)

model = Sequential()

model.add(Dense(300, input_dim=300, activation=relu))
model.add(Dropout(0.2))
model.add(BatchNormalization())

model.add(Dense(300, activation=relu))
model.add(Dropout(0.3))
model.add(BatchNormalization())

model.add(Dense(14))
model.add(Activation(softmax))

對模型進行編譯和擬合:

model.compile(loss=categorical_crossentropy, optimizer=adam)

model.fit(xtrain_w2v_scl, y=ytrain_enc, batch_size=64,
epochs=5, verbose=1,
validation_data=(xvalid_v_scl, yvalid_enc))

logloss: 0.422

你需要不斷的對神經網路的參數進行調優,添加更多層,增加Dropout以獲得更好的結果。 在這裡,筆者只是簡單的實現下,追求速度而不是最終效果,並且它比沒有任何優化的xgboost取得了更好的結果:)

為了更進一步,筆者使用LSTM,我們需要對文本數據進行Tokenize:

token = text.Tokenizer(num_words=None)
max_len = 70

token.fit_on_texts(list(xtrain) + list(xvalid))
xtrain_seq = token.texts_to_sequences(xtrain)
xvalid_seq = token.texts_to_sequences(xvalid)

對文本序列進行zero填充:

xtrain_pad = sequence.pad_sequences(xtrain_seq, maxlen=max_len)
xvalid_pad = sequence.pad_sequences(xvalid_seq, maxlen=max_len)

word_index = token.word_index

基於已有的數據集中的辭彙創建一個詞嵌入矩陣(Embedding Matrix):

embedding_matrix = np.zeros((len(word_index) + 1, 100))
for word, i in tqdm(word_index.items()):
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector

基於前面訓練的Word2vec詞向量,使用1個兩層的LSTM模型:

model = Sequential()
model.add(Embedding(len(word_index) + 1,
100,
weights=[embedding_matrix],
input_length=max_len,
trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(LSTM(100, dropout=0.3, recurrent_dropout=0.3))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation(softmax))
model.compile(loss=categorical_crossentropy, optimizer=adam)

model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100, verbose=1, validation_data=(xvalid_pad, yvalid_enc))

logloss: 0.312

現在,我們看到分數小於0.5。我跑了很多個epochs都沒有獲得最優的結果,但我們可以使用early stopping來停止在最佳的迭代節點。

那我們該如何使用early stopping?

好吧,其實很簡單的。讓我們再次compile模型(基於前面訓練的Word2vec詞向量,使用1個兩層的LSTM模型):

#基於前面訓練的Word2vec詞向量,使用1個兩層的LSTM模型
model = Sequential()
model.add(Embedding(len(word_index) + 1,
100,
weights=[embedding_matrix],
input_length=max_len,
trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(LSTM(100, dropout=0.3, recurrent_dropout=0.3))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation(softmax))
model.compile(loss=categorical_crossentropy, optimizer=adam)

在模型擬合時,使用early stopping這個回調函數(Callback Function):

earlystop = EarlyStopping(monitor=val_loss, min_delta=0, patience=3, verbose=0, mode=auto)
model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100,
verbose=1, validation_data=(xvalid_pad, yvalid_enc), callbacks=[earlystop])

logloss: 0.487

一個可能的問題是:為什麼我會使用這麼多的dropout? 嗯,fit模型時,沒有或很少的dropout,你會出現過擬合(Overfit):)

讓我們看看雙向長短時記憶(Bi-Directional LSTM)是否可以給我們帶來更好的結果。 對於Keras來說,使用Bilstm小菜一碟:)

基於前面訓練的Word2vec詞向量,構建1個2層的Bidirectional LSTM :

model = Sequential()
model.add(Embedding(len(word_index) + 1,
100,
weights=[embedding_matrix],
input_length=max_len,
trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(Bidirectional(LSTM(100, dropout=0.3, recurrent_dropout=0.3)))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation(softmax))
model.compile(loss=categorical_crossentropy, optimizer=adam)

在模型擬合時,使用early stopping這個回調函數(Callback Function)

earlystop = EarlyStopping(monitor=val_loss, min_delta=0, patience=3, verbose=0, mode=auto)
model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100,
verbose=1, validation_data=(xvalid_pad, yvalid_enc), callbacks=[earlystop])

logloss: 0.119

很接近最優結果了! 讓我們嘗試兩層的GRU:

# 基於前面訓練的Word2vec詞向量,構建1個2層的GRU模型
model = Sequential()
model.add(Embedding(len(word_index) + 1,
100,
weights=[embedding_matrix],
input_length=max_len,
trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(GRU(100, dropout=0.3, recurrent_dropout=0.3, return_sequences=True))
model.add(GRU(100, dropout=0.3, recurrent_dropout=0.3))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(1024, activation=relu))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation(softmax))
model.compile(loss=categorical_crossentropy, optimizer=adam)

模型擬合時,使用early stopping這個回調函數(Callback Function)

earlystop = EarlyStopping(monitor=val_loss, min_delta=0, patience=3, verbose=0, mode=auto)
model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100,
verbose=1, validation_data=(xvalid_pad, yvalid_enc), callbacks=[earlystop])

logloss: 0.107

太好了! 比我們以前的模型好多了! 持續優化,模型的性能將不斷提高。

在文本分類的比賽中,想要獲得最高分,你應該擁有1個合成的模型。 讓我們來看看吧!

模型集成(Model Ensembling)

集多個文本分類模型之長,合成一個很棒的分類融合模型。

#創建一個Ensembling主類,具體使用方法見下一個cell
import numpy as np
from sklearn.metrics import roc_auc_score
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import StratifiedKFold, KFold
import pandas as pd
import os
import sys
import logging

logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)s] %(levelname)s %(message)s",
datefmt="%H:%M:%S", stream=sys.stdout)
logger = logging.getLogger(__name__)

再來編寫主類:

class Ensembler(object):
def __init__(self, model_dict, num_folds=3, task_type=classification, optimize=roc_auc_score,
lower_is_better=False, save_path=None):
"""
Ensembler init function
:param model_dict: 模型字典
:param num_folds: ensembling所用的fold數量
:param task_type: 分類(classification) 還是回歸(regression)
:param optimize: 優化函數,比如 AUC, logloss, F1等,必須有2個函數,即y_test 和 y_pred
:param lower_is_better: 優化函數(Optimization Function)的值越低越好還是越高越好
:param save_path: 模型保存路徑
"""

self.model_dict = model_dict
self.levels = len(self.model_dict)
self.num_folds = num_folds
self.task_type = task_type
self.optimize = optimize
self.lower_is_better = lower_is_better
self.save_path = save_path

self.training_data = None
self.test_data = None
self.y = None
self.lbl_enc = None
self.y_enc = None
self.train_prediction_dict = None
self.test_prediction_dict = None
self.num_classes = None

def fit(self, training_data, y, lentrain):
"""
:param training_data: 二維表格形式的訓練數據
:param y: 二進位的, 多分類或回歸
:return: 用於預測的模型鏈(Chain of Models)

"""

self.training_data = training_data
self.y = y

if self.task_type == classification:
self.num_classes = len(np.unique(self.y))
logger.info("Found %d classes", self.num_classes)
self.lbl_enc = LabelEncoder()
self.y_enc = self.lbl_enc.fit_transform(self.y)
kf = StratifiedKFold(n_splits=self.num_folds)
train_prediction_shape = (lentrain, self.num_classes)
else:
self.num_classes = -1
self.y_enc = self.y
kf = KFold(n_splits=self.num_folds)
train_prediction_shape = (lentrain, 1)

self.train_prediction_dict = {}
for level in range(self.levels):
self.train_prediction_dict[level] = np.zeros((train_prediction_shape[0],
train_prediction_shape[1] * len(self.model_dict[level])))

for level in range(self.levels):

if level == 0:
temp_train = self.training_data
else:
temp_train = self.train_prediction_dict[level - 1]

for model_num, model in enumerate(self.model_dict[level]):
validation_scores = []
foldnum = 1
for train_index, valid_index in kf.split(self.train_prediction_dict[0], self.y_enc):
logger.info("Training Level %d Fold # %d. Model # %d", level, foldnum, model_num)

if level != 0:
l_training_data = temp_train[train_index]
l_validation_data = temp_train[valid_index]
model.fit(l_training_data, self.y_enc[train_index])
else:
l0_training_data = temp_train[0][model_num]
if type(l0_training_data) == list:
l_training_data = [x[train_index] for x in l0_training_data]
l_validation_data = [x[valid_index] for x in l0_training_data]
else:
l_training_data = l0_training_data[train_index]
l_validation_data = l0_training_data[valid_index]
model.fit(l_training_data, self.y_enc[train_index])

logger.info("Predicting Level %d. Fold # %d. Model # %d", level, foldnum, model_num)

if self.task_type == classification:
temp_train_predictions = model.predict_proba(l_validation_data)
self.train_prediction_dict[level][valid_index,
(model_num * self.num_classes):(model_num * self.num_classes) +
self.num_classes] = temp_train_predictions

else:
temp_train_predictions = model.predict(l_validation_data)
self.train_prediction_dict[level][valid_index, model_num] = temp_train_predictions
validation_score = self.optimize(self.y_enc[valid_index], temp_train_predictions)
validation_scores.append(validation_score)
logger.info("Level %d. Fold # %d. Model # %d. Validation Score = %f", level, foldnum, model_num,
validation_score)
foldnum += 1
avg_score = np.mean(validation_scores)
std_score = np.std(validation_scores)
logger.info("Level %d. Model # %d. Mean Score = %f. Std Dev = %f", level, model_num,
avg_score, std_score)

logger.info("Saving predictions for level # %d", level)
train_predictions_df = pd.DataFrame(self.train_prediction_dict[level])
train_predictions_df.to_csv(os.path.join(self.save_path, "train_predictions_level_" + str(level) + ".csv"),
index=False, header=None)

return self.train_prediction_dict

def predict(self, test_data, lentest):
self.test_data = test_data
if self.task_type == classification:
test_prediction_shape = (lentest, self.num_classes)
else:
test_prediction_shape = (lentest, 1)

self.test_prediction_dict = {}
for level in range(self.levels):
self.test_prediction_dict[level] = np.zeros((test_prediction_shape[0],
test_prediction_shape[1] * len(self.model_dict[level])))
self.test_data = test_data
for level in range(self.levels):
if level == 0:
temp_train = self.training_data
temp_test = self.test_data
else:
temp_train = self.train_prediction_dict[level - 1]
temp_test = self.test_prediction_dict[level - 1]

for model_num, model in enumerate(self.model_dict[level]):

logger.info("Training Fulldata Level %d. Model # %d", level, model_num)
if level == 0:
model.fit(temp_train[0][model_num], self.y_enc)
else:
model.fit(temp_train, self.y_enc)

logger.info("Predicting Test Level %d. Model # %d", level, model_num)

if self.task_type == classification:
if level == 0:
temp_test_predictions = model.predict_proba(temp_test[0][model_num])
else:
temp_test_predictions = model.predict_proba(temp_test)
self.test_prediction_dict[level][:, (model_num * self.num_classes): (model_num * self.num_classes) +
self.num_classes] = temp_test_predictions

else:
if level == 0:
temp_test_predictions = model.predict(temp_test[0][model_num])
else:
temp_test_predictions = model.predict(temp_test)
self.test_prediction_dict[level][:, model_num] = temp_test_predictions

test_predictions_df = pd.DataFrame(self.test_prediction_dict[level])
test_predictions_df.to_csv(os.path.join(self.save_path, "test_predictions_level_" + str(level) + ".csv"),
index=False, header=None)

return self.test_prediction_dict

# specify the data to be used for every level of ensembling:
train_data_dict = {0: [xtrain_tfv, xtrain_ctv, xtrain_tfv, xtrain_ctv], 1: [xtrain_glove]}
test_data_dict = {0: [xvalid_tfv, xvalid_ctv, xvalid_tfv, xvalid_ctv], 1: [xvalid_glove]}

model_dict = {0: [LogisticRegression(), LogisticRegression(), MultinomialNB(alpha=0.1), MultinomialNB()],

1: [xgb.XGBClassifier(silent=True, n_estimators=120, max_depth=7)]}

為每個level的集成指定使用數據:

ens = Ensembler(model_dict=model_dict, num_folds=3, task_type=classification,
optimize=multiclass_logloss, lower_is_better=True, save_path=)
ens.fit(train_data_dict, ytrain, lentrain=xtrain_w2v.shape[0])
preds = ens.predict(test_data_dict, lentest=xvalid_w2v.shape[0])

檢視損失率:

multiclass_logloss(yvalid, preds[1])

logloss: 0.09

因此,我們看到集成模型在很大程度上提高了分數!但要注意,集成模型只有在參與集成的模型勢均力敵 - 表現都不差的情況下才能取得良好的效果,不然會出現拖後腿的情況,導致模型的整體性能還不如單個模型的要好~

由於本文只是一個教程,更多的技術細節還沒有深入下去,對此,你可以利用空餘時間多多優化下,也可以嘗試其他方法,比如:

  • 基於CNN的文本分類,達到的效果類似於N-gram,效率奇高
  • 基於Attention機制的BiLSTM、Hierarchical LSTM等
  • 基於ELMO、BERT等預訓練模型來提取高質量的文本特徵,再餵給分類器

...

以上就是筆者的分享,希望大家喜歡,也希望大家踴躍留言,發表看法和意見,我會持續更新的。

Note:需要訓練語料的朋友請關注我的公眾號【Social Listening與文本挖掘】,在後臺回復 「語料」即可得到訓練語料的下載鏈接。

筆者在和鯨(科賽)上的notebook附加資料 :

  1. 基於attention的情感分析,kesci.com/home/project/
  2. 【NLP文本表示】如何科學的在Tensorflow裏使用詞嵌入 ,kesci.com/home/project/
  3. 基於Position_Embedding和 Attention機制進行文本分類,kesci.com/home/project/
  4. 【BERT-至今最強大的NLP大殺器!】基於BERT的文本分類,kesci.com/home/project/
  5. NLP分析利器】利用Foolnltk進行自然語言處理,kesci.com/home/project/
  6. 文本挖掘】基於DBSCAN的文本聚類,kesci.com/home/project/

推薦閱讀:

相關文章