🤖

【16日目】パイプラインを構築する【2021アドベントカレンダー】

2021/12/16に公開

2021年1人アドベントカレンダー(機械学習)、16日目の記事になります。

https://qiita.com/advent-calendar/2021/solo_advent_calendar

テーマは パイプライン になります。

scikit-learn の pipelineを使います。

Colab のコードはこちら Open In Colab

Pipeline の基本形

基本形は下記になります。

from sklearn.pipeline import Pipeline

# Pipeline の中身
step = [
  (
    "name",           # 名称
    StandardScaler(), # 実行したいインスタンス
    ),
]

pipe = Pipeline(step) # Pipeline のインスタンスの宣言

# Pipeline の実行
pipe.fit()
pipe.predict()
pipe.fit_transform() 
pipe.transform()

実行できるメソッドに限りがあるので要注意です。

https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html

また、指定したカラムだけを対象にできる、ColumnTransformer という Pipeline もあります。

例えば数値データのカラムだけに StandardScaler() を適用する、といったことが可能です。

from sklearn.compose import ColumnTransformer

# カラムの指定
number_columns = df.select_dtypes(include="number").columns

# 数値データ用の変換 Pipeline
numeric_transformer = Pipeline(steps=[
    ('num_imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())]
)

columns_transformers = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, number_columns), # 名称、pipeline のインスタンス、対象とするカラム
    ]
)

https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html

詳細

カラムの指定

指定するカラムを作成します。

数値データのカラム、OneHotエンコーディングの対象にするカーディナリティの低いカラム、Ordinalエンコーディングの対象にするカーディナリティの高いカラムを作成します。

# 数値データカラム名を取得
number_columns = df.drop(["Global_Sales",  "NA_Sales", "PAL_Sales", "JP_Sales", "Other_Sales"], axis=1).select_dtypes(include="number").columns

# カテゴリデータカラム名を取得
category_columns = df.drop(["Global_Sales",  "NA_Sales", "PAL_Sales", "JP_Sales", "Other_Sales"], axis=1).select_dtypes(include="object").columns

# カテゴリデータカラムの各カラムのカテゴリーの数を取得
category_unique_num = df.select_dtypes(include="object").nunique()

# カテゴリーの数にしきい値を設けて、カテゴリー数の多いカラムと少ないカラムに分ける
thread = 10

many_kinds_category_columns = category_unique_num[category_unique_num >= thread].index
few_kinds_category_columns = category_unique_num[category_unique_num < thread].index

カラムごとの Pipeline を作成

# 数値データ用の変換
numeric_transformer = Pipeline(steps=[
    ('num_imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())]
)


# カテゴリーのエンコーディング法則を指定する
ordinal_all_cols_mapping = []

for column in many_kinds_category_columns:
    ordinal_one_cols_mapping = []
    for category in natsorted(X_train[column].unique()):
        ordinal_one_cols_mapping.append(category)

    ordinal_all_cols_mapping.append(ordinal_one_cols_mapping)

# カテゴリー数が多いカテゴリーデータ用の変換
many_kinds_categorical_transformer = Pipeline(
    steps=[
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('ordinal', OrdinalEncoder(
                handle_unknown = 'use_encoded_value', # 未知数をunknown valueに置き換える設定
                unknown_value = -1,
                categories = ordinal_all_cols_mapping
            )
        )
])

# カテゴリー数が少ないカテゴリーデータ用の変換
few_kinds_categorical_transformer = Pipeline(
    steps=[
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('onehot', OneHotEncoder(handle_unknown='ignore'))
    ]
)

ColumnTransformerの作成

カラムごとの Pipeline を ColumnTransformer にまとめます。

columns_transformers = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, number_columns),
        ('many_kinds', many_kinds_categorical_transformer, many_kinds_category_columns),
        ('few_kinds', few_kinds_categorical_transformer, few_kinds_category_columns)
    ]
)

パイプラインの作成

学習モデルと ColumnTransformer をまとめた Pipeline を作成します。

step = [
        ("columns_transformers", columns_transformers),
        ('model', lgb.LGBMRegressor(random_state=42))
     ]

# パイプラインの作成
pipe = Pipeline(
    step
)

学習・推論

pipe.fit(X_train, y_train)

# 推論
y_pred = pipe.predict(X_test)

GroupKFold で Pipeline を実行

# 学習・推論
gkf = GroupKFold(n_splits=5)

groups = X_train["Genre"]

best_params, history = {}, []

cv_result = []

for i, (train_index, test_index) in enumerate(gkf.split(X_train, y_train, groups)):
    X_train_gkf, X_test_gkf = X_train.iloc[train_index], X_train.iloc[test_index]
    y_train_gkf, y_test_gkf = y_train.iloc[train_index], y_train.iloc[test_index]

    # 学習、推論
    pipe.fit(X_train_gkf, y_train_gkf)

    y_pred = pipe.predict(X_test_gkf)

    rmse = mean_squared_error(y_test_gkf, y_pred, squared=False)
    cv_result.append(rmse)

print("RMSE:", cv_result)
print("RMSE:", np.mean(cv_result))

Pipeline を使ってStacking

下記の様に設定することで、Pipeline を使って Stacking することも可能です。

for i, (model_name, model) in enumerate(models.items()):

    print(i, model)

    # パイプライン全体の設定
    step = [
        ("columns_transformers", columns_transformers),
        ('model', model)
     ]

         # パイプラインの作成
    pipe = Pipeline(
        step
    )

一層目 学習用の予測結果を作成

Stacking 用に一層目の各種モデルの予測データを作成します。

from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import ElasticNet
from sklearn.svm import SVR
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import RandomForestRegressor
import xgboost as xgb
from catboost import CatBoostRegressor

models = {
    "ridge":Ridge(random_state=SEED),
    "lasso":Lasso(random_state=SEED),
    "linear":LinearRegression(),
    "elastic_net":ElasticNet(random_state=SEED),
    "svm":SVR(),
    "random_forest":RandomForestRegressor(random_state=SEED),
    "gradient":GradientBoostingRegressor(random_state=SEED),
    "catboost":CatBoostRegressor(random_state=SEED, 
                                 silent=True, # ログを非表示
                                 ),
    "xgboost":xgb.XGBRegressor(
        random_state=SEED,
        objective='reg:squarederror'
        ),
    "lightgbm":lgb.LGBMRegressor(random_state=SEED),
}

gkf = GroupKFold(n_splits=5)
groups = X_train["Genre"]

cv_result_stck = {}
pred_df = pd.DataFrame()

for i, (model_name, model) in enumerate(models.items()):

    print(i, model)

    # パイプライン全体の設定
    step = [
        ("columns_transformers", columns_transformers),
        ('model', model)
     ]

    # パイプラインの作成
    pipe = Pipeline(
        step
    )

    each_model_df = pd.DataFrame()
    each_model_result = []

    for train_index, test_index in gkf.split(X_train, y_train, groups):

        X_train_gkf, X_test_gkf = X_train.iloc[train_index], X_train.iloc[test_index]
        y_train_gkf, y_test_gkf = y_train.iloc[train_index], y_train.iloc[test_index]

        pipe.fit(X_train_gkf, y_train_gkf)
        y_pred = pipe.predict(X_test_gkf)

        tmp_df = pd.DataFrame(
                        [y_pred],
                        columns=test_index
                    )

        rmse = mean_squared_error(y_test_gkf, y_pred, squared=False)
        each_model_result.append(rmse)
        each_model_df = pd.concat([each_model_df , tmp_df.T]) # 各KFold ごとの予測結果をDataFrameに縦に並べる

    cv_result_stck[model_name] = each_model_result # 各モデルのRMSEを集計
    each_model_df.columns = [model_name] # カラム名をモデル名に変更
    pred_df = pd.concat([pred_df, each_model_df.sort_index()], axis=1) # 予測結果集計用DataFrameに各モデルの予測結果をくっつける

ベストモデルを選択します。

best_model_score = 9999
best_model_name = ""

for model_name, rmse in cv_result_stck.items():
    print(model_name, np.mean(rmse))

    if best_model_score > np.mean(rmse):
        best_model_score = np.mean(rmse)
        best_model_name = model_name

print()
print("Best Model is ", best_model_name, "! Best Score is ", best_model_score, "!")

二階層目 各モデルの予測結果をもとに予測

一層目で作成した各種モデルの予測データをもとに学習・推論を行います。

# 学習・推論
gkf = GroupKFold(n_splits=5)

y_train
y_test

groups = pred_df[best_model_name] # 最もスコアの良かった model_name をGroup に設定

cv_result_stck = []

step = [
    ("numeric_transformers", numeric_transformer), # 全て数値カラムなので、数値カラムのTransformerに変更
    ('model', models[best_model_name]) # 最もスコアの良かったモデルを使用
]

# パイプラインの作成
pipe = Pipeline(
    step
)

for train_index, test_index in gkf.split(pred_df, y_train, groups):

    X_train_gkf, X_test_gkf = pred_df.iloc[train_index], pred_df.iloc[test_index]
    y_train_gkf, y_test_gkf = y_train.reset_index(drop=True).iloc[train_index], y_train.reset_index(drop=True).iloc[test_index]

    pipe.fit(X_train_gkf, y_train_gkf)
    y_pred = pipe.predict(X_test_gkf)

    rmse = mean_squared_error(y_test_gkf, y_pred, squared=False)
    cv_result_stck.append(rmse)

16日目は以上になります、最後までお読みいただきありがとうございました。

Discussion