动态

详情 返回 返回

彩筆運維勇闖機器學習--擬合 - 动态 详情

前言

今天我們來討論擬合的問題

在之前的篇幅,主要討論的是線性迴歸的問題,不管是一元、多元、多項式,本質都是線性迴歸問題。線性迴歸在機器學習中屬於“監督學習”,也就是使用已有的、預定義的“訓練數據”集合,訓練系統,在解釋未知數據時,也能夠很好的解釋

而模型訓練完成之後,可能會有3中狀態:“欠擬合”、“最佳適配”、“過擬合”。本小節就來消息討論一下,怎麼判斷訓練出來的模型處於什麼樣的狀態

過擬合

老規矩,先運行起來,再探索原理

import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score

np.random.seed(0)
X = np.linspace(0, 1, 30)
y_true = np.sin(2 * np.pi * X)
y = y_true + np.random.normal(0, 0.2, X.shape)

X_train, X_test, y_train, y_test = train_test_split(X.reshape(-1, 1), y, test_size=0.3)

degree = 10
model = Pipeline([
    ('poly', PolynomialFeatures(degree=degree)),
    ('line', LinearRegression())
])
model.fit(X_train, y_train)

y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

mse_train = mean_squared_error(y_train, y_train_pred)
r2_train = r2_score(y_train, y_train_pred)
mse_test = mean_squared_error(y_test, y_test_pred)
r2_test = r2_score(y_test, y_test_pred)

print(f"訓練集 MSE: {mse_train:.4f} ,R²:{r2_train}")
print(f"驗證集 MSE: {mse_test:.4f} ,R²:{r2_test}")

數據是由sin函數加上一些噪點組成的,按照37比例分成訓練集與測試集。而模型則是最高階為10的多項式

腳本!啓動:
watermarked-fit_regression_1_1

在訓練數據上表現不錯,但是在測試數據上表現就不行了,誤差明顯上升,調整係數R²也下降了,這就是所謂的過擬合現象

交叉驗證

從上面看到,將訓練數據手動劃分為兩部分,訓練集與測試集,通過測試集,就發現了模型的過擬合現象。那將訓練數據多次劃分,並且重複訓練與驗證,就能有更大的概率提前發現模型過擬合情況。當然,手動做這個工作耗時耗力,而本小節要討論的交叉驗證就是為了完成這個工作的

留出法

這在之前的演示中已經給出來了,就是主動劃分訓練集與測試集,通過random_state來決定每次劃分的集合不同

  • 優點:簡單易用
  • 缺點:結果受單次劃分影響大,尤其在小數據集中波動性高
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

k折交叉驗證

將數據集均等分為k個子集(k通常取5或10),依次選取第i個子集作為驗證集,其餘k-1個子集作為訓練集,重複k次,每次計算模型性能指標(如準確率、F1值等),最終結果為k次驗證的平均值。本質上就是多次計算去平均值

  • 優點:降低數據劃分的隨機性,結果更穩定
  • 缺點:計算成本較高(需訓練k次模型)
kf = KFold(n_splits=5, shuffle=True, random_state=0)
from sklearn.model_selection import KFold, cross_val_score

kf = KFold(n_splits=5, shuffle=True, random_state=0)

neg_mse_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=kf, scoring='neg_mean_squared_error')
mse_scores = -neg_mse_scores
print("5折MSE:{}".format(np.round(mse_scores, 2)))
print("平均MSE:{} \n".format(round(np.mean(mse_scores), 2)))

r2_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=kf, scoring="r2")
print("5折R²分數:{}".format(r2_scores))
print("平均R²:{}\n".format(r2_scores.mean()))

這裏也可以使用cross_validate獲取多個指標

腳本!啓動:

watermarked-fit_regression_1_2

由於k折交叉驗證非常常用,可以適應大部分情況,這裏給出第二種寫法,靈活使用

mse_list = []
r2_list = []

for train_index, test_index in kf.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

    model.fit(X_train.reshape(-1, 1), y_train.reshape(-1, 1))
    y_pred = model.predict(X_test.reshape(-1, 1))

    mse = mean_squared_error(y_test.reshape(-1, 1), y_pred)
    r2 = r2_score(y_test.reshape(-1, 1), y_pred)

    mse_list.append(mse)
    r2_list.append(r2)

print("5折MSE:{}".format(np.round(mse_list, 2)))
print("平均MSE:{} \n".format(round(np.mean(mse_list), 2)))

print("5折R²分數:{}".format(np.round(r2_list, 2)))
print("平均R²:{}\n".format(round(np.mean(r2_list), 4)))

第二種寫法更是解釋了,k折交叉驗證本質就是自動劃分訓練集與測試集,然後再去進行模型訓練

綜上所述,在某個定義域內(0~1),10階多項式去解釋sin函數(加入噪點),平均mse是0.25,平均R²是0.56,模型泛化能力是非常差的

最後補充一點:

n_splits這個參數不但控制了折數,還控制了訓練集與測試集的比例,比如n_splits=5,每次用 4/5 的數據做訓練集,1/5 做測試集;比如n_splits=3,每次用 2/3 的數據做訓練集,1/3 做測試集

留一交叉驗證

k折交叉是按照比例,按照折數(通常5折或10折),對訓練集與測試集進行“比例”劃分,由n_splits參數控制。而留一交叉每次只會選擇1個樣本作為測試機,其餘的為訓練集,然後遍歷整個樣本進行訓練

舉個例子,如果樣本數為[1,2,3,4,5]

訓練集 測試集
[1,2,3,4] [5]
[1,2,3,5] [4]
[1,2,4,5] [3]
[1,2,3,5] [2]
[2,3,4,5] [1]

如果樣本量很小的情況,那留一交叉驗證就非常適合,因為每個樣本都被用作驗證集一次,不浪費任何一個數據點。但是一旦樣本數量變多,那訓練的速度就會非常慢

from sklearn.model_selection import LeaveOneOut, cross_val_score

loo = LeaveOneOut()
neg_mse_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=loo, scoring='neg_mean_squared_error')
mse_scores = -neg_mse_scores

print("平均MSE:{} \n".format(round(np.mean(mse_scores), 2)))

watermarked-fit_regression_1_3

小結

還有2個常用的分層k折交叉驗證、時間序列交叉驗證,這裏做一個對比,就不展開細説了

方法 適用場景 優點 缺點
留出法 大數據集快速驗證 計算快 結果受單次劃分影響大
k折交叉驗證 通用場景 結果穩健 計算成本中等
留一法(LOO) 極小數據集 無偏差 計算成本極高
分層k折 類別不平衡數據 保持類別分佈 僅適用於分類問題
時間序列CV 時間相關數據 防止未來信息泄露 必須按時間順序劃分

學習曲線

通過訓練誤差與測試誤差,來判斷模型是否過擬合:

  • 欠擬合:訓練誤差和驗證誤差都很高,模型太簡單
  • 過擬合:訓練誤差很低,但驗證誤差很高,模型太複雜
  • 恰到好處:訓練誤差和驗證誤差都低,並且兩者接近
train_sizes, train_scores, valid_scores = learning_curve(
    model, X_train, y_train, cv=5, n_jobs=-1,
    train_sizes=np.linspace(0.1, 1.0, 10)
)

train_scores_mean = np.mean(train_scores, axis=1)
valid_scores_mean = np.mean(valid_scores, axis=1)

plt.figure()
plt.plot(train_sizes, train_scores_mean, 'o-', color='r', label='Training score')
plt.plot(train_sizes, valid_scores_mean, 'o-', color='g', label='Validation score')

plt.xlabel('Training examples')
plt.ylabel('Score')
plt.title('Learning Curve')
plt.legend(loc='best')
plt.grid(True)
plt.show()

腳本!啓動:

watermarked-fit_regression_1_4

這圖一看就不正常,我們丟進gpt,讓它幫我們分析一下

watermarked-fit_regression_1_5

watermarked-fit_regression_1_6

驗證曲線

用來評估模型性能與某個超參數之間關係的一種可視化工具。而所謂的超參數,則是模型中必須要設置的參數,比如多項式中的階數degree、lasso|ridge中的alpha等等

from sklearn.model_selection import validation_curve
from sklearn.model_selection import train_test_split

param_range = np.arange(1, 15)
train_scores, valid_scores = validation_curve(
    model, X.reshape(-1, 1), y,
    param_name='poly__degree',
    param_range=param_range,
    cv=5,
    scoring='r2'
)

train_mean = np.mean(train_scores, axis=1)
valid_mean = np.mean(valid_scores, axis=1)

plt.figure(figsize=(8, 5))
plt.plot(param_range, train_mean, label='Training Score', marker='o', color='r')
plt.plot(param_range, valid_mean, label='Validation Score', marker='o', color='g')
plt.xlabel('Polynomial Degree')
plt.ylabel('R² Score')
plt.legend(loc='best')
plt.grid(True)
plt.xticks(param_range)
plt.show()

腳本!啓動:

watermarked-fit_regression_1_7

懶了,直接丟ai!

watermarked-fit_regression_1_8

watermarked-fit_regression_1_9

watermarked-fit_regression_1_10

正則化

所謂的正則化,就是:

  • L1 正則化(Lasso 迴歸)
    • 在損失函數中添加模型參數的絕對值之和
    • 特點:傾向於將某些參數壓縮到 0,從而實現特徵選擇
  • L2 正則化(Ridge 迴歸)
    • 在損失函數中添加模型參數的平方和
    • 特點:傾向於將參數值縮小,但不會完全壓縮到 0
  • 彈性網絡(Elastic Net)
    • 結合 L1 和 L2 正則化

lasso與ridge我們之前在線性迴歸的時候用過,用來降低無用特徵的對結果的影響,而lasso與ridge也可以抑制高階項係數

用lasso來測試一下

from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LassoCV

lassoCV = LassoCV(alphas=np.logspace(-4, 0, 20), cv=5, max_iter=1000000)
lasso = Pipeline([
    ('poly', PolynomialFeatures(degree=degree)),
    ('scaler', StandardScaler()),
    ('lasso', lassoCV)
])

lasso.fit(X_train, y_train)
lasso_train_pred = lasso.predict(X_train)
lasso_test_pred = lasso.predict(X_test)

mse_train = mean_squared_error(y_train, lasso_train_pred)
r2_train = r2_score(y_train, lasso_train_pred)
mse_test = mean_squared_error(y_test, lasso_test_pred)
r2_test = r2_score(y_test, lasso_test_pred)

print('===='*20)
print('lasso:\n')
print(f"訓練集 MSE: {mse_train:.4f} ,R²:{r2_train}")
print(f"驗證集 MSE: {mse_test:.4f} ,R²:{r2_test}")

lasso與之前的使用方式不同,使用了LassoCV,新方式可以自動選擇alpha,並且會嘗試所有的alpha可能值,再加上交叉驗證,使得lasso迴歸的結果達到最佳狀態

腳本!啓動:

watermarked-fit_regression_1_11

通過正則化L1,也就是lasso迴歸,能夠答覆提高模型的泛化能力,其實和之前線性迴歸去掉無用特徵一樣,在高階多項式中,lasso迴歸一樣能夠去掉無用的階數,保留真正影響結果的階數

print('lasso迴歸係數')
print(lasso.named_steps['lasso'].coef_)

watermarked-fit_regression_1_12

由此可見,lasso刪除了,0、3、4、7、8、9階,保留了1、2、5、6、10階

超參數與普通參數

  • 普通參數是自己學習到的,比如線性迴歸中的迴歸係數、截距
  • 超參數是模型訓練之前就要設置的,比如多項式的階數degree

由於超參數無法通過模型自己去學習,所以需要通過多種方法去嘗試、調優,而超參數調優的是一件非常非常複雜的工作,涉及到多種不同模型,有很多不同的方法。這裏我們看的是單個超參數(比如多項式的階數),目的也很簡單,就是通過不同超參數的表現,查看過擬合的情況

列一下一些常見的超參數,而對應的模型在今後的文章中多少都會涉及到

模型 超參數 含義
線性迴歸(帶正則) alpha(Ridge/Lasso) 正則化強度
決策樹 max_depth 樹的最大深度
K 近鄰 n_neighbors 鄰居個數
SVM C、gamma 懲罰係數 / 核函數參數
多項式迴歸 degree 多項式的階數
神經網絡 learning_rate、batch_size、epochs 學習速率 / 批量大小 / 訓練輪數

小結

本文通過一個過擬合的例子,使用不同的方法,交叉驗證、學習曲線、正則化等方法驗證了怎麼去評估模型過擬合

聯繫我

  • 聯繫我,做深入的交流


至此,本文結束
在下才疏學淺,有撒湯漏水的,請各位不吝賜教...

Add a new 评论

Some HTML is okay.