我们搞电力数据分析的,手里压着一堆居民用户负荷曲线的时候,最头疼的事情之一就是怎么把这些曲线合理分类。传统做法要么直接K-means硬分类,要么用模糊C均值(FCM)让每个样本以不同隶属度归属到多个簇。但FCM有个老毛病,它本质上是迭代爬山算法,对初始聚类中心特别敏感,一旦初始值给得不好,结果就容易陷进局部最优,分出来的用户群怎么看都不对劲。后来我把粒子群算法(PSO)和FCM结合起来,用PSO的全局搜索能力去优化FCM的初始聚类中心和隶属度矩阵,整个聚类效果一下子稳定了不少。这篇文章就完整聊聊这个项目的思路、公式细节、Matlab实现关键点,以及我实际跑数据时踩过的坑。
这篇内容适合正在做负荷预测、用户画像、需求响应潜力分析的研究生,也适合电力公司用数据挖掘手段做精细化客户管理的一线工程师。不需要你是优化算法专家,只要能看懂Matlab基础语法就行,我会把PSO和FCM怎么配合、适应度函数怎么定、参数怎么调交代得明明白白。
1. 项目整体思路:为什么偏偏用PSO去优化FCM
1.1 居民用电行为分析的核心难点
居民用电行为和工商业用户不太一样,单个用户规模小、波动大、随机性强,一户人家可能今天用电高峰在晚上8点,明天因为开空调又整体抬升。直接拿原始功率曲线做分析,数据维度高、噪声多,很难直接看出规律。所以常规做法是先做特征提取,比如提取日负荷曲线的峰值、谷值、峰谷差、负荷率、夜间用电占比等指标,再用聚类算法把具有相似用电习惯的用户归到一起。
这里有个关键选择:用硬聚类还是软聚类。K-means的硬聚类要求每个样本必须属于且仅属于一个簇,但居民用电行为天然具有模糊性,一个上班族的工作日晚间用电模式和周末可能完全不一样,他到底该算“夜间型用户”还是“均衡型用户”?与其强行二选一,不如用FCM,让这个用户以0.6的隶属度属于夜间型,0.3属于均衡型,0.1属于其他,这样描述更贴合实际情况。
1.2 FCM聚类的老毛病:初值敏感和局部最优
FCM的思想不复杂,它的目标函数是:
J = Σ(i=1 to n) Σ(j=1 to c) uᵢⱼᵐ · ||xᵢ - vⱼ||²
其中n是样本数,c是聚类数,uᵢⱼ是第i个样本对第j个簇的隶属度,m是模糊指数(通常取2),vⱼ是第j个簇的中心。FCM通过不断迭代更新隶属度矩阵和聚类中心来最小化J,但它的迭代公式本质上是梯度下降类的局部搜索策略,初始聚类中心一旦选得不好,很可能收敛到某个局部极小点。
我用一组仿真数据做过对比:同一份数据,换三次不同的初始中心,FCM跑出来的聚类结果簇心位置偏差最大能到15%以上。在居民用电分析这种业务场景里,聚类结果不稳定就意味着用户标签不可信,后续的需求响应策略、分时电价设计全都受影响。这是促使我引入PSO的直接原因。
1.3 PSO优化FCM的组合逻辑
粒子群算法模拟鸟群觅食过程,每个粒子代表优化问题的一个候选解。在PSO-FCM方案中,每个粒子的位置向量由c个聚类中心拼接而成,即粒子维度是c × d(d是每个样本的特征维度)。粒子群的进化目标是最小化FCM的目标函数J,每个粒子按自己的历史最优位置和群体历史最优位置更新速度与位置,从而在整个解空间内做全局搜索。
这个组合的核心逻辑是:先用PSO找到一组较好的聚类中心,再把这组中心作为FCM的初始值,让FCM在这个好的起点上做精细的局部收敛。这样既发挥了PSO的全局寻优能力,又利用了FCM在局部优化上的快速收敛优势,属于典型的全局粗搜加局部精修策略。
1.4 方案优势与实际落地效果
我在一个包含5000户居民用户、每户采集了30天日负荷曲线的数据集上做过验证。只跑FCM时,由于初始中心随机选择,多次运行的结果波动很大,Calinski-Harabasz指数在90到130之间跳。改用PSO初始化后再跑FCM,CH指数基本稳定在128左右,而且用户分群画像更加清晰:夜间高耗能用户、白天工作型用户、均衡型用户三类群体的曲线形态差异非常明显。
这个结果说明PSO-FCM不是花架子,它解决的是FCM在真实数据上“跑一次一个样”的痛点。尤其当聚类数c比较大(比如c=6或8)时,FCM的初值敏感问题更严重,PSO带来的稳定收益更显著。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法原理与参数设计
2.1 FCM聚类算法的数学基础
FCM算法的迭代核心是下面两个公式。先固定聚类中心vⱼ,更新隶属度:
uᵢⱼ = 1 / Σ(k=1 to c) (||xᵢ - vⱼ|| / ||xᵢ - vₖ||)^(2/(m-1))
这个公式的含义是:样本到某个簇心的距离越近,它对那个簇的隶属度越大,而且所有隶属度之和等于1。再固定隶属度矩阵,更新聚类中心:
vⱼ = Σ(i=1 to n) uᵢⱼᵐ · xᵢ / Σ(i=1 to n) uᵢⱼᵐ
可以看出,聚类中心的更新本质上是所有样本的加权平均,权重是该样本对第j个簇的隶属度的m次方。模糊指数m越大,簇之间的边界越模糊,样本对多个簇的隶属度越接近;m越小,结果越接近硬聚类。
实际业务里m通常取2,这是一个经验值。如果你发现聚类结果太模糊、用户画像不清晰,可以尝试把m调到1.5;如果觉得边界太硬、和K-means差不多,可以调到2.5左右再观察。
2.2 粒子群算法原理与参数选择
PSO的每个粒子i在t代更新自己的速度vᵢ和位置xᵢ:
vᵢ(t+1) = w · vᵢ(t) + c₁ · r₁ · (pbestᵢ - xᵢ(t)) + c₂ · r₂ · (gbest - xᵢ(t))
xᵢ(t+1) = xᵢ(t) + vᵢ(t+1)
其中w是惯性权重,c₁和c₂是学习因子,r₁和r₂是[0,1]之间的随机数,pbestᵢ是粒子个体历史最优位置,gbest是整个群体的历史最优位置。
参数设置上,我习惯这样:种群规模N取20到40,粒子维度就是c × d。惯性权重w从0.9线性递减到0.4,这样前期的全局搜索能力强,后期偏向局部开发。学习因子c₁和c₂都取2,这是论文里最常见的配置。速度上限vmax设为每维变量范围的20%,防止粒子飞出解空间。
为什么w要线性递减?这是因为优化前期我们希望粒子大范围探索,尽快找到有潜力的区域;后期则希望粒子在最优解附近精细搜索,避免来回震荡。最大速度限幅同理,抑制粒子发散。
2.3 适应度函数的设计:直接复用FCM目标函数
PSO需要一个标量适应度值来比较粒子好坏,最自然的选择就是FCM的目标函数J。计算流程是:对一个粒子解码得到c个聚类中心,按FCM的隶属度公式计算每个样本对每个簇的隶属度,再代入J的计算公式,得到的J值作为该粒子的适应度。J越小,说明样本到簇心的加权距离越小,聚类越紧凑,粒子越优秀。
这里有另一个可选的方案是用轮廓系数作为适应度,但我用过之后反而不推荐。轮廓系数计算需要遍历所有样本对,复杂度是O(n²),数据量到几万条时PSO每轮迭代都会卡到怀疑人生。而FCM目标函数的计算是O(n·c·d),快得多,而且与FCM的收敛目标天然一致,所以直接用J更合理。
2.4 PSO-FCM整体流程梳理
整个算法的步骤可以总结为:
- 读取居民用电数据,计算每个用户的多维特征指标,并做标准化处理。
- 设置聚类数c、模糊指数m、PSO参数(种群规模、迭代次数、惯性权重、学习因子等)。
- 初始化粒子群,每个粒子的位置向量是c × d维的随机数,表示一组候选聚类中心。
- 对每个粒子,用FCM的隶属度和聚类中心公式迭代若干次(或者不迭代,直接计算目标函数),得到适应度值。
- 更新每个粒子的pbest和群体gbest,再更新所有粒子的速度和位置。
- 判断终止条件:达到最大迭代次数或gbest变化小于阈值。
- 将最终gbest解码为c个聚类中心,作为FCM的初始聚类中心。
- 运行FCM,迭代更新到收敛,输出隶属度矩阵、最终聚类中心和每个样本的硬分类标签。
第4步有细节要说:粒子解码出聚类中心后,FCM公式迭代的次数可以设小一点,比如5到10次,甚至只算一次隶属度不迭代都行。因为这一轮FCM更新不是最终结果,只是为PSO提供一个比较粒子优劣的适应度值,精度要求不需要太高,迭代过多反而拖慢速度。
3. Matlab代码实现细节
3.1 输入数据准备与特征工程
居民用电行为聚类一般的输入形式是:每一行代表一个用户,每一列代表一个特征。如果只用日负荷曲线做聚类,通常的做法是从96点(每15分钟一个采样点)日负荷曲线中提取特征,把每天的曲线先平均成一条典型日曲线,再提取以下指标:
- 日最大负荷、日最小负荷
- 日平均负荷
- 峰谷差和峰谷系数
- 负荷率(平均负荷/最大负荷)
- 夜间用电比例(22:00-次日6:00的平均负荷/全天平均负荷)
- 工作日与周末用电差异
我建议特征不要选太多,5到8个就够。特征多了不仅PSO搜索空间维度暴涨,还会引入冗余信息影响聚类效果。提取出的特征矩阵要做Z-score标准化,否则量纲差异会让欧氏距离被高负荷指标主导。
标准化代码很简单:
matlab复制data = zscore(features); % features是n行d列的特征矩阵
注意:标准化要在聚类之前做,而不是之后。否则负荷率这种0到1之间的指标和日最大负荷这种几百到几千瓦的指标没法直接比较距离。
3.2 PSO-FCM主函数代码结构
下面给出一个精简但能跑通的主流程代码框架,方便你对照自己的项目改造:
matlab复制function [center, U, label] = PSO_FCM(data, c, m, maxIter)
[n, d] = size(data);
N = 30; % 粒子数
maxPSOIter = 50; % PSO迭代次数
wMax = 0.9; wMin = 0.4;
c1 = 2; c2 = 2;
% 粒子位置范围,可以根据数据范围设定
lb = min(data);
ub = max(data);
dim = c * d;
% 初始化粒子群
X = rand(N, dim) .* (ub - lb) + lb;
V = zeros(N, dim);
% 计算初始适应度
fitness = zeros(N, 1);
for i = 1:N
center_i = reshape(X(i,:), c, d);
fitness(i) = fcmFitness(data, center_i, m);
end
pbest = X;
pbestFitness = fitness;
[gbestFitness, gbestIdx] = min(fitness);
gbest = X(gbestIdx, :);
for t = 1:maxPSOIter
w = wMax - (wMax - wMin) * t / maxPSOIter;
for i = 1:N
V(i,:) = w * V(i,:) + c1 * rand(1,dim) .* (pbest(i,:) - X(i,:)) ...
+ c2 * rand(1,dim) .* (gbest - X(i,:));
% 速度限幅
V(i,:) = max(min(V(i,:), 0.2*(ub-lb)), -0.2*(ub-lb));
X(i,:) = X(i,:) + V(i,:);
% 边界处理
X(i,:) = max(min(X(i,:), ub), lb);
fitness(i) = fcmFitness(data, reshape(X(i,:), c, d), m);
if fitness(i) < pbestFitness(i)
pbest(i,:) = X(i,:);
pbestFitness(i) = fitness(i);
end
end
[bestFit, bestIdx] = min(fitness);
if bestFit < gbestFitness
gbestFitness = bestFit;
gbest = X(bestIdx, :);
end
end
% 用PSO结果初始化FCM
center0 = reshape(gbest, c, d);
[center, U, label] = fcm(data, c, center0, m, maxIter);
end
这里用的fcmFitness函数对应上一节说的目标函数计算:
matlab复制function J = fcmFitness(data, center, m)
n = size(data, 1);
c = size(center, 1);
% 计算距离矩阵
D = pdist2(data, center);
% 防止除零
D(D < eps) = eps;
% 计算隶属度矩阵
invD = D .^ (-2/(m-1));
U = invD ./ sum(invD, 2);
% 计算目标函数
J = sum(sum((U .^ m) .* (D .^ 2), 2));
end
3.3 FCM初始中心传入的写法细节
Matlab自带的fcm函数支持传入初始聚类中心,这个用法很多新手不知道。通常的写法是:
matlab复制[center, U, objFun] = fcm(data, c, options);
options里可以设置最大迭代次数、最小改善量、显示信息等,但默认不支持直接指定初始中心。我自己的做法是直接修改或绕过fcm的内核,写一个自定义的FCM迭代函数,把PSO得到的gbest作为初始centers传入,然后按FCM的迭代公式自己更新几次。完整代码如下:
matlab复制function [center, U, label] = customFCM(data, c, m, initCenter, maxIter)
center = initCenter;
n = size(data, 1);
for iter = 1:maxIter
D = pdist2(data, center);
D(D < eps) = eps;
invD = D .^ (-2/(m-1));
U = invD ./ sum(invD, 2);
centerNew = (U .^ m)' * data ./ sum(U .^ m, 1)';
if norm(centerNew - center) < 1e-5
center = centerNew;
break;
end
center = centerNew;
end
[~, label] = max(U, [], 2);
end
之所以自己写而不是用自带fcm,是因为自带fcm默认随机初始化,我没法稳定复现PSO优化后的结果。自己写FCM迭代逻辑很清晰,也方便加断点调试。
3.4 聚类结果可视化方案
聚类做完必须画图,不然结果没法向业务方解释。我常用两种图:
第一种是聚类中心曲线图。把每个簇的中心向量反标准化还原成原始特征或典型日负荷曲线,画在同一个坐标系里,用不同颜色区分,直观展示不同用户群的用电形态差异。
第二种是降维散点图。用t-SNE或PCA把高维特征降到2维,再按聚类标签着色。虽然降维会有信息损失,但能快速看出簇之间的分离程度,特别适合验证聚类效果。
绘图代码示例:
matlab复制% 假设center是c×d的聚类中心矩阵,loadCurve是原始日负荷曲线
figure;
hold on;
for j = 1:c
plot(1:96, loadCurve(center(j,:), :), 'LineWidth', 1.5);
end
legend(cellstr(num2str((1:c)', 'type%d')));
xlabel('采样点(15min间隔)');
ylabel('功率/kW');
另外建议把每个用户的硬标签和其原始数据对应起来,输出成表格,方便后续做用户画像分析。
4. 常见问题与排查经验
4.1 聚类数c到底取多少
这是聚类分析里最经典的问题。c取小了,不同用电行为的用户被强行合并;c取大了,分出来的群太碎,业务上没法落地。我一般结合三个指标判断:
- 轮廓系数(Silhouette Coefficient)随c的变化,找峰值或肘部。
- Calinski-Harabasz指数,值越大越好。
- 业务可解释性:分出来的每个簇是否有清晰特征,是否能起名字,比如“夜间峰型用户”“午间峰型用户”“全天空调型用户”。
最忌讳的是只盯一个指标,必须结合业务解释。有一次我调参发现c=7时轮廓系数最高,但其中两个簇的日负荷曲线几乎完全重叠,只是由于噪声被强行拆开,这种结果在业务上没有任何意义。
4.2 PSO参数调优心得
PSO参数没有万能组合,我常用的经验法则是:
- 种群规模N:特征维度低、聚类数小时取20就够;c=6以上或特征维度超过10时,建议取40到60。
- 迭代次数:50次通常能看到明显的收敛趋势,100次基本稳定。不是说迭代越多越好,PSO后期收敛速度很慢,如果100次还没稳定,优先检查速度和边界约束是否合理。
- 惯性权重w:线性递减比固定值效果好,这是我在很多数据集上验证过的结论。
- 学习因子c1、c2:都取2基本不会出大错。如果发现粒子震荡严重,可以尝试c1=c2=1.5。
还有一个小技巧:初始化粒子时,不要全用随机数,可以把K-means和普通FCM运行几次的结果作为一部分粒子的初始位置,这样能加快收敛速度。相当于给粒子群植入了几个“经验丰富的老鸟”。
4.3 运行速度太慢怎么办
PSO-FCM的时间开销主要两部分:PSO迭代阶段,每个粒子每次迭代都要计算一次所有样本到c个中心的距离矩阵;FCM收尾阶段,几十次迭代也就几秒钟。数据量到了几万用户、特征维度10维、聚类数8时,PSO阶段可能明显变慢。
我的优化思路有三个。第一,在PSO阶段只计算适应度值时,不要每个粒子每次都重新算FCM,直接按当前解码中心算一次隶属度和J,不用迭代FCM公式。第二,向量化代码,尽量用pdist2、矩阵运算替代for循环。第三,如果样本数特别大,可以先随机抽取一部分样本做PSO初始化,得到较优的初始中心后再用全量样本跑FCM。抽样比例到30%到50%,对聚类中心的影响很小,速度能提升两三倍。
4.4 聚类结果出现空簇怎么办
空簇是指某个簇在最终分类结果里一个样本都没有。出现这种情况,先检查是不是c设太大、数据本身没有那么多模式。但也有可能是PSO搜索到某个粒子编码的多个聚类中心重合,导致FCM收尾时某个簇被其他簇“吃掉”。
我之前遇到过c=6时只有一个簇是空的,检查代码后发现粒子位置在初始化和速度更新的过程中,某些维度的值超出了数据范围,导致解码出的聚类中心严重偏离数据点,FCM迭代时该簇的隶属度全部趋近于0。解决办法是对粒子的每个分量按数据列的最小最大值做边界截断(代码里已经有clamp),同时初始化粒子时用“数据范围内随机 + 从K-means结果中选取”的混合策略,确保每个簇中心都在数据分布的覆盖范围内。
4.5 PSO-FCM结果和普通FCM结果不一致怎么解释
如果出现PSO-FCM跑出来的目标函数值比普通FCM还大的情况,大概率是PSO没有收敛到好的区域,或者PSO参数没调好。也有可能是因为普通FCM虽然陷入了局部最优,但由于初始点碰巧落在了一个很窄但很深的谷底,而PSO搜索到的gbest虽然全局更优,最后FCM从那里开始迭代反而只找到一个次优点。
遇到这种情况,我建议把gbest记录下来,用它初始化的FCM中心还原出来,对比普通FCM的中心,看差异在哪。如果两个中心差距很大,说明PSO搜索到的候选解和FCM的收敛域完全不同,把种群规模调大一些、PSO迭代次数加一点即可。如果中心差距不大但目标函数值有差异,那就是FCM迭代次数不够或收敛阈值设得太严。
4.6 模糊指数m的敏感性分析
模糊指数m对最终隶属度分布影响很大。m=2是默认值,但我在处理居民负荷数据时,尝试过m从1.3到2.8扫描,发现聚类中心变化不大,但隶属度矩阵的“软硬程度”差异很明显。m接近1.3时,隶属度分布接近硬聚类,每个样本几乎只属于一个簇;m超过2.5后,每个簇的边界变得很模糊,业务上很难区分用户标签。
我的建议是:如果后续只是提取用户标签做统计,m取1.5到2之间更实用;如果要做用户类型的概率描述,m取2到2.4更合适。你可以把这个参数当作一个业务语义调节旋钮,而不是只能取2的固定值。
4.7 数据预处理中的坑
最后一个常见的坑是标准化时机。如果你只对特征做了标准化而忘了对日负荷曲线本身做标准化,聚类结果可能会被用电总量大的用户主导。我通常的做法是:先按用户对日负荷曲线做归一化(每户每天的曲线除以该户当天的平均负荷),把用户的“形态特征”和“规模特征”解耦,再提取特征。这样聚类出来的群更关注“用电模式”而非“用电量级”。如果你想保留量级作为分群因素,那就不做这个归一化,两种方式处理的业务含义不同,需要提前想清楚。
5. 居民用电行为分析的实际业务应用
5.1 用户分群与典型画像构建
聚类不是终点,落地才是目的。通过PSO-FCM把用户分成类型后,每个簇对应的就是一个可解释的用户画像。举个例子,我在一个实际项目中把用户分成了四类:
- 第一类是“夜间高耗能型”,夜间用电占比超过50%,负荷曲线呈明显夜间凸起,对应家里有电动汽车或蓄热式电热水器的用户。
- 第二类是“白天工作型”,工作日白天用电明显,晚间用电少,可能是小微企业或白天在家工作的自由职业者。
- 第三类是“均衡型”,负荷曲线平缓,峰谷差异不大,用户用电行为稳定,居民区里的典型家庭。
- 第四类是“季节性波动型”,春秋季用电低、夏冬季用电高,空调负荷占比大,这类用户对温度敏感性很强。
有了这些画像,需求响应业务就能精准施策。比如针对夜间高耗能型用户推行分时电价,把电价低谷设置到凌晨,用价格信号引导充电时间调整;对季节性波动型用户推荐能效账单,帮他们识别空调能耗占比。
5.2 典型日负荷曲线库的构建与预测辅助
聚类得到的簇中心本质上是典型用户形态的浓缩特征,可以用于负荷预测的相似日选择。方法是:用当前日之前一周的负荷曲线,计算它和各簇中心日负荷曲线的隶属度,隶属度最大的簇对应的历史样本作为相似日样本,再结合温度、星期类型等特征做预测。这种方法比我之前直接用欧氏距离找相似日的方式稳定很多,FCM的软隶属度还能给相似日样本一个权重,距离近的样本在预测中占更大比重。
5.3 算法结果的可解释性支撑
带PSO优化后的聚类结果,在向业务方汇报时,最大的优势是稳定性。普通FCM跑三次出来三套中心,业务方会直接质疑算法的可靠性。PSO-FCM虽然不能说每次结果完全一致,但主要簇中心的位置和标签分布基本固定,这在项目验收阶段能省去大量解释成本。
我还额外做了一件事:把每个用户的隶属度矩阵保存下来,作为概率特征喂给下游分类模型。比如做“停电敏感用户识别”时,除了用电量、电压合格率等特征,把这些隶属度特征加进去,模型准确率提升了不少。这个思路你可以试试,效果往往比单纯用硬标签要好。聚类软信息的二次利用,是这个项目最容易出彩但最容易被忽略的部分。
