ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

无人机辅助MEC计算卸载的DDPG实现:从建模到避坑全记录

无人机辅助MEC计算卸载的DDPG实现:从建模到避坑全记录 简介一套基于深度确定性策略梯度DDPG的无人机辅助移动边缘计算卸载优化Python代码面向计算机、电子信息、数学等专业学生适用于课程设计、期末大作业与毕业设计等场景也便于研究者快速验证边缘计算决策算法。代码采用参数化编程关键参数可灵活调整注释清晰并附可直接运行的案例数据方便从零复现算法流程。压缩包共17个文件以16个Python脚本为主涵盖DDPG主程序、Actor-Critic网络、DQN对比实现以及本地/边缘/无人机计算模式等模块另有1个Markdown说明文档整体仅45KB结构简洁、目录层级清晰。目前已有147人学习下载。通过这套代码可完整观察计算卸载动态决策全过程对比DDPG与DQN的策略差异并修改网络结构、超参数及场景配置以适配自身任务从而深入理解强化学习与移动边缘计算的结合方式。1. 无人机辅助MEC的计算卸载优化为什么 DDPG 成了默认解在无人机辅助移动边缘计算MEC仿真里计算卸载是一个绕不开的优化问题地面用户算力不够任务要么本地跑要么把一部分卸载到头顶的无人机边缘服务器上。麻烦的是这个优化要在每个时隙做一次而且卸载比例是连续的——0.3 还是 0.7在线决定。传统数学规划在这里被非凸约束和动态信道卡住而 DQN 这类离散动作算法又没法输出连续卸载比。深度确定性策略梯度DDPG正好补上这个位置Actor 网络直接输出连续动作Critic 网络评估决策质量配合经验回放和软更新让策略在仿真环境里一步步收敛。这篇文章面向要复现、要跑基线、要调参的从业者讲清楚怎么把 DDPG 落到无人机 MEC 卸载仿真的 Python 代码里以及哪些坑会在路上一一等着你。2. 问题建模卸载决策为什么难难点在哪几个维度2.1 系统模型无人机、用户、任务三者怎么在仿真里对话先要把场景固定下来。最常见的系统模型是一架装有边缘服务器的无人机在固定高度 H 飞行或悬停服务下方 N 个地面用户每个用户设备有一定本地算力但不足以低时延处理全部任务。每个时隙 t每个用户 i 会产生一个计算任务大小用 L_i(t) 表示。用户可以选择把其中 x_i(t) 比例的任务量通过上行链路发送给无人机剩下的 1-x_i(t) 在本地执行。无人机收到任务后用自己的边缘算力计算再把结果下传。由于下行结果数据量通常远小于任务本身绝大多数文献把下行时延当作固定小开销处理仿真里我也这样简化重点只算上行传输和边缘计算这两大块。仿真环境的参数表是复现的第一步我习惯按下面这组取值起步参数典型取值说明无人机高度 H100 m太低有遮挡太高信道损耗大用户数量 N4~8超过 10 后状态维度明显上升任务大小 L_i100~500 KB指数分布或均匀分布采样用户本地算力0.5 GHz手机级别算力无人机边缘算力5 GHz一个数量级差距足够体现卸载价值信道带宽 B5 MHz空对地链路的典型带宽路径损耗指数2~3.5按城市或郊区环境设定这几个参数决定了问题的难度算力差距越大卸载收益越明显任务越大传输时延占比越高盲目全卸载反而不划算。我在复现论文时发现很多新手一上来就把无人机算力设成用户的上百倍结果策略无脑全卸载也能拿高分根本凸显不出优化的意义。算力差距控制在 10 倍左右才能逼着策略去权衡传输时延与计算时延。2.2 优化目标与约束时延、能耗、无人机能量预算怎么取舍卸载一个比例为 x 的任务用户侧的时延由两部分组成本地计算用时和“传输 边缘计算”用时。本地和边缘是并行执行的所以用户 i 的总时延近似取两者较大者再加上上行队列排队时间。能耗则分开算用户能耗包括本地计算能耗和上传能耗无人机能耗主要是边缘计算能耗。飞行能耗可以加进去也可以当作固定值取决于论文的主题是纯卸载优化还是卸载与轨迹联合优化——我一般先拿固定值跳过飞行能耗把问题聚焦在卸载决策上。目标函数可以写成每个时隙内所有用户的时延和能耗的加权和权重因子直接决定策略的脾气。w_e 调大策略会偏向低能耗路径卸载比例给得保守w_t 调大策略会追求低时延把更多任务推给无人机。这个权重在训练前就必须定下来中途不要改——经验回放里存着旧奖励分布换了权重等于拿新旧两套标准在训同一个策略训练曲线必然乱跳。约束有三类卸载比例在 [0,1] 之间无人机接收的总任务量不能超过它的计算容量无人机每个时隙的能耗不能超过电池预算。前两个约束好办第三个在 DDPG 里比较别扭因为 Actor 输出的是连续值硬约束没法直接套。常见做法是把超预算做成奖励里的负向惩罚项让策略自己学会远离危险区而不是在代码里强制截断。提示权重因子 w_t / w_e 在训练前就要定好。中途调整会让经验回放里的旧样本与新奖励口径不一致训练基本没法收敛。先把一组权重跑通再扫不同权重做对比实验。2.3 为什么传统优化方法在这里失灵组合爆炸与动态信道如果卸载比例是离散的 0 或 1问题还能交给整数规划或者分支定界硬算但改成本文要处理的连续比例后再加上任务随机到达、信道随无人机位置实时变化问题就变成了非凸、时变的在线决策。每个时隙重跑一次优化器非常不现实——求解时间很可能超过时隙长度本身决策还没出来环境已经变了。传统办法里贪心算法和遗传算法都有人试过。贪心的问题在于只看当前时隙不考虑电量长期可持续性早期多卸载几轮后面无人机没电了整体性能断崖下跌遗传算法每时隙都要跑一个种群的迭代几毫秒的时隙根本扛不住。深度强化学习把决策收敛到神经网络的一次前向传播里推理时间从秒级压到毫秒级这是它在无人机 MEC 场景里逐渐成为默认解的根本原因。你复现时拿传统优化方法当离线基线没问题但一旦场景要求在线决策DDPG 在推理速度和自适应能力上的优势就压不住了。3. 从理论到 PythonDDPG 怎么处理连续卸载比3.1 Actor-Critic 框架与 MEC 卸载场景的对应DDPG 是 Actor-Critic 家族里面向连续动作的代表。Actor 网络输入当前状态 s直接输出一个确定性动作 a也就是 N 个用户的卸载比例向量Critic 网络输入状态加动作输出一个 Q 值表示“当前状态下采取这组卸载比例的长期累积奖励期望”。训练的本质就是让 Actor 沿着 Critic 给出的梯度方向不断修正卸载比例同时让 Critic 自己越来越准确地估计长期回报。对应到 MEC 场景状态 s 就是任务信息、用户位置、无人机位置和剩余电量拼成的向量动作 a 就是连续卸载比例向量维度等于用户数。相比 DQNDDPG 多了一套目标网络目标 Actor 和目标 Critic。它们不直接参与训练只用来计算时序差分里的目标 Q 值并通过软更新缓慢同步主网络的权重避免 Q 值估计剧烈波动。这个机制在卸载场景里特别关键——因为奖励里既包含传输时延又包含能耗尺度差异大没有目标网络缓冲Critic 很容易震荡。有人会问PPO 也能做连续动作为什么不直接用 PPO差别主要在样本效率和计算开销DDPG 是 off-policy一次交互产生的数据可以反复从缓冲区采样学习在仿真环境里每一步都是成本这个复用能力很值钱PPO 是 on-policy需要不断采集新数据想收敛通常要跑更多仿真步数。如果你的实验环境能并行开几百个模拟器PPO 也能接受但单无人机多用户的 MEC 场景往往不具备这种并行条件DDPG 更顺手。另外 DDPG 的 Critic 会产出 Q 值曲线这个曲线在调试奖励设计时很有参考价值PPO 里没有这么直观的诊断信号。3.2 设计一个动态计算卸载层状态、动作与奖励函数怎么定才稳把状态、动作、奖励封装成强化学习接口时我一般会把状态向量定义为每个用户的任务大小 L_i、用户横纵坐标、无人机当前坐标、无人机剩余电量以及上一时隙的卸载比例。最后一维很关键它给了策略一定的时间连续性让卸载比例不会在相邻时隙间无理由地剧烈跳变。如果无人机轨迹是由无人机路径规划模块给出的固定航点状态里可以不放完整轨迹只放当前目标点如果卸载和轨迹要联合优化再把下一航点坐标拼进状态维度对应增加。动作空间是 N 维连续向量每个分量是一个用户的卸载比例边界是 [0,1]。实现时输出层先用 tanh 把数值压到 [-1,1]再线性映射到 [0,1]。注意 tanh 接近 ±1 时会进入饱和区梯度近乎为零训练后期如果策略偏向全卸载或全本地更新速度会明显变慢这个坑在第 5 章详细展开。奖励函数决定策略往哪个方向收敛。我的做法是reward -(α × 归一化时延 (1-α) × 归一化能耗) - 约束惩罚归一化是指把当前时隙的总时延和总能耗分别除以“全部本地计算”这个基线值让两者都落在相近量级。这个步骤不能省——时延以毫秒计、能耗以焦耳计不归一化的话数值大的那一项会彻底淹没另一项策略等于只优化一个目标。约束惩罚包括无人机电量超预算给 -1、传输失败给 -2幅度要比主奖励大让策略明显感受到不能触碰红线。这样封装后外界看 DDPG 的输入输出就是一个“动态计算卸载层”喂状态出卸载比拿奖励。3.3 训练流程与关键超参数学习率、软更新率、探索噪声怎么选DDPG 训练循环的骨架固定得很死采集经验放进回放缓冲区随机采样一个小批量更新 Critic再用 Critic 的梯度更新 Actor最后软更新目标网络。每一步都不复杂但超参数直接决定收敛成败。我常用的初始参数如下超参数初始值作用Actor 学习率1e-4控制策略网络步长太大易震荡Critic 学习率1e-3让 Q 值先快速准确折扣因子 γ0.99长期奖励的折算权重软更新系数 τ0.001目标网络同步速度经验回放容量100000决定样本多样性批量大小64每次更新使用的样本量探索噪声 σ0.1→0.01前期探索、后期收敛Critic 学习率比 Actor 高一个数量级因为 Critic 必须先学会准确的 Q 值Actor 才能拿到正确的梯度方向。τ0.001 意味着目标网络每次只向主网络移动 0.1%这个慢速同步是防 Q 值发散的关键。探索噪声我习惯用高斯噪声给 Actor 的输出加上标准差为 σ 的扰动随训练步数衰减OU 噪声虽然时序相关性强适合平滑动作的场景但卸载比例的相邻跳变本来就被允许高斯噪声更好调。训练的前几千步基本是黑匣子状态你只能盯着损失曲线猜问题等第 4 章代码跑通后再回头调这些超参数。4. 跑通最小可复现项目网络结构与核心代码解析4.1 Actor 与 Critic 网络结构定义层数、激活与输出边界这部分是方案的主干PyTorch 是这个方向最常见的实现框架。先定义两个网络import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): Actor网络状态 - 连续卸载比例向量 def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.fc1 nn.Linear(state_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc3 nn.Linear(hidden, action_dim) def forward(self, state): x F.relu(self.fc1(state)) x F.relu(self.fc2(x)) # tanh压缩到[-1,1]再映射到[0,1]作为卸载比例 out torch.tanh(self.fc3(x)) return (out 1) / 2.0 class Critic(nn.Module): Critic网络状态动作 - Q值 def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.fc1 nn.Linear(state_dim action_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc3 nn.Linear(hidden, 1) def forward(self, state, action): # 状态和动作拼接后一起输入 x torch.cat([state, action], dim-1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)逻辑说明Actor 的输入维度是状态向量长度 state_dim输出维度等于用户数量 N中间两个隐藏层各 256 个神经元。隐藏层数量不是越多越好MEC 状态复杂度用两到三层全连接就够再加层数容易过拟合到训练环境。Critic 的输入要把状态和动作拼接在一起这是 Actor-Critic 的标准做法因为 Q 值本身依赖具体动作不拼接 Critic 就不知道自己在评估哪一组卸载比例。参数说明hidden256 是常用默认值但用户数少于 4 时可以降到 128 加速收敛动作输出先用 tanh 压到 [-1,1]再加 1 除以 2 映射到 [0,1]这个线性变换在输出层完成不用额外加约束层。注意如果发现两个隐藏层仍然过拟合验证集先把 hidden 从 256 降到 128 重试而不是加正则项。这个场景下正则化收效甚微把网络缩小是最直接的惩罚。4.2 经验回放与软更新两个让训练不崩的细节经验回放和软更新是 DDPG 里两个专门防崩的模块。经验回放把交互产生的 (state, action, reward, next_state, done) 存进缓冲区训练时随机采样破坏样本间的时间相关性软更新则是让目标网络缓慢追踪主网络稳定训练过程。import random from collections import deque class ReplayBuffer: def __init__(self, capacity100000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states torch.FloatTensor([b[0] for b in batch]) actions torch.FloatTensor([b[1] for b in batch]) rewards torch.FloatTensor([b[2] for b in batch]) next_states torch.FloatTensor([b[3] for b in batch]) dones torch.FloatTensor([b[4] for b in batch]) return states, actions, rewards, next_states, dones def __len__(self): return len(self.buffer) def soft_update(target, source, tau): 软更新target参数缓慢向source靠拢 for target_param, source_param in zip(target.parameters(), source.parameters()): target_param.data.copy_(tau * source_param.data (1.0 - tau) * target_param.data)逻辑说明ReplayBuffer 用 deque 实现容量到上限后自动丢弃最老的样本保证训练用的总是近期经验。batch_size 决定每次更新的样本量取 64 是稳定性和更新频率的折中取太小梯度噪声大取太大又拖慢每步更新速度。实际工程里通常只建一个缓冲区主网络和目标网络共用同一批采样结果做更新。参数说明soft_update 的 tau 传入 0.001表示目标网络参数每次只移动 0.1%。zip 遍历要求两个网络的参数结构和形状完全一致否则运行时会直接报形状不匹配——这是新手复现 DDPG 常见的翻车点定义目标网络时务必和主网络用完全相同的类实例化。4.3 完整训练循环环境交互、目标网络更新与收敛判断把网络和缓冲区拼起来的训练循环是这个方案的主干也是大多数人最容易写乱的部分import numpy as np def train_episode(env, actor, critic, actor_target, critic_target, buffer, optimizer_actor, optimizer_critic, args): state env.reset() episode_reward 0 for t in range(args.max_steps): # 1. 用当前策略选动作加探索噪声 state_tensor torch.FloatTensor(state).unsqueeze(0) action actor(state_tensor).detach().numpy()[0] noise np.random.normal(0, args.noise_std, sizeaction.shape) action np.clip(action noise, 0.0, 1.0) # 2. 环境执行卸载返回下一状态和奖励 next_state, reward, done, _ env.step(action) buffer.push(state, action, reward, next_state, done) episode_reward reward # 3. 缓冲区样本足够后开始训练 if len(buffer) args.batch_size: states, actions, rewards, next_states, dones buffer.sample(args.batch_size) # 4. 更新Critic最小化TD误差 with torch.no_grad(): next_actions actor_target(next_states) target_q rewards args.gamma * critic_target(next_states, next_actions) * (1 - dones) current_q critic(states, actions) critic_loss F.mse_loss(current_q, target_q) optimizer_critic.zero_grad() critic_loss.backward() optimizer_critic.step() # 5. 更新Actor最大化Q值 actor_loss -critic(states, actor(states)).mean() optimizer_actor.zero_grad() actor_loss.backward() optimizer_actor.step() # 6. 软更新目标网络 soft_update(actor_target, actor, args.tau) soft_update(critic_target, critic, args.tau) state next_state if done: break return episode_reward逻辑说明主循环每一步都按“采集-存储-采样-更新”的顺序走。更新 Critic 时用目标网络计算下一状态的 Q 值作为回归目标外面包一层 no_grad()是因为目标网络不参与本轮梯度反传只负责提供一个稳定的目标值。Actor 的损失写成-critic(states, actor(states)).mean()含义是把采样状态输入 Actor 得到动作再用 Critic 评估这批动作的 Q 值Q 值越高损失越低从而推动 Actor 不断朝高价值动作方向调整。参数说明noise_std 初始给 0.1每 1000 步线性衰减到 0.01gamma 取 0.99 让长期奖励权重适中。dones 乘进 TD 目标里是因为任务终止后没有下一时隙状态不能让死胡同的价值错误传播给前面的状态。env 需要自己在外面实现成标准 Gym 接口把动作转成实际卸载比例、计算时延和能耗再生成奖励。没有现成环境时先写一个简单数学模拟器验证网络本身没 bug再接完整环境否则定位问题时会同时怀疑网络和环境两头。5. 无人机 MEC 卸载优化的避坑记录5 个我踩过的坑5.1 训练初期 Q 值爆炸奖励没归一化是根源现象训练到几百步时 critic loss 突然跳到几十万Actor 输出的卸载比例跑向两个极端要么全是 0 要么全是 1奖励曲线急剧下坠。这是我最早复现时吃过的亏先讲它是因为受害面最大。原因奖励数值没有归一化。时延以毫秒为单位能耗以焦耳为单位两者数量级差距一大Q 值在时序差分里就一路累加梯度被放大最后直接溢出。解决对奖励做归一化把每个时隙的时延和能耗分别除以“全部本地执行”这个基线值再按权重相加保证奖励量级落在 [-1,0] 附近。如果归一化后仍不稳定把 Critic 学习率从 1e-3 降到 3e-4 再跑一轮基本能压住。5.2 策略收敛到全卸载奖励塑形把中间地带逼没了现象训练一万步后策略学成了不管什么任务都把卸载比例设成接近 1本地算力闲置奖励曲线看着收敛了实际上停在一个偷懒的方案上。原因奖励塑形里无人机算力带来的时延优势太大卸载几乎总是正收益策略没有动力探索“部分卸载”这个中间地带。奖励函数没有给中等卸载比例相应的平滑收益Actor 干脆一把梭。解决在奖励里加一个随任务大小超线性增长的能耗项卸载越多能耗增速越快逼策略在时延收益和能耗代价之间找平衡。之后再观察卸载比例分布应该能看到不同任务大小下有梯度反应而不是一个常数。5.3 评估比训练差一大截探索噪声没关对比全白做现象训练跑完把 Actor 切成无探索模式去评估平均奖励比训练时低 20% 以上。我一度以为是模型没收敛后来发现是评估方式的问题。原因训练时的奖励曲线本身包含探索噪声带来的随机波动评估时去掉噪声后策略在边界区域的表现缺陷暴露出来。另外如果评估阶段没有固定随机种子评估和训练的状态序列路径完全不同两组数字根本没有可比性。解决评估前固定随机种子关闭所有探索噪声每个环境测试多次取平均而且用同一组初始状态集合去跑训练评估对比。这样出来的数字才诚实也能暴露出模型是否过拟合了训练分布。5.4 信道模型和无人机移动模型对不上几何关系必须一致现象把用户坐标画出来后发现信道增益分布完全不符合无人机在头顶绕圈的预期某些位置距离很近但信道差得离谱。原因复现时把自由空间路径损耗当成信道模型忽略了无人机高度、遮挡和地面反射的联合效应。更隐蔽的是无人机的移动轨迹在环境里是按直线或正弦波写的信道却按悬停假设计算两个模型用的是两套坐标假设。解决路径损耗改成对数距离模型加一个与无人机高度和建筑物密度相关的阴影衰落项轨迹和信道必须来自同一个几何关系——高度 H 固定时水平距离 r 的计算公式只有一套不要在环境里各写各的。5.5 换机器或换种子结果对不上随机种子和依赖版本都要锁死现象同一份代码在一台机器上收敛换一台机器跑同样的随机种子却发散。实验室的“复现成功”到宿舍就变成翻车现场。原因PyTorch 在 GPU 上的非确定性操作、CPU 浮点运算顺序差异、操作系统线程调度都会让训练路径产生偏差。只设一个 numpy 种子远远不够三套随机源必须全锁。解决torch.manual_seed、numpy.random.seed、random.seed 三处全设训练时关闭 cudnn 的 benchmark 模式设置 torch.backends.cudnn.deterministicTrue环境里的任务随机生成也要纳入同一套种子体系。最终结果至少跑 3 个种子取均值单种子是玄学三种子取均值才有资格写进对比表。另外把 torch 和 numpy 的版本号记录在 requirements 里半年后回看才知道当前结果是在哪一版依赖下跑出来的。6. 验证与进阶你的 DDPG 卸载策略比基线强多少才算真的强6.1 基线设计本地计算、全部卸载、随机卸载怎么选完成训练只是第一步没有基线对比收敛曲线只是好看的数字。我一般会跑四个基线全部本地计算、全部卸载到无人机、随机卸载比例、以及按信道增益贪心选择卸载比例的启发式。前两个给出性能上下界随机卸载证明问题不是随机策略能搞定的启发式用来说明 DDPG 比人工规则好在哪里。横轴用训练步数纵轴用平均累积奖励四条线画在同一张图里。DDPG 最终应该压过随机和启发式逼近甚至超过边界值——如果比全卸载还差先查奖励塑形多半是能耗权重设太高而不是算法本身的问题。6.2 收敛曲线怎么看平均奖励、任务成功率、能耗三个视角只看奖励曲线容易自欺欺人。我习惯把训练过程中的三个指标拆开记录任务在截止时间内的完成率、单位时隙的无人机能耗、系统总时延。奖励曲线平稳但任务完成率低说明时延权重太弱能耗曲线稳步下降但时延不变说明能耗权重过高。三个指标里至少两个同时往好的方向走才能确认策略真的学会了权衡。评估时把三个指标的方差也打出来方差过大说明策略鲁棒性差换一个初始位置就可能崩掉。6.3 换场景迁移用户变多、任务到达率变高要不要重训一个常见的误用是训练时用 4 个用户评估时直接上 10 个用户然后断言 DDPG 失效。这不是失效是状态维度变了网络输入层需要重新设计。我一般这样处理训练环境保持 4 到 6 个用户评估环境只改变用户位置、任务到达率和信道种子保持维度一致要做用户数量扩展实验就单独训一个适配 10 用户的新模型别指望同一个网络跨维度泛化。任务到达率变高时先拿旧模型跑几步看奖励掉多少掉得不多说明策略有迁移能力掉得明显就把高到达率的样本混进经验回放里继续训练做增量微调而不是从零重训。要说教训我最深的一条是千万不要拿训练过程的奖励曲线直接当结论。有一版我把探索噪声衰减调快了训练奖励看着漂亮评估时卸载比例却卡在 0.99后来加装评估模式并固定随机种子重新跑才拿到真实可用的数字。我的习惯是先写评估脚本再写训练脚本评估能跑通才允许训练这个顺序帮我挡住了很多次自我安慰。如果你准备深入这个方向下一步可以试试把策略部署到凤凰无人机模拟器里做硬件在环验证用真实飞控数据修正信道模型——那一步完成这套卸载优化才算真正闭环。希望这篇避坑记录能帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进