# アーキテクチャ設計 (Architecture)

## 基本方針 (Principles)

1. **実データに触るコードはサーバでしか動かさない**．手元では合成データで同じコードパスを流す
2. **手元への回収はホワイトリスト方式**．回収対象を明示的に列挙し，列挙外（画像・manifest・患者 ID）は持ち帰れないようにする
3. 設定は YAML に集約し，pydantic で検証する．コードに絶対パス・閾値を書かない
4. 実験は `runs/<実験ID>/` に閉じる．同じ設定と seed で同じ結果に辿れるようにする

## ディレクトリ構成 (Directory Layout)

```text
TIASJudgeModel/
├── configs/                      # 実験設定 YAML
├── src/tias_judge_model/
│   ├── schema.py                 # pydantic 設定スキーマ
│   ├── data/                     # manifest 読み込み・Dataset・分割・前処理
│   ├── models/                   # timm ラッパ（アーキテクチャ差し替え点）
│   ├── training/                 # 学習ループ・損失・スケジューラ
│   ├── evaluation/               # 指標・混同行列・レポート生成
│   ├── baseline/                 # ルールベース比較（扱いは未定）
│   ├── remote/                   # リモート実行（tar over ssh・tmux・回収）
│   └── cli/                      # エントリポイント
├── scripts/                      # 合成データ生成・manifest 生成
├── tests/
├── data/                         # .gitignore（合成データ．実データは置かない）
└── runs/                         # .gitignore（実験結果）
```

依存方向は `cli` → `training` / `evaluation` → `data` / `models` の一方向とする．`data` と `models` は CLI・学習ループを import しない．

## データ契約 (Data Contract)

元データは研究室 PC の `C:\Users\Papin\clinical-data\TIASJudgeModel\` にラベルごとの 9 フォルダとして置かれている（[ENV_03](../02_ENV/ENV_03_管理者用環境構築手順.md)）．サーバへコピーした後の配置（`~/data/TIASJudgeModel/`）:

```text
~/data/TIASJudgeModel/
├── images/
│   ├── light_red_normal/    # ラベル＿程度のスラッグごとのディレクトリ
│   ├── dark_red_strong/
│   ├── dark_red_mild/
│   ├── purple_strong/
│   ├── purple_mild/
│   ├── red_strong/
│   ├── red_mild/
│   ├── pale_strong/
│   └── pale_mild/
└── manifest.csv             # 学習が読む唯一の入口
```

ディレクトリ名は `<label>_<severity>` のスラッグとする（研究室 PC 側の日本語フォルダ名との対応は [ENV_03](../02_ENV/ENV_03_管理者用環境構築手順.md)．日本語名のまま転送すると文字コード差で壊れるため）．

`manifest.csv` の列:

| 列名 | 内容 | 備考 |
| --- | --- | --- |
| `image_path` | `images/` からの相対パス | — |
| `patient_id` | 患者を一意に識別する ID | **必須**．分割の単位になる |
| `label` | 舌色 5 分類のいずれか | 元データのフォルダ名に由来 |
| `severity` | 程度（`strong` / `mild` / `normal`） | 学習には使わず，評価の層別分析に使う |
| `note` | 任意の備考 | 除外理由等 |

- manifest はサーバ上の生成スクリプトで作る．元データはラベルごとのフォルダに分かれているため，**フォルダ名から `label` と `severity` を決める**（対応表は [TEST_01 データセット構成調査](../06_TEST/TEST_01_データセット構成調査.md)）
- **`patient_id` について**: 現在のデータセットは 1 患者 1 枚であることを確認済み（2026-09-27）．したがって画像 ID をそのまま `patient_id` とする．**この前提が崩れる（同一患者の複数枚が追加される）場合は，実際の患者 ID を入れ直す必要がある**（同一患者が分割を跨ぐと評価が壊れるため）．分割処理は常に `patient_id` 単位で行い，前提が変わってもコードを変えずに済むようにする
- 前提が確認できない新規データに対しては，画像 ID を患者 ID と見なす代替を**自動では行わない**．生成を止めて人間が判断する

### manifest の検証規則 (Validation Rules)

読み込み時・生成時とも，問題があれば**黙って除外せず `ValueError` で停止する**（`.claude/rules/error-handling.md`）．メッセージは行番号・件数・クラス名のみで示し，患者 ID・ファイル名を含めない．

| 対象 | 停止する条件 |
| --- | --- |
| 読み込み | 必須列（`image_path`・`patient_id`・`label`・`severity`）の欠落，`patient_id` が空，未知の `label` / `severity`，無効な組み合わせ，`image_path` の重複，同一 `patient_id` に異なる `label`，`image_path` が絶対パス・`..` を含む・区切りが `\` |
| 生成 | `images/` 直下の想定外のディレクトリ名・ファイル，クラスディレクトリ内のサブディレクトリ・PNG 以外のファイル，**画像が 0 枚のクラスディレクトリ**（クラスが欠けたまま学習が進むのを防ぐため），ファイル名の stem の重複（患者 ID と見なせなくなるため） |

- `note` 列は読み込み時に省略できる（無ければ空文字）
- 同一 `patient_id` で同じ `label` の複数行は許す（1 患者複数枚に備える）

## 設定ファイル (Configuration)

実験条件は `configs/*.yaml` に置き，pydantic で検証する（未知のキーは拒否する）．

| キー | 内容 | 既定値 |
| --- | --- | --- |
| `data.root` | データのルート（`images/` と manifest を含む）．`~` を展開する．相対パスはカレントディレクトリ基準 | なし（必須） |
| `data.manifest` | manifest のパス．相対パスは `data.root` 基準 | `manifest.csv` |
| `data.severities` | 学習・評価に使う程度（`strong`・`mild`・`normal` の部分集合）．空・重複・未知の値は不可．**分割の後に適用する**（[SPEC_02](SPEC_02_学習・評価仕様.md)「程度（ややあり）の扱い」） | `[strong, mild, normal]` |
| `split.seed` | 分割の seed | 42 |
| `split.val_ratio`・`split.test_ratio` | 単一分割の比率．各 0 より大きく，合計 1 未満 | 0.15・0.15 |
| `split.n_folds` | 交差検証の fold 数（2 以上） | 5 |

- **環境変数 `TIAS_DATA_ROOT` が設定されていれば `data.root` より優先する**（サーバと手元で同じ設定ファイルを使い回せるようにするため）
- 学習・モデル関連の設定は `model`・`train` 節に置く（キーと既定値は [SPEC_02「学習の設定」](SPEC_02_学習・評価仕様.md)）．学習しないコマンド（`tias-dataset`）では省略できる

## クラス定義 (Class Definition)

内部表現は英字 slug，表示は日本語とし，整数 ID を固定して混同行列の軸をぶらさない．

| ID | slug | 表示 |
| --- | --- | --- |
| 0 | `pale` | 淡白 |
| 1 | `light_red` | 淡紅 |
| 2 | `red` | 紅 |
| 3 | `dark_red` | 暗赤紅 |
| 4 | `purple` | 紫 |

※ 表示名は元データのフォルダ名に合わせている（「暗赤紅」）．順序尺度として扱えるかは未確定（[SPEC_02](SPEC_02_学習・評価仕様.md) 参照）．

## 実験の出力 (Run Artifacts)

```text
runs/<実験ID>/
├── config.yaml            # 解決済み設定 ＋ git コミットハッシュ ＋ seed
├── history.csv            # エポックごとの損失・指標
├── metrics.json           # 最終指標（macro F1・accuracy・クラス別）
├── confusion_matrix.csv   # 混同行列の数値
├── log.txt
├── figures/               # 図専用（学習曲線・混同行列等）．症例画像は書き込まない
│   └── confusion_matrix.png
├── dataset_summary.json   # データセット集計の結果（集計値のみ．データセット集計の実行時）
├── split.json             # 分割結果（患者 ID を含む．回収しない）
└── checkpoints/best.pt
```

実験 ID の書式: `YYYYmmdd-HHMMSS_<名前>_<git短縮ハッシュ>`（`<名前>` は学習なら設定ファイル名，データセット集計なら `survey`）

- サーバには `.git` を送らないため，git のハッシュは手元で求めて `tias-remote run` が環境変数 `TIAS_GIT_COMMIT` で渡す（[SPEC_03](SPEC_03_リモート実行仕様.md)）．未コミットの変更（**未追跡ファイルを含む**．`git status --porcelain` が空でない）があるときは末尾に `-dirty` が付く．値は英数字で始まり英数字・`_`・`-` の 64 文字以内でなければ拒否する．環境変数が無い場合（手元での直接実行等）は `nogit` とする

- **`figures/` には図だけを置き，症例画像（舌画像そのもの・切り出し・誤分類例の画像）を書き込まない**．回収ホワイトリストが `figures/*.png` を回収対象にしているため
- 患者 ID・画像ファイル名を含む出力（分割結果・個票の予測）は上記の回収対象外の名前で保存する

## 回収ホワイトリスト (Pull Whitelist)

手元へ回収できるのは以下の**名前が一致するファイルのみ**．列挙外のファイルは回収しない（拡張子や中身での判断はしない）．リモート実行ツール（[SPEC_03](SPEC_03_リモート実行仕様.md)）がこの制約を実装する．

| 回収する（`runs/<実験ID>/` 配下） | 回収しない |
| --- | --- |
| `config.yaml`・`history.csv`・`metrics.json`・`confusion_matrix.csv`・`log.txt`・`dataset_summary.json` | 上記以外のすべて（`split.json`・個票の予測・画像・manifest 等） |
| `figures/*.png` | `figures/` 以外にある `*.png` |
| `checkpoints/*.pt`（**明示的に指定したときのみ**） | 指定が無いときの `checkpoints/` |

- ジョブのログ（`runs/_remote_logs/<ジョブ名>.log`）はファイルとしては回収せず，`tias-remote logs` で表示して確認する（ログに個人情報を出さない規則が前提．`.claude/rules/coding-style.md`）
- データディレクトリ（`~/data/TIASJudgeModel/`）からは何も回収しない
- 個票（画像ごとの予測）が必要な分析はサーバ上で行い，集計結果だけを上記の名前で書き出して持ち帰る

## CLI (Entry Points)

| コマンド | 役割 | 実行場所 |
| --- | --- | --- |
| `tias-dataset build-manifest` | `images/` を走査して `manifest.csv` を書き出す（既存があれば `--overwrite` が必要） | サーバ |
| `tias-train cv --config PATH` | 5-fold 交差検証で学習・評価し，`runs/<実験ID>/` に書き出す（[SPEC_02「交差検証の実行」](SPEC_02_学習・評価仕様.md)）．最後に実験 ID を出力する | サーバ（GPU） |
| `tias-train color-cv --config PATH` | 色特徴分類器（[SPEC_02「色特徴分類器」](SPEC_02_学習・評価仕様.md)）を同じ分割で交差検証し，同じ形式で書き出す（学習曲線・重みは出さない）．最後に実験 ID を出力する | サーバ（CPU） |
| `tias-dataset survey` | データセットを集計して `runs/<実験ID>/` に集計値だけを書き出す（下記） | サーバ（結果の要約のみ手元へ） |
| 学習 | 設定 YAML を受け取って学習する | サーバ |
| 評価 | 学習済み run の評価・レポート生成 | サーバ |
| リモート実行 | コード送信・ジョブ実行・結果回収・GPU 空き確認（複数サーバ．[SPEC_03](SPEC_03_リモート実行仕様.md)） | 手元 |

コマンド名は実装時に確定し，[ENV_04 開発コマンド](../02_ENV/ENV_04_開発コマンド.md)に記載する．

### データセット集計の出力 (Dataset Survey Output)

`tias-dataset survey` は manifest と画像を読み，**集計値だけ**を `runs/<実験ID>/dataset_summary.json` と `runs/<実験ID>/figures/` に書き出す．**画像単位の値・患者 ID・ファイル名は出力しない**（ログも同様）．集計の単位は `<label>_<severity>` の 9 グループと，クラス（5）・全体．

| 項目 | 内容 |
| --- | --- |
| 件数 | グループ・クラス・程度ごとの画像数と患者数．条件ごと（`data.severities` の 2 条件）の件数 |
| 読み込み | 読めなかった画像の件数，RGB 以外のモード・アルファチャンネル付きの件数 |
| サイズ | 幅・高さの最小・最大・平均・中央値 |
| 舌領域の割合 | 画像に占める舌領域（背景でない画素）の割合の最小・平均・最大 |
| **舌領域の色（Lab）** | 画像ごとに舌領域の L\*・a\*・b\* の平均を求め，グループごとにその平均・標準偏差・中央値・10/90 パーセンタイルを出す |
| **グループ間の色差** | グループ平均どうしの色差 ΔE（CIE76）．特に各 `mild` グループについて「淡紅（`light_red_normal`）との ΔE」と「同じクラスの `strong` との ΔE」を並べる |
| 分割の確認 | 既定の設定で分割したときの，各分割・グループの件数（患者 ID は出さない） |

- **背景の定義**: RGB がすべて 0 の画素を背景（マスク外）とし，それ以外を舌領域とする（[TEST_01](../06_TEST/TEST_01_データセット構成調査.md)．マスク外は黒 (0,0,0)）
- **Lab の計算**: sRGB（ガンマ補正を外して線形化）→ XYZ（D65）→ CIE L\*a\*b\*（D65 白色点）．新しい依存を増やさず NumPy で計算する．画像の平均 Lab は，**画素ごとに Lab へ変換してから舌領域で平均する**（平均 RGB を変換するのではない）
- RGBA 等のアルファ付き画像は「RGB 以外のモード」と「アルファ付き」の両方に数え，RGB に変換してから測る
- **図**（`figures/`）: グループごとの L\*・a\*・b\* の分布（箱ひげ図等）．**画像そのもの・画像の切り出しは描かない**
- 読めない画像があっても集計は止めず，件数として報告する（集計は現状把握のためであり，学習時の fail fast とは目的が異なる）．ただし manifest の検証エラーは従来どおり停止する
