多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

自组织映射SOM算法:无监督神经网络聚类与降维可视化实战

自组织映射SOM算法:无监督神经网络聚类与降维可视化实战 简介这份资源面向机器学习初学者与需要做数据可视化、聚类分析的开发者提供完整的自组织映射SOM算法Python实现。SOM是一种无监督神经网络方法可将高维数据映射到二维网格并保持拓扑结构常用于降维、聚类与可视化探索。代码包含网络初始化、BMU查找、权重更新等核心流程并支持高斯、墨西哥帽、气泡三种邻域函数以及指数与线性两种衰减策略。资源包共31个文件以9个py源码、10张png可视化图、5个csv数据集为主另有md说明与txt依赖清单压缩包约3.55MB。示例覆盖螺旋数据聚类、鸢尾花降维、RGB颜色聚类及参数对比输出权重网格、U-Matrix、激活图与训练历史等图像并给出量化误差、拓扑误差、邻域保持度等评估指标。已有84人学习适合希望理解SOM原理并快速上手实验的读者。1. 自组织映射 SOM 算法无监督神经网络聚类与降维可视化到底在做什么如果你手上有一批没有标签的高维数据比如几十维的客户行为特征、上百维的文本向量、上千维的传感器采样直接扔给 KMeans 往往效果一般因为欧氏距离在高维空间会变得稀疏而失去区分度。自组织映射 SOMSelf-Organizing Map就是为这种场景准备的它是一种无监督的神经网络能把高维输入映射到一个低维通常是二维的规则网格上同时保留原始数据的拓扑关系——相近的样本会落到相邻的神经元上。这意味着你既拿到了聚类结果又拿到了一张可以直接看的“数据地图”降维可视化一步到位。SOM 和 PCA、t-SNE 的区别在于PCA 是线性投影t-SNE 擅长局部结构但全局拓扑容易失真而 SOM 通过竞争学习让整个网格自适应地铺展在数据流形上既能看全局分布又能放大局部密度差异。它适合谁适合做探索性数据分析的工程师、需要给业务方展示“数据长什么样”的算法同学以及想用 Python 快速跑通神经网络聚类和降维可视化的入门者。下面从原理到代码把这条路走通。2. SOM 的竞争学习机制与 Python 实现选型2.1 权重向量、竞争与邻域更新三个核心动作SOM 的网络结构很简单一个二维的神经元网格每个神经元持有一个和输入维度相同的权重向量。训练时对每个输入样本做三件事。第一竞争计算样本与所有神经元权重的距离距离最小的那个神经元获胜称为 BMUBest Matching Unit。第二协作不只是更新 BMU还更新它周围邻域内的神经元邻域半径随训练轮次衰减。第三自适应更新公式为w(t1) w(t) α(t) * h(t) * (x - w(t))其中 α 是学习率h 是邻域函数通常是高斯核两者都随迭代衰减。这个机制的关键在于邻域函数早期邻域很大整个网格一起向数据靠拢形成粗略的全局拓扑后期邻域收缩到几乎只有 BMU 自己权重向量精细地贴合局部样本分布。学习率和邻域半径的衰减策略直接决定最终地图质量这也是后面调参的重点。2.2 用 MiniSom 还是手写 NumPy选型对比Python 生态里做 SOM 有几个选择。MiniSom 是最轻量的库纯 NumPy 实现安装无依赖负担API 简洁适合快速验证和中小规模数据。SOMPY 功能更全但维护活跃度一般。sklearn 没有内置 SOM所以不要指望from sklearn.som import。如果你要深入理解算法或做定制化邻域函数手写 NumPy 版本反而更可控。方案安装方式适合场景自定义难度MiniSompip install minisom快速验证、中小数据、教学低可继承重写手写 NumPy无需安装理解原理、定制邻域/衰减高完全可控SOMPYpip install sompy需要内置可视化中我一般先用 MiniSom 跑通看效果如果邻域策略需要改或者要嵌入到已有训练管线里再换成手写版本。下面两节分别给出可复现的代码。2.3 手写 NumPy 版 SOM最小可运行代码import numpy as np def som_train(X, grid_h10, grid_w10, epochs100, lr00.5, sigma0None): X: (n_samples, n_features) 输入数据 grid_h, grid_w: 神经元网格尺寸 epochs: 训练轮数 lr0: 初始学习率 sigma0: 初始邻域半径默认取网格较大边的一半 n_samples, n_features X.shape # 初始化权重从数据分布中随机采样比全零或纯随机更稳 idx np.random.choice(n_samples, grid_h * grid_w, replaceFalse) weights X[idx].reshape(grid_h, grid_w, n_features).copy() if sigma0 is None: sigma0 max(grid_h, grid_w) / 2.0 # 预计算神经元坐标用于邻域距离 coords np.array([[i, j] for i in range(grid_h) for j in range(grid_w)]) for epoch in range(epochs): # 学习率和邻域半径指数衰减 lr lr0 * np.exp(-epoch / epochs) sigma sigma0 * np.exp(-epoch / epochs) # 每个 epoch 打乱样本顺序 for x in X[np.random.permutation(n_samples)]: # 竞争找 BMU diff weights.reshape(-1, n_features) - x # (grid_h*grid_w, n_features) dist np.linalg.norm(diff, axis1) bmu_idx np.argmin(dist) bmu_coord coords[bmu_idx] # 协作高斯邻域 d2 np.sum((coords - bmu_coord) ** 2, axis1) h np.exp(-d2 / (2 * sigma ** 2)) # 自适应更新所有权重 h h.reshape(grid_h, grid_w, 1) weights lr * h * (x - weights) return weights # 生成测试数据三个高斯簇 np.random.seed(42) c1 np.random.randn(200, 2) np.array([0, 0]) c2 np.random.randn(200, 2) np.array([5, 5]) c3 np.random.randn(200, 2) np.array([0, 5]) X np.vstack([c1, c2, c3]) weights som_train(X, grid_h10, grid_w10, epochs100) print(训练完成权重形状:, weights.shape)这段代码把竞争、协作、自适应三个动作完整实现了一遍。几个参数需要说明lr00.5是初始学习率数据标准化后这个值通常够用如果数据尺度大可以降到 0.1sigma0默认取网格较大边的一半保证早期邻域覆盖足够广epochs100对几百到几千样本够用样本上万时建议加到 200 以上。注意权重初始化从数据中随机采样这比全零初始化收敛更快也避免了所有神经元初始状态相同导致的对称性问题。2.4 MiniSom 版十行代码跑通聚类与可视化from minisom import MiniSom import numpy as np import matplotlib.pyplot as plt # 沿用上面的 X som MiniSom(x10, y10, input_len2, sigma1.5, learning_rate0.5) som.random_weights_init(X) som.train_random(X, num_iteration5000) # 绘制 U-Matrix距离矩阵颜色越深表示神经元间距离越大 plt.figure(figsize(8, 6)) plt.pcolor(som.distance_map().T, cmapbone_r) plt.colorbar() # 把每个样本映射到 BMU 并标在图上 markers [o, s, D] colors [r, g, b] for i, x in enumerate(X): w som.winner(x) plt.plot(w[0] 0.5, w[1] 0.5, markers[i // 200], markerfacecolorNone, markeredgecolorcolors[i // 200], markersize8, markeredgewidth1.5) plt.title(SOM U-Matrix with Sample Mapping) plt.show()MiniSom 的distance_map()返回的是每个神经元到其邻居的平均距离这就是 U-Matrix。颜色深的区域是簇之间的边界颜色浅的区域是簇内部。样本映射用som.winner(x)拿到 BMU 坐标叠加在 U-Matrix 上就能直观看到三个高斯簇被分到了地图的不同区域。sigma1.5和learning_rate0.5是 MiniSom 的推荐起点num_iteration5000对 600 个样本足够样本量增大时按比例增加。3. 把 SOM 用到真实数据标准化、网格尺寸与量化误差3.1 数据标准化不做这一步后面全是玄学SOM 基于欧氏距离如果某个特征的量纲是另一个的几百倍距离计算会被它主导其他特征等于没参与。常见做法是 Z-score 标准化让每个特征均值为 0、方差为 1。对于有偏分布的特征可以先做对数变换再标准化。注意标准化参数必须从训练集计算然后应用到验证集和测试集否则会引入数据泄漏。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X) # 如果特征有偏先做 log1p 再标准化 X_log np.log1p(np.abs(X)) * np.sign(X) X_log_scaled StandardScaler().fit_transform(X_log)fit_transform在训练集上同时计算均值和方差并转换验证集只能用transform。这一步看起来简单但我见过太多人直接把原始数据扔进 SOM然后抱怨“聚类结果不稳定”——十有八九是量纲问题。3.2 网格尺寸怎么定经验公式与量化误差曲线网格太小多个簇会被挤到同一个神经元网格太大训练慢且地图稀疏。经验规则是神经元数量约为样本量的平方根到五倍平方根之间。比如 600 个样本sqrt(600)≈24网格可以取 5x5 到 10x10。更靠谱的做法是画量化误差Quantization Error曲线量化误差是每个样本到其 BMU 权重的平均距离网格越大误差越小但边际收益递减拐点就是合适的尺寸。def quantization_error(som, X): 计算平均量化误差 qe 0.0 for x in X: w som.winner(x) qe np.linalg.norm(x - som.get_weights()[w[0], w[1]]) return qe / len(X) # 测试不同网格尺寸 for size in [5, 8, 10, 15, 20]: som MiniSom(size, size, input_lenX_scaled.shape[1], sigma1.5, learning_rate0.5) som.random_weights_init(X_scaled) som.train_random(X_scaled, num_iteration5000) qe quantization_error(som, X_scaled) print(f网格 {size}x{size}, 量化误差: {qe:.4f})量化误差随网格增大而下降但当它下降变缓时再增大网格只是增加计算量而不提升表达能力。我一般选误差下降曲线拐点前一到两档的尺寸兼顾效果和速度。3.3 拓扑误差比量化误差更能反映地图质量量化误差只看样本到 BMU 的距离不关心 BMU 之间是否相邻。拓扑误差Topographic Error衡量的是样本的第一 BMU 和第二 BMU 在网格上是否相邻。如果不相邻说明地图的拓扑保持得不好。计算方式是统计不相邻的比例越低越好。def topographic_error(som, X): 计算拓扑误差第一和第二 BMU 不相邻的样本比例 weights som.get_weights() grid_h, grid_w weights.shape[:2] te 0 for x in X: # 计算到所有神经元的距离 dists np.linalg.norm( weights.reshape(-1, weights.shape[-1]) - x, axis1) sorted_idx np.argsort(dists) bmu1 np.unravel_index(sorted_idx[0], (grid_h, grid_w)) bmu2 np.unravel_index(sorted_idx[1], (grid_h, grid_w)) # 判断是否相邻曼哈顿距离为 1 if abs(bmu1[0] - bmu2[0]) abs(bmu1[1] - bmu2[1]) 1: te 1 return te / len(X)拓扑误差低于 0.1 通常说明地图质量不错。如果偏高可以增大初始邻域半径或增加训练轮数让邻域收缩更平滑。4. 踩坑记录SOM 训练中五个高频翻车现场4.1 所有样本映射到同一个神经元现象训练完发现大部分样本的 BMU 是同一个地图上只有一个区域有映射。原因通常是学习率太大或邻域半径衰减太快导致权重向量还没来得及铺开就锁死了。解决把lr0降到 0.1 到 0.3sigma0设为网格较大边的一半以上并确保epochs足够让衰减走完。另外检查数据是否已经标准化。4.2 地图出现“死神经元”现象某些神经元从未成为任何样本的 BMU权重一直停在初始值。原因是初始化时这些神经元恰好落在数据稀疏区或者邻域更新时被“遗忘”。解决改用从数据中采样的初始化方式或者训练结束后统计每个神经元的命中次数对死神经元做二次初始化再补训几轮。MiniSom 的random_weights_init就是从数据中采样比纯随机好很多。4.3 聚类结果每次跑都不一样现象同样的数据和参数两次运行得到的聚类边界差异明显。SOM 本身是随机算法初始化不同结果会有波动但如果波动大到影响结论说明参数设置有问题。解决固定随机种子增加训练轮数让收敛更充分或者用多次运行取量化误差最小的那次。如果数据本身簇结构不明显SOM 的结果本来就会不稳定这时候要考虑是不是不该用聚类。4.4 高维数据下 U-Matrix 看不出结构现象输入维度上百维时U-Matrix 一片模糊看不出明显的簇边界。原因是高维空间中距离集中效应导致神经元间距离差异被压缩。解决先用 PCA 降到 10 到 30 维再喂给 SOM既保留了主要方差又缓解了距离集中。注意 PCA 要在标准化之后做且降维后的维度不要低于 5否则会丢失太多信息。4.5 把 SOM 当分类器用现象有人拿 SOM 的 BMU 标签直接当预测结果发现准确率很低。SOM 是无监督聚类它不优化分类边界BMU 只代表“最相似的神经元”不代表类别。解决如果要做分类用 SOM 做特征提取或降维把 BMU 坐标或权重向量作为新特征喂给有监督分类器或者用学习向量量化LVQ在 SOM 基础上做有监督微调。5. 进阶技巧用 SOM 命中矩阵做聚类归组与可视化增强5.1 命中矩阵与聚类归组训练完 SOM 后每个神经元有一个命中次数多少样本以它为 BMU。命中矩阵本身就是一个降维后的密度图可以直接用来做聚类对命中矩阵做层次聚类或 KMeans把相邻的高密度神经元归为一组就得到了样本的簇标签。这比直接在原始高维空间聚类更稳因为 SOM 已经做了拓扑平滑。from scipy.cluster.hierarchy import linkage, fcluster # 构建命中矩阵 hits np.zeros((10, 10)) for x in X_scaled: w som.winner(x) hits[w] 1 # 对命中矩阵做层次聚类 # 把 10x10 展平成 100 个点每个点的特征是其命中次数 flat_hits hits.reshape(-1, 1) Z linkage(flat_hits, methodward) clusters fcluster(Z, t3, criterionmaxclust) cluster_map clusters.reshape(10, 10) # 可视化聚类结果 plt.figure(figsize(8, 6)) plt.pcolor(cluster_map, cmapSet3) plt.colorbar() plt.title(SOM Hit-Matrix Clustering) plt.show()这里用层次聚类对命中矩阵分组t3表示期望分成 3 簇。实际使用时可以用轮廓系数或肘部法确定簇数。归组后每个样本的簇标签就是其 BMU 所在神经元的簇标签。这个方法的好处是簇的形状不受限于球形SOM 的拓扑保持让任意形状的簇都能被捕捉到。5.2 用 BMU 坐标做二维散点可视化除了 U-Matrix另一种直观的可视化是把每个样本的 BMU 坐标网格上的 x、y当作二维嵌入用散点图画出来颜色用真实标签或聚类标签。这样得到的图和 t-SNE 类似但计算量小得多而且新样本可以直接通过som.winner()映射进来不需要重新训练。# 用 BMU 坐标做散点图 bmu_coords np.array([som.winner(x) for x in X_scaled]) plt.figure(figsize(8, 6)) plt.scatter(bmu_coords[:, 0] np.random.randn(len(X)) * 0.1, bmu_coords[:, 1] np.random.randn(len(X)) * 0.1, cnp.array([0]*200 [1]*200 [2]*200), cmapviridis, alpha0.6, s30) plt.title(SOM BMU Coordinates as 2D Embedding) plt.xlabel(Grid X) plt.ylabel(Grid Y) plt.colorbar(labelTrue Cluster) plt.show()加一点随机抖动是为了避免同一点上多个样本完全重叠。这张图能直接看出三个簇在网格上的分布如果簇之间有明显的空白带说明 SOM 成功分开了它们。5.3 增量映射与新样本处理SOM 训练好后新样本不需要重新训练整个网络直接调用som.winner(new_x)就能拿到 BMU 坐标进而得到簇标签或二维嵌入。这是 SOM 相比 t-SNE 的一个实用优势t-SNE 没有自然的 out-of-sample 扩展而 SOM 有。注意新样本必须用训练时的标准化参数做同样的预处理否则距离计算会偏。我自己的习惯是每次做完 SOM 训练先把量化误差和拓扑误差记下来作为后续调参的基线然后把命中矩阵和 BMU 散点图各存一份方便和业务方沟通时直接展示。这套流程在客户分群、异常检测、文本主题探索里都跑通过最深的教训是——标准化和网格尺寸这两件事没做对后面所有可视化都是自欺欺人。希望帮到你。本文还有配套的精品资源点击获取
返回列表