本人仅以 PaddlePaddle 深度学习 101 官网教程为指导,添加个人理解和笔记,仅作为学习练习使用,若有错误,还望批评指教。–ZJ
环境:
- Python 2.7
- Ubuntu 16.04
词向量
本教程源代码目录在book/word2vec, 初次使用请参考PaddlePaddle安装教程,更多内容请参考本教程的视频课堂。
背景介绍
本章我们介绍词的向量表征,也称为 word embedding。
- 词向量是自然语言处理中常见的一个操作,是搜索引擎、广告系统、推荐系统等互联网服务背后常见的基础技术。
在这些互联网服务里,我们经常要比较两个词或者两段文本之间的相关性。为了做这样的比较,我们往往先要把词表示成计算机适合处理的方式。
- 最自然的方式恐怕莫过于向量空间模型(vector space model)。
- 在这种方式里,每个词被表示成一个实数向量(one-hot vector),其长度为字典大小,每个维度对应一个字典里的每个词,除了这个词对应维度上的值是 1,其他元素都是 0。
One-hot vector虽然自然,但是用处有限。
比如,在互联网广告系统里,如果用户输入的 query 是“母亲节”,而有一个广告的关键词是“康乃馨”。
虽然按照常理,我们知道这两个词之间是有联系的——母亲节通常应该送给母亲一束康乃馨;
但是这两个词对应的one-hot vectors之间的距离度量,无论是欧氏距离还是余弦相似度(cosine similarity),由于其向量正交,都认为这两个词毫无相关性。
- 得出这种与我们相悖的结论的根本原因是:每个词本身的信息量都太小。所以,仅仅给定两个词,不足以让我们准确判别它们是否相关。要想精确计算相关性,我们还需要更多的信息——从大量数据里通过机器学习方法归纳出来的知识。
在机器学习领域里,各种“知识”被各种模型表示,词向量模型(word embedding model) 就是其中的一类。通过词向量模型可将一个 one-hot vector 映射到一个维度更低的实数向量(embedding vector),
如 embedding(母亲节)=[0.3,4.2,−1.5,...],embedding(康乃馨)=[0.2,5.6,−2.3,...] e m b e d d i n g ( 母 亲 节 ) = [ 0.3 , 4.2 , − 1.5 , . . . ] , e m b e d d i n g ( 康 乃 馨 ) = [ 0.2 , 5.6 , − 2.3 , . . . ] 。
在这个映射到的实数向量表示中,希望两个语义(或用法)上相似的词对应的词向量“更像”,这样如“母亲节”和“康乃馨”的对应词向量的余弦相似度就不再为零了。
词向量模型可以是概率模型、共生矩阵 (co-occurrence matrix) 模型或神经元网络模型。
在用神经网络求词向量之前,传统做法是统计一个词语的共生矩阵
X
X
。 是一个
|V|×|V|
|
V
|
×
|
V
|
大小的矩阵,
Xij
X
i
j
表示在所有语料中,词汇表V
(vocabulary)中第 i 个词和第 j 个词同时出现的词数,
|V|
|
V
|
为词汇表的大小。对
X
X
做矩阵分解(如奇异值分解,Singular Value Decomposition [5]),得到的 即视为所有词的词向量:
但这样的传统做法有很多问题:
1) 由于很多词没有出现,导致矩阵极其稀疏,因此需要对词频做额外处理来达到好的矩阵分解效果;
2) 矩阵非常大,维度太高(通常达到
106∗106
10
6
∗
10
6
的数量级);
3) 需要手动去掉停用词(如 although, a,…),不然这些频繁出现的词也会影响矩阵分解的效果。
基于神经网络的模型不需要计算存储一个在全语料上统计的大表,而是通过学习语义信息得到词向量,因此能很好地解决以上问题。在本章里,我们将展示基于神经网络训练词向量的细节,以及如何用 PaddlePaddle 训练一个词向量模型。
效果展示
本章中,当词向量训练好后,我们可以用数据可视化算法 t-SNE[4]画出词语特征在二维上的投影(如下图所示)。从图中可以看出,语义相关的词语(如a, the, these; big, huge)在投影上距离很近,语意无关的词(如 say, business; decision, japan)在投影上的距离很远。
图1. 词向量的二维投影
另一方面,我们知道两个向量的余弦值在 [−1,1] [ − 1 , 1 ] 的区间内:
- 两个完全相同的向量余弦值为 1,
- 两个相互垂直的向量之间余弦值为 0,
- 两个方向完全相反的向量余弦值为 -1,
- 即相关性和余弦值大小成正比。因此我们还可以计算两个词向量的余弦相似度:
similarity: 0.899180685161
please input two words: big huge
please input two words: from company
similarity: -0.0997506977351
以上结果可以通过运行`calculate_dis.py`, 加载字典里的单词和对应训练特征结果得到,我们将在[应用模型](#应用模型)中详细描述用法。 ## 模型概览 在这里我们介绍三个训练词向量的模型:N-gram 模型,CBOW 模型和 Skip-gram 模型,它们的中心思想都是通过上下文得到一个词出现的概率。对于 N-gram 模型,我们会先介绍语言模型的概念,并在之后的[训练模型](#训练模型)中,带大家用 PaddlePaddle 实现它。而后两个模型,是近年来最有名的神经元词向量模型,由 Tomas Mikolov 在 Google 研发
训练数据 | 验证数据 | 测试数据 |
ptb.train.txt | ptb.valid.txt | ptb.test.txt |
42068句 | 3370句 | 3761句 |
数据预处理
本章训练的是 5-gram 模型,表示在 PaddlePaddle 训练时,每条数据的前 4 个词用来预测第 5个词。PaddlePaddle 提供了对应PTB数据集的 python 包paddle.dataset.imikolov
,自动做数据的下载与预处理,方便大家使用。
预处理会把数据集中的每一句话前后加上开始符号<s>
以及结束符号<e>
。然后依据窗口大小(本教程中为 5),从头到尾每次向右滑动窗口并生成一条数据。
如”I have a dream that one day” 一句提供了5条数据:
<s> I have a dream
I have a dream that
have a dream that one
a dream that one day
dream that one day <e>
最后,每个输入会按其单词次在字典里的位置,转化成整数的索引序列,作为 PaddlePaddle 的输入。
编程实现
本配置的模型结构如下图所示:
图5. 模型配置中的N-gram神经网络模型
首先,加载所需要的包:
import math
import paddle.v2 as paddle
然后,定义参数:
embsize = 32 # 词向量维度
hiddensize = 256 # 隐层维度
N = 5 # 训练5-Gram
用于保存和加载word_dict和embedding table的函数
# save and load word dict and embedding table
def save_dict_and_embedding(word_dict, embeddings):
with open("word_dict", "w") as f:
for key in word_dict:
f.write(key + " " + str(word_dict[key]) + "\n")
with open("embedding_table", "w") as f:
numpy.savetxt(f, embeddings, delimiter=',', newline='\n')
def load_dict_and_embedding():
word_dict = dict()
with open("word_dict", "r") as f:
for line in f:
key, value = line.strip().split(" ")
word_dict[key] = int(value)
embeddings = numpy.loadtxt("embedding_table", delimiter=",")
return word_dict, embeddings
接着,定义网络结构:
- 将 wt w t 之前的 n−1 n − 1 个词 wt−n+1,...wt−1 w t − n + 1 , . . . w t − 1 ,通过 |V|×D | V | × D 的矩阵映射到D维词向量(本例中取D=32)。
def wordemb(inlayer):
wordemb = paddle.layer.table_projection(
input=inlayer,
size=embsize,
param_attr=paddle.attr.Param(
name="_proj",
initial_std=0.001,
learning_rate=1,
l2_rate=0,
sparse_update=True))
return wordemb
- 定义输入层接受的数据类型以及名字。
paddle.init(use_gpu=False, trainer_count=3) # 初始化PaddlePaddle
word_dict = paddle.dataset.imikolov.build_dict()
dict_size = len(word_dict)
# 每个输入层都接受整形数据,这些数据的范围是[0, dict_size)
firstword = paddle.layer.data(
name="firstw", type=paddle.data_type.integer_value(dict_size))
secondword = paddle.layer.data(
name="secondw", type=paddle.data_type.integer_value(dict_size))
thirdword = paddle.layer.data(
name="thirdw", type=paddle.data_type.integer_value(dict_size))
fourthword = paddle.layer.data(
name="fourthw", type=paddle.data_type.integer_value(dict_size))
nextword = paddle.layer.data(
name="fifthw", type=paddle.data_type.integer_value(dict_size))
Efirst = wordemb(firstword)
Esecond = wordemb(secondword)
Ethird = wordemb(thirdword)
Efourth = wordemb(fourthword)
- 将这n-1个词向量经过concat_layer连接成一个大向量作为历史文本特征。
contextemb = paddle.layer.concat(input=[Efirst, Esecond, Ethird, Efourth])
- 将历史文本特征经过一个全连接得到文本隐层特征。
hidden1 = paddle.layer.fc(input=contextemb,
size=hiddensize,
act=paddle.activation.Sigmoid(),
layer_attr=paddle.attr.Extra(drop_rate=0.5),
bias_attr=paddle.attr.Param(learning_rate=2),
param_attr=paddle.attr.Param(
initial_std=1. / math.sqrt(embsize * 8),
learning_rate=1))
- 将文本隐层特征,再经过一个全连接,映射成一个
|V|
|
V
|
维向量,同时通过softmax归一化得到这
|V|
个词的生成概率。
predictword = paddle.layer.fc(input=hidden1,
size=dict_size,
bias_attr=paddle.attr.Param(learning_rate=2),
act=paddle.activation.Softmax())
- 网络的损失函数为多分类交叉熵,可直接调用
classification_cost
函数。
cost = paddle.layer.classification_cost(input=predictword, label=nextword)
然后,指定训练相关的参数:
- 训练方法(optimizer): 代表训练过程在更新权重时采用动量优化器,本教程使用Adam优化器。
- 训练速度(learning_rate): 迭代的速度,与网络的训练收敛速度有关系。
- 正则化(regularization): 是防止网络过拟合的一种手段,此处采用L2正则化。
parameters = paddle.parameters.create(cost)
adagrad = paddle.optimizer.AdaGrad(
learning_rate=3e-3,
regularization=paddle.optimizer.L2Regularization(8e-4))
trainer = paddle.trainer.SGD(cost, parameters, adagrad)
下一步,我们开始训练过程。paddle.dataset.imikolov.train()
和paddle.dataset.imikolov.test()
分别做训练和测试数据集。这两个函数各自返回一个reader——PaddlePaddle中的reader是一个Python函数,每次调用的时候返回一个Python generator。
paddle.batch
的输入是一个reader,输出是一个batched reader —— 在PaddlePaddle里,一个reader每次yield一条训练数据,而一个batched reader每次yield一个minbatch。
def event_handler(event):
if isinstance(event, paddle.event.EndIteration):
if event.batch_id % 100 == 0:
print "Pass %d, Batch %d, Cost %f, %s" % (
event.pass_id, event.batch_id, event.cost, event.metrics)
if isinstance(event, paddle.event.EndPass):
result = trainer.test(
paddle.batch(
paddle.dataset.imikolov.test(word_dict, N), 32))
print "Pass %d, Testing metrics %s" % (event.pass_id, result.metrics)
with open("model_%d.tar"%event.pass_id, 'w') as f:
trainer.save_parameter_to_tar(f)
trainer.train(
paddle.batch(paddle.dataset.imikolov.train(word_dict, N), 32),
num_passes=100,
event_handler=event_handler)
Pass 0, Batch 0, Cost 7.870579, {'classification_error_evaluator': 1.0}, Testing metrics {'classification_error_evaluator': 0.999591588973999}
Pass 0, Batch 100, Cost 6.136420, {'classification_error_evaluator': 0.84375}, Testing metrics {'classification_error_evaluator': 0.8328699469566345}
Pass 0, Batch 200, Cost 5.786797, {'classification_error_evaluator': 0.8125}, Testing metrics {'classification_error_evaluator': 0.8328542709350586}
...
训练过程是完全自动的,event_handler里打印的日志类似如上所示:
经过30个pass,我们将得到平均错误率为classification_error_evaluator=0.735611。
保存词典和embedding
训练完成之后,我们可以把词典和embedding table单独保存下来,后面可以直接使用
# save word dict and embedding table
embeddings = parameters.get("_proj").reshape(len(word_dict), embsize)
save_dict_and_embedding(word_dict, embeddings)
应用模型
训练模型后,我们可以加载模型参数,用训练出来的词向量初始化其他模型,也可以将模型查看参数用来做后续应用。
查看词向量
PaddlePaddle训练出来的参数可以直接使用parameters.get()
获取出来。例如查看单词apple
的词向量,即为
embeddings = parameters.get("_proj").reshape(len(word_dict), embsize)
print embeddings[word_dict['apple']]
[-0.38961065 -0.02392169 -0.00093231 0.36301503 0.13538605 0.16076435
-0.0678709 0.1090285 0.42014077 -0.24119169 -0.31847557 0.20410083
0.04910378 0.19021918 -0.0122014 -0.04099389 -0.16924137 0.1911236
-0.10917275 0.13068172 -0.23079982 0.42699069 -0.27679482 -0.01472992
0.2069038 0.09005053 -0.3282454 0.12717034 -0.24218646 0.25304323
0.19072419 -0.24286366]
修改词向量
获得到的embedding为一个标准的numpy矩阵。我们可以对这个numpy矩阵进行修改,然后赋值回去。
def modify_embedding(emb):
# Add your modification here.
pass
modify_embedding(embeddings)
parameters.set("_proj", embeddings)
计算词语之间的余弦距离
两个向量之间的距离可以用余弦值来表示,余弦值在
[−1,1]
[
−
1
,
1
]
的区间内,向量间余弦值越大,其距离越近。这里我们在calculate_dis.py
中实现不同词语的距离度量。
用法如下:
from scipy import spatial
emb_1 = embeddings[word_dict['world']]
emb_2 = embeddings[word_dict['would']]
print spatial.distance.cosine(emb_1, emb_2)
0.99375076448
总结
- 本章中,我们介绍了词向量、语言模型和词向量的关系、以及如何通过训练神经网络模型获得词向量。
- 在信息检索中,我们可以根据向量间的余弦夹角,来判断 query 和文档关键词这二者间的相关性。
- 在句法分析和语义分析中,训练好的词向量可以用来初始化模型,以得到更好的效果。
- 在文档分类中,有了词向量之后,可以用聚类的方法将文档中同义词进行分组。希望大家在本章后能够自行运用词向量进行相关领域的研究。
参考文献
- Bengio Y, Ducharme R, Vincent P, et al. A neural probabilistic language model[J]. journal of machine learning research, 2003, 3(Feb): 1137-1155.
- Mikolov T, Kombrink S, Deoras A, et al. Rnnlm-recurrent neural network language modeling toolkit[C]//Proc. of the 2011 ASRU Workshop. 2011: 196-201.
- Mikolov T, Chen K, Corrado G, et al. Efficient estimation of word representations in vector space[J]. arXiv preprint arXiv:1301.3781, 2013.
- Maaten L, Hinton G. Visualizing data using t-SNE[J]. Journal of Machine Learning Research, 2008, 9(Nov): 2579-2605.
- https://en.wikipedia.org/wiki/Singular_value_decomposition
本教程 由 PaddlePaddle 创作,采用 知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。