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阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容