數據科學競賽-文本分類

文本分類

簡介

這是達觀在2018年舉辦的一個文本分類比賽,是一場經典的NLP比賽,關於NLP賽的思路在之前的博客中提到過,目前這場比賽已經結束,但是仍舊可以在DC上提交成績,作爲一個demo的比賽了。本文將簡要對該比賽的思路進行介紹,採用傳統方法和深度方法提交baseline模型。

數據探索

數據集可以直接到官網下載,下載後解壓文件可以得到訓練集和測試集,均爲CSV格式的表格文件,可以採用Pandas進行讀取和分析。

在這裏插入圖片描述

對訓練集的數據進行初步探索,結果如下。其中id爲文本標識碼,article爲文本字表示(每個數字對應一個漢字,不知道字表無法還原,這是爲了保護文本中的隱私信息,稱爲脫敏操作,該操作不允許建模預測等),word_seg是文本的詞表示,每個數字編號了一個單詞,class爲該文本的類別標籤號。

在這裏插入圖片描述

爲了後面模型的設計,有必要知道文本的長度範圍是什麼(下圖探索詞長度,即一個文本多少個詞)。文本平均含有716個單詞,是一個典型的長文本分類。

在這裏插入圖片描述

對詞信息進行計數統計,結果如下。高頻詞出現了500萬次,總共有875129個詞。當然,低頻詞也有很多,這裏不多說明,具體見文末Github地址。

在這裏插入圖片描述

對字的處理類似上面對於詞的處理,不多說明,不過在NLP中詞的信息遠遠多於字的信息量。

最後,可以看看標籤的分佈,可以使用Pandas接口輕易完成。

在這裏插入圖片描述

經過數據探索,不難發現這是一個長文本分類問題,且樣本類別分佈不均衡,同時詞量特別大,會導致特徵數量很多。

Pipeline制定(傳統方法baseline)

在這一部分會進行數據集的特徵工程,構建模型,預測提交結果。

特徵工程

首先採用傳統方法提取文本特徵,分別採用TFIDF和N-Gram,這兩種方法都是在NLP中比較傳統的基於統計的文本特徵提取思路,其原理這裏不多贅述,這裏使用sklearn中封裝好的API,其具體使用可以參考scikit-learn的官方文檔。

使用下面的代碼生成每個文本的特徵向量,生成的特徵向量是稀疏矩陣,scikit-learn中模型支持稀疏矩陣的輸入。

from sklearn.feature_extraction.text import TfidfVectorizer

word_vec = TfidfVectorizer(analyzer='word',
            ngram_range=(1,2),
            min_df=3,  # 低頻詞
            max_df=0.9,  # 高頻詞
            use_idf=True,
            smooth_idf=True, 
            sublinear_tf=True)

train_doc = word_vec.fit_transform(df_train['word_seg'])
test_doc = word_vec.transform(df_test['word_seg'])

模型構建

這一部分先是採用最基本的機器學習分類模型—邏輯迴歸進行模型的訓練及測試集的預測。

from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(C=4) 
clf.fit(train_doc, df_train['label'])
test_prob = clf.predict_proba(test_doc)
test_pred = np.argmax(test_prob, axis=1)
df_test['class'] = lb.inverse_transform(test_pred)
df_test[["id","class"]].to_csv("submission.csv", index=False, header=True, encoding='utf-8')

這個baseline的提交成績如下圖,採用的metric是F1得分,這個成績的排名是648位。

在這裏插入圖片描述

優化思路

後續的優化都是基於上面的baseline進行的,本部分具體代碼見文末Github。

首先,上述的特徵工程均只使用了詞的信息,沒有使用字的信息,可以使用字的特徵組合詞的特徵從而達到充分利用數據的目的,這裏簡單的將兩種特徵向量橫向堆疊,這樣會出現特徵維度太高的問題,需要進行降維。

接着,邏輯迴歸畢竟只是一個基本的分類模型,其實可以使用更強的集成模型如LightGBM、XGBoost等,這裏使用LightGBM進行建模(關於LGB調參技巧本文不多說明)。

上述的思路其實有一個問題,我們始終沒有在線下得到模型的效果(事實上,正規的比賽對提交次數都是有限制的,不可能優化一次代碼就提交一次觀察線上得分變化,這會造成部分人的刷分,必須在線下進行模型評估。),通常,我們採用構建驗證集的方法在線下進行模型評估(k折交叉驗證得到的驗證得分更加合適,但是資源消耗大)。

這一部分主要是使用集成模型且進行交叉驗證,得到平均的預測結果。當然,這部分需要大量人工的特徵工程和模型調參,後面會介紹效果更佳顯著的深度學習方法。

Pipeline制定(深度方法)

在這一部分不會過度強調特徵工程、模型等步驟,因爲在深度學習方法中主要目的是構建端到端的一個應用系統(如文本類別識別系統)。

數據準備

首先,將文本轉化爲序列(分詞),這個過程會建立詞表,這樣每個文本變爲了一個序列。(模型期待輸入是固定的維度所以對不等長文本需要進行截斷和補全,截取或者補全後的序列長度視情況而定)

同時,需要對標籤進行onehot編碼以便於使用softmax進行輸出層激活且計算loss。

文本特徵

詞有很多表示方法,最簡單的是onehot編碼單詞,每個單詞就是其對應的onehot編碼,但是onehot有一個顯著的特徵—無法衡量不同單詞之間的距離(因爲距離都是相同的)且維度很高。後來提出的Word2Vec方法可以構造低維有距的詞向量,它的產生有兩種策略分別爲CBOW和Skip-gram,具體的理論這裏不多贅述。

這裏將輸入的文本詞序列輸入模型構建詞嵌入(word embedding),利用詞表和詞向量構建詞嵌入可以大幅減少內存消耗。

模型構建

下面構建整個深度模型,使用雙向GRU+池化層構建網絡,全連接層作爲分類器。

def build_model(sequence_length, embedding_weight, class_num):
    content = Input(shape=(sequence_length, ), dtype='int32')
    embedding = Embedding(
        name='word_embedding',
        input_dim=embedding_weight.shape[0],
        weights=[embedding_weight],
        output_dim=embedding_weight.shape[1],
        trainable=False
    )
    x = SpatialDropout1D(0.2)(embedding(content))
    x = Bidirectional(GRU(200, return_sequences=True))(x)
    x = Bidirectional(GRU(200, return_sequences=True))(x)
    
    avg_pool = GlobalAveragePooling1D()(x)
    max_pool = GlobalMaxPooling1D()(x)
    conc = concatenate([avg_pool, max_pool])
    x = Dense(1000)(conc)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = Dropout(0.2)(x)
    x = Dense(500)(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    output = Dense(19, activation='softmax')(x)
    model = tf.keras.models.Model(inputs=content, outputs=output)
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    return model

模型訓練

訓練採用交叉驗證,並綜合多次的預測結果以獲得更好的模型表現。(綜合的方法是10折的預測取均值,即線性加權。)

kf = KFold(n_splits=10, shuffle=True, random_state=2019)
train_pre_matrix = np.zeros((df_train.shape[0], 19))
test_pre_matrix = np.zeros((10, df_test.shape[0], 19))
cv_scores = []

for i, (train_index, valid_index) in enumerate(kf.split(train_)):
    x_train, x_valid = train_[train_index, :], train_[valid_index, :]
    y_train, y_valid = train_label[train_index], train_label[valid_index]
    train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train)).shuffle(10000).batch(64)
    valid_ds = tf.data.Dataset.from_tensor_slices((x_valid, y_valid)).batch(64)
    test_ds = tf.data.Dataset.from_tensor_slices((test_, np.zeros((test_.shape[0], 19)))).batch(64)
    
    model = build_model(1800, embedding_matrix, 19)
    model.fit(train_ds, epochs=30, validation_data=valid_ds, verbose=1)
    
    valid_prob = model.predict(valid_ds)
    valid_pred = np.argmax(valid_prob, axis=1)
    valid_pred = lb.inverse_transform(valid_pred)
    y_valid = np.argmax(y_valid, axis=1)
    y_valid = lb.inverse_transform(y_valid)
    f1_score = f1_score(y_valid, valid_pred, average='macro')
    print("F1 score", f1_score)
    train_pre_matrix[valid_index, :] = valid_prob
    test_pre_matrix[i, :, :] = model.predict(test_ds)
    del model
    gc.collect()
    tf.keras.backend.clear_session()

np.save('test.npy', test_pre_matrix)

結果提交

將綜合後的預測結果提交到比賽平臺,可以看到,得分如下,排名從傳統方法的600多到達了前100,說明深度方法的學習能力是很強的。

補充說明

本文簡要以達觀的文本分類爲例,講述了NLP賽的如今主流思路,2019年達觀舉辦了另外一場比賽,有興趣也可以參與。本文所有代碼開源於我的Github倉庫,歡迎star或者fork。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章