资讯动态

ns3-gym入门:linear-mesh网络仿真与强化学习信道选择示例拆解

发布时间:2026/10/3 7:56:17 来源:尧图企业网站定制
如果你已经装好了 ns3-gym翻遍仓库里的 examples大概率会在linear-mesh这个例子上停下来。它被官方当成入门示例但第一次打开simple.cc那几百行 C 时还是会有点懵一个叫“线性网格”的网络到底模拟了什么agent 的动作、观测、奖励是怎么从 ns-3 的仿真世界里传出去的为什么跑完训练脚本之后Q-learning 真的能学会挑信道这篇就来把linear-mesh彻底拆开从网络场景到代码实现从编译运行到结果分析再到把它改成你自己的自定义环境。不管你是刚接触网络仿真还是已经在 ns-3 里写过一些脚本但还没搞懂 RL 接口这篇都值得看完。1. linear-mesh 到底在模拟什么网络场景先解决最基础的困惑这个例子运行起来后ns-3 里到底搭了一个什么网1.1 不是 Mesh 协议的“线性网格”名字带 mesh但这里其实没有跑 802.11s mesh 路由协议。linear-mesh描述的更像是一种空间拓扑一个 AP 放在中间若干个 STA 沿着一条水平线分布在 AP 两侧间距固定。把节点位置画出来就是一条线所以叫“linear”节点之间通过 Wi-Fi 连接成一个基础设施网络形式上又有点 mesh 的味道于是合成linear-mesh。我之前看这个例子时也被名字误导过以为要理解 Mesh 路由、HWMP 协议之类的。实际上你只需要关心一个非常朴素的问题在 Wi-Fi 干扰环境中AP 选择哪个信道能让所有 STA 的总吞吐量最大。1.2 场景中的角色与流量场景大概长这样1 个 AP 节点负责接收所有 STA 的上行流量。例如 7 个 STA均匀分布在 AP 两侧相邻节点间距固定。每个 STA 产生 UDP 流量持续向 AP 发送数据包。Wi-Fi 标准用的是 802.11a信道集合一般是 {36, 40, 44} 这样的 OFDM 信道。环境中还有一个“干扰源”逻辑特定 STA 之间的同频干扰会让某些信道组合表现变差。你可能已经在心里吐槽这不就是一个普通的基础设施 Wi-Fi 场景吗没错。它的价值恰恰在这里——把“选择信道”这个网络决策问题压缩到一个最小可复现的强化学习环境里。每个 STA 的吞吐量会随 AP 信道选择、节点间干扰、时间推进而变化这个变化规律不是人工写死的规则而是 ns-3 物理层、MAC 层真实仿真出来的结果。1.3 为什么官方拿它当入门第一个例子之前带几个师弟上手 ns3-gym我的结论是linear-mesh是少有的“麻雀虽小五脏俱全”的 RL 环境示例。它包含了一个完整的 Gym 环境所要求的全部要素动作空间AP 从 3 个信道里选一个属于离散动作。观测空间每个 STA 的吞吐量或接收字节数属于连续向量。奖励函数所有 STA 的吞吐量之和语义非常直观。终止条件仿真时间达到上限一个 episode 结束。真正复杂的网络决策问题比如路由选择、拥塞控制、资源块调度本质上都可以抽象成“状态 动作 奖励”的三元组。只不过 linear-mesh 把状态和动作都简化了状态就是一行吞吐量数字动作只要选一个信道。这个简化让它特别适合理解 ns3-gym 的“环境封装”机制——先不纠结算法把接口跑通后面换场景就是依样画葫芦。2. 两个进程一只 Q 表ns3-gym 的通信架构速览在逐行看代码之前得先弄明白一件事linear-mesh这个例子跑起来后其实不是一个程序在工作而是两个进程在协作。2.1 ns-3 负责“世界”Python 负责“大脑”一边是 ns-3 仿真进程里面跑着完整的 Wi-Fi 协议栈、信道模型、节点移动和流量发生器。另一边是 Python 脚本里面住着 Q-learning agent。这两个进程之间并不是简单地调个函数而是通过网络通信来交换信息。ns-3 侧的核心类是OpenGymInterface它把 ns-3 仿真器包装成了一个 Gym 环境。你写代码时主要继承OpenGymEnv这个虚基类实现几个关键方法GetObservationSpace()告诉外层这个环境的观测长什么样。GetActionSpace()告诉外层动作空间是离散还是连续有多少取值。GetObservation()从当前仿真状态里提取出观测向量。GetReward()计算当前一步的奖励。GetDone()判断仿真是否结束。GetGameOver()判断 episode 是否因失败结束。ns-3 每运行一个时间步就会通过OpenGymInterface::NotifyCurrentState()把观测、奖励、是否结束等信息发给 Python 进程。Python 进程的env.step(action)返回(obs, reward, done, info)本质上就是跨进程读到了 ns-3 里这些方法的返回值。Python 侧则通过ns3gym.GymEnv(rlib:linear-mesh)创建环境对象。GymEnv后面跟的那个字符串是 ns-3 里的配置比如agentType、iterations、agentId等它们会被用来启动对应的 ns-3 仿真进程。2.2 ZMQ 还是 gRPC版本差异别踩坑不同版本的 ns3-gym 通信方式不一样。较早的版本基于 ZeroMQ通过一个固定的端口进行消息交换而 1.1.0 之后的版本改用了 gRPC通信协议更重一点但配置更简单。这一点在实际使用时非常重要因为网上的教程很多写的是旧版方法你照着pip install zmq装了半天发现新版根本没用到。我现在用的版本是 ns3-gym 1.1.0 ns-3.38通信已经走 gRPC。跑之前不需要手动装 ZeroMQ但需要保证 Python 侧有grpcio和protobuf。如果你用的是旧版 ns3-gym那就需要留意libzmq的安装以及端口占用问题——上一个 ns-3 进程没杀干净再跑一次就会报“端口被占用”。2.3 一个 episode 对应一次仿真进程理解 ns3-gym 的循环方式是很多人卡壳的地方。在标准 Gym 环境里env.reset()只是把状态归零代码还在同一个进程里。但在 ns3-gym 里env.reset()通常会重新启动一次 ns-3 仿真进程。新仿真从头开始搭建节点、启动流量、推进时钟。也就是说一个 episode 一次 ns-3 仿真运行。一个 step 不一定等于一个固定的仿真秒数而是由 ns-3 内部的调度决定。linear-mesh的simple-test.py里Q-learning 会循环很多个 episode 来训练。每开新 episodens-3 就重新起一遍仿真。这也是为什么 RL 训练神经网络时几百个 episode 就要跑很久——每次 episode 都是一次完整的 Wi-Fi 仿真计算量并不小。理解了这两个进程的分工再去看simple.cc和simple-test.py思路就会清晰很多前者负责描述“环境怎么变”后者负责“Agent 怎么学”。3. 拆解 simple.cc如何把一个网络决策问题描述成 Gym 环境simple.cc是 linear-mesh 例子里的 ns-3 端代码。它做的事情可以分成两层先搭仿真场景再把场景包装成 RL 环境。3.1 仿真场景的搭建逻辑在 ns-3 里搭场景有一套固定的流程创建节点容器、配置 Wi-Fi 信道、安装网络设备、设置 IP 地址、挂载应用。linear-mesh 的特别之处在于节点的位置排列。大致代码如下我根据仓库源码做了简化方便说明结构NodeContainer staNodes; NodeContainer apNode; apNode.Create(1); staNodes.Create(numSta); // 布置位置AP 在 x0STA 向左右两侧线性展开 for (uint32_t i 0; i numSta / 2; i) { staNodes.Get(i)-SetPosition(... Vector(-(i 1) * spacing, 0, 0)); } for (uint32_t i numSta / 2; i numSta; i) { staNodes.Get(i)-SetPosition(... Vector((i - numSta / 2 1) * spacing, 0, 0)); } // 配置 Wi-Fi 11a WifiHelper wifi; wifi.SetStandard(WIFI_STANDARD_80211a); wifi.SetRemoteStationManager(ns3::ConstantRateWifiManager, DataMode, StringValue(OfdmRate54Mbps)); YansWifiChannelHelper channel YansWifiChannelHelper::Default(); YansWifiPhyHelper phy YansWifiPhyHelper::Default(); phy.SetChannel(channel.Create());numSta、spacing这些参数可以通过命令行传入。这个例子里默认是 7 个 STA间距可以调整。值得注意的是AP 的物理层会被专门拿出来保存成指针因为之后 agent 的动作会直接控制这个 AP 选择哪个 Wi-Fi 信道。3.2 Observed 结构为什么吞吐量是观测而不是丢包率或信号强度linear-mesh的观测向量设计得非常直白每个 STA 近期收到的数据包字节数或吞吐量拼成一个向量。在OpenGymEnv的子类里GetObservationSpace()和GetObservation()是成对出现的。空间描述用OpenGymBoxSpace表示这是一个连续的、有上下界的向量PtrOpenGymSpace MyEnv::GetObservationSpace() { float low 0.0f, high 1000000.0f; std::vectoruint32_t shape {m_numSta}; std::string dtype float32; return CreateObjectOpenGymBoxSpace(low, high, shape, dtype); }对应的观测提取核心是读取每个 STA 的流量接收器UdpServer或PacketSink里累计收到的字节数然后做差得到最近一个决策窗口内的吞吐量增量。这个实现非常典型——它不直接给你瞬时速率而是给一个时间段内的累计变化量。这样设计的好处是环境模型简单agent 容易从数值变化里感知到动作的效果。3.3 动作空间信道列表不是从 1 到 11而是离散的集合Wi-Fi 信道听起来像是一个连续范围但实际可用信道往往是离散的几个。linear-mesh里把动作空间定义成一个离散空间比如从 {36, 40, 44} 三个信道里选一个。在 ns3-gym 里离散空间用OpenGymDiscreteSpace表示PtrOpenGymSpace MyEnv::GetActionSpace() { return CreateObjectOpenGymDiscreteSpace(3); }注意这里传的是 3表示动作取值 0、1、2对应三个信道索引。代码里维护一个信道数组m_channels {36, 40, 44}动作索引到实际信道的映射发生在ExecuteActions()里void MyEnv::ExecuteActions(PtrOpenGymDataContainer actionContainer) { auto action DynamicCastOpenGymDiscreteContainer(actionContainer); uint32_t channelIndex action-GetValue(); uint32_t channel m_channels[channelIndex]; m_wifiPhy-SetChannelNumber(channel); }这里有个门道动作不是立刻生效。ns3-gym 的交互模式是环境先发状态给 PythonPython 返回动作ns-3 再把动作应用回仿真。这个“应用”动作的动作是在一个Simulator::Schedule的回调函数里完成的确保它发生在正确的仿真时刻。理解这一点后就不会困惑为什么ExecuteActions里只是存了个值后面才真正去改物理层信道。3.4 奖励函数设计的标准套路linear-mesh的奖励就是所有 STA 的吞吐量之和。可能有人觉得这也太简单了但恰恰是这个简单让学习目标非常清晰让总吞吐量尽可能大。实现上GetReward()会比较当前累计字节数与上一次的差值除以时间间隔得到这个窗口内的总吞吐量。这样一个 episode 里每做一次决策agent 就能拿到一个与动作直接挂钩的反馈。奖励塑形在这里没有做太复杂的事比如没有为频繁切换信道加惩罚项。这样做的结果是agent 可能会在相近性能的信道之间反复横跳但对入门来说完全够用。如果之后你想做更精细的调度可以自己加信道切换惩罚系数让 agent 学会稳定在一个好信道上。3.5GetDone()与 episode 终止GetDone()返回的是仿真是否已经结束通常与仿真时间挂钩。linear-mesh 里一个 episode 的仿真时长是有限的可能只有几十秒仿真时间跑完后环境告诉 agent“本局结束请开始下一局。”这里需要区分“时间结束game over”和“失败lost”的含义。ns3-gym 用两个不同的方法表达这两种终止很多从标准 Gym 过来的同学容易混淆GetDone()管的是整个仿真是否结束GetGameOver()管的是当前 episode 是否应该提前结束比如性能太差。linear-mesh 没有提前终止机制所以GetGameOver()直接返回 false。看到这里你会发现整个 simple.cc 的核心就是把“网络状态”翻译成“RL 接口”的几个方法。场景搭建部分虽然长但都是 ns-3 常规操作。4. 拆解 simple-test.pyQ-learning 如何学会挑信道ns-3 端构建好环境Python 端的任务就很单纯了跑一个学习算法。linear-mesh 的例子用最原始的 Q-learning连神经网络都没有这反而是好事因为你可以完整地看到学习过程在做什么。4.1 从 GymEnv 到标准 RL 循环创建环境时代码大致是这样import ns3gym env ns3gym.GymEnv(rlib:linear-mesh)这一行背后发生的事情比表面复杂Python 会解析rlib:linear-mesh这个描述符去启动一个相应的 ns-3 进程并建立 gRPC 连接。所有环境参数通过命令行传给 ns-3比如env ns3gym.GymEnv(rlib:linear-mesh, agentTypeQlearning, iterations20)iterations决定了一个训练脚本里要跑多少个 episode。之后的循环和标准 Gym 完全一致obs env.reset() while True: action agent.get_action(obs) obs, reward, done, info env.step(action) agent.update(obs, action, reward, done) if done: breakenv.step(action)会像前面说的那样把动作通过 gRPC 发给 ns-3 进程ns-3 执行完一个决策周期后再把新的观测、奖励和 done 返回。整个过程对 Python 脚本来说几乎是透明的——你感知不到跨进程通信的存在。4.2 Q 表的状态是怎么来的标准的 Q-learning 需要离散状态。linear-mesh里最直接的离散状态就是“AP 当前选的信道”。但只看当前信道agent 是无法感知网络状况变化的。所以代码里通常会把观测值做阈值化处理把连续吞吐量分成几个档位组合成有限个状态。举个例子如果把每个 STA 的吞吐量粗略分成“低/中/高”三档那 7 个 STA 就有 (3^7 2187) 种组合再叠加上当前信道状态空间就明显变大了。不过 linear-mesh 为了保持示例简洁通常会进一步简化比如只关心总吞吐量高低或者用少数关键 STA 的吞吐量来划分。这里我想提醒一句Q-learning 这类表格型方法状态空间一大就跑不动了所以示例代码里的离散化策略非常粗。你真正要学的不是这个离散化方案多高明而是“连续观测如何映射成离散索引”这个思考过程——后面换成 DQN 时就不再需要人工做这个映射了。4.3 epsilon-greedy 探索与学习参数Q-learning 的训练过程不外乎反复执行这样的步骤以 epsilon 概率随机选一个动作否则按 Q 表选最大 Q 值的动作。执行动作获得奖励和新状态。更新 Q 表(Q(s,a) \leftarrow Q(s,a) \alpha \cdot (r \gamma \cdot \max_{a} Q(s,a) - Q(s,a)))。慢慢降低 epsilon让 agent 从“乱试”转向“利用已学经验”。linear-mesh的示例里学习率alpha通常设为 0.1 左右折扣因子gamma设为 0.9epsilon 从 1.0 线性衰减到 0.01。跑完几千个 episode 后Q 表会把每个状态下的最优信道选择记录得非常清楚——最终训练结束时你可以打印 Q 表看到在某个网络状态下哪条信道是最佳选择。我在实际跑的时候发现这个例子收敛得比想象中快。因为总共只有 3 个动作信道组合有限即使状态划分粗一点几百个 episode 之后 Q 表就基本稳定了。如果换成连续动作或高维状态就没这么轻松了。4.4 训练输出里到底该看什么跑完训练脚本终端会打印每个 episode 的 reward。我印象最深的输出模式是前几个 episodereward 起伏很大因为 epsilon 接近 1基本是随机信道选择。中间阶段reward 开始集中在某个高值附近偶尔还会跳到低值——这是探索还没完全停下来的表现。最后几百个 episodereward 波动明显减小基本稳定在最优信道对应的吞吐量上。如果你把每个 episode 的 reward 画成曲线会看到一条典型的强化学习收敛曲线前期震荡中期上升后期收敛。看到这条曲线时你就彻底理解 linear-mesh 想表达的内容了——一个网络决策问题如何通过学习算法被自动求解。5. 从安装到复现完整跑通记录与排坑经验这一节不讲理论纯粹讲操作。我按自己在 Ubuntu 20.04 上完整的复现过程来写环境不同的小细节也会提到。5.1 依赖安装与源码编译我目前的推荐组合是ns-3.38 ns3-gym 1.1.0 Python 3.8 以上。ns-3 的依赖项不少一条命令装齐sudo apt update sudo apt install g python3 python3-pip cmake ninja-build \ libboost-all-dev libprotobuf-dev protobuf-compiler \ libgrpc-dev libprotobuf-dev libfmt-devns-3.38 已经成为 ns-3 的主力版本源码下载后解压到工作目录。ns3-gym 仓库要直接放到contrib/目录下cd ns-3.38 git clone https://github.com/tkn-tub/ns3-gym.git contrib/ns3-gym然后配置和编译./ns3 configure --enable-examples --enable-tests --enable-python-bindings ./ns3 build编译时间根据机器性能不同通常要 10 到 30 分钟。编译完成后Python 侧的 ns3gym 包也要安装在contrib/ns3-gym目录下执行pip install -e .这样写 Python 脚本时import ns3gym就不会报找不到模块了。5.2 装完最容易踩的坑第一个坑是网络描述符无法解析。如果运行ns3gym.GymEnv(rlib:linear-mesh)时报错找不到对应的配置通常是因为 ns-3 端没有编译检查出这个示例或者命令行参数解析失败。解决办法是先手动跑一次对应的 ns-3 程序确认能起来./ns3 run linear-mesh第二个坑是gRPC 版本兼容问题。如果你之前装过旧版 ns3-gym 或 grpcio 版本太旧启动时会报TypeError或者StatusCode.UNKNOWN之类莫名其妙的错误。我遇到过好几次最后都是重装grpcio和protobuf解决的。第三个坑是日志刷屏导致看起来像卡死。如果加了--verbose或没关 Wi-Fi 日志ns-3 会把每个包的收发都打到终端看起来就像程序卡住了一样。跑例子时建议保持默认日志如果嫌输出太多可以在 ns-3 端把日志级别改成LOG_ERROR。5.3 跑通之后的输出与结果验证在examples/linear-mesh目录下运行 Python 训练脚本python3 simple-test.py如果一切正常你会看到类似下面的输出Iteration: 0, reward: 12.3456, done: False Iteration: 0, reward: 23.4567, done: True Iteration: 1, reward: 45.678, done: False ...我建议第一次跑的时候把iterations设小一点比如 10先确认整个链路没问题再跑完整的几百个 episode。另外一个很实用的验证方式训练结束后把 epsilon 直接设为 0再用 Q 表跑几个 episode观察最终 reward 是不是稳定在最优值附近。这个“只用最优策略跑测试”的习惯在真实项目里也很重要——它能把“训练表现”和“最终策略表现”区分开。5.4 如何判断训练真的收敛了只盯着终端输出很难判断收敛最好是把 reward 曲线画出来。哪怕不用 matplotlib也可以把每个 episode 的 reward 写到文件里然后用任何绘图工具看趋势。我个人的判断标准是最后 100 个 episode 的 reward 方差明显小于前 100 个且平均 reward 不再继续上升。linear-mesh 场景小这个标准很容易达到。如果你的曲线一直在震荡先不急着调算法检查一下环境侧有没有问题——比如信道切换动作有没有真的作用到 AP 上观测里是不是混入了无关特征。6. 从 linear-mesh 到第一个自定义场景跑通 linear-mesh 只是第一步。这个例子更大的价值是给你一张“改造地图”从它出发稍微改几个地方就能变成完全属于自己的 RL 网络仿真环境。6.1 改网络规模与信道列表想把 STA 数量从 7 改成 15只需在simple.cc里调整numSta参数并在 ns-3 命令行传参或者在代码里固定改变。还需要同步修改观测空间的 shape 定义从{7}改成{15}。这一步能让你立刻体会到“环境定义一致性”的重要性——观测空间和奖励实现必须同步更新。如果想改信道集合比如用 5 个信道只需要把m_channels数组扩充并把GetActionSpace()里的离散维度从 3 改成 5。这种改动不会影响 RL 接口逻辑但会让学习任务更难——探索空间变大了Q-learning 的收敛速度会明显下降。对入门者来说这是一次很好的复杂度体验。6.2 修改流量模型与干扰逻辑默认的流量是 UDP 固定速率改成 ON/OFF 流量模型只需要在 ns-3 脚本里把OnOffHelper换成UdpServerHelper的配置方式。更进阶一点可以加入背景干扰节点让信道质量随时间动态变化。这样即使 agent 找到了一个好信道过一段时间干扰源跑过来最优信道也可能改变agent 就得学会“跟踪”变化。这种动态性会让训练时间变长但对真实场景的模拟度更高。6.3 多 Agent 扩展思路linear-mesh 是单 Agent 环境只有 AP 做决策STA 是被动的流量源。如果把 STA 也变成可以决策的节点比如各自选择发送功率就变成了多 Agent 强化学习问题。ns3-gym 本身支持多 Agent 场景核心是在一个 ns-3 进程里注册多个OpenGymInterface实例Python 端则用GymEnv分别连接不同的agentId。多 Agent 的坑比单 Agent 多得多奖励怎么分配、不同 Agent 是否共享观测、行动顺序怎么同步这些都要重新设计。我的建议是先把 linear-mesh 的单 Agent 调稳再去看官方多 Agent 示例否则很容易被通信时序问题淹没。个人经验是整个 ns3-gym 上手过程里linear-mesh 的simple.cc是最值得逐行读透的代码它把“网络仿真”和“强化学习”两个领域的边界问题处理得非常清晰。后面不管你是要做智能路由、无线资源调度还是边缘计算任务卸载本质上都是在这套接口框架里换场景、换观测、换奖励函数。把这一课吃透后续的扩展其实就是工作量的问题不是理解力的问题了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑