研一刚进实验室那会儿,导师把一沓无线传感器网络的论文甩给我,第一句话就是:先把LEACH给我跑通。我当时连LEACH和LEACH-C的差别都说不清,更别说Matlab仿真了。后来自己啃了代码、踩了一堆坑,才真正理解这套经典分簇路由协议的价值。这篇博文就是想把LEACH和LEACH-C从原理到Matlab实现完整捋一遍,适合正在做WSN方向毕设、科研仿真,或者想快速复现经典算法对比实验的同学。内容不绕弯子,直接讲清能耗模型、簇头选举逻辑、代码模块设计和仿真里最容易翻车的几个细节。
1. 能耗模型:读协议之前,先看懂节点是怎么把电耗光的
无线传感器网络的核心约束从来不是计算能力,而是电池。节点部署以后基本没法换电池,可传统网络协议根本不考虑能耗,这是WSN路由协议和普通Ad Hoc协议最大的分水岭。LEACH之所以成为经典,就是因为它把“能耗均衡”这个指标作为第一优先级。
1.1 一阶无线电能耗模型
绝大多数LEACH仿真都采用一阶无线电模型(First Order Radio Model)。发送方发射一个 l bit 的数据包到距离为 d 的接收方,发射能耗分为两部分:
- 发射电路耗能:
l × E_elec - 功率放大器耗能:取决于距离,距离小于阈值 d0 时采用自由空间模型
l × ε_fs × d²,距离大于等于 d0 时采用多径衰落模型l × ε_mp × d⁴
接收方收到这个数据包,同样要消耗l × E_elec。簇头对簇内数据做融合时,每比特还要额外消耗E_DA的能量。
典型参数如下,这也是很多论文和公开代码里通用的设置:
| 参数 | 取值 |
|---|---|
| 网络区域 | 100 m × 100 m |
| 节点总数 N | 100 |
| 基站位置 | (50, 175) |
| 初始能量 E0 | 0.5 J |
| 数据包长度 | 4000 bit |
| 控制包长度 | 100 bit |
| E_elec | 50 nJ/bit |
| ε_fs | 10 pJ/(bit·m²) |
| ε_mp | 0.0013 pJ/(bit·m⁴) |
| E_DA | 5 nJ/bit |
| 簇头比例 P | 5% |
d0 的计算方式是:
d0 = sqrt(ε_fs / ε_mp) = sqrt(10 / 0.0013) ≈ 87.7 m
这个阈值很重要:距离小于87.7米,发送能耗随距离平方增长;距离超过87.7米,能耗随距离四次方增长,一下子涨得非常快。这也是为什么WSN节点不能动不动就“一杆子捅到基站”——距离一远,发送成本极其昂贵。
1.2 分簇为什么能省这么多能量
拿一个最直接的例子算一下。100 m × 100 m 的网络,基站在 (50, 175),网络最远端节点到基站直线距离大约130米左右。如果节点直接把4000 bit的数据包发给基站:
E_Tx = 4000 × 50nJ + 4000 × 0.0013pJ × 130⁴
按130米估算,功率放大耗能部分大约是4000 × 1.3e-15 × 2.8561e8 ≈ 1.48e-3 J,再算上电路耗能0.2 mJ,总能耗接近1.7 mJ。这个数字看着不大,但初始能量只有0.5 J,如果每轮都这么发,几百轮就没了。
再看分簇的情况。簇内普通节点到簇头的距离通常只有十几二十米,小于d0,发送功耗按平方算:
E_Tx = 4000 × 50nJ + 4000 × 10pJ × 20² ≈ 0.2e-3 + 0.016e-3 = 0.216 mJ
每个节点先把数据发给近处簇头,簇头聚合所有成员的数据后再统一发给基站。这样真正承担“远距离通信”的只有少数簇头节点,其他节点只需要低功率短距离发送。这就是分簇能大量节省能耗的根本逻辑。
2. LEACH的簇头选举机制:为什么说它是“分布式抽签”
LEACH的全称是Low Energy Adaptive Clustering Hierarchy,它的运行是按“轮”来进行的。每一轮分成两个阶段:成簇阶段(Set-up Phase)和稳定传输阶段(Steady-state Phase)。整个协议的精髓,都藏在簇头选举那个阈值公式里。
2.1 阈值公式里的门道
LEACH中每个节点生成一个0到1之间的随机数,如果这个随机数小于阈值 T(n),节点就成为本轮的簇头。T(n)的经典定义是:
T(n) = P / (1 - P * (r mod round(1/P))),当 n 属于 G 集合 T(n) = 0,当 n 不属于 G 集合其中 P 是簇头比例(通常取0.05,也就是网络里约5%的节点当簇头),r 是当前轮数,G 是最近round(1/P)轮内没有当选过簇头的节点集合。
这个公式的巧妙之处在于:第一轮所有节点都在G集合里,每个节点当选簇头的概率大约是P;当选过簇头的节点会从G集合中移除,在未来1/P轮(20轮左右)内不再参与选举,从而保证簇头角色不会总被同一批节点霸占。随着轮数增加,G集合不断缩小,剩余节点的 T(n) 会越来越大,直到所有节点都当过簇头,G集合被重置。
这本质上是个“负载均衡抽签”机制:每个节点都有均等机会当簇头,又不会连续当选。缺点是纯靠随机数决定,没有考虑节点剩余能量,也没有考虑节点空间分布,可能选出来的簇头扎堆在某个角落,也可能选到一个快没电的节点当簇头。这也是LEACH后续各种改进协议的切入点。
2.2 成簇阶段:广播、入簇和TDMA调度
簇头选出来之后,进入成簇阶段。每个簇头向全网广播一个ADV消息,普通节点收到多个簇头的广播后,选择距离最近的簇头加入(在Matlab仿真里,通常直接按欧氏距离选择,因为广播信号强度和距离在理想模型下是等价的)。
簇头确定成员后,会为每个成员分配一个TDMA时隙,并把时隙表广播给成员。这里用的时分多址机制,目的是避免簇内节点同时发送数据产生碰撞,同时让节点只在属于自己的时隙里醒来发送,其余时间休眠,进一步降低能耗。
2.3 稳定传输阶段:数据融合与低占空比
稳定传输阶段才是网络真正干活的阶段。每个成员节点在自己的TDMA时隙内,把采集到的数据包发送给簇头;簇头收到簇内所有成员的数据后,做数据融合(通常是最简单但很实用的“取平均”或“取最大”,仿真代码里往往直接当成把多个包汇总成一个包处理),再把融合结果发送给基站。
这个过程结束后进入下一轮,重新选举簇头。LEACH的设计者显然意识到“簇头是个苦差事”,所以每轮都要换人干。这种定期轮换的思路,后来被几乎所有分层路由协议继承下来。
3. LEACH-C:当“随机抽签”变成“集中排班”
LEACH-C(C代表Centralized)是LEACH的集中式改进版,核心区别一句话就能说清楚:簇头不再由节点自己抽签决定,而是由基站根据全网节点的位置和能量信息统一规划。
3.1 为什么需要集中式规划
LEACH的分布式随机选举有一个天然缺陷:随机数不看空间分布。假设某个区域里五个节点同时成为簇头,另一个区域却一个簇头都没有,前一个区域的节点距离簇头很近,后一个区域的节点却要跨越半个网络把数据送给远处簇头,能耗立刻失衡。LEACH-C的做法就是用全局信息换结构合理性。
每一轮开始时,所有节点先把自身当前位置和剩余能量发送给基站。基站收集完信息后,运行一个模拟退火(Simulated Annealing)算法,从所有存活节点中选出M个节点作为本轮簇头,目标是让整个网络的“簇内距离平方和”最小,同时兼顾节点剩余能量。
3.2 为什么选模拟退火而不是穷举
从N个节点中选M个簇头,是个组合优化问题。N=100、M=5的穷举组合数高达7500多万,每轮都跑一遍穷举不现实。模拟退火是这类离散组合优化场景最省事的元启发式,它在高温阶段允许目标函数暂时变差(对应物理退火中粒子跳出局部最优),然后随时间降温逐渐收敛到较优解。实现上也简单,几百行Matlab就能跑起来。
目标函数通常写成最小化所有普通节点到对应簇头的距离平方之和:
J = sum( sum( d²(member_i, CH_i) ) )每次迭代随机把当前簇头集合里的一个节点和一个普通节点交换,重新计算目标函数;如果新的目标函数更小,就接受交换;如果更大,则以exp(-ΔJ / T)的概率接受,这个概率随温度T降低而越来越小,最终收敛到一个较优解。
LEACH-C每轮都要所有节点向基站报一次位置和能量,通信开销比LEACH大,但它换来的是簇空间分布更均匀、能量分配更合理,整体网络寿命通常明显优于LEACH。
| 维度 | LEACH | LEACH-C |
|---|---|---|
| 簇头选举方式 | 节点分布式随机 | 基站集中计算 |
| 所需全局信息 | 不需要 | 需要所有节点位置和能量 |
| 附加通信开销 | 成簇广播 | 每轮上报一次 |
| 簇空间分布 | 随机、不均匀 | 均匀、受优化 |
| 能耗均衡性 | 一般 | 较好 |
| 实现复杂度 | 低 | 中高 |
4. Matlab仿真实现:从参数配置到两个协议的代码骨架
这一部分我会按“初始化—LEACH核心逻辑—LEACH-C核心逻辑—能量更新和统计”的顺序来写代码模块。完整可运行的Matlab代码版本比较长,这里给出核心逻辑骨架,把最容易写错和最容易理解错的地方都标出来。
4.1 节点初始化和参数配置
仿真开头最重要的就是统一参数。我习惯先写一个参量表,再用结构体数组保存节点信息。每个节点需要记录坐标、剩余能量、本轮角色、G标记(最近是否当选过簇头)和存活状态。
% 网络参数 N = 100; % 节点数 X = 100; Y = 100; % 网络区域 Sink.X = 50; Sink.Y = 175; % 基站位置 % 能耗参数 E0 = 0.5; % 初始能量 J Eelec = 50e-9; % 发射/接收电路耗能 J/bit Efs = 10e-12; % 自由空间功放系数 J/bit/m^2 Emp = 0.0013e-12; % 多径功放系数 J/bit/m^4 EDA = 5e-9; % 数据融合耗能 J/bit % 数据包长度 packetLen = 4000; % 数据包 bit ctrlLen = 100; % 控制包 bit % 节点初始化 for i = 1:N node(i).x = rand * X; node(i).y = rand * Y; node(i).E = E0; node(i).G = 0; % 0表示可以参与簇头选举 node(i).alive = 1; node(i).CH = 0; % 是否是本轮的簇头 end注意这里参数单位必须统一。Matlab里通常全部使用J、bit、m三件套,Eelec写50e-9,而不是50nJ这种带单位的写法。
4.2 LEACH的簇头选举核心逻辑
每一轮先判断所有节点的存活状态,然后让存活且G标记为0的节点进行随机抽签:
for r = 1:maxRounds % 计算阈值 P = 0.05; T = P / (1 - P * mod(r, round(1/P))); for i = 1:N if node(i).alive == 1 && node(i).G == 0 if rand < T node(i).CH = 1; % 当选簇头 clusterCount = clusterCount + 1; end end end % 非簇头节点选择最近簇头入簇 for i = 1:N if node(i).alive == 1 && node(i).CH == 0 minDist = inf; for c = 1:clusterCount d = sqrt( (node(i).x - ch(c).x)^2 + (node(i).y - ch(c).y)^2 ); if d < minDist minDist = d; node(i).cluster = c; end end end end % 簇头收集数据、融合、发给基站,更新能量 % ... % 每轮结束更新G标记:当过簇头的节点,在round(1/P)轮内不再参与 if mod(r, round(1/P)) == 0 for i = 1:N if node(i).alive == 1 node(i).G = 0; end end end end这段代码有两个细节要注意。第一,阈值公式里的mod(r, round(1/P)),不同公开代码对r从0还是从1开始有不同约定,但只要和G集合的更新时间保持一致,最终效果没区别。第二,G集合的更新必须在每轮结束后进行,且只有在当前轮数满足mod(r, round(1/P)) == 0时,才把所有存活节点的G清零,让新一轮选举周期重新洗牌。
4.3 能量更新的关键公式
在仿真过程中,簇内节点和簇头的能耗必须分开算。普通节点只做一件事:把数据包发送给簇头。簇头要做两件事:接收所有成员的数据,做融合,再把结果发给基站。
% 普通节点发送数据给簇头 dist = sqrt( (node(i).x - ch(c).x)^2 + (node(i).y - ch(c).y)^2 ); if dist < d0 node(i).E = node(i).E - packetLen * Eelec - packetLen * Efs * dist^2; else node(i).E = node(i).E - packetLen * Eelec - packetLen * Emp * dist^4; end % 簇头接收成员数据 ch(c).E = ch(c).E - ctrlLen * Eelec; % 簇头接收每个成员的数据包 for m = 1:size(member, 2) ch(c).E = ch(c).E - packetLen * Eelec; end % 簇头数据融合 ch(c).E = ch(c).E - packetLen * EDA * (numMembers + 1); % 簇头发送融合数据给基站 distSink = sqrt( (ch(c).x - Sink.X)^2 + (ch(c).y - Sink.Y)^2 ); if distSink < d0 ch(c).E = ch(c).E - packetLen * Eelec - packetLen * Efs * distSink^2; else ch(c).E = ch(c).E - packetLen * Eelec - packetLen * Emp * distSink^4; end我见过不少同学在算簇头接收能耗时只算一次,漏掉了“簇头要接收每一个成员的数据包”这件事。如果一个簇有15个成员,接收能耗就得乘以15。这块漏了会让仿真结果出现严重偏差。
4.4 LEACH-C的集中式分簇与模拟退火骨架
LEACH-C的仿真逻辑比LEACH多一个环节:每轮开始时所有节点向基站上报位置和能量,基站在中心节点运行模拟退火,选出最优簇头集合。
% 模拟退火参数 T0 = 1; T_end = 0.001; alpha = 0.95; maxIter = 200; M = 5; % 期望簇头数,按5%取 % 初始随机选M个存活节点作为簇头 currentSet = randperm(N, M); currentCost = clusterCost(currentSet, node, M); T = T0; while T > T_end for iter = 1:maxIter newSet = currentSet; % 随机交换一个簇头和一个普通节点 swapIdx = randi(M); newNode = randi(N); if ismember(newNode, newSet) continue; end newSet(swapIdx) = newNode; newCost = clusterCost(newSet, node, M); delta = newCost - currentCost; if delta < 0 || rand < exp(-delta / T) currentSet = newSet; currentCost = newCost; end end T = T * alpha; end其中clusterCost的目标函数要让每个普通节点都找到离它最近的簇头,并累计距离平方:
function cost = clusterCost(chSet, node, M) cost = 0; N = length(node); for i = 1:N if node(i).alive == 0 continue; end minD = inf; for c = 1:M d = sqrt( (node(i).x - node(chSet(c)).x)^2 + ... (node(i).y - node(chSet(c)).y)^2 ); if d < minD minD = d; end end cost = cost + minD^2; end end基站选完簇头后,把簇头ID广播出去;普通节点根据自己的位置选择最近的簇头入簇,剩下的流程和LEACH一致。有些论文版本会让基站直接计算出最优簇划分再广播时隙表,但仿真上两者差别不大,至少在做LEACH和LEACH-C的对比时,核心结论是稳定的。
4.5 死节点判定
死节点判定看似简单,实际上直接决定仿真的结果曲线能不能被复现。常见做法是当节点能量小于等于0就判死。我建议用node(i).E > 0 && node(i).alive == 1作为存活判断条件,同时在能量更新后用阈值清理:如果能量低于一个极小数(比如1e-9),直接置为0并标记死亡。这样能避免负能量节点继续参与簇头选举,导致概率计算出现异常。
5. 仿真结果的核心观察维度:只看LND很容易被误导
做完仿真,拿到数据,不会看结果等于白跑。很多论文只画一张“存活节点数-轮数”的折线图,但那张图里能挖的信息比你想象的多。
5.1 生命周期指标:FND、HND、LND分开看
评估WSN网络寿命有多个节点级指标:
| 指标 | 全称 | 含义 |
|---|---|---|
| FND | First Node Dies | 第一个节点死亡的轮数 |
| HND | Half Node Dies | 一半节点死亡的轮数 |
| LND | Last Node Dies | 最后一个节点死亡的轮数 |
LEACH-C通常在FND和HND上明显优于LEACH,因为集中式规划让每轮簇头的空间分布更均衡,没有节点因为长期处于超远距离通信而快速耗光。但LND的差距反而不一定大,有时LEACH甚至更晚耗尽最后一个节点,原因是LEACH的随机性会让部分节点一直“运气好”,始终没当上簇头,存活时间被意外拉长。所以做对比实验时,一定要把FND、HND、LND三个指标全部列出来,尤其以FND和HND作为主要论据,否则审稿人很容易质疑结论的全面性。
5.2 剩余能量分布
比存活节点数更细的观察角度是每个节点的剩余能量分布。LEACH-C的节点剩余能量会更集中,方差小;LEACH则会出现部分节点还有很多电、部分节点已经死亡的两极分化。画法很简单:选定某个轮数(比如总轮数的一半),把所有存活节点的剩余能量画成柱状图或直方图,一眼就能看出来。
5.3 累计数据包数量
累计到达基站的数据包数量反映的是网络真正完成了多少有效工作。LEACH-C在早期就建立更均衡的簇结构,单位能耗产出的数据量更高;到了后期,由于网络拓扑逐渐瓦解,两者的差距会缩小。这个指标可以配合“每成功发送一个数据包的平均能耗”来看,能更直观体现协议的能量效率。
6. 复现LEACH仿真的五个经典坑:每个我都踩过
跑LEACH仿真最大的错觉是“代码跑通了就完事了”。实际上,仿真结果能不能描述协议的真实特性,取决于很多隐藏细节。下面这几个坑我不止一次踩过,写出来给后来人省时间。
6.1 阈值公式里的轮数取值从0还是从1开始
不同版本的公开代码里,阈值公式中r mod round(1/P)的写法几乎一样,但r的起始值不同,会导致首轮阈值计算有细微差别,进而影响首轮簇头数量。更常见的坑是误把r直接带入r - 1或r + 1,导致每轮阈值整体偏移。解决方法是:先在网络初始状态下跑10轮,统计每轮簇头数量是否稳定在5个左右;如果偏差超过2个,大概率是轮数边界写错了。
6.2 能量单位混用导致节点“秒死”
50nJ/bit如果写成50而不是50e-9,一个4000bit的数据包发送能耗就是20万焦耳,节点一帧就死,整条曲线变直线。这种问题第一眼看不出来,因为代码逻辑没错、语法没错、图也能画出来,但结果完全不可用。我建议写完仿真先做一次单元验证:让一个节点向10米外的目标发送1个数据包,手动核算能耗是否符合预期。数值对得上,再去跑完整网络。
6.3 随机数种子不固定,结果无法复现
LEACH和LEACH-C的成簇过程都用到了随机数。如果你没有固定随机数种子,每次跑仿真都会得到不同曲线,你根本无法判断两个协议的差距是真实差异还是随机波动。仿真开始时设置rng(0),或者任何固定种子,保证同一套代码、同一组参数可以复现相同结果。做对比实验时,建议多跑几次取平均,或者至少固定多种种子分别跑一遍,看趋势是否稳定。
6.4 数据融合的开关不统一
LEACH-C的优势很大程度来自数据融合带来的通信量压缩。如果仿真中把数据融合关闭,比如每个成员都独立发一份完整数据包到基站,那分簇的能耗优势会被大幅削弱,两个协议的性能差距会变得不明显。相反,如果LEACH关闭融合但LEACH-C开启融合,对比结果又会被夸大。做公平对比时,融合机制和参数必须在两个协议中保持完全一致。
6.5 基站位置不是一个可以随意改的参数
经典LEACH仿真里基站放在(50, 175),也就是网络区域外上方。如果放在(50, 50)即网络正中心,所有节点到基站的通信距离整体缩短,分布也更均匀,LEACH和LEACH-C之间的差距会被压缩;如果基站放得更远,两种协议的优势差距会被放大。这不代表基站位置改变会导致结论反转,但它会影响绝对数值,写论文时一定要在实验设置里标明基站坐标,对比横向工作时不要混用两套基站位置。
7. 仿真之外的一点体会:别只满足于把图跑出来
做WSN协议仿真这几年,我最大的感觉是:LEACH和LEACH-C虽然老,但它们几乎包揽了所有现代分层路由协议的底层设计思想——能耗模型、周期轮换、分簇、数据融合、集中式优化。把这两个协议的代码彻底吃透,后面再看SEP、TEEN、DEEC这些改进算法,基本就是“换目标函数、换选举公式、换优化算法”的事。
如果你打算在这个方向上做更细的研究,可以尝试三件事:一是把簇头选举的阈值公式改成“剩余能量加权”,观察网络寿命的变化;二是把模拟退火换成粒子群或遗传算法,对比不同寻优方法在同一目标函数下的收敛效果;三是固定网络拓扑但调整节点初始能量分布,看看协议在非均匀网络里的表现。这些扩展实验工作量不大,但写论文时非常出效果。
最后说一个实操小技巧:Matlab的仿真代码里,尽量把网络参数、能耗参数、仿真轮数全部集中到文件头部统一配置,不要散落在代码各处。等你开始跑一百组对照实验的时候,会发现这个习惯帮你省下大量改参数的时间。做协议研究,跑通只是起点,能把每个细节讲清楚、每个现象解释通,才是真正把这项技能变成了自己的东西。