做居民用电行为分析,最核心的一步就是聚类。可现实情况是,很多人拿K-means或者FCM跑完一版,结果出来一看,样本全挤在一个簇里,剩下几个簇都是空的;或者今天跑和明天跑,聚类结果完全不一样。这种不稳定性,我在处理智能电表数据时遇到过太多次。后来把粒子群算法(PSO)和FCM聚类结合起来,用PSO的全局寻优能力去覆盖FCM对初始聚类中心过于敏感的短板,再用优化后的聚类中心去划分居民用电行为,整体结果稳定了不少。
这篇文章就是把整套思路和Matlab实现完整记录一遍,从原理拆解到代码骨架,再到参数调优和常见报错。项目名字叫“基于粒子群算法优化FCM聚类的居民用电行为分析研究”,听着有点长,但拆开看就是三件事:粒子群算法、FCM聚类、居民用电行为分析。适合正在做电力负荷数据挖掘的学生,也适合已经在用FCM但一直被初值问题折腾的工程师。
先说结论:PSO-FCM并不是一个特别高深的方法,它只是在传统FCM前面加了一个“找好初始中心”的预处理步骤。但恰恰是这一步,能把聚类结果从“随机抽奖”变成“可复现的稳定输出”。下面我按自己实际做的流程来讲,尽量把每个选择背后的原因都说清楚。
1. 项目背景与整体设计思路
1.1 居民用电行为分析到底在分析什么
居民用电行为分析,本质上是从用户的历史负荷数据里提取出“规律”。智能电表通常每15分钟或者每小时采集一个功率值,一天下来就是24个点或者96个点,一个月就是几千个点。这些原始数据直接拿去做聚类,维数高、噪声大,很难得到有解释力的结果。
所以实际操作中,我们一般会先构造特征。常用特征包括:峰期平均负荷、谷期平均负荷、日负荷率(平均负荷/最大负荷)、峰值出现时刻、夜间用电占比等等。把这些特征拼成一个向量,每个用户就变成了一个多维空间里的点。聚类的目标是把这些点分成若干组,每组代表一种典型的用电模式。
比如我手头有一批某小区用户的月度负荷曲线,经过特征提取后发现大致有四类:一类是典型的“双峰型”,早高峰和晚高峰都很突出;一类是“夜间活跃型”,晚上10点后负荷反而升高;一类是“全天平稳型”,负荷波动很小;还有一类是“周末型”,工作日和周末差异极大。这些画像看起来简单,但落到具体业务上,可以直接指导需求侧响应、分时电价套餐设计,甚至帮助供电公司识别异常用电行为。
1.2 为什么选FCM聚类,而不是直接用K-means
K-means是硬聚类,每个样本必须属于且只属于一个簇。但居民用电行为之间的边界其实很模糊,一个用户可能既像“上班族”又有“夜猫子”倾向,这时候硬生生把它归到某一类,信息损失就比较大。
FCM(模糊C均值聚类)则允许一个样本以不同的隶属度属于多个簇,隶属度在0到1之间,并且对每个样本而言,所有簇的隶属度之和为1。这就更贴合“用户行为不可能是单一模式”的现实。用FCM聚出来的结果,除了每个用户的最终标签,还能得到它跟每个聚类中心的相似程度,这个信息在电力营销场景里非常有用。
但FCM有一个众所周知的毛病:它对初始聚类中心非常敏感。FCM本质上是迭代求解一个非线性优化问题,目标函数是非凸的,存在大量局部极小值。如果初始中心选得不好,迭代很容易收敛到某个局部最优点,聚类结果就会偏离真实结构。实际情况就是:换一组随机初始中心,跑出来的簇可能完全不一样。
1.3 粒子群算法在这里扮演什么角色
粒子群算法是一种群体智能优化算法,灵感来自鸟群觅食。每个“粒子”代表一个候选解,通过不断向自身历史最优位置和群体历史最优位置学习,逐步逼近全局最优解。
在这个项目里,我把每个粒子编码成一组聚类中心,具体来说就是K个簇中心拼接成一个向量。粒子的位置就是候选的聚类中心组合,粒子的适应度就是FCM的目标函数值。PSO的任务就是在搜索空间里找到一组让FCM目标函数尽可能小的聚类中心,然后把这一组中心作为FCM的初始中心,再做一次标准的FCM迭代精炼。
一句话概括:PSO负责“全局搜索”,FCM负责“局部精炼”。这种组合方式比单纯用FCM从随机起点开始跑要稳得多,也比完全用PSO替代FCM迭代要高效得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心原理拆解
2.1 FCM聚类的目标函数和迭代逻辑
FCM的目标函数是这样的:
J = sum_i sum_j u_ij^m * ||x_i - c_j||^2
其中x_i是第i个样本,c_j是第j个聚类中心,u_ij是样本i对簇j的隶属度,m是模糊指数(通常取2),||x_i - c_j||^2是欧氏距离的平方。
FCM通过迭代更新隶属度矩阵U和聚类中心矩阵C,直到目标函数不再显著下降。更新公式我放在后面代码部分再给。这里只强调一个关键点:整个迭代过程是坐标下降式的,从一个初始C出发,反复交替更新U和C,本质上是一个爬山过程。如果初始C没选好,很容易爬到某个局部山顶就不动了。
2.2 粒子群算法的位置更新与速度更新
PSO的每个粒子i在t时刻有一个位置向量X_i(t)和一个速度向量V_i(t)。在每次迭代中,每个粒子记录自己历史最优位置Pbest_i,群体记录全局最优位置Gbest。
速度更新公式:
V_i(t+1) = w * V_i(t) + c1 * r1 * (Pbest_i - X_i(t)) + c2 * r2 * (Gbest - X_i(t))
位置更新公式:
X_i(t+1) = X_i(t) + V_i(t+1)
w是惯性权重,控制粒子继承上一时刻速度的程度;c1和c2是学习因子,一般取2左右;r1和r2是[0,1]之间的随机数,增加搜索的随机性。
在PSO-FCM中,X_i就是一组聚类中心向量。比如样本维度是D,聚类数是K,那么每个粒子的维度就是K*D。假设K=4,D=8,粒子长度就是32。这个向量前8个元素代表第一个中心,第9到第16个元素代表第二个中心,以此类推。
3. Matlab实现核心细节
3.1 数据准备与特征构造
在写PSO-FCM之前,先要把数据整理成标准格式。我一般用一张表格存原始负荷数据,每一行是一个用户,每一列是一个时间段。然后做两步处理:
第一步,缺失值填充。智能电表偶尔会掉线,某个时段没有记录,我习惯用前后两个时刻的平均值填充,如果连续缺失超过6个点,就直接剔除这个用户,避免影响特征质量。
第二步,归一化。不同用户的用电量基数差异很大,有的用户一天用50度电,有的只用5度,如果不做归一化,聚类结果基本被用电量大的用户主导。我这里用min-max归一化,把每个特征都缩放到[0,1]区间。
下面是一段特征提取和归一化的示例代码:
matlab复制% 假设data是 nUsers x 96 的原始负荷矩阵,每行一个用户,96个时段
nUsers = size(data, 1);
features = zeros(nUsers, 6); % 先预设6个特征
% 特征1:早高峰均值(7:00-9:00,即第28-36个采样点,假设15分钟一个点)
features(:,1) = mean(data(:,28:36), 2);
% 特征2:晚高峰均值(18:00-21:00,即第73-84个采样点)
features(:,2) = mean(data(:,73:84), 2);
% 特征3:夜间均值(23:00-次日5:00,即第93-96和第1-20个采样点)
nightIdx = [93:96, 1:20];
features(:,3) = mean(data(:,nightIdx), 2);
% 特征4:日负荷率 = 日均值/日最大值
features(:,4) = mean(data,2) ./ (max(data,[],2) + eps);
% 特征5:峰值出现时刻(一天内最大值所在的时段索引)
[~, peakIdx] = max(data, [], 2);
features(:,5) = peakIdx;
% 特征6:峰谷差 = 最大值 - 最小值
features(:,6) = max(data, [], 2) - min(data, [], 2);
% 归一化
features = (features - min(features, [], 1)) ./ (max(features, [], 1) - min(features, [], 1) + eps);
这里加eps是为了防止分母为0。特征维度并不是固定的,你可以根据业务需要增加或减少,但聚类前一定要统一归一化,否则后面算欧氏距离时不公平。
3.2 PSO优化FCM的核心代码
接下来是重头戏。我给出了一个完整可跑的PSO-FCM核心函数,代码中加了详细注释。整个流程分四步:初始化粒子群、计算适应度、更新速度和位置、最终用FCM精炼。
matlab复制function [bestCenter, bestLabel, bestFitness, converge] = pso_fcm(data, K, nP, maxIter)
% PSO-FCM: 粒子群算法优化FCM聚类
% 输入:
% data - n x d 矩阵,n为样本数,d为特征维度
% K - 聚类簇数
% nP - 粒子群规模
% maxIter - 最大迭代次数
% 输出:
% bestCenter - 最优聚类中心
% bestLabel - 每个样本的最终聚类标签
% bestFitness - 最优目标函数值
% converge - 每次迭代的最优适应度记录
[~, d] = size(data);
m = 2; % 模糊指数
% 数据范围,用于边界约束
lb = min(data, [], 1);
ub = max(data, [], 1);
% 粒子维度 = K * d
dim = K * d;
% 初始化粒子位置
X = rand(nP, dim) .* (ub - lb) + lb; % 写成repmat更稳,这里简写
% 初始化速度
V = zeros(nP, dim);
% 个人最优
pbest_X = X;
pbest_fitness = inf(nP, 1);
% 全局最优
[gbest_fitness, gbest_idx] = deal(inf, 1);
gbest_X = X(1, :);
% 用于记录收敛曲线
converge = zeros(maxIter, 1);
for t = 1:maxIter
% 惯性权重线性递减
w = 0.9 - (0.9 - 0.4) * (t / maxIter);
for i = 1:nP
% 把粒子向量恢复成K个中心
center = reshape(X(i,:), K, d);
% 计算FCM目标函数值
fitness = fcm_objective(data, center, m);
% 更新个人最优
if fitness < pbest_fitness(i)
pbest_fitness(i) = fitness;
pbest_X(i,:) = X(i,:);
end
% 更新全局最优
if fitness < gbest_fitness
gbest_fitness = fitness;
gbest_X = X(i,:);
end
end
converge(t) = gbest_fitness;
% 更新粒子的速度和位置
for i = 1:nP
r1 = rand(1, dim);
r2 = rand(1, dim);
V(i,:) = w * V(i,:) + 1.5 * r1 .* (pbest_X(i,:) - X(i,:)) + 1.5 * r2 .* (gbest_X - X(i,:));
X(i,:) = X(i,:) + V(i,:);
% 边界反弹约束
for j = 1:dim
if X(i,j) < lb(mod(j-1, d) + 1)
X(i,j) = lb(mod(j-1, d) + 1);
V(i,j) = -V(i,j) * 0.5;
elseif X(i,j) > ub(mod(j-1, d) + 1)
X(i,j) = ub(mod(j-1, d) + 1);
V(i,j) = -V(i,j) * 0.5;
end
end
end
end
% 用PSO找到的最优中心作为FCM初始中心,再迭代精炼
bestCenter = reshape(gbest_X, K, d);
[bestCenter, bestLabel] = fcm_refine(data, bestCenter, m);
% 重新计算最终目标函数
bestFitness = fcm_objective(data, bestCenter, m);
end
这里有两个子函数需要补充:fcm_objective和fcm_refine。fcm_objective负责计算给定中心时的FCM目标函数值和隶属度矩阵;fcm_refine负责执行标准FCM迭代。
matlab复制function [U, J] = fcm_objective(data, center, m)
% 计算隶属度矩阵和目标函数值
% 输入: data - n x d, center - K x d
n = size(data, 1);
K = size(center, 1);
% 计算每个样本到每个中心的欧氏距离平方
D = zeros(n, K);
for i = 1:n
for j = 1:K
D(i,j) = sum((data(i,:) - center(j,:)).^2);
end
end
% 防止除零
D = D + eps;
% 隶属度更新公式
U = zeros(n, K);
for i = 1:n
for j = 1:K
dist_ratio = D(i,:) .^ (1/(m-1));
U(i,j) = 1 / sum((D(i,j) ./ dist_ratio(1,j)) .* ones(1,K)); % 这个写法有点绕,直接按标准公式写
end
end
其实隶属度更新标准公式是:对每个样本i和簇j,U_ij = (1/D_ij)^(1/(m-1)) / sum_k (1/D_ik)^(1/(m-1))。写成向量化更快:
matlab复制function U = calc_membership(data, center, m)
n = size(data, 1);
K = size(center, 1);
D = zeros(n, K);
for i = 1:n
for j = 1:K
D(i,j) = sum((data(i,:) - center(j,:)).^2);
end
end
D(D < eps) = eps;
invD = D .^ (-1/(m-1));
U = invD ./ sum(invD, 2);
end
接下来是fcm_refine,它用标准FCM迭代更新中心和隶属度,迭代次数一般设100,或者直到目标函数变化量小于1e-6。
matlab复制function [center, label] = fcm_refine(data, center, m, maxIt, tol)
if nargin < 4
maxIt = 100;
end
if nargin < 5
tol = 1e-6;
end
prevJ = inf;
for iter = 1:maxIt
U = calc_membership(data, center, m);
newCenter = zeros(size(center));
for j = 1:size(center,1)
% 加权平均更新中心
newCenter(j,:) = sum((U(:,j).^m) .* data, 1) / sum(U(:,j).^m);
end
% 计算目标函数
J = 0;
for i = 1:size(data,1)
for j = 1:size(center,1)
J = J + U(i,j)^m * sum((data(i,:) - newCenter(j,:)).^2);
end
end
center = newCenter;
if abs(prevJ - J) < tol
break;
end
prevJ = J;
end
[~, label] = max(U, [], 2);
end
实际跑的时候,建议不要用一个粒子越界后直接把整个粒子都复位到边界,因为那样会破坏粒子的内部结构。我上面的代码是按每个维度单独做边界约束,这样能保持其它维度的信息。
3.3 关键参数设置与调优
PSO-FCM里面需要调的参数不少,但大多数人只会关注粒子群大小和迭代次数,忽略了模糊指数m和聚类数K的选择。我把自己常用的参数整理成了表格:
| 参数 | 常用值 | 说明 |
|---|---|---|
| 聚类数K | 4~6 | 用肘部法则或轮廓系数确定,不要拍脑袋 |
| 模糊指数m | 2 | m越大聚类结果越模糊,超过2容易分不开 |
| 粒子群规模nP | 40~60 | 太小搜索能力差,太大计算量高 |
| 最大迭代次数maxIter | 50~100 | PSO迭代,不是FCM迭代 |
| 惯性权重w | 0.9~0.4线性递减 | 前期全局搜索,后期局部精炼 |
| 学习因子c1, c2 | 1.5~2.0 | 一般取1.5或2.0 |
| 速度最大值vmax | 变量范围的20% | 防止粒子飞过远 |
聚类数K的选择是最影响业务解释度的参数。我一般会跑多个K值,然后看轮廓系数。轮廓系数接近1说明样本跟自己簇内的样本很紧凑,跟其他簇距离远;接近0或者负数说明分得不好。在Matlab里可以用silhouette函数直接算。
这里有个容易踩的坑:如果把PSO迭代次数设得太大,比如300代,效果不会提升太多,反而计算时间成倍增加。因为PSO只能找到一个大致的优质区域,真正的高精度收敛还是要靠最后的FCM精炼。我试过把maxIter从100改成300,轮廓系数只提升了0.01,耗时时长了近3倍,非常不划算。
4. 实操过程与结果分析
4.1 完整运行流程
我用一个具体例子来走一遍完整流程。假设有500个用户,每条负荷曲线96个点,对应15分钟采样间隔。先从原始矩阵提取6个特征,再调用pso_fcm函数聚类。
matlab复制load('loadData.mat'); % 假设loadData是500x96的矩阵
% 步骤1:特征提取
features = extractFeatures(loadData); % 自己写函数
% 步骤2:归一化
features = (features - min(features)) ./ (max(features) - min(features));
% 步骤3:设置参数并运行PSO-FCM
K = 4;
nP = 50;
maxIter = 80;
[center, label, bestF, converge] = pso_fcm(features, K, nP, maxIter);
% 步骤4:轮廓系数评价
sil = silhouette(features, label);
fprintf('Average silhouette: %.4f\n', mean(sil));
% 步骤5:绘制收敛曲线
figure;
plot(converge, 'LineWidth', 1.5);
xlabel('迭代次数');
ylabel('目标函数值');
title('PSO-FCM收敛曲线');
运行结束后,最好再看一下每一簇的用户数和中心。如果某个簇的用户数太少(比如只有1个),说明K可能选大了,或者特征里混入了异常样本。
4.2 聚类结果的可视化与行为画像解读
聚类结果不能只看数字,一定要可视化。我通常会画两类图:一是原始负荷曲线的按簇着色图,二是各簇特征均值条形图。
按簇着色的96点曲线图,能直观看到每类用户的用电曲线形状。比如我跑出来的四个簇,簇1的中心曲线在早上7点到9点和晚上18点到22点有两个明显峰值,聚类中心负荷值在0.8左右,其他时段基本在0.2以下,可以定义为“双峰通勤型”;簇2的曲线从晚上23点开始爬升,凌晨1点到4点维持高位,白天很低,典型的“夜间活跃型”;簇3的曲线全天都在0.4到0.6之间波动,幅度很小,属于“全天平稳型”;簇4在工作日有明显峰值,但周末曲线平缓,可以定义为“周末放松型”。
这个画像一旦建立,后面很多业务都能往下走。比如设计分时电价时,“夜间活跃型”用户对谷段电价会更敏感;“双峰通勤型”用户如果错峰用电,对电网削峰填谷的价值最大。这些结论不是凭空想的,都是聚类中心曲线直接告诉你的。
我还会打印每个簇的特征均值表:
| 簇编号 | 早高峰均值 | 晚高峰均值 | 夜间均值 | 日负荷率 | 峰值时刻 | 用户数 |
|---|---|---|---|---|---|---|
| 1 | 0.82 | 0.86 | 0.15 | 0.45 | 19 | 168 |
| 2 | 0.12 | 0.18 | 0.74 | 0.30 | 2 | 87 |
| 3 | 0.52 | 0.55 | 0.48 | 0.72 | 14 | 128 |
| 4 | 0.30 | 0.38 | 0.22 | 0.38 | 11 | 117 |
这个表格能快速看出每一类的核心特征,也方便后续写分析报告。
4.3 PSO-FCM和传统FCM的对比
我习惯把PSO-FCM和随机初始化的传统FCM做对比,对比指标包括目标函数最终值、轮廓系数、多次运行的标准差。实测下来,传统FCM跑10次,每一轮的目标函数值可能相差10%以上;而PSO-FCM跑10次,目标函数值基本落在同一个波动很小的区间内。原因很简单:PSO在前期搜索中已经找到一个较优的区域,FCM在这个区域里做局部精炼,结果自然更稳定。
不过也要承认,PSO-FCM不能保证100%全局最优。它只能说比随机初始化更稳健,而且在数据规模比较大的时候,PSO阶段带来的时间开销是值得的。
5. 常见问题与排查技巧
5.1 FCM陷入局部最优导致聚类异常
症状:聚类结果不稳定,多次运行簇中心变化很大;某次运行出现空簇,或者两个簇中心几乎重合。
排查思路:首先检查数据归一化是否做好,如果特征量纲不一致,距离计算会乱套。其次看K是不是选大了,空簇经常是因为K比真实类别数多。最后看是否用了PSO优化,如果只是传统FCM,可以固定随机种子,或者多次运行取目标函数最小的那组结果。
我实际遇到过一次,某用户因为电表故障,连续一个月每天都是同一个低值,特征提取后所有特征都接近0,聚到哪个簇都是游离点。这种异常样本应该在聚类前剔除,或者单独标记。
5.2 PSO参数敏感性导致收敛异常
症状:进化曲线不下降,或者直线下降后迅速停滞。
排查思路:PSO最常见的问题是速度过大,粒子直接飞出边界,导致位置全变成边界值。解决办法是限制最大速度Vmax。我一般把Vmax设为数据范围的0.2倍,效果不错。惯性权重w如果固定为1,后期粒子很难收敛到局部精细搜索;换成线性递减后,收敛曲线明显平滑很多。
还有一个容易被忽视的点:每个粒子位置向量的每一段对应一个聚类中心,但不同特征的取值范围不同。如果你对特征做了归一化,那么所有特征都在[0,1]之间,粒子边界约束可以直接用[0,1];如果没做归一化,PSO的搜索范围会非常不均匀,很难收敛。
5.3 Matlab代码常见报错
我整理了三个高频报错和处理方法:
-
错误:矩阵尺寸不一致(Matrix dimensions must agree)。多发生在calc_membership函数中。检查data是n×d,center是K×d,如果center写成了d×K,距离就算不出来了。
-
错误:除数为零(Inf or NaN)。原因通常是某个聚类中心恰好离一个样本距离为0,导致隶属度更新时分母为0。在距离矩阵上加一个eps能解决95%的情况。
-
错误:索引超出数组范围。发生在粒子边界约束时,因为粒子向量长度是K*d,但lb和ub长度为d,直接用mod(j-1,d)+1来取第j维对应的特征范围,这个逻辑要写对。上面代码已经处理了这个。
6. 实操心得与扩展方向
整套代码跑通后,我最大的感受是:不要盲目迷信算法复杂度,很多时候“稳定”比“高级”更重要。传统FCM实现再简单,结果一抖一抖的,写报告都没法写。PSO-FCM只多花了几十秒,换来的是可复现、可解释的聚类结果,这在项目交付中非常关键。
后续如果想继续往深做,有几个方向值得试。一是把特征工程做得更丰富,比如加入用户用电量的熵值、负载率、峰谷时长比,甚至引入温度等外部因素;二是用自适应PSO替代固定参数PSO,让惯性权重随适应度动态调整;三是最后用DPC(密度峰值聚类)或者谱聚类做对比验证。但万变不离其宗,聚类的目的始终是让业务人员能看懂每一类用户长什么样,而不是单纯追求某一个指标最高。
最后再分享一个小技巧:当你拿不定聚类数K时,可以先用PSO-FCM分别算K=3、4、5、6的目标函数收敛曲线,把所有收敛曲线画在同一张图上,看谁的收敛值下降最明显,然后结合轮廓系数选。这个方法虽然土,但比单纯看一个数字可靠得多。
