機器學習實戰教程(十二):線性迴歸提升篇之樂高玩具套件二手價預測

原文連接:cuijiahua.com/blog/2017/1…php

1、前言

本篇文章講解線性迴歸的縮減方法,嶺迴歸以及逐步線性迴歸,同時熟悉sklearn的嶺迴歸使用方法,對樂高玩具套件的二手價格作出預測。html

2、嶺迴歸

若是數據的特徵比樣本點還多應該怎麼辦?很顯然,此時咱們不能再使用上文的方法進行計算了,由於矩陣X不是滿秩矩陣,非滿秩矩陣在求逆時會出現問題。爲了解決這個問題,統計學家引入嶺迴歸(ridge regression)的概念。git

一、什麼是嶺迴歸?

嶺迴歸即咱們所說的L2正則線性迴歸,在通常的線性迴歸最小化均方偏差的基礎上增長了一個參數w的L2範數的罰項,從而最小化罰項殘差平方和:github

簡單說來,嶺迴歸就是在普通線性迴歸的基礎上引入單位矩陣。迴歸係數的計算公式變形以下:算法

式中,矩陣I是一個mxm的單位矩陣,加上一個λI從而使得矩陣非奇異,進而能對矩陣求逆。windows

嶺迴歸最早用來處理特徵數多於樣本數的狀況,如今也用於在估計中加入誤差,從而獲得更好的估計。這裏經過引入λ來限制了全部w之和,經過引入該懲罰項,可以減小不重要的參數,這個技術在統計學中也能夠叫作縮減(shrinkage)。api

縮減方法能夠去掉不重要的參數,所以能更好地裂解數據。此外,與簡單的線性迴歸相比,縮減法可以取得更好的預測效果。數組

二、編寫代碼

爲了使用嶺迴歸和縮減技術,首先須要對特徵作標準化處理。由於,咱們須要使每一個維度特徵具備相同的重要性。本文使用的標準化處理比較簡單,就是將全部特徵都減去各自的均值併除以方差。bash

代碼很簡單,只須要稍作修改,其中,λ爲模型的參數。咱們先繪製一個迴歸係數與log(λ)的曲線圖,看下它們的規律,編寫代碼以下:網絡

# -*-coding:utf-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import numpy as np
def loadDataSet(fileName):
    """ 函數說明:加載數據 Parameters: fileName - 文件名 Returns: xArr - x數據集 yArr - y數據集 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []; yArr = []
    fr = open(fileName)
    for line in fr.readlines():
        lineArr =[]
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr, yArr
def ridgeRegres(xMat, yMat, lam = 0.2):
    """ 函數說明:嶺迴歸 Parameters: xMat - x數據集 yMat - y數據集 lam - 縮減係數 Returns: ws - 迴歸係數 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    xTx = xMat.T * xMat
    denom = xTx + np.eye(np.shape(xMat)[1]) * lam
    if np.linalg.det(denom) == 0.0:
        print("矩陣爲奇異矩陣,不能轉置")
        return
    ws = denom.I * (xMat.T * yMat)
    return ws
def ridgeTest(xArr, yArr):
    """ 函數說明:嶺迴歸測試 Parameters: xMat - x數據集 yMat - y數據集 Returns: wMat - 迴歸係數矩陣 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    #數據標準化
    yMean = np.mean(yMat, axis = 0)                        #行與行操做,求均值
    yMat = yMat - yMean                                    #數據減去均值
    xMeans = np.mean(xMat, axis = 0)                    #行與行操做,求均值
    xVar = np.var(xMat, axis = 0)                        #行與行操做,求方差
    xMat = (xMat - xMeans) / xVar                        #數據減去均值除以方差實現標準化
    numTestPts = 30                                        #30個不一樣的lambda測試
    wMat = np.zeros((numTestPts, np.shape(xMat)[1]))    #初始迴歸係數矩陣
    for i in range(numTestPts):                            #改變lambda計算迴歸係數
        ws = ridgeRegres(xMat, yMat, np.exp(i - 10))    #lambda以e的指數變化,最初是一個很是小的數,
        wMat[i, :] = ws.T                                 #計算迴歸係數矩陣
    return wMat
def plotwMat():
    """ 函數說明:繪製嶺迴歸係數矩陣 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
    abX, abY = loadDataSet('abalone.txt')
    redgeWeights = ridgeTest(abX, abY)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.plot(redgeWeights)    
    ax_title_text = ax.set_title(u'log(lambada)與迴歸係數的關係', FontProperties = font)
    ax_xlabel_text = ax.set_xlabel(u'log(lambada)', FontProperties = font)
    ax_ylabel_text = ax.set_ylabel(u'迴歸係數', FontProperties = font)
    plt.setp(ax_title_text, size = 20, weight = 'bold', color = 'red')
    plt.setp(ax_xlabel_text, size = 10, weight = 'bold', color = 'black')
    plt.setp(ax_ylabel_text, size = 10, weight = 'bold', color = 'black')
    plt.show()
if __name__ == '__main__':
    plotwMat()

複製代碼

運行結果以下:

圖繪製了迴歸係數與log(λ)的關係。在最左邊,即λ最小時,能夠獲得全部係數的原始值(與線性迴歸一致);而在右邊,係數所有縮減成0;在中間部分的某個位置,將會獲得最好的預測結果。想要獲得最佳的λ參數,可使用交叉驗證的方式得到,文章的後面會繼續講解。

3、前向逐步線性迴歸

前向逐步線性迴歸算法屬於一種貪心算法,即每一步都儘量減小偏差。咱們計算迴歸係數,再也不是經過公式計算,而是經過每次微調各個迴歸係數,而後計算預測偏差。那個使偏差最小的一組迴歸係數,就是咱們須要的最佳迴歸係數。

前向逐步線性迴歸實現也很簡單。固然,仍是先進行數據標準化,編寫代碼以下:

# -*-coding:utf-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import numpy as np
def loadDataSet(fileName):
    """ 函數說明:加載數據 Parameters: fileName - 文件名 Returns: xArr - x數據集 yArr - y數據集 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []; yArr = []
    fr = open(fileName)
    for line in fr.readlines():
        lineArr =[]
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr, yArr
 
def regularize(xMat, yMat):
    """ 函數說明:數據標準化 Parameters: xMat - x數據集 yMat - y數據集 Returns: inxMat - 標準化後的x數據集 inyMat - 標準化後的y數據集 Website: https://www.cuijiahua.com/ Modify: 2017-11-23 """    
    inxMat = xMat.copy()                                                        #數據拷貝
    inyMat = yMat.copy()
    yMean = np.mean(yMat, 0)                                                    #行與行操做,求均值
    inyMat = yMat - yMean                                                        #數據減去均值
    inMeans = np.mean(inxMat, 0)                                                   #行與行操做,求均值
    inVar = np.var(inxMat, 0)                                                     #行與行操做,求方差
    inxMat = (inxMat - inMeans) / inVar                                            #數據減去均值除以方差實現標準化
    return inxMat, inyMat
 
def rssError(yArr,yHatArr):
    """ 函數說明:計算平方偏差 Parameters: yArr - 預測值 yHatArr - 真實值 Returns: Website: https://www.cuijiahua.com/ Modify: 2017-11-23 """
    return ((yArr-yHatArr)**2).sum()
def stageWise(xArr, yArr, eps = 0.01, numIt = 100):
    """ 函數說明:前向逐步線性迴歸 Parameters: xArr - x輸入數據 yArr - y預測數據 eps - 每次迭代須要調整的步長 numIt - 迭代次數 Returns: returnMat - numIt次迭代的迴歸係數矩陣 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    xMat = np.mat(xArr); yMat = np.mat(yArr).T                                         #數據集
    xMat, yMat = regularize(xMat, yMat)                                                #數據標準化
    m, n = np.shape(xMat)
    returnMat = np.zeros((numIt, n))                                                #初始化numIt次迭代的迴歸係數矩陣
    ws = np.zeros((n, 1))                                                            #初始化迴歸係數矩陣
    wsTest = ws.copy()
    wsMax = ws.copy()
    for i in range(numIt):                                                            #迭代numIt次
        # print(ws.T) #打印當前迴歸係數矩陣
        lowestError = float('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                                                         #記錄numIt次迭代的迴歸係數矩陣
    return returnMat
def plotstageWiseMat():
    """ 函數說明:繪製嶺迴歸係數矩陣 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
    xArr, yArr = loadDataSet('abalone.txt')
    returnMat = stageWise(xArr, yArr, 0.005, 1000)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.plot(returnMat)    
    ax_title_text = ax.set_title(u'前向逐步迴歸:迭代次數與迴歸係數的關係', FontProperties = font)
    ax_xlabel_text = ax.set_xlabel(u'迭代次數', FontProperties = font)
    ax_ylabel_text = ax.set_ylabel(u'迴歸係數', FontProperties = font)
    plt.setp(ax_title_text, size = 15, weight = 'bold', color = 'red')
    plt.setp(ax_xlabel_text, size = 10, weight = 'bold', color = 'black')
    plt.setp(ax_ylabel_text, size = 10, weight = 'bold', color = 'black')
    plt.show()
if __name__ == '__main__':
    plotstageWiseMat()

複製代碼

運行結果以下:

仍是,咱們打印了迭代次數與迴歸係數的關係曲線。能夠看到,有些係數從始至終都是約爲0的,這說明它們不對目標形成任何影響,也就是說這些特徵極可能是不須要的。逐步線性迴歸算法的優勢在於它能夠幫助人們理解有的模型並作出改進。當構建了一個模型後,能夠運行該算法找出重要的特徵,這樣就有可能及時中止對那些不重要特徵的收集。

總結一下:

縮減方法(逐步線性迴歸或嶺迴歸),就是將一些係數縮減成很小的值或者直接縮減爲0。這樣作,就增大了模型的誤差(減小了一些特徵的權重),經過把一些特徵的迴歸係數縮減到0,同時也就減小了模型的複雜度。消除了多餘的特徵以後,模型更容易理解,同時也下降了預測偏差。可是當縮減過於嚴厲的時候,就會出現過擬合的現象,即用訓練集預測結果很好,用測試集預測就糟糕不少。

4、預測樂高玩具套件的價格

樂高(LEGO)公司生產拼裝類玩具,由不少大小不一樣的塑料插塊組成。它的樣子以下圖所示:

通常來講,這些插塊都是成套出售,它們能夠拼裝成不少不一樣的東西,如船、城堡、一些著名建築等。樂高公司每一個套裝包含的部件數目從10件到5000件不等。

一種樂高套件基本上在幾年後就會停產,但樂高的收藏者之間仍會在停產後彼此交易。本次實例,就是使用迴歸方法對收藏者之間的交易價格進行預測。

一、獲取數據

書中使用的方法是經過Google提供的API進行獲取數據,可是如今這個API已經關閉,咱們沒法經過api獲取數據了。不過幸運的是,我在網上找到了書上用到的那些html文件。

原始數據下載地址:數據下載

咱們經過解析html文件,來獲取咱們須要的信息,若是學過個人《Python3網絡爬蟲》,那麼我想這部分的內容會很是簡單,解析代碼以下:

# -*-coding:utf-8 -*-
from bs4 import BeautifulSoup
 
def scrapePage(retX, retY, inFile, yr, numPce, origPrc):
    """ 函數說明:從頁面讀取數據,生成retX和retY列表 Parameters: retX - 數據X retY - 數據Y inFile - HTML文件 yr - 年份 numPce - 樂高部件數目 origPrc - 原價 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    # 打開並讀取HTML文件
    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):
    """ 函數說明:依次讀取六種樂高套裝的數據,並生成數據矩陣 Parameters: 無 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    scrapePage(retX, retY, './lego/lego8288.html', 2006, 800, 49.99)                #2006年的樂高8288,部件數目800,原價49.99
    scrapePage(retX, retY, './lego/lego10030.html', 2002, 3096, 269.99)                #2002年的樂高10030,部件數目3096,原價269.99
    scrapePage(retX, retY, './lego/lego10179.html', 2007, 5195, 499.99)                #2007年的樂高10179,部件數目5195,原價499.99
    scrapePage(retX, retY, './lego/lego10181.html', 2007, 3428, 199.99)                #2007年的樂高10181,部件數目3428,原價199.99
    scrapePage(retX, retY, './lego/lego10189.html', 2008, 5922, 299.99)                #2008年的樂高10189,部件數目5922,原價299.99
    scrapePage(retX, retY, './lego/lego10196.html', 2009, 3263, 249.99)                #2009年的樂高10196,部件數目3263,原價249.99
 
if __name__ == '__main__':
    lgX = []
    lgY = []
    setDataCollect(lgX, lgY)

複製代碼

運行結果以下:

咱們對沒有的商品作了處理。這些特徵分別爲:出品年份、部件數目、是否爲全新、原價、售價(二手交易)。

html解析頁面不會使用?那就學習一下爬蟲知識吧~!若是對此不感興趣,也能夠跳過獲取數據和解析數據,這個過程,看成已知數據,繼續進行下一步。

二、創建模型

咱們已經處理好了數據集,接下來就是訓練模型。首先咱們須要添加全爲0的特徵X0列。由於線性迴歸的第一列特徵要求都是1.0。而後使用最簡單的普通線性迴歸i,編寫代碼以下:

# -*-coding:utf-8 -*-
import numpy as np
from bs4 import BeautifulSoup
def scrapePage(retX, retY, inFile, yr, numPce, origPrc):
    """ 函數說明:從頁面讀取數據,生成retX和retY列表 Parameters: retX - 數據X retY - 數據Y inFile - HTML文件 yr - 年份 numPce - 樂高部件數目 origPrc - 原價 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    # 打開並讀取HTML文件
    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):
    """ 函數說明:依次讀取六種樂高套裝的數據,並生成數據矩陣 Parameters: 無 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    scrapePage(retX, retY, './lego/lego8288.html', 2006, 800, 49.99)                #2006年的樂高8288,部件數目800,原價49.99
    scrapePage(retX, retY, './lego/lego10030.html', 2002, 3096, 269.99)                #2002年的樂高10030,部件數目3096,原價269.99
    scrapePage(retX, retY, './lego/lego10179.html', 2007, 5195, 499.99)                #2007年的樂高10179,部件數目5195,原價499.99
    scrapePage(retX, retY, './lego/lego10181.html', 2007, 3428, 199.99)                #2007年的樂高10181,部件數目3428,原價199.99
    scrapePage(retX, retY, './lego/lego10189.html', 2008, 5922, 299.99)                #2008年的樂高10189,部件數目5922,原價299.99
    scrapePage(retX, retY, './lego/lego10196.html', 2009, 3263, 249.99)                #2009年的樂高10196,部件數目3263,原價249.99
def regularize(xMat, yMat):
    """ 函數說明:數據標準化 Parameters: xMat - x數據集 yMat - y數據集 Returns: inxMat - 標準化後的x數據集 inyMat - 標準化後的y數據集 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """    
    inxMat = xMat.copy()                                                        #數據拷貝
    inyMat = yMat.copy()
    yMean = np.mean(yMat, 0)                                                    #行與行操做,求均值
    inyMat = yMat - yMean                                                        #數據減去均值
    inMeans = np.mean(inxMat, 0)                                                   #行與行操做,求均值
    inVar = np.var(inxMat, 0)                                                     #行與行操做,求方差
    # print(inxMat)
    print(inMeans)
    # print(inVar)
    inxMat = (inxMat - inMeans) / inVar                                            #數據減去均值除以方差實現標準化
    return inxMat, inyMat
def rssError(yArr,yHatArr):
    """ 函數說明:計算平方偏差 Parameters: yArr - 預測值 yHatArr - 真實值 Returns: Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    return ((yArr-yHatArr)**2).sum()
def standRegres(xArr,yArr):
    """ 函數說明:計算迴歸係數w Parameters: xArr - x數據集 yArr - y數據集 Returns: ws - 迴歸係數 Website: https://www.cuijiahua.com/ Modify: 2017-11-12 """
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    xTx = xMat.T * xMat                            #根據文中推導的公示計算迴歸係數
    if np.linalg.det(xTx) == 0.0:
        print("矩陣爲奇異矩陣,不能轉置")
        return
    ws = xTx.I * (xMat.T*yMat)
    return ws
 
def useStandRegres():
    """ 函數說明:使用簡單的線性迴歸 Parameters: 無 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-11-12 """
    lgX = []
    lgY = []
    setDataCollect(lgX, lgY)
    data_num, features_num = np.shape(lgX)
    lgX1 = np.mat(np.ones((data_num, features_num + 1)))
    lgX1[:, 1:5] = np.mat(lgX)
    ws = standRegres(lgX1, lgY)
    print('%f%+f*年份%+f*部件數量%+f*是否爲全新%+f*原價' % (ws[0],ws[1],ws[2],ws[3],ws[4]))     
 
if __name__ == '__main__':
    useStandRegres()
複製代碼

運行結果以下圖所示:

能夠看到,模型採用的公式如上圖所示。雖然這個模型對於數據擬合得很好,可是看上不沒有什麼道理。套件裏的部件數量越多,售價反而下降了,這是不合理的。

咱們使用嶺迴歸,經過交叉驗證,找到使偏差最小的λ對應的迴歸係數。編寫代碼以下:

# -*-coding:utf-8 -*-
import numpy as np
from bs4 import BeautifulSoup
import random
 
def scrapePage(retX, retY, inFile, yr, numPce, origPrc):
    """ 函數說明:從頁面讀取數據,生成retX和retY列表 Parameters: retX - 數據X retY - 數據Y inFile - HTML文件 yr - 年份 numPce - 樂高部件數目 origPrc - 原價 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    # 打開並讀取HTML文件
    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 ridgeRegres(xMat, yMat, lam = 0.2):
    """ 函數說明:嶺迴歸 Parameters: xMat - x數據集 yMat - y數據集 lam - 縮減係數 Returns: ws - 迴歸係數 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    xTx = xMat.T * xMat
    denom = xTx + np.eye(np.shape(xMat)[1]) * lam
    if np.linalg.det(denom) == 0.0:
        print("矩陣爲奇異矩陣,不能轉置")
        return
    ws = denom.I * (xMat.T * yMat)
    return ws
 
def setDataCollect(retX, retY):
    """ 函數說明:依次讀取六種樂高套裝的數據,並生成數據矩陣 Parameters: 無 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    scrapePage(retX, retY, './lego/lego8288.html', 2006, 800, 49.99)                #2006年的樂高8288,部件數目800,原價49.99
    scrapePage(retX, retY, './lego/lego10030.html', 2002, 3096, 269.99)                #2002年的樂高10030,部件數目3096,原價269.99
    scrapePage(retX, retY, './lego/lego10179.html', 2007, 5195, 499.99)                #2007年的樂高10179,部件數目5195,原價499.99
    scrapePage(retX, retY, './lego/lego10181.html', 2007, 3428, 199.99)                #2007年的樂高10181,部件數目3428,原價199.99
    scrapePage(retX, retY, './lego/lego10189.html', 2008, 5922, 299.99)                #2008年的樂高10189,部件數目5922,原價299.99
    scrapePage(retX, retY, './lego/lego10196.html', 2009, 3263, 249.99)                #2009年的樂高10196,部件數目3263,原價249.99
 
def regularize(xMat, yMat):
    """ 函數說明:數據標準化 Parameters: xMat - x數據集 yMat - y數據集 Returns: inxMat - 標準化後的x數據集 inyMat - 標準化後的y數據集 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """    
    inxMat = xMat.copy()                                                        #數據拷貝
    inyMat = yMat.copy()
    yMean = np.mean(yMat, 0)                                                    #行與行操做,求均值
    inyMat = yMat - yMean                                                        #數據減去均值
    inMeans = np.mean(inxMat, 0)                                                   #行與行操做,求均值
    inVar = np.var(inxMat, 0)                                                     #行與行操做,求方差
    # print(inxMat)
    print(inMeans)
    # print(inVar)
    inxMat = (inxMat - inMeans) / inVar                                            #數據減去均值除以方差實現標準化
    return inxMat, inyMat
 
def rssError(yArr,yHatArr):
    """ 函數說明:計算平方偏差 Parameters: yArr - 預測值 yHatArr - 真實值 Returns: Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    return ((yArr-yHatArr)**2).sum()
def standRegres(xArr,yArr):
    """ 函數說明:計算迴歸係數w Parameters: xArr - x數據集 yArr - y數據集 Returns: ws - 迴歸係數 Website: https://www.cuijiahua.com/ Modify: 2017-11-12 """
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    xTx = xMat.T * xMat                            #根據文中推導的公示計算迴歸係數
    if np.linalg.det(xTx) == 0.0:
        print("矩陣爲奇異矩陣,不能轉置")
        return
    ws = xTx.I * (xMat.T*yMat)
    return ws
 
def crossValidation(xArr, yArr, numVal = 10):
    """ 函數說明:交叉驗證嶺迴歸 Parameters: xArr - x數據集 yArr - y數據集 numVal - 交叉驗證次數 Returns: wMat - 迴歸係數矩陣 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    m = len(yArr)                                                                        #統計樣本個數 
    indexList = list(range(m))                                                            #生成索引值列表
    errorMat = np.zeros((numVal,30))                                                    #create error mat 30columns numVal rows
    for i in range(numVal):                                                                #交叉驗證numVal次
        trainX = []; trainY = []                                                        #訓練集
        testX = []; testY = []                                                            #測試集
        random.shuffle(indexList)                                                        #打亂次序
        for j in range(m):                                                                #劃分數據集:90%訓練集,10%測試集
            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個不一樣lambda下的嶺迴歸係數
        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)                        #根據ws預測y值
            errorMat[i, k] = rssError(yEst.T.A, np.array(testY))                            #統計偏差
    meanErrors = np.mean(errorMat,0)                                                    #計算每次交叉驗證的平均偏差
    minMean = float(min(meanErrors))                                                    #找到最小偏差
    bestWeights = wMat[np.nonzero(meanErrors == minMean)]                                #找到最佳迴歸係數
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    meanX = np.mean(xMat,0); varX = np.var(xMat,0)
    unReg = bestWeights / varX                                                            #數據通過標準化,所以須要還原
    print('%f%+f*年份%+f*部件數量%+f*是否爲全新%+f*原價' % ((-1 * np.sum(np.multiply(meanX,unReg)) + np.mean(yMat)), unReg[0,0], unReg[0,1], unReg[0,2], unReg[0,3]))    
 
def ridgeTest(xArr, yArr):
    """ 函數說明:嶺迴歸測試 Parameters: xMat - x數據集 yMat - y數據集 Returns: wMat - 迴歸係數矩陣 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    #數據標準化
    yMean = np.mean(yMat, axis = 0)                        #行與行操做,求均值
    yMat = yMat - yMean                                    #數據減去均值
    xMeans = np.mean(xMat, axis = 0)                    #行與行操做,求均值
    xVar = np.var(xMat, axis = 0)                        #行與行操做,求方差
    xMat = (xMat - xMeans) / xVar                        #數據減去均值除以方差實現標準化
    numTestPts = 30                                        #30個不一樣的lambda測試
    wMat = np.zeros((numTestPts, np.shape(xMat)[1]))    #初始迴歸係數矩陣
    for i in range(numTestPts):                            #改變lambda計算迴歸係數
        ws = ridgeRegres(xMat, yMat, np.exp(i - 10))    #lambda以e的指數變化,最初是一個很是小的數,
        wMat[i, :] = ws.T                                 #計算迴歸係數矩陣
    return wMat
 
if __name__ == '__main__':
    lgX = []
    lgY = []
    setDataCollect(lgX, lgY)
    crossValidation(lgX, lgY)

複製代碼

運行結果以下圖所示:

這裏隨機選取樣本,由於其隨機性,因此每次運行的結果可能略有不一樣。不過總體如上圖所示,能夠看出,它與常規的最小二乘法,即普通的線性迴歸沒有太大差別。咱們本指望找到一個更易於理解的模型,顯然沒有達到預期效果。

如今,咱們看一下在縮減過程當中迴歸係數是如何變化的。編寫代碼以下:

# -*-coding:utf-8 -*-
import numpy as np
from bs4 import BeautifulSoup
import random
 
def scrapePage(retX, retY, inFile, yr, numPce, origPrc):
    """ 函數說明:從頁面讀取數據,生成retX和retY列表 Parameters: retX - 數據X retY - 數據Y inFile - HTML文件 yr - 年份 numPce - 樂高部件數目 origPrc - 原價 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    # 打開並讀取HTML文件
    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 ridgeRegres(xMat, yMat, lam = 0.2):
    """ 函數說明:嶺迴歸 Parameters: xMat - x數據集 yMat - y數據集 lam - 縮減係數 Returns: ws - 迴歸係數 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    xTx = xMat.T * xMat
    denom = xTx + np.eye(np.shape(xMat)[1]) * lam
    if np.linalg.det(denom) == 0.0:
        print("矩陣爲奇異矩陣,不能轉置")
        return
    ws = denom.I * (xMat.T * yMat)
    return ws
def setDataCollect(retX, retY):
    """ 函數說明:依次讀取六種樂高套裝的數據,並生成數據矩陣 Parameters: 無 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    scrapePage(retX, retY, './lego/lego8288.html', 2006, 800, 49.99)                #2006年的樂高8288,部件數目800,原價49.99
    scrapePage(retX, retY, './lego/lego10030.html', 2002, 3096, 269.99)                #2002年的樂高10030,部件數目3096,原價269.99
    scrapePage(retX, retY, './lego/lego10179.html', 2007, 5195, 499.99)                #2007年的樂高10179,部件數目5195,原價499.99
    scrapePage(retX, retY, './lego/lego10181.html', 2007, 3428, 199.99)                #2007年的樂高10181,部件數目3428,原價199.99
    scrapePage(retX, retY, './lego/lego10189.html', 2008, 5922, 299.99)                #2008年的樂高10189,部件數目5922,原價299.99
    scrapePage(retX, retY, './lego/lego10196.html', 2009, 3263, 249.99)                #2009年的樂高10196,部件數目3263,原價249.99
 
def regularize(xMat, yMat):
    """ 函數說明:數據標準化 Parameters: xMat - x數據集 yMat - y數據集 Returns: inxMat - 標準化後的x數據集 inyMat - 標準化後的y數據集 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """    
    inxMat = xMat.copy()                                                        #數據拷貝
    inyMat = yMat.copy()
    yMean = np.mean(yMat, 0)                                                    #行與行操做,求均值
    inyMat = yMat - yMean                                                        #數據減去均值
    inMeans = np.mean(inxMat, 0)                                                   #行與行操做,求均值
    inVar = np.var(inxMat, 0)                                                     #行與行操做,求方差
    # print(inxMat)
    print(inMeans)
    # print(inVar)
    inxMat = (inxMat - inMeans) / inVar                                            #數據減去均值除以方差實現標準化
    return inxMat, inyMat
 
def rssError(yArr,yHatArr):
    """ 函數說明:計算平方偏差 Parameters: yArr - 預測值 yHatArr - 真實值 Returns: Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    return ((yArr-yHatArr)**2).sum()
def standRegres(xArr,yArr):
    """ 函數說明:計算迴歸係數w Parameters: xArr - x數據集 yArr - y數據集 Returns: ws - 迴歸係數 Website: https://www.cuijiahua.com/ Modify: 2017-11-12 """
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    xTx = xMat.T * xMat                            #根據文中推導的公示計算迴歸係數
    if np.linalg.det(xTx) == 0.0:
        print("矩陣爲奇異矩陣,不能轉置")
        return
    ws = xTx.I * (xMat.T*yMat)
    return ws
 
def ridgeTest(xArr, yArr):
    """ 函數說明:嶺迴歸測試 Parameters: xMat - x數據集 yMat - y數據集 Returns: wMat - 迴歸係數矩陣 Website: https://www.cuijiahua.com/ Modify: 2017-11-20 """
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    #數據標準化
    yMean = np.mean(yMat, axis = 0)                        #行與行操做,求均值
    yMat = yMat - yMean                                    #數據減去均值
    xMeans = np.mean(xMat, axis = 0)                    #行與行操做,求均值
    xVar = np.var(xMat, axis = 0)                        #行與行操做,求方差
    xMat = (xMat - xMeans) / xVar                        #數據減去均值除以方差實現標準化
    numTestPts = 30                                        #30個不一樣的lambda測試
    wMat = np.zeros((numTestPts, np.shape(xMat)[1]))    #初始迴歸係數矩陣
    for i in range(numTestPts):                            #改變lambda計算迴歸係數
        ws = ridgeRegres(xMat, yMat, np.exp(i - 10))    #lambda以e的指數變化,最初是一個很是小的數,
        wMat[i, :] = ws.T                                 #計算迴歸係數矩陣
    return wMat
 
if __name__ == '__main__':
    lgX = []
    lgY = []
    setDataCollect(lgX, lgY)
    print(ridgeTest(lgX, lgY))

複製代碼

運行結果以下圖所示:

看運行結果的第一行,能夠看到最大的是第4項,第二大的是第2項。

所以,若是隻選擇一個特徵來作預測的話,咱們應該選擇第4個特徵,也就是原始加個。若是能夠選擇2個特徵的話,應該選擇第4個和第2個特徵。

這種分析方法使得咱們能夠挖掘大量數據的內在規律。在僅有4個特徵時,該方法的效果也許並不明顯;但若是有100個以上的特徵,該方法就會變得十分有效:它能夠指出哪一個特徵是關鍵的,而哪些特徵是不重要的。

5、使用Sklearn的linear_model

老規矩,最後讓咱們用sklearn實現下嶺迴歸吧。

官方英文文檔地址:[點擊查看](cuijiahua.com/blog/tag/sk…

sklearn.linear_model提供了不少線性模型,包括嶺迴歸、貝葉斯迴歸、Lasso等。本文主要講解嶺迴歸Ridge。

一、Ridge

讓咱們先看下Ridge這個函數,一共有8個參數:

參數說明以下:

  • alpha:正則化係數,float類型,默認爲1.0。正則化改善了問題的條件並減小了估計的方差。較大的值指定較強的正則化。 fit_intercept:是否須要截距,bool類型,默認爲True。也就是是否求解b。
  • normalize:是否先進行歸一化,bool類型,默認爲False。若是爲真,則迴歸X將在迴歸以前被歸一化。 當fit_intercept設置爲False時,將忽略此參數。 當迴歸量歸一化時,注意到這使得超參數學習更加魯棒,而且幾乎不依賴於樣本的數量。 相同的屬性對標準化數據無效。然而,若是你想標準化,請在調用normalize = False訓練估計器以前,使用preprocessing.StandardScaler處理數據。
  • copy_X:是否複製X數組,bool類型,默認爲True,若是爲True,將複製X數組; 不然,它覆蓋原數組X。
  • max_iter:最大的迭代次數,int類型,默認爲None,最大的迭代次數,對於sparse_cg和lsqr而言,默認次數取決於scipy.sparse.linalg,對於sag而言,則默認爲1000次。
  • tol:精度,float類型,默認爲0.001。就是解的精度。
  • solver:求解方法,str類型,默認爲auto。可選參數爲:auto、svd、cholesky、lsqr、sparse_cg、sag。
    • auto根據數據類型自動選擇求解器。
    • svd使用X的奇異值分解來計算Ridge係數。對於奇異矩陣比cholesky更穩定。
    • cholesky使用標準的scipy.linalg.solve函數來得到閉合形式的解。
    • sparse_cg使用在scipy.sparse.linalg.cg中找到的共軛梯度求解器。做爲迭代算法,這個求解器比大規模數據(設置tol和max_iter的可能性)的cholesky更合適。
    • lsqr使用專用的正則化最小二乘常數scipy.sparse.linalg.lsqr。它是最快的,但可能在舊的scipy版本不可用。它是使用迭代過程。
    • sag使用隨機平均梯度降低。它也使用迭代過程,而且當n_samples和n_feature都很大時,一般比其餘求解器更快。注意,sag快速收斂僅在具備近似相同尺度的特徵上被保證。您可使用sklearn.preprocessing的縮放器預處理數據。
  • random_state:sag的僞隨機種子。 以上就是全部的初始化參數,固然,初始化後還能夠經過set_params方法從新進行設定。

知道了這些,接下來就能夠編寫代碼了:

# -*-coding:utf-8 -*-
import numpy as np
from bs4 import BeautifulSoup
import random
 
def scrapePage(retX, retY, inFile, yr, numPce, origPrc):
    """ 函數說明:從頁面讀取數據,生成retX和retY列表 Parameters: retX - 數據X retY - 數據Y inFile - HTML文件 yr - 年份 numPce - 樂高部件數目 origPrc - 原價 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    # 打開並讀取HTML文件
    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):
    """ 函數說明:依次讀取六種樂高套裝的數據,並生成數據矩陣 Parameters: 無 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-03 """
    scrapePage(retX, retY, './lego/lego8288.html', 2006, 800, 49.99)                #2006年的樂高8288,部件數目800,原價49.99
    scrapePage(retX, retY, './lego/lego10030.html', 2002, 3096, 269.99)                #2002年的樂高10030,部件數目3096,原價269.99
    scrapePage(retX, retY, './lego/lego10179.html', 2007, 5195, 499.99)                #2007年的樂高10179,部件數目5195,原價499.99
    scrapePage(retX, retY, './lego/lego10181.html', 2007, 3428, 199.99)                #2007年的樂高10181,部件數目3428,原價199.99
    scrapePage(retX, retY, './lego/lego10189.html', 2008, 5922, 299.99)                #2008年的樂高10189,部件數目5922,原價299.99
    scrapePage(retX, retY, './lego/lego10196.html', 2009, 3263, 249.99)                #2009年的樂高10196,部件數目3263,原價249.99
 
def usesklearn():
    """ 函數說明:使用sklearn Parameters: 無 Returns: 無 Website: https://www.cuijiahua.com/ Modify: 2017-12-08 """
    from sklearn import linear_model
    reg = linear_model.Ridge(alpha = .5)
    lgX = []
    lgY = []
    setDataCollect(lgX, lgY)
    reg.fit(lgX, lgY)
    print('%f%+f*年份%+f*部件數量%+f*是否爲全新%+f*原價' % (reg.intercept_, reg.coef_[0], reg.coef_[1], reg.coef_[2], reg.coef_[3]))    
 
if __name__ == '__main__':
    usesklearn()

複製代碼

運行結果以下圖所示:

咱們不搞太複雜,正則化項係數設爲0.5,其他參數使用默認便可。能夠看到,得到的結果與上小結的結果相似。

6、總結

與分類同樣,迴歸也是預測目標值的過程。迴歸與分類的不一樣點在於,前者預測連續類型變量,然後者預測離散類型變量。 嶺迴歸是縮減法的一種,至關於對迴歸係數的大小施加了限制。另外一種很好的縮減法是lasso。lasso難以求解,但可使用計算簡便的逐步線性迴歸方法求的近似解。 縮減法還能夠看作是對一個模型增長誤差的同時減小方法。 下篇文章講解樹迴歸。

PS: 若是以爲本篇本章對您有所幫助,歡迎關注、評論、贊!

本文出現的全部代碼和數據集,都可在個人github上下載,歡迎Follow、Star:點擊查看

參考文獻:

《機器學習實戰》的第五章內容。


相關文章和視頻推薦

圓方圓學院聚集 Python + AI 名師,打造精品的 Python + AI 技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。 公開課地址: ke.qq.com/course/3627…

相關文章
相關標籤/搜索