Notebookの差分をhtmlで出力するGithub Actionsを作った

diff-notebooks

Notebookの差分をArtifactとして生成するdiff-notebooksをGithub Actionsとして公開しました。

github.com

想定する使い方はpull_requestをトリガーとするGithub Actionsへの組み込みです。

実行すると画面のようにNotebookの差分結果がArtifactとして保存されます。

中身はhtmlになっておりブラウザ上で開くとこのように差分を確認できるので、プルリクを受け取ったときのNotebookの差分チェックがGithub上の操作で完結します。

nbdiff-notebooksの中身

このGithub Actionsの中では、nbdiff-webで生成した差分のhtmlをファイルとして出力する自作ツールnbdiff-web-exporterを実行しています。

github.com

nbdiff-web-exporterは後で触れるnbdiff-webのラッパーです。nbdiff-webのオプションのほとんどをそのままサポートしています。

nbdiff-webはブラウザ上でNotebookの差分を確認できるツールです。ブラウザのボタン操作で差分ファイルをダウンロードできるのですが、差分ファイルをダウンロードには1ファイルごとにボタン操作が必要になります。

nbdiff-web-exporterはそれらの手間を省くためのラッパーになっており、baseとremoteを指定して実行すると差分結果のhtmlをローカルに保存するシンプルなCLIです。

Github Actionsで複数のNotebookの差分結果をArtifactとして保存するためにはブラウザの操作を除外する必要があったので、nbdiff-webを実行してからのブラウザの操作をseleniumで自動化しました。

なぜツールを作ったのか

Githubのレビュー画面ではNotebookの差分が分かりづらい

Jupyterで書かれたNotebookのプルリクを受け取ると、Github上でレビューしようとしても差分がよく分からないことがほとんどです。

Notebookの実態がjsonなのでGithubのchanged filesを開くと以下のようにjsonの差分として表示されてしまいます。下の例ではコードとプロットされた画像が変わっているのですが、どこが本質的な変更なのか画面からは分かりません。

既存のツールではあと一歩が足りない(個人の好みによる)

これ以降はnbdiff-web-exporterを作る前に既存ツールを調べてなぜそれらを使わなかったかをまとめています。

nbdimeと内部ツール

nbdime Notebook Extension

nbdimeをNotebook ExtensionとしてインストールするとJupyter上で差分を確認できます。

ただし、毎回Jupyterを立ち上げてgit fetchしてExtensionを起動するというステップが必要です。

nbdiff CLI

nbdimeに組み込まれたnbdiffを使うとターミナル上でdiffを実行するのと同じ使い勝手で差分を見れます。nbdiffならば差分確認のためにJupyterを立ち上げる手間は必要ありません。

$ nbdiff notebook/test.ipynb notebook/test2.ipynb 
nbdiff notebook/test.ipynb notebook/test2.ipynb
--- notebook/test.ipynb  2022-10-10 10:16:24.487987
+++ notebook/test2.ipynb  2022-10-10 10:16:24.489169
## modified /cells/1/source:
@@ -1,2 +1,2 @@
-X = np.linspace(-10, 10, 1000)
+X = np.linspace(-100, 100, 1000)
 y = np.sin(X)


## inserted before /cells/2/outputs/0:
+  output:
+    output_type: display_data
+    data:
+      image/png: iVBORw0K...<snip base64, md5=56df3dbb7c0efb80...>
+      text/plain: <Figure size 640x480 with 1 Axes>

## deleted /cells/2/outputs/0:
-  output:
-    output_type: display_data
-    data:
-      image/png: iVBORw0K...<snip base64, md5=43dd28726681bf24...>
-      text/plain: <Figure size 640x480 with 1 Axes>

ただし、こちらもJupyterを起動せずに済むこと以外はnbdime Notebook Extensionと同じくgitの操作の手間がかかります。また、画像の差分が分かりづらいためレビューに向いているとは言いづらいです。

nbdiff-web

nbdiffよりもリッチな画面で差分を見たいならnbdiff-webを使えばブラウザ上で差分を確認できます。

$ nbdiff-web origin/main notebook/test.ipynb 
[I nbdimeserver:422] Listening on 127.0.0.1, port 50532
[I webutil:29] URL: http://127.0.0.1:50532/difftool

一見これでもよさそうですが、レビューのたびにコマンドを実行しなければならず面倒です。

また、先ほどお話したようにhtmlをファイルとして取得するにはブラウザの操作が必要になります。nbdiff-webでブラウザを操作することなくhtmlを出力する機能の要望は挙がっているものの開発が途中で止まっているようです。

Add nbdiff-web-export to export HTML diff using just command line by trams · Pull Request #552 · jupyter/nbdime · GitHub

VSCodePython Extension

VSCodePython Extentionはipynb形式のファイルのレンダリングをサポートしており差分の確認もできます。普段から VSCodeでレビューをしていれば他のコードと同じようにレビューできます。

機能には満足していますが、個人的にはブラウザ上で作業が完結する方がうれしいこともあります。

ReviewNB

ReviewNBはまさにGithubのPR画面にNotebookの差分を表示してレビューするためのツールです。

私は使ったことがないのですが、差分箇所を指定してコメントをつけたり変更点をSuggestしたりとGithubのPR画面でできることはほぼできるように見えます。

プルリクを受け取ってGithubのPRを開くと必要な情報が揃っており、個人的には理想的なツールです。

ただしRebiewNBはOSSやアカデミックな分野以外で利用しようとすると有料になります。そのため手軽に試すには敷居が高いです。

まとめ

複数のツールを使い分けることなく無料でNotebooksのレビューができるGithub Actionsを作りました。

レビューだけならばVSCodeでも十分ですが、diff-notebooksを使えばpushごとの差分を常にArtifactとして記録できるメリットもあります。

リアルフォースとHHKBのデュアルキーボード環境を作った

タイトルの通りリアルフォースとHHKBを2台並べて同時に使うデュアルキーボード環境を作りました。

f:id:kuromt:20220323231621j:plain

ちょうど今月に超PayPay祭があり、3万円以上の商品が10%オフになっていたこと、さらにポイントの割合も悪くなくフリマアプリで買うよりも新品を買う方が安いという状況に後押しされてHHKB Hybrid Type-sの購入を決意しました。

まだ1日目ですがタイピングにも慣れ全体的に満足しています。

今回の構成を取るにあたって懸念(リスク)がないかネットで調べたものの、なかなかリアルフォースとHHKBのデュアルキーボードを使っている人が見当たらなかったので、自分でためした結果を記事にまとめることにしました。

これまでの環境

kuromt.hatenablog.com

上の記事の最後にあるようにもともとはリアルフォースを使っていました。リアルフォースは素晴らしいキーボードでまったく不満はありません。

ただし、どんなにいいキーボードを使っていても長くタイピングをしていると肩が縮み背も曲がり当然の結果としてひどい肩こりに悩まされます。

だんだん肩こりに我慢できなくなったので、分割キーボードかデュアルキーボードの導入を決めました。

デュアルキーボードに決めた理由

自作の分割キーボードデビューしたい気持ちは強くあったのですが最終的にデュアルキーボードに落ち着きました。

分割キーボードに問題があったのではなく、単純に今あるリアルフォースを有効活用しようとして自然とデュアルキーボードに落ち着いた形です。

最高のキーボードと言われるリアルフォースとHHKBの二つが手に入った以上は、あとは自作キーボード沼に落ちるしかないだろうなと感じています。

リアルフォースをもう一台買うことも考えたのですが、せっかくなので気分で使い分けることができて持ち運びにも便利なHHKBにしました。消極的な理由としては机が狭いので2台分のカーソルキーにスペースを取られたくなかったというのもあります。

Hybridを選んだのはキーボードのためのケーブルを増やしたくなかった一方でBluetooth接続のHHKBでチャタリングに近い症状がいくつも報告されていたからで、Type-Sにしたのは店頭でHybrid二機種を試して打鍵感が気に入ったこととオフィスやカフェなど自宅外で使用するなら静音タイプの方が好ましいだろうと考えたのが理由です。

あきらめた構成

本当はHHKBを英字配列にして、気分でキーボードの種類だけではなく配列も切り替えることも検討していました。英字配列と日本語配列のキーの特徴は右側に現れるので、デュアルキーボードなら右に置くキーボードを入れ替えるだけでも英字配列、日本語配列の切り替えができるだろうというのが理由です。

しかし、それには日本語配列Macに英字配列と日本語配列のキーボードをつなげてそれぞれのキーで認識されることが前提になります。

手元にある英字配列の別のキーボードで試したのですがなかなかうまくいかなかったため、今回は安全側に倒してHHKBもリアルフォースと同じ日本語配列にしました。

解決方法があれば英字配列のHHKBも買ってしまうと思います。

今の形に落ち着くまでの試行錯誤

モニターのUSB端子が左側にあるため、当初は有線しか使えないリアルフォースが左、無線のHHKBを右に配置するつもりでした。

しかしHHKBのバックスペースやかな変換の位置に慣れず四苦八苦していたところ、試しに左右を交換するとすぐに慣れたのでこの形に落ち着いています。機会があればもう一度逆パターンにチャレンジしたいです。

追記

翌日になって多少違和感が出たので手のひらの傾きに合わせてハの時の傾斜つけてみることにしました。

リアルフォースもHHKBも高さ調節用のスタンドがついているので、ためしに左右に傾斜がつくように違う高さでスタンドを立てたところ、リアルフォースは本体の重量があるおかげで安定したのですがHHKBがタイピングのたびにグラつくようになりました。(左に置いたHHKBの右側のスタンドを最大の高さ、左側を最小の高さでスタンドをたてています)。

グラつきの原因が最大の高さのスタンドを立てている右側の手元部分で机とHHKB本体にスペースができてしまうことだったのでスペースを埋めるためのアイテムを設置することにしました。HHKBは外出時にも持ち出す想定だったため、底面に貼り付けるのではなく必要に応じて取り外せることを条件にしています。

昼休みに近所のホームセンターにいって探して、200円ほどで下のようなもの買ってきました。

f:id:kuromt:20220324234001j:plain

実際にHHKBの下に引くとこのような見た目になります。三角形の傾斜がついているので傾斜の微調整が効くのが素晴らしいです。これを導入したあとは前日よりもさらに快適にタイピングできるようになりました。 f:id:kuromt:20220324234250j:plainf:id:kuromt:20220324234252j:plain

まとめ

肩こりに悩まされたエンジニアがリアルフォースとHHKBのデュアルキーボード環境を作るまでに検討したことをご紹介しました。

肝心のリアルフォースとHHKBの組み合わせのデュアルキーボードですが、覚悟していたHHKBのチャタリングは一度も起きず左右のキーボードの組み合わせのIDEショートカットも問題なく動作していています。今日一日朝から夜まで机の前に座っていて、肩こりが軽減されたことを実感していて総じて満足です。長期的に使ってみて最初に気づかなかった不満が出てくればここに追記します。

最後に別画面からも画像を載せておきます。

f:id:kuromt:20220323231628j:plain

革専門の小売店で革を買ってデスクマットにした話

デスクマットを購入しようとした経緯

作業用のデスクを半年前に買い替えたところタイピング時にパームレストがぐらつくようになりました。

パームレストは木材を買ってヤスリがけした自作のものでゴム製の滑り止めをつけていたのですがあまり効果がなく、安定したタイピングに支障が出ていたので滑らないようにデスクマットをひくことにしました。

デスクマットに求める条件

探す方針を決めるためにデスクマットに求める条件を書き出してみました。

  • サイズは横100cm、縦35cm前後
    • マット部分と机部分の段差を気にしなくて済むように机と同じ横幅のもの
    • 机の天板を挟み込むモニターアームに干渉しないように奥行きは机よりやや短いもの
  • 滑り止めの効果がしっかりしている本革
  • メモを取るのに支障がない程度にざらつきが少ないもの
  • 机の上に飲み物を常備しているのでできれば水染みに強い素材
  • 色は黒か茶色

上の条件であれば耐水性のあるゴムや人工革(PUレザー)のほうが実用性は高いのですが、せっかくなら使っていて気分が上がり風合いの変化を楽しめる本革にすることにしました。また、レザーのデスクマットを購入した方のツイートを見てうらやましくなったというのもあります。

なぜ革専門の小売店に行こうと思ったのか

大手のECサイトやハンドクラフトの通販サイトで探したところ100cmのものが少なく、あったとしても縦幅が30cmしかなかったりと条件に合うものがなかなか見つかりませんでした。

そこで店で革を購入して自分で裁断する方針に切り替えました。

裁断することを考えると元の条件のサイズより大きな革でなければいけません。また、自分で革を選べるならいろいろ見比べたいので革の専門店が集まる浅草エリアで探すことにしました。

店探しはこちらのサイトが参考になりました。

kumosha.com

店内で購入するまでの流れ

店内にはところ狭しと革が並んでいます。お店の方に許可をいただき撮影した写真はこちらです。実際にはこの10倍くらいの広さがあります。

f:id:kuromt:20210104160024j:plain

店の方に探している革の条件を伝えて相談したところ100cmというのは成牛であってもなかなか取れないサイズなのでより大きいものから裁断して購入したほうがいいとアドバイスをもらい、このとき初めて気に入った革で指定したサイズで購入することができることを知りました。

他にも革についていろいろ教えていただきました。たとえば革は10cm x 10cmを1ds(デシ)とする規格が定められていて購入価格は革の面積とdsあたりの値段で決まる(1dsが100円だと30dsで3千円、60dsで6千円)ことであったり、革の染め方には顔料を使うものと染料を使うものがあり顔料で染めた革の方が水に強いなどです。

話を聞いてから店内を見てまわり他の革に比べて厚みがあり触っていて気持ちがよかった牛のヌバック(表革をヤスリ掛け加工して起毛させたもの)に決めました。もともと挙げていた水染みに強いという条件には合わないのですがぬれてオイルが抜けても革靴用オイルでメンテナンスすれば多少は長持ちするそうです。

下の写真は実際に裁断されている様子です。分厚い革がいい音を立てながらはさみで裁断されているのは見ていて面白かったです。

f:id:kuromt:20210104160015j:plain

帰宅後にも床に置いて写真を撮ってみました。

f:id:kuromt:20210104160013j:plain

使ってみて

実際に机に置いた様子がこちらです。

f:id:kuromt:20210104175930j:plainf:id:kuromt:20210104193147j:plain
左: before 右: after

f:id:kuromt:20210104184452j:plain

タイピングしてみるとパームレストのぐらつきがなくなり手首の位置が安定するようになりました。ヌバックは起毛しているためか摩擦が強く多少の力ではびくともしません。

値段は税込で5千円以下でした。既製品で本革の大型デスクマットを買った場合に比べればかなり安く済んだと思っています。

新しい分野の勉強ができたことを含め総じて良い買い物ができました。

革を買うときの注意点

ここまで良い点ばかりを挙げてきましたが買った革をデスクマットにすることはメリットだけではありません。とはいえデメリットというほどではないと感じたので注意すべき点として紹介します。

たわみがある

先ほどの写真でも目立ちましたが革の端になるほど強いたわみがありました。素人考えですが小さな革(15ds以下)ではたわみの印象はなかったので大きな革の特徴なのかもしれません。

とりあえずはヘッドホンスタンドで挟んだり重りを乗せて伸ばすことにしています*1。キーボードやパームレストを置く中心部分は平らなので実用上は困っていません。

f:id:kuromt:20210104175922j:plain

革には傷があるので購入前によく確認する

これは文字通りです。元々知識としてはあったのですが実際にたくさんの革を見ていると想像よりも目立つ傷が多く、手に取った革には傷があって当たり前と思うくらいでちょうどよいと思いました。

今回購入した革はヌバックで起毛しており多少の傷は手でこすれば目立たなくすることができます。

匂いをたしかめる

このご時世なので店内でもずっとマスクをつけていたのですが、革には匂いがつきものなので購入前に確認すべきだったと帰宅してから気づきました。幸い購入したものは匂いはほとんどありませんでしたが匂いが気になるほどだったら買い直しになっていたので反省しています。

*1:SRE本シリーズや詳解システムパフォーマンスを積み上げています

MLflow Pluginの作り方

この記事はMLOps Advent Calender 2020の21日目の記事です.

日本でもMLflowの利用事例が増えてきていて特に実験管理に使われることが多いです. 特に2020年はMLflowを使った記事がいくつも公開されています.

MLflowは実験管理以外にもモデルの管理機能やモデルのサービング, 処理の実行機能があるのですがそれらはあまり使われていない印象です. モデルのサービングはTensorflowやPyTorchといった学習フレームワーク自身がサポートしはじめていていくつかMLOpsの事例を見てもそれらを使うケースが多いようです. 一方で今年のData + AI Summit Europe 2020ではMLflowのモデルサービングをプロダクション環境で使った発表もあり今後増えていくこともあるだろうと思っています.

メタデータ管理としてのMLflow

MLflowの実験管理はGoogleMLOps: 機械学習における継続的デリバリーと自動化のパイプラインでいえばMLOps Level1のメタデータ管理の機能に相当します. メタデータ管理が記録するのはMLの再現性の担保やデバッグに活用される実行時の情報です.

大手クラウドプロバイダや海外のベンチャー企業が様々なMLOpsのためのサービスを提供していますが単一のマネージドサービスで全ての要件が賄えることは稀です. 実際に使ってみると足りない機能が次々に見えてきて別のシステムと繋ぎ合わせることで補うということが多くあります. これらのシステムでも実行時の情報が必要になることもあり, MLflowをメタデータの管理として扱うのであれば連携するシステムで必要な情報もMLflowに保存したいと思うのは自然な流れです.

実際に導入するには追加で保存したい情報を整理しチームや組織の間でそれらに対応するタグを決めて mlflow.set_tag で保存する方法が考えられます. しかし人間は間違えるものなのでコードの中で一つ一つ指定するのではなく自動的に情報を記録する仕組みを作るべきです.

このような場合にMLflow Pluginが有効です.

MLflow Plugin

MLflowにはpluginの仕組みがあります.

仕組みは単純でPythonのEntrypointパッケージを使い特定のエントリーポイントを持つPythonパッケージをロードしてMLflowのpluginの抽象クラスを継承したクラスの特定のメソッドを実行すること実現されています.

pluginの抽象化クラスは用途ごとに分けられています. MLflowの実験管理で特定の情報を自動的に保存するには mlflow.run_context_provider をエントリーポイントとするPythonパッケージで mlflow.tracking.context.abstract_context.RunContextProviderを継承したクラスを実装します.

RunContextProviderクラスは二つのメソッドを持ちます.

  • def in_context(self):
    • Boolを返す. Trueを返すとmlflowのクライアントが tags メソッドを実行しFalseを返すと何もしない.
    • 特定の状況下ではpluginの処理を実行したくない場合に使う.
  • def tags(self):
    • MLflowに記録させたいtagをDictで返す.

pluginのexampleとしてmlflowのクライアントのバージョンを client.version タグに記録する mlflow-plugin-example プラグインを用意しました. (kuromt/mlflow-plugin-example)

実装はシンプルです.

from mlflow.tracking.context.abstract_context import RunContextProvider
import mlflow

class LoggingExample(RunContextProvider):
    def in_context(self):
        return True

    def tags(self):
        return { 'client.version': mlflow.__version__ }

このpluginを実装するときは二つ注意があります.

一つ目は独自に定義するタグの名前付けです. MLflowのタグには利用者が任意に指定するタグとMLflowが内部で予約しているシステムタグがあるのですがpluginはMLflowのシステムタグも上書きできてしまうので意図的に上書きしたくない場合はタグの名付けに注意してください.

二つ目はRunContextProviderを継承したpluginの処理が実行されるタイミングです. このpluginは mlflow.start_run() を実行するタイミングで処理されます. そのため自動的に保存できるのは mlflow.start_run() を実行する前までに確定した情報のみです.

import mlflow

# ここまでの処理内容と環境情報は自動的に保存できる

with mlflow_start_run():
  # ここの処理は自動保存できない

実際に実行してみる

MLflowとpluginの動作確認のためにJupyterhubとMLflow+MySQLの環境をDockerコンテナで構築するためのdocker-composeを含むデモ用のリポジトリを用意しました.

まずはデモ用のリポジトリからcloneして事前準備をします.

$ git clone https://github.com/kuromt/mlops_advent_calendar2020_demo.git
$ cd mlops_advent_calendar2020_demo/docker-compose/

移動したディレクトリにMySQLのパスワードを記述したファイルを作ります.

$ cat << EOF > .env
> MYSQL_ROOT_PASSWORD="mlflow"
> MYSQL_DATABASE="mlflow"
> MYSQL_USER="mlflow"
> MYSQL_PASSWORD="mlflow"
> EOF

続いてJupyterhubとMLflow + MySQLのコンテナを立ち上げます. 初めて立ち上げるときは自動でイメージのビルドが走ります.

$ docker-compose up -d

コンテナが立ち上がるとlocalhostの80ポートでMLflowに, 8080ポートでJupyterhubにアクセスできるようになります.

まずはpluginなしで実行してみます. JupyterのNotebookで以下を実行します.

import mlflow

tracking_server = "http://mlflow"
mlflow.set_tracking_uri(tracking_server)

with mlflow.start_run():
    mlflow.log_param("param1", 1)

MLflowのUIにアクセスすると "default" の Experimentsにrunが追加されています.

f:id:kuromt:20201220174353p:plain
mlflow1

次にJupyterhubの環境に先ほどのpluginをインストールします.

$ pip install git+https://github.com/kuromt/mlflow-plugin-example

インストールが終わればあとは自動的にmlflowのクライアントのバージョン情報を保存するpluginが有効になります. pluginの動作を確認するために先ほどのNotebookを再実行してみましょう. MLflowのpluginは import mlflow のタイミングで登録されるので先ほどの実行で使ったカーネルをrestartが必要です.

f:id:kuromt:20201220174901p:plain

カーネルをrestartした後に先ほどと同じコードを実行しました.

MLflowのUIを確認してみると先ほど登録されていなかった client.version タグが記録されており, 自動的に実行時の情報が記録されていることが分かります.

f:id:kuromt:20201220175017p:plain
mlflow_with_plugin

このようにMLflow Pluginを使うことで独自に決めた実行情報のメタデータの保存を自動化できるようになります.

また, 保存したい情報が増えても実験用のコードを修正するのではなくpluginの機能を拡張するだけで済むので拡張性も高くなるというメリットもあります.

まとめ

この記事ではMLflowの実験管理を拡張するpluginの実装方法を紹介しました.

他のpluginを作りたい場合は Writing Your Own MLflow Plugins を参考にしながら実装してください.

MLOpsの事例やツールの情報収集

この記事はMLOps Advent Calendar 2020の6日目の記事です。

MLOps関連の情報を入手するのに大変お世話になっている便利なサイト等を紹介します。 情報収集のために毎朝見ているサイトや購読しているメルマガからMLだけに興味があるという人向けに次の条件を満たすものを選びました。

日本語

ML-News

何か新しい話がないかと思ったときにまず見に行くのがこのサイトです。おそらくTwitterのアクティビティを見て掲載する記事が決まっており、Twitterを見ていなくてもここを見るだけで注目度が高い記事を知ることができます。

取り上げられるのは日本語の記事が多いですが海外の記事でも注目度が高いものは掲載されます。一度掲載された内容は1日近く残るので頻繁に確認しなくても取りこぼすことはなくとても便利です。

このサイトを運営している方にはとても感謝しています。

Weekly Kaggle News

週に一度更新されるメルマガです。名前のとおりKaggleのコンペの話題を扱っているのが他にはない特徴です。メルマガを配信されている方はKaggleコンペに参加されている方で配信される記事のコメントも参加者ならではの視点でためになります。Kaggle以外にもMLを使った事例紹介やツールの話が取り上げられることもあります。

英語

Databricks Blog

SparkやHadoop、その他データ分析のOSSを使ったシステムの事例紹介や各種OSSの仕組みや新機能が取り上げブログとしてまとめられています。DatabricksはSparkやMLflowをリードしている企業でそれらの最新情報も得られます。

分散処理が必要ないわゆるビッグデータを扱う事例・技術が中心であるためか取り上げられる事例は大規模なシステムであることが多く読んでいて面白いです。

Venturebeat

主に企業が取り組んでいる研究内容や自社サービスに適用したAI事例の紹介です。

これを読み始めて面白い事例だったり研究なのに日本のメディアでは取り上げられないものもあるんだなと思うようになりました。

ML in Production

weeklyに届くメルマガです。タイトルの通り実環境や実際のサービスにMLを適用する際に気を付けるべきノウハウや実際の事例を中心に紹介しています。自分はMLOpsに万能な仕組みやアーキテクチャは存在せず、基本を押さえたあとは自分たちの環境に合わせた仕組みを適用することが大事だと感じているのでこういった実際の事例は参考になります。

AI Weekly

ML in Productionに似ていますがツールの紹介も含むことがあるのが特徴です。個人的にはML-Newsに近い位置づけで読んでいます。

Project to Know

タイトルにはついていませんがこれもweekly系です。毎回3つのPerpersと3つのProjects、3つのContentsを紹介しています。アカデミックな分野と実務の両方のニュースを配信してくれています。研究者にとってもサービス開発者にとっても有用な情報があります。

お願い

他にも面白いトピックを扱っているサイトやメルマガがあればどんどん増やしていきたいので、これは見ておいた方がいいというサイトやweeklyなどがあればTwitterのつぶやきでもこのブログのコメントでもどこでもいいので教えてください。

MLOps好き仲間の研究者やエンジニアにこんなサイトやメルマガがあったのかと思えてもらえることほどこの記事を書いて良かったと思うことはありません。

お手軽で欲しい機能が揃っている実験管理ツールGuild AIの紹介

機械学習の実験管理ツールにGuild AIというものがあります。特に大きな特徴はコード追加なしで実験管理ができるというものです。

試しに触ってみたところ、まさにコード追加なしで簡単に試せる、ちょっとした条件を変えた実行も簡単、結果の可視化はシンプルなCLIもリッチなGUIもどちらも用意されている、ローカルだけではなくS3にもデータを保存できる、しかもWebサーバを別に立てる必要がなく手元の環境で完結します。

ただ、Guild AIは一部の方にSNS上で言及されているものの日本語で書かれたドキュメントやブログは見つかりませんでした。良いツールが埋もれるのはもったいないと思いGuild AIの記事を書くことにしました。

この記事の前半では実行条件を変えながら実行して結果を可視化するまでの流れを紹介します。Guild AIのお手軽さをお伝えすることを意識しました。

後半はある程度実務での状況を想定し複数の人が作業していても欲しい結果のみを参照する方法を紹介しています。

他にも紹介しきれない機能はあるのですが、最後にある資料からドキュメントを読んでみてください。簡単にですがここに書いていない機能にどんなものがあるかをリストにしてあります。

目次

この記事に書いていること、書いていないこと

  • 書いていること
    • Guild AIの手軽さを体験する
    • 条件を変えながら実行する
    • Optimizerを使って複数の実験条件をバッチ実行する
    • CLIを使った実行結果の管理、検索
    • GUIを使った実行結果の管理、検索
    • 他の実験と区別した実行、検索
  • 書いていないこと
    • パイプライン実行
    • 実験のパッケージング
    • 実験情報をS3に保存する
    • リモート環境で実行
    • 他の実験管理ツールとの機能比較

ここで紹介しきれない機能は ドキュメントexamples をご覧ください。

GuildAIの紹介

Guild AIを手軽さを体験する

まず、guildaiパッケージをインストールしましょう。これで guild コマンドがインストールされます。

$ pip install guildai

次に実行する度に違う値を返すrun_random.pyを用意します。これを実験用スクリプトとみなしてguildの手軽さを体験していきます。

import random

start = 0
end = 10

print("result: %f" % random.uniform(start, end))

さっそく用意したスクリプトguild runで実行します。

$ guild run run_random.py 
Refreshing flags...
You are about to run /home/jovyan/work/run_random.py
  end: 10
  start: 0
Continue? (Y/n) y
result: 4.966394

元コードに手を加えていないのにguildコマンドがスクリプト中の startend 変数を認識していることがわかります。これはguildaiがスクリプトグローバル変数の定義を読み込みflagとして認識するためです。

このまま何度か乱数を出力するスクリプトを実行してみます。

$ guild run run_random.py  -y
result: 7.314514
$ guild run run_random.py  -y
result: 2.449769
$ guild run run_random.py  -y
result: 1.361825
$ guild run run_random.py  -y
result: 6.973784

合計5回実行しました。それでは実行した結果を見てみましょう。 resultが小さいものから上位3つを取り出してみます。

$ guild compare --table --min result --top 3
run       operation      started              time     status     label  end  start  step  result
9d9b1c1d  run_random.py  2020-09-10 12:08:02  0:00:00  completed         10   0      0     1.361824
e0f6d49c  run_random.py  2020-09-10 12:08:01  0:00:00  completed         10   0      0     2.449769
99a5dbbd  run_random.py  2020-09-10 11:58:22  0:00:00  completed         10   0      0     4.966393

4番目に実行した1.361824が一番上に表示されています。

guildaiはkey: valueのフォーマットで標準出力に出された数値を認識します。これをguildaiではScalarsといいます。このフォーマットは設定で変更することもできます。

先程のスクリプトでは"result"をScalarsのkeyにしていたので--min result を引数に指定することで生成した乱数を降順に取り出せました。機械学習の実験の場合は評価関数の結果を出力することでモデルの最適化に応用できます。

ここまででguildaiがどれだけ簡単に実験管理できるか雰囲気を感じることができたと思います。とはいえ乱数を出力するスクリプトを叩いただけではつまらないので次はもう少し実験らしいことをしてみます。

乱数を生成する条件を変えながら実行する

先程は0から10の間の数値を生成していましたがもう少し大きな値を取り出したくなったとしましょう。

endを1000にして実行してみます。

$ guild run run_random.py end=1000 
You are about to run /home/jovyan/work/run_random.py
  end: 1000
  start: 0
Continue? (Y/n) y
result: 258.577733

先程と同じく何度か実行してみます。

$ guild run run_random.py end=1000 -y
result: 584.182183
$ guild run run_random.py end=1000 -y
result: 111.938457
$ guild run run_random.py end=1000 -y
result: 430.517918

実行した結果を確認しましょう。先程はCLIを使いました。CLIを使った確認は手軽ですが今回はguildaiの独自GUIであるViewで結果を確認してみましょう。

guild viewコマンドを実行します。実行したらブラウザから http://localhost:10000 にアクセスします。

$ guild view --port 10000 --host 0.0.0.0
Running Guild View at http://localhost:10000

ブラウザにアクセスすると個別の実験内容の詳細を確認できるダッシュボードが表示されます。

f:id:kuromt:20200910212807p:plain

guildaiはこのView以外にもTensorBoardを組み込んでいます。Viewはguildaiの独自概念であるrunの可視化のために使い、パラメータの可視化は高機能なTensorBoardを使うという役割分担になっています。

Viewの左上のボタンからTensorBoardを開いて end で与えたパラメータによって result がどのように変化したか確認できます。

f:id:kuromt:20200910213558p:plain

次はオープンデータのサンフランシスコの住宅価格データを使って実際にモデルを作る実験管理をしていきます。

オープンデータを使った実行

サンフランシスコの住宅価格データをダウンロードして保存します。

from sklearn.datasets import fetch_california_housing
import pandas as pd

data = fetch_california_housing()

data_df = pd.DataFrame(data.data, columns=data.feature_names)
target_df = pd.DataFrame(data.target, columns=["target"])
full_df = pd.concat([data_df, target_df], axis=1)

from sklearn.model_selection import train_test_split

def split_and_save(dataframe=None, target_path=None):
    train, validate = train_test_split(dataframe, test_size=0.3)
    train.to_csv(target_path + "/train.csv")
    validate.to_csv(target_path + "/validate.csv")

split_and_save(full_df, "./data")

次に、住宅価格をxgboostの回帰で学習するスクリプト train_validate.pyを用意します。

import pandas as pd
import xgboost as xgb

train_data_path = "/home/jovyan/work/data/train.csv"
validation_data_path = "/home/jovyan/work/data/validate.csv"

train_df = pd.read_csv(train_data_path)
validation_df = pd.read_csv(validation_data_path)

x_train = train_df.drop("target", axis=1)
y_train = train_df["target"]

max_depth = 1
min_child_weight = 0.1
learning_rate = 0.1

clf = xgb.XGBRegressor(max_depth=max_depth, min_child_weight=min_child_weight, learning_rate=learning_rate)

xg_model = clf.fit(x_train, y_train)

x_predict = validation_df.drop("target", axis=1)
y_predict = validation_df["target"]

predict = xg_model.predict(x_predict)

from sklearn.metrics import mean_squared_error
mse = mean_squared_error(predict, y_predict)

print("mse: %f" % mse)

乱数生成のスクリプトで試したようにmax_depthlearning_weightなどのパラメータを外部から与えてguild runを何度も実行しパラメータ最適化をするんだろうと想像がつくかと思います。

しかし、この環境には乱数生成のスクリプトを使った実行結果が残っています。このまま実行すると今回の実行と前の実行結果が混ざって表示されてしまい確認したい結果に集中できません。 また、毎回違うパラメータを指定して何度も実行することは大変です。

そこで、サンフランシスコの住宅の予測モデルの生成スクリプトの実行にpredict-house-priceタグを付与して乱数生成の実行と区別がつくようにします。

さらにguildaiのパラメータ最適化の機能を使いそれぞれのパラメータの探索範囲を指定し最大20回のトライアルでランダムサーチを実行します。

guildaiはランダムサーチの他にグリッドサーチ、ベイジアン最適化をサポートしているので目的に合わせた使い分けができます。

$ guild run train_validate.py learning_rate=[0.01:0.1] max_depth=[2:4] min_child_weight=[0.1:0.3]  --max-trials 20 --label predict-house-price
Refreshing flags...
You are about to run /home/jovyan/work/train_validate.py with random search (max 20 trials)
  learning_rate: [0.01:0.1]
  max_depth: [2:4]
  min_child_weight: [0.1:0.3]
  train_data_path: /home/jovyan/work/data/train.csv
  validation_data_path: /home/jovyan/work/data/validate.csv
Continue? (Y/n) y
INFO: [guild] Initialized trial 669896de (learning_rate=0.028297447038768214, max_depth=4, min_child_weight=0.17322408721578914, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [guild] Running trial 669896de: train_validate.py (learning_rate=0.028297447038768214, max_depth=4, min_child_weight=0.17322408721578914, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [numexpr.utils] NumExpr defaulting to 2 threads.
mse: 0.348852
INFO: [guild] Initialized trial 2ce684f7 (learning_rate=0.07674815292169297, max_depth=2, min_child_weight=0.15809164243532572, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [guild] Running trial 2ce684f7: train_validate.py (learning_rate=0.07674815292169297, max_depth=2, min_child_weight=0.15809164243532572, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [numexpr.utils] NumExpr defaulting to 2 threads.
mse: 0.332837

パラメータ探索の実行が終わったらguild compareで結果を確認しましょう。-l predict-house-price を指定することで結果を絞りこんでいます。

$ guild compare --table --min mse --top 5 -l predict-house-price
run       operation          started              time     status     label                learning_rate  max_depth  min_child_weight  train_data_path                    validation_data_path                  step  mse
1f2013fe  train_validate.py  2020-09-11 12:30:39  0:00:02  completed  predict-house-price  0.095549       4          0.206510          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.242914
1a13f5bb  train_validate.py  2020-09-11 12:30:35  0:00:02  completed  predict-house-price  0.069911       4          0.254429          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.256736
673398fc  train_validate.py  2020-09-11 12:30:10  0:00:02  completed  predict-house-price  0.098693       3          0.163653          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.264573
8e83cb7b  train_validate.py  2020-09-11 12:30:19  0:00:02  completed  predict-house-price  0.098743       3          0.133931          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.266335
274f5aa2  train_validate.py  2020-09-11 12:29:56  0:00:02  completed  predict-house-price  0.087953       3          0.184524          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.273010

TensorBoardでも確認してみます。ここでもラベルを指定して表示するrunを絞り込んでいます。

$ guild view -p 10000 -h 0.0.0.0 -l predict-house-price
Running Guild View at http://localhost:10000

f:id:kuromt:20200911213710p:plain

まとめ

guildaiをインストールしてコード追加なしにパラメータを変えた実行と結果の可視化、ラベルを使った実験管理の方法を紹介しました。

Kaggle用に良さそうなツールを探していたのですがguildaiが自分にいちばんハマる感覚があったので当分使ってみようと思います。

ちなみにguildaiは実験情報をS3に保存する機能があるのでminioを用意してそこに実験情報を保存してみようとしたのですがS3へのデータ保存が aws-cli を直接叩く実装*1になっていて保存先をminioに向けるための--endpoint-url を外から与える手段が見つからなかったため諦めました。*2

資料

*1:https://github.com/guildai/guildai/blob/5d32cdf6eca60ad4195c7432612e5a86dc48f8e8/guild/commands/s3_sync_impl.py#L85-97

*2:/etc/hostsで名前解決を捻じ曲げればできそうな気もしますが...

データ分析のための数理モデル入門

paypaymall.yahoo.co.jp

この本を買うきっかけ

TwitterのTLに著者の方のツイートが流れてきて興味をもったのがきっかけです。

本を買う前の印象

そのままTwitterで検索したりAmzonの口コミを見て

  • 初学者にも分かりやすいように数式を使わず数理モデルを平易に解説している
  • 網羅的に描かれていて辞書のように使える
  • 図が多くしかもフルカラー

といった特徴に惹かれて購入しました。

全体の感想

実際に読んでみると数式がまったくでないというわけではありませんが、微積を知っていれば問題ないものばかりです。数理モデルを理論をベースにして式変形で導き出すのではなく、最初から式を提示したあとに各項ごとの意味を解説してくれています。おかげで、頭の中で式変形を考えなくてもサラサラと読み進めていくことができました。

著者の方がたびたび書かれているように、データ分析を行うときにどの数理モデルを使えばよいかを考えるための指標を学ぶことができました。これからデータ分析の理論を学ぶ入門書として素晴らしい本だと思います。