线性回归

线性回归是非常基础的统计学知识,也是所有机器学习研究的源头,因为现在的数学都还没有能很好的解决非线性的问题,所以基本上所有的机器学习的思路都是用非线性核函数,将数据转换到近似线性的空间中,然后再用线性方法如线性回归进行解决。一般的,线性回归是要解决如下的问题,对线性公式: $$ y = wx + b $$ 我们采集了大量 $x$ 和 $y$ 的值对,代表客观观察,现在想要还原他的参数 $w$ 和 $b$ ,从而还原出整个公式。但是由于实际的数据会有噪音,不妨假设这个噪音符合正态分布,则我们可以用如下公式表示这个真实公式: $$ y = wx + b + N(0,\epsilon) $$ 其中 $\epsilon$ 代表噪音的标准差。我们使用 NumPy 生成人造数据:

import numpy as np

N=100 # 100 个数据

w_true=5 # 假设 w = 5

b_true=5 # 假设 b = 5

noise_scale=.1
# 构建 Nx1 维的 x 数据

x_np=np.random.rand(N,1)
# 生成正态分布的 Nx1 维的噪音

noise = np.random.normal(scale=noise_scale,size=(N,1))
# 生成 y 数据,这里的 y 是 1xN 维的,方便计算

y_np=np.reshape(w_true*x_np+b_true+noise,(-1))

使用 matplotlib 画个图看一下生成的数据的分布,在 jupyter 中使用

%matplotlib inline
import matplotlib
import numpy as np
import matplotlib.pyplot as plt

line,=plt.plot(x_np,y_np,'bo')

结果如图所示

生成的数据分布

设置训练网络

机器学习的步骤最主要的就是3步:

  1. 定义损失函数,也就是如何评价当前参数的指标,一般来说损失函数的值越小,当前得到的参数性能越好
  2. 定义优化算法。常见的优化算法有梯度下降算法,退火算法和遗传算法。用于在当前参数下,快速找到下一批待选择参数组合
  3. 反复迭代,寻找最优解。利用上面两个步骤反复多次,直到函数收敛,得到最终的参数,也就是模型训练完成。

本次案例中,我们用最简单的方式来定义我们的训练网络。损失函数就直接使用预测值与真实值之间的空间距离: $$ loss = ( y_{pred} - y )^2 $$

而优化算法直接使用梯度下降算法: $$ W_{next} = W - \alpha\Delta W $$ 其中的 $\alpha$ 就是迭代步长,是一个超参数,需要手动选择或者使用网格搜索才能调优的参数。

具体的代码如下:

import tensorflow as tf

# 定义数据占位符

with tf.name_scope("placeholders"): 
    x = tf.placeholder(tf.float32, (N, 1))
    y = tf.placeholder(tf.float32, (N,))
# 定义变量占位符

with tf.name_scope("weights"):
    # 使用正态分布的初始化数据

    W = tf.Variable(tf.random_normal((1, 1)))
    b = tf.Variable(tf.random_normal((1,)))
# 定义计算函数

with tf.name_scope("prediction"):
    y_pred = tf.matmul(x, W) + b
# 定义损失函数

with tf.name_scope("loss"):
    l = tf.reduce_sum((y - tf.squeeze(y_pred))**2)
# 定义下降算法

with tf.name_scope("optim"):
    # 使用默认的梯度下降算法, alpha = 0.001

    train_op = tf.train.AdamOptimizer(.001).minimize(l)
# 定义统计项

with tf.name_scope("summaries"):
    # 统计损失函数

    tf.summary.scalar("loss", l)
    # 将所有指标写入磁盘

    merged = tf.summary.merge_all()

执行训练

我们将我们生成的模拟数据,填入到训练网络的占位符,多次迭代进行网络训练。每次迭代,都将中间结果写入磁盘,方便我们观察我们的网络的有效性。代码如下:

# 定义结果写入文件

train_writer = tf.summary.FileWriter('logs/train', tf.get_default_graph())

n_steps = 8000 # 迭代 8000 次

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    # 训练模型

    for i in range(n_steps):
        feed_dict = {x: x_np, y: y_np}
        _, summary, loss = sess.run([train_op, merged, l], feed_dict=feed_dict)
        print("step %d, loss: %f" % (i, loss))
        train_writer.add_summary(summary, i)
        
    # 计算参数

    w_final, b_final = sess.run([W, b])

    # 计算预测值

    y_pred_np = sess.run(y_pred, feed_dict={x: x_np})

8000步后,损失函数稳定值在3.59左右

迭代损失函数最终得到的值

我们使用命令查看一下刚才的训练过程

tensorboard --logdir logs/train

损失函数的下降图

上图为损失函数的下降曲线。可以看到快接近最终值时,曲线明显收敛。

另一张图是网络的执行图:

网络训练可视化

上图可以看到训练过程中每一步的流程和执行次数,线条越粗执行的次数越多。这个图可以方便的帮助我们优化自己的训练网络。

验证训练结果

现在我们已经得到了一个训练好的网络,这个预测网络的两个参数 $W_{final}$ 和 $b_{final}$,也就得到了最终的线性方程的公式: $$ y_{pred}=W_{final}x+b_{final} $$

现在我们用两种不同的指标来验证我们训练的这个模型: $R^2$ 和 RMSE (root-mean-squared error,均方根差). $R^2$ 是用于表示两个变量之间的相关性,用0~1之间的值表示,0表示不相关,1表示非常相关。对于数据集合 X 和 Y ,我们定义其 $R^2$ 如下 $$ R^2 = \frac{cov(X,Y)^2}{\sigma^2_X\sigma^2_Y} $$ 其中 $cov(X, Y)$ 是 X和Y的协方差,表示这两个数据集之间的相似情况,而 $\sigma_X$ 和 $\sigma_Y$ 是标准差,记录数据集各自内部数据的独立性。所以 $R^2$表示在两个数据集内,有多少个独立变量可以解释数据集的相关性。我们用如下代码计算 $y$和$y_{pred}$的$R^2$:

from scipy.stats import pearsonr

y_pred_np = np.resharpe(y_pred_np, -1)
r2 = pearsonr(y_np, y_pred_np)[0]**2 
print("Pearson R^2:", r2)
>> Pearson R^2: 0.994
# 绘制图像

plt.clf()
plt.xlabel["Y-true"]
plt.ylabel["Y-pred"]
plt.title("Predicted versus True valuees")
plt.scatter(y_np, y_pred_np)

预测值和真实值之间的关系

从值可以看出,预测值和真值之间 $R^2$ 近似 1 ,高度相关,说明我们的模型基本上学会了数据的真实规则。但是我们仔细观察绘制的图像发现,两个数轴之间的值并不完全对应。说明 $R^2$ 无法在缩放尺度上描述模型的准确性。我们使用 RMSE 计算:

from sklearn.metrics import mean_squared_error

rms = np.sqrt(mean_squared_error(y_np, y_pred_np))
print("RMSE: ", rms)
>> RMSE: 1.027
# 绘制 RMSE 图像 

plt.clf()
plt.xlabel("x")
plt.ylabel("y")
plt.title("True Model versus Learned Model ")
plt.xlim(0, 1)
plt.scatter(x_np, y_np)
x_left = 0
y_left = w_final[0]*x_left + b_final
x_right = 1
y_right = w_final[0]*x_right + b_final
plt.plot([x_left, x_right], [y_left, y_right], color='k')

线性公式图

RMSE代表数据预测值和真值之间的平均差异,这个值大约 1.027,说明在一定程度上,数据和真实值之间有一定差距,说明我们的算法在梯度下降过程中,达到了局部最优险井,这也是梯度下降算法常见的问题,我们可以通过多初始点为,多步长值等超参数的调优,说着增加迭代次数的方式,降低这种风险。

构建的时候遇到的问题

  • 运行时报错: FailedPreconditionError (see above for traceback): Attempting to use uninitialized value weights_2/Variable

    这个问题是因为我在开始忘了执行 sess.run(tf.global_variables_initializer()) ,导致tf的session无法找到对应的全局值,从而报错。在初始化 session 时加上这句话就行。

  • 运行时报错:You must feed a value for placeholder tensor ‘placeholders_6/Placeholder’ with dtype float and shape [100,1]

这是因为我在交互式命令行下执行的 python 语句,在多次执行某一语句块时,干扰了图的设定。所以在每次执行设定网络参数前,先执行一句 tf.reset_default_graph() 重置网络配置就行。

总结

本篇只是用一个简单的例子说明了线性回归的基本流程。但是基本上覆盖了机器学习的全部流程:定义模型,定义损失函数,定义调优函数,然后迭代运行,在执行方法上也涵盖了tf的大部分功能点。同时因为线性回归是基本上所有机器学习的基础,所以需要仔细体会。