如何将一个句子、段落、或者文档用一个向量表示?词袋模型,该模型将每个文档转换为固定长度的整数向量。例如,给定以下句子:
John likes to watch movies. Mary likes movies too. John also likes to watch football games. Mary hates football.
模型输出向量:
[1, 2, 1, 1, 2, 1, 1, 0, 0, 0, 0] [1, 1, 1, 1, 0, 1, 0, 1, 2, 1, 1]
每个向量有 10 个元素,其中每个元素计算特定单词在文档中出现的次数。元素的顺序是任意的。在上面的示例中,元素的顺序对应于单词:
["John", "likes", "to", "watch", "movies", "Mary", "too", "also", "football", "games", "hates"]
词袋模型非常有效,但是存在以下两个缺点:
- 它们丢失了所有关于词序的信息:”John likes Mary” 和 “Mary likes John” 对应于相同的向量。有一个解决方案:n-gram 模型袋考虑长度为 n 的词短语将文档表示为固定长度的向量,以捕获局部词序,但会受到数据稀疏和高维的影响。
- 该模型不会尝试学习潜在单词的含义,词袋模型表示的向量之间的距离并不能够反映单词含义的差异。
Word2Vec 使用浅层神经网络将单词嵌入到低维向量空间中。其中,向量距离较近表示其在原文上下文中具有相似的含义。例如,strong 和 powerful 会很近,strong 和 Paris 会相对较远。
使用该Word2Vec模型,我们可以计算文档中每个单词的向量。但是如果我们想为整个文档计算一个向量呢?我们可以平均文档中每个单词的向量——虽然这既快速又粗略,但通常很有用。不过,还有更好的办法……
doc2vec paper :https://arxiv.org/pdf/1405.4053.pdf
下面主要根据论文描述了 doc2vector 的两种用于产生 doc vector 的模型,它们分别是:
- PV-DM(Distributed Memory Model of Paragraph Vector)
- PV-DBOW(Distributed Bag Of Words Version of Paragraph Vector)
1. PV-DM
在 CBOW 中,我们输入上下文词,来预测中心词。简单的过程是输入 the、cat、sat 向量,并将向量拼接或者取平均,从而去预测 on 单词。

将上面的模型改动一下,在输入的地方增加一个额外的向量 D, 用来表示句子向量。即:输入除了 the、cat、sat 之外,还多了一个向量 D,将这 4 个向量平均或者拼接,去预测 on 单词。

上图中多出的 D 向量表示段落标记(paragraph token),可以被当做一个特殊的词,它扮演了记忆的角色。我想到了 RNN,它在每个时间步不停在向一个向量上去累积句子的信息,把最后一个时间步得到的向量作为整个句子的向量表示。只不过在这里这个用于累积信息的向量就是 D 而已。
模型的输入和 word2vec 是一样的,通过滑动窗口,将上下文词作为输入,其中一个词作为预测。而 D 向量则是在输入每个窗口词时,都要添加进来的,即:这个段落在滑动过程中,都需要输入 D 向量。但是 D 向量不在不同段落之间共享,段落不同就需要重新初始化 D 向量。
模型训练完成后,我们如何得到一个输入句子的向量呢?
RNN 训练完成之后,参数就会固定,我们依次累积计算每个时间步,最终就会得到句子的向量表示。PV-DM 模型训练完成之后,其参数是固定的,我们只需要将句子按照滑动窗口,扔到模型里进行计算,并计算损失,更新 D 向量的参数(其他学习完的参数不更新),当损失近乎稳定时,我们就可以拿到 D 向量做为句子向量表示了。
2. PV-DBOW
这种方式输入:段落向量 D,预测:随机从段落中抽取一个窗口,从窗口中随机抽取一个词。

每个句子向量可以由 PV-DM、PV-DBOW 产生,大多数任务使用 PV-DM 就可以胜任。但是作者建议结合两者来使用。
3. 训练词向量
Gensim 的 doc2vec 实现默认使用的是 pv-dm 模型,也可以通过设置 dm=0 来强制使用 DBOW 模型。
from gensim.models.doc2vec import TaggedDocument
from gensim.models.doc2vec import Doc2Vec
import pandas as pd
from gensim.models.callbacks import CallbackAny2Vec
from tqdm import tqdm
import jieba
jieba.setLogLevel(0)
from data_select import select_questions
class Callback(CallbackAny2Vec):
def __init__(self, epoch):
super(Callback, self).__init__()
self.progress = tqdm(range(epoch))
def on_train_begin(self, model):
self.progress.set_description('开始句向量训练')
def on_epoch_begin(self, model):
pass
def on_epoch_end(self, model):
self.progress.set_description('正在句向量训练')
self.progress.update()
def on_train_end(self, model):
self.progress.set_description('结束句向量训练')
self.progress.close()
# 语料由所有的问题和答案组成
def build_train_corpus():
# 读取原问题、增强问题、问题答案
questions = select_questions()
# 对文档进行标记
model_train_corpus = []
for qid, question in questions:
tokens = jieba.lcut(question)
# TaggedDocument 用于构建 doc2vec 的训练语料,第二个参数用于标记样本唯一性
document = TaggedDocument(tokens, [qid])
model_train_corpus.append(document)
return model_train_corpus
epochs = 100
def train_doc2vec_pvdm():
# 构建训练语料
documents = build_train_corpus()
# 训练向量模型
model = Doc2Vec(vector_size=256, negative=128, alpha=1e-4, dm=1, min_count=2, epochs=epochs, window=5, seed=42)
# 构建训练词表
model.build_vocab(documents)
model.train(documents,
total_examples=model.corpus_count,
epochs=model.epochs,
# start_alpha=1e-2,
# end_alpha=1e-4,
callbacks=[Callback(epochs)])
# 存储词向量模型
model.save('finish/semantics/doc2vec/pvdm.bin')
def train_doc2vec_pvbbow():
# 构建训练语料
documents = build_train_corpus()
# 训练向量模型
model = Doc2Vec(vector_size=128, negative=5, alpha=1e-4, dm=0, min_count=2, epochs=epochs, window=5, seed=42)
# 构建训练词表
model.build_vocab(documents)
model.train(documents,
total_examples=model.corpus_count,
epochs=model.epochs,
# start_alpha=1e-2,
# end_alpha=1e-4,
callbacks=[Callback(epochs)])
# 存储词向量模型
model.save('finish/semantics/doc2vec/pvdbow.bin')
if __name__ == '__main__':
train_doc2vec_pvdm()
# train_doc2vec_pvbbow()
3. 相似度比较
generate_question_embedding_doc2vec_single 函数使用 PV-DM 或者 PV-DBOW 来计算句子向量。
generate_question_embedding_doc2vec_combine 函数则是将 PV-DM 或者 PV-DBOW 计算结果拼接起来作为句子向量表示。最终将得到的句子向量存储到 faiss 数据库中。
示例代码:
import torch
from gensim.models import Doc2Vec
import numpy as np
import pandas as pd
import faiss
from tqdm import tqdm
import torch.nn.functional as F
import jieba
jieba.setLogLevel(0)
from data_select import select_questions
def generate_question_embedding_doc2vec_single():
estimator = Doc2Vec.load('finish/semantics/doc2vec/pvdm.bin')
# estimator = Doc2Vec.load('finish/semantics/doc2vec/pvdbow.bin')
questions = select_questions()
database = faiss.IndexIDMap(faiss.IndexFlatIP(256))
progress = tqdm(range(len(questions)), desc='开始计算问题向量')
ids, embeddings = [], []
for index, question in questions:
embedding = estimator.infer_vector(jieba.lcut(question))
# 向量转换成单位向量
ids.append(index)
embeddings.append(embedding.tolist())
# 存储问题向量及其编号
progress.update()
progress.set_description('结束计算问题向量')
progress.close()
# 向量转换为单位向量
embeddings = F.normalize(torch.tensor(embeddings), dim=-1)
# 存储向量索引对象
database.add_with_ids(embeddings, ids)
faiss.write_index(database, 'finish/semantics/doc2vec/doc2vec.faiss')
# 拼接 pvdm + pvdbow 模型的向量
def generate_question_embedding_doc2vec_combine():
estimator1 = Doc2Vec.load('finish/semantics/doc2vec/pvdm.bin')
estimator2 = Doc2Vec.load('finish/semantics/doc2vec/pvdbow.bin')
questions = select_questions()
database = faiss.IndexIDMap(faiss.IndexFlatIP(256))
progress = tqdm(range(len(questions)), desc='开始计算问题向量')
qids, embeddings = [], []
for qid, question in questions:
# 拼接两个向量
question = jieba.lcut(question)
embedding1 = estimator1.infer_vector(question)
embedding2 = estimator2.infer_vector(question)
embedding = torch.concat([torch.tensor(embedding1), torch.tensor(embedding2)], dim=-1)
qids.append(qid)
embeddings.append(embedding)
progress.update()
progress.set_description('结束计算问题向量')
progress.close()
# 将张量列表转换为张量类型
embeddings = torch.stack(embeddings, dim=0)
# 向量转换为单位向量
embeddings = F.normalize(embeddings, dim=-1)
# 存储向量索引对象
database.add_with_ids(embeddings, qids)
faiss.write_index(database, 'finish/semantics/doc2vec/doc2vec.faiss')
if __name__ == '__main__':
generate_question_embedding_doc2vec_single()
# generate_question_embedding_doc2vec_combine()

冀公网安备13050302001966号