📋 模型訓練 SOP / 注意事項

2026-05-18 v1 · 整合自過往訓練踩過的坑 + 設計決策

章節: 📌 訓練前 5 分鐘 checklist 📦 CVAT export 規則(最常踩坑) 🔬 Partial-label / unknown 處理 ⚙️ Hyperparams 規則(沿用 vs 改動) 🏗️ 模型架構規則(per-task baseline) 🚀 部署與驗證 📊 評估與 audit 🖥️ 5090-2 / gx10 環境細節 ❌ 過往失敗教訓

📌 訓練前 5 分鐘 checklist

讀上一版 summary.json(backbone, img_size, aug, loss, weights, ep best)
列出本版 hyperparams diff vs 上一版(即使全沿用也要列)
user confirm hyperparams(不靠命名推斷)
cvat2 對應 project 全 task scan:subset 分布、deleted_frames、(empty) 殘留
**掃描所有 annotation shape type 分布**(polygon / rectangle / polyline),rectangle 數 > 0 → 必走 bbox YOLO det
**確認每 task 的 jobs stage=acceptance + state=completed**(光 subset != "" 還不夠,標注可能還 in_progress / in_validation)
資料 distribution:是否 yes/no 嚴重不平衡?某 attr 樣本 < 30?val 完全沒 negative?
**有任何資料問題先停下跟 user 討論**,不要自己改
checkout disk 空間(5090-2 / gx10)夠不夠
checkout GPU memory 是否被別人占用
確認 tmux session 不會撞到別人

📦 CVAT export 規則(最常踩坑)

必守 訓練前掃 shape type 分布;含 rectangle → 強制 bbox YOLO(不准只取 polygon)

Why:2026-05-19 forklift v20260518 翻車:cvat #9 有 10,625 rectangle + 4,105 polygon + 1 polyline,但 export_p9_v20260518.pyif sh["type"] != "polygon": continue → 直接丟掉 72% 的 annotation(10,625 個 rectangle),訓練只用了 4,017 個 bbox。導致看起來 model 大退步,實際是資料餓死。
How to apply:
from collections import Counter
ctr = Counter()
for ann in annotations:
    for sh in ann.get("shapes", []):
        ctr[sh["type"]] += 1
print(ctr)  # {"rectangle": 10625, "polygon": 4105, "polyline": 1}

if ctr.get("rectangle", 0) > 0:
    # 必須走 bbox YOLO det,rectangle 直接取 points[0:4]
    # polygon 也轉 bbox (min/max xy) 一併納入
    task_type = "det"  # NOT "seg"
elif ctr.get("polygon", 0) > 0:
    # 可選 seg 或 det(polygon→bbox 轉換)
    task_type = "seg or det"
規則:有 rectangle 就絕不能丟。rectangle 是標記師明確意圖「只標 bbox」的 annotation,丟掉等於丟一手資料。polyline 一律忽略(forklift 是 noise)。
對應 task 類型:cvat #1 (person) 全 polygon → polygon→bbox;cvat #9 (forklift) 混 polygon+rectangle → 必走 bbox det 把兩者合併;cvat #16 (river_debris) 看分布決定。

必守data_id 拼 disk path,不用 task_id

Why:cvat2 上 task_id ≠ data_id 占 30%。用 task_id 拼 path 會撈到別 task 的 frame。2026-05-03 v6 safety_rope 訓練 + audit 都中過此坑。
How to apply:從 cvat API GET /api/tasks/{id}data field 當 data_id,path = /mnt/ssd/cvat2/data/cvat_data/data/{data_id}/raw/<fname>
def resolve_disk(did, fname):
    cands = []
    if "/" in fname: cands.append(SHARE_ROOT / fname)
    cands.append(DATA_ROOT / str(did) / "raw" / Path(fname).name)
    cands.append(DATA_ROOT / str(did) / "compressed" / Path(fname).name)
    if "/" not in fname: cands.append(SHARE_ROOT / fname)
    for c in cands:
        if c.exists(): return c
    return None

必守 過濾 deleted_frames

Why:cvat 標記師會用「delete frame」標記不要的 frame。如果沒過濾,會包含 user 明確認為「不該訓練」的 frame。
How to apply:meta = GET /api/tasks/{id}/data/meta; deleted = set(meta["deleted_frames"]) 跑迴圈時 skip

必守 用 task.subset 切分 Train/Val/Test,不能 hash split

Why:同一支影片抽不同 frame 高度相關,hash split 會導致 train/val leak。task.subset 是標記師明確分配的 split,遵守它即可避免 leak。
How to apply:整個 task 進一個 subset。SUBSET_MAP = {"Train": "train", "Validation": "val", "Test": "test"}(empty) 跳過或 ping user 確認

必守 task 的 jobs 必須 stage=acceptance + state=completed 才可訓

Why:cvat job stage 流程是 annotation → validation → acceptance,state 是 new → in progress → completed。**有 tag 不等於通過 acceptance review**。例如 PUBLIC_FASDD 等公開 dataset task 有 10000+ tag 但 stage 仍是 annotation/new,標注品質未驗證。2026-05-18 cvat #2 1257 個 task 看 tag 似乎 311k 可用,但實際 acceptance 完成只 110 個(8.7%)。
How to apply:
jobs = s.get(f"{CVAT2}/api/jobs?project_id={pid}&page_size=500").json()
done_tasks = {j["task_id"] for j in jobs
              if j["stage"] == "acceptance" and j["state"] == "completed"}
# 訓練只用 done_tasks ∩ subset!="" 的 task
若需批次 force 完成(user 明確授權):PATCH /api/jobs/{id} {"stage": "acceptance", "state": "completed"}

注意 cvat2 API quirks

優先 下載走 disk path > rsync > bulk API

How to apply:5090-2 上 cvat2 在 docker,原圖 mount 在 /mnt/ssd/cvat2/data/cvat_data/data/{data_id}/raw/,直接 cp / symlink,**100× 快於 API 下載**。從 mac local 跑要 ssh 進 5090-2。

🔬 Partial-label / unknown 處理

必守 unknown 完全不參與 loss / metric 計算

Why:cvat2 多 attr 任務裡,標記師沒空把每個 attr 都標 yes/no,多數 attr 留 unknown。若把 unknown 當 negative 訓,模型會錯誤學「沒標 = 沒有」。
How to apply:
def masked_bce(logits, y, mask, pw, nw=None):
    if nw is None:
        raw = F.binary_cross_entropy_with_logits(logits, y, pos_weight=pw, reduction="none")
    else:
        lp = F.logsigmoid(logits); l1p = F.logsigmoid(-logits)
        raw = -pw * y * lp - nw * (1.0 - y) * l1p
    m = mask.sum()
    return (raw * mask).sum() / m if m > 0 else raw.mean()*0   # 分母用 mask.sum()
mixup 也要 element-wise min mask:torch.minimum(m, m[idx])
關鍵點:unknown 對 loss 是 zero contribution,但該 frame 過 backbone 仍會貢獻 visual representation 學習。這是好事,partial-label 仍能榨出 unlabeled attr 的視覺資訊。

必守 eval 同樣 mask unknown,不要當 0/負或忽略整個 sample

How to apply:keep = masks[:, attr_idx] > 0.5,per-attr 不同 sample 集合是合理的。用 sklearn.metrics.precision_recall_curve 不要自己寫 metric(v1 合成圍裙踩坑教訓)。

⚙️ Hyperparams 規則

必守 訓練前必讀上一版 summary,列差異 user confirm

Why:2026-05-15 PPE v515 第一輪用了錯的 harness=20(從命名 negw_harness20 推斷),實際上一版 v504 summary 寫 harness=2.0 + hard_hat=1.3 + safety_vest=1.3。**不能靠命名推斷 hyperparams**。
How to apply:cat ~/runs_new/<previous_version>/summary.json,列:backbone / img_size / aug / loss / weights / best epoch。user confirm 後再訓。

建議 新 dataset → 預設沿用 baseline hyperparams

Why:2026-05-15 PPE v515b 對齊 v504 hyperparams 反而比 v515 第一輪略差。新 dataset 跟舊 hyperparams 不一定 match,但**完全沿用是最 conservative 起點**,效果不好再 ablation。

🏗️ 模型架構規則(per-task baseline)

safety_rope RoI Align + 外擴 1.0/0.2/1.5 + HD 1280×720

Why:rotation+blur aug 降 FP -35%。外擴比例 (x=1.0, y_top=0.2, y_bot=1.5) 是 v5+ 試出來的最佳值。
How to apply:backbone DINOv3-S 22M params,class_weights wrong=1.5/correct=1.0,jitter center=0.2 size=[0.7,1.4]
注意:場域 FP 真嫌是 YOLO det 假框,不是 ViT 分類器。e2e audit 揭露 78% FP 來自 YOLO 假框、漏報 97% 來自 YOLO 漏框。改 ViT 邊際遞減,要救先救 person YOLO。

PPE 21/22-attr MobileNetV3-L baseline / 大 backbone +7-8pp

person YOLO yolo11n.pt detect, 100 ep, batch=64, imgsz=640, 雙卡

v20260501 baseline: SGD lr0=0.01 mom=0.937 wd=0.0005, mosaic=1.0 close_mosaic=10, fliplr=0.5, cache=ram。 **cvat #1 全 polygon (SAM3 標)** → 訓 det 需要 polygon → bbox 轉 (min/max xy)。

door_state / hatch MobileNetV3-L + 2 binary heads + SWA-4

partial-label BCE, has_open / has_close 兩 head, img 384, val_mAP 0.99

river_debris YOLO11n det, cvat2 #16 為主

iseek-river.intemotech.com 上線使用。重訓後熱換模型。

fire_smoke MobileNetV3-L + 2 binary heads (smoke + fire), partial-label BCE

v20260410 SK 暴露的問題(v20260518 要解):

🚀 部署與驗證

必守 Pages 用 scripts/deploy_reports.sh 全量部署

Why:不全量會讓舊報告消失。deploy_reports.sh 從 gx10 rsync 所有 reports → inject R2 banner → 生 index.html → wrangler pages publish。
How to apply:
scp my_report.html gx10:/home/rai/kaggle_work/reports/
set -a; source ~/code/秘書/infra/cloudflare.env; set +a
export CLOUDFLARE_API_TOKEN="$CLOUDFLARE_PAGES_TOKEN"
bash scripts/deploy_reports.sh

必守 Cloudflare token 走 secretary env,不要自己 wrangler login

Token 在 ~/code/秘書/infra/cloudflare.envCLOUDFLARE_API_TOKEN / CLOUDFLARE_PAGES_TOKEN。R2 用 ~/code/秘書/scripts/r2.sh 不用 wrangler r2(wrangler put 要加 --remote 否則只上 local cache)。

必守 部署後要 curl / logs 驗證上線

Why:wrangler publish 顯示「Success」不代表 production 真的吃新版。CDN cache、worker hot-reload、reverse proxy 都可能擋。
How to apply:curl -sI "https://<url>" -H "User-Agent: Mozilla/5.0" 確認 200,必要時 curl ... | grep <new content> 確認內容也是新的。

📊 評估與 audit

必守 safety_rope audit 不要單張看,要時序 ±N frame

Why:動態作業任務(拉繩 / 掛勾)單張難判斷對錯。verdict JSON 不可直接信任,要看前後幀 motion / context。

建議 用 recall @ precision=0.95 當 low-FP 主指標

Why:iSeek 通報路徑最忌 false alarm。F1 / mAP 不夠針對「低誤報」,R@P95 直接量化「鎖死 95% precision 能拉多少 recall」。

教訓 不要信 CLIP / VLM 自動 eval(先 calibrate)

Why:v1 合成圍裙 CLIP eval 96.7% 假分數,人工肉眼真實 4.7%。CLIP 對「銀色金屬反光」過敏,把氣瓶 / 防護衣 / 水印商品圖都當高分。
How to apply:新 metric 必須先對 baseline 數據回測,看是否跟人工 audit 結果吻合(v2 用 GroundingDINO+HSV+neg-check 三閘對 v1 prod 43 張回測 9.3% vs 人工 5-11% → 校準)才能用。

建議 Ensemble 對「真實弱點 attr」無效,要補資料

Why:cotton_gloves 11 個 model R@P95 都 0.04-0.14。當所有 model 同樣拉胯 → 不是 model capacity 問題,是 train data signal 問題。**換大 backbone / 加 ensemble 都白做工**。

必守 Per-channel / per-source 分開評估,不混算 metric

Why:fire_smoke v20260410 SK 報告:CH05/CH06 t=0.01 達 100% recall,CH15 要 t=0.81 才 51% recall。整體 mAP 0.99 但 36/76 task NOT READY。單一整體 metric 會把 domain shift 蓋掉
How to apply:

建議 連續幀 flip rate 是 temporal smoothing 觸發指標

Why:v20260410 SK 報告 CH01 flip rate 43%、CH04 30% — 同一段影片忽抓忽漏。模型本身可能已不可能更穩,但推論側加 N-frame sliding mean / track-aware smoothing 能直接救。
How to apply:per-task 算 flip rate = sum(每幀 vs 前幀 prediction 不同) / total frame。flip rate > 20% → 加 temporal smoothing。詳見 YOLO Tracker-Aided Temporal Smoothing 設計

建議 Per-channel threshold 不入 ckpt,放 inference layer config

Why:fire_smoke v20260410 SK 報告 per-channel threshold 從 0.01-0.81 變化 80×。若寫死 ckpt 內 → 新場景就要重訓;放 iSeek / ppe-demo config → 可動態調。
How to apply:model 內保 default threshold (如 0.5),per-camera 在 iSeek rule engine / ppe-demo handler 階段套自己的 threshold。便於 production 持續 tune 不重訓。

建議 Grad-CAM audit 是黃金標準(不只看數字)

Why:v20260410 SK 報告抓出「prob 0.93 但看錯位置」「prob 0.05 但其實有看到煙」的案例。數字對不代表注意力對
How to apply:per-task 抽 3-5 個 FP/FN(極端 prob)做 Grad-CAM 視覺化,貼進 report。CH13 / CH04 等 problem channel 必看。

🖥️ 5090-2 / gx10 環境細節

項目路徑 / 細節
cvat2 mount/mnt/ssd/cvat2/data/cvat_data/data/{data_id}/raw/
cvat_share/mnt/ssd/cvat2/data/cvat_share/
cvat2 API(內網)http://localhost:8181 + Host: raicvat5090.intemotech.com
cvat2 API(外部)https://raicvat5090.intemotech.com + User-Agent: Mozilla/5.0
HF cache(多服務共用)~/hf_cache 144 GB — 不要砍
5090-2 GPU 占用GPU 0 已被 ppe-demo + hunyuan worker 占 ~18 GB,剩 ~13 GB free
R2 base urlhttps://pub-478929a98a5c440cb22c2241c0bde314.r2.dev
R2 bucketrai-models
Pageskaggle-reports.pages.dev 從 gx10:~/kaggle_work/reports/ 同步
gx10 LAN192.168.53.21 (rai)
5090-2 aliasssh 5090-2 (ubuntu user)

tmux send-keys 注意

claude code TUI 對 multi-byte (中文) prompt 後接 Enter 偶爾沒 submit。tmux capture-pane 看 idle 就單獨補 tmux send-keys ... Enter

❌ 過往失敗教訓(避免重蹈)

日期事件根因學到的規則
2026-05-03safety_rope v6 訓練 + audit 30% 撈到別 task frame用 task_id 拼 disk path用 data_id 不用 task_id
2026-05-15PPE v515 第一輪 harness=20 訓壞從命名推斷 hyperparams 沒讀 summary.json訓練前必讀上一版 summary
2026-05-17aluminized_apron 合成 v1 CLIP 96.7% 假分數自動 eval 沒校準新 metric 必須對 baseline 回測校準
2026-05-17aluminized_apron 合成 v2 仍 10% 失敗garment_set 爬蟲污染(戰士 / 防護衣 / 水印)合成路線:標記師親手挑 reference set 才能啟動
2026-05-17覆蓋 5090-2 上別人的 training_mission_device2/model_viewer/app.py同名 dir 但完全不同服務動 5090 source 前先 git status 確認
2026-05-18person v20260518 export pagination DNS 失敗cvat API 回 next absolute URL,docker 內部 DNS 不到自己 page+=1 不 follow next URL
2026-04-10fire_smoke SK 36/76 task NOT READY,per-channel threshold 差 80×單一 model 對所有 channel 不一致,整體 mAP 0.99 蓋住 channel-level domain shiftper-channel/per-source 分開評估per-channel threshold 不入 ckpt
2026-04-10fire_smoke SK 連續幀 flip rate 30-43%純 single-frame model 缺時序穩定性temporal smoothing 推論層
2026-05-18cvat #2 / #11 多個 task subset 已分但 jobs 還在 in_progress / in_validation標記師分 subset 跟 acceptance review 是兩個獨立流程,被誤以為「分 split = 完成」訓練前要 scan job stage/state,不只看 task subset;可批次 PATCH 強制 acceptance/completed
2026-05-19forklift v20260518 看似大退步(mAP50 0.02)cvat #9 有 10625 rectangle + 4105 polygon,export 寫 if type != "polygon": continue 把 72% annotation 丟掉,訓練只用 4017 bbox(資料餓死)訓練前必掃 shape type 分布;含 rectangle 強制 bbox YOLO det,不准只取 polygon
2026-05-22factory_ppe v521 export pagination bug 揭露(v519 也中)export_p12_v519.pypage_size=100 沒 paginate,cvat #12 有 704 task 但只 fetch 前 100,v519 訓練 dataset 嚴重不完整。v521 修了 best_val_mAP 從 0.9503 跳到 0.9741(+2.4pp)cvat API 任何 list endpoint 必 paginate(while r.get("next"): page+=1
2026-05-225090-2 root disk 100% 滿,factory_ppe export crashhf_cache 144G + cvat-trainer 135G + datasets 94G 堆系統碟 / dev/nvme1n1p2 937G 用盡訓練前 df -h / 看 < 80% 才動;hf_cache / datasets 建議搬 /mnt/ssd + symlink;crops_v* 舊版定期清
2026-05-25forklift v20260525 翻車(video-mode task 整支影片只 export 第 1 frame)既有 export_p1 / p9 / p12 / hatch 用 meta["frames"] iterate,但 cvat task mode="interpolation" 是 video-mode,meta["frames"] 只 1 entry → frames 全丟cvat task ["mode"]=="annotation" → image-mode iterate frames OK;==="interpolation" → video-mode 需走 disk chunks 或 cv2.VideoCapture 解 frame。寫共用 helper cvat_frame_iter() 三個 export 共用
2026-05-26person v526 訓練 DataLoader ConnectionResetError 崩潰train script device="1" 但 GPU 1 被 ComfyUI 占 24GB;同事直接 ssh 跑前景沒 tmux,ssh 斷則 process die;DataLoader workers=8 多進程 queue read 被 reset訓練前 nvidia-smi 確認 GPU 空間;**必走 tmux session**(不要 ssh 前景跑 train);train chain script 帶 done file 給 watcher 通知
2026-05-26CLAUDE.md 寫錯 hatch v518 production;同事踩 hatch (#7) vs door_state (#11) 混淆cvat #7 hatch 系列已凍結(最新 v20260505_swa);cvat #11 door_state 4-class 是新接班(v20260518);CLAUDE.md 寫成同一條對應「hatch v518」是 typo不要改 export_hatch.py PROJECT_ID=11;要訓 door_state 必須複製新 export_door_state_v*.py 設 PROJECT_ID=11

🔗 相關連結