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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d6adea5b05
commit
4fc0fa5353
@@ -1,2 +1,244 @@
|
||||
# ModelTrainEval
|
||||
|
||||
薄膜缺陷**分类 + 定位**模型的训练与评估。模型最终部署在实时设备上,因此只使用基于
|
||||
convolution 的结构(不含 attention / transformer),并支持导出 ONNX。
|
||||
|
||||
## 目录
|
||||
|
||||
| 目录 | 作用 |
|
||||
| --- | --- |
|
||||
| `configs/` | yaml 配置(`model/` 模型、`dataset/` 数据、`train/` 训练、`eval/` 评估) |
|
||||
| `models/` | 模型骨架:`backbones/` + `necks/` + `heads/`,由 `defect_net.py` 组装 |
|
||||
| `datasets/` | 抛出数据样本的 Dataset 类、数据增强、监督信号编码 |
|
||||
| `data/` | 数据集本体或软链接(不入库,见 [data/README.md](data/README.md)) |
|
||||
| `scripts/` | 格式转换、数据集切分、ONNX 导出等批处理脚本 |
|
||||
| `engine/` | 训练 / 评估循环、损失、指标、优化器、检查点、SwanLab 可视化 |
|
||||
| `utils/` | 配置、注册表、日志、随机种子、GPU 守卫 |
|
||||
| `tests/` | 单元测试 |
|
||||
| `docs/` | 算法与数学说明文档 |
|
||||
| `figures/` | 文档用的 mermaid 图(`.mmd` 源 + `.md` 预览 + `.png`) |
|
||||
|
||||
入口在仓库根目录:[train.py](train.py)、[eval.py](eval.py)。
|
||||
|
||||
## 环境
|
||||
|
||||
```bash
|
||||
uv sync # 安装依赖(含 swanlab)
|
||||
uv sync --group export # 需要导出 ONNX 时再装
|
||||
uv run python scripts/check_env.py # 自检(Python / PyTorch / Nvidia GPU)
|
||||
```
|
||||
|
||||
> **训练机必须有 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`
|
||||
|
||||
## 数据准备
|
||||
|
||||
标注为 **YOLO 风格**:`images/` 下每张图在同层 `labels/` 有一个同名 `.txt`,
|
||||
每行 `class_id cx cy w h`(相对宽高归一化到 [0,1]),空文件或缺失 `.txt` 即无缺陷样本。
|
||||
|
||||
```
|
||||
data/film/
|
||||
├── images/{train,val}/0001.png
|
||||
├── labels/{train,val}/0001.txt
|
||||
└── data.yaml # path / train / val / nc / names
|
||||
```
|
||||
|
||||
```bash
|
||||
# 1. labelme / VOC -> YOLO txt(顺带生成 data.yaml)
|
||||
uv run python scripts/convert_annotations.py --format labelme \
|
||||
--input data/film/labelme --images data/film/images \
|
||||
--output data/film/labels --data-yaml data/film/data.yaml
|
||||
|
||||
# 2. 切分 train / val(默认建软链,不复制数据)
|
||||
uv run python scripts/split_dataset.py --images data/film/images \
|
||||
--labels data/film/labels --val-ratio 0.2 --data-yaml data/film/data.yaml
|
||||
```
|
||||
|
||||
配置里的 `classes` 既可以直接写类别列表,也可以写成 `data/film/data.yaml`,由框架读 `names`。
|
||||
读取时也接受 YOLO 分割格式的多边形行(取外接矩形)。细节见 [data/README.md](data/README.md)
|
||||
与 [datasets/yolo.py](datasets/yolo.py)。
|
||||
|
||||
## 训练与评估
|
||||
|
||||
```bash
|
||||
uv run python train.py --config configs/train/baseline.yaml
|
||||
uv run python train.py --config configs/train/baseline.yaml --set train.epochs=10 train.optimizer.lr=5e-4
|
||||
uv run python train.py --config configs/train/baseline.yaml --resume runs/baseline/last.pth
|
||||
|
||||
uv run python eval.py --config configs/eval/baseline.yaml --checkpoint runs/baseline/best.pth
|
||||
uv run python scripts/export_onnx.py --config configs/train/baseline.yaml \
|
||||
--checkpoint runs/baseline/best.pth --output runs/baseline/model.onnx
|
||||
```
|
||||
|
||||
产物写在 `work_dir`(默认 `runs/<name>/`):`train.log`、`config.yaml`、`last.pth`、`best.pth`、`swanlog/`。
|
||||
|
||||
## 多类 / 单类分类
|
||||
|
||||
图像级分类的形式由 yaml 的 `label_mode` 决定,模型的分类头、分类损失、分类指标都跟着切换
|
||||
(定位分支不受影响,始终按 `classes` 逐类输出 heatmap):
|
||||
|
||||
| `label_mode` | 含义 | 分类头输出 | 损失 | 主要指标 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `multilabel`(默认) | **多类**:一张图可同时含多种缺陷,全 0 表示 OK 片 | `len(classes)` | 逐类 BCE | micro P/R/F1、`cls_exact_match` |
|
||||
| `multiclass` | **单类**:一张图只归一个类别,互斥 | `len(classes) + 1`(末位为"无缺陷") | softmax CE | `cls_accuracy`、macro P/R/F1 |
|
||||
|
||||
```bash
|
||||
uv run python train.py --config configs/train/baseline.yaml # 多类
|
||||
uv run python train.py --config configs/train/multiclass.yaml # 单类
|
||||
uv run python train.py --config configs/train/baseline.yaml --set label_mode=multiclass
|
||||
```
|
||||
|
||||
单类模式下若一张图标注了多种缺陷,取框数最多的类别(并列取下标最小的);
|
||||
无缺陷样本落到背景下标 `len(classes)`。两种模式的分类指标都含 `cls_f1`,
|
||||
因此 `train.monitor` 不必跟着改。
|
||||
|
||||
## 训练可视化(SwanLab)
|
||||
|
||||
训练过程通过 [SwanLab](https://swanlab.cn) 记录:按 step 记录各项损失与学习率,按 epoch 记录
|
||||
验证指标与显存峰值,实验的超参就是完整的 yaml 配置。在 `train.swanlab` 里配置:
|
||||
|
||||
```yaml
|
||||
train:
|
||||
swanlab:
|
||||
enabled: true
|
||||
project: film-defect
|
||||
name: null # 缺省用 work_dir 目录名
|
||||
mode: online # online(云端) | local(本地看板) | offline(仅存盘) | disabled
|
||||
tags: [tiny_conv, center-head]
|
||||
```
|
||||
|
||||
```bash
|
||||
swanlab login # 首次使用云端模式需登录
|
||||
uv run python train.py --config configs/train/baseline.yaml --set train.swanlab.mode=offline
|
||||
uv run swanlab watch runs/baseline/swanlog # 本地看板
|
||||
```
|
||||
|
||||
未安装 swanlab、未登录或网络不通时会打一条警告并继续训练,不会中断。关掉用
|
||||
`--set train.swanlab.enabled=false`。
|
||||
|
||||
## 推理性能与显存分析
|
||||
|
||||
部署前用 `torch.profiler` 看单帧耗时与显存占用:
|
||||
|
||||
```bash
|
||||
uv run python scripts/profile_inference.py --config configs/train/baseline.yaml \
|
||||
--checkpoint runs/baseline/best.pth --batch-size 1 4 8 --postprocess
|
||||
|
||||
# 导出 chrome trace,用 chrome://tracing 或 ui.perfetto.dev 打开
|
||||
uv run python scripts/profile_inference.py --config configs/train/baseline.yaml \
|
||||
--checkpoint runs/baseline/best.pth --trace runs/baseline/trace.json --out runs/baseline/profile.json
|
||||
```
|
||||
|
||||
输出包含:每个 batch 的单帧延迟 / FPS、CUDA 显存峰值(`max_memory_allocated` 与
|
||||
`max_memory_reserved`)、权重占用,以及按显存和耗时各排一次序的算子级明细。
|
||||
`--amp` 用 fp16 推理对比,`--postprocess` 把 heatmap 解码也计入。没有 GPU 时自动退回 CPU
|
||||
(只有耗时与 CPU 内存)。
|
||||
|
||||
## 未分类样本的无监督聚类
|
||||
|
||||
`data/film` 里大量样本还没人工分完。这条流水线把目录下**所有图片拉平**(不看任何
|
||||
目录名),做无监督聚类,产出可人工核对的分组:
|
||||
|
||||
```bash
|
||||
# ImageNet 预训练卷积特征,不训练,CPU 上 14.5k 张约 100 秒
|
||||
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)
|
||||
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --set method=simclr
|
||||
```
|
||||
|
||||
簇数由配置的 `num_clusters` 决定,也可以 `--num-clusters 6` 临时覆盖(实测 4 类的换种子稳定性最高,见文档 §10.4)。产出在 `work_dir`:
|
||||
`assignments.csv`(每张图的簇号与到质心的距离)、`summary.json`、
|
||||
`clusters/cluster_XX/`(按簇建的软链目录,直接用看图软件翻)、
|
||||
`cluster_XX_preview.png`(每簇最靠近质心的代表样本拼图)。
|
||||
|
||||
翻图时注意 `clusters/` 里文件名前缀是排序号:`0000_` 最靠近质心(最典型),
|
||||
末尾 20 张是离质心最远的离群样本——最能暴露这个簇是不是混了东西。
|
||||
|
||||
**尺寸不统一的处理**:这批图是已裁好的缺陷小图,中位 152×113,宽高比从 0.24 到 39.9。
|
||||
尺寸和宽高比是上游 ROI 裁剪的产物、不是缺陷属性,所以预处理的目标是把它抹掉:
|
||||
直接拉伸到 128×128(不用 letterbox——填充比例等价于把宽高比明着喂给网络),
|
||||
再做逐图 z-score 抹掉整体亮度/对比度。
|
||||
|
||||
无标签时用两个指标判断质量:**轮廓系数**(簇是否紧致)和**换随机种子的稳定性**
|
||||
(同一份数据换初始化后划分是否还一样,低说明是在硬切一团连续数据)。
|
||||
|
||||
> 完整的数学推导、算法流程图与全部消融数据见
|
||||
> **[docs/unsupervised-clustering.md](docs/unsupervised-clustering.md)**。
|
||||
|
||||
### 自监督预训练(SimCLR)
|
||||
|
||||
`method: pretrained`(默认)用 ImageNet 权重直接抽特征,不训练;`method: simclr` 则在
|
||||
**本批薄膜图上自监督训练**一个项目自己的纯卷积骨架,域适配更好。同一张图的两个随机增强
|
||||
视图互为正样本,batch 内其余 2B−2 个视图为负样本,用 NT-Xent 损失训练。
|
||||
|
||||
**这属于训练,因此必须有 Nvidia GPU**(走 `utils.device.require_cuda()` 守卫,
|
||||
在无卡机器上会直接报错退出):
|
||||
|
||||
```bash
|
||||
# 1. 自监督训练 + 聚类一步跑完(首次运行)
|
||||
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --set method=simclr
|
||||
|
||||
# 2. 只想改簇数、复用已经训好的骨架:把权重路径填进 simclr.checkpoint
|
||||
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --set method=simclr simclr.checkpoint=runs/cluster/simclr_backbone.pth --num-clusters 6
|
||||
|
||||
# 3. 调训练超参
|
||||
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`(**只含骨架,不含投影头**——投影头被训练成
|
||||
对增强不变,会丢掉下游有用的信息,所以聚类和保存都只用骨架特征)。相关配置:
|
||||
|
||||
```yaml
|
||||
method: simclr
|
||||
|
||||
simclr:
|
||||
# 自监督用项目自己的纯卷积骨架(与 method=pretrained 的顶层 `backbone` 各自独立,
|
||||
# 两者字段完全不同,分开配置以免 _base_ 合并时串味)
|
||||
backbone: {name: tiny_conv, in_channels: 3, widths: [32, 64, 128, 256], depths: [1, 2, 2, 2]}
|
||||
epochs: 100
|
||||
batch_size: 256
|
||||
temperature: 0.2 # 越小越关注难负样本
|
||||
amp: true
|
||||
checkpoint: null # 填已有权重可跳过重复训练
|
||||
projection: {hidden_channels: 256, out_channels: 128}
|
||||
optimizer: {name: adamw, lr: 0.001, weight_decay: 0.0001}
|
||||
scheduler: {name: cosine}
|
||||
|
||||
view_transforms: # 生成两个视图的随机增强
|
||||
- {name: RandomResizedCrop, size: [128, 128], scale: [0.5, 1.0]}
|
||||
- {name: RandomHorizontalFlip, p: 0.5}
|
||||
- {name: RandomVerticalFlip, p: 0.5}
|
||||
- {name: GaussianBlur, p: 0.3}
|
||||
```
|
||||
|
||||
> `RandomResizedCrop` 的 `scale` 下限别低于 0.4:裁太狠会让两个视图都只剩背景,
|
||||
> "背景 vs 背景"被当成正样本对,学出来的特征会退化。
|
||||
|
||||
训练出的骨架和检测模型用的是同一套卷积骨架,可以直接拿去初始化 `DefectNet` 的 backbone——
|
||||
标注量有限时,通常比 ImageNet 权重更贴合薄膜的成像特性。
|
||||
|
||||
## 方法
|
||||
|
||||
- **骨架**:`tiny_conv`(自研深度可分离卷积,默认)或 `torchvision`(resnet / mobilenet / shufflenet)。
|
||||
- **neck**:nearest 上采样 + 3×3 卷积,把特征恢复到 stride 4,兼顾小缺陷定位与速度。
|
||||
- **定位头**:anchor-free 中心点(CenterNet 风格)——`heatmap` + `wh` + `offset`,
|
||||
推理端用 3×3 max-pool 做峰值抑制,无 NMS 循环,便于导出到 TensorRT / OpenVINO。
|
||||
- **分类头**:全局池化 + 全连接,输出**多热**标签(一张图可同时含多种缺陷,全 0 表示 OK 片)。
|
||||
- **指标**:定位 precision / recall / F1 / mean IoU(IoU 阈值匹配),分类 micro-F1 与整图完全正确率。
|
||||
`train.monitor` 决定用哪个指标挑选 `best.pth`。
|
||||
|
||||
配置支持 `_base_` 继承与命令行 `--set a.b.c=value` 覆盖,见 [utils/config.py](utils/config.py)。
|
||||
新增骨架 / 数据集只需用注册表装饰器注册,再在 yaml 里按 `name` 引用。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
uv run pytest # 单元测试
|
||||
uv run ruff check . # 静态检查
|
||||
```
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# data
|
||||
|
||||
存放数据集本体或指向数据集的软链接;除本文件外的内容都不入库(见 .gitignore)。
|
||||
|
||||
标注采用 **YOLO 风格**:`images/` 下每张图在同层 `labels/` 里有一个同名 `.txt`。
|
||||
|
||||
```
|
||||
data/
|
||||
└── film/
|
||||
├── images/ # 原图(或指向 NAS 的软链:ln -s /mnt/nas/film/images data/film/images)
|
||||
│ ├── train/0001.png
|
||||
│ └── val/0002.png
|
||||
├── labels/ # 与 images 目录结构一一对应
|
||||
│ ├── train/0001.txt
|
||||
│ └── val/0002.txt
|
||||
└── data.yaml # path / train / val / nc / names
|
||||
```
|
||||
|
||||
标注文件每行一个目标,坐标**相对图像宽高归一化**到 [0, 1],中心点格式:
|
||||
|
||||
```
|
||||
<class_id> <cx> <cy> <w> <h>
|
||||
0 0.312500 0.204688 0.087500 0.065625
|
||||
1 0.640625 0.712500 0.120000 0.093750
|
||||
```
|
||||
|
||||
- `class_id` 是 `data.yaml` / 配置里 `classes` 的下标,顺序即含义,改动必须重新训练。
|
||||
- **空文件或缺失 `.txt` = 无缺陷样本(OK 片)**,会作为纯负样本参与训练。
|
||||
- 也接受 YOLO 分割格式的多边形行(`class_id x1 y1 x2 y2 ...`),读取时取外接矩形。
|
||||
|
||||
`data.yaml` 与 ultralytics 通用:
|
||||
|
||||
```yaml
|
||||
path: data/film
|
||||
train: images/train
|
||||
val: images/val
|
||||
nc: 5
|
||||
names: {0: scratch, 1: bubble, 2: particle, 3: crease, 4: stain}
|
||||
```
|
||||
|
||||
配置里的 `classes` 既可以直接写类别列表,也可以写成 `data/film/data.yaml` 由框架读取 `names`。
|
||||
|
||||
## 还没分类的样本
|
||||
|
||||
`data/film` 下按人工整理留下的目录(`grease`、`wrinkle`、`反面纵黑点` ……)只是历史产物,
|
||||
不是最终类别。要重新梳理类别时用无监督聚类,它会忽略这些目录名、把所有图拉平处理:
|
||||
|
||||
```bash
|
||||
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml
|
||||
```
|
||||
|
||||
结果里的 `clusters/cluster_XX/` 是软链目录,人工确认后再决定每个簇对应什么缺陷、
|
||||
以及是否要合并/拆分。
|
||||
|
||||
## 从其它格式转过来
|
||||
|
||||
```bash
|
||||
# labelme / VOC -> YOLO txt(顺带生成 data.yaml)
|
||||
uv run python scripts/convert_annotations.py --format labelme \
|
||||
--input data/film/labelme --images data/film/images \
|
||||
--output data/film/labels --data-yaml data/film/data.yaml
|
||||
|
||||
# 划分 train / val(默认建软链,不复制数据)
|
||||
uv run python scripts/split_dataset.py --images data/film/images \
|
||||
--labels data/film/labels --val-ratio 0.2 --data-yaml data/film/data.yaml
|
||||
```
|
||||
@@ -0,0 +1,776 @@
|
||||
# 未分类样本的无监督聚类:算法与数学说明
|
||||
|
||||
本文档说明 `scripts/cluster_unlabeled.py` 背后的完整算法:每一步在做什么数学变换、
|
||||
为什么这样选、以及在 `data/film` 上的实测依据。
|
||||
|
||||
代码入口:[`scripts/cluster_unlabeled.py`](../scripts/cluster_unlabeled.py)、
|
||||
[`engine/cluster.py`](../engine/cluster.py)、[`engine/simclr.py`](../engine/simclr.py)、
|
||||
[`datasets/unlabeled.py`](../datasets/unlabeled.py)。配置:[`configs/cluster/film.yaml`](../configs/cluster/film.yaml)。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [问题设定与记号](#1-问题设定与记号)
|
||||
2. [流水线总览](#2-流水线总览)
|
||||
3. [阶段一:预处理——抹掉采集因子](#3-阶段一预处理抹掉采集因子)
|
||||
4. [阶段二:特征提取——为什么用中间层](#4-阶段二特征提取为什么用中间层)
|
||||
5. [阶段三:归一化与 PCA 降维](#5-阶段三归一化与-pca-降维)
|
||||
6. [阶段四:K-Means](#6-阶段四k-means)
|
||||
7. [阶段五:无标签评估](#7-阶段五无标签评估)
|
||||
8. [阶段六:SimCLR 自监督(可选)](#8-阶段六simclr-自监督可选)
|
||||
9. [复杂度与实测耗时](#9-复杂度与实测耗时)
|
||||
10. [消融实验数据](#10-消融实验数据)
|
||||
11. [已知局限与改进方向](#11-已知局限与改进方向)
|
||||
12. [公式与代码对照表](#12-公式与代码对照表)
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题设定与记号
|
||||
|
||||
给定一组**无标注**图像 $\mathcal{I} = \{I_1, \dots, I_N\}$(`data/film` 下递归扫描到
|
||||
$N = 14{,}579$ 张 PNG),在没有任何类别标签的前提下,把它们划分成 $k$ 个互不相交的簇
|
||||
|
||||
$$
|
||||
\mathcal{C} = \{C_1, \dots, C_k\}, \qquad
|
||||
\bigcup_{j=1}^{k} C_j = \{1,\dots,N\}, \qquad C_j \cap C_{j'} = \varnothing \ (j \neq j'),
|
||||
$$
|
||||
|
||||
使得同簇样本在"缺陷形态"这一语义上尽量相似。$k$ 由配置 `num_clusters` 给定(实测 $k=4$ 的稳定性最高,见 [§10.4](#104-簇数扫描);命令行可用 `--num-clusters` 或 `--sweep-k 2-8` 覆盖与扫描)。
|
||||
|
||||
| 记号 | 含义 |
|
||||
| --- | --- |
|
||||
| $I_i$ | 第 $i$ 张原图,尺寸 $H_i \times W_i$ 各不相同 |
|
||||
| $x_i \in \mathbb{R}^{3\times128\times128}$ | 预处理后的张量 |
|
||||
| $f_\theta$ | 卷积骨架(特征提取器) |
|
||||
| $z_i \in \mathbb{R}^{384}$ | 骨架输出的 embedding |
|
||||
| $y_i \in \mathbb{R}^{32}$ | PCA 降维后的向量 |
|
||||
| $\mu_j \in \mathbb{R}^{32}$ | 第 $j$ 个簇的质心 |
|
||||
| $c(i) \in \{1,\dots,k\}$ | 样本 $i$ 的簇标签 |
|
||||
| $\tau$ | SimCLR 的温度系数 |
|
||||
|
||||
**这批数据的关键统计量**(400 张随机抽样测得):
|
||||
|
||||
| 量 | 值 |
|
||||
| --- | --- |
|
||||
| 样本数 | 14,579 |
|
||||
| 尺寸中位数 | $152 \times 113$ |
|
||||
| 尺寸范围 | $84\times83$ ~ $4464\times1139$ |
|
||||
| 抽样内不同尺寸种类 | 326 / 400 |
|
||||
| 宽高比范围 | 0.24 ~ 39.86(中位 1.36) |
|
||||
| 通道模式 | 混合 RGB / RGBA |
|
||||
|
||||
即:**样本是已裁好的缺陷小图(ROI crop),不是整幅薄膜图**,且尺寸几乎两两不同。
|
||||
|
||||
---
|
||||
|
||||
## 2. 流水线总览
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef input fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef prep fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef feat fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef reduce fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef cluster fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
|
||||
scan["data/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input
|
||||
|
||||
subgraph PRE["① 预处理 — 抹掉采集因子"]
|
||||
direction LR
|
||||
resize["双线性拉伸<br/>H×W → 128×128<br/>不保长宽比"]:::prep
|
||||
tensor["转张量 + RGBA/L → RGB<br/>x ∈ ℝ³ˣ¹²⁸ˣ¹²⁸"]:::prep
|
||||
zscore["逐图标准化<br/>x̃ = (x − μₓ) / σₓ<br/>抹掉亮度 μ 与对比度 σ"]:::prep
|
||||
resize --> tensor --> zscore
|
||||
end
|
||||
|
||||
subgraph FEAT["② 特征提取 — 纹理优先"]
|
||||
direction LR
|
||||
stem["resnet18 stem<br/>conv1 → bn → relu → maxpool"]:::feat
|
||||
l1["layer1<br/>64ch, stride 4"]:::feat
|
||||
l2["layer2<br/>128ch, stride 8"]:::feat
|
||||
l3["layer3<br/>256ch, stride 16"]:::feat
|
||||
gap2["GAP → ℓ₂ 归一化<br/>u⁽²⁾ ∈ S¹²⁷"]:::feat
|
||||
gap3["GAP → ℓ₂ 归一化<br/>u⁽³⁾ ∈ S²⁵⁵"]:::feat
|
||||
concat["拼接<br/>z = [u⁽²⁾; u⁽³⁾] ∈ ℝ³⁸⁴"]:::feat
|
||||
stem --> l1 --> l2 --> l3
|
||||
l2 --> gap2 --> concat
|
||||
l3 --> gap3 --> concat
|
||||
end
|
||||
|
||||
subgraph RED["③ 降维"]
|
||||
direction LR
|
||||
norm["ℓ₂ 归一化<br/>ẑ = z / ‖z‖₂"]:::reduce
|
||||
pca["PCA 投影 384 → 32<br/>y = Uᵀ(ẑ − z̄)<br/>不白化:保留 λ 谱"]:::reduce
|
||||
norm --> pca
|
||||
end
|
||||
|
||||
subgraph CLU["④ 聚类 K-Means"]
|
||||
direction LR
|
||||
init["k-means++ 初始化<br/>p(xᵢ) ∝ D(xᵢ)²"]:::cluster
|
||||
lloyd["Lloyd 迭代<br/>指派 ↔ 更新质心"]:::cluster
|
||||
best["n_init = 10<br/>取 inertia 最小的一次"]:::cluster
|
||||
assign["全量指派<br/>cᵢ = argminⱼ ‖yᵢ − μⱼ‖₂"]:::cluster
|
||||
init --> lloyd --> best --> assign
|
||||
end
|
||||
|
||||
subgraph EVAL["⑤ 无标签评估"]
|
||||
direction LR
|
||||
sil["轮廓系数 s̄<br/>簇是否紧致"]:::metric
|
||||
stab["换种子稳定性<br/>NMI(C_seed0, C_seed7)"]:::metric
|
||||
end
|
||||
|
||||
subgraph OUT["⑥ 产出"]
|
||||
direction LR
|
||||
csv["assignments.csv<br/>路径 / 簇号 / 距质心"]:::out
|
||||
links["clusters/cluster_XX/<br/>软链目录,按距质心排序"]:::out
|
||||
prev["cluster_XX_preview.png<br/>代表样本拼图"]:::out
|
||||
summ["summary.json<br/>簇大小 / 轮廓 / 稳定性"]:::out
|
||||
end
|
||||
|
||||
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>需 Nvidia GPU"]:::feat
|
||||
|
||||
scan --> PRE
|
||||
PRE -->|"x̃ᵢ"| FEAT
|
||||
scan -.->|"双视图增强"| simclr
|
||||
simclr -.->|"域内骨架权重"| FEAT
|
||||
FEAT -->|"z ∈ ℝᴺˣ³⁸⁴"| RED
|
||||
RED -->|"y ∈ ℝᴺˣ³²"| CLU
|
||||
CLU -->|"标签 c"| EVAL
|
||||
CLU -->|"标签 c + 距离 d"| OUT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 阶段一:预处理——抹掉采集因子
|
||||
|
||||
### 3.1 核心前提
|
||||
|
||||
样本的**尺寸、宽高比、整体亮度都是采集环节的产物,不是缺陷属性**:
|
||||
|
||||
- 尺寸与宽高比由上游 ROI 裁剪框决定;
|
||||
- 整体亮度与对比度由光照和相机增益决定。
|
||||
|
||||
因此预处理的目标不是"保留信息",而是**主动消除这些混淆变量**。若不消除,聚类会
|
||||
把它们当成最显著的方差方向——这在实测中确实发生了(见 [§10.3](#103-归一化方式))。
|
||||
|
||||
### 3.2 尺寸归一:为什么用拉伸而不是 letterbox
|
||||
|
||||
两种候选:
|
||||
|
||||
| 方案 | 定义 | 宽高比信息的去向 |
|
||||
| --- | --- | --- |
|
||||
| Letterbox | 等比缩放 $s=\min(128/W, 128/H)$,四周补灰边 | **保留**:灰边占比 $1 - s\cdot H/128$ 直接编码了宽高比 |
|
||||
| 拉伸 Resize | 各向异性缩放 $(H,W) \to (128,128)$ | **抹掉**:所有图变成同一画幅 |
|
||||
|
||||
Letterbox 的填充比例是宽高比的可逆函数,等于把这个采集噪声明着喂给网络——
|
||||
卷积网络识别一条恒定灰边是平凡的。既然宽高比不是缺陷属性,就选拉伸。
|
||||
|
||||
**代价**:拉伸让纹理各向异性形变,$40{:}1$ 的长条尤其明显。备选方案是在原分辨率上取
|
||||
固定大小的中心 patch(`CenterCrop`,不缩放不填充),实测轮廓 0.258、稳定性 0.735,
|
||||
略逊于拉伸的 0.256/0.989——主要是极端长条被裁掉了大部分内容。配置里两种都可切换。
|
||||
|
||||
### 3.3 逐图标准化:抹掉亮度与对比度
|
||||
|
||||
对每张图,在**全部像素与通道**上求统计量后做 z-score:
|
||||
|
||||
$$
|
||||
\mu_x = \frac{1}{3HW}\sum_{c,h,w} x_{c,h,w}, \qquad
|
||||
\sigma_x = \sqrt{\frac{1}{3HW}\sum_{c,h,w}(x_{c,h,w} - \mu_x)^2}, \qquad
|
||||
\tilde{x} = \frac{x - \mu_x}{\sigma_x + \varepsilon}.
|
||||
$$
|
||||
|
||||
**这个变换对仿射光照变化严格不变。** 设 $x' = a x + b$($a>0$ 为增益,$b$ 为偏置),则
|
||||
$\mu_{x'} = a\mu_x + b$、$\sigma_{x'} = a\sigma_x$,于是
|
||||
|
||||
$$
|
||||
\tilde{x}' = \frac{(ax + b) - (a\mu_x + b)}{a\sigma_x} = \frac{a(x - \mu_x)}{a\sigma_x} = \tilde{x}.
|
||||
$$
|
||||
|
||||
即 $\tilde{x}$ 是仿射亮度群 $\{x \mapsto ax+b\}$ 作用下的**不变量**,光照与增益被整除掉了。
|
||||
|
||||
对比固定的 ImageNet `mean/std` 归一化:那只是全数据集共用的一个仿射变换,不同图之间
|
||||
的亮度差异原样保留。实测结果非常直接——用固定归一化时,4 个簇里有 3 个在区分
|
||||
"背景偏亮/偏暗",改成逐图标准化后簇才变成"条纹状/团块状/弱纹理"([§10.3](#103-归一化方式))。
|
||||
|
||||
> 实现见 [`datasets/transforms.py`](../datasets/transforms.py) 的 `PerImageStandardize`,
|
||||
> 单元测试 `test_per_image_standardize_removes_brightness_and_contrast` 直接验证了上面的不变性。
|
||||
|
||||
---
|
||||
|
||||
## 4. 阶段二:特征提取——为什么用中间层
|
||||
|
||||
### 4.1 多 stage 特征的构造
|
||||
|
||||
设骨架为 resnet18,其 `features` 是一个 `nn.Sequential`,下标 4–7 依次是
|
||||
`layer1`–`layer4`。取集合 $S = \{5, 6\}$(即 `layer2`、`layer3`),对每个 $s \in S$:
|
||||
|
||||
$$
|
||||
A^{(s)} = \text{layer}_s(\cdots) \in \mathbb{R}^{C_s \times H_s \times W_s},
|
||||
\qquad
|
||||
u^{(s)} = \frac{\text{GAP}(A^{(s)})}{\lVert \text{GAP}(A^{(s)}) \rVert_2},
|
||||
\qquad
|
||||
\text{GAP}(A)_c = \frac{1}{H_sW_s}\sum_{h,w} A_{c,h,w}.
|
||||
$$
|
||||
|
||||
最终 embedding 是各 stage 单位向量的拼接:
|
||||
|
||||
$$
|
||||
z = \big[\, u^{(2)} ;\; u^{(3)} \,\big] \in \mathbb{R}^{128 + 256} = \mathbb{R}^{384}.
|
||||
$$
|
||||
|
||||
**逐 stage 先归一化再拼接**很重要:不同 stage 的激活幅度差一个数量级,直接拼会让幅度大的
|
||||
stage 独占距离。各自归一化后两段等权,$\lVert z \rVert_2 = \sqrt{|S|}$ 恒定。
|
||||
|
||||
### 4.2 为什么中间层优于最后一层
|
||||
|
||||
$\text{GAP}(A^{(s)})$ 是特征图各通道的**空间一阶统计量**。对卷积网络:
|
||||
|
||||
- **浅层/中层**通道响应的是方向性边缘、条纹频率、颗粒尺度等**纹理基元**,其空间平均刻画
|
||||
"这张图由什么纹理构成";
|
||||
- **最后一层**(`layer4`)在 ImageNet 上被训练成对 1000 个**物体类别**线性可分,其语义
|
||||
是"猫/车/狗"。薄膜缺陷图里没有任何 ImageNet 物体,这个语义对本任务基本是噪声。
|
||||
|
||||
缺陷之间的差异恰恰是纹理层面的:颗粒感、条纹方向、边缘锐度。实测(2000 张随机样本,
|
||||
$k=4$)完全印证:
|
||||
|
||||
| 特征来源 | 维度 | 轮廓系数 | 换种子稳定性 |
|
||||
| --- | --- | --- | --- |
|
||||
| 只用 layer4(最后一层) | 512 | 0.191 | 0.410 |
|
||||
| **layer2 + layer3** | **384** | **0.256** | **0.989** |
|
||||
| layer1 + layer2 + layer3 | 448 | 0.262 | 0.771 |
|
||||
| layer2 + layer3 + layer4 | 896 | 0.220 | 0.627 |
|
||||
| layer1 … layer4 | 960 | 0.222 | 0.702 |
|
||||
|
||||
加回 `layer4` 反而把稳定性从 0.989 拉低到 0.627——它引入的方差与任务无关。
|
||||
`layer1` 太低级(近乎原始梯度),也不稳。
|
||||
|
||||
---
|
||||
|
||||
## 5. 阶段三:归一化与 PCA 降维
|
||||
|
||||
### 5.1 ℓ₂ 归一化:欧氏距离等价于余弦距离
|
||||
|
||||
$\hat{z} = z / \lVert z \rVert_2$ 后,两点欧氏距离与余弦相似度一一对应:
|
||||
|
||||
$$
|
||||
\lVert \hat{z}_a - \hat{z}_b \rVert_2^2
|
||||
= \lVert\hat{z}_a\rVert^2 + \lVert\hat{z}_b\rVert^2 - 2\hat{z}_a^\top \hat{z}_b
|
||||
= 2\big(1 - \cos(z_a, z_b)\big).
|
||||
$$
|
||||
|
||||
因此后续用欧氏度量的 K-Means 实际上在做**球面上的余弦聚类**,只比较特征的"方向"
|
||||
(即激活模式的构成比例),忽略"强度"。
|
||||
|
||||
### 5.2 PCA 投影
|
||||
|
||||
对中心化矩阵 $\bar{Z} = \hat{Z} - \mathbf{1}\bar{z}^\top \in \mathbb{R}^{N \times 384}$,
|
||||
协方差 $\Sigma = \frac{1}{N-1}\bar{Z}^\top\bar{Z}$,特征分解 $\Sigma = U\Lambda U^\top$,
|
||||
其中 $\Lambda = \mathrm{diag}(\lambda_1 \ge \cdots \ge \lambda_{384})$。取前 $q=32$ 列:
|
||||
|
||||
$$
|
||||
y_i = U_{:,1:q}^\top (\hat{z}_i - \bar{z}) \in \mathbb{R}^{32}.
|
||||
$$
|
||||
|
||||
实现用 `torch.pca_lowrank`(随机化 SVD,Halko et al. 2011),复杂度 $O(Ndq)$ 而非
|
||||
完整 SVD 的 $O(Nd^2)$;对 $N{=}14579, d{=}384, q{=}32$ 约 $1.8\times10^8$ 次乘加,毫秒级。
|
||||
|
||||
### 5.3 白化为什么必须关掉
|
||||
|
||||
白化在投影后再除以各方向标准差:
|
||||
|
||||
$$
|
||||
y^{\text{white}}_i = \Lambda_{1:q}^{-1/2} U_{:,1:q}^\top (\hat{z}_i - \bar{z}),
|
||||
\qquad \text{使得} \quad \mathrm{Var}(y^{\text{white}}_{\cdot,j}) = 1 \ \ \forall j .
|
||||
$$
|
||||
|
||||
等价地,白化后的欧氏距离就是原空间的**马氏距离**:
|
||||
|
||||
$$
|
||||
\lVert y^{\text{white}}_a - y^{\text{white}}_b \rVert_2^2
|
||||
= (\hat{z}_a - \hat{z}_b)^\top \Sigma^{-1} (\hat{z}_a - \hat{z}_b)
|
||||
= \sum_{j=1}^{q} \frac{\big(u_j^\top(\hat{z}_a - \hat{z}_b)\big)^2}{\lambda_j}.
|
||||
$$
|
||||
|
||||
第 $j$ 项被 $1/\lambda_j$ 放大。真实特征的谱极其陡峭:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 1250
|
||||
height: 520
|
||||
titlePadding: 16
|
||||
themeVariables:
|
||||
xyChart:
|
||||
titleColor: "#1F2937"
|
||||
plotColorPalette: "#2563EB, #EA580C"
|
||||
---
|
||||
xychart
|
||||
title "主成分方差谱:白化会把末尾的噪声方向抬到与信号同权"
|
||||
x-axis "主成分序号 j (PCA 降到 32 维)" [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]
|
||||
y-axis "方差占比 (%)" 0 --> 40
|
||||
bar [38.2, 13.0, 10.8, 6.7, 4.5, 3.6, 3.4, 2.3, 2.0, 1.6, 1.5, 1.3, 1.2, 1.0, 0.9, 0.8, 0.7, 0.7, 0.7, 0.7, 0.5, 0.5, 0.5, 0.4, 0.4, 0.4, 0.3, 0.3, 0.3, 0.3, 0.3, 0.2]
|
||||
line [3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1]
|
||||
```
|
||||
|
||||
- 前 4 个主成分占总方差的 **68.7%**,前 8 个占 82.6%;
|
||||
- $\lambda_1 / \lambda_{32} \approx \mathbf{165\times}$。
|
||||
|
||||
白化把所有方向拉平到 $1/32 = 3.1\%$:**PC1 被压掉约 12 倍,PC32 被放大约 14 倍**。
|
||||
结构只存在于前几个方向,白化后距离却由后面 28 个近乎噪声的方向主导。
|
||||
|
||||
在受控的合成数据上(3 个信号维 + 200 个噪声维,4 个分离良好的高斯团):
|
||||
|
||||
| $q$ | 不白化 轮廓 | 白化 轮廓 | 白化后最大簇占比 |
|
||||
| --- | --- | --- | --- |
|
||||
| 16 | 0.923 | 0.078 | 58% |
|
||||
| 32 | 0.922 | 0.050 | 59% |
|
||||
| 64 | 0.922 | 0.047 | 82% |
|
||||
|
||||
不白化时结果对 $q$ 几乎不敏感(都是 0.92),白化则全线崩溃。在真实数据上,
|
||||
早期开白化的版本最大簇吃掉 **96%** 的样本、稳定性仅 0.01;关掉后最大簇降到 38%。
|
||||
|
||||
> 这条结论被回归测试 `test_whitening_amplifies_noise_directions` 锁死。
|
||||
|
||||
---
|
||||
|
||||
## 6. 阶段四:K-Means
|
||||
|
||||
### 6.1 目标函数
|
||||
|
||||
$$
|
||||
\min_{\mathcal{C},\, \{\mu_j\}} \ J(\mathcal{C}, \mu) = \sum_{j=1}^{k}\sum_{i \in C_j} \lVert y_i - \mu_j \rVert_2^2 .
|
||||
$$
|
||||
|
||||
这是 NP-hard 问题(即使 $k=2$ 或 $d=2$),实践中只求局部最优。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef start fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef step fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef decide fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
classDef guard fill:#FFE4E6,stroke:#BE123C,stroke-width:2px,color:#881337
|
||||
classDef done fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
|
||||
input["输入 Y = { yᵢ } ⊂ ℝ³², i = 1…N (可先随机抽子集拟合:fit_max_samples)<br/>目标 min_{C,μ} J(C,μ) = Σᵢ ‖yᵢ − μ_{c(i)}‖² — NP-hard,只求局部最优"]:::start
|
||||
|
||||
subgraph INIT["① k-means++ 初始化 (期望近似比 E[J] ≤ 8(ln k + 2)·J_opt)"]
|
||||
direction LR
|
||||
pick1["均匀随机取 μ₁"]:::step
|
||||
dist["D(y) = min_{j≤t} ‖y − μⱼ‖"]:::step
|
||||
sample["按 p(y) ∝ D(y)² 采样 μ_{t+1}<br/>离已有质心越远越易被选中"]:::step
|
||||
kdone{"够 k 个?"}:::decide
|
||||
pick1 --> dist --> sample --> kdone
|
||||
kdone -.->|"否,继续挑"| dist
|
||||
end
|
||||
|
||||
subgraph LLOYD["② Lloyd 迭代 (J 单调不增且有下界,故必收敛)"]
|
||||
direction LR
|
||||
assign["指派步<br/>c(i) = argminⱼ ‖yᵢ − μⱼ‖²<br/>一次 cdist 算完 N×k 距离"]:::step
|
||||
update["更新步<br/>μⱼ ← mean{ yᵢ : c(i) = j }<br/>均值是簇内平方距离的唯一最小点"]:::step
|
||||
empty{"有空簇?"}:::decide
|
||||
steal["把离自身质心最远的点<br/>提为该空簇的新质心<br/>保证簇数不缩水"]:::guard
|
||||
conv{"|J_prev − J| ≤ tol·J_prev<br/>或迭代满 100 次?"}:::decide
|
||||
assign --> update --> empty
|
||||
empty -->|"否"| conv
|
||||
empty -->|"是"| steal --> conv
|
||||
conv -.->|"否,回到指派步"| assign
|
||||
end
|
||||
|
||||
repeat["③ 以上 ①② 整体重复 n_init = 10 次<br/>每次换随机种子 seed + r"]:::step
|
||||
best["④ 取 10 次里 J 最小的一次 (C*, μ*)<br/>缓解 Lloyd 只保证局部最优"]:::done
|
||||
final["⑤ 全量指派 cᵢ = argminⱼ ‖yᵢ − μ*ⱼ‖,i = 1…N<br/>并记录到质心的距离 dᵢ,供排序与离群检查"]:::done
|
||||
|
||||
input --> INIT
|
||||
kdone ==>|"是,得到初值 μ⁽⁰⁾"| assign
|
||||
conv ==>|"是,本次收敛"| repeat
|
||||
repeat --> best --> final
|
||||
```
|
||||
|
||||
### 6.2 Lloyd 迭代与收敛性
|
||||
|
||||
两步交替,每步都在**各自变量上精确最小化** $J$:
|
||||
|
||||
**指派步**(固定 $\mu$,优化 $\mathcal{C}$):
|
||||
$$c(i) = \arg\min_j \lVert y_i - \mu_j \rVert_2^2 .$$
|
||||
|
||||
**更新步**(固定 $\mathcal{C}$,优化 $\mu$):对 $\mu_j$ 求梯度并置零,
|
||||
|
||||
$$
|
||||
\frac{\partial}{\partial \mu_j}\sum_{i \in C_j}\lVert y_i - \mu_j\rVert^2 = -2\sum_{i\in C_j}(y_i - \mu_j) = 0
|
||||
\quad\Longrightarrow\quad
|
||||
\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 次迭代。
|
||||
|
||||
> **注意**:收敛保证的是局部最优,不是全局最优——这正是需要 `n_init` 的原因。
|
||||
|
||||
### 6.3 k-means++ 初始化
|
||||
|
||||
均匀随机取初值时,多个初始质心可能落在同一团里,导致明显的坏解。k-means++ 按到已选
|
||||
质心的平方距离采样:
|
||||
|
||||
$$
|
||||
\Pr(\text{选中 } y) = \frac{D(y)^2}{\sum_{y'} D(y')^2},
|
||||
\qquad D(y) = \min_{j \le t} \lVert y - \mu_j \rVert_2 .
|
||||
$$
|
||||
|
||||
Arthur & Vassilvitskii (2007) 证明它给出 $O(\log k)$ 的**期望近似比**:
|
||||
|
||||
$$
|
||||
\mathbb{E}[J_{\text{k-means++}}] \le 8(\ln k + 2)\, J_{\text{opt}} .
|
||||
$$
|
||||
|
||||
这是初始化就能拿到的保证,Lloyd 迭代只会让 $J$ 进一步下降。
|
||||
|
||||
### 6.4 空簇处理
|
||||
|
||||
更新步中若某簇没有任何成员,$\mu_j$ 无定义。实现的处理是:**把当前离自身质心最远的点
|
||||
提为该空簇的新质心**。这保证输出恰好 $k$ 个非空簇(否则实际簇数会悄悄缩水),且由于选的是
|
||||
$J$ 贡献最大的点,这一步通常还会降低 $J$。
|
||||
|
||||
### 6.5 多次重启
|
||||
|
||||
跑 `n_init = 10` 次完整的(初始化 + Lloyd),取 $J$ 最小的一次。设单次落入某个坏局部最优的
|
||||
概率为 $p$,则 10 次全坏的概率降到 $p^{10}$。
|
||||
|
||||
### 6.6 子集拟合 + 全量指派
|
||||
|
||||
可选地只用随机子集 $\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 本来就只要几秒。
|
||||
|
||||
---
|
||||
|
||||
## 7. 阶段五:无标签评估
|
||||
|
||||
没有真值标签时无法算准确率,用两个内部指标。
|
||||
|
||||
### 7.1 轮廓系数(簇是否紧致)
|
||||
|
||||
对样本 $i$,设 $a(i)$ 为它到**同簇**其他点的平均距离,$b(i)$ 为它到**最近的其他簇**的平均距离:
|
||||
|
||||
$$
|
||||
a(i) = \frac{1}{|C_{c(i)}|-1}\sum_{\substack{i' \in C_{c(i)} \\ i' \neq i}} \lVert y_i - y_{i'}\rVert,
|
||||
\qquad
|
||||
b(i) = \min_{j \neq c(i)} \frac{1}{|C_j|}\sum_{i' \in C_j} \lVert y_i - y_{i'}\rVert,
|
||||
$$
|
||||
|
||||
$$
|
||||
s(i) = \frac{b(i) - a(i)}{\max\{a(i),\, b(i)\}} \in [-1, 1],
|
||||
\qquad
|
||||
\bar{s} = \frac{1}{N}\sum_i s(i).
|
||||
$$
|
||||
|
||||
$s(i) \to 1$ 表示样本牢牢待在自己簇里,$s(i) < 0$ 表示它离别的簇更近(八成分错了)。
|
||||
计算是 $O(N^2 d)$,故超过 `max_samples = 2000` 时随机抽样估计。
|
||||
|
||||
### 7.2 换种子稳定性(划分是否可复现)
|
||||
|
||||
同一份特征,用两个不同随机种子各跑一次完整聚类,得到划分 $\mathcal{A}$ 与 $\mathcal{B}$,
|
||||
计算归一化互信息:
|
||||
|
||||
$$
|
||||
\mathrm{NMI}(\mathcal{A}, \mathcal{B}) = \frac{I(\mathcal{A};\mathcal{B})}{\sqrt{H(\mathcal{A})\,H(\mathcal{B})}},
|
||||
$$
|
||||
|
||||
$$
|
||||
I(\mathcal{A};\mathcal{B}) = \sum_{a}\sum_{b} p(a,b)\log\frac{p(a,b)}{p(a)p(b)},
|
||||
\qquad
|
||||
H(\mathcal{A}) = -\sum_a p(a)\log p(a),
|
||||
$$
|
||||
|
||||
其中 $p(a,b) = |A_a \cap B_b| / N$。NMI 对簇编号的置换不变(换标签名不影响),
|
||||
取值 $[0,1]$,1 表示两次划分完全一致。
|
||||
|
||||
**为什么这个指标关键**:数据里真有 $k$ 团结构时,不同初始化会收敛到同一套划分,NMI ≈ 1;
|
||||
若是在硬切一团连续的数据,边界位置随初值乱跳,NMI 会很低。它比轮廓系数更能识破
|
||||
"强行分 $k$ 类"的情形。实测中它一眼揪出了白化的问题(稳定性 0.01)。
|
||||
|
||||
> **不使用外部标签。** `data/film` 下的目录名(`grease`、`wrinkle`、`反面纵黑点`……)
|
||||
> 是人工整理的历史产物,既不完整也未必正确,因此既不参与聚类、也不用于评估。
|
||||
|
||||
---
|
||||
|
||||
## 8. 阶段六:SimCLR 自监督(可选)
|
||||
|
||||
`method: pretrained` 用的是 ImageNet 权重——域外的。`method: simclr` 则在**这批薄膜图
|
||||
本身**上学特征,域适配更好。它是训练,因此按项目约定必须有 Nvidia GPU
|
||||
(`utils.device.require_cuda()` 守卫)。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef data fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef aug fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef net fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef loss fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
classDef note fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
|
||||
batch["一个 batch:B 张无标注图 { Iᵢ }, i = 1…B"]:::data
|
||||
|
||||
aug["随机增强分布 𝒯:RandomResizedCrop 0.5~1.0 · 水平/垂直翻转 · 高斯模糊<br/>对每张图独立采样两次 t, t′ ~ 𝒯"]:::aug
|
||||
|
||||
v1["视图 1 x⁽¹⁾ᵢ = t(Iᵢ)"]:::aug
|
||||
v2["视图 2 x⁽²⁾ᵢ = t′(Iᵢ)"]:::aug
|
||||
|
||||
bb1["卷积骨架 f_θ"]:::net
|
||||
bb2["卷积骨架 f_θ (共享同一份参数)"]:::net
|
||||
|
||||
h1["h⁽¹⁾ᵢ = f_θ(x⁽¹⁾ᵢ) ∈ ℝᴰ"]:::net
|
||||
h2["h⁽²⁾ᵢ = f_θ(x⁽²⁾ᵢ) ∈ ℝᴰ"]:::net
|
||||
|
||||
g1["投影头 g_φ<br/>Linear → BN → ReLU → Linear"]:::net
|
||||
g2["投影头 g_φ (共享参数)"]:::net
|
||||
|
||||
z1["z⁽¹⁾ᵢ = g_φ(h⁽¹⁾ᵢ) ∈ ℝ¹²⁸"]:::net
|
||||
z2["z⁽²⁾ᵢ = g_φ(h⁽²⁾ᵢ) ∈ ℝ¹²⁸"]:::net
|
||||
|
||||
sim["ℓ₂ 归一化后拼成 Z ∈ ℝ²ᴮˣ¹²⁸<br/>相似度矩阵 S = ZZᵀ / τ ∈ ℝ²ᴮˣ²ᴮ,τ = 0.2<br/>对角线置 −∞,排除自己和自己"]:::loss
|
||||
|
||||
nt["NT-Xent 损失<br/>ℒ = − (1/2B) Σₖ log [ exp(S_{k,π(k)}) / Σ_{m≠k} exp(S_{k,m}) ]<br/>π(k):同一张图的另一个视图"]:::loss
|
||||
|
||||
pos["分子 = 正样本对(1 个)<br/>同图另一视图 π(k)"]:::note
|
||||
neg["分母 = 全部 2B−2 个负样本<br/>batch 内其他所有视图"]:::note
|
||||
|
||||
step["反向传播同时更新 θ 与 φ<br/>AdamW + cosine,AMP"]:::loss
|
||||
|
||||
ckpt["训练收敛后只保存骨架<br/>simclr_backbone.pth<br/>投影头 g_φ 丢弃"]:::data
|
||||
|
||||
use["聚类阶段用 h = f_θ(x),不用 z<br/>投影头会滤掉对比任务用不上、<br/>但下游有用的信息"]:::note
|
||||
|
||||
batch --> aug
|
||||
aug --> v1
|
||||
aug --> v2
|
||||
v1 --> bb1 --> h1 --> g1 --> z1
|
||||
v2 --> bb2 --> h2 --> g2 --> z2
|
||||
z1 --> sim
|
||||
z2 --> sim
|
||||
pos -.-> nt
|
||||
neg -.-> nt
|
||||
sim --> nt --> step --> ckpt --> use
|
||||
```
|
||||
|
||||
### 8.1 正样本对的构造
|
||||
|
||||
从增强分布 $\mathcal{T}$ 中对每张图独立采样两次 $t, t' \sim \mathcal{T}$:
|
||||
|
||||
$$
|
||||
x_i^{(1)} = t(I_i), \qquad x_i^{(2)} = t'(I_i).
|
||||
$$
|
||||
|
||||
$\mathcal{T}$ 包含 `RandomResizedCrop(scale=0.5~1.0)`、水平/垂直翻转、高斯模糊。
|
||||
**监督信号完全来自"这两个视图来自同一张图"这一事实**,不需要任何人工标签。
|
||||
|
||||
> `scale` 下限不宜低于 0.4:裁得太狠容易两个视图都只剩背景,"背景 vs 背景"被当成正样本对,
|
||||
> 学出的特征会退化。另外主 transforms 里的 `PerImageStandardize` 在增强**之后**执行,
|
||||
> 因此 `ColorJitter` 的亮度扰动最终会被抵消,真正起作用的是裁剪/翻转/模糊。
|
||||
|
||||
### 8.2 NT-Xent 损失
|
||||
|
||||
把一个 batch 的 $2B$ 个视图编码、投影、$\ell_2$ 归一化后堆成 $Z \in \mathbb{R}^{2B \times 128}$,
|
||||
相似度矩阵
|
||||
|
||||
$$
|
||||
S = \frac{ZZ^\top}{\tau} \in \mathbb{R}^{2B\times2B},
|
||||
\qquad S_{kk} := -\infty \ \ (\text{排除自身}).
|
||||
$$
|
||||
|
||||
设 $\pi(k)$ 为 $k$ 的配对视图下标($\pi(i) = i+B$,$\pi(i+B) = i$),损失为
|
||||
|
||||
$$
|
||||
\boxed{\ \mathcal{L} = -\frac{1}{2B}\sum_{k=1}^{2B}
|
||||
\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)`。
|
||||
|
||||
**温度 $\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}.
|
||||
$$
|
||||
|
||||
这也解释了为什么 batch 越大效果越好——负样本越多,下界越紧。
|
||||
|
||||
### 8.3 为什么聚类用骨架特征而不是投影头输出
|
||||
|
||||
$g_\varphi$ 被显式训练成对增强 $\mathcal{T}$ **不变**。凡是被增强破坏掉的信息(模糊掉的
|
||||
高频细节、翻转掉的方向性),在 $z$ 里都会被压掉。而这些信息对下游任务往往有用。
|
||||
$h = f_\theta(x)$ 位于投影头之前,保留得更完整——这是 SimCLR 原论文的既有结论
|
||||
(线性评估上 $h$ 比 $z$ 高十几个点)。因此:**训练用 $z$ 算损失,聚类和保存权重都只用 $h$。**
|
||||
|
||||
### 8.4 与检测任务的衔接
|
||||
|
||||
`simclr_backbone.pth` 里的骨架就是项目检测模型用的同一个卷积骨架,可以直接拿去初始化
|
||||
`DefectNet` 的 backbone——在标注量有限时,用 14.5k 张无标注图预训练过的权重通常比
|
||||
ImageNet 权重更贴合薄膜的成像特性。
|
||||
|
||||
---
|
||||
|
||||
## 9. 复杂度与实测耗时
|
||||
|
||||
| 阶段 | 复杂度 | 实测(14,579 张,Apple Silicon CPU) |
|
||||
| --- | --- | --- |
|
||||
| 图像读取 + 预处理 | $O(N \cdot HW)$ | 与特征提取重叠(4 个 worker) |
|
||||
| 特征提取 | $O(N \cdot \mathrm{FLOPs}(f_\theta))$ | **~84 s**(占总耗时绝大部分) |
|
||||
| ℓ₂ 归一化 | $O(Nd)$ | < 0.1 s |
|
||||
| PCA(随机化 SVD) | $O(Ndq) \approx 1.8\times10^8$ | < 1 s |
|
||||
| K-Means | $O(n_{\text{init}} \cdot T \cdot Nkq)$ | ~2 s($T \approx 20$–30 次迭代收敛) |
|
||||
| 轮廓系数 | $O(M^2 q)$,$M = 2000$ | < 1 s |
|
||||
| 稳定性(再跑 2 次聚类) | 同 K-Means × 2 | ~4 s |
|
||||
| 产出(软链 + 拼图) | $O(N + k\cdot\text{preview})$ | ~2 s |
|
||||
| **合计** | | **~100 s** |
|
||||
|
||||
GPU 上特征提取会快一个数量级;瓶颈会转移到图像解码(可增大 `loader.num_workers`)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 消融实验数据
|
||||
|
||||
以下实验均在**从全库纯随机抽取的 2000 张**上进行(不按目录分层),$k=4$,PCA 32 维。
|
||||
|
||||
### 10.1 特征来源
|
||||
|
||||
| 方案 | 维度 | 轮廓系数 | 稳定性 | 最大簇占比 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 只用 layer4 | 512 | 0.191 | 0.410 | 0.37 |
|
||||
| **layer2 + layer3** | **384** | **0.256** | **0.989** | 0.39 |
|
||||
| layer1+2+3 | 448 | 0.262 | 0.771 | 0.39 |
|
||||
| layer2+3+4 | 896 | 0.220 | 0.627 | 0.37 |
|
||||
| layer1…4 | 960 | 0.222 | 0.702 | 0.38 |
|
||||
| layer2+3(灰度输入) | 384 | 0.272 | 0.963 | 0.37 |
|
||||
| layer2+3(CenterCrop) | 384 | 0.258 | 0.735 | 0.37 |
|
||||
|
||||
### 10.2 白化(合成数据:3 信号维 + 200 噪声维,4 个高斯团)
|
||||
|
||||
| $q$ | 不白化 | 白化 |
|
||||
| --- | --- | --- |
|
||||
| 16 | 0.923 | 0.078 |
|
||||
| 32 | 0.922 | 0.050 |
|
||||
| 64 | 0.922 | 0.047 |
|
||||
|
||||
### 10.3 归一化方式
|
||||
|
||||
| 归一化 | 轮廓系数 | 稳定性 | 簇的实际内容 |
|
||||
| --- | --- | --- | --- |
|
||||
| 固定 ImageNet mean/std | 0.256 | 0.966 | 4 个簇里 3 个在分"背景偏亮/偏暗" |
|
||||
| **逐图标准化** | **0.322** | **0.995** | 条纹状 / 团块状 / 弱纹理 |
|
||||
|
||||
### 10.4 簇数扫描
|
||||
|
||||
| $k$ | 轮廓系数 | 稳定性 | 最大簇占比 | 簇大小 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 2 | 0.183 | 0.770 | 0.71 | [1418, 582] |
|
||||
| 3 | 0.260 | 0.671 | 0.51 | [209, 776, 1015] |
|
||||
| **4** | 0.257 | **0.957** | 0.38 | [185, 769, 443, 603] |
|
||||
| 5 | 0.238 | 0.738 | 0.28 | [328, 385, 195, 522, 570] |
|
||||
| 6 | 0.228 | 0.665 | 0.28 | [242, 359, 161, 518, 556, 164] |
|
||||
| 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`。
|
||||
|
||||
### 10.5 全量最终结果
|
||||
|
||||
| 指标 | 值 |
|
||||
| --- | --- |
|
||||
| 样本数 | 14,579 |
|
||||
| 簇大小 | 4507 / 3795 / 4849 / 1428 |
|
||||
| 轮廓系数 | **0.3255** |
|
||||
| 换种子稳定性 | **0.9641** |
|
||||
| 各簇平均距质心 | 0.145 / 0.173 / 0.237 / 0.333 |
|
||||
| 耗时(CPU) | ~100 s |
|
||||
|
||||
人工核对代表样本后的簇语义:
|
||||
|
||||
| 簇 | 数量 | 目视内容 |
|
||||
| --- | --- | --- |
|
||||
| cluster_00 | 4507 | 近乎空白的弱纹理底图(带缓慢灰度梯度) |
|
||||
| cluster_01 | 3795 | 近乎空白的弱纹理底图(另一种梯度形态) |
|
||||
| cluster_02 | 4849 | **亮底上的团块 / 点状缺陷**(蚊虫状、颗粒) |
|
||||
| cluster_03 | 1428 | **暗底上的条纹 / 拉丝 / 褶皱** |
|
||||
|
||||
---
|
||||
|
||||
## 11. 已知局限与改进方向
|
||||
|
||||
1. **两个簇是"近乎空白"的。** 这批裁片里确实有很大比例在 128×128 尺度下看不出明显缺陷,
|
||||
$k=4$ 就会把其中两个名额花在它们的细微差异上。这是数据本身的性质,不是算法缺陷。
|
||||
对策:调大 $k$ 让缺陷簇进一步细分;或先用一个简单的对比度/能量阈值把"无明显缺陷"
|
||||
的样本筛掉,再对剩下的做聚类。
|
||||
|
||||
2. **ImageNet 特征终究是域外的。** `method: simclr` 在本批数据上自监督预训练,理论上更贴合,
|
||||
但需要 GPU 且要调 epoch / batch size。建议在 GPU 机器上跑一次并与 pretrained 对比
|
||||
轮廓系数与稳定性。
|
||||
|
||||
3. **拉伸带来的各向异性形变**在极端长条(40:1)上很严重。若发现某个簇专门收集长条,
|
||||
可换 `CenterCrop`(原分辨率取中心 patch),或改成多 patch 聚合(对每张图取若干 patch
|
||||
分别编码后取平均/最大)。后者对"缺陷是局部的"这一先验更契合,目前尚未实现。
|
||||
|
||||
4. **K-Means 假设簇是各向同性的球。** 若缺陷类别在特征空间里呈细长流形,K-Means 会切错。
|
||||
备选:谱聚类(构图 + 拉普拉斯特征向量)或 HDBSCAN(不需要预设 $k$,能标记噪声点)。
|
||||
代价是复杂度更高、超参更难调。
|
||||
|
||||
5. **轮廓系数 0.33 属于"弱但存在"的结构。** 不要指望聚类结果能直接当标签用;
|
||||
它的定位是**把人工核对的工作量从 14,579 张降到 4 个簇的抽查**。
|
||||
|
||||
---
|
||||
|
||||
## 12. 公式与代码对照表
|
||||
|
||||
| 公式 / 概念 | 实现位置 |
|
||||
| --- | --- |
|
||||
| 逐图标准化 $\tilde{x} = (x-\mu_x)/\sigma_x$ | `datasets/transforms.py::PerImageStandardize` |
|
||||
| 拉伸 / letterbox / 中心裁剪 | `datasets/transforms.py::Resize` / `Letterbox` / `CenterCrop` |
|
||||
| 递归扫描与统一通道 | `datasets/unlabeled.py::UnlabeledImageDataset` |
|
||||
| 多 stage GAP + ℓ₂ + 拼接 | `engine/cluster.py::StageFeatureExtractor` |
|
||||
| $\hat{z} = z/\lVert z\rVert_2$ | `engine/cluster.py::l2_normalize` |
|
||||
| PCA 投影与白化 | `engine/cluster.py::pca_reduce` |
|
||||
| k-means++ 初始化 | `engine/cluster.py::kmeans_plusplus` |
|
||||
| Lloyd 迭代 + 空簇处理 + n_init | `engine/cluster.py::kmeans` |
|
||||
| 轮廓系数 | `engine/cluster.py::silhouette_score` |
|
||||
| NMI | `engine/cluster.py::normalized_mutual_info` |
|
||||
| 换种子稳定性 | `engine/cluster.py::cluster_stability` |
|
||||
| 随机子集拟合 | `engine/cluster.py::random_fit_index` |
|
||||
| 端到端聚类 | `engine/cluster.py::cluster_features` |
|
||||
| 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` |
|
||||
| CLI 与产出 | `scripts/cluster_unlabeled.py` |
|
||||
|
||||
**相关测试**:`tests/test_cluster.py`(K-Means 正确性、白化回归、稳定性、NMI)、
|
||||
`tests/test_unlabeled_dataset.py`(扫描、混合尺寸/通道、逐图标准化的不变性)。
|
||||
|
||||
---
|
||||
|
||||
## 参考文献
|
||||
|
||||
1. Arthur, D., Vassilvitskii, S. *k-means++: The Advantages of Careful Seeding.* SODA 2007.
|
||||
2. Lloyd, S. *Least Squares Quantization in PCM.* IEEE Trans. Information Theory, 1982.
|
||||
3. Rousseeuw, P. *Silhouettes: a graphical aid to the interpretation and validation of cluster analysis.* J. Comput. Appl. Math., 1987.
|
||||
4. Strehl, A., Ghosh, J. *Cluster Ensembles — A Knowledge Reuse Framework for Combining Multiple Partitions.* JMLR 2002.(NMI)
|
||||
5. Halko, N., Martinsson, P.-G., Tropp, J. *Finding Structure with Randomness: Probabilistic Algorithms for Constructing Approximate Matrix Decompositions.* SIAM Review, 2011.(随机化 SVD)
|
||||
6. Chen, T., Kornblith, S., Norouzi, M., Hinton, G. *A Simple Framework for Contrastive Learning of Visual Representations.* ICML 2020.(SimCLR / NT-Xent)
|
||||
7. van den Oord, A., Li, Y., Vinyals, O. *Representation Learning with Contrastive Predictive Coding.* arXiv 2018.(InfoNCE 下界)
|
||||
@@ -0,0 +1,80 @@
|
||||
# 无监督聚类流水线
|
||||
|
||||
从 `data/film` 的原始图片到可人工核对的分组结果:六个阶段的数据流、张量形状与每步的数学操作。
|
||||
虚线是可选的 SimCLR 自监督分支(`method: simclr`,需要 Nvidia GPU)。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef input fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef prep fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef feat fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef reduce fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef cluster fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
|
||||
scan["data/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input
|
||||
|
||||
subgraph PRE["① 预处理 — 抹掉采集因子"]
|
||||
direction LR
|
||||
resize["双线性拉伸<br/>H×W → 128×128<br/>不保长宽比"]:::prep
|
||||
tensor["转张量 + RGBA/L → RGB<br/>x ∈ ℝ³ˣ¹²⁸ˣ¹²⁸"]:::prep
|
||||
zscore["逐图标准化<br/>x̃ = (x − μₓ) / σₓ<br/>抹掉亮度 μ 与对比度 σ"]:::prep
|
||||
resize --> tensor --> zscore
|
||||
end
|
||||
|
||||
subgraph FEAT["② 特征提取 — 纹理优先"]
|
||||
direction LR
|
||||
stem["resnet18 stem<br/>conv1 → bn → relu → maxpool"]:::feat
|
||||
l1["layer1<br/>64ch, stride 4"]:::feat
|
||||
l2["layer2<br/>128ch, stride 8"]:::feat
|
||||
l3["layer3<br/>256ch, stride 16"]:::feat
|
||||
gap2["GAP → ℓ₂ 归一化<br/>u⁽²⁾ ∈ S¹²⁷"]:::feat
|
||||
gap3["GAP → ℓ₂ 归一化<br/>u⁽³⁾ ∈ S²⁵⁵"]:::feat
|
||||
concat["拼接<br/>z = [u⁽²⁾; u⁽³⁾] ∈ ℝ³⁸⁴"]:::feat
|
||||
stem --> l1 --> l2 --> l3
|
||||
l2 --> gap2 --> concat
|
||||
l3 --> gap3 --> concat
|
||||
end
|
||||
|
||||
subgraph RED["③ 降维"]
|
||||
direction LR
|
||||
norm["ℓ₂ 归一化<br/>ẑ = z / ‖z‖₂"]:::reduce
|
||||
pca["PCA 投影 384 → 32<br/>y = Uᵀ(ẑ − z̄)<br/>不白化:保留 λ 谱"]:::reduce
|
||||
norm --> pca
|
||||
end
|
||||
|
||||
subgraph CLU["④ 聚类 K-Means"]
|
||||
direction LR
|
||||
init["k-means++ 初始化<br/>p(xᵢ) ∝ D(xᵢ)²"]:::cluster
|
||||
lloyd["Lloyd 迭代<br/>指派 ↔ 更新质心"]:::cluster
|
||||
best["n_init = 10<br/>取 inertia 最小的一次"]:::cluster
|
||||
assign["全量指派<br/>cᵢ = argminⱼ ‖yᵢ − μⱼ‖₂"]:::cluster
|
||||
init --> lloyd --> best --> assign
|
||||
end
|
||||
|
||||
subgraph EVAL["⑤ 无标签评估"]
|
||||
direction LR
|
||||
sil["轮廓系数 s̄<br/>簇是否紧致"]:::metric
|
||||
stab["换种子稳定性<br/>NMI(C_seed0, C_seed7)"]:::metric
|
||||
end
|
||||
|
||||
subgraph OUT["⑥ 产出"]
|
||||
direction LR
|
||||
csv["assignments.csv<br/>路径 / 簇号 / 距质心"]:::out
|
||||
links["clusters/cluster_XX/<br/>软链目录,按距质心排序"]:::out
|
||||
prev["cluster_XX_preview.png<br/>代表样本拼图"]:::out
|
||||
summ["summary.json<br/>簇大小 / 轮廓 / 稳定性"]:::out
|
||||
end
|
||||
|
||||
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>需 Nvidia GPU"]:::feat
|
||||
|
||||
scan --> PRE
|
||||
PRE -->|"x̃ᵢ"| FEAT
|
||||
scan -.->|"双视图增强"| simclr
|
||||
simclr -.->|"域内骨架权重"| FEAT
|
||||
FEAT -->|"z ∈ ℝᴺˣ³⁸⁴"| RED
|
||||
RED -->|"y ∈ ℝᴺˣ³²"| CLU
|
||||
CLU -->|"标签 c"| EVAL
|
||||
CLU -->|"标签 c + 距离 d"| OUT
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
flowchart TB
|
||||
classDef input fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef prep fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef feat fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef reduce fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef cluster fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
|
||||
scan["data/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input
|
||||
|
||||
subgraph PRE["① 预处理 — 抹掉采集因子"]
|
||||
direction LR
|
||||
resize["双线性拉伸<br/>H×W → 128×128<br/>不保长宽比"]:::prep
|
||||
tensor["转张量 + RGBA/L → RGB<br/>x ∈ ℝ³ˣ¹²⁸ˣ¹²⁸"]:::prep
|
||||
zscore["逐图标准化<br/>x̃ = (x − μₓ) / σₓ<br/>抹掉亮度 μ 与对比度 σ"]:::prep
|
||||
resize --> tensor --> zscore
|
||||
end
|
||||
|
||||
subgraph FEAT["② 特征提取 — 纹理优先"]
|
||||
direction LR
|
||||
stem["resnet18 stem<br/>conv1 → bn → relu → maxpool"]:::feat
|
||||
l1["layer1<br/>64ch, stride 4"]:::feat
|
||||
l2["layer2<br/>128ch, stride 8"]:::feat
|
||||
l3["layer3<br/>256ch, stride 16"]:::feat
|
||||
gap2["GAP → ℓ₂ 归一化<br/>u⁽²⁾ ∈ S¹²⁷"]:::feat
|
||||
gap3["GAP → ℓ₂ 归一化<br/>u⁽³⁾ ∈ S²⁵⁵"]:::feat
|
||||
concat["拼接<br/>z = [u⁽²⁾; u⁽³⁾] ∈ ℝ³⁸⁴"]:::feat
|
||||
stem --> l1 --> l2 --> l3
|
||||
l2 --> gap2 --> concat
|
||||
l3 --> gap3 --> concat
|
||||
end
|
||||
|
||||
subgraph RED["③ 降维"]
|
||||
direction LR
|
||||
norm["ℓ₂ 归一化<br/>ẑ = z / ‖z‖₂"]:::reduce
|
||||
pca["PCA 投影 384 → 32<br/>y = Uᵀ(ẑ − z̄)<br/>不白化:保留 λ 谱"]:::reduce
|
||||
norm --> pca
|
||||
end
|
||||
|
||||
subgraph CLU["④ 聚类 K-Means"]
|
||||
direction LR
|
||||
init["k-means++ 初始化<br/>p(xᵢ) ∝ D(xᵢ)²"]:::cluster
|
||||
lloyd["Lloyd 迭代<br/>指派 ↔ 更新质心"]:::cluster
|
||||
best["n_init = 10<br/>取 inertia 最小的一次"]:::cluster
|
||||
assign["全量指派<br/>cᵢ = argminⱼ ‖yᵢ − μⱼ‖₂"]:::cluster
|
||||
init --> lloyd --> best --> assign
|
||||
end
|
||||
|
||||
subgraph EVAL["⑤ 无标签评估"]
|
||||
direction LR
|
||||
sil["轮廓系数 s̄<br/>簇是否紧致"]:::metric
|
||||
stab["换种子稳定性<br/>NMI(C_seed0, C_seed7)"]:::metric
|
||||
end
|
||||
|
||||
subgraph OUT["⑥ 产出"]
|
||||
direction LR
|
||||
csv["assignments.csv<br/>路径 / 簇号 / 距质心"]:::out
|
||||
links["clusters/cluster_XX/<br/>软链目录,按距质心排序"]:::out
|
||||
prev["cluster_XX_preview.png<br/>代表样本拼图"]:::out
|
||||
summ["summary.json<br/>簇大小 / 轮廓 / 稳定性"]:::out
|
||||
end
|
||||
|
||||
simclr["可选:SimCLR 自监督<br/>method = simclr<br/>需 Nvidia GPU"]:::feat
|
||||
|
||||
scan --> PRE
|
||||
PRE -->|"x̃ᵢ"| FEAT
|
||||
scan -.->|"双视图增强"| simclr
|
||||
simclr -.->|"域内骨架权重"| FEAT
|
||||
FEAT -->|"z ∈ ℝᴺˣ³⁸⁴"| RED
|
||||
RED -->|"y ∈ ℝᴺˣ³²"| CLU
|
||||
CLU -->|"标签 c"| EVAL
|
||||
CLU -->|"标签 c + 距离 d"| OUT
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
@@ -0,0 +1,47 @@
|
||||
# K-Means:k-means++ 初始化 + Lloyd 迭代
|
||||
|
||||
`engine/cluster.py` 里 `kmeans()` 与 `cluster_features()` 的完整控制流:空簇处理、收敛判据、多次重启取最优,以及子集拟合 + 全量指派。
|
||||
虚线是各阶段内部的循环回边。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef start fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef step fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef decide fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
classDef guard fill:#FFE4E6,stroke:#BE123C,stroke-width:2px,color:#881337
|
||||
classDef done fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
|
||||
input["输入 Y = { yᵢ } ⊂ ℝ³², i = 1…N (可先随机抽子集拟合:fit_max_samples)<br/>目标 min_{C,μ} J(C,μ) = Σᵢ ‖yᵢ − μ_{c(i)}‖² — NP-hard,只求局部最优"]:::start
|
||||
|
||||
subgraph INIT["① k-means++ 初始化 (期望近似比 E[J] ≤ 8(ln k + 2)·J_opt)"]
|
||||
direction LR
|
||||
pick1["均匀随机取 μ₁"]:::step
|
||||
dist["D(y) = min_{j≤t} ‖y − μⱼ‖"]:::step
|
||||
sample["按 p(y) ∝ D(y)² 采样 μ_{t+1}<br/>离已有质心越远越易被选中"]:::step
|
||||
kdone{"够 k 个?"}:::decide
|
||||
pick1 --> dist --> sample --> kdone
|
||||
kdone -.->|"否,继续挑"| dist
|
||||
end
|
||||
|
||||
subgraph LLOYD["② Lloyd 迭代 (J 单调不增且有下界,故必收敛)"]
|
||||
direction LR
|
||||
assign["指派步<br/>c(i) = argminⱼ ‖yᵢ − μⱼ‖²<br/>一次 cdist 算完 N×k 距离"]:::step
|
||||
update["更新步<br/>μⱼ ← mean{ yᵢ : c(i) = j }<br/>均值是簇内平方距离的唯一最小点"]:::step
|
||||
empty{"有空簇?"}:::decide
|
||||
steal["把离自身质心最远的点<br/>提为该空簇的新质心<br/>保证簇数不缩水"]:::guard
|
||||
conv{"|J_prev − J| ≤ tol·J_prev<br/>或迭代满 100 次?"}:::decide
|
||||
assign --> update --> empty
|
||||
empty -->|"否"| conv
|
||||
empty -->|"是"| steal --> conv
|
||||
conv -.->|"否,回到指派步"| assign
|
||||
end
|
||||
|
||||
repeat["③ 以上 ①② 整体重复 n_init = 10 次<br/>每次换随机种子 seed + r"]:::step
|
||||
best["④ 取 10 次里 J 最小的一次 (C*, μ*)<br/>缓解 Lloyd 只保证局部最优"]:::done
|
||||
final["⑤ 全量指派 cᵢ = argminⱼ ‖yᵢ − μ*ⱼ‖,i = 1…N<br/>并记录到质心的距离 dᵢ,供排序与离群检查"]:::done
|
||||
|
||||
input --> INIT
|
||||
kdone ==>|"是,得到初值 μ⁽⁰⁾"| assign
|
||||
conv ==>|"是,本次收敛"| repeat
|
||||
repeat --> best --> final
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
flowchart TB
|
||||
classDef start fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef step fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef decide fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
classDef guard fill:#FFE4E6,stroke:#BE123C,stroke-width:2px,color:#881337
|
||||
classDef done fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
|
||||
input["输入 Y = { yᵢ } ⊂ ℝ³², i = 1…N (可先随机抽子集拟合:fit_max_samples)<br/>目标 min_{C,μ} J(C,μ) = Σᵢ ‖yᵢ − μ_{c(i)}‖² — NP-hard,只求局部最优"]:::start
|
||||
|
||||
subgraph INIT["① k-means++ 初始化 (期望近似比 E[J] ≤ 8(ln k + 2)·J_opt)"]
|
||||
direction LR
|
||||
pick1["均匀随机取 μ₁"]:::step
|
||||
dist["D(y) = min_{j≤t} ‖y − μⱼ‖"]:::step
|
||||
sample["按 p(y) ∝ D(y)² 采样 μ_{t+1}<br/>离已有质心越远越易被选中"]:::step
|
||||
kdone{"够 k 个?"}:::decide
|
||||
pick1 --> dist --> sample --> kdone
|
||||
kdone -.->|"否,继续挑"| dist
|
||||
end
|
||||
|
||||
subgraph LLOYD["② Lloyd 迭代 (J 单调不增且有下界,故必收敛)"]
|
||||
direction LR
|
||||
assign["指派步<br/>c(i) = argminⱼ ‖yᵢ − μⱼ‖²<br/>一次 cdist 算完 N×k 距离"]:::step
|
||||
update["更新步<br/>μⱼ ← mean{ yᵢ : c(i) = j }<br/>均值是簇内平方距离的唯一最小点"]:::step
|
||||
empty{"有空簇?"}:::decide
|
||||
steal["把离自身质心最远的点<br/>提为该空簇的新质心<br/>保证簇数不缩水"]:::guard
|
||||
conv{"|J_prev − J| ≤ tol·J_prev<br/>或迭代满 100 次?"}:::decide
|
||||
assign --> update --> empty
|
||||
empty -->|"否"| conv
|
||||
empty -->|"是"| steal --> conv
|
||||
conv -.->|"否,回到指派步"| assign
|
||||
end
|
||||
|
||||
repeat["③ 以上 ①② 整体重复 n_init = 10 次<br/>每次换随机种子 seed + r"]:::step
|
||||
best["④ 取 10 次里 J 最小的一次 (C*, μ*)<br/>缓解 Lloyd 只保证局部最优"]:::done
|
||||
final["⑤ 全量指派 cᵢ = argminⱼ ‖yᵢ − μ*ⱼ‖,i = 1…N<br/>并记录到质心的距离 dᵢ,供排序与离群检查"]:::done
|
||||
|
||||
input --> INIT
|
||||
kdone ==>|"是,得到初值 μ⁽⁰⁾"| assign
|
||||
conv ==>|"是,本次收敛"| repeat
|
||||
repeat --> best --> final
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,29 @@
|
||||
# PCA 主成分谱与白化的失效
|
||||
|
||||
**蓝柱**:真实特征在各主成分上的方差占比 λⱼ/Σλ(2000 张随机样本,layer2+3,384 维)。
|
||||
前 4 个成分占 68.7%,λ₁/λ₃₂ ≈ 165×。
|
||||
|
||||
**橙线**:白化后所有方向被强制拉平到 1/32 = 3.1%。
|
||||
|
||||
PC1 被压掉约 12 倍、PC32 被放大约 14 倍——K-Means 的欧氏距离于是主要由末尾那些
|
||||
近乎噪声的方向决定。实测开白化时最大簇吃掉 96% 的样本,关掉后降到 38%。
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 1250
|
||||
height: 520
|
||||
titlePadding: 16
|
||||
themeVariables:
|
||||
xyChart:
|
||||
titleColor: "#1F2937"
|
||||
plotColorPalette: "#2563EB, #EA580C"
|
||||
---
|
||||
xychart
|
||||
title "主成分方差谱:白化会把末尾的噪声方向抬到与信号同权"
|
||||
x-axis "主成分序号 j (PCA 降到 32 维)" [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]
|
||||
y-axis "方差占比 (%)" 0 --> 40
|
||||
bar [38.2, 13.0, 10.8, 6.7, 4.5, 3.6, 3.4, 2.3, 2.0, 1.6, 1.5, 1.3, 1.2, 1.0, 0.9, 0.8, 0.7, 0.7, 0.7, 0.7, 0.5, 0.5, 0.5, 0.4, 0.4, 0.4, 0.3, 0.3, 0.3, 0.3, 0.3, 0.2]
|
||||
line [3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1]
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
width: 1250
|
||||
height: 520
|
||||
titlePadding: 16
|
||||
themeVariables:
|
||||
xyChart:
|
||||
titleColor: "#1F2937"
|
||||
plotColorPalette: "#2563EB, #EA580C"
|
||||
---
|
||||
xychart
|
||||
title "主成分方差谱:白化会把末尾的噪声方向抬到与信号同权"
|
||||
x-axis "主成分序号 j (PCA 降到 32 维)" [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]
|
||||
y-axis "方差占比 (%)" 0 --> 40
|
||||
bar [38.2, 13.0, 10.8, 6.7, 4.5, 3.6, 3.4, 2.3, 2.0, 1.6, 1.5, 1.3, 1.2, 1.0, 0.9, 0.8, 0.7, 0.7, 0.7, 0.7, 0.5, 0.5, 0.5, 0.4, 0.4, 0.4, 0.3, 0.3, 0.3, 0.3, 0.3, 0.2]
|
||||
line [3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,56 @@
|
||||
# SimCLR 自监督对比学习
|
||||
|
||||
同一张图的两个随机增强视图互为正样本,batch 内其余 2B−2 个视图全是负样本。
|
||||
训练完只保留卷积骨架 f_θ,投影头 g_φ 丢弃。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef data fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef aug fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef net fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef loss fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
classDef note fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
|
||||
batch["一个 batch:B 张无标注图 { Iᵢ }, i = 1…B"]:::data
|
||||
|
||||
aug["随机增强分布 𝒯:RandomResizedCrop 0.5~1.0 · 水平/垂直翻转 · 高斯模糊<br/>对每张图独立采样两次 t, t′ ~ 𝒯"]:::aug
|
||||
|
||||
v1["视图 1 x⁽¹⁾ᵢ = t(Iᵢ)"]:::aug
|
||||
v2["视图 2 x⁽²⁾ᵢ = t′(Iᵢ)"]:::aug
|
||||
|
||||
bb1["卷积骨架 f_θ"]:::net
|
||||
bb2["卷积骨架 f_θ (共享同一份参数)"]:::net
|
||||
|
||||
h1["h⁽¹⁾ᵢ = f_θ(x⁽¹⁾ᵢ) ∈ ℝᴰ"]:::net
|
||||
h2["h⁽²⁾ᵢ = f_θ(x⁽²⁾ᵢ) ∈ ℝᴰ"]:::net
|
||||
|
||||
g1["投影头 g_φ<br/>Linear → BN → ReLU → Linear"]:::net
|
||||
g2["投影头 g_φ (共享参数)"]:::net
|
||||
|
||||
z1["z⁽¹⁾ᵢ = g_φ(h⁽¹⁾ᵢ) ∈ ℝ¹²⁸"]:::net
|
||||
z2["z⁽²⁾ᵢ = g_φ(h⁽²⁾ᵢ) ∈ ℝ¹²⁸"]:::net
|
||||
|
||||
sim["ℓ₂ 归一化后拼成 Z ∈ ℝ²ᴮˣ¹²⁸<br/>相似度矩阵 S = ZZᵀ / τ ∈ ℝ²ᴮˣ²ᴮ,τ = 0.2<br/>对角线置 −∞,排除自己和自己"]:::loss
|
||||
|
||||
nt["NT-Xent 损失<br/>ℒ = − (1/2B) Σₖ log [ exp(S_{k,π(k)}) / Σ_{m≠k} exp(S_{k,m}) ]<br/>π(k):同一张图的另一个视图"]:::loss
|
||||
|
||||
pos["分子 = 正样本对(1 个)<br/>同图另一视图 π(k)"]:::note
|
||||
neg["分母 = 全部 2B−2 个负样本<br/>batch 内其他所有视图"]:::note
|
||||
|
||||
step["反向传播同时更新 θ 与 φ<br/>AdamW + cosine,AMP"]:::loss
|
||||
|
||||
ckpt["训练收敛后只保存骨架<br/>simclr_backbone.pth<br/>投影头 g_φ 丢弃"]:::data
|
||||
|
||||
use["聚类阶段用 h = f_θ(x),不用 z<br/>投影头会滤掉对比任务用不上、<br/>但下游有用的信息"]:::note
|
||||
|
||||
batch --> aug
|
||||
aug --> v1
|
||||
aug --> v2
|
||||
v1 --> bb1 --> h1 --> g1 --> z1
|
||||
v2 --> bb2 --> h2 --> g2 --> z2
|
||||
z1 --> sim
|
||||
z2 --> sim
|
||||
pos -.-> nt
|
||||
neg -.-> nt
|
||||
sim --> nt --> step --> ckpt --> use
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
flowchart TB
|
||||
classDef data fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#064E3B
|
||||
classDef aug fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
|
||||
classDef net fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
|
||||
classDef loss fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
|
||||
classDef note fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
|
||||
|
||||
batch["一个 batch:B 张无标注图 { Iᵢ }, i = 1…B"]:::data
|
||||
|
||||
aug["随机增强分布 𝒯:RandomResizedCrop 0.5~1.0 · 水平/垂直翻转 · 高斯模糊<br/>对每张图独立采样两次 t, t′ ~ 𝒯"]:::aug
|
||||
|
||||
v1["视图 1 x⁽¹⁾ᵢ = t(Iᵢ)"]:::aug
|
||||
v2["视图 2 x⁽²⁾ᵢ = t′(Iᵢ)"]:::aug
|
||||
|
||||
bb1["卷积骨架 f_θ"]:::net
|
||||
bb2["卷积骨架 f_θ (共享同一份参数)"]:::net
|
||||
|
||||
h1["h⁽¹⁾ᵢ = f_θ(x⁽¹⁾ᵢ) ∈ ℝᴰ"]:::net
|
||||
h2["h⁽²⁾ᵢ = f_θ(x⁽²⁾ᵢ) ∈ ℝᴰ"]:::net
|
||||
|
||||
g1["投影头 g_φ<br/>Linear → BN → ReLU → Linear"]:::net
|
||||
g2["投影头 g_φ (共享参数)"]:::net
|
||||
|
||||
z1["z⁽¹⁾ᵢ = g_φ(h⁽¹⁾ᵢ) ∈ ℝ¹²⁸"]:::net
|
||||
z2["z⁽²⁾ᵢ = g_φ(h⁽²⁾ᵢ) ∈ ℝ¹²⁸"]:::net
|
||||
|
||||
sim["ℓ₂ 归一化后拼成 Z ∈ ℝ²ᴮˣ¹²⁸<br/>相似度矩阵 S = ZZᵀ / τ ∈ ℝ²ᴮˣ²ᴮ,τ = 0.2<br/>对角线置 −∞,排除自己和自己"]:::loss
|
||||
|
||||
nt["NT-Xent 损失<br/>ℒ = − (1/2B) Σₖ log [ exp(S_{k,π(k)}) / Σ_{m≠k} exp(S_{k,m}) ]<br/>π(k):同一张图的另一个视图"]:::loss
|
||||
|
||||
pos["分子 = 正样本对(1 个)<br/>同图另一视图 π(k)"]:::note
|
||||
neg["分母 = 全部 2B−2 个负样本<br/>batch 内其他所有视图"]:::note
|
||||
|
||||
step["反向传播同时更新 θ 与 φ<br/>AdamW + cosine,AMP"]:::loss
|
||||
|
||||
ckpt["训练收敛后只保存骨架<br/>simclr_backbone.pth<br/>投影头 g_φ 丢弃"]:::data
|
||||
|
||||
use["聚类阶段用 h = f_θ(x),不用 z<br/>投影头会滤掉对比任务用不上、<br/>但下游有用的信息"]:::note
|
||||
|
||||
batch --> aug
|
||||
aug --> v1
|
||||
aug --> v2
|
||||
v1 --> bb1 --> h1 --> g1 --> z1
|
||||
v2 --> bb2 --> h2 --> g2 --> z2
|
||||
z1 --> sim
|
||||
z2 --> sim
|
||||
pos -.-> nt
|
||||
neg -.-> nt
|
||||
sim --> nt --> step --> ckpt --> use
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
Reference in New Issue
Block a user