ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

无监督学习实战笔记:从聚类、降维到自编码器与GAN

无监督学习实战笔记:从聚类、降维到自编码器与GAN 作为一门只靠数据本身找结构的学问无监督学习在我接触深度学习的这几年里几乎是“越学越觉得值”的一个方向。很多人一开始都是被监督学习带进门的——给图片打标签、给文本分好类模型照着正确答案不断修正。但真实世界里绝大多数数据是没有标签的甚至你根本不知道应该分成几类、该保留哪些特征。这时候无监督学习就是那个“在没有答案的题海里找规律”的思路它不依赖人工标注而是从数据本身的结构、分布、相似度里挖东西。这篇学习笔记就把我踩过的坑、梳理清楚的知识点和能直接跑的代码一起整理出来给正在入门或者准备做特征探索、数据预训练、异常检测的朋友做参考。无监督学习覆盖的东西很广从经典的K-Means、PCA到深度时代的自编码器、VAE、GAN整个家族其实有一个统一的底层逻辑在没有标签的前提下设计一个Loss或者目标函数让模型在优化过程中自动学到数据里的结构。我最早以为无监督就是“聚类”后来发现聚类只是冰山一角。真正把无监督玩明白需要对“数据分布”“表征空间”“重构误差”这些概念有直觉。这篇文章我尽量不堆公式重点讲清楚每个方法在干什么、怎么用代码实现、以及实际跑实验时有哪些容易翻车的细节。1. 无监督学习的整体思路在没有答案的题海里找规律1.1 为什么无监督学习是深度学习的“隐藏主线”我学Deep Learning的过程里有一个很明显的感受教程里90%的精力都在讲卷积、循环网络、Attention这些有监督的结构但真正到了实际项目里无监督的思路却无处不在。预训练要无监督数据探索要无监督特征可视化还是要无监督。举一个很直观的例子假设你拿到了一堆用户行为日志没有标注“这个用户是正常还是异常”也没有标“这批用户属于什么类型”。你想要做的第一件事往往是先看这批数据能不能自然分成几群或者哪几条数据明显偏离主体。这个“先看结构”的动作就是无监督学习。它的核心逻辑和人有监督学习是反着来的。有监督学习先给答案再让模型去拟合无监督学习不给答案让模型自己去回答“什么是重要的结构”。在深度学习的框架下这个“自我回答”通常通过三种方式实现重建任务把输入压成一个低维向量再从这个向量恢复原始输入恢复得越好说明低维向量越保留了关键信息。自编码器就是这条路。对比任务让模型判断“哪些样本是相似的、哪些是不同的”通过构造正负样本来学习表征。SimCLR、MOCO等对比学习都属于这类。对抗任务让生成器骗过判别器在博弈中让生成器学会真实数据的分布。GAN就是这条路。有监督学习就像给你一份带答案的练习册刷完题就能上考场无监督学习则更像直接把你扔进一个陌生的城市让你自己摸索出街道的分布、人群的聚集区域。后者看起来更“难”但一旦掌握了就能应对大量根本没有答案的真实场景。1.2 无监督学习的三个主要分支很多初学者有一个误区以为无监督学习就是“聚类”。实际上我在实际使用中把它分成三个大的方向每个方向解决的问题完全不同聚类目标是发现离散的类别结构。比如把用户分成几种消费习惯把图片内容分成几种风格把日志分成几类行为模式。代表算法有K-Means、DBSCAN、层次聚类、高斯混合模型。降维目标是找低维表示同时尽可能保留原始数据的差异信息。PCA是最经典的方法深度版本就是自编码器。t-SNE和UMAP则更多用于可视化。降维不一定要把数据分成清晰的几堆更多是压缩数据、提取主要变量。密度估计与生成目标是学习数据的概率分布然后能从分布中采样出新的、不存在的样本。VAE和GAN是深度生成模型的代表。这类方法的价值在于它不只是“压缩”或“分类”而是真正理解了数据长什么样才能生成出像模像样的新样本。这三者不是互斥的。比如自编码器既能用来降维学到的低维向量经过聚类之后又可以当成特征来用。GAN的判别器也可以用来做异常检测。学无监督学习的时候脑子里不要给方法画死框要始终想“这个方法是靠什么样的自监督信号在优化”。理解了这一点后面的公式和代码都会好懂很多。我的个人建议是初学阶段不要贪多。先把聚类和降维吃透用sklearn跑熟K-Means、DBSCAN、PCA再去碰自编码器和GAN。因为聚类和降维的数学门槛低、结果直观、调试容易是建立“无监督直觉”最好的土壤。直接上手GAN很容易被训练崩溃、模式坍塌这些问题劝退。2. 聚类把相似的东西自动归堆2.1 K-Means的“中心点迭代法”到底在干什么K-Means是我接触最早、也是用得最多的聚类算法。它的思路简单到一句话就能说清随机放K个中心点然后把每个样本分给最近的中心点分完之后把中心点挪到这一堆样本的均值位置重复这个过程直到中心点不再变化。我在学习K-Means的时候困扰我的一个问题就是明明初始中心是随机放的为什么最后总能保证收敛后来我理解了K-Means本质上是在优化一个目标函数整个数据集的组内平方和Within-Cluster Sum of Squares, WCSS。它希望让每一个样本点到它所属簇中心的距离平方之和尽量小。交替更新样本归属和中心点正是一个坐标下降的过程每次迭代目标函数只会下降不会上升所以一定会收敛只是可能收敛到局部最优而不是全局最优。实际使用K-Means时有一个我吃过亏的点数据必须先做标准化。假设你有一个数据集第一维是“年龄”取值范围是20到50第二维是“年收入”取值范围是5万到100万。如果不做标准化K-Means算欧氏距离的时候“收入”这一维的数值差异会彻底淹没“年龄”的影响聚类结果基本等于只用收入在分。所以我在实际项目中凡是准备用距离类算法第一步都是Z-Score标准化或者Min-Max缩放。K值的选择也是初学者最容易卡住的地方。我常用的方法有两个。第一个是肘部法则画出K从1到10的WCSS曲线看哪个点之后下降速度明显变慢那个点就是“肘部”。第二个是轮廓系数计算每个样本的簇内紧密度和簇间分离度综合成0到1之间的分数分数越高说明聚类效果越好一般取分数最高的K值。下面这个例子是我在一个人群画像项目里实际跑过的代码结构数据是模拟的但流程完全一致from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler from sklearn.metrics import silhouette_score import numpy as np import matplotlib.pyplot as plt # 模拟数据3个簇600个样本2个特征 np.random.seed(42) X np.vstack([ np.random.normal(loc[0, 0], scale0.5, size(200, 2)), np.random.normal(loc[5, 5], scale0.6, size(200, 2)), np.random.normal(loc[0, 5], scale0.5, size(200, 2)), ]) # 标准化是距离类算法的生命线 X_scaled StandardScaler().fit_transform(X) # 用轮廓系数选择K best_k, best_score 2, -1 for k in range(2, 7): labels KMeans(n_clustersk, n_init10, random_state42).fit_predict(X_scaled) score silhouette_score(X_scaled, labels) print(fK{k}, silhouette{score:.4f}) if score best_score: best_k, best_score k, score print(f最优K{best_k}, 轮廓系数{best_score:.4f})这里有个细节KMeans在sklearn新版本里默认n_init已经不是1了但为了结果稳定我习惯显式设置n_init10或更多。因为K-Means对初始中心敏感多跑几次初始化能有效降低落到差局部最优的概率。2.2 DBSCAN不要告诉我几堆我自己看密度K-Means有个硬伤你得提前告诉它K是多少。但现实中的很多场景你根本不知道有几类甚至数据里就是有一堆“谁都归不进去”的异常点。这时候DBSCAN就派上用场了。DBSCAN的核心逻辑非常直觉一个簇是由密度相连的样本点组成的。它只需要两个参数半径epsilon和最小样本数min_samples。算法从任意一个未被访问的点出发如果它半径epsilon内至少有min_samples个邻居就从这个点开始“扩张”把密度可达的点全归到同一个簇。那些到哪儿都够不着其他点的样本就被标记为噪声点。我当年第一次用DBSCAN的时候最头大的是调epsilon。这个值选大了所有点都能连成一片选小了到处都是孤立的噪声点。后来我发现一个实用的经验先画出所有样本点的K-距离图也就是每个点到它的第k个最近邻居的距离排序曲线曲线出现明显拐点的位置对应的距离就是一个不错的epsilon起点。这个“拐点”的原理是簇内点的K-距离比较小簇间点的K-距离会突然变大拐点就反映了密度的分界线。下面是一个用DBSCAN做异常点检测的简单示例from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler # 沿用上面的模拟数据额外加入20个离群点 np.random.seed(42) outliers np.random.uniform(low-5, high10, size(20, 2)) X np.vstack([X, outliers]) X_scaled StandardScaler().fit_transform(X) # 先试一组参数 db DBSCAN(eps0.5, min_samples5) labels db.fit_predict(X_scaled) # 判断哪些点是噪声 n_noise (labels -1).sum() print(f噪声点数量: {n_noise}) # 分出来的簇数量 n_clusters len(set(labels)) - (1 if -1 in labels else 0) print(f簇数量: {n_clusters})我在实践中得到的经验是min_samples不要太大一般取特征维度数的两倍左右或者5到10太小会把一些稀疏区域误判成簇太大会把一些小簇合并掉或者把所有点都标成噪声。DBSCAN另一个好处是你不提前规定簇数所以它特别适合对数据分布完全没有先验知识的探索阶段。如果DBSCAN跑出来发现噪声点特别多往往不是数据脏而是数据本身的密度就不均匀这时候反而提示你该考虑高斯混合模型这种带概率假设的聚类方法。2.3 高维数据聚类的“诅咒”聚类方法里面距离计算是一切的基础。但我第一次把K-Means用到几十维的特征上时发现结果怎么调都不对劲。后来才知道这背后是“维数灾难”在捣鬼在高维空间里所有样本之间的距离会越来越接近也就是说每个点看起来都像“等距”的聚类算法很难区分什么是近、什么是远。应对高维聚类我的做法通常是三步走先用PCA或自编码器把特征压缩到20维以下能解释80%以上方差最好。在低维空间上用DBSCAN做一次粗聚类“污染严重”的噪声点先踢掉。对剩下的数据再用K-Means精细分簇最后通过t-SNE可视化人工检查分簇是否合理。这个流程听起来朴素但非常实用。高维聚类本身是个难题与其指望换一个玄学算法解决问题不如先把数据质量做好把特征维度降下来。无监督学习比赛里排名靠前的方案绝大多数都不是用什么神仙算法而是把预处理、降维、聚类、人工检查这套基本功做到极致。3. 降维与表征学习压缩数据里的“真正特征”3.1 PCA线性时代的地基主成分分析PCA应该是我在所有无监督方法里使用频率最高的工具了。它的目标很简单找到一组新的正交方向让数据投影到这组方向上的方差尽可能大。第一个方向就是第一主成分第二个与它正交的方向就是第二主成分以此类推。为什么方差大就重要因为方差体现了信息的分散程度。如果某个方向上所有样本的投影值都几乎相同那这个方向对区分不同样本没有任何帮助。反过来说方差最大的方向就是最能拉开样本差异的方向。在实际项目中我经常用PCA做三件事降维提速特征太多模型训练慢先用PCA把维度从几千压到几百再拿去训练。去相关PCA变换之后各主成分之间是正交不相关的对某些线性模型比如线性回归、LDA有帮助。可视化把数据降到二维或三维配合散点图观察聚类结构。有一件事必须说清楚PCA是无监督的它只管找方差大的方向完全不考虑数据有没有标签。所以它找到的方向不一定是对“分类”最有利的方向它保留的是“整体结构”而不是“类别差异”。如果用PCA可视化之后发现两个类混在一起别急着说PCA没用更合理的是说“从整体方差结构来看这两个类别本身就不容易分”。from sklearn.decomposition import PCA import matplotlib.pyplot as plt # 对标准化后的数据做PCA pca PCA(n_components2) X_pca pca.fit_transform(X_scaled) print(f解释方差比例: {pca.explained_variance_ratio_}) print(f累计解释方差: {pca.explained_variance_ratio_.cumsum()}) # 可视化 plt.figure(figsize(6, 5)) plt.scatter(X_pca[:, 0], X_pca[:, 1], clabels, cmapviridis, s10) plt.xlabel(PC1) plt.ylabel(PC2) plt.title(PCA Visualization) plt.savefig(pca_result.png, dpi120)我一般会同时画出主成分的碎石图各主成分解释方差比例的条形图通过拐点确定保留多少个主成分。另一个经验是PCA对量纲敏感必须标准化不然结果会被大数值特征主导。3.2 自编码器用深度网络学非线性压缩PCA只能做线性变换而现实中的数据结构往往是非线性的。比如一张人脸图片角度的变化、表情的变化这些因素叠加在一起形成的像素空间关系线性变换很难用一个平面去捕捉。自编码器就是来解决这个问题的。自编码器的结构可以用一句大白话概括一个编码器网络把输入压缩成一个低维向量一个解码器网络把这个低维向量还原成原始输入整个网络通过最小化还原误差来训练。训练完成后编码器输出的那个低维向量就是数据的“表征”。我在学习自编码器时有一个关键的理解转折点自编码器不是简单地“记住”输入。如果中间层维度够大、参数够多网络完全可以复制输入学不到任何有意义的特征。所以自编码器的关键在于“瓶颈”设计——中间层维度必须足够小逼着网络不得不丢弃不重要的细节只保留最有价值的信息。这个思想跟PCA一脉相承只是自编码器用了神经网络可以拟合非线性映射。写一个最简单的自编码器用PyTorch只要几十行import torch import torch.nn as nn import torch.optim as optim class Autoencoder(nn.Module): def __init__(self, input_dim, hidden_dim): super().__init__() self.encoder nn.Sequential( nn.Linear(input_dim, hidden_dim * 2), nn.ReLU(), nn.Linear(hidden_dim * 2, hidden_dim), ) self.decoder nn.Sequential( nn.Linear(hidden_dim, hidden_dim * 2), nn.ReLU(), nn.Linear(hidden_dim * 2, input_dim), ) def forward(self, x): code self.encoder(x) recon self.decoder(code) return recon, code # 以28x28784维的MNIST为例 model Autoencoder(input_dim784, hidden_dim32) optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() # 训练循环示意 for epoch in range(10): for x_batch, _ in train_loader: x_flat x_batch.view(x_batch.size(0), -1) recon, _ model(x_flat) loss criterion(recon, x_flat) optimizer.zero_grad() loss.backward() optimizer.step() print(fepoch {epoch}, loss: {loss.item():.4f})在MNIST这种简单数据集上模型收敛很快几轮之后重建图片就已经很清楚了。我自己跑的时候发现一个有意思的现象32维的瓶颈层学到的特征虽然人看不出每个维度具体是什么意思但把它们拿到下游分类任务里用效果还不错。这说明自编码器确实在无监督条件下学到了对下游任务有用的表征。使用自编码器时有几个实际经验供参考重建损失不一定非要MSE。图像数据用MSE会倾向于生成模糊的结果可以试L1损失或者感知损失。不要一味压得太低。我试过把MNIST压到2维重建结果完全看不清数字再降到这种程度信息丢失太多了瓶颈维度要根据具体任务试探。正则化很有用。加L2权重衰减、Dropout或者用稀疏自编码器加KL散度惩罚都能让学到的特征更干净。3.3 t-SNE可视化大杀器的使用心得t-SNE是我做数据可视化最常用的工具。它和PCA完全不同它并不试图保留全局的远近关系而是更关注把高维空间中的“局部邻居关系”在低维空间里重现。正因为这个特性t-SNE做出来的图里簇与簇之间的距离没有绝对意义但“哪些点聚在一起”这个信息往往很可靠。使用t-SNE时最重要的参数是perplexity。它的作用是控制算法在计算相似度时考虑多少个邻居一般取值在5到50之间。我每次拿到新数据都会多跑几个perplexity值对比因为没有一个万能值。perplexity设太小时图里容易形成大量碎散的小点设太大时原本分离的簇可能被拉到一起。另一个坑t-SNE第一次跑出来的结果不要把坐标方向当成有意义的方向。旋转、翻转后的图表达的是完全相同的簇结构。所以看t-SNE图的时候重点永远是“簇的相对结构和分离度”而不是某个点在图中的具体位置。还有一点t-SNE计算量很大样本数超过几万就很难跑。我的做法是先降维PCA或UAMP到30到50维再送进t-SNE。这个两步法既能保留全局结构又能让t-SNE算得动而且结果通常更干净。4. 生成模型让机器从数据分布里“无中生有”4.1 从AE到VAE给隐变量加上“概率约束”自编码器虽然能学到低维编码但它的编码空间往往是“非规则”的。什么意思呢你随便在隐空间里取一个点输入给解码器画出来的图大概率是一团乱七八糟的噪声因为你取的这个点根本不在训练样本编码的分布区域里。也就是说自编码器的隐空间是不连续、不满的。VAE解决这个问题的办法很巧妙它不再让编码器输出一个确定的隐变量而是让编码器输出一个分布是一个均值和方差然后从这个分布中采样得到隐变量。通过加上KL散度正则项VAE逼迫所有样本的隐变量分布向标准正态分布靠拢。这样做的结果是隐空间变得连续且结构化相邻隐变量对应的生成图像也是相似的采样新点能生成合理的新样本。训练VAE的损失函数有两个部分一个是重建损失衡量解码器恢复原始输入的能力一个是KL散度衡量编码分布与标准正态分布的距离。这两者之间有一个微妙的平衡重建损失逼迫编码器保留足够信息KL损失逼迫编码器把分布压向标准正态。我在调参时发现如果重建损失权重太大VAE就退化成普通自编码器隐空间充满空洞如果KL权重太大生成结果又模糊因为模型偷懒直接把所有输入都编码成标准正态分布的均值重建时自然只能得出各类特征的平均值。VAE生成的图像比GAN要模糊是已知事实这一点从设计上就注定了。因为VAE本质上是在做“重建整个输入”的任务用像素级损失衡量时模型会选择平均掉那些不确定的细节来降低损失。所以我在实际项目中通常是VAE做表征和生成探索GAN做追求视觉逼真度的生成。二者不是替代关系是侧重不同。如果想把VAE当生成器做图像生成一个很关键的体验是先固定种子多跑几次生成对比不同隐变量采样得到的图片差异。这样你能直观感受隐空间每个维度大概控制什么变化。我曾在一个人像数据集上跑到30维隐空间发现某些维度对应脸部朝向、某些维度对应肤色深浅这种可解释性是无监督生成模型非常迷人的地方。4.2 DCGAN把卷积用在对抗生成上GAN这个词太有名了但很多人第一次训练GAN就心态爆炸——Loss不降、生成图像全是噪点、模式坍塌这些问题我全遇到过。学习GAN最推荐入手的结构其实是DCGAN它在普通GAN的基础上做了几个关键改进用卷积代替全连接、去掉池化层改用带步长的卷积、用批量归一化稳定训练、判别器用LeakyReLU避免梯度饱和。我当初第一次把DCGAN在CelebA这种128x128的数据上跑起来的时候生成效果是逐步变好的前几个epoch全是灰色噪点大概20个epoch后能看到模糊的人脸轮廓50个epoch后能看出五官到100个epoch才相对清晰。这个过程中最需要的是耐心和正确的检查手段而不是频繁调参。一个很实用的检查手段是把生成器在训练过程中的loss画出来但不要只看生成器loss或者判别器loss的绝对值。GAN的训练本质是两者的博弈评判标准最终要看生成图像的质量。我建议固定一个batched的随机噪声z每隔一定epoch把生成图像存下来做成gif或矩阵图这样能直观看到训练是否在往好的方向走。如果训练过程出现判别器loss持续下降、生成器loss持续上升不要慌张这不一定意味着模型崩了。我遇到更多的情况是判别器太强把生成器的梯度“压死”了。快速的解决办法就是降低判别器的学习率或者给判别器增加Dropout让它别学得太快。模式坍塌是另一个经典问题表现为生成图像千篇一律我用的一个简单有效的手段是minibatch discrimination或者直接换用WGAN的Wasserstein距离来做损失。# DCGAN生成器核心代码片段PyTorch风格 class Generator(nn.Module): def __init__(self, latent_dim100, ngf64): super().__init__() self.main nn.Sequential( nn.ConvTranspose2d(latent_dim, ngf * 8, 4, 1, 0, biasFalse), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf), nn.ReLU(True), nn.ConvTranspose2d(ngf, 3, 4, 2, 1, biasFalse), nn.Tanh() ) def forward(self, z): return self.main(z)在训练GAN的过程中我最大的心得是先在小数据集、小模型、低分辨率上把流程跑通再上大规模实验。很多人一上来就追求256x256的高清生成结果训练资源不够模型怎么都收敛不了最后连一个能看的实验结果都没有。从64x64开始验证模型能学到基本结构再逐步放大这个路径稳妥得多。4.3 生成模型落地时除了“看起来像”还该关注什么很多人只看生成图像好不好看我初期也是这样。但后来做过几个实际项目之后发现生成模型的评价维度远不止“像不像”。有几点特别值得关注尤其是准备把生成模型用到实际业务里的朋友覆盖度模型是不是只会生成同一张脸、同一种款式如果生成样本多样性不足说明模式坍塌再逼真也没用。隐空间的可编辑性能不能通过修改特定的隐变量维度控制生成结果的某个属性VAE和StyleGAN在这方面比普通DCGAN做得好得多。异常检测能力用生成模型做重建正常样本重建误差小、异常样本重建误差大这个特性可以用于工业质检、金融风控等场景。我在一个工业数据集上试过用自编码器重建误差做缺陷检测效果比传统的阈值方法好不少。我印象最深的一个项目是用无监督方法做“数据画像”某个数据集的标签缺失严重完全无法做有监督。我就先用自编码器把高维特征压缩到16维再用DBSCAN分簇最后每簇取中心样本做t-SNE可视化人工给每个簇打业务标签。整个过程没有任何人工标注参与最终产出的用户分群方案比之前拍脑袋分的合理得多。这件事让我彻底改变了看法无监督学习不是“在没有标签时的备选”而是“在挖掘未知结构时的首选”。5. 工具与学习路径从笔记本到可复现实验5.1 当前几个顺手的学习工具和经济选择我在系统学无监督学习时主要靠三套工具组合Python生态、MATLAB Deep Learning Toolbox、以及专门的图像数据集。Python生态里scikit-learn足够处理K-Means、DBSCAN、PCA、t-SNE这些经典方法语法统一、文档详细。真正进入深度无监督模型之后我转到PyTorch因为我个人觉得它的自动求导和模型定义方式更适合研究性实验调试起来也更透明。TensorFlow我也用过但最终PyTorch的print式调试更符合我的习惯。MATLAB的Deep Learning Toolbox值得单独提一句。很多人忽略它其实它在网络结构可视化、交互式调参、数据导入导出方面比纯Python方便不少尤其在快速做数据探索、教学演示的时候非常好用。MATLAB的Deep Network Designer可以拖拽式搭建自编码器或生成网络能看到每一层的激活结果和权重的可视化。我自己在笔记本上同时装了Python环境和MATLAB跑小型实验和可视化时用MATLAB大规模训练和写分布式脚本时用Python。两者各自发挥长板没必要站队。这里补充一下如果主要做工程落地那Python依然是重点MATLAB更适合快速验证想法和教学场景。5.2 推荐学习路径和资料无监督学习的资料很多但系统性强的少。我把自己实际跟过的几条路径整理一下参考价值比较直接李宏毅的深度学习课程他在YouTube上有完整的机器学习/深度学习课程其中“无监督学习”章节讲得相当清楚特别是用很生活化的例子解释生成模型对初学者极其友好。李航《统计学习方法》聚类、降维相关的经典方法讲得严谨适合想打基础的读者我把它当工具书用。Ian Goodfellow的《Deep Learning》第14章讲自编码器第20章讲生成模型是理解原理最重要的参考书但数学略难建议有一定基础再通读。PaperUnsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks也就是DCGAN原始论文非常值得精读。它没太多数学推导大量篇幅在讲架构设计和训练技巧尤其适合想搞懂“为什么我的GAN不收敛”这个问题的人。我的学习顺序是先看课程建立直觉再读论文对照自己的代码最后回到数据集上跑通一个小实验。很多人学习无监督学习最大的障碍不是资料不够而是资料太多导致不知道该按什么顺序看。我的建议很明确先是聚类和降维再是自编码器最后GAN。这个顺序是梯度的每一步都会用到上一步的直觉。5.3 写实验记录的两个习惯无监督学习实验和训练有监督模型有一个很大的不同没有明确的指标告诉你模型好不好。分类任务有准确率检测任务有mAP但无监督任务经常只有定性的观察。这让实验记录变得非常重要。我每次跑无监督实验都坚持记录三件事数据状态用的是什么数据集做过哪些预处理标准化参数是多少样本量多少。数据状态一旦变了结果就没有可比性。模型结构编码器每一层的通道数、卷积核大小、步长、激活函数、是否有BN层GAN的判别器和生成器的结构参数都要逐一记下来。超参数和Loss曲线学习率、batch size、优化器、权重初始化方式、Seed值以及每个epoch的Loss数值。哪怕当时觉得没用后面回头看会发现这些信息都是排查问题的关键线索。在跑GAN的时候我还习惯定期保存生成器的checkpoint并且固定一组测试噪声z。这样每隔几十个epoch把同一组z喂给生成器就能用一组固定序列的图像对比每个阶段的变化。这个习惯帮我避过好几次“训练了半天发现前100个epoch就已经崩了”的尴尬。6. 常见问题与排查技巧实录6.1 聚类结果的“稳定复现”问题无监督学习里一个很让人头疼的问题是为什么同一次实验我换了一台机器或者换了一个随机种子聚类结果就对不上这个问题的根源在于K-Means和初始化有关的随机性以及t-SNE这类随机优化算法的随机性。我的解决办法是全局固定随机种子import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42)但这不是万能的因为某些GPU算子在不同型号的卡上会有不同的浮点行为。所以在可复现要求极高的场合我倾向于把最终选定的结果重新用固定参数跑一遍哪怕只是保存一份最终结果也好过一遍一遍地跑出完全不一致的图。6.2 聚类结果到底怎么评估无监督聚类没有一个绝对正确的答案这让评估变得很微妙。我在实际项目里总结了一个三层评估思路内部指标轮廓系数、Davies-Bouldin指数、Calinski-Harabasz指数。这些指标全是基于距离计算的不需要真实标签但只能反映“簇内紧密、簇间分离”这个数学意义上的好坏不一定对应业务意义。外部指标如果有部分已知标签或业务规则哪怕只有一部分样本可以计算调整兰德指数ARI或互信息NMI。我在很多项目里都发现只用部分已知标签做验证比完全无监督地调参安全得多。人工检查把聚类结果通过t-SNE或者UMAP画出来结合业务人员经验人工确认每个簇是否合理。很多聚类算法在数值上很漂亮业务上一看就是一堆无法解释的样本混在一起。这一步虽然累但很必要。避坑提示不要只看一个指标就下结论。轮廓系数高的结果不一定业务可用轮廓系数低的结果也不一定完全不可用。多种评估方式交叉验证才靠谱。6.3 训练GAN时的“Loss不下降”恐慌我见过太多初学者在训练GAN时盯着Loss曲线一看判别器Loss从0.7涨到1.2就以为模型坏了立刻停止训练去调参。实际上GAN的训练过程本身就充满波动Loss不下降很可能只是因为你没有按固定的噪声z去对比生成图片。我有一套自己比较稳的排查顺序第一步先把固定z对应生成的图片打出来看看是不是比上一次epoch更清楚。如果图片在变好哪怕Loss曲线看起来很乱模型实际就在往正确的方向走。第二步判别器loss如果在0附近通常意味着判别器已经完全强大到轻易分辨真假生成器的梯度已经没啥用了。这时把判别器学习率调小或者给它加Dropout。第三步生成器loss如果出现NAN或者急剧增大检查生成器输出层激活函数是不是Tanh解码器最后一层有没有配对应的范围比如图像像素在[-1,1]。这是很多代码实现里容易出现低级错误的地方。我在DCGAN的复现中遇到最夸张的一次问题是生成器把输入噪声拉伸成了一个绿色的纯色矩形。追查之后发现是转置卷积的初始化值过大导致输出数值饱和到了Tanh的边界。把权重初始化改成均值为0、标准差0.02之后问题瞬间消失。这个细节在DCGAN论文的附录里面明确写了但很多人会漏掉。6.4 无监督特征可视化与解释的落地坑很多人在自编码器或对比学习run完之后准备把学到的特征可视化给业务方看结果发现完全解释不通。我遇到次数最多的情况是PCA或t-SNE图看起来类别分明但给业务方看的时候他们问一句“这两团之间有什么区别”你一时答不上来。要解决这个问题我研究出了一个比较有效的方法对每个簇做“特征归因”。先把原始特征标准化然后按簇分组计算每个簇在原始特征上的均值和全量数据均值做差取差异最大的10到20个特征作为该簇的“特征画像”。如果是图像数据可以把簇中心的样本图直接打印出来高亮那些重建误差大的区域。这样做之后业务方一眼就能看懂这个簇“大概是什么样的人”或“这批缺陷主要出现在哪个部位”。无监督学习最忌讳的就是“看起来好看但说不清楚”。不管用什么算法最后一定要能落到“这个簇的用户在哪些行为上有什么差异”这样具体的结论上。如果聚类结果解释不了哪怕指标再漂亮也要反过来重新调整特征或者聚类参数。这也是无监督学习在工程落地时最考验从业者功力的地方。我自己经历过很多次从“模型跑通”到“真正可用”的漫长调试。现在回头看无监督学习最吸引我的地方恰恰是它没有一个标准答案。每一个数据集都像是一块没有说明书的拼图而你学会的每个方法只是不同角度的光照帮你看清那些数据里本来就在、但从未被标注出来的形状。如果你正在学习这条线我的建议只有一条别急着把每个算法刷完找一个自己熟悉领域的数据集把聚类、降维、自编码器走一遍再把生成模型跑起来你会发现很多东西自然就连起来了。
返回列表