《机器学习实战》——第8章 预测数值型数据:回归

8.1 用线性回归找到最佳拟合直线

线性回归
优点:结果易于理解,计算不复杂。
缺点:对非线性的数据拟合不好。
适用数据类型:数值型和标称型数据。

回归的目的是预测数值型的目标值。最直接的办法是依据输入写出一个目标值的计算公式。下面给出一个回归方程:

HorsePower = 0.0015*annualSalary-0.99*hoursListeningTopublicRadio

其中的0.0015和-0.99称为回归系数,求回归系数的过程就是回归。
一般的回归指的都是线性回归,所以本章回归和线性回归代表同一个意思。线性回归意味着可以将输入项分别乘以一些常量,再将结果加起来得到输出。
假定输入数据存放在矩阵x中,而回归系数存放在向量w中。那么对于给定的数据X_{1},预测结果将会通过Y_{1}=X_{1}^{T}w。已知一些x和对应的y,找出w的常用方法是预测y值和真实y值之间的差值,使用该误差的简单累加将使得正差值和负差值相会抵消,所以我们采用平方误差。
平方误差可以写作:

\sum_{i=1}^{m}\left(y_{i}-x_{i}^{\mathrm{T}} w\right)^{2}

用矩阵表示还可以写作(Y-X w)^{T}(Y-X w)。如果对w求导,得到X^{T}(Y-Xw),令其等于零,解出w如下:

\hat{w}=\left(\boldsymbol{X}^{\mathrm{T}} \boldsymbol{X}\right)^{-1} \boldsymbol{X}^{\mathrm{T}} y

\widehat{w}的含义是这是当前可以估计出的w的最优解。从现有数据上估计出的w可能并不是数据中的真实w值,所以这里使用了一个符号来表示它仅是w的一个最佳估计。
上述公式包含X^{T}X^{-1},也就是需要对矩阵求逆,因此这个方程只在逆矩阵存在的时候适用。然而矩阵的逆可能并不存在,因此必须要在代码中对此作出判断。
下面给出一幅散点图,介绍如何画出该数据的最佳拟合直线。

创建一个新的文件regression.py并添加代码:

from numpy import *

def loadDataSet(fileName):      #general function to parse tab -delimited floats
    numFeat = len(open(fileName).readline().split('\t')) - 1 #get number of fields 
    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

def standRegres(xArr,yArr):
    xMat = mat(xArr); yMat = mat(yArr).T
    xTx = xMat.T*xMat
    if linalg.det(xTx) == 0.0:
        print("This matrix is singular, cannot do inverse")
        return
    ws = xTx.I * (xMat.T*yMat)
    return ws

第一个函数 loadDataSet() 与之前的同名函数相同,打开一个用tab键分割的文本文件,仍然默认文件每行的最后一个值是目标值。第二个函数 standRegress() 用来计算最佳拟合直线。该函数首先读入x和y并将它们保存到矩阵中;然后计算X^{T}X,然后判断它的行列式是否为零,如果行列式为零,那么计算逆矩阵的时候将出现错误。NumPy提供一个线性代数的库linalg,其中包含很多有用的函数。可以直接调用 linalg.det() 来计算行列式。最后,如果行列式非零,计算并返回w。如果没有检查行列式是否为零就试图计算矩阵的逆,将会出现错误。NumPy的线性代数库还提供一个函数来解未知矩阵,如果使用该函数,代码ws=xTx.I*(xMat.T*yMat)应写成ws=linalg.solve(xTx,xMat.T*yMatT)。

使用 loadDataSet() 将从数组中得到两个数组,分别存放在x和y中。与分类算法中的类别标签类似,这里的y是目标值。

import regression
from numpy import *
xArr,yArr = regression.loadDataSet('ex0.txt')
ws = regression.standRegres(xArr,yArr)
print(xArr[0:2])
print(ws)

ws存放的就是回归系数。再用内积来预测y的时候,第一维将乘以前面的常数x0,第二维将乘以输入变量x1。因为前面假定了x0=1,所以最终会得到 y=ws[0]+ws[1]*X1。 这里的y实际是预测出的,为了和真实的y值区分开,将其记为yHat。下面使用新的ws值计算yHat,绘制数据集散点图和最佳拟合直线图:

import regression
from numpy import *
import matplotlib.pyplot as plt
xArr,yArr = regression.loadDataSet('ex0.txt')
ws = regression.standRegres(xArr,yArr)
xMat = mat(xArr)
yMat = mat(yArr)
yHat = xMat*ws
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(xMat[:,1].flatten().A[0],yMat.T[:,0].flatten().A[0])
xCopy = xMat.copy()
xCopy.sort(0)
yHat = xCopy*ws
ax.plot(xCopy[:,1],yHat)
plt.show()

几乎任意数据集都可以用上述的方法建立模型,那么,如何判断模型的好坏呢?比较下面两张图,如果在两个数据集上分别作线性回归,将得到完全一样的模型(拟合直线)。显然两个数据是不一样的,那么我们该如何比较其效果呢?计算这两个序列的相关系数可以判断预测值yHat序列和真实值y序列的匹配程度。

Python中,NumPy库提供了相关系数的计算方法:可以通过命令 corrcoef(yEstimate,yActual) 来计算预测值和真实值的相关性。

import regression
from numpy import *
import matplotlib.pyplot as plt
xArr,yArr = regression.loadDataSet('ex0.txt')
ws = regression.standRegres(xArr,yArr)
xMat = mat(xArr)
yMat = mat(yArr)
yHat = xMat*ws
print(corrcoef(yHat.T,yMat))#计算相关系数

输出的矩阵包含所有两两组合的相关系数。对角的相关系数为1,这是因为yMat和自己的匹配是完美的,而yHat和yMat的相关系数为0.98。

8.2 局部加权线性回归

线性回归的一个问题是有可能出现欠拟合现象,因为它求的是具有最小均方误差的无偏估计。显而易见,如果模型欠拟合将不能取得最好的预测结果。所以有些方法允许在估计中引入一些偏差,从而降低预测的均方误差。
其中的一个方法是局部加权线性回归。在该算法中,我们给待预测点附近的每个点赋予一定的权重;然后与8.1节类似,在这个子集上基于最小均方差来进行普通的回归。与kNN一样,这种算法每次预测均需要事先选取出对应的数据子集。该算法解出回归系数w的形式如下:

\hat{w}=\left(\boldsymbol{X}^{\mathrm{T}} \boldsymbol{W} \boldsymbol{X}\right)^{-1} \boldsymbol{X}^{\mathrm{T}} \boldsymbol{W} y

其中w是一个矩阵,用来给每个数据点赋予权重。
LWLR使用“核”来对附近的点赋予更高的权重。核的类型可以自由选择,最常用的核就是高斯核,高斯核对应权重如下:

w(i, i)=\exp \left(\frac{\left|x^{(i)}-x\right|}{-2 k^{2}}\right)

这样就构建了一个只含对角元素的权重矩阵w,并且点x与x(i)越近,w(i,i)将会越大。上述公式包含一个需要用户指定的参数k,它决定了对附近的点赋予多大的权重,也就是使用LWLR时唯一需要考虑的参数。下图反映了参数k与权重的关系。

将下面的代码加入regression.py中 

def lwlr(testPoint,xArr,yArr,k=1.0):
    xMat = mat(xArr); yMat = mat(yArr).T
    m = shape(xMat)[0]
    weights = mat(eye((m)))#eye(m,n)产生mxn的单位矩阵
    for j in range(m):                      #next 2 lines create weights matrix
        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):  #loops over all the data points and applies lwlr to each one
    m = shape(testArr)[0]
    yHat = zeros(m)
    for i in range(m):
        yHat[i] = lwlr(testArr[i],xArr,yArr,k)
    return yHat

上述代码的作用是,给定x空间中的任意一点,计算出对应的预测值yHat。函数 lwlr() 的开头与之前的 standRegres() 类似,读入数据并创建所需矩阵,之后创建对角权重矩阵weights。权重矩阵是一个方阵,阶数等于样本点个数。也就是说,该矩阵为每个样本点初始化了一个权重。接着,算法将遍历数据集,计算每个样本点对应的权重值:随着样本点与待预测点距离的递增,权重将以指数级衰减。输入参数k控制衰减的速度。与之前的函数 stand-Regress() 一样,在权重矩阵计算完毕后,就可以得到对回归系数ws的一个估计。
另一个函数是 lwlrTest() ,用于为数据集中每个点调 lwlr(),这有助于求解k的大小。

import regression
from numpy import *
import matplotlib.pyplot as plt
xArr,yArr = regression.loadDataSet('ex0.txt')
# 对单点进行估计
print(yArr[0])
print(regression.lwlr(xArr[0],xArr,yArr,1.0))
print(regression.lwlr(xArr[0],xArr,yArr,0.001))
# 为了得到数据集里所有点估计,可以调用lwlrTest()函数
yHat = regression.lwlrTest(xArr,xArr,yArr,1.0)
# 绘出估计值和原始值,查看yHat拟合效果。绘图函数需要对数据点按序排列,首先对xArr排序:
xMat = mat(xArr)
srtInd = xMat[:,1].argsort(0)
xSort = xMat[srtInd][:,0,:]
# Matplotlib绘图
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(xSort[:,1],yHat[srtInd])
ax.scatter(xMat[:,1].flatten().A[0],mat(yArr).T.flatten().A[0],s=2,c='red')
plt.show()

上图给出了k=0.1、0.01、0.003的结果图。当k=1.0时权重很大,如同将所有的数据视为等权重,得出的最佳拟合直线与标准的回归一致。使用k=0.01得到了非常好的效果,抓住了数据的潜在模式。k=0.003纳入了过多的噪声点,拟合的直线与数据点过于接近。所以三者分别是欠拟合、正常、过拟合的图像。

8.3 示例:预测鲍鱼的年龄

在regression.py中加入下列代码:

def rssError(yArr,yHatArr): #yArr and yHatArr both need to be arrays
    return ((yArr-yHatArr)**2).sum()

为了分析预测误差的大小,可以用 rssError() 计算出这一指标:

import regression
abX,abY = regression.loadDataSet('abalone.txt')
yHat01 = regression.lwlrTest(abX[0:99] ,abX[0:99],abY[0:99],0.1)
yHat1=regression. lwlrTest(abX[0:99],abX[0:99],abY[0:99],1)
yHat10=regression.lwlrTest(abX[0:99],abX[0:99],abY[0:99],10)
print(regression.rssError(abY[0:99],yHat01.T))
print(regression.rssError(abY[0:99],yHat1.T))
print(regression.rssError(abY[0:99],yHat10.T))

 

使用较小的核将得到较低的误差。但是,使用最小的核将造成过拟合,对新数据不一定能达到最好的预测效果。 

import regression
from numpy import *
import matplotlib.pyplot as plt
abX,abY = regression.loadDataSet('abalone.txt')
yHat01 = regression.lwlrTest(abX[100:199],abX[0:99],abY[0:99],0.1)
print(regression.rssError(abY[100:199],yHat01.T))
yHat1 = regression. lwlrTest(abX[100:199],abX[0:99],abY[0:99],1)
print(regression.rssError(abY[100:199],yHat1.T))
yHat10 = regression.lwlrTest(abX[100:199],abX[0:99],abY[0:99],10)
print(regression.rssError(abY[100:199],yHat10.T))
ws = regression.standRegres(abX[0:99],abY[0:99])
yHat = mat(abX[100:199]) * ws
print(regression.rssError(abY[100:199],yHat.T.A))

三个参数中,核大小等于10时的测试误差最小,但在训练集上的误差却是最大的。
简单线性回归达到了与局部加权线性回归类似的效果。

8.4 缩减系数来“理解”数据

当数据特征比样本点还多时,由于在计算(X^{T}X)^{-1}的时候会出错,不可以再使用线性回归和之前的方法来做预测。
如果特征比样本点还多(m>m),也就是说输入的矩阵x不是满秩矩阵。非满秩矩阵在求逆时会出现问题。
为了解决这个问题,统计学家引入了岭回归的概念,这是本节将介绍的第一种缩减方法。接着是lasso法,效果好但计算方法。最后介绍了第二种缩减方法,称为向前逐步回归。可以得到与lasso差不多的效果,且更容易实现。

8.4.1 岭回归

简单地说,岭回归是在矩阵X^{T}X上加一个\lambda I从而使得矩阵非奇异,进而能对X^{T}X+\lambda I求逆。其中矩阵I是一个m×m的单位矩阵,对角线上元素全为1,其他元素全为0。而λ是一个用户定义的数值。在这种情况下,回归系数的计算公式变为:

\hat{w}=\left(\boldsymbol{X}^{\mathrm{T}} \boldsymbol{X}+\lambda \boldsymbol{I}\right)^{-1} \boldsymbol{X}^{\mathrm{T}} y

岭回归最先用来处理特征数多于样本数的情况,现在也用于在估计中加入偏差,从而得到更好的估计。通过引入λ来限制了所有w之和,通过引入该惩罚项,能够减少不重要的参数,这个技术在统计学中也叫作缩减。

岭回归中的岭是什么?
岭回归使用了单位矩阵乘以常量λ,我们观察其中的单位矩阵I,可以看到值1贯穿整个对角线,其余元素全是0。形象地,在0构成的平面上有一条1组成的“岭”,这就是岭回归中的“岭”。

缩减方法可以去掉不重要的参数,因此能更好地理解数据。此外,与简单的线性回归相比,缩减法能取得更好的预测效果。
与前几章里训练其他参数所用的方法类似,这里通过预测误差最小化得到λ:数据获取之后,首先抽一部分数据用于测试,剩余的作为训练集用于训练参数w。训练完毕后在测试集上测试预测性能。通过选取不同的λ来重复上述测试过程,最终得到一个使预测误差最小的λ。

添加下列代码到regression.py中:

def ridgeRegres(xMat, yMat, lam=0.2):
    xTx = xMat.T * xMat
    denom = xTx + eye(shape(xMat)[1]) * lam
    if linalg.det(denom) == 0.0:
        print("This matrix is singular, cannot do inverse")
        return
    ws = denom.I * (xMat.T * yMat)
    return ws

def ridgeTest(xArr, yArr):
    xMat = mat(xArr);
    yMat = mat(yArr).T
    yMean = mean(yMat, 0)
    yMat = yMat - yMean  # to eliminate X0 take mean off of Y
    # regularize X's
    xMeans = mean(xMat, 0)  # calc mean then subtract it off
    xVar = var(xMat, 0)  # calc variance of Xi then divide by it
    xMat = (xMat - xMeans) / xVar
    numTestPts = 30
    wMat = zeros((numTestPts, shape(xMat)[1]))
    for i in range(numTestPts):
        ws = ridgeRegres(xMat, yMat, exp(i - 10))
        wMat[i, :] = ws.T
    return wMat

上述代码包含了两个函数:函数 ridgeRegress() 用于计算回归系数,而函数 ridgeTest() 用于在一组λ上测试结果。
第一个函数 ridgeRegres() 实现了给定lambda下的岭回归求解。如果没指定lambda,则默认为0.2。由于lambda是Python保留的关键字,因此程序中使用了lam来代替。该函数首先构建矩阵X^{T}X,然后用lam乘以单位矩阵(可调用NumPy库中的方法eye()来生成)。在普通回归方法可能会产生错误时,岭回归仍可以正常工作。如果lambda设定为0的时候一样可能会产生错误,所以这里仍需要对行列式是否为零进行检查。最后,如果矩阵非奇异就计算回归系数并返回。
为了使用岭回归和缩减技术,首先需要对特征做标准化处理。ridgeTest() 函数展示了数据标准化的过程。具体的做法是所有特征都减去各自的均值并除以方差。
处理完成后就可以在30个不同的lambda下调用 ridgeRegres() 函数。这里的lambda应以指数级变化,这样可以看出lambda在取非常小的值时和取非常大的值时分别对结果造成的影响。最后将所有的回归系数输出到一个矩阵并返回。

import regression
from numpy import *
import matplotlib.pyplot as plt
# 获得30个不同lambda对应的回归系数
abX,abY = regression.loadDataSet('abalone.txt')
ridgeWeights = regression.ridgeTest(abX,abY)

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(ridgeWeights)
plt.show()

如图绘出了回归系数与 log(λ) 的关系。在最左边,即λ最小时,可以得到所有系数的原始值(与线性回归一致);而在右边,系数全部缩减成0;在中间部分的某值将可以取得最好的预测效果。为了定量地找到最佳参数值,还需要进行交叉验证。另外,要判断那些变量对结果预测最具有影响力,在图中观察它们对应的系数大小就可以。
还有一些其他的缩减方法,如lasso、LAR、PCA回归以及子集选择等。与岭回归一样,这些方法不仅可以提高预测精确率,而且可以解释回归系数。

8.4.2 lasso

不难证明,在增加如下约束时,普通的最小二乘法回归会得到与岭回归一样的公式:

 

上式限定了所有回归系数的平方和不能大于λ。使用普通的最小二乘法回归在当两个或更多的特征相关时,可能会得出一个很大的正系数和一个很大的负系数。正是因为上述限制条件的存在,使用岭回归可以避免这个问题。
与岭回归类似,另一个缩减方法lasso也对回归系数做了限定,对应的约束条件如下:

\sum_{k=1}^{n}\left|w_{k}\right| \leqslant \lambda

不通电在于,这个约束条件使用绝对值取代了平方和。虽然约束形式只是稍作变化,结果却大相径庭:在λ足够小的时候,一些系数会因此被迫缩减到0,这个特性可以帮助我们更好地理解数据。这两个约束条件在公式上看起来相差无几,但细微的变化却极大地增加了计算复杂度(为了在这个新的约束条件下解除回归系数,需要使用二次规划算法)。、

8.4.3 前向逐步回归

向前逐步回归算法可以得到与lasso差不多的效果,但更加简单。它属于一种贪心算法,即每一步都尽可能减少误差。一开始,所有的权重都设为1,然后每一步做的决策是对某个权重增加或减少一个很小的值。伪代码如下:

打开regression.py文件加入下列代码:

def stageWise(xArr,yArr,eps=0.01,numIt=100):
    xMat = mat(xArr); yMat=mat(yArr).T
    yMean = mean(yMat,0)
    yMat = yMat - yMean     #can also regularize ys but will get smaller coef
    xMat = regularize(xMat)
    m,n=shape(xMat)
    returnMat = zeros((numIt,n)) #testing code remove
    ws = zeros((n,1)); wsTest = ws.copy(); wsMax = ws.copy()
    for i in range(numIt):
        print(ws.T)
        lowestError = inf;
        for j in range(n):
            for sign in [-1,1]:
                wsTest = ws.copy()
                wsTest[j] += eps*sign
                yTest = xMat*wsTest
                rssE = rssError(yMat.A,yTest.A)
                if rssE < lowestError:
                    lowestError = rssE
                    wsMax = wsTest
        ws = wsMax.copy()
        returnMat[i,:]=ws.T
    return returnMat

def regularize(xMat):#regularize by columns
    inMat = xMat.copy()
    inMeans = mean(inMat,0)   #calc mean then subtract it off
    inVar = var(inMat,0)      #calc variance of Xi then divide by it
    inMat = (inMat - inMeans)/inVar
    return inMat

函数 stageWise() 是一个逐步线性回归算法的实现,它与lasso做法相近但计算简单。该函数的输入包括:输入数据xArr和与预测变量yArr。此外还有两个参数:一个是eps,表示每次迭代需要调整的步长;另一个是numIt,表示迭代次数。
函数首先将输入数据转换并存入矩阵中,然后把特征按照均值为0方差为1进行标准化处理。在这之后创建了一个向量ws来保存w的值,并且为了实现贪心算法建立了ws的两份副本。接下来的优化过程需要迭代numIt次,并且在每次迭代时都打印出w向量,用于分析算法执行的过程和效果。
贪心算法在所有特征上运行两次for循环,分别计算增加或减少该特征对误差的影响。这里使用的是平方误差,通过之前的函数 rssError() 得到。该误差初始值设为正无穷,经过与所有的误差比较后取最小的误差。整个过程循环迭代进行。
对代码进行验证:

import regression
# 获得30个不同lambda对应的回归系数
xArr,yArr = regression.loadDataSet('abalone.txt')
print(regression.stageWise(xArr,yArr,0.01,200))

w1和w6都是0,这表明它们不对目标值造成任何影响,也就是说这些特征很可能是不需要的。另外,在参数eps设置为0.01的情况下,一段时间后系数就已经饱和并在特定值之间来回震荡,这是因为步长太大的缘故。可以看到第一个权重在0.04和0.05之间来回震荡。
下面试着用更小的步长和更多的步数,并把结果与最小二乘法进行比较:

import regression
from numpy import *
import matplotlib.pyplot as plt
# 获得30个不同lambda对应的回归系数
xArr,yArr = regression.loadDataSet('abalone.txt')
print(regression.stageWise(xArr,yArr,0.001,5000))
xMat = mat(xArr)
yMat = mat(yArr).T
xMat = regression.regularize(xMat)
yM = mean(yMat,0)
yMat = yMat - yM
weights = regression.standRegres(xMat,yMat.T)
print(weights.T)

 

观察结果可知,迭代后逐步线性回归算法与常规的最小二乘法效果类似。使用0.005的epsilon值并经过1000次迭代后的结果如下图:

 

逐步线性回归算法的优点在于它可以帮助人们理解现有的模型并做出改进。当构建了一个模型后,可以运行该算法找出重要的特征,这样就有可能及时停止对那些不重要特征的收集。最后,如果用于测试,该算法每100次迭代后就可以构建出一个模型,可以使用类似于10折交叉验证的方法比较这些模型,最终选择使误差最小的模型。
当应用缩减方法时,模型也就增加了偏差(bias),与此同时却减小了模型的方差。

8.5 权衡偏差与方差

先举一个例子,一个数据的生成公式如下:

y=3.0+1.7 x+0.1 \sin (30 x)+0.06 \mathrm{~N}(0,1)

其中N(0,1)是一个均值为0、方差为1的正态分布。尝试仅用一条直线来拟合该数据时,不难想到,直线所能得到的最佳拟合应该是3.0+1.7x这一部分,误差部分就是0.1sin(30x)+0.06N(0,1)。下图给出了训练误差和测试误差的曲线图,上面的曲线就是测试误差,下面的曲线是训练误差。根据8.3节的实验我们知道:如果降低核的大小,那么训练误差将变小。从下图看,从左到右就表示了核逐渐减小的过程。

一般认为,上述两种误差由三个部分组成:偏差、测量误差和随机噪声。在之前,我们引入了三个越来越小的核来不断增大模型的方差。 
8.4节介绍了缩减法,可以将一些系数缩减成很小的值或直接缩减为0,这是一个增大模型偏差的例子。通过把一些特征的回归系数缩减到0,同时也就减少了模型的复杂度。例子中有8个特征,消除其中两个后不仅使模型更易理解,同时还降低了预测误差。上图的左侧是参数缩减过于严厉的结果,而右侧是无缩减的效果。
方差是可以度量的。如果从鲍鱼数据中取一个随机样本集(例如取其中100个数据)并用线性模型拟合,将会得到一组回归系数。同理,再取出另一组随机样本集并拟合,将会得到另一组回归系数。这些系数间的差异大小也就是模型方差大小的反映。上述偏差与方差折中的概念在机器学习十分流行并且反复出现。
 

猜你喜欢

转载自blog.csdn.net/fjyalzl/article/details/126914637