NLP文本分類實戰: 傳統方法與深度學習

問題描述

文檔分類是指給定文檔p(可能含有標題t),將文檔分類為n個類別中的一個或多個,本文以人機寫作為例子,針對有監督學習簡單介紹傳統機器學習方法和深度學習方法。 文檔分類的常見應用:

  • 新聞的分類,也就是給新聞打標籤,一般標籤有幾千個,然後要選取k個標籤,多分類問題,可見2017知乎看山杯比賽,該比賽是對知乎的問題打標籤;
  • 人機寫作判斷,判斷文章是人寫的還是機器寫的,二分類問題,可見CCF2017的360人機大戰題目;
  • 情感識別,例如判斷豆瓣影評中的情感是正向、負向、中立,這個問題很常見而且應用場景很廣泛;

傳統機器學習模型

使用傳統機器學習方法解決文檔分類問題一般分為:文檔預處理、特徵提取、分類器選取、Adaboost多次訓練的過程。

文檔預處理

  • 分詞:中文任務分詞必不可少,一般使用jieba分詞,工業界的翹楚。
  • 詞性標註:在分詞後判斷詞性(動詞、名詞、形容詞、副詞...),在使用jieba分詞的時候設置參數就能獲取。
  • WORD—EMBEDDING:通過詞與上下文、上下文與詞的關係,有效地將詞映射為低維稠密的向量,可以很好的表示詞,一般是把訓練和測試的語料都用來做word-embedding。可以把word-embedding作為傳統機器學習演算法的特徵,同時也是深度學習方法必不可少的步驟(深度學習中單字和詞的embedding都需要)。 本文使用Word2Vector實現Word Embedding,參數設置情況如下
  • size=256 <Word Embedding的維度,如果是詞的話一般設置為256,字的話設置為100就差不多,畢竟漢字數量為9w左右常用字7000左右>
  • window=5, <滑動窗口的大小,詞一般設置為5左右,表示當前詞加上前後詞數量為5,如果為字的話可以設置大一點>
  • min_count=5, <最小詞頻,超過該詞頻的才納入統計,字的話詞頻可以設置高一點>
  • workers=15, <線程數量,加速處理>
  • 分詞、Word Embedding訓練的代碼如下,推薦使用pickle進行中間數據存儲:

import pickleimport codecsimport jiebaimport multiprocessingimport codecsimport pandas as pdfrom gensim.models.word2vec import Word2Vectrain_file = "train.csv"test_file = "test.csv"train_file = codecs.open(train_file, r, utf-8)train_lines = train_file.readlines()test_file = codecs.open(test_file, r, utf-8)test_lines = test_file.readlines()label = []train_title = []train_content = []train_title_cut = []train_content_cut = []test_id = []test_title = []test_content = []test_title_cut = []test_content_cut = []print("Segment train title/content...")for i in range(len(train_lines)): if i % 10000 == 0: print(i) if len(train_lines[i].split( )) != 4: continue article_id, title, content, l = train_lines[i].split( ) if NEGATIVE in l: label.append(0) else: label.append(1) train_title.append(title) train_content.append(content) train_title_cut.append( .join(jieba.cut(title.strip(
), cut_all=False))) train_content_cut.append( .join(jieba.cut(content.strip(
), cut_all=False)))print("Segment train completed.")print("Segment test title/content...")for i in range(len(test_lines)): if i % 10000 == 0: print(i) if len(test_lines[i].split( )) != 3: continue article_id, title, content = test_lines[i].split( ) test_id.append(article_id) test_title.append(title) test_content.append(content) test_title_cut.append( .join(jieba.cut(title.strip(
), cut_all=False))) test_content_cut.append( .join(jieba.cut(content.strip(
), cut_all=False)))print("Segment test completed.")pickle.dump(label, open(train_label.p, wb))pickle.dump(train_title, open(train_title.p, wb))pickle.dump(train_content, open(train_content.p, wb))pickle.dump(train_title_cut, open(train_title_cut.p, wb))pickle.dump(train_content_cut, open(train_content_cut.p, wb))pickle.dump(test_id, open(test_id.p, wb))pickle.dump(test_title, open(test_title.p, wb))pickle.dump(test_content, open(test_content.p, wb))pickle.dump(test_title_cut, open(test_title_cut.p, wb))pickle.dump(test_content_cut, open(test_content_cut.p, wb))corpus = train_title_cut + train_content_cut + test_title_cut + test_content_cutclass CorpusData: def __init__(self, corpus): self.corpus = corpus def __iter__(self): for doc in corpus: origin_words = doc.split( ) yield origin_wordsprint("Train word to vector...")corpus_data = CorpusData(corpus)model = Word2Vec(corpus_data, size=256, window=5, min_count=5, workers=15)model.save(w2v_model_s256_w5_m5.save)print("Train w2v completed.")

特徵提取

可以說提取的特徵決定了整個任務分數的上限,強的或者說敏感的特徵對文檔分類有及其大的影響,而弱特徵的組合有時候也能發揮意向不到的效果,提取過程一般是選取文檔的常規特徵、針對具體任務設計的特徵、對特徵的強度計算和篩選。

常規特徵

  • TF-IDF:詞頻-逆文檔頻率,用以評估詞對於一個文檔集或一個語料庫中的其中一個文檔的重要程度,計算出來是一個DxN維的矩陣,其中D為文檔的數量,N為詞的個數,通常會加入N-gram,也就是計算文檔中N個相連詞的的TF-IDF。一般用sklearn的庫函數計算,具體用法詳見sklearn.feature_extraction.text.TfidfVectorizer。在人機寫作判斷的問題來看,TF-IDF是很強的一個特徵。
  • LDA(文檔的話題):可以假設文檔集有T個話題,一篇文檔可能屬於一個或多個話題,通過LDA模型可以計算出文檔屬於某個話題的概率,這樣可以計算出一個DxT的矩陣。LDA特徵在文檔打標籤等任務上表現很好。
  • LSI(文檔的潛在語義):通過分解文檔-詞頻矩陣來計算文檔的潛在語義,和LDA有一點相似,都是文檔的潛在特徵。
  • 詞性的TD—IDF:以詞的詞性表示詞,再次計算其tf-idf,由於詞性種類很有限,矩陣比較小。

針對具體任務設計特徵

本文是以人機寫作判斷為例子,為此設計了以下特徵,其中每種特徵都選取最大值、最小值、平均值、中位數、方差:

  • 句子長度:文檔短句之後,統計句子長度;
  • 標點數:文檔斷句之後,每個句子中的標點個數
  • jaccard相似度:分詞後的每個句子與分詞後的標題的jaccard距離;
  • 重複句子:文檔中是否有重複句子
  • 英文、數字個數:斷句後句子中的英文、數字個數

特徵的強度計算和篩選

我們要儘可能選取任務敏感的特徵,也就是特徵足夠強可以影響分類的結果,一般用樹模型判斷特徵的重要程度,xgboost的get_fscore就可以實現這一功能。計算特徵強度之後,選取較強的特徵,摒棄弱特徵。可以嘗試組合不同的特徵來構造新的特徵,然後測試新特徵的強弱,反覆如此獲取更多的強特徵。

分類器選取

特徵提取相當於構造了一個DxF的矩陣,其中D為文檔數量,F為特徵數量,一篇文檔用N維空間上的一個點表示,成為一個數學問題:如何將點分類。機器學習中的一個重要問題就是分類,相對回歸問題來說分類問題更加簡化、模糊、不確定,往往因為不能設計定量的回歸問題而設計成分類問題,例如以前天氣預報給的結果是60%的概率降雨,現在可以給出70%的概率降雨100ml的結果,就是隨著計算能力的提高的計算技術的發展講分類問題轉換為回歸問題。前人給我留下了諸如樸素貝葉斯、邏輯回歸、SVM、決策樹、神經網路等分類演算法,sklearn等機器學習庫將其封裝為簡單容易上手的API,供我們選擇。

  • 樸素貝葉斯:以前很多人拿到數據就會馬上用樸素貝葉斯驗證一下數據集,但是往往由於各個特徵不是獨立同分布的所以樸素貝葉斯的效果一般不好,但是也可以嘗試一下,如果效果不錯作為最後模型融合的一個模型。針對人機寫作判斷的任務來說效果太差了,就沒有記錄。
  • 邏輯回歸:用來解決回歸問題的方法,分類問題可以看做回歸問題的特例,也能嘗試用此方法。但是結果和樸素貝葉斯差不多的樣子。
  • SVM:在神經網路火起來之前,可以說SVM撐起了一半分類問題的解決方案,SVM的想法很質樸:找出兩類點之間的最大間隔(也包括軟間隔,即間隔中有樣本點)把樣本點分類。但是在人機寫作判斷的問題上,SVM的表現也不好。
  • 樹模型:樹結構很適合文檔分類,我主要使用LightGBM模型對文檔進行分類,其中調參的經驗是樹的深度不要太深、讓樹儘可能矮寬,這樣分類比較充分,一般深度為4~6就行。樹模型的調參是一件很痛苦的事情,推薦基於貝葉斯優化的調參方法。有熟悉XGBoost的同學也可以嘗試,但是總體來說LGB比XGB速度要快好幾倍更適合驗證特徵以及調參。人機寫作任務中,第一批數據是10w訓練+5w測試數據,LGB在這個數據集上表現很優秀,單模型就接近0.9了,但是由於後期60w訓練+40w測試數據,LGB的性能明顯下降,而且怎麼調參優化都不起作用。

Adaboost多次訓練

這是我和隊友針對人機寫作判斷問題,根據Adaboost演算法設計的一個小Trick。如果每次分類結果中,把大量的負樣本分為正樣本,或者大量負樣本分為正樣本,就根據正樣本和負樣本的錯誤率調整正負樣本的權重,例如正負樣本的錯誤率分別為P(正)、P(負),當前權重分別為W(正)、W(負),則根據以下方式調整:

W(正) = P(正)+ [1 - abs(P(正) + P(負)) / 2]W(負) = P(負)+ [1 - abs(P(正) + P(負)) / 2]

根據這種動態調整權重的方法,可以充分發揮樹模型在分類問題上的優勢。

小結

新數據發放之後,樹模型的能力明顯降低,在訓練集都達不到0.7的F1值,我在費力搗鼓樹模型時候,隊友已經開始嘗試深度學習的模型,明顯優於傳統機器學習模型,分數在0.84以上。而且傳統機器學習方法中的特徵提取環節太過費時費力,而且經常很不討好。建議剛接觸文檔分類或者其他機器學習任務,可以嘗試傳統的機器學習方法(主要是統計學習方法),可以體驗一下這幾個過程,但是如果想取得好成績最好還是嘗試深度學習。

深度學習模型

深度學習模型的重點是模型的構建和調參,相對來說任務量能小不少。RNN、LSTM等模型由於擁有記憶能力,因而在文本處理上表現優異,但是缺點很明顯就是計算量很大,在沒有GPU加速情況下,不適合處理大批的數據,CNN在FaceBook的翻譯項目上大放異彩也表明CNN在文本處理領域上的重要性,而且相對RNN來說,速度明顯提升。本文嘗試了多層CNN、並行CNN、RNN與CNN的結合、基於Hierarchical Attention的RNN、遷移學習、多任務學習、聯合模型學習。在單模型和聯合模型學習上,我們復現、借鑒了2017知乎看山杯比賽第一名的方案,在此表示感謝。深度學習部分代碼都是使用Keras框架實現的,Keras搭建模型非常方便適合快速驗證自己的想法和模型。

文本預處理

分詞、Word Embedding已經介紹過,一般文本內容輸入到神經網路作為Input,要先進行Tokenizer,然後對空白部分做padding,並且獲得Word Embedding的emnedding_matrix其中Tokenizer、padding都是使用Keras自帶的API,因為我剛開始使用深度學習處理文本時候這個過程不是很明白,就分享一下代碼,具體過程如下:

from keras.preprocessing.text import Tokenizerfrom gensim.models.word2vec import Word2Vecmax_nb_words = 100000 #常用詞設置為10wtokenizer = Tokenizer(num_words=max_nb_words, filters=)tokenizer.fit_on_texts(train_para_cut) #使用已經切分的訓練語料進行fitword_index = tokenizer.word_indexvocab_size = len(word_index)model = Word2Vec.load(w2v_file) #Load之前訓練好的Word Embedding模型word_vectors = model.wvembeddings_index = dict()for word, vocab_obj in model.wv.vocab.items(): if int(vocab_obj.index) < max_nb_words: embeddings_index[word] = word_vectors[word]del model, word_vectorsprint("word2vec size: {}".format(len(embeddings_index)))num_words = min(max_nb_words, vocab_size)not_found = 0embedding_matrix = np.zeros((num_words+1, w2v_dim)) # 與訓練好的,神經網路Embedding層需要用到for word, i in word_index.items(): if i > num_words: continue embedding_vector = embeddings_index.get(word) if embedding_vector is not None: embedding_matrix[i] = embedding_vector else: not_found += 1print("not found word in w2v: {}".format(not_found))print("input layer size: {}".format(num_words))print("GET Embedding Matrix Completed")train_x = tokenizer.texts_to_sequences(train_content_cut)train_x = pad_sequences(train_x, maxlen=max_len, padding="post", truncating="post")test_x = tokenizer.texts_to_sequences(test_content_cut)test_x = pad_sequences(test_x, maxlen=max_len, padding="post", truncating="post")

必要的數據統計

一篇文檔及其標籤作為神經網路的一個輸入,經Tokenizer之後,需要設置定長的輸入,必須統計文檔長度、句子數、句子長度、標題長度,推薦使用pandas進行統計,方便簡潔。就人機寫作判斷任務的數據統計情況如下:

Label on train:NEGATIVE 359631POSITIVE 240369Content length on train:POSITVE:count 240369.000000mean 1030.239369std 606.937210min 2.00000025% 554.00000050% 866.00000075% 1350.000000max 3001.000000NEGATIVE:count 359631.000000mean 1048.659999std 607.034089min 186.00000025% 574.00000050% 882.00000075% 1369.000000max 3385.000000Content length on test:count 400000.000000mean 1042.695075std 608.866342min 136.00000025% 567.00000050% 877.00000075% 1362.000000max 4042.000000Sentence number on traincount 600000.000000mean 64.429440std 43.248348min 2.00000025% 33.00000050% 53.00000075% 85.000000max 447.000000

設想差不多2個字1個詞,分完詞後句子詞數最大不超過2000,神經網路的Input length可以設置為2000,文檔句子數設為100,句子長度設為50,最夠覆蓋絕大部分文檔。

正文與標題

文檔分為正文和標題兩部分,一般兩部分分開處理,可以共享Embedding層也可以不共享,人機寫作分類問題中我們沒有共享Embedding。

正文多層CNN,未使用標題

CNN需要設置不同大小的卷積核,並且多層卷積才能較好的捕獲文本的特徵,具體網路結構如下:

正文 CNN Inception,未使用標題

RCNN處理正文,多層CNN處理標題

基於Hierarchical Attention的RNN處理正文

模型是根據論文《Hierarchical Attention Networks for Document Classification》實現的,論文中的模型如下

具體實現過程中的網路結構如下:

遷移學習

設計模型M,M在數據集A上訓練到最佳效果保存模型的權重,然後再使用訓練好的M在數據集B上訓練,這個過程可以看做簡單的遷移學習。因為人機寫作判斷任務中,先後有兩個數據集,早期數據可以看做A,後期也就是最終的數據看做B,而模型在A上的表現比B上要好很多,這樣就可以使用遷移學習來使模型在B上表現也好。 我們是使用RCNN處理正文,多層CNN處理標題這個模型來實現遷移學習的,具體過程如下:

多任務學習

相同模型同時在數據集A和B上訓練,可以稱為多任務學習,我們也是使用RCNN處理正文,多層CNN處理標題這個模型來實現多任務學習的,具體過程如下:

聯合模型學習

選取表現最好的2個單模型,在數據集上預訓練到最優,然後聯合在一起訓練,可以共享Embedding層,也可以不共享,由於我們表現最好的單模型是RCNN處理正文,多層CNN處理標題的Model A和基於Hierarchical Attention的RNN的Model B,Embedding的形式不同所以不能共享,具體形式如下所示,也可以聯合多個單模型一起訓練,但缺點就是訓練時間過長:

小結

深度學習在文檔分類問題上比傳統機器學習方法有太大的優勢,仔細分析就知道文本的特徵很難提取,而且這些特徵不能很好的表示文檔的語義、語法,丟失了很大一部分的有用信息,而深度學習就是將特徵提取這個環節交給深度網路去自動完成,通過更高的計算成本換取更全面更優良的文本特徵。

模型Stacking

一般是針對評測或者比賽,融合多個模型的結果,不同的模型會導致其預測結果的多樣性,Stacking可以有效的融合其多養性達到提高分數的目的。關於Stacking的介紹,可以看這篇文章。 傳統Stacking方法一般使用樹模型,例如LightGBM、XGBoost等,我們使用神經網路的方式實現,Model就是兩個全連接層。

總結

傳統機器學習方法可以作為任務的Baseline,而且通過特徵的設計和提取能夠感受數據,不能把數據也就是文檔看做黑盒子,對數據了解足夠設計模型肯定事半功倍。至於深度學習的方法,我們只是借鑒、改進經典論文提出的模型,也使用了前人比賽的Trick,如果想深入了解還是需要閱讀更多的論文,ACL是計算語言學年會彙集了全球頂尖NLP領域學者的思想,可以關注這個會議閱讀其收錄的論文。

引用

  • Kim Y. Convolutional Neural Networks for Sentence Classification[J]. Eprint Arxiv, 2014.
  • Yang Z, Yang D, Dyer C, et al. Hierarchical Attention Networks for Document Classification[C]// Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies. 2017:1480-1489.
  • zhuanlan.zhihu.com/p/28
  • zhuanlan.zhihu.com/p/25

推薦閱讀:

TAG:深度學習DeepLearning | 自然語言處理 | 機器學習 |