【CS20-TF4DL】03 Tensorflow 搭建模型

前面我们学习了 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
2
3
4
5
def huber_loss(labels, predictions, delta=14.0):
residual = tf.abs(labels - predictions)
def f1(): return 0.5 * tf.square(residual)
def f2(): return delta * residual - 0.5 * tf.square(delta)
return tf.cond(residual < delta, f1, f2)

得到的图像如下:

现在我们有了多条曲线,问题来了:怎么样知道哪条曲线最好呢?答案很简单:搞一个测试集。

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
2
3
4
5
# 原型1 tf.data.Dataset.from_tensor_slices((features, labels))
# 原型2 tf.data.Dataset.from_generator(gen, output_types, output_shapes)
dataset = tf.data.Dataset.from_tensor_slices((data[:, 0], data[:, 1]))
iterator = dataset.make_initializable_iterator()
X, Y = iterator.get_next()

除了 tf.data.Dataset.from_tensor_slices 之外,还可以直接从文件读取数据:

  • tf.data.TextLineDataset:文件中的一行会成为一条数据,比较适合 csv 文件
  • tf.data.FixedLengthRecordDataset:数据集中的每一份数据的长度是一样的,比如 CIFAR 或 ImageNet
  • tf.data.TFRecordDataset:读取以 TFRecord 格式保存的数据

对于 Iterator 也有两种方式进行创建:

  1. dataset.make_one_shot_iterator 遍历数据集一次,不需要初始化
  2. dataset.make_initializable_iterator 可以遍历多次,但是每个 epoch 都需要进行初始化

我们什么时候应该用 tf.data 呢?

  • 在做算法原型的时候,使用 feed dict 更加容易更加好写
  • 当预处理比较复杂,或者有多个数据源的时候,使用 tf.data 可能不太容易
  • NLP 数据一般来说就是一系列的整数,在这个情况下向 GPU 传输数据非常快,所以 tf.data 的加速效果不明显

Optimizers 优化器

前面介绍了很多,有两行代码尚未提及,我们现在来看一下

1
2
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss)
sess.run([optimizer])

可能很多人都会有如下的疑问:

  • 为啥 session 的 run 里面要有 optimizer?
  • TF 怎么知道要更新哪些变量呢?

简单来说就是:session 会更新所有跟 loss 相关的 trainable 的变量!

所有的变量在声明的时候默认都是 trainable 的。如果有一些变量我不想拿来训练,只要在声明的变量的时候设置为 False 即可,像下面这样

1
2
3
4
global_step = tf.Variable(0, trainable=False, dtype=tf.int32)
learning_rate = 0.01 * 0.99 ** tf.cast(global_step, tf.float32)
increment_step = global_step.assign_add(1)
optimizer = tf.train.GradientDescentOptimizer(learning_rate) # learning rate can be a tensor

我们还可以更加深入控制计算梯度的过程,比如:

1
2
3
4
5
6
7
8
9
10
11
12
# 创建优化器
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.1)

# 可以计算变量的梯度
grads_and_vars = optimizer.compute_gradients(loss, <list of variables>)

# grads_and_vars 是 tuples (gradient, variable) 列表
# 可以自定义操作梯度的值,比如这里减去 1
subtracted_grads_and_vars = [(gv[0] - 1.0, gv[1]) for gv in grads_and_vars]

# 然后可以再让优化器完成梯度下降更新变量值的操作
optimizer.apply_gradients(subtracted_grads_and_vars)

我们也可以不让某些变量参与到梯度更新中 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
一点打赏,十分感谢,百分动力