自然語言處理中的輿情分析、情感分析有很多種方法,但是基于模型的方法對(duì)語料的質(zhì)量要求高造虏,如果不能弄到高質(zhì)量的語料,很多時(shí)候并不準(zhǔn)確踢械。如果需要預(yù)測(cè)的樣本量很小酗电,通常到最后還是使用最原始的方法--基于詞庫(kù)匹配。雖然詞庫(kù)匹配效果一定不好内列,效率還低,但是它非常容易實(shí)現(xiàn)背率,隨隨便便就能弄一個(gè)出來試試效果话瞧,如果在樣本很小領(lǐng)域很單一的情況,基于這種方法就能湊合著用用了寝姿。
如果訓(xùn)練樣本量足夠大交排,且樣本屬于單一領(lǐng)域,用機(jī)器學(xué)習(xí)的方法也會(huì)有不錯(cuò)的效果饵筑。最簡(jiǎn)單的方法是使用tf-idf把已標(biāo)注的文本(通常是正負(fù)類或者中立)根據(jù)關(guān)鍵詞向量化埃篓,然后用向量化的文本和他們對(duì)應(yīng)的標(biāo)簽訓(xùn)練分類器,最后用保存好的分類器來預(yù)測(cè)新的文本根资。
最近看到一篇用word2vec/doc2vec做情感分析的文章架专,試了一下效果還行同窘,于是把文檔翻譯出來,文檔是15年的,有些部分已經(jīng)過時(shí)部脚,筆者進(jìn)行了測(cè)試和改寫想邦,也會(huì)在文檔中添加一些自己的心得。文中使用word2vec/doc2vec來代替sklearn的tf-idf把文本向量化委刘,再用常用機(jī)器學(xué)習(xí)分類算法分類丧没,大體思路和普通的機(jī)器學(xué)習(xí)情感分析一樣,但是word2vec和doc2vec批量讀取語料十分麻煩锡移,輸入數(shù)據(jù)需要給成嵌套列表的形式呕童,例如:[[words],[],[],……],每個(gè)句子是一個(gè)word列表淆珊,這些句子列表組合成一個(gè)完整的語料庫(kù)夺饲。word2vec中使用from gensim.models.word2vec import LineSentence
把txt文件直接轉(zhuǎn)化成正確的形式,例如:model = Word2Vec(LineSentence('體育.txt'), size=300,window=5,min_count=1, workers=2)
而該文主要貢獻(xiàn)是為doc2vec情感分析提供了一種批量讀取訓(xùn)練語料的預(yù)處理方法套蒂。
- 原文地址github:https://github.com/linanqiu/word2vec-sentiments
- 譯者:wong小堯
使用Doc2Vec做情感分析
Word2Vec是個(gè)蠢貨钞支。簡(jiǎn)而言之,它吃進(jìn)去語料操刀,為每一個(gè)詞生成向量烁挟。你會(huì)問,這些向量有什么特別之處?嗯骨坑,相似的詞的向量彼此相近撼嗓。此外,這些向量表示我們?nèi)绾问褂眠@些單詞欢唾。例如且警,v_man - v_woman近似等于v_king - v_queen,說明“男人對(duì)女人如國(guó)王對(duì)女王”的關(guān)系礁遣。這個(gè)過程斑芜,在NLP領(lǐng)域中,被稱為詞嵌入祟霍。這種表征方法已得到廣泛應(yīng)用杏头。看了Doc2Vec的介紹會(huì)覺得更棒沸呐,它不僅代表單詞醇王,還代表完整的句子和文檔。想象一下崭添,用一個(gè)固定長(zhǎng)度的向量來表示一個(gè)完整的句子寓娩,然后運(yùn)行所有的標(biāo)準(zhǔn)分類算法。這簡(jiǎn)直太神奇了不是嗎?
然而,Word2Vec文檔是垃圾棘伴。c代碼幾乎是不可讀的(700行高度優(yōu)化的代碼寞埠,有時(shí)是古怪的優(yōu)化代碼)。我個(gè)人花費(fèi)了大量的時(shí)間去整理Doc2Vec排嫌,并且由于執(zhí)行錯(cuò)誤而導(dǎo)致了50%的錯(cuò)誤畸裳。本教程旨在幫助其他用戶使用Word2Vec進(jìn)行自己的研究。我們使用Word2Vec進(jìn)行情緒分析淳地,試著將康奈爾IMDB電影評(píng)論集合分類怖糊。
模塊
我們使用gensim,因?yàn)間ensim的Word2Vec(和Doc2Vec)更易于閱讀颇象。祝福那些家伙伍伤。我們也使用numpy來進(jìn)行數(shù)組操作,使用sklearn來訓(xùn)練邏輯回歸分類器遣钳。
# gensim modules
from gensim import utils
from gensim.models.doc2vec import LabeledSentence
#LabeledSentence新版本由TaggedDocument替代
#from gensim.models.doc2vec import TaggedDocument
from gensim.models import Doc2Vec
# numpy
import numpy
# classifier
from sklearn.linear_model import LogisticRegression
# random
import random
輸入格式
我們不能輸入來自康奈爾電影評(píng)論數(shù)據(jù)庫(kù)的原始評(píng)論扰魂。相反,我們通過將所有內(nèi)容轉(zhuǎn)換為小寫并刪除標(biāo)點(diǎn)來清洗它們蕴茴。我是通過bash完成的劝评,你可以通過Python、JS或您最喜歡的語言輕松完成這一任務(wù)倦淀。這一步很簡(jiǎn)單蒋畜。
其結(jié)果是有五個(gè)文件:
- “test-neg。txt:來自測(cè)試數(shù)據(jù)的12500個(gè)負(fù)面電影評(píng)論撞叽。
- “test-pos姻成。txt:來自測(cè)試數(shù)據(jù)的12500個(gè)正面電影評(píng)論。
- “train-neg愿棋。txt:來自培訓(xùn)數(shù)據(jù)的12500個(gè)負(fù)面電影評(píng)論科展。
- “train-pos。txt:來自tr的12500個(gè)正面電影評(píng)論糠雨。
- train-unsup才睹。txt: 50000沒有標(biāo)簽的電影評(píng)論。
每一篇評(píng)論的格式都應(yīng)該是這樣的:
上面的示例包含兩個(gè)電影評(píng)論甘邀,每個(gè)都占據(jù)了一整行砂竖。是的,每個(gè)文件應(yīng)該在一行上鹃答,被新行隔開。這是非常重要的突硝,因?yàn)槲覀兊慕馕銎饕蕾囉谶@個(gè)來識(shí)別句子测摔。
為Doc2Vec提供數(shù)據(jù)
Doc2Vec(Doc2Vec算法gensim
部分的實(shí)現(xiàn)。)在詞嵌入中做的很好,但是在讀取文件中做的很差锋八。它只包含了“LabeledLineSentence”類浙于,一個(gè)用來生成“LabeledSentence”的類,一個(gè)基于gensim.models.doc2vec
并用來表示簡(jiǎn)單句子的類挟纱。
為什么使用“Labeled”這個(gè)詞?這就是Doc2Vec與Word2Vec的不同之處羞酗。
Word2Vec只是將一個(gè)單詞轉(zhuǎn)換成一個(gè)向量。
Doc2Vec不止這些紊服,他還將一個(gè)句子中的所有單詞聚合成一個(gè)向量檀轨。為了做到這一點(diǎn),它只是把一個(gè)句子的標(biāo)簽當(dāng)作一個(gè)特殊的詞欺嗤,并且在這個(gè)特殊的詞上做了一些巫術(shù)参萄。因此,這個(gè)特殊的單詞是一個(gè)句子的標(biāo)簽煎饼。
所以我們必須把句子格式化成如下格式
[['word1', 'word2', 'word3', 'lastword'], ['label1']]
LabeledSentence只是一種完成此項(xiàng)工作更簡(jiǎn)潔的方式讹挎。它包含一個(gè)單詞列表和一個(gè)句子的標(biāo)簽。我們并不需要關(guān)心標(biāo)簽的工作原理吆玖,我們只需要知道它存儲(chǔ)了兩種東西——一個(gè)單詞列表和一個(gè)標(biāo)簽筒溃。
但是,我們需要一種方法來將我們的新行分隔的語料庫(kù)轉(zhuǎn)換成一個(gè)LabeledSentences(已經(jīng)有標(biāo)簽的句子)的集合沾乘。
Doc2Vec中默認(rèn)的LabeledLineSentence類的默認(rèn)構(gòu)造函數(shù)可以對(duì)單個(gè)文本文件執(zhí)行此操作怜奖,但對(duì)于多個(gè)文件不能這樣做。但是在分類任務(wù)中意鲸,我們通常需要處理多個(gè)文檔(test, training, positive, negative等)烦周。只能單個(gè)處理,不討厭嗎?
所以我們寫我們自己的LabeledLineSentence類怎顾。構(gòu)造函數(shù)接受一個(gè)字典读慎,該字典定義要讀的文件,而標(biāo)簽前綴則是來自該文檔的句子槐雾。然后夭委,Doc2Vec可以直接通過迭代器讀取集合,或者我們可以直接訪問數(shù)組募强。
我們還需要一個(gè)函數(shù)來返回一個(gè)經(jīng)過修改的LabeledSentences數(shù)組株灸。
我們稍后會(huì)看到原因。
class LabeledLineSentence(object):
def __init__(self, sources):
self.sources = sources
flipped = {}
# make sure that keys are unique
for key, value in sources.items():
if value not in flipped:
flipped[value] = [key]
else:
raise Exception('Non-unique prefix encountered')
def __iter__(self):
for source, prefix in self.sources.items():
with utils.smart_open(source) as fin:
for item_no, line in enumerate(fin):
yield LabeledSentence(utils.to_unicode(line).split(), [prefix + '_%s' % item_no])
def to_array(self):
self.sentences = []
for source, prefix in self.sources.items():
with utils.smart_open(source) as fin:
for item_no, line in enumerate(fin):
self.sentences.append(LabeledSentence(utils.to_unicode(line).split(), [prefix + '_%s' % item_no]))
return self.sentences
def sentences_perm(self):
shuffled = list(self.sentences)
random.shuffle(shuffled)
return shuffled
現(xiàn)在我們可以將數(shù)據(jù)文件提供給LabeledLineSentence擎值。
正如我們前面提到的慌烧,LabeledLineSentence簡(jiǎn)單地使用一個(gè)帶鍵的字典作為文件名,并且句子值的特殊前綴是來自文檔鸠儿。前綴必須是唯一的屹蚊,因此不同文檔的句子沒有含糊不清的地方厕氨。
前綴將會(huì)有一個(gè)計(jì)數(shù)器,用于在documetns中標(biāo)記單個(gè)句子汹粤。
sources = {'test-neg.txt':'TEST_NEG', 'test-pos.txt':'TEST_POS', 'train-neg.txt':'TRAIN_NEG', 'train-pos.txt':'TRAIN_POS', 'train-unsup.txt':'TRAIN_UNS'}
sentences = LabeledLineSentence(sources)
Model
建立詞匯表
Doc2Vec要求我們構(gòu)建詞匯表(只需簡(jiǎn)單地消化所有單詞并過濾出唯一的單詞命斧,然后用他們做一些基本的計(jì)算)。所以我們給它提供了句子數(shù)組嘱兼。model.build_vocab
接受了LabeledLineSentence數(shù)組, 因此国葬,在LabeledLineSentences類中我們需要使用to_array
函數(shù)。
如果您對(duì)參數(shù)感興趣芹壕,請(qǐng)閱讀Word2Vec文檔汇四。或者哪雕,這里提供一個(gè)簡(jiǎn)單的綱要:
- min_count:忽略所有頻率低于這個(gè)的單詞船殉。您必須將其設(shè)置為1,因?yàn)榫錁?biāo)簽只出現(xiàn)一次斯嚎。把它設(shè)置成高于1就會(huì)漏掉句子利虫。
- window: 句子中當(dāng)前單詞和預(yù)測(cè)單詞之間的最大距離。Word2Vec使用skip-gram模型堡僻,而這只是skip -gram模型的窗口大小糠惫。英文可以設(shè)置小一點(diǎn)例如10。(注意:在中文中钉疫,兩到四個(gè)字才能組成一個(gè)詞語硼讽,可以設(shè)置大一點(diǎn)15-25左右。)
- size: 特征向量在輸出中的維數(shù)牲阁。100是個(gè)好數(shù)字固阁。如果你是需要極端情況,這個(gè)值可以達(dá)到400左右城菊。大的size需要更多的訓(xùn)練數(shù)據(jù),但是效果會(huì)更好. 推薦值為幾十到幾百备燃。
- sample: 配置高頻詞的閾值。#高頻詞匯的隨機(jī)降采樣的配置閾值凌唬,默認(rèn)為1e-3并齐,官網(wǎng)給的解釋 1e-5效果比較好。設(shè)置為0時(shí)是詞最少的時(shí)候客税!不進(jìn)行降采樣况褪,結(jié)果詞少,當(dāng)設(shè)置1e-5更耻,相應(yīng)的詞展現(xiàn)更豐富测垛!
- workers: 使用這許多工作線程來訓(xùn)練模型。
* 新版本中迭代的輪數(shù)也在這里確定,用(iter)或者(epochs)參數(shù)來確定秧均,epochs是最新版本用法
model = Doc2Vec(min_count=1, window=10, size=100, sample=1e-4, negative=5, workers=7)
##新版本size改成了vector_size
#model = Doc2Vec(min_count=1, window=10, vector_size=100, sample=1e-4, negative=5, workers=7)
model.build_vocab(sentences.to_array())
訓(xùn)練 Doc2Vec
現(xiàn)在我們訓(xùn)練模型赐纱。如果在每一個(gè)訓(xùn)練階段脊奋,模型的訓(xùn)練都比較好,那么喂給模型的句子順序是隨機(jī)的疙描。這一點(diǎn)很重要:錯(cuò)過這個(gè)步驟會(huì)給你帶來非常糟糕的結(jié)果。這就是我們?yōu)槭裁次覀儭癓abeledLineSentences”類中有“sentences_perm”讶隐。
我們訓(xùn)練它10輪起胰。如果我有更多的時(shí)間,我就會(huì)做20個(gè)巫延。
這個(gè)過程大約需要10分鐘效五,所以去喝杯咖啡。
for epoch in range(10):
model.train(sentences.sentences_perm(),epochs=model.iter)
注意:建立詞匯表和訓(xùn)練Doc2Vec的部分使用了舊版本的寫法
可能會(huì)有以下錯(cuò)誤
ValueError: You must specify either total_examples or total_words, for proper alpha and progress calculations. The usual value is total_examples=model.corpus_count.
而且epochs=model.iter寫法也已經(jīng)過時(shí)炉峰,需要修改為model.epochs
新版本寫法如下:
model = Doc2Vec(vector_size=100, window=10, min_count=1, workers=7,sample=1e-4,epochs=10)
model.build_vocab(sentences.to_array())
model.train(sentences.to_array(), epochs=model.epochs, total_examples=model.corpus_count)
檢查模型
讓我們看看我們的模型給出了什么畏妖。它似乎有點(diǎn)理解good
這個(gè)詞,因?yàn)樽钕嗨频脑~是glamorous
, spectacular
, astounding
等等疼阔。
這真的很棒(而且很重要)戒劫,因?yàn)槲覀冋谧銮楦蟹治觥?/p>
我們看看那和‘good’最相似的詞,以及相似度婆廊。
model.most_similar('good')
注意:此方法已經(jīng)過時(shí)迅细,新版本:
model.wv.most_similar('good')
充分訓(xùn)練后的結(jié)果:
注意: 實(shí)際上word2vec學(xué)習(xí)的向量和真正語義還有差距,更多學(xué)到的是具備相似上下文的詞淘邻,比如“good”“bad”相似度也很高茵典,反而是文本分類任務(wù)輸入有監(jiān)督的語義能夠?qū)W到更好的語義表示。
我們還可以查看模型實(shí)際包含的內(nèi)容宾舅。這是模型中單詞和句子的每個(gè)向量统阿。我們可以使用模型訪問它們。 syn0(對(duì)于你們中的極客來說筹我,syn0只是淺層神經(jīng)網(wǎng)絡(luò)的輸出層扶平。)
但是,我們不想使用整個(gè)syn0崎溃,因?yàn)樗藛卧~的向量蜻直,我們只對(duì)句子感興趣。
以下是對(duì)負(fù)面評(píng)論的訓(xùn)練中第一句話的樣本向量:
model['TRAIN_NEG_0']
保存和加載模型
為了避免再次訓(xùn)練模型袁串,我們可以保存它概而。
model.save('./imdb.d2v')
And load it.
model = Doc2Vec.load('./imdb.d2v')
情感分類
訓(xùn)練向量
現(xiàn)在讓我們用這些向量來訓(xùn)練分類器。首先囱修,我們必須提取訓(xùn)練向量赎瑰。請(qǐng)記住,我們總共有25000個(gè)訓(xùn)練樣本破镰,有相同數(shù)量的正例和反例(12500正餐曼,12500個(gè)負(fù)數(shù))压储。因此,我們創(chuàng)建了一個(gè)numpy數(shù)組(因?yàn)槲覀兪褂玫姆诸惼髦唤邮躰umpy數(shù)組)源譬。有兩個(gè)平行數(shù)組集惋,一個(gè)包含向量(train_array),另一個(gè)包含標(biāo)簽(train_label)踩娘。
我們只是把正數(shù)放在數(shù)組的前半部分刮刑,負(fù)的放在下半部分。
train_arrays = numpy.zeros((25000, 100))
train_labels = numpy.zeros(25000)
for i in range(12500):
prefix_train_pos = 'TRAIN_POS_' + str(i)
prefix_train_neg = 'TRAIN_NEG_' + str(i)
train_arrays[i] = model[prefix_train_pos]
train_arrays[12500 + i] = model[prefix_train_neg]
train_labels[i] = 1
train_labels[12500 + i] = 0
訓(xùn)練數(shù)組看起來是這樣的:表示每個(gè)句子向量的行和行構(gòu)成了數(shù)組养渴。
print (train_arrays)
標(biāo)簽只是句子向量的類別標(biāo)簽——1代表正雷绢,0代表負(fù)。
print (train_labels)
[ 1. 1. 1. ..., 0. 0. 0.]
測(cè)試向量
對(duì)于測(cè)試數(shù)據(jù)理卑,我們也做同樣的事情——在我們使用訓(xùn)練數(shù)據(jù)訓(xùn)練它之后翘紊,我們要給分類器提供數(shù)據(jù)。
這使我們能夠評(píng)估我們的結(jié)果藐唠。這個(gè)過程和提取訓(xùn)練數(shù)據(jù)的結(jié)果差不多帆疟。
test_arrays = numpy.zeros((25000, 100))
test_labels = numpy.zeros(25000)
for i in range(12500):
prefix_test_pos = 'TEST_POS_' + str(i)
prefix_test_neg = 'TEST_NEG_' + str(i)
test_arrays[i] = model[prefix_test_pos]
test_arrays[12500 + i] = model[prefix_test_neg]
test_labels[i] = 1
test_labels[12500 + i] = 0
分類
現(xiàn)在我們使用訓(xùn)練數(shù)據(jù)訓(xùn)練邏輯回歸分類器。
classifier = LogisticRegression()
classifier.fit(train_arrays, train_labels)
接著發(fā)現(xiàn)我們?cè)谇榫w分析方面的有著接近87%的準(zhǔn)確度中捆。這是相當(dāng)難以置信的鸯匹,因?yàn)槲覀冎皇褂靡粋€(gè)線性支持向量機(jī)(代碼中明明是邏輯回歸……)和一個(gè)非常淺的神經(jīng)網(wǎng)絡(luò)。
classifier.score(test_arrays, test_labels)
0.86968000000000001
這不是很棒嗎?希望以上內(nèi)容能為你節(jié)省一些時(shí)間!
References
- Doc2vec: https://radimrehurek.com/gensim/models/doc2vec.html
- Paper that inspired this: http://arxiv.org/abs/1405.4053
上面內(nèi)容翻譯自原作者的github泄伪。
使用gensim包訓(xùn)練Word2Vec并不復(fù)雜殴蓬,麻煩的是把數(shù)據(jù)整理成Word2Vec和Doc2vec的所支持的格式,雖然官方文檔中提供了很多例子蟋滴,但是并不能批量處理染厅。以上作者給出了批量處理的方案,并使用常用的機(jī)器學(xué)習(xí)分類算法來分類津函。(聽說word2vec生成的詞向量搭配深度學(xué)習(xí)會(huì)更好肖粮,也可以考慮用RNN之類的深度學(xué)習(xí)來做分類)
改成中文只需要把讀取的那幾個(gè)txt文件修改成中文即可,每一行代表一個(gè)句子或者說一篇文章尔苦,因?yàn)槭莇oc2vec涩馆,所以也不需要做什么分詞或者去除停用詞之類的預(yù)處理,但是需要去除標(biāo)點(diǎn)符號(hào)之類的非中文的部分允坚。另外魂那,想要模型準(zhǔn)確,需要一個(gè)比較完善的語料庫(kù)稠项。