from abc import ABC, abstractmethod
class BaseEstimator(ABC):
"""Estimator Common Interface"""
@abstractmethod
def fit(self, X, y):
pass
@abstractmethod
def predict(self, X):
pass3 メッセージパッシングとポリモーフィズムの関係性
NoteTL;DRs
- クラス: 変数とサブルーチン(処理)をひとまとめにして独立性の高い部品を作る仕組み
- 継承: コードの重複を排除する仕組み
- ポリモーフィズム: 汎用性の高い部品を作る仕組み
メッセージパッシング
Definition 3.1 メッセージパッシング
- オブジェクトに対して「メソッド呼び出し(=メッセージ)」を送ること
- メッセージを送られた側が何をするかは,基本的にメッセージを送られた側(=メソッドを呼び出された型)の判断事項
model.predict(X)というメソッド呼び出しは
- 「predict」というメッセージを
modelに送る modelがそのメッセージにどう応えるかはmodel側の実装に依存
相手が具体的にどのクラスのインスタンスであるかは意識せず,メッセージを送られたオブジェクトがメッセージに応えられるかを重要視する考え方という感じです.
ポリモーフィズム
Definition 3.2 ポリモーフィズム
- 類似したクラスに対するメッセージの送り方を共通にする仕組み
- オブジェクト間で統一されたインターフェース(同じ名称のメソッド)を有するが,そのメソッドの動作はオブジェクトごとに異なるという実装になる
典型的なポリモーフィズムの一つとして,継承ベースのポリモーフィズムがあります.継承ベースでは基底クラスを継承した派生クラスが共通インターフェースを実装します.
Example 3.1 (統計パッケージと継承ベースのポリモーフィズム)
まず推定量の抽象クラスとなる基底クラスを考えます
これは統計推定量クラスの共通API(fit, predict)を強制するための抽象クラスになります.インターフェースを統一することで Pipeline,複数モデルの比較,グリッドサーチといった処理を共通の呼び出し方法で実現できるようになるメリットがあります.
次に,この基底クラスを継承する形で LinearRegression と KernelRegression の2つの推定量クラスを実装します..
import numpy as np
from sklearn.metrics.pairwise import rbf_kernel
# -----------------------------
# OLS Regression
# -----------------------------
class LinearRegression(BaseEstimator):
def fit(self, X, y):
# add constant
X_ = np.c_[np.ones(X.shape[0]), X]
# OLS regression
self.coef_ = np.linalg.pinv(X_.T @ X_) @ X_.T @ y
return self
def predict(self, X):
X_ = np.c_[np.ones(X.shape[0]), X]
return X_ @ self.coef_
# -----------------------------
# Kernel Regression
# -----------------------------
class KernelRegression(BaseEstimator):
def __init__(self, gamma=1.0):
self.gamma = gamma
def fit(self, X, y):
"""
K(x, y) = exp(-gamma ||x-y||^2)
"""
self.X_train = X
self.y_train = y
# use gaussian kernel
self.K = rbf_kernel(X, X, gamma=self.gamma)
# Moore-Penrose pseudoinverse to solve the least squares problem.
self.alpha_ = np.linalg.pinv(self.K) @ y
return self
def predict(self, X):
K_test = rbf_kernel(X, self.X_train, gamma=self.gamma)
return K_test @ self.alpha_上記の統一APIを活用して予測を行います
# ダミーデータ
X = np.random.randn(100, 2)
y = 2*X[:,0] - X[:,1] + np.random.randn(100)*0.1
models = [LinearRegression(), KernelRegression(gamma=0.5)]
for model in models:
model.fit(X, y)
print(model.predict(X[:3]))[1.26715646 1.93175836 1.1192046 ]
[1.27131283 2.14333344 0.96539307]
継承によらないポリモーフィズム
Pythonは継承によるポリモーフィズムも利用できますが,型に依存しない振る舞いベースのポリモーフィズムも利用できます. このスタイルをダックタイピングと呼びます.
Definition 3.3 ダックタイピング
- 「もしもそれがアヒルのように歩き,アヒルのように鳴くのなら,それはアヒルに違いない」
- オブジェクトの型や継承関係をチェックするのではなく,必要なメソッドが実装されているかどうかだけに注目するプログラミングスタイル
- インタフェース(振る舞い)を満たしていれば,そのクラスが何を継承していても,していなくても構わない
Example 3.2 (fit と predict を持つなら何でも推定量として扱う)
def evaluate_estimator(estimator, X_train, y_train, X_test, y_test):
# Don't check whether estimator object inherit a specific class
estimator.fit(X_train, y_train)
y_pred = estimator.predict(X_test)
# Return MSE
return ((y_pred - y_test) ** 2).mean()fit と predict を持つなら何でも推定量として扱っています.なにかしらの BaseEstimator を継承しているかどうかは問いません.