Files
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

151 lines
7.5 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 未分类样本的无监督聚类。
#
# 预处理决策(针对 data/raw/film 的实际情况:14.5k 张已裁好的缺陷小图,
# 中位尺寸 152x113,宽高比 0.24~39.9,尺寸几乎每张都不同):
#
# 前提:图片的尺寸和宽高比是上游 ROI 裁剪环节的产物,不是缺陷属性。
# 所以预处理的目标是**把这个采集噪声抹掉**,只留下缺陷的纹理/形态。
#
# 1. 直接 Resize 拉伸到 128x128,不用 letterbox —— letterbox 的填充比例
# 恰好等价于编码了宽高比,灰边占多少就等于把采集噪声明着喂给网络;
# 拉伸则让所有图变成同一个画幅,宽高比不再是可用信号。
# 2. use_shape_features 关掉 —— 同理,尺寸不进 embedding。
# 3. 逐图标准化(PerImageStandardize)而不是固定的 ImageNet mean/std ——
# 整体亮度来自光照和相机增益,同样不是缺陷属性。用固定归一化时,
# 4 个簇里有 3 个花在"背景偏亮/偏暗"上;改成逐图 z-score 后,
# 簇变成"条纹状 / 团块状 / 弱纹理",轮廓 0.256->0.322,稳定性 0.966->0.995。
# 4. 不下采样到 224 —— 这批图本来就小,放大只会糊,128 已经够。
#
# 代价:拉伸会让纹理各向异性形变(40:1 的长条尤其明显)。如果后续发现
# 聚类被形变干扰,可选的替代是在原始分辨率上取固定大小的中心 patch
# (不缩放、不填充),配置里把 Resize 换成 CenterCrop 即可。
work_dir: runs/film
seed: 42
device: auto # auto 按 cuda > mps > cpu 挑最快的可用后端;也可写 cuda / mps / cpu
num_clusters: 4 # 想分几类改这里(或 --num-clusters 6)
# pretrained: ImageNet 预训练卷积骨架直接抽特征,不训练,CPU 也能跑,几分钟出结果。
# simclr: 在本目录数据上自监督训练项目自己的卷积骨架,域适配更好;Mac 上走 MPS 即可。
method: pretrained
# method=pretrained 用的骨架(method=simclr 的骨架配在 simclr.backbone,两者字段完全不同,
# 各自独立以免 _base_ 合并时串味)
backbone:
name: torchvision
arch: resnet18
pretrained: true
in_channels: 3
image_size: [128, 128]
use_shape_features: false # 尺寸/宽高比是采集产物,不作为聚类特征;true 仅供对照实验
# 取哪几个 stage 的特征拼起来(torchvision resnet 的 features 下标:4~7 = layer1~4)。
# 实测(2000 张随机样本,k=4):
# 只用最后一层 layer4 轮廓 0.191 换种子稳定性 0.410
# layer2+3 轮廓 0.256 稳定性 0.989 <- 默认
# layer2+3+4 轮廓 0.220 稳定性 0.627
# 缺陷之间的差别主要在纹理(颗粒感、条纹方向、边缘锐度),中间层保留得比最后一层好;
# 最后一层的 ImageNet 语义("这是猫还是车")对这批图基本是噪声。
feature_stages: [5, 6]
# 特征降维。whiten 默认关闭:CNN 特征只有前若干主成分携带结构,
# 白化会把剩下几十个噪声方向抬到同权,簇立刻塌成"一个大簇 + 几个碎簇"
# (实测开白化时最大簇占 96%,关掉后降到 38%)。
pca_dim: 32
whiten: false
n_init: 10
# 只用一部分样本拟合质心以节省时间(0 = 用全部);纯随机抽取,不按目录分层——
# 目录名是人工整理的历史产物,不参与聚类。拟合完仍会把全部样本分配到最近的簇。
fit_max_samples: 0
dataset:
name: unlabeled
root: data/raw
gray: false
exclude: [images, labels] # 跳过已经整理成 YOLO 结构的部分
transforms:
- {name: Resize, size: [128, 128]} # 拉伸,不保长宽比:见文件开头的说明
- {name: ToTensor}
- {name: PerImageStandardize} # 逐图 z-score,抹掉整体亮度/对比度
loader:
batch_size: 128
num_workers: 8
# method=simclr 时每个视图各自过一遍的随机增强(正样本对就是同一张图的两个视图)
view_transforms:
- {name: RandomResizedCrop, size: [128, 128], scale: [0.5, 1.0]}
- {name: RandomHorizontalFlip, p: 0.5}
- {name: RandomVerticalFlip, p: 0.5}
- {name: ColorJitter, brightness: 0.4, contrast: 0.4}
- {name: GaussianBlur, p: 0.3}
# 注意:主 transforms 里的 PerImageStandardize 会在这些增强之后执行,
# 所以 ColorJitter 的亮度扰动最终会被抵消掉,起作用的主要是裁剪/翻转/模糊。
simclr:
# 自监督用项目自己的纯卷积骨架:练出来的权重可以直接拿去初始化 DefectNet 的 backbone
backbone:
name: tiny_conv
in_channels: 3
widths: [32, 64, 128, 256]
depths: [1, 2, 2, 2]
block: dw
feature_stages: null # tiny_conv 不是 nn.Sequential,取最终输出
epochs: 100
batch_size: 128
temperature: 0.2 # 越小越关注难负样本
amp: auto # CUDA 上开、MPS 上关
save_interval: 5 # 每 5 个 epoch 落一次盘,中途中断也能拿到权重
# 续训时若调度已接近末尾,cosine 会把新设的 lr 乘上接近 0 的系数。
# 打开这个开关:按剩余轮数重开一条曲线,新 lr 从满值开始。
restart_schedule: false
# 训练可视化,字段与 train.swanlab 相同。默认 local:不需要登录,
# 日志写到 <work_dir>/swanlog,另开终端 `uv run swanlab watch runs/simclr/swanlog` 看板。
swanlab:
enabled: true
project: film-simclr
name: null # 缺省用 work_dir 目录名
mode: local # online(云端) | local(本地看板) | offline(仅存盘) | disabled
tags: [simclr, tiny_conv]
log_interval: 20
# checkpoint 与 resume 二选一,语义不同:
# checkpoint: 有权重就**跳过训练**,直接拿它抽特征聚类(换簇数时用这个)
# resume: 载入骨架+投影头+优化器+调度器,从上次的 epoch **接着训练**
checkpoint: null # 例:runs/simclr/simclr_backbone.pth
resume: null # 例:runs/simclr/simclr_last.pth
projection: {hidden_channels: 256, out_channels: 128}
optimizer: {name: adamw, lr: 0.001, weight_decay: 0.0001}
scheduler: {name: cosine}
# 输出:每簇放多少张图 / 预览拼图放多少张
#
# link_mode 决定 clusters/ 里放什么:
# hard(默认)——硬链接。不占额外磁盘,但表现得和真实文件一样:看图软件、
# 在 runs/ 下直接继续分子类的工具都能用,整个目录拷到别的机器
# 也不会断链。要求与源图同一文件系统,跨盘会自动退回软链并警告。
# sym ——软链接。跨盘可用,但记的是绝对路径,换机器就断
# (export_clusters.py 能重定位回来)。
# copy ——直接复制。跨盘可用且绝不断链,代价是占一份磁盘。
#
# 注意 hard 与原图是同一份数据:在 clusters/ 里**改图会改到原图**;
# 只挪动、删除条目不影响原图。
link_mode: hard
link_limit: 300
preview_count: 100
# 簇分布图 cluster_scatter.png(与 preview 拼图同级):
# 左边是聚类空间的 PCA 二维投影(散点按簇着色,X 是质心),右边是质心两两距离矩阵。
# 用 PCA 而不是 t-SNE/UMAP——后两者为了把簇拉开会扭曲簇间距离,而这张图就是要看距离。
scatter:
enabled: true
max_points: 5000 # 点太多会糊成一团且文件很大,超过就随机抽样
point_size: 6
alpha: 0.35
figsize: [14, 6]
dpi: 150