目录
- 1 线性回归找最佳拟合直线
- 2 局部加权线性回归
- 3 示例:预测鲍鱼的年龄
前面我们介绍了分类,分类的目标变量是标称型数据,而本章将会对连续型的数据做出预测,也就是我们的回归任务。
1 线性回归找最佳拟合直线
回归的目的是预测数值型的目标值。直接的办法是依据输入写出一个目标值的计算公式。假如你想要预测姐姐男友汽车的功率大小,可能会这么计算:
这就是所谓的回归方程(regression equation),其中的0.0015和-0.99称作回归系数(regression weights),求这些回归系数的过程就是回归。
一旦有了这些回归系数,再给定输入,做预测就非常容易了。具体的做法是用回归系数乘以输入值,再将结果全部加在一起,就得到了预测值。 当回归系数比价多时,就会用到矩阵运算。
说到回归,一般都是指线性回归(linear regression)。线性回归意味着可以将输入项分别乘以一些常量,再将结果加起来得到输出。需要说明的是,存在另一种称为非线性回归的回归模型,该模型不认同上面的做法,比如认为输出可能是输入的乘积。这里我们不做讨论。
回归的一般方法:
- (1) 收集数据:采用任意方法收集数据。
- (2) 准备数据:回归需要数值型数据,标称型数据将被转成二值型数据。
- (3) 分析数据:绘出数据的可视化二维图将有助于对数据做出理解和分析,在采用缩减法求得新回归系数之后,可以将新拟合线绘在图上作为对比。
- (4) 训练算法:找到回归系数。
- (5) 测试算法:使用R2或者预测值和数据的拟合度,来分析模型的效果。
- (6) 使用算法:使用回归,可以在给定输入的时候预测出一个数值,这是对分类方法的提升,因为这样可以预测连续型数据而不仅仅是离散的类别标签。
应当怎样从一大堆数据里求出回归方程呢?
假定输入数据存放在矩阵X中,而回归系数存放在向量w中。那么对于给定的数据X1,预测结果将会通过Y1=XT1w给出。现在的问题是,手里有一些X和对应的y,怎样才能找到w呢?一个常用的方法就是找出使误差小的w。这里的误差是指预测y值和真实y值之间的差值,使用该误差的简单累加将使得正差值和负差值相互抵消,所以我们采用平方误差。
平方误差可以写做:
用矩阵表示还可以写做(y-Xw)T(y-Xw)。如果对w求导,得到XT(Y-Xw),令其等于零,解出w如下:
w上方的小标记表示,这是当前可以估计出的w的优解。从现有数据上估计出的w可能并不是数据中的真实w值,所以这里使用了一个“帽”符号来表示它仅是w的一个佳估计。
值得注意的是,上述公式中包含XTX-1,也就是需要对矩阵求逆,因此这个方程只在逆矩阵存在的时候适用。然而,矩阵的逆可能并不存在,因此必须要在代码中对此作出判断。
上述的佳w求解是统计学中的常见问题,除了矩阵方法外还有很多其他方法可以解决。通过调用NumPy库里的矩阵方法,我们可以仅使用几行代码就完成所需功能。该方法也称作OLS, 意思是“普通小二乘法”(ordinary least squares)。
下面看看实际效果,对于下图中的散点图,下面来介绍如何给出该数据的佳拟合直线。
首先编写一个函数载入我们的以逗号分隔的文档数据,前面我们写过类似的函数:
from numpy import *
def loadDataSet(fileName):
numFeat = len(open(fileName).readline().split('\t')) - 1 #获取特征X的数量
dataMat = [] # 存储数据的列表
labelMat = [] # 存储标签列的表
fr = open(fileName)
for line in fr.readlines(): # 逐行读取
lineArr =[]
curLine = line.strip().split('\t')
for i in range(numFeat):
lineArr.append(float(curLine[i]))
dataMat.append(lineArr)
labelMat.append(float(curLine[-1]))
return dataMat,labelMat
接着我们计算w:
def standRegres(xArr,yArr):
xMat = mat(xArr); yMat = mat(yArr).T # 转为矩阵
xTx = xMat.T*xMat #计算x的转置点乘x
if linalg.det(xTx) == 0.0: # 如果行列式为0,不存在逆矩阵
print ("This matrix is singular, cannot do inverse")
return
ws = xTx.I * (xMat.T*yMat) # 计算w
return ws
下面看看实际效果,使用loadDataSet()将从数据中得到两个数组,分别存放在x和y中。与分类算法中的类别标签类似,这里的y是目标值。
xArr, yArr = loadDataSet('ex0.txt')
print(xArr[:2])
# 结果如下
[[1.0, 0.067732], [1.0, 0.42781]]
第一个值总是等于1.0,即X0。我们假定偏移量就是一个常数。第二个值X1,也就是我们图中的横坐标值。
现在看一下standRegres()函数的执行效果:
ws = standRegres(xArr,yArr)
print(ws)
# 效果如下
[[3.00774324]
[1.69532264]]
变量ws存放的就是回归系数。在用内积来预测y的时候,第一维将乘以前面的常数X0,第二维将乘以输入变量X1。因为前面假定了X0=1,所以终会得到:y=ws[0]+ws[1]*X1。
这里的y实际是预测出的,为了和真实的y值区分开来,我们将它记为yHat。下面使用新的ws值计算yHat,并画出拟合图像:
xmat = mat(xArr)
ymat = mat(yArr)
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(xmat[:,1].flatten().A[0],ymat.T[:,0].flatten().A[0]) # 把常量x0去掉了,x1是我们的横坐标
# flatten()函数返回一个折叠成一维的数组。
# 矩阵.A(等效于矩阵.getA())变成了数组。
# 把点按照升序排列,防止图线乱
xcopy = xmat.copy()
xcopy.sort(0)
yhat = xcopy*ws
ax.plot(xcopy[:,1],yhat)
图像如下:
几乎任一数据集都可以用上述方法建立模型,那么,如何判断这些模型的好坏呢?比较下图两个子图,如果在两个数据集上分别作线性回归,将得到完全一样的模型(拟合直线)。显然两个数据是不一样的,那么模型分别在二者上的效果如何?我们当如何比较这些效果的好坏呢? 有种方法可以计算预测值yHat序列和真实值y序列的匹配程度,那就是计算这两个序列的相关系数。
在Python中,NumPy库提供了相关系数的计算方法:可以通过命令corrcoef(yEstimate, yActual)来计算预测值和真实值的相关性。下面我们就在前面的数据集上做个实验。
yhat = xmat*ws
print(corrcoef(yhat.T,ymat))
# 结果如下
[[1. 0.98647356]
[0.98647356 1. ]]
该矩阵包含所有两两组合的相关系数。可以看到,对角线上的数据是1.0,因为yMat和自己的匹配是完美的,而YHat和yMat的相关系数为0.98。
最佳拟合直线方法将数据视为直线进行建模,具有十分不错的表现。但是上图的数据当中似乎还存在其他的潜在模式。那么如何才能利用这些模式呢?我们可以根据数据来局部调整预测,下面就会介绍这种方法。
2 局部加权线性回归
线性回归的一个问题是有可能出现欠拟合现象,因为它求的是具有小均方误差的无偏估计。显而易见,如果模型欠拟合将不能取得好的预测效果。所以有些方法允许在估计中引入一些偏差,从而降低预测的均方误差。
其中的一个方法是局部加权线性回归(Locally Weighted Linear Regression,LWLR)。在该算法中,我们给待预测点附近的每个点赋予一定的权重;然后上面类似,在这个子集上基于小均方差来进行普通的回归。与kNN一样,这种算法每次预测均需要事先选取出对应的数据子集。该算法解出回归系数w的形式如下:
其中w是一个矩阵,用来给每个数据点赋予权重。
LWLR使用“核”(与支持向量机中的核类似)来对附近的点赋予更高的权重。核的类型可以自由选择,常用的核就是高斯核,高斯核对应的权重如下:
这样就构建了一个只含对角元素的权重矩阵w,并且点x与x(i)越近,w(i,i)将会越大。上述公式包含一个需要用户指定的参数k,它决定了对附近的点赋予多大的权重,这也是使用LWLR时唯一需要考虑的参数,在下图中可以看到参数k与权重的关系。
代码如下:
def lwlr(testPoint,xArr,yArr,k=1.0):
xMat = mat(xArr)
yMat = mat(yArr).T
m = xMat.shape[0] # 样本数据大小
weights = mat(eye((m))) # 生成对角矩阵
for j in range(m): #遍历每个样本
diffMat = testPoint - xMat[j,:] # 计算测试点与样本点的坐标差
weights[j,j] = exp(diffMat*diffMat.T/(-2.0*k**2)) # 计算当前样本点的权重
# 下面与普通线性回归一样,计算每个样本的系数
xTx = xMat.T * (weights * xMat)
if linalg.det(xTx) == 0.0:
print ("This matrix is singular, cannot do inverse")
return
ws = xTx.I * (xMat.T * (weights * yMat))
return testPoint * ws
def lwlrTest(testArr,xArr,yArr,k=1.0):
testArr = mat(testArr)
m = testArr.shape[0]
yHat = zeros(m)
for i in range(m): # 遍历每个样本点
yHat[i] = lwlr(testArr[i],xArr,yArr,k) # 计算每个样本点在不同样本权重下的预测值
return yHat
我们可以先来测试单个点:
xArr, yArr = loadDataSet('ex0.txt')
print(yArr[0])
# 3.176513
print(lwlr(xArr[0],xArr,yArr,1))
# [[3.12204471]]
print(lwlr(xArr[0],xArr,yArr,0.01))
# [[3.20366661]]
可以看到k越小,越不受离测试点较远的影响,即与样本点的值就越近,这样就越容易过拟合。
借下来我们使用全部样本进行预测并绘图:
xArr, yArr = loadDataSet('ex0.txt')
yhat1 = lwlrTest(xArr,xArr,yArr,1)
yhat2 = lwlrTest(xArr,xArr,yArr,0.01)
yhat3 = lwlrTest(xArr,xArr,yArr,0.003)
xmat = mat(xArr)
ymat = mat(yArr)
srtInd = xmat[:,1].argsort(0) # 排序索引
xsort = xmat[srtInd][:,0,:]
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(311)
ax.plot(xsort[:,1],yhat1[srtInd])
ax.scatter(xmat[:,1].flatten().A[0],ymat.T.flatten().A[0],s=2,c='red')
ax = fig.add_subplot(312)
ax.plot(xsort[:,1],yhat2[srtInd])
ax.scatter(xmat[:,1].flatten().A[0],ymat.T.flatten().A[0],s=2,c='red')
ax = fig.add_subplot(313)
ax.plot(xsort[:,1],yhat3[srtInd])
ax.scatter(xmat[:,1].flatten().A[0],ymat.T.flatten().A[0],s=2,c='red')
plt.tight_layout()
下面是k在三种不同取值下的结果图:
第一张图:当k = 1.0时权重很大,如同将所有的数据视为等权重,得出的佳拟合直线与标准的回归一致。
第二张图:使用k = 0.01得 到了非常好的效果,抓住了数据的潜在模式。
第三张图:使用k = 0.003纳入了太多的噪声点,拟合的直线与数据点过于贴近,进而导致了过拟合现象 。
局部加权线性回归也存在一个问题,即增加了计算量,因为它对每个点做预测时都必须使用整个数据集。
3 示例:预测鲍鱼的年龄
接下来,我们将回归用于真实数据。在data目录下有一份来自UCI数据集合的数据,记录了鲍鱼(一种介壳类水生动物)的年龄。鲍鱼年龄可以从鲍鱼壳的层数推算得到。
abX, abY = loadDataSet('abalone.txt')
yhat01 = lwlrTest(abX[0:99],abX[0:99],abY[0:99],0.1)
yhat1 = lwlrTest(abX[0:99],abX[0:99],abY[0:99],1)
yhat10 = lwlrTest(abX[0:99],abX[0:99],abY[0:99],10)
# 定义计算误差的函数
def rssError(yArr,yHatArr):
return ((yArr-yHatArr)**2).sum()
# 分别打印不同核的预测误差
print(rssError(abY[0:99],yhat01.T)) # 56.78420911837208
print(rssError(abY[0:99],yhat1.T)) #429.89056187030394
print(rssError(abY[0:99],yhat10.T)) #549.1181708826065
可以看到,使用较小的核将得到较低的误差。那么,为什么不在所有数据集上都使用小的核呢? 这是因为使用小的核将造成过拟合,对新数据不一定能达到好的预测效果。下面就来看看它们在新数据上的表现:
yhat01 = lwlrTest(abX[100:199],abX[0:99],abY[0:99],0.1)
yhat1 = lwlrTest(abX[100:199],abX[0:99],abY[0:99],1)
yhat10 = lwlrTest(abX[100:199],abX[0:99],abY[0:99],10)
print(rssError(abY[100:199],yhat01.T))
# K为0.1时的误差:25119.459111157415
print(rssError(abY[100:199],yhat1.T))
# K为1时的误差:573.5261441895706
print(rssError(abY[100:199],yhat10.T))
# K为1时的误差:573.5261441895706
从上述结果可以看到,在上面的三个参数中,核大小等于10时的测试误差小,但它在训练集上的误差却是大的。接下来再来和简单的线性回归做个比较:
ws = standRegres(abX[0:99],abY[0:99])
yhat = mat(abX[100:199])*ws
print(rssError(abY[100:199],yhat.T.A)) # 518.6363153249081
简单线性回归达到了与局部加权线性回归类似的效果。这也表明一点,必须在未知数据上比较效果才能选取到佳模型。那么最佳的核大小是10吗?或许是,但如果想得到更好的效果,应该用10个不同的样本集做10次测试来比较结果。
本例展示了如何使用局部加权线性回归来构建模型,可以得到比普通线性回归更好的效果。局部加权线性回归的问题在于,每次必须在整个数据集上运行。也就是说为了做出预测,必须保存所有的训练数据。