☰
基于Matlab的竞争神经网络聚类分析:从原理到调参避坑
2026/10/17 5:09:42 网站建设 项目流程

看到“基于Matlab的CNN竞争神经网络的聚类分析”这个标题,我第一反应是:这个CNN到底是指卷积神经网络(Convolutional Neural Network),还是竞争神经网络(Competitive Neural Network)?再往后看“聚类分析”四个字,答案就清楚了——这里说的确实是竞争神经网络,一套不需要标签就能把数据自动分群的无监督方法。用Matlab来做竞争神经网络的聚类分析,是我在多个数据处理项目里反复用到的方案,流程短、见效快、结果也直观。这篇文章我想把从概念到代码再到调参避坑的完整经验都整理出来,帮助那些刚接触神经网络聚类、或者想找一个能直接落地的聚类工具的朋友,少走点弯路。

1. 标题里的CNN是什么:先把这个概念彻底讲清楚

1.1 竞争神经网络与卷积神经网络:两个不同物种

神经网络领域里缩写撞车是很常见的事,CNN就是这样。绝大部分人一看到CNN,默认就是做图像识别的卷积神经网络,但竞争神经网络是完全不同的另一个东西。

卷积神经网络是监督学习模型,它需要大量带标签的数据,通过反向传播不断调整卷积核参数,目标是学习输入到输出的映射关系,典型的应用场景是图像分类、目标检测、语音识别。而竞争神经网络是无监督学习模型,它的训练数据不需要任何标签,网络自己通过神经元之间的竞争来发现数据内部的结构。打个不太严谨但容易理解的生活类比:卷积神经网络像是一个被老师带着反复做题、最后能识别题型的学生;竞争神经网络则像是一个没有人告诉他对错、只能自己观察总结规律的学习者。

从网络结构上看,竞争神经网络通常只有两层,输入层加竞争层,竞争层的每个神经元都保存着一个权值向量。它的核心机制被称作“胜者为王”,英文叫Winner-Take-All,简称WTA。每次输入一个样本,竞争层所有神经元都会去计算自己的权值向量和该样本的距离,谁距离最近谁就获胜,然后在竞争中获胜的神经元才有资格更新自己的权值。这个机制决定了竞争神经网络天然适合做聚类,也和K-Means算法有着千丝万缕的联系。

1.2 竞争神经网络是怎样完成聚类的

想要真正理解竞争神经网络,最值得抓住的一条主线就是:竞争层神经元的权值向量,本质上就是一组动态迭代出来的聚类中心。

拿一个最简单的例子来说。假设我们要把一群用户分成三组,那么竞争层就放三个神经元。刚开始这三个神经元的权值向量是随机初始化的,基本可以理解为三个随便猜出来的聚类中心。每送入一个用户的特征向量,三个神经元分别计算一下自己跟这个用户的距离,距离最小的那个神经元获胜,然后它就把自己的权值向量往这个用户的方向拉近一点,更新的幅度由学习率控制。权值更新的公式长这样:

w_new = w_old + lr * (x - w_old)

其中x是当前输入样本,lr是学习率。理解这个公式有几个关键点。第一,只有获胜神经元的权值会更新,输掉的神经元原地不动,这就是“竞争”的残酷之处。第二,权值向量只会往输入样本的方向靠近,绝不会离数据而去。第三,随着训练样本不断送入,每个神经元的权值向量会逐渐移动到它所代表的那堆样本的中心区域。

训练结束后,每个神经元的权值向量就稳定在某个数据簇的中心位置。预测阶段就很好办了,把任意一个新样本送进去,算出它和每个神经元权值向量的距离,距离最近的那个神经元编号,就是这个样本的聚类标签。一句话总结:聚类中心的确定过程,就是竞争层神经元权值的收敛过程。

1.3 竞争神经网络与K-Means的关系,以及各自的优劣

说到聚类中心,很多人会立刻想到K-Means。确实,竞争神经网络和K-Means之间的血缘关系非常近,可以认为竞争神经网络是K-Means的一种在线版本。

K-Means的思路是迭代两步:第一步把每个样本分给最近的聚类中心,第二步重新计算每个簇的平均值作为新的聚类中心,然后循环。竞争神经网络则没有这种显式的两阶段划分,它每看到一个样本就实时更新一次。K-Means用簇内所有样本的平均值来更新中心,竞争神经网络只用当前这一个样本的梯度方向来更新,因此在线的性质更强,可以支持流式数据,来一个样本更新一次,不用把全部数据反复重扫。

不过,纯粹的竞争神经网络在聚类任务上有一个非常明显的短板:它完全抛弃了神经元之间的空间关系。本来距离很近的两个聚类中心,可能在竞争层中标号差了十万八千里。想解决这个问题,就得看下一章的SOM自组织映射了。

1.4 什么场景适合用竞争神经网络做聚类

根据我的实际使用经验,竞争神经网络最舒服的适用区间有这么几类:

  • 手头的数据没有标签,又不想费人力去标注,只想先探索一下数据分成几堆比较自然。
  • 需要在线学习的场景。比如传感器数据流持续过来,希望聚类模型能不断吸收新样本,而不是隔一段时间重训一次。
  • 维度不算特别高,一般在几十维以内,数据量也比较可观,希望快速有个能用的基线模型。
  • 想给高维数据一个可解释的二维拓扑展示,这一步尤其适合用SOM,因为能把高维空间压缩到二维网格上还能保持近邻关系。

它也有不太适合的时候。数据维度非常高,比如几千维的稀疏文本向量,或者对聚类结果的概率输出有硬性要求,这些时候竞争神经网络就不是最优选了,可以考虑谱聚类或者更现代的深度嵌入聚类方案。

2. 方案选型与准备工作:为什么用Matlab,该用哪个工具箱

2.1 选Matlab的三个核心理由

我最初接触竞争神经网络时也犹豫过是用Python还是Matlab,后来在实际项目中反复对比,还是觉得Matlab有它无可替代的地方。

第一是神经网络工具箱把整个流程封装得极其干净。创建网络、设置参数、训练、仿真,前前后后只需要十几个函数调用,没有复杂的依赖关系。Python那边虽然有scikit-learn能调SOM或者自己手写竞争层,但要做可视化、做拓扑图、做距离矩阵,需要拼接的库就明显更多了。

第二是内置可视化函数对结果诊断的帮助巨大。比如plotsomhits可以直接画出一个网格上每个神经元命中了多少个样本,plotsomnd画出U-Matrix距离图,这些工具自己用Python从零实现要耗费不少时间,而MATLAB一行代码就能出来。做聚类的同学都清楚,聚类效果好不好,光看数字指标还不够,眼见为实的可视化工具有多重要。

第三是矩阵化编程非常适合快速做参数对比实验。竞争神经网络本身结构简单,数据预处理、训练、评估用脚本串起来,改一个参数重新跑一遍的成本很低,特别适合我这种喜欢做大量对照实验的人。

2.2 newc还是selforgmap:纯竞争层与自组织映射的取舍

用MATLAB做竞争神经网络聚类时,工具箱里最常见的两个选择是newc和selforgmap。它俩的关系可以这么理解:newc是那个最基础的纯竞争层网络,只有WTA机制,没有邻域概念;而selforgmap是SOM自组织映射,它在竞争机制之上增加了邻域更新——获胜神经元不仅自己更新权值,它周围的神经元也会跟着更新,只是一开始影响范围较大,随着训练推进逐渐收紧到一个神经元。

这两者的适用场景差异非常明显,我整理了一张对比表:

对比维度newc纯竞争层selforgmap自组织映射
神经元是否考虑空间邻域不考虑,各自独立考虑,同一邻域内协同更新
聚类稳定性差,容易出现死神经元好,邻域机制缓解死神经元
收敛速度快,结构简单相对慢,需要较多迭代
可视化能力弱,基本只能看指标强,有专门的拓扑可视化函数
适合场景快速验证、类别数已知、数据简单真正把聚类当正式课题做、需要解释性
主要风险权值初始化敏感、死神经元多参数多,需要调优

我的经验是,做演示或快速验证概念时用newc够用,但一旦要为业务决策提供依据,老老实实用selforgmap,后面的结果解读和可视化会让你省心得多。

2.3 数据预处理比网络结构更值得花时间

做聚类的人往往喜欢纠结网络结构、神经元数量,但根据我踩坑的经验,数据预处理对结果的影响往往比网络结构本身更大,甚至大到一票否决的程度。竞争神经网络计算距离时用的是欧氏距离,这个细节直接决定了对量纲极敏感的特性。

举个例子,比如某个数据集中特征A的取值范围是0到1,特征B的取值范围是100到10000,那么计算欧氏距离时,特征B的差异会完全淹没特征A的差异,聚类结果几乎等于只看特征B。所以归一化是绕不开的一步。我常用的做法是mapminmax把每个特征线性缩放到0到1区间,或者用zscore标准化成均值为0标准差为1。这两种方式选哪个,取决于后续是否要保持特征的相对物理比例,如果只是纯粹做距离聚类,zscore的稳健性会更好一点。

还有一个容易忽略的点是异常值。聚类本质上是在找数据的自然分布结构,一个离群很远的值会把某个神经元的权值向量拉得偏离正常区域,甚至间接导致其他簇的划分失衡。所以在训练之前,建议先用箱线图或者简单百分位看一下特征分布,对明显异常的值做截断或者剔除。至于那些缺失值,最简单的均值填充就够了,毕竟聚类对个别噪声的容忍度比监督学习要高一些。

3. 完整实操流程:从零开始用SOM做聚类

这一章我直接用Matlab自带的经典鸢尾花数据集走一遍完整流程,这个数据集包含150个样本,每个样本有4个特征,真实类别有3种。虽然它有真实标签,但我把标签只作为训练后的验证手段,训练过程中完全不使用。

3.1 加载数据与归一化

第一步是加载数据并做预处理。鸢尾花数据集的4个特征分别是花萼长度、花萼宽度、花瓣长度、花瓣宽度,取值范围差异明显,花瓣相关的特征数值明显偏大,必须归一化。

clear; clc; rng(0); % 加载鸢尾花数据集 load fisheriris X = meas'; % 4x150,每列是一个样本 trueLabel = species; % 用于训练后验证,不参与训练 % 归一化到 [0,1] 区间 [Xn, ps] = mapminmax(X, 0, 1);

这里有一个细节需要注意:mapminmax默认是按行处理,也就是每个特征对应一行。所以我把原始数据转置了一下,让meas变成4行150列的矩阵,每一行是一个特征维度,每一列是一个样本。这是整个MATLAB神经网络工具箱的基本数据格式——样本按列排,特征按行排,很多新手在这里栽过跟头,训练结果全是NaN,排查了半天发现是数据方向搞反了。

3.2 创建SOM网络并理解每一个参数

创建SOM网络用selforgmap,一个典型的调用是这样:

net = selforgmap([6 6], 100, 3, 'hextop', 'linkdist'); net.trainParam.epochs = 200;

第一参数[6 6]是SOM输出网格的尺寸,代表6乘6共36个竞争神经元。网格为什么要用方块状?因为SOM的可视化都是在二维网格上展示的,方形网格方便观察拓扑关系。神经元数量到底取多少合适,后面我会专门做一组对照实验,这里先取36个。

第二个参数100是覆盖步数,这个参数在文档里表述比较抽象,我理解为一个对邻域和学习率进行调度的时间基准:训练初期,获胜神经元的影响范围会从这里出发逐渐收窄。取值太小,邻域收缩太快,拓扑排序没做完;取值太大,收敛慢。第三个参数3是最初的邻域半径,单位是网格上的神经元距离。

后面的'hextop'和'linkdist'分别指定网格拓扑为六边形结构、距离计算方式为连接距离。六边形拓扑在SOM里比较常用,因为它的邻域关系比正方形更自然,每个神经元都有6个邻居,视觉上也更紧凑。

创建好网络后还要设置迭代轮数。默认epochs通常是200,但训练样本越多、网格越大,需要的迭代也越多。我这里保持200不动,后面实验时再对照观察。

3.3 训练过程怎么看、怎么判断收敛

训练代码很简单,调用train就行:

net = train(net, Xn);

训练时MATLAB会弹出一个训练进度窗口,可以看到当前迭代到第几轮、训练耗费的时间。SOM的训练和反向传播网络不一样,它没有一个传统的“损失函数”在单调下降,所以不能只看误差曲线。我判断收敛的方式一般有两个:一是看训练完成后权值是否还在明显变化,把epochs设大一点,如果后面的迭代基本不再改变权值分布,说明已经收敛;二是直接看可视化结果,如果plotsomhits图中各个神经元的命中分布比较均匀,没有明显的未激活区域,通常说明训练比较充分。

另外还应该理解SOM训练内置的两个阶段。第一阶段是排序阶段,训练早期学习率大、邻域半径大,目的是让权值向量快速铺满数据所在的空间,建立大致的拓扑秩序。第二阶段是微调阶段,学习率调小、邻域收窄到1,只对获胜神经元本身做精细调整。这种两阶段设计是SOM能同时保证全局结构和局部精度的关键。

3.4 把神经元映射结果转成清晰的簇标签

训练完成后,用sim函数把所有样本过一遍网络,每个样本会映射到某个神经元,得到一个one-hot向量,再用vec2ind转换成神经元编号:

y = sim(net, Xn); cIdx = vec2ind(y); % 每个样本映射到的神经元编号,1~36

到这一步拿到的还不是最终的簇编号,因为36个神经元并不等于36个簇。真实业务中我们通常只需要少数几个大类,所以需要对这36个神经元做二次合并。一个简单好用的办法是对神经元的权值向量再跑一次层次聚类或K-Means。

% 获得神经元的权值矩阵 W = net.IW{1}; % 36x4 % 对36个神经元权值做再次聚类,得到3个簇 k = 3; neuronClusters = kmeans(W, k, 'Replicates', 10); % 把样本映射结果转换为最终簇编号 finalCluster = neuronClusters(cIdx)';

这样一来,每个样本就对应到了3个最终簇中的某一个。为什么先SOM再K-Means而不是直接用K-Means?我的理解是,SOM先做了一个拓扑约束下的平滑聚类,它的输出神经元权值已经在某种程度上消除了原始数据中的噪声,这时候再做K-Means稳定性会更好,而且在可视化层面也能给出更一致的簇结构。

3.5 可视化:像看地图一样看聚类结果

接下来是SOM最漂亮的部分,可视化。几个常用的函数我逐个说:

figure; plotsomhits(net, Xn); % 每个神经元的命中次数 figure; plotsompos(net); % 神经元权值在输入空间中的位置 figure; plotsomnd(net); % U-Matrix距离矩阵

plotsomhits会在六边形网格的每个格子上显示数字,代表该神经元赢下了多少个样本。数据分布越均衡,命中图就越均匀。如果某个格子的数字是0,说明这个神经元在整个训练中没有赢过任何样本,这就是典型的死神经元问题,后面我会讲解决办法。

plotsompos展示的是每个神经元的权值向量在输入空间中的位置连线,可以看到网络如何把高维空间的数据压到网格上。

plotsomnd也就是U-Matrix,是最值得仔细看的图。它会用颜色深浅表示相邻神经元权值向量的距离:颜色越深的区域表示这里的神经元之间差异越大,通常就是簇与簇的边界;颜色浅的区域表示神经元之间差异小,属于簇内部。用U-Matrix判断合适的簇数量,比纯看数字指标要直观得多,这也是SOM在探索性数据分析里最受好评的一个能力。

我训练出来的结果和预期的三类分布基本吻合,三个主要的簇在网格上有明显的分区,边界清晰。

4. 效果评估与实验对比:怎么证明聚类分得好

聚类做好了,下一步自然是要回答一个问题:分得准吗?好坏怎么衡量?这部分我分享一套我自己常用的评估方法,以及一组对照实验的结果。

4.1 用真实标签计算聚类准确度

由于鸢尾花数据集有真实标签,我可以把聚类结果和真实标签做对比。但这里有个坑:聚类出来的簇编号是1、2、3,真实标签是setosa、versicolor、virginica,两者的编号根本没有对应关系。直接算“准确率”是错误的。

标准做法是先做一个标签对齐,把每个簇映射到占比最高的那个真实类别上。比如簇1里面80%是setosa,那簇1就当作setosa,然后把映射后的预测标签和真实标签做逐个比较。我写了一个简单的函数来做这件事:

function acc = computeClusteringAcc(clusterIdx, trueLabel) % clusterIdx: 聚类簇编号 % trueLabel: 真实类别标签(cell数组) labels = unique(trueLabel); k = max(clusterIdx); % 构建簇到真实标签的映射 mapping = cell(k, 1); for c = 1:k idx = find(clusterIdx == c); if ~isempty(idx) mapping{c} = mode(trueLabel(idx)); % 簇内出现最多的类别 end end % 计算准确率 pred = cell(size(clusterIdx)); for i = 1:length(clusterIdx) pred{i} = mapping{clusterIdx(i)}; end acc = sum(strcmp(pred, trueLabel)) / length(trueLabel); end

这个函数只能处理簇编号和真实类别一一对应的情况,如果某个真实类别在数据里被拆成了两个簇,准确率就会被低估。好在鸢尾花数据集本身比较规整,三个类别基本能各自聚成一团,所以这个方法够用。更严谨的做法是计算调整兰德指数ARI或者标准化互信息NMI,这两个指标不要求簇编号对齐,直接比较两个划分之间的一致性。MATLAB的Statistics and Machine Learning Toolbox里没有现成的ARI函数,不过实现并不复杂,需要时可以自己写。

4.2 三路对比:newc、SOM、K-Means哪个更好用

为了验证方案选型,我把三种方法在同一个数据集上做了对比实验:

方法聚类准确率特征备注
newc纯竞争层(3神经元)88.7%速度快,但死神经元明显多次运行结果波动较大
SOM 6x6 + K-Means合并95.3%稳定,可视化强参数稍多,训练时间可接受
K-Means直接聚类93.3%简单直接对初始中心敏感,需多次重复

从准确率看,SOM合并方案最高,K-Means次之,newc最差。这个结果其实很有启发性:SOM先通过邻域约束的平滑训练把数据结构摸了一遍,再做合并聚类时噪声更少,所以精度能压过直接用K-Means。而newc作为纯竞争网络,因为没有邻域协同,神经元之间各自为战,很容易出现某个神经元被初始化在数据空隙里,最后成了永远赢不了的死神经元。

需要特别说明,这不是说newc一无是处。它结构最简单、训练最快,在类别数比较少、数据结构非常清晰的时候,结果和SOM差不了太多。但一旦数据稍微复杂一点,稳定性就明显不如SOM了。

4.3 参数敏感度实验:网格大小、coverSteps、epochs

聚类这类无监督方法最让人头疼的是参数选择没有统一答案,所以我专门做了一组敏感度实验,把实际效果记录下来供参考。

先说网格大小。我把SOM网格依次设为[3 3]、[6 6]、[10 10]、[15 15],其他参数不变,观察准确率变化:

gridSizes = {[3 3], [6 6], [10 10], [15 15]}; for g = 1:length(gridSizes) net = selforgmap(gridSizes{g}, 100, 3, 'hextop', 'linkdist'); net.trainParam.epochs = 200; net = train(net, Xn); y = sim(net, Xn); cIdx = vec2ind(y); W = net.IW{1}; neuronClusters = kmeans(W, 3, 'Replicates', 10); finalCluster = neuronClusters(cIdx)'; acc = computeClusteringAcc(finalCluster, trueLabel); fprintf('网格 %dx%d: 准确率 %.2f%%\n', ... gridSizes{g}(1), gridSizes{g}(2), acc * 100); end

实验结果很能说明问题:[3 3]网格只有9个神经元,分辨率太低,簇边界糊在一起,准确率只有85%左右;[6 6]网格36个神经元,准确率最高,达到95%以上;[10 10]网格100个神经元开始出现部分空神经元,准确率小幅回落到93%;[15 15]网格225个神经元,大量神经元没有样本命中,准确率掉到90%出头。结论就是神经元不是越多越好,网格太密不仅训练慢,还会把样本切得七零八落。

再看coverSteps的影响。它控制邻域收敛的节奏,过小时邻域快速收缩,全局拓扑还没建立就进入了微调阶段,准确率明显下降。过大时收敛慢,迭代次数不变的情况下最终结果也偏保守。在鸢尾花数据集上,coverSteps取100到200是比较合适的区间。

最后是epochs。SOM训练对epochs的敏感度其实比很多人想的低,只要排序阶段能完成,微调阶段多跑几轮只是让权值更平滑,准确率增益很小。我用200轮和500轮分别测试,准确率差距在1个百分点以内。所以建议优先保证网格尺寸和coverSteps合理,epochs用默认值附近就行,省下的时间做更多初始化次数更重要。

5. 常见问题排查与避坑手册

写到这一章,是把我在实际项目里踩过的坑和解决问题的思路全部梳理出来,这些内容在文档里通常找不到,但对动手做的人帮助最大。

5.1 所有样本被分到同一个神经元,怎么破

这是新手最容易遇到的问题,训练完了之后,vec2ind输出的结果全是同一个数字,感觉网络根本没学习。我从经验来看,大概率是以下几个原因叠加的结果。

第一是数据没有归一化。这是最常见的原因,某个特征的量级特别大,导致所有样本算距离时都只看这个特征,而初始化后某个神经元的这个特征分量恰好靠中间,就出现了通吃所有样本的局面。处理方法就是回到第3.1节,老老实实做mapminmax或zscore。

第二是学习率设置不合理。学习率太大,权值向量会在样本之间剧烈跳动,始终稳定不到某个簇中心;学习率太小,更新幅度又不足以让权值从初始位置移动到数据区域。用默认的SOM参数通常不会有大问题,但如果是自己手写竞争层或者调整了learnRate,可以先用极小的学习率加多轮迭代来验证。

第三是初始化位置太差。如果所有神经元的权值初始值都落在数据分布之外,那训练过程很难把权值拉进数据区域。最简单的排查办法是把权值矩阵打印出来,对比一下数据范围,看看神经网络是不是压根没覆盖到数据。

5.2 死神经元问题及相关技巧

死神经元指训练结束后从未赢过任何一个样本的神经元,在plotsomhits图上表现为某些格子为0。死神经元的成因有几个:初始化位置不好、学习率过大、邻域太小、数据本身存在明显的不平衡。

在纯newc竞争网络中,死神经元几乎是家常便饭,因为输家永远没有机会更新。SOM因为有邻域机制,即使只是获胜神经元的邻域也能跟着更新,死神经元问题得到明显缓解,但并没有根治。处理死神经元的几个实用技巧:

  • 用SOM替代纯竞争层。邻域更新让“输家”也有翻身机会。
  • 增加邻域初始半径,让训练早期有更大的区域参与更新,相当于给偏远位置的神经元一个学习窗口。
  • 尝试对权值做K-Means初始化而不是随机初始化。把K-Means跑出来的中心点作为初始权值,让每个神经元一开始就在数据区域里,死神经元基本不会出现。
  • 如果数据类别本身就不平衡,比如某一类只占总样本的5%,那它在竞争层中能赢得的神经元数量自然很少。可以适当增大网格密度,或者对样本做一些重采样。

5.3 神经元数量不等于簇数量怎么办

SOM的网格有多少个神经元,并不代表最终聚类就有多少个簇。很多人第一次做完映射后会困惑:为什么设置了[8 8]的网格,得到64个编号,我却只需要3个类别?

这里要理清楚:SOM的输出是神经元拓扑映射,不是最终聚类标签。每个簇在网格上通常覆盖一片区域,包含多个神经元。要想得到最终簇,就要做二次聚类,如前面3.4节的做法,对神经元权值跑K-Means或层次聚类。这样既发挥了SOM保持拓扑结构的优势,又满足了业务上对簇数量的需求。

那怎么判断二次聚类该取几个簇呢?我推荐两个辅助手段:一是看U-Matrix,颜色深的地方就是天然的簇边界,数一数深色分割出来的浅色区块数量,大致就是比较自然的簇数;二是用轮廓系数,对不同k值分别计算轮廓系数,取峰值附近的k值。实际项目中这两个佐证结合,比拍脑袋定k要可靠得多。

5.4 聚类结果每次都在变,如何保证可复现

无监督聚类和神经网络结合之后,会遇到一个让很多人头疼的现象:代码没改,每次跑出来的结果都不一样。原因主要在两个随机源:初始权值随机化,以及二次聚类时K-Means的初始中心是随机的。

SOM不是凸优化问题,初始化不同,最终权值可能收敛到不同的局部最优,所以结果不稳定是正常现象。应对方法分两个层面。短期看,用rng函数固定随机种子,比如在代码开头加一句rng(0),至少能保证每次运行复现同一份结果。长期看,正式项目里我会跑多次初始化,比如20次,对每次结果都计算一遍与真实标签的对应准确率,或者用聚类一致性来筛,挑出最优或出现频率最高的那一个结果。这是无监督方法的普遍共识——不要相信单次运行的结果。

另外还要注意簇编号本身是随机的,第一次训练得到“簇1是A类,簇2是B类”,第二次可能就反过来了。做结果对比时一定要做标签对齐,否则会发现准确率忽高忽低,其实只是编号互换的错觉。

5.5 训练很慢、数据集很大的优化思路

SOM的训练复杂度主要跟三个量有关:神经元数量、特征维度、样本数量。当数据集规模上来之后,训练时间会明显拉长。我的优化思路排序是这样的:

优先考虑减少神经元数量。网格从[12 12]改成[8 8],神经元数量从144个降到64个,训练时间直接减半,很多时候精度损失并不大。

其次可以采样训练。SOM本质上是通过大量样本的反复迭代来调整权值,如果原始数据有几十万条,完全可以随机抽取几万条来做训练,然后用全量数据来做映射和评估。我实测过不少场景,采样后聚类结构基本不变,时间却快了一个数量级。

最后才是并行和性能层面的手段。MATLAB对SOM这类训练过程的并行化支持有限,与其纠结怎么并行,不如考虑先用PCA把特征维度降下来,比如从100维降到20维,训练速度和内存占用都会显著改善。

5.6 数据不平衡时的应对经验

最后一类常见问题是类别严重不平衡。比如场景里正常样本占95%,异常样本只占5%,SOM训练完后,大部分网格都被正常样本占据,异常样本只能挤在角落里,甚至完全覆盖不到。

我处理这类问题时有三个办法。一是对少数类做重采样,在送入网络之前把少数类样本复制若干份,让它们在训练中能赢得足够的神经元。二是增加网格密度,让少数类有机会在网格上占据一片独立区域。三是调整训练策略,把邻域初始半径调大一些,训练初期少数类虽然样本少,但借助邻域传播也能带动周围神经元朝它的方向移动。这些方法不一定普适,但至少在物联网异常检测相关项目里,我用这几招把少数类的识别率从不到30%拉到了70%以上。

另外提醒一下,聚类出来后如果发现某个簇特别小且远离其他簇,不要急着当成噪声扔掉,先看一下这个簇里的样本在原特征上有没有共性。在很多实际项目中,这种“边缘小簇”往往才是最有价值的部分。


最后再分享一点个人体会。做了这么多次竞争神经网络聚类,最大的感受是:这个算法真正厉害的地方不在于它比K-Means精度高多少,而在于SOM能把数据压在二维网格上,让人直接“看到”数据的整体结构和边界。有一说一,我在做用户分群项目时,甲方最满意的往往不是那个准确率数字,而是plotsomnd那张U-Matrix图——业务人员一眼就能看出哪几类用户是截然不同的群体。

至于后续扩展,我给自己的实践路线是:先跑通newc理解竞争机制,再用selforgmap做正式分析,最后把SOM输出的神经元激活模式作为特征,接到下游分类器或异常检测模块里去。这套组合拳,在不少无标签数据项目里都帮了大忙。如果你手头正好有一批没有标签的数据,不妨把这段代码跑一遍,看看SOM的网格上会不会出现让你意外的结构。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询