如果数据的特征比样本点还多应该怎么办?是否还可以使用线性回归和之前的方法来做预测?答案是否定的,即不能再使用前面介绍的方法。这是因为在计算(XTX)-1
的时候会出错。
如果特征比样本点还多(n > m
),也就是说输入数据的矩阵X
不是满秩矩阵。非满秩矩阵在求逆时会出现问题。
为了解决这个问题,统计学家引入了岭回归(ridge regression)的概念,这就是本节将介绍的第一种缩减方法。接着是lasso法,该方法效果很好但计算复杂。本节最后介绍了第二种缩减方法,称为前向逐步回归,可以得到与lasso差不多的效果,且更容易实现。
8.4.1 岭回归
简单说来,岭回归就是在矩阵XTX
上加一个λI
从而使得矩阵非奇异,进而能对XTX + λI
求逆。其中矩阵I是一个m×m
的单位矩阵,对角线上元素全为1,其他元素全为0。而λ
是一个用户定义的数值,后面会做介绍。在这种情况下,回归系数的计算公式将变成:
岭回归最先用来处理特征数多于样本数的情况,现在也用于在估计中加入偏差,从而得到更好的估计。这里通过引入λ
来限制了所有w
之和 ,通过引入该惩罚项,能够减少不重要的参数,这个技术在统计学中也叫做缩减(shrinkage)。
岭回归中的岭是什么?
岭回归使用了单位矩阵乘以常量
λ
,我们观察其中的单位矩阵I
,可以看到值1贯穿整个对角线,其余元素全是0。形象地,在0构成的平面上有一条1组成的“岭”,这就是岭回归中的“岭”的由来。
缩减方法可以去掉不重要的参数,因此能更好地理解数据。此外,与简单的线性回归相比,缩减法能取得更好的预测效果。
与前几章里训练其他参数所用的方法类似,这里通过预测误差最小化得到λ
:数据获取之后,首先抽一部分数据用于测试,剩余的作为训练集用于训练参数w
。训练完毕后在测试集上测试预测性能。通过选取不同的λ
来重复上述测试过程,最终得到一个使预测误差最小的λ
。
下面看看实际效果,打开regression.py
文件并添加程序清单8-3的代码。
程序清单8-3 岭回归
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 wsdef ridgeTest(xArr,yArr): xMat = mat(xArr); yMat=mat(yArr).T yMean = mean(yMat,0) #❶ 数据标准化 yMat = yMat - yMean xMeans = mean(xMat,0) xVar = var(xMat,0) 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
程序清单8-3中的代码包含了两个函数:函数ridgeRegres
用于计算回归系数,而函数ridgeTest
用于在一组λ
上测试结果。
第一个函数ridgeRegres
实现了给定lambda下的岭回归求解。如果没指定lambda,则默认为0.2。由于lambda是Python保留的关键字,因此程序中使用了lam
来代替。该函数首先构建矩阵XTX
,然后用lam
乘以单位矩阵(可调用NumPy库中的方法eye
来生成)。在普通回归方法可能会产生错误时,岭回归仍可以正常工作。那么是不是就不再需要检查行列式是否为零,对吗?不完全对,如果lambda设定为0的时候一样可能会产生错误,所以这里仍需要做一个检查。最后,如果矩阵非奇异就计算回归系数并返回。
为了使用岭回归和缩减技术,首先需要对特征做标准化处理。回忆一下,第2章已经用过标准化处理技术,使每维特征具有相同的重要性(不考虑特征代表什么)。程序清单8-3中的第二个函数ridgeTest
就展示了数据标准化的过程。具体的做法是所有特征都减去各自的均值并除以方差❶。
处理完成后就可以在30个不同的lambda下调用ridgeRegres
函数。注意,这里的lambda应以指数级变化,这样可以看出lambda在取非常小的值时和取非常大的值时分别对结果造成的影响。最后将所有的回归系数输出到一个矩阵并返回。
下面看一下鲍鱼数据集上的运行结果。
>>> reload(regression)>>> abX,abY=regression.loadDataSet(/'abalone.txt/')>>> ridgeWeights=regression.ridgeTest(abX,abY)
这样就得到了30个不同lambda所对应的回归系数。为了看到缩减的效果,在Python提示符下输入如下代码:
>>> import matplotlib.pyplot as plt>>> fig = plt.figure>>> ax = fig.add_subplot(111)>>> ax.plot(ridgeWeights)>>> plt.show
运行之后应该看到一个类似图8-6的结果图,该图绘出了回归系数与log(λ)
的关系。在最左边,即λ
最小时,可以得到所有系数的原始值(与线性回归一致);而在右边,系数全部缩减成0;在中间部分的某值将可以取得最好的预测效果。为了定量地找到最佳参数值,还需要进行交叉验证。另外,要判断哪些变量对结果预测最具有影响力,在图8-6中观察它们对应的系数大小就可以。
图8-6 岭回归的回归系数变化图。λ
非常小时,系数与普通回归一样。而λ
非常大时,所有回归系数缩减为0。可以在中间某处找到使得预测的结果最好的λ值
还有一些其他缩减方法,如lasso、LAR、PCA回归1以及子集选择等。与岭回归一样,这些方法不仅可以提高预测精确率,而且可以解释回归系数。下面将对lasso方法稍作介绍。
1. Trevor Hastie, Robert Tibshirani, and Jerome Friedman, The Elements of Statistical Learning: Data Mining, Infer- ence, and Prediction, 2nd ed. (Springer, 2009).
8.4.2 lasso
不难证明,在增加如下约束时,普通的最小二乘法回归会得到与岭回归的一样的公式:
上式限定了所有回归系数的平方和不能大于λ。使用普通的最小二乘法回归在当两个或更多的特征相关时,可能会得出一个很大的正系数和一个很大的负系数。正是因为上述限制条件的存在,使用岭回归可以避免这个问题。
与岭回归类似,另一个缩减方法lasso也对回归系数做了限定,对应的约束条件如下:
唯一的不同点在于,这个约束条件使用绝对值取代了平方和。虽然约束形式只是稍作变化,结果却大相径庭:在λ足够小的时候,一些系数会因此被迫缩减到0,这个特性可以帮助我们更好地理解数据。这两个约束条件在公式上看起来相差无几,但细微的变化却极大地增加了计算复杂度(为了在这个新的约束条件下解出回归系数,需要使用二次规划算法)。下面将介绍一个更为简单的方法来得到结果,该方法叫做前向逐步回归。
8.4.3 前向逐步回归
前向逐步回归算法可以得到与lasso差不多的效果,但更加简单。它属于一种贪心算法,即每一步都尽可能减少误差。一开始,所有的权重都设为1,然后每一步所做的决策是对某个权重增加或减少一个很小的值。
该算法的伪代码如下所示:
数据标准化,使其分布满足0均值和单位方差在每轮迭代过程中: 设置当前最小误差lowestError为正无穷 对每个特征: 增大或缩小: 改变一个系数得到一个新的W 计算新W下的误差 如果误差Error小于当前最小误差lowestError:设置Wbest等于当前的W 将W设置为新的Wbest
下面看看实际效果,打开regression.py
文件并加入下列程序清单中的代码。
程序清单8-4 前向逐步线性回归
def stageWise(xArr,yArr,eps=0.01,numIt=100): xMat = mat(xArr); yMat=mat(yArr).T yMean = mean(yMat,0) yMat = yMat - yMean xMat = regularize(xMat) m,n=shape(xMat) returnMat = zeros((numIt,n)) 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
程序清单8-4中的函数stageWise
是一个逐步线性回归算法的实现,它与lasso做法相近但计算简单。该函数的输入包括:输入数据 xArr
和预测变量yArr
。此外还有两个参数:一个是eps
,表示每次迭代需要调整的步长;另一个是numIt
,表示迭代次数。
函数首先将输入数据转换并存入矩阵中,然后把特征按照均值为0方差为1进行标准化处理。在这之后创建了一个向量ws
来保存w
的值,并且为了实现贪心算法建立了ws
的两份副本。接下来的优化过程需要迭代numIt
次,并且在每次迭代时都打印出w
向量,用于分析算法执行的过程和效果。
贪心算法在所有特征上运行两次for
循环,分别计算增加或减少该特征对误差的影响。这里使用的是平方误差,通过之前的函数rssError
得到。该误差初始值设为正无穷,经过与所有的误差比较后取最小的误差。整个过程循环迭代进行。
下面看一下实际效果,在regression.py
里输入程序清单8-4的代码并保存,然后在Python提示符下输入如下命令:
>>> reload(regression)<module /'regression/' from /'regression.pyc/'>>>> xArr,yArr=regression.loadDataSet(/'abalone.txt/')>>> regression.stageWise(xArr,yArr,0.01,200)[[ 0. 0. 0. 0. 0. 0. 0. 0. ]][[ 0 . 0. 0. 0.01 0. 0. 0. 0. ]][[ 0. 0. 0. 0.02 0. 0. 0. 0. ]] . .[[ 0.04 0. 0.09 0.03 0.31 -0.64 0. 0.36]][[ 0.05 0. 0.09 0.03 0.31 -0.64 0. 0.36]][[ 0.04 0. 0.09 0.03 0.31 -0.64 0. 0.36]]
上述结果中值得注意的是w1
和w6
都是0,这表明它们不对目标值造成任何影响,也就是说这些特征很可能是不需要的。另外,在参数eps
设置为0.01的情况下,一段时间后系数就已经饱和并在特定值之间来回震荡,这是因为步长太大的缘故。这里会看到,第一个权重在0.04和0.05之间来回震荡。
下面试着用更小的步长和更多的步数:
>>> regression.stageWise(xArr,yArr,0.001,5000)[[ 0. 0. 0. 0. 0. 0. 0. 0.]][[ 0. 0. 0. 0.001 0. 0. 0. 0. ]][[ 0. 0. 0. 0.002 0. 0. 0. 0. ]] . .[[ 0.044 -0.011 0.12 0.022 2.023 -0.963 -0.105 0.187]][[ 0.043 -0.011 0.12 0.022 2.023 -0.963 -0.105 0.187]][[ 0.044 -0.011 0.12 0.022 2.023 -0.963 -0.105 0.187]]
接下来把这些结果与最小二乘法进行比较,后者的结果可以通过如下代码获得:
>>> xMat=mat(xArr)>>> yMat=mat(yArr).T>>> xMat=regression.regularize(xMat)>>> yM = mean(yMat,0)>>> yMat = yMat - yM>>> weights=regression.standRegres(xMat,yMat.T)>>> weights.Tmatrix([[ 0.0430442 , -0.02274163, 0.13214087, 0.02075182, 2.22403814, -0.99895312, -0.11725427, 0.16622915]])
可以看到在5000次迭代以后,逐步线性回归算法与常规的最小二乘法效果类似。使用0.005的epsilon值并经过1000次迭代后的结果参见图8-7。
图8-7 鲍鱼数据集上执行逐步线性回归法得到的系数与迭代次数间的关系。逐步线性回归得到了与lasso相似的结果,但计算起来更加简便
逐步线性回归算法的实际好处并不在于能绘出图8-7这样漂亮的图,主要的优点在于它可以帮助人们理解现有的模型并做出改进。当构建了一个模型后,可以运行该算法找出重要的特征,这样就有可能及时停止对那些不重要特征的收集。最后,如果用于测试,该算法每100次迭代后就可以构建出一个模型,可以使用类似于10折交叉验证的方法比较这些模型,最终选择使误差最小的模型。
当应用缩减方法(如逐步线性回归或岭回归)时,模型也就增加了偏差(bias),与此同时却减小了模型的方差。下一节将揭示这些概念之间的关系并分析它们对结果的影响。