こんにちは。前回書いた突撃!隣のキーボード M3 2019という記事が、HHKBの公式Twitterアカウントにツイートされ、舞い上がっているエムスリーエンジニアリングGの河合 (@vaaaaanquish) です。
今回はエムスリー AIチームが開発、運用している機械学習プロジェクト向けのPythonライブラリである「gokart」の説明と、その周辺ライブラリとなる「cookiecutter-gokart」「thunderbolt」「redshells」について紹介したいと思います。よろしくお願いします。
はじめに
近年、多くの機械学習プロジェクトにおいてPipelineライブラリが用いられ、データ収集から加工、モデルの学習、推論を1つのワークフローとして取り扱うのが一般的となっています*1*2。 各社各チームで多くの機械学習エンジニア、データエンジニアが注目する1つの課題となっており、実際に日本国内でもデータパイプラインに関する勉強会などが開催され、その運用方法が共有、議論されています。
Pipelineライブラリではscikit-learn Pipelineやluigiを代表に、クラウドや分散環境を意識したDigdagやAirflowといったツールの運用事例も耳にするようになっています。近年ではGoogle Cloud AutoML、Amazon SageMakerといったクラウドサービスとして、機械学習タスクの一部を自動化、Pipeline化する試みも出てきています*3。
そんな中、エムスリー AIチームでもgokartというluigiのwrapperライブラリをOSSとして開発、運用しています。
Pipeline化のメリット・デメリット
Pipelineライブラリを利用するにあたって、いくつかのメリット・デメリットが存在すると考えています。 先述したように、Pipelineライブラリの利用は機械学習プロジェクトにおけるデファクトスタンダードになりつつありますが、導入の際には、チームや職場に環境が整っているか、各ライブラリの特性は活かせるか、慎重な検討が必要になると思います。 以下では、luigiやAirflowのようなPipelineライブラリのメリット、デメリットについて、筆者の考えを示します。
Pipeline化のメリット
機械学習プロジェクトにおいてluigiのような小さなPipelineライブラリを用いる事の利点は、各所で多く議論されていますが、私個人としては以下の3つに集約されると考えています。
- データ、モデルの再現性の確保
- タスクの共通化
- 開発とprod運用移行のしやすさ
機械学習プロジェクトの多くは、一般的なソフトウェア開発に比べ、ログからの再現が非常に困難という課題を常に持ち合わせています。これは、同じパラメータを利用したとしても元のデータの変化や環境の変化によって同等の結果を得られない場合があるといった、機械学習モデルの特性から来る課題です。また機械学習モデルの入力に用いる特徴量を生成する「前処理」と呼ばれる工程においても、その順番が変わるだけで結果が変わるなど、管理の難しい処理が多く含まれています。機械学習プロジェクト向けのPipelineライブラリの多くは、「再現性」を重要視し、データやログ、モデルの保存、実行順序を様々な形でサポートする事で、この課題を解決しています。
また、機械学習モデルの開発フェーズでは、データの取得、加工、調整、テスト等を繰り返す回数が多くなりがちです。各タスク毎にクラス化する過程での社内システムやスクリプトの共通化、テンプレート化が、全体の開発速度を高める事は明確です。実際、MLOpsの文脈では、データ基盤の整理に加えて、Pipelineライブラリによってタスクを共通化しながら、データの活用から機械学習モデル開発、リリース、運用、ビジネス活用までをシームレスにしていくといった開発スタイルが主流になりつつあります。
Pipeline化のデメリット
メリットに反して、私の考えるPipelineライブラリ利用のデメリットは、以下の2つがあります。
- データ保持や計算、セキュリティ等のリソースが嵩張る
- インフラ、ツールの選定やタスク設計の難しさ
Pipelineライブラリは、データの再現性を確保するという反面で、再現性確保のためのリソースが必要となり、それらへのコスト意識が低くなりがちです。 例えば、モデルや辞書ファイルを各タスク実行時点で保持する事は、多くのストレージ容量が必要となるでしょう。 また、計算コストの小さなタスクと大きなタスクが混在するPipelineにおいては、オートスケールするようなマシンリソースを利用していなければ、計算リソースについても嵩張る事になってしまいます。 実際にGoogle AutoMLで26万の請求が来た話やApache Airflowを使ってみたけど運用には乗らなかった話など、パイプライン化されている事によってコスト意識が減衰し、反して実コストが高まるといった事例は多く存在しています。また、「単一のDBからモデルを作る」だけの処理にも関わらず、過去のモデルを保存するストレージ、ログ用のクラウドサービス等、データを保持する場所が増えてしまう場合も多いでしょう。Pipelineライブラリを利用するにあたっては、それらのセキュリティ担保に対する人的リソースを割く必要もあります。
加えて「Pipelineとして共通化」されている事で、最も理想とする開発、運用形態が利用できないという場合もあるでしょう。Pipelineを徹底する事で、本来低コストで運用できるタスクに対しても計算リソースを割いてしまうといった状態は少なくありません。機械学習のタスクの多くは実行時間の分散が大きくなりがちでもありますし、どのPipelineツールを使うか、どのようにPipelineを組むべきか、機械学習モデル開発者が考える必要が出てきてしまいます。機械学習モデル開発者が多くの役割を持ち、データ管理、分析、インフラ開発、リリース後の運用まで見えている場合はこれらを意識できますが、逆にデータエンジニア、機械学習エンジニアと分業しているような体制では意識するのが難しくなっていくでしょう。
gokart
前述した通り機械学習プロジェクトに対するPipelineライブラリ導入にあたっては、開発状況やチーム、事業のタイミング、メリットを活かしたまま、デメリットを吸収できるかを考えなければなりません。また、Pipelineライブラリのメリット・デメリットに加えて、純粋なソフトウェアエンジニアリング、開発体制、環境に基づくメリット・デメリットもあるでしょう。そららの兼ね合いを考慮した上で、エムスリーではgokartというライブラリを開発、運用するに至っています。
gokartは、spotifyがOSSとして開発している「luigi」のwrapperとして開発しているPythonライブラリです。
エムスリーでは、過去ブログにも書いた通りluigiを用いたワークフロー開発を行っていました。 www.m3tech.blog www.m3tech.blog
その中で見えてきた先述のようなメリット、デメリットを元に、gokartは、luigiに対して以下のような機能を付与しています。
- タスク共通化のための出力ファイルの制約と拡張
- 強力かつ簡易な再現性のためのデータ保持
- クラウドサービスやSlack通知のサポート
共通化のための出力ファイル形式の制約と拡張
Pipelineライブラリを利用していたとしても、それぞれの機械学習エンジニアによって管理しているデータのフォーマットが違う(例えばAさんはfeatherを使っているがBさんはpickleメインといったシーンのようにdump形式が違ったり、各ライブラリのバージョンが違ったりといった)場合に、タスクの共通化までの障壁が無駄に大きくなってしまいます。gokartでは、タスクのoutput、loadに利用できるファイル形式を制限しています。それぞれ以下のFileProcessorクラスによって定義され、デフォルトではpickle、npz、gz、txt、csv、tsv、json、xmlをサポートしています*4。
gokart/file_processor.py at master · m3dev/gokart · GitHub
もちろん、制約した中でのメリットを作れるよう、機械学習モデリングでよく利用されるpickleをデファクトスタンダードとして扱い、ファイルサイズが大きくなる場合にファイルを自動で分割するといった拡張も加えています。この制約と拡張の機能によって、タスクの共通化や、機械学習エンジニア、データエンジニア間のコードのやり取りをスムーズに行う事ができています。
強力かつ簡易な再現性のためのデータ保持
FileProcessorで定義されたフォーマットであれば、make_targetメソッドを利用して、出力を簡易に設定する事が可能です。 下記のように、make_targetメソッドによって拡張子を判定し指定のpathに対して出力を保存する事ができます。
import gokart from luigi import IntParameter import pandas as pd class SampleTask1(gokart.TaskOnKart): task_namespace = 'sample' num_param = IntParameter() def output(self): return self.make_target('output/sample.pkl') def run(self): df = pd.DataFrame([1]*self.num_param) self.dump(df)
これはluigiにおけるFileSystemTargetに似ていますが、gokartでは、出力ファイルに対して「requiresとして実行される各タスクのパラメータ」「自タスクのパラメータ」から文字列ハッシュを算出し、出力ファイルに自動で付与するようになっています。上記のタスク実行時には、パラメータから生成されたハッシュに応じて、以下のようなファイルが出力されます。
$ tree . ./ ├── log │ ├── module_versions │ │ └── SampleTask1_6d384b6bdcd078fb13b025966f537692.txt │ ├── processing_time │ │ └── SampleTask1_6d384b6bdcd078fb13b025966f537692.pkl │ ├── task_log │ │ └── SampleTask1_6d384b6bdcd078fb13b025966f537692.pkl │ └── task_params │ └── SampleTask1_6d384b6bdcd078fb13b025966f537692.pkl └── output └── sample_6d384b6bdcd078fb13b025966f537692.pkl
各パラメータに応じたハッシュが付いた状態で、各ログと出力が保存されています。 パラメータがハッシュになっているため「パラメータや日付でファイルを管理する」といった問題の多い管理方法を回避する事ができるようになっています。 デフォルトでは、以下のファイルがdumpされます。
- output/sample_.pkl : SampleTask1でdumpしたファイル
- module_versions: タスクを実行した際に利用した全てのモジュールのバージョン
- processing_time: タスクの実行にかかった時間
- task_log: タスクがloggerを通して出力したログ
- task_params: タスク実行に利用したparameter
各ログと出力のdumpしたファイルを参照すれば、タスクが必ず再現できるという事を意識した作りになっており、各DBからファイルをダウンロードするタスクや、前処理、機械学習モデルの学習においても、各パラメータとハッシュが一意に定まる事で、データの再現を確実に行えるだけでなく、「パラメータを変更して繰り返し実験を行う」という機械学習モデリングにおいて最も重要な作業を容易にしています。
また、pickleを利用する場合はoutputメソッドを省略する事も可能です。
import gokart from luigi import IntParameter from luigi.util import requires from sample_task1 import SampleTask1 class Sample(gokart.TaskOnKart): task_namespace = 'sample' @requires(SampleTask1) class SampleTask2(Sample): sample_param = IntParameter() def run(self): df = self.load() df = df.sample(self.sample_param) self.dump(df)
上記の例のようにtask_namespaceを定義したクラスを事前に作成しておけば、「タスクを実行、結果をdumpする」というスクリプトが「task_namespaceを持つクラスを継承した上で、requiresデコレータによりSampleTask1をrequiresに指定、そのファイルを読み込み加工するrunメソッドを用意する」の3つになるため、上記のようにかなりシンプルな記述になります。これだけの記載で安易にタスクを定義でき、かつ再現性が担保されるよう様々な情報を保存しておける所がgokartの利点で、luigiよりさらに手軽にPipelineライブラリのメリットを体感できます。
上記のtaskを実行する際にはgokart.runメソッドを実行するmain.pyなるスクリプトとconfigファイルを別途用意すると良いでしょう。
以下にconfigファイルの例を示します。
[TaskOnKart] workspace_directory=./resources local_temporary_directory=./resources/tmp [sample.SampleTask1] num_param=10 [sample.SampleTask2] sample_param=2
workspace_directoryがmake_targetが出力先として利用するディレクトリです。ここには、「s3://hoge」「gs://piyo」のような形式でAWS S3やGoogle Cloud Storageを指定する事もできますし、「${WORK_SPACE}」「%(WORK_SPACE)s」といった記法で環境変数から値を読み込む事も可能です*5。 利用可能なクラウドサービスは現在GCPとAWSの2つですが、先にデメリットで示した通り、これらクラウドサービスの管理には気を使う必要が出てきます。セキュリティの管理も手慣れたエンジニアがエムスリーに居るサービスを選択しているといった形にはなっていますが、S3のようなKVSであれば料金も安く、一般的にも簡易に運用可能ではあると思います。
以下には実際に実行するmain.pyの例を示します。
import luigi import gokart import sample_task1 import sample_task2 if __name__ == '__main__': luigi.configuration.LuigiConfigParser.add_config_path('param.ini') gokart.run()
luigiのadd_config_pathメソッドを利用して、先程のconfigファイルを読み込んでいます。
以下のようなコマンドで実行します。configファイルの設定でなく引数を用いてパラメータを変更する事もできます。
python main.py sample.SampleTask2 --local-scheduler --sample-param=3
これが一般的なgokart実行の所作となっており、様々な条件で繰り返し実験を行いながら、簡単に全ての実験の再現を行う事ができるよう作成してあります。
クラウドサービスやSlack通知のサポート
前述の通り、タスクの実行結果の保存にはAWS S3やGoogle Cloud Storageを選択する事ができます。ハッシュ値を付けない設定も可能なので、あるストレージにあるパラメータで学習済みモデルが設置されるようなBatchを簡単に作成する事が可能です。
また各タスクの開始、終了、異常終了といったluigi.Eventに応じてSlackに通知を投げる機能も備えています。 設定は以下のようにSlackConfigに対してToken、Channel、ReplyするUserを指定するだけです。
[SlackConfig] token=${SLACK_TOKEN} " 環境変数から読まれる channel=sample_notice to_user=kawai
SlackConfigに関する設定がない場合は通知は行われない、シンプルな実装ですが、プロダクションで動くgokartに問題が発生した場合に検知する仕組みとして役立つでしょう。
gokartのメリット、デメリット
上記のような機能から、gokartは、Pipelineライブラリのメリットである「データ、モデルの再現性」「開発とprod運用移行のしやすさ」を大きく前に押し出したライブラリと言えます。 機械学習プロジェクトの開発スピードを高め、それにより発生する機械学習ならではの課題をよしなに解決してくれるのがgokartです。
一方で、Pipelineライブラリのデメリットである「データ保持や計算、セキュリティ等のリソースが嵩張る」といった点は、より顕著になってしまいます。 再現性のために、全てのタスクの結果とパラメータ、モジュールバージョン、ログを保存する機能があったり、タスクを出来るだけ細かく簡易に書けるよう設計されているため、当然と言えば当然です。
現状エムスリーでは、AIチームの構成や事業内容から、上記のようなデメリットが増大してもメリットが上回っていると感じます。
1人が1プロジェクトを通して持つため、委任や引き継ぎの観点でコードの共通化、モジュールのバージョン管理等が重要になる事。各タスク結果を保存しても、計算リソースやストレージに問題が出ない程度のデータ量である事*6が大きな要因です。
もし仮に、新たなサービス開始によってデータが急増し、バックエンドにhadoop等を利用しないといけなくなった場合、長期の視点からもgokartを使うメリットは薄くなってしまうなとも思います。
データ量やチームの形態、事業内容に合わせてPipelineライブラリを使う事が重要ですが、gokartは中でも小〜中規模辺りのチームに向いたライブラリだと思います。MLチーム立ち上げ時や、20人以下のプロジェクト、Kaggle等のコンテストでチーム内などで利用する事で、機械学習モデリングの立ち上がりスピードを早める事ができるはずです。
cookiecutter-gokart
gokartでの立ち上げをさらに加速させるため、cookiecutterを用いたテンプレートもOSSとして公開しています。
cookiecutterコマンドを使って、以下のように対話形式でgokartプロジェクトをスタートさせる事ができます。一般的なcookiecutterテンプレート同様、デフォルトのままで良い場合は空白のままEnterを押します。
cookiecutter https://github.com/m3dev/cookiecutter-gokart project_name [project_name]: m3sample # プロジェクトのルートディレクトリ名 package_name [package_name]: sample # Pythonモジュールにする際のパッケージ名 python_version [3.6]: # 利用するPythonバージョン author [your name]: m3dev # 作成者の名前 package_description [What is this project?]: this is sample # 作るプロジェクトの説明文 license [MIT License]: # 利用するライセンス
このコマンドで、以下を含むgokartプロジェクトのディレクトリが作成されます。
- sampleタスクのスクリプト
- configのsample
- sampleタスクを動作させるためのmain.py
- sampleタスクのunittestスクリプト
- モジュールとして利用するためのsetup.py
- unittestをチェックするためのGitHub Actions CI/CD
- LICENSE、README.md
このcookiecutter-gokartを利用する事で、GitHub上でgokartプロジェクトをすぐ始められるでしょう。 エムスリー社内でも社内向けの設定を付与したものを多く利用している程、gokartでcookiecutterを利用する価値は高いと考えています。 Pipelineで忘れがちなテストコードについてもサンプルを示していますので、参考にしていただければと思います。
thunderbolt
機械学習プロジェクトでは、各タスクで複数回に渡ってパラメータを変更して実験を行ったり、日常的に走るBatchのモデルを取得したりといった場面に多く出くわします。gokartでの開発においては、それらのファイルを手元に設置、確認してPythonから読み込むという所作を簡単に行うために、thunderboltというOSSを公開しています。
thunderboltは、gokartが出力する「task_log」「task_params」を読み込み、それらの情報をpandas.DataFrameで閲覧できます。また、その情報から直接タスクがdumpしたファイルをloadする事ができます。
thunderboltについては、jupyter notebook上でのexamplesを見てもらうのが一番早いかと思います。 thunderbolt/example.ipynb at master · m3dev/thunderbolt · GitHub
exampleでも行っているように、gokartで実行したデータの表示、ロードをPython上で行う事ができます。
from thunderbolt import Thunderbolt tb = Thunderbolt(os.environ['TASK_WORKSPACE_DIRECTORY']) # 各パラメータの表示 print( tb.get_task_df() ) # thunderboltが持つtask_idを指定してデータをロード data = tb.load(task_id=1)
これにより、複数回試した実験的タスクから、サーバ上や他人が実行したタスクまで、出力のルートディレクトリを指定する事で、Pythonによって管理する事を可能にしています。タスク名のフィルタやgokartの自動ファイル分割にも対応しており、gokartでパラメータを変えて沢山実験した後にjupyter notebookで結果を可視化したり、不要な実験結果を削除するPythonスクリプトを書くといったフローを実現するためのツールになっています。
redshells
エムスリーで作られた機械学習モデル構築に関連するgokartタスクについては、その多くをredshellsというOSSとして公開しています。
redshellsは、実際にエムスリーのプロダクションで動いている機械学習モデルのコードです。基本的なTF-IDFやtext embedding、xgboost、Matrix Factorizationといった手法からGraph Convolutional Neural Network等の比較的新しい手法まで、エムスリー内で使われる多くのモデルをgokartタスク化し公開しています。
Preferred Networksさんが公開しているOptunaにも一部対応しており、基本的には、パラメータなどの複雑な事を考えずpipelineを組めばモデルが出来るようなライブラリとなっています。
こちらについては、gokartの動作に軽く触れた上でexamplesを見てもらえればと思います。
redshells/examples at master · m3dev/redshells · GitHub
私の開発・運用形態
ここまで紹介したツールを使い、gokart、redshells -> thunderbolt -> cookiecutter-gokartという開発サイクルを用いて、機械学習におけるデータ分析からモデル構築、パラメータ調整等の実験、プロダクションコード化をシームレスに進められるようにしています。
以下に私の開発フローを模したSampleのJupyter Notebookを示します。
luigiは主にCLIから実行しますが、ipynb上でgokart (luigi)を動かすために以下のようなスクリプトを設定しており、ipynbでgokart開発を進められるようにしています。
# luigiがsys.exitでプロセスを終了させるのを回避する import sys def ipy_exit(*args): exit(keep_kernel=True) sys.exit = ipy_exit # ~~~ Task記述 ~~~ # gokartタスクはプロセスロックが発生するので--no-lockが必要 gokart.run(['sample.LoadIrisData', '--local-scheduler', '--no-lock'])
ipynbでの開発の良いところは、可視化やデータ加工の気軽さにありますが、その分コードが汚くなりがちです。データサイエンティスト、機械学習エンジニアのコード品質の議論が古くからあるような話です。可視化についてはipynbの機能を使いつつ、前処理、データ加工については細かくタスク化し、gokart.runで動かしています。
タスクの実行結果については、パラメータに応じたハッシュ付きファイルが生成されているので、そちらを利用しますが、パラメータとハッシュの突き合わせのためにthunderboltを利用しています。 thunderboltを利用してメモリ上にデータをロード、結果のチェックも行いながらgokartタスクを構築していきます。 sns.pairplotやpandas_profilingを利用してEDA、redshellsを使ってOptunaによる最適化をかけてXGBoostを学習と、定番の流れをipynbで完結させています。
ここまでgokartを利用して記述しておけば、cookiecutter-gokartで作成したテンプレートへの移植も簡単です。 また、コメントやメモを残しやすいipynbにコードの意図を残す事で、適切にモデルのテストコードを書く事ができ、プロダクションに持っていく時もかなりスムーズになりました。
おわりに
本記事では、機械学習向けのPipelineライブラリのメリット、デメリットに加えて、エムスリーが開発、運用、公開しているPipelineライブラリであるgokartの説明と、その周辺ライブラリの紹介を行いました。
gokartの起源の多くは、AIチーム チームリーダーの西場(@m_nishiba)の過去の技術イベント登壇資料にも書かれていますのでこちらも参考になると思います。
こちらのスライドにもある通り、AIチーム自体が出来て数年のチームという側面もありますし、gokartや周辺ライブラリはまだまだ不便な所がある状態ですが、エムスリー AIチームと共に成長していく事になると思います。皆さんの気軽なPull Requestやissue投稿をお待ちしています。
エムスリーでは、こういったOSSの公開や技術検討、議論とそれらの公開が「技術向上」として評価されるようになっています。 gokartやその他ライブラリへのStar、使ってみた感想の投稿に加えて、エムスリー株式会社採用への応募も心よりお待ちしております。宜しくお願いします。
*1:https://towardsdatascience.com/build-a-pipeline-for-harvesting-medium-top-author-data-c4d7ed73729f
*2:https://databricks.com/session/netflixs-recommendation-ml-pipeline-using-apache-spark
*3:Pipelineの定義にも寄りますがここでは広義に捉えています
*4:featherやApache Arrowといったフォーマットもよく使われますがバージョン毎の変更も多く再現性の観点で未サポートです
*5:後者はgokart独自の記法です
*6:日本の医師が35万人程なのでEC等に比べれば当然総データは小さくなります