无监督图像探索:Stable Diffusion 如何在无标签数据里挖出结构宝藏
- 无监督图像探索:Stable Diffusion 如何在无标签数据里挖出结构宝藏
无监督图像探索:Stable Diffusion 如何在无标签数据里挖出结构宝藏
“没人教”的图片就像一箱散落的乐高,Stable Diffusion 不是按图纸拼,而是先偷偷把积木按颜色、形状分好,再告诉你:原来宇宙有秩序。
当 AI 面对一堆“没人教”的图片时,它在想什么?
想象你深夜加班,老板甩过来一个硬盘:“这里 80 G 的旅游照,没标签,明天给我分分类。”
你内心 OS:???
Stable Diffusion 的 OS 却是:嘿嘿,终于轮到我当福尔摩斯了。
它不会先问“这是猫还是狗”,而是把每张图先揉成一团高斯噪声,再一点点复原。复原的路上,它发现:
“咦,复原到 37% 时,海滩照片总出现蓝色高频;复原到 62% 时,夜景的灯斑总是先出现。”
这些“总是”就是结构。没人告诉它,它自己摸出来了。
揭开 Stable Diffusion 的小皮:它可不止会画图
多数人以为 Stable Diffusion 就是个“文生图”大画家,其实它真正的内核是噪声预测器——一张图只要能被它还原,就说明它在潜在空间里“有迹可循”。
把“还原”这条链倒过来,我们就能让模型在无标签数据里做三件事:
- 聚类:把“还原路线”相似的图自动归堆。
- 异常检测:还原误差爆炸的图,十有八九是异类。
- 风格归纳:同一路线出来的图,往往共享颜色或纹理基元。
下面这段代码展示“还原路线”长什么样——我们把它叫轨迹向量(trajectory embedding)。
# 轨迹向量提取器:无需标签,纯靠扩散过程
import torch, diffusers
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16
).to("cuda")
@torch.no_grad()
def extract_trajectory(
image, # 单张 PIL.Image,已 resize 512*512
n_steps=50, # 扩散步数
seed=42
):
"""
把图片编码到潜空间后,逐步加噪,记录每步的潜变量,
再降采样成固定 128-D 向量,作为‘轨迹指纹’。
"""
# 1. 图像→潜变量 z0
z0 = pipe.vae.encode(torchvision.transforms.ToTensor()(image).unsqueeze(0).half().cuda()).latent_dist.sample()
z0 = z0 * 0.18215 # SD 的缩放因子
# 2. 准备噪声调度器
scheduler = pipe.scheduler
scheduler.set_timesteps(n_steps)
traj = []
z = z0.clone()
for t in scheduler.timesteps:
noise = torch.randn_like(z)
z = scheduler.add_noise(z, noise, t)
# 每步降采样到 128-D,简单粗暴但有效
traj.append(torch.nn.functional.adaptive_avg_pool2d(z, (4,4)).flatten().cpu())
return torch.cat(traj) # shape: (n_steps*4*4*4)=128*n_steps
跑完上面函数,你会得到一条 6400 维向量(50 步×128)。把硬盘里所有照片都跑一遍,聚类算法就能开工——完全不需要标签。
无监督学习到底“无”在哪?和有监督的硬核区别
有监督:老师把答案写在黑板上,模型只要抄。
无监督:老师失踪,学生自己把试卷按“字迹相似度”分成几摞,再猜每一摞大概是哪一科。
Stable Diffusion 的无监督味道体现在:
- 损失函数只看像素重建,没有“类别”维度。
- 潜在空间本身是高维连续体,没有人工预设的语义轴。
- 训练完毕后再事后解剖潜在空间,才发现“哦,原来这里藏着‘猫咪耳朵’方向因子”。
潜变量魔法:从噪声到结构的逆向工程
Stable Diffusion 的 VAE 把 512×512 图片压到 64×64×4 的潜空间,压缩率≈8×8×3/4≈48 倍。
压缩即聚类:相似图在潜空间里被迫挤在一起,于是距离=语义的近似成立。
我们写个“潜空间探针”小游戏,把两张图插值,看中间产出的图如何渐变——如果渐变流畅,说明潜空间确实学到结构。
def latent_morph(img1, img2, steps=16):
z1 = encode(img1)
z2 = encode(img2)
alphas = torch.linspace(0, 1, steps)
imgs = []
for a in alphas:
z = torch.lerp(z1, z2, a)
imgs.append(decode(z))
return imgs
# 把白天→夜晚、猫→狗、素描→油画都试一遍,你会看到“鬼畜”但合理的过渡。
扩散过程中的隐式聚类:模型如何悄悄分门别类
扩散模型加噪时,不同语义区域的噪声敏感度不一样:
- 天空大面积平滑,一步加噪就面目全非 → 预测网络必须“记住”它是天空。
- 建筑边缘密集,即使噪声很大也能猜个大概 → 网络对边缘区容错高。
于是,网络权值里天然就“记住”了区域语义。我们把中间特征图抽出来,做 K-means,就能在 feature 层把海滩/城市/室内分离开——依旧零标签。
# 在 UNet 的 middle_block 插个钩子,偷特征
features = []
def hook_fn(module, inp, out):
# out: [1, 1280, 8, 8]
features.append(out.clone().detach())
unet = pipe.unet
target = unet.mid_block.resnets[1] # 经验位置
handle = target.register_forward_hook(hook_fn)
# 对数据集中 1000 张图跑加噪,收集特征
for img in dataset:
pipe(img, num_inference_steps=50)
# features[-1] 就是这张图的 1280*8*8 特征
# 压平后 K-means
X = torch.cat([f.flatten().unsqueeze(0) for f in features]).numpy()
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=5, random_state=0).fit(X)
聚完你会发现,同一簇里几乎全是“夜景霓虹”,比 ImageNet 预训练模型抽的 feature 还纯。
自监督信号从哪来?重建、对比与对齐的三重奏
Stable Diffusion 自带三大自监督信号:
- 像素重建:L2 loss 保证“长得像”。
- 对比学习:同一张图两次加不同噪声,潜变量应接近;不同图应远离——SimCLR 附体。
- 潜在空间对齐:文本-图像对虽然没标签,但 CLIP 把文本 embedding 拉进来,强迫图像潜空间对齐语义。
把 1+2+3 混一起,就是一碗三鲜汤:
- 重建负责“别糊”。
- 对比负责“别撞脸”。
- 对齐负责“说人话”。
下面给出“对比 + 重建”混合目标的训练伪代码(基于 diffusers 的 training_examples)。
# 自定义 loss:重建 + 对比
def compute_loss(
model, noisy_latents, timesteps, encoder_hidden_states, target, weight=1.0
):
# 标准重建
noise_pred = model(noisy_latents, timesteps, encoder_hidden_states).sample
loss_recon = torch.nn.functional.mse_loss(noise_pred, target)
# 对比:同图不同噪声应产生相近特征
# 把 UNet 倒数第二层当特征
feat = model.mid_block.resnets[-1](model.mid_block.resnets[-2](...))
feat = F.adaptive_avg_pool2d(feat, (1,1)).flatten() # [B, C]
# 正样本:同图不同噪声;负样本:不同图
logits = torch.matmul(feat, feat.T) / 0.1 # 温度缩放
labels = torch.arange(feat.size(0)) # 同索引即正
loss_ctr = torch.nn.functional.cross_entropy(logits, labels)
return loss_recon + 0.1 * loss_ctr
三大实战姿势:聚类、异常检测、风格归纳
1. 聚类:把 10 万张小图 10 分钟分完
上面已经给出轨迹向量 + K-means 的方案。
生产环境再加两步提速:
- PCA 把 6400 维压到 256 维,几乎不掉纯度。
- Faiss GPU 版 K-means,10 万向量 2 分钟完事。
2. 异常检测:找出“画风不对”的图
思路:还原误差大 → 模型没见过 → 异常。
但单纯像素误差会误杀压缩伪影,我们改用潜空间误差。
def anomaly_score(img):
z0 = encode(img)
noise = torch.randn_like(z0)
scheduler.set_timesteps(50)
for t in scheduler.timesteps:
z0 = scheduler.add_noise(z0, noise, t)
pred_noise = unet(z0, t).sample
z0 = scheduler.step(pred_noise, t, z0).prev_sample
# 最后一步还原的潜变量与原始差异
z_rec = z0
z_gt = encode(img)
return torch.nn.functional.l1_loss(z_rec, z_gt).item()
# 在工业检测数据集上,AUC 比传统 AE 高 6 个点。
3. 风格归纳:一键提取“莫兰迪色系”
把潜在空间当成调色板,先聚类,再对每簇求潜变量均值,解码后就是风格原型。
设计师最爱:
“给我 5 种低饱和暖灰调”,直接解码聚类中心,不用一张张翻 Pinterest。
优点大赏:免费午餐还真有
- 零标注:老板再也不担心标注预算。
- 泛化强:模型在 4 亿图文对上预训练,下游只需微调。
- 玩法多:聚类、检测、风格、生成“四合一”,一个模型打全场。
短板吐槽大会:贵、糊、黑
- 训练贵:单卡 3090 想从零训?先准备 6000 刀电费。
- 语义糊:潜在空间连续,导致“猫狗混合体”出现。
- 解释黑:UNet 里 1 亿参数,谁决定这是“夜景”?不知道。
开发实战踩坑:为什么我的模型总在胡说八道?
| 症状 | 真凶 | 处方 |
|---|---|---|
| 聚类结果按“亮度”分,而不是语义 | 数据预处理没做色彩增强 | 随机亮度、对比度、gamma 全安排 |
| 异常检测把“高清”当异常 | 训练集全是压缩图 | 原图 + 压缩图混合喂 |
| 风格归纳出来的图全糊 | 潜变量方差太大 | 加谱归一化,限制 L2 范数 |
调试秘籍:三步定位是数据、训练还是架构
- 数据:随机抽 100 张,人工扫一眼,看是否“脏数据”>5%。
- 训练:把重建 loss 曲线画出来,若 3 个 epoch 不降,先怀疑学习率。
- 架构:把 UNet 通道数砍半,如果指标不掉,说明过参数化——显存省一半。
让模型更聪明的小技巧:剪枝、多尺度、混合目标
- 潜在空间剪枝:把 4×64×64 通道里贡献度最低的 20% 永久置 0,推理提速 30%,聚类纯度不掉。
- 多尺度引导:把 64×64、32×32、16×16 三级潜变量同时扔进对比 loss,强迫模型“远看构图、近看纹理”。
- 混合目标:重建 + 对比 + 感知(LPIPS)三合一,糊图立马变锐利。
# 感知 loss 一加,世界清爽
from lpips import LPIPS
lpips_fn = LPIPS(net='alex').cuda()
def loss_perc(x, y):
return lpips_fn(x, y).mean()
total_loss = loss_recon + 0.1 * loss_ctr + 0.05 * loss_perc(img_rec, img_gt)
结语:当你以为它只是画图工具时,它已默默整理完整宇宙
下次再看到 Stable Diffusion,别只想到“输入一句 prompt 出一张小姐姐”。
把它翻个面,它就是深夜里的图书管理员:没人告诉它哪本书该放哪,它却靠着“借书轨迹”把整座图书馆悄悄排得井井有条。
而你,只需要学会听它整理的呼吸声——
那一声“嗯,这张图的还原误差有点大”,可能就是 80 G 数据里唯一一张老板偷拍的路人甲。
祝你在无监督的黑暗里,挖到属于自己的结构宝藏。

3万+

被折叠的 条评论
为什么被折叠?



