Doc2vec預測IMDB評論情感

本文內容源自于國外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 來學習每個詞的最優向量。

W(t) 代表當前的單詞,而w(t-2), w(t-1) 等則是鄰近的單詞

這些詞向量現在可以考慮到上下文的語境了。這可以看作是利用基本的代數式來挖掘詞的關系(例如:“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)。

選自《Distributed Representations of Sentences and Documents》

一旦經過訓練,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')
roc
image.png

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容