常年在无线电台站做监测数据处理的人,应该都有过这种感觉:手里攥着成千上万条信号测量记录,却没有一条能告诉你它们分别来自哪部发射设备。我们唯一能看到的是每条记录的到达角、带宽、信号强度、脉宽等参数,然后要在毫无标签的情况下判断“这几条属于同一个干扰源,那几条属于另一个干扰源”。这就是干扰源聚类分析典型的使用场景。
这篇文章想聊的就是这件事:用Matlab把几种主流聚类方法在干扰源分选任务里跑通,包括K-means、DBSCAN、层次聚类和高斯混合模型,并给出能直接改来用的完整代码。适合正在做频谱监测、信号分选、电子侦查数据处理的朋友参考,也适合那些刚接触聚类、想知道方法之间到底差在哪的Matlab使用者。我不会只贴代码,会把每种方法为什么适合这类数据、踩过的坑和调参经验一起写清楚。
1. 干扰源聚类在做什么:一个真实的信号分选场景
1.1 问题本质:从“疑似”到“确认”
先说清楚任务本身。假设你在城市里架设了一台监测接收站,某频段内出现了不明干扰。接收站按时间顺序捕获了几千条信号测量记录,每条记录本质上是一个特征向量,通常包含到达时间、频率、带宽、信号强度、到达角、脉宽等。这些记录里既有真正干扰源发出的信号,也有环境噪声和偶发信号,而干扰源本身可能不止一部——可能是几部设备在不同位置同时工作。
聚类的目标就是:把这些没有标签的样本按照“来自同一部干扰源”的相似度分成若干组。完成这一步之后,后续的干扰源定位、优先级排序、处置决策才有依据。所以这种聚类分析不是学术游戏,而是频谱管理流程里非常前置的一个环节。
这里有个很容易误解的地方:很多人把聚类当成分类来做,总想先训练一个模型再预测。但干扰源场景里根本拿不到可靠标签,你没法提前知道“这个信号属于哪部设备”,更不可能把所有可能出现的新干扰源都预先建进训练集。聚类的好处是它不做这种假设,直接从数据自身的分布结构出发,把相似的样本归到一起。
1.2 为什么通用检测方法不够用
在实际项目里,最先被尝试的往往是两种方法:门限检测和人工分选。门限检测的原理很简单,比如设定“到达角在30度附近、强度大于-70dBm”就算命中某个源,但当干扰源本身有频率漂移、功率起伏时,门限很容易误判,窄了漏检,宽了误报。人工分选在数据量少的时候可以,几百条数据还能在散点图里圈一圈,样本上万时就完全不可行了。
还有一种思路是上监督学习,比如用SVM或神经网络做分类。问题是这类算法需要的标注数据恰好是我们最缺的东西。就算你今天花大力气标了一批数据,明天出现一部新型干扰源,它的特征分布和原来的都不一样,模型基本失效。
所以聚类分析才会成为这类任务中的主力工具。它不做先验假设,能从数据本身发现结构,而且对“类别数不确定”的情况天然友好。但“聚类”这个词下面其实藏着一大堆方法,不同方法对数据形态的假设差别很大,选错了结果会很难看。这正是我接下来要展开的重点。
2. 四种经典聚类方法在干扰源数据上的取舍
2.1 K-means:快速摸底但脆弱
K-means应该是大多数人接触到的第一个聚类算法。它的核心思路是:确定类别数K,随机初始化K个质心,然后反复迭代,让每个样本归到距离最近的质心,再重新计算质心位置,直到收敛。数学上是在最小化样本到所属质心的欧氏距离平方和。
在干扰源场景里,K-means最大的价值是快。数据量上万甚至几十万条时,它依然能在很短时间跑完,特别适合做数据摸底——先粗略看看大致有几堆。但也有三个明显的弱点。
第一,它必须预先指定K。在真实干扰监测中你往往不知道现场有几部干扰源,只能通过尝试多个K值来猜。第二,它对噪声和离群点非常敏感。一次偶然捕获的异常信号,比如远处雷电引起的突发脉冲,会被单独拉成一个质心,或者把某个簇的中心拽偏。第三,它假设簇形状是类球形的。真实干扰源的特征分布往往比这复杂,簇可能细长、弯曲,甚至相互搭边,K-means这时候就力不从心了。
2.2 DBSCAN:密度聚类更贴近信号散布
DBSCAN是另一条完全不同的路子。它不看质心,而是看密度:如果一个点周围足够密集,就把它所在的区域向外扩展,连成一片;那些处在低密度区域的孤点则被标记为噪声。它有两个关键参数:epsilon,定义“邻域”的半径,以及minPts,定义“邻域内至少要有几个点才算密集”。
这种算法对干扰源数据的适配性比K-means好很多。真实信号在特征空间里的分布往往就是“一团一团”的,干扰源的观测样本越多,那一团就越密实,簇与簇之间通常存在稀疏地带——这正好是DBSCAN最喜欢的数据形态。而且它不需要预先指定聚类数,还能自动把环境噪声单独标出来,相当于聚类和滤噪一步完成。
但DBSCAN也有自己的麻烦。它对epsilon极其敏感,设小了会把一个簇拆成好几块,设大了会把两个相邻干扰源合在一起。还有一个更麻烦的问题:它使用全局统一的密度阈值,如果现场既有关得很近的固定台站,又有距离监测站很远、信号散布很大的移动干扰源,单一epsilon就很难同时照顾这些情况。这个问题我后面会专门讲应对方案。
2.3 层次聚类:不预设类别数的最大优势
层次聚类和前面两种都不一样,它不直接给你一个分组结果,而是先递归地把样本合并成树状结构。Matlab里最常用的是自底向上的聚合层次聚类,每一步都把距离最近的两组样本合并,直到所有样本成为一个整体。这棵“谱系树”会把数据从单个样本到完整一类的全部合并过程记录下来。
对干扰源分析来说,层次聚类最有吸引力的地方在于它的探索性。你可以先用算法生成树,然后在任意截断高度取分组——不需要提前纠结K值,而是看完树状图再决定从哪里切。我常把这个过程类比成看地图:K-means是别人替你标好了几个城市,而层次聚类是先把所有道路画出来,你再看哪里是天然的分界。
不过代价是计算开销大。我在工程里一般只对不超过两万条样本的数据跑层次聚类,超过这个量级,ward方法(也就是最小化合并后簇内方差的合并策略)的计算时间会迅速膨胀。所以它在项目中更适合小规模、需要仔细判断的场景,比如对K-means和DBSCAN结果不一致的样本做复核。
2.4 高斯混合模型:处理特征重叠的软聚类工具
GMM和K-means有相似之处,都要预先指定类别数K,但它比K-means灵活得多。GMM假设每类数据都来自一个多维高斯分布,整个数据集是多个高斯分布的混合。算法通过期望最大化迭代,估计每个高斯成分的均值、协方差和混合权重,然后输出每个样本属于每个成分的概率。
这种“软划分”能力在干扰源特征出现重叠时特别好用。比如两部干扰源的到达角接近,强度范围也差不多,它们的特征分布在平面上有交叠。K-means只能硬生生把交叠区域切开,产生明显错分;而GMM能给出“这个样本有60%属于源A、40%属于源B”的概率结果,让后续的决策留有回旋余地。
GMM的短板在于训练不稳定。协方差矩阵的估计容易受到奇异点影响,有时候需要加正则化参数才能收敛。它同样需要预设K,而且对初始化比较敏感。我的做法是把GMM当作验证工具:当K-means和DBSCAN的结果有分歧时,用GMM输出一个更精细的概率判断,而不是把它当主力方法。
为了直观比较,我总结了一张表:
| 方法 | 需要预设类别数 | 能否识别噪声 | 对密度不均数据 | 计算开销 | 典型用途 |
|---|---|---|---|---|---|
| K-means | 是 | 否 | 较差 | 低 | 快速摸底、大样本粗分 |
| DBSCAN | 否 | 是 | 中等 | 中 | 类别数未知、存在异常样本 |
| 层次聚类 | 最终切树时需指定 | 否 | 较好 | 高 | 小样本探索、复核分歧 |
| GMM | 是 | 软划分 | 较好 | 中高 | 特征重叠、边界模糊 |
3. Matlab代码实现:从模拟数据到四种聚类结果
3.1 先按干扰源的物理特征造一份模拟数据
很多朋友拿到真实测量数据之后喜欢直接跑算法,我建议先别急。第一,真实数据没有标签,你即使跑出结果也没法验证算法选得对不对;第二,真实数据的噪声形态很复杂,刚开始就处理它容易把问题和方法混在一起。正确流程是:先用模拟数据把整个链路跑通,确认每种方法的行为符合预期,再切换到真实数据上去。
我构造一个典型的场景:四个干扰源,每个源用三个特征描述——到达角(单位:度)、带宽(单位:MHz)、信号强度(单位:dBm)。四个源的参数设置不同,然后在每个源周围加一定幅度的随机扰动,模拟测量误差和信号波动。
rng(42); % 四个干扰源的样本数量 n = [60; 50; 55; 45]; % 干扰源中心:[到达角, 带宽, 信号强度] mu = [35, 2.0, -60; 120, 1.5, -55; 230, 3.2, -70; 310, 1.8, -48]; % 每个源的扰动幅度(标准差) sigma = [1.5, 0.2, 1.5; 2.0, 0.3, 2.0; 2.5, 0.4, 1.8; 1.8, 0.25, 2.5]; data = []; truth = []; for i = 1:4 block = randn(n(i), 3) .* sigma(i,:) + mu(i,:); data = [data; block]; truth = [truth; i * ones(n(i), 1)]; end % 随机打乱顺序,模拟实际捕获时的无规律状态 perm = randperm(size(data, 1)); data = data(perm, :); truth = truth(perm, :);这里有个细节容易出错:很多人在打乱顺序时会写两次randperm,一次给数据、一次给标签,结果标签和数据没有同步置换,后面做效果评估就全错了。正确写法是像上面这样,只生成一次索引,然后同时作用到两个变量上。
3.2 标准化预处理:这一步不做,后面全白搭
如果直接把上面这份数据丢给聚类算法,结果会非常糟糕。原因很简单:到达角以度为单位,数值在几十到三百多之间;带宽以MHz为单位,数值只有一到几;信号强度以dBm为单位,数值在负几十左右。三个特征的尺度天差地别,在计算欧氏距离时,量纲大的特征会完全吞掉量纲小的特征,等于只有信号强度这一个维度在起作用。
解决办法是标准化。Matlab里一行命令就能完成:
data_z = zscore(data);zscore做的事很简单:每个特征减去均值,再除以标准差,使每个特征变换成均值为0、标准差为1的分布。这一步做完,三个特征的贡献权重就平衡了。后面所有聚类算法都应该在data_z上运行,而不是原始数据。
强调一下,特别是用过DBSCAN的朋友:标准化对DBSCAN的影响比K-means大得多,因为DBSCAN的距离阈值epsilon是直接把半径限定在一个数值上,如果某个特征尺度特别大,epsilon选多大都会被它主导,结果就是要么整片区域都连成一体,要么全被标成噪声。先把特征尺度统一,后续调参才有意义。
3.3 四种算法一次跑通
现在把四种聚类方法都跑一遍。需要注意,kmeans、dbscan、linkage、fitgmdist这些函数属于Statistics and Machine Learning Toolbox,没有这个工具箱的话需要先装上。
K = 4; % 1. K-means [idx_k, C_k] = kmeans(data_z, K, 'Replicates', 10, 'MaxIter', 500); % 2. DBSCAN epsilon = 0.5; minPts = 5; idx_db = dbscan(data_z, epsilon, minPts); % 3. 层次聚类(ward方法,欧氏距离) Z = linkage(data_z, 'ward', 'euclidean'); idx_h = cluster(Z, 'MaxClust', K); % 4. 高斯混合模型 gm = fitgmdist(data_z, K, 'RegularizationValue', 0.01); idx_g = cluster(gm, data_z);关于K-means里的'Replicates', 10,这是一个很值得养成的习惯。K-means的初始质心是随机的,不同初始点可能收敛到不同的局部最优解,多跑几次可以降低这种随机性,保留最优的那次结果。DBSCAN那边,epsilon我初步取0.5、minPts取5,这两个参数不是拍脑袋,后续我会在参数调优部分详细说怎么通过k-距离图来确定。
层次聚类的cluster(Z, 'MaxClust', K)表示把树在某个高度切断,得到K个簇。GMM里的RegularizationValue是防止协方差矩阵奇异用的,样本特征相关性太强或者某个簇样本太少时会遇到收敛失败,加一点正则值能避免这个情况。
3.4 结果可视化与自动标记
聚类结果是用来看的,更是用来判断的。Matlab里最方便的工具是gscatter,它能把不同类别的样本用不同颜色和符号画出来。我通常把四个子图画在同一个figure里,方便对比四种方法的行为差异。
figure; tiledlayout(2,2); nexttile; gscatter(data(:,1), data(:,3), idx_k); title('K-means (K=4)'); xlabel('到达角 (度)'); ylabel('信号强度 (dBm)'); nexttile; gscatter(data(:,1), data(:,3), idx_db); title('DBSCAN'); xlabel('到达角 (度)'); ylabel('信号强度 (dBm)'); nexttile; gscatter(data(:,1), data(:,3), idx_h); title('层次聚类'); xlabel('到达角 (度)'); ylabel('信号强度 (dBm)'); nexttile; gscatter(data(:,1), data(:,3), idx_g); title('GMM (K=4)'); xlabel('到达角 (度)'); ylabel('信号强度 (dBm)');这里我用的是原始数据中的到达角和信号强度两维,而不是标准化后的数据,主要是为了让坐标轴有明确的物理含义。有一点需要特别提醒:DBSCAN的idx_db里可能含有-1标签,代表噪声点。gscatter对噪声点的处理是把它单独归为一类,画出来没问题,但计算轮廓系数时一定要先把这些点滤掉,否则会报错。这类小细节在实际使用中容易卡住人。
4. 聚类效果评估与调参实操:把参数定到能落地的程度
4.1 轮廓系数与evalclusters的使用
跑完聚类只算完成了第一步,关键问题是:这些结果到底行不行?对带标签的模拟数据,我们可以拿聚类结果和真实分组做对比,计算调整兰德指数;但对没有标签的真实测量数据,我们只能用内部指标评估,其中轮廓系数是最常用的一种。
轮廓系数的含义简单说就是:对一个样本,看它跟同簇其他样本的平均距离(记为a),再看它跟最近邻簇样本的平均距离(记为b),轮廓值就等于(b-a)/max(a,b)。这个值越接近1,说明该样本离自己簇的紧密程度明显优于离其他簇的接近程度,聚类效果越好;接近-1则说明很可能分错了簇。整体聚类质量用所有样本轮廓系数的均值来衡量。
sil_k = mean(silhouette(data_z, idx_k)); % DBSCAN需先去掉噪声点(标签为-1的样本) valid = idx_db > 0; sil_db = mean(silhouette(data_z(valid, :), idx_db(valid))); sil_h = mean(silhouette(data_z, idx_h)); sil_g = mean(silhouette(data_z, idx_g)); fprintf('平均轮廓系数:K-means=%.3f, DBSCAN=%.3f, 层次=%.3f, GMM=%.3f\n', ... sil_k, sil_db, sil_h, sil_g);需要说明的是,轮廓系数本身不能完全代表聚类正确性,它更像一个内部自洽性的度量。如果某个方法轮廓系数低,先别急着否认方法,很可能是参数没调好——这正是下一步要解决的问题。
Matlab里还有一个集成函数evalclusters,可以直接帮你扫一组候选K值并计算指标,避免手动写循环:
eva = evalclusters(data_z, 'kmeans', 'Silhouette', 'KList', 2:7); figure; plot(eva);evalclusters的KList可以设成你怀疑的范围。在干扰源场景里,我一般把K从2扫描到7或8、配合轮廓系数和Calinski-Harabasz指标一起看,如果两个指标给出的最佳K不一致,就说明数据本身的结构不够清晰,需要结合人工经验判断。
4.2 DBSCAN的epsilon怎么定:k-距离图
DBSCAN是唯一不需要预设簇数的算法,但它的epsilon参数比K值更难猜。很多人在这里用暴力枚举法,从0.1试到2.0,看轮廓系数变化。这方法不是不行,只是效率低,而且容易因为轮廓系数波动错过合适的区间。
更稳妥的做法是先画k-距离图。基本思路是:对每个样本,找出离它第minPts近的那个邻居的距离,把所有这些距离升序排列画成曲线。曲线通常会出现一个明显拐点,拐点对应的纵坐标就是比较合理的epsilon。
minPts = 5; distances = pdist2(data_z, data_z); kdist = zeros(size(data_z, 1), 1); for i = 1:size(data_z, 1) d = sort(distances(i, :), 'ascend'); kdist(i) = d(minPts + 1); % 因为d(1)是样本自身,距离为0 end kdist = sort(kdist, 'ascend'); figure; plot(kdist, 'o'); xlabel('样本序号(按距离排序)'); ylabel(sprintf('第%d近邻距离', minPts)); grid on;理论上,如果数据里有明确的密度分界,拐点会非常清晰;但真实实测数据的k-距离图往往是一条缓坡曲线,没有明显拐点。这时候我的经验是:以k-距离曲线的中后段为参考,选一个能让轮廓系数稳定在较高水平的epsilon,同时在0.3到0.8这个范围内多扫几个值对比。minPts的设定则和数据规模有关,样本越多minPts可以越大,一般取5到10之间就够了,取太大容易把小簇全部吞掉。
4.3 GMM成分数与BIC
GMM的成分数K没法用轮廓系数直接选,因为GMM输出的是软聚类概率,轮廓系数在这种输入下并不完全合适。工程上更常用的是BIC(贝叶斯信息准则)。BIC把所有可能的高斯成分数都拟合一遍,然后看哪个K对应的BIC值最低,模型在拟合能力和复杂度之间取得平衡。
bic_values = zeros(1, 6); for g = 1:6 gm_tmp = fitgmdist(data_z, g, 'RegularizationValue', 0.01); bic_values(g) = gm_tmp.BIC; end figure; plot(1:6, bic_values, '-o'); xlabel('高斯成分数 K'); ylabel('BIC'); grid on;注意BIC的绝对值没有意义,只有相对大小有意义,所以只要比较“哪个K的值最低”或“下降趋势在哪里变缓”就行。而且GMM对初值敏感,同一个K可能因为初始参数不同跑到不同的局部最优,所以每个K值最佳尝试跑两三次,取BIC最小的那次。
4.4 综合调参顺序建议
在实际项目里,我不会在一个指标上纠结太久,更常用的是“先粗后细”的流程:
- 第一步,用
kmeans配合evalclusters快速扫描K,大致确定干扰源数量的合理范围。 - 第二步,用DBSCAN的k-距离图选epsilon,扫几个候选值,看轮廓系数和噪声比例。噪声比例过高通常意味着epsilon太小或minPts太大。
- 第三步,如果前两步结果分歧明显,用GMM+BIC做交叉验证,看GMM给出的成分数和最可能的簇中心落在哪里。
从我个人的经验看,这个流程基本能把参数定在“能落地”的程度。相比某一次的具体取值,更重要的是整个过程的可复现性:每次调整参数,都记录轮廓系数、噪声比例、簇中心位置。尤其在频谱监测这种需要定期出报告的场合,可复现的记录比一次漂亮的聚类结果更有价值。
5. 实测中最容易翻车的四个细节:量纲、密度、规模与时变性
5.1 特征量纲问题:DBSCAN对距离度量极其敏感
我最初跑真实监测数据时犯过一个特别低级的错误:直接把原始特征矩阵丢进dbscan,结果整片数据几乎被聚成一团。当时我一度怀疑是数据本身的问题,后来检查发现根本没有做标准化。到达角、频率和信号强度三个特征,量纲差异大到让距离计算完全失效。
解决办法不只是zscore一种。如果某些特征本身就是区间型的,比如到达角是0到360度,直接按数值计算距离会忽略“0度和360度是同一个方向”这个事实。对这类角度特征,更合理的做法是先变换成单位圆上的坐标,也就是把角度转成正弦值和余弦值两个维度,再做标准化。不过这个处理会让特征维度增加,需要结合你的具体特征定义来判断。至少,任何特征不做归一化就上聚类算法,在干扰源这种混合量纲数据上是必翻车的。
5.2 全局epsilon与密度不均:远处干扰源的簇更松散
DBSCAN的epsilon是全局的,这在实际电磁环境中会带来麻烦。靠近监测站的干扰源信号强、测量稳定,特征散布小,簇很密实;而距离较远的干扰源信号经过长距离传播和多径效应,特征波动明显,簇明显松弛。如果你按远处源的标准选大epsilon,近处的几个源就可能被连成一片;按近处源的标准选小epsilon,远处的源又会碎成噪声。
两条应对思路供你参考。一是把特征空间划分成几个子区域,比如先按到达角分区间,再在每个区间里单独做DBSCAN,让密度估计更贴合局部数据。二是干脆放弃DBSCAN,改用层次聚类或GMM,因为这两种方法不用全局密度阈值,对密度不均的容忍度更高。我的建议是:同时保留DBSCAN和GMM的结果,如果两者在某个样本上的归属不一致,就把这个样本标记为“需要复核”,而不是简单选一个结果。
5.3 数据量大时的计算瓶颈:层次聚类从好用到不可用
Matlab的linkage在样本量小的时候非常好用,谱系图也能看得很清楚。但一旦样本量超过两三万,ward方法的计算量会急剧上升,而且linkage需要保存距离矩阵的中间信息,内存占用很容易失控。我在一次处理连续监测数据时就遇到过这种情况,程序跑了十几分钟还没结束,最后只能强制中断。
如果你的数据规模也很大,可以考虑两个策略。第一个策略是先做一次快速的K-means预处理,把样本粗分成几十个“子簇”,再用层次聚类去分析子簇之间的关系。这种两级策略在文献里叫两阶段聚类,精度损失不大但速度提升非常明显。第二个策略是换工具,DBSCAN在大样本上的表现比层次聚类稳定得多,因为它不需要构造全局谱系树,计算量可以接受。如果连DBSCAN也慢,那就先抽样一部分做参数标定,再对全量数据跑参数已经确定的模型。
5.4 特征漂移与时变性:同一个干扰源今天和昨天“长得不一样”
干扰源不是一成不变的。固定台站的信号参数相对稳定,但移动干扰车、无人机搭载的干扰设备会随着运动持续改变到达角和信号强度;有些干扰源还有跳频或扫频工作模式,带宽特征也会随时间变化。如果在很长的时间跨度内直接做一次聚类,同一个源的特征分布被拉伸变形,很容易被算法拆成多个“伪源”。
我的处理办法是给时间加上约束。具体操作是:把长时间数据按小时或半小时切成时间窗,在每个窗口内单独做聚类,得到每个干扰源在该时段的簇中心;然后对簇中心做跟踪,判断哪些簇中心随时间连续移动、属于同一个移动源,哪些静止不动、属于固定源。这样做的好处是既利用了短时数据的稳定性,又不会把时变特征误当成多个干扰源。这个方法比单纯把时间作为一个特征维度丢进聚类算法要可靠得多,因为前者尊重了特征的物理含义。
最后再说点实际操作层面的体会。我到今天做干扰源聚类分析,依然不会只跑一种方法,常规流程是先上K-means快速摸底,再用DBSCAN精分并自动剔除噪声,最后对落点模糊的样本用GMM验一遍概率。三种方法互相咬合,比任何单一算法的输出都值得信赖。如果你现在只是刚开始接触,建议先把我给的模拟数据代码改造成你自己的特征矩阵,把整个流程跑通一次,再逐步加入调参、评估这些环节。这套思路不只在干扰源分选上适用,任何“特征已知、标签未知”的分组问题都可以照搬。