# 開発進捗ログ

<!--
本ファイルは追記型のフル進捗ログ．

- 最新エントリは下に追記する（時系列順．古いものが上，新しいものが下）
- CLAUDE.md の「開発進捗」セクションは最新 1 行のみ（上書き運用）
- 詳細・失敗パターン・設計判断はすべて本ファイルに残す
- 書式・運用ルールは .claude/rules/progress-log.md を参照
  （本ファイルの編集時に自動ロードされる）
-->

## 立ち上げ（フェーズ 1: 方針決定）

- [x] **プロジェクト方針の確定と要件定義書の作成**: TIASjudge（ルールベースの舌診支援アプリ）の舌色 5 分類を機械学習で推定する研究プロジェクトとして，目的・スコープ・成功基準を確定し [PLAN_01 要件定義書](03_PLAN/PLAN_01_要件定義書.md) を作成した．開発モードは solo．
  - 前提（データ取り扱い）: 臨床データは手元 PC に置かない．原本は研究室 PC に保管し，研究室 LAN 内で計算サーバへ一度だけコピーする．手元へ持ち帰るのは非個人情報の派生物（集計指標・ログ・モデル重み）のみ．リモート実行の構成は SmTIAS-LightSim の方式（コードを手元から送り，結果を回収）を踏襲する
  - 設計判断: 本フェーズのゴールを「現状把握」とし精度の絶対値目標を置かない．評価は**患者単位分割を必須**とし（画像単位のランダム分割は同一患者が train / test に跨り過大評価になる），既存ルールベースを同一テストセットのベースラインとして並べて比較する
  - スコープ外: セグメンテーションの学習・5 分類以外の所見・TIASjudge への組み込み実装・撮影条件の色補正
  - 未確定: 画像枚数・患者数・クラス分布・ラベル形式・患者 ID の追跡可否は研究室 PC で実データを確認して確定する

## 立ち上げ（フェーズ 2〜3: 技術選定・環境構築）

- [x] **技術スタックの確定**: PyTorch ＋ timm・uv・YAML ＋ pydantic・リモート実行は SmTIAS-LightSim の `sm-remote` 移植，実験記録は軽量自作，CI なしと決め [ENV_01](02_ENV/ENV_01_技術スタック.md) を作成した．
  - 設計判断: 実験管理に W&B・MLflow を採らず自作にしたのは，臨床データ由来の指標を外部サービスへ送らない方針と，実験数に対する運用コストの釣り合いから
- [x] **環境構築**: GitHub private リポジトリを作成し Dependabot alerts / security updates を有効化（検証済み）．`.github/dependabot.yml`（`uv` ＋ `github-actions`）・`.gitignore` 更新（`data/`・`runs/`・重み）・`pyproject.toml`（Python 3.12）・パッケージ骨組み・README を作成し，[ENV_02](02_ENV/ENV_02_環境構築手順.md)〜[ENV_04](02_ENV/ENV_04_開発コマンド.md) を整備した．Newton に `~/work/TIASJudgeModel` と `~/data/TIASJudgeModel`（700）を作り，コードを tar over ssh で送って `uv sync` を通し **torch 2.14.0+cu130・CUDA 有効・GPU 3 台**を確認した．
  - 確認できたこと: Linux では PyPI の wheel がそのまま CUDA 版で入る（ENV_01 の想定どおりで `[tool.uv.sources]` での対処は不要）．GPU は 3 台のうち 1 台が他ユーザー使用中で，共用前提の GPU 指定が必要
  - 失敗パターン: ① 手元の `uv sync` が PyPI からのダウンロードで connection reset（`safetensors`）→ 再試行中 / ② 非対話 ssh では `uv` が PATH に無く `command not found`（`export PATH="$HOME/.local/bin:$PATH"` が必要）/ ③ 踏み台経由で `Newton.local` の名前解決が一時的に失敗（再試行で復帰）．② ③ は ENV_02 のトラブルシューティングに記録した
  - 未実施: push・PR，臨床データの初回コピー（研究室 PC → Newton），倫理審査・データ利用許諾の確認．仕様設計（フェーズ 4）はドラフトを提示済みで保存保留

## 立ち上げ（フェーズ 4〜6: 仕様設計・規約整備・開発計画）

- [x] **仕様設計**: [SPEC_01 アーキテクチャ設計](04_SPEC/SPEC_01_アーキテクチャ設計.md)と [SPEC_02 学習・評価仕様](04_SPEC/SPEC_02_学習・評価仕様.md)を作成した．
  - 設計判断 ①: 手元へ持ち帰れるものを**回収ホワイトリスト**として仕様に固定した（画像・manifest・個票は回収しない）．「回収しないよう気を付ける」では守れないため，ツールの制約として実装しテストで固定する
  - 設計判断 ②: `manifest.csv` を学習の唯一の入口とし，`patient_id` を必須にした．取得できない場合に画像 ID を患者 ID と見なす代替はしない（同一患者が分割を跨ぐと評価が壊れる）
  - 設計判断 ③: **色を変えるデータ拡張を既定で禁止**した．舌の色そのものが正解ラベルであり，color jitter 等はラベルを壊す
  - 設計判断 ④: クラス ID を固定表（0=淡白 … 4=紫）にして混同行列の軸をぶらさないようにした
- [x] **規約整備**: `.claude/rules/` に coding-style・project-structure・testing・error-handling を作成し（path-scoped rule），`pyproject.toml` に ruff の選択ルールと pytest の `slow` マーカーを追加した．CLAUDE.md の必須ルール欄にプロジェクト固有の制約（データの持ち出し禁止・患者単位分割・色拡張の禁止等）を明記した．
  - 方針: 書式・命名・docstring 等の標準部分は SmTIAS-LightSim と同一にし，本プロジェクト固有の規則（臨床データの扱い・個人情報を含むログの禁止・データ整合性の fail fast）だけを追加した
- [x] **開発計画**: [PLAN_02 開発ステップ](03_PLAN/PLAN_02_開発ステップ.md)を作成した（Step 1 基盤 → 2 リモート実行 → 3 データセット集計 → 4 学習最小版 → 5 評価 → 6 ベースライン比較 → 7 実験反復 → 8 結論）．
  - 順序の理由: Step 1・2 は手元で完結しデータのコピー待ちの間に進められる．実データの素性が判明する Step 3 の後に SPEC を更新してから学習に入る

## データセット構成の調査（Step 3 の前倒し分）

- [x] **研究室 PC 上の実データの構成を調査し，仕様に反映**: 画像本体とファイル名を手元に持ち出さず，フォルダ名・件数・画像メタデータ・画素値の集計のみを取得した（記録は [TEST_01](06_TEST/TEST_01_データセット構成調査.md)）．
  - 判明したこと: 9 フォルダ（`<クラス>＿あり` / `<クラス>＿ややあり`，淡紅は「（正常）」のみ）に PNG 計 1085 枚．5 クラス集約で 暗赤紅 380・淡紅 337・紫 145・紅 119・淡白 104（不均衡 3.7 倍）．全画像 `Format24bppRgb`（アルファなし）で**マスク外は黒 (0,0,0)**．サイズは幅 449〜900・高さ 279〜1025 でばらばら（舌領域のバウンディングボックス切り出し）
  - 確定した設計: 程度（あり / ややあり）は manifest の `severity` 列として保持し，**学習は 5 クラス**（9 クラスは最小 51 枚・不均衡 6.6 倍で不利）．評価では `severity` 別の層別 recall を見る．リサイズの余白は背景と同じ黒で埋める．クラス表示名は元データに合わせ「暗赤紅」に修正
  - 分割の前提: **1 患者 1 枚**とユーザーが確認したため画像単位＝患者単位となる．ただし実装は `patient_id` 単位で分割し，将来複数枚が入っても壊れないようにする（前提が変わった時に patient_id を実 ID へ入れ直す）
  - 転送方式の判断: 日本語フォルダ名をそのまま scp すると CP932 と UTF-8 の差で名前が壊れるため，`<label>_<severity>` の ASCII スラッグへ対応付けてコピーする手順を ENV_03 に記載した
  - 運用上の発見: 研究室 PC は「ドキュメント」が OneDrive（大学テナント）にリダイレクトされているため，臨床データをそこに置くとクラウドへ同期される．プロファイル直下の独自フォルダ（`clinical-data`）を使う方針を ENV_03 に明記した
  - 調査の技術メモ: 研究室 PC の PowerShell 出力は CP932 で返るため取得側で UTF-8 変換が必要．複雑なスクリプトは `-EncodedCommand`（UTF-16LE + Base64）で渡すと引用符の問題を回避できる

## 臨床データの転送（Step 3 の前提）

- [x] **研究室 PC から Newton へ臨床データ 1085 枚（443 MB）を転送**: 研究室 PC 上で `scp` を実行させる形にし，画像は研究室 LAN 内で直送した（手元 PC は制御コマンドのみ）．件数はフォルダごとに元と完全一致，非 PNG は 0 件．手順と実施記録は [ENV_03](02_ENV/ENV_03_管理者用環境構築手順.md)．
  - 失敗パターン: ① 研究室 PC には Newton 用の鍵が無く非対話のパスワード認証は不可 / ② **Windows の OpenSSH サーバーはエージェント転送に非対応**のため手元の鍵を貸せない / ③ Newton から研究室 PC を引く経路も不可（研究室 PC の sshd は Tailscale 範囲のみ許可）→ 研究室 PC 専用鍵を作って Newton に登録する方式で解決
  - 判断: 手元 PC を中継する方式は技術的には可能だが，臨床データが手元を通るため方針違反として採らなかった
  - 実装上の注意: PowerShell はネイティブコマンドの引数を展開しないため `scp "...\*.png"` は使えない．ファイルを列挙して 100 件ずつ渡す（コマンドライン長の上限対策）．日本語フォルダ名は `<label>_<severity>` の ASCII スラッグへ対応付けて転送した
  - ユーザー確認事項の反映: データ利用は問題なし・1 患者 1 枚は確定・撮影は TIAS・セグメンテーションは専用の機械学習モデルによる自動処理（TEST_01 に記録）．ベースライン比較（Step 6）は入力条件を揃える設計判断が必要なため一旦後回しとした

## Step 1: 基盤

- [x] **設定スキーマ・クラス定義・manifest の生成/検証・患者単位分割・合成データ生成を実装**: 実データと同じ `images/<label>_<severity>/` 構成を前提に，manifest を学習の唯一の入口とする土台を作った．仕様は [SPEC_01](04_SPEC/SPEC_01_アーキテクチャ設計.md)「manifest の検証規則」「設定ファイル」と [SPEC_02](04_SPEC/SPEC_02_学習・評価仕様.md)「データ分割」に反映した．
  - 設計判断: 1 患者 1 ラベルを manifest の検証で保証し，**患者の一覧をクラスで層化して分ける**方式にした（これで「患者単位」と「クラス層化」が同時に成り立つ）．K-fold は `StratifiedGroupKFold` ではなく患者一覧への `StratifiedKFold` を使う（1 患者 1 ラベルなら層化がより正確）
  - 設計判断: 合成データの `--overwrite` は生成時の目印ファイルがある場所だけを消す（`--out` の指定ミスで実データを消す事故を防ぐ）
  - 発見 ①: `.gitignore` の `data/` がパッケージ本体 `src/tias_judge_model/data/` にも一致し，**パッケージが Git に入らない状態だった** → `/data/`（リポジトリ直下のみ）に修正
  - 発見 ②: サブエージェントの「ruff 通過」報告が実際と食い違っていた（行長エラー 2 件）．以後，lint・テストの成否は司令塔が自分で実行して確認する
  - 判断: 画像が 0 枚のクラスディレクトリは当初警告のみで続行していたが，クラスが欠けたまま学習が進むのを防ぐため**エラーで停止する**よう仕様（SPEC_01）と実装を変更した（ユーザー判断）．大文字の拡張子 `.PNG` は受け付けたままとする（実データはすべて小文字で実害なし）

## Step 2: リモート実行ツール

- [x] **`tias-remote` を実装（Newton・Archimedes の 2 サーバ対応）**: SmTIAS-LightSim の `sm-remote` を移植し，空いている GPU を両サーバから自動で選んでジョブを起動し，結果を回収ホワイトリストに従って持ち帰るツールにした．仕様は [SPEC_03](04_SPEC/SPEC_03_リモート実行仕様.md)．
  - 設計判断 ①: GPU の空き判定は **10 秒観測した最大値**で行う．瞬間値だと，起動と停止を繰り返す GPU を空きと誤認する（Newton で 10 秒間に 0〜74% を往復する GPU を実測．瞬間値判定では「空き」と出ていた）
  - 設計判断 ②: **計算プロセスが無い GPU（未使用）をサーバの優先順位より優先する**（ユーザー判断）．使用率 0% でも他人のプロセスがメモリを確保している GPU は後回しにする
  - 設計判断 ③: 回収ホワイトリストを**名前の完全一致**に厳格化し，図は `figures/` 直下に限定した（`*.png` を広く許すと，誤分類例の舌画像などを持ち帰る経路になる）．pull は受信した tar を全件検査し，1 件でも不正があれば何も展開しない
  - 設計判断 ④: ジョブに `CUDA_DEVICE_ORDER=PCI_BUS_ID` を渡す（CUDA と nvidia-smi で GPU 番号がずれ，空きと判定したのとは別の GPU を使う事故を防ぐ）
  - 環境: Archimedes をセットアップした（手元の鍵の登録は人間が実施．uv の導入・研究室 PC 専用鍵の登録・臨床データ 1085 枚のコピー・Python 環境）．**Linux の torch を cu126 に統一**した（Archimedes のドライバ 535 は CUDA 12.2 までで，PyPI 既定の cu130 版では GPU が使えなかった．両サーバで同じビルドになる利点もある）
  - 失敗パターン: ① IP 指定の ssh が `known_hosts` 未登録で失敗した（これまで別名 `newton.local` でしか接続していなかった）→ IP で登録し，既存の鍵と指紋が一致することを確認 / ② ジョブ終了を待つループが，ログの終了行の書式（実際は `exit N`）を読み違えて空回りした
  - 発見: Archimedes の GPU 0 に，42 日間起動したまま観測中ずっと 0% のプロセス（メモリ確保のみ）がある．今回の表示で消し忘れのようなプロセスが一目で分かるようになった
  - 未決・既知の制約: 「最後に使った時刻」は nvidia-smi に履歴が無いため取れない（サーバに常駐の記録処理を置く必要があり別作業とした）．計算プロセスの一覧が空で返った場合，本当に 0 件か権限で見えないだけかは区別できない（現状の両サーバでは他人のプロセスが見えている）

## Step 3: データセット集計

- [x] **`tias-dataset`（manifest 生成・集計）と「ややあり」の実験切り替えを実装し，実データを集計**: 両サーバで manifest を生成し，Newton で 1085 枚を集計して集計値と図だけを回収した（結果は [TEST_02](06_TEST/TEST_02_データセット集計結果.md)）．
  - 背景: 「ややあり」は臨床上は淡紅（正常）と判断されることが多い境界例である（ユーザー）．**入れる場合と入れない場合の両方で実験する**ことにした（設定 `data.severities`）
  - 設計判断 ①: **分割は全データで 1 回だけ行い，除外は分割の後に適用する**．条件ごとに分割し直すと test の患者が入れ替わり，差が条件によるものか分割によるものか切り分けられない．「ややあり除外」の各分割は「込み」の部分集合になる
  - 設計判断 ②: 層化をクラス単位から**クラス × 程度の 9 グループ**に変えた（mild を抜いた後もクラス比を保つため）
  - 設計判断 ③: `tias-remote run --cpu` を追加した．manifest 生成・集計は CPU だけで動くのに，`run` が必ず空き GPU を要求していた（Newton の GPU が全て使用中で起動できない状況があった）
  - 結果: 読めない画像 0．**ややありの 4 グループ中 3 つは色（ΔE）の上でも淡紅に近く**，臨床の知見と整合した（紅だけは等距離）．一方でグループ間の平均色の差（ΔE 4〜13）に比べてグループ内のばらつきが大きく，平均色だけでは分布が大きく重なる → 精度の見込みは控えめに置く
  - 仕組み: 実験 ID の git ハッシュは，サーバに `.git` を送らないため `tias-remote run` が手元で求めて `TIAS_GIT_COMMIT` で渡す

## Step 4〜5: 学習と 5-fold 交差検証（夜間の自律作業）

- [x] **`tias-train cv` を実装し，実データで 4 モデル × 2 条件 × 3 seed の交差検証を実行**: ユーザー就寝中（2026-09-28 00:23〜）に，確認を省略して実装→テスト→リファクタ→実行まで進めた（ユーザーの許可による．作業ブランチへのコミットも許可済み．GPU は Archimedes の 2 枚を使う許可）．結果は [TEST_03](06_TEST/TEST_03_交差検証結果.md)．
  - 設計判断 ①: 1 ジョブで 5 fold を順に回し 1 つの実験ディレクトリにまとめる．held_out は `data.severities` に関わらず全程度で評価し，部分集合（all・common・mild）ごとに指標を出す．これで「ややあり込み／除外」を同じ土俵で比べられる
  - 設計判断 ②: macro F1 は部分集合の正解に現れるクラスだけで平均する（mild には淡紅の正解が無いため）
  - 進め方の工夫: 本番の交差検証を先に走らせ，その裏でテスト・リファクタを行った（リファクタは挙動を変えず，実行中のサーバのコードを上書きしないよう push を止めた）．比較用の設定を先にすべて作って 1 回の push で済ませた
  - 所要時間の記録: fold ごとの学習・評価秒数・エポック数・1 エポック平均・全体・デバイス・パラメータ数を `metrics.json` と `history.csv` に残す（ユーザー要望）．1 本（5 fold）2〜4 分，36 本で合計 88 分
  - 結果: 「ややあり」を学習から外すと共通部分（あり・淡紅）の macro F1 が上がる（4 組中 3 組で 11〜14/15 fold）．外したモデルは「ややあり」の約半数を淡紅と予測する．最良は ResNet-50（学習率 1e-3）・除外で common macro F1 0.637．紫・淡白の再現率が低い
  - 失敗パターン: 全モデル共通の学習率 1e-4 では ResNet-50 が学習不足になり，「除外」では多数派クラスしか予測しない状態（紫の再現率 0）で止まった → 学習損失の推移から学習不足と判断し，BatchNorm を持つ CNN 2 モデルを 1e-3 で回し直して解消．**この学習率は結果を見て選んだので楽観的になりうる**点を TEST_03 に明記した
  - 止めた判断: 残り時間はあったが，学習率の調整をこれ以上重ねると評価データへの合わせ込みが強まるため，追加実験はせず Step 7 の計画（入れ子の交差検証で全モデル同じ候補から選ぶ）に回した
  - 未実施: ベースライン比較（Step 6）
  - 対応済み: 36 本の重み（約 12 GB）が Archimedes の共用ディスク（残り 40 GB・98%）を圧迫していたため，ユーザーの判断で削除した（空き 52 GB に回復）．重みは同じ設定で数分で作り直せる（ビット単位の一致はしない）．今後は保存するかを設定で選べるようにする
