本文內容源自于國外2015年的一篇博客,中文翻譯可以在伯樂在線看到。可以整體了解一些word2vec和doc2vec的使用方法,但是由于時間過去很久了,gensim的api也發生了變化,因此特意重新在源代碼基礎上做了修改,也回顧一下word2vec和doc2vec的使用
環境要求
- python2.7或python3+
- gensim
- numpy
- matplotlib
情感分析基本原理
情感分析(Sentiment analysis)是自然語言處理(NLP)方法中常見的應用,尤其是以提煉文本情緒內容為目的的分類。利用情感分析這樣的方法,可以通過情感評分對定性數據進行定量分析。雖然情感充滿了主觀性,但情感定量分析已經有許多實用功能,例如企業藉此了解用戶對產品的反映,或者判別在線評論中的仇恨言論。
情感分析最簡單的形式就是借助包含積極和消極詞的字典。每個詞在情感上都有分值,通常 +1 代表積極情緒,-1 代表消極。接著,我們簡單累加句子中所有詞的情感分值來計算最終的總分。顯而易見,這樣的做法存在許多缺陷,最重要的就是忽略了語境(context)和鄰近的詞。例如一個簡單的短語“not good”最終的情感得分是 0,因為“not”是 -1,“good”是 +1。正常人會將這個短語歸類為消極情緒,盡管有“good”的出現。
另一個常見的做法是以文本進行“詞袋(bag of words)”建模。我們把每個文本視為 1 到 N 的向量,N 是所有詞匯(vocabulary)的大小。每一列是一個詞,對應的值是這個詞出現的次數。比如說短語“bag of bag of words”可以編碼為 [2, 2, 1]。這個值可以作為諸如邏輯回歸(logistic regression)、支持向量機(SVM)的機器學習算法的輸入,以此來進行分類。這樣可以對未知的(unseen)數據進行情感預測。注意這需要已知情感的數據通過監督式學習的方式(supervised fashion)來訓練。雖然和前一個方法相比有了明顯的進步,但依然忽略了語境,而且數據的大小會隨著詞匯的大小增加。
Word2Vec 和 Doc2Vec
近幾年,Google 開發了名為 Word2Vec 新方法,既能獲取詞的語境,同時又減少了數據大小。Word2Vec 實際上有兩種不一樣的方法:CBOW(Continuous Bag of Words,連續詞袋)和 Skip-gram。對于 CBOW,目標是在給定鄰近詞的情況下預測單獨的單詞。Skip-gram 則相反:我們希望給定一個單獨的詞(見圖 1)來預測某個范圍的詞。兩個方法都使用人工神經網絡(Artificial Neural Networks)來作為它們的分類算法。首先,詞匯表中的每個單詞都是隨機的 N 維向量。在訓練過程中,算法會利用 CBOW 或者 Skip-gram 來學習每個詞的最優向量。
這些詞向量現在可以考慮到上下文的語境了。這可以看作是利用基本的代數式來挖掘詞的關系(例如:“king” – “man” + “woman” = “queen”)。這些詞向量可以作為分類算法的輸入來預測情感,有別于詞袋模型的方法。這樣的優勢在于我們可以聯系詞的語境,并且我們的特征空間(feature space)的維度非常低(通常約為 300,相對于約為 100000 的詞匯)。在神經網絡提取出這些特征之后,我們還必須手動創建一小部分特征。由于文本長度不一,將以全體詞向量的均值作為分類算法的輸入來歸類整個文檔。
然而,即使使用了上述對詞向量取均值的方法,我們仍然忽略了詞序。Quoc Le 和 Tomas Mikolov 提出了 Doc2Vec 的方法對長度不一的文本進行描述。這個方法除了在原有基礎上添加 paragraph / document 向量以外,基本和 Word2Vec 一致,也存在兩種方法:DM(Distributed Memory,分布式內存)和分布式詞袋(DBOW)。DM 試圖在給定前面部分的詞和 paragraph 向量來預測后面單獨的單詞。即使文本中的語境在變化,但 paragraph 向量不會變化,并且能保存詞序信息。DBOW 則利用paragraph 來預測段落中一組隨機的詞(見圖 2)。
一旦經過訓練,paragraph 向量就可以作為情感分類器的輸入而不需要所有單詞。這是目前對 IMDB 電影評論數據集進行情感分類最先進的方法,錯誤率只有 7.42%。當然,如果這個方法不實用,說這些都沒有意義。幸運的是,一個 Python 第三方庫 gensim 提供了 Word2Vec 和 Doc2Vec 的優化版本。
Doc2vec預測IMDB評論情感分析
一旦文本上升到段落的規模,忽略詞序和上下文信息將面臨丟失大量特征的風險。這樣的情況下更適合使用 Doc2Vec 創建輸入特征。我們將使用 IMDB 電影評論數據集 作為示例來測試 Doc2Vec 在情感分析中的有效性。數據集中包含了 25,000 條積極評論,25,000 條消極評論和 50,000 條未標記的電影評論。
數據準備
鏈接:https://pan.baidu.com/s/1snfuPB3 密碼:v68x
導入依賴庫
# 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
讀取影評內容
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)
創建TaggedDocument對象
Gensim 的 Doc2Vec 工具要求每個文檔/段落包含一個與之關聯的標簽。我們利用 TaggedDocument進行處理。格式形如 “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'))
實例化Doc2vec模型
下面我們實例化兩個 Doc2Vec 模型,DM 和 DBOW。gensim 文檔建議多次訓練數據,并且在每一步(pass)調節學習率(learning rate)或者用隨機順序輸入文本。接著我們收集了通過模型訓練后的電影評論向量。DM 和 DBOW會進行向量疊加,這是因為兩個向量疊加后可以獲得更好的結果
size = 100
# 實例化 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)
# 對所有評論創建詞匯表
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)
獲取生成的向量
獲取向量有兩種方式,一種是根據上面我們定義的標簽來獲取,另一種通過輸入一篇文章的內容來獲取這篇文章的向量。更推薦使用第一種方式來獲取向量。
#第一種方法
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))
預測
通過預測我們得到了88%的正確率,原論文為90+,這和我們訓練的epoch有關系,也和眾多的超參數有關系
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預測
上面我們用doc2vec預測的,下面我們用word2vec進行預測看看差距有多大。為了結構化分類器的輸入,我們對一篇文章所有詞向量之和取均值。最后得到結果為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
# 實例化 DM 和 DBOW 模型
log.info('D2V')
model = Word2Vec(size=200,window=10,min_count=1)
# 對所有評論創建詞匯表
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)
# 對訓練數據集創建詞向量,接著進行比例縮放(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))
后續工作
參考GitHub上一篇文章比較word2vec與FastText