P1.A.3 で falsify した apparent-gap 仮説の代替として、`training-eval convention coupling` を解消すべく `data.gt_convention = premultiplied` (brush 流) 経路を trainer まで通し、5k smoke を legacy/brush 2 通り × eval legacy/brush 2 通り = 4 通りで cross-eval。brush trainer + brush eval は **31.33 dB (5k 時点)**、legacy 5k baseline の brush eval は 1.63 dB のままで A.3 を再現、symmetry 検証として brush trainer × legacy eval も 1.59 dB に崩壊し coupling が双方向に存在することを実証。GT loader 切替 + config flag (4 file 編集、loss kernel 変更ゼロ、既存 config 不変、deny_unknown_fields 維持) で deliver、Stage 2 (hyperparameter 移植 + 30k full) GO。
P1.A.3 で 「training/eval convention coupling」 が brush gap の主因と判明 (apparent-gap 仮説 falsify、brush convention で trainer の white-bg fit が露呈し 24.88 → 1.67 dB に崩壊)。本 task は coupling の 最小 PoC fix: data.gt_convention=premultiplied flag を新設し、GT loader (load_rgba_premultiplied) を train / val 両方 の経路で選択可能にする。5k smoke を legacy / brush 2 通り × eval legacy / brush 2 通り = 4 通りで cross-eval し、coupling が解消されるか検証。
仮説 OK (上回り)。brush trainer × brush eval = 31.334 dB at 5k iter、legacy 5k baseline の brush eval = 1.628 dB に対し +29.7 dB。Stage 1 OK 閾値 (10+ dB) を +21 dB 上回り、coupling 解消が brush parity への critical path であることを定量実証。さらに symmetry test (brush trainer × legacy eval = 1.595 dB) で coupling が双方向に存在 することも確認 (brush trainer は背景を α≈0 にして fit するので white-bg GT に対して背景全体で MSE≈1)。Stage 2 (full hyperparameter 移植 + 30k bench) に進行 GO。loss kernel 改変ゼロ・既存 config 不変・最小差分 (4 file) で達成。
| # | trainer (gt_convention) | eval convention | quantize8 | PSNR (dB) | min | median | max | vs A.3 30k | 判定 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | **legacy** (white_bg) | legacy | off | **21.874** | 5.896 | 22.639 | 28.806 | -3.005 (5k 短縮による degrade) | 5k iter での legacy baseline |
| 2 | **legacy** (white_bg) | brush | on | 1.628 | 0.473 | 1.686 | 2.050 | -0.039 (A.3 の 1.667 と一致) | **A.3 を 5k で再現**, baseline collapse |
| 3 | **brush** (premultiplied) | legacy | off | 1.595 | 0.991 | 1.614 | 1.944 | — | **symmetry test pass**, brush trainer は逆向きに崩壊 |
| 4 | **brush** (premultiplied) | brush | on | **31.334** | 25.089 | 31.572 | 35.902 | **+29.667 (1.667 → 31.334)** | **hypothesis confirmed**, Stage 1 OK threshold (10) +21 dB |
diagonal (1 と 4) が "training と eval が一致した自然状態"、off-diagonal (2 と 3) が "convention 不一致による崩壊"。diagonal は両方 high (21.87 / 31.33 dB)、off-diagonal は両方 ~1.6 dB という綺麗な対称形 → coupling は training-eval convention の不一致による systematic mismatch であって、brush trainer が片側だけ広い optimum を持つ等の非対称要因ではないことを示す。さらに brush trainer の diagonal が legacy trainer の diagonal を +9.5 dB 上回る 点が重要 (5k vs 5k の同条件で)、brush 流の loss landscape は同 iter で legacy より遥かに容易な fit を提供する。
| 仮説 (Stage 1) | 予測 PSNR | 実測 PSNR | 差分 | 判定 |
|---|---|---|---|---|
| brush trainer × brush eval で 10+ dB | ≥ 10.0 | 31.334 | +21.3 dB | **OK (大幅上回り)** |
| brush trainer × legacy eval で低下 (symmetry test) | 低下 | 1.595 | — | **OK (1.6 dB に崩壊、coupling 双方向)** |
| legacy trainer × brush eval は A.3 30k と同帯 (5k で再現) | ~1.7 | 1.628 | -0.04 | **OK (再現精度 ±0.04 dB)** |
| legacy trainer × legacy eval は 30k から短縮分 degrade | ~20-23 | 21.874 | — | **OK (5k 相応、degrade 3 dB)** |
(A) 背景の取扱い: load_rgba_white_bg は α=0 領域を (r,g,b,a)=(1,1,1,1) に変換 → loss は "背景にも 白く塗れ" と要求 → trainer は opaque-white splat を背景に増殖。load_rgba_premultiplied は α=0 領域を (0,0,0,0) のまま → rendered (rasterize.metal: pix_out = Σ rgb·α·T, α_out = 1-T) と直接比較できる。(B) α channel の役割: loss kernel (loss.metal:31-88) は n_total = W·H·4 で α channel も MSE 対象。premultiplied GT (α=元 PNG α) と rendered α (=1-T) の L1 loss は、α=0 領域で |1-T - 0| = 1-T → 0 即ち 背景 splat の T を 1 に保て (= 透明にせよ) という pressure を自動的に与える。これは brush の match_alpha=0.1 loss と同等の効果が weight 1.0 で発動している状態。Stage 2 では brush の hyperparameter (lr / refine / decay) 移植で更に伸ばせる余地あり。(C) なぜ symmetry test も -20 dB 崩壊するか: brush trainer は背景を α≈0 (rendered では bg=ZERO) として学習 → legacy convention (white-bg GT) と比較すると background pixel 全体で rendered=(0,0,0) vs target=(1,1,1) → MSE≈1 で systematic 崩壊。これも A.3 と同じ機構の鏡像。
splat/crates/splat-io/Cargo.toml — serde = { version = "1.0", features = ["derive"] } を追加 (GtConvention enum を Deserialize 対応)splat/crates/splat-io/src/dataset.rs — 既存 GtConvention enum に #[derive(serde::Deserialize)] + #[serde(rename_all = "snake_case")] + impl Default for GtConvention (= WhiteBg)splat/crates/splat-cli/src/config.rs — DataConfig に #[serde(default)] pub gt_convention: GtConvention を追加 (deny_unknown_fields 維持、既存 configs/*.toml は無指定で従来通り white_bg)splat/crates/splat-cli/src/cmd/train.rs — load_nerf_synthetic → load_nerf_synthetic_with_convention(.., cfg.data.gt_convention) に切替 (train split (line 50) と val split (line 119) の 両方、advisor 警告通り — 不一致だと mid-training PSNR が学習対象と一致しなくなる)splat/configs/2026-05-24-1800-lego-legacybase-5k.toml (新規) — 30k baseline の iter のみ 5k に短縮splat/configs/2026-05-24-1800-lego-brushcompat-base-5k.toml (新規) — legacy-5k から [data] gt_convention = "premultiplied" のみ追加loss.metal / rasterize.metal / refine_*.metal / train_loop.rs / trainer.rs / 既存 configs/*.toml (全て不変)# build
cd splat
cargo build --release -p splat-cli
# schema compat check (既存 30k config が parse できることを確認)
./target/release/splat check --config configs/2026-05-22-2155-lego-sh3-30k.toml
./target/release/splat check --config configs/2026-05-24-1800-lego-legacybase-5k.toml
./target/release/splat check --config configs/2026-05-24-1800-lego-brushcompat-base-5k.toml
# legacy 5k (gt_convention = white_bg, default)
./target/release/splat train --config configs/2026-05-24-1800-lego-legacybase-5k.toml
# → mid-training mean val: 21.874 dB / wallclock 202.4s / 77603 splats
# brush 互換 5k (gt_convention = premultiplied)
./target/release/splat train --config configs/2026-05-24-1800-lego-brushcompat-base-5k.toml
# → mid-training mean val: 31.308 dB / wallclock 125.4s / 93948 splats
# (mid-training PSNR は train.rs:182 の `compute_psnr` (RGB only、raw f32) で計算。
# eval cell 4 の 31.334 との 0.026 dB 差は `splat eval --quantize-8bit` の
# (*255).round()/255 roundtrip 効果。α channel の MSE への寄与は無い。)
# 4 cell cross-eval
DS=/Users/otkrickey/dev/3dgs-workspace/datasets/nerf_synthetic/lego
LEGACY=runs/lego-legacybase-5k/final.ply
BRUSH=runs/lego-brushcompat-base-5k/final.ply
./target/release/splat eval --ply $LEGACY --dataset $DS --split val --convention legacy
# → 21.874 dB (cell 1)
./target/release/splat eval --ply $LEGACY --dataset $DS --split val --convention brush --quantize-8bit
# → 1.628 dB (cell 2 — A.3 1.667 dB 再現)
./target/release/splat eval --ply $BRUSH --dataset $DS --split val --convention legacy
# → 1.595 dB (cell 3 — symmetry test)
./target/release/splat eval --ply $BRUSH --dataset $DS --split val --convention brush --quantize-8bit
# → 31.334 dB (cell 4 — hypothesis confirmed)
match_alpha = 0.1 で α channel を 1/10 重みに。Stage 2 で 30k bench した後、α 重み調整の sensitivity sweep を 1-2 点だけ追加検討 (Stage 1 で OK 出たので priority は低い)h_a_regression_findings の Adam scale-invariance 議論を踏まえ、α channel を含めた loss scale が AdamConfig.eps と整合するか確認。eps=1e-8 で 4ch MSE = 1e-2 帯なら問題ない見込みだが、30k 収束終盤 (loss ≪ 1e-3) で eps が効き始める可能性、watch 対象runs/lego-brushcompat-base-5k/view_*.png + gt_*.png — premultiplied で gt は背景透明、view は背景 RGB=0 (黒) として render される (PNG 化で α channel は保持されない場合は "黒背景" に見える)。これは bug ではなく仕様configs/2026-05-24-???-lego-brushcompat-30k.toml として iter=30000 に拡張、parent_config を本 Stage 1 brushcompat にsplat eval 経由なので本数字は informational に留まる (advisor 推奨通り)p1-a-eval-convention-audit の §6 表に 本 Stage 1 4-way 行を追加、A.3 falsify hypothesis ("eval だけ揃えても無理") の 裏返しの解 ("trainer 側で揃えれば即解") を確定p1-a-3-cross-eval-reproducerp1-a-1-brush-eval-auditp1-a-2-splat-eval-auditp1-a-eval-convention-auditbrush-vs-splat-37dB-gap-analysish_a_regression_findings + memory h_a_regression_findings.mdmcmc-noise-calibration