掌握sklearn必須知道這三個強大的工具。因此,在建立機器學習模型時,學習如何有效地使用這些方法是至關重要的。
在深入討論之前,我們先從兩個方面著手:
Transformer:Transformer是指具有fit()和transform()方法的對象,用於清理、減少、擴展或生成特徵。簡單地說,transformers幫助你將數據轉換為機器學習模型所需的格式。OneHotEncoder和MinMaxScaler就是Transformer的例子。Estimator:Estimator是指機器學習模型。它是一個具有fit()和predict()方法的對象。我們將交替使用模型和Estimator這2個術語。該連結是一些Estimator的例子:https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html。
安裝
如果你想在你電腦上運行代碼,確保你已經安裝了pandas,seaborn和sklearn。我在Jupyter notebook中在python3.7.1中編寫腳本。
讓我們導入所需的庫和數據集。關於這個數據集(包括數據字典)的詳細信息可以在這裡找到(這個源實際上是針對R的,但是它似乎引用了相同的底層數據集):https://vincentarelbundock.github.io/Rdatasets/doc/reshape2/tips.html。
# 設置種子seed = 123# 為數據導入包/模塊import pandas as pdfrom seaborn import load_dataset# 為特徵工程和建模導入模塊from sklearn.model_selection import train_test_splitfrom sklearn.base import BaseEstimator, TransformerMixinfrom sklearn.preprocessing import OneHotEncoder, MinMaxScalerfrom sklearn.impute import SimpleImputerfrom sklearn.pipeline import Pipeline, FeatureUnionfrom sklearn.compose import ColumnTransformerfrom sklearn.linear_model import LinearRegression# 加載數據集df = load_dataset('tips').drop(columns=['tip', 'sex']).sample(n=5, random_state=seed)# 添加缺失的值df.iloc[[1, 2, 4], [2, 4]] = np.nandf
使用少量的記錄可以很容易地監控每個步驟的輸入和輸出。因此,我們將只使用數據集中5條記錄的樣本。
管道
假設我們想用smoker、day和time列來預測總的帳單。我們將先刪除size列並對數據進行劃分:
# 劃分數據X_train, X_test, y_train, y_test = train_test_split(df.drop(columns=['total_bill', 'size']), df['total_bill'], test_size=.2, random_state=seed)通常情況下,原始數據不是我們可以直接將其輸入機器學習模型的狀態。因此,將數據轉換為可接受且對模型有用的狀態成為建模的必要先決條件。讓我們做以下轉換作為準備:
用「missing」填充缺失值one-hot編碼以下完成這兩個步驟:
# 輸入訓練數據imputer = SimpleImputer(strategy='constant', fill_value='missing')X_train_imputed = imputer.fit_transform(X_train)# 編碼訓練數據encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)X_train_encoded = encoder.fit_transform(X_train_imputed)# 檢查訓練前後的數據print("******************** Training data ********************")display(X_train)display(pd.DataFrame(X_train_imputed, columns=X_train.columns))display(pd.DataFrame(X_train_encoded, columns=encoder.get_feature_names(X_train.columns)))# 轉換測試數據X_test_imputed = imputer.transform(X_test)X_test_encoded = encoder.transform(X_test_imputed)# 檢查測試前後的數據print("******************** Test data ********************")display(X_test)display(pd.DataFrame(X_test_imputed, columns=X_train.columns))display(pd.DataFrame(X_test_encoded, columns=encoder.get_feature_names(X_train.columns)))你可能已經注意到,當映射回測試數據集的列名時,我們使用了來自訓練數據集的列名。這是因為我更喜歡使用來自於訓練Transformer的數據的列名。但是,如果我們使用測試數據集,它將給出相同的結果。
對於每個數據集,我們首先看到原始數據,然後是插補後的輸出,最後是編碼後的輸出。
這種方法可以完成任務。但是,我們將上一步的輸出作為輸入手動輸入到下一步,並且有多個臨時輸出。我們還必須在測試數據上重複每一步。隨著步驟數的增加,維護將變得更加繁瑣,更容易出錯。
我們可以使用管道編寫更精簡和簡潔的代碼:
# 將管道與訓練數據匹配pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])pipe.fit(X_train)# 檢查訓練前後的數據print("******************** Training data ********************")display(X_train)display(pd.DataFrame(pipe.transform(X_train), columns=pipe['encoder'].get_feature_names(X_train.columns)))# 檢查測試前後的數據print("******************** Test data ********************")display(X_test)display(pd.DataFrame(pipe.transform(X_test), columns=pipe['encoder'].get_feature_names(X_train.columns)))
使用管道時,每個步驟都將其輸出作為輸入傳遞到下一個步驟。因此,我們不必手動跟蹤數據的不同版本。這種方法為我們提供了完全相同的最終輸出,但是使用了更優雅的代碼。
在查看了轉換後的數據之後,現在是在我們的示例中添加模型的時候了。讓我們從為第一種方法添加一個簡單模型:
# 輸入訓練數據imputer = SimpleImputer(strategy='constant', fill_value='missing')X_train_imputed = imputer.fit_transform(X_train)# 編碼訓練數據encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)X_train_encoded = encoder.fit_transform(X_train_imputed)# 使模型擬合訓練數據model = LinearRegression()model.fit(X_train_encoded, y_train)# 預測訓練數據y_train_pred = model.predict(X_train_encoded)print(f"Predictions on training data: {y_train_pred}")# 轉換測試數據X_test_imputed = imputer.transform(X_test)X_test_encoded = encoder.transform(X_test_imputed)# 預測測試數據y_test_pred = model.predict(X_test_encoded)print(f"Predictions on test data: {y_test_pred}")
我們將對管道方法進行同樣的處理:
# 將管道與訓練數據匹配pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False)), ('model', LinearRegression())])pipe.fit(X_train, y_train)# 預測訓練數據y_train_pred = pipe.predict(X_train)print(f"Predictions on training data: {y_train_pred}")# 預測測試數據y_test_pred = pipe.predict(X_test)print(f"Predictions on test data: {y_test_pred}")
你可能已經注意到,一旦我們訓練了一條管道,進行預測是多麼簡單。pipe.predict(X)對原始數據進行轉換,然後返回預測。也很容易看到步驟的順序。讓我們直觀地總結一下這兩種方法:
使用管道不僅可以組織和簡化代碼,而且還有許多其他好處,下面是其中一些好處:
微調管道的能力:當構建一個模型時,你可能需要嘗試不同的方法來預處理數據並再次運行模型,看看預處理步驟中的調整是否能提高模型的泛化能力。在優化模型時,微調不僅存在於模型的超參數中,而且存在於預處理步驟的實現中。考慮到這一點,當我們有一個統一了Transformer和Estimator的管道對象時,我們可以微調整個管道的超參數,包括使用GridSearchCV或RandomizedSearchCV的Estimator和兩個Transformer。更容易部署:在訓練模型時用於準備數據的所有轉換步驟在進行預測時也可以應用於生產環境中的數據。當我們訓練管道時,我們訓練一個包含數據轉換器和模型的對象。一旦經過訓練,這個管道對象就可以用於更平滑的部署。ColumnTransformer
在前面的例子中,我們以相同的方式對所有列進行插補和編碼。但是,我們經常需要對不同的列組應用不同的transformer。例如,我們希望將OneHotEncoder僅應用於分類列,而不應用於數值列。這就是ColumnTransformer的用武之地。
這一次,我們將對保留所有列的數據集進行分區,以便同時具有數值和類別特徵。
# 劃分數據X_train, X_test, y_train, y_test = train_test_split(df.drop(columns=['total_bill']), df['total_bill'], test_size=.2, random_state=seed)# 定義分類列categorical = list(X_train.select_dtypes('category').columns)print(f"Categorical columns are: {categorical}")# 定義數字列numerical = list(X_train.select_dtypes('number').columns)print(f"Numerical columns are: {numerical}")
我們根據數據類型將特徵分為兩組。列分組可以根據數據的適當情況進行。例如,如果不同的預處理管道更適合分類列,則可以將它們進一步拆分為多個組。
上一節的代碼現在將不再工作,因為我們有多個數據類型。讓我們看一個例子,其中我們使用ColumnTransformer和Pipeline在存在多個數據類型的情況下執行與之前相同的轉換。
# 定義分類管道cat_pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])# 使ColumnTransformer擬合訓練數據preprocessor = ColumnTransformer(transformers=[('cat', cat_pipe, categorical)], remainder='passthrough')preprocessor.fit(X_train)# 準備列名cat_columns = preprocessor.named_transformers_['cat']['encoder'].get_feature_names(categorical)columns = np.append(cat_columns, numerical)# 檢查訓練前後的數據print("******************** Training data ********************")display(X_train)display(pd.DataFrame(preprocessor.transform(X_train), columns=columns))# 檢查測試前後的數據print("******************** Test data ********************")display(X_test)display(pd.DataFrame(preprocessor.transform(X_test), columns=columns))
分類列的輸出與上一節的輸出相同。唯一的區別是這個版本有一個額外的列:size。我們已經將cat_pipe(在上一節中稱為pipe)傳遞給ColumnTransformer來轉換分類列,並指定remainment='passthrough'以保持其餘列不變。
讓我們用中值填充缺失值,並將其縮放到0和1之間:
# 定義分類管道cat_pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])# 定義數值管道num_pipe = Pipeline([('imputer', SimpleImputer(strategy='median')), ('scaler', MinMaxScaler())])# 使ColumnTransformer擬合訓練數據preprocessor = ColumnTransformer(transformers=[('cat', cat_pipe, categorical), ('num', num_pipe, numerical)])preprocessor.fit(X_train)# 準備列名cat_columns = preprocessor.named_transformers_['cat']['encoder'].get_feature_names(categorical)columns = np.append(cat_columns, numerical)# 檢查訓練前後的數據print("******************** Training data ********************")display(X_train)display(pd.DataFrame(preprocessor.transform(X_train), columns=columns))# 檢查測試前後的數據print("******************** Test data ********************")display(X_test)display(pd.DataFrame(preprocessor.transform(X_test), columns=columns))
現在所有列都被插補,範圍在0到1之間。使用ColumnTransformer和Pipeline,我們將數據分成兩組,將不同的管道和不同的Transformer應用到每組,然後將結果粘貼在一起:
儘管在我們的示例中,數值管道和分類管道中的步驟數相同,但管道中可以有任意數量的步驟,並且不同列子集的步驟數不必相同。現在我們將一個模型添加到我們的示例中:
# 定義分類管道cat_pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])# 定義數值管道num_pipe = Pipeline([('imputer', SimpleImputer(strategy='median')), ('scaler', MinMaxScaler())])# 組合分類管道和數值管道preprocessor = ColumnTransformer(transformers=[('cat', cat_pipe, categorical), ('num', num_pipe, numerical)])# 在管道上安裝transformer和訓練數據的estimatorpipe = Pipeline(steps=[('preprocessor', preprocessor), ('model', LinearRegression())])pipe.fit(X_train, y_train)# 預測訓練數據y_train_pred = pipe.predict(X_train)print(f"Predictions on training data: {y_train_pred}")# 預測測試數據y_test_pred = pipe.predict(X_test)print(f"Predictions on test data: {y_test_pred}")
為了將ColumnTransformer中指定的預處理步驟與模型結合起來,我們在外部使用了一個管道。以下是它的視覺表現:
當我們需要對不同的列子集執行不同的操作時,ColumnTransformer很好地補充了管道。
FeatureUnion
以下代碼的輸出在本節中被省略,因為它們與ColumnTransformer章節的輸出相同。
FeatureUnion是另一個有用的工具。它可以做ColumnTransformer剛剛做過的事情,但要做得更遠:
# 自定義管道class ColumnSelector(BaseEstimator, TransformerMixin): """Select only specified columns.""" def __init__(self, columns): self.columns = columns def fit(self, X, y=None): return self def transform(self, X): return X[self.columns]# 定義分類管道cat_pipe = Pipeline([('selector', ColumnSelector(categorical)), ('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])# 定義數值管道num_pipe = Pipeline([('selector', ColumnSelector(numerical)), ('imputer', SimpleImputer(strategy='median')), ('scaler', MinMaxScaler())])# FeatureUnion擬合訓練數據preprocessor = FeatureUnion(transformer_list=[('cat', cat_pipe), ('num', num_pipe)])preprocessor.fit(X_train)# 準備列名cat_columns = preprocessor.transformer_list[0][1][2].get_feature_names(categorical)columns = np.append(cat_columns, numerical)# 檢查訓練前後的數據print("******************** Training data ********************")display(X_train)display(pd.DataFrame(preprocessor.transform(X_train), columns=columns))# 檢查測試前後的數據print("******************** Test data ********************")display(X_test)display(pd.DataFrame(preprocessor.transform(X_test), columns=columns))我們可以將FeatureUnion視為創建數據的副本,並行地轉換這些副本,然後將結果粘貼在一起。這裡的術語副本更像是一種輔助概念化的類比,而不是實際採用的技術。
在每個管道的開始,我們添加了一個額外的步驟,在這裡我們使用一個定製的轉換器來選擇相關的列:第14行和第19行的ColumnSelector。下面是我們可視化上面的腳本的圖:
現在,是時候向腳本添加模型了:
# 定義分類管道cat_pipe = Pipeline([('selector', ColumnSelector(categorical)), ('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])# 定義數值管道num_pipe = Pipeline([('selector', ColumnSelector(numerical)), ('imputer', SimpleImputer(strategy='median')), ('scaler', MinMaxScaler())])# 組合分類管道和數值管道preprocessor = FeatureUnion(transformer_list=[('cat', cat_pipe), ('num', num_pipe)])# 組合分類管道和數值管道pipe = Pipeline(steps=[('preprocessor', preprocessor), ('model', LinearRegression())])pipe.fit(X_train, y_train)# 預測訓練數據y_train_pred = pipe.predict(X_train)print(f"Predictions on training data: {y_train_pred}")# 預測測試數據y_test_pred = pipe.predict(X_test)print(f"Predictions on test data: {y_test_pred}")它看起來很像我們用ColumnTransformer做的。
如本例所示,使用FeatureUnion比使用ColumnTransformer要複雜得多。因此,在我看來,在類似的情況下最好使用ColumnTransformer。
然而,FeatureUnion肯定有它的位置。如果你需要以不同的方式轉換相同的輸入數據並將它們用作特徵,FeatureUnion就是其中之一。例如,如果你正在處理一個文本數據,並且希望對數據進行tf-idf矢量化以及提取文本長度,FeatureUnion是一個完美的工具。
總結
你可能已經注意到,Pipeline是超級明星。ColumnTransformer和FeatureUnion是用於管道的附加工具。ColumnTransformer更適合於並行劃分,而FeatureUnion允許我們在同一個輸入數據上並行應用多個轉換器。下面是一個簡單的總結: