手擼機器學習算法 - 嶺迴歸

系列文章目錄:

算法介紹

今天我們來一起學習一個除了線性迴歸多項式迴歸外最最最簡單的迴歸算法:嶺迴歸,如果用等式來介紹嶺迴歸,那麼就是:\(嶺迴歸 = 多項式迴歸 + 懲罰項\)\(多項式迴歸 = 線性迴歸 + 多項式特徵構建\),從上述等式可以看到,所謂學習嶺迴歸,只需要學習多項式和懲罰項即可,由於之前我們已經學習過多項式迴歸了,因此現在的重點是懲罰項或者叫正則項

從多項式迴歸到嶺迴歸

嶺迴歸是在多項式迴歸的基礎上增加了懲罰項,準確的說法是:在多項式迴歸的優化函數上增加了約束條件用於限制算法的假設空間,以應對模型的過擬合問題,下面我們分別看看如何增加約束條件爲什麼可以防止過擬合約束條件對推導的影響

算法推導

既然嶺迴歸是在多項式迴歸的基礎上實現的,那麼我們就以一個二元二次多項式迴歸爲例子:
\(w_0*x_0^2+w_1*x_1^2+w_2*x_0*x_1+w_3*x_0+w_4*x_1+b\)
假設現在通過上述模型擬合數據顯示過擬合,一般的做法是將模型從二階降低到一階(降階可以減少特徵數),則模型變爲:
\(w_3*x_0+w_4*x_1+b\)
這個降階的方式可以爲手動指定w0w1w2爲0來實現,對於多項式迴歸來說,它唯一控制模型複雜度的就是階數,階數越大,特徵越多,模型越複雜,反之則越簡單,但是這種控制方法難免顯得不夠靈活平滑,如果我們期望更平滑的降低複雜度的方法呢,這時就需要通過懲罰項來實現;

如何增加約束條件

增加約束的方式也很簡單,從公式上看就是增加了服從條件,如下對條件W增加的約束,使得W的可取範圍爲半徑爲r的圓內:

\[y = w_0*x + w_1*b \\ s.t. ||W||^2 < r^2, W=(w_0, w_1) \]

爲什麼可以防止過擬合

對於上述約束,我們可以這樣理解它,在沒有加約束之前W=(w0 w1)的所有取值爲整個二維平面上的點,而\(||W||^2 < r^2\)W限制在原點爲中心,半徑爲r的圓內,由於它減少了W的可取範圍,因此起到了降低算法的假設空間(或者說是算法複雜度)的效果,也就可以作爲一個有效的懲罰項;

約束條件下的公式推導

首先我們回顧下線性迴歸的公式推導,首先優化目標如下:

\[argmin \frac{1}{N}\sum_{i=1}^{N}(w*x_i+b-y_i)^2 \]

在嶺迴歸中,優化目標增加了約束條件,如下:

\[argmin \frac{1}{N}\sum_{i=1}^{N}(w*x_i+b-y_i)^2 \\ s.t. ||W||^2 < r^2, W=(w_0, w_1) \]

通過拉格朗日將約束條件轉爲函數的一部分,通過添加拉格朗日乘子,注意下述公式的λ作爲超參數,因此看作常量,如下:

\[argmin \frac{1}{N}\sum_{i=1}^{N}(Wx_i-y_i)^2 + λ(||W||^2-r^2) \]

對上述公式針對W求導並令其爲零有:

\[\frac{1}{N}N(2x_i^Tx_iW-2x_i^Ty_i+0)+2λW = 0 \\ 2x_i^Tx_iW + 2λW = 2x_i^Ty_i \\ (x_i^Tx_i+λI)W = x_i^Ty_i, I爲單位陣 \\ W = (x_i^Tx_i+λI)^{-1}x_i^Ty_i \]

代碼實現

嶺迴歸對象初始化

可以看到,當嶺迴歸的拉格朗日乘子λ爲0時,嶺迴歸退化爲多項式迴歸:

if self.lambdaVal == 0:
    return super(RidgeRegression,self).train()

參數W計算

xTx = self.X.T @ self.X
I = np.eye(xTx.shape[0])
self.w = np.linalg.inv(xTx + self.lambdaVal*I) @ self.X.T @ self.y
self.w = self.w.reshape(-1)
self.w,self.b = self.w[1:],self.w[0]

運行結果

下面使用5階多項式迴歸來觀察看使用懲罰項與不使用的區別,可以很容易的看到,由於懲罰項參數λ的存在,使得同樣爲5階的模型,懲罰係數越大,模型越趨於簡單;

不使用懲罰項

分別使用λ=0.1、1、10作爲懲罰係數

全部代碼

import numpy as np
import matplotlib.pyplot as plt
from 線性迴歸最小二乘法矩陣實現 import LinearRegression as LR
from 多項式迴歸 import PolynomialRegression as PR

'''
懲罰項:亦稱爲罰項、正則項,用於限制模型複雜度,在公式上可看到是增加了某個約束條件,即:subject to xxxx;

以多項式迴歸理解懲罰項:
對於二元二次多項式迴歸,所有假設空間可能爲:w0*x0^2+w1*x1^2+w2*x0*x1+w3*x0+w4*x1+b
當二階多項式過擬合時,通常考慮退回到一階,即線性迴歸,假設空間爲:w0*x0+w1*x1+b
這種退化可以看到是對二階多項式增加了約束條件:w0=0,w1=0,w2=0
因此對於多項式迴歸,任意低階都可以看作是其高階+懲罰項的組合結果

懲罰項的意義:通過對公式增加靈活的約束條件,可以更平滑的控制模型複雜度,只要約束條件是有意義的,那麼它就降低了原假設空間的大小,例如對於線性迴歸w0*x0+b,W=(w0 w1),即W的可取範圍爲整個二維平面,如果增加約束條件w0^2+w1^2<r^2,則W的取值範圍爲二維平面上以r爲半徑的圓內,而W決定了線性迴歸的假設空間大小,因此通過約束條件得以降低假設空間大小的目的;

嶺迴歸 = 線性迴歸 + 優化目標(argmin MSE)上增加約束條件(s.t. ||w||^2<=r^2)
'''

class RidgeRegression(PR):
    def __init__(self,X,y,degrees=1,lambdaVal=0):
        super(RidgeRegression,self).__init__(X,y,degrees)
        self.lambdaVal = lambdaVal

    def train(self):
        if self.lambdaVal == 0:
            return super(RidgeRegression,self).train()
        xTx = self.X.T @ self.X
        I = np.eye(xTx.shape[0])
        self.w = np.linalg.inv(xTx + self.lambdaVal*I) @ self.X.T @ self.y
        self.w = self.w.reshape(-1)
        self.w,self.b = self.w[1:],self.w[0]
        return self.w,self.b

def pain(pos=141,xlabel='x',ylabel='y',title='',x=[],y=[],line_x=[],line_y=[]):
    plt.subplot(pos)
    plt.title(title)
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    plt.scatter(x,y)
    plt.plot(line_x,line_y)

if __name__ == '__main__':
    rnd = np.random.RandomState(3)
    x_min, x_max = 0, 10
    
    def pain(pos=141,xlabel='x',ylabel='y',title='',x=[],y=[],line_x=[],line_y=[]):
        plt.subplot(pos)
        plt.title(title)
        plt.xlabel(xlabel)
        plt.ylabel(ylabel)
        plt.scatter(x,y)
        plt.plot(line_x,line_y)
    
    # 上帝函數 y=f(x)
    def f(x):
        return x**5-22*x**4+161*x**3-403*x**2+36*x+938
    
    # 上帝分佈 P(Y|X)
    def P(X):
        return f(X) + rnd.normal(scale=30, size=X.shape)
    
    # 通過 P(X, Y) 生成數據集 D
    X = rnd.uniform(x_min, x_max, 10)   # 通過均勻分佈產生 X
    y = P(X)                            # 通過 P(Y|X) 產生 y

    X,y = X.reshape(-1,1),y.reshape(-1,1)
    x_min,x_max = min(X),max(X)


    for pos,deg in zip([331,332,333],[2,5,10]):
        model = PR(X=X,y=y,degrees=deg)
        w,b = model.train()
        print(f'最小二乘法的矩陣方式結果爲:w={w} b={b}')
        line_x = [x_min+(x_max-x_min)*(i/100) for i in range(-1,102,1)]
        line_y = [model.predict(x) for x in line_x]
        pain(pos,'X','y','DEG='+str(deg),X[:,0],y[:,0],line_x,line_y)
    for pos,deg,lambdaVal in zip([334,335,336],[5,5,5],[0.1,1,10]):
        model = RidgeRegression(X=X,y=y,degrees=deg,lambdaVal=lambdaVal)
        w,b = model.train()
        print(f'最小二乘法的矩陣方式結果爲:w={w} b={b}')
        line_x = [x_min+(x_max-x_min)*(i/100) for i in range(-1,102,1)]
        line_y = [model.predict(x) for x in line_x]
        pain(pos,'X','y','DEG='+str(deg)+', λ='+str(lambdaVal),X[:,0],y[:,0],line_x,line_y)
    for pos,deg,lambdaVal in zip([337,338,339],[10,10,10],[0.1,1,10]):
        model = RidgeRegression(X=X,y=y,degrees=deg,lambdaVal=lambdaVal)
        w,b = model.train()
        print(f'最小二乘法的矩陣方式結果爲:w={w} b={b}')
        line_x = [x_min+(x_max-x_min)*(i/100) for i in range(-1,102,1)]
        line_y = [model.predict(x) for x in line_x]
        pain(pos,'X','y','DEG='+str(deg)+', λ='+str(lambdaVal),X[:,0],y[:,0],line_x,line_y)
    
    plt.show()

最後

相對於多項式迴歸,由於嶺迴歸有懲罰項的存在,因此它可以更加肆無忌憚的使用更高階的多項式而不需要太擔心過擬合的問題,因此理論上多項式迴歸能做到的,嶺迴歸可以做的更好,當然了由於參數λ的存在,嶺迴歸需要調的參數也更多了;

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