LEACH分簇路由协议原理与Python仿真实现详解
2026/9/11 10:37:41 网站建设 项目流程

简介:资源包提供LEACH(低能量自适应分簇层次协议)的MATLAB仿真代码,面向无线传感器网络方向的研究者与学习者,用于复现经典簇头选举机制并分析网络能耗均衡效果。包内仅含1个m脚本,压缩后整体大小约3KB,文件精简,适合直接读取和修改节点数量、簇头选举概率、初始能量等核心参数,快速开展协议对比实验。已有133人在线学习浏览,是入门WSN分簇路由仿真的轻量级素材。代码覆盖网络初始化、基于概率p的簇头轮换、簇内数据汇聚与传输、节点死亡判定等完整流程,运行后可以直观看到不同参数下节点生存时间与网络寿命变化,有助于理解LEACH延长网络生命周期的核心思想,也可为后续改进算法或与其他节能策略对比提供可复现的仿真基准。

1. 从一个被低估的协议开始:LEACH 为什么必须自己实现一次

LEACH 是无线传感器网络里被介绍得最多、也最容易被跑错的分簇路由协议。拿它当论文对比基线的人很多,能说清 T(n) 阈值公式里 r 是当前轮号还是上一轮号的人很少。如果你在做 WSN 能效仿真,或者想设计低功耗多节点数据汇聚方案,这篇讲原理也讲可复现的 Python 实现。LEACH 把静态网络的能耗问题拆成分簇和簇头轮换:节点定期以概率 p 竞选簇头,当选者在稳定时间内收集成员数据、聚合后单跳发给基站。机制听着简单,但概率直觉、时序设计、能量账本直接决定仿真可信度。

2. 实现 LEACH 之前需要吃透的四个机制:阈值、时序、能耗与物理假设

2.1 T(n) 阈值公式:簇头选举概率怎么算出来的

LEACH 的簇头选举不是简单掷硬币,而是一个带记忆的概率过程。每个节点每轮生成一个 [0,1] 随机数,小于阈值 T(n) 就宣布当选。阈值定义为:

T(n) = p / (1 - p × (r mod (1/p)))

条件是节点属于集合 G,G 是最近 1/p 轮内没有当选过簇头的节点。r 从 0 开始计轮,窗口期就是 1/p 轮。以 p=0.05 为例,窗口是 20 轮。第 0 轮 r mod 20 = 0,阈值就是 0.05,100 个节点的网络期望簇头数是 100×0.05=5;到第 9 轮 r mod 20 = 9,阈值变成 0.05/(1-0.05×9) ≈ 0.0909;到第 19 轮,阈值变成 0.05/(1-0.05×19)=1。

也就是说,越接近窗口末尾,还没当选过的节点被选中的概率越高,最后几乎是必选。这保证了两件事:每个节点在 20 轮里至少获得一次簇头机会,以及长期来看网络里不会出现少数节点反复当选、提前耗尽能量的情况。这个「窗口 + 递增阈值」的设计是 LEACH 能延长网络生命周期的核心,后面做任何改进,动的最多的就是这一行公式。

2.2 一轮通信:从 ADV、JOIN-REQ 到 TDMA 调度的四段时序

LEACH 把时间切成轮(round),每轮包含建立阶段和稳定阶段。建立阶段分三步:簇头广播 ADV(advertisement)消息宣告自己当选;普通节点收到多个 ADV 后,按信号强度(仿真里一般用距离)选择最近的簇头,回发 JOIN-REQ;簇头汇总成员列表,生成 TDMA 时隙表广播给簇内成员。

稳定阶段是数据面,成员只在自己分配到的时隙发送数据,其余时间休眠,簇头收齐后做一次聚合把结果发给基站。一轮结束进入下一轮,重新选举。常见消息长度可以直接参考原论文,我平时拿来当仿真默认值:

消息方向作用长度(字节)
ADV簇头 → 全网宣布当选20
JOIN-REQ成员 → 簇头请求入簇20
TDMA 调度簇头 → 成员分配发送时隙44
DATA成员 → 簇头 / 簇头 → 基站感知数据500

注意 TDMA 调度意味着 LEACH 隐含依赖全网时间同步,真实嵌入式平台上做到微秒级同步并不容易,这后来成了协议被批判的焦点之一。仿真可以忽略同步过程本身,但不能忽略调度是整套协议成立的前提。

2.3 能量账本:E_elec、ε_fs、ε_mp 与 d0 的分界线

LEACH 的能耗模型是后来大量协议复用的标准。发送 l 比特、距离 d 的数据,能耗按距离分成两段:

E_Tx(l, d) = l × E_elec + l × ε_fs × d²(d ≤ d0) E_Tx(l, d) = l × E_elec + l × ε_mp × d⁴(d > d0)

接收只看包长,E_Rx(l) = l × E_elec;聚合时按数据量和合并流数再收一笔 E_DA。

参数含义常见取值
E_elec收发电路能耗50 nJ/bit
ε_fs自由空间放大器系数10 pJ/bit/m²
ε_mp多径衰落放大器系数0.0013 pJ/bit/m⁴
E_DA数据聚合能耗5 nJ/bit
d0平方/四次方模型切换距离≈87.7 m

d0 不是人设的,是令 ε_fs × d² = ε_mp × d⁴ 求出的切换点,约 87.7m。簇内节点到簇头一般几十米以内,用平方模型;簇头到基站经常超过 87.7m,四次方项会让能耗迅速上涨。理解这一点,才能理解 LEACH 为什么必须靠聚合压缩数据来对抗长距离传输,也才能理解后面所有多跳改进的动机。

2.4 单跳与聚合:LEACH 的两个设计支柱

LEACH 的原始假设是基站固定且离网络较远、节点位置固定、所有节点同构且初始能量相同、链路对称、发射功率可调。在这些假设下,它选择了「成员单跳到簇头 + 簇头单跳到基站」的拓扑。原因很务实:感知数据在空间上高度相关,温度、湿度、振动这类量在同一个簇内差别不大,聚合后能大幅压缩送往基站的数据量,省下的远距离传输能量远超分簇控制开销。

单跳是这套设计的薄弱点,簇头离基站太远时,四次方能耗直接击穿能量预算。后面出现的多跳 LEACH、异构 LEACH、非均匀分簇,本质上都是在动这两个支柱:要么改传输路径,要么改选举概率,要么改簇半径。

3. 用 Python 手写 LEACH:从节点类到全生命周期指标的最小实现

3.1 参数表和节点类:把公式落成可运行代码

仿真参数集中放在文件头部,一次改动全链路生效。下面这段是我习惯的配置组织方式:

import numpy as np AREA_W = AREA_H = 100.0 # 网络区域 100m × 100m BS_POS = (50.0, 175.0) # 基站放在区域外的常见基准位置 N = 100 # 节点数 P = 0.05 # 簇头概率 E0 = 0.5 # 初始能量,单位 J PKT_DATA = 4000 # 数据包长度,bit PKT_CTRL = 200 # 控制包长度,bit E_ELEC = 50e-9 # J/bit E_FS = 10e-12 # J/bit/m^2 E_MP = 0.0013e-12 # J/bit/m^4 E_DA = 5e-9 # J/bit D0 = np.sqrt(E_FS / E_MP) # 约 87.7m class Node: def __init__(self, idx, x, y): self.idx = idx self.x, self.y = x, y self.energy = E0 self.role = 0 # 1 簇头, 0 成员, -1 死亡 self.last_ch = -10**9 # 上次当选簇头的轮号

数据包长度 4000 bit 对应常见的 500 字节报文,控制包 200 bit 用来做 ADV、JOIN-REQ 这类短消息的预算。Node 里的 last_ch 初值故意设成负大数,保证第 0 轮所有节点都有选举资格。role 字段在每轮开始时会被重设,避免上一轮角色残留影响判断。

能耗函数也一并定义。三个函数对应 2.3 节的三种能耗来源,tx_energy 按 d 与 D0 的关系切换平方/四次方模型:

def tx_energy(bits, d): if d <= D0: return bits * (E_ELEC + E_FS * d * d) return bits * (E_ELEC + E_MP * d ** 4) def rx_energy(bits): return bits * E_ELEC def agg_energy(bits, streams): return bits * E_DA * streams

agg_energy 里的 streams 参数表示聚合的数据流数量,含义是每合并一条流付一次聚合费,这是文献里最常见的线性近似。真要精细建模可以换成按 CPU 指令周期估算,但对协议对比来说线性近似足够。

3.2 选举、分簇、稳定传输:三个核心函数

簇头选举严格按阈值公式实现,窗口判断用「当前轮号减上次当选轮号」而不是简单比较相等,因为轮号每轮递增,窗口资格才会自然滚动:

def elect_heads(nodes, r): window = int(1.0 / P) r_mod = r % window heads = [] for n in nodes: if n.energy <= 0: n.role = -1 continue n.role = 0 # 先重置为普通节点 if r - n.last_ch < window: # 窗口内已当过,本轮无资格 continue th = P / (1.0 - P * r_mod) if np.random.random() < th: n.role = 1 n.last_ch = r heads.append(n) return heads

这里有个明显的原版缺陷值得注意:阈值 th 的计算没有考虑节点剩余能量,一个快没电的节点和一个满电节点当选概率完全相同。这就是 LEACH 生命周期曲线末尾出现拖尾的一个原因,也是第 5 章异构协议要解决的痛点。

分簇用「欧氏距离最近」代替原协议里的 RSSI 判断,仿真里没有信道衰减模型时这是最合理的近似。稳定传输阶段按角色记账,簇头要付三笔费用,成员只付一笔,漏掉任何一笔都会让生命周期曲线失真:

def join_cluster(nodes, heads): clusters = {h.idx: [] for h in heads} for n in nodes: if n.role != 0: continue dists = [((n.x - h.x)**2 + (n.y - h.y)**2) ** 0.5 for h in heads] if not dists: # 没有可用的簇头 n.role = -1 continue h = heads[int(np.argmin(dists))] clusters[h.idx].append(n) return clusters def steady_state(nodes, heads, clusters): for h in heads: members = clusters[h.idx] h.energy -= rx_energy(len(members) * PKT_DATA) h.energy -= agg_energy(PKT_DATA, len(members) + 1) d_bs = np.hypot(h.x - BS_POS[0], h.y - BS_POS[1]) h.energy -= tx_energy(PKT_DATA, d_bs) for m in members: d_ch = np.hypot(m.x - h.x, m.y - h.y) m.energy -= tx_energy(PKT_DATA, d_ch)

注意聚合能耗里成员数加 1,表示簇头自己的数据也要参与聚合。控制包 PKT_CTRL 在这个简化版本里没有计入能耗,因为每轮控制包总量远小于数据包;如果真的要计较 p 很大时的开销占比,可以把 ADV、JOIN-REQ、TDMA 三条消息按对应距离加进对应节点的能耗函数里。

3.3 主循环:跑出 FND、HND、LND 曲线

主循环的核心就是每轮「选举 → 分簇 → 稳态传输 → 统计存活数」,判断顺序有一个约束:能量扣减必须在统计 alive 之前,否则会把本轮的死亡推迟到下一轮才记录:

def run(max_round=2000, seed=0): rng = np.random.default_rng(seed) nodes = [Node(i, rng.uniform(0, AREA_W), rng.uniform(0, AREA_H)) for i in range(N)] fnd = hnd = lnd = -1 alive_curve = [] for r in range(max_round): heads = elect_heads(nodes, r) if heads: clusters = join_cluster(nodes, heads) steady_state(nodes, heads, clusters) alive = sum(1 for n in nodes if n.energy > 0) alive_curve.append(alive) if fnd < 0 and alive < N: fnd = r if hnd < 0 and alive <= N / 2: hnd = r if alive == 0: lnd = r break return {"fnd": fnd, "hnd": hnd, "lnd": lnd, "alive": alive_curve, "rounds": r + 1}

三个生命周期指标的定义在论文里非常常见,含义却经常被用混,列成表比较清楚:

指标定义用途
FND第一个节点能量耗尽的轮号网络最早失效点
HND半数节点死亡的轮号协议对比最常用指标
LND全部节点死亡的轮号反映能耗曲线尾部

跑完代码后你会发现,同一组参数不同随机种子下 FND 和 HND 的差距很大。这是 LEACH 的随机分簇特性决定的,不是 bug。所以后面做参数对比时必须多次运行取平均,并记录方差,单次运行结果没有对比意义。

4. LEACH 仿真参数怎么调才合理:p、d0 与能量预算的联动

4.1 p 的取值窗口:为什么 0.05 是 100 节点网络的经验下限

p 是 LEACH 里最敏感的参数。期望簇头数是 N×p,p=0.05 时每轮平均 5 个簇头;p 太低保不住簇头数量,簇成员离簇头远、聚合负担重、控制包占比上升;p 太高则每个簇只覆盖两三个节点,而每个簇头都要用远距离链路向基站发包,总能耗反而上涨。原系列论文在 100 节点、100m 见方场景下验证过的取值范围基本在 0.05~0.1,这已经是多年沿用的经验窗口。

调 p 的正确姿势是扫面而不是拍脑袋:对 [0.01, 0.03, 0.05, 0.08, 0.1] 各跑 10~20 个随机种子,画存活节点曲线族,看 FND 和 HND 的峰值出现在哪。注意 p 跟网络面积强耦合,区域变大时最优 p 通常会下降,因为簇头到基站的平均距离变长,簇头自身发射成本抬高,再密集地设簇头会浪费能量。

4.2 d0 与能量预算联动:何时真的会触发四次方模型

d0 由两个放大器系数决定,按 2.3 节的公式算出来约 87.7m。这里常见的错误是把 d0 当成网络半径,以为区域小于 100m 就不会进入四次方区。实际要看链路长度:基站放在 (50, 175) 时,区域边缘节点到基站的距离可以接近 150m,远超 d0,这时单跳发送的能量是平方模型的数倍。

我一般先按网络规模定一组起点参数,再根据曲线形状微调。下面是一个经验配置参考表,不是定理,但能帮你少走弯路:

网络规模初始能量建议 p观察重点
100m×100m,BS 在区域外0.25~0.5 J0.05~0.1单跳为主,簇头能耗占比高
200m×200m,BS 在区域外0.5~1 J0.03~0.05远端簇头触发四次方,可试多跳
500m×500m1~2 J0.01~0.03单跳基本不可行,必须分层或多跳

区域扩大后只增加 E0 不是好办法。四次方模型下的能耗增长远快于线性增加电池容量,应该优先改拓扑策略,比如引入分层或中继。

4.3 三个高频翻车的设置问题

仿真协议对比最怕的不是跑不通,而是跑通了数据看着合理、账目实际是错的。我常遇到的翻车点有三个:

  1. 把通信距离 d 写成常量。每个节点每次传输都应按实际坐标实时计算距离,否则所有链路能耗一样,簇头离基站多远都被抹平,曲线会平滑得不真实。

  2. 只算发送能耗、漏算接收能耗。簇头是流量汇聚点,接收费用按包长计费,成员越多接收账越重。漏算接收把 HND 明显延后,看起来像性能更好,其实是假象。

  3. 随机种子设置走极端。完全不设种子,两次对比实验方差太大;固定一个种子,结果又只代表一种随机拓扑。建议每组参数用 10~20 个种子跑均值与标准差,并在论文里写明 seed 集合,方便别人复现。

提示:对比多组参数时,把 seed 集合统一成一批固定整数(例如 0 到 19),让每组参数共用同一批随机拓扑,这样差异才真的来自参数而不是随机噪声。

5. 从 LEACH 出发的三条进阶路线:多跳、异构与非均匀分簇

把原版 LEACH 跑通之后,你会发现瓶颈总是固定的两个:离基站远的簇头先死,以及能量较少的节点因为随机当选太频繁而早死。文献里的改进路线基本围绕这两点展开,彼此之间不互斥,可以按场景叠加。

5.1 多跳 LEACH:远端簇头不再单跳硬顶基站

多跳的思路是把「簇头 → 基站」的单跳改成「簇头 → 中继簇头 → … → 基站」。簇头保留聚合功能,远端簇头把压缩后的数据沿链路接力转发,代价是中间簇头要额外承担转发流量,形成热区。典型实现是在稳态传输之后加一步中继选择:

# 伪代码:为簇头 h 挑选转发中继 def choose_relay(h, heads, bs_pos): candidates = [x for x in heads if x is not h and dist(x, bs_pos) < dist(h, bs_pos)] if not candidates: return None # 没有更近的簇头,仍直接发基站 return min(candidates, key=lambda x: dist(h, x))

转发条件里「比当前簇头更靠近基站」是为了避免回环。伪代码只演示选择逻辑,实际系统还要把中继剩余能量、队列负载和跳数一并纳入评分,否则热区节点会更快被拖垮。

5.2 异构 LEACH:让能量多的节点多当几次簇头

异构路线承认节点初始能量不均的事实,把 T(n) 里的固定概率 p 替换成与剩余能量挂钩的加权概率,典型代表是 SEP 和 DEEC。SEP 按初始能量把节点分普通/高级两类,高级节点获得更高当选概率;DEEC 则参考全局平均剩余能量,能量高于平均水平的节点后续轮次里概率更高。

这个改进在实现上非常便宜,改一行选举代码就行:

# 在 elect_heads 里,把固定阈值替换为能量加权阈值 # p_i = P * (n.energy / avg_energy) th = p_i / (1.0 - p_i * r_mod)

前提是节点能拿到网络平均剩余能量,分布式场景下这需要额外广播轮,控制开销要计入生命周期模型,否则改进带来的增益会被高估。

5.3 非均匀分簇:热区问题的拓扑级解法

多跳解决了远距离,但热区仍在:离基站近的簇头大量转发别人数据,死得比谁都早。非均匀分簇的思路是做拓扑级调整,让近基站的簇半径更小、远端簇半径更大,近基站簇头收的数据少,省下能量给转发。实现上可以通过按距离调整簇头当选阈值,或者在分簇阶段直接微调簇半径。

三条路线的取舍可以一张表看清:

路线核心改动主要收益主要代价
多跳 LEACH簇头间接力转发降低远端簇头单跳损耗中继簇头形成热区
异构 LEACH选举概率随剩余能量调整延迟 FND需要全网能量信息同步
非均匀分簇簇半径随位置自适应均衡簇头能耗分布分簇过程变复杂

实验顺序我建议是:先用原版 LEACH 稳住基线,再根据 FND 和 HND 的差距判断瓶颈在哪,从上面选一条改进叠加。一上来就套复杂协议的论文,往往连对照实验都解释不清。

6. 用 tracer 表验证自己的 LEACH 实现没有悄悄跑偏

6.1 tracer 表:每轮记录角色、能量与窗口资格

仿真跑出曲线不算数,还得能向自己证明每个节点的能耗都按账本发生。我会在 elect_heads 入口记录一行状态,把每轮每个节点的角色、剩余能量、距离上次当选的轮号写进 tracer 表:

import csv def trace_round(nodes, r): rows = [] for n in nodes: rows.append((r, n.idx, n.role, round(n.energy, 6), r - n.last_ch)) return rows def dump_trace(nodes, max_round, fname="trace.csv"): with open(fname, "w", newline="") as f: w = csv.writer(f) w.writerow(["round", "node", "role", "energy", "since_ch"]) for r in range(max_round): w.writerows(trace_round(nodes, r))

把 trace_round 的调用插进第 3 章 run 函数每轮 elect_heads 之后,就得到完整时间线。这个 CSV 放到 pandas 里透视,按节点画能量曲线、按轮次画簇头位置分布,比只看存活数直观得多。

6.2 能量守恒校验:让每轮账目对上

最容易出现的实现 bug 是账目漏记,比如少收某条链路的接收费、聚合费用重复计算。校验方法很朴素,对每轮前后做一次全局对账:

def energy_check(before, after): delta = sum(b.energy - a.energy for b, a in zip(before, after)) # manual 由 steady_state 内部累计:接收 + 聚合 + 发送 manual = steady_state_round_fee return abs(delta - manual) < 1e-9

对不上的时候,按三点排查:簇头接收成员数据用的是成员数还是总包数,聚合费用按流数算还是按包数算,控制包能耗是否被忽略。浮点累加十几轮之后会有微小误差,1e-9 的阈值就是为了容纳这种噪声。

6.3 三个一眼不对劲的现象

第一轮就死一批节点,多半是距离单位混用,把米写成了千米,四次方项直接炸掉能量预算。FND 远早于 HND,大概率是选举窗口判断写成了r - n.last_ch > window而不是>=,导致窗口末尾节点被漏选。存活曲线像一条光滑直线且几乎没有波动,说明随机种子被固定得太死,分簇结果丧失了随机性,换几个种子重新跑。

这三类问题从曲线形状很难直接确定根因,打开 tracer 表按轮次逐行核对,通常十分钟内能锁定。仿真协议最怕的不是出错,而是账目错了曲线却看起来完全正常。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询