一、前言
从上一次发的关于机器学习的文章到现在已经快一年了。 期间懈怠了很长一段时间,最近重新开启学习机器学习之路,准备一边学习,一边记录下这个过程, 所以就有了写 《My Machine Learn》系列文章的想法。当然所思缩写均是我自己的理解,不能保证完全正确,如果疏漏的地方,还请看官不灵赐教。 闲话少叙,言归正传。
二、简介
说道机器学习,深度学习之类的东西,大家首先想到的可能就是神经网路(或者说人工神经网路), 一说神经网路,大家同样会很自然的联想到,咱人类大脑内部数之不尽的神经元,大概就是下图所示的样子。
从生物仿生学方面来说,或许真是如此。 但如果从数学角度来说的话,机器学习,神经网络更像是一堆数学公式的集合。
因此首先就来看看这一切的基础中的基础: 线性回归。
什么是线性回归呢? 从百度百科中可以看到有如下解释:
线性回归是利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法,运用十分广泛。其表达形式为y = w’x+e,e为误差服从均值为0的正态分布。
回归分析中,只包括一个自变量和一个因变量,且二者的关系可用一条直线近似表示,这种回归分析称为一元线性回归分析。如果回归分析中包括两个或两个以上的自变量,且因变量和自变量之间是线性关系,则称为多元线性回归分析
因此咱就来聊聊这个一元线性回归(主要是俺们对多元线性回归也不熟悉 (⊙o⊙)… )
一元线性方程这玩意儿,应该从小学的时候就开始学了吧,或者应该说一元一次方程,形如: ax + b = 0
太细节的这里就不再啰嗦了,有兴趣的可以去翻翻小学或者初中课本。
我还是比较喜欢按照机器学习的风格来定义,这里就将方程定义为:
y = w x + b y = wx + b y=wx+b
其大致图形如下所示:
结合上面的方程,其中,w表示 直线的斜率, b 表示直线在y轴上的截距。
三、应用
在线性回归的定义中提到, 线性回归是利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法。 也就是说它可以用来进行比较神秘的“预测”操作。 简单来说,给一堆数据,这对数据包含自变量部分与因变量部分。 自变量对应方程中的“X”, 因变量对应方程中的“Y”。 拿机器学习的术语来说,就是 数据集和标签集。
我们拿到这些数据与标签之后,就可以进行回归分析,也就是确定这个数据与标签之间的依赖关系,最终得到一条“合理”的直线,而成对的数据(X) 和 标签(y)就是分布在这条直线周围“点”。 而且这条“合理”的直线必须满足,所有点到直线距离之和最小。
四、训练
所谓“训练”,就是上面说的, 找到这条“合理”直线的过程, 或者说数据拟合直线的过程。这里就举一些例子来简单说明。
4.1. 首先给出一条直线的方程以及其图形
方程(为了后面好记,在公式当中添加 f(x) 字样):
y = f ( x ) = 2 x + 1 y = f(x) = 2x + 1 y=f(x)=2x+1
图形:
上图就是方程对应的图形(用excel画的)。 前面我们说到,要找到这条“合理”的直线,必须要满足,所有的点离直线的距离的总和最近。
咱们先从单个的点开始, 先看图中的点A, 其坐标是(6, 20)。 这个点在直线f(x)的上方,如果我们要让f(x)靠近点A有什么方法呢?
答案很简单,就是修改f(x)的参数。 从直观上就可以看出, 要让直线f(x)靠近点A, 可以将直线向上平移, 也可以改变直线的倾斜度(即斜率),还可以同时平移和改变斜率。
4.2. 移动直线
移动直线的实现方法,我们这里讲3种,绝对值技巧, 平方技巧, 梯度下降。接下来我们就依次来看这3个东东到底是什么玩意儿。
- 绝对值技巧
首先要说明的一点是,所谓绝对值技巧,并不是对某个公式取绝对值,而是非常简单粗暴的方式在f(x)的常参数(w和b)加上或减去一个绝对的值, 比如需要增大斜率时,就给f(x)中的 w 加上一个绝对的值,反之就减去一个绝对的值。
假如有如下直线,以及A,B,C,D四个点:
如果想让直线靠近点A, 那么我们可以 给f(x)中的 b 加 1,这样直线就像上平移了一个单位。同时我们也可以给w加上一个值,如何来决定这个“绝对的值”呢, 这里我们直接取点A的x坐标,即p1, 移动之后的直线方程为:
f ( x ) = ( w + p 1 ) x + ( b + 1 ) f(x) = (w + p1)x + (b + 1) f(x)=(w+p1)x+(b+1)
同理,我们可以处理让直线靠近点B, 因为点B在直线下方,所以对b 减 1,让直线向下平移,对w减去一个“绝对的值”来减小斜率,让直线倾斜度变小,更靠近点B, 同样,我们以点B的x坐标p1作为这个“绝对的值”。移动之后的直线方程为:
f ( x ) = ( w − p 1 ) x + ( b − 1 ) f(x) = (w - p1)x + (b - 1) f(x)=(w−p1)x+(b−1)
注: 上面的 p1和1都是“绝对的值”,当然也可以选取其他的值作为“绝对的值”, 比可以选 3,4 分别作为对w和b操作的“绝对”的值
从上面两点其实可以总结出,如果直线要靠近的点在直线的上方,就加上一个绝对的值,如果在直线的下方就减去一个绝对的值。同理,在处理点C的时候,就对f(x)的w和b加上一个“绝对的值”。看官你可能要问了, 对于点C, 应该要减小直线的倾斜度(即w减去某个值)才能靠近点C。 因为对于点C, 其x坐标p2是一个负值, 所以会有 w + p2 < w 的结果。 这里就能明白在对绝对值技巧的定义解释的时候,为什么要说是“绝对的值”,而不是“绝对值(形如: |a|)”了。
一切看上去很美好,但这都是表象,绝对值技巧这部电影的反派们终于登场了。 第一位就是, 我们在使用绝对值技巧对直线进行移动之后,很可能出现移动过头的情况,还是用上面的例子来说明,让直线f(x)靠近点A, 其移动之后的直线可能会是下面的样子:
这可能并不是我们想要的,为什么会出现这个问题呢? 是因为点A的x坐标p1可能很大,这样对直线f(x)的斜率增加的幅度就会很大。 还好这个反派不是很厉害,咱们还有解决掉它的方法。
这个方法也是比较简单粗暴的,就是给这个“绝对的值”加一个系数 α \alpha α, 也就是机器学习中的学习率,一般它是一个0 ~ 1 之间的值。增加了学习率 α \alpha α之后的公式如下:
y = f ( x ) = ( w + p 1 ∗ α ) x + ( b + α ) y = f(x) = (w + p1 * \alpha )x + (b + \alpha) y=f(x)=(w+p1∗α)x+(b+α)
so,第一个反派已被咱KO了, 接下来看第二个反派。
因为咱们是通过 w加 目标点的x坐标来调整直线的斜率。 所以这里就有一个隐藏的Bug, 假设现在出现了新的点E(p3, q1), 如下图所示:
从图上可以看出,要让直线f(x)靠近点E,需要调整的斜率和靠近点A需要调整的斜率相比,是要小很多的。但是根据“绝对值技巧”的方法,在直线上方的点,调整斜率都是 w 加上 x坐标乘以学习率 α \alpha α, 而点E的x坐标p3比点A的x坐标p1要大一些,得到的新斜率 w + p 3 ∗ α w + p3 * \alpha w+p3∗α 要大于 $ w + p1 * \alpha$, 这与我们的想要的结果可不太一样。
这一个反派比较厉害一点, 俺们有点招架不住了。 所以先跑了再说,看下一个方法。
- 平方技巧
上一个技巧是使用到了目标点的x坐标, 而这里是在绝对值技巧的基础上增加了对目标点y坐标的使用。
如上图所示, 直线 f ( x ) = w x + b f(x) = wx + b f(x)=wx+b上的点F与直线上方的点A其x坐标是一样的,通过直线的方程可以计算得到,点F对应的y坐标为:
y ^ = w p 1 + b \hat{y} = wp1 + b y^=wp1+b
其中 y ^ \hat{y} y^ 表示预期值,也就是根据自变量x得到的因变量y。
同样的原理,如果点A是目标点的话,p1就是输入数据(自变量),q1就是标签结果(因变量)。 那么点A和点F的y坐标差值 就表示预期值和目标值的误差,可表示为:
y e r r o r = q 1 − y ^ y_{error} = q1 - \hat{y} yerror=q1−y^
所以在绝对值技巧的基础上再加上这里的y坐标误差之后的平方技巧对移动直线的结果为:
y = f ( x ) = ( w + p 1 ∗ α ∗ y e r r o r ) x + ( b + α ∗ y e r r o r ) y = f(x) = (w + p1 * \alpha * y_{error})x + (b + \alpha * y_{error}) y=f(x)=(w+p1∗α∗yerror)x+(b+α∗yerror)
似乎这也不复杂。 而且这个技巧还干掉了前面 绝对值技巧难以面对的反派二,因为在绝对值技巧遇到的反派二中,虽然x坐标变大了,但是这个目标点距离直线的距离却变小了, 所以也能在一定程度上减小直线的移动范围。
其实从这里就可以看出,平方技巧有一个比较厉害的技能,那就是根据目标点和直线之间的距离来决定直线移动的范围。
比如目标点的y坐标很大, 也就表示目标点在直线f(x)上方更远的地方。因此,如果想让直线靠近目标点的话,就需要在较大的范围来调整直线的斜率和截距。
当然只要使用了平方技巧一切都不用怕了。 因为我们得到的直线和目标点的误差值 y e r r o r y_{error} yerror也同样不小。也就是说 ( w + p ∗ α ∗ y e r r o r ) (w + p * \alpha * y_{error}) (w+p∗α∗yerror) 和 $ (b + \alpha * y_{error})$这两个公式对直线f(x)的斜率和截距的影响很大,妥妥的能够满足咱们的需求。
同理,如果目标点与直线距离很近,那么 y e r r o r y_{error} yerror的值也就相对较小,咱们对直线的移动范围也就相对较小。
不但如此,平方技巧还能解决绝对值技巧中的一个尴尬, 在绝对值技巧中,目标点在直线上方,对w和b的操作就要用“+”, 如果目标点在直线下方,就要用“-”。
平方技巧就比较厉害了,我们不需要去关心这些东东。因为当目标点在直线下方时, y e r r o r y_{error} yerror的值就会是一个负值, 加上这个负值,就相当于减去 ∣ y e r r o r ∣ |y_{error}| ∣yerror∣。
最后关于为啥这个技巧叫平方技巧呢? 俺的理解是, 对w和b都使用了 ( q − y ^ ) (q - \hat{y}) (q−y^)进行调整,是不是就因此叫“平方”了呢? 不知对错,还请看官们指点指点。
- 梯度下降
一说起梯度下降,咱们首先想到的可能就是神经网络了。 但梯度下降是一种方法或者说思想, 既然神经网络能用,那咱线性回归应该也未尝不可用吧。
其实梯度下降在线性回归的应用,形式和上面的平方技巧很相似,其结果也是等价的。 为何会这样呢? 咱且看梯度下降是如何表现的。
还是和平方技巧一样的图,直线需要靠近点A。 首先咱们在x轴的p1对应到直线上的值为:
y ^ = w p 1 + b \hat{y} = wp1 + b y^=wp1+b
在梯度下降中,我们需要计算误差(也就是y坐标值得误差),其计算公式为:
E r r o r = 1 2 m ∑ i = 0 m ( q − y ^ ) 2 Error = \frac{1}{2m}\sum_{i=0}^m(q - \hat{y})^2 Error=2m1i=0∑m(q−y^)2
其中: m表示点的数目,q表示目标值, y ^ \hat{y} y^表示预期值。
这里只有点A一个点, 所以,点A和直线的误差为:
E r r o r = 1 2 ( q 1 − y ^ ) 2 Error = \frac{1}{2}(q1 - \hat{y})^2 Error=21(q1−y^)2
要使用梯度下降,就需要对“Error”求导,更确切的说,是分别对直线函数的w和b求偏导。这个Error其实是一个复合函数。假定Error的函数为h(x), 而直线函数为f(x), 那么就有:
E r r o r = h ( x ) = h ( 1 2 ( q 1 − f ( x ) ) 2 ) Error = h(x) = h(\frac{1}{2}(q1 - f(x))^2) Error=h(x)=h(21(q1−f(x))2)
根据复合函数的求导法则:
如果: $h(x) = f(g(x)) $
那么: h ′ ( x ) = f ′ ( g ( x ) ) ∗ g ′ ( x ) h \prime(x) = f \prime(g(x)) * g \prime(x) h′(x)=f′(g(x))∗g′(x)
那么咱就分别对w和b来求偏导(也就是说: 当对w求偏导时,把 f ( x ) = w x + b f(x)=wx + b f(x)=wx+b 中的w看做自变量,x和b都看做常量)。
首先是对w求偏导:
∂ ∂ w E r r o r = h ′ ( x ) ∗ f ′ ( x ) \frac{\partial}{\partial w} Error = h\prime(x) * f\prime(x) ∂w∂Error=h′(x)∗f′(x)
在 h ( x ) h(x) h(x)中自变量为 y ^ \hat{y} y^, 在 f ( x ) f(x) f(x)中自变量为w
∂ ∂ w E r r o r = ( 1 2 ( q 1 − y ^ ) 2 ) ′ ∗ ( w x + b ) ′ \frac{\partial}{\partial w} Error = (\frac{1}{2}(q1 - \hat{y})^2)\prime * (wx +b)\prime ∂w∂Error=(21(q1−y^)2)′∗(wx+b)′
∂ ∂ w E r r o r = − ( q 1 − y ^ ) ∗ x \frac{\partial}{\partial w} Error = -(q1 - \hat{y}) * x ∂w∂Error=−(q1−y^)∗x
同理可得:
∂ ∂ b E r r o r = − ( q 1 − y ^ ) \frac{\partial}{\partial b} Error = -(q1 - \hat{y}) ∂b∂Error=−(q1−y^)
以上就是求导的结果,也就是所谓的梯度,而梯度下降就是减去这个梯度, 即:
w → w − α ∂ ∂ w E r r o r = w + α ∗ ( q 1 − y ^ ) ∗ x w \rightarrow w - \alpha \frac{\partial}{\partial w} Error = w + \alpha * (q1 - \hat{y}) * x w→w−α∂w∂Error=w+α∗(q1−y^)∗x
b → b − α ∂ ∂ b E r r o r = b + α ∗ ( q 1 − y ^ ) b \rightarrow b - \alpha \frac{\partial}{\partial b} Error = b + \alpha * (q1 - \hat{y}) b→b−α∂b∂Error=b+α∗(q1−y^)
这是不是和上面的平方技巧很像啊。
五、示例
以上聊了那么多,现在咱就来实战一把。
这里的示例,是根据BMI指数来预测人的寿命。
BMI指数:即身体质量指数,简称体质指数,英文为Body Mass Index,简称BMI
要预测就得先对咱们的“直线”进行训练,即将训练数据用一条直线来拟合。这个数据时以前老美统计的数据,看看就好,不必当真,重要的是理解线性回归这东东。
这个数据所在的文件名(此文件会包含在源码的git仓库中,后续会提到): bmi_and_life_expectancy.csv
因为有一百多条数据,也就不全部贴出来了,这里就只贴前面几条,以展示数据的结构:
Country,Life expectancy,BMI
Afghanistan,52.8,20.62058
Albania,76.8,26.44657
Algeria,75.5,24.5962
Andorra,84.6,27.63048
Angola,56.7,22.25083
Armenia,72.3,25.355420000000002
Australia,81.6,27.56373
Austria,80.4,26.467409999999997
Azerbaijan,69.2,25.65117
Bahamas,72.2,27.24594
Bangladesh,68.3,20.39742
Barbados,75.3,26.384390000000003
Belarus,70.0,26.16443
Belgium,79.6,26.75915
Belize,70.7,27.02255
Benin,59.7,22.41835
Bhutan,70.7,22.8218
Bolivia,71.2,24.43335
从上面可以看出,数据分三列,国家,预期寿命,BMI值。这里咱们只需要预期生命和BMI值两列数据。
5.1 数据分布情况
首先咱先整理的看一下数据分布状况,这里有两种方式:
- Excel 展示
在excel上可以根据这两列数据绘制散点图。 惭愧的是,我试了几次,弄出来的散点图总是不对。
它excel把x轴(BMI)的取值范围自动改了,我怎么弄都搞不定,也只能怪我是excel的菜鸟。
- 使用Python来展示
Python这东东不管是在机器学习还是数据可视化方面都是很牛叉的存在,这里就用简单的几行代码就能够读取这个csv数据,并以散点图的形式显示出来,其代码如下所示:
#导入需要的用到的库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
#读取csv数据
bmi_life_data = pd.read_csv('bmi_and_life_expectancy.csv')
#分别以BMI值和预期寿命的数据创建numpy的数组
x = np.array(bmi_life_data[["BMI"]])
y = np.array(bmi_life_data["Life expectancy"])
#两BMI值和预期寿命以x和y坐标对的方式输入到plot中
for i in range(len(x)):
plt.scatter(x[i], y[i], color='blue', edgecolor='k')
#设置x轴和Y轴标签(名称)
plt.xlabel('BMI')
plt.ylabel('Life expectancy')
#显示散点图
plt.show()
5.2 代码实现
- c/c++实现
接下来, 咱们首先以c/c++ 语言(没办法他两才是俺们的老搭档,老相好)来实现这个线性回归的算法。
二话不说,直接上硬菜。为了简单,这里将 epoch, learning rate,weight, bias等参数均定义为全局变量,
如下所示:
int gEpochs = 1000; /** 训练(或迭代)的次数(或代数) */
double gLearning_rate = 0.001; /** 学习率 */
double gW = 1.0; /** 初始权重(或者说斜率) w */
double gB = 0.0; /** 初始偏置(或者说截距) b */
接着是重头戏,训练函数,这里使用的是前面的平方技巧,但是有点差别,那就是对“绝对的值”的选择不太一样。 前面在介绍概念的时候,是选择的x坐标。在这里咱们直接选择权重(或者说斜率)作为“绝对的值”(咱线性回归也可以向神经网络致敬吧)。
/**
* x[] 是输入数据,自变量
* lenx 表示输入数据的数量
* y[] 标签值
*/
void linear_regression(double x[], int lenx, double y[])
{
for(int i = 0; i < gEpochs; i++) /** 要迭代(或训练) gEpochs次 */
{
for (int j = 0; j < lenx; j++)
{
double y_hat = gW * x[j] + gB; /** 计算,对于输入x[j]的预期输出 */
/** 计算本次调整幅度, 其中(y[j] - y_hat) 表示
* 线性回归的输出误差(即实际值和预测值的误差)
*/
double delta = gLearning_rate * (y[j] - y_hat);
/** 调整权重(或斜率),这里使用权重本身替代了x坐标作为“绝对的值” */
gW += gW * delta;
gB += delta; /** 调整偏置(或截距) */
}
}
}
意不意外,惊不惊喜,没想到会是如此这般的简单吧。 最后就是预测函数, 即输入一个BMI值预测预期寿命,当然这个函数就更简单了。
/**
* w 训练后的权重(或斜率)
* b 训练后的偏置(或截距)
* bmi 输入的BMI值
*/
double predict(double w, double b, double bmi)
{
return w * bmi + b; /** 根据线性回归函数 f(x) = wx + b 计算预测寿命值 */
}
最后断后结尾的是咱们的测试代码:
int main(int argc, char* argv[])
{
/** 这里的gX和gY是两个全局数组,分别对应BMI值和预期寿命,
* 由于较多就不贴出来了,看官们可取完整源码中查看 */
/** 训练 线性回归函数 */
linear_regression(gX, sizeof(gX) / sizeof(gX[0]), gY);
double preBMI = 21.07931; /** 用于测试的BMI值 */
double output = predict(gW, gB, preBMI); /** 预测预期寿命 */
/** 注意: 这里格式化输出会出现小数点精度损失的问题, 可以使用
* visual studio等工具单步调试的时候查看精确的双精度
* 预测预期寿命
*/
printf("parameters after train, w: %lf, b: %lf\n", gW, gB);
printf("BMI: %lf, output life expectancy: %lf\n", preBMI, output);
getchar();
return 0;
}
其输出结果为(使用的visual studio编译和运行):
- Python实现
Python实现同样是无与伦比的简单。
和上面一样,首先来看一些默认参数的定义:
#首先是训练(或迭代)的次数 和 学习率的定义
epochs = 1000
learning_rate = 0.001
看官们看到这里可能会有些奇怪,默认的权重(或斜率)和偏置(或截距)值得定义呢, 上哪儿去了? 被你吃了?
看官们莫急,且听在下娓娓道来。
#实现线性回归训练函数
def linear_regression(x, y):
#啊哈,咱们的默认权重和偏置跑这里来了
w = 1
b = 0
for epoch in range(epochs):
for i in range(len(x)):
#计算预测输出
y_hat = w * x[i] + b
#计算调整幅度,(y[i] - y_hat)为输出偏差
delt = learning_rate * (y[i] - y_hat)
#调整权重和偏置
w += w * delt
b += delt
return w, b
#实现预测函数
def predict(w, b, bmi):
return w[0] * bmi + b[0]
#根据前面绘制散点图时分离出来的x和y作为输入数据和标签数据
#训练线性回归函数
w, b = linear_regression(x, y)
#测试代码,预测预期寿命
predBMI = 21.07931
output = predict(w, b, predBMI)
print("parameters after train, w: %f, b: %f" %(w, b))
print("test BMI: %lf output life expectancy: %lf" %(predBMI, output))
#注意: 这里使用了 %lf或%f格式化输出,这样打印出的精度会有损失。
# 可使用形如print(output)这样的方式,可以打印出完整的双精度浮点
其输出结果为(使用的是pycharm 运行):
对了顺便贴一下训练之后的图形(数据散点图,以及线性回归直线 ):
六、附录
所有的源码和数据文件均在 开源中国的 码云当中, 其地址如下:
https://gitee.com/xunawolanxue/my_demo_code/tree/master/My_Machine_Learn/线性回归
整个目录树为:
- c_cpp :c/c++版本的线性回归实现
- line_regression:visual studio工程所在目录
- README.md :c/c++实现版本的说明
- Python :Python版本的线性回归实现(包括绘图部分)
- bmi_and_life_expectancy.csv : csv数据
- draw_scatter.py : 绘制数据的散点图
- line_regression.py : 线性回归实现
- draw_after_train.py : 绘制训练后的散点图(包含线性回归的直线)
- README.md : python实现版本的说明
- README.md :整个项目的说明
【2018-06-05】更新了demo代码的网络链接