線性模型系數解釋中的常見缺陷?

在線性模型中,目標值被建模為特征的線性組合(請參閱scikit-learn中“Linear Models 用戶指南”一節中對一組可用的線性模型的描述)。多個線性模型中的系數表示給定特征與目標之間的關系,假設所有其他特征保持不變(條件依賴)。這與繪制和擬合線性關系不同:在這種情況下,其他特征的所有可能值都會在估計中得到考慮(邊際依賴)。

此示例將為解釋線性模型中的系數提供一些提示,指出當線性模型不適合描述數據集或當特征相關時會出現的問題。

我們會利用1985年的“目前人口統計調查”的數據,根據經驗、年齡或教育程度等各方面因素來預測工資。

print(__doc__)

import numpy as np
import scipy as sp
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

數據集:工資

我們從OpenML獲取數據。注意,將參數 as_frame 設置為True, 將以pandas數據的形式檢索數據。

from sklearn.datasets import fetch_openml

survey = fetch_openml(data_id=534, as_frame=True)

然后,我們識別特征X和目標y:列WAGE是我們的目標變量(即我們想要預測的變量)。

X = survey.data[survey.feature_names]
X.describe(include="all")

注意,數據集包含分類變量和數值變量。在以后對數據集進行預處理時,我們需要考慮到這一點。

X.head()

我們的預測目標是:the wage。工資被描述為以每小時美元為單位的浮點數。

y = survey.target.values.ravel()
survey.target.head()
0    5.10
1    4.95
2    6.67
3    4.00
4    7.50
Name: WAGE, dtype: float64

我們將樣本分解成一個訓練集和一個測試集。只有訓練數據集將用于以下探索性分析。這是一種模擬在未知目標上執行預測的真實情況的方法,我們不希望我們的分析和決定因我們對測試數據有所了解而有偏差。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, random_state=42
)

首先,讓我們通過查看變量分布和它們之間的成對關系來獲得一些洞察力。只數值變量被使用。在下面的圖中,每個點表示一個示例。

train_dataset = X_train.copy()
train_dataset.insert(0"WAGE", y_train)
_ = sns.pairplot(train_dataset, kind='reg', diag_kind='kde')

仔細看一看工資分布,就會發現它是長尾的。因此,我們應該把它的對數近似地轉化為正態分布(線性模型,如嶺回歸或Lasso最適合用于正態分布的誤差)。

隨著 EDUCATION 的增加,WAGE 在增加。請注意,這里表示的工資和教育之間的依賴是邊際依賴,也就是說,它描述了一個特定變量的行為,而沒有保持其他變量的固定。

此外,EXPERIENCE 與 AGE 呈高度線性相關。

機器學習工作流

為了設計機器學習管道,我們首先手動檢查需要處理的數據類型:

survey.data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex:
 534 entries, 0 to 533
Data columns (total 10 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   EDUCATION   534 non-null    float64
 1   SOUTH       534 non-null    category
 2   SEX         534 non-null    category
 3   EXPERIENCE  534 non-null    float64
 4   UNION       534 non-null    category
 5   AGE         534 non-null    float64
 6   RACE        534 non-null    category
 7   OCCUPATION  534 non-null    category
 8   SECTOR      534 non-null    category
 9   MARR        534 non-null    category
dtypes: category(7), float64(3)
memory usage: 17.1 KB

如前所述,數據集包含具有不同數據類型的列,我們需要對每個數據類型應用特定的預處理。特別是,如果分類變量不首先編碼為整數,則不能將分類變量包括在線性模型中。此外,為了避免將分類特征視為有序值,我們需要對它們進行一次獨熱編碼。我們的預處理器:

  • 獨熱編碼(即按類別生成一列)分類列;
  • 作為第一種方法(我們將看到數值的標準化將如何影響我們的討論),保持數值的原樣。
from sklearn.compose import make_column_transformer
from sklearn.preprocessing import OneHotEncoder

categorical_columns = ['RACE''OCCUPATION''SECTOR',
                       'MARR''UNION''SEX''SOUTH']
numerical_columns = ['EDUCATION''EXPERIENCE''AGE']

preprocessor = make_column_transformer(
    (OneHotEncoder(drop='if_binary'), categorical_columns),
    remainder='passthrough'
)

為了將數據集描述為線性模型,我們使用了一個正則化程度很小的嶺回歸,并對WAGE的對數進行了建模。

from sklearn.pipeline import make_pipeline
from sklearn.linear_model import Ridge
from sklearn.compose import TransformedTargetRegressor

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=Ridge(alpha=1e-10),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

處理數據集

首先,我們擬合模型。

_ = model.fit(X_train, y_train)

然后,我們檢查計算模型的性能,繪制它在測試集上的預測,并計算,例如,模型的中位絕對誤差。

from sklearn.metrics import median_absolute_error

y_pred = model.predict(X_train)

mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'
fig, ax = plt.subplots(figsize=(55))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")
plt.text(320, string_score)
plt.title('Ridge model, small regularization')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

學習到的模型遠不是一個做出準確預測的好模型:從上面的情節來看,這是顯而易見的,在那里,好的預測應該在紅線上。

在下一節中,我們將解釋模型的系數。當我們這樣做的時候,我們應該記住,我們得出的任何結論都是關于我們建立的模型,而不是關于數據的真實(現實世界)生成過程。

解釋系數:尺度問題

首先,我們可以查看我們所擬合的回歸系數的值。

feature_names = (model.named_steps['columntransformer']
                      .named_transformers_['onehotencoder']
                      .get_feature_names(input_features=categorical_columns))
feature_names = np.concatenate(
    [feature_names, numerical_columns])

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)

coefs

年齡系數以“每活一年美元/小時”表示,而教育系數則以“每受教育一年美元/小時”表示。這個系數的表示說明了模型的實際預測:1歲的增長意味著每小時減少0.030867美元,而1年的教育增長意味著每小時增加0.054699美元。另一方面,范疇變量(如聯合變量或性別變量)是取值0或1的維數,其系數以美元/小時表示。然后,由于特征的度量單位不同,因此不能比較不同系數的大小,因為它們具有不同的自然尺度,因而具有不同的取值范圍。如果我們繪制系數,這是更明顯的。

coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, small regularization')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

事實上,從上面的圖來看,決定WAGE的最重要因素似乎是UNION,即使我們的直覺可能告訴我們,像 EXPERIENCE這樣的變量應該有更大的影響。

觀察系數圖來衡量特征的重要性可能會產生誤導,因為其中一些變化很小,而另一些,如AGE,變化更大,甚至幾十年。

如果我們比較不同特征的標準差,這是可見的。

X_train_preprocessed = pd.DataFrame(
    model.named_steps['columntransformer'].transform(X_train),
    columns=feature_names
)

X_train_preprocessed.std(axis=0).plot(kind='barh', figsize=(97))
plt.title('Features std. dev.')
plt.subplots_adjust(left=.3)

將系數乘以相關特征的標準差,將所有系數減少到相同的度量單位。我們將看到,這相當于將數值變量標準化為其標準差,如:

在這種情況下,我們強調,特征的方差越大,輸出的相應系數的權重就越大,所有這些都是相等的。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_ *
    X_train_preprocessed.std(axis=0),
    columns=['Coefficient importance'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, small regularization')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

現在系數已經被縮放了,我們可以安全地比較它們。

警告:為什么上面的情節意味著年齡的增長會導致工資的下降?為什么 initial pairplot告訴我們相反的情況?

上面的圖告訴我們,當所有其他特性保持不變(即條件依賴關系)時,特定特性與目標之間的依賴關系。即:條件依賴。當所有其他特征不變時,年齡的增加將導致工資的下降。相反,當所有其他特征不變時,經驗的增加會導致工資的增加。此外,年齡、經驗和教育是對模型影響最大的三個變量。

檢驗系數的可變性

我們可以通過交叉驗證來檢驗系數的可變性:它是數據擾動的一種形式(與重采樣有關)。

如果系數在改變輸入數據集時變化很大,則不能保證它們的魯棒性,因此很可能需要謹慎地解釋它們。

from sklearn.model_selection import cross_validate
from sklearn.model_selection import RepeatedKFold

cv_model = cross_validate(
    model, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_ *
     X_train_preprocessed.std(axis=0)
     for est in cv_model['estimator']],
    columns=feature_names
)
plt.figure(figsize=(97))
sns.swarmplot(data=coefs, orient='h', color='k', alpha=0.5)
sns.boxplot(data=coefs, orient='h', color='cyan', saturation=0.5)
plt.axvline(x=0, color='.5')
plt.xlabel('Coefficient importance')
plt.title('Coefficient importance and its variability')
plt.subplots_adjust(left=.3)

關聯變量問題

AGE和EXPERIENCE 系數受強變異性的影響,這可能是由于這兩個特征之間的共線性所致:由于AGE和EXPERIENCE在數據中的差異,它們的影響很難分開。

為了驗證這一解釋,我們繪制了年齡和經驗系數的變異性圖。

plt.ylabel('Age coefficient')
plt.xlabel('Experience coefficient')
plt.grid(True)
plt.xlim(-0.40.5)
plt.ylim(-0.40.5)
plt.scatter(coefs["AGE"], coefs["EXPERIENCE"])
_ = plt.title('Co-variations of coefficients for AGE and EXPERIENCE '
              'across folds')

有兩個區域:當EXPERIENCE系數為正值時,AGE為負,反之亦然。

為了更進一步,我們刪除了這兩個特征中的一個,并檢查了對模型穩定性的影響。

column_to_drop = ['AGE']

cv_model = cross_validate(
    model, X.drop(columns=column_to_drop), y,
    cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_ *
     X_train_preprocessed.drop(columns=column_to_drop).std(axis=0)
     for est in cv_model['estimator']],
    columns=feature_names[:-1]
)
plt.figure(figsize=(97))
sns.swarmplot(data=coefs, orient='h', color='k', alpha=0.5)
sns.boxplot(data=coefs, orient='h', color='cyan', saturation=0.5)
plt.axvline(x=0, color='.5')
plt.title('Coefficient importance and its variability')
plt.xlabel('Coefficient importance')
plt.subplots_adjust(left=.3)

經驗系數的估計現在變小了,對所有在交叉驗證過程中訓練的模型仍然很重要。

預處理數值變量

如前所述(請參閱“機器學習工作流”),我們也可以選擇在訓練模型之前縮放數值。這將有助于將類似的數量正則化應用于嶺中的所有。為了將均值和尺度變化減去單位方差,對預處理器進行了重新定義。

from sklearn.preprocessing import StandardScaler

preprocessor = make_column_transformer(
    (OneHotEncoder(drop='if_binary'), categorical_columns),
    (StandardScaler(), numerical_columns),
    remainder='passthrough'
)

模型將保持不變。

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=Ridge(alpha=1e-10),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

_ = model.fit(X_train, y_train)

再次,我們用模型的中位絕對誤差和R方系數來檢驗計算模型的性能。

y_pred = model.predict(X_train)
mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'
fig, ax = plt.subplots(figsize=(66))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")

plt.text(320, string_score)

plt.title('Ridge model, small regularization, normalized variables')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

對于系數分析,這次不需要縮放。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, small regularization, normalized variables')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

我們現在檢查幾個交叉驗證的系數。

cv_model = cross_validate(
    model, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_
     for est in cv_model['estimator']],
    columns=feature_names
)
plt.figure(figsize=(97))
sns.swarmplot(data=coefs, orient='h', color='k', alpha=0.5)
sns.boxplot(data=coefs, orient='h', color='cyan', saturation=0.5)
plt.axvline(x=0, color='.5')
plt.title('Coefficient variability')
plt.subplots_adjust(left=.3)

結果與非歸一化情況十分相似。

正則化線性模型

在機器學習實踐中,嶺回歸更常用于不可忽略的正則化。

上面,我們把這個正規化限制在很小的數量上。正則化改善了問題的條件,減少了估計的方差。RidgeCV應用交叉驗證來確定正則化參數(Alpha)的哪個值最適合于預測。

from sklearn.linear_model import RidgeCV

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=RidgeCV(alphas=np.logspace(-101021)),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

_ = model.fit(X_train, y_train)

首先,我們檢查選擇了哪個值的α。

model[-1].regressor_.alpha_

然后我們檢查預測的質量。

y_pred = model.predict(X_train)
mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'

fig, ax = plt.subplots(figsize=(66))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")

plt.text(320, string_score)

plt.title('Ridge model, regularization, normalized variables')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

正則化模型的數據再現能力與非正則模型相似。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, regularization, normalized variables')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

這些系數有顯著性差異。年齡和經驗系數都是正的,但它們現在對預測的影響較小。

正則化減少了相關變量對模型的影響,因為權重在兩個預測變量之間是共享的,因此兩者都不會有很強的權重。

另一方面,正則化獲得的權重更穩定(參見嶺回歸和分類用戶指南一節)。在交叉驗證中,從從數據擾動中獲得的圖中可以看出這種增加的穩定性。這個圖可以和前一個相比較。

cv_model = cross_validate(
    model, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_ *
     X_train_preprocessed.std(axis=0)
     for est in cv_model['estimator']],
    columns=feature_names
)

plt.ylabel('Age coefficient')
plt.xlabel('Experience coefficient')
plt.grid(True)
plt.xlim(-0.40.5)
plt.ylim(-0.40.5)
plt.scatter(coefs["AGE"], coefs["EXPERIENCE"])
_ = plt.title('Co-variations of coefficients for AGE and EXPERIENCE '
              'across folds')

稀疏系數的線性模型

在數據集中考慮相關變量的另一種可能性是估計稀疏系數。在某種程度上,當我們在以前的嶺估計中刪除AGE列時,我們已經手動完成了。

LASSO模型(見Lasso用戶指南部分)估計稀疏系數。LassoCV應用交叉驗證來確定正則化參數(Alpha)的哪個值最適合于模型估計。

from sklearn.linear_model import LassoCV

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=LassoCV(alphas=np.logspace(-101021), max_iter=100000),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

_ = model.fit(X_train, y_train)

首先,我們檢查了選擇了哪個值的α。

model[-1].regressor_.alpha_
0.001

然后我們檢查預測的質量。

y_pred = model.predict(X_train)
mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'

fig, ax = plt.subplots(figsize=(66))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")

plt.text(320, string_score)

plt.title('Lasso model, regularization, normalized variables')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

同樣,對于我們的數據集,模型也不是很有預見性。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Lasso model, regularization, normalized variables')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

Lasso模型識別了年齡和經驗之間的相關性,并為了預測而壓制其中一個。

重要的是要記住,被丟棄的系數可能仍然與結果本身有關:模型選擇了抑制它們,因為它們在其他特征之上很少或沒有額外的信息。

課程學習

  • 要檢索特征重要性,系數必須縮放到相同的度量單位。使用特性的標準差對它們進行縮放是一個有用的代理。
  • 多元線性模型中的系數表示給定特征與目標之間的依賴關系,這有賴于其他特征。
  • 相關特征導致線性模型系數不穩定,其影響不能很好地分解。
  • 不同的線性模型對特征相關性的響應不同,各相關系數可能存在顯著差異。
  • 通過交叉驗證循環對系數進行檢測,給出了它們的穩定性概念。

腳本的總運行時間:(0分21.601秒)。

Download Python source code: plot_linear_model_coefficient_interpretation.py

Download Jupyter notebook: plot_linear_model_coefficient_interpretation.ipynb