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 | import tensorflow # version >= 1.50 |
有了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 | dataset = tf.data.Dataset.from_generator(gen, |
接下来定义权重(这里也就是词嵌入矩阵)。矩阵中的每一行都是一个单词的表示向量。如果向量的维度是EMBED_SIZE,那么嵌入矩阵的维度应该是\(\rm VOCAB\_SIZE \times EMBED\_SIZE\)。矩阵通常是随机初始化,这里使用均匀分布
1 | embed_matrix = tf.get_variable('embed_matrix', |
然后,定义正向传播的计算过程。为了得到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 | nce_weight = tf.get_variable('nce_weight', |
最终的输出得分定义为
1 | tf.matmul(embed, tf.transpose(nce_weight)) + nce_bias |
损失计算定义为
1 | loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weight, |
最后,优化器直接使用梯度下降优化器
1 | optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(loss) |
执行计算图
执行计算图的代码与前面非eager execution版本的线性回归/logistic回归大同小异。完整的代码可以参考github
如何组织你的TF模型
到目前为止,我们设计的模型多少都是相同的,包括两个阶段:
第一阶段是组装计算图,包含五步
- 通过
tf.data
或者占位符导入数据 - 定义权重
- 定义模型如何做推断
- 定义损失函数
- 定义优化器
第二阶段是执行计算图,这是实际上训练模型的过程,也包含五步
- 初始化所有模型变量
- 初始化迭代器/向占位符送入(feed in)训练数据
- 在训练数据上执行模型推断的过程
- 计算损失函数值
- 调整模型参数,进行优化
既然整个过程大同小异,那么如何让模型易于复用呢?可以利用Python的面向对象特性,创建一个类,如下所示
1 | class SkipGramModel: |