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

JesusBilly發表於2021-06-18

系列文章目錄:

演算法介紹

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

從多項式迴歸到嶺迴歸

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

演算法推導

既然嶺迴歸是在多項式迴歸的基礎上實現的,那麼我們就以一個二元二次多項式迴歸為例子:
\(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()

最後

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

相關文章