Blog
Optunaによる最適化結果を手軽に確認できるWebダッシュボードの開発
AI Labの芝田です (GitHub: @c-bata)。
以前、Optunaによる最適化結果を手軽に確認できるWebダッシュボードを開発・公開しました。公開からすでに半年以上が経過し、現在は公式に利用を推奨されるようになりました。Google Summer of Codeなどを通してcontributorやcommitterも増えつつある一方で、設計や実装に関しては資料を残してきませんでした(※1)。本記事ではダッシュボードの紹介をするとともに、開発に興味がある方向けに開発に役立つ情報をまとめておきます。
GitHub: https://github.com/optuna/optuna-dashboard
optuna-dashboardとは?
optuna-dashboardはOptunaによるハイパーパラメータの最適化結果をWebブラウザ上で簡単に確認できるツールです(※2)。試行結果だけではなく、ハイパーパラメータの重要度や相関関係なども確認できます。
インストールや使い方はとても簡単です。Optunaの試行結果をRDB(SQLite3など)に保存し、そのStorage URLを指定するだけです。OptunaのRDBバックエンドを利用したことのある方であれば迷わずに使えるでしょう。
1 2 |
$ pip install optuna-dashboard $ optuna-dashboard sqlite:///db.sqlite3 |
実行すると次のように、各Trialでサンプルされたハイパーパラメーターや評価値が手軽に確認できます。ハイパーパラメータ間の相関関係、重要度もあわせて表示されるため、探索空間の見直し等にも有用です。
また公式に推奨している方法ではありませんが、OptunaのIn-Memory Storageを使っている方であっても、次のように呼び出すことも可能です(APIは変更する可能性もあるのでご注意ください)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import optuna from optuna_dashboard.app import create_app def objective(trial): x1 = trial.suggest_float("x1", 0, 10) x2 = trial.suggest_float("x2", 0, 10) return (x1 - 2) ** 2 + (x2 - 5) ** 2 if __name__ == "__main__": storage = optuna.storages.InMemoryStorage() study = optuna.create_study( study_name="single-objective", storage=storage ) study.optimize(objective, n_trials=100) app = create_app(storage) app.run(...) |
optuna-dashboardの設計
optuna-dashboardは、TypeScript + React製のSPA(Single Page Application)とBottle製のJSON APIサーバーの2つからなります。
設計はとてもシンプルです。最適化の試行結果はAPIポーリングにより定期的に同期されます。デフォルトでは最大で10秒程度の同期遅延が発生しますが、ほとんどのユースケースにおいては問題にならないでしょう(※3)。またJSON APIのエンドポイントも上の図に示す5つだけです。
optuna-dashboardのWebフロントエンドのコードは、もともとGoptunaのために実装しました(※4)。SPAとして実装されているのはこれが理由です。移植性を踏まえるとこの選択は正解でしたが、OptunaのユーザーやPython開発者はかならずしもWebフロントエンド技術に詳しいわけではないため、開発に興味があってもここの理解が大変かもしれません。以降はWebフロントエンドまわりを中心に知っておくと開発役立つことを解説します。
React Routerによるクライアントサイドルーティング
SPAとして実装したため、ルーティングは基本的にクライアントサイドで行われます。クライアントサイドのルーティングにはReact Routerを使用しています。クライアントサイドでのルーティングは、BrowserRouterを使う方法とHashRouterを使う方法があります。BrowserRouterが /dashboard/studies/1 のようにサーバーサイドでのルーティングと同じようなURL設計で行うのに対し、HashRouterは /#dashboard/studies/1 のようにURLのハッシュ部分の文字列を使用します。
今回はBrowserRouterを利用しています。BrowserRouterを使う際には、サーバー側のルーティング機能とコンフリクトしないように気をつける必要があります。今回は/dashboard のURLパスから始まるリクエストを受け取った際には、次のようにSPAのJSファイルを読み込むHTMLファイルを返すことで、それ以降のルーティングをすべてクライアントサイドでルーティングにまかせています(コードはこちら)。
1 2 3 4 5 |
# Accept any following paths for client-side routing @app.get("/dashboard<:re:(/.*)?>") def dashboard() -> BottleViewReturn: response.content_type = "text/html" return INDEX_HTML |
Recoilによる状態管理
Reactのステート管理には、Recoilを利用しています。APIから取得したStudyやTrialの一覧はさまざまなページから参照されます。ページ遷移の前後でAPIを叩き直すことを防ぐためのナイーブな解決策として、React Routerよりも上位のコンポーネントのstateに保存し、propsとして引き渡していく方法があります。しかし上位のコンポーネントのstateに多くの情報を詰め込むと、stateの一部が更新されたときにそれ以下の多くのコンポーネントが再レンダリングされてしまい、Reactの良さを活かせません。
この問題を防ぐ方法としてreact-reduxなどのライブラリーを使うことも可能ですが、やや重厚で比較的学習コストが大きいように感じています。RecoilはまだExperimentalなソフトウェアで、APIも安定していないようですが、Reactユーザーであれば手軽に扱えるライブラリーです。今後もAPIが変わる可能性があるため使い方をここでは解説しません。詳細は公式ドキュメントを読んでみてください。
Plotly.jsによる可視化
Optunaの可視化機能は、PlotlyのPythonライブラリーを使って optuna.visualizationモジュール に実装されています。optuna-dashboardは、この処理をPlotlyのJavaScriptライブラリーを使って再実装しています。この作業はやや大変で、完璧に移植できていない可視化機能もあります。もし表示崩れや問題になるケースを見つけたら、ぜひPRをくださると嬉しいです。
Plotly.jsを利用するにあたって、Reactコンポーネントとして扱うための薄いラッパーライブラリーもあるようです。しかしそちらを使用するとplotly.js-distが使えず依存関係が大幅に増えることや、使わなくともそれほど大変ではないことを踏まえ利用していません。Reactコンポーネント側では <div id=”graph-edf”> のようなdivタグを表示し、Plotly.jsを直接呼び出しています(こちらも結局やっていることはPlotlyのReactライブラリーも同じようです)。
React Testing LibraryとJestによるユニットテスト
optuna-dashboardのユニットテストは、Google Summer of Codeで参加したHuzi Cheng(@chenghuzi)さんが追加してくれました。テストランナーおよびAssertionライブラリーとしてJestを使用し、ReactコンポーネントのレンダリングやDOMへのアクセスにはreact-testing-libraryを使用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
it("Filter rows of DataGrid", () => { interface DummyAttribute { id: number key: string value: number } const dummyAttributes = [ { id: 1, key: "foo", value: 1 }, { id: 2, key: "bar", value: 1 }, { id: 3, key: "bar", value: 2 }, { id: 4, key: "foo", value: 2 }, { id: 5, key: "foo", value: 3 }, ] const columns: DataGridColumn[] = [ { field: "key", label: "Key", filterable: true }, { field: "value", label: "Value", sortable: true, }, ] const { queryAllByText } = render( <DataGrid columns={columns} rows={dummyAttributes} keyField={"id"} /> ) expect(queryAllByText("bar").length).toBe(2) // Filter rows by "foo" fireEvent.click(queryAllByText("foo")[0]) expect(queryAllByText("foo").length).toBe(3) expect(queryAllByText("bar").length).toBe(0) }) |
みなさんが開発をする中でユニットテストの結果がなぜかFAILしているというときには、 screen.logTestingPlaygroundURL() を呼ぶとブラウザ上でDOMの状態をチェックできるのでぜひ開発の際は活用してください。
Pyppeteer(with Headless Chrome)によるVisual Regression Testing
Plotly.jsによるグラフ描画が主な処理になるため、ユニットテストで保証できる振る舞いはそれほど多くありません。グラフの描画に必要な情報もやや複雑で、OptunaのFrozenTrialという試行情報を各テストケースで用意するのは大変です。また可視化用のコンポーネントは多く、そのすべてにおいて網羅的なユニットテストを確保することは現実的ではありませんでした。
そこでPyppeteerとHeadless Chromeを使った簡易的なVisual Regression Testingを用意しています。実際にさまざまな目的関数をOptunaで評価して、その評価結果の入ったInMemoryStorageをもとにDashboardを起動、Pyppeteerでアクセスします (全体のコードは こちら)。実行すると↓の画像のようにスクリーンショットが生成されるため、様々な目的関数に対する表示結果をひと目で確認できるようになりました。
この方法では、実際にOptunaで最適化を回し、そのStorage情報を使ってJSON APIサーバーを立ち上げているので、FrozenTrialのfixtureを手動で頑張って用意する必要はありません。Optunaは最適化の過程で探索空間が変化することがあります。さまざまな探索空間が存在しうる状態でダッシュボードが壊れないことを確認するのは大変な作業です。過去には探索空間が変化するStudyを表示する際にクラッシュしてしまうバグを見逃し、PRをマージしてしまったこともありましたが、このテストにより確認が容易になりました。
但しPyppeteerは開発がほぼ止まりつつあるソフトウェアなので、あまりヘビーに使うのは避けたほうがいいかもしれません。今後動かなくなるタイミングがくればSelenium等に移行する予定です。
おわりに
本記事ではoptuna-dashboardの設計や仕様技術について解説しました。Optunaの利用ユーザーの多くはWebフロントエンド技術に親しみがないかもしれませんが、本記事をきっかけに開発に参加してくれる方が増えてくれると嬉しいです。
脚注
※1: プログラムのビルドやテスト・Linterの実行方法については DEVELOPMENT.md に残しています。
※2: Optunaは以前からBokeh CLIを使った簡易的なWebダッシュボードを提供していました。こちらは実装がシンプルだった一方で機能の拡張が難しく、現在はすでにDeprecatedされています。
※3: 過去にはWebSocketを利用することや、サーバー側でグラフを画像としてレンダリングしてフロントエンド側で表示することを提案されたこともありましたが、設計をシンプルに保ちつつ、ダッシュボードのインタラクティブ性を損なわないためにはこの設計が最もバランスがいいと思います。
※4: Goptunaには多目的最適化やハイパーパラメータの重要度などいくつかの機能がまだサポートされていません。そのためoptuna-dashboardのほうがGoptunaのWebダッシュボードよりも開発が進んでいます。
Author