【论文笔记】Distributed Representations of Sentences and Documents

文章目录
  1. 1. 文章主要解决的问题及作用
  2. 2. 模型算法
    1. 2.1. 学习单词的向量表示
      1. 2.1.1. 基于深度学习模型学习词向量
      2. 2.1.2. 基于语言模型的词向量训练
    2. 2.2. 论文中的词向量训练方法
    3. 2.3. 段落向量:一种分布式的记忆模型
    4. 2.4. 无词序的段落向量:分布式词袋
  3. 3. 实验
    1. 3.1. 使用斯坦福情感树库数据集进行情感分析
      1. 3.1.1. 树库情感分析实验设置
      2. 3.1.2. 树库情感分析实验结果
    2. 3.2. 超越单句:用IMDB数据集进行情感分析
      1. 3.2.1. IMDB情感分析实验设置
      2. 3.2.2. IMDB情感分析实验结果
  4. 4. 论文复现
    1. 4.1. 数据预处理
    2. 4.2. 切词
    3. 4.3. 统计单词并构建词表
    4. 4.4. 编号表示
    5. 4.5. 窗口数据集构建
    6. 4.6. 代码运行后获得的相关数据
  5. 5. 根据文章构建模型
    1. 5.1. 初始化部分模型参数
    2. 5.2. 创建占位符
    3. 5.3. 构建模型
      1. 5.3.1. 定义训练参数的区域
      2. 5.3.2. 定义测试集的参数变量
      3. 5.3.3. 定义需要训练哪个部分
    4. 5.4. 设置情感分类器
    5. 5.5. 训练情感分类器
  6. 6. 训练模型
  7. 7. 运行过程
  8. 8. 测试模型

这篇笔记是基于Mikolov大神的论文《Distributed Representations of Sentences and Documents》,句子和文档的分布式表示。

注:笔记学习参考深度之眼人工智能Paper训练营NLP方向第三课时课程。

文章主要解决的问题及作用

本文提出了一种从句子、段落和文档等可变长度的文本中学习固定长度特征表示的无监督算法。段落向量算法(Paragraph Vector)用一个密集的向量来表示每个文档,该向量被训练来预测文档中的单词。

Bag-of-Words模型和Bag-of-n-grams模型对单词的语义或更正式的单词之间的距离几乎没有意义。这意味着,尽管从语义上讲,powerful应该比Paris更接近strong,但strong、strong和Paris这三个词之间的距离是一样的。


Bag-of-Words模型的缺点:
(1)丢失词序.
(2)不包含语义信息.

Bag-of-n-grams模型的缺点:
(1)不包含语义信息;
(2)虽然n-gram包含一定词序信息,但是n不会很大,最多是4-gram,所以也只是保留了较少的信息。

作者提出的分布式模型可以应用于可变长度的文本,从短语或句子到大型文档的任何内容。

更确切地说,作者是将段落向量与一个段落中的几个单词向量连接起来,并在给定的上下文中预测以下单词。训练词向量和段落向量,通过随机梯度下降和反向传播(Rumelhart等,1986)。尽管段落中段落向量是唯一的,但是单词向量是共享的。在预测时间,段落向量是通过固定单词向量和训练新的段落向量来推断的,直到收敛为止。

模型算法

学习单词的向量表示

基于深度学习模型学习词向量

首先构建词表,再使用Word2Vec的方法进行训练来得到词向量,再将句子输入神经网络得到句子的分布式表示。

这类模型的缺点在于必须使用标注数据进行训练。

基于语言模型的词向量训练

语言模型可以给出句子是句子的概率:

每个词的概率定义为n-gram形式,即每个词出现只与前n-1个词有关:

模型示意:

评价语言模型的好坏是指标困惑度。

论文中的词向量训练方法

学习词向量分布表示的著名框架如图1所示,其任务是根据上下文中的其他单词来预测一个单词。在此框架中,每个字被映射到由矩阵W中的列表示的唯一矢量。列通过词汇表中的字的位置进行索引。词向量的连接或和被用来作为预测句子中下一个单词的特征。

给出一系列训练单词,w1,w2,w3…,wt,词向量模型的目标就是最大化平均对数概率。

预测任务通常通过多类分类器(如Softmax)来完成:

对于每个输出单词i,每个yi都是非标准化的对数概率,计算为:

其中U、b为Softmax参数。h由从W提取的单词向量的连接或平均值构成。


平均值计算补充:
对每个句子中包含的单词w1,w2,…,wn对应的词向量v1,v2,…,vn,句子的表示为:

这种方法和BOW模型一样,都丢失了词序信息。

在实际训练中,分层Softmax训练更快。本文中的分层SoftMax的结构是一个二进制霍夫曼树,其中短代码被分配给频繁的字。
Mikolov大神在Word2vec一文中也详细地讲过词向量的训练方法了,词向量训练结束之后会将意义相近的单词映射到向量空间中的相似位置。

段落向量:一种分布式的记忆模型

段落向量也被要求来从给定的许多段落样本的上下文中来做预测下一个词的任务。

段落向量的结构图如下图所示,每个段落被映射为一个单独的向量,由矩阵D中的一列表示。同时,每个单词也被映射为一个单独的向量,由矩阵W中的一列表示。词向量和段落向量连接起来或者计算平均后来预测下一个单词。

段落向量模型和词向量模型中唯一的不同就是h来自于W和D。

段落标记可以看作是另一个词。它作为记忆来记忆当前上下文中缺少的内容–或者段落中的主题。出于这个原因,常称其为建立段落向量的分布式存储模型(Distributed Memory
Model of Paragraph Vectors,PV-DM).

上下文是固定长度的,并从段落上方的滑动窗口中取样.段落向量在同一段落生成的所有上下文中共享,但不跨段落共享。然后词向量矩阵W在段落间是共享的。在随机梯度下降的每一步,都可以从随机段落中抽取固定长度的上下文,从图2中的网络中计算误差梯度,并使用梯度更新模型中的参数。

在预测时,需要执行一个推断步骤来计算新段落的段落向量。这也是通过梯度下降得到的。在这个步骤中,其余的参数该模型,字向量W和软件最大权值是固定的。

假设有N个段落需要映射到p维,W个词汇需要映射到q维,模型的参数总数为:N*p+W*q。即使当N很大时,参数的数目也可能很大,但是训练期间的更新通常是稀疏的,因此是有效的。

总的来说,论文中的算法有两个关键步骤:
(1)对已经出现的段落训练来得到词向量W,softmax权重U,b,以及段落向量D;
(2)“推断阶段”(inference stage),通过在D中增加更多的列,并在D上降梯度,同时保持W、U、b不变,从而得到新段落的段落向量D(以前从未见过)。我们用D来做一个关于使用标准分类器的一些特殊标签,例如Logistic回归。

段落向量的优点:段落向量的一个重要优点是,它们是从未标记的数据中学习的,因此可以在没有足够标记数据的任务中很好地工作。

无词序的段落向量:分布式词袋

上述方法考虑段落向量与单词向量的连接,以预测文本窗口中的下一单词。另一种方法是忽略输入中的上下文词,而是强制模型预测输出中段落中随机抽取的单词。实际上,这意味着在随机梯度下降的每次迭代中,先对一个文本窗口采样,然后从文本窗口中随机抽取一个单词,并给定段落向量形成一个分类任务。

这个过程称作分布式词袋版本的段落向量(Distributed
Bag of Words version of Paragraph Vector,PV-DBOW),训练段落向量以预测小窗口中的单词,相当于前面提到的PV-DM,结构如下图所示:
Figure 3

在作者的实验中,段落向量由以下两部分组成:
(1)一种由分布式记忆的标准段落组成;
(2)另一种由分布式词袋的段落向量组成。

PV-DM在大多数任务上具有较好的性能,但是它与PV-DBOW的结合通常在我们尝试的许多任务中更加一致,因此强烈推荐使用它们的结合版。

实验

作者进行实验来更好地理解段落向量的行为。为了实现这一点,作者在两个文本理解问题上对段落向量进行了基准测试,这两个问题需要段落的固定长度向量表示:情感分析和信息检索。

情感分析使用了两种数据集:
(1)IMDB电影评论集;
(2)斯坦福情感树库数据集。

同时作者还在信息检索任务上测试模型,目标是确定该算法是否能在给定查询的情况下检索文档。

使用斯坦福情感树库数据集进行情感分析

Soher等人提出了两种方法来作为基准线。首先,我们可以考虑一个5路细粒度的分类任务,其中标签是{Very Negative, Negative, Neutral, Positive, Very Positive},或者是双向粗粒度的分类任务。标签为{Negative, Positive}。另一个变化轴是我们是否应该标记整个句子或句子中的所有短语。在本文中,作者只考虑标注标签完整的句子。

树库情感分析实验设置

遵循如(Socher等人,2013b)所述的实验方案。为了利用可用的标记数据,在我们的模型中,每个子短语被视为一个独立的句子,我们学习训练集中所有子短语的表示。

在学习了训练句子及其子短语的向量表示后,将它们反馈给逻辑回归,以学习电影评分的预测器。

在测试时,我们冻结每个单词的向量表示,并使用梯度下降学习句子的表示。一旦学习了测试句子的向量表示,我们就通过逻辑回归来预测电影评分。

在我们的实验中,我们使用验证集交叉验证窗口大小,最佳窗口大小为8。给分类器的向量是两个向量的串联,一个来自PV-DBOW,一个来自PV-DM。在PV-DBOW中,学习向量表示有400维。在PV-DM中,学习向量表示对于单词和段落都有400个维度。为了预测第8个单词,将段落向量和7个单词向量连接起来。其中特殊的符号,比如,!…等被看作是一个常规的单词,如果段落少于9个单词,使用一个特殊的NULL来填补。

树库情感分析实验结果

情感分析结果如下表所示,其中NB,SVM,BiNB的表现效果较差,单纯的计算向量的平均值(针对词袋模型)也不会提高结果。这是因为袋的单词模型不考虑每个句子是如何构成的(例如,单词排序),因此不能识别出许多复杂的语言现象,例如挖苦。

超越单句:用IMDB数据集进行情感分析

之前的一些技巧只适用于句子,但不是段落/文件有几个句子。作者提出的方法不需要解析,因此它可以为一个由多个句子组成的长文档生成一个表示。这个优点使他们的方法比其他方法更通用。

IMDB情感分析实验设置

作者使用了75,000个训练文档(25,000个标记的和50,000个未标记的实例)来学习向量和段落向量。然后,通过一个有50个隐含层的神经网络和一个逻辑分类器输入25,000个标记实例的段落向量,以学习预测情绪。

在测试时,给定一个测试句子,再次冻结网络的其余部分,并通过梯度下降法学习用于测试复习的段落向量。一旦学习了向量,我们通过神经网络来预测评论的情绪。
固定模型结构示意图,圈起来的部分是固定的,初始化段落向量:

我们的段落向量模型的超参数的选择方式与以前的任务相同。特别是,我们交叉验证了窗口的大小,最优的窗口大小是10字。T型向分类器提出的矢量是两个向量的级联,一个来自PV-DBOW,另一个来自PV-DM。在PV-DBOW中,学习到的向量表示有400维.在PV-DM中,学习向量表示对于单词和文档都有400个维度。为了预测第10个单词,我们将段落向量和单词向量连接起来。特殊字符,如,!?被视为正常的单词。如果文档少于9个单词,我们使用一个特殊的空单词符号NULL进行预填充。

IMDB情感分析实验结果

实验结果如下表所示:

论文复现这块一直是个人的弱点,所以本篇论文着重于论文复现上。

论文复现

数据预处理

确定使用的数据库是IMDB数据库,数据库中有25000条训练数据,25000条测试数据,还有50000条未标记数据,每条评论单独放在一个.txt文件中。
按照我之前的理解,英文的分词使用split(‘ ‘)就可以解决了,实际则不是,英文中包含的连写,比如 I’m 如果单纯的使用空格的来切分则处理得不到位,可以使用NLTK库来处理。

前面四个步骤属于NLP语料库预处理的常规操作部分,第五步窗口数据集构建,假如窗口大小为10,也就是需要要前9个单词来预测第10个单词。
在本文中,每个样本就相当于用前9个词和一个句向量(Paragraph Vector)来预测第10个词,第10个词也就是所谓的标签(Label)。

切词

nltk.word_tokenize处理之后得到的是一个list类型的数据,其中包含切分后的结果。

1
2
3
4
5
6
7
8
9
10
11
12
def get_data(self, path):
datas = []
paths = os.listdir(path)
paths = [path + filename for filename in paths]
for i, file in enumerate(paths):
if i % 1000 == 0:
print('当前读取评论条数: ', i, ' / ', len(paths))
data = open(file, 'r', encoding='utf-8').read()
data = data.lower()
data = nltk.word_tokenize(data)
datas.append(data)
return datas

统计单词并构建词表

这个属于常规操作,之前在复现Word2Vec的过程中有涉及到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_all_words(self, train_datas, train_unsup):
'''
从训练集句子和无监督句子中统计所有出现过的词以及它们的频率并取出频率最高的29998个词加上pad和unk
来构建一个大小为30000的词表
:return: 大小为30000的词表及每个词对应的id
'''
all_words = []
for sentence in train_datas:
all_words.extend(sentence)
for sentence in train_unsup:
all_words.extend(sentence)
count = Counter(all_words)
count = dict(count.most_common(29998))
word2id = {'<pad>': 0, '<unk>': 1}
for word in count:
word2id[word] = len(word2id)
return word2id

编号表示

1
2
3
4
5
6
7
8
9
10
def convert_data_word_to_id(self, word2id, datas):
'''
:param word2id: 包含30000词的词表及其编号
:param datas: 需要转化为序号表示的句子
:return:转化完成后的句子集合
'''
for i, sentence in enumerate(datas):
for j, word in enumerate(sentence):
datas[i][j] = word2id.get(word, 1)
return datas

这个循环的作用就是得到每个词的编号。

第一重循环datas里包含的是所有postive,negtive以及未标注的句子分词后的list集合,类似于[[‘manu’,’and’],[‘thomas’,’play’,’pai’]],第一重循环取出来的就是句子及其编号。

第二重循环就是取出句子中的每个单词及编号。实际上datas就是一个二维数组,datas[i][j]就是把所有的单词根据词表转换为编号。

1
word2id.get(word,1)

这行代码的意思也就是获取对应单词在词表中的编号,如果没有,就返回1,也就对应一开始就预设值好的’unk’。

窗口数据集构建

这里直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def convert_data_to_new_data(self, datas):
'''
根据句子生成窗口大小为10的语言模型训练集,当句子长度不够10时需要在前面补pad。
:param datas: 句子,可以只使用训练句子,也可以使用训练句子+无监督句子,后续需要训练更久。
:return: 返回窗口大小为10的训练集,句子id和词标签。
'''
# 训练集是9个单词+一个句向量构成
# 单词向量
new_word_datas = []
# 句向量
new_papr_datas = []
new_labels = []
for i, data in enumerate(datas):
# 输出当前数据条数
if i % 1000 == 0:
print(i, len(datas))
for j in range(len(data)):
if len(data) < 10: # 如果句子长度不够10,开始pad
tmp_words = [0] * (10 - len(data)) + data[0:-1]
if set(tmp_words) == {1}: # 同样,连续9个词都是unk就舍去
break
new_word_datas.append(tmp_words)
new_papr_datas.append(i)
new_labels.append(data[-1])
break
tmp_words = data[j:j + 9]
if set(tmp_words) == {1}: # 开始发现存在连续出现unk的句子,这种句子没有意义,所以连续9个词都是unk,那么就舍去
continue
new_papr_datas.append(i)
new_word_datas.append(tmp_words)
new_labels.append(data[j + 9])
if j + 9 + 1 == len(data): # 到最后10个单词break
break
new_word_datas = np.array(new_word_datas)
new_papr_datas = np.array(new_papr_datas)
new_labels = np.array(new_labels)
print(new_word_datas.shape)
print(new_papr_datas.shape)
print(new_labels.shape)
return new_word_datas, new_papr_datas, new_labels

代码运行后获得的相关数据


根据文章构建模型

对于我本人来说,这个步骤比之前的预处理或者是读懂论文这些都要相对重要一些,因为在研究的路上我本人最缺乏的就是复现代码的代码,所以,对于这篇论文,我打算认真弄清楚其对应论文的每一个细节。

使用tensorflow构建模型

初始化部分模型参数

初始化的参数包括窗口大小,句子总数等

1
2
3
4
5
def __init__(self,train_first,train_second):
self.window=10 # 使用连续的9个词预测下一个词
self.para_num=25000
self.create_placeholder()
self.model(train_first,train_second)

创建占位符

构建模型第一步,设置输入和标签大小,word_label表示预测下一个词是什么的标签,label则是情感分析的标签(negtive or postive)。

1
2
3
4
5
6
7
8
9
10
11
12
13
def create_placeholder(self):
'''
创建图输入的placeholder
self.word_input: n-gram前n-1个词的输入
self.para_input: 篇章id的输入
self.word_label: 语言模型预测下一个词的词标签
self.label: 这句话属于正类还是负类的类别标签
:return:
'''
self.word_input = tf.placeholder(dtype=tf.int32, shape=[None, self.window - 1])
self.para_input = tf.placeholder(dtype=tf.int32, shape=[None, 1])
self.word_label = tf.placeholder(dtype=tf.int32, shape=[None])
self.label = tf.placeholder(dtype=tf.int32, shape=[None])

构建模型

在构建模型之前首先重新打开原文,可以看到其中包含了如下结构的模型:
(1)使用句子和词向量的连接来预测下一个词:


(2)使用段落向量来预测一个小窗口中的词:

定义训练参数的区域

文章中在IMDB数据集上测试情感分类的时候固定了除句向量以外的参数,定义参数区域就是为了方便取出需要训练的参数,后期在复现论文的过程中需要留意有没有需要固定参数的部分,若存在需要固定参数的部分则必须定义参数区域。

(1)定义训练集的参数区域

para_num表示的是包含的句子数,400是文章中设置的训练维度(在斯坦福数据集实验中提及)。词嵌入中设置的3000指的则是最常出现的30000个单词,这个数量由人为设置。

参数说明:

-trainable:参数是可训练的.

-name:命名空间的名字.

1
2
3
4
5
with tf.variable_scope("train_paramaters"):
self.train_para_embedding = tf.Variable(initial_value=tf.truncated_normal(
shape=[self.para_num, 400]), trainable=True, name='train_para_embedding')
self.word_embedding = tf.Variable(initial_value=tf.truncated_normal(
shape=[30000, 400]), trainable=True, name='word_embedding')

定义测试集的参数变量

测试集中包含了12500条pos数据和25000条neg数据,训练的维度是400维,所以大小是25000x400。

1
2
3
with tf.variable_scope("test_parameters"):
self.test_para_embedding = tf.Variable(initial_value=tf.truncated_normal(shape=[25000, 400]), trainable=True,
name="test_para_embedding")

定义需要训练哪个部分

-train_first: train_first为True时,表示训练训练集的词向量和句向量.

-train_second: train_second为True时,表示固定词向量和句向量,开始训练单隐层神经网络分类器用于情感分类。

只要需要训练,就把相应的段落嵌入和段落输入取出来作为一个矩阵。反之,如果不需要训练任何东西,那么则是在测试阶段,将测试集中的东西根据输入取出作为矩阵。

1
2
3
4
if train_first or train_second:
para_input = tf.nn.embedding_lookup(self.train_para_embedding, self.para_input) # batch_size*1*400
else:
para_input = tf.nn.embedding_lookup(self.test_para_embedding, self.para_input)


注:这里说一下关于tf.nn.embedding_lookup

第一个参数是需要从中取值的矩阵

第二个参数是取值的行数,下面代码示意。

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
import tensorflow as tf

m=np.random.rand(5,5)
print(m)
n=np.array([1,3])
n.reshape((2,1))

t=tf.nn.embedding_lookup(m,n)
#print(t)
sess=tf.Session()
print(sess.run(t))
sess.close()


输出的结果如下图所示,其中n设定的1和3也就相当于需要取出的句子编号。





#### 通过one-hot向量来设置词的标签

1
labels=tf.one_hot(self.word_label,30000)


#### 分别取出训练和测试的参数

1
2
train_var=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,"train_parameters")
test_var=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,"test_parameters")


#### 设置正则

这个正则存在的意义我个人是这样思考的,在测试阶段仅仅训练句向量,但是参数会报错,所以加入正则避免报错。

可能当前的理解不是很到位,后续复现其他论文的时候会对这个小东西进行测试。
1
reg=tf.contrib.layers.apply_regularization(tf.contrib.layers.l2_regularizer(1e-10),tf.trainable_variables())


### 设置损失函数

设置完损失函数之后要设置其在训练时以及测试时的优化,因为这篇论文比较特殊,即使在测试时也需要对句向量进行训练。
1
2
3
4
self.loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=labels, logits=output)) + reg

self.train_op = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(self.loss_op, var_list=train_var)
self.test_op = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(self.loss_op, var_list=test_var)


关于这个tf.reduce_mean函数,我发现在设置损失时一直会用到,把用法链接放在这里好了:https://blog.csdn.net/dcrmg/article/details/79797826

设置情感分类器

这篇论文中一共包含两个任务:

(1)通过句子和词汇预测下一个词.

(2)情感分类问题.

所以,下面需要对分类器进行设置。

1
2
3
4
5
6
7
# 分类器训练
# 原先的para_input的大小是batch_size*1*400
mlp_input=tf.reshape(para_input,[-1,400])
with tf.variable_scope('classification_parameters'):
h1=tf.layers.dense(mlp_input,units=50,activation='relu',trainable=True,name='h1')
# 情感分类的结果是二分类结果
mlp_output=tf.layers.dense(h1,2,trainable=True,name='mlp_output')

训练情感分类器

首先设置label,损失函数,分类器参数,优化损失函数,最后通过tf.argmax得出结果。

1
2
3
4
5
6
7
8
mlp_labels = tf.one_hot(self.label, 2)
self.mlp_loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2
(labels=mlp_labels, logits=mlp_output))
classification_var = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, "classification_parameters")
self.mlp_train_op = tf.train.AdamOptimizer(learning_rate=0.02). \
minimize(self.mlp_loss_op, var_list=classification_var)

self.predict_op = tf.argmax(mlp_output, axis=1)

训练模型

训练的过程包含了训练句向量以及训练分类两部分,根据需要进行训练即可。
注:其中batch_size可调。

运行过程


注:这里训练模型的部分可以在原来的基础上进行训练这点需要弄清楚。

测试模型

这里测试的是情感分类过程,也就是将句子根据batch_size放入模型中进行训练,将预测的结果单独保存下来,再通过skit-learn 的 accuracy_score函数计算出准确率。