Commit Graph
59 Commits
Author SHA1 Message Date
Shaun ZhouandClaude Opus 5 891f930de2 refactor: prepare_label_studio.py 的内容改动(补上 a3e08a9 漏掉的部分)
a3e08a9 只提交了 git mv 的改名,内容修改因 git add 的路径报错没有进入暂存区。
本提交补上:

- 缺省输出 runs/<source>_label_studio/,不再写到 data/;该目录同时是 Label Studio
  的 DOCUMENT_ROOT,打印的导入步骤里提醒复核期间不要删除或 --overwrite。
- 总是生成 tasks.json 与 labeling config,去掉 --label-studio 开关。
- 不再写 data.yaml:复核材料不是训练集,没有 data.yaml 就不会被误拿去训练,
  原先"train 与 val 相同"的告警也随之移除(detect.py 里的同类告警保留)。
- detect.py、configs/detect/film.yaml、README 里对旧脚本的引用改为新流程。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:26:22 +08:00
Shaun ZhouandClaude Opus 5 a3e08a9bbc refactor: prepare_yolo_dataset.py 改为 prepare_label_studio.py,产出放到 runs/
弱监督框要先在 Label Studio 里人工修正才能训练,这个脚本实际上只负责准备复核材料,
名字和产出位置都按这个职责调整:

- 改名 scripts/prepare_label_studio.py(git mv 保留历史),测试同步改名。
- 缺省输出 runs/<source>_label_studio/,不再写到 data/;该目录同时是 Label Studio
  的 DOCUMENT_ROOT,打印的导入步骤里提醒复核期间不要删除或 --overwrite。
- 总是生成 tasks.json 与 labeling config,去掉 --label-studio 开关。
- 不再写 data.yaml:复核材料不是训练集,没有 data.yaml 就不会被误拿去训练,
  原先"train 与 val 相同"的告警也随之移除(detect.py 里的同类告警保留)。

修正后的标签从 Label Studio 导出、划分 train/val 到 data/ 的脚本另写。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:26:05 +08:00
Shaun ZhouandClaude Opus 5 af0897f8c7 refactor(detect): 数据集改为扁平结构、不划分 train/val,Label Studio 文件放到根目录
prepare_yolo_dataset.py 的产出先导入 Label Studio 复核,划分留到复核之后,于是:

- 去掉按类别分层的 train/val 切分以及 --val-ratio / --seed,images/ 与 labels/
  下直接是文件;name_map.csv 去掉 split 列,summary.json 的逐类数量改为单列。
- label_studio/ 子目录的内容上移一层:扁平的数据集本身就是 label-studio-converter
  要求的 <input>/{images,labels,classes.txt},tasks.json 与 labeling config 直接
  生成在根目录,不再需要额外的硬链接视图,DOCUMENT_ROOT 即数据集根目录。
- data.yaml 的 train 与 val 只能指向同一个 images/。ultralytics 能读,但验证指标
  算的是训练集本身,mAP 虚高、早停与 best.pt 的选择失真。脚本结束时告警,
  detect.py 检测到 train == val 时也告警。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:19:53 +08:00
Shaun ZhouandClaude Opus 5 1a43267b55 feat(detect): --label-studio 一步生成可导入 Label Studio 的复核视图
label-studio-converter import yolo 只认 <input>/{images,labels,classes.txt},且
os.listdir 不递归,看不见 train/val 子目录,直接喂数据集会 "No labels converted"。
按类别重命名后文件名全局唯一,于是在 <out>/label_studio/ 下把两个 split 拍平
(硬链接:Label Studio 本地存储会校验路径在 DOCUMENT_ROOT 之下,软链接可能越界),
再调用 converter 生成 tasks.json 与 labeling config。

- 框以 predictions 导入:它们是 SHAP/Grad-CAM 生成的,审核员提交后才成为
  annotation,导出时没审过的框不会混进去。
- 不传 image_dims:真实数据有 3223 种尺寸,传了会写错每条标注的宽高。
- 任务数与图片数不符时报错,而不是静默少导。
- converter 自带的导入提示漏掉了 LABEL_STUDIO_LOCAL_FILES_* 环境变量(它在循环里
  改写了 image_root_url,之后与默认值比较失败),静音后由脚本打印带实际路径的步骤。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:06:40 +08:00
Shaun ZhouandClaude Opus 5 7adc5b6332 feat(detect): YOLO 数据集统一为 PNG、按类别重命名并附 classes.txt
- 图片统一成 PNG。pearlescent / blue / uv 的原始数据是 BMP,generate_boxes.py
  按原后缀链接,于是 runs/boxes_shap 里 3685 张 BMP 与 7526 张 PNG 混在一起,
  ripple 与 uneven 两类全是 BMP。按文件头(而非后缀)判断:真 PNG 照旧硬链接,
  其余无损转换,不缩放像素,归一化框坐标原样有效;CMYK 等 PNG 存不下的模式转 RGB。
  在真实数据上核对过 3685 张转换结果逐像素一致。
- 文件按 <类别>_<序号> 重新编号。类别取标签第一列,文件名不会与标签矛盾;
  序号在类内按源路径排序、与划分无关,换种子重跑名字不变。新旧名对照写入
  name_map.csv,源文件名里的正反面、位置、时间戳不丢。--keep-names 保留原名。
- 输出 classes.txt(行号即 class_id),供 LabelImg / X-AnyLabeling 复核框时使用。
- 撞名检查改为按输出文件名的小写比较:统一后缀后 a.png 与 a.bmp 会撞,
  macOS 默认文件系统也不区分大小写。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 16:45:58 +08:00
xzhou cc053a6e9c fix(deps): add label-studio dependency in pyproject.toml 2026-09-15 08:26:46 +00:00
xzhou 960f0d753a fix(configs): update training epochs in film.yaml and film.yaml for classifier and YOLO models 2026-09-15 06:49:16 +00:00
Shaun ZhouandClaude Opus 5 012b3efc73 feat(detect): 按来源类别处置无框图片(--empty-policy)
空标签在 YOLO 里的语义是"这张图什么都没有",是一个明确的负样本断言,而不是
"有缺陷但没能定位出来"。原先一律保留,于是 runs/boxes_gradcam 的 339 张空标签
里有 270 张是人工标成缺陷、Grad-CAM 却失手的图(blackdot 占该类 11%、black 占
24%),它们正以"此图无缺陷"的身份进入训练,等于在教模型抑制真实缺陷。

--empty-policy 接一个全局默认加若干 `类别=策略`:
  keep 当负样本保留(默认)/ drop 丢弃 / full 退化成整图框

整图框需要重写标签而不是链接源文件(内容不同),且有了确定类别之后要跟着本类
一起参与分层划分。类别名或策略拼错直接报错——静默忽略会让策略看着生效其实没有。
没显式指定、又落到 keep 的类别会打一条告警。

替换掉原来的 --drop-empty:它是一刀切,会把 normal 那 69 张有价值的负样本一起扔掉。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:34:16 +08:00
Shaun ZhouandClaude Opus 5 d6c6aca325 feat(detect): 默认模型换成 yolo26n
CLAUDE.md 已把"只用 convolution 模型"收窄到聚类与数据集生成阶段,检测这一步
不再受限,于是换成最新的 YOLO26:端到端(NMS-free),推理侧少一道后处理。
取最小的 n 档——数据 1.1 万张且标签是弱监督的,模型大了容易把标注噪声学进去。

同时去掉 test_config.py 里"不许用 yolo11/yolo12"的断言,它的依据已随
CLAUDE.md 的改动失效。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 14:34:16 +08:00
Shaun Zhou 0e830fe253 fix(config): update data YAML path from boxes_gradcam to boxes_shap 2026-09-11 14:29:27 +08:00
Shaun ZhouandClaude Opus 5 8dc2fe6a2e feat(detect): 把弱监督框数据集链接到 data/ 并接上 YOLO 训练
第五步产出的框标签按类别分目录,便于人工核对;检测训练要的是 train/val
划分——类别写在标签第一列,不靠目录名。补上两个入口:

- scripts/prepare_yolo_dataset.py:拍平类别目录、按类别分层切分、硬链接到
  data/。分层是必需的,black 只有 53 张,随机切容易让小类在验证集里挂零。
  无框的图默认保留(检测训练里是压低误检的负样本);拍平后的同名冲突直接
  报错而不是静默覆盖。
- detect.py + configs/detect/film.yaml:包一层 ultralytics,沿用本仓库的
  配置继承、设备解析与 SwanLab 可视化。

选 YOLOv8 而不是 v11/v12:CLAUDE.md 限定只用 convolution 模型,YOLO11 骨架
里有 C2PSA、YOLO12 以注意力为主,只有 v8 的 C2f + SPPF 是纯卷积。
mosaic/mixup 关掉——每张图只有一个弱监督框,拼接后小目标被缩得更小,对
blackdot/whitedot 这类只占千分之几面积的类别是净伤害。

ultralytics 的 project 必须传绝对路径,相对路径会被它再拼到全局 settings
的 runs_dir 上,产出会跑到 runs/detect/runs/detect/<name> 去。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 12:05:50 +08:00
Shaun ZhouandClaude Opus 5 86c86b105c refactor: 按实际的五步工作流裁掉无关代码
实际工作流是:simclr 训练 -> 聚类 -> 人工归类 -> 分类训练 -> GradCAM/SHAP 生成
热力图与 box。据此移除不参与其中的检测/定位链路,共 31 个文件、约 2600 行:

- 模型:defect_net、postprocess、necks/、heads/center_head
- 数据:film_defect(YOLO 读取端)、target_encoder
- 引擎:trainer、evaluator、losses
- 入口:train.py、eval.py
- 脚本:convert_annotations、split_dataset、export_onnx、profile_inference
- 配置:configs/{train,eval,model,dataset}
- utils/labels(多类/单类标签模式,仅检测分类头使用)

共享模块按实际用量瘦身:engine/metrics 只留 AverageMeter(检测指标随之移除);
datasets/yolo 只留写出侧(IMAGE_SUFFIXES 与 write_data_yaml)——第 5 步产出的
YOLO 标签是给外部检测训练用的交换格式,读取端不再需要。

裁剪过程中修掉一个真 bug:generate_boxes 的 --stage 默认 6(layer3)对 tiny_conv
这类非 nn.Sequential 骨架直接抛异常。现在自动退回骨架最终输出并警告,
另加 --stage -1 显式选择最终输出。

五步工作流已端到端实跑验证(simclr 1 轮 -> 聚类 -> 导出 -> 分类 1 轮 ->
gradcam 与 shap 两种方法各生成一遍)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:27:20 +08:00
Shaun ZhouandClaude Opus 5 57446c2f10 refactor: 合并为单一的框生成入口,gradcam / shap 可选,默认全量
此前是两个脚本:gradcam_boxes.py 全量跑但只有一种方法,shap_boxes.py 默认抽样
且强制输出对比图。两者合并为 scripts/generate_boxes.py:

- --method gradcam|shap 二选一,默认 gradcam
- 默认对全量数据生成;--limit / --per-class 降级为试参数用的可选项
- --min-area-ratio 缺省随方法而变(gradcam 0.02、shap 0.002):两者归因图分辨率差
  16 倍,同一个小黑点在 128x128 上只占百来像素,共用阈值会被整个滤掉

产出改为符合 CLAUDE.md 工作流第 8 步的规格,是一个自成一体的 YOLO 数据集:
images/ 是原图硬链接、labels/ 是 YOLO 标签、heatmaps/ 是热力图叠加框。
images/ 与 labels/ 的对应关系符合仓库的 YOLO 约定,FilmDefectDataset 可直接读取,
把分类产出接回了检测训练链路(已加测试验证这条闭环)。

热力图目标类别改为模型 top1 预测(原为目录标注),--target label 可切回;
summary.json 记录两者一致率(实测 0.9873)。

修掉一个会静默丢标签的 bug:产出按预测类别分目录,不同源目录下的同名图
(dirA/x.png 与 dirB/x.png)若被预测到同一类会写同一个标签路径,后写的覆盖前者。
现在显式检测并中止,提示用 export_clusters.py --prefix-parent 重新导出。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:26:56 +08:00
Shaun ZhouandClaude Opus 5 9aa87d31a6 feat(localization): SHAP Partition 定位方法,并把每图框数上限默认收紧为 1
新增基于 SHAP Partition explainer 的定位,与 Grad-CAM 形成互补对照:

- engine/shap_explain.py:对目标类别求 SHAP 归因(只保留正贡献),以及方框平滑。
  遮挡用数值 masker 而非教程里的 inpaint_telea——后者依赖 opencv,而我们的图已做过
  逐图标准化,用 0(即该图均值)遮挡语义更干净
- scripts/shap_boxes.py:按类别抽样生成标签,并输出「原图 | Grad-CAM | SHAP」并排图
- shap 放入可选依赖组:uv sync --group explain

实测(225 张,每类 15):SHAP 在小目标类别上明显更准,框面积占比
blackdot 0.019 vs 0.269(14 倍)、mosquito 0.030 vs 0.173、whitedot 0.076 vs 0.253。
原因是 Grad-CAM 受限于 8x8 特征图(每格 16x16 像素),几像素的黑点根本不可分辨。
代价是单张 0.55 秒,全量 11k 张约 1.7 小时,而 Grad-CAM 全量只要 2 分钟。

调参时发现的坑:SHAP 第一版全部出 0 框,因为沿用了给 8x8 调的
min_area_ratio=0.02(在 128x128 上等于 328 像素)。参数不能跨分辨率照搬,
SHAP 默认改为 0.002。

--max-boxes 两个脚本都默认改为 1(原为 2):一张裁片通常只对应一处缺陷。
库函数 cam_to_boxes 的默认值同步收紧,相关测试改为显式传参而不依赖默认值。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:10:50 +08:00
Shaun ZhouandClaude Opus 5 fc7136fddf feat: 用 Grad-CAM 从分类模型生成弱监督框标签
分类模型只学过"是什么缺陷",Grad-CAM 把分类分数对卷积特征的梯度当作通道重要性,
加权求和得到空间热力图,阈值化 + 连通域外接矩形即得到框标签,输出 YOLO 格式,
可直接接入既有的检测训练链路。

- engine/gradcam.py:整批一次 backward 的 Grad-CAM(各样本只回传自己那一类的分数)、
  numpy 实现的 4-邻接连通域标记(不为这一处引入 scipy/opencv)、CAM 转归一化框
- scripts/gradcam_boxes.py:遍历数据集生成标签,并输出「原图 | 热力图 | 叠加+框」
  三联图供人工核对;框的类别取自图片所在目录而非模型预测

两处按实图定下的默认值:
- --stage 默认 6(layer3, 8×8)。layer4 只有 4×4 太粗,layer2 响应太散导致 40% 出不了框
- --peak-ratio 0.75:次要连通域峰值需达全局最大值的 75%,否则会在无缺陷的角落刷假框;
  实测把两框比例从 53% 降到 20%

全量 11,212 张已生成:61.8% 出 1 框、35.2% 出 2 框、3.0% 无框,每类留 25 张可视化。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:46:30 +08:00
Shaun ZhouandClaude Opus 5 33fdf396ee feat: 新增缺陷分类训练与评估链路
在聚类 + 人工归类得到的数据集(data/<集>/<类别名>/*.png)上训练分类模型。

- datasets/classification.py:按目录组织的分类数据集,**逐类分层**切分 train/val
  (others 只有 65 张,整体随机切很容易让它在验证集里挂零),固定种子可复现
- models/classifier.py:卷积骨架 + 分类头,与 DefectNet 共用 backbone 注册表,
  因此自监督产出的 simclr_backbone.pth 可用 init_from 直接初始化;
  frozen_backbone 可做线性探针
- engine/classification.py:训练循环 + 指标。主指标用 macro-F1 而非准确率——
  数据从 normal 2679 张到 others 65 张差 41 倍,全预测成 normal 也有 24% 准确率;
  损失按类别频率加权,避免小类被淹没;输出逐类 P/R/F1 与混淆矩阵
- classify.py:训练/评估入口,--eval-only 可单独评估并导出 eval_report.json
- configs/classify/film.yaml:预处理沿用聚类阶段的结论(拉伸 + 逐图标准化)。
  抹掉亮度在这里尤其重要:有几类几乎只来自单一原始数据集,不抹掉的话模型可能
  靠"认出这是哪个数据集"作弊
- 续训语义与检测/自监督一致:检查点只提供进度,超参以配置为准

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:58:41 +08:00
Shaun ZhouandClaude Opus 5 33d28dcc7c feat(cluster): clusters/ 默认改用硬链接,便于直接在 runs/ 下继续分子类
软链接记的是绝对路径,从服务器拷到本机后全部失效,也不便于直接在 runs/ 下整理。
硬链接不占额外磁盘却表现得和真实文件一样:看图软件能开、可以随意挪动、建子目录
继续分子类,整个目录拷到别的机器也不会断链。

- 新增 link_mode: hard(默认)/ sym / copy;跨文件系统时 hard 自动退回软链并警告
- 落盘后打印各方式的条目数,便于确认实际用的是哪种
- export_clusters.py 适配硬链接:条目本身就是原图,但路径在 runs/ 下、名字还带
  0125_ 排序前缀。按 inode 反查 assignments.csv 还原原图路径,这样导出的文件名与
  --prefix-parent 才能拿到真正的来源目录;查不到则剥掉前缀后用文件名索引兜底

实测:14,579 张跑聚类后 clusters/ 全为硬链接(nlink=2、可直接打开);
把簇改名、建子目录挪图后再导出,排序前缀已剥除、来源目录正确还原为「反面纵黑点」,
120 张内容与原图逐字节一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 12:04:08 +08:00
Shaun ZhouandClaude Opus 5 f8654f447b feat(export): 改为按 clusters/ 目录导出,并重定位失效的软链
原来按 assignments.csv 导出,反映的是聚类当时的划分;而 clusters/ 目录会被人工
整理——挪动分错的图、把 cluster_03 改名成 grease。现在以整理后的目录为准。

- 扫描 clusters/ 下的子目录,目录名即类别名并原样保留(不再是数字编号)
- 文件名用原图的名字,去掉 clusters/ 里的 NNNN_ 排序前缀
- 失效软链的重定位:读出链接目标,按其中的 data/ 段落到本机 --source-root 上;
  再不行用文件名在源目录里兜底索引(惰性构建,用不到就不扫盘)。
  从服务器拷回来的 clusters/ 软链全部指向 /home/deep/... 在本机失效,这是常态
- 解析不到的条目列出并中止,--skip-missing 可忽略
- manifest 记录类别、每张图的原始路径与被跳过的条目

在真实目录上验证:11,212 张、15 个类别,软链 100% 失效但全部重定位成功,
导出后抽查 200 张与原图逐字节一致。跨数据集的数字文件名(blue/113.bmp 与
pearlescent/113.bmp)会被重名检查拦下,--prefix-parent 可化解。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 11:26:02 +08:00
Shaun ZhouandClaude Opus 5 9b31473b27 feat(export): 支持把原父目录名作为文件名前缀
新增 --prefix-parent:复制时把图片原来的父目录名拼到文件名前
(grease/abc.png -> grease_abc.png)。导出后目录只剩簇号,来源信息本来会丢失,
加前缀能留住它,人工核对时一眼看出某张图原本在哪个目录。

- 重名检查改为按**目标文件名**判重:加前缀后原本会撞车的文件可能就不撞了;
  不加前缀而撞车时,报错会提示可以试试这个开关
- manifest.json 记下 prefix_parent,导出的 file 字段也用实际写出的文件名

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:26:00 +08:00
Shaun ZhouandClaude Opus 5 6e543660ce feat(configs): 新增 resnet101 的自监督聚类配置
resnet101 此前不在 TorchvisionBackbone 的支持列表里,先补进 _SUPPORTED
(out_channels 2048),随后被既有的参数化测试自动覆盖:能前向、声明维度与实际一致。

configs/cluster/film_simclr_resnet101.yaml 仿照 resnet18/50 两份,只改骨架与
batch_size,其余超参一致以便横向对比:
- 42.5M 参数(resnet18 11.2M / resnet50 23.5M),layer2+3 仍是 1536 维,
  与 resnet50 相同,差别在深度(layer3 有 23 个 block 而非 6 个)
- batch_size 取 128:MPS 上 resnet50 在 1024 已 OOM,resnet101 约重 1.8 倍故再降一档;
  注释里说明该数字来自 MPS,16 GB 独显上可往上调
- pretrained 首次使用会下载约 171 MB 权重

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:51:16 +08:00
xzhou c9e7e4104c fix(configs): update dataset root path in film.yaml 2026-09-07 12:36:51 +00:00
Shaun ZhouandClaude Opus 5 92c88b785c fix(config): --set 拦下不存在的键,并支持续训时重开学习率曲线
用户反馈"聚类配置里改不了 lr/weight_decay",实测是两件事:

1. --set 的键打错字母会凭空造出一个谁也不读的键,训练照跑但改动无效。
   实例:simclr.optmizer.weight_decay=0.0(optimizer 少个 i)被静默接受。
   现在 apply_overrides 默认只允许覆盖已存在的键,报错时给出同级可用键
   与最接近的拼写("是不是想写 'optimizer'?")。需要新增键时可 strict=False。

2. lr 其实生效了,但从 2229/2500 续训时 cosine 只剩满值的 3%,
   新设的 1e-3 落到 2.87e-5。新增 restart_schedule 开关:按剩余轮数重开曲线,
   新 lr 从满值开始;SimCLR 与检测训练都支持。续训日志补上调度进度,
   并在 lr 不足 initial_lr 的 10% 时警告并给出两种解法。

顺带:严格校验立刻暴露出 configs/cluster/film.yaml 里根本没有 scatter 段
(代码 cfg.get("scatter", {}) 兜底走默认值,所以功能正常但配置项形同虚设),已补上。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:30:17 +08:00
Shaun ZhouandClaude Opus 5 506fde0290 fix(cluster): 配置层级写错不再静默忽略,并始终打印生效的超参
聚类配置里 optimizer/epochs 等键都在 `simclr:` 段下。写到顶层会被完全忽略、
不报任何错——用户改了 lr 却毫无反应,极难排查。同样的坑还有:改了基类
configs/cluster/film.yaml 的 simclr.optimizer,但运行的是子配置
film_simclr_resnet18.yaml,子配置里的同名键会盖掉基类的修改。

- 顶层出现 optimizer / scheduler / epochs / temperature / projection /
  save_interval / resume / checkpoint / log_interval 时警告并给出正确写法
- method != simclr 却写了 simclr: 段时提示该段不会生效
- SimCLRTrainer 初始化时无条件打印实际生效的 epochs / temperature /
  optimizer / scheduler,不再只在续训时才打印
- 修掉一处假差异:旧存档没有 scheduler_cfg 字段时会误报 "scheduler None -> cosine",
  那是"不知道"而非"当时没有调度器"

已验证 --set 覆盖、直接改 yaml、旧格式存档三条路径下 lr/weight_decay 均正常生效。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:11:31 +08:00
Shaun ZhouandClaude Opus 5 105cb38831 fix(trainer): 检测训练续训以配置为准,并恢复学习率调度进度
train.py --resume 走的是与 SimCLR 不同的一套代码,同样的问题在这里更严重:

- load_checkpoint 直接 optimizer.load_state_dict,把 lr/weight_decay 连同动量缓冲
  一起还原成存档值:yaml 写 lr=5e-4,实际仍按检查点的 1e-3 训
- save_checkpoint 根本没保存调度器状态,续训时 scheduler.last_epoch 从 0 开始,
  而 optimizer 的 lr 又来自检查点,两者互相矛盾,cosine 曲线完全错乱

改为与 SimCLR 一致的「配置文件是唯一真相源」:

- save_checkpoint 补上 scheduler 状态、global_step 与当时的 train 配置快照
- 新增 Trainer.resume():加载优化器状态后按组写回配置里的超参(只留动量缓冲,
  并保住 BN/bias 那组 weight_decay=0 的结构),调度器按当前配置重建再快进到已训 epoch
- 配置与检查点不一致时逐项打日志说明按当前配置执行
- train.py 改用 trainer.resume(),不再自己拼恢复逻辑

实测续训到 10 轮时 lr=3.273e-04,与 cosine 在 4/10 处的理论值精确一致;
weight_decay、T_max、调度进度、动量缓冲均正确。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:06:37 +08:00
xzhou 01cfff9b02 fix(configs): update optimizer settings and add projection parameters in film_simclr_resnet50.yaml 2026-09-07 06:25:54 +00:00
Shaun ZhouandClaude Opus 5 0f5bb4a5c0 feat(cluster): 加 --train-only,自监督训练与聚类可分两步跑
engine/simclr.py 与 engine/cluster.py 本来就互不引用,唯一接口是
simclr_backbone.pth;耦合只在 CLI——method=simclr 会训练完直接聚类。
此前"只聚类"可以用 simclr.checkpoint 做到,"只训练"没有办法。

- --train-only:训练存完权重即退出,不抽特征也不聚类,并打印下一步的聚类命令
- 与 method=pretrained / simclr.checkpoint / --sweep-k 组合时立刻报错说明原因,
  而不是跑完才发现没有产出
- 典型用法:GPU 机器上 --train-only 跑几十小时,之后拿权重反复调 k,每次约 1 分钟

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:38:59 +08:00
Shaun ZhouandClaude Opus 5 5d395b7c25 chore: 数据迁到 data/raw/ 后同步所有路径引用
原始数据从 data/<数据集> 移到了 data/raw/<数据集>。

- 聚类配置的 dataset.root:film / pearlescent 均改为 data/raw/ 下
- 检测配置的 img_dir 与 label_dir 注释同步
- README、data/README、算法文档、流程图(含重新渲染的 PNG)与各脚本
  docstring 里的路径示例一并更新
- data/README 补充目录约定:原始数据只读放 data/raw/,
  export_clusters.py 的派生数据集放在 data/ 下与 raw/ 平级
- 顺手把 unlabeled 数据集加进 datasets/__init__.py:此前它没在包初始化时注册,
  build_dataset({"name": "unlabeled"}) 只有在别处先 import 过该模块时才可用

迁移后重跑全量聚类,结果与迁移前逐项一致(轮廓 0.3255、稳定性 0.9641、
簇大小 [4507, 3795, 4849, 1428])。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:19:42 +08:00
Shaun ZhouandClaude Opus 5 5f31ed0a92 feat(configs): 新增 resnet50 的自监督聚类配置
仿照 film_simclr_resnet18.yaml,只改骨架与 batch_size,其余超参保持一致以便对比。

batch_size 取 256 而非照抄 resnet18 的 1024:实测 resnet50 在 Apple MPS 上
batch 1024 直接 OOM(需 18.7 GiB),batch 128 时 604 MB。每 epoch 耗时与 batch
基本无关(约 155 秒,算力受限),所以调小 batch 不花额外时间,只是负样本变少。

配置注释里记下了实测数据与降级路径(显存不够继续调到 128 / 64),
并说明 resnet50 的 layer2+3 是 1536 维(resnet18 为 384 维)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:07:55 +08:00
Shaun ZhouandClaude Opus 5 f8a52c9382 feat(scripts): 按聚类结果导出数据集
新增 scripts/export_clusters.py:读聚类产出的 assignments.csv,把原图复制成
data/<源目录名>_<骨架标签>_cluster_k<簇数>/,子目录用从 0 开始的数字编号。

- 复制真实文件(copy2 保留修改时间),不建软链、不改名
- 目录名自动推导:源目录名取各路径的公共父目录,骨架标签取自 summary.json 的
  feature_source;--tag / --out 可覆盖
- manifest.json 记录来源 run、特征配置与每张图的原始路径,便于追溯
- 保护:源文件缺失、同簇内重名(保持原名会互相覆盖)直接报错;
  目标目录非空需显式 --overwrite;--dry-run 只打印计划
- 聚类产出的 summary.json 新增 feature_source 字段(method / backbone /
  feature_stages),供上面的自动命名与日后追溯

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 13:17:53 +08:00
xzhou 7f13ec5d18 fix(configs): update training epochs and optimizer settings in film_simclr_resnet18.yaml 2026-09-06 05:05:47 +00:00
Shaun ZhouandClaude Opus 5 004d7a1ce7 fix(simclr): 换优化器种类时不再加载不兼容的动量缓冲
adamw 换成 sgd 时 load_state_dict 不会报错,SGD 会收下 AdamW 的 exp_avg/exp_avg_sq
却永远用不上——动量实际从零开始,但状态里白占内存,之后存盘还会一路带下去。
改为检测到优化器种类变化就跳过加载并给出日志,模型权重与训练进度不受影响。

README 补一张表,说明续训时哪些配置按 yaml、哪些来自存档、哪些不能改。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 11:10:04 +08:00
Shaun ZhouandClaude Opus 5 b7505a743d fix(simclr): 续训以配置为准,不再被存档里的超参静默覆盖
optimizer.load_state_dict 会整体覆盖 param_groups,把 lr / weight_decay 连同动量缓冲
一起还原成存档时的值。改了 yaml 再续训,改动会被静默吃掉:配置写 lr=5e-4,
实际仍按存档的 1e-3 训,scheduler.base_lrs 也是旧值。

改为「配置文件是唯一真相源,存档只提供进度」:

- 加载优化器状态前先抄下配置构造出的每组超参,加载后按组写回,只留下动量缓冲;
  按组写回也保住了 build_optimizer 的分组结构(BN 与 bias 那组 weight_decay=0)
- 调度器统一按当前配置重建再快进到已训 epoch,不再恢复存档状态(它带着旧的
  T_max 与 base_lrs)。cosine/step/multistep 都是 epoch 的确定性函数,
  快进与恢复状态等价,因此不存在第二种行为
- 存档里额外记下当时的 optimizer/scheduler 配置,续训时逐项列出差异,
  明确说明按当前配置执行
- 补回归测试:改 lr/weight_decay/总轮数后续训,三者都按 yaml 生效,
  分组结构与动量缓冲不受影响

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:46:40 +08:00
xzhou 703ba7076b fix(configs): update num_clusters and training parameters in film_simclr_resnet18.yaml 2026-09-04 13:41:41 +00:00
Shaun ZhouandClaude Opus 5 865cdd7210 fix(simclr): 续训时改变总轮数不再让学习率卡在 0
resume 会恢复调度器状态,其中包含存档时的 T_max。原计划 10 轮的 cosine 跑完后
学习率已降到 0,此时把 epochs 改成 30 接着练,学习率仍是 0——训练照常进行、
日志照常打印,但一步都学不动。是静默失败,比报错更难发现。

- 总轮数与存档一致时行为不变,直接恢复调度器状态
- 不一致时按新的总轮数重建调度器,并快进到当前 epoch;重建前先把 lr 复位回
  initial_lr,因为 cosine 的链式实现基于当前 lr 递推,从 0 出发只会一路留在 0
- 实测续训到 30 轮时 lr 恢复为 7.50e-04,与 cosine 在 10/30 处的理论值一致
- 补三个回归测试:改变总轮数后的学习率、总轮数不变时直接恢复状态、
  save_interval 落的中间盘可直接续训
- 修正一个旧测试:它断言续训后 lr 仍等于旧计划末尾的 0,编码的是错误行为

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:25:45 +08:00
xzhou 6b11128357 fix(configs): update film_simclr_resnet18.yaml with new clustering parameters and optimizer settings 2026-09-04 09:19:15 +00:00
Shaun ZhouandClaude Opus 5 6f00074199 feat(cluster): 新增 resnet18 自监督聚类配置,并修复 --set 的 YAML 标量解析
- 新增 configs/cluster/film_simclr_resnet18.yaml:用 resnet18 做域内自监督后聚类。
  backbone 块带 _replace_,避免 base 里 tiny_conv 的 widths/depths 残留
- parse_override 此前只用 ast.literal_eval,不认 YAML 风格的 null/true/false:
  `--set xxx.enabled=false` 会得到字符串 "false",而非空字符串是真值,开关反被打开。
  改为先试 Python 字面量(保住 1e-3 这类),再退回 YAML 标量解析
- feature_stages 默认由 null 改为 [5, 6]:实测 1 epoch 自监督后 layer2+3 的轮廓
  0.243/稳定性 0.997,明显优于 layer4 的 0.156/0.528。原注释里"自监督后 layer4
  已是域内特征"的推断被数据推翻
- README 补充三种方案的实测对比与成本

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:51:28 +08:00
Shaun ZhouandClaude Opus 5 47fb0f1125 fix(configs): 修复 resnet18 训练配置无法构建,并补上对应的评估配置
- configs/train/resnet18.yaml 此前直接报错:它同时继承 tiny_conv 与 resnet18 的模型
  配置,深度合并后 backbone 混了两边字段。在 configs/model/resnet18.yaml 的 backbone
  上加 _replace_: true 整块替换
- _replace_ 标记此前不生效:_load_raw 对每个 base 都会先 deep_merge({}, data),
  标记在参与替换判断之前就被剥掉了。改为合并期间保留、load_config 末尾统一剥除
- 新增 configs/eval/resnet18.yaml:评估必须与训练用同一套模型配置,否则构建出的
  骨架与权重对不上,而此前只有 baseline 一个评估配置
- 新增参数化测试:仓库里每个配置都要能真的构建出组件(片段自动跳过),
  且训练/评估配置对的 model 段必须一致
- README 补充骨架与配置对的对应表

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:42:14 +08:00
Shaun ZhouandClaude Opus 5 e581d9b3d7 fix(backbones): 修复 shufflenet 无法构建,并让换骨架不再踩配置合并的坑
- shufflenet_v2_x1_0 列在支持清单里却直接 AttributeError:ShuffleNetV2 既不是
  扁平 children 也没有 .features,需要按 conv1/maxpool/stage2..4/conv5 自己拼。
  该 bug 同时影响 SimCLR 与检测模型
- 新增参数化测试覆盖清单里的每个 arch:能前向、声明的 out_channels 与实际一致
- 配置支持 `_replace_: true`:子字典整块替换而不与 base 合并。换骨架时
  tiny_conv 的 widths/depths 会残留并传给新骨架,此前无论用 --set 还是 _base_
  都绕不开,只能改原文件
- Registry.build 提前校验字段,报错直接指出是哪些字段不被接受、该怎么办,
  取代裸 TypeError
- README 补充可选骨架表与三种改法(含不该用 --set 改 name 的说明)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:28:49 +08:00
Shaun ZhouandClaude Opus 5 e4bd794025 feat(cluster): 输出簇分布图,直观展示簇间距离
在 preview 拼图的同级目录生成 cluster_scatter.png:
- 左:聚类空间的 PCA 二维投影,散点按簇着色 + 质心标记,标题标注两个主成分的方差占比
- 右:质心两两距离矩阵(在聚类空间里算,不是投影后),带数值标注

投影用 PCA 而不是 t-SNE/UMAP——后两者为了把簇拉开会扭曲簇间距离,
而这张图的目的就是看距离。

- ClusterResult 新增 points 字段,保留 KMeans 实际使用的空间供可视化复用,
  质心与散点用同一组主成分变换,避免质心飘到簇外
- summary.json 新增 centroid_distances
- 新增 scatter 配置块(enabled / max_points / point_size / alpha / figsize / dpi)
- 中文字体按 PingFang / Noto Sans CJK 等顺序探测,找不到自动退回英文标签,
  不会画成豆腐块
- 新增依赖 matplotlib

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:09:35 +08:00
xzhou 4e6c8aa2ea fix: update work directory in film.yaml and add pearlescent.yaml configuration 2026-09-04 06:54:06 +00:00
xzhou a92d1125d4 fix: uv 换源至清华源 2026-09-04 06:26:16 +00:00
Shaun Zhou 4550fdfa8e feat(cluster): add ImageNet pre-trained model section and adjust batch sizes 2026-09-03 15:47:37 +08:00
Shaun ZhouandClaude Opus 5 345dee50da feat(simclr): 支持断点续训,恢复优化器与调度器状态
此前 simclr.checkpoint 的语义是"有权重就跳过训练",没有真正的续训:
中途停掉再启动只能从头再来,或者放弃已训的轮次。

- 新增 simclr_last.pth:骨架 + 投影头 + 优化器 + 调度器 + scaler + epoch + step,
  与给下游用的 simclr_backbone.pth 并存,两者在同一时机落盘
- 新增 simclr.resume:载入完整状态从下一个 epoch 接着练。只恢复权重不恢复动量与
  cosine 进度的话,续训时损失会先反弹一段
- resume 与 checkpoint 互斥(同时给直接报错);拿只含骨架的 simclr_backbone.pth
  去 resume 会给出可操作的报错,提示改用 checkpoint
- 续训轮数已达计划值时不再训练,只打警告

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 09:34:55 +08:00
Shaun ZhouandClaude Opus 5 be6173c746 feat(simclr): 接入 SwanLab 可视化,并补上本地看板缺失的依赖
- SimCLRTrainer 记录 simclr/loss、loss_avg、lr(按 step)与 epoch_loss、
  epoch_seconds、gpu_mem_peak_mb(按 epoch),超参为完整 yaml 配置;
  正常结束与 KeyboardInterrupt 两条路径都会 finish()
- 配置新增 simclr.swanlab(字段同 train.swanlab),默认 mode: local,
  不需要登录,日志写到 <work_dir>/swanlog
- 依赖改为 swanlab[dashboard]:此前 `swanlab watch` 在干净环境里会报
  "Please install the swanboard package",README 里检测训练那节的看板命令
  同样受影响,现已随 uv sync 装好
- README 补齐本地看板/云端/离线补传三种用法与端口、局域网参数

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 09:24:39 +08:00
Shaun ZhouandClaude Opus 5 7d5b6ec423 docs(readme): 补全 SimCLR 训练与复用权重聚类的完整流程
- 拆成三步:训练+聚类一条命令(含产出目录树、45 分钟预期、nohup 后台跑法)、
  用 simclr.checkpoint 复用权重重新聚类(约 1 分钟,含 k=2 与 --sweep-k)、
  以及把 --set 固化进 yaml 的写法
- 提醒复用权重时要换 --work-dir,否则会覆盖上次的聚类产出
- 修复三条 uv run 命令的续行反斜杠(此前被折叠成单行)
- 目录表 utils/ 的描述由"GPU 守卫"改为"设备解析(cuda/mps/cpu)"
- profiler 小节补充三后端差异并链到"计算后端"

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 09:10:36 +08:00
Shaun ZhouandClaude Opus 5 938d62ea53 docs: SimCLR 域内自监督的实测对比与多后端说明
- 新增 §10.5:SimCLR vs ImageNet 预训练在 14,579 张上的同设置对比。
  只训 1 个 epoch 的域内特征在 k=4 上即追平(轮廓 0.334 vs 0.325),
  在 k=2 上大幅胜出(轮廓 0.469、稳定性 0.991)——目视确认该二分正是
  "有明显缺陷 / 弱纹理近空白",可直接作为流水线的第一级过滤
- 复杂度表补 MPS 实测:特征提取 84s(CPU) -> 51s(MPS),SimCLR 约 65s/epoch
- README 增加"计算后端"小节(三后端的 AMP 默认值、显存统计与限制)
- 流程图与对照表同步去掉已移除的 GPU 守卫

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:43:35 +08:00
Shaun ZhouandClaude Opus 5 b6f83fcad0 fix(simclr): 周期性落盘,中途中断不再丢失训练成果
此前 fit() 只在全部 epoch 跑完后才保存骨架,一次跑到 epoch 12 被停掉后
13 分钟的训练全部作废。

- 每 save_interval(默认 5)个 epoch 保存一次骨架
- 捕获 KeyboardInterrupt,中断时先落盘再返回
- 聚类脚本改用 resolve_device,自监督分支不再有 GPU 守卫

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:43:35 +08:00
Shaun ZhouandClaude Opus 5 141cb907f2 feat: 支持 cuda / mps / cpu 三后端,Mac 上可用 MPS 训练
移除"训练必须有 Nvidia GPU"的硬性守卫,改为按后端能力分派。

- utils/device: resolve_device("auto") 按 cuda > mps > cpu 挑最快的可用后端;
  显式指定不可用的后端时直接报错,不会悄悄退回 CPU——训练慢几十倍却不吭声是最坏的情况
- AMP 随后端取默认值:CUDA 开、MPS 关。MPS 上 autocast 数值正常但实测更慢
  (tiny_conv @128²:fp32 76 ms/步、fp16 90 ms、bf16 108 ms),需要时 amp: true 可强开
- PeakMemory 统一显存峰值统计:CUDA 读内置计数器,MPS 无峰值 API 改用逐 step 采样
  (是下界,日志与产出里已标注)
- profile_inference 支持 MPS,并提示 torch.profiler 没有 MPS 的 ProfilerActivity,
  算子明细只反映 CPU 侧下发耗时
- train.py / eval.py 新增 --device,配置默认改为 auto
- check_env 改为列出全部可用后端与 auto 的选择结果

实测:tiny_conv 训练在 MPS 上比 CPU 快约 32 倍。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:43:35 +08:00
Shaun ZhouandClaude Opus 5 4fc0fa5353 docs: 无监督聚类的数学说明、mermaid 图与 README
- docs/unsupervised-clustering.md: 逐图标准化对仿射光照群的不变性证明、
  多 stage 特征的取舍、PCA 白化失效的谱分析、KMeans 收敛性与 kmeans++ 近似比、
  轮廓系数与 NMI 稳定性的定义、NT-Xent 与 InfoNCE 下界,附全部消融数据与代码对照表
- figures/: 四张 mermaid 图(流水线、KMeans 控制流、SimCLR、主成分谱),
  含 .mmd 源、.md 预览与渲染好的 .png
- README: 数据准备、训练评估、多类/单类、SwanLab、Profiler、聚类与自监督的运行说明

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:55:48 +08:00
Shaun ZhouandClaude Opus 5 d6adea5b05 test: 单元测试与端到端冒烟
88 个测试覆盖配置继承、注册表、GPU 守卫、YOLO 格式解析、数据集、模型形状、
标签模式、聚类原语与 SimCLR 损失,以及 train.py / eval.py / 聚类脚本的入口接线。

含两条回归测试:
- PCA 白化放大噪声方向导致簇塌陷(默认 whiten=False 的依据)
- classes 写成 data.yaml 路径时 num_classes 必须是类别数而非路径字符串长度

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:55:47 +08:00