背景介紹
筆者實驗室項目正好需要用到文本分類,作為NLP領域最經(jīng)典的場景之一,文本分類積累了大量的技術實現(xiàn)方法,如果將是否使用深度學習技術作為標準來衡量,實現(xiàn)方法大致可以分成兩類:
- 基于傳統(tǒng)機器學習的文本分類
- 基于深度學習的文本分類
facebook之前開源的fastText屬于簡化版的第二類,詞向量取平均直接進softmax層,還有業(yè)界研究上使用比較多的TextCNN模型屬于第二類。有一個github項目很好的把這些模型都集中到了一起,并做了一些簡單的性能比較,想要進一步了解這些高大上模型的同學可以查看如下鏈接:
all kinds of text classificaiton models and more with deep learning
本文的目的主要記錄筆者自己構建文本分類系統(tǒng)的過程,分別構建基于傳統(tǒng)機器學習的文本分類和基于深度學習的文本分類系統(tǒng),并在同一數(shù)據(jù)集上進行測試。
經(jīng)典的機器學習方法采用獲取tf-idf文本特征,分別喂入logistic regression分類器和隨機森林分類器的思路,并對兩種方法做性能對比。
基于深度學習的文本分類,這里主要采用CNN對文本分類,考慮到RNN模型相較CNN模型性能差異不大并且耗時還比較久,這里就不多做實驗了。
實驗過程有些比較有用的small trick分享,包括多進程分詞、訓練全量tf-idf、python2對中文編碼的處理技巧等等,在下文都會仔細介紹。
食材準備
本文采用的數(shù)據(jù)集是很流行的搜狗新聞數(shù)據(jù)集,get到的時候已經(jīng)是經(jīng)過預處理的了,所以省去了很多數(shù)據(jù)預處理的麻煩,數(shù)據(jù)集下載鏈接如下:
(感謝張凱強同學指出了我的錯誤,數(shù)據(jù)集是THUCnews的,清華大學根據(jù)新浪新聞RSS訂閱頻道2005-2011年間的歷史數(shù)據(jù)篩選過濾生成,非常感謝,下面的鏈接也更新過一次,參考鏈接中有原始我參考的博文,如果鏈接再失效,數(shù)據(jù)集也可以去那里找找看,由于我的學業(yè)和實習導致我的生活越來越忙,不能及時回復大家了,請多見諒,謝謝!)
密碼:kxxa
數(shù)據(jù)集一共包括10類新聞,每類新聞65000條文本數(shù)據(jù),訓練集50000條,測試集10000條,驗證集5000條。
經(jīng)典機器學習方法
分詞、去停用詞
調用之前短文本分類博文中提到的分詞工具類,對訓練集、測試集、驗證集進行多進程分詞,以節(jié)省時間:
import multiprocessing
tmp_catalog = '/home/zhouchengyu/haiNan/textClassifier/data/cnews/'
file_list = [tmp_catalog+'cnews.train.txt', tmp_catalog+'cnews.test.txt']
write_list = [tmp_catalog+'train_token.txt', tmp_catalog+'test_token.txt']
def tokenFile(file_path, write_path):
word_divider = WordCut()
with open(write_path, 'w') as w:
with open(file_path, 'r') as f:
for line in f.readlines():
line = line.decode('utf-8').strip()
token_sen = word_divider.seg_sentence(line.split('\t')[1])
w.write(line.split('\t')[0].encode('utf-8') + '\t' + token_sen.encode('utf-8') + '\n')
print file_path + ' has been token and token_file_name is ' + write_path
pool = multiprocessing.Pool(processes=4)
for file_path, write_path in zip(file_list, write_list):
pool.apply_async(tokenFile, (file_path, write_path, ))
pool.close()
pool.join() # 調用join()之前必須先調用close()
print "Sub-process(es) done."
計算tf-idf
這里有幾點需要注意的,一是計算tf-idf是全量計算,所以需要將train+test+val的所有corpus都相加,再進行計算,二是為了防止文本特征過大,需要去低頻詞,因為是在jupyter上寫的,所以測試代碼的時候,先是選擇最小的val數(shù)據(jù)集,成功后,再對test,train數(shù)據(jù)集迭代操作,希望不要給大家留下代碼冗余的影響...[悲傷臉]。實現(xiàn)代碼如下:
def constructDataset(path):
"""
path: file path
rtype: lable_list and corpus_list
"""
label_list = []
corpus_list = []
with open(path, 'r') as p:
for line in p.readlines():
label_list.append(line.split('\t')[0])
corpus_list.append(line.split('\t')[1])
return label_list, corpus_list
tmp_catalog = '/home/zhouchengyu/haiNan/textClassifier/data/cnews/'
file_path = 'val_token.txt'
val_label, val_set = constructDataset(tmp_catalog+file_path)
print len(val_set)
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
tmp_catalog = '/home/zhouchengyu/haiNan/textClassifier/data/cnews/'
write_list = [tmp_catalog+'train_token.txt', tmp_catalog+'test_token.txt']
tarin_label, train_set = constructDataset(write_list[0]) # 50000
test_label, test_set = constructDataset(write_list[1]) # 10000
# 計算tf-idf
corpus_set = train_set + val_set + test_set # 全量計算tf-idf
print "length of corpus is: " + str(len(corpus_set))
vectorizer = CountVectorizer(min_df=1e-5) # drop df < 1e-5,去低頻詞
transformer = TfidfTransformer()
tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus_set))
words = vectorizer.get_feature_names()
print "how many words: {0}".format(len(words))
print "tf-idf shape: ({0},{1})".format(tfidf.shape[0], tfidf.shape[1])
"""
length of corpus is: 65000
how many words: 379000
tf-idf shape: (65000,379000)
"""
標簽數(shù)字化,抽取數(shù)據(jù)
因為本來文本就是以一定隨機性抽取成3份數(shù)據(jù)集的,所以,這里就不shuffle啦,偷懶一下下。。但是如果能shuffle的話,盡量還是做這一步,堅持正途。
from sklearn import preprocessing
# encode label
corpus_label = tarin_label + val_label + test_label
encoder = preprocessing.LabelEncoder()
corpus_encode_label = encoder.fit_transform(corpus_label)
train_label = corpus_encode_label[:50000]
val_label = corpus_encode_label[50000:55000]
test_label = corpus_encode_label[55000:]
# get tf-idf dataset
train_set = tfidf[:50000]
val_set = tfidf[50000:55000]
test_set = tfidf[55000:]
喂入分類器
- logistic regression分類器
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
# from sklearn.metrics import confusion_matrix
# LogisticRegression classiy model
lr_model = LogisticRegression()
lr_model.fit(train_set, train_label)
print "val mean accuracy: {0}".format(lr_model.score(val_set, val_label))
y_pred = lr_model.predict(test_set)
print classification_report(test_label, y_pred)
分類報告如下(包括準確率、召回率、F1值):
mean accuracy: 0.9626
precision recall f1-score support
0 1.00 1.00 1.00 1000
1 0.99 0.98 0.98 1000
2 0.94 0.87 0.91 1000
3 0.91 0.91 0.91 1000
4 0.97 0.93 0.95 1000
5 0.97 0.98 0.98 1000
6 0.93 0.96 0.95 1000
7 0.99 0.97 0.98 1000
8 0.94 0.99 0.96 1000
9 0.95 0.99 0.97 1000
avg / total 0.96 0.96 0.96 10000
- Random Forest 分類器
# 隨機森林分類器
from sklearn.ensemble import RandomForestClassifier
rf_model = RandomForestClassifier(n_estimators=200, random_state=1080)
rf_model.fit(train_set, train_label)
print "val mean accuracy: {0}".format(rf_model.score(val_set, val_label))
y_pred = rf_model.predict(test_set)
print classification_report(test_label, y_pred)
分類報告如下(包括準確率、召回率、F1值):
val mean accuracy: 0.9228
precision recall f1-score support
0 1.00 1.00 1.00 1000
1 0.98 0.98 0.98 1000
2 0.89 0.57 0.69 1000
3 0.81 0.97 0.88 1000
4 0.95 0.89 0.92 1000
5 0.97 0.96 0.97 1000
6 0.85 0.94 0.89 1000
7 0.95 0.97 0.96 1000
8 0.95 0.97 0.96 1000
9 0.91 0.99 0.95 1000
avg / total 0.93 0.92 0.92 10000
分析
上面采用邏輯回歸分類器和隨機森林分類器做對比:
可以發(fā)現(xiàn),除了個別分類隨機森林方法有較大進步,大部分都差于邏輯回歸分類器
并且200棵樹的隨機森林耗時過長,比起邏輯回歸分類器來說,運算效率太低
CNN文本分類
這一部分主要是參考tensorflow社區(qū)的一份博客進行實驗的,這里也不再贅述,博客講的非常好,附上原文鏈接,前去膜拜:NN-RNN中文文本分類,基于tensorflow
字符級特征提取
這里和前文差異比較大的地方,主要是提取文本特征這一塊,這里的CNN模型采用的是字符級特征提取,比如data目錄下cnews_loader.py中:
def read_file(filename):
"""讀取文件數(shù)據(jù)"""
contents, labels = [], []
with open_file(filename) as f:
for line in f:
try:
label, content = line.strip().split('\t')
contents.append(list(content)) # 字符級特征
labels.append(label)
except:
pass
return contents, labels
def build_vocab(train_dir, vocab_dir, vocab_size=5000):
"""根據(jù)訓練集構建詞匯表,存儲"""
data_train, _ = read_file(train_dir)
all_data = []
for content in data_train:
all_data.extend(content)
counter = Counter(all_data)
count_pairs = counter.most_common(vocab_size - 1)
words, _ = list(zip(*count_pairs))
# 添加一個 <PAD> 來將所有文本pad為同一長度
words = ['<PAD>'] + list(words)
筆者做了下測試:
#! /bin/env python
# -*- coding: utf-8 -*-
from collections import Counter
"""
字符級別處理,
對于中文來說,基本不是原意的字,但是也能作為一種統(tǒng)計特征來表征文本
"""
content1 = "你好呀大家"
content2 = "你真的好嗎?"
# content = "abcdefg"
all_data = []
all_data.extend(list(content1))
all_data.extend(list(content2))
# print list(content) # 字符級別處理
# print "length: " + str(len(list(content)))
counter = Counter(all_data)
count_pairs = counter.most_common(5)
words, _ = list(zip(*count_pairs))
words = ['<PAD>'] + list(words) #['<PAD>', '\xe5', '\xbd', '\xa0', '\xe4', '\xe7']
這種基本不是原意的字符級別的特征,也能從統(tǒng)計意義上表征文本,從而作為特征,這一點需要清楚。
遷移python2
github上的版本是python3的,由于筆者一直使用的是python2,所以對上述工作做了一點版本遷移,使得在如下環(huán)境下也能順利運行:
Python 2.7
TensorFlow 1.3
numpy
scikit-learn
除了p3和py2差異比較明顯的類定義、print、除法運算外,還有就是中文編碼,使用codecs模塊可以很好的解決這個問題,由于是細枝末節(jié),這里也就不展開來說了。
最終,在同一數(shù)據(jù)集上,得到的測試報告如下:
Test Loss: 0.13, Test Acc: 96.06%
Precision, Recall and F1-Score...
precision recall f1-score support
sports 0.99 0.99 0.99 1000
finance 0.96 0.99 0.98 1000
house 1.00 0.99 1.00 1000
living 0.99 0.88 0.93 1000
education 0.90 0.93 0.92 1000
tech 0.92 0.99 0.95 1000
fashion 0.95 0.97 0.96 1000
policy 0.97 0.92 0.94 1000
game 0.97 0.97 0.97 1000
entertaiment 0.95 0.98 0.96 1000
avg / total 0.96 0.96 0.96 10000
分析
可以看出與傳統(tǒng)機器學習方法相比,貌似深度學習方法優(yōu)勢不大,但是考慮到數(shù)據(jù)集數(shù)量不多、深度學習模型仍舊是個baseline,還可以通過進一步的調節(jié)參數(shù),來達到更好的效果,深度學習在文本分類性能優(yōu)化方面,依舊是大有可為的。
- 參考資料
詳細代碼見筆者的github:中文文本分類對比(經(jīng)典方法和CNN)
××××××××××××××××××××××××××××××××××××××××××
本文屬于筆者(EdwardChou)原創(chuàng)
轉載請注明出處
××××××××××××××××××××××××××××××××××××××××××