ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ns3-gym多智能体路由仿真:linear-mesh示例深度解析

ns3-gym多智能体路由仿真:linear-mesh示例深度解析 1. linear-mesh 这个例子到底做了什么为什么值得细读1.1 它解决的是一类什么问题ns3-gym 这个框架最吸引人的地方就是把 ns-3 这种离散事件网络仿真器和 OpenAI Gym 的强化学习接口焊在了一起。你在 ns-3 里构造网络场景再把仿真过程中的网络状态抽出来作为 observation把智能体给出的动作映射成网络行为然后通过 reward 告诉智能体做得好不好。这个过程说起来简单但真正上手之后你会发现坑全藏在“状态怎么抽”“动作怎么映射”“reward 怎么定”这些细节里。linear-mesh 这个例子就是解决“从单智能体走向多智能体”这个问题的最佳切入点。它不是用强化学习去做一个集中式的路由决策而是让网络里的每一个节点都成为一个独立智能体每个智能体根据自己的局部观察来决定把数据包转发给哪个邻居。你可以把它想象成城市里没有中央交通指挥中心每个路口的信号灯自己观察车流、自己决定放行方向。这样一来网络行为的涌现是大量局部决策叠加出来的结果这种模式非常接近真实分布式网络的工作方式。从应用角度看这种多智能体决策模式可以迁移到动态路由、数据中心流量调度、无线自组网接入控制等场景。你不需要更换 ns3-gym 的主干代码只需要把 linear-mesh 的状态、动作、reward 改成你关心的问题就能在几十行代码内搭建一个可运行的强化学习网络仿真实验。所以很多人把它当作 ns3-gym 系列里“从会跑到会飞”的跳板。1.2 它在整个学习路径里的位置如果你是从最基础的示例开始接触 ns3-gym那么 linear-mesh 之前你大概率已经跑通过一个最简单的单智能体示例比如某个节点需要根据当前观测选择拥塞窗口或者切换链路。那个阶段的核心目标是弄明白 ns-3 和 Gym 之间“一发一收”的基本交互。但那种单智能体场景掩盖了很多工程问题多个环境实例怎么管理多个智能体共享一个 Python 训练进程时观测和动作会不会串仿真器里的每个节点如何与 Python 端的 gym.Env 一一对应linear-mesh 把这些问题全部摊开了。它创建了一个线性排布的 mesh 拓扑多个节点同时扮演 Gym 环境的角色每个节点就是一个独立的 Ns3GymEnv 子类实例。你在代码里能清楚地看到“一个节点一个环境”的注册方式也能看到状态怎么拼装、动作怎么解析、reward 怎么反馈。把这份代码读透了再去设计自己的多智能体场景基本就是套模板的事情。另外我特别建议你把 linear-mesh 和它对应的 Python 训练脚本放在一起读。只看 C 侧代码你会困惑“这些观察到底是谁来消费”只看 Python 侧代码你会困惑“这个 observation 到底从哪来”。两边对照着啃才是打开 ns3-gym 的正确姿势。这篇博文也会一直用双线并行的方法来讲。2. 跑通 linear-mesh环境准备、启动步骤与日志解读2.1 跑之前的准备在开始之前建议先确认一下你的编译链和依赖是否完整。ns3-gym 的 C 侧强依赖两个库一个是 protobuf用于消息序列化另一个是 ZMQ用于进程间通信。编译前先把这两个装好。Debian/Ubuntu 系统上一般是 libprotobuf-dev、libzmq3-dev不同 Linux 发行版名字稍有差异装好之后用 pkg-config 验证一下即可。我踩过一次protobuf版本不一致的坑系统自带版本和项目要求的版本对不上configure 阶段不报错真正跑起来才崩所以最好在编译前就确认版本。Python 侧的核心依赖有 gym、numpy、tensorflow或 pytorch。这里有一个非常容易忽略的细节ns3-gym 官方仓库里的训练脚本很多是用 TensorFlow 1.x 写的如果你装的是 TensorFlow 2.x直接跑会报一堆 API 不兼容的错误。我当时的处理方式是在 Conda 里单独建一个 Python 3.6 的环境装 gym0.19 和 tensorflow1.15这样跑官方示例最省心。当然你也可以自己把训练代码改成 TensorFlow 2.x 的写法但刚入门的时候先不要折腾能用原版跑通比什么都重要。编译参数上也需要留个心眼。如果官方仓库里的例子没有被编出来大概率是 configure 的时候没有打开 examples。用 waf 编译时加上 --enable-examples 再重新 build然后检查 build 目录下是否有 linear-mesh 二进制文件。确认无误之后我们进入启动环节。2.2 启动步骤的先后顺序这个先后的顺序很容易搞反我专门说一下。ns3-gym 的通信模式是 ns-3 进程主动向 Python 进程发起连接请求等待 Python 端分配 gym 环境并返回初始观测。所以正确的启动顺序是先启动 Python 训练脚本让它进入监听状态再启动 ns-3 仿真程序让它主动连上来。先用一个终端运行训练脚本cd ns3-gym/src/opengym/examples/linear-mesh python3 linear-mesh-train.py然后再开一个终端运行 ns-3 侧程序cd ns3-gym ./waf --run linear-mesh如果你的编译路径不在默认目录waf 会提示找不到程序这时候要么切到源代码根目录要么用绝对路径指定 build 目录里的可执行文件。第一跑起来的时候你会看到 Python 终端先打印出类似“等待连接”或者端口绑定的信息ns-3 终端则开始输出仿真日志。两个终端都平稳运行说明握手已经完成。这里有个小细节ns-3 仿真是按虚拟时间推进的而 Python 侧的训练循环是按真实时间执行的所以两者天然不在同一个时钟域。ns3-gym 的设计中Python 的 gym.step 调用会被转换成一条 RPC 消息通知 ns-3 仿真器执行下一步仿真器再根据事件调度推进。你不需要手动同步时钟但你要明白“仿真时间”和“墙上时间”之间没有严格对应关系。后面调参的时候你会不断遇到“明明 print 了很多东西怎么仿真才走了几毫秒”的现象。2.3 第一次跑通时你会在终端看到什么跑通之后别急着关掉先观察几轮输出。Python 端会周期性打印每个节点的观测值、动作、以及当前智能体拿到的 reward。ns-3 端会打印数据包的转发路径、节点收到的包数量以及环境注册的相关日志。我建议你把注意力放在三件事上。第一看“环境注册”日志。每个节点都应该成功注册为独立的 Gym 环境并印出自己的 env_id。如果节点数量对不上说明拓扑创建可能有问题或者某些节点构造环境失败。第二看“观测内容”。节点上报的观测里应该包含当前节点 ID、邻居信息、目的节点 ID以及可能的到目的地的距离估计。这些数值会在训练初期剧烈变化因为策略还没稳定动作基本是乱选的。第三看“Reward 的反馈频率”。一个数据包从源节点到目的节点中间每经过一个节点就会产生一次 Gym step 交互。也就是说一条多跳路径会产生一串 reward 反馈而不是最终到达时才给一个奖励。这就是多智能体场景和单智能体场景明显的体验差异你一定要习惯这种“每跳反馈”的节奏。如果日志里出现连接中断、端口占用、观测维度不匹配之类的错误先别急着怀疑硬件优先查通信链路和状态定义我在第六节会统一整理排查方法。3. C 侧环境实现状态空间、动作空间与奖励设计3.1 一个节点就是一个 Gym Environment在 linear-mesh 中C 侧的核心是 MeshNetEnvironment 类。它继承自 Ns3GymEnv 这个基类并重写了几个关键函数构造时注册环境信息运行时上报状态、接收动作、产生奖励。每个 MeshNetEnvironment 对象都绑定到一个 ns-3 节点同时持有一个指向该节点设备或队列的指针。这样设计的好处很直接智能体给出的动作可以直接写进设备层而设备层产生的统计信息又能被环境回收成新的状态不需要在仿真器里来回找对象。从代码结构上看构造函数里一般会调用基类的 SetObservationSpace 和 SetActionSpace 来声明状态空间和动作空间的形状。这两步决定了 Python 端 gym.make 之后观测和动作到底长什么样。很典型的错误是忘记设置空间结果 Python 端收到的观测全是空的或者动作维度对不上报出 shape mismatch 的异常。所以你把环境跑起来之前最好先检查这两个函数有没有被正确调用。通信回调方面主要有两个关键切入点NotifyNewState 和 GetActionFromAgent。前者发生在节点完成一次状态更新时会把当前观测推送给 Python 端后者发生在 Python 端返回动作之后C 侧在这里解析动作并映射成实际的网络行为。整体逻辑可以粗略理解成这样的伪代码void MeshNetEnvironment::NotifyNewState() { // 组装当前观测 auto state BuildState(); // 上报给 Python 端并等待动作返回 action GetActionFromAgent(state); // 根据动作设置转发决策 ApplyAction(action); // 根据转发结果计算奖励 ComputeReward(); }这个流程虽然被简化了但主线是对的。读源码的时候你会发现实际实现里会拆得更细比如用 Simulator::Schedule 把环境更新挂到事件队列里而不是在回调里同步执行。这个设计在第五节我会展开讲先记住“每个节点都有这么一套状态上报-动作接收-奖励计算”的循环就够了。3.2 状态和动作如何编码成数字state 的设计我重点说一下。MeshNetEnvironment 里的状态不是把整个网络拓扑塞给智能体而是只给局部信息。一般包含当前节点的 ID、目的节点 ID、邻居数量以及可能附加的当前队列长度或链路质量估计用连续值数组表示。比如用前两个维度表示当前节点和目的节点后面的维度用来表示到各邻居的距离或跳数估计。这样设计是为了贴近真实的分布式网络因为真实路由器不可能知道全网状态它只能根据局部交换信息做决策。动作空间的编码是另一个容易搞混的地方。ns3-gym 的接口要求动作是一个数值或数值数组不能直接把“把包发给邻居节点 3”这种语义传给仿真器必须编码成离散索引。linear-mesh 的做法通常是建立一个“邻居列表”动作值就是在这个列表里选择下标。比如当前节点有 2 个邻居动作空间就是 0 或 1对应转发到第一个或第二个邻居。用这种编码方式不同节点的邻居数量可以不同但动作空间统一成“最多邻居数量”不够的部分填充无效动作或者显式屏蔽掉。这里有一个非常需要注意的细节如果你把动作空间固定为网络里最大邻居数那么某些节点实际可选的邻居数很少那么智能体就可能输出一个无效动作。linear-mesh 在解析动作时会对这种越界情况做防御比如忽略这个动作或者随机选择邻居。你的自定义环境里也必须处理这种“局部动作空间不一致”的问题否则训练过程中会出现大量无效交互智能体完全学不到东西。3.3 奖励该怎么设计reward 是这类多智能体路由场景里最能体现设计水平的地方。linear-mesh 的奖励逻辑并不复杂大体上可以总结成“到达目的地给正奖励中间每跳给一个小的负惩罚”。你可以理解成鼓励智能体用尽量少的跳数把数据包送达到目标同时又不能让它在没有任何反馈时束手无策。一个比较标准的奖励形式是当数据包到达目的节点时reward 1每经过一个转发节点时reward -0.1 或一个很小的负值如果出现丢包或者到达不可达的邻居reward -1这种设计背后的直觉是如果只给到达奖励那么一条 3 跳路径和一条 10 跳路径对于智能体来说没有差异它没有动机去优化路径长度而带上逐跳惩罚之后路径越短累计奖励越高智能体自然会被引导到最少跳数的策略上。实际效果也确实不错训练后大多数数据包都会走最短路径而不是随机乱转。我在做自定义场景时通常还会加一个“队列长度”相关的惩罚项。因为网络仿真里如果只考虑跳数智能体可能会把所有包都发给同一个“看起来很优”的邻居导致那个节点瞬间拥塞。这种情况在真实网络里也很常见单纯看跳数优化一定会踩到拥塞的坑。你可以在 reward 里减去当前节点或下一跳节点的拥塞队列长度或者设置一个阈值当下一跳队列过长时哪怕路径不是最短也强烈建议绕行。奖励设计没有标准答案但你一定要能解释清楚“这个式子里的每一项到底在鼓励什么”这样调参时才不会手足无措。4. Python 侧训练逻辑从 gym.Env 到 DQN 的训练闭环4.1 训练脚本的整体结构Python 侧的训练脚本一般会同时承担三件事把 ns-3 暴露出来的远端环境封装成 gym.Env 的样子、构建强化学习智能体、执行训练循环。很多第一次接触 ns3-gym 的人会误以为它跟普通 gym 环境一样用现成的 stable-baselines 之类的库直接训练就行。但实际用起来你会发现因为远端环境的步进要和仿真器的事件推进同步所以直接套用通用强化学习框架时经常会出现步进阻塞、环境重置失败、状态空间不匹配等奇怪问题。官方示例自己写 DQL并不是因为它“高级”而是为了把依赖降到最低同时让你把每一步交互看得清清楚楚。linear-mesh 的 Python 训练脚本结构大概是这样的一个 LinearMeshEnv 类负责与远端交互并暴露 reset 和 step一个 Buffer 类负责存储经验数据一个 DQL 类封装神经网络和训练更新逻辑最后是主训练循环不断跑 episode每跑若干步做一次梯度下降。class LinearMeshEnv(gym.Env): def __init__(self, node_id): super(LinearMeshEnv, self).__init__() self.node_id node_id # 这里会建立与 ns-3 环境的连接 self.env gym.make(...) def reset(self): return self.env.reset() def step(self, action): obs, reward, done, info self.env.step(action) return obs, reward, done, info核心关键点在于这里每个节点对应一个独立的 env 实例而不是所有节点共享同一个 env。运行时一个 Python 进程里会同时维护多个 env 句柄每当某个节点上报状态时对应的 env 就被唤醒一次完成一次 step。这种多句柄的管理方式是需要你在自己的场景里认真复用的因为它直接影响你后续能不能做多智能体扩展。4.2 经验回放与 DQN 更新DQN 部分的代码逻辑和其他强化学习场景大同小异用一个 Q 网络来预测每个动作的价值用经验回放来打破数据之间的相关性用目标网络来稳定训练目标。# 训练循环中的核心片段 obs env.reset() for step in range(max_steps): action agent.choose_action(obs) next_obs, reward, done, info env.step(action) buffer.add(obs, action, reward, next_obs, done) if len(buffer) batch_size: batch buffer.sample(batch_size) agent.update(batch) obs next_obs训练里几个参数值得关注。batch_size 一般取 32 或者 64太小会让梯度噪声大太大又会让单次更新变慢。gamma 通常设在 0.9 到 0.99 之间因为在这个场景下未来奖励会随着跳数衰减一个偏低的 gamma 反而更容易让智能体聚焦到短期收益。epsilon 从 0.1 或者 0.5 开始逐步衰减前期多探索、后期多利用。learning rate 建议先尝试 1e-4 到 1e-3Adam 优化器在这里的表现通常好于随机梯度下降。一个我踩过很多次的坑是经验缓冲区的容量设定。如果你把 buffer 设得太小比如几千条那么在多智能体同时交互的情况下很快就会被写满然后旧经验被弹出训练样本分布变得非常不稳定设得太大呢又会发现学习速度特别慢因为新样本在批量采样中的占比太低。线性网格这个小场景里buffer 容量可以设成 20000 左右大部分情况下已经够用。你可以观察训练早期的 loss 曲线如果 loss 剧烈震荡或者长时间不下降优先检查 buffer 的容量和 alpha 衰减节奏。4.3 怎么判断训练是否有效训练脚本的输出一般会打印当前 episode 的平均 reward、每条链路的转发总数、到达目的地的包数等。最直接有效的观察方式是看“平均 reward 随 episode 是否在上升”以及“随机策略下的 baseline 是多少”的对比。我习惯先把某个固定路由策略比如最短路径或者随机路由在同一个拓扑上的表现跑出来当作 baseline。然后让 DQN 训练若干轮观察智能体的平均 reward 是否超过 baseline以及“实际转发路径是否在向最短路径逼近”。如果网络里明明存在 3 跳路径但训练之后智能体还在走 6 跳路径那说明 reward 设计或者状态表达里缺少了对“路径长度”的感知或者说 epsilon 衰减速度太快智能体根本没有来得及探索到那条短路径。另外一个容易被忽视的指标是“目标网络更新频率”。如果目标网络更新太频繁DQN 的训练目标本身就在不断移动loss 很容易发散更新太慢则学习进度迟缓。linear-mesh 这种小规模场景一般用“每训练 N 步复制一次主网络参数到目标网络”N 在 100 到 1000 之间都是合理的但你需要通过实验找到适合自己环境的值。5. 仿真器与智能体握手ns3-gym 的通信机制拆解5.1 握手组件与消息格式ns3-gym 的交互从底层看就是进程间通信。C 侧暴露一个 OpenGymInterface 对象Python 侧启动一个服务端节点两端通过 ZMQ 通信。ns-3 仿真器作为客户端在启动时会向 Python 端发起请求请求里包含当前仿真进程想要创建多少个 Gym 环境、每个环境的状态空间和动作空间定义Python 端收到请求后为每个环境分配一个实例并持有对应的套接字连接。当 Python 执行 env.reset 时实际上发送的是一个“SimGetObservation”类型的请求请求体里带着环境 IDns-3 侧收到请求后会调用对应环境的获取状态函数把状态打包成 protobuf 消息返回。Python 再根据返回的向量重建 numpy 数组。同理env.step(action) 发送的是“SimSetAction”加“SimAdvanceSim”的请求ns-3 侧执行动作并推进仿真时间然后返回新的观测和奖励。理解这套消息流程的意义在于你能一眼看出什么操作特别昂贵。比如频繁调用 env.step 意味着频繁的 ZMQ 消息往返这会严重拖慢整体仿真速度。所以你经常会看到 ns3-gym 示例里的智能体决策频率被刻意降低比如每隔几个仿真时间片才做一次转发决策。这不是算法限制而是通信机制的现实约束。5.2 完整的交互时序一次标准的交互时序可以这样梳理仿真器启动创建线性拓扑每个节点构造自己的 MeshNetEnvironment并调用 OpenGymInterface 的注册函数把环境信息推给 Python 端。Python 端在 모든环境注册完成后开始训练循环。训练循环开始时Python 端先对所有环境执行 reset。ns-3 侧收到 reset 请求后会在仿真器事件队列里插入一个“收集状态”的任务。比如当前节点可能刚收到一个数据包那么事件回调会更新节点的转发状态并准备观测向量。随后 ns-3 侧把观测返回给 Python 端。Python 端拿到观测后通过神经网络计算动作再调用 step。这个 step 请求里带着当前环境 ID 和动作值。ns-3 侧解析动作设置数据包的下一跳接口继续推进仿真。等下一个事件触发时节点状态再次变化环境又会生成新的观测。如此循环往复直到数据包到达目的节点或者被丢弃这个 episode 结束Python 端再发起 reset 开启下一轮。这段流程里的一个关键设计是ns-3 侧对 Python 请求的处理是异步的也就是说并不是 ns-3 每跑一个事件就要卡住等 Python。只有当环境明确调用 GetActionFromAgent 并等待动作返回时仿真才会停下来等待。这就意味着你可以在一个 ns-3 仿真过程中让网络事件尽量多跑一会儿或者在某几个关键时间点才停下来做决策从而大幅降低通信开销。5.3 一个数据包的旅程结合 linear-mesh 的场景来看数据包的旅程可以描述成“到达一个节点 - 触发环境状态更新 - 上报 Python - 等待动作 - 按动作转发到下一跳 - 下一跳继续这个过程”。对每个中间节点来说它只负责一个局部决策这个包应该往哪个邻居送。源节点负责把包注入网络目的节点负责确认送达。这种逐跳决策模式和集中式路由选择完全不同。集中式路由可以拿到全局拓扑和全局队列信息一次性算出一条端到端路径。而 linear-mesh 里的智能体只能看到自己当前节点和有限邻居的信息它必须学会“只看局部也能做出好的转发选择”。你会发现经过足够多轮训练后智能体并不需要全局信息也能自发地形成一个从源到目的的“涌现路径”这恰恰是多智能体路由让人着迷的地方。从工程实现角度看你可以把这种逐跳决策理解成一个流水线而 ns-3 的事件队列就是流水线的传送带。每当一个包进入节点就会触发一次环境通知环境又向 Python 请求动作动作返回后包被送到下一跳。任何一环处理过慢都会让仿真速度明显下降。所以当你想放大拓扑规模时首先要考虑的不是算法复杂度而是通信和事件处理的开销。6. 常见问题排查与实战调参心得6.1 启动阶段的典型问题这一节我把我实际遇到过的启动期问题整理成表格方便你对照排查。现象可能原因解决办法Python 端一直等待连接ns-3 程序没有先启动或者 Python 端监听端口不一致先启动 Python 脚本再启动 waf 仿真程序检查端口配置是否一致ns-3 端提示连接失败ZMQ 服务端口被占用或防火墙拦截杀掉占用端口的进程或者换一个闲置端口观测维度报错C 侧设置的状态空间与 Python 侧解析不一致检查 SetObservationSpace 和 gym 环境里的空间定义确保一致TensorFlow 版本报错官方脚本是 TensorFlow 1.x 写法环境装的是 2.x单独建 Python 3.6 环境安装 TensorFlow 1.15 重跑二进制找不到编译时没有启用 examples重新 configure 并加上 --enable-examples然后重编译程序跑起来后没有任何 Python 输出Python 代码被阻塞在等待连接阶段检查是否有多个 Python 环境实例混用了不同端口如果你跑了几分钟还没有任何输出最快捷的定位方法是打开任务管理器或者 top 命令看两个进程是否都在活跃运行。如果 ns-3 进程 CPU 使用率很低且一直卡住多半是它在等待 Python 端的回复如果 Python 进程占用率极低且同样卡住那就要考虑是不是消息格式或者端口配置上出了问题。6.2 训练不收敛的排查思路多智能体强化学习场景下训练不收敛是我遇到最多、也最让人头疼的问题。它不像普通深度学习那样 tune 一下 learning rate 就能解决因为不收敛的根源往往不在神经网络本身而在“环境和智能体的交互回路”上。第一件事检查随机策略下的 baseline。先把 DQN 里的 epsilon 固定在 1.0也就是完全随机选择动作跑若干个 episode计算平均 reward。如果随机策略的平均 reward 本身就是负得很离谱说明 reward 设计或者状态表达里可能存在严重问题。比如每跳惩罚太大几步之后累计奖励就变成很大的负数训练一开始就处于“无论怎么选都惩罚”的状态智能体很难学到有效信号。正确的做法是让随机策略平均来说至少不亏太多也就是说随机转发时虽然可能绕路但不会频繁触发严重惩罚。第二件事单步调试观测。在 Python 端打印每一步 env.step 收到的观测确认这些数字的物理含义跟你预期的一致。我遇到过一种情况C 侧想用“当前节点 ID”作为观测的一部分但因为变量类型问题Python 端收到的是一个没有归一化的超大整数导致神经网络输入分布严重不平衡。类似的问题如果不打日志光看 loss 曲线很难发现。第三件事检查动作是否真的影响了仿真结果。如果你的智能体明明输出了动作但 ns-3 侧解析动作后没有改变任何转发决策那么这个“动作”对于环境来说就是无效的训练也不可能收敛。这种问题在自定义动作映射时特别容易发生比如动作空间明明有 5 个值但 C 侧只处理了前 2 个其余全部走了默认分支。先把“动作改变 - 路径改变 - 奖励改变”这条链路拉通再谈算法调优。6.3 调参心得先改奖励再改网络我自己的经验是当训练效果不理想时优先调整奖励函数其次调整状态表达最后才调神经网络的超参数。神经网络参数的普适性很强在一个小规模拓扑上调来调去很可能只是过拟合了当前场景反而是奖励函数和状态表达直接决定了智能体能不能从环境里提取到可用信号。具体到 linear-mesh如果你发现智能体总是绕路试着加大逐跳惩罚的权重如果发现它总是把包发给同一个邻居试着在状态里加入“邻居队列长度”或者“上次转发失败的标记”如果你发现训练前期 loss 剧烈震荡试着降低 learning rate或者把 epsilon 衰减速度调到更慢给智能体更多探索空间。另外多智能体场景还有一个隐藏的“非平稳性”问题每个智能体的策略都在随时间变化一个智能体的策略变化会改变另一个智能体遭遇的状态分布。这也是为什么你常常会感觉多智能体训练比单智能体难收敛得多。如果你后续想在这个方向上深入建议尽早引入“参数共享 集中训练分布执行”这种折中方案它能明显缓解非平稳性带来的训练不稳定性。6.4 一个很容易忽略的暗坑陈旧状态最后分享一个我自己踩了很久的暗坑陈旧状态。当 ns-3 仿真器因为某些原因没有及时触发环境更新Python 端拿到的观测可能还是上一个时间点的旧数据。在 linear-mesh 这种事件驱动的宽松场景下这个问题偶尔出现一旦出现智能体就会在错误的观测下做出动作进而产生完全错误的经验样本。定位这个问题的办法是在 Python 端记录每个观测对应的时间戳或者仿真步数检查 step 之后返回的观测是否确实来自“动作对应的那一步”。如果发现观测变化跟不上动作执行的节奏那多半是 C 侧的事件调度有问题比如状态更新事件没有挂在正确的节点上或者环境通知函数被调用的时机不对。修好调度逻辑再去训练你会发现同样的超参数下收敛速度和稳定性都会有明显提升。如果你已经顺利跑通了 linear-mesh并且能改得动状态、动作和奖励那么 ns3-gym 对你就不会再有什么黑盒了。之后再去尝试更复杂的拓扑、更复杂的算法或者引入多智能体协作机制也会从容很多。
返回列表