機器學習推導+python實現(一):線性迴歸

寫在開頭:這個系列的靈感已經整個系列的思路會根據公衆號機器學習實驗室的節奏進行,相當於做一個自己的理解版本,並且按照以往慣例我們會增加一些問題來對小細節進行討論。

內容安排

筆者覺得如果單單的去調用sklearn庫的機器學習的方法有些不妥,這個系列本應該在去年就開始了,但一直拖着沒有更新。所以從今天開始我們一起來探究機器學習的樂趣吧。這個系列開始後,我們還會增加很多細節上的思考問題的討論系列。
根據公衆號機器學習實驗室的節奏安排我們預計會涉及以下幾個內容的實現:線性迴歸(一)、邏輯迴歸(二)、K近鄰(三)、決策樹值ID3(四)、CART(五)、感知機(六)、神經網絡(七)、線性可分支持向量機(八)、線性支持向量機(九)、線性不可分支持向量機(十)、樸素貝葉斯(十一)、Lasso迴歸(十二)、Ridge嶺迴歸(十三)。
相信通過這樣的推導我們應該可以更好的去理解和應用及其學習的算法。

1.線性迴歸的數學推導

這裏筆者爲了節約編寫的時間對於公式的推導我們會從“西瓜書”、《統計學習方法》或者其他資料中進行復制,並在需要補充的地方進行公式擴充。根據上幾個深度學習的系列,我們可以進行概念的相互理解。
迴歸模型(得分函數)
線性迴歸的形式如下,
f(xi)=wxi+b,使f(xi)yi f(x_i) = wx_i+b,使得f(x_i)\simeq y_i 也就是企圖通過變量的線性關係來對結果進行推測,希望儘可能得到本來的結果,也就是個預測問題。那麼我們有了得分函數後應當怎麼樣去計算誤差呢?
均方誤差(損失函數)
這裏我們就使用均方誤差來度量回歸模型的性能,我們希望求得w和b使得均方誤差最小,視圖尋找到一條是直線使得到所有樣本的歐氏距離最小
(w,b)=arg min(w,b) i=1m(f(xi)yi)2 (w^*,b^*)={\underset {(w,b)}{\operatorname {arg\,min} }}\ \sum\limits_{i=1}^{m}(f(x_i)-y_i)^2 (w,b)=arg min(w,b) i=1m(yiwxib)2 (w^*,b^*)={\underset {(w,b)}{\operatorname {arg\,min} }}\ \sum\limits_{i=1}^{m}(y_i-wx_i-b)^2
最小二乘估計
對於線性迴歸模型我們不用太着急使用梯度的算法進行更新,我們這裏使用函數推導的形式進行計算,計算最優值,我們的首先計算其偏導等於0的點,
E(w,b)w=2i=1m(yiwxib)(xi)=2(wi=1mxi2i=1m(yib)xi)=0 \frac{\partial E_{(w,b)}}{\partial w}=2\sum\limits_{i=1}^{m}(y_i-wx_i-b)(-x_i)=2(w\sum\limits_{i=1}^{m}x_i^2-\sum\limits_{i=1}^{m}(y_i-b)x_i)=0 E(w,b)b=2i=1m(yiwxib)(1)=2(mbi=1m(yiwxi))=0 \frac{\partial E_{(w,b)}}{\partial b}=2\sum\limits_{i=1}^{m}(y_i-wx_i-b)(-1)=2(mb-\sum\limits_{i=1}^{m}(y_i-wx_i))=0
分別令上面兩個式子等於0通過變形就可以得到兩個式子的最優解,這裏注意可以先算第二個式子然後反帶回第一個式子,那麼對第二個式子移項得到,
b=1mi=1m(yiwxi)=yˉwxˉ b=\frac{1}{m}\sum\limits_{i=1}^{m}(y_i-wx_i)=\bar y-w\bar x 將這個式子代入第一個式子對w
進行求解得到,
(wi=1mxi2i=1m(yi(yˉ+wxˉ)xi)=0 (w\sum\limits_{i=1}^{m}x_i^2-\sum\limits_{i=1}^{m}(y_i-(\bar y+w\bar x)x_i)=0 打開括號就得到,
w(i=1mxi2i=1mxˉxi)i=1m(yiyˉ)xi=0 w(\sum\limits_{i=1}^{m}x_i^2-\sum\limits_{i=1}^{m}\bar xx_i)-\sum\limits_{i=1}^{m}(y_i-\bar y)x_i=0 w(i=1mxi21mi=1mi=1mxixi)i=1m(yi1mi=1myi)xi=0 w(\sum\limits_{i=1}^{m}x_i^2-\frac{1}{m}\sum\limits_{i=1}^{m}\sum\limits_{i=1}^{m}x_ix_i)-\sum\limits_{i=1}^{m}(y_i-\frac{1}{m}\sum\limits_{i=1}^{m}y_i)x_i=0 w(i=1mxi21m(i=1mxi)2)i=1m(xi1mi=1mxi)yi=0 w(\sum\limits_{i=1}^{m}x_i^2-\frac{1}{m}(\sum\limits_{i=1}^{m}x_i)^2)-\sum\limits_{i=1}^{m}(x_i-\frac{1}{m}\sum\limits_{i=1}^{m}x_i)y_i=0
通過移項並且將y的均值和x的均值進行展開再合併的技巧得到w的最優解,
w=i=1myi(xixˉ)i=1mxi21m(i=1mxi)2 w=\frac{\sum\limits_{i=1}^{m}y_i(x_i-\bar x)}{\sum\limits_{i=1}^{m}x_i^2-\frac{1}{m}(\sum\limits_{i=1}^{m}x_i)^2} b=yˉwxˉ b=\bar y-w\bar x 這樣我們就得到了w和b的最優解。
多元迴歸形式
下面讓我們將回歸從一元往多元進行擴展,
f(xi)=wTxi+b,使f(xi)yi f(x_i) = w^Tx_i+b,使得f(x_i)\simeq y_i 首先X就從一元變爲了多元,並且包含常數項,
X=[x11x12...x1d1x21x22...x2d1xm1xm2...xmd1]=[x1T1x2T1xmT1] X=\begin{bmatrix} x_{11} & x_{12} &...&x_{1d}&1 \\ x_{21} & x_{22} &...&x_{2d}&1 \\\vdots &\vdots&\ddots&\vdots&\vdots \\ x_{m1} & x_{m2} &...&x_{md}&1 \end{bmatrix}\quad= \begin{bmatrix} x_{1}^T &1 \\ x_{2}^T&1 \\\vdots&\vdots \\ x_{m}^T&1 \end{bmatrix}\quad 然後w的最優解的最小二乘就可以寫成,
w^=arg minw^(yXw^)T(yXw^) \hat w^*={\underset {\hat w}{\operatorname {arg\,min} }}(y-X\hat w)^T(y-X\hat w) Ew^=(yXw^)T(yXw^)E_{\hat w}=(y-X\hat w)^T(y-X\hat w)然後對w^\hat w求導,
Ew^w^=(yXw^)T(yXw^)w^=(yTyyTXw^(Xw^)Ty+(Xw^)TXw^)w^ \frac{\partial E_{\hat w}}{\partial \hat w}=\frac{\partial (y-X\hat w)^T(y-X\hat w)}{\partial \hat w}=\frac{\partial(y^Ty-y^TX\hat w-(X\hat w)^Ty+(X\hat w)^TX\hat w)}{\partial \hat w} 這裏的yTXw^(Xw^)Ty=2(Xw^)Ty-y^TX\hat w-(X\hat w)^Ty=-2(X\hat w)^Ty於是得到, Ew^w^=2XTy+XTXw^=2XT(Xw^y) \frac{\partial E_{\hat w}}{\partial \hat w}=-2X^Ty+X^TX\hat w=2X^T(X\hat w-y) 然後令導數等於0,這時不能直接刪去XTX^T,通過移項求逆得到,
w^=(XTX)1XTy \hat w^*=(X^TX)^{-1}X^Ty
於是以上就是對於線性迴歸模型的參數估計過程。

2.線性迴歸的python實現

在計算迴歸函數,我們重點是在其參數的更新上,如果數據量過大,那麼我們傳統的最小二乘法計算矩陣的逆就很複雜,還有可能矩陣不可逆無法計算,因此可以通過梯度下降的方法來尋找最優的參數。
首先我們定義一下回歸模型的主體,需要定義得分函數、損失函數以及參數的偏導數,

import numpy as np
def linearRegression(X, y, W, b):
    numTrain = X.shape[0]
    numFeatures = X.shape[1]
    
    #得分函數編寫
    yHat = np.dot(X, W) + b
    
    #損失函數編寫
    loss = np.sum(np.square(yHat - y)) / numTrain
    
    #梯度的編寫
    dW = 2 * np.dot(X.T, (yHat - y))/ numTrain
    db = 2 * np.sum(yHat - y)/ numTrain
    
    return yHat, loss, dW, db

然後定義初始參數函數,對於初始化這一塊也需要最近學一下,看看有沒有更好的初始化辦法

def initialParams(d):
    W = np.zeros((d, 1)) + 0.01
    b = 0.01
    return W, b

接着定義訓練函數,並輸出累計損失以及參數,

def linearTrain(X, y, learningRate = 0.001, numIters = 1000000):
    W, b = initialParams(X.shape[1])
    lossHistory = []
    
    for it in range(1, numIters):
        yHat, loss, dW, db = linearRegression(X, y, W, b)
        
        lossHistory.append(loss)
        W += -learningRate * dW
        b += -learningRate * db
        
        if it % 100000 == 0:
            print("%d numIters loss is %f"%(it, loss))
            
        params = {
            'W': W,
            'b': b
        }
        
        grads = {
            'dW': dW,
            'db': db
        }
        
    return lossHistory, params, grads

緊接着定義預測函數,

def linearPredict(X, params):
    W, b = params['W'], params['b']
    yPred = np.dot(X, W) + b
    return yPred

然後我們函數就定義完了,下面我們來加載數據,並劃分訓練集與測試集,這裏使用sklearn自帶數據

from sklearn.datasets import load_diabetes
from sklearn.utils import shuffle

diabetes = load_diabetes()
X = diabetes.data
y = diabetes.target

offset = int(X.shape[0] * 0.9)
X_train, y_train = X[:offset], y[:offset]
X_test, y_test = X[offset:], y[offset:]
y_train = y_train.reshape((-1, 1))
y_test = y_test.reshape((-1, 1))

print('Train data shape: ', X_train.shape)
print('Train labels shape: ', y_train.shape)
print('Test data shape: ', X_test.shape)
print('Test labels shape: ', y_test.shape)
Train data shape:  (397, 10)
Train labels shape:  (397, 1)
Test data shape:  (45, 10)
Test labels shape:  (45, 1)

下面來訓練一下我們的線性迴歸模型吧,

lossHistory, params, grads = linearTrain(X_train, y_train)
100000 numIters loss is 3502.374208
200000 numIters loss is 3198.313524
300000 numIters loss is 3091.734345
400000 numIters loss is 3047.314501
500000 numIters loss is 3028.072201
600000 numIters loss is 3019.457399
700000 numIters loss is 3015.441710
800000 numIters loss is 3013.466611
900000 numIters loss is 3012.422030

我們來畫個圖看一下損失loss的變化情況,

import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(lossHistory)
plt.xlabel('numIters')
plt.ylabel('loss')
plt.show()

在這裏插入圖片描述
從圖上可以看到在可能很少的迭代損失函數就急速的降爲了比較小的值。下面進行預測,計算其樣本平均誤差,並繪製圖像,

yPred = linearPredict(X_test)
print("predict average loss is %f"%(np.sum(yPred - y_test)/X_test.shape[0]))

plt.scatter(range(X_test.shape[0]), y_test, color = "lightblue")
plt.plot(yPred, color = "#ffcfdc")
plt.xlabel('X')
plt.ylabel('y')
plt.show()
predict average loss is 2.185061

在這裏插入圖片描述
總體來說還是可以效果。
下面我們用class簡單的封裝一下這個代碼,並且編寫了一個交叉驗證的函數,這個地方是借鑑機器學習實驗室中的封裝技術,這個封裝還需要多學習,

import numpy as np
from sklearn.utils import shuffle
from sklearn.datasets import load_diabetes
class LrModel():
    def __init__(self):
        pass
    
    def prepareData(self):
        X = load_diabetes().data
        y = load_diabetes().target
        y = y.reshape(-1, 1)
        data = np.concatenate((X, y), axis=1)
        return data
    
    def linearPredict(self, X):
        W, b = params['W'], params['b']
        yPred = np.dot(X, W) + b
        return yPred
    def initialParams(self, d):
        W = np.zeros((d, 1)) + 0.01
        b = 0.01
        return W, b
    def linearRegression(self, X, y, W, b):
        numTrain = X.shape[0]
        numFeatures = X.shape[1]
        yHat = np.dot(X, W) + b
        loss = np.sum(np.square(yHat - y)) / numTrain
        dW = 2 * np.dot(X.T, (yHat - y))/ numTrain
        db = 2 * np.sum(yHat - y)/ numTrain
        return yHat, loss, dW, db
    
    def linearTrain(self, X, y, learningRate = 0.001, numIters = 1000000):
        W, b = self.initialParams(X.shape[1])
        for it in range(1, numIters):
            yHat, loss, dW, db = self.linearRegression(X, y, W, b)
            W += -learningRate * dW
            b += -learningRate * db
            if it % 100000 == 0:
                print("%d numIters loss is %f"%(it, loss))
            params = {
                'W': W,
                'b': b
            }  
            grads = {
                'dW': dW,
                'db': db
            }  
        return loss, params, grads
    
    def linearPredict(self, X, params):
        W, b = params['W'], params['b']
        yPred = np.dot(X, W) + b
        return yPred
    
    def linearCrossValidation(self, data, k, randomize=True):
        if randomize:
            data = list(data)
            shuffle(data)
        
        slices = [data[i::k] for i in range(k)] 
        for i in range(k):
            validation = slices[i]
            train = [data
                    for s in slices if s is not validation for data in s]
            train = np.array(train)
            validation = np.array(validation)
            yield train, validation

if __name__=='__main__':
    lr = LrModel()
    data = lr.prepareData()
    
    for train, validation in lr.linearCrossValidation(data, 5):
        X_train = train[:, :10]
        y_train = train[:, -1].reshape((-1, 1))
        X_valid = validation[:, :10]
        y_valid = validation[:, -1].reshape((-1, 1))
        loss5 =[]
        loss, params, grads = lr.linearTrain(X_train, y_train)
        loss5.append(loss)
        score = np.mean(loss5)
        print('five kold cross validation score is', score)
        y_pred = lr.linearPredict(X_valid, params)
        valid_score = np.sum(((y_pred - y_valid) ** 2)) / len(X_valid)
        print('valid score is', valid_score)

這個封裝函數需要注意的是在交叉驗證中使用的yeild函數,能夠保住多次循環儲存的時候在上一次基礎上進行儲存。


結語
好了,今天的關於手推線性迴歸+python實現的部分就到這裏了,後面會不斷更新之後的內容。
謝謝閱讀。
參考
公衆號:機器學習實驗室

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章