简介:SOM(自组织映射)是一种基于竞争学习的无监督神经网络,常用于非线性降维与数据可视化。以MATLAB为环境的SOM聚类资源,专为希望掌握SOM原理并快速上手的初学者设计,通过鱼类种类特征数据,演示了从网络创建、训练到结果可视化的完整流程。资源包共2个文件,以1个.m脚本和1个.mat数据文件为主,脚本内包含可直接运行的SOM聚类过程,mat文件提供示例特征数据,总体积仅2KB,轻量易用。已有756人学习下载,说明其在基础教学与快速验证场景中具有实用价值。读者可从中了解selforgmap、train等核心函数的使用,学会设置网络尺寸、迭代次数,并对映射结果进行聚类与评估;同时,代码结构简洁,便于在此基础上替换为自己的数据集,迁移至其他聚类任务,是入门SOM聚类的便捷起步包。
1. SOM聚类在MATLAB里到底解决什么问题
用k-means做聚类时,每个样本被强行分到一个离散中心,结果经常取决于初始质心和随机种子;层次聚类的树状图在高维下几乎糊成一片。SOM聚类则不同:它把高维样本映射到二维网格,原型向量按网格拓扑保持邻接关系,样本是“绕成两团”还是“连成一条带”,一眼能从拓扑图上判断出来。MATLAB里做SOM聚类,既可以用Deep Learning Toolbox的selforgmap,也可以不依赖工具箱手写训练循环。很多人在MATLAB里跑SOM,结果全是一坨或者没有结构,根源不在算法,而在网格尺寸、邻域半径衰减节奏和训练轮数这三个参数。这一篇就围绕这三个参数把SOM聚类讲透,最后给出一套量化误差和拓扑误差的验收办法。
2. SOM聚类原理与MATLAB工具箱的边界
2.1 SOM为什么能聚类:竞争学习与拓扑保持
SOM聚类(自组织映射)是Kohonen在1982年前后提出的竞争神经网络模型。输入一个样本后,网格上所有原型向量同时计算距离,距离最小的神经元胜出,这个胜出者称为BMU(Best Matching Unit)。和k-means聚类算法matlab实现的不同点在于:SOM胜出后不只是BMU自身朝样本移动,它周围的神经元也按照邻域核函数一起靠近样本。这个“一损俱损”的邻域更新机制,让相邻网格上的原型向量在数据空间里也彼此接近。
训练完成后,每个原型向量大致落在数据密度较高的区域,相当于用非参数方式逼近数据分布;同时,高维空间中相近的样本会被映射到网格上相近的BMU,这就是拓扑保持。反观k-means,每个簇中心彼此独立,簇之间是空洞还是平滑过渡完全看不出来。欧氏聚类也是如此,只按固定距离阈值划分邻域,没有“压缩到二维棋盘的顺序关系”。所以SOM聚类的适用场景很明确:数据维度高、样本分布有内部结构、你需要把聚类结果画出来给人看。如果只要一个类别编号然后建模,SOM不一定比kmeans快,但它的二维布局对后续可视化有不可替代的价值。
2.2 selforgmap的四参数:网格、轮数、邻域半径与拓扑函数
Deep Learning Toolbox里直接建SOM的入口是selforgmap,常见调用方式是这样:
net = selforgmap([8 8], 100, 3, 'hextop', 'linkdist'); net.trainParam.epochs = 200; net = init(net); net = train(net, X_T); % X_T:特征数×样本数,注意转置 y = net(X_T); % 竞争层输出,每个样本一个one-hot向量 bmuIndex = vec2ind(y); % 转成每个样本对应的BMU编号第一个参数是网格尺寸,这里创建一个8乘8的矩形网格,相当于64个聚类原型;100表示初始训练轮数,工具箱内部会按这个轮数做权重更新;3是初始邻域半径,意思是训练最初阶段,BMU周围3步以内的神经元都参与更新;最后两个参数指定网格拓扑和距离函数。训练完成后,net(X_T)返回的是one-hot形式的竞争层输出,需要vec2ind把每列里的1提取出来,才能得到bmudIndex。
| selforgmap参数 | 作用 | 常见取值 |
|---|---|---|
[8 8] | 网格行列数,原型总数=行×列 | 500行以下数据从[6 6]起步 |
100 | 初始训练轮数 | 100~500,轮数少了结构不稳定 |
3 | 初始邻域半径 | 网格较长边的一半左右 |
'hextop' | 网格拓扑 | hextop六边形、gridtop方形、randtop随机 |
'linkdist' | 邻域距离函数 | linkdist常配hextop,dist配gridtop |
这里有一条工具箱边界:selforgmap默认使用增量随机顺序训练,邻域高斯核、学习率衰减系数都封装在内部。我一般会先跑一次selforgmap快速看趋势,之后要调参再切到手写循环。另一个容易踩的坑是输入维度方向:selforgmap要求特征是行、样本是列,和多数业务数据的CSV存储方向相反,忘转置会导致训练后BMU编号全错或者误差不收敛。
提示:selforgmap输出的是one-hot编码,不是序号数组。后续统计命中频率时,记得先vec2ind一次,别直接拿net输出当索引用。
3. MATLAB手写SOM聚类:训练循环与参数控制
3.1 导入数据与标准化
既然要控制每一个衰减参数,手写SOM循环比工具箱更直观。数据准备和普通聚类没有区别,先用readtable导入,再提取特征矩阵,最后zscore标准化:
T = readtable('wine.csv'); % 假设CSV最后一列是类别标签 X0 = table2array(T(:, 1:end-1)); % 特征部分,样本数×特征数 X = zscore(X0); % 每列零均值单位方差这里zscore是必须的。SOM用欧氏距离衡量相似度,特征量纲一旦不一致,数值大的特征会主导BMU选择,其他特征等于被屏蔽。我见过不少案例是用原始数值跑SOM,结果部位误差和用量特征的权重八成以上,网格上只能分出“大数值特征”的层次,和真实类别对不上。
3.2 初始化网格权重
网格尺寸这里取[10 10],原型总数100。权重初始化应在数据范围内均匀随机,不能全置零,也不能用大数据集里几个随机样本直接充当原型:
G = [10 10]; % 网格行数×列数 W = rand(prod(G), size(X, 2)); % 原型矩阵,每行一个原型向量 W = W .* (max(X) - min(X)) + min(X); % 把随机权重拉到数据范围内 [gx, gy] = meshgrid(1:G(2), 1:G(1)); % 生成网格坐标 gx = gx(:); gy = gy(:); gridPos = [gx, gy]; % 网格上每个神经元的(x,y)坐标meshgrid生成坐标时,第一个参数是列方向,第二个是行方向,这里让gx对应列号、gy对应行号。后面找BMU时,BMU编号是W的行号,通过gridPos(bmu,:)能反查到该原型在网格上的位置,这是计算邻域距离的基础。初始化范围用训练数据的min和max,保证所有原型都落在同一个量纲区间内。
3.3 训练循环:找BMU、衰减、更新
核心训练循环我一般这样写,把学习率和邻域半径按指数衰减到预设终点:
maxEpoch = 300; lr0 = 0.9; lrEnd = 0.001; sigma0 = max(G) / 2; % 初始邻域半径取网格边长一半 sigmaEnd = 0.3; tau1 = maxEpoch / log(lr0 / lrEnd); tau2 = maxEpoch / log(sigma0 / sigmaEnd); for epoch = 1:maxEpoch lr = lr0 * exp(-(epoch - 1) / tau1); % 学习率指数衰减 sigma = sigma0 * exp(-(epoch - 1) / tau2); % 邻域半径指数衰减 for s = randperm(size(X, 1)) % 每个epoch都随机打乱样本顺序 d = sum((W - X(s, :)).^2, 2); % 当前样本到全部原型的欧氏距离 [~, bmu] = min(d); % 胜者神经元BMU dist2 = sum((gridPos - gridPos(bmu, :)).^2, 2); % 到BMU的网格距离平方 h = exp(-dist2 / (2 * sigma^2)); % 高斯邻域核 W = W + lr * h .* (X(s, :) - W); % 向量化更新 end end这段代码里,d是每一行一个原型的距离向量,min(d)返回距离和索引,bmu就是当前样本的胜者神经元编号。dist2利用预生成的gridPos计算所有网格节点到BMU的拓扑距离,h是高斯邻域核,BMU自身距离为0所以h=1,距离sigma越远的神经元更新幅度越小。最后一行实现“原型朝样本移动”,h是列向量,乘的时候对W的每一行乘不同权重。
提示:
lr * h .* (X(s, :) - W)里h是原型个数×1的列向量,X(s,:)-W是原型个数×特征数的矩阵,MATLAB自动做每行缩放。如果写成while循环遍历每个神经元逐个更新,结果一样但慢一个量级。
学习率从0.9降到0.001,前期快速铺开全局结构,后期只做精细调整。邻域半径从5降到0.3,sigma降到0.3以下时,只有BMU自己能明显更新,相当于训练散度接近结束。
3.4 样本映射成BMU编号,并可视化命中频率
训练完的W就是聚类原型集合。对新样本做推理时,找到距离最近的原型,它的网格坐标就是映射结果:
function bmu = mapSOM(W, X) d = pdist2(X, W); % 每行一个样本到所有原型的距离 [~, bmu] = min(d, [], 2); % 每行取最小距离对应的原型编号 end bmu = mapSOM(W, X); hit = accumarray(bmu, 1, [prod(G), 1]); % 统计每个原型的命中次数 imagesc(reshape(hit, G)); % 转回10×10网格形状 axis xy; % 修正y轴方向 colorbar;pdist2来自Statistics and Machine Learning Toolbox,没有的话可以手动写sum(X.^2,2) + sum(W.^2,2)' - 2*X*W',再sqrt开根号。accumarray把BMU编号转换成直方图,reshape后喂给imagesc,这一步已经进入matlab图像处理的常见操作:用热图看哪些原型被频繁命中、哪些从没被选中,死神经元会直接显示为深色格点。
| 脚本超参 | 默认值 | 调参区间 |
|---|---|---|
| G网格尺寸 | [10 10] | 500行数据[8 8]起 |
| maxEpoch训练轮数 | 300 | 100~500 |
| lr学习率 | 0.9→0.001 | 起点0.5~0.9,终点越小越好 |
| sigma邻域半径 | 5→0.3 | 起点取max(G)/2,终点0.2~0.5 |
4. 网格尺寸、邻域半径与学习率的参数实战
4.1 网格尺寸怎么定:多设比少设好
网格大小是SOM聚类里第一个要定的参数。一个常见错误是直接把网格设成期望类别数,比如数据有3类就建[1 3]网格,这样做出来的SOM等于带邻域惩罚的k-means,完全没有拓扑意义。我的经验是期望类别数为k时,网格原型数取k的1.5到3倍以上。
| 样本量 | 常用网格 | 原型数 | 注意事项 |
|---|---|---|---|
| < 300 | [5 5]~[6 6] | 25~36 | 过大网格会大量死神经元 |
| 300~3000 | [8 8]~[10 10] | 64~100 | 命中直方图应有脊状分布 |
| > 3000 | [12 12]以上 | 144+ | 建议用批处理SOM或Mini-batch更新 |
训练结束后看hit直方图,如果死神经元比例超过10%,说明网格偏大,原型没有足够样本支撑。如果命中集中在少数几个格子,先别急着缩小网格,检查一下是不是sigma衰减过快导致邻域更新在后期失效。
4.2 邻域半径:单调衰减,别用常数
邻域半径固定不变是SOM训练里最致命的错误。sigma保持不变时,训练后期BMU周围2步内的原型还在大幅移动,网格结构会一直“抖动”,无法收敛到细粒度布局。指数衰减和线性衰减的差别在于前期覆盖速度不同:
sigmaExp = sigma0 * exp(-(0:maxEpoch - 1) / tau2); sigmaLin = linspace(sigma0, sigmaEnd, maxEpoch); % 查看两种衰减曲线 plot(1:maxEpoch, sigmaExp, 'LineWidth', 2); hold on; plot(1:maxEpoch, sigmaLin, 'LineWidth', 2); legend('exp', 'linear');指数衰减的特点是前期快速收缩,把网格整体方向定下来,后半段缓慢调整,适合数据在空间中分布比较均匀的情况。线性衰减在前中期下降更缓,网格会有更长的“多神经元联动”时间,适合全局结构复杂、容易散落成一堆孤立片段的场景。初始sigma0取网格最长边的一半,能保证第一个epoch里BMU周围邻域覆盖到全图,不会出现网格折纸现象。
4.3 学习率:前期大步走,后期精细调
SOM对学习率的敏感度低于邻域半径,但固定学习率会造成原型在最优位置附近震荡。kmeans聚类算法matlab的实现可以用kmeans(X, k, 'Replicates', 10)在一次调用里避免局部最优,而手写SOM没有这种便捷选项,只能在衰减上做文章。
学习率指数衰减里tau1控制衰减快慢。tau1太小,学习率从0.9降到0.001只需要十几轮,后期原型基本不动,训练轮数再多也没意义;tau1太大,后期学习率还有0.1以上,原型一直波动。按我常用的公式,tau1 = maxEpoch / log(lr0 / lrEnd),300轮时tau1约等于39,意味着每39轮学习率下降一个e量级,前100轮完成大部分结构学习,后200轮用于收敛细调。一个可操作的验证点:把训练轮数设成500,对比epoch=200和epoch=500的量化误差,如果两者差小于5%,说明学习率衰减的设置是合理的,网格容量才是决定上限的因素。
4.4 与层次聚类python、子空间聚类的边界
既然是SOM聚类,说明关注点在于“聚类后的空间关系”。scipy.cluster.hierarchy里的层次聚类适合小样本、需要级联树状图的场景,但它对样本间距离矩阵的复杂度是O(n²),上万样本直接慢到不可用。子空间聚类概述里常说的稀疏子空间聚类(SSC)适合数据确实落在多个低维子流形上的情况,理论上比SOM严谨,但谱聚类步骤的计算量和调参成本高出很多。
SOM聚类的定位介于两者之间:不做强分布假设,又能给出二维布局。如果下游任务只需要类别标签,k-means更快;如果要做聚类热图、趋势图之类的展示,SOM按BMU坐标排序后行的过渡天然平滑,比层次聚类需要额外裁枝更省力。这个取舍在工程上是SOM最实际的价值,也是我推荐先用SOM做探索性分析的原因。
5. 用量化误差和拓扑误差检验SOM聚类结果
5.1 两个误差指标的MATLAB算式
训练是否到位不能只看聚类图漂亮与否,要用两个客观指标:量化误差(QE)和拓扑误差(TE)。量化误差衡量所有样本到其BMU的平均距离,越小表示原型拟合数据越紧;拓扑误差衡量“第二近的原型不是BMU的网格邻居”的样本比例,越小表示高维邻近关系在网格上保留得越好。
function [qe, te] = somError(W, X, bmu, gridAdj) qe = mean(sqrt(sum((X - W(bmu, :)).^2, 2))); % 样本到BMU的平均距离 te = 0; for i = 1:size(X, 1) [~, idx] = sort(sum((W - X(i, :)).^2, 2)); % 距离从小到大排序 if ~gridAdj(idx(1), idx(2)) % 第二近邻不在BMU邻域 te = te + 1; end end te = te / size(X, 1); end调用前用网格坐标生成邻接矩阵:两神经元在网格上曼哈顿距离为1时视为相邻。
gridAdj = (abs(gy - gy') == 1 & gx == gx') | ... (abs(gx - gx') == 1 & gy == gy'); [qe, te] = somError(W, X, mapSOM(W, X), gridAdj);注意gridAdj是逻辑矩阵,第(i,j)个元素为true表示网格上第i个和第j个原型相邻。拓扑误差公式里,第二近邻是指距离第二小的那个原型,如果它不是BMU的网格邻居,说明高维空间里的“第二选项”在网格上被放到了远处,拓扑关系出现了撕裂。
5.2 结合命中频率与轮廓系数的验收顺序
网络训练完,先打印hit直方图、QE和TE三个结果。QE低但TE高,说明原型位置和原始数据拟合得很好,但网格布局乱成一团,典型原因是sigma0太小或网格拓扑选择不当;QE高但TE低,说明网格骨架正确但原型还没靠拢数据,需要增加训练轮数或调低学习率终点。两个指标都差,先回头查数据标准化,zscore漏掉是最容易糊弄到最后才发现的错误。
轮廓系数(silhouette)可以拿来和kmeans聚类算法matlab做横向对比:把SOM聚类结果当作簇标签,调用silhouette(X, bmu)算出轮廓值均值,再和kmeans(X, k, 'Replicates', 10)的结果比较。两者差距在0.05以内都算正常,因为SOM的原型受到网格邻域约束,簇间边界会被平滑,牺牲少量簇内紧致度以换取拓扑可视性。如果SOM的轮廓系数比k-means低很多,优先检查是否选错了样本顺序和归一化方式,不必急着换层次聚类python实现。先把命中频率直方图和TOP误差打印出来,看两条误差曲线同时变平了,再往下游做聚类热图排序和业务指标对比。
本文还有配套的精品资源,点击获取