我在做居民用电行为分析的时候,最头疼的问题不是算法跑不出来,而是跑出来的结果没法解释。单一用K-means这类硬聚类,会把用户硬生生切到某个类别里,可现实中“偶尔晚睡的用户”和“长期夜猫子用户”的边界哪有那么清楚。后来切到模糊C均值聚类(FCM),结果合理了不少,但FCM本身对初始聚类中心特别敏感,换一组随机种子,聚类结果就飘。直到把粒子群算法(PSO)塞进去做初始化优化,才真正把结果稳定下来。这篇文章就把我基于Matlab实现的PSO-FCM居民用电行为分析完整过程分享出来,包括原理拆解、代码框架、参数调试和踩过的坑,给同样在做负荷分析、用户画像、精细化营销的朋友一条能直接走的路。
这套方案主要解决三个问题:一是FCM对初始中心敏感导致的局部最优问题,二是居民用电数据维度高、噪声大导致聚类不稳定问题,三是算法参数调起来没有方向的问题。适合电力行业的数据分析师、做用户分群的算法工程师,以及相关专业的研究生参考。代码主体基于Matlab实现,不需要额外的工具箱,纯脚本就能跑通。
1. 整体思路与方案选型拆解
1.1 为什么居民用电行为分析要用聚类
居民用电行为分析说白了就是把“谁在什么时候用了多少电”这件事抽象成可计算的特征,然后找到规律。单个用户的日负荷曲线如果直接看,几乎是乱糟糟的锯齿状,但把几千户用户的负荷曲线放在一起,就能看出明显的模式:有的用户早高峰和晚高峰各一个尖峰,典型的上班族;有的用户深夜还在高位,可能是夜宵店或者打游戏到天亮的人;还有的用户中午有个小高峰,白天用电量普遍偏高,可能是家里有老人或者自由职业者。
聚类就是为了自动把这些模式找出来,不用人工一条条看曲线。但选什么聚类算法是有讲究的。传统K-means在负荷聚类里用得最多,速度也确实快,但它有个隐含前提:每个样本只能属于一个簇。这在真实世界里站不住脚。一个用户可能工作日像上班族,周末像宅家党,如果硬切一刀,这个人到底算哪类?于是FCM的价值就出来了,它给出的是“该用户属于每一类的概率或隶属度”,你可以说这个用户有0.6的概率属于“上班族”,0.3的概率属于“夜猫子”,0.1的概率属于“均衡型”。这种软划分显然更适合刻画真实用电行为的模糊性。
1.2 FCM聚类有个绕不开的硬伤
FCM本身是个迭代优化算法,它通过最小化目标函数来迭代更新隶属度矩阵和聚类中心。这个过程的每次迭代都是沿着梯度方向走,本质上是个局部搜索。问题就出在这里——FCM的初始聚类中心是随机生成的,如果初始值落在某个不好的位置,迭代很容易收敛到局部最优解,而不是全局最优。
我做对比实验的时候对这个问题感受极深。同一份数据,FCM跑了十次,有时候聚出来三类,有时候聚出来四类,甚至有一次跑出的某一类只有一个用户。聚类结果不稳定,后续的用户画像、策略制定就全是空中楼阁。这就像你去爬山,随机挑了个起点就开始往上走,大概率只能爬到某个小山头,而不是真正的山顶。
1.3 为什么选PSO来做这个优化
粒子群算法的思路来自鸟群觅食。一群鸟在天空飞,每只鸟有一个位置和速度,它们根据自己找到过的最好食物位置和整个群体找到过的最好食物位置来调整飞行方向。把这个映射到聚类问题上:每个粒子就是一个候选的聚类中心组合,粒子的位置就是一组具体的聚类中心取值,适应度函数就是FCM的目标函数值(越小越好)。
选PSO而不是遗传算法或者模拟退火,我的理由很实际。第一,PSO没有交叉变异那些算子,参数少、结构简单,Matlab写起来一百行内搞定。第二,PSO的收敛速度快,在迭代前期能很快逼近全局最优区域,后面再用FCM细调精度,配合起来正好互补。第三,PSO对连续型变量(聚类中心是连续值)的优化效果出奇地好,而FCM的优化变量天然就是连续的。可以说,PSO负责“找到好位置”,FCM负责“在这个位置上精雕细琢”。
整体方案就是:先用PSO迭代若干次,把一组较优的聚类中心找出来,再把这组中心作为FCM的初始聚类中心,跑FCM到收敛,最后输出稳定的聚类结果和隶属度矩阵。
2. FCM原理与居民用电数据预处理
2.1 FCM的目标函数和迭代逻辑
FCM的目标函数是:
[ J = \sum_{i=1}^{n} \sum_{j=1}^{c} u_{ij}^m |x_i - v_j|^2 ]
其中 (u_{ij}) 是第 (i) 个样本对第 (j) 类的隶属度,取值范围[0,1]而且满足每行求和为1;(v_j) 是第 (j) 类的聚类中心;(m) 是模糊指数,一般取2。FCM通过迭代不断更新隶属度矩阵和聚类中心,直到目标函数变化小于阈值或者达到最大迭代次数。
隶属度更新公式:
[ u_{ij} = \frac{1}{\sum_{k=1}^{c} \left(\frac{|x_i - v_j|}{|x_i - v_k|}\right)^{2/(m-1)}} ]
聚类中心更新公式:
[ v_j = \frac{\sum_{i=1}^{n} u_{ij}^m x_i}{\sum_{i=1}^{n} u_{ij}^m} ]
这两个公式交替迭代,理论上保证目标函数单调不增。但从任意随机初始化出发,只能保证收敛到目标函数的局部极小值点,因此初始位置决定最终聚类质量。
2.2 居民用电数据的标准化处理是命门
很多人在这一步栽跟头。居民用电的原始数据计量单位看着是统一的“度”(kWh),但量纲差异依然巨大。一个总用电量800度/月的用户和一个80度/月的用户,如果直接拿原始数值算欧氏距离,大数值用户会完全主导聚类结果,小数值用户就算形态特征再独特也被吞掉。
我的做法是分两层标准化。第一层是最大最小值归一化,把每个采样点的负荷值压到[0,1]区间。第二层是对每个用户的完整负荷曲线做模长归一化,也就是把每条曲线除以它的二范数,这样做的好处是把用户的“用电总量”因素与“用电形态”因素解耦。在居民用电行为分析场景里,我们更关心的是用户的用电模式,而不是用电总量绝对值,否则聚类结果里会出现“用电大户”和“用电小户”这种完全按财力划分的类别,对行为分析没有意义。
归一化之后还要做特征降维。96点日负荷曲线(每15分钟一个采样点)维度倒不算高,直接聚类问题不大,但为了剔除异常尖峰和噪声,我会先做一次主成分分析,取累积贡献率95%以上的主成分作为聚类输入。这一步能显著提升聚类稳定性和运行速度,而且几乎不损失有效信息。
2.3 聚类数的选择
聚类数 (c) 的确定,我习惯综合三个指标来看:轮廓系数(Silhouette Coefficient)、戴维森堡丁指数(Davies-Bouldin Index,DBI)和FCM目标函数的收敛曲线。轮廓系数越大越好,DBI越小越好。实际操作中,我会在 (c=2) 到 (c=8) 之间逐个跑一遍,把每个 (c) 的聚类结果拿这三个指标评分,最后挑选综合表现最好的那个 (c)。
在居民用电场景里,聚类数不建议选太多。我做过实验,当聚类数超过6时,某些类别之间用电行为特征差异已经不显著,营销策略上也难以区分对待。比较合理的落点通常在3到5类之间,对应早出晚归型、夜猫型、全天均衡型等基本生活规律。
3. PSO-FCM算法的核心实现与代码框架
3.1 PSO的数学模型
标准PSO里,每个粒子 (i) 包含位置向量 (X_i) 和速度向量 (V_i)。迭代公式为:
[ V_i^{(t+1)} = w V_i^{(t)} + c_1 r_1 (P_i - X_i) + c_2 r_2 (G - X_i) ]
[ X_i^{(t+1)} = X_i^{(t)} + V_i^{(t+1)} ]
其中 (w) 是惯性权重,(c_1) 是自我认知学习因子,(c_2) 是社会认知学习因子,(r_1) 和 (r_2) 是[0,1]之间的随机数,(P_i) 是粒子的历史最优位置,(G) 是全局最优位置。
在聚类问题里,每个粒子的位置向量长度是 (c \times d),其中 (c) 是聚类数,(d) 是特征维度。把它reshape成 (c \times d) 的矩阵,就是当前这组聚类中心。粒子的适应度直接取FCM目标函数值 (J),粒子飞行就是在寻找能让 (J) 最小的那组聚类中心。
3.2 PSO和FCM的两种结合方式
PSO和FCM的结合不是只有一种玩法,工程上有两种常见实现:
第一种叫串行式结合,先用PSO独立跑完预迭代,把得到的全局最优位置当作FCM的初始聚类中心,然后FCM继续迭代到收敛。这种方式的优点是实现简单、计算开销可控,缺点是PSO阶段没有利用FCM的梯度信息,纯靠随机搜索逼近。
第二种叫混合迭代式结合,在每次PSO迭代后,对当前粒子群产生的最优位置执行一次或数次FCM迭代,把FCM的精细化能力融进PSO的每次迭代中。这种方式收敛速度更快,代价是每次迭代的计算量明显增加。
我用的是第一种的增强版:PSO先跑30代,但每5代对全局最优粒子执行一次FCM的局部精调,这样既控制了计算量,又不浪费FCM的精细优化能力。实测下来,比纯粹串行式快了20%左右收敛,比混合迭代式省了将近一半时间。
3.3 核心Matlab代码框架
完整代码结构如下,这里给出关键部分的实现思路:
% 参数设置 nParticles = 30; % 粒子数,一般取20~40 maxIterPSO = 30; % PSO最大迭代次数 w = 0.9; % 惯性权重初始值,后期线性递减到0.4 c1 = 2.0; % 自我认知系数 c2 = 2.0; % 社会认知系数 c = 4; % 聚类数 m = 2; % FCM模糊指数 % 初始化粒子群 % 每个粒子位置是 c*d 的聚类中心矩阵展平后的向量 dim = c * size(data, 2); X = zeros(nParticles, dim); V = zeros(nParticles, dim); for i = 1:nParticles % 从数据集中随机选c个样本作为初始聚类中心 idx = randperm(size(data,1), c); X(i,:) = reshape(data(idx,:)', 1, []); end % PSO主循环 for iter = 1:maxIterPSO w = 0.9 - (0.9 - 0.4) * iter / maxIterPSO; % 惯性权重线性递减 for i = 1:nParticles centers = reshape(X(i,:), size(data,2), c)'; % 计算FCM目标函数值作为适应度 fitness_i = calcFCMFitness(data, centers, m, c); if fitness_i < pBestFitness(i) pBestFitness(i) = fitness_i; pBest(i,:) = X(i,:); end if fitness_i < gBestFitness gBestFitness = fitness_i; gBest = X(i,:); end end % 每5代对全局最优做一次FCM局部精调 if mod(iter, 5) == 0 centers = reshape(gBest, size(data,2), c)'; [centers_new, ~] = fcmStep(data, centers, m, c); gBestFitness_tmp = calcFCMFitness(data, centers_new, m, c); if gBestFitness_tmp < gBestFitness gBestFitness = gBestFitness_tmp; gBest = reshape(centers_new', 1, []); end end % 更新速度和位置 for i = 1:nParticles r1 = rand(1, dim); r2 = rand(1, dim); V(i,:) = w * V(i,:) + c1 * r1 .* (pBest(i,:) - X(i,:)) ... + c2 * r2 .* (gBest - X(i,:)); X(i,:) = X(i,:) + V(i,:); end end % 用PSO得到的最优解初始化FCM initCenters = reshape(gBest, size(data,2), c)'; [U, centers_final, objFunHistory] = myFCM(data, initCenters, m, c);这段代码的关键点有两个。第一是粒子群的初始化方式,随机从数据集中选取样本点作为初始聚类中心,比全随机生成更合理,因为能保证每个粒子起点都在数据分布范围内,极大加快收敛。第二是惯性权重的线性递减策略,前期大步长探索全局,后期小步长精细搜索,这是标准PSO性能好坏的核心所在。
3.4 适应度函数与约束处理
适应度函数计算的是FCM目标函数值,加上一个对空聚类的惩罚项。空聚类指某个聚类中心在迭代过程中被所有样本的隶属度都趋近于0,这个聚类中心成了“死节点”,白白占用一个类别名额,还会导致返回的聚类数少于设定值c。
我的处理方式是:在适应度函数里检测每个聚类中心对应隶属度之和,如果哪个类的隶属度总和小于某个极小阈值(比如1e-6),就在目标函数值上额外加一个很大的惩罚值。这样粒子在搜索过程中会自动远离那些产生空聚类的区域。这一步在工程上很重要,不然迭代后期可能莫名其妙只剩三个类,还找不到原因。
function fitness = calcFCMFitness(data, centers, m, c) n = size(data, 1); distMat = zeros(n, c); for j = 1:c diff = data - repmat(centers(j,:), n, 1); distMat(:,j) = sum(diff.^2, 2); end % 计算隶属度矩阵 invDist = 1 ./ (distMat + eps) .^ (1/(m-1)); U = invDist ./ repmat(sum(invDist, 2), 1, c); % 目标函数 fitness = sum(sum((U.^m) .* distMat)); % 空聚类惩罚 clusterMass = sum(U, 1); if any(clusterMass < 1e-6) fitness = fitness + 1e6; end end4. PSO-FCM在居民用电数据上的实验设计与结果分析
4.1 实验数据的构造与处理
我没有用公开数据集,而是自己构造了一组模拟数据来验证算法性能。构造方法是:生成三类典型的居民用电日负荷曲线——第一类是早晚双峰型(上班族),第二类是夜间单峰型(晚睡人群),第三类是白天平峰型(居家人群)。每类曲线在峰值位置、峰值大小、曲线平滑度上加随机扰动,模拟真实个体差异。
数据规模设置为300个用户,每个用户96个采样点(15分钟间隔),再额外加入5%的随机噪声和个别异常曲线(比如整条曲线全是0或者突然暴涨的尖峰),检验算法的鲁棒性。
数据标准化和PCA降维处理后,最终输入聚类的维度是50维左右(主成分累积贡献率95%)。
4.2 评价指标的设计
为了对比PSO-FCM和标准FCM的效果,我用了三个评价指标:
第一个是目标函数值的收敛稳定度。同一份数据分别跑20次,记录每次迭代终止时的目标函数值,计算均值和方差。方差越小说明算法对初始值的敏感度越低,稳定性越好。
第二个是轮廓系数。轮廓系数综合了簇内紧凑度和簇间分离度,取值在[-1,1]之间,越接近1说明聚类效果越好。公式是:
[ S = \frac{1}{n} \sum_{i=1}^{n} \frac{b(i) - a(i)}{\max{a(i), b(i)}} ]
其中(a(i))是样本(i)到同类其他样本的平均距离,(b(i))是样本(i)到最近其他簇内所有样本的平均距离。
第三个是兰德指数。因为有模拟数据,我知道每个样本的真实类别标签,可以直接用调整兰德指数(ARI)评估聚类结果和真实标签的吻合程度。ARI越接近1说明聚类结果越接近真实划分。
4.3 实验结果对比和解读
我跑了20次标准FCM和20次PSO-FCM,典型的实验数据如下:
| 指标 | 标准FCM(20次均值) | PSO-FCM(20次均值) | 提升幅度 |
|---|---|---|---|
| 目标函数值方差 | 452.3 | 22.6 | 95.0% |
| 目标函数值均值 | 1542.7 | 1521.4 | 1.4% |
| 轮廓系数 | 0.432 | 0.471 | 9.0% |
| 兰德指数 | 0.857 | 0.893 | 4.2% |
数据表现最亮眼的不是目标函数均值下降了多少,而是方差从452.3降到了22.6。这意味着PSO-FCM把聚类结果从“碰运气”变成了“稳定发挥”。任何一个业务方都不会放心把策略建立在一个跑十次变五次结果的模型上,而方差减少了95%之后,聚类结果基本每次都一样,这个特性价值极大。
轮廓系数从0.432提升到0.471,说明聚类结构更清晰了。兰德指数从0.857提升到0.893,说明PSO找到的初始中心确实更逼近全局最优,后续FCM精调的空间更小但结果更准。
4.4 聚类结果在居民用电行为上的业务解读
聚类完成后,我拿每类的平均负荷曲线和高峰时段做了行为画像。结果大致是这样的:
第一类用户,早高峰集中在7点到9点,晚高峰集中在18点到22点。这类用户的负荷曲线呈现明显的M形,用电行为规律、可控,适合参与需求响应和分时电价优化。
第二类用户,负荷从晚上22点开始持续爬升,夜间零点到凌晨三点达到峰值,白天整体偏低。这类用户对夜间电价更敏感,是错峰用电、夜间充电策略的重点对象。
第三类用户,负荷曲线相对平缓,白天平均水平较高,没有明显尖峰。这类用户可能是自由职业者或者家中有老人孩子,白天用电占比大,通常适合装光伏,白天自发自用抵消峰时电价。
这些结论在标准FCM的结果里也会出现,但标准FCM每次跑出来的类别边界不稳定,第一类和第三类偶尔会混在一起。PSO-FCM把这种边界不确定性压到了很低的程度,做业务报告的时候不用再心虚地加一句“结果仅供参考”。
5. 实操中的常见问题与排查技巧
5.1 迭代后期粒子群收敛到同一位置但目标函数还在跳
这个问题我排查了很久才找到根因。现象是PSO迭代到20代之后,粒子位置几乎完全一样,看起来已经收敛了,但目标函数值还在小幅波动。后面加日志打印才发现,问题出在速度更新上。粒子位置虽然相同,但速度没有衰减到零,还在继续扰动位置,导致粒子在最优解附近“抖”。
解法有两个:一是在速度更新时加入速度最大值限制 (V_{max}),通常设为变量取值范围的10%-20%;二是初始惯性权重w设置足够大(0.9左右),并在后期衰减到0.4以下,否则粒子收敛后惯性作用仍然过大,停不下来。
5.2 FCM里设置了4个聚类中心,结果只返回3个类
这是空聚类问题。在迭代过程中,某个聚类中心离所有样本都很远,所有样本对它的隶属度都趋近于零,这个中心就“死”了。我在适应度函数里加了空聚类的惩罚项之后,这个问题基本消失。
但另一种情况要特别小心:如果聚类数相对于数据本身的自然分布来说过大,即使加了惩罚项也还是会出空类。比如数据天然只有三类,你非要聚五类,那PSO再怎么优化也救不回来。这时候先降聚类数,不要一味调算法参数。
5.3 不同数据集的归一化方式会对聚类结果造成巨大影响
我做过一组对比实验:同一批数据,一种方式是用全体数据的最大值做归一化,另一种方式是按每个用户自己的最大值分别归一化。结果聚类出来的行为画像完全不同。前者突出了用户用电总量之间的差异,后者突出了用电形态之间的差异。
如果你关注的是用电行为模式,强烈建议对每条负荷曲线单独做归一化(按用户自己的最大负荷值或总电量来归一化)。但如果你关注的是用户价值分层,那就应该保留总量差异信息,用全局归一化。这个选择的业务含义完全不一样,必须在项目一开始就想清楚。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| PSO收敛慢,50代还不稳 | 惯性权重过大或粒子数太少 | w初始设为0.9,线性递减到0.4;粒子数加到40 |
| FCM目标函数值方差大 | 初始聚类中心不好,局部最优 | 换PSO-FCM方案,或跑多次取最优 |
| 聚类结果出现空类 | 聚类数过多或初始中心不覆盖数据范围 | 减小c值;初始化时从样本中随机选中心 |
| 轮廓系数一直很低(<0.3) | 数据未标准化,或特征噪声太大 | 检查归一化;用PCA或T-SNE降噪后重跑 |
| 跑出的类别业务意义不明显 | 特征选择有问题,信息冗余 | 只保留关键时段统计量,如峰谷差、夜间均值占比等 |
| Matlab报“矩阵维度不一致” | 粒子位置维度和数据维度不一致 | 检查reshape是否跟(特征维度,聚类数)的转置匹配 |
5.5 一个容易忽略的工程细节:随机数种子
Matlab里rand和randperm每次运行的结果都不同。对于贪心式的FCM聚类来说,这意味着结果天然不可复现。我的习惯是在脚本开头显式设置随机种子:
rng(42);这一行代码不算什么高深技巧,但对于论文复现、算法对比、业务审查这些场景来说,可复现性是硬要求。另外在对比算法时,要保证各算法用的是同一组初始中心,这样对比才公平。做法是先用rng固定种子,再把初始中心存下来,传给每个算法。
6. 代码工程化的进一步建议
6.1 用匿名函数和子函数拆分逻辑
我的Matlab代码没有写成一个巨型脚本,而是拆成了三个文件:main.m负责参数设置、数据加载和结果可视化,psoFCM.m封装PSO-FCM完整算法,calcFCMFitness.m封装适应度函数。这样调试起来方便得多,尤其是想把手动改成自动对比实验的时候,直接循环调用psoFCM就行。
% main.m中循环跑不同聚类数的示例 for c = 2:6 [U, centers, hist] = psoFCM(data, c, params); silhouettes = evalclusters(data, 'kmeans', 'Silhouette'); % 记录结果... end6.2 从“能跑”到“跑得稳”的参数调优路径
我是按这个顺序调参的:先固定其他参数,把聚类数c从2到8全部扫一遍,用轮廓系数和DBI选最优c;然后固定c,调节PSO的粒子数和迭代次数,看目标函数变化曲线是否平滑收敛;最后调FCM的模糊指数m,从1.5到2.5按0.1的步长扫描,观察对轮廓系数的影响。
实践中最值得花时间的是聚类数扫描,这个对结果的影响远大于PSO参数微调。很多初学者把精力花在调PSO的w和c1、c2上,折腾半天发现轮廓系数只涨了0.01,但c从3改成4,轮廓系数直接涨了0.08。方向比努力重要。
6.3 画图看结果比看数字直观得多
聚类分析一定要画图。我强烈建议至少画三张图。第一张是各类别的平均负荷曲线,把每条曲线按类别分开画,供业务方直观理解;第二张是每个用户负荷曲线按聚类类别着色后的PCA降维散点图,看聚类边界是否清晰;第三张是目标函数随迭代次数变化的收敛曲线,验证算法是否真的收敛。
Matlab里就是plot和scatter的组合,配上legend和grid on,效果足够专业。需要声明的是,仿真数据实验理论上得不出唯一结果,但观察到的趋势(方差骤降、轮廓系数稳定提升、收敛曲线平滑)是稳定的,换随机种子基本都成立。
6.4 从Matlab代码到实际业务落地的衔接
Matlab实现了算法验证之后,真正上线时Python居多。我的做法是先用Matlab把整套流程跑通,确定最优参数,然后用Python的pyswarm库和scikit-fuzzy库做功能对等移植,最后封装成服务接口。Matlab版本依然是黄金参考标准,任何参数变化先在Matlab里验证,验证通过再同步到线上环境。
7. 几点实操后的真心话
PSO-FCM组合在居民用电行为分析这个场景下,最大的价值不是说聚类效果比FCM好了一个数量级,而是把结果从“看运气”变成了“看规律”。业务部门最怕的不是模型效果差,而是模型效果不稳定,昨天三个类别今天四个类别,前天的结论拿到今天就不成立。这种不稳定性对信任的杀伤力是致命的。
如果数据量不大(几千个用户以内),PSO-FCM在Matlab里跑到收敛也就几秒钟到十几秒钟,完全够用。但如果数据量到了百万级别,建议先把用户按用电总量粗分桶,在桶内分别用PSO-FCM,避免一次性聚类带来的性能瓶颈和可解释性灾难。
另外,模糊隶属度这个信息别浪费。FCM输出的隶属度矩阵除了用来分类,还可以做很多事。比如设定一个置信度阈值,把高置信用户和低置信用户分开运营;再比如对每个用户计算它到每一个类的隶属度向量,做二级特征喂给下游的推荐或者营销模型。这些都是FCM比硬聚类多出来的信息价值。
如果你正在做类似的工作,我的建议是:先把数据标准化和特征选择做到位,这两个环节决定了问题的上限;再用PSO-FCM把聚类结果稳住,这决定了你能拿到多少上限;最后别急着写论文或者汇报,把每一类的负荷曲线画出来找业务方聊一聊,让算法结果和业务认知相互印证,这个闭环走通了,这个项目的价值才真正体现出来。