手把手教你PaddlePaddle 做词向量模型 SkipGram实战
收藏

在做 NLP 的任务时,一个非常 basic 的操作就是如何编码自然语言中的符号,例如词、短语,甚至词缀。目前流行的方法有大约三种:

• 特征工程这类方法依赖于手工特征,例如 tf-idf 同时考虑词频和词的稀缺度;

 统计方法统计上常常通过矩阵分解(如 SVD、EVD)来建模大规模文档集合;

• 神经网络目前非常流行通过神经网络端到端的建模语言模型,得到词向量副产品;

今天要讲解的就是 SkipGram 模型就属于第三种方法,它的主要思想是利用的词义的分布式表示。除了让您彻底弄懂什么是语言模型以及 SkipGram 的基本原
理。我们还会详细的说明如何一步步的用 PaddlePaddle 实现它。

1、什么是词向量

首先我们需要了解什么是词向量。NLP 和图像不太一样,图像的输入本身就是一个有数值特征的矩阵,而 NLP 的输入通常只是一堆自然语言的符号,它们本身是不具备数学特性的。因此,在计算语言学中,我们通常会希望用数值向量来表示这些符号。例如现在我们希望比较词汇“米饭”与“猪肉”和“家具”之间词义的相似性,可以考虑用下面这样的 two-stage 范式。

从上面这个例子可以看出,使用向量数值表示法最关键的地方在于如何获取词汇的 向量的表示,而 SkipGram 就是一个良方。

2、什么是语言模型

词向量一般不是直接获取的,而是某些任务的副产品。它们通常是随机初始化的, 然后通过不断的数值优化过程中获得语义信息,例如上述的相似性。因此,训练词 向量的办法可以有很多,但是如何高效的获得高质量的词向量很重要,另外任务也 应该有一定的可拓展性,例如语料充足,不需要额外标注。

语言模型是一个非常好的选择。因为它语料充足,只要有文章,有帖子,那就有数 据;同时由于其任务的特殊性,不需要人工进行额外的数据标注(网上有很多称这 是无监督,但我觉得不是特别合适,不需要数据标注和无监督概念有所差异)。那 么什么是语言模型呢?

3、什么是 SkipGram

经过前两节的解释,相信您对词向量有了很深的认识了。这一小节中我将会介绍 SkipGram,一种有效训练语言模型的方法。

说到 SkipGram,一定有同学会想到 CBOW。实际上 CBOW 更符合常人的思考逻 辑,它建模词语上下文的方法很简单,如下图所示:

虽然看起来 CBOW 更合理,但很多文献指出,用 SkipGram 训出来的词向量效果更 好。笔者分析可能存在下面一些原因:

 SkipGram 用一个中心词去预测上下文,这样相当于对这个中心词的表示要求更高,这就好像 一个学生(中心词)同时受到了多个老师(上下文)的教导(这个学习的过程可以被理解为 中间的梯度传播),效果肯定比一个老师教导多个学生(因此梯度是均分的,没有区分性, 而且由于梯度均分,容易破坏一个窗口中词向量的异构性)效果要好得多;


 其次,SkipGram 这种强调中心词的结构对某些具有较低频率的生僻词比较友好,因此低频词 也可以学到质量较高的向量表示;


但可能是因为 CBOW 的结构相对简单些, 经验显示,CBOW 的训练速度要比 SkipGram 快的多,因此两者其实各有优势。

拿上面提到的例子 “Can you please come here ?” 说明 SkipGram 的流程。假设滑动 窗口的长度为 5,那么现在窗口 cover 住了片段 “can you please come here”。此时 以中心词 please 为输入并度量与上下文 can, you, come, here 的相似度,优化时希 望这个值尽量高。

在工程上,实现词向量模型有很多 trick,例如概率平滑化,高频词抽样等。但如 果做个 demo 不需要考虑太多这些细节。不过无论是 CBOW 还是 SkipGram 都无法 规避一个问题,就是过高的词典容量。正常情况下,英语词典的容量在 3000 ~ 4000 上下,因此当训练语料很大时会造成巨大的计算负担。为了权衡质量和效率,目前最常用的方法就是负采样。

通俗的来说,就是我不再把整个词典当成负 样本了,而是随机抽取若干词作为负样本。实现时,这个随机抽取的数量是一个超 参数,大概是 20 ~ 30 之间,这样很明显大大提高了计算效率。另外,也有人指 出,用一些重要性采样的技术可以进一步改善效果。

4、用PaddlePaddle实现

现在你已经基本了解了什么是 SkipGram,而实现它需要借助现有的深度学习框 架。PaddlePaddle 是百度自主研发的深度学习框架,功能非常强大,同时支持稠 密参数、稀疏参数并行训练;静态网络、动态网络等。而且有非常丰富的中英文文 档,非常方便您使用。下面我们就用强大的 PaddlePaddle 一步一步实现它

首先,我们需要导入一些必要的计算库。

# PaddlePaddle 计算引擎.
import paddle
from paddle import fluid

# 一些常用的科学计算库.
import numpy as np
import matplotlib.pyplot as plt
然后设置定义一些超参数用于控制网络结构和训练逻辑
EMBEDDING_DIM = 64 # 词向量维度.
WINDOW_SIZE = 5 # 滑动窗口大小.
BATCH_SIZE = 200 # 迭代 batch 大小.
EPOCH_NUM = 10 # 训练的轮数.
RANDOM_STATE = 0 # 设置伪随机数种子.


# 然后就是文本数据,这里使用 PaddlePaddle 自带(会自动下载)的 PTB 数据集,导入如下:
from paddle.dataset import imikolov

word_vocab = imikolov.build_dict()
vocab_size = len(word_vocab)

# 打印 PTB 数据字典的容量大小.
print("imikolov 字典大小为 " + str(vocab_size))

# 类似 Pytorch 的 DataLoader, 用于在训练时做 batch, 很方便.
data_loader = paddle.batch(imikolov.test(word_vocab, WINDOW_SIZE), BATCH_SIZE)

imikolov 字典大小为 2074
# 下面我们需要搭建 SkipGram 的网络结构,我们用一个函数打包如下:
def build_neural_network():
assert WINDOW_SIZE % 2 == 1
medium_num = WINDOW_SIZE // 2

# 定义输入变量, 是从文本中截取的连续的文本段.
var_name_list = [str(i) + "-word" for i in range(0, WINDOW_SIZE)]
word_list = [fluid.layers.data(name=n, shape=[1], dtype="int64") for n in var_name_list]

# 取中心词作为输入, 而周围上下文作为输出.
input_word = word_list[medium_num]
output_context = word_list[:medium_num] + word_list[medium_num + 1:]

# 将输入输出都做词向量表示, 并且将输出拼起来.
embed_input = fluid.layers.embedding(
input=input_word, size=[vocab_size, EMBEDDING_DIM],
dtype="float32", is_sparse=True, param_attr="input_embedding")
embed_output_list = [fluid.layers.embedding(
input=w, size=[vocab_size, EMBEDDING_DIM], dtype="float32",
is_sparse=True, param_attr="output_embedding") for w in output_context]
concat_output = fluid.layers.concat(input=embed_output_list, axis=1)

# 用 -log(sigmoid(score)) 作为度量损失函数.
var_score = fluid.layers.matmul(embed_input, concat_output, transpose_x=True)
avg_loss = 0 - fluid.layers.mean(fluid.layers.log(fluid.layers.sigmoid(var_score)))

# 使用 Adam 优化算法, 并注意需要返回变量定义名.
fluid.optimizer.AdamOptimizer().minimize(avg_loss)
return avg_loss, var_name_list
# 运行 PaddlePaddle 计算引擎前需要一些热身代码。
# 确定执行的环境, 如果支持 CUDA 可以调用 GPUPlace 函数.
device_place = fluid.CPUPlace()
executor = fluid.Executor(device_place)

main_program = fluid.default_main_program()
star_program = fluid.default_startup_program()

# 固定伪随机数种子, 一般用于保证论文效果可复现.
main_program.random_seed = RANDOM_STATE
star_program.random_seed = RANDOM_STATE

# 定义模型的架构 (之前定义函数输出) 以及模型的输入.
train_loss, tag_list = build_neural_network()
feed_var_list = [main_program.global_block().var(n) for n in tag_list]
data_feeder = fluid.DataFeeder(feed_list=feed_var_list, place=device_place)
# 下面开始训练模型的流程,将迭代器产生的 batch 不断喂入网络中:
executor.run(star_program)
for epoch_idx in range(EPOCH_NUM):
total_loss, loss_list = 0.0, []

for batch_data in data_loader():
total_loss += float(executor.run(
main_program, feed=data_feeder.feed(batch_data),
fetch_list=[train_loss])[0])
loss_list.append(total_loss)
print("[迭代轮数 {:4d}], 在训练集的损失为 {:.6f}".format(epoch_idx, total_loss))

[迭代轮数 0], 在训练集的损失为 75.395110

[迭代轮数 1], 在训练集的损失为 2.346059

[迭代轮数 2], 在训练集的损失为 0.797208

[迭代轮数 3], 在训练集的损失为 0.413886

[迭代轮数 4], 在训练集的损失为 0.254423

[迭代轮数 5], 在训练集的损失为 0.171255

[迭代轮数 6], 在训练集的损失为 0.121907

[迭代轮数 7], 在训练集的损失为 0.090095

[迭代轮数 8], 在训练集的损失为 0.068378

[迭代轮数 9], 在训练集的损失为 0.052923
# 我们可以将刚才训练过程中的损失用 matplotlib 的库函数画出来。
plt.plot(np.array(range(0, len(loss_list))), loss_list)


好啦,以上就是本次所要分享。总的来说,本节我们主要讲述了什么是词向量,什么是语言模型,SkipGram 算法的内容以及其特性,相对 CBOW 来说它对低频词更友好,而且词向量质量更佳,最后我们还细致的教您一步一步用 PaddlePaddle 实现一个简单的 SkipGram 模型。希望您多多支持,咱们下期再会。



推荐阅读:

transformer中的attention为什么scaled?

如何评价Word2Vec作者提出的fastText算法?深度学习是否在文本分类等简单任务上没有优势?

从Word2Vec到Bert,聊聊词向量的前世今生(一)

官方公众号