1 用线性回归找到最佳拟合直线
线性回归
优点:结果易于理解,计算上不复杂
缺点:对非线性的数据拟合不好
使用数据类型:数值型数据和标称型数据。
- 回归方程:是根据样本资料通过回归分析所得到的反映一个变量(因变量)对另一个或一组变量(自变量)的回归关系的数学表达式
- 回归系数:在回归方程中表示自变量x对因变量y影响大小的参数
- 回归:求回归系数的过程
- 说到回归,一般都是指线性回归(linear regression),还存在非线性回归模型
- 假定输入数据存放在矩阵 X X X中,而回归系数存放在向量 ω \omega ω中,那么对于给定的数据 X 1 X_1 X1,预测结果将会通过: Y 1 = X 1 T ω Y_1 = X_1^T\omega Y1=X1Tω
- 现在的问题是,手里有一些x和对应的y,如何找到 ω \omega ω呢?
- 常用的方法是找到使误差(一般采用平方误差,否则正负误差会相互抵消)最小的w 。平方误差可写作:
∑ i = 1 m ( y i − x i T ω ) 2 \sum_{i=1}^m(y_i - x_i^T\omega)^2 i=1∑m(yi−xiTω)2 - 用矩阵表示还可以写成: ( Y − X ω ) T ( Y − X ω ) (Y - X\omega)^T(Y - X\omega) (Y−Xω)T(Y−Xω)
- 对
ω
\omega
ω求导: 令其为0,得到最优解、最佳估计:
ω ^ = ( X T X ) − 1 X T y \widehat{\omega} = (X^TX)^{-1}X^Ty ω =(XTX)−1XTy - 公式中的 ( X T X ) − 1 (X^TX)^{-1} (XTX)−1是对矩阵求逆,需要在代码中判断逆矩阵是否存在,可以调用NumPy中函数linalg.det()来计算行列式,如果行列式为零(奇异矩阵),那么计算逆矩阵的时候会出现错误.
- 这种求解最佳 ω \omega ω的方法称为OLS“普通最小二乘法”
python代码实现:
import numpy as np
def loadDataSet(fileName):
"""
加载数据集
:param fileName: 文件名
:return:
dataMat:数据矩阵
labelMat:标签矩阵
"""
numFeat = len(open(fileName).readline().split('\t')) - 1
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):
"""
线性回归
:param xArr: 输入的样本数据,包含每个样本数据的feature
:param yArr: 对应于输入数据的类别标签,也就是每个样本对应的目标变量
:return:
ws:回归系数
"""
# np.mat()函数将xArr,yArr转换为矩阵,mat().T代表的是对矩阵进行转置操作
xMat = np.mat(xArr)
yMat = np.mat(yArr).T
# 矩阵乘法的条件是左矩阵的列数等于右矩阵的行数
xTx = xMat.T * xMat
# 因为要用到xTx逆矩阵,所以事先需要确定计算得到的xTx是否可逆,条件是矩阵的行列式不为0
# np.linalg.det()函数是用来求得矩阵的行列式的,如果矩阵的行列式为0,则这个矩阵是不可逆的,就无法进行接下来的运算
if np.linalg.det(xTx) == 0.0:
print("this matrix is singular,cannot do inverse")
return
# 最小二乘法
ws = xTx.I * (xMat.T * yMat)
return ws
2 局部加权线性回归
- 线性回归会出现欠拟合现象
→
\rightarrow
→它求的是最小均方误差的无偏估计。
- 可以在估计中引入一些偏差bias,从而降低预测的均方误差
- 其中一个方法是局部加权线性回归,该算法中给待测点附近的每个点赋予一定的权重,然后在这个子集上基于最小均方差来进行普通的回归。与kNN一样,此算法每次预测均需事先选取出对应的数据子集
- 该算法解出的回归系数的形式如下:
ω ^ = ( X T W X ) − 1 X T W y \widehat{\omega} = (X^TWX)^{-1}X^TWy ω =(XTWX)−1XTWy - 其中W是一个矩阵,用来给每个数据点赋予权重
- 局部加权线性回归使用“核”(与支持向量机中的核类似)来对附近的点赋予更高的权重
- 核的类型可以自由选择,最常用的是高斯核,高斯核对应的权重如下:
ω ( i , i ) = e x p ( ∣ x ( i ) − x ∣ 2 − 2 k 2 ) \omega(i,i) = exp(\frac {|x^{(i)} - x|_2}{-2k^2}) ω(i,i)=exp(−2k2∣x(i)−x∣2)- 这样就构建了一个只含对角元素的权重矩阵W,并且点x(预测点)与 x ( i ) x^{(i)} x(i)越近(样本点), ω ( i , i ) \omega(i,i) ω(i,i)将会越大
- 公式中的k需要用户指定,它决定了对附近的点赋予多大的权重
- 注意区分权重W和回归系数 ω \omega ω,与kNN一样,该加权模型认为样本点距离越近,越可能符合同一个线性模型
x为某个预测点,
x
(
i
)
x^{(i)}
x(i)为样本点,样本点距离预测点越近,贡献的误差越大(权值越大),越远则贡献的误差越小(权值越小)。
python代码实现:
def lwlr(testPoint, xArr, yArr, k=1.0):
"""
局部加权线性回归,在待预测点附近的每一个点赋予一定的权重,在子集上基于最小均方差来进行普通的回归
:param testPoint:样本点
:param xArr:样本的特征数据,即feature
:param yArr:每个样本对应的类别标签,即目标变量
:param k:关于赋予权重矩阵的核的每一个参数,与权重的衰减速率有关
:return:
testPoint*ws:数据点与具有权重的系数相乘得到的预测点
"""
xMat = np.mat(xArr)
yMat = np.mat(yArr).T
row = np.shape(xMat)[0]
# eye()返回一个对角线元素为1,其他元素为0的二维数组,创建传递矩阵weights
weights = np.mat(np.eye(row))
for j in range(row):
# testPoint的形式是一个行向量
# 计算testPoint与输入样本点之间的距离,然后下面计算出每个样本贡献误差的权值
diffMat = testPoint - xMat[j, :]
# k控制衰减的速度
weights[j, j] = np.exp(diffMat * diffMat.T / (-2.0 * k ** 2))
# 根据矩阵乘法计算xTx,其中的weights矩阵是样本点对应的权值矩阵
xTx = xMat.T * (weights * xMat)
if np.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):
"""
测试局部加权线性回归函数
:param testArr:测试样本点
:param xArr:样本的特征数据,即feature
:param yArr:每个样本对应的类别标签,即目标变量
:param k:关于赋予权重矩阵的核的每一个参数,与权重的衰减速率有关
:return:
"""
row = np.shape(testArr)[0]
yHat = np.zeros(row)
for i in range(row):
yHat[i] = lwlr(testArr[i], xArr, yArr, k)
return yHat
3 实例:预测鲍鱼的年龄
def rssError(yArr, yHatArr):
return ((yArr - yHatArr) ** 2).sum()
if __name__ == "__main__":
abX, abY = loadDataSet('abalone.txt')
print("训练集与测试集相同:局部加权线性回归")
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)
print('k=0.1时,误差大小为:', rssError(abY[0:99], yHat01.T))
print('k=1时,误差大小为:', rssError(abY[0:99], yHat1.T))
print('k=10时,误差大小为:', rssError(abY[0:99], yHat10.T))
print('')
print("训练集与测试集不同:局部加权线性回归")
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('k=0.1时,误差大小为:', rssError(abY[100:199], yHat01.T))
print('k=1时,误差大小为:', rssError(abY[100:199], yHat1.T))
print('k=10时,误差大小为:', rssError(abY[100:199], yHat10.T))
print('')
print("训练集与测试集不同:简单的线性回归与k=1时的局部加权线性回归对比:")
print('k=1时,误差大小为:', rssError(abY[100:199], yHat1.T))
ws = standRegres(abX[0:99], abY[0:99])
yHat = np.mat(abX[100:199]) * ws
print('简单的线性回归误差大小:', rssError(abY[100:199], yHat.T.A))
plt.show()
4 缩减系数来“理解”数据
- 如果数据的特征比样本点还多(n>m)应该怎么办?是否还可以使用线性回归和之前的方法来做预测?
- 答案是否定的,即不能再使用前面介绍的方法。这是因为在计算矩阵求逆的时候会出错(如果特征比样本点还多(n > m),也就是说输入数据的矩阵x 不是满秩矩阵。非满秩矩阵求逆时会出现问题。)
- 为了解决这个问题,引入了缩减方法:岭回归,lasso法和前向逐步回归(正则化,通过在模型中增加额外信息来解决过拟合、引入惩罚项,降低模型参数的影响)。
4.1 岭回归
- 岭回归(正则化线性归回方法)就是在矩阵上加一个λI从而使得矩阵非奇异,进而能求逆。其中矩阵I是一个单位矩阵, 对角线上元素全为1,其他元素全为0。而(“岭”)λ是一个用户定义的数值。
- 在这种情况下,回归系数的计算公式将变成: ω ^ = ( X T X + λ I ) − 1 X T y \widehat{\omega} = (X^TX + \lambda I)^{-1}X^Ty ω =(XTX+λI)−1XTy
- 岭回归最先用来处理特征数多于样本数的情况,现在也用于在估计中加入偏差,从而得到更好的估计。这里通过引入λ 来限制了所有w 之和,通过引入该惩罚项,能够减少不重要的参数,这个技术在统计学中也叫作缩减。
python代码实现
def ridgeRegres(xMat, yMat, lam=0.2):
"""
:param xMat:
:param yMat:
:param lam:
:return:
"""
xTx = xMat.T * xMat
denom = xTx + np.eye(np.shape(xMat)[1]) * lam
if np.linalg.det(denom) == 0.0:
print("this matrix is singular,cannot do inverse")
ws = np.linalg.pinv(denom) * (xMat.T * yMat)
ws = denom.I * (xMat.T * yMat)
return ws
def ridgeTest(xArr, yArr):
"""
:param xArr:
:param yArr:
:return:
"""
xMat = np.mat(xArr)
yMat = np.mat(yArr).T
yMean = np.mean(yMat, 0)
yMat = yMat - yMean
xMeans = np.mean(xMat, 0)
xVar = np.var(xMat, 0)
xMat = (xMat - xMeans) / xVar
numTestPts = 30
wMat = np.zeros((numTestPts, np.shape(xMat)[1]))
for i in range(numTestPts):
ws = ridgeRegres(xMat, yMat, np.exp(i - 10))
wMat[i, :] = ws.T
return wMat
if __name__ == "__main__":
abX, abY = loadDataSet('abalone.txt')
ridgeWeights = ridgeTest(abX,abY)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(ridgeWeights)
plt.show()
回归系数与log(λ)的关系,最左边λ最小时,8个系数的原始值与线性回归一样,最右边8个回归系数全部缩减为0,为找到最佳参数λ,还需要将ws乘以测试集比较原始测试集对比误差大小。0对应
λ
=
e
x
p
(
0
−
10
)
\lambda = exp(0-10)
λ=exp(0−10),30对应
λ
=
e
x
p
(
30
−
10
)
\lambda=exp(30-10)
λ=exp(30−10)
4.2 lasso
-
在增加约束:所有回归系数的平方和不能大于λ的条件下、普通的最小二乘法回归会得到与岭回归一样的公式
∑ k = 1 n ω k 2 ⩽ λ \sum_{k=1}^n\omega_k^2\leqslant\lambda k=1∑nωk2⩽λ- 限定了所有回归系数的平方和不能大于λ(L2正则化)。使用普通的最小二乘法回归在当两个或更多的特征相关时,可能会得到一个很大的正系数和一个很大的负系数。因为上述限制条件的存在,使用岭回归可以避免这个问题
-
与岭回归类似,另一个缩减方法lasso也对回归系数做了限定,对应的约束条件如下:
∑ k = 1 n ∣ ω k ∣ ⩽ λ \sum_{k=1}^n|\omega_k|\leqslant\lambda k=1∑n∣ωk∣⩽λ- 唯一的不同点在于,这个约束条件使用绝对值取代了平方和(L1正则化)。在λ 足够小的时候,一些系数会因此被迫缩减到0.这个特性可以帮助我们更好地理解数据。
- 计算复杂度高(二次规划算法求解)
4.3 前向逐步回归
- 前向逐步回归算法可以得到与lasso 差不多的效果,但更加简单。它属于一种贪心算法
- 即每一步都尽可能减少误差。一开始,所有权重都设置为1,然后每一步所做的决策是对某个权重增加或减少一个很小的值。
逐步线性回归算法的主要优点在于它可以帮助人们理解现有的模型并作出改进。当构建了一个模型后,可以运行该算法找出重要的特征,这样就有可能及时停止对那些不重要特征的收集。
4.5 权衡偏差与方差
-
当应用缩减方法(如逐步线性回归或岭回归)时,模型也就增加了偏差(bias),与此同时减小了模型的方差(variance)
-
模型和测量值之间存在的差异,叫做误差error。当考虑模型中存在“噪声”或者说误差时,必须考虑其来源。
- 对复杂的过程简化,会导致模型和测量值之间出现“噪声”和误差
- 无法理解数据的真实生成过程,也会导致差异的发生。
- 测量过程本身也可能产生“噪声”或问题
- 局部加权线性回归通过引入越来越小的核来不断增大模型的方差
- 缩减法可以将一些系数缩减成很小的值或直接缩减为0,增大模型偏差。
- 方差是模型之间的差异,而偏差指的是模型预测值与数据之间的差异。偏差与方差折中的概念在机器学习中十分流行
-
方差度量了同样大小的训练集的变动所导致的学习性能的变化, 即 刻画了数据扰动所造成的影响
-
偏差度量了学习算法的期望预测与真实结果的偏离程度,即 刻画了学习算法本身的拟合能力
-
噪声表达了在当前任务上任何学习算法所能达到的期望泛化误差的下界, 即 刻画了学习问题本身的难度
4.6 实例:预测乐高玩具套装的价格
from time import sleep
import json
import urllib
def searchForSet(retX,retY,setNum,yr,numPce,origPrc):
sleep(10)
myAPIstr = 'get from code.google.com'
searchURL = 'https://www.googleapis.com/shopping/search/v1/public/products?key=%s&country=US&q=lego+%d&alt=json' % (myAPIstr, setNum)
pg = urllib.request.urlopen(searchURL)
retDict = json.loads(pg.read())
for i in range(len(retDict['items'])):
try:
currItem = retDict['items'][i]
if currItem['product']['condition'] == 'new':
newFlag = 1
else: newFlag = 0
listOfInv = currItem['product']['inventories']
for item in listOfInv:
sellingPrice = item['price']
if sellingPrice > origPrc * 0.5:
print ("%d\t%d\t%d\t%f\t%f" % (yr,numPce,newFlag,origPrc, sellingPrice))
retX.append([yr, numPce, newFlag, origPrc])
retY.append(sellingPrice)
except: print ('problem with item %d' % i)
def setDataCollect(retX, retY):
searchForSet(retX, retY, 8288, 2006, 800, 49.99)
searchForSet(retX, retY, 10030, 2002, 3096, 269.99)
searchForSet(retX, retY, 10179, 2007, 5195, 499.99)
searchForSet(retX, retY, 10181, 2007, 3428, 199.99)
searchForSet(retX, retY, 10189, 2008, 5922, 299.99)
searchForSet(retX, retY, 10196, 2009, 3263, 249.99)
def crossValidation(xArr,yArr,numVal=10):
m = len(yArr)
indexList = list(range(m))
#创建numVal*30的误差矩阵
##30的由来:ridgeTest()使用了30个不同的lambda值来创建不同的回归系数,即numTestPts = 30
errorMat = np.zeros((numVal,30))
for i in range(numVal):
#创建测试集和训练集容器
trainX=[]; trainY=[]
testX = []; testY = []
#打乱索引顺序,实现随机选取训练集和测试集数据点
np.random.shuffle(indexList)
#90%训练+10%测试
for j in range(m):
if j < m*0.9:
trainX.append(xArr[indexList[j]])
trainY.append(yArr[indexList[j]])
else:
testX.append(xArr[indexList[j]])
testY.append(yArr[indexList[j]])
wMat = ridgeTest(trainX,trainY) #获得30组ws向量
for k in range(30):
matTestX = np.mat(testX); matTrainX=np.mat(trainX)
#用训练集参数将测试集数据标准化
meanTrain = np.mean(matTrainX,0)
varTrain = np.var(matTrainX,0)
matTestX = (matTestX-meanTrain)/varTrain
yEst = matTestX * np.mat(wMat[k,:]).T + np.mean(trainY)
errorMat[i,k]=np.rssError(yEst.T.A,np.array(testY))
#print errorMat[i,k]
#计算不同岭回归ws下errorMat的平均值,观察平均性能 #meanErrors:1*30矩阵
meanErrors = np.mean(errorMat,0)
minMean = float(min(meanErrors))
#nonzero(meanErrors==minMean)返回的是误差最小的索引,因此bestWeights为误差最小的那个w向量
bestWeights = wMat[np.nonzero(meanErrors==minMean)]
#岭回归使用了数据标准化,而standRegres没有,为了比较可视化,因此需要将数据还原
#标准化后 Xreg = (x-meanX)/var(x),预测y=Xreg*w+meanY
#因此,利用未标准化的x来计算y= x*w/var(x) - meanX*w/var(x) +meanY
#其中unReg=w/var
xMat = np.mat(xArr); yMat=np.mat(yArr).T
meanX = np.mean(xMat,0); varX = np.var(xMat,0)
unReg = bestWeights/varX
#print ("the best model from Ridge Regression is:\n",unReg)
#特别注意这里的sum函数,一定是np.sum,因为一般的sum只能对list求和,而这里的参数是matrix
#print ("with constant term: ",-1*np.sum(multiply(meanX,unReg)) + mean(yMat))
yHat=xMat*unReg.T-1*np.sum(np.multiply(meanX,unReg)) + np.mean(yMat)
return yHat
5 总结
- “普通最小二乘法”:对于二维数据而言,线性回归就是找出一个一次函数去拟合数据,使得平方误差最小。是的,这里的损失函数是平方损失。
- 并不是所有数据都能用线性回归来拟合,有的时候数据有转折或是其他规律。于是有了局部加权线性回归,该方法更加关注将要预测的数据周围的点,也就是通过高斯核给预测点周围的数据赋予权值,选择合适的高斯系数能使结果更好,当然这种算法的缺点是:对于每一个要预测的点,都要重新依据整个数据集计算一个线性回归模型出来,使得算法代价极高。
- 岭回归是一种常见的shrinkage coefficient(缩减系数)方法,还有其他缩减方法,如lasso(效果很好但计算复杂)等。所谓shrinkage,就是通过一些手段来减少不重要的参数。岭回归是一种专用于线性数据分析的有偏估计回归方法,实质上是一种改良的最小二乘估计法,通过放弃最小二乘法的无偏性,以损失部分信息、降低精度为代价获得回归系数更为符合实际、更可靠的回归方法,对病态数据的拟合要强于普通最小二乘法