# 学習・評価仕様 (Training & Evaluation)

## 前処理 (Preprocessing)

| 項目 | 方針 |
| --- | --- |
| リサイズ | アスペクト比を保って 224×224 に収め，余白は黒で埋める（元画像は幅 449〜900・高さ 279〜1025 でばらつく） |
| 正規化 | ImageNet の mean / std（事前学習の統計に合わせる） |
| マスク外領域 | **黒 (0, 0, 0)**（実データで確認済み．[TEST_01](../06_TEST/TEST_01_データセット構成調査.md)）．リサイズ時のパディングも黒で統一する |

## データ拡張 (Augmentation)

**色を変える拡張は既定で無効にする．** 舌の色そのものが正解ラベルであり，color jitter・彩度変更・ホワイトバランス変更はラベルを壊す．

| 使う | 使わない（既定） |
| --- | --- |
| 左右反転・微小回転・微小スケール・ランダムクロップ | 色相・彩度・明度ジッタ・グレースケール化・強い正規化 |

※ 撮影条件差の吸収のために色系の拡張を試す場合は，**独立した実験**として効果を測る．

## モデル (Model)

| 項目 | 初期設定 |
| --- | --- |
| アーキテクチャ | ConvNeXt-Tiny（timm・ImageNet 事前学習） |
| 出力 | 5 クラス |
| 学習方式 | 全層ファインチューニング（データが少ないと判断した場合は段階的凍結を試す） |
| 最適化 | AdamW ＋ cosine annealing（warmup 付き） |
| 早期終了 | 検証セットの macro F1 を基準にする |

まず 1 本を通すことを優先し，比較は timm での差し替えで後から行う．

### 学習の設定 (Training Settings)

設定 YAML の `model`・`train` 節で与える（[SPEC_01](SPEC_01_アーキテクチャ設計.md)「設定ファイル」）．既定値は次のとおり．

| 項目 | 既定値 | 備考 |
| --- | --- | --- |
| `model.name` | `convnext_tiny` | timm のモデル名 |
| `model.pretrained` | `true` | ImageNet 事前学習の重み（初回はサーバがダウンロードする）．合成データの試験では `false` |
| `train.image_size` | 224 | アスペクト比を保って長辺を合わせ，余白を黒で埋める（レターボックス） |
| `train.batch_size` | 32 | — |
| `train.max_epochs` | 40 | — |
| `train.patience` | 8 | 検証の macro F1 がこのエポック数改善しなければ打ち切る |
| `train.lr` | 1e-4 | AdamW |
| `train.weight_decay` | 0.05 | AdamW |
| `train.warmup_epochs` | 2 | 線形 warmup の後 cosine で減衰 |
| `train.inner_val_ratio` | 0.15 | 交差検証の各 fold で，学習側から早期終了用の検証データを取る割合（下記） |
| `train.num_workers` | 4 | DataLoader |
| `train.amp` | `true` | CUDA のときだけ混合精度 |
| `train.class_weighting` | `none` | 損失のクラス重み（下記「クラス不均衡」→「対策の設定」） |
| `train.sampler` | `random` | 学習データの抜き出し方（同上） |

- データ拡張（学習時のみ）: 左右反転（確率 0.5）・回転 ±10°・拡大縮小 0.9〜1.1 倍・平行移動わずか．はみ出した部分は黒で埋める．**色を変える拡張は行わない**（上記「データ拡張」）
- 正規化: ImageNet の mean / std
- デバイス: CUDA があれば `cuda`（`CUDA_VISIBLE_DEVICES` で割り当てられた GPU），無ければ CPU
- 高速化のため，レターボックス済みの画像（uint8）をメモリに載せてから学習してよい（1085 枚 × 224² × 3 ≒ 160 MB）
- 平行移動は ±5%．学習時は端数のバッチを捨てる（BatchNorm を持つモデルで 1 件のバッチになると止まるため）
- 学習率は step 単位の線形 warmup の後，`max_epochs` の終わりで 0 になる cosine
- 早期終了は検証 macro F1 の厳密な改善（`>`）で判定し，同点なら先のエポックを残す
- `convnext_tiny` の事前学習の重みは timm の既定タグ `convnext_tiny.in12k_ft_in1k`（ImageNet-12k で事前学習し ImageNet-1k で微調整したもの）が使われる

## 交差検証の実行 (Cross-Validation Run)

**1 つのジョブで 5 つの fold を順に学習・評価**し，1 つの実験ディレクトリ `runs/<実験ID>/` にまとめて書き出す．「ややあり込み」と「ややあり除外」は別のジョブ（別の実験 ID）として回す．

1. manifest 全体で患者単位・9 グループ層化の 5-fold 分割を作る（`data.severities` に関わらず同じ分割．上記「程度（ややあり）の扱い」）
2. 各 fold について
   1. 学習側（held_out 以外）から `train.inner_val_ratio` の患者を検証用に取る（患者単位・9 グループ層化．**`data.severities` を適用する前の全程度から取る**．seed は `split.seed + fold 番号`．学習の seed も同じ）
   2. `data.severities` を**学習用と検証用にだけ**適用する（held_out には適用しない．下記）
   3. 検証の macro F1 が最良のエポックの重みを採用する（早期終了）
   4. **held_out（全程度を含む）で 1 回だけ評価する**
3. 全 fold の held_out の予測をまとめて集計する

### 評価の出し方 (Evaluation Output)

held_out は `data.severities` に関わらず**全程度の画像**で評価し，次の部分集合ごとに指標を出す．これで 2 つの条件のジョブを同じ土俵で比べられる．

| 部分集合 | 内容 | 用途 |
| --- | --- | --- |
| `all` | held_out の全画像 | ややあり込みの全体性能 |
| `common` | held_out のうち `strong` と `normal` | **2 条件の比較**（両条件とも学習に含まれる部分） |
| `mild` | held_out のうち `mild` | 境界例の扱い |

- 各部分集合で: macro F1・accuracy・クラス別 precision / recall / F1・混同行列（`common` と `all`）
- `mild` では加えて**予測の内訳**を出す: 淡紅と予測された割合・元のクラスと予測された割合・その他のクラスと予測された割合（クラス別と全体）
- fold ごとの値と，fold 間の平均 ± 標準偏差（不偏標準偏差．ddof=1）を出す．全 fold の予測をまとめた値（`pooled`）と，混同行列の全 fold の合計も出す
- **macro F1 は，その部分集合の正解に現れるクラスだけで平均する**（`mild` には淡紅の正解が無いため，5 クラスで平均すると淡紅への予測が F1 = 0 として平均を下げてしまう）．クラス別の指標も正解に現れるクラスだけを出す．淡紅への流れ込みは `mild` の予測の内訳で見る
- 画像ごとの予測（個票）は `predictions.csv` に保存し，**回収しない**

### 出力 (Run Artifacts)

[SPEC_01「実験の出力」](SPEC_01_アーキテクチャ設計.md)の名前に従う．

| ファイル | 内容 | 回収 |
| --- | --- | --- |
| `config.yaml` | 解決済み設定・git ハッシュ・seed | する |
| `metrics.json` | fold ごと・fold 平均の指標（上記の部分集合ごと），各 fold の最良エポック，**所要時間**（下記） | する |
| `history.csv` | fold・エポックごとの学習損失・検証損失・検証 macro F1・学習率・**そのエポックの所要秒数** | する |
| `confusion_matrix.csv` | 全 fold 合計の混同行列（`common` と `all`） | する |
| `log.txt` | ログ | する |
| `figures/*.png` | 学習曲線・混同行列 | する |
| `split.json`・`predictions.csv` | 分割と個票の予測（患者 ID を含む） | しない |
| `checkpoints/fold<k>.pt` | 各 fold の最良の重み（サーバには常に保存する） | `--with-checkpoints` のときだけ |

**所要時間の記録**（2026-09-28 ユーザー要望）: `metrics.json` に次を含める．モデル間・条件間で計算コストも比べられるようにするため．

- 全体の所要秒数（データの読み込みから書き出しまで）と，データの読み込み（レターボックス化）にかかった秒数
- fold ごとの学習の所要秒数・held_out の評価の所要秒数・実際に回したエポック数・1 エポックあたりの平均秒数
- 実行環境: デバイス（GPU 名または CPU）・モデル名・パラメータ数

## データ分割 (Data Split)

**すべての分割は患者単位で行う．** 同一患者の画像が複数の分割に跨ってはならない．

現在のデータセットは **1 患者 1 枚**であることを確認済みのため（2026-09-27．[TEST_01](../06_TEST/TEST_01_データセット構成調査.md)），画像単位の分割が患者単位の分割と一致する．ただし実装は常に `patient_id` 単位で分割し，将来同一患者の複数枚が追加されても正しく動くようにする．

| 局面 | 方式 |
| --- | --- |
| 日常の試行 | 患者単位の単一分割（train / val / test）．クラス分布を層化する |
| 結論を出すとき | 患者単位の 5-fold 交差検証 |

- 分割は `patient_id` と seed から決定的に生成し，`config.yaml` に記録する
- **test セットはモデル選択に使わない**（val で選び，test は最後に一度だけ見る）
- **層化は「クラス × 程度」（`<label>_<severity>` の 9 グループ）で行う**．「ややあり除外」の条件で mild を抜いた後も，各分割のクラス比が保たれるようにするため（下記「程度（ややあり）の扱い」）．最小のグループは 51 枚で，比率 0.15 なら各分割に 8 枚程度入る
  - 患者の層化グループは，その患者の `label` と `severity` で決める．同じ患者に複数の `severity` がある場合（将来の複数枚）は，最も強いもの（`strong` > `mild` > `normal`）を使う
- 分割後にグループごとの件数を記録し，偏りを確認する
- **実装方式**: 1 患者は 1 つの label しか持たない（manifest の検証で保証）ため，患者の一覧を層化グループで分ければ「患者単位」と「層化」が同時に成り立つ
  - 単一分割: 層化グループごとに患者を seed で並べ替え，「患者数 × 比率」の四捨五入で test・val の人数を取り，残りを train とする．train に患者が残らないグループがあれば停止する
  - K-fold: 患者の一覧に scikit-learn の `StratifiedKFold`（shuffle あり・seed 指定．層化ラベルは層化グループ）を使う．評価側を `held_out` と呼ぶ（fold 内でさらに val を取るかは Step 4・5 で決める）．患者数が fold 数に満たないグループがあれば停止する
  - どちらも最初に患者 ID を並べ替えるため，manifest の行順に依存しない
- 分割結果（各分割の患者 ID）は**個票に当たるため手元へ回収しない**（サーバ上の `runs/` にのみ保存する．[SPEC_01](SPEC_01_アーキテクチャ設計.md)「回収ホワイトリスト」）

## 程度（ややあり）の扱い (Severity Handling)

**「ややあり」（`mild`）は，臨床上は淡紅（正常）と判断されることが多い境界例である**（2026-09-27 ユーザー確認）．学習に入れるか否かで結果が変わりうるため，**入れる場合と入れない場合の両方で実験する**．

### 実験条件 (Conditions)

設定 `data.severities`（[SPEC_01](SPEC_01_アーキテクチャ設計.md)「設定ファイル」）で使う程度を選ぶ．

| 条件 | `data.severities` | 使う画像 | 枚数 |
| --- | --- | --- | --- |
| ややあり込み | `[strong, mild, normal]`（既定） | すべて | 1085 |
| ややあり除外 | `[strong, normal]` | 「あり」と淡紅（正常） | 754 |

- どちらの条件でも `label` は元のまま（5 クラス）．「ややあり」を淡紅に付け替える扱いは行わない
- 「ややあり除外」でも 5 クラスすべてが残る（淡紅は `normal` のみ，他の 4 クラスは `strong` が残るため）

### 分割との関係 (Split)

- **分割は `data.severities` に関わらず，manifest 全体（1085 枚）で 1 回だけ行う**．条件ごとの除外は，分割の**後**に各分割（train / val / test）から該当する画像を取り除いて適用する
- これにより，「ややあり除外」の各分割は「ややあり込み」の各分割の部分集合になり，**2 つの条件のモデルを同じ患者の test で比較できる**（条件ごとに分割し直すと test の患者が入れ替わり，差が条件によるものか分割によるものか切り分けられない）
- 同じ seed なら，どちらの条件でも同じ分割結果になる

### 評価 (Evaluation)

| 評価 | 対象 | 目的 |
| --- | --- | --- |
| 条件間の比較 | test のうち `strong` と `normal`（両条件に共通する部分） | 同じデータで 2 つの条件のモデルを比べる |
| ややあり込みモデルの層別評価 | test 全体と，`mild` の部分 | 境界例でどれだけ誤るか（「評価指標」の層別評価） |
| **ややあり除外モデルに mild を推論させる** | test のうち `mild` | mild の画像が **淡紅と予測される割合**と**元のクラスと予測される割合**を出す．臨床で淡紅と判断されやすいという知見がデータ上でも成り立つかを確かめる |

- `mild` の画像に対して淡紅と予測することを「誤り」と断定しない．上表の割合として報告し，解釈は人間が行う

## クラス不均衡 (Class Imbalance)

実データのクラス分布は最大 / 最小 = 380 / 104 ≒ **3.7 倍**（[TEST_01](../06_TEST/TEST_01_データセット構成調査.md)）．中程度の不均衡であり，初回は対策を入れずに素の結果を見る．混同行列でどのクラスが落ちているかを確認してから，クラス重み付き損失等を**独立した実験**として追加し，効果を切り分けて評価する．

程度ラベル（`severity`）を含めた 9 クラス分類は採らない（最小クラス 51 枚・不均衡 6.6 倍となり学習が不安定になる．目的は TIASjudge の 5 分類の置き換えであるため）．

### 対策の設定 (Countermeasures)

素の結果（[TEST_03](../06_TEST/TEST_03_交差検証結果.md)）で紫・淡白の再現率が低かったため，次の 2 つを設定で選べるようにする．既定はどちらも「対策なし」で，既定のままなら従来と同じ学習になる．

| キー | 値 | 内容 |
| --- | --- | --- |
| `train.class_weighting` | `none`（既定）・`inverse`・`sqrt_inverse` | 損失のクラス重み．クラス c の学習画像数を n_c，全体を N，クラス数を K として，`inverse` は w_c ∝ N / (K・n_c)，`sqrt_inverse` はその平方根．**重みは学習データでの平均が 1 になるよう正規化する**（Σ_c (n_c / N)・w_c = 1．損失の大きさを対策なしと揃え，学習率の意味を変えないため） |
| `train.sampler` | `random`（既定）・`balanced` | `balanced` は各画像を 1 / n_c の確率で重複ありで抜き出し，どのクラスもほぼ同じ頻度で学習に出す．1 エポックに抜き出す枚数は学習画像数と同じ |

- クラスの画像数 n_c は，**各 fold の学習用データ（`data.severities` を適用した後）**から数える．検証用・held_out の枚数は使わない
- 学習用データに 1 枚も無いクラスがある場合は停止する（重みが定義できないため）．対策を指定しないとき（既定）は従来どおり停止しない
- **重み付きの損失はバッチの件数で割って平均する**（Σ_i w_{y_i}・CE_i / B）．PyTorch の `CrossEntropyLoss(weight=w)` の既定は重みの和で割るため，上の正規化が効かなくなる．正規化で揃うのは学習データ全体での平均であり，1 つのミニバッチの損失はクラス比の偏りに応じて対策なしとずれうる
- `sampler: balanced` でも，学習時に端数のバッチを捨てる規則（「学習の設定」）は従来どおりかかる
- 重みは学習時の損失にだけかける．**検証の損失は重みなし**で計算する（対策の有無で早期終了の比較が変わらないようにするため．早期終了の基準は従来どおり検証 macro F1）
- `class_weighting` と `sampler: balanced` は同時に指定できるが，偏りを二重に補正することになるため実験では組み合わせない
- `metrics.json` に，使った対策（トップレベルの `imbalance`）と，fold ごとのクラス別枚数・クラス重み・1 回の抜き出しで各クラスが選ばれる確率（各 fold の `class_balance`．使わない側は `null`）を記録する
- 重みを使うと `history.csv` の学習損失は重み付きの値になる（検証損失は重みなし）．対策なしの run と学習損失を直接比べない

効果は，対策なしの結果と**同じ seed・同じ fold どうしの対応比較**で評価する（common の macro F1 と，クラス別の再現率・適合率・混同行列）．少ないクラスの再現率が上がっても，他クラスの適合率が下がる入れ替わりが起きうるため，全体の数字だけで判断しない．

## 色特徴分類器 (Color Feature Classifier)

ラベルが舌の色そのものであり，データが少ないため，舌領域の色の統計量を特徴にした古典的な分類器を CNN と**同じ分割・同じ評価**で比べる（`tias-train color-cv`．結果は [TEST_05](../06_TEST/TEST_05_色特徴分類器の実験.md)）．GPU は使わない．

### 特徴量 (Features)

元解像度の画像の**舌領域の画素だけ**から取る（背景は RGB がすべて 0 の画素．レターボックス縮小は補間で背景の黒が縁に混ざるため使わない）．長辺が `color.max_side`（既定 512．`null` で縮小しない）を超える画像は最近傍法で縮小する（新しい色を作らないため）．画素を CIE L\*a\*b\*（D65）に変換し，次の 107 次元を作る．

| 特徴 | 次元 |
| --- | --- |
| L\*・a\*・b\*・C\*（彩度 √(a\*² + b\*²)）・h（色相角 atan2(b\*, a\*)．度，(−180, 180]）それぞれの平均・標準偏差（母標準偏差．ddof=0）・10/25/50/75/90 パーセンタイル | 5 × 7 = 35 |
| a\*-b\* の 2 次元ヒストグラム（a\* [0, 50]・b\* [−10, 30] を 8 × 8 に等分．舌領域の画素数に対する割合） | 64 |
| L\* のヒストグラム（[20, 100] を 8 等分．割合） | 8 |

- ヒストグラムの範囲外の画素は端のビンに入れる（割合の合計を 1 に保つ）．範囲は全データの平均色（[TEST_02](../06_TEST/TEST_02_データセット集計結果.md)）を根拠に固定する
- 色相角は 0° をまたがないよう (−180, 180] で表す（舌の色は赤〜紫で a\* > 0 に集まるため）
- 舌領域の画素が無い画像は停止する（manifest の何件目かで示し，ファイル名は出さない）

### 分類器と候補の選び方 (Classifier & Selection)

設定の `color` 節で指定する（CNN の設定には不要．`color` 節が無い設定で `color-cv` を実行すると停止する）．

| キー | 値 | 内容 |
| --- | --- | --- |
| `color.classifier` | `logistic`・`hist_gb` | 標準化＋多クラスのロジスティック回帰，または scikit-learn の HistGradientBoostingClassifier |
| `color.logistic.c_values` | 正の数のリスト（既定 0.01・0.1・1・10・100） | 正則化の強さ C の候補 |
| `color.hist_gb.learning_rates` × `max_depths` | 正の数・正の整数または `null` のリスト | 学習率と木の深さの候補（組み合わせ） |
| `color.max_side` | 正の整数または `null`（既定 512） | 特徴抽出の前に縮小する長辺の上限 |

- 分割は CNN と同じ（「交差検証の実行」の 1〜2.2．同じ seed なら `split.json` が完全に一致する）
- 各 fold で，候補ごとに学習用データで学習し，**内側の検証の macro F1 が最良の候補**を採用する（厳密な改善で更新し，同点なら先の候補．CNN の最良エポックの選び方に対応）．採用したモデル（学習用データだけで学習したもの）で held_out を 1 回だけ予測する
- `train.class_weighting` のクラス重み（「クラス不均衡」）を sample_weight として使う．`train.sampler: balanced` は未対応で，指定すると停止する
- 学習用データに無いクラスの予測確率は 0 とし，5 クラスの形を保つ
- 出力は CNN と同じ名前・同じ形式（「出力」）．ただし `history.csv`・学習曲線・重み（`checkpoints/`）は出さない．`metrics.json` の `environment.model_name` は `color_logistic`・`color_hist_gb`，最良エポックは `null` で，fold ごとの採用した候補（`selected_params`）と候補ごとの検証 macro F1（`candidates`），特徴名（`color_classifier`），特徴抽出の所要秒数（`timing.feature_extraction_seconds`）を加える

### 判定モデルの書き出し (Model Export)

TIASjudge に渡す最終モデルは `tias-train color-export` で作る（引き渡しの手順は [SPEC_04](SPEC_04_TIASjudge引き渡し仕様.md)．結果は [TEST_07](../06_TEST/TEST_07_セグメンテーションの再現と判定モデルの書き出し.md)）．

- 当面は `color.classifier: logistic` のみ（`hist_gb` は停止する）
- C の選び方: 全データで交差検証と同じ患者単位 5-fold（`split.seed`）を組み，学習側と held_out の両方に `data.severities` を適用して，候補ごとの held_out macro F1 の fold 平均が最良の C を採る（厳密な改善で更新し，同点なら先の候補）．この値は C を選ぶためのもので，性能の評価には交差検証（`color-cv`）の値を使う
- 最終モデルは `data.severities` 適用後の全データで学習する（乱数は `split.seed`，`train.class_weighting` は sample_weight）．学習データに 1 枚も無いクラスがあれば停止する
- 出力は `color_model.json`（回収対象．患者 ID・ファイル名・画像単位の値を含まない）と，C の選択の記録（`metrics.json` の `c_selection`）
- NumPy だけの参照実装 `src/tias_judge_model/deploy/color_model.py` は，学習側と同じ特徴（ビット一致）と予測確率（差 1e-12 以下）を返す．本パッケージの他のモジュールに依存させない（TIASjudge にそのままコピーして使うため）

## 評価指標 (Metrics)

| 指標 | 役割 |
| --- | --- |
| **macro F1** | 主指標（クラス不均衡下で少数クラスを無視できないため） |
| accuracy | 参考（クラス分布に引っ張られる） |
| クラス別 precision / recall | どのクラスが弱いかの特定 |
| 混同行列 | 誤りの方向（隣接クラスとの混同か，飛んだ誤りか）の確認 |

- 舌色が順序尺度として扱える（淡白 → 淡紅 → 紅 → 暗赤紅 → 紫 のような並び）なら，**quadratic weighted kappa** を追加する．順序の妥当性は TIASjudge の定義と医学的な解釈を確認してから決める（未確定）
- **程度別の層別評価**: `severity` ごと（`strong` / `mild`）にクラス別 recall を出す．「ややあり」（`mild`）は境界例であり誤りが集中すると予想されるため，全体の指標だけでは改善の効果が見えない
- 医師ラベルの評価者間一致率が分かる場合は，モデル性能の上限の目安として併記する

## ベースライン (Baseline)

既存 TIASjudge のルールベース診断を同一テストセットで実行し，同じ指標で並べる．実装の取り込み方（移植 / 別リポジトリ参照）は未定とし，ベースライン実装に着手する時点で決める．

## 再現性 (Reproducibility)

- seed を設定ファイルで固定し，`config.yaml` に git コミットハッシュとともに記録する
- 完全なビット再現性（`torch.use_deterministic_algorithms`）は速度とのトレードオフのため求めない．代わりに**同一設定で複数 seed を回して振れ幅を確認する**
- 共用サーバのため，使用する GPU は明示的に指定する

## 未確定事項 (Open Items)

- 舌色 5 分類を順序尺度として扱えるか（quadratic weighted kappa の採用可否）
- 入力解像度 224 で足りるか（元画像は平均 675×614 で，約 1/3 に縮小することになる）
- 黒背景が面積の大半を占めるため，正規化・統計を画像全体で取るか舌領域内のみで取るか
- 「ややあり」を含めるか否かの結論（両条件の実験結果を見て決める）
- ルールベース診断の取り込み方（色特徴分類器の「平均色だけ」の版を作れば，TIASjudge に近い条件の比較の土台になる．TEST_05）
- 医師ラベルの評価者間一致率（分かれば性能の上限の目安になる）
