CS20 04. Eager Execution与word2vec

Eager Execution

TensorFlow本身被设计为声明式的编程框架。开发人员先定义好的是一个计算图,然后这个图在某个会话里被解释执行。这样的设计方式使得TF代码易于优化、部署和重写,但是也引入了一些问题

  • 难以调试。TF都是在图构成好以后很久才报错,而且不能使用pdb或其它打印语句来调试
  • 不pythonic。TF的控制流甚至都没用Python自己的关键字(例如用tf.while_loop而不是Python的while),而且很难将自定义的数据结构用来构建图

针对这样的问题,TF从1.5版本开始引入了一种称为“Eager Execution”的库,这个库使开发人员可以像numpy那样编写TF程序,同时支持GPU加速、自动微分、pdb断点调试。Eager execution允许用户使用python的数据结构和控制流,可以提供即时的错误报告。例如

1
2
3
4
5
6
7
8
9
import tensorflow # version >= 1.50
import tensorflow.contrib.eager as tfe


tfe.enable_eager_execution

i = tf.constant(0)
while i < 1000:
i = tf.add(i, 1)

有了eager execution以后,就不用再绞尽脑汁如何使用占位符、会话、延迟加载等问题

大部分TF API在eager execution下都可用,但是如果启用了这种模式,建议使用tfe.Variable来声明和定义变量,使用tfe.Iterator来迭代数据集,使用面向对象的层定义(例如tf.layers.Dense

更多内容可参考官方Readme

使用TensorFlow实现词嵌入

理论简介

Hinton的课程里曾经提到过,可以使用神经网络学习到词语的分布式表示(向量表示),学到的称为词向量(word vector)或词嵌入(word embedding)。词向量现在是语言模型、机器翻译、情感分析等任务的核心。Tomas Mikolov在2013年发表了两篇论文,提出了一组称为word2vec的模型来产生词向量。算法的具体细节将在之后的笔记中介绍,其核心算法有两种模型实现,分别称为skip-gram和CBOW(Continuous Bag-of-Words,连续词袋模型)。算法上两者思想类似,CBOW是通过上下文来预测中间单词,skip-gram是根据单词预测上下文。CBOW适用于小数据集,而skip-gram适用于大数据集。本课将试图使用TF来实现skip-gram模型,即训练一个有一个隐藏层的神经网络,提取隐藏层的权重,将其作为要学习的“词向量”或“嵌入矩阵”

word2vec的模型会使用softmax函数来得到中间单词(或周围单词)的概率分布。但是当单词数很多的时候,softmax会造成性能上的瓶颈。解决这个问题有两种主流的方法:分层softmax和基于采样的softmax。Mikolov等人在论文Distributed Representations of Words and Phrases and their Compositionality中给出了一种称为负采样的算法,使用该算法可以更快地训练,还可以得出常见单词的更好的向量表示。这种算法可以看作是噪声对比估计(Noise Contrastive Estimation, NCE)的一种简化形式,但是负采样法并不能在理论上保证其导数能趋向于softmax函数的梯度,但是NCE能够保证这一点。因此在本讲使用NCE。需要注意的是,基于采样的方法仅在训练时有用,在预测时仍然需要使用全softmax函数来得到归一化的概率分布

实现

组装计算图

由于eager execution是TF的新特性,还不成熟,因此这里还是用传统的计算图方式来做。首先要做的是创建数据集和生成采样数据,这里使用的是单词的ID而不是单词本身,因此对BATCH_SIZE条数据,输入的形状是[BATCH_SIZE],而输出的形状是[BATCH_SIZE, 1]

1
2
3
4
5
dataset = tf.data.Dataset.from_generator(gen, 
(tf.int32, tf.int32),
(tf.TensorShape([BATCH_SIZE]), tf.TensorShape([BATCH_SIZE, 1])))
iterator = dataset.make_initializable_iterator()
center_words, target_words = iterator.get_next()

接下来定义权重(这里也就是词嵌入矩阵)。矩阵中的每一行都是一个单词的表示向量。如果向量的维度是EMBED_SIZE,那么嵌入矩阵的维度应该是\(\rm VOCAB\_SIZE \times EMBED\_SIZE\)。矩阵通常是随机初始化,这里使用均匀分布

1
2
3
embed_matrix = tf.get_variable('embed_matrix', 
shape=[VOCAB_SIZE, EMBED_SIZE],
initializer=tf.random_uniform_initializer())

然后,定义正向传播的计算过程。为了得到batch中中心词的向量表示,需要从嵌入矩阵中找到具体的那一行。可以使用TF提供的方法tf.nn.embedding_lookup便捷地完成这项任务。当需要做独热向量(one-hot vector)与其它矩阵的乘法时,使用这个方法可以避免很多不必要的计算。具体用法为

1
embed = tf.nn.embedding_lookup(embed_matrix, center_words, name='embed')

再接下来是损失函数。TF已经为NCE提供了一个封装好的实现tf.nn.nce_loss。要使用这个函数,需要先定义隐藏层的权重和偏置,它们在训练中会被优化器更新

1
2
3
4
nce_weight = tf.get_variable('nce_weight', 
shape=[VOCAB_SIZE, EMBED_SIZE],
initializer=tf.truncated_normal_initializer(stddev=1.0 / (EMBED_SIZE ** 0.5)))
nce_bias = tf.get_variable('nce_bias', initializer=tf.zeros([VOCAB_SIZE]))

最终的输出得分定义为

1
tf.matmul(embed, tf.transpose(nce_weight)) + nce_bias

损失计算定义为

1
2
3
4
5
6
loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weight, 
truetruetruetruetrue biases=nce_bias,
truetruetruetruetrue labels=target_words,
truetruetruetruetrue inputs=embed,
truetruetruetruetrue num_sampled=NUM_SAMPLED,
truetruetruetruetrue num_classes=VOCAB_SIZE))

最后,优化器直接使用梯度下降优化器

1
optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(loss)

执行计算图

执行计算图的代码与前面非eager execution版本的线性回归/logistic回归大同小异。完整的代码可以参考github

如何组织你的TF模型

到目前为止,我们设计的模型多少都是相同的,包括两个阶段:

第一阶段是组装计算图,包含五步

  • 通过tf.data或者占位符导入数据
  • 定义权重
  • 定义模型如何做推断
  • 定义损失函数
  • 定义优化器

第二阶段是执行计算图,这是实际上训练模型的过程,也包含五步

  • 初始化所有模型变量
  • 初始化迭代器/向占位符送入(feed in)训练数据
  • 在训练数据上执行模型推断的过程
  • 计算损失函数值
  • 调整模型参数,进行优化

既然整个过程大同小异,那么如何让模型易于复用呢?可以利用Python的面向对象特性,创建一个类,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SkipGramModel:
""" Build the graph for word2vec model """
def __init__(self, params):
pass

def _import_data(self):
""" Step 1: import data """
pass

def _create_embedding(self):
""" Step 2: in word2vec, it's actually the weights that we care about """
pass

def _create_loss(self):
""" Step 3 + 4: define the inference + the loss function """
pass

def _create_optimizer(self):
""" Step 5: define optimizer """
pass
坚持原创技术分享,您的支持将鼓励我继续创作!