 Shaun ZhouandClaude Opus 5
|
dce2eef0a7
|
fix(ml-backend): 推理分批,避免 Retrieve Predictions 撑爆显存;新增批量推送脚本
Retrieve Predictions 会把选中的任务一次发给后端,而后端把整个列表交给
model.predict(source=paths)——ultralytics 不理会 batch 参数,会当成一个 batch,
于是显存爆掉。这与之前 visualize_detections 踩的是同一个坑。后端现在按
YOLO_BATCH(默认 16)自己分批。
另加 scripts/push_predictions.py:重训完在本地分批推理,再用批量导入接口
POST /api/projects/<id>/import/predictions 提交,不经过后端服务,显存与请求体都可控。
支持 --replace 先删同版本旧预测(预测是追加的,重复跑会堆两份)、--limit 与 --dry-run。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-22 12:11:06 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
8fa7a9d142
|
fix(ml-backend): 重训后真正切到新权重
重训脚本是 ML backend 的子进程,继承了后端 export 的 YOLO_WEIGHTS。启动脚本里
${YOLO_WEIGHTS:-默认值} 于是让旧值赢了——实测重训完成后后端仍加载旧权重,
current.pt 软链接形同虚设。同样被继承的还有 RETRAIN_EXTRA_SET,会让下次训练
沿用上次的冒烟参数(epochs=1)。
重启时显式传 YOLO_WEIGHTS 并 env -u 清掉 RETRAIN_EXTRA_SET。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-21 11:03:14 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
e61fd5887c
|
feat(ml-backend): Start Training 按钮真正触发重训,启动/重训脚本进 scripts/
原先点 Label Studio 的 Start Training 只是把计数加一:START_TRAINING 和普通标注事件
走同一条阈值判断,没配 RETRAIN_COMMAND 时更是静默什么都不做。实际使用中用户点了按钮、
以为在训练,其实权重一直没变。
- Start Training 无条件触发重训;攒够 RETRAIN_AFTER 条新标注也自动触发。
- 只数 ANNOTATION_CREATED:UPDATED / DELETED 是对已有标注的修改,不该把阈值顶到。
- 已有重训在跑时重复点击被忽略,不叠加启动。
- 没配 RETRAIN_COMMAND 时打警告说明不会真的训练,并在返回值里说清楚。
- 子进程输出重定向到 RETRAIN_LOG(默认 /tmp/retrain.log),训练进度有地方看。
scripts/start_ml_backend.sh、scripts/retrain_from_label_studio.sh 放进仓库,
可从工作区直接启动。重训末尾把 runs/detect/current.pt 软链接指向新权重,
启动脚本默认读它,换模型不必改脚本。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-21 11:00:01 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
1b8c8d7cac
|
chore: 忽略 ml_backend 的运行时缓存
cache.db 是 label-studio-ml 用来存 model_version、标注计数的本地缓存,
上一提交误把它带了进去。顺带忽略后端的独立虚拟环境 .venv-ml/。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 17:36:25 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
a4d099e41d
|
feat(ml-backend): 把训练好的 YOLO 接进 Label Studio 做预标注
ml_backend/ 是一个 label-studio-ml 后端:Label Studio 打开任务时调 /predict,
后端跑 YOLO 并把框转成 Label Studio 的百分比坐标返回,人工只需修正。
- model.py:FilmDefectDetector。权重懒加载且只加载一次(Flask 多线程,加锁);
控件名从项目的 labeling config 读取,不写死;model_version 带权重的修改时间,
换权重后 Label Studio 能区分新旧预测。
- convert.py:坐标转换,刻意不 import label_studio_ml,好在主环境里单测。
- fit():每条标注累加计数,达到 RETRAIN_AFTER 就起子进程执行 RETRAIN_COMMAND
并立刻返回——训练几十分钟,同步做会卡住 Label Studio。
- _wsgi.py 由官方模板生成,ruff 跳过,便于跟随上游。
必须用独立虚拟环境:label-studio-ml 会把 jinja2 降到 3.0.3,而 label-studio 1.23
需要 3.1,装进主环境会直接弄坏 Label Studio。PyPI 上的 1.0.9 是 2023 年的旧版,
要装 GitHub 上的现行版本。
本地用 COCO 权重端到端验证过:/health、/predict(bus.jpg 出 10 个框、坐标换算正确)、
/webhook(第 3 条标注触发重训命令并清零计数)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 17:36:12 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
154677032f
|
Revert "约束预测框互不重叠"(eba533a、9439cbb)
去掉强制非重叠:detect.py 不再给 DetectionValidator.postprocess 打补丁,
配置里移除 train.max_overlap 与 agnostic_nms,可视化脚本恢复为直接使用模型原始输出,
engine/detection 里的 suppress_overlaps / filter_overlapping_outputs 一并移除。
约束会把 mAP50 从 0.555 压到 0.483(precision 0.536->0.612),代价是按置信度抑制时
高分错框会压掉本可命中的低分框。需要时用 git revert 这次提交即可取回。
按类别分目录的可视化产出结构(36fff98)保留。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 15:34:13 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
36fff984b7
|
refactor(detect): 可视化产出改为按类别分目录,与 generate_boxes 一致
原先按 split 和结果类型(ok/missed/extra/none)分目录,层级太深。改成
<类别>/<图名>.png,与 runs/boxes_shap 的结构一致。类别取真值里框最多的那个;
负样本没有真值框,按文件名前缀归类(<类别>_<序号> 是 prepare_label_studio 的命名),
认不出来放 background/。
split 与结果类型仍记在 index.csv 的列里,排序筛选即可。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 15:06:13 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
9439cbb56a
|
feat(detect): 训练期的验证也约束预测框互不重叠
ultralytics 没有可用的开关:yolo26 是端到端模型,non_max_suppression 对它早退
(utils/nms.py 里 end2end 分支),只按置信度与 max_det 截断,传入的 iou 和 agnostic
都不参与 IoU 抑制;agnostic_nms 只作用于检测头 top-k(每个 anchor 取一个类别),
压不掉不同 anchor 产生的重叠框。
所以给 DetectionValidator.postprocess 包一层 filter_overlapping_outputs。训练期验证、
早停、best.pt 的选择都随之遵守这条约束,指标与最终推理一致。配置项 train.max_overlap
(默认 0,负数关闭),由 detect.py 摘出后自行实现,不传给 ultralytics。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 11:30:48 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
eba533a5b2
|
feat(detect): 约束预测框互不重叠
人工标注的真值里一处缺陷只有一个框:11211 张图、151 张多框图,重叠框对为 0。
而模型预测在验证集上有 6.9% 的图存在重叠框——NMS 只在同类内、按固定 IoU 阈值抑制,
压不掉两类情况:同一处缺陷被两个类别各框一次(实测 IoU 高达 0.99),以及 IoU 低于
NMS 阈值的同类重叠(grease 66 对、wrinkle 45 对)。
- engine/detection.suppress_overlaps:按置信度贪心保留互不重叠的框,可设允许的
最大 IoU;另可丢弃被大框完全包住的小框(面积悬殊时 IoU 很小,但显然是重复表达)。
- visualize_detections 默认 --max-overlap 0(与真值一致),并放出 --nms-iou 与
类别无关 NMS 开关;--max-overlap -1 可看模型原始输出。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-18 11:19:47 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
43c8c4a179
|
fix(detect): 可视化推理自己按批切分,避免 CUDA OOM
model.predict 不理会传入的 batch 参数,把整个图片列表当成一个 batch,8969 张图在
16 GiB 显卡上直接 OOM(单次要分配 10.27 GiB)。改成自己按 --batch 切片调用。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 21:47:44 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
788bc36ab8
|
feat(detect): 全量推理可视化,预测框与真值框画在一起
scripts/visualize_detections.py:在整个数据集上推理,绿=真值、蓝=命中的预测、
红=没对上真值的预测,按结果类型分目录(ok / missed / extra / none),再配一份
index.csv(逐图的真值数、预测数、命中数、最高置信度)与 summary.json(逐类 TP/FP/FN)。
--only errors 只输出有问题的图。
匹配逻辑放在 engine/detection.py:按置信度贪心、IoU 阈值、类别需一致,与 mAP 的规则
一致,所以图上看到的漏检误检和指标算的是同一回事。same_class=False 可以单看定位准不准
——定位对但分类错,和完全没找到是两种问题。
train 与 val 分开放:训练集的图模型见过,那部分结果不能当泛化能力看。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 21:47:02 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
ad50c38f41
|
chore(detect): 配置指向人工复核后的数据集 data/film_yolo
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 16:45:14 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
59d4b8a9c4
|
fix(detect): 钳掉 Label Studio 导出坐标的浮点溢出
真实导出里出现 height=1.0000000000000002:Label Studio 按百分比存框,导出时 /100
留下浮点误差。ultralytics 读标签时对 >1 的坐标直接报错,所以这点溢出必须钳掉,
否则装配出的数据集根本训不起来。
只钳 1e-6 量级的溢出,真正越界的坐标仍然报错。顺带把坐标统一写成 6 位小数。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 16:44:42 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
998e6601d4
|
feat(detect): 把 Label Studio 的 YOLO 导出装配成带 train/val 划分的数据集
scripts/build_yolo_dataset.py:导出(zip 或目录)+ 复核材料里的 images/ -> data/ 下
可训练的数据集。Label Studio 的 YOLO 导出只有标签,本地存储的图片不会打包进去
(get_local_path 对 local-files 直接返回磁盘路径,YOLO_WITH_IMAGES 也一样),
所以图片从准备复核材料的目录按文件名取,硬链接过去。
- 类别顺序照抄导出的 classes.txt:转换器把标签收进 set 再排序编号,与 labeling
config 的顺序无关,标签里的 class_id 是按它写的。
- 按类别分层切分,分层用"框最多的那个类别"——人工复核后一张图可能有多个类别的框。
- 标签逐行校验:字段数、类别下标、坐标范围。坏标签训练时不会崩,只会悄悄拉低指标。
- 空标签默认保留为负样本;标签找不到图直接报错,图多于标签则告警。
顺带把 link_or_copy 提到 utils/fs.py 共用(此前 generate_boxes 与
prepare_label_studio 各有一份)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 16:42:53 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
65bc6b2045
|
feat(boxes): 新增传统 CV 定位方法(--method cv),不依赖分类模型
背景差分:大半径方框均值估背景 → 残差(可选极性)→ 平滑 → 有无判定 → 按峰值归一化,
再交给与 gradcam / shap 同一套 cam_to_boxes 出框。只依赖 numpy。
与归因方法的差别:不需要 --checkpoint,分类器还没训练出来就能先跑一版框,也不受分类
精度影响;类别只能来自目录(--target label);找的是偏离局部背景的区域,而不是模型
据以判断类别的区域,所以对局部目标好、对铺满整图的纹理类给不出有意义的框。
有无缺陷是显式判定的:残差峰值要同时高于绝对对比度下限与噪声水平(MAD 估计)的 15 倍,
否则返回全零图。这条是必需的——实测纯噪声图的"峰值/MAD"稳定在 9 附近(最大 10.9,
几乎与噪声大小无关),有 0.25 对比度黑点时是 25~330;若只按峰值归一化,正常图的噪声
会被拉到 1,满图假框。阈值 15 取在两者之间。
顺带把积分图方框均值从 shap_explain.smooth 提到 engine/classical.box_mean 共用,
避免两处实现各自漂移。
实测:单张 0.011 秒(全量 11k 约 2 分钟);真实薄膜图上框能套住黑点、虫状物与划痕。
已知局限:背景估计在图像边界处对明暗阶跃有响应,贴边的交界可能出假框。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-17 11:03:13 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
32e56cb230
|
feat(boxes): generate_boxes.py 的 --target 默认改为 label
框的类别应该来自人工归类(图片所在目录),分类模型只负责定位。原先默认 predicted,
框的类别取模型 top1 预测,模型分错的图会把错误类别写进标签:boxes_shap 那次预测与
目录一致率 0.9915,约 95 张图的类别因此是错的。
两种取值只在预测与目录不一致的图上有区别,其余图解释的是同一个 logit、框完全相同。
--target predicted 仍保留。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 20:31:40 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
7dd0f89d18
|
fix(label-studio): 图片 URL 按实际的 DOCUMENT_ROOT 生成(--document-root)
原先任务里的图片 URL 固定是 /data/local-files/?d=images/<图名>,隐含假设输出目录就是
LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT。服务器上 DOCUMENT_ROOT=/home/deep/labeling,
Label Studio 会去找 /home/deep/labeling/images/<图名>,11211 张图全部 404。
- 新增 --document-root,缺省读 LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT;URL 写成相对它的
路径,Local files 存储路径随之算好并打印。
- 输出目录在 DOCUMENT_ROOT 之外时,在 DOCUMENT_ROOT 里建同名软链接指过去,产出仍留在
runs/,不用手动复制;DOCUMENT_ROOT 里已有同名真目录或指向别处的软链接时报错,不覆盖。
- 更正此前"软链接解析后会落到 DOCUMENT_ROOT 外面"的说法:核对 Label Studio 1.23.0
源码,safe_join 用 abspath、存储路径用 normpath 做字符串前缀比较,都不解析软链接。
- 测试照搬 localfiles_data() 的判定逐个任务验证能否被服务,并加入旧 URL 在
/home/deep/labeling 下全部 404 的回归用例。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-15 20:04:04 +08:00 |
|
 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 |
|
 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 |
|
 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 |
|
 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 |
|
 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 |
|
 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 |
|
 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 |
|
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 |
|
 Shaun ZhouandClaude Opus 5
|
f433e5cf59
|
feat: 未分类样本的无监督聚类(多 stage 特征 + KMeans,可选 SimCLR)
把 data/film 下所有图片拉平做聚类,不使用任何目录名,簇数由 num_clusters 决定。
预处理针对该批数据的实际情况(14.5k 张已裁好的缺陷小图,中位 152x113,
宽高比 0.24~39.9):尺寸与亮度都是 ROI 裁剪和光照的产物、不是缺陷属性,
因此拉伸到 128x128(不用 letterbox,其填充比例等价于编码了宽高比)并逐图 z-score。
- cluster: torch 实现的 kmeans++/Lloyd(含空簇处理与 n_init)、PCA、轮廓系数、
NMI 稳定性、多 stage 特征提取;不引 scikit-learn
- 白化默认关闭:CNN 特征谱极陡(λ1/λ32≈165x),白化会把噪声方向抬到与信号同权,
实测最大簇一度吃掉 96% 的样本
- simclr: NT-Xent 自监督预训练,用项目自己的卷积骨架,权重可直接初始化 DefectNet;
属于训练,走 require_cuda() 守卫
- 产出 assignments.csv、summary.json、按簇的软链目录与代表样本拼图,供人工核对
全量 14,579 张:轮廓系数 0.326、换种子稳定性 0.964,CPU 约 100 秒。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:55:47 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
4b1147ed8f
|
feat(scripts): 数据转换、ONNX 导出与推理性能分析
- convert_annotations: labelme / VOC -> YOLO txt,顺带生成 data.yaml
- split_dataset: 切分成 images/{train,val} 与 labels/{train,val},默认建软链不复制数据
- export_onnx: 导出部署用 ONNX,CPU 可跑,缺依赖时提示 `uv sync --group export`
- profile_inference: torch.profiler 测单帧延迟/FPS、CUDA 显存峰值与算子级明细
- check_env: Python / PyTorch / Nvidia GPU 自检
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:55:20 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
b0d0dfffbb
|
feat: 训练与评估入口及 yaml 配置
- train.py: 先过 require_cuda() 守卫再建模型/数据,支持 --resume 与 --set 覆盖
- eval.py: 允许 CPU,输出定位与分类指标并可写 json
- configs: model/dataset/train/eval 四类,通过 `_base_` 组合;
classes 既可直接写列表,也可指向 YOLO data.yaml 由框架读 names
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:55:20 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
ef64e5bf77
|
feat(engine): 训练评估循环、CenterNet 损失与指标
- trainer: AMP、梯度裁剪、cosine/step 调度、last/best 检查点,按 monitor 指标挑选 best
- losses: penalty-reduced focal loss(heatmap)+ 掩码 L1(wh/offset)+ 分类损失
(多类 BCE / 单类 CE,随 label_mode 切换)
- metrics: 定位 IoU 匹配下的 P/R/F1 与 mean IoU;分类多标签 micro-F1 与单标签 accuracy,
两种模式都输出 cls_f1,故 train.monitor 无需跟着改
- optim: BatchNorm 与 bias 不做 weight decay
- visualizer: SwanLab 记录损失/学习率/验证指标/显存峰值,未安装或离线时降级为警告
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:55:20 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
af22dc639c
|
feat(datasets): YOLO 风格标注数据集与几何/像素增强
- yolo: images/ 与同层 labels/ 的路径约定、`class_id cx cy w h` 归一化行解析
(兼容分割多边形取外接矩形)、data.yaml 读写;缺失或空 txt 即无缺陷样本
- film_defect: 抛出图像、框、图像级标签;label_mode 决定多热向量还是互斥下标
- transforms: 图框同步的 Resize / Letterbox / CenterCrop / RandomResizedCrop /
翻转 / ColorJitter / GaussianBlur / PerImageStandardize
- target_encoder: 高斯半径与 heatmap / wh / offset / mask 监督信号编码
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:55:20 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
3a36aad6c3
|
feat(models): 纯卷积缺陷分类定位模型
面向实时设备部署,全程只用 convolution,不含 attention / transformer。
- backbones: 自研 tiny_conv(深度可分离,默认)与 torchvision 封装(resnet/mobilenet/shufflenet)
- necks: nearest 上采样 + 3x3 卷积,把特征恢复到 stride 4,兼顾小缺陷定位与速度
- heads: anchor-free 中心点定位头(heatmap + wh + offset)与图像级分类头;
分类头输出维度随 label_mode 切换(多类多热 / 单类互斥含背景位)
- postprocess: 3x3 max-pool 峰值抑制替代 NMS 循环,无循环、可导出 TensorRT / OpenVINO
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:54:55 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
f4eff8beee
|
feat(utils): 配置继承、注册表、日志与 Nvidia GPU 守卫
- config: yaml `_base_` 继承 + 深度合并 + 命令行 `--set a.b.c=value` 覆盖,检测循环引用
- registry: 组件按名注册与构建,模型/数据集共用
- device: require_cuda() 守卫,按 CLAUDE.md 约定禁止在无 Nvidia GPU 的设备上训练;
评估与导出走 resolve_device() 允许 CPU
- labels: multilabel / multiclass 两种图像级标签形式的共用定义
- logging / seed: 控制台+文件日志、随机种子与 cudnn 确定性
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:54:55 +08:00 |
|
 Shaun ZhouandClaude Opus 5
|
0645fc19f7
|
chore: 初始化 uv 项目配置与忽略规则
- pyproject.toml 采用 uv 应用模式(package = false),直接从仓库根运行入口脚本
- 依赖:torch / torchvision / numpy / pillow / pyyaml / swanlab
- dev 组含 pytest 与 ruff,export 组按需安装 onnx / onnxscript
- 忽略数据集本体、runs/ 训练产物、权重与 .DS_Store
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-02 17:54:55 +08:00 |
|