scikit-learn 入門

colab-logo

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

本章では、この scikit-learn というライブラリを用いて、データを使ってモデルを訓練し、評価するという一連の流れを解説し、Chainer を使ったディープラーニングの解説に入る前に、共通する重要な項目について説明します。

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

  • Step 1:データセットの準備
  • Step 2:モデルを決める
  • Step 3:目的関数を決める
  • Step 4:最適化手法を選択する
  • Step 5:モデルを訓練する

前章では、ステップ 2ステップ 3ステップ 4 & 5 という 3 ステップに分けて説明を行いましたが、この 5 つのステップに分けて考える方法は、後の章で解説する Chainer を用いたニューラルネットワークの訓練においても共通しています。 また、上記の 5 つが完了した後には、通常、訓練済みモデルによるテストデータを用いた精度検証を行いますが、この点も共通しています。

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

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

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

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

本章では、前章までのような人工データではなく、米国ボストンの 506 の地域ごとの住環境の情報などと家賃の中央値の情報を収集して作られた Boston house prices dataset というデータセットを使用します。

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

属性名 説明
CRIM 人口 1 人あたりの犯罪発生率
ZN 25,000 平方フィート以上の住宅区画が占める割合
INDUS 小売業以外の商業が占める面積の割合
CHAS チャールズ川の川沿いかどうか (0 or 1)
NOX 窒素酸化物の濃度
RM 住居の平均部屋数
AGE 1940 年より前に建てられた持ち主が住んでいる物件の割合
DIS 5 つのボストン雇用施設からの重み付き距離
RAD 環状高速道路へのアクセシビリティ指標
TAX $10,000 あたりの固定資産税率
PTRATIO 町ごとにみた教師 1 人あたりの生徒数
B 町ごとにみた黒人の比率を Bk としたときの (Bk - 0.63)^2 の値
LSTAT 給与の低い職業に従事する人口の割合
MEDV 物件価格の中央値

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

[1]:
from sklearn.datasets import load_boston

dataset = load_boston()

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

[2]:
x = dataset.data
t = dataset.target

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

[3]:
x.shape
[3]:
(506, 13)

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

[4]:
t.shape
[4]:
(506,)

データセットの分割

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

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

  • 10 年分の過去問全てを使って勉強したあと、もう一度同じ問題を使って実力をはかる
  • 5 年分の過去問だけを使って勉強し、残りの 5 年分の未だ見たことがない問題を使って実力をはかる

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

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

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

[5]:
# データセットを分割する関数の読み込み
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 クラスを読み込んで、インスタンスを作成しましょう。

[6]:
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 を与えます。

[7]:
# モデルの訓練
reg_model.fit(x_train, t_train)
[7]:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,
         normalize=False)

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

[8]:
# 訓練後のパラメータ w
reg_model.coef_
[8]:
array([-1.21310401e-01,  4.44664254e-02,  1.13416945e-02,  2.51124642e+00,
       -1.62312529e+01,  3.85906801e+00, -9.98516565e-03, -1.50026956e+00,
        2.42143466e-01, -1.10716124e-02, -1.01775264e+00,  6.81446545e-03,
       -4.86738066e-01])
[9]:
# 訓練後のバイアス b
reg_model.intercept_
[9]:
37.93710774183255

モデルの訓練が完了したら、精度の検証を行います。 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 であり、値が大きいほどモデルが与えられたデータに当てはまっていることを表します。

[10]:
# 精度の検証
reg_model.score(x_train, t_train)
[10]:
0.7645451026942549

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

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

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

[11]:
reg_model.predict(x_test[:1])
[11]:
array([24.9357079])

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

[12]:
t_test[0]
[12]:
22.6

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

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

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

[13]:
reg_model.score(x_test, t_test)
[13]:
0.6733825506400194

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

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

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

各ステップの改善

Step 1 の改善:前処理

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

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

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

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

[14]:
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

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

[15]:
scaler.fit(x_train)
[15]:
StandardScaler(copy=True, with_mean=True, with_std=True)

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

[16]:
# 平均
scaler.mean_
[16]:
array([3.35828432e+00, 1.18093220e+01, 1.10787571e+01, 6.49717514e-02,
       5.56098305e-01, 6.30842655e+00, 6.89940678e+01, 3.76245876e+00,
       9.35310734e+00, 4.01782486e+02, 1.84734463e+01, 3.60601186e+02,
       1.24406497e+01])
[17]:
# 分散
scaler.var_
[17]:
array([6.95792305e+01, 5.57886665e+02, 4.87753572e+01, 6.07504229e-02,
       1.33257561e-02, 4.91423928e-01, 7.83932705e+02, 4.26314655e+00,
       7.49911344e+01, 2.90195600e+04, 4.93579208e+00, 7.31040807e+03,
       4.99634123e+01])

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

[18]:
x_train_scaled = scaler.transform(x_train)
x_test_scaled  = scaler.transform(x_test)

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

[19]:
reg_model = LinearRegression()

# モデルの訓練
reg_model.fit(x_train_scaled, t_train)
[19]:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,
         normalize=False)
[20]:
# 精度の検証(訓練データ)
reg_model.score(x_train_scaled, t_train)
[20]:
0.7645451026942549
[21]:
# 精度の検証(テストデータ)
reg_model.score(x_test_scaled, t_test)
[21]:
0.6733825506400195

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

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

[22]:
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)
[22]:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,
         normalize=False)
[23]:
# 訓練データでの決定係数
reg_model.score(x_train_scaled, t_train)
[23]:
0.7859862562650238
[24]:
# テストデータでの決定係数
reg_model.score(x_test_scaled, t_test)
[24]:
0.7002856552456189

結果が改善しました。

パイプライン化

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

[25]:
from sklearn.pipeline import Pipeline

# パイプラインの作成 (scaler -> svr)
pipeline = Pipeline([
    ('scaler', PowerTransformer()),
    ('reg', LinearRegression())
])
[26]:
# scaler および reg を順番に使用
pipeline.fit(x_train, t_train)
[26]:
Pipeline(memory=None,
     steps=[('scaler', PowerTransformer(copy=True, method='yeo-johnson', standardize=True)), ('reg', LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,
         normalize=False))])
[27]:
# 訓練用データセットを用いた決定係数の算出
pipeline.score(x_train, t_train)
[27]:
0.7859862562650238
[28]:
# テスト用データセットを用いた決定係数の算出
linear_result = pipeline.score(x_test, t_test)

linear_result
[28]:
0.7002856552456189

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