目次
背景
- 最近めっきり自分の手でコードを書かなくなった
- AIに書いてもらったほうが速くて正確だから
- しかし、AIでAIシステムを作る時は課題が多い
- そこでいくつかのパターンに分けてやり方を変えている
AIシステムの開発パターン
AIシステムの開発パターンは大きく分けて2パターンある:
- ①AIモデルを使うシステムを作成
- 自分で実際にシステムを実行してデータを貯め、問題を見つけて改善する
- マニュアルでシステムの実験が必要
- ②AIモデル自体の作成
- 基本的に自分で実行してデータを貯める必要はない
- seedやデータを予め用意して実験
つまり、モデルを開発するのか、モデルを使うものを開発するのかの違い。
共通する課題
課題
- AIでAIシステムを開発する一番の問題は変更がシーソー化する件
- AI駆動でコードを書くと機能を追加するのが速いし、レビューが追いつかない
- そのため、AIを信じて機能をガンガン追加してもらう
- そうすると気が付かないうちにエンバグやデグレを起こしている事が多い
- Aを解決するためにBを足した結果、CとDの問題が新たに発生したみたいな現象
- それをさらにアドホックに直すと課題が発散して、まるで開発が収束しない
- さらに、そもそも課題だと思っていたが実際は課題じゃないことも多い
- 加えて、特に安い簡便な解決策ほど副作用があったりする(例えば正規表現で安易に回避など)
解決策
- そのシーソー化の解決策は、検証型開発
- 検証型開発は課題解決に飛びつかない実装方法
- 課題を検証して、仮説も検証して、解決策の検証する
検証型開発は一言でいうと以下になる。
検証型開発とは、観測された問題をすぐに修正せず、再現条件、原因仮説、評価指標、比較対象を先に定義し、効果が確認された変更だけを本番コードへ反映する開発方法
具体例
①AIモデルを使うシステムを作成する場合
- 自分の手で動かしてAIモデルの検査をしなければならない時のフロー
- 以下の6段階のワークフローを通している
| |
それぞれの意味は以下になる:
- runs:
- 自分でシステムを動かした時に出たすべての実行ログ、実行日時ごとにフォルダ分けされる
- runsは実行時のログは実行時の画像や動画、音声も含めてjsonlなどで全ての実行ログを保存しておく
- 例えば、
runs/2024-1010-1012/{001_face.jpg, 001_out.txt, 002_voice.wav, logs.jsonl}みたいな感じ
- issues:
- 課題をフォルダにまとめて再現コードも含めたもの、課題を見つけてもすぐ直さず温めておく場所
- issuesは問題が起きてもすぐ直さず、runsのデータから再現をさせて一旦放置する場所
- そこでまずは問題をrunsのログをベースに簡単なコードで再現できる形にする
- その上で原因の特定や解決方法の仮説を出す
- その理由はその課題を解決することによるside effectもあるし、実はedge caseだったりすることもあるから
- 原因と相関を混同したり、そもそも問題ではないことが多いのが理由
- eval:
- 課題を解決する方法を検証するために、A vs Bなどの検証を行う場所
- ここで仮説の検証や実際の効果測定を行う
- src:
- 実際のコード置き場、課題解決方法で検証が終わったあとにここに変更を加える
- tests:
- 課題解決方法で検証が終わったものに対して再発しないようにCheckを入れる場所
- 課題に対して再発しないように回帰テストを入れる
- なお、失敗に対してもxfail(期待通り失敗)も入れる
- docs:
- 諸々のドキュメントをまとめる。意思決定のログなども
つまり、観測 → 再現 → 仮説 → 比較検証 → 本番昇格 → 再発防止という流れ。
②AIモデル自体の作成の場合
AIモデルの開発では、以下のようなフローをたどる。
| |
以下のようなrunsをトップにしたフォルダ分けを行う。
| |
- こちらはシステムではないので先に実行する事が先行しない
- データを基に精度を検証していくフローとなる
- データが不足する場合はData Augumentationをすることになるがそれもrunのテーマとなる
- 例えば、人の手で少数データを作る(シード)-> 帰納法法で属性や作り方のを抽象化する(レシピ)-> コードなどに落として増やすなど
共通
secretについて
.envrcにwandbとhugginfaceなどの環境変数を配置して、direnvで読み込ませている。
| |
セットアップ
モデルのDLや外部リポジトリのセットアップは以下のようなコードを用意する。
| |
こうしないと、HFの.cacheフォルダに無駄にデータが貯まる事になるため、こうしている。
パッケージの管理方法
パッケージ名のフォーマットは以下にしている:
{workspace_name}-{category_name}-{package_name}-{application_name}-{version}
- つまり、モノリポのように管理している
- こう切っている理由は、前方一致が効くので見やすく、整理もされて、検索しやすいのが理由
- また、粗結合にすると、いらない時に入れ替えたり、削除したりすることも容易になる
- 特に、何が性能がいいかは前提や実験方法にもよるので、複数の手段が必要になる
application_nameは例えば、api_serverやmodel_trainingなど
2つの接続パターン:
- 依存としてimportする場合
- パッケージを別のパッケージにimportする場合はBOM(Bill of Materials)が必要になるはず
- 依存として粗結合につなげる場合
- http serverなどのように粗結合につなげる場合はBOMは不要になる
パッケージ化のイメージとしてはJavaのMavinのマルチモジュールに近いかもしれない。
全体を通したエッセンス
- まずは実行ログは全て
runsに保存する - その目的は、問題が再現して原因を完全に判明するため
- つまり、AIがguessで解答させなくする目的がある(再現できなければ別の原因)
- そして、issueでmdベースでdiscussionする
- 理由は、課題が再現しても本物の課題かを議論するため
- つまり、課題->解決とは安易に進まない事が目的
- solutionに安易に飛びつくと正規表現だらけのその場限りのスペゲティコードになる
- 一番大切なのはdon’t guess, measure
- 別の言い方だと、探索と活用を別々に分けることが本質
- (そう考えると、最適停止理論(37%ルール)が適用できるのだろうか?)
まとめ
- AIによるシーソーゲームの解決方法は細かく仮説検証すること
- つまり、ラチェットを締めるように確実に開発する手法
- 言い換えると、「一方向には進むが、逆戻りしない歯車」みたいに開発を進めるべき
- 奇しくも、ローカルでgithubのissueやPRを再現している事に近い
- まあ、AIで推測でコードを書かせないのが大切
- シンプルにデータドリブンでの開発ということ
