このブログのいくつかの記事で、「四角い箱の中に処理の名前を書いて矢印でつなげた図」を使いました。パイプラインとは、こういった処理のフローのことです。一方でモジュール化とは、「機能的にまとまった単位で分割し、入力->出力の形式のプロシージャとして定義すること」として考えます。ここでは、モジュール化とパイプラインがなぜデータサイエンスにおいて重要なのかを書きます。
2つのパイプライン
上記の図は、2つのパイプラインを表しています。一つは、モデルの訓練までの流れで、もう一方は訓練したモデルをつかった予測を行う流れです。
この2つのパイプラインの共通点は、データ取得と特徴量設計の部分であることがわかります。
これらのボックスの中身を、それぞれモジュール化しておくことが重要である、というのがこれから説明しようと思っていることです。
モジュール化の利点
これらのパイプラインの各ボックスをモジュール化することにどんな意味があるのでしょうか。
特徴量設計の同一性
まず、共通点に着目すれば、データ取得の部分と特徴量設計の部分をそれぞれモジュール化することにより、同じ方法でデータが定義されたことが保証できます。訓練済みのモデルは同じ特徴量を用いることが前提となるため、このようなモジュール化によってデータ生成が共通化されることで、間違ったデータが渡されることがなくなります。
交換可能性
次に、訓練や予測のモジュール化の利点は、異なる機械学習アルゴリズムと交換できることがその利点の一つです。scikit-learnでは、Estimatorというインタフェースにより、機械学習アルゴリズムがfitやpredictといった共通のメソッドを持つことを保証しています。これらのメソッドの入力や出力は共通しているので、他の機械学習アルゴリズムに交換することが容易に行えるというわけです。
保守性と簡潔さ
最終的に、何らかの形でモデルが運用されなければなりません。例えば、Web APIによる運用がその一つですが、APIは「データを受け取り、予測結果のjsonを出力する」といった簡潔な形になります。パイプラインが明確に定義され、簡潔なモジュールを結合した形になっていれば、以下のようなコードをAPIとして記述するだけで済みます。
# 簡潔な入出力によって定義されたAPI def predict(query, model, settings): data = get_data(query) X = feature_engineering(data, settings) y = model.predict(X) return y2json(y)
上記のpredict関数は少々詳細が隠されているようにも見えますが、これでいいのです。モジュール化により、APIは簡潔で可読性が高く、保守しやすいものになっています。get_data, feature_engineering, modelはそれぞれ独立しており、直交性が高いです。これらのフローが簡潔である点は、以下のように書き換えられることです。
return y2json(
model.predict(
feature_engineering(
get_data(query), settings
)
)
)
実際にはこのような書き方はしませんが、このように書けることは、データのフローが一貫していることを意味します。つまり、「入力->出力という形式のモジュールを結合する」という構造化によって複雑さを最小限に抑えられるのです。
実際的な方法
モジュール化の利点はわかりましたが、実際的には、最初からモジュール内の詳細がわかっているわけではありません。そこで、実際的な方法として以下の手順を取ることがあります:
- データ取得、特徴量設計、訓練、予測をJupyterでとりあえず実行する。
- Jupyterのコードをjupyter内でモジュール化して再実行する。
- 各モジュールを.pyファイルで分離して、Jupyter内でそれらをインポートして再実行する。
- flaskなどで3をインポートして作ったAPIのプロトタイプを作成し、実行する。
最初に詳細な設計を行うよりは、「とりあえず動くもの」から段階的に抽象化したほうが現実的です。というのも、詳細な設計を最初から行えば行うほど、設計の間違い等に起因する問題に対するリスクが高まるからです。
まとめ
- パイプラインを大雑把に設計しよう。
- とりあえず動くコードを書こう。
- 段階的に抽象化(モジュール化)しよう。