まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

テキトーに予測したベースラインを作成するDummyRegressorとDummyRegressorを試す

DummyClassifier/DummyRegressorとは

過去記事(3. ベースラインを定める)にも書いたが、機械学習モデルを作成した際の評価は何かしらのベースラインと比較しなければその値が良いのか悪いのかわからない。
単純な比較対象として、「テキトーに予測した」結果と比較することが往々にしてある。

knknkn.hatenablog.com

この「テキトー」は、分類モデルの場合はTrainデータのラベル比率をもとに確率でラベルを決めたり、常に最頻値を出力することが多い。

イメージとしては、0/1の2値分類において、「頑張って作った機械学習モデル vs 90%は0なので90%の確率で0って言うだけのモデル」で後者と対して精度が変わらないなら機械学習モデル使う意味無いよね?みたいなイメージ。 どんだけ精度が悪くてもこれよりは高くないと意味ねぇ 、っていう意味での「ベースライン」 。

なお、このあたりの「テキトーなベースライン」の意義の詳細は以下の本のどこかに書いていた気がする。

scikit-learnとTensorFlowによる実践機械学習

scikit-learnとTensorFlowによる実践機械学習

  • 作者:Aurélien Géron
  • 発売日: 2018/04/26
  • メディア: 単行本(ソフトカバー)

github.com

この「テキトーなベースライン」は、sklearn.dummyDummyClassifierDummyRegressorで簡単に作ることができる。

scikit-learn.org

scikit-learn.org

DummyClassifier

DummyClassifierは分類モデル用のベースラインを作成する。

引数strategyを変えることで、Trainで学習したラベルをもとに

  • stratified : ラベルの比率(確率分布)をもとに予測(0, 1, 2が 3:5:1の比率で存在する場合、50%の確率で0が予測値として出力
  • most_frequent : 最頻値を常に予測値として出力
  • prior : 予測値はmost_frequentと同じだが、予測確率がラベルの比率と同じ結果になる(most_frequentは常に0か1)。
  • uniform : 完全にランダムに予測した値を返す(一様乱数)。例えば2値分類なら各ラベルになる確率はどれも50%だし、4値分類ならそれぞれ25%。
  • constant : 指定した値を予測値として常に返す

実践

データとしてはirisデータを使う。

import pandas as pd
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
import random

# データ準備
iris = datasets.load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target_names[iris.target]
# train, testを分ける
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, train_size = 0.8, shuffle = True)

X.head()

f:id:chito_ng:20200604202519p:plain:w600

このデータに対して学習と予測をおこない、評価したところ精度0.93のモデルなようだ。

# SVMでTrain
clf_svm = SVC(probability=True)
clf_svm.fit(X_train, y_train)

# 予測および評価
y_pred = clf_svm.predict(X_test)

accuracy_score(y_test, y_pred)
# => 0.9333333333333333

次に、本題のDummyClassifierを使った「テキトーなベースライン」を作成する。
作り方は、他のsklearnモデルと同様の流れでおこなうことができる。

なお、形式上fitで学習データX, yを渡しているが前述のように、確率分布などに沿った値を返すだけなのでラベルyの中身は使っているが、特徴量Xは使われていない。

# DummyClassifierで各クラス割合(確率分布)に伴う予測結果を返す
clf_dummy = DummyClassifier(strategy = 'stratified') # デフォルト

# モデル学習
# X_trainを渡しているが、中身を使ってない
clf_dummy.fit(X_train, y_train)

# 各クラス割合に伴って予測 
y_pred_dummy = clf_dummy.predict(X_test)

このときのy_trainから読み取った各ラベルの比率はsatose : versicolor : virginica = 0.325 : 0.35833333 : 0.31666667となっている。そのため、strategy = 'stratifiedより、この確率に沿って予測がおこなわれている。
なお、class_prior_を使うとそのモデルでの確率分布を見ることができる。

# y_train自体から確認
# Train用のyのラベルは以下の比率
values, count = np.unique(y_train, return_counts=True)
ratio = count / np.sum(count)

display(values, ratio)

# => array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
# => array([0.325     , 0.35833333, 0.31666667])

# fit済モデルから確認
clf_dummy.class_prior_
# => array([0.325     , 0.35833333, 0.31666667])

ではここで、先程clf_dummyで予測された結果y_predを見ると、以下のような出力比となっている*1

# 予測されたyのラベルは以下の比率
values, count = np.unique(y_pred_dummy, return_counts=True)
ratio = count / np.sum(count)

display(values, ratio)

# => array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
# => array([0.16666667, 0.46666667, 0.36666667])

これは、先程見たy_train のラベル比率と違うように見えるが120インスタンスの予測結果なので、インスタンス数を増やすと大数の法則より実際の比率に近くなることから、確率分布だということがわかる。

# yを100000つ生成したいので、テキトーにXを100000つ生成
# 大数の法則より y_trainの分布に近づく

val_100000 = [[random.uniform(0, 5)] * 4 for i in range(100000)]
X_dummy = pd.DataFrame(val_100000, columns=iris.feature_names)


y_pred_dummy = clf_dummy.predict(X_dummy)

# 予測されたyのラベルは以下の比率
values, count = np.unique(y_pred_dummy, return_counts=True)
ratio = count / np.sum(count)

display(values, ratio)

# => array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
# => array([0.32518, 0.35839, 0.31643])

前述のように予測結果は確率で出力されるゆえに、実際に使用する際は1回だけだと偏りが出ることもあるので複数回おこなった平均スコアを出してベースラインとする。

# 確率なので安定化させるために10000回施行をおこなった平均を使う
scores = []

for _ in range(10000):
    # DummyClassifierで各クラス割合(確率分布)に伴う予測
    # satose : versicolor : virginica = 0.325 : 0.358 : 0.317
    clf_dummy = DummyClassifier()

    # モデル学習
    # X_trainは中身を使ってない
    clf_dummy.fit(X_train, y_train)

    # 各クラス割合に伴って予測 
    y_pred_dummy = clf_dummy.predict(X_test)

    scores.append(accuracy_score(y_test, y_pred_dummy))
    
np.mean(scores)
# => 0.32887999999999995

このとき、比率に応じてテキトーに出力したモデルの精度は0.33ほどとなる。このことからSVMモデルの0.933は使うに値するモデルだということがわかる。

なお、最頻値の場合の精度は0.233となった。

# 確率なので安定化させるために10000回施行をおこなった平均を使う

scores = []

for _ in range(10000):
    # DummyClassifierで最頻値を予測として返す。以下より、常に`versicolor`として予測。
    # satose : versicolor : virginica = 0.325 : 0.358 : 0.317
    clf_dummy = DummyClassifier(strategy='prior')

    # モデル学習
    # X_trainは中身を使ってない
    clf_dummy.fit(X_train, y_train)

    # 各クラス割合に伴って予測 
    y_pred_dummy = clf_dummy.predict(X_test)

    scores.append(accuracy_score(y_test, y_pred_dummy))
    
np.mean(scores)
# 0.23333333333333334

DummyRegressor

次に、連続値予測モデルについて同様にベースラインを作れるDummyRegressorを考える。

こちらもstrategyを変えることで、Trainしたy(train_y)に応じた以下を出力することができる。

  • mean : 常にtrain_yの平均値を出力
  • median : 常にtrain_yの中央値を出力
  • quantile : 常にtrain_yの任意の分位点を出力
  • constant : 常に任意の値を出力

DummyRegressorのように確率をもとにせず、常に同じ値を出力するのでloopを回してscoreの安定をはかる必要はない。

実践

データとしてボストン住宅価格を用いる。

from sklearn.datasets import load_boston

dsn = load_boston()

X = pd.DataFrame(dsn.data, columns=dsn.feature_names)
y = pd.DataFrame(dsn.target, columns=['target'])

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, train_size = 0.8, shuffle = True)

np.mean(y_train.values)
# => 22.98193069306931

y_trainの平均値は22.98となっている。

回帰モデルを用いたscore結果(決定係数)は0.63になった。

from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

model_ols = LinearRegression()
model_ols.fit(X_train, y_train)
y_pred = model_ols.predict(X_test)

# RMSE
np.sqrt(mean_squared_error(y_test, y_pred))
# => 4.786190310043946

次にDummyRegressorを用いる。このとき、y_trainの平均値は22.98なので常に22.98を予測値として返すモデルを評価することになる。

from sklearn.dummy import DummyRegressor

model_dummy = DummyRegressor(strategy='mean')
model_dummy.fit(X_train, y_train)
y_pred = model_dummy.predict(X_test)

# RMSE
np.sqrt(mean_squared_error(y_test, y_pred))
# => 8.179532051041386

ここから、LinearRegressionの場合はRMSEが4.79、テキトーに平均値を返すモデルは8.18なのでLinearRegressionは半分近く誤差が少ないモデルなので使用する価値はある。

ちなみに、DummyRegressorではちゃんと予測結果は常に22.98となっている

f:id:chito_ng:20200604212106p:plain:w600

参考

blog.amedama.jp

*1:確率分布なので人によって値は違う