本文內(nèi)容源自于國外2015年的一篇博客,中文翻譯可以在伯樂在線看到??梢哉w了解一些word2vec和doc2vec的使用方法,但是由于時(shí)間過去很久了,gensim的api也發(fā)生了變化,因此特意重新在源代碼基礎(chǔ)上做了修改,也回顧一下word2vec和doc2vec的使用
環(huán)境要求
- python2.7或python3+
- gensim
- numpy
- matplotlib
情感分析基本原理
情感分析(Sentiment analysis)是自然語言處理(NLP)方法中常見的應(yīng)用,尤其是以提煉文本情緒內(nèi)容為目的的分類。利用情感分析這樣的方法,可以通過情感評(píng)分對(duì)定性數(shù)據(jù)進(jìn)行定量分析。雖然情感充滿了主觀性,但情感定量分析已經(jīng)有許多實(shí)用功能,例如企業(yè)藉此了解用戶對(duì)產(chǎn)品的反映,或者判別在線評(píng)論中的仇恨言論。
情感分析最簡單的形式就是借助包含積極和消極詞的字典。每個(gè)詞在情感上都有分值,通常 +1 代表積極情緒,-1 代表消極。接著,我們簡單累加句子中所有詞的情感分值來計(jì)算最終的總分。顯而易見,這樣的做法存在許多缺陷,最重要的就是忽略了語境(context)和鄰近的詞。例如一個(gè)簡單的短語“not good”最終的情感得分是 0,因?yàn)椤皀ot”是 -1,“good”是 +1。正常人會(huì)將這個(gè)短語歸類為消極情緒,盡管有“good”的出現(xiàn)。
另一個(gè)常見的做法是以文本進(jìn)行“詞袋(bag of words)”建模。我們把每個(gè)文本視為 1 到 N 的向量,N 是所有詞匯(vocabulary)的大小。每一列是一個(gè)詞,對(duì)應(yīng)的值是這個(gè)詞出現(xiàn)的次數(shù)。比如說短語“bag of bag of words”可以編碼為 [2, 2, 1]。這個(gè)值可以作為諸如邏輯回歸(logistic regression)、支持向量機(jī)(SVM)的機(jī)器學(xué)習(xí)算法的輸入,以此來進(jìn)行分類。這樣可以對(duì)未知的(unseen)數(shù)據(jù)進(jìn)行情感預(yù)測(cè)。注意這需要已知情感的數(shù)據(jù)通過監(jiān)督式學(xué)習(xí)的方式(supervised fashion)來訓(xùn)練。雖然和前一個(gè)方法相比有了明顯的進(jìn)步,但依然忽略了語境,而且數(shù)據(jù)的大小會(huì)隨著詞匯的大小增加。
Word2Vec 和 Doc2Vec
近幾年,Google 開發(fā)了名為 Word2Vec 新方法,既能獲取詞的語境,同時(shí)又減少了數(shù)據(jù)大小。Word2Vec 實(shí)際上有兩種不一樣的方法:CBOW(Continuous Bag of Words,連續(xù)詞袋)和 Skip-gram。對(duì)于 CBOW,目標(biāo)是在給定鄰近詞的情況下預(yù)測(cè)單獨(dú)的單詞。Skip-gram 則相反:我們希望給定一個(gè)單獨(dú)的詞(見圖 1)來預(yù)測(cè)某個(gè)范圍的詞。兩個(gè)方法都使用人工神經(jīng)網(wǎng)絡(luò)(Artificial Neural Networks)來作為它們的分類算法。首先,詞匯表中的每個(gè)單詞都是隨機(jī)的 N 維向量。在訓(xùn)練過程中,算法會(huì)利用 CBOW 或者 Skip-gram 來學(xué)習(xí)每個(gè)詞的最優(yōu)向量。

這些詞向量現(xiàn)在可以考慮到上下文的語境了。這可以看作是利用基本的代數(shù)式來挖掘詞的關(guān)系(例如:“king” – “man” + “woman” = “queen”)。這些詞向量可以作為分類算法的輸入來預(yù)測(cè)情感,有別于詞袋模型的方法。這樣的優(yōu)勢(shì)在于我們可以聯(lián)系詞的語境,并且我們的特征空間(feature space)的維度非常低(通常約為 300,相對(duì)于約為 100000 的詞匯)。在神經(jīng)網(wǎng)絡(luò)提取出這些特征之后,我們還必須手動(dòng)創(chuàng)建一小部分特征。由于文本長度不一,將以全體詞向量的均值作為分類算法的輸入來歸類整個(gè)文檔。
然而,即使使用了上述對(duì)詞向量取均值的方法,我們?nèi)匀缓雎粤嗽~序。Quoc Le 和 Tomas Mikolov 提出了 Doc2Vec 的方法對(duì)長度不一的文本進(jìn)行描述。這個(gè)方法除了在原有基礎(chǔ)上添加 paragraph / document 向量以外,基本和 Word2Vec 一致,也存在兩種方法:DM(Distributed Memory,分布式內(nèi)存)和分布式詞袋(DBOW)。DM 試圖在給定前面部分的詞和 paragraph 向量來預(yù)測(cè)后面單獨(dú)的單詞。即使文本中的語境在變化,但 paragraph 向量不會(huì)變化,并且能保存詞序信息。DBOW 則利用paragraph 來預(yù)測(cè)段落中一組隨機(jī)的詞(見圖 2)。

一旦經(jīng)過訓(xùn)練,paragraph 向量就可以作為情感分類器的輸入而不需要所有單詞。這是目前對(duì) IMDB 電影評(píng)論數(shù)據(jù)集進(jìn)行情感分類最先進(jìn)的方法,錯(cuò)誤率只有 7.42%。當(dāng)然,如果這個(gè)方法不實(shí)用,說這些都沒有意義。幸運(yùn)的是,一個(gè) Python 第三方庫 gensim 提供了 Word2Vec 和 Doc2Vec 的優(yōu)化版本。
Doc2vec預(yù)測(cè)IMDB評(píng)論情感分析
一旦文本上升到段落的規(guī)模,忽略詞序和上下文信息將面臨丟失大量特征的風(fēng)險(xiǎn)。這樣的情況下更適合使用 Doc2Vec 創(chuàng)建輸入特征。我們將使用 IMDB 電影評(píng)論數(shù)據(jù)集 作為示例來測(cè)試 Doc2Vec 在情感分析中的有效性。數(shù)據(jù)集中包含了 25,000 條積極評(píng)論,25,000 條消極評(píng)論和 50,000 條未標(biāo)記的電影評(píng)論。
數(shù)據(jù)準(zhǔn)備
鏈接:https://pan.baidu.com/s/1snfuPB3 密碼:v68x
導(dǎo)入依賴庫
# gensim modules
from gensim import utils
from gensim.models.doc2vec import TaggedDocument
from gensim.models import Doc2Vec
# numpy
import numpy as np
# classifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import logging
import sys
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
%matplotlib inline
讀取影評(píng)內(nèi)容
with utils.smart_open('./data/pos.txt','r',encoding='utf-8') as infile:
pos_reviews = []
line = infile.readline()
while line:
pos_reviews.append(line)
line = infile.readline()
with utils.smart_open('./data/neg.txt','r',encoding='utf-8') as infile:
neg_reviews = []
line = infile.readline()
while line:
neg_reviews.append(line)
line = infile.readline()
with utils.smart_open('./data/unsup.txt','r',encoding='utf-8') as infile:
unsup_reviews = []
line = infile.readline()
while line:
unsup_reviews.append(line)
line = infile.readline()
數(shù)據(jù)劃分
# 1 代表積極情緒,0 代表消極情緒
y = np.concatenate((np.ones(len(pos_reviews)), np.zeros(len(neg_reviews))))
x_train, x_test, y_train, y_test = train_test_split(np.concatenate((pos_reviews, neg_reviews)), y, test_size=0.2)
創(chuàng)建TaggedDocument對(duì)象
Gensim 的 Doc2Vec 工具要求每個(gè)文檔/段落包含一個(gè)與之關(guān)聯(lián)的標(biāo)簽。我們利用 TaggedDocument進(jìn)行處理。格式形如 “TRAIN_i” 或者 “TEST_i”,其中 “i” 是索引
import gensim
def labelizeReviews(reviews, label_type):
for i,v in enumerate(reviews):
label = '%s_%s'%(label_type,i)
yield gensim.models.doc2vec.TaggedDocument(gensim.utils.simple_preprocess(v,max_len=100), [label])
x_train_tag = list(labelizeReviews(x_train, 'train'))
x_test_tag = list(labelizeReviews(x_test, 'test'))
unsup_reviews_tag = list(labelizeReviews(unsup_reviews, 'unsup'))
實(shí)例化Doc2vec模型
下面我們實(shí)例化兩個(gè) Doc2Vec 模型,DM 和 DBOW。gensim 文檔建議多次訓(xùn)練數(shù)據(jù),并且在每一步(pass)調(diào)節(jié)學(xué)習(xí)率(learning rate)或者用隨機(jī)順序輸入文本。接著我們收集了通過模型訓(xùn)練后的電影評(píng)論向量。DM 和 DBOW會(huì)進(jìn)行向量疊加,這是因?yàn)閮蓚€(gè)向量疊加后可以獲得更好的結(jié)果
size = 100
# 實(shí)例化 DM 和 DBOW 模型
log.info('D2V')
model_dm = gensim.models.Doc2Vec(min_count=1, window=10, vector_size=size, sample=1e-3, negative=5, workers=3,epochs=10)
model_dbow = gensim.models.Doc2Vec(min_count=1, window=10, vector_size=size, sample=1e-3, negative=5, dm=0, workers=3,epochs=10)
# 對(duì)所有評(píng)論創(chuàng)建詞匯表
alldata = x_train_tag
alldata.extend(x_test_tag)
alldata.extend(unsup_reviews_tag)
model_dm.build_vocab(alldata)
model_dbow.build_vocab(alldata)
def sentences_perm(sentences):
shuffled = list(sentences)
random.shuffle(shuffled)
return (shuffled)
for epoch in range(10):
log.info('EPOCH: {}'.format(epoch))
model_dm.train(sentences_perm(alldata),total_examples=model_dm.corpus_count,epochs=1)
model_dbow.train(sentences_perm(alldata),total_examples=model_dbow.corpus_count,epochs=1)
獲取生成的向量
獲取向量有兩種方式,一種是根據(jù)上面我們定義的標(biāo)簽來獲取,另一種通過輸入一篇文章的內(nèi)容來獲取這篇文章的向量。更推薦使用第一種方式來獲取向量。
#第一種方法
train_arrays_dm = numpy.zeros((len(x_train), 100))
train_arrays_dbow = numpy.zeros((len(x_train), 100))
for i in range(len(x_train)):
tag = 'train_' + str(i)
train_arrays_dm[i] = model_dm.docvecs[tag]
train_arrays_dbow[i] = model_dbow.docvecs[tag]
train_arrays = np.hstack((train_arrays_dm, train_arrays_dbow))
test_arrays_dm = numpy.zeros((len(x_test), 100))
test_arrays_dbow = numpy.zeros((len(x_test), 100))
for i in range(len(x_test)):
tag = 'test_' + str(i)
test_arrays_dm[i] = model_dm.docvecs[tag]
test_arrays_dbow[i] = model_dbow.docvecs[tag]
test_arrays = np.hstack((test_arrays_dm, test_arrays_dbow))
#第二種
def getVecs(model, corpus):
vecs = []
for i in corpus:
vec = model.infer_vector(gensim.utils.simple_preprocess(i,max_len=300))
vecs.append(vec)
return vecs
train_vecs_dm = getVecs(model_dm, x_train)
train_vecs_dbow = getVecs(model_dbow, x_train)
train_vecs = np.hstack((train_vecs_dm, train_vecs_dbow))
預(yù)測(cè)
通過預(yù)測(cè)我們得到了88%的正確率,原論文為90+,這和我們訓(xùn)練的epoch有關(guān)系,也和眾多的超參數(shù)有關(guān)系
classifier = LogisticRegression()
classifier.fit(train_arrays, y_train)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, penalty='l2', random_state=None, tol=0.0001)
log.info(classifier.score(test_arrays, y_test))
y_prob = classifier.predict_proba(test_arrays)[:,1]
fpr,tpr,_ = roc_curve(y_test, y_prob)
roc_auc = auc(fpr,tpr)
plt.plot(fpr,tpr,label='area = %.2f' %roc_auc)
plt.plot([0, 1], [0, 1], 'k--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.legend(loc='lower right')


word2vec預(yù)測(cè)
上面我們用doc2vec預(yù)測(cè)的,下面我們用word2vec進(jìn)行預(yù)測(cè)看看差距有多大。為了結(jié)構(gòu)化分類器的輸入,我們對(duì)一篇文章所有詞向量之和取均值。最后得到結(jié)果為72%
# gensim modules
from gensim import utils
from gensim.models import Word2Vec
# numpy
import numpy as np
# classifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import logging
import sys
log = logging.getLogger()
log.setLevel(logging.INFO)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
log.addHandler(ch)
with utils.smart_open('./data/pos.txt','r',encoding='utf-8') as infile:
pos_reviews = []
line = infile.readline()
while line:
pos_reviews.append(line)
line = infile.readline()
with utils.smart_open('./data/neg.txt','r',encoding='utf-8') as infile:
neg_reviews = []
line = infile.readline()
while line:
neg_reviews.append(line)
line = infile.readline()
with utils.smart_open('./data/unsup.txt','r',encoding='utf-8') as infile:
unsup_reviews = []
line = infile.readline()
while line:
unsup_reviews.append(line)
line = infile.readline()
# 1 代表積極情緒,0 代表消極情緒
y = np.concatenate((np.ones(len(pos_reviews)), np.zeros(len(neg_reviews))))
x_train, x_test, y_train, y_test = train_test_split(np.concatenate((pos_reviews, neg_reviews)), y, test_size=0.2)
import gensim
def labelizeReviews(reviews):
print(len(reviews))
for i,v in enumerate(reviews):
yield gensim.utils.simple_preprocess(v,max_len=100)
x_train_tag = list(labelizeReviews(x_train))
x_test_tag = list(labelizeReviews(x_test))
unsup_reviews_tag = list(labelizeReviews(unsup_reviews))
size = 100
# 實(shí)例化 DM 和 DBOW 模型
log.info('D2V')
model = Word2Vec(size=200,window=10,min_count=1)
# 對(duì)所有評(píng)論創(chuàng)建詞匯表
alldata = x_train_tag
alldata.extend(x_test_tag)
alldata.extend(unsup_reviews_tag)
model.build_vocab(alldata)
import random
def sentences_perm(sentences):
shuffled = list(sentences)
random.shuffle(shuffled)
return (shuffled)
log.info('Epoch')
for epoch in range(10):
log.info('EPOCH: {}'.format(epoch))
model.train(sentences_perm(alldata),total_examples=model.corpus_count,epochs=1)
# 對(duì)訓(xùn)練數(shù)據(jù)集創(chuàng)建詞向量,接著進(jìn)行比例縮放(scale)。
size=200
def buildWordVector(text):
vec = np.zeros(size).reshape((1, size))
count = 0.
for word in text:
try:
vec += model[word]
count += 1.
except KeyError:
continue
if count != 0:
vec /= count
return vec
from sklearn.preprocessing import scale
train_vecs = np.concatenate([buildWordVector(gensim.utils.simple_preprocess(z,max_len=200)) for z in x_train])
train_vecs = scale(train_vecs)
test_vecs = np.concatenate([buildWordVector(gensim.utils.simple_preprocess(z,max_len=200)) for z in x_test])
test_vecs = scale(test_vecs)
classifier = LogisticRegression()
classifier.fit(train_vecs, y_train)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, penalty='l2', random_state=None, tol=0.0001)
log.info(classifier.score(test_vecs, y_test))
后續(xù)工作
參考GitHub上一篇文章比較word2vec與FastText