Featured image of post デカルトモードでAI駆動開発する方法

デカルトモードでAI駆動開発する方法

目次

背景

  • 最近めっきり自分の手でコードを書かなくなった
  • AIに書いてもらったほうが速くて正確だから
  • しかし、AIでAIシステムを作る時は課題が多い

課題

AI駆動開発の課題

  • AIでAIシステムを開発する一番の問題は変更がシーソー化する件
  • AI駆動でコードを書くと機能を追加するのが速いし、レビューが追いつかない
  • そのため、AIを信じて機能をガンガン追加してもらう
  • そうすると気が付かないうちにエンバグやデグレを起こしている事が多い
  • Aを解決するためにBを足した結果、CとDの問題が新たに発生したみたいな現象
  • それをさらにアドホックに直すと課題が発散して、まるで開発が収束しない
  • さらに、そもそも課題だと思っていたが実際は課題じゃないことも多い
  • 加えて、特に安い簡便な解決策ほど副作用があったりする(例えば正規表現で安易に回避など)

シーソー化の解決策

  • そのシーソー化の解決策は、検証しながら開発すること
  • 疑って確認しながら開発するので、デカルトモードと名付けた
  • デカルトモードは課題解決に飛びつかない実装方法
  • 課題を検証して、仮説も検証して、解決策の検証する

デカルトモードは一言でいうと以下になる。

デカルトモードとは、観測された問題をすぐに修正せず、再現条件、原因仮説、評価指標、比較対象を先に定義し、効果が確認された変更だけを本番コードへ反映する開発方法

注意点

  • AIモデルの開発はモデルのデータセットのDLや特徴量の計算など色々容量を食う
  • そのため、数TBのHDDとかの大容量のStorageで作業を行う事を前提としている
  • 学習に使う特徴量とかはIOの関係上SSDに置かれるが、作業場自体はHDDでやっている
  • このフローでは、Storageに関しては富豪プログラミングのように使うので注意

事前準備

環境変数

.envrcにwandbとhugginfaceなどの環境変数を配置して、direnvで読み込ませる。

1
2
export WANDB_API_KEY=wandb_v1_xxxx
export HF_HOME=/media/mike/xxx/.huggingface

特に実験ログはwandbに保存するとよさげ。

安定化モード

seedは再現性確保で最も重要な話なので、必ず決定論的に実験ができるようにする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def set_seed(seed: int):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)


def enable_determinism(strict: bool):
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
    torch.backends.cuda.matmul.allow_tf32 = False
    torch.backends.cudnn.allow_tf32 = False
    torch.use_deterministic_algorithms(strict, warn_only=not strict)
    print(f'use_deterministic_algorithms(strict={strict}, warn_only={not strict})  '
          f'+ TF32 無効化 + cudnn.deterministic=True')


# 実行
set_seed(seed)
enable_determinism(True)
print('set_seed / enable_determinism 定義完了')

逆説的に、決定論的にしないと、そこが常に疑えるので、そもそもAIとdiscussionや検証しても意味がなくなる。

セットアップ系のコード

モデルのDLや外部リポジトリのセットアップは、それぞれのpackageに以下のようなコードを用意する。

1
2
3
setup/01_dl.sh
setup/02_build.sh
setup/03_run.sh

複数の実験で使いまわすmodelなどは、.cacheではなく、パッケージ毎に、modelsなどに保存しておく。

全体的な構造

基本的なフォルダ構造

以下のようなpackage_nameが複数ある、モノリポ上で開発や実験を管理する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
repository_name/
├── package_name_b/
│   ├── sprint_20241020/
│   │   ├── README.md
│   │   ├── exp_01_baseline/
│   │   ├── exp_02_threshold_tuning/
│   │   └── exp_03_ood_gate/
│   ├── sprint_20241103/
│   │   ├── README.md
│   │   ├── exp_01_hard_negative/
│   │   └── exp_02_ensemble/
│   └── sprint_20241202/
│       ├── README.md
│       └── exp_01_final_evaluation/
└── package_name_a/

名前空間はpackagessprintsexprimentsを省いた、フラットなディレクトリ構造にする(その代わり接頭辞が必要)。

NOTE: expはexperimentの省略

フォルダの意味

  • repository_name:
    • いわゆるgitフォルダのトップ
  • package_name:
    • モノリポのパッケージの一つ
    • マルチパッケージで分割統治して関心の分離が目的
  • sprint_xxx:
    • 期間毎に分けるのが目的
    • sprint単位で.venvpyproject.tomlなども分ける
  • exp_xxx:
    • あるテーマの実験のPDCAを回すためのフォルダ
    • ここが基本的な作業場所になる

基本的にパッケージ配下のsprintやexpはガンガンコピーして使う。

全体的な意味

  • 複数のシステムを混ぜない
    • システムはマルチパッケージのモノリポにする
    • そのイメージとしてはJavaのMavenのマルチモジュールを参考にしている
  • 複数の期間を混ぜない
    • 過去のシステムを変更してrollbackしたいことがある
    • もしくは過去のシステムと比較検証する必要もある
    • sprintで区切らないと検証に手間がかかる
  • 複数の実験を混ぜない
    • 複数の実験を闇雲にやると混乱のもと
    • 実験はテーマを決めて、expで単一の実験のみを行う

パッケージ

package_nameのルール

パッケージ名のフォーマットは以下にしている:

{repository_name}-{category_name}-{package_name}-{application_name}-{version}

  • {repository_name}-{category_name}
    • パッケージ名のprefixとなっている
  • category_name
    • そのパッケージの目的を表すとグループ名として使う
  • application_name
    • 例えば、trainingserverなど

package_nameの理由

  • こう切っている理由は、repository_nameで前方一致が効くので検索しやすくなるため
  • また、このように粗結合にすると、いらない時に入れ替えたり、削除したりすることも容易になる
  • 特に、何が性能がいいかは前提や実験方法にもよるので、比較するためには複数の手段が必要になる
  • そのため、その目的であるcategory_nameというグループ名も入れている

packageの接続パターン

2つの接続パターンは次の2種類ある:

  • 依存としてimportする場合
    • パッケージを別のパッケージにimportする場合はBOM(Bill of Materials)が必要になる
    • その場合は、cateogry_namecommonとかにするとわかりやすい
  • 依存として粗結合につなげる場合
    • http serverなどのように粗結合につなげる場合はBOMは不要になる
    • IPCの場合は相互の依存関係になるので、adapter系のpackageがあると便利

expの中の構造

データを自分で作る場合

  • 自分の手で動かしてAIモデルの検査をしなければならない時のフロー
    • 例えば、自分の音声や動画、画像など
  • 以下の6段階のワークフローを通している
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
runs
  ↓ 観測事実・証拠を保存
issues
  ↓ 再現可能な課題と根因調査、仮説を作る
eval
  ↓ A/B検証して採用案のメリデメを明らかにする
src
  ↓ 検証済みの変更だけ昇格
tests
  ↓ 解決した課題を再発不能にする回帰テストを残す
docs
  ↓ 判断理由と棄却案と知見を記録する

それぞれの意味は以下になる:

  • 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:
    • 諸々のドキュメントをまとめる。意思決定のログなども

つまり、観測 → 再現 → 仮説 → 比較検証 → 本番昇格 → 再発防止という流れ。

データをマニュアルで作らない場合

マニュアルでたくさんデータが不要な場合のフロー:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
テーマを決める
テーマについてissue.md上でdiscussionをする
issueが本当かを検証する
issueの解決策について検証する
解決策を実行するために、データを集めたり作ったりする
コードで実験
検証
分析

以下のようなフォルダ分けを行う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.
├── issues.md
├── eval.md
├── error_analysis.md
├── sumamry.md
├── input/
│   ├── seeds/
│   ├── train/
│   └── val/
├── output/
│   └── checkpoints/
├── third_party/
├── notebooks/
│   └── xxx.ipynb
├── src/
│   ├── train.py
│   └── eval.py
└── tests/
    └── test.py
  • こちらはシステムではないので先に実行する事が先行しない
  • データを基に精度を検証していくフローとなる
  • データが不足する場合はData Augmentationをすることになるがそれもrunのテーマとなる
  • 例えば、人の手で少数データを作る(シード)-> 帰納法法で属性や作り方のを抽象化する(レシピ)-> コードなどに落として増やすなど

全体を通したエッセンス

  • データと再現性
    • まずは実行logなどは全て保存する
    • データドリブンでAIに開発させるのが目的
    • データがあれば問題の再現性を検証できる
  • AIにGuessさせない
    • 一番大切なのはdon’t guess, measure
    • つまり、AIがguessで解答させなくする
    • これをやられると信頼関係が崩れる
  • 課題や解決策に飛びつかない
    • issueでmdベースで課題についてdiscussionする
    • 理由は、課題が再現しても本物の課題かを議論する
    • 前提が間違っていたり、実現可能性が引くい可能性もある
  • 解決策に飛びつかない
    • 解決策についてもmdでdiscussionする
    • つまり、課題->解決とは安易に進まない
    • solutionに安易に飛びつくとパッチワークになる
    • 例えば、正規表現だらけのその場限りのスペゲティコードとか

改めて考えると、最適化停止問題のように、探索と活用を別々に分けることが本質かもしれない。

まとめ

  • AIによるシーソーゲームの解決方法は細かく仮説検証すること
  • つまり、ラチェットを締めるように確実に開発する手法
  • 言い換えると、「一方向には進むが、逆戻りしない歯車」みたいに開発を進めるべき
  • 奇しくも、ローカルでgithubのissueやPRを再現している事に近い
  • まあ、AIで推測でコードを書かせないのが大切
  • シンプルにデータドリブンで疑いながら検証しつつ開発する手法
Built with Hugo
テーマ StackJimmy によって設計されています。