FeatureUnionを使って機械学習パイプライン内で複数の変換結果を連結する

2021.09.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

データアナリティクス事業本部の鈴木です。

今回は、sklearn.pipelineモジュールから、同一のカラムに異なるtransformerを適用し、その結果を結合する際に利用できるFeatureUnionを紹介します。

FeatureUnionとは

入力データに対して、複数のtransformerを適用し、結果をaxis=1方向に連結します。

sklearn.pipeline.FeatureUnion — scikit-learn documentation

pipeline.Pipelineでは変換を適用する順序を定義しましたが、FeatureUnionはパイプライン内で作成する特徴をまとめる役割をします。

準備

検証した環境

  • コンテナ:jupyter/datascience-notebook
  • scikit-learn:0.24.2

データの作成

今回は、sklearn.datasetsモジュールのアイリスデータセットを使います。アイリスデータセットは言わずと知れた有名なデータセットで、3種のアヤメの情報が合計150サンプル分入っています。

動作検証に使えるよう、アイリスデータをデータフレームに変換します。

import numpy as np
import pandas as pd
from sklearn import datasets

# seedの固定
np.random.seed(42)

# アヤメのデータをsklearnから読み出す。
iris = datasets.load_iris()

# pandasデータフレームに変換する。
# カラム名にスペースが入っていたので、扱いやすい名前に変えた。
feature_names = ["sepal_length", "sepal_width", 
                 "petal_length", "petal_width"]
df_iris = pd.DataFrame(iris["data"], columns=feature_names)

# 各行が何の種類なのか取り出す。
df_iris['species'] = iris.target_names[iris["target"]]

今回は、FeatureUnionの利用例として、欠損値がある際に、以下のような前処理を行うことを考えます。

  • 欠損値を中央値で埋める。
  • 欠損値があったことを表す特徴量を追加する。

アイリスデータには欠損値がないため、検証用にpetal_widthカラムに欠損値を手作りします。

# 10%をランダムにNoneに置き換える。
df_iris["petal_width"] = [item if np.random.rand() < 0.9 
                          else None for item in df_iris["petal_width"]]

欠損値がランダムに生み出されているので、上記の前処理の意義はあまりありませんが、今回は例ということでご容赦ください。

意図通り加工ができていることを確認します。petal_widthカラムには12レコード分の欠損値があることが分かります。

df_iris.info()

## <class 'pandas.core.frame.DataFrame'>
## RangeIndex: 150 entries, 0 to 149
## Data columns (total 5 columns):
##  #   Column        Non-Null Count  Dtype  
## ---  ------        --------------  -----  
##  0   sepal_length  150 non-null    float64
##  1   sepal_width   150 non-null    float64
##  2   petal_length  150 non-null    float64
##  3   petal_width   138 non-null    float64
##  4   species       150 non-null    object 
## dtypes: float64(4), object(1)
## memory usage: 6.0+ KB

後から前処理結果と比較しやすいよう、最初の10行を確認しておきます。

df_iris["petal_width"].head(10)

## 0    0.2
## 1    0.2
## 2    0.2
## 3    0.2
## 4    0.2
## 5    NaN
## 6    0.3
## 7    0.2
## 8    NaN
## 9    0.1
## Name: petal_width, dtype: float64

やってみる

FeatureUnionの動作確認

まずは簡単な例から確認します。以下の変換を行うFeatureUnionのインスタンスを使り、petal_widthカラムに適用します。

  • 欠損値を中央値で埋める。
  • 欠損値があったことを表す特徴量を追加する。
from sklearn.impute import MissingIndicator
from sklearn.impute import SimpleImputer
from sklearn.pipeline import FeatureUnion

# petal_widthカラムの欠損値を中央値で埋め、さらに欠損値の有無を表す特徴を作成する。
petal_width_union = FeatureUnion([("SimpleImputer", SimpleImputer(strategy='median')),
                                  ("MissingIndicator", MissingIndicator())
                                  ])
                                  
# petal_widthカラムを変換するためにColumnTransformerを作成する。
ct = ColumnTransformer(
        transformers=[
            ('petal_width_transformer', petal_width_union, ["petal_width"])
        ])

# 変換を行う。
ct.fit_transform(df_iris) 
## array([[0.2, 0. ],
##        [0.2, 0. ],
##        [0.2, 0. ],
##        [0.2, 0. ],
##        [0.2, 0. ],
##        [1.3, 1. ],
##        [0.3, 0. ],
##        [0.2, 0. ],
##        [1.3, 1. ],
##        [0.1, 0. ],
## ...

fit_transformの結果から、期待した変換ができていることを確認できます。

make_unionを使うことで、各ステップに名前を付けずにFeatureUnionを構築することも可能です。

from sklearn.pipeline import make_union
from sklearn import set_config
set_config(display='diagram')  

make_union(SimpleImputer(strategy='median'), 
           MissingIndicator())

make_unionの結果のダイアグラム

パイプラインの作成

次に、機械学習パイプラインに組み込んだ例も試してみます。

  • petal_widthカラムには2種類の変換を行う。
    • 欠損値を中央値で埋め、標準化する。
    • 欠損値があったことを表す特徴量を追加する。
  • petal_widthカラム以外は標準化する。
from sklearn.compose import ColumnTransformer
from sklearn.compose import make_column_selector
from sklearn.experimental import enable_hist_gradient_boosting
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.impute import MissingIndicator
from sklearn.impute import SimpleImputer
from sklearn.pipeline import FeatureUnion
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

# petal_widthカラム用の変換
petal_width_transformer = Pipeline(steps=[
    ('petal_width_imputer', SimpleImputer(strategy='median')),
    ('petal_width_scaler', StandardScaler())])

# petal_widthカラム用の変換と追加する特徴量用の変換のFeatureUnion
union = FeatureUnion([("petal_width_transformer", petal_width_transformer),
                      ("petal_width_missing_indicator", MissingIndicator())])

# 各カラムに適用する変換を定義
ct = ColumnTransformer(
    transformers=[
        ('num_with_null', union, ["petal_width"]),
        ('num_without_null', StandardScaler(), 
         ["sepal_length", "sepal_width", "petal_length"])
    ])

# 全体のパイプラインの作成
pipe = Pipeline([("column_transformer", ct),
                 ("Classifier", HistGradientBoostingClassifier())])

ダイアグラムは以下のようになります。

from sklearn import set_config
set_config(display='diagram')   
pipe

作成したパイプラインのダイアグラム

参考までにfitscoreを実行し、学習と推論ができることを確認しておきます。

from sklearn.model_selection import train_test_split

# 訓練・テストデータの作成
X = df_iris.drop("species", axis=1)
y = df_iris["species"]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# 学習
pipe.fit(X_train, y_train)

# テストデータに対するaccuracyを計算する。
pipe.score(X_test, y_test)
## 1.0

最後に

FeatureUnionを使って、入力した一つのカラムに対して複数の変換を行う方法を紹介しました。ColumnTransformerと組み合わせると、かなり複雑な機械学習パイプラインを構築することができるようになりました。

一つのカラムに対して複数の変換を行い、特徴量としたい場合は多いので、参考になれば幸いです。