使用Tensorflow训练一元线性模型


引言

这是一次使用python进行机器学习的实验。

一是总结自己学过的各种python包,二是了解一下使用python进行机器学习大概是什么样的,不过这次使用主要的目的还是熟悉Tensorflow的使用。

本次实验使用到的python包及其版本:

  • Tensorflow 1.8.0
  • Numpy 1.14.3
  • Pandas 0.22.0
  • Matplotlib 2.2.2

机器环境是:macOS 10.13

Tensoflow基础知识

这里只介绍本次实验所用到的Tensorflow的关键知识、概念,想了解详情可以参考官方文档:https://tensorflow.google.cn/programmers_guide/low_level_intro

张量

TensorFlow 中的核心数据单位是张量。一个张量由一组形成阵列(任意维数)的原始值组成。张量的是它的维数,而它的形状是一个整数元组,指定了阵列每个维度的长度。以下是张量值的一些示例:

3. # 0阶张量;也叫标量;形状是[]
[1., 2., 3.] # 1阶张量;也叫向量;形状是[3]
[[1., 2., 3.], [4., 5., 6.]] # 2阶张量;也叫矩阵;形状是[2, 3]
[[[1., 2., 3.]], [[7., 8., 9.]]] # 3阶张量;形状是[2, 1, 3]

TensorFlow 使用 numpy 阵列来表示张量

您可以将 TensorFlow Core 程序看作由两个互相独立的部分组成:

  1. 构建计算图 (tf.Graph)。
  2. 运行计算图 (tf.Session)。

计算图是排列成一个图的一系列 TensorFlow 指令。图由两种类型的对象组成。

  • 指令(或“op”):图的节点。 指令说明的是消耗和生成张量的计算。
  • 张量:图的边。它们代表将流经图的值。大多数 TensorFlow 函数会返回 tf.Tensors

重要提示tf.Tensors 不具有值,它们只是计算图中元素的手柄。

我们来构建一个简单的计算图。最基本的指令是一个常量。构建指令的 Python 函数将一个张量值作为输入值。生成的指令不需要输入值。它在运行时输出的是被传递给构造函数的值。我们可以创建如下所示的两个浮点数常量 ab

a = tf.constant(3.0, dtype=tf.float32)
b = tf.constant(4.0) # 默认dtype=tf.float32
total = a + b
print(a)
print(b)
print(total)

打印语句会生成:

Tensor("Const:0", shape=(), dtype=float32)
Tensor("Const_1:0", shape=(), dtype=float32)
Tensor("add:0", shape=(), dtype=float32)

请注意,打印张量并不会如您可能预期的那样输出值 3.04.07.0。上述语句只会构建计算图。这些 tf.Tensor 对象仅代表将要运行的指令的结果。

图中的每个指令都拥有唯一的名称。这个名称不同于使用 Python 分配给相应对象的名称。张量是根据生成它们的指令命名的,后面跟着输出索引,如上文的 "add:0" 所示。

会话

要评估张量,您需要实例化一个 tf.Session 对象(通常被称为会话)。会话会封装 TensorFlow 运行时的状态,并运行 TensorFlow 指令。如果说 tf.Graph 像一个 .py 文件,那么 tf.Session 就像一个可执行的 python

下面的代码会创建一个 tf.Session 对象,然后调用其 run 方法来评估我们在上文中创建的 total 张量:

sess = tf.Session()
print(sess.run(total))

当您使用 Session.run 请求输出节点时,TensorFlow 会回溯整个图,并流经提供了所请求的输出节点对应的输入值的所有节点。因此此指令会打印预期的值 7.0:

7.0

您可以将多个张量传递给 tf.Session.runrun 方法以透明方式处理元组或字典的任何组合,如下例所示:

print(sess.run({'ab':(a, b), 'total':total}))

它返回的结果拥有相同的布局结构:

{'total': 7.0, 'ab': (3.0, 4.0)}

在调用 tf.Session.run 期间,任何 tf.Tensor 都只有单个值。例如,以下代码调用 tf.random_uniform 来生成一个 tf.Tensor,后者会生成随机的三元素矢量(值位于 [0,1)):

vec = tf.random_uniform(shape=(3,))
out1 = vec + 1
out2 = vec + 2
print(sess.run(vec))
print(sess.run(vec))
print(sess.run((out1, out2)))

每次调用 run 时,结果都会显示不同的随机值,但在单个 run 期间(out1out2 接收到相同的随机输入值),结果显示的值是一致的:

[ 0.52917576  0.64076328  0.68353939]
[ 0.66192627  0.89126778  0.06254101]
(
  array([ 1.88408756,  1.87149239,  1.84057522], dtype=float32),
  array([ 2.88408756,  2.87149239,  2.84057522], dtype=float32)
)

部分 TensorFlow 函数会返回 tf.Operations,而不是 tf.Tensors。对指令调用 run 的结果是 None。您运行指令是为了产生副作用,而不是为了检索一个值。这方面的例子包括稍后将演示的[初始化](https://tensorflow.google.cn/programmers_guide/low_level_intro#Initializing Layers)和训练指令。

占位符

目前来讲,这个图不是特别有趣,因为它总是生成一个常量结果。图可以参数化以便接受外部输入,也称为占位符占位符表示承诺在稍后提供值,它就像函数参数。

x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)
z = x + y

前面三行有点像函数。我们定义了这个函数的两个输入参数(xy),然后对它们运行指令。我们可以使用 run 方法feed_dict 参数来为占位符提供真正的值,从而通过多个输入值来评估这个图:

print(sess.run(z, feed_dict={x: 3, y: 4.5}))
print(sess.run(z, feed_dict={x: [1, 3], y: [2, 4]}))

上述操作的结果是输出以下内容:

7.5
[ 3.  7.]

另请注意,feed_dict 参数可用于覆盖图中的任何张量。占位符和其他 tf.Tensors 的唯一不同之处在于如果没有提供值给它们,那么占位符会显示错误。

Estimator

它是一种可极大地简化机器学习编程的高阶 TensorFlow API。Estimator 会封装下列操作:

  • 训练
  • 评估
  • 预测
  • 导出以供使用

为了更好的理解它是个啥,请看后面的 Premade EstimatorCustom Estimator 章节。

理论基础

直接看线性模型的目标是什么吧,以下图为例:

模型的目标是找到一条直线(图红色直线),让每一个蓝色点到与直线的y距离最小。

下面来更数学化一点的介绍:

给定一个大小为n的点集 $S = { (x_1,y_1), (x_2,y_2), … (x_n,y_n)}$ ,

线性模型的目标就是寻找一组 $W$ 和 $b$ 构成的直线 $y = Wx + b $ ,

使得所有点的损失值 $loss = \sum_i^n (Wx_i + b - y_i) ^2$ 越小越好。

因为如果我们找到了这么一组 $W$ 和 $b$ ,我们就可以预测某一个 $x_m$ 的 $ y_m$ 值。

这里我想多说几句,线性模型在实际应用中不一定能很好的预测 $y_m$ 的值,这是因为实际的数据分布也许不是线性的,可能是二次、三次、圆形甚至无规则,所以判断什么时候能用线性模型很重要。一个比较好的实践方法是,先用matplotlib画出数据分布,观察一下看看,就好比上面的蓝色点,一看就知道是线性分布,所以可以用线性模型来做。但是这种方法在大部分情况下也不能用,因为数据很多情况下有多个特征(多元),一元、二元都还好,能看出来,到了三元、四元数据,可能连图都画不出来。。这时候又怎么办呢?也很简单,用线性模型和其他模型一起套一下,评估、对比看看结果如何。

那么现在问题是,怎么让 loss 最小呢?请接着往下看。

基础版本

模型

废话不多说,直接上写好的代码:

def fit_linear_model(data, num_steps, alpha):
    """
    train with the machine learning

    :param data: training data
    :param num_steps: training steps
    :param alpha: learning rate
    :return: W and b of trained linear model
    """
    # variables
    W = tf.Variable(1, dtype=tf.float64)
    b = tf.Variable(1, dtype=tf.float64)
    x = tf.placeholder(tf.float64)
    y = tf.placeholder(tf.float64)

    # predict
    pred = W * x + b

    # loss
    loss = tf.reduce_sum(tf.square(pred - y))

    # optimizer
    optimizer = tf.train.GradientDescentOptimizer(alpha)

    # train
    train = optimizer.minimize(loss)

    train_set, test_set = split_test_set(data, frac=0.3, random=True)

    sess = tf.Session()
    sess.run(tf.global_variables_initializer())
    for i in range(num_steps):
        sess.run(train, {x: train_set['x'], y: train_set['y']})

    final_W, final_b = sess.run([W, b], {x: train_set['x'], y: train_set['y']})

    # evaluate
    final_loss, evaluate_loss = evaluate(train_set, test_set, final_W, final_b)

    print('W: {}, b: {}, final loss: {}, evaluate loss: {}'.format(final_W, final_b, final_loss, evaluate_loss))

    return final_W, final_b

下面一步一步讲解代码:

  • 11~14行:定义需要用到的张量,其中 $W$ 和 $b$ 是变量,并且都给了初始值 1;$x, y$ 是占位符用于接收数据。
  • 17行:计算预测值
  • 20行:计算损失值 loss
  • 23、26行:使用 GradientDescentOptimizer 来优化模型,减小loss ,这个类的原理是梯度下降,可以看到我们传递了学习速率 alphaα 。可以好奇的是,我们没有计算梯度,而是调用了minimize 方法,这个方法分两步进行,第一步是使用 compute_gradients 计算梯度,第二步是使用 apply_gradients 更新参数值,凭借经验可以知道,第一步其实就是在计算偏导数,那么tensorflow是怎么做到,可以计算任意元线性模型的偏导数的呢,我大概扫描了一下源码,猜测应该是用了计算图
  • 28行:使用自定义方法 split_test_set 将数据集划分为 训练集:测试集= 7:3,即有30%的数据用作测试集。
  • 30~33行:训练模型。我比较好奇 global_variables_initializer 是个神魔恋,几乎tensorflow应用都要执行这个东西,看看文档就知道它会创建初始化程序时图中就存在的变量比如代码中的 $W$ 和 $b$,其实就是 variables_initializer(global_variables()) 的缩写。
  • 35行:获取最终模型,即 $W$ 和 $b$ ,这里需要转变以下思维, $W$ 和 $ b $ 是张量(即图中的一个节点),所以需要通过 run 方法获取到。讲道理应该可以通过类似 get_variable 的方法拿到值,看了下貌似没有这个方法。
  • 38行。使用自定义方法 evaluate 评估模型,计算模型在训练集、测试集上的损失值。
  • 40行。打印训练结果。
  • 42行。返回模型。

这是个封装好的函数,只要传入数据集、训练步数、学习率就可以得到训练好的模型了(即 $W$ 和 $b$)。

需要提醒一下的是,第33、35行的 {x: train_set['x'], y: train_set['y']} 不能把 key 写成单引号的 {'x': train_set['x'], 'y': train_set['y']}

数据

那么下面是需要拿到数据,这里我用的是随机生成的数据:

def linear_data(data_size, devi_degree):
    """
    Make random linear data
    :param data_size: data size
    :param devi_degree: degree of deviation
    :return: linear data with x and y
    """
    # standard linear function
    x = np.array(range(data_size), dtype=np.float64)
    y = 3 * x + 0.6

    # make deviation
    y += np.random.randn(data_size) * devi_degree

    data = pd.DataFrame({'x': x, 'y': y})

    return data

做法是:

  • 先拿到大小为data_size 的标准一元一次函数点集
  • 在此基础上将每个点在y轴方向上随机偏移一段距离,偏离程度是 devi_degree

再来划分训练集和测试集:

def split_test_set(df, frac=0.3, random=True):
    """
    Split DataFrame to train set and test set
    :param df:
    :param frac:
    :param random:
    :return:
    """
    test_size = int(len(df) * min(frac, 1))
    if random:
        df = df.sample(frac=1).reset_index(drop=True)
    return df[test_size:].reset_index(drop=True), df[:test_size].reset_index(drop=True)

评估

def evaluate(train_set, test_set, W, b):
    """
    Evaluate the model's loss
    :param train_set:
    :param test_set:
    :param W:
    :param b:
    :return: train_loss, evaluate_loss
    """
    x = tf.placeholder(tf.float64)
    y = tf.placeholder(tf.float64)

    # predict
    pred = W * x + b

    # loss
    loss = tf.reduce_sum(tf.square(pred - y))

    sess = tf.Session()
    sess.run(tf.global_variables_initializer())

    train_loss = sess.run(loss, {x: train_set['x'], y: train_set['y']})
    evaluate_loss = sess.run(loss, {x: test_set['x'], y: test_set['y']})

    sess.close()

    return train_loss, evaluate_loss

这里的loss评估是使用预测值-实际值平方再求和,与 理论基础 章节描述的 loss 一样。

绘图

这部分就简单了,把点画一下,把直线画一下就可以了,注意直线最好和点不一样的颜色,所以标红。

def print_linear_model(data, W, b):
    """
    print the data and the predictions of linear model
    :param data:
    :param W: W of linear model
    :param b: b of linear model
    """
    x = np.array(data['x'])
    y = np.array(data['y'])

    pred = np.array(W * x + b)

    plt.scatter(x, y, linewidths=1)
    plt.plot(x, pred, color='red')

    plt.show()

主程序

每一部分的函数写完啦,现在要做的是将这几个函数组合起来用,上代码。

import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--data_size', default=50, type=int, help='data size')
parser.add_argument('--num_steps', default=1000, type=int, help='number of trannig steps')
parser.add_argument('--devi_degree', default=10, type=int, help='Degree of deviation in stand linear data')
parser.add_argument('--alpha', default=0.00001, type=float, help='learning rate of gradient decent')

def main(argv):
    args = parser.parse_args(argv[1:])

    data = linear_data(args.data_size, args.devi_degree)

    W, b = fit_linear_model(data, args.num_steps, args.alpha)

    print_linear_model(data, W, b)

if __name__ == '__main__':
    tf.logging.set_verbosity(tf.logging.INFO)
    tf.app.run(main)

这里用到了 argparse 包来处理传入的参数

把我上面给的所有代码,都复制到tf-linear.py 文件里,接着运行:

python tf-linear.py --data_size=100 --alpha=0.000001 --devi_degree=30 --num_steps=5000

即可看到之前那个图啦。

这里需要注意一下,alpha 参数不要设太大了。。不然会梯度爆炸。。程序员运行不出结果($W$ 和 $b$ 都会变成 nan

Premade Estimator

模型

如果你看懂了基础版本的代码。。。那这部分就简单的很了,还是一样,直接上代码:

def fit_estimator(data, num_steps):
    """
    train with estimator

    :param data:
    :param num_steps:
    :return:
    """
    feature_columns = [
        tf.feature_column.numeric_column('x')
    ]

    estimator = tf.estimator.LinearRegressor(feature_columns=feature_columns)

    train_set, test_set = split_test_set(data, frac=0.3, random=True)

    input_fn = tf.estimator.inputs.numpy_input_fn(
        {'x': train_set['x']}, train_set['y'], batch_size=4, num_epochs=None, shuffle=True
    )

    estimator.train(input_fn=input_fn, steps=num_steps)

    W = estimator.get_variable_value('linear/linear_model/x/weights')
    b = estimator.get_variable_value('linear/linear_model/bias_weights')
    final_W, final_b = float(W), float(b)

    final_loss, evaluate_loss = evaluate(train_set, test_set, final_W, final_b)

    print('W: {}, b: {}, final loss: {}, evaluate loss: {}'.format(final_W, final_b, final_loss, evaluate_loss))

    return final_W, final_b

还是按步骤来解释吧:

  • 9~11行:定义特征列
  • 13行:创建 线性回归器 ,也就是 estimator
  • 15行:使用自定义方法 split_test_set 将数据集划分为 训练集:测试集= 7:3,即有30%的数据用作测试集。
  • 17~21行:使用训练集训练模型,也就是训练线性回归器
  • 23~24行:获取最终模型,即 $W$ 和 $b$ 。可以好奇的是,为什么 $W$ 和 $b$ 的获取方式这么奇葩。。。其实我是通过dir 命令才知道,可以用这种方法获取到参数,如果有其他优雅的方法可以告诉我哦。
  • 27行。使用自定义方法 evaluate 评估模型,计算模型在训练集、测试集上的损失值。
  • 29行。打印训练结果。
  • 31行。返回模型。

可以看到,比咱们自己手撸模型要简单多了。首先,预测函数不用自己写了,优化器也不用自己创建了,甚至连for-loop也不用了,直接调用 train 方法就万事大吉了。

这里需要注意一下,LinearRegressor 有个 weight_column 参数,如果不给的话,初始值是1,形状是 (1,),可以理解为是 np.array([1.]),所以才能把他转成 float ,如果大于1阶,估计会转换失败。

主程序

数据、绘图都不变,只需要修改 main 方法为如下即可:

def main(argv):
    args = parser.parse_args(argv[1:])

    data = linear_data(args.data_size, args.devi_degree)

    # W, b = fit_linear_model(data, args.num_steps, args.alpha)

    W, b = fit_estimator(data, args.num_steps)

    print_linear_model(data, W, b)

接着还是运行:

python tf-linear.py --data_size=100 --alpha=0.000001 --devi_degree=30 --num_steps=5000

然后可以看到和之前那个图差不多的样子啦。

Custom Estimator

模型

依然先上代码:

def fit_custom_estimator(data, num_steps, alpha):
    """
    train with custom estimator

    :param data:
    :param num_steps:
    :param alpha:
    :return:
    """

    def model_fn(features, labels, mode):
        W = tf.get_variable('W', 1., dtype=tf.float64)
        b = tf.get_variable('b', 1., dtype=tf.float64)

        # predict
        pred = W * tf.cast(features['x'], dtype=tf.float64) + b

        # loss
        loss = tf.reduce_sum(tf.square(pred - labels))

        # optimizer
        optimizer = tf.train.GradientDescentOptimizer(alpha)

        # global step
        global_step = tf.train.get_global_step()

        # train
        train = tf.group(
            optimizer.minimize(loss),
            tf.assign_add(global_step, 1)
        )

        return tf.estimator.EstimatorSpec(
            mode=mode,
            predictions=pred,
            loss=loss,
            train_op=train
        )

    feature_columns = [
        tf.feature_column.numeric_column('x')
    ]

    estimator = tf.estimator.Estimator(
        model_fn=model_fn
    )

    train_set, test_set = split_test_set(data, frac=0.3, random=True)

    input_fn = tf.estimator.inputs.numpy_input_fn(
        {'x': train_set['x']}, train_set['y'], batch_size=4, num_epochs=None, shuffle=True
    )

    estimator.train(input_fn=input_fn, steps=num_steps)

    W = estimator.get_variable_value('W')
    b = estimator.get_variable_value('b')
    final_W, final_b = float(W), float(b)

    final_loss, evaluate_loss = evaluate(train_set, test_set, final_W, final_b)

    print('W: {}, b: {}, final loss: {}, evaluate loss: {}'.format(final_W, final_b, final_loss, evaluate_loss))

    return final_W, final_b

可以分为两部分,第一部分是编写 model_fn ,第二部分是调用自定义的estimator。

先看第一部分吧,也就是11行~38行。

  • 12~13行:获取当前session中的 $W$ 和 $b$ 变量,如果不存在就设置他们为 1
  • 16行:计算预测值。
  • 19行:计算损失值 loss
  • 22行:实例化 GradientDescentOptimizer 类,用于优化模型。
  • 25行:拿到 global_step 即当前步数(这个步数是全局的)。
  • 28~31行:定义训练节点要做的操作,即 使用 GradientDescentOptimizer 优化模型,并且把 global_step 加一。group 方法表示把多个操作放在一个节点里,这个方法没有返回值。
  • 33~38行。关键行,实例化 Estimator 类,并把一些必要参数传给它。

第二部分调用自定义的estimator大部分代码和调用预定义的estimator一样,就是在第44~46行,我们把自己写的 model_fn 作为参数传入到实例化的 Estimator 里。

在我们调用estimator的 train 方法时,tensorflow就会在内部调用我们写的 model_fn 方法来计算loss、prediction以及训练模型。

model_fnmode 参数是啥?它其实有三个值分别是:ModeKeys.TRAIN、ModeKeys.EVAL、ModeKeys.PREDICT,用来指示本次调用的目的是训练、评估还是预测。不过在本次实验中,我们写了自己的evaluate 方法,所以就不区分这三种情况了。

主程序

def main(argv):
    args = parser.parse_args(argv[1:])

    data = linear_data(args.data_size, args.devi_degree)

    # W, b = fit_linear_model(data, args.num_steps, args.alpha)

    # W, b = fit_estimator(data, args.num_steps)
    
    W, b = fit_custom_estimator(data, args.num_steps, args.alpha)

    print_linear_model(data, W, b)

* 扩展阅读

Tensorboard

在使用 Estimator 训练模型的时候,tensorflow会自动记录训练过程中某些数据的变化,比如 loss 值,拿premade estimator来说,在训练完成后,控制会打印出如图所示的一些信息:

可以看到有一些数据被存到了:

/var/folders/4c/14xc6rkj1ndgw2x5kw5sw8hr0000gn/T/tmpo5gg328l/model.ckpt.

查看这个目录发现里面还有很多其他文件

下面运行Tensorboard,来查看这次训练中的数据变化:

tensorboard --logdir /var/folders/4c/14xc6rkj1ndgw2x5kw5sw8hr0000gn/T/tmpo5gg328l

这会启动一个web服务,点击终端提示的网址即可打开,我的是在 6006 端口。

源码和参考资料

本次实验的开源github地址:https://github.com/JerryCheese/tensorflow-study

参考的文档:

[1] TensorFlow 完整的TensorFlow入门教程, https://blog.csdn.net/lengguoxing/article/details/78456279

[2] TensorFlow 使用入门, https://tensorflow.google.cn/get_started/premade_estimators


文章作者: jerrycheese
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jerrycheese !
  目录