scikit-learn 入門#

scikit-learn は Python のオープンソース機械学習ライブラリです。
様々な機械学習の手法が統一的なインターフェースで利用できるようになっています。 scikit-learn では NumPy の ndarray でデータやパラメータを取り扱うため、他のライブラリとの連携もしやすくなっています。

本章では、この scikit-learn というライブラリを用いて、データを使ってモデルを訓練し、評価するという一連の流れを解説します

機械学習の様々な手法を用いる際には、データを使ってモデルを訓練するまでに、以下の 5 つのステップがよく共通して現れます。

  • Step 1:データセットの準備

  • Step 2:モデルを決める

  • Step 3:目的関数を決める

  • Step 4:最適化手法を選択する

  • Step 5:モデルを訓練する

上記の 5 つが完了した後には、通常、訓練済みモデルによるテストデータを用いた精度検証を行います。

本章では、これらの 5 つのステップ + テストデータでの精度検証までを、scikit-learn の機能を使って簡潔に紹介します。

scikit-learn を用いた重回帰分析#

前章で NumPy を用いて実装した重回帰分析を、scikit-learn を使ってより大きなデータセットに対し適用してみましょう。

Step 1:データセットの準備#

本章では、前章までのような人工データではなく、カリフォルニアの住宅価格の表形式データ(部屋数や築年数などの8項目)+ラベル(住宅価格)の California housing dataset というデータセットを使用します。

このデータセットには20640件のサンプルが含まれており、各サンプルは以下の情報を持っています。

属性名

説明

MedInc

収入の中央値

HouseAge

築年数の中央値

AveRooms

家にある部屋数の平均値

AveBedrms

家にある寝室数の平均値

Population

人口

AveOccup

家に住んでいる人数の平均値

Latitude

緯度

Longitude

経度

MedHouseVal

住宅価格の中央値

このデータセットを用いて、 最後の MedHouseVal 以外の 8 個の指標から、 MedHouseVal を予測する回帰問題に取り組んでみましょう。このデータセットは、scikit-learn の fetch_california_housing()という関数を呼び出すことで読み込むことができます。

from sklearn.datasets import fetch_california_housing

dataset = fetch_california_housing()

読み込んだデータセットは、data という属性と target という属性を持っており、それぞれに入力値と目標値を並べた ndarray が格納されています。 これらを取り出して、それぞれ x と t という変数に格納しておきましょう。

x = dataset.data
t = dataset.target

入力値が格納されている x は、20640 個の 8 次元ベクトルを並べたものになっています。 形を確認してみましょう。

x.shape
(20640, 8)

一方 t は、各データ点ごとに 1 つの値を持つため、20640 次元のベクトルになっています。 形を確認してみましょう。

t.shape
(20640,)

データセットの分割#

ここで、まずこのデータセットを 2 つに分割します。 それは、モデルの訓練に用いるためのデータと、訓練後のモデルのパフォーマンスをテストするために用いるデータは、異なるものになっている必要があるためです。 これは、機械学習に使われる数学の章で少しだけ触れた汎化性能というものに関わる重要なことです。

ここで、例え話を使ってなぜデータセットを分割する必要があるかを説明します。 例えば、大学受験の準備のために 10 年分の過去問を購入し、一部を勉強のために、一部を勉強の成果をはかるために使用したいとします。 10 年分という限られた数の問題を使って、結果にある程度の信頼のおけるような方法で実力をチェックするには、下記の 2 つのうちどちらの方法がより良いでしょうか。

  • 10 年分の過去問全てを使って勉強したあと、もう一度同じ問題を使って実力をはかる

  • 5 年分の過去問だけを使って勉強し、残りの 5 年分の未だ見たことがない問題を使って実力をはかる

一度勉強した問題を再び解くことができると確認できても、大学受験の当日に未知の問題が出たときにどの程度対処できるかを事前にチェックするには不十分です。 よって、後者のような方法で数限られた問題を活用する方が、本当の実力をはかるには有効でしょう。

これは機械学習におけるモデルの訓練と検証でも同様に言えることです。 実力をつけるための勉強に使うデータの集まりを、訓練用データセット (training dataset) といい、実力をはかるために使うデータの集まりを、テスト用データセット (test dataset) と言います。 このとき、訓練用データセットとテスト用データセットに含まれるデータの間には、重複がないようにします。

早速、さきほど用意した xt を、訓練用データセットとテスト用データセットに分割しましょう。 どのように分けるかには色々な方法がありますが、単純に全体の何割かを訓練用データセットとし、残りをテスト用データセットとする、といった分割を行う方法はホールドアウト法 (holdout method) と呼ばれます。 scikit-learn では、データセットから指定された割合(もしくは個数)のデータをランダムに抽出して訓練用データセットを作成し、残りをテスト用データセットとする処理を行う関数が提供されています。

# データセットを分割する関数の読み込み
from sklearn.model_selection import train_test_split

# 訓練用データセットとテスト用データセットへの分割
x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.3, random_state=0)

ここで、train_test_split()test_size という引数に 0.3 を与えています。 これはテスト用データセットを全体の 30% のデータを用いて作成することを意味しています。 自動的に残りの 70% は訓練用データセットとなります。 上のコードは全サンプルの中からランダムに 70% を訓練データとして抽出し、残った 30% をテストデータとして返します。

例えば、データセット中のサンプルが、目標値が 1 のサンプルが 10 個、2 のサンプルが 8 個、3 のサンプルが 12個…というように、カテゴリごとにまとめられて並んでいることがあります。 そのとき、データセットの先頭から 18 個目のところで訓練データとテストデータに分割したとすると、訓練データには目標値が 3 のデータが 1 つも含まれないこととなります。

そこで、ランダムにデータセットを分割する方法が採用されています。 random_state という引数に毎回同じ整数を与えることで、実行のたびに結果が変わることを防いでいます。

それでは、分割後の訓練データを用いてモデルの訓練、精度の検証を行いましょう。

Step 2 ~ 4:モデル・目的関数・最適化手法を決める#

scikit-learn で重回帰分析を行う場合は、LinearRegression クラスを使用します。 sklearn.linear_model 以下にある LinearRegression クラスを読み込んで、インスタンスを作成しましょう。

from sklearn.linear_model import LinearRegression

# モデルの定義
reg_model = LinearRegression()

上記のコードは、前述の 2 〜 4 までのステップを行います。 LinearRegression は最小二乗法を行うクラスで、目的関数や最適化手法もあらかじめ内部で用意されたものが使用されます。 詳しくはこちらのドキュメントを参照してください。 参考:sklearn.linear_model.LinearRegression

Step 5:モデルの訓練#

次にモデルの訓練を行います。 scikit-learn に用意されている手法の多くは、共通して fit() というメソッドを持っており、 再利用可能なコードが書きやすくなっています。

reg_model を用いて訓練を実行するには、fit() の引数に入力値 x と目標値 t を与えます。

# モデルの訓練
reg_model.fit(x_train, t_train)
LinearRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

モデルの訓練が完了しました。 求まったパラメータの値を確認してみましょう。 重回帰分析では、重み w とバイアス b の2つがパラメータでした。 求まった重み w の値は model.coef_ に、バイアス b の値は model.intercept_ に格納されています。

# 訓練後のパラメータ w
reg_model.coef_
array([ 4.46773975e-01,  9.18409990e-03, -1.18116775e-01,  6.42290879e-01,
       -9.37026507e-06, -4.08535934e-03, -4.09023312e-01, -4.23419564e-01])
# 訓練後のバイアス b
reg_model.intercept_
-36.01222888765237

モデルの訓練が完了したら、精度の検証を行います。 LinearRegression クラスは score() メソッドを提供しており、入力値と目標値を与えると訓練済みのモデルを用いて計算した決定係数 (coefficient of determination) という指標を返します。

これは、使用するデータセットのサンプルサイズを \(N\)\(n\) 個目の入力値に対する予測値を \(y_{n}\)、目標値を \(t_n\)、そしてそのデータセット内の全ての目標値の平均値を \(\bar{t}\) としたとき、

\[ R^{2} = 1 - \dfrac{\sum_{n=1}^{N}\left( t_{n} - y_{n} \right)^{2}}{\sum_{n=1}^{N}\left( t_{n} - \bar{t} \right)^{2}} \]

で表される指標です。

決定係数の最大値は 1 であり、値が大きいほどモデルが与えられたデータに当てはまっていることを表します。

# 精度の検証
reg_model.score(x_train, t_train)
0.6112941337977225

訓練済みモデルを用いて訓練用データセットで計算した決定係数は、およそ 0.765 でした。

新しい入力値に対する予測の計算(推論)#

訓練が終わったモデルに、新たな入力値を与えて、予測値を計算させるには、predict() メソッドを用います。 訓練済みのモデルを使ったこのような計算は、推論 (inference) と呼ばれることがあります。 今回は、訓練済みモデル reg_model を用いて、テスト用データセットからサンプルを 1 つ取り出し、推論を行ってみましょう。 このとき、predict() メソッドに与える入力値の ndarray の形が (サンプルサイズ, 各サンプルの次元数) となっている必要があることに気をつけてください。

reg_model.predict(x_test[:1])
array([2.2702672])

この入力に対する目標値を見てみましょう。

t_test[0]
1.369

1.369 という目標値に対して、およそ 2.270 という予測値が返ってきました。

テスト用データセットを用いた評価#

それでは、訓練済みモデルの性能を、テスト用データセットを使って決定係数を計算することで評価してみましょう。

reg_model.score(x_test, t_test)
0.5926087785518772

訓練用データセットを用いて算出した値(およそ 0.611)よりも、低い値がでてしまいました。

教師あり学習の目的は、訓練時には見たことがない新しいデータ、ここではテスト用データセットに含まれているデータに対しても、高い性能を発揮するように、モデルを訓練することです。 逆に、訓練時に用いたデータに対してはよく当てはまっていても、訓練時に用いなかったデータに対しては予測値と目標値の差異が大きくなってしまう現象を、過学習 (overfitting) と言います。

過学習を防ぐために、色々な方法が研究されています。 ここでは、データに前処理を行い、テスト用データセットを用いて計算した決定係数を改善します。

各ステップの改善#

Step 1 の改善:前処理#

前処理 (preprocessing) とは、欠損値の補完、外れ値の除去、特徴量選択、正規化などの処理を訓練を開始する前にデータセットに対して行うことです。

手法やデータに合わせた前処理が必要となるため、適切な前処理を行うためには手法そのものについて理解している必要があるだけでなく、使用するデータの特性についてもよく調べておく必要があります。

今回のデータは、入力値の値の範囲が指標ごとに大きく異なっています。 そこで、各入力変数ごとに平均が 0、分散が 1 となるように値をスケーリングする標準化 (standardization) をおこなってみましょう。

scikit-learn では sklearn.preprocessing というモジュール以下に StandardScaler というクラスが定義されています。 今回は、これを用いてデータセットに対し標準化を適用します。 それでは、StandardScaler クラスを読み込み、インスタンスを作成します。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

標準化を行うためには、データセットの各入力変数ごとの平均と分散の値を計算する必要があります。 この計算は、scaler オブジェクトがもつ fit() メソッドを用いて行います。 引数には、平均・分散を計算したい入力値の ndarray を渡します。

scaler.fit(x_train)
StandardScaler()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

すべてのサンプルではなく、訓練用データセットのみを用いてこれらの値を算出します。 先ほどの fit() の実行の結果、算出された平均値が mean_ 属性に、分散が var_ 属性に格納されているので、確認してみましょう。

# 平均
scaler.mean_
array([ 3.86666741e+00,  2.86187016e+01,  5.42340368e+00,  1.09477484e+00,
        1.42515732e+03,  3.04051776e+00,  3.56300934e+01, -1.19566647e+02])
# 分散
scaler.var_
array([3.57623030e+00, 1.58665727e+02, 5.23284007e+00, 2.04970135e-01,
       1.32144307e+06, 4.71684495e+01, 4.57979889e+00, 4.02611175e+00])

これらの平均・分散の値を使って、データセットに含まれる値に標準化を施すには、transform() メソッドを使用します。

x_train_scaled = scaler.transform(x_train)
x_test_scaled  = scaler.transform(x_test)

それでは、標準化を行ったデータを使って、同じモデルを訓練してみましょう。

reg_model = LinearRegression()

# モデルの訓練
reg_model.fit(x_train_scaled, t_train)
LinearRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
# 精度の検証(訓練データ)
reg_model.score(x_train_scaled, t_train)
0.6112941337977225
# 精度の検証(テストデータ)
reg_model.score(x_test_scaled, t_test)
0.5926087785518777

結果は変わりませんでした。

べき変換をする別の前処理を適用し、再度同じモデルの訓練を行ってみましょう。

from sklearn.preprocessing import PowerTransformer

scaler = PowerTransformer()
scaler.fit(x_train)

x_train_scaled = scaler.transform(x_train)
x_test_scaled = scaler.transform(x_test)

reg_model = LinearRegression()
reg_model.fit(x_train_scaled, t_train)
LinearRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
# 訓練データでの決定係数
reg_model.score(x_train_scaled, t_train)
0.6229916507316948
# テストデータでの決定係数
reg_model.score(x_test_scaled, t_test)
0.6131647319379137

結果が少し改善しました。 ここで、目標値である MedHouseValにも前処理として対数変換を適用し再度同じモデルの訓練を行ってみましょう。尚、目的変数に変換をした場合、精度を確認するときにはモデルの予測に逆変換を行って元のスケールで行います。sklearnのTransformerTargetRegressorを用いると目的変数の対数変換とその逆変換を学習の前後に自動的に行なってくれます。

import numpy as np
from sklearn.compose import TransformedTargetRegressor

reg_model = TransformedTargetRegressor(
    regressor=LinearRegression(), # モデルにはLinearRegressionを指定
    func=np.log1p, # 学習前に行う変換の関数を指定
    inverse_func=np.expm1 # 学習後の精度確認前に行う逆変換の関数を指定
)
reg_model.fit(x_train_scaled, t_train)
TransformedTargetRegressor(func=<ufunc 'log1p'>, inverse_func=<ufunc 'expm1'>,
                           regressor=LinearRegression())
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
# 訓練データでの決定係数
reg_model.score(x_train_scaled, t_train)
0.6658173722018323
# テストデータでの決定係数
reg_model.score(x_test_scaled, t_test)
0.6575264578077608

結果が改善しました。

パイプライン化#

前処理用の scaler と 重回帰分析を行う reg_model は、両方 fit() メソッドを持っていました。 scikit-learn には、パイプラインと呼ばれる一連の処理を統合できる機能があります。 これを用いて、これらの処理をまとめてみましょう。

from sklearn.pipeline import Pipeline

# パイプラインの作成 (scaler -> reg)
pipeline = Pipeline([
    ('scaler', PowerTransformer()),
    ('reg', TransformedTargetRegressor(
        regressor=LinearRegression(), # モデルにはLinerRegressionを指定
        func=np.log1p, # 学習前に行う変換の関数を指定
        inverse_func=np.expm1 # 精度確認前に行う逆変換の関数を指定
    ))
])
# scaler および reg を順番に使用
pipeline.fit(x_train, t_train)
Pipeline(steps=[('scaler', PowerTransformer()),
                ('reg',
                 TransformedTargetRegressor(func=<ufunc 'log1p'>,
                                            inverse_func=<ufunc 'expm1'>,
                                            regressor=LinearRegression()))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
# 訓練用データセットを用いた決定係数の算出
pipeline.score(x_train, t_train)
0.6658173722018323
# テスト用データセットを用いた決定係数の算出
linear_result = pipeline.score(x_test, t_test)

linear_result
0.6575264578077608

パイプライン化を行うことで、x_train_scaled のような中間変数を作成することなく、同じ処理が行えるようになりました。 これによってコード量が減らせるだけでなく、評価を行う前にテスト用データセットに対しても訓練用データセットに対して行ったのと同様の前処理を行うことを忘れてしまうといったミスを防ぐことができます。