第1课 单变量线性回归
线性回归问题是什么?
线性回归问题,首先关注的名词是回归。回归则意味着模型最终输出的标签量(y值)是一个连续值,回归问题的本质可以说是基于一组特征数据来预测一个标签数据。
例如选用一组特征来描述一套房子(例如:户型、大小、位置、楼层、房龄……),根据特征值的不同,来预测这套房子的价格。 线性回归问题,第二个关注的名词是线性。线性也就是说我们接受这样一种假设,我们选择的特征变量,与所要预测的标签量之间存在线性关系。用函数的形式可以表达为:
\[h(x)=\theta_0+\theta_1x_1+\theta_2x_2+\ldots+\theta_nx_n\]其中,${x_1,x_2,\ldots,x_n}$为描述样本的特征,$h(x)$代表模型的输出值,即对应训练样本中的标签$y$。
最简单的线性回归问题就是只有一个特征变量的问题,即单变量线性回归,其假设函数为:
\[h(x)=\theta_0+\theta_1x_1\]在这个假设函数中,$\theta_i$表示参数,可以将参数理解为特征值相对预测结果$h(x)$的权重。而利用训练数据所要训练的也就是这些参数。
现在假设已经获取了一组训练数据,这组训练数据可以表示为${(x^{(1)},y^{(1)}),(x^{(2)},y^{(2)}),\ldots,(x^{(m)},y^{(m)})}$
$m$表示样本的数量,$(x^{(i)},y^{(i)})$表示第$i$个样本。
在这样一个训练集上训练模型,最终会得到一组确定的$\theta_0,\theta_1$组合,但是训练的效果怎么样,还是需要通过一个指标来进行评价(也就是前面的性能测量P)。这里用来评价的方式是采用代价函数。
在讨论代价函数之前,还是要先从线性回归假设模型的训练目的说起,模型的训练目的是通过确定参数$\theta_0,\theta_1$的值,使得假设最后输出的结果$h(x)$能够与训练集最大程度的匹配。怎样才算是最大程度匹配,肯定是$h(x)$与训练集$y$之间的差距越小越好,放在整个训练集上,那就是模型的输出与训练集标签差的平方和要尽可能小,用函数表现为:
\[J(\theta_0,\theta_1)=\frac{1}{2m}\sum_{i=1}^{m}[h(x^{(i)})-y^{(i)}]^2\]函数$J(\theta)$也被称为是代价函数,要想实现线性回归模型训练的目的,也就是需要让$J(\theta)$取到最小值,即:
\[\mathop{min}\limits_{\theta_0,\theta_1}J(\theta_0,\theta_1)=\mathop{min}\limits_{\theta_0,\theta_1}\frac{1}{2m}\sum_{i=1}^m(\theta_0+\theta_1x_1^{(i)}-y^{(i)})^2\]这样的代价函数也被叫做平方差代价函数。
线性回归问题如何求解?
现在我们的目的就很明确了,利用$m$组训练样本数据,计算得到最合适的$\theta_0,\theta_1$组合。至于求解的方式,采用梯度下降算法(也叫批量梯度下降)。
梯度下降算法其实是一种优化方法,优化问题中:
目标函数为$\mathop{min}\limits_{\theta_0,\theta_1}J(\theta_0,\theta_1,\cdots,\theta_n)$
这里考虑的是一般情况,假设有$n$个特征值,再加$\theta_0$。 利用梯度下降算法求解参数的步骤,基本可以分为以下几步:
- 首先,将所有的参数值初始化,将$(\theta_0,\theta_1,\cdots,\theta_n)$视为向量$\vec{\theta}$,则$\vec{\theta}=0$
- 第二,利用$J(\theta_0,\theta_1,\cdots,\theta_n)$相对$\theta_i$的偏导数的偏导数作为迭代更新量,迭代更新每一个参数。
需要注意的是,$\theta_i$的每一次迭代更新都是批量更新,即在所有$n+1$个参数都完成偏导计算后,同步更新为新版参数。此外,因为单变量线性回归模型的假设函数是一个凸函数,因此能够保证迭代最终给总能得到全局最优解。
另外,公式中的$\alpha$为学习速率,在实际算法实现过程中,需要自己选择合适的学习速率,如果速率过小,那么计算时长可能很长,收敛速度过慢;如果速率过大,算法可能会错过收敛点,导致持续震荡。
梯度下降算法的好处在于,随着迭代逐渐接近收敛点,偏微分项$\frac{\partial}{\partial\theta_i}J(\theta_0,\theta_1,\cdots,\theta_n)$的值会逐渐减小,因此即使不调整学习速率,参数下降的速度也会逐渐降低。
回到单变量线性回归的简单案例中,
假设函数为:$h(x)=\theta_0+\theta_1x_1$
代价函数为:$J(\theta_0,\theta_1)=\frac{1}{2m}\sum_{i=1}^{m}(\theta_0+\theta_1x_1^{(i)}-y^{(i)})^2$ 在梯度下降迭代过程中,参数的更新形式为:
$\theta_0:=\theta_0-\frac{\alpha}{m}\sum_{i=1}^{m}(\theta_0+\theta_1x_1^{(i)}-y^{(i)})$
$\theta_1:=\theta_1-\frac{\alpha}{m}\sum_{i=1}^{m}(\theta_0+\theta_1x_1^{(i)}-y^{(i)})x_1^{(i)}$
在实际算法实现过程中,通常会采用向量运算的形式。
首先需要对特征向量进行调整,增加一个等于1的特征量$x_0$,此时假设函数可以视为:
$h(x)=\theta_0x_0+\theta_1x_1+\theta_2x_2+\cdots+\theta_nx_n$
将参数$\vec{\theta}$看作是$(n+1)\times1$维的向量,即:
$\vec{\theta}=\begin{bmatrix}
\theta_0\
\theta_1\
\theta_2\
\vdots\
\theta_n
\end{bmatrix}$
同时,将特征变量$X$看作是$m\times(n+1)$维矩阵:
\[X=\begin{bmatrix} 1&x_1^{(1)}&x_2^{(1)}&\cdots&x_n^{(1)}\\ 1&x_1^{(2)}&x_2^{(2)}&\cdots&x_n^{(2)}\\ 1&x_1^{(3)}&x_2^{(3)}&\cdots&x_n^{(3)}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ 1&x_1^{(n)}&x_2^{(n)}&\cdots&x_n^{(n)} \end{bmatrix}\]那么假设函数可以改写为:$h(x)=X\vec{\theta}$
在利用梯度下降求解参数的过程中,首先令$\vec{\theta}=0$
然后按照下面公式诶迭代更新参数:
$\vec{\theta}:=\vec{\theta}-\frac{\alpha}{m}(X\vec{\theta}-\vec{y})^TX$
这里也可以解释一下为什么梯度下降算法也被称为是批量梯度下降,那是因为在每一步迭代过程中,所有$m$个训练样本$X$都会参与到计算中。
单变量线性回归的代码实现:
# 载入必要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 载入数据
# 因为原始数据没有表头,因此在生成数据框时,选择header=None
data = pd.read_csv('ex1data1.txt',header = None)
# 为便于阅读,给原始数据赋予列名。
data.columns = ['feature','label']
X = data.loc[:,['feature']]
y = data.loc[:,['label']]
m = len(y)
# 将X,y转换为矩阵形式
X = X.values
y = y.values
# 绘图
# data.plot(x='feature',y='label',kind='scatter')
plt.scatter(X,y)
# 在利用梯度下降算法计算代价函数之前,
# 首先在X前面插入一列全1向量,作为x0。
# 此处0表示插入位置,axis=1表示按列插入。
X = np.insert(X, 0, values=1, axis=1)
# 初始化参数矩阵为(0,0)
theta = np.zeros((2,1))
# 设定迭代次数和学习速率
iterations = 1500
alpha = 0.01
# 编写代价函数
def computeCost(X,y,theta):
'''
计算线性回归的代价函数
函数利用训练数据中的X和y值,以及模型的参数计算模型与真实值之间的误差
'''
#初始化相关变量
m = len(y) #样本数量m
J = 0 #计算误差
J = np.sum((np.dot(X,theta) - y)**2)/2/m
return J
# 计算初始条件下的误差
J = computeCost(X,y,theta)
# 用(-1,2)初始化参数,继续求解误差
J = computeCost(X,y,[[-1],[2]])
# 编写梯度下降函数
def gradientDescent(X, y, theta, alpha, num_iters):
'''
梯度下降函数利用梯度下降算法求解参数theta
theta,J_history = gradientDecent(X, y, theta, alpha, num_iters)
其中J_history用来存储代价函数每步迭代的记录。
'''
# 初始化参数
m = len(y) #样本的数量
J_history = np.zeros((num_iters,1))
# 迭代num_iters次
for iter in range(num_iters):
"""
对参数数据框执行单步迭代。
"""
theta = theta - alpha / m * np.dot((np.dot(X,theta) - y).T,X).T
J_history[iter] = computeCost(X, y, theta)
return theta,J_history
# 利用训练数据集的X,y,初始化的theta,以及自定义的学习速率alpha和迭代次数iterations进行模型训练,返回训练后的theta并记录误差的变化J_historty
theta, J_history = gradientDescent(X, y, theta, alpha, iterations)
# 绘制拟合曲线与原始数据对比
plt.scatter(X[:,1],y,label='Raw Data')
plt.plot(X[:,1],np.dot(X,theta),color='red',label='Fiiting Line')
plt.legend()
# 根据新的特征值预测X=3.5
predict1 = np.dot([1,3.5],theta)
# 绘制代价函数的变化趋势
# 绘制网格计算网格
theta0_vals = np.linspace(-10, 10, 100) #从-10到10,分100份
theta1_vals = np.linspace(-1, 4, 100) #从-1到4,分100份
# 初始化J_vals矩阵,用此矩阵存储不同参数值下的J值。
J_vals = np.zeros((len(theta0_vals), len(theta1_vals)))
# 填充J_vals矩阵
for i in range(len(theta0_vals)):
for j in range(len(theta1_vals)):
t = [[theta0_vals[i]],[theta1_vals[j]]]
J_vals[i,j] = computeCost(X, y, t)
np.meshgrid(theta0_vals,theta1_vals)
fig = plt.figure()
ax = Axes3D(fig)
ax.plot_surface(theta0_vals,theta1_vals,J_vals, rstride=1, cstride=1, cmap='rainbow')
ax.view_init(elev=45,azim=90)