
我一直觉得无线传感器网络WSN这行做久了最难的不是部署了多少节点也不是采集了多少数据而是怎么让节点活得久一点。换电池不现实一颗颗节点丢在野外或者厂房里能省一口电就省一口电。这个“WSN能耗优化分簇路由算法”的项目做的就是这件事在路由层面把全网能耗摊开、摊匀用分簇的思路减少长距离通信浪费让网络的生命周期明显拉长。如果你正在做WSN协议仿真、做毕业设计、或者准备投传感器网络的论文这套思路和代码会很实用。接下来我会把一个可落地的能耗优化分簇路由算法从原理拆到代码实现把参数怎么调、坑在哪、怎么排查效率问题全部过一遍。文章里用到的仿真环境是MATLAB代码风格偏工程实现读起来不累拿到手上也能改。1. 为什么WSN一定要考虑分簇路由1.1 节点能耗的大头到底在哪在做能耗优化之前得先弄清楚节点的能量到底耗在哪儿。一个典型的WSN节点由三部分组成传感器模块负责采集处理器模块负责运算无线通信模块负责发送和接收。很多刚接触WSN的同学想当然地认为“计算很耗电”实际上在绝大多数场景里通信才是真正的电老虎。处理器跑一条指令的能耗往往比发送一个bit的低几个数量级。无线电能耗模型里有一个非常经典的结论发送数据的能耗不仅和包长成正比还和传输距离的n次方成正比。在自由空间模型里发射能耗与距离的平方成正比到了多径衰落模型距离指数会升到4次方。这也意味着两个相距80米的节点直接通信和经过一个中间节点分两跳通信后者的总能耗反而可能要小得多。无线传感器网络里的路由协议设计本质上就是在“直接传”和“多跳传”之间做博弈找到整体能耗最小的路径组合。如果没有分簇每个节点都要独立决定把数据发给谁那整个网络的通信拓扑会非常混乱有长链路有重复链路有大量低效的转发。更麻烦的是靠近基站的节点会因为转发很多数据而提前耗尽能量形成所谓的“热区”问题。分簇路由正是针对这些痛点提出来的。1.2 分簇到底在解决哪几个问题分簇路由的核心思想是把整个网络划分成若干个小区域每个区域叫一个簇簇内选出一个簇头Cluster HeadCH普通节点把自己的数据发给簇头簇头做数据融合之后统一发给基站Sink。这样做第一个直接好处就是减少了远距离通信的次数。如果没有簇头100个节点每个都要和基站通信那就是100条长链路有了簇头之后假如分了10个簇就只有10条长链路其余90条都是短距离的簇内通信。长距离通信是能耗大头砍掉这个大头全网能耗数据会好看很多。第二个好处是数据融合。簇头收到簇内多个节点的数据后可以对同类数据进行去重、求均值、特征提取发送的有效信息量并没有少多少但发送的比特数大幅下降这又是一笔可观的节能收益。第三个好处才是真正把网络“用久”的关键簇头是可以轮换的。通过周期性重新分簇、重新选举簇头能耗可以被摊到全网节点上避免某些节点过早死亡。这也说明了为什么分簇路由是WSN研究里一个长期热门的方向。它不涉及复杂的数学推导更多的是工程设计和策略取舍非常适合用来做优化改进、算法对比、仿真实战。2. 算法设计思路在LEACH基础上如何改2.1 经典LEACH算法的基线逻辑提到分簇路由绕不开LEACHLow Energy Adaptive Clustering Hierarchy。它是这类算法公认的基线。LEACH以“轮”为单位运行每一轮分成两个阶段成簇阶段Set-up Phase和稳定传输阶段Steady-state Phase。成簇阶段负责选簇头、组建簇稳定阶段负责数据传输并且为了节能稳定阶段的时长远大于成簇阶段。LEACH选簇头的方式是随机阈值法。每轮开始的时候每个节点生成一个0到1之间的随机数如果这个随机数小于阈值T(n)节点就宣布自己是簇头。阈值公式如下T(n) P / (1 - P * (r mod (1/P)))当节点在本轮之前还未当选过簇头时否则T(n) 0。其中P是网络中簇头节点所占的期望比例r是当前轮数。这个公式里有几个细节值得注意T(n)只对“本轮开始前还没当过簇头”的节点生效所以它保证了每个节点都有机会轮换随着轮数增加未当选簇头的节点数量变少分母变小阈值会变大节点当选的概率会逐渐上升这样能把尚未当过簇头的节点尽快推上去。LEACH的思路很直接但它有很明显的短板选簇头时完全不看剩余能量、节点位置、邻居分布。有可能一个只剩0.01J能量的节点被选为簇头下一轮数据还没发完就挂了也有可能在同一个区域同时冒出两三个簇头其他大片区域一个簇头都没有甚至可能出现一个孤立节点离最近的簇头隔了100米数据传输能耗反而比直接发基站还高。所以我对LEACH做改进时第一刀就砍在簇头选举上让剩余能量高、离基站近、簇内分布更中心的节点有更大概率当选簇头。2.2 改进后的簇头选举策略改进后的簇头选举阈值在LEACH原公式基础上乘一个加权因子公式可以写成T(n)_new T(n) * [ α * (E_res / E_avg) β * (1 - d_BS / d_max) γ * (1 - d_cluster / d_cluster_max) ]其中E_res是节点当前剩余能量E_avg是网络中存活节点的平均剩余能量d_BS是这个节点到基站的距离d_max是网络中节点到基站的最大距离d_cluster是节点与它通信半径内其他节点的平均距离换个说法节点与“候选邻居”之间的平均距离d_cluster_max是网络中该值的最大值α、β、γ是权重系数。这个公式的物理含义非常直观。第一项E_res / E_avg是能量项剩余能量高于平均水平的节点乘出来的值更大当选概率更高避免低能量节点“被推上断头台”。第二项1 - d_BS / d_max是位置项离基站越近的节点这个值越大簇头离基站近长距离传输的能耗就低。第三项1 - d_cluster / d_cluster_max是分布项节点周围邻居越多、距离越近它做簇头时接收数据的总能耗越低簇内通信的链路也更可靠。从实测经验来看α取0.6、β取0.2、γ取0.2是一个比较均衡的起点。原因在于剩余能量应该是“第一优先级”它直接决定节点能不能撑完一整轮而位置和中心度只是优化项权重太大会出现“低能量但位置好的节点被反复选中”的问题。当然这三项权重并不是死的网络场景变化时比如基站距离很远的野外大范围部署可以适当调高β。除了加权选举我还在代码里加了一个兜底机制如果一个节点当选簇头之后发现自己的剩余能量已经低于“完成一轮簇头工作所需能耗”的估算值它就主动放弃资格把机会让给簇内剩余能量最高的备用节点。这个兜底逻辑虽然简单但在仿真里能明显减少“簇头当两轮就暴毙”的情况。3. 仿真代码框架与关键实现细节3.1 仿真环境与整体流程这个项目用的是MATLAB版本在R2019b之后基本都能跑不需要额外工具箱只用基础绘图和矩阵运算。为什么选MATLAB而不是Python原因很简单一是做学术对比时大多数WSN经典算法的参考代码都是MATLAB写的放在同一环境下对比更公平二是MATLAB的矩阵操作对“节点距离计算”“批量能耗更新”这类操作非常友好代码写出来比Python循环短一大截跑起来也不慢。整个仿真流程分四步初始化在固定区域随机撒N个节点设置基站坐标、初始能量、通信参数、包长、轮数上限。成簇每轮开始前按阈值公式计算各节点的簇头选举概率选出簇头后普通节点加入最近的簇头形成簇。稳定传输簇内普通节点以TDMA方式按顺序把数据发给簇头簇头做数据融合后把聚合数据发给基站同时更新所有节点的当前剩余能量。状态统计每轮结束后统计存活节点数、网络剩余总能量、基站收到的数据包数并记录绘图数据。3.2 关键参数设置仿真参数是整个实验的“地基”参数不一致实验结果对比起来就没意义。我常用的参数表如下参数取值说明网络覆盖范围100m x 100m经典LEACH实验场景节点总数100常规同构网络规模基站坐标(50, 175)位于网络区域外上方节点初始能量E00.5 J统一初始能量发射/接收电路能耗Eelec50 nJ/bit处理每bit的基础能耗自由空间功放系数ε_fs10 pJ/bit/m²距离小于d0时使用多径功放系数ε_mp0.0013 pJ/bit/m⁴距离大于d0时使用数据融合能耗EDA5 nJ/bit簇头每聚合1bit的能耗数据包长度4000 bit常规数据帧大小簇头比例P0.1每轮期望簇头数10最大仿真轮数3600以节点全死或到最大轮数为止这里有个值得单独说明的参数是通信距离阈值d0它由自由空间模型和多径模型的分界点决定d0 sqrt(ε_fs / ε_mp)代进去算就是sqrt(10 * 10^-12 / 0.0013 * 10^-12)大约等于87.7米。当节点间距离小于这个阈值时使用自由空间模型发送能耗与d²成正比当距离大于或等于这个阈值时换用多径衰落模型发送能耗与d⁴成正比。这个设计的背景是远距离传输时信号衰减更严重用高次方模型才能比较真实地反映能耗。很多新手在改代码时会忽略这个切换全程只用一种模型导致结果偏乐观或偏悲观。3.3 节点初始化与主循环实现在代码里我习惯用一个结构体数组来保存每个节点的信息包含坐标、剩余能量、存活状态、是否当选过簇头、所属簇头编号。初始化部分的核心代码长这样% 初始化网络参数 net.length 100; % 网络横向长度 net.width 100; % 网络纵向长度 net.sink [50, 175]; % 基站坐标 net.n 100; % 节点总数 net.rmax 3600; % 最大轮数 % 节点初始化 nodes.x net.length * rand(1, net.n); % x坐标 nodes.y net.width * rand(1, net.n); % y坐标 nodes.E ones(1, net.n) * 0.5; % 剩余能量 nodes.alive ones(1, net.n); % 存活状态 1存活 0死亡 nodes.didCH zeros(1, net.n); % 是否当选过簇头 nodes.cluster zeros(1, net.n); % 所属簇头编号主循环以轮为最小单位每一轮经历成簇和数据传输。成簇阶段第一步是算簇头选举概率对应代码如下p 0.1; % 簇头比例 thresh zeros(1, net.n); for i 1:net.n if nodes.alive(i) 1 if nodes.didCH(i) 0 thresh(i) p / (1 - p * mod(r - 1, round(1 / p))); else thresh(i) 0; end else thresh(i) 0; end end注意这里的mod计算它保证了LEACH的循环轮换机制每过1/P轮所有存活节点都有机会重新参与簇头选举。代码里r是当前轮数起始从1计数所以公式里用了r-1这个细节很容易被忽略但对结果影响不小写错了会出现部分节点永远选不上簇头的情况。之后按改进后的阈值公式乘上能量项、位置项和中心度项得到新的选举概率newThresh。这一步的实现并不复杂但需要注意一个点加权因子可能大于或小于1所以newThresh不一定在0到1之间。处理方式是把newThresh和随机数rand(1)比较前先做一次裁剪防止阈值大于1导致“必选”或者负数导致“永不选”E_avg mean(nodes.E(nodes.alive 1)); d_BS sqrt((nodes.x - net.sink(1)).^2 (nodes.y - net.sink(2)).^2); d_max max(d_BS(nodes.alive 1)); alpha 0.6; beta 0.2; gamma 0.2; energyFactor nodes.E / E_avg; locFactor 1 - d_BS / (d_max 1e-9); % 简化中心度用节点与所有存活节点平均距离的倒数归一化 % 实际代码中为节省时间可在成簇阶段用距离矩阵补齐 centerFactor 1 - distAvg(i) / (distAvgMax 1e-9); newThresh thresh .* (alpha * energyFactor beta * locFactor gamma * centerFactor); newThresh max(0, min(1, newThresh)); % 裁剪 CH_flag rand(1, net.n) newThresh; nodes.didCH(CH_flag) 1;其实严格来说centerFactor依赖的distAvg需要提前计算节点与其通信范围内邻居的平均距离。在100个节点的规模下用两层for循环计算没有任何问题如果节点数上千则需要提前构造好一个n×n的距离矩阵再用掩码矩阵做一次批量平均能省掉一层循环。3.4 成簇过程与数据传输阶段簇头选定之后每个普通节点要选择一个簇头加入。常见做法是就近加入计算普通节点到所有簇头的距离选择最近的簇头作为自己的簇头。这一步的代码实现非常直接% 为每个普通节点分配最近的簇头 for i 1:net.n if nodes.alive(i) 1 CH_flag(i) 0 % 计算到所有簇头的距离 d_ave sqrt((nodes.x(i) - nodes.x(CH_flag)).^2 (nodes.y(i) - nodes.y(CH_flag)).^2); [~, idx] min(d_ave); % 找到对应簇头编号 CH_list find(CH_flag); nodes.cluster(i) CH_list(idx); end end如果出现了“某节点到所有簇头的距离都比到基站还远”的情况一个更稳妥的做法是允许该节点直接和基站通信。这个兜底逻辑在节点稀疏、随机种子不佳时特别重要有了它网络开局不容易出现大面积孤立节点。数据传输阶段按“簇内单跳簇头到基站单跳”的模型来仿真。普通节点发送自己的数据给簇头计算发送能耗簇头接收所有成员节点数据、做数据融合、再发送聚合数据给基站分别计算接收能耗、融合能耗和发送能耗。能耗更新公式如下普通节点i发送l bit数据到距离d的簇头 E_i E_i - l * Eelec - l * ε_fs * d²簇头接收所有成员数据后 E_ch E_ch - m * l * Eelec - m * l * EDA - l * Eelec - l * ε_fs * d_to_BS²其中m是簇内普通节点数量。这里有一个算法调优的小技巧普通节点把数据发到簇头后簇头不需要逐段解析每一个bit而是使用数据融合把m个l bit数据压缩成一份l bit数据所以计算融合数据时只按一份l bit计算。3.5 结果统计与绘图生命周期曲线为了让改进算法的效果看得清楚我在代码里保存了三个关键指标每轮存活节点数、每轮全网剩余总能量、每轮基站收到的数据包数。绘图部分用subplot画三张子图subplot(1,3,1); plot(round_num, alive_num, LineWidth, 1.5); xlabel(轮数); ylabel(存活节点数); grid on; subplot(1,3,2); plot(round_num, total_energy, LineWidth, 1.5); xlabel(轮数); ylabel(总剩余能量/J); grid on; subplot(1,3,3); plot(round_num, cumulative_packet, LineWidth, 1.5); xlabel(轮数); ylabel(基站累积接收数据包数); grid on;存活节点数曲线是最直观的生命周期指标。第一个节点死亡轮数FND和一半节点死亡轮数HND是评估网络生命周期的常用指标FND越晚说明能耗均衡性做得越好。总剩余能量曲线可以反映全网的能量效率如果曲线下降过快说明一轮里消耗的能量太多需要检查是不是长距离通信的簇头太多或者包长太大。基站累计收到的数据包数则体现了网络在生命周期内真正“干了多少活”这是比单纯看存活时间更务实的指标。4. 常见问题与排查技巧实录4.1 簇头数量和分布仿真结果不稳定的元凶很多人在跑分簇路由代码时都会遇到一个问题同样一组参数跑三次得到的三条生命周期曲线差别很大甚至第一次FND是800轮第三次FND直接掉到400轮。这个问题的根源往往是簇头数量不稳定和簇头分布不均。在纯LEACH里簇头数量虽然有期望值P×N但实际每一轮出来的簇头数量可能只有两三个也可能突然爆出十五六个。簇头数量过少每个簇头要管理大量节点簇头能耗太大提前死亡簇头数量过多长距离通信次数变多全网能耗变高。改进算法虽然加上了能量和位置的加权但随机性依然存在。我的处理方式是加一个“簇头数量补偿”如果当前选出的簇头数少于期望值的一半则在剩余节点中按剩余能量从高到低补选如果多于期望值的两倍则让距离基站最近的簇头概率性地退位并把这部分节点重新并入其他簇。这个策略牺牲了一点随机性但会让结果稳定很多。4.2 节点死亡判断条件不当导致统计口径差异“节点死亡”怎么定义直接影响FND、HND的值。最严格的定义是节点剩余能量小于0更实用一点的定义是节点剩余能量小于“它发送一次数据帧所需的最小能量”。原因很好理解即使节点还有一点点残余能量但如果连一个数据帧都发不出去那它实际上已经不工作了。如果只用能量小于0来判断很多节点会在“僵死”状态熬好几轮导致FND被严重推迟曲线看起来漂亮实则失真。我在代码里统一用这个判断条件minEnergy 1e-6; nodes.alive(i) nodes.E(i) minEnergy;1e-6 J这个值具体怎么定可以参考一下发送4000bit到10米外的节点能耗大约是4000×50e-9 4000×10e-12×10²约2e-4 J所以1e-6足够小既能排除“完全耗尽”的节点又不会把接近耗尽的节点算成活节点。4.3 死循环和仿真跑不动循环效率优化当节点数到200以上、仿真轮数到3000轮时MATLAB里频繁的for循环会逐渐变得让人无法忍受。最常见的问题是成簇阶段三层循环遍历所有节点找簇头、遍历所有簇头算距离、遍历所有普通节点做分配。这个复杂度在500节点以下还好到1000节点时会明显卡顿。优化手段有两个。第一个是用距离矩阵一次性算出所有节点两两之间的距离成簇阶段直接用矩阵切片操作避免每轮重复计算。第二个是对“稳定阶段”做帧计数压缩TDMA在一个稳定阶段内有多帧传输不需要逐帧挨个更新能耗而是用矩阵乘法把它一次算完。代码里把这两个优化加进去之后1000节点的仿真速度能提升五倍以上。4.4 参数敏感性与公平对比做算法对比时很多人只顾着换算法核心代码却忽略了参数必须完全一致。我见过好几份对比结果改进算法生命周期比LEACH长一倍但一看参数表改进算法的包长是2000bitLEACH的包长是4000bit这种对比是站不住脚的。公平对比必须做到四个统一节点坐标初始化方式统一用同一个随机种子生成或固定坐标数组、能量模型参数统一、数据包长统一、运行轮数统一。坐标初始化尤其重要因为节点空间分布对分簇效果影响很大两种算法必须在完全相同的节点分布下跑出来的曲线才有可比性。4.5 排查问题时的几个实用技巧在成簇阶段用“breakpoint 变量检查”看每一轮选出的簇头编号、剩余能量和阈值确认选举公式是否按预期工作。在能耗更新之后单独画一张每个节点能耗的直方图如果发现个别节点能耗异常高或异常低优先怀疑距离计算和d0模型切换出了问题。协议改进时先跑100轮看输出曲线是否符合直觉再跑完整3600轮。一次性跑完整仿真调试时眼睛会看花。随机种子建议一开始就固定下来例如rng(2024)保证每次调试结果可复现。最后正式跑结果时再开多组随机种子做平均不要让单次随机结果决定算法好坏。5. 结果对比与扩展方向5.1 结果对比图怎么看跑完之后我会把LEACH基线算法和本文改进算法放在同一坐标系里对比。最典型的结果是改进算法的FND轮数明显延后HND可能从1300轮提高到2200轮LND略提升但仍会逐渐归零总剩余能量曲线整条线压在基线上方基站累计数据包数在寿命中后期持续增长。这三组曲线分别对应能耗效率、能耗均衡性和网络吞吐能力组合起来才是一个完整的算法评价。如果改进算法只是FND提前、LND更晚那说明算法让早期节点延缓了死亡但实际上把能耗压力转移到了少数节点反而加剧了后期的节点崩溃。看结果时不要只盯着FND一个指标HND和LND同样重要三个指标要一起分析。5.2 从同构网络到异构网络与多基站场景这个改进算法可以直接平移到异构网络里。SEP、DEEC等经典异构算法的核心思想是给不同节点设置不同的初始能量在簇头选举时把初始能量也计入加权因子。把本文公式里的E_avg改成按初始能量归一化的“能量比例”再增加一个初始能量参数就变成了一个类似DEEC的异构版本。多基站场景的扩展也很自然。把单基站坐标扩展成基站数组簇头选举时不再只对比到单一基站的距离而是选择“当前轮次距离自己最近且负载最低的基站”成簇和传输阶段就会自动形成多棵数据传输树能进一步分散热点节点的压力。5.3 与移动Sink、强化学习的结合思路再往后走可以把簇头到基站的数据传输改成移动Sink按路径遍历簇头收集数据簇头不需要长距离发送能耗模型又变了一层。如果结合深度强化学习和网络拓扑的动态预测簇头选举、簇大小、传输功率都可以做成连续决策问题这已经是目前WSN能耗优化研究比较前沿的方向。不过无论扩展方向怎么变化底层还是那套“减少远距离通信、均衡全网能耗、做数据融合”的设计逻辑先把这套分簇路由代码吃透后边的路会顺很多。我自己的体会是WSN能耗优化这类项目难点从来不是公式推导而是“代码里每一处实现细节都可能改变最终结果”。你改一个包长、改一个d0阈值判断条件、改一行簇头补选逻辑生命周期曲线就会有大变化。跑仿真的时候多追问自己几个为什么把每个参数背后的物理含义都想明白才能把算法真正改出自己的东西来。