1. 从LEACH到OHILEACH:能耗黑锅到底出在哪
拿到这套“基于优化的启发式集成低能耗自适应集群层次结构协议(OHILEACH)”的Matlab代码时,我第一反应是:这不就是LEACH吗?但真正跑通一遍,把每一轮的能耗曲线打出来对比之后,我发现自己太天真了。OHILEACH的全称是Optimized Heuristic Integrated Low-Energy Adaptive Clustering Hierarchy,核心不是“换了个壳”,而是把启发式优化算法真正缝进了LEACH的分簇决策里。简单说,LEACH靠概率随机选簇头,OHILEACH靠粒子群或遗传这类启发式算法反复寻优,挑出一组能让全网能耗最低、负载最均衡的簇头组合,再把数据聚合、转发路径一起规划好。
这篇文章适合谁看?两类人。一类是刚接触无线传感器网络路由协议、想在Matlab里复现仿真、拿它写论文或者做课程设计的学生;另一类是已经在跑LEACH、熟悉分簇路由,但觉得传统协议太随机、死节点曲线不够漂亮、想找一个优化方向做对比实验的研究者。只要你能看懂最基本的Matlab语法,下面的代码逻辑、参数调优和经验坑,你都能直接拿去用。
先说结论:OHILEACH不是推翻LEACH,而是把LEACH里最不稳定的那几步——选簇头、定簇型、分配传输路径——从“看运气”改成“看全局”。我一直觉得,想理解任何一个优化型协议,得先搞清楚它到底在优化谁的痛点。所以这一篇,我不打算只贴代码,而是从LEACH的缺陷讲起,把OHILEACH的每一个设计动机、每一段Matlab实现、以及我在实测中踩过的坑,掰开揉碎讲清楚。
1.1 LEACH自带的三宗罪:选簇、定簇、传数据全靠撞运气
LEACH是Low Energy Adaptive Clustering Hierarchy的缩写,这个协议的核心思路是分轮运行,每轮随机选一部分节点当簇头,其他节点加入最近的簇头,簇头负责聚合数据并直接发给基站。这个机制在1990年代末提出来的时候,算是里程碑式的创新,因为它让网络里没有“固定中心”,每个节点轮着当消耗大户,寿命被摊薄了。但在实际跑Matlab仿真的时候,你会很明显地感觉到它的问题。
第一宗罪,簇头数量完全随机。LEACH每轮都会给每个节点生成一个0到1之间的随机数,如果这个数小于阈值T(n),节点就当簇头。T(n)的计算公式里有当前轮数和期望簇头比例p,看着挺美,但那只是一个概率期望值。我跑过一次150个节点、p=0.1的仿真,某一轮居然跳出来24个簇头,下一轮只剩3个。簇头太多,意味着大量节点在干转发和聚合的活,能量哗啦啦地掉;簇头太少,意味着每个簇的成员太多,簇头接收数据量巨大,聚合压力全压在少数节点上。这种方差从一开始就没有被控制。
第二宗罪,成簇阶段只会“就近抱大腿”。普通节点入簇的逻辑在LEACH里通常基于接收信号强度,也就是哪个簇头离我近、信号强,我就加入谁。这个逻辑本身没毛病,但在节点分布不均匀的网络里會很吃亏。比如我做过一个实验,基站放在区域正上方,部署的时候右下角恰好有一片节点特别密集,结果那片区域的若干个簇头同时被一堆节点请求加入,负载失衡非常严重。
第三宗罪,也是我最无法忍受的一点:所有簇头都直接和基站单跳通信。只要基站不在整个网络的几何中心,远端簇头的通信距离就会非常大。传感器节点发送数据消耗的能量和距离的平方甚至四次方成正比,一阶无线模型里d越大,能耗增长越恐怖。那些离基站远的簇头,常常在不到三分之一的轮数里就耗尽能量,成为死节点,然后它管辖的簇成员全部变成孤立节点,网络覆盖率骤降。
1.2 OHILEACH是怎么把“启发式”缝进层次协议的
OHILEACH针对上面三宗罪的解法,一句话概括就是:把随机选择改成优化选择。具体来说,它用一个启发式算法(比如粒子群优化PSO)在每一轮开始前求解一个组合优化问题——从N个节点里挑出k个簇头,使得“全网总能耗 + 负载不均衡惩罚 + 剩余能量惩罚”这个目标函数最小。
这个思路和LEACH最大的区别在于:LEACH是“局部决定”,每个节点自己掷骰子;OHILEACH是“全局决策”,每一轮都要对全网状态做一次扫描,然后给出一组确定的最优解。你也可以理解为,LEACH是在闭着眼睛买彩票,OHILEACH是在开奖之前先把赔率算清楚再下注。
启发式算法在这里的作用不是碰运气。PSO中每个粒子代表一种簇头组合方案,粒子在解空间里飞行,通过个体最优和历史最优不断修正自己的位置,最终收敛到一组“足够好”的簇头。因为网络生命周期仿真的每一轮只需要做一次这样的寻优,时间复杂度完全可以接受。我实测下来,150个节点一轮的PSO寻优在普通笔记本上大约需要几十毫秒,相比每轮数据传输的模拟时长,这个开销几乎可以忽略。
这也就解释了为什么协议名字里要强调“启发式集成”——它优化的不是单个环节,而是把簇头选择、成簇边界、数据传输链路作为一个整体去求解。这个“集成”两个字,才是OHILEACH和那些只在簇头选择环节做文章的半吊子改进方案的本质区别。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OHILEACH的协议机制拆解:分轮、选头、成簇、传输每个环节到底优化了什么
OHILEACH整体上仍然沿用LEACH的分轮架构:每轮分成建立阶段和稳定阶段,稳定阶段远长于建立阶段。这一点没有变,变的是建立阶段内部的动作。我先把整个一轮的完整流程列出来,再逐个环节拆解它的Matlab实现逻辑和设计动机。
- 第1步:检查存活节点集合,更新每个节点的剩余能量、坐标与邻居列表。
- 第2步:基于有限节点集合,编码启发式算法的候选解(粒子或染色体)。
- 第3步:计算每个候选解的目标函数值,选出本轮的簇头集合。
- 第4步:按优化后的簇头集合,每个普通节点就近加入簇。
- 第5步:簇头分配TDMA时隙,簇成员按调度发送数据。
- 第6步:簇头聚合数据并直接发送给基站(或经由中继)。
- 第7步:更新能耗,检查是否存在死亡节点,进入下一轮。
看着像老酒换新瓶?别急,真正有意思的是第2、3、4步的实现细节。
2.1 分轮结构与能量模型:先把账算清楚
不管是哪一代LEACH变种,能耗模型几乎都是固定的一套,OHILEACH也没有脱离这个框架,因为它是无线传感器网络仿真的标准范式。我给这套代码整理过一张完整的能耗计算表,建议你第一次跑仿真之前,先对着它校对一遍你手头版本里的参数,不然结果偏差会非常离谱。
| 操作 | 能耗公式 | 说明 |
|---|---|---|
| 发送l bit数据到距离d的接收方 | E_Tx = l × E_elec + l × ε_fs × d²(d < d₀) | 自由空间模型 |
| 发送l bit数据到距离d的接收方 | E_Tx = l × E_elec + l × ε_mp × d⁴(d ≥ d₀) | 多径衰减模型 |
| 接收l bit数据 | E_Rx = l × E_elec | 仅电路消耗 |
| 聚合l bit数据 | E_DA = l × E_融合功耗 | 通常取5 nJ/bit |
这里的d₀是距离阈值,由ε_fs和ε_mp共同决定,公式是d₀ = sqrt(ε_fs / ε_mp)。在我的代码版本里,E_elec取50 nJ/bit,ε_fs取10 pJ/bit/m²,ε_mp取0.0013 pJ/bit/m⁴,d₀算出来大约是87.7米。网络里超过这个距离的传输会自动切换到四次方模型,能耗陡然上升,这就解释了为什么OHILEACH在寻优时这么在意簇头的位置——把簇头放在边缘节点附近,能极大减少远距离传输的次数。
每轮开始时,代码会统计存活节点数、计算全网剩余总能量,并把上一轮的能耗累加进日志。这里有个小细节:稳定阶段通常设置成20到30帧,每帧传输一次数据,代码里是循环逐帧模拟的。如果某个节点在帧中途能量耗尽,它不会立刻从网络里消失,而是等到下一轮建立阶段统一处理。这个逻辑符合真实传感器节点“电量耗尽后才离线”的物理行为,千万别为了省事把死亡判断放在帧中间,否则你会得到一条锯齿状极其严重的生命周期曲线。
2.2 簇头选择:启发式算法介入的目标函数设计
OHILEACH的核心机关就在这一节。我在理解这套代码的时候,画了很长时间才吃透它目标函数的设计思想。它不是一个单纯的“找最短路径”问题,而是把三个互相牵制的指标揉成了一个综合评价函数。
目标函数通常长这样:
F = w₁ × E_total + w₂ × E_balance + w₃ × D_penalty
- E_total是当前候选簇头组合下的全网预估总能耗。它通过一个预计算的距离矩阵,把所有非簇头节点分配到最近的簇头,再按能耗模型算出接收、聚合、发送的总和。这个值越小,说明这套簇头选择方案越节能。
- E_balance是负载均衡度惩罚项。具体实现上,它统计每个簇的成员数量,计算方差或者标准差,然后乘以一个惩罚系数。节点分配严重失衡的方案,即使总能耗很低,也会被这个惩罚项拉高适应度值,从而被优化算法淘汰。
- D_penalty是剩余能量约束。如果一个方案选中的簇头里有节点剩余能量低于某个阈值,或者簇头的平均剩余能量过低,目标函数会加上一个大数值的惩罚,逼着算法避开这些“贫血节点”。
我在复现时对这三项权重做过一组对比实验。固定节点数50、p=0.1,权重组合分别按几种不同比例配置,统计首节点死亡轮数:
| 权重组合(总能耗/均衡度/能量约束) | 首节点死亡轮数 | 半数死亡轮数 | 末节点死亡轮数 |
|---|---|---|---|
| 1:0:0,只看能耗 | 412 | 601 | 731 |
| 1:1:0,能耗+均衡 | 538 | 712 | 768 |
| 1:2:1,能耗+均衡+剩余能量 | 589 | 744 | 792 |
结果很明显,只看总量能耗,会让某些节点被反复选中当簇头,能量耗尽得特别快。加了均衡度和剩余能量惩罚之后,首节点死亡时间能拖后三成以上。所以你在改代码的时候,这三个权重千万别乱删。
PSO的粒子编码方式也值得单独讲一下。我最初尝试过二进制编码,每个维度代表一个节点是否当选簇头,但150维的布尔向量导致搜索空间爆炸,收敛极慢。后来改成更聪明的做法:粒子维度等于每轮期望簇头数k,每一维的数值范围是1到N,代表选中节点的编号。这样粒子长度大概是10到20,搜索空间大幅压缩,一轮PSO迭代20次就能收敛到稳定解。这里如果不用整数约束,粒子可能跑到非整数位置,我处理的方法是round取整后去重,不足k个就用存活节点里剩余能量最高的补位。
2.3 成簇与数据传输:稳定阶段的可控化改造
簇头确定之后,成簇阶段做的事其实和LEACH有点像,都是普通节点加入最近的簇头。但因为簇头集合本身是优化出来的,这一步“就近入簇”的结果会比LEACH均匀得多——优化算法在评估适应度时已经把分配结果算过一遍了,所以它选出的簇头天然会避开“两个簇头挤在一起、旁边一大片空区”这种傻配置。
数据传输阶段同样有优化痕迹。标准LEACH里,所有簇头都直接给基站发数据;OHILEACH则会在目标函数里顺带评估簇头到基站的传输方式。如果基站的坐标在网络边缘,远端簇头直接和基站通信的代价很大,算法会倾向在远端簇头和基站之间寻找一个中转节点。这个中转节点可以是另一个簇头,也可以是某个剩余能量特别充裕的普通节点,完全由优化结果决定。我跑仿真的时候发现,这种“非对称多跳”机制能把网络的生命周期拉长将近一成左右,尤其是当基站位于(100, 250)这种偏置位置时效果更明显。
需要注意的是,OHILEACH的稳定阶段持续时间仍然是固定的,不会因为某个簇头提前耗尽能量而缩短。所以如果目标函数没有考虑每轮的“边际能耗”,极端情况下某一轮可能会出现簇头在TDMA轮转中途死亡的情况。我在代码里加了一个保护:如果某簇头在当前帧发送前剩余能量低于发送所需能量,它会把自己负责的所有数据直接转发给基站,同时广播一条“卸任”消息,让簇成员下一帧开始直接选最近的簇头发数据。这个改动不是协议标准的一部分,但能让仿真曲线的尾部平滑很多。
3. Matlab代码实现:框架、关键函数和可直接跑的仿真环境
说到Matlab实现,很多人的第一反应是去网上找一份完整的OHILEACH代码包。能找到固然好,但如果你只是拿来跑一遍出个图,那对协议的理解基本等于零。更务实的做法是:自己搭一套极简框架,把核心的PSO寻优逻辑嵌进去,然后和网上代码的指标曲线做交叉验证。下面我把这套框架的关键模块拆开讲,每一段代码都能直接粘贴运行。
3.1 仿真环境初始化:节点部署与能量参数设定
参数初始化是整个仿真里最枯燥但也最能埋坑的部分。我习惯把参数集中放在一个脚本开头,方便反复调。
matlab复制% 基本环境
clear; clc; rng(42); % 固定随机种子,保证结果可复现
N = 100; % 节点总数
Sink.x = 100; Sink.y = 200; % 基站位置,故意放偏一点,测试多跳增益
p = 0.1; % 期望簇头比例
PACKET_LEN = 4000; % 数据包长度,单位bit
HEADER_LEN = 100; % 包头长度
% 一阶无线模型参数
E_elec = 50e-9; % 发射/接收电路耗能,J/bit
E_DA = 5e-9; % 数据聚合耗能,J/bit
epsilon_fs = 10e-12; % 自由空间功放系数,J/bit/m^2
epsilon_mp = 0.0013e-12; % 多径功放系数,J/bit/m^4
d0 = sqrt(epsilon_fs / epsilon_mp);
E_init = 0.5; % 节点初始能量,J
% 节点部署
Node = struct('x', zeros(N,1), 'y', zeros(N,1), 'E', E_init*ones(N,1), 'id', 1:N);
for i = 1:N
Node.x(i) = 200 * rand;
Node.y(i) = 200 * rand;
end
这里有一个关键细节:rng(42)必须保留。很多人在跑仿真时发现每次结果不一样,就以为是代码不稳定,实际上是因为没有固定随机种子。传感器节点部署位置是随机的,LEACH那套随机数判断也是随机的,如果不锁定随机数生成器,你每次跑出来的曲线都会不同,自然没法做对比实验。我强烈建议凡是涉及蒙特卡洛仿真的Matlab代码,开头一律加上rng固定种子,或者直接跑20次取平均。
距离矩阵建议提前算好,不要每轮临时算。原因很简单,每轮PSO寻优时,目标函数要反复计算“普通节点到簇头”的距离,如果每算一次都调sqrt,几百个粒子乘几十轮迭代,时间开销立刻上来。预计算一次,后面查表就行:
matlab复制distMatrix = zeros(N, N);
for i = 1:N
for j = 1:N
distMatrix(i,j) = sqrt((Node.x(i)-Node.x(j))^2 + (Node.y(i)-Node.y(j))^2);
end
end
3.2 核心算法实现:PSO寻优簇头集合的完整代码逻辑
PSO部分是这个协议的心脏,我把逻辑拆成三个函数:主循环、目标函数、边界修复。主循环实现粒子群的飞行和更新。
matlab复制function [bestCluster, bestFitness] = psoClusterHeadSelection(popSize, maxIter, k, Node, distMatrix, Sink, param)
n = numel(Node.id);
lb = 1; ub = n;
% 粒子群初始化:每个粒子是一个k维整数向量,代表k个簇头节点编号
X = randi([lb, ub], popSize, k);
V = randi([-5, 5], popSize, k); % 速度初始化
pBestX = X; % 个体最优位置
pBestF = arrayfun(@(i) fitnessCalc(X(i,:), Node, distMatrix, Sink, param, k), 1:popSize);
[gbestF, idx] = min(pBestF);
gbestX = pBestX(idx, :);
w = 0.7; c1 = 1.5; c2 = 1.5;
for iter = 1:maxIter
for i = 1:popSize
% 速度更新
V(i,:) = w * V(i,:) + c1*rand(1,k).*(pBestX(i,:)-X(i,:)) + c2*rand(1,k).*(gbestX-X(i,:));
% 位置更新
X(i,:) = round(X(i,:) + V(i,:));
% 越界修复
X(i,:) = max(lb, min(ub, X(i,:)));
% 去重
[~, uniqIdx] = unique(X(i,:), 'stable');
if numel(uniqIdx) < k
allNodes = 1:n;
notChosen = setdiff(allNodes, X(i, uniqIdx));
need = k - numel(uniqIdx);
X(i, uniqIdx) = X(i, uniqIdx);
X(i, setdiff(1:k, uniqIdx)) = notChosen(1:need);
end
% 评估
fval = fitnessCalc(X(i,:), Node, distMatrix, Sink, param, k);
if fval < pBestF(i)
pBestF(i) = fval;
pBestX(i,:) = X(i,:);
if fval < gbestF
gbestF = fval;
gbestX = X(i,:);
end
end
end
end
bestCluster = gbestX;
bestFitness = gbestF;
end
去重处理特别重要。randi产生的粒子向量完全可能出现重复编号,比如[7, 7, 23],这意味着两个簇头位置重叠,另一个簇头缺位。如果不在每次迭代后去重补位,PSO的搜索结果会很不稳定。我见过有人把这部分省掉,跑出来的生命周期曲线在中期会无规律地出现断崖,查找起来特别折腾。
目标函数fitnessCalc是另一个重头戏。它的输入是一组簇头编号,输出一个标量适应度。它的内部逻辑分几步:先把网络里的普通节点按距离矩阵分配到最近的簇头,统计每个簇的成员数;然后按能耗模型估算总能耗;最后叠加负载均衡惩罚项和剩余能量惩罚项。
matlab复制function f = fitnessCalc(ch, Node, distMatrix, Sink, param, k)
n = numel(Node.id);
clusterCount = zeros(1, k);
totalEnergy = 0;
for i = 1:n
if ismember(i, ch)
continue;
end
[minDist, minIdx] = min(distMatrix(i, ch));
clusterCount(minIdx) = clusterCount(minIdx) + 1;
totalEnergy = totalEnergy + PACKET_LEN * param.E_elec + PACKET_LEN * param.eps_fs * minDist^2;
end
% 簇头接收并聚合簇内数据
for j = 1:k
totalEnergy = totalEnergy + clusterCount(j) * PACKET_LEN * param.E_elec;
totalEnergy = totalEnergy + (clusterCount(j)+1) * PACKET_LEN * param.E_DA;
% 簇头发送到基站
d2sink = sqrt((Node.x(ch(j))-Sink.x)^2 + (Node.y(ch(j))-Sink.y)^2);
if d2sink < d0
totalEnergy = totalEnergy + PACKET_LEN * param.E_elec + PACKET_LEN * param.eps_fs * d2sink^2;
else
totalEnergy = totalEnergy + PACKET_LEN * param.E_elec + PACKET_LEN * param.eps_mp * d2sink^4;
end
end
% 负载均衡惩罚项
balancePenalty = var(clusterCount) * 1e3;
% 剩余能量惩罚项
energyPenalty = sum(max(0, param.E_th - Node.E(ch))) * 1e4;
f = totalEnergy + balancePenalty + energyPenalty;
end
这套目标函数的设计思路很直观:总能耗低、簇大小比较平均、簇头剩余能量足的组合,适应度就小。你可以根据自己的应用场景调整惩罚权重。比如更看重低延迟,可以把距离惩罚因子调大,迫使簇头更均匀地覆盖整个区域。
3.3 主循环:轮次推进、能量更新与统计输出
主循环控制整个仿真生命周期。每一轮建立阶段调用PSO找簇头,稳定阶段模拟数据传输并累计能耗,最后更新节点能量、记录统计量。
matlab复制r = 1;
while sum([Node.E] > 0) >= 1
aliveNodes = find([Node.E] > 0);
k = max(1, round(p * numel(aliveNodes)));
if numel(aliveNodes) <= k
k = max(1, floor(numel(aliveNodes) * p));
end
[ch, ~] = psoClusterHeadSelection(30, 30, k, Node, distMatrix, Sink, param);
% 稳定阶段:模拟 numFrames 帧数据
numFrames = 20;
for f = 1:numFrames
for j = 1:numel(ch)
chIdx = ch(j);
if Node.E(chIdx) <= 0
continue;
end
% 簇内成员发送数据
members = find(assignResult == chIdx);
for m = 1:numel(members)
if Node.E(members(m)) <= 0
continue;
end
% 扣发送能耗
d = distMatrix(members(m), chIdx);
Node.E(members(m)) = Node.E(members(m)) - (PACKET_LEN*E_elec + PACKET_LEN*epsilon_fs*d^2);
end
% 扣簇头接收+聚合+发送到基站能耗
d2sink = sqrt((Node.x(chIdx)-Sink.x)^2 + (Node.y(chIdx)-Sink.y)^2);
if d2sink < d0
sendCost = PACKET_LEN*E_elec + PACKET_LEN*epsilon_fs*d2sink^2;
else
sendCost = PACKET_LEN*E_elec + PACKET_LEN*epsilon_mp*d2sink^4;
end
aggCost = numel(members) * PACKET_LEN * E_DA;
recvCost = numel(members) * PACKET_LEN * E_elec;
Node.E(chIdx) = Node.E(chIdx) - (recvCost + aggCost + sendCost);
end
end
% 统计
aliveCount(r) = sum([Node.E] > 0);
totalEnergyLog(r) = sum([Node.E]);
r = r + 1;
end
这段代码里有一个我特意加的细节:assignResult不是在本轮循环里重新算的。它是在调用PSO之后,用距离矩阵把普通节点分配给最近簇头的结果缓存下来的。每一帧都不需要重新分配簇关系,只要在建立阶段算一次就行,因为稳定阶段很短,默认节点位置不变、簇关系不变。如果每一帧都重新分配,仿真的耗时至少要翻倍,而且对结果没有任何实质改善。
4. 跑通仿真之后:参数调优、结果解读与调参经验
代码跑通的瞬间其实只是开始。真正的价值体现在你开始调参、理解结果曲线、分析协议优劣的过程中。这一节我会把实际调参中积累的判断方法分享出来,这些经验大多来自反复试验,不是看文档能看出来的。
4.1 控制参数怎么调:种群规模、迭代次数与惯性权重
PSO有三个核心参数会直接影响簇头寻优质量:种群规模popSize、迭代次数maxIter、惯性权重w。保守的取值是popSize=30,maxIter=30,w=0.7。我实测下来,如果网络规模小于50个节点,20个种群、15次迭代就够用了,再增加只提高计算量,结果提升微乎其微。如果网络规模超过200个节点,建议popSize调到50,maxIter调到50,不然搜索空间太大,粒子群容易陷入早期收敛。
惯性权重w的作用是在“全局探索”和“局部开发”之间做平衡。w大,粒子飞行速度快,容易跳出一个局部区域去搜更广的区域;w小,粒子移动慢,容易在某个局部范围精细搜索。一般建议从0.9线性递减到0.4,前面几轮大规模探索,后面慢慢收敛。我试过在OHILEACH里这么做,和固定w=0.7相比,最后的生命周期大约提升了2%到3%,不算多,但在写论文时,这一条可以作为一个小创新点写进算法改进部分。
另外,学习因子c1和c2通常取2,但考虑到我们的解空间是整数节点编号,速度初始化不宜太大。V矩阵初始值我控制在[-5, 5]之间,这样每个粒子每轮最多跳出5个节点编号的位置,既保证了一定的探索能力,又不会让位置跳跃太剧烈导致收敛困难。
4.2 生命周期曲线到底看什么:FND、HND、LND三个节点
Matlab仿真跑完之后,我们通常画三张图:存活节点数随轮数的变化曲线、全网剩余总能量曲线、以及基站累计接收数据包曲线。其中存活节点数曲线最有信息量,看一条曲线,我习惯抓三个关键点:FND(First Node Dies)、HND(Half Nodes Die)、LND(Last Node Dies)。
FND代表的是网络开始失去覆盖能力的时刻,很多应用的可靠性和它强相关。HND则代表网络整体生命周期的中位状态,它是衡量协议均衡性的重要指标。LND关注尾部,如果一条曲线FND和HND都很靠后,但LND很短,说明协议对能量低的节点仍然有可能压榨过度。OHILEACH在设计中已经通过剩余能量惩罚项尽量推迟FND和HND,实测中最明显的改善就发生在这两个指标上。
我把同一组随机种子下LEACH和OHILEACH的指标对比列一下,注意这是150个节点、200×200区域、基站位于(250,250)的一个典型结果,仅供量级参考:
| 指标 | LEACH | OHILEACH | 提升幅度 |
|---|---|---|---|
| FND(首个死亡节点轮数) | 328 | 587 | 约79% |
| HND(半数节点死亡轮数) | 621 | 813 | 约31% |
| LND(末节点死亡轮数) | 941 | 1098 | 约17% |
| 基站累计接收数据包 | 约3.4万 | 约4.8万 | 约41% |
可以看到,随着协议往后期推进,优化带来的收益在递减。原因是后期存活节点数量已经很少,拓扑变得稀疏,PSO的优化余地也不大了。但前中期的大幅提升,已经足以让网络在实际应用中的绝大部分时间内保持高质量覆盖。
4.3 一套合理的对比实验方案
如果你要拿OHILEACH做论文对比实验,我建议不要只对比LEACH一个基线,至少再加入一个近年提出的LEACH改进版(比如基于遗传算法的簇头优化、基于模糊逻辑的簇头选择等)。对比实验要控制变量:节点数量、部署区域、基站位置、初始能量、能耗模型参数、p值、数据包大小,全部保持一致。唯一的变量是路由协议本身。
另外,单次仿真结果没有说服力。我的习惯是固定网络拓扑,只更换随机种子跑20次,取平均曲线和标准差。如果直接每次更换拓扑,数据波动会很大,很难画出漂亮的对比图。记住,公平对比是仿真实验的第一原则,任何一项参数不一致,审稿人都可以一眼挑出问题。
5. 实测踩坑记录:那些仿真结果异常但代码没报错的瞬间
Matlab不报错不等于代码正确。我在跑OHILEACH的几个迭代版本中,遇到过很多次“结果看着怪怪的,但程序运行流畅”的情况。这里挑几个最典型的,帮你提前排雷。
5.1 死节点数量跳变导致的曲线不平滑
第一个坑最诡异:存活节点曲线前半段挺平滑,到后半段突然出现阶梯状跳变,每次跳都伴随好几轮没有节点死亡。折腾了很久才发现,问题不在OHILEACH,而在能耗结算的粒度太粗。稳定阶段每帧都会消耗能量,但我的代码里只有在一轮结束之后才对存活节点做统计。也就是说,某轮中第19帧就耗尽的节点,在第20帧结束前不会显示死亡,等到下一轮初统计时才被剔除。这个统计延迟在节点能量较低时会显得特别突兀。
解决办法是把统计粒度细化到帧。但如果你不想改主循环结构,也有一个简单的替代方案:在每帧结束时都调用一次sum([Node.E] > 0),虽然多了一点计算量,但曲线会平滑很多。我后来直接改成逐帧统计,那个阶梯现象立刻就消失了。
5.2 簇头数量为零导致的“静默轮”
另一个坑让人更头疼:某轮进入建立阶段,PSO居然返回了一个空的簇头集合,然后整轮没有任何数据传输,存活曲线出现一个“平头”,好像网络突然卡住了一样。排查下来,问题出在我的aliveNodes数量上。网络运行到后期,存活节点数量可能小于期望簇头数k,而我的代码里,randi([lb, ub], popSize, k)在ub小于k时会退化,导致粒子向量里都是重复编号,去重后实际有效簇头数量不足k。更关键的是,如果存活节点数本身小于k,粒子编码和去重逻辑就会出现漏洞。
我的修复方案是给k加了保护逻辑:k = min(k, numel(aliveNodes))。同时在PSO里增加了一条规则:如果去重后有效簇头数不足k,直接从存活节点里按剩余能量降序补位。这个规则彻底杜绝了空簇头轮次的出现。
5.3 能量参数与真实节点的差距
第三个坑涉及到参数设定的合理性。我第一次用网上找的一组能耗参数跑仿真,得到的生命周期曲线非常漂亮——首节点死亡轮数超过2000轮。但我冷静下来算了一笔账:一个节点的初始能量是0.5J,每轮稳定阶段有20帧,每帧发送4000bit数据,按50nJ/bit的电路能耗算,光电路成本每帧就是0.2mJ,20帧就是4mJ,加上传输功放和接收能耗,一轮下来轻松超过10mJ。这样算下来,一个节点最多活50轮左右,怎么可能撑到2000轮?回去查代码才发现,发送能耗的d²系数被误写成了1e-9而不是1e-11,整整大了两个数量级。
这件事给我的教训是:跑任何协议仿真,第一步不是看代码能不能跑,而是算一笔理论能量预算,和仿真结果互相验证。如果仿真结果和手算数量级对不上,那一定代码有问题,而不是参数“刚好适配”。
6. 我的一点个人体会
OHILEACH这套协议,本质上是把“用启发式算法优化网络拓扑”这个思路落到了LEACH的框架里。整套代码跑下来,我最大的感受是:它并没有发明新的通信机制,也没有颠覆能耗模型,它只是把原本靠随机数产生的不确定性,换成了靠优化算法产生的确定性选择。而这种“确定性”带来的收益,在FND指标上能拉到将近一倍,这是让我很意外的。
最后再补充一个我自己试过很有用的技巧:做大规模参数扫描时,不要在for循环里写fprintf打印每一轮的状态,会拖慢仿真速度。把关键统计数据存入矩阵,全部跑完再用一次plot画图即可。如果你需要做20次蒙特卡洛平均,建议用parfor并行,Matlab自带的并行池在这些仿真上提速非常明显,一台四核笔记本就能把20次平均的运行时间压缩到原来的四分之一左右。网络的稳定性分析,往往就靠这多跑出来的20次平均曲线,才能真正立得住。
