ML in Action - Linear Regression

之前的几章都是监督学习中的分类方法,分类的目标变量是标称型数据,现在开始学习回归方法,对连续型数据作出预测。

Linear Regression:

优点:计算不复杂

缺点:对非线性的数据拟合不好

适用于数值型和标称型数据

回归的目的是预测数值型的目标值。

参考链接:

从零开始学Python【24】--岭回归及LASSO回归(理论部分

机器学习总结(一):线性回归、岭回归、Lasso回归

Python3《机器学习实战》学习笔记(十二):线性回归提高篇之乐高玩具套件二手价预测

扫描二维码关注公众号,回复: 5779432 查看本文章

现在有数据如下:

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(i,i)=exp\left ( \frac{\left | x^{(i)}-x \right |}{-2k^{2}} \right )

这样构建了一个只含有对角元素的权重矩阵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),也就是在矩阵X^{T}X上加一个λI让矩阵非奇异,这一就能对X^{T}X+\lambda I求逆了。其中矩阵I是一个m*m的单位矩阵,对角线上的元素都是1,其他为0。而λ是自定义数值。回归系数的计算公式变成:

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

岭回归最先用来处理特征数多余样本数的情况,现在也用于在古籍中加入偏差,从而得到更好的估计。这里通过引入λ来限制所有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,也是一种缩减方法。

增加这个约束,普通最小二乘法回归会得到与岭回归一样的公式:\sum_{k=1}^{n}w_k^{2}\leq \lambda

这个约束限定了所有回归系数的平方和不能大于λ。使用普通的最小二乘法回归在当两个或以上的特征相关时,可能得出一个很大的正系数和一个很大的负系数。但约束了上面这条件后,岭回归可以避免这个问题。

LASSO方法也对回归系数做了限定:\sum_{k=1}^{n}\left | w_k \right |\leq \lambda

这里用绝对值取代了平方和。虽然只是约束形式稍作变化,结果却大相径庭:在λ足够小的时候,一些系数会变成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,逐步线性回归。

缩减法可以看作是对一个模型增加偏差的同时减少方差。偏差方差折中可以棒我们理解现有模型并做出改进,从而得到更好的模型。

三者的损失函数:

线性回归的损失函数:\jmath (\theta )=\frac{1}{2m}\sum_{i=1}^{m}\left ( h_0(x^{(i)}) -y^{(i)} \right )^2

岭回归的损失函数:\jmath (\theta )=\frac{1}{2m}\sum_{i=1}^{m}\left ( h_0(x^{(i)}) -y^{(i)} \right )^2+\lambda \sum_{j=1}^{n} \theta _j^{2}

Lasso回归的损失函数:\jmath (\theta )=\frac{1}{2m}\sum_{i=1}^{m}\left ( h_0(x^{(i)}) -y^{(i)} \right )^2+\lambda \sum_{j=1}^{n}\left | \theta _j \right |

岭回归和Lasso回归都是通过一个λ系数调整。岭回归里面要求所有θ都存在,而Lasso可以把很多θ均为0。

猜你喜欢

转载自blog.csdn.net/gritsissi/article/details/87451670