はじめに
数理最適化が関わるプロジェクトは、技術的にも業務的にも不確実性が高く、想像以上に疲弊しがちです。
「モデルは合っているはずなのに、実務では使われない」「リリース直前で要件がひっくり返る」「実行不可能との戦いが終わらない」──心当たりがある方も多いのではないでしょうか。
本記事は、数理最適化システム開発を売りにする企業のエンジニア向けに、
- 最適化が関わるプロジェクトを
- できるだけ苦労せず回せるようになるための
- 実装・設計・進め方のプラクティス
を、個人的な経験をもとにまとめたものです1。
数理最適化プロジェクトで消耗しがちな方の、何か1つでも負担を減らせたら幸いです。
対象読者
- 最適化システムを、手を動かして開発している人(俗にいうエンジニア)
- 最適化システムの開発をマネジメントしている人(俗にいうプロジェクトマネージャー)
背景
最近『実務で使える数理最適化の考え方』を読みました。『実務で使える数理最適化の考え方-基礎から学ぶモデリング』著者の岩永さんも記事にしている通り、最適化を実務で活かすための総括が書かれた良書だと思います。
本を読めば、
- 何に気を付けるべきか
- 何をやらなければならないか
はかなり明確になります。一方で、
- それを現場でどう実装・運用するか
- どうすれば無理なく続けられるか
は、各チーム・各エンジニアに委ねられている部分でもあります。
本記事では、私自身の経験から見えてきた「こうすると、比較的負担が少なく(=ラクに)回せることが多い」という実践パターンを共有します。
本論
最適化プロジェクトで『ラク』するために心掛けていること
結論から言うと、意識しているのは次の3点です。
- 早く、頻繁にお客さんに結果を見せる
- 変化に柔軟なコード設計
- 最適化モデルをデバッグしやすくする
以下、それぞれについて詳しく書いていきます。
早く、頻繁にお客さんに結果を見せる
『実務で使える数理最適化の考え方』でも強調されていますが、最適化プロジェクトでは「ユーザーは本当に欲しいものを理解していない」という前提に立つことが非常に重要です。
多くの場合、要件定義では言語化しやすい制約や目的からモデル化が始まります。しかし実務では、実際の解を見て初めて気づく重要な要素や、事前には言葉にできなかった違和感が必ず存在します。その結果、モデル自体は完成しているにもかかわらず、リリース直前になって「この結果では業務に使えない」という判断が下される、という悲劇が起こりがちです。
これを避けるために私が意識しているのは、プロジェクトのごく初期段階から、最適化を行うとどのような解が出てくるのかを具体的に見せることです。精度の高いモデルである必要はなく、まずは「最適化するとこういう形で決まる」というイメージを共有することが重要です。
さらに、一度見せて終わりにするのではなく、入力条件やケースを変えながら、できるだけ頻繁に結果を見せます。言語化されていない要求は、複数の結果を比較して初めて顕在化することが多いためです。
変化に柔軟なコード設計
『実務で使える数理最適化の考え方』ではアンチパターンとして「頻繁に生じる急な要件変更」が挙げられていますが、正直、これは避けられないと思います。前述の通り、ユーザーに頻繁に結果を見せると、
- 「ここはこうしたい」
- 「ここは絶対に必要」
といった要望が大量に出てきます2。要望を引き出すために結果を見せているので当たり前なのですが。
なので重要なのは、
要件変更をなくすこと
ではなく、
要件変更が来ても致命傷にならない設計にすること
です。リリース間近での変更が来ないことを祈るよりかは、来るものだと考えて、そのリスクをできるだけ減らそうと努力することの方がずっと有意義です。
では、変更に柔軟なコード設計とはどうすればよいのか。私個人が、数理最適化エンジニアが実践した方がよいと思うものを2つ取り上げます。
- 得たい解を得る方法は Strategy パターン
- テスト駆動で開発する
得たい解を得る方法は Strategy パターン
まず大前提として、あなたが解きたいその問題は、最適化技術を使う必要はない、ということを念頭においてください。ソルバーで解く場合も、greedy やヒューリスティックで解く場合も、本質的には「何らかの値を決定する」という点では同じです。
この「解の出し方」を柔軟に切り替えられるようにするため、有効なのがGOFのデザインパターン3の1つ、Strategy パターンです。他のかたの記事 でも記述されていますが、Strategy パターンは「アルゴリズムの切り替え」を簡単にしてくれます。単純にソルバーに解かせるだけでなく、特別な前処理をした後にソルバーで解くとか、辞書式最適化で何回もソルバーを回して解を得るとか、実験でやってみたいことはたくさん出てきます。その時に Strategy パターンを使用していれば、「何かしらの値を決定する」部分だけ変更すれば、他の入出力ファイルの取得をするコードなどには手をつけなくてよくなるわけですね。
例えば、Python で簡単に書くと次のような形になります。
from abc import ABC, abstractmethod
# Strategy
class SolveStrategy(ABC):
@abstractmethod
def solve(self, problem):
pass
class SolverStrategy(SolveStrategy):
def solve(self, problem):
# 数理最適化ソルバーを呼ぶ
return problem.solve_with_solver()
class GreedyStrategy(SolveStrategy):
def solve(self, problem):
# 簡易的なヒューリスティック
return problem.solve_greedily()
# Context
class Optimizer:
def __init__(self, strategy: SolveStrategy):
self.strategy = strategy
def run(self, problem):
return self.strategy.solve(problem)
上記であれば、辞書式最適化を試してみたい、となったときも SolveStrategy を継承した DictionaryStrategy を作成して Optimizer に渡せばよいだけで、他の SolverStrategy などの処理に影響は及ぼさずに済みます。
テスト駆動で開発する
「テスト駆動開発」という単語はご存知でしょうか?
そもそもの言葉の意味と、それにより「変更に柔軟なコード設計」が達成される理屈については、名著『テスト駆動開発』の訳者 t-wadaさんが保守しやすいソフトウェアを支えるテスト駆動開発に関する記事を執筆されています。
私の感覚では、テスト駆動開発の実践によって、変化に対する対応が簡単なコードベースを作れるようになった、と感じます。
「なぜ変化に柔軟なコード設計のために、テスト駆動で開発するのか」については上記の記事を、「ではどのようにやれば、テスト駆動で開発するといえるのか」については『テスト駆動開発』を読んでみてください。非常にわかりやすくまとまっております。
最適化モデルをデバッグしやすくする
最適化モデルは単純なプログラムと違い、
- 決定変数
- 制約条件
- 目的関数
が揃って初めて意味を持ちます。「テストによって、コードのすべての行を通ったことを確認した」だけでは意味がありません。複雑な if-else の組み合わせを網羅することと同様に、最適化モデルにおいて「入力がこの状況なら最適化によってこういった解が出力される」ということを把握しておかなければ、最適化モデルの挙動がおかしい時の原因追及に苦労します。
しかし現実的な問題として、すべての組み合わせを網羅していては時間がいくらあっても足りません。最適化モデルをデバッグしやすくするために、私は以下2つを心がけています。
- 実行不可能は極限まで起こさないようにする
- エッジケースに関する軽量なテストを残す
実行不可能は極限まで起こさないようにする
『実務で使える数理最適化の考え方』でも、
最適化システムの開発は実行不可能から始まる
と述べられています。とはいえ、「入力で0以上を仮定している部分に負の値が入っていた」などの不毛なデバッグは減らしたいですね。
そのために、私は具体的には以下2つを実践しています。
- データ不備は validation で止める
- Python なら pydantic が非常に使いやすいです
- それでも実行不可能になる場合、制約を緩和して解き直し、得られた解がどの制約に違反するのかわかるようにする
- 制約を緩和するモデルは計算が重くなるため、本番稼働時にはパフォーマンスの問題が出るかもしれないです。なので、デバッグ時は制約緩和あり、本番稼働時には制約緩和なし、といったように解き方を分けられるとよいです。ここで上述の Strategy パターンが役に立つわけですね
エッジケースに関する軽量なテストを残す
最適化プロジェクトにおいてよくあるのが、エッジケースに対応するための制約の組み込みです。これの扱いの難しいところは、普段は意識しないので、制約が効いているのか否か判断が難しいところですね。他の制約との兼ね合いの結果、わけわからん解を出すときに初めて「この制約はおかしい」となるのですが、いかんせん普段意識していないのでデバッグに時間がかかります。
そこで私が意識しているのは、テストデータを人力で作って回せるようにする、です。
機械学習と違い、最適化はある程度決定的に解を決められることが多いです。少なくともサイズの小さいモデルであれば、即時的に、常に同じ最適解を得られることが多いです4。これはテストにおいて大きな利点です
「ユーザーのエッジケースでのデータ直接使えばいいのでは?」という話もあるかと思いますが、ユーザーが使用するデータは基本問題サイズが大きいのでテストに時間がかかる上、入力データ形式が変更したなどのときに回らなくなります。
付随して、テストのためのデータを自分で作ることで、「このモデルはこういう状況だとこう動くんだな」という、モデルへの理解を深められる「文書」になるので、テストで残しておくのはおすすめです。
終わりに
他にも
- クラス設計はドメイン駆動設計
- 最適化のシステム開発は agile 開発と親和性高い
なども書きたいのですが、分量が爆発するので今回はここまでにします。
本記事が、**「最適化プロジェクトを、少しラクに回す」**一助になれば幸いです。