問題描述
文件分類是指給定文件p(可能含有標題t),將文件分類為n個類別中的一個或多個,本文以人機寫作為例子,針對有監督學習簡單介紹傳統機器學習方法和深度學習方法。 文件分類的常見應用:
新聞的分類,也就是給新聞打標籤,一般標籤有幾千個,然後要選取k個標籤,多分類問題,可見2017知乎看山杯比賽,該比賽是對知乎的問題打標籤;
人機寫作判斷,判斷文章是人寫的還是機器寫的,二分類問題,可見CCF2017的360人機大戰題目;
情感識別,例如判斷豆瓣影評中的情感是正向、負向、中立,這個問題很常見而且應用場景很廣泛;
傳統機器學習模型
使用傳統機器學習方法解決文件分類問題一般分為:文件預處理、特徵提取、分類器選取、Adaboost多次訓練的過程。
文件預處理
分詞
:中文任務分詞必不可少,一般使用jieba分詞,工業界的翹楚。
詞性標註
:在分詞後判斷詞性(動詞、名詞、形容詞、副詞。。。),在使用jieba分詞的時候設定引數就能獲取。
WORD—EMBEDDING
:透過詞與上下文、上下文與詞的關係,有效地將詞對映為低維稠密的向量,可以很好的表示詞,一般是把訓練和測試的語料都用來做
word-embedding
。可以把word-embedding作為傳統機器學習演算法的特徵,同時也是深度學習方法必不可少的步驟(深度學習中單字和詞的embedding都需要)。 本文使用Word2Vector實現Word Embedding,引數設定情況如下
size=256
window=5, <滑動視窗的大小,詞一般設定為5左右,表示當前詞加上前後詞數量為5,如果為字的話可以設定大一點>
min_count=5, <最小詞頻,超過該詞頻的才納入統計,字的話詞頻可以設定高一點>
workers=15, <執行緒數量,加速處理>
分詞、Word Embedding訓練的程式碼如下,推薦使用pickle進行中間資料儲存:
import pickle
import codecs
import jieba
import multiprocessing
import codecs
import pandas as pd
from gensim。models。word2vec import Word2Vec
train_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(‘\t’)) != 4:
continue
article_id, title, content, l = train_lines[i]。split(‘\t’)
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(‘\n’), cut_all=False)))
train_content_cut。append(‘ ’。join(jieba。cut(content。strip(‘\n’), 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(‘\t’)) != 3:
continue
article_id, title, content = test_lines[i]。split(‘\t’)
test_id。append(article_id)
test_title。append(title)
test_content。append(content)
test_title_cut。append(‘ ’。join(jieba。cut(title。strip(‘\n’), cut_all=False)))
test_content_cut。append(‘ ’。join(jieba。cut(content。strip(‘\n’), 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_cut
class CorpusData:
def __init__(self, corpus):
self。corpus = corpus
def __iter__(self):
for doc in corpus:
origin_words = doc。split(‘ ’)
yield origin_words
print(“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 Tokenizer
from gensim。models。word2vec import Word2Vec
max_nb_words = 100000 #常用詞設定為10w
tokenizer = Tokenizer(num_words=max_nb_words, filters=‘’)
tokenizer。fit_on_texts(train_para_cut) #使用已經切分的訓練語料進行fit
word_index = tokenizer。word_index
vocab_size = len(word_index)
model = Word2Vec。load(w2v_file) #Load之前訓練好的Word Embedding模型
word_vectors = model。wv
embeddings_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_vectors
print(“word2vec size: {}”。format(len(embeddings_index)))
num_words = min(max_nb_words, vocab_size)
not_found = 0
embedding_matrix = np。zeros((num_words+1, w2v_dim)) # 與訓練好的,
神經網路Embeddin
g層需要用到
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 += 1
print(“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 359631
POSITIVE 240369
Content length on train:
POSITVE:
count 240369。000000
mean 1030。239369
std 606。937210
min 2。000000
25% 554。000000
50% 866。000000
75% 1350。000000
max 3001。000000
NEGATIVE:
count 359631。000000
mean 1048。659999
std 607。034089
min 186。000000
25% 574。000000
50% 882。000000
75% 1369。000000
max 3385。000000
Content length on test:
count 400000。000000
mean 1042。695075
std 608。866342
min 136。000000
25% 567。000000
50% 877。000000
75% 1362。000000
max 4042。000000
Sentence number on train
count 600000。000000
mean 64。429440
std 43。248348
min 2。000000
25% 33。000000
50% 53。000000
75% 85。000000
max 447。000000
設想差不多2個字1個詞,分完詞後句子詞數最大不超過2000,神經網路的Input length可以設定為2000,文件句子數設為100,句子長度設為50,最夠覆蓋絕大部分文件。
正文與標題
文件分為正文和標題兩部分,一般兩部分分開處理,可以共享Embedding層也可以不共享,人機寫作分類問題中我們沒有共享Embedding。
正文多層CNN,未使用標題
CNN需要設定不同大小的卷積核,並且多層卷積才能較好的捕獲文字的特徵,具體網路結構如下:
正文 CNN Inception,未使用標題
RCNN處理正文,
多層CN
N處理標題
基於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。
https://
zhuanlan。zhihu。com/p/28
923961
https://
zhuanlan。zhihu。com/p/25
928551