之前的几章都是监督学习中的分类方法,分类的目标变量是标称型数据,现在开始学习回归方法,对连续型数据作出预测。
Linear Regression:
优点:计算不复杂
缺点:对非线性的数据拟合不好
适用于数值型和标称型数据
回归的目的是预测数值型的目标值。
参考链接:
从零开始学Python【24】--岭回归及LASSO回归(理论部分
Python3《机器学习实战》学习笔记(十二):线性回归提高篇之乐高玩具套件二手价预测
现在有数据如下:
1.000000 0.067732 3.176513
1.000000 0.427810 3.816464
1.000000 0.995731 4.550095
1.000000 0.738336 4.256571
1.000000 0.981083 4.560815
第一列都是1
代码如下
def loadDataSet(fileName):
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):
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
执行结果如下:
这里ws是回归系数。在用内积来预测y的时候,第一维将乘以前面的常数X0,第二维乘以输入变量X1。因为前面假定了X0=1,所以最终会得到ws[0]+ws[1]*X1。得到的结果是估计值,用yHat表示。
xMat = mat(xArr)
yMat = mat(yArr).T
yHat = xMat * ws
然后绘制散点图和最佳拟合线
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(xMat[:,1].flatten().A[0], yMat[:,0].flatten().A[0], s=1) #这里flatten().A[0]类似于转成list
上面这里是散点图,绘制的是原始数据,为了绘制最佳拟合线,需要用到yHat的值。在这里如果直线上的数据点次序混乱,绘图会出现问题,所以要先排序。
ax.scatter是散点图,用的是原始数据。
ax.plot是直线,用的是预测的数据。
几乎所有数据集都可以用上面这方法建立模型,那么如何判断这些模型的好坏呢?
比较下面两个子图,如果在两个数据集分别做线性回归,那么得到完全一样的模型(拟合直线),但是显然这两个数据集是不逸雅阁的,所有要怎么判别它们的效果呢。有一种方法可以计算预测值yHat序列和真实值y序列的匹配程度,那就是计算它们的相关系数。
numpy库提供了相关系数的计算方法,通过命令corrcoef(yEstimate, yActual)来计算。需要保证两个向量都是行向量。
对角线1表示自己和自己匹配最完美。
最佳拟合直线方法把数据视为直线进行建模,但是数据还会有其他潜在模式,那么怎么利用这些模式呢?完美可以根据数据进行具不调整预测。
局部加权线性回归:
线性回归的一个问题是有可能出现underfitting现象,因为它求的是具有最小均方误差的无偏估计,很显然,如果模型欠拟合就不能得到最好的预测效果,所以有些方法允许在估计中引入一些偏差,从而降低预测的均方误差。
其中一个方法就是具不加权线性回归LWLR(locally weighted linear regression)。在这个算法中,完美给待预测点附近的每个点赋予一定的权重,然后在自己上基于最小均方差来进行普通的回归。与kNN一样,这种算法每次预测都需要事先选出对应的数据子集。LWLR用kernel来对附近的点赋予更高的权重。kernel type可以自由选择,常用的是Gaussian Kernel,对应的权重:
这样构建了一个只含有对角元素的权重矩阵w,并且点x与x(i)越近,w(i,i)也越大。(TODO:看看是怎么构建的对角元素矩阵)
这里包含了一个需要指定的参数k,它决定了对附近的点赋予多大的权重,这是使用LWLR唯一需要考虑的参数。下图是参数k与权重的关系。
接下来使用这个模型的代码如下:
def lwlr(testPoint, xArr, yArr, k=1.0):
xMat = mat(xArr)
yMat = mat(yArr)
m = shape(xMat)[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
可以对单点进行估计
集合一个yHat序列,得到所有点的估计:
def lwlrTest(testArr, xArr, yArr, k=1.0):
m = shape(testArr)[0]
yHat = zeros(m)
for i in range(m):
yHat[i] = lwlr(testArr[i], xArr, yArr, k)
return yHat
对估计值进行排序:
srtInd = xMat[:,1].argsort(0)
xSort = xMat[srtInd][:,0,:] #等价于xMat[srtInd.flatten().A[0]]
然后进行绘图
import matplotlib.pyplot as plt
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='r')
plt.show()
得到下面图:
通过改变k的话,会得到不一样的数据。在下图中,当k=1时权重很大,等于把所有数据视为等权重,得出的最佳拟合直线和标准的回归一致。用k=0.01时效果最好,抓住了数据的潜在模式。当使用k=0.003纳入很多噪音点,变成了overfitting。
使用局部加权线性回归的问题是增加了计算量,因为对每个点做预测时候都需要用到整个数据集。在k=0.01时会发现大多数据点的权重都接近0,如果避免这些计算可以减少计算时间缓解计算量。
下面来预测鲍鱼的年龄,可以通过它们壳的层数推算。
数据格式如下:
1 0.455 0.365 0.095 0.514 0.2245 0.101 0.15 15
1 0.35 0.265 0.09 0.2255 0.0995 0.0485 0.07 7
-1 0.53 0.42 0.135 0.677 0.2565 0.1415 0.21 9
1 0.44 0.365 0.125 0.516 0.2155 0.114 0.155 10
0 0.33 0.255 0.08 0.205 0.0895 0.0395 0.055 7
用L2的和分析误差大小:
def rssError(yArr, yHatArr):
return ((yArr - yHatArr)**2).sum()
读取数据查看表现
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)
rE01 = regression.rssError(abY[0:99], yHat01.T)
rE1 = regression.rssError(abY[0:99], yHat01.T)
rE10 = regression.rssError(abY[0:99], yHat01.T)
print(rE01)
print(rE1)
print(rE10)
结果是
56.78868743050092
429.89056187038
549.1181708827924
可以看到用较小的kernel得到较低的误差。上面是选取了一部分的数据,那么如果用到所有的数据集上呢?用最小的kernel会造成overfitting,对新数据不一定有很好的效果,现在来新数据上测试看看:
yHatNew01 = regression.lwlrTest(abX[100:199], abX[0:99], abY[0:99], 0.1)
yHatNew1 = regression.lwlrTest(abX[100:199], abX[0:99], abY[0:99], 1)
yHatNew10 = regression.lwlrTest(abX[100:199], abX[0:99], abY[0:99], 10)
rNewE01 = regression.rssError(abY[100:199], yHatNew01.T)
rNewE1 = regression.rssError(abY[100:199], yHatNew1.T)
rNewE10 = regression.rssError(abY[100:199], yHatNew10.T)
print(rNewE01)
print(rNewE1)
print(rNewE10)
结果是
57913.51550155911
573.5261441895982
517.5711905381903
从这里看到kernel等于10时误差才是最小,但在训练的时候误差却是最大的。
现在完美把它和简单的线性回归做个比较:
ws = regression.standRegres(abX[0:99], abY[0:99])
yHat = mat(abX[100:199]) * ws
regression.rssError(abY[100:199], yHat.T.A)
得到
518.6363153245542
简单线性回归达到了局部加权线性回归类似的效果。这表明,必须在未知数据上比较效果才能选到最好的模型。但是这一一次测试并不能完全决定最佳的kernel,需要多次测试比较才可以。
下面开始学习另一种提高预测精度的方法:缩减系数(shrinkage)
如果数据特征比样本的海多,就不能用线性回归和之前的方法来做预测了,因为输入数据的矩阵X不是满秩矩阵,求逆的时候会出现问题。
第一种方法叫岭回归(ridge regression),也就是在矩阵上加一个λI让矩阵非奇异,这一就能对求逆了。其中矩阵I是一个m*m的单位矩阵,对角线上的元素都是1,其他为0。而λ是自定义数值。回归系数的计算公式变成:
岭回归最先用来处理特征数多余样本数的情况,现在也用于在古籍中加入偏差,从而得到更好的估计。这里通过引入λ来限制所有w的和,通过引入这个penalty term,减少不重要的参数,这一方法在统计学成为shrinkage。
岭回归里面的岭的是什么意思呢?在岭回归中,使用了单位矩阵乘以常量λ,观察单位矩阵I可以看到它只有对角线有一条1,像是一条岭。
缩减方法去掉不重要的参数,可以更好理解数据。此外,与简单的线性回归相比,缩减发能有更好的预测效果。
首先我们通过预测误差最小化得到λ:数据获取完之后,首先抽一部分数据用于测试,剩余的作为训练集用于训练参数w。训练完毕后再到测试集上测试。通过选取不同λ重复测试过程,最终得到一个使预测误差最小的λ。
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
xMeans = mean(xMat, 0)
xVar = (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
在这里,使用岭回归和缩减技术的话,必须对特征做标准化处理,使每维特征具有相同的重要性。在上面代码里,昨晚数据标准化处理后,在30个不同的λ下调用ridgeRegres()函数。这里指数级变化,可以看出λ在非常小的值和非常大的值分别对结果的影响
结果如下
上图是回归系数与log(λ)的关系。左边λ最小时,可以得到所有系数的原始值(与线性回归一致),在右边,系数全部缩减为0。在中间部分的某值可以取得最好的预测效果。为了找到最佳参数值,需要交叉验证。要判断哪些变量对结果最有影响力,在上图观察对应的系数大小就可以了。(上图的8条线是8个特征,30次变化)
接下来学习Lasso,也是一种缩减方法。
增加这个约束,普通最小二乘法回归会得到与岭回归一样的公式:
这个约束限定了所有回归系数的平方和不能大于λ。使用普通的最小二乘法回归在当两个或以上的特征相关时,可能得出一个很大的正系数和一个很大的负系数。但约束了上面这条件后,岭回归可以避免这个问题。
LASSO方法也对回归系数做了限定:
这里用绝对值取代了平方和。虽然只是约束形式稍作变化,结果却大相径庭:在λ足够小的时候,一些系数会变成0,这个特性可以棒我们更好理解数据。但是这一变化,会极大增加计算复杂度,下面我们学习一个简单的方法,叫前向逐步回归
前向回归算法可以得到和LASSO差不多的效果,但更加简单。它属于一种贪心算法,即每一步都尽可能减少误差。初始化所有的权重都为1,然后每一步决策都是对某个权重增加或者减少一个很小的值。
下面通过代码实现
def stageWise(xArr, yArr, eps=0.01, numIt=100):
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
m, n = shape(xMat)
ws = zeros((n, 1))
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() # 本轮循环完所有特征后的最佳w
returnMat[i, :] = ws.T
return returnMat
结果如下
可以看到有一些是0,表明它们不对目标值造成影响,说明这些特征很可能是不需要的。另外,在eps设置为0.01情况下,一段时间后系数就已经饱和,在特定值之间来回震荡,这是因为步长太大。可以试着用更小的步长和更多的步数。
接下来把结果和最小二乘法进行比较
展开的话可以看到经过5000次迭代,逐步线性回归算法和常规的最小二乘法结果相似。
如果把eps设为0.005时经过1000次迭代的话会得到下面的结果:
逐步线性回归得到了LASSO相似的结果。
逐步线性回归的优点是可以帮助我们理解现有模型做出改进。我们构建一个模型后,可以用该算法找出重要特征,这一就可以及时停止对不重要特征的手机。最后,如果用于测试,该算法每100次迭代后就可以构建一个模型,可以使用类似与10折交叉验证的方法比较这些模型,最终选择误差最小的模型。
当应用缩减方法(比如逐步线性回归或岭回归—),模型增加了偏差,同时减少了模型的方差。
接下来我们研究偏差和方差的的影响。
如果有一些数据是根据这个公式生成的:y = 3.0 + 1.7x + 0.1sin(30x) + 0.06N(0, 1)
其中N(0, 1)是一个均值0方差1的正态分布。我们可以想到,最佳拟合线是3.0+1.7x这一部分。这样的话,误差部分就是0.1sin(30x) + 0.06N(0.1)。训练误差和测试误差曲线图如下:(上为测试误差,下为训练误差)
在之前的学习里,我们知道,降低kernel大小,可以让训练误差变小。上图中从左到右就表示了kernel逐渐减小的过程。
我们限制需要调整模型复杂度达到测试误差的最小值。
一般认为,两种误差由三个部分组成:偏差,测量误差和随机噪音。之前我们通过引入越来越小的kernel值来不断增大模型的方差。
之前我们学习的缩减法,可以把一些系数缩减成很小的值或者直接缩减为0,这是增大模型偏差的例子。通过把特征的回归系数缩减到0,可以减少模型的复杂度。之前鲍鱼的特征由8个,我们消除了2个,使得模型更容易理解,同时降低了预测误差。
方差是怎么度量的呢?
比如从所有数据集随机取出两组随机样本集,分别用线性模型拟合,得到两组回归系数。这些系数间的差异就是模型方差的大小。
接下来我们预测乐高玩具套装的价格。
数据是从其他博主的网站下的
代码和书本不同,也参照博主的博客做了改动。
from bs4 import BeautifulSoup
def searchForSet(retX, retY, inFile, yr, numPce, origPrc):
with open(inFile, encoding='utf-8') as f:
html = f.read()
soup = BeautifulSoup(html)
i = 1
# 根据HTML页面结构进行解析
currentRow = soup.find_all('table', r = "%d" % i)
while(len(currentRow) != 0):
currentRow = soup.find_all('table', r = "%d" % i)
title = currentRow[0].find_all('a')[1].text
lwrTitle = title.lower()
# 查找是否有全新标签
if (lwrTitle.find('new') > -1) or (lwrTitle.find('nisb') > -1):
newFlag = 1.0
else:
newFlag = 0.0
# 查找是否已经标志出售,我们只收集已出售的数据
soldUnicde = currentRow[0].find_all('td')[3].find_all('span')
if len(soldUnicde) == 0:
print("商品 #%d 没有出售" % i)
else:
# 解析页面获取当前价格
soldPrice = currentRow[0].find_all('td')[4]
priceStr = soldPrice.text
priceStr = priceStr.replace('$','')
priceStr = priceStr.replace(',','')
if len(soldPrice) > 1:
priceStr = priceStr.replace('Free shipping', '')
sellingPrice = float(priceStr)
# 比原价低一半以上认为是不完整套装,去掉这些数据
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)
i += 1
currentRow = soup.find_all('table', r = "%d" % i)
def setDataCollect(retX, retY):
searchForSet(retX, retY, 'datafiles/lego10030.html', 2002, 3096, 269.99)
searchForSet(retX, retY, 'datafiles/lego10179.html', 2007, 5195, 499.99)
searchForSet(retX, retY, 'datafiles/lego10181.html', 2007, 3428, 199.99)
结果
然后我们用这些数据构建一个模型。需要添加对应常数项的特征X0=1
进行线性回归:
含义是
-992.000000+0.984375*年份-0.164886*部件数量-41.454083*是否为全新+3.373047*原价
上面看到部件数量反而价格减少,这样是不合理的,现在我们来使用岭回归,通过交叉验证,来找到使误差最小的λ对应的回归系数。
def crossValidation(xArr, yArr, numVal=10):
m = len(yArr)
indexList = list(range(m))
errorMat = zeros((numVal, 30)) # 为了创建30组回归系数
for i in range(numVal):
trainX = []
trainY = []
testX = []
testY = []
random.shuffle(indexList)
for j in range(m): # 一共m个样本
if j < m * 0.9: # 训练集90%,测试集10%
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组训练数据的岭回归系数,shape为30*n
for k in range(30):
matTestX = mat(testX)
matTrainX=mat(trainX)
meanTrain = mean(matTrainX, 0)
varTrain = var(matTrainX, 0)
matTestX = (matTestX - meanTrain) / varTrain # 把测试集按照训练集标准化
yEst = matTestX * mat(wMat[k,:]).T + mean(trainY) # wMat[k,:]表示根据30组的每一组去测试
errorMat[i, k] = rssError(yEst.T.A, array(testY))
meanErrors = mean(errorMat, 0)
minMean = float(min(meanErrors))
bestWeights = wMat[nonzero(meanErrors == minMean)]
xMat = mat(xArr)
yMat = mat(yArr).T
meanX = mean(xMat, 0)
varX = var(xMat, 0)
unReg = bestWeights / varX # 这里还原数据,因为之前进行了标准化
print('%f%+f*年份%+f*部件数量%+f*是否为全新%+f*原价' % ((-1 * sum(multiply(meanX, unReg)) + mean(yMat)), unReg[0,0], unReg[0,1], unReg[0,2], unReg[0,3]))
得到结果
43000.476225-21.475666*年份+0.004529*部件数量-79.532601*是否为全新+2.444479*原价
可以看到这个和常规的最小二乘法没有太大差异,那么没有达到预期效果。我们来查看以下缩减过程中回归系数的变化:
这些系数是经过不同程度的缩减得到的,可以看到有些列的特别大。我们可以通过这样看出哪些特征是重要的。
总结:
回归也是预测目标值的过程,它与分类的不同在于,它预测连续变量,分类预测离散变量。在回归方程里,求特征的最佳回归系数的方法是最小化误差的平方和。给定输入矩阵X,如果xTx的逆存在并可以求出,那么直接使用回归法。数据集计算出的会规范成不一定是最好的,可以使用预测值yHat和原始值y的相关性来度量回归方程的好坏。
当数据样本比特征少的时候,xTx的逆不能直接计算,即使样本比特征多也可能出现这样情况,折是因为特征可能高度相关。这时可以考虑使用缩减法:岭回归,LASSO,逐步线性回归。
缩减法可以看作是对一个模型增加偏差的同时减少方差。偏差方差折中可以棒我们理解现有模型并做出改进,从而得到更好的模型。
三者的损失函数:
线性回归的损失函数:
岭回归的损失函数:
Lasso回归的损失函数:
岭回归和Lasso回归都是通过一个λ系数调整。岭回归里面要求所有θ都存在,而Lasso可以把很多θ均为0。