前面我们学习了 TF 的基础知识,这节课我们就来搭建一个真正的模型!
更新历史
- 2019.08.06: 完成初稿
课程回顾
- 在 Tensorflow 里计算的定义和执行是分开的
- 第一步:搭建 graph
- 第二步:用 session 来执行 graph 中的 operations
- Tensorboard 可以显示构造和执行 graph 的各类事件
- 常量 Constant 会保存在 graph 的定义中
- 变量 Variable 则是由 session 分配内存来存储
- 使用 dict 来给 placeholders 提供具体的值,很好用但是性能比较差
- 尽量避免 lazy loading
线性回归
所谓线性回归,就是找到变量 X 和 Y 之间的线性关系,其中 Y 是由 X 来决定的,这样说可能不是很清晰,我们先来看一张图片
这张图片展示的是 World Development Indicators 数据集的可视化结果,横轴表示预期寿命,数轴表示出生率,圆圈的大小表示该国家的人口。更加具体的图片可以在 世界发展指标 页面上查看。
粗略来看,似乎是孩子生得越多,预期寿命就越短。那么如果我想要更加精确量化这种关系的话,可以怎么做呢?换句话说,假如一个国家的出生率是 X,预期寿命是 Y,我们能不能找到一个形如 $Y=f(X)$ 这样的公式呢?
答案是肯定的,我们假设公式为 $Y{predicted} = wX + b$,预测值和真实值的误差为 MSE(Mean Square Error),即 $E[(y-y{predicted})^2]$。简单起见,我们这里只选取了 2010 年的数据,其中 X 是出生率,Y 是预期寿命,一共有 190 个数据。
为了找到公式中的 w 和 b,我们在一层神经网络上通过反向传播来计算,损失函数是前面提到的 MSE。每经过一个 epoch,我们对于预测值和真实值计算一下 MSE。具体的代码请参考 05_basic_linreg.py。
训练完成后,我们可以看到 Loss 最终约为 30.04,而 w 和 b 的值为 -6.07021427154541 和 84.92951202392578。这也验证了我们之前感性的判断,出生率确实是和预期寿命负相关的。注意,这并不是说生一个孩子就少活六年,请放心。使用 tensorboard --logdir='./graph'
然后访问 localhost:6006
就可以看到图的结构
我们也可以把真实的数据和预测的数据都画出来,有一个更加直观的感受
当然,我们也可以换另外的函数,比如 $Y_{predicted}=wX^2+uX+b$,我们对之前的函数稍加修改即可,因为这里是 X 的二次项,所以 loss 也会变大(这里的结果可以在下一小节看到,名为 L2 + MSE
的曲线)
流控制
仔细观察上图中蓝色点的分布,我们发现有一些点偏离得比较厉害,使得我们拟合的曲线向他们偏移,导致模型表现不佳。这个时候我们就可以使用 Huber loss 来降低这些异常点对曲线的影响,详情可以参考 这里,具体的公式为:
那么问题来了,如何实现,if y - y_predicted < delta
这样吗?很遗憾,不行(只有在 TF 的 eager 模式下才可以,这里暂时略过)。我们需要用 TF 自带的流控制来实现类似 if 这样的功能。相关操作有:
- 流控制:
tf.count_up_to
,tf.cond
,tf.case
,tf.while_loop
,tf.group
, … - 比较:
tf.equal
,tf.not_equal
,tf.less
,tf.greater
,tf.where
, … - 逻辑:
tf.logical_and
,tf.logical_not
,tf.logical_or
,tf.logical_xor
- 调试:
tf.is_finite
,tf.is_inf
,tf.is_nan
,tf.Assert
,tf.Print
, …
有了这些操作,我们就可以来实现 Huber loss 了,像下面这样:
1 | def huber_loss(labels, predictions, delta=14.0): |
得到的图像如下:
现在我们有了多条曲线,问题来了:怎么样知道哪条曲线最好呢?答案很简单:搞一个测试集。
tf.data
本节代码请参考 06_basic_linreg_ds.py
前面我们用的是 placeholder + feed_dict
组合来输入数据,这种方式把数据处理的工作放到 Tensorflow 之外,可以用 Python 轻松处理数据。这样做的问题在于,Python 在用的时候往往会因为写法不注意,导致只使用单一线程处理数据,就成为了拖慢计算的瓶颈。为了解决这个问题,我们可以使用 tf.data
!关于 tf.data 的详细介绍可以参考 这里。
回顾下之前的方式:我们把输入数据保存在 numpy 数组中,每一行包含 (x,y) 一对数值,为了输入数据,我们创建了 x 和 y 两个 placeholder,然后针对每一行,通过 feed_dict
把 x 和 y 具体的值绑定到 placeholder 上。这种从 numpy 数组输送数据到 TF 模型的方式非常低效。
使用 tf.data 的话,我们直接把数据保存到 tf.data.Dataset
对象中,然后通过 tf.data.Iterator
来访问。具体到代码,针对我们的数据,可以这么写:
1 | # 原型1 tf.data.Dataset.from_tensor_slices((features, labels)) |
除了 tf.data.Dataset.from_tensor_slices
之外,还可以直接从文件读取数据:
tf.data.TextLineDataset
:文件中的一行会成为一条数据,比较适合 csv 文件tf.data.FixedLengthRecordDataset
:数据集中的每一份数据的长度是一样的,比如 CIFAR 或 ImageNettf.data.TFRecordDataset
:读取以 TFRecord 格式保存的数据
对于 Iterator 也有两种方式进行创建:
dataset.make_one_shot_iterator
遍历数据集一次,不需要初始化dataset.make_initializable_iterator
可以遍历多次,但是每个 epoch 都需要进行初始化
我们什么时候应该用 tf.data
呢?
- 在做算法原型的时候,使用 feed dict 更加容易更加好写
- 当预处理比较复杂,或者有多个数据源的时候,使用
tf.data
可能不太容易 - NLP 数据一般来说就是一系列的整数,在这个情况下向 GPU 传输数据非常快,所以
tf.data
的加速效果不明显
Optimizers 优化器
前面介绍了很多,有两行代码尚未提及,我们现在来看一下
1 | optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss) |
可能很多人都会有如下的疑问:
- 为啥 session 的 run 里面要有 optimizer?
- TF 怎么知道要更新哪些变量呢?
简单来说就是:session 会更新所有跟 loss 相关的 trainable 的变量!
所有的变量在声明的时候默认都是 trainable 的。如果有一些变量我不想拿来训练,只要在声明的变量的时候设置为 False 即可,像下面这样
1 | global_step = tf.Variable(0, trainable=False, dtype=tf.int32) |
我们还可以更加深入控制计算梯度的过程,比如:
1 | # 创建优化器 |
我们也可以不让某些变量参与到梯度更新中 stop_gradient(input, name=None)
,这在 GAN 和 EM 中都有广泛应用。我们也可以用 tf.gradients
指定 TF 去计算特定的梯度(这在只训练部分模型参数的时候特别有用)。
Tensorflow 提供各种各样的优化器,如果你不知道用什么,直接用 AdamOptimizer
就好,更加详细的对比分析可以查看 这里
LR + MNIST
又是你!MNIST 数据集!每张图的尺寸是 28x28,展开成一维的张量,长度就是 784。这里的 X 就是手写数字的图片,Y 是图片对应的数字。对应的公式是 $Y{predicted}=softmax(X*w+b)$,使用的 loss 是 Cross Entropy Loss $-log(Y{predicted})$
本节的代码请参考 7_basic_linreg_mnist.py
其中计算图大概长这样
下期预告
- 用 TF 构造模型
- Word2Vec
- Eager Execution