動機
うちの会社でJDLA E資格対策のために使わせて頂いている教材に特になんの説明も無くfunctools.partial
が出てきていて、歴代の受験者が
なんだろう、コレ...?
となっていたので解説記事を作ることにした。
やりたいこと
単純な線形回帰問題を最急降下法か何かで実装しようとしているとする。
線形回帰の仮説(hypothesis
)をこんな感じで実装した。
import numpy as np
def hypothesis(X, W, b):
"""仮説関数
線形回帰の仮説(WX + b)を計算して結果を返す
Args:
X (numpy.ndarray): 学習データ(データ数, 項目数)
W (numpy.ndarray): 重み(データの項目数,1)
b (numpy.ndarray): バイアス(1,1)
Returns:
numpy.ndaray: 仮説の計算結果(データ数,)
"""
# WX+bを行列の積で計算するためにXの列にバイアス項(全て1)を繋げる
X_with_bias = np.c_[X, np.ones((X.shape[0],1))]
# バイアス項を繋げたXとW,bを行方向に繋げた行列の積を取る
return X_with_bias.dot(np.r_[W,b])
例えば、このように使用する。
X = np.array([[7,5],[8,10],[8.5,1]]) #学習データ(テキトーです)
W = np.random.randn(X.shape[1]) #重みの初期化
b = np.zeros(1) #バイアスの初期化
y = hypothesis(X, W, b)
なんやかんやで学習が収束して、精度高く予想できる重みとバイアスが決まったとする。
learned_W = np.array([0.8,-0.05]) #学習済みの重み(テキトー)
learned_b = np.array([-0.8]) #学習済みのバイアス(テキトー)
学習が終わったので、このモデルを使って実際に予測を行いたい。予測したい入力X
をX_to_predict
と置いたとき、
X_to_predict = np.array([[7,10],[10,1]]) #予測したい入力X(テキトー)
予測値(prediction
)はhypothesis
関数と学習済みのlearned_W
やlearned_b
を使ってこのように取得することができる。
prediction = hypothesis(X_to_predict, learned_W, learned_b)
しかし、learned_W
やlearned_b
は学習済みの重み/バイアスでもはや固定値なので、毎回予測をするたびに引数に与えるのは面白くない。そこで、learned_W
やlearned_b
を引数に渡さずに入力X
だけを渡して予測値を返すpredict
関数を作りたい。
# こんな関数が欲しい!
prediction = predict(X_to_predict)
意外と実装が難しい
もちろん、こんな実装はよろしくない。
def predict(X):
"""予測関数
学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を使用した予測値を返す
Args:
X (numpy.ndarray): 予測したいデータ(データ数, 項目数)
Returns:
numpy.ndaray: 予測結果(データ数,)
"""
# WX+bを行列の積で計算するためにXの列にバイアス項(全て1)を繋げる
X_with_bias = np.c_[X, np.ones((X.shape[0],1))]
# バイアス項を繋げたXとW,bを行方向に繋げた行列の積を取る
return X_with_bias.dot(np.r_[learned_W,learned_b])
明らかにDRY原則に反しているので、もう少しなんとかしたい。
せめて、先に実装したhypothesis
関数を使って、
def predict(X):
"""予測関数
学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を使用した予測値を返す
Args:
X (numpy.ndarray): 予測したいデータ(データ数, 項目数)
Returns:
numpy.ndaray: 予測結果(データ数,)
"""
# learned_Wとlearned_bを決め打ちしてhypothesis関数を呼び出す
return hypothesis(X, learned_W, learned_b)
こんな感じでどうだろうか。
『なかなかシンプルな実装でいいんじゃなかろうか』などと思っていると、後ろに怖い先輩が立っている。
お前、グローバル変数に依存してるのはどういうことだ!?結合度って概念知ってるか?
この関数どうやってユニットテストする気だ?えぇ!?
...なるほど。どうやって解決しようか。
learned_W
やlearned_b
をpredict
関数の引数に戻してしまったら意味が無いわけで。。
そうだ!Pythonは広い意味での関数型言語なので、関数型のテクニックを使って解決しよう。
クロージャを作ってlearned_W
やlearned_b
のスコープを分離してしまえばいい。
def create_predict_func(learned_W, learned_b):
"""予測関数を作成する関数
hypothesis関数に学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を適用して、
predict(予測)関数を作成する
Args:
learned_W (numpy.ndarray): 学習済みの重み(データの項目数,1)
learned_b (numpy.ndarray): 学習済みのバイアス(1,1)
Returns:
func : 予測関数
Args:
X: 予測したいデータ(データ数, 項目数)
Returns:
numpy.ndaray: 予測結果(データ数,)
"""
# Xを引数にとってhypothesisの戻り値を返す関数を返す
return lambda X: hypothesis(X, learned_W, learned_b)
# (参考) lambda式を使わない別解
#def predict(X):
# return hypothesis(X, learned_W, learned_b)
#
#return predict
これをこのように使う。
# learned_Wとlearned_bを指定してpredict関数を作る
predict = create_predict_func(learned_W, learned_b)
# 一度作ったpredict関数は今後何度でもXのみで(learned_Wやlearned_bをいちいち指定せず)呼べる
prediction = predict(X_to_predict)
# (参考)
# 今回は解説のためにわざわざcreate_predict_func関数を作ったが、以下でも良い
#
# # learned_Wとlearned_bを指定してpredict関数を作る
# predict = lambda X: hypothesis(X, learned_W, learned_b)
# # 一度作ったpredict関数は今後何度でもXのみで(learned_Wやlearned_bをいちいち指定せず)呼べる
# prediction = predict(X_to_predict)
こんなんでどうでしょう?グローバル先輩(あだ名)!
いや、hypothesisもグローバル関数だから!
...わかりましたよ。。じゃあhypothesisも引数にとって、
def create_predict_func(hypothesis, learned_W, learned_b):
"""予測関数を作成する関数
hypothesis関数に学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を適用して、
predict(予測)関数を作成する
Args:
hypothesis (func): 重みとバイアスを適用する予測関数
learned_W (numpy.ndarray): 学習済みの重み(データの項目数,1)
learned_b (numpy.ndarray): 学習済みのバイアス(1,1)
Returns:
func : 予測関数
Args:
X: 予測したいデータ(データ数, 項目数)
Returns:
numpy.ndaray: 予測結果(データ数,)
"""
# Xを引数にとってhypothesisの戻り値を返す関数を返す
return lambda X: hypothesis(X, learned_W, learned_b)
# hypothesisにlearned_Wとlearned_bを適用してpredict関数を作る
predict = create_predict_func(hypothesis, learned_W, learned_b)
# 一度作ったpredict関数は今後何度でもXのみで(learned_Wやlearned_bをいちいち指定せず)呼べる
prediction = predict(X_to_predict)
これで文句ないだろ!グロ先(あだ名)!!
いやお前さ、この実装、hypothesisの引数の順番が(X,W,b)であることに依存してんじゃん!
(W,b,X)になったらどうする気?
...もうイヤ。
あ〜あ、誰か任意の関数の引数の一部を固定した新たな関数を作ってくれる関数を用意してくれないかなぁ。。。
そこで部分適用
ということで、
ある関数の引数の一部を固定して新たな関数を作る
ことを部分適用(partial application)といい、Pythonではfunctools.partial
という関数が用意されている。
先ほどから苦労していたpredict
関数の実装はこのfunctools.partial
を使って以下のように書くことができる。
from functools import partial
# hypothesis関数の引数W,bにそれぞれlearned_W,learned_bを部分適用してpredict関数を作る
predict = partial(hypothesis, W=learned_W, b=learned_b)
# 一度作ったpredict関数は引数にXのみを渡して呼ぶことができる
prediction = predict(X_to_predict)
functools.partial
は便利なことにW=learned_W
,b=learned_b
のように順番関係なく引数名で部分適用する引数を指定できるので、先ほどのグロ先の要望も満たすことができる。
関数型のテクニックでサクッとスコープ管理を
正直なところ、functools.partial
を紹介する前の段階、クロージャを作り出したあたりからすでに実質hypothsisに対する部分適用をしており、関数型のテクニックを使ってグローバル変数への参照を避けてスコープ管理をしていた。
では、部分適用という考え方を知らず、それを用いないで今回の要件を実現するにはどうしたら良いか。もちろん、DRYでかつスコープ管理もきちんと行った上でだ。
オブジェクト指向的な考え方をする人であれば、以下のように実現するかも知れない。
class Predictor:
def __init__(self, hypothesis, learned_W, learned_b):
self._hypothesis = hypothesis
self._W = learned_W
self._b = learned_b
def predict(self, X):
return self._hypothesis(X, self._W, self._b)
predictor = Predictor(hypothesis, learned_W, learned_b)
prediction = predictor.predict(X_to_predict)
もちろん、この方法自体は全く問題無い。ただ、このオブジェクト指向的な解決方法の欠点をあげるとすれば、この程度のスコープ管理のためにわざわざclassを宣言しなければいけないことだろうか。
Java6
からそれ以前あたりのレガシーなJavaで本格的に開発したことがある方なら共感いただけると思うが、オブジェクト指向のみで真面目にスコープ管理・カプセル化をしようとすると、とにかくたくさんのclassやinterfaceを宣言しなくてはならない。
今回取り上げたPython
や最近のJava
を含め、現在のメジャーな言語のうちほとんどは程度の差こそあれオブジェクト指向と関数型のハイブリッドができるようになっているので、ケースバイケースで適切な方法でスコープ管理ができるようになると生産性が高くかつ安全にコードが組めるようになるのではなかろうか。グローバル先輩もきっとそれを伝えたかったのだろう。
それにしても、本職がプログラマでは無い人もたくさん受講するE資格対策の教材でいきなり部分適用を説明も無しに持ってくるのは流石にエグいと思う。。正直、本職のプログラマでも知らん人多いのに。。。