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>
This commit is contained in:
Shaun Zhou
2026-09-02 21:43:35 +08:00
co-authored by Claude Opus 5
parent b6f83fcad0
commit 938d62ea53
5 changed files with 93 additions and 84 deletions
+34 -10
View File
@@ -25,13 +25,30 @@ convolution 的结构(不含 attention / transformer),并支持导出 ONNX
```bash
uv sync # 安装依赖(含 swanlab)
uv sync --group export # 需要导出 ONNX 时再装
uv run python scripts/check_env.py # 自检(Python / PyTorch / Nvidia GPU)
uv run python scripts/check_env.py # 自检(Python / PyTorch / 可用后端)
```
> **训练机必须有 Nvidia GPU。** 所有训练入口先调用 `utils.device.require_cuda()`,
> 检测不到 CUDA 构建的 PyTorch 或可用 GPU 时直接报错退出。评估与 ONNX 导出允许 CPU。
> Linux 上 PyPI 的 torch 即 CUDA 构建;如需指定 CUDA 版本:
> `uv pip install --index-url https://download.pytorch.org/whl/cu124 torch torchvision`
### 计算后端
支持 **CUDA / MPS(Apple Silicon)/ CPU**。配置里的 `device: auto` 按 `cuda > mps > cpu`
自动挑最快的可用后端,也可以显式写 `cuda` / `mps` / `cpu`,或用命令行 `--device` 覆盖:
```bash
uv run python train.py --config configs/train/baseline.yaml # auto
uv run python train.py --config configs/train/baseline.yaml --device mps # 强制 Mac GPU
uv run python train.py --config configs/train/baseline.yaml --device cpu
```
**显式指定不可用的后端会直接报错退出**,不会悄悄退回 CPU——训练慢几十倍却不吭声是最坏的情况。
| 后端 | AMP 默认 | 显存统计 | 备注 |
| --- | --- | --- | --- |
| `cuda` | **开** | 精确峰值 | Linux 上 PyPI 的 torch 即 CUDA 构建;指定版本用 `uv pip install --index-url https://download.pytorch.org/whl/cu124 torch torchvision` |
| `mps` | **关** | 采样下界 | Apple Silicon。autocast 可用且数值正常,但实测比 fp32 慢(tiny_conv @128²:fp32 76 ms/步,fp16 90 ms),故默认不开;`amp: true` 可强制打开 |
| `cpu` | 不支持 | 无 | 实测比 MPS 慢约 30 倍,仅适合调试与精度回归 |
`amp` 字段接受 `auto`(随后端)/ `true` / `false`。MPS 上 `torch.profiler` 抓不到 Metal kernel
(没有对应的 `ProfilerActivity`),算子明细只反映 CPU 侧下发耗时。
## 数据准备
@@ -148,7 +165,7 @@ uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml
# 先扫一遍簇数再决定分几类
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --sweep-k 2-8
# 换成在本地数据上自监督训练的特征(SimCLR,属于训练,需要 Nvidia GPU)
# 换成在本地数据上自监督训练的特征(SimCLR,Mac 上自动走 MPS)
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --set method=simclr
```
@@ -177,8 +194,8 @@ uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --
**本批薄膜图上自监督训练**一个项目自己的纯卷积骨架,域适配更好。同一张图的两个随机增强
视图互为正样本,batch 内其余 2B−2 个视图为负样本,用 NT-Xent 损失训练。
**这属于训练,因此必须有 Nvidia GPU**(走 `utils.device.require_cuda()` 守卫,
在无卡机器上会直接报错退出):
训练走 `device: auto` 选中的后端——**Mac 上直接用 MPS 即可**,实测 14,579 张、
batch 256 时约 65 秒/epoch:
```bash
# 1. 自监督训练 + 聚类一步跑完(首次运行)
@@ -191,7 +208,13 @@ uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --set method=simclr simclr.epochs=200 simclr.batch_size=512 simclr.temperature=0.1
```
训练完权重存到 `<work_dir>/simclr_backbone.pth`(**只含骨架,不含投影头**——投影头被训练成
**实测效果**(14,579 张,与 pretrained 用完全相同的聚类设置):只训 **1 个 epoch** 的域内特征
在 k=4 上就追平 ImageNet(轮廓 0.334 vs 0.325),在 **k=2 上大幅胜出**(轮廓 0.469、
稳定性 0.991)——目视确认这个二分正是"有明显缺陷 / 弱纹理近空白",可直接当作流水线的
第一级过滤。详见 [文档 §10.5](docs/unsupervised-clustering.md#105-simclr-域内自监督-vs-imagenet-预训练)。
训练每 `save_interval` 个 epoch 落一次盘,中途 Ctrl-C 也能拿到当前权重继续用。
权重存到 `<work_dir>/simclr_backbone.pth`(**只含骨架,不含投影头**——投影头被训练成
对增强不变,会丢掉下游有用的信息,所以聚类和保存都只用骨架特征)。相关配置:
```yaml
@@ -204,7 +227,8 @@ simclr:
epochs: 100
batch_size: 256
temperature: 0.2 # 越小越关注难负样本
amp: true
amp: auto # CUDA 上开、MPS 上关
save_interval: 5 # 每 5 个 epoch 落一次盘
checkpoint: null # 填已有权重可跳过重复训练
projection: {hidden_channels: 256, out_channels: 128}
optimizer: {name: adamw, lr: 0.001, weight_decay: 0.0001}
+56 -71
View File
@@ -20,7 +20,7 @@
7. [阶段五:无标签评估](#7-阶段五无标签评估)
8. [阶段六:SimCLR 自监督(可选)](#8-阶段六simclr-自监督可选)
9. [复杂度与实测耗时](#9-复杂度与实测耗时)
10. [消融实验数据](#10-消融实验数据)
10. [消融实验数据](#10-消融实验数据)(含 [SimCLR vs ImageNet 对比](#105-simclr-域内自监督-vs-imagenet-预训练))
11. [已知局限与改进方向](#11-已知局限与改进方向)
12. [公式与代码对照表](#12-公式与代码对照表)
@@ -130,7 +130,7 @@ flowchart TB
summ["summary.json<br/>簇大小 / 轮廓 / 稳定性"]:::out
end
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>需 Nvidia GPU"]:::feat
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>cuda / mps 均可"]:::feat
scan --> PRE
PRE -->|"x̃ᵢ"| FEAT
@@ -330,8 +330,7 @@ xychart
| 32 | 0.922 | 0.050 | 59% |
| 64 | 0.922 | 0.047 | 82% |
不白化时结果对 $q$ 几乎不敏感(都是 0.92),白化则全线崩溃。在真实数据上,
早期开白化的版本最大簇吃掉 **96%** 的样本、稳定性仅 0.01;关掉后最大簇降到 38%。
不白化时结果对 $q$ 几乎不敏感(都是 0.92),白化则全线崩溃。在真实数据上,早期开白化的版本最大簇吃掉 **96%** 的样本、稳定性仅 0.01;关掉后最大簇降到 38%。
> 这条结论被回归测试 `test_whitening_amplifies_noise_directions` 锁死。
@@ -405,16 +404,13 @@ $$
\mu_j = \frac{1}{|C_j|}\sum_{i \in C_j} y_i .
$$
由于 $J$ 在两步中都不增、且 $J \ge 0$ 有下界,同时 $N$ 个点划分成 $k$ 簇的方式**有限**,
算法必在有限步内停止。收敛判据用相对变化:$|J_{\text{prev}} - J| \le \text{tol}\cdot J_{\text{prev}}$,
`tol = 1e-5`,上限 100 次迭代。
由于 $J$ 在两步中都不增、且 $J \ge 0$ 有下界,同时 $N$ 个点划分成 $k$ 簇的方式**有限**,算法必在有限步内停止。收敛判据用相对变化:$|J_{\text{prev}} - J| \le \text{tol}\cdot J_{\text{prev}}$,`tol = 1e-5`,上限 100 次迭代。
> **注意**:收敛保证的是局部最优,不是全局最优——这正是需要 `n_init` 的原因。
### 6.3 k-means++ 初始化
均匀随机取初值时,多个初始质心可能落在同一团里,导致明显的坏解。k-means++ 按到已选
质心的平方距离采样:
均匀随机取初值时,多个初始质心可能落在同一团里,导致明显的坏解。k-means++ 按到已选质心的平方距离采样:
$$
\Pr(\text{选中 } y) = \frac{D(y)^2}{\sum_{y'} D(y')^2},
@@ -431,28 +427,22 @@ $$
### 6.4 空簇处理
更新步中若某簇没有任何成员,$\mu_j$ 无定义。实现的处理是:**把当前离自身质心最远的点
提为该空簇的新质心**。这保证输出恰好 $k$ 个非空簇(否则实际簇数会悄悄缩水),且由于选的是
$J$ 贡献最大的点,这一步通常还会降低 $J$。
更新步中若某簇没有任何成员,$\mu_j$ 无定义。实现的处理是:**把当前离自身质心最远的点提为该空簇的新质心**。这保证输出恰好 $k$ 个非空簇(否则实际簇数会悄悄缩水),且由于选的是 $J$ 贡献最大的点,这一步通常还会降低 $J$。
### 6.5 多次重启
跑 `n_init = 10` 次完整的(初始化 + Lloyd),取 $J$ 最小的一次。设单次落入某个坏局部最优的
概率为 $p$,则 10 次全坏的概率降到 $p^{10}$。
跑 `n_init = 10` 次完整的(初始化 + Lloyd),取 $J$ 最小的一次。设单次落入某个坏局部最优的概率为 $p$,则 10 次全坏的概率降到 $p^{10}$。
### 6.6 子集拟合 + 全量指派
可选地只用随机子集 $\mathcal{S} \subset \{1..N\}$(`fit_max_samples`)拟合质心,再把全部
$N$ 个样本按最近质心指派:
可选地只用随机子集 $\mathcal{S} \subset \{1..N\}$(`fit_max_samples`)拟合质心,再把全部 $N$ 个样本按最近质心指派:
$$
\mu^* = \arg\min_\mu J_{\mathcal{S}}(\mu), \qquad
c(i) = \arg\min_j \lVert y_i - \mu^*_j \rVert, \ \ i = 1,\dots,N .
$$
合理性:质心是簇内均值的估计,其标准误随 $1/\sqrt{|\mathcal{S}|}$ 下降,几千个样本已足够。
**子集必须是纯随机抽取**——按目录或任何人工整理的历史标签做分层,等于把先验偷偷注入
无监督流程。默认 `fit_max_samples: 0`(用全部样本),因为 14.5k × 32 维的 K-Means 本来就只要几秒。
合理性:质心是簇内均值的估计,其标准误随 $1/\sqrt{|\mathcal{S}|}$ 下降,几千个样本已足够。**子集必须是纯随机抽取**——按目录或任何人工整理的历史标签做分层,等于把先验偷偷注入无监督流程。默认 `fit_max_samples: 0`(用全部样本),因为 14.5k × 32 维的 K-Means 本来就只要几秒。
---
@@ -476,8 +466,7 @@ s(i) = \frac{b(i) - a(i)}{\max\{a(i),\, b(i)\}} \in [-1, 1],
\bar{s} = \frac{1}{N}\sum_i s(i).
$$
$s(i) \to 1$ 表示样本牢牢待在自己簇里,$s(i) < 0$ 表示它离别的簇更近(八成分错了)。
计算是 $O(N^2 d)$,故超过 `max_samples = 2000` 时随机抽样估计。
$s(i) \to 1$ 表示样本牢牢待在自己簇里,$s(i) < 0$ 表示它离别的簇更近(八成分错了)。计算是 $O(N^2 d)$,故超过 `max_samples = 2000` 时随机抽样估计。
### 7.2 换种子稳定性(划分是否可复现)
@@ -494,12 +483,9 @@ I(\mathcal{A};\mathcal{B}) = \sum_{a}\sum_{b} p(a,b)\log\frac{p(a,b)}{p(a)p(b)},
H(\mathcal{A}) = -\sum_a p(a)\log p(a),
$$
其中 $p(a,b) = |A_a \cap B_b| / N$。NMI 对簇编号的置换不变(换标签名不影响),
取值 $[0,1]$,1 表示两次划分完全一致。
其中 $p(a,b) = |A_a \cap B_b| / N$。NMI 对簇编号的置换不变(换标签名不影响),取值 $[0,1]$,1 表示两次划分完全一致。
**为什么这个指标关键**:数据里真有 $k$ 团结构时,不同初始化会收敛到同一套划分,NMI ≈ 1;
若是在硬切一团连续的数据,边界位置随初值乱跳,NMI 会很低。它比轮廓系数更能识破
"强行分 $k$ 类"的情形。实测中它一眼揪出了白化的问题(稳定性 0.01)。
**为什么这个指标关键**:数据里真有 $k$ 团结构时,不同初始化会收敛到同一套划分,NMI ≈ 1;若是在硬切一团连续的数据,边界位置随初值乱跳,NMI 会很低。它比轮廓系数更能识破"强行分 $k$ 类"的情形。实测中它一眼揪出了白化的问题(稳定性 0.01)。
> **不使用外部标签。** `data/film` 下的目录名(`grease`、`wrinkle`、`反面纵黑点`……)
> 是人工整理的历史产物,既不完整也未必正确,因此既不参与聚类、也不用于评估。
@@ -508,9 +494,7 @@ $$
## 8. 阶段六:SimCLR 自监督(可选)
`method: pretrained` 用的是 ImageNet 权重——域外的。`method: simclr` 则在**这批薄膜图
本身**上学特征,域适配更好。它是训练,因此按项目约定必须有 Nvidia GPU
(`utils.device.require_cuda()` 守卫)。
`method: pretrained` 用的是 ImageNet 权重——域外的。`method: simclr` 则在**这批薄膜图本身**上学特征,域适配更好。它是训练,设备由 `device: auto` 按 `cuda > mps > cpu` 自动挑选:CUDA 与 Apple Silicon 的 MPS 都能跑(实测 14,579 张、batch 256 时 MPS 约 65 秒/epoch),CPU 慢约 30 倍不建议。
```mermaid
flowchart TB
@@ -572,12 +556,9 @@ $$
x_i^{(1)} = t(I_i), \qquad x_i^{(2)} = t'(I_i).
$$
$\mathcal{T}$ 包含 `RandomResizedCrop(scale=0.5~1.0)`、水平/垂直翻转、高斯模糊。
**监督信号完全来自"这两个视图来自同一张图"这一事实**,不需要任何人工标签。
$\mathcal{T}$ 包含 `RandomResizedCrop(scale=0.5~1.0)`、水平/垂直翻转、高斯模糊。**监督信号完全来自"这两个视图来自同一张图"这一事实**,不需要任何人工标签。
> `scale` 下限不宜低于 0.4:裁得太狠容易两个视图都只剩背景,"背景 vs 背景"被当成正样本对,
> 学出的特征会退化。另外主 transforms 里的 `PerImageStandardize` 在增强**之后**执行,
> 因此 `ColorJitter` 的亮度扰动最终会被抵消,真正起作用的是裁剪/翻转/模糊。
> `scale` 下限不宜低于 0.4:裁得太狠容易两个视图都只剩背景,"背景 vs 背景"被当成正样本对,学出的特征会退化。另外主 transforms 里的 `PerImageStandardize` 在增强**之后**执行,因此 `ColorJitter` 的亮度扰动最终会被抵消,真正起作用的是裁剪/翻转/模糊。
### 8.2 NT-Xent 损失
@@ -596,16 +577,11 @@ $$
\log \frac{\exp\!\big(S_{k,\pi(k)}\big)}{\sum_{m \neq k}\exp\!\big(S_{k,m}\big)}\ }
$$
分子只有 1 个正样本对,分母是全部 $2B-2$ 个负样本加正样本对。形式上就是把
"在 $2B-1$ 个候选中认出配对视图"当成一个 $(2B-1)$ 类的分类问题——实现里直接用
`F.cross_entropy(S, targets)`,`targets[k] = π(k)`。
分子只有 1 个正样本对,分母是全部 $2B-2$ 个负样本加正样本对。形式上就是把"在 $2B-1$ 个候选中认出配对视图"当成一个 $(2B-1)$ 类的分类问题——实现里直接用 `F.cross_entropy(S, targets)`,`targets[k] = π(k)`。
**温度 $\tau$** 控制分布的尖锐度:$\tau$ 小 ⇒ softmax 更尖 ⇒ 梯度集中在最难的负样本上;
$\tau$ 大 ⇒ 所有负样本近乎等权。默认 $\tau = 0.2$。
**与互信息的关系**:NT-Xent 是 InfoNCE 的一个实例,最小化 $\mathcal{L}$ 等价于最大化两个
视图表征之间互信息的一个下界:
**温度 $\tau$** 控制分布的尖锐度:$\tau$ 小 ⇒ softmax 更尖 ⇒ 梯度集中在最难的负样本上;$\tau$ 大 ⇒ 所有负样本近乎等权。默认 $\tau = 0.2$。
**与互信息的关系**:NT-Xent 是 InfoNCE 的一个实例,最小化 $\mathcal{L}$ 等价于最大化两个视图表征之间互信息的一个下界:
$$
I\big(h^{(1)}; h^{(2)}\big) \;\ge\; \log(2B-1) - \mathcal{L}.
$$
@@ -614,16 +590,12 @@ $$
### 8.3 为什么聚类用骨架特征而不是投影头输出
$g_\varphi$ 被显式训练成对增强 $\mathcal{T}$ **不变**。凡是被增强破坏掉的信息(模糊掉的
高频细节、翻转掉的方向性),在 $z$ 里都会被压掉。而这些信息对下游任务往往有用。
$h = f_\theta(x)$ 位于投影头之前,保留得更完整——这是 SimCLR 原论文的既有结论
(线性评估上 $h$ 比 $z$ 高十几个点)。因此:**训练用 $z$ 算损失,聚类和保存权重都只用 $h$。**
$g_\varphi$ 被显式训练成对增强 $\mathcal{T}$ **不变**。凡是被增强破坏掉的信息(模糊掉的高频细节、翻转掉的方向性),在 $z$ 里都会被压掉。而这些信息对下游任务往往有用。
$h = f_\theta(x)$ 位于投影头之前,保留得更完整——这是 SimCLR 原论文的既有结论(线性评估上 $h$ 比 $z$ 高十几个点)。因此:**训练用 $z$ 算损失,聚类和保存权重都只用 $h$。**
### 8.4 与检测任务的衔接
`simclr_backbone.pth` 里的骨架就是项目检测模型用的同一个卷积骨架,可以直接拿去初始化
`DefectNet` 的 backbone——在标注量有限时,用 14.5k 张无标注图预训练过的权重通常比
ImageNet 权重更贴合薄膜的成像特性。
`simclr_backbone.pth` 里的骨架就是项目检测模型用的同一个卷积骨架,可以直接拿去初始化 `DefectNet` 的 backbone——在标注量有限时,用 14.5k 张无标注图预训练过的权重通常比 ImageNet 权重更贴合薄膜的成像特性。
---
@@ -641,7 +613,7 @@ ImageNet 权重更贴合薄膜的成像特性。
| 产出(软链 + 拼图) | $O(N + k\cdot\text{preview})$ | ~2 s |
| **合计** | | **~100 s** |
GPU 上特征提取会快一个数量级;瓶颈会转移到图像解码(可增大 `loader.num_workers`)。
换到 Apple Silicon 的 MPS(`device: auto` 自动选中)后,同样 14,579 张的特征提取从 84 秒降到 51 秒——瓶颈已转移到图像解码,继续加速要靠增大 `loader.num_workers`。SimCLR 自监督训练在 MPS 上约 65 秒/epoch(batch 256,双视图,显存峰值约 137 MB)。
---
@@ -688,10 +660,32 @@ GPU 上特征提取会快一个数量级;瓶颈会转移到图像解码(可
| 7 | 0.230 | 0.692 | 0.28 | [236, 345, 87, 509, 559, 150, 114] |
| 8 | 0.272 | 0.758 | 0.34 | [676, 215, 58, 27, 176, 99, 430, 319] |
$k=4$ 的稳定性在 $k \in [3,7]$ 中最高,轮廓也接近峰值——默认值 4 有数据支撑。
命令行随时可扫:`--sweep-k 2-8`。
$k=4$ 的稳定性在 $k \in [3,7]$ 中最高,轮廓也接近峰值——默认值 4 有数据支撑。命令行随时可扫:`--sweep-k 2-8`。
### 10.5 全量最终结果
### 10.5 SimCLR 域内自监督 vs ImageNet 预训练
在全部 14,579 张上、用**完全相同**的聚类设置(PCA 32 维、不白化、`n_init=10`)对比两种特征。
SimCLR 一侧用的是在本批数据上**只训了 1 个 epoch**(Apple M 系列 MPS,65 秒)的 tiny_conv 骨架:
| 特征 | 维度 | $k=4$ 轮廓 | $k=4$ 稳定性 | $k=2$ 轮廓 | $k=2$ 稳定性 |
| --- | --- | --- | --- | --- | --- |
| ImageNet 预训练 layer2+3 | 384 | 0.325 | **0.977** | 0.354 | 0.887 |
| SimCLR 域内(1 epoch) | 256 | **0.334** | 0.829 | **0.469** | **0.991** |
两点值得注意:
1. **只训 1 个 epoch 就在 $k=4$ 上追平并略微超过 ImageNet 特征**(0.334 vs 0.325),而它的维度还少三分之一。域内自监督的性价比很高。
2. **$k=2$ 上 SimCLR 明显更强**(轮廓 0.469、稳定性 0.991)。目视确认这个二分就是"有明显缺陷 / 弱纹理近空白":8,750 张落在空白侧,5,829 张落在缺陷侧(褶皱、V 形折痕、条纹、异物、点串)。这正是 [§11](#11-已知局限与改进方向) 提到的"先筛掉无缺陷样本再细分"那一步——SimCLR 特征直接把它做出来了,可以作为流水线的第一级过滤。
$k=4$ 时 SimCLR 的四个簇为:暗底细长条纹/点串/异物(2,423)、空白暗底两类(5,801 + 2,927)、亮底团块/污渍(3,428)。相比 ImageNet 特征的划分,两个缺陷簇的代表样本明显更同质。
$k=4$ 稳定性略低于 ImageNet(0.83 vs 0.98),符合"数据的主结构是二分、强行切四份时边界不唯一"的解释。
> **这是下界,不是 SimCLR 的上限。** 原计划跑 40 epoch,中途在 epoch 12(loss 4.55 → 2.66)停掉,
> 而当时 `fit()` 只在全部 epoch 结束后落盘,训练成果没保住。该缺陷已修复(`save_interval`
> 每 5 个 epoch 存一次,并捕获 KeyboardInterrupt),上表用的是此前完整跑完的 1-epoch 权重。
### 10.6 全量最终结果
| 指标 | 值 |
| --- | --- |
@@ -715,25 +709,15 @@ $k=4$ 的稳定性在 $k \in [3,7]$ 中最高,轮廓也接近峰值——默
## 11. 已知局限与改进方向
1. **两个簇是"近乎空白"的。** 这批裁片里确实有很大比例在 128×128 尺度下看不出明显缺陷,
$k=4$ 就会把其中两个名额花在它们的细微差异上。这是数据本身的性质,不是算法缺陷。
对策:调大 $k$ 让缺陷簇进一步细分;或先用一个简单的对比度/能量阈值把"无明显缺陷"
的样本筛掉,再对剩下的做聚类。
1. **两个簇是"近乎空白"的。** 这批裁片里确实有很大比例在 128×128 尺度下看不出明显缺陷,$k=4$ 就会把其中两个名额花在它们的细微差异上。这是数据本身的性质,不是算法缺陷。对策:调大 $k$ 让缺陷簇进一步细分;或先用一个简单的对比度/能量阈值把"无明显缺陷"的样本筛掉,再对剩下的做聚类。
2. **ImageNet 特征终究是域外的。** `method: simclr` 在本批数据上自监督预训练,理论上更贴合,
但需要 GPU 且要调 epoch / batch size。建议在 GPU 机器上跑一次并与 pretrained 对比
轮廓系数与稳定性。
2. **ImageNet 特征终究是域外的。** 已实测:只训 1 个 epoch 的域内 SimCLR 就在 $k=4$ 上追平(0.334 vs 0.325),在 $k=2$ 上大幅胜出(0.469 / 稳定性 0.991),见 [§10.5](#105-simclr-域内自监督-vs-imagenet-预训练)。继续训到几十个 epoch 值得一试。
3. **拉伸带来的各向异性形变**在极端长条(40:1)上很严重。若发现某个簇专门收集长条,
可换 `CenterCrop`(原分辨率取中心 patch),或改成多 patch 聚合(对每张图取若干 patch
分别编码后取平均/最大)。后者对"缺陷是局部的"这一先验更契合,目前尚未实现。
3. **拉伸带来的各向异性形变**在极端长条(40:1)上很严重。若发现某个簇专门收集长条,可换 `CenterCrop`(原分辨率取中心 patch),或改成多 patch 聚合(对每张图取若干 patch 分别编码后取平均/最大)。后者对"缺陷是局部的"这一先验更契合,目前尚未实现。
4. **K-Means 假设簇是各向同性的球。** 若缺陷类别在特征空间里呈细长流形,K-Means 会切错。
备选:谱聚类(构图 + 拉普拉斯特征向量)或 HDBSCAN(不需要预设 $k$,能标记噪声点)。
代价是复杂度更高、超参更难调。
4. **K-Means 假设簇是各向同性的球。** 若缺陷类别在特征空间里呈细长流形,K-Means 会切错。备选:谱聚类(构图 + 拉普拉斯特征向量)或 HDBSCAN(不需要预设 $k$,能标记噪声点)。代价是复杂度更高、超参更难调。
5. **轮廓系数 0.33 属于"弱但存在"的结构。** 不要指望聚类结果能直接当标签用;
它的定位是**把人工核对的工作量从 14,579 张降到 4 个簇的抽查**。
5. **轮廓系数 0.33 属于"弱但存在"的结构。** 不要指望聚类结果能直接当标签用;它的定位是**把人工核对的工作量从 14,579 张降到 4 个簇的抽查**。
---
@@ -757,11 +741,12 @@ $k=4$ 的稳定性在 $k \in [3,7]$ 中最高,轮廓也接近峰值——默
| NT-Xent 损失 | `engine/simclr.py::nt_xent_loss` |
| SimCLR 训练循环 | `engine/simclr.py::SimCLRTrainer` |
| 投影头 $g_\varphi$ | `models/heads/projection.py::ProjectionHead` |
| GPU 守卫 | `utils/device.py::require_cuda` |
| 设备解析(cuda/mps/cpu,auto 优先级) | `utils/device.py::resolve_device` |
| AMP 随后端的默认值 | `utils/device.py::resolve_amp` / `default_amp` |
| 跨后端显存峰值 | `utils/device.py::PeakMemory` |
| CLI 与产出 | `scripts/cluster_unlabeled.py` |
**相关测试**:`tests/test_cluster.py`(K-Means 正确性、白化回归、稳定性、NMI)、
`tests/test_unlabeled_dataset.py`(扫描、混合尺寸/通道、逐图标准化的不变性)。
**相关测试**:`tests/test_cluster.py`(K-Means 正确性、白化回归、稳定性、NMI)、`tests/test_unlabeled_dataset.py`(扫描、混合尺寸/通道、逐图标准化的不变性)。
---
+2 -2
View File
@@ -1,7 +1,7 @@
# 无监督聚类流水线
从 `data/film` 的原始图片到可人工核对的分组结果:六个阶段的数据流、张量形状与每步的数学操作。
虚线是可选的 SimCLR 自监督分支(`method: simclr`,需要 Nvidia GPU)。
虚线是可选的 SimCLR 自监督分支(`method: simclr`,CUDA 与 Apple MPS 均可)。
```mermaid
flowchart TB
@@ -67,7 +67,7 @@ flowchart TB
summ["summary.json<br/>簇大小 / 轮廓 / 稳定性"]:::out
end
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>需 Nvidia GPU"]:::feat
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>cuda / mps 均可"]:::feat
scan --> PRE
PRE -->|"x̃ᵢ"| FEAT
+1 -1
View File
@@ -61,7 +61,7 @@ flowchart TB
summ["summary.json<br/>簇大小 / 轮廓 / 稳定性"]:::out
end
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>需 Nvidia GPU"]:::feat
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>cuda / mps 均可"]:::feat
scan --> PRE
PRE -->|"x̃ᵢ"| FEAT
Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB