ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Matlab实现PSO优化Kmeans:居民用电行为聚类分析

Matlab实现PSO优化Kmeans:居民用电行为聚类分析 不废话直接进入正题。电费账单背后那串数字放在电力系统里看其实是一类非常典型的聚类分析问题。把成千上万个居民用户的用电负荷曲线按行为模式分门别类后面才好做分时电价、需求响应、精准营销这些事。我最近用Matlab把“粒子群算法(PSO)优化Kmeans聚类”的流程完整跑了一遍从算法原理、代码实现到结果解读踩了不少坑这篇就把整个项目的思路和实操过程整理出来。适合正在做电力用户画像、数据挖掘课题或者想把群智能优化算法落地的朋友参考。1. 项目拆解到底解决什么问题值不值得做1.1 居民用电行为分析的本质居民用电行为分析本质上是把每个家庭/用户在某段时间内的负荷曲线比如一天24小时或96点的用电功率看作一条多维向量。维度可能是时间点、用电量、峰谷占比等然后需要把海量用户划分成若干典型类别。做这件事的直接驱动力有两个电网侧想优化负荷调度知道哪些用户适合参与削峰填谷、需求响应而不是眉毛胡子一把抓。售电/能源服务侧想做用户画像推出差异化套餐和增值服务比如给夜间用电占比高的用户推谷段优惠。所以这个项目的第一层核心是从用户负荷数据中找规律把用户分成几组行为模式接近的群体。第二层核心才是聚类算法本身。为什么偏偏要选Kmeans又为什么要用粒子群去“优化”它看完下面两个小节就清楚了。1.2 经典Kmeans聚类的“死穴”Kmeans的原理很朴素随机挑K个质心把每个样本归到距离最近的质心然后重新计算质心位置反复迭代直到收敛。通俗点说就像选K个“队长”先随便指定再让所有队员投奔最近的队长然后重新选举队长直到队伍划分稳定。问题就出在“先随便指定”这六个字上。Kmeans是典型的“爬山式”算法迭代过程只沿着误差下降方向走。一旦初始质心选得不巧结果就会收敛到局部最优而不是全局最优。我举个直观例子假设一条数据流里有三个自然簇如果初始两个质心恰好落在同一个簇内另一个簇空着那最终这两个质心很难再“翻过山”去找到真正属于自己的簇。最让人头疼的是Kmeans的最终聚类结果高度依赖随机初始化。同一份数据你多跑几次每次出来的类中心偏差可能很大。这种不稳定性在做用户行为分析时非常致命今天聚类出来用户分4类明天跑出来3类合并或者分裂业务没法接。类中心漂移导致用户标签不稳定后续模型和策略全受影响。1.3 粒子群算法为什么能“补位”粒子群算法Particle Swarm Optimization, PSO是一种模拟鸟群觅食行为的群智能优化算法。它的核心思想很简单一批“粒子”在解空间里飞每个粒子记住自己历史最优位置和整个群体的历史最优位置不断向最优解靠拢。把它和Kmeans结合最经典也最直接的思路是用PSO去搜索Kmeans的初始质心组合而不是随机生成。每个粒子代表一组初始质心也就是一个完整聚类方案的起点粒子飞行位置的变化等于在搜索不同质心组合。适应度函数用聚类误差比如SSE来评价PSO迭代寻找能让SSE最小的质心初始组合。这样Kmeans再跑起来就站在一个“相对全局最优的山脚”而不是“随机山脚”。这个组合的优势很实际大幅降低Kmeans对随机初始化的敏感性结果稳定跑多次几乎一致。相对全局搜索能力强不容易陷在某个局部最优里不出来。代码结构清晰把PSO的粒子更新逻辑和Kmeans的迭代逻辑解耦改起来不痛苦。做的时候我心里要有个预期这是“用算力换精度”。PSO-O-Kmeans的耗时肯定比裸Kmeans高但在数据量几千到几万的用户负荷分析场景下差距完全可接受。2. 核心算法原理与目标函数设计2.1 Kmeans的目标函数和迭代逻辑Kmeans的最终目标是让所有样本到各自质心的欧氏距离平方和最小数学上写成J Σ_{i1}^{N} Σ_{k1}^{K} r_{ik} * ||x_i - μ_k||²其中 (x_i) 是第i个样本(μ_k) 是第k个簇的质心(r_{ik}) 是0/1指示变量当样本i属于簇k时取1。这个J也叫SSE误差平方和。算法迭代就两步分配步骤固定质心把每个样本分到最近的质心。更新步骤重新计算每个簇的质心也就是取簇内所有样本的均值。这两步重复到质心不再变化或者达到最大迭代次数。刚才说过了普通Kmeans的第一步“固定质心”是随机的所以就需要PSO进来把初始质心先算好。2.2 PSO的粒子更新数学表达标准的PSO每个粒子有位置和速度两个属性。位置代表一组解速度代表解在迭代中的变化方向和幅度。每次迭代按下面两个公式更新v_{id}^{t1} w * v_{id}^{t} c1 * r1 * (pBest_{id} - x_{id}^{t}) c2 * r2 * (gBest_{d} - x_{id}^{t}) x_{id}^{t1} x_{id}^{t} v_{id}^{t1}参数含义如下(w)惯性权重控制“继承上次速度”的比例。w大全局搜索强w小局部精细搜索强。(c1)、(c2)个体认知系数和社会学习系数控制粒子向自身最优和群体最优靠拢的力度。(r1)、(r2)[0,1]均匀分布的随机数保证搜索的随机性。(pBest)粒子个体历史最优位置。(gBest)整个粒子群历史最优位置就是整个群体的“全局最优”。2.3 PSO如何编码一组Kmeans质心这是整个项目里最容易卡壳的一步怎么把“一组质心”表示成粒子位置假设我们要聚成K类比如K4每条负荷曲线有d维特征比如24个时段d24。那么一个粒子的位置就是一个长度为 (K*d) 的向量前d个维度第1个质心在第d维坐标上的值。第d1到2d个维度第2个质心。以此类推第k组d维就是第k个质心。比如K3、d24粒子就是72维。需要注意的是粒子的初始位置怎么给。我实践中比较稳的做法是从原始数据里随机抽K个样本作为初始粒子的一部分位置而不是纯随机生成。纯随机生成可能产生远离数据分布的无效质心白浪费PSO的迭代次数。粒子的适应度值计算流程是这样把粒子向量拆成K组得到K个质心。用这K个质心跑Kmeans的分配更新迭代注意这里已经把“初始化”换成了PSO给定的质心。记录最终达到的SSE作为适应度值。PSO的目标就是最小化这个SSE。这里有细节要注意如果每个粒子都完整跑Kmeans计算量会比较大。所以通常限定每个粒子内部的Kmeans迭代次数比如50次内不需要跑到完全收敛因为PSO迭代本身就是逐步接近最优的过程内层Kmeans跑得太精细反而浪费。这个思想有点像“早停”early stopping实测效果很好。2.4 目标函数选择的取舍最常见的目标函数就是SSE类内误差平方和因为它和Kmeans的迭代目标天然一致。但如果你光看SSE会出现一个问题K越大SSE必然越小所以PSO只能在固定K的前提下找最优。这是两回事目标函数不负责自动选K。再补充一点有些论文会把“轮廓系数最大化”或“Davies-Bouldin指数最小化”当适应度函数。我实际测下来这类指标在PSO迭代里用起来稳定性稍微差一点——轮廓系数计算要遍历样本两两关系计算开销大而且数值波动大容易误导粒子群跳来跳去。如果你是首次跑通整个流程建议先用SSE后面再扩展多目标或复合指标稳妥得多。3. Matlab实操数据准备到核心代码全流程3.1 实验环境与数据集我用的是Matlab R2022b不需要装专业工具箱基本函数就能搞定。如果你的Matlab版本低一点问题也不大逻辑完全一样。数据集这块有两种玩法手头有电力负荷数据比如智能电表采集的96点日负荷曲线直接用。没有真实数据用自带函数合成模拟数据。比如用gmdistribution生成高斯混合分布模拟三类不同的用电行为模式。为了复现方便我下面给一种模拟数据生成的思路先跑通代码再换真实数据。真实数据格式一般是N24或N96矩阵每行一个用户、每列一个时间段的功率或电量。3.2 数据预处理这一步直接决定结果质量聚类对数据尺度极其敏感。假设你电网数据里有负荷功率和峰谷电量占比前者数值是几千后者是0到1直接用原始值算欧氏距离功率特征会完全淹没占比特征聚类出来基本只看功率一个维度。标准做法是归一化我习惯用z-score标准化function data_norm zscore_normalize(data) % data: N x d 原始矩阵 % 输出每列零均值、单位方差 mu mean(data, 1); sigma std(data, 1); sigma(sigma 0) 1; % 防除零 data_norm (data - mu) ./ sigma; end注意上面这行sigma(sigma 0) 1;是个小细节如果某一列全是同一个值比如全零直接除会出NaN后面全链路崩。这种坑我踩过不止一次。另外如果特征是尖峰分布比如有极端高耗电用户还可以考虑分位数缩放或者对数变换但这一步要看实际数据分布再决定不要盲目叠加。3.3 PSO-Kmeans核心代码实现下面给核心代码框架包含PSO初始化、适应度函数、粒子更新三个关键部分。function [best_particle, best_sse] pso_kmeans(data, K, pop_size, max_iter) % 参数说明 % data: N x d 归一化后的负荷数据 % K: 聚类数 % pop_size: 粒子群规模建议 20~50 % max_iter: PSO迭代次数建议 100~300 [N, d] size(data); dim K * d; % 每个粒子的维度 % PSO参数 w 0.9; % 惯性权重初始值后期可线性递减 c1 2.0; % 个体学习因子 c2 2.0; % 社会学习因子 v_max 0.5 * range(data(:)); % 速度上限防发散 % 初始化粒子群 —— 从样本中随机抽K个点作为质心组合 particles zeros(pop_size, dim); velocities zeros(pop_size, dim); for i 1:pop_size idx randperm(N, K); particles(i, :) reshape(data(idx, :), 1, []); velocities(i, :) -v_max 2*v_max*rand(1, dim); end pBest particles; pBestScore inf(pop_size, 1); [gBest, gBestScore] deal([]); % 主迭代 for iter 1:max_iter % 对每个粒子计算适应度 for i 1:pop_size sse compute_sse(data, particles(i, :), K); if sse pBestScore(i) pBestScore(i) sse; pBest(i, :) particles(i, :); end end % 更新全局最优 [best_idx_score, best_idx] min(pBestScore); if isempty(gBestScore) || best_idx_score gBestScore gBestScore best_idx_score; gBest pBest(best_idx, :); end % 线性递减惯性权重 w_current 0.9 - (0.9 - 0.4) * (iter / max_iter); % 更新速度和位置 for i 1:pop_size r1 rand(1, dim); r2 rand(1, dim); velocities(i, :) w_current * velocities(i, :) ... c1 * r1 .* (pBest(i, :) - particles(i, :)) ... c2 * r2 .* (gBest - particles(i, :)); velocities(i, :) max(min(velocities(i, :), v_max), -v_max); particles(i, :) particles(i, :) velocities(i, :); end % 调试输出跑大迭代数时建议打开 % fprintf(iter %d: gBestScore %.4f\n, iter, gBestScore); end best_particle gBest; best_sse gBestScore; end适应度函数是核心我单独拆出来function sse compute_sse(data, particle, K) % 粒子解码成K个质心 [N, d] size(data); centroids reshape(particle, K, d); % 计算每个样本到每个质心的欧氏距离 distances pdist2(data, centroids, euclidean); % 每个样本归属到最近质心 [min_dist, ~] min(distances, [], 2); sse sum(min_dist.^2); % 注意这里可以再追加一次Kmeans精调可选 % 但在PSO迭代早期不建议计算量太大 end这里有个简单的改进技巧在计算完sse后加一个可选的Kmeans精调逻辑。就是拿到粒子给的质心后跑完整Kmeans迭代直到收敛再算SSE。这个方案精度更高但速度慢很多。我的经验是在PSO主迭代的前80%部分用短迭代不精调最后20%的粒子再精调既保证收敛速度又保证最终精度。3.4 关键参数设置与调参逻辑这部分直接从实操中总结照着调基本不会翻车参数建议范围说明聚类数K业务经验或手肘法确定用户分类通常3~6类粒子群规模pop_size20~50数据量大可以适量减少维度高适量增加迭代次数max_iter100~300迭代越多越稳定但耗时翻倍惯性权重w0.9递减到0.4前期全局搜索后期局部精修学习因子里c1、c2通常都是2.0不用太纠结除非收敛异常速度上限v_max数据幅度的0.2~0.5倍过大振荡过小收敛慢关于K的确定我要多说一句。很多新手一上来就直接把K设成4然后跑结果这是错的。正确流程是先拿少部分样本跑一遍传统Kmeans手肘图用SSE随K的变化曲线找一个拐点然后再用PSO-Kmeans在选定K上做精细聚类。手肘拐点不清晰的时候可以结合业务实际比如营销需要分3到5类来选择。另外评价聚类效果不只是SSE还要看轮廓系数silhouette轮廓系数接近1说明簇内紧致、簇间分离好这个后续在评估部分详细讲。4. 实验对比与结果分析4.1 对照实验怎么设计任何聚类算法项目都可以设计对照实验证明自己的有效。我的方案是同一份数据跑三个方法原生Kmeans随机初始化跑20次取最优Kmeans初始化Matlab自带kmeans函数默认就是这种PSO-Kmeans每组固定K相同跑多次记录SSE均值、标准差、轮廓系数均值和运行时间。传统Kmeans对初始值敏感的表现就是不同随机种子下结果波动大。PSO-Kmeans因为多粒子并行搜索每次找到的gBest基本一致。这个稳定性指标才是你做这个项目时最该拿出来吹的。4.2 聚类结果与典型用户行为解读拿到聚类质心后别忘了把质心反归一化再画成负荷曲线。这一步很容易被忽略直接拿归一化后的质心画图纵轴没有物理意义业务看不懂。假设聚成4类我举个例子意思到了就行第1类全天高耗能平稳型。24小时负荷均衡夜间休息时段也不低。可能是家庭作坊、养鱼增氧泵或者家里老人长期开空调。第2类双峰通勤型。早高峰7-9点用电量迅速上升晚高峰18-22点又是高峰午间相对平缓。典型上班族家庭。第3类夜间活跃型。白天负荷极低晚22点到凌晨2点反而有明显用电。可能是白天上班、晚上洗衣充电的年轻群体或者特色夜宵经营者。第4类避峰型/柔型。整体负荷偏低且主要用电集中在平谷段。这四类出来之后业务含义就很直观了第2类和第3类用户可以作为分时电价或者需求响应比如错峰充电的候选群体第1类用户很难参与削峰填谷因为人家一天到晚都在用强制拉平会影响生活。所以这个聚类项目本身就是给电力营销和电网调度提供决策依据的。4.3 评价指标怎么算怎么解释SSE是核心指标在PSO里面已经用过了。完整复现时还要算对比实验轮廓系数是另一个非常直观的指标。对一个样本 (xi)设 (a_i) 是它所在簇内所有其他样本的平均距离(b_i) 是它到最近的其他簇所有样本的平均距离。轮廓系数计算为 ((b_i - a_i) / max(a_i, b_i))取值范围[-1, 1]。为负说明样本可能分错了簇接近1说明聚类效果很好。计算轮廓系数在Matlab里可以直接用S silhouette(data, cluster_labels); mean_sil mean(S); % 全局平均轮廓系数另外还有Davies-Bouldin指数DBI和Calinski-Harabasz指数CHI但这两个在对比实验里足够作为补充了。我不建议把它们接入PSO适应度函数前面已经说过原因。4.4 结果可视化技巧可视化这一步投入产出的性价比很高。推荐三种图我实测下来展示效果好折线图聚成K条典型负荷曲线横轴24小时纵轴用反归一化后的功率/电量颜色区分簇。直接展示“这4类人长什么样”。散点图加降维高维负荷数据先PCA降成二维/三维然后按聚类类别着色。可以直观看出簇的分离度评估轮廓是否清晰。各类别占比饼图或横向条形图展示每类用户占比便于业务侧决策优先级。PCA降维的代码比较简单[coeff, score, ~] pca(data); score2d score(:, 1:2); % 提取前两个主成分 figure; gscatter(score2d(:, 1), score2d(:, 2), cluster_labels); xlabel(PC1); ylabel(PC2);需要注意降维后簇边界可能有重叠这是PCA的本质导致的不代表聚类效果差。所以解读结果时也要以此为准别只盯着二维图说事。5. 常见问题与排错实录5.1 聚类结果每次跑都不一样这是所有跑Kmeans相关项目的人第一个遇到的坑。原因基本可以分成三类随机初始化没有固定种子。PSO-Kmeans如果每次结果都不同先检查粒子初始位置是不是每次随机抽样的可以固定rng(N)让实验可复现。速度更新越界没钳制。粒子位置飞出数据范围导致质心落到无意义的空白区。适应度函数没让Kmeans内部迭代收敛或收敛条件太松。最快的排查手段是先跑5次输出每次的SSE看方差是否大。如果SSE方差大优先检查初始化和PSO参数如果SSE方差小但聚类标签不同大概率是K选得不合适或者数据里有极强的离群点。5.2 PSO收敛太慢迭代半天还在缓慢下降我遇到过这种情况原因是多维粒子群内部各个维度是独立的但Kmeans质心的维度之间高度相关。如果数据量很大收敛慢几乎是必然的。想优化可以参考这个方案用一小部分样本跑PSO搜索质心比如1000个样本而不是全量10万。质心位置对样本量的敏感性低于对样本分布结构的敏感性用少量数据找到质心初值再用全量Kmeans精跑一遍速度提升非常明显。把内层Kmeans迭代次数降到10~30次。前面说过的“早停”策略PSO各代本身就是不断逼近的。惯性权重动态递减改为自适应前期大全局搜索后期局部精搜也可以用混沌映射或者随机扰动来避免粒子早熟。5.3 Matlab运行卡顿和内存溢出Matlab在处理N96的全量负荷数据时如果用pdist2算样本到质心的距离当N达到几十万时内存占用会爆炸——因为会生成NK的稠密距离矩阵。解决办法很直接不要一次算全量分块处理距离矩阵。比如每个粒子内部适应度计算时拆成若干个batch分别pdist2再拼接。再一个经验是在PSO主迭代开始前确认所有中间变量用single类型存储会快一点。Matlab对大矩阵的single运算精度影响不大但对内存和速度影响相当可观。5.4 数据归一化的隐藏坑前文提到过sigma0导致除零。还有另外一个类似的坑如果数据里有NaN或Inf整个链路都会崩掉或者产生NaN质心。建议在数据读入后的第一时间处置data(isnan(data)) 0; % 或者用插值补齐 data(isinf(data)) 0;对于用电数据来说NaN通常意味着电表采集缺失。如果缺失比例超过10%建议把这个用户样本直接剔除不用硬补。缺失比例小可以用该列中位数或者相邻时间点均值补上。这一步直接影响质心计算的正确性别嫌麻烦直接跳过。5.5 关于“Matlab License”等环境问题的多余提醒很多人在跑项目第一步就卡在环境搭建上。我只提醒一点Matlab版本差异对本文代码影响不大但建议先用R2020b或更新的版本避免有些内置函数比如pdist2在某些老版本里需要额外配置统计工具包异常。如果你的环境没有这些工具包手动实现欧氏距离也就几行代码别被环境卡住导致项目玩不下去。6. 后续还能怎么扩展我个人做完这个项目后最大的体会是PSO-Kmeans只是一个起点它最大的价值在于提供了一个随时可以替换核心优化器的工作流。把PSO换成遗传算法GA、模拟退火SA或者用蝙蝠算法、鲸鱼算法只需要改粒子更新那一段逻辑整个框架不需要重写。这是个非常开放的工作流适合论文里写对比实验。数据源方面如果有条件接入真实智能电表数据建议额外增加一道特征工程除了原始24点曲线把高耗能时长、峰谷比、夜间电量占比作为衍生特征加进去。这些特征对你的聚类结果会有明显改善比纯原始曲线更容易聚类出业务可解释的模式。还有一点想特别提一下。聚类这个事算法是工具业务可解释性才是根本。我在实际项目里发现单纯靠数据跑出来的类未必直接可用最好把聚类结果交给业务人员看一轮把类中心对应到典型用户场景里确认每一类都有明确的行为画像。如果某个类的质心曲线形态很怪比如两个用户差别极大却被分到一类那可能K设小了或者特征维度过大需要回头调整。最后分享一个让我印象很深的小技巧在PSO更新后可以加一个“局部微调”环节——把gBest粒子的质心当作初始值再做一次标准Kmeans的精调。这个操作在绝大多数情况下会再降低一点SSE而且几乎不费什么时间我习惯把它当作整个聚类的最后一道收尾工序。别看它简单实测能让SSE再下降几个百分点。这个项目本质上就是一个“优化器聚类器”的组合改造。跑通代码不难难的是理解每一步为什么这样设计、出了问题去哪里排查。希望这篇复盘能帮你少踩几个坑把更多精力放在结果解读和业务落地上。
返回列表