摘要:本文主要描述了一种文章向量(doc2vec)表示及其训练的相关内容,并列出相关例子。两位大牛Quoc Le 和 Tomas Mikolov(搞出Word2vec的家伙)在2014年的《Distributed Representations of Sentences and Documents》所提出文章向量(Documents vector),或者称句向量(Sentences vector),当然在文章中,统一称这种向量为Paragraph Vector,本文也将已doc2vec称呼之。文章中讲述了如何将文章转换成向量表示的算法。
先简述一下Word2vec相关原理,因为本文要讲述的doc2vec是基于Word2vec思想的算法。w2v的数学知识还比较丰富,网络上相关资料也很多。如果要系统的讲述,我可能会涉及包括词向量的理解、sigmoid函数、逻辑回归、Bayes公式、Huffman编码、n-gram模型、浅层神经网络、激活函数、最大似然及其梯度推导、随机梯度下降法、词向量与模型参数的更新公式、CBOW模型和 Skip-gram模型、Hierarchical Softmax算法和Negative Sampling算法。当然还会结合google发布的C源码(好像才700+行),讲述相关部分的实现细节,比如Negative Sampling算法如何随机采样、参数更新的细节、sigmod的快速近似计算、词典的hash存储、低频与高频词的处理、窗口内的采样方式、自适应学习、参数初始化、w2v实际上含有两中方法等,用C代码仅仅700+行实现,并加入了诸多技巧,推荐初识w2v的爱好者得看一看。
Google出品的大多都是精品 ,w2v也不例外。Word2Vec实际上使用了两种方法,Continuous Bag of Words (CBOW) 和Skip-gram,如下图所示。在CBOW方法中,目的是将文章中某个词的上下文经过模型预测该词。而Skip-gram方法则是用给定的词来预测其周边的词。而词向量是在训练模型中所得到的一个副产品,此模型在源码中是为一个浅层的神经网络(3层)。在训练前,每一个词都会首先初始化为一个N维的向量,训练过程中,会对输入的向量进行反馈更新,在进行大量语料训练之后,便可得到每一个词相应的训练向量。而每一种模型方法都可以使用两种对应的训练方法Hierarchical Softmax算法和Negative Sampling算法,有兴趣的盆友可以自行查阅相关内容。
训练出的向量有一定的特性,即相近意义的词在向量空间上其距离也是相近。
有一个经典例子就是 V(‘king’) – V(‘man’) + V(‘woman’) ≈ V(‘queen’)
Doc2Vec 也叫做 paragraph2vec, sentence embeddings,是一种非监督式算法,可以获得 sentences/paragraphs/documents 的向量表达,是 word2vec 的拓展。学出来的向量可以通过计算距离来找 sentences/paragraphs/documents 之间的相似性,
或者进一步可以给文档打标签。
基于上述的Word2Vec的方法,Quoc Le 和Tomas Mikolov又给出了Doc2Vec的训练方法。如下图所示,其原理与Word2Vec非常的相似。分为Distributed Memory (DM) 和Distributed Bag of Words (DBOW),可以看出 Distributed Memory version of Paragraph Vector
(PV-DM)方法与Word2Vec的CBOW方法类似,Bag of Words version of Paragraph Vector (PV-DBOW)与Word2Vec的Skip-gram方法类似。不同的是,给文章也配置了向量,并在训练过程中更新。熟悉了w2v之后,Doc2Vec便非常好理解。具体细节可以看原文《Distributed Representations of Sentences and Documents》
1. A distributed memory model
训练句向量的方法和词向量的方法非常类似。训练词向量的核心思想就是说可以根据每个单词的上下文预测,也就是说上下文的单词对是有影响的。那么同理,可以用同样的方法训练doc2vec。例如对于一个句子i want to drink water,如果要去预测句子中的单词want,那么不仅可以根据其他单词生成feature, 也可以根据其他单词和句子来生成feature进行预测。因此doc2vec的框架如下所示:
每个段落/句子都被映射到向量空间中,可以用矩阵的一列来表示。每个单词同样被映射到向量空间,可以用矩阵的一列来表示。然后将段落向量和词向量级联或者求平均得到特征,预测句子中的下一个单词。
这个段落向量/句向量也可以认为是一个单词,它的作用相当于是上下文的记忆单元或者是这个段落的主题,所以我们一般叫这种训练方法为Distributed Memory Model of Paragraph Vectors(PV-DM)
在训练的时候我们固定上下文的长度,用滑动窗口的方法产生训练集。段落向量/句向量 在该上下文中共享。
总结doc2vec的过程, 主要有两步:
gensim 实现:
model = gensim.models.Doc2Vec(documents,dm = 1, alpha=0.1, size= 20, min_alpha=0.025)
2. Paragraph Vector without word ordering: Distributed bag of words
还有一种训练方法是忽略输入的上下文,让模型去预测段落中的随机一个单词。就是在每次迭代的时候,从文本中采样得到一个窗口,再从这个窗口中随机采样一个单词作为预测任务,让模型去预测,输入就是段落向量。如下所示:
我们称这种模型为 Distributed Bag of Words version of Paragraph Vector(PV-DBOW)
在上述两种方法中,我们可以使用PV-DM或者PV-DBOW得到段落向量/句向量。对于大多数任务,PV-DM的方法表现很好,但我们也强烈推荐两种方法相结合。
gensim 实现:
model = gensim.models.Doc2Vec(documents,dm = 0, alpha=0.1, size= 20, min_alpha=0.025)
二者在 gensim 实现时的区别是 dm = 0 还是 1.
Doc2Vec 的目的是获得文档的一个固定长度的向量表达。
数据:多个文档,以及它们的标签,可以用标题作为标签。
影响模型准确率的因素:语料的大小,文档的数量,越多越高;文档的相似性,越相似越好。
这里要用到 Gensim 的 Doc2Vec:
import gensim
LabeledSentence = gensim.models.doc2vec.LabeledSentence
from os import listdir
from os.path import isfile, join
docLabels = []
docLabels = [f for f in listdir("myDirPath") if f.endswith('.txt')]
data = []
for doc in docLabels:
data.append(open(“myDirPath/” + doc, ‘r’)
class LabeledLineSentence(object):
def __init__(self, filename):
self.filename = filename
def __iter__(self):
for uid, line in enumerate(open(filename)):
yield LabeledSentence(words=line.split(), labels=[‘SENT_%s’ % uid])
如果是用文档集合来训练模型,则用:
class LabeledLineSentence(object):
def __init__(self, doc_list, labels_list):
self.labels_list = labels_list
self.doc_list = doc_list
def __iter__(self):
for idx, doc in enumerate(self.doc_list):
yield LabeledSentence(words=doc.split(),labels=[self.labels_list[idx]])
在 gensim 中模型是以单词为单位训练的,所以不管是句子还是文档都分解成单词。
将 data, docLabels 传入到 LabeledLineSentence 中,
训练 Doc2Vec,并保存模型:
it = LabeledLineSentence(data, docLabels)
model = gensim.models.Doc2Vec(size=300, window=10, min_count=5, workers=11,alpha=0.025, min_alpha=0.025)
model.build_vocab(it)
for epoch in range(10):
model.train(it)
model.alpha -= 0.002 # decrease the learning rate
model.min_alpha = model.alpha # fix the learning rate, no deca
model.train(it)
model.save(“doc2vec.model”)测试模型:
Gensim 中有内置的 most_similar:
print (model.most_similar(“documentFileNameInYourDataFolder”))
model[“documentFileNameInYourDataFolder”]
使用Doc2Vec进行分类任务,我们使用 IMDB电影评论数据集作为分类例子,测试gensim的Doc2Vec的有效性。数据集中包含25000条正向评价,25000条负面评价以及50000条未标注评价。
#!/usr/bin/python
import sys
import numpy as np
import gensim
from gensim.models.doc2vec import Doc2Vec,LabeledSentence
from sklearn.cross_validation import train_test_split
LabeledSentence = gensim.models.doc2vec.LabeledSentence
##读取并预处理数据
def get_dataset():
#读取数据
with open(pos_file,'r') as infile:
pos_reviews = infile.readlines()
with open(neg_file,'r') as infile:
neg_reviews = infile.readlines()
with open(unsup_file,'r') as infile:
unsup_reviews = infile.readlines()
#使用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)
#对英文做简单的数据清洗预处理,中文根据需要进行修改
def cleanText(corpus):
punctuation = """.,?!:;(){}[]"""
corpus = [z.lower().replace('\n','') for z in corpus]
corpus = [z.replace('
', ' ') for z in corpus]
#treat punctuation as individual words
for c in punctuation:
corpus = [z.replace(c, ' %s '%c) for z in corpus]
corpus = [z.split() for z in corpus]
return corpus
x_train = cleanText(x_train)
x_test = cleanText(x_test)
unsup_reviews = cleanText(unsup_reviews)
#Gensim的Doc2Vec应用于训练要求每一篇文章/句子有一个唯一标识的label.
#我们使用Gensim自带的LabeledSentence方法. 标识的格式为"TRAIN_i"和"TEST_i",其中i为序号
def labelizeReviews(reviews, label_type):
labelized = []
for i,v in enumerate(reviews):
label = '%s_%s'%(label_type,i)
labelized.append(LabeledSentence(v, [label]))
return labelized
x_train = labelizeReviews(x_train, 'TRAIN')
x_test = labelizeReviews(x_test, 'TEST')
unsup_reviews = labelizeReviews(unsup_reviews, 'UNSUP')
return x_train,x_test,unsup_reviews,y_train, y_test
##读取向量
def getVecs(model, corpus, size):
vecs = [np.array(model.docvecs[z.tags[0]]).reshape((1, size)) for z in corpus]
return np.concatenate(vecs)
##对数据进行训练
def train(x_train,x_test,unsup_reviews,size = 400,epoch_num=10):
#实例DM和DBOW模型
model_dm = gensim.models.Doc2Vec(min_count=1, window=10, size=size, sample=1e-3, negative=5, workers=3)
model_dbow = gensim.models.Doc2Vec(min_count=1, window=10, size=size, sample=1e-3, negative=5, dm=0, workers=3)
#使用所有的数据建立词典
model_dm.build_vocab(np.concatenate((x_train, x_test, unsup_reviews)))
model_dbow.build_vocab(np.concatenate((x_train, x_test, unsup_reviews)))
#进行多次重复训练,每一次都需要对训练数据重新打乱,以提高精度
all_train_reviews = np.concatenate((x_train, unsup_reviews))
for epoch in range(epoch_num):
perm = np.random.permutation(all_train_reviews.shape[0])
model_dm.train(all_train_reviews[perm])
model_dbow.train(all_train_reviews[perm])
#训练测试数据集
x_test = np.array(x_test)
for epoch in range(epoch_num):
perm = np.random.permutation(x_test.shape[0])
model_dm.train(x_test[perm])
model_dbow.train(x_test[perm])
return model_dm,model_dbow
##将训练完成的数据转换为vectors
def get_vectors(model_dm,model_dbow):
#获取训练数据集的文档向量
train_vecs_dm = getVecs(model_dm, x_train, size)
train_vecs_dbow = getVecs(model_dbow, x_train, size)
train_vecs = np.hstack((train_vecs_dm, train_vecs_dbow))
#获取测试数据集的文档向量
test_vecs_dm = getVecs(model_dm, x_test, size)
test_vecs_dbow = getVecs(model_dbow, x_test, size)
test_vecs = np.hstack((test_vecs_dm, test_vecs_dbow))
return train_vecs,test_vecs
##使用分类器对文本向量进行分类训练
def Classifier(train_vecs,y_train,test_vecs, y_test):
#使用sklearn的SGD分类器
from sklearn.linear_model import SGDClassifier
lr = SGDClassifier(loss='log', penalty='l1')
lr.fit(train_vecs, y_train)
print 'Test Accuracy: %.2f'%lr.score(test_vecs, y_test)
return lr
##绘出ROC曲线,并计算AUC
def ROC_curve(lr,y_test):
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
pred_probas = lr.predict_proba(test_vecs)[:,1]
fpr,tpr,_ = roc_curve(y_test, pred_probas)
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.show()
##运行模块
if __name__ == "__main__":
#设置向量维度和训练次数
size,epoch_num = 400,10
#获取训练与测试数据及其类别标注
x_train,x_test,unsup_reviews,y_train, y_test = get_dataset()
#对数据进行训练,获得模型
model_dm,model_dbow = train(x_train,x_test,unsup_reviews,size,epoch_num)
#从模型中抽取文档相应的向量
train_vecs,test_vecs = get_vectors(model_dm,model_dbow)
#使用文章所转换的向量进行情感正负分类训练
lr=Classifier(train_vecs,y_train,test_vecs, y_test)
#画出ROC曲线
ROC_curve(lr,y_test)
训练结果的,test分类精度为86%,AUC面积为0.94
model = Doc2Vec.load("model.model",mmap='r')
#推测文本的向量
Vector1 = model.infer_vector(text1,steps=6,alpha=0.025)
Vector2 = model.infer_vector(text2,steps=6,alpha=0.025)
发现模型load一次,用这个模型对同一段文本推测两次,两次结果是不一样的,即下面的v1和v2是不一样的。
关于gensim的doc2vec该如何推测非训练语料库里文本的向量(训练语料库里的文本直接用函数model.docvecs[]便可求得),查了一些资料,大概有以下3种方法:
方法一:
模型加载后,每次推测之前都要设置model.random.seed()
model = Doc2Vec.load("model.model",mmap='r')
model.random.seed(0)
v1 = model.infer_vector(text1,steps=6,alpha=0.025)
model.random.seed(0)
v2 = model.infer_vector(text1,steps=6,alpha=0.025)
方法2:
每次推测之前都要加载一次模型,这种方式太耗时,不适合频繁使用。
model = Doc2Vec.load("model.model",mmap='r')
v1 = model.infer_vector(text1,steps=6,alpha=0.025)
model = Doc2Vec.load("model.model",mmap='r'
v2 = model.infer_vector(text1,steps=6,alpha=0.025)
数据量大时,预处理的脚本要写成分布式的形式
数据量大时,写“数据预处理“脚本一定要写成分布式的。
预处理时要给数据去重和去噪
模型加载速度慢
“文本相似度计算“这是一个需要实时跑的需求,也就是说我需要根据web页面点击实时加载模型,推测向量,计算余弦值。但加载模型(模型1.28g)就花了4s,太慢,这个问题后来解决了。
计算文本相似性的思考
可以用两段文本重合词百分比来衡量两段文本是否在说同一件事,如是否都在说武侠,是否都在说天气,但更细一层的语义便没法衡量了。
模型评估的思考
有老师说相似度值的大小并不能确切地说明模型的好与不好,所以设计了衡量标准:不同模型,根据文本的topN是否稳定来决定调参的方向。
于此同时,尽管有老师说“不能看相似度的数值”来判断模型好和不好,我还是认为数值具有借鉴意义。模型最终确定时,用完全一样的两个文本的相似度来衡量“模型阐述文本相似的能力”。如300组完全一样的文本,通过查看“相似度值聚集的范围”,来衡量“模型阐述文本相似的能力”。
参考资料
https://arxiv.org/abs/1405.4053
https://rare-technologies.com/doc2vec-tutorial/
https://medium.com/@klintcho/doc2vec-tutorial-using-gensim-ab3ac03d3a1
https://blog.csdn.net/aliceyangxi1987/article/details/75097598
https://blog.csdn.net/john_xyz/article/details/79208564
https://blog.csdn.net/lenbow/article/details/52120230
相关文章:
word2vec 模型思想和代码实现
怎样做情感分析
推测非训练语料库里文本的向量-model.random.seed()等
推测非训练语料库里文本的向量-每次推测前模型都加载一次
基于gensim的Doc2Vec简析