简介:本资源面向通信、边缘计算与强化学习方向的学生及研究人员,提供一套基于无人机辅助移动边缘计算(UAV-MEC)的计算卸载优化完整Python实现,核心采用深度确定性策略梯度(DDPG)算法,并附带DQN、Actor-Critic等对比方案,适合毕业设计、课程大作业及科研复现使用。压缩包共17个文件,以16个py源码与1个md说明文档为主,整体约44KB,涵盖UAV_env.py环境建模、ddpg_algo.py算法实现、state_normalization.py状态归一化及Edge_only、Local_only等基线脚本,代码注释详尽,新手也能读懂。目前已有130人学习下载。读者可据此掌握无人机轨迹与卸载决策的联合优化思路,理解DDPG在连续动作空间中的训练流程,并借助对比实验快速搭建自己的仿真环境,具备较高的实际应用与二次开发价值。
1. 无人机辅助边缘计算卸载:这套 DDPG 源码到底能跑出什么
做无人机辅助移动边缘计算方向的同学,大概率都卡在同一个地方:论文里的系统模型看懂了,公式推导也跟下来了,但真要自己写一套能收敛、能出对比曲线的计算卸载仿真,就无从下手。这套源码解决的正是这个断层——它把「无人机 + 移动边缘计算 + 计算卸载」这个组合场景,用深度确定性策略梯度(DDPG)完整实现了一遍,并且把 Edge_only、Local_only、DQN、DDPG 四组基线都摆在一起,方便你直接跑出对比结果。它适合正在做毕设、课程设计或期末大作业的本科生和低年级研究生,也适合刚接触强化学习、想找一个连续动作空间实战案例的工程师。整套代码是纯 Python,环境用 Gym 风格封装,算法文件独立,注释密度对新手友好,部署门槛不高。
2. 系统模型与算法选型:为什么是 DDPG 而不是 DQN
2.1 无人机辅助 MEC 的计算卸载问题长什么样
先把场景说清楚。地面有一批终端设备(比如传感器、摄像头、手持终端),它们自己算力有限,要么本地算,要么把任务通过无线链路传给空中的无人机,由无人机上的边缘服务器代算。无人机在覆盖区域内可以移动,位置会影响信道质量和传输能耗。每个时隙,系统要决定:这个任务卸载多少比例、无人机往哪飞、发射功率给多大。
这就是一个典型的连续控制问题。卸载比例是 [0,1] 的连续值,飞行方向和速度是连续向量,发射功率也是连续值。动作空间连续,状态空间包含任务队列长度、信道增益、无人机位置、剩余电量等。目标是最小化长期加权开销——通常是时延和能耗的加权和。
常见做法是把每个时隙建模成马尔可夫决策过程:状态 s_t 是当前系统快照,动作 a_t 是卸载决策加飞行控制,奖励 r_t 是负的加权开销。智能体的任务就是学一个策略 π(a|s),让累计折扣奖励最大。
2.2 连续动作空间下 DDPG 与 DQN 的分工
DQN 只能处理离散动作。如果你把卸载比例离散成 10 档、飞行方向离散成 8 个方向,动作组合会爆炸,而且精度受离散粒度限制。DDPG 走的是 Actor-Critic 路线:Actor 网络直接输出连续动作,Critic 网络评估这个动作的价值,两者交替更新。它用确定性策略加行为噪声来兼顾探索和利用,在连续控制里比 DQN 自然得多。
这套源码里 DQN 和 DDPG 是并列的,不是让你二选一,而是让你做对比实验。DQN 那组把动作离散化,DDPG 那组保持连续,跑出来的曲线差异正好能写进论文的实验分析章节。Edge_only 和 Local_only 是两个极端基线:全部卸载到边缘、全部本地计算,用来证明你的智能体确实学到了东西,而不是随便乱动。
2.3 源码目录结构与各文件职责
拿到压缩包后,先别急着跑。花五分钟把目录结构理清楚,后面调参会省很多事。
| 目录/文件 | 职责 |
|---|---|
Edge_only/Edge_only.py | 全卸载基线,任务全部传给无人机边缘服务器 |
Local_only/Local_only.py | 全本地基线,任务全部在终端本地计算 |
DQN/ | 离散动作版本,含dqn_algo.py、UAV_env.py、state_normalization.py |
DDPG/ | 主算法目录,含ddpg_algo.py、UAV_env.py、state_normalization.py |
DDPG_without_state_normalization/ | 去掉状态归一化的消融版本 |
DDPG_without_behavior_noise/ | 去掉行为噪声的消融版本 |
Actor Critc/ | Actor-Critic 基础版本,含ac_algo.py |
README.md | 运行说明和依赖列表 |
这个结构的好处是消融实验已经帮你拆好了。写论文时「状态归一化对收敛的影响」「行为噪声对探索的作用」这两节,直接跑对应目录就能出数据,不用自己改代码。
2.4 环境封装:状态、动作、奖励三件套
UAV_env.py是整个项目的地基。它定义了reset()和step(action)两个核心接口,和 Gym 的约定一致。状态向量一般包含:各终端的任务队列长度、当前信道增益、无人机三维位置、无人机剩余能量、上一时隙的卸载比例。动作向量包含:卸载比例、飞行方向角、飞行速度、发射功率。
奖励函数是调参的重灾区。常见写法是:
# UAV_env.py 中奖励计算的典型结构 def _compute_reward(self, offload_ratio, fly_action, power): # 本地计算时延与能耗 local_delay = (1 - offload_ratio) * self.task_bits / self.local_cpu local_energy = (1 - offload_ratio) * self.task_bits * self.local_energy_per_bit # 卸载时延 = 传输时延 + 边缘计算时延 trans_delay = offload_ratio * self.task_bits / self.trans_rate edge_delay = offload_ratio * self.task_bits / self.edge_cpu offload_delay = trans_delay + edge_delay offload_energy = power * trans_delay + offload_ratio * self.task_bits * self.edge_energy_per_bit # 飞行能耗与推进功耗 fly_energy = self.fly_power * self.delta_t total_delay = max(local_delay, offload_delay) # 并行执行取较大者 total_energy = local_energy + offload_energy + fly_energy # 加权开销,w1 w2 是可调权重 cost = self.w1 * total_delay + self.w2 * total_energy return -cost # 奖励取负开销这段逻辑里有两个参数最影响结果:w1和w2。它们决定时延和能耗的相对重要性。如果你论文里侧重时延敏感场景,就把w1调大;侧重能耗受限场景,就把w2调大。建议先跑一组w1=0.5, w2=0.5的对称配置作为基准,再跑偏斜配置做敏感性分析。
注意:
max(local_delay, offload_delay)这个写法假设本地和卸载两条路径并行执行。如果你的模型是串行或者部分并行,这里要改,否则奖励信号会偏乐观,智能体会学到不真实的策略。
3. 跑通 DDPG 训练:从环境配置到收敛曲线
3.1 依赖安装与 Python 环境准备
这套代码依赖不算重,主要是 numpy、torch、matplotlib、gym。建议用 conda 建一个独立环境,避免和系统 Python 打架。
# 创建并激活虚拟环境 conda create -n uav_mec python=3.9 -y conda activate uav_mec # 安装核心依赖 pip install numpy matplotlib gym pip install torch --index-url https://download.pytorch.org/whl/cpu如果你有 CUDA 显卡,把最后一行换成对应 CUDA 版本的 torch 安装命令。CPU 版本跑这个规模的网络也够用,只是训练轮数多了会慢一些。Python 版本建议 3.8 到 3.10,太新的版本有些老版本 gym 会报兼容性错误。
装完之后进DDPG/目录,先跑一个最小验证:
cd DDPG python -c "import torch; import gym; import numpy; print('ok')"没有报错就说明环境通了。如果提示No module named 'gym',检查是不是 conda 环境没激活,或者 pip 装到了系统 Python 里。
3.2 DDPG 主循环:Actor、Critic 与目标网络
ddpg_algo.py是核心。DDPG 的结构是四个网络:Actor 在线网络、Actor 目标网络、Critic 在线网络、Critic 目标网络。在线网络负责训练,目标网络负责稳定 TD 目标。
# ddpg_algo.py 中更新逻辑的简化版 class DDPG: def select_action(self, state): state = torch.FloatTensor(state).unsqueeze(0) action = self.actor(state).detach().numpy()[0] # 训练时加行为噪声,测试时不加 if self.training: noise = np.random.normal(0, self.noise_std, size=action.shape) action = np.clip(action + noise, self.action_low, self.action_high) return action def update(self, batch): states, actions, rewards, next_states, dones = batch # Critic 更新:最小化 TD 误差 with torch.no_grad(): next_actions = self.actor_target(next_states) target_q = rewards + self.gamma * (1 - dones) * self.critic_target(next_states, next_actions) current_q = self.critic(states, actions) critic_loss = nn.MSELoss()(current_q, target_q) self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step() # Actor 更新:最大化 Critic 对当前动作的评分 actor_loss = -self.critic(states, self.actor(states)).mean() self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step() # 软更新目标网络 self._soft_update(self.actor, self.actor_target, self.tau) self._soft_update(self.critic, self.critic_target, self.tau)几个关键参数:gamma是折扣因子,一般 0.95 到 0.99;tau是软更新系数,常见 0.001 到 0.005;noise_std是行为噪声标准差,训练初期可以大一点帮助探索,后期衰减。action_low和action_high要和环境的动作边界严格对齐,否则 clip 之后动作分布会偏。
3.3 状态归一化:消融实验里最容易被忽略的一环
state_normalization.py做的是把状态向量各维度缩放到相近量级。状态里既有队列长度(可能几百上千),又有信道增益(可能 1e-6 量级),不归一化的话网络很难学。
# state_normalization.py 的典型实现 class StateNormalizer: def __init__(self, state_dim): self.mean = np.zeros(state_dim) self.std = np.ones(state_dim) self.count = 1e-4 def update(self, state): # 在线更新均值和标准差,Welford 风格的简化版 self.count += 1 delta = state - self.mean self.mean += delta / self.count self.std = np.sqrt((self.std ** 2 * (self.count - 1) + delta * (state - self.mean)) / self.count) def normalize(self, state): return (state - self.mean) / (self.std + 1e-8)DDPG_without_state_normalization/这个目录就是把这层去掉的版本。跑对比时你会发现,去掉归一化后前期收敛明显更慢,甚至在某些随机种子下不收敛。这个消融结果写进论文很有说服力。
3.4 训练脚本运行与结果观察
主训练入口一般在ddpg_algo.py底部或者单独一个main.py。运行方式:
cd DDPG python ddpg_algo.py训练过程中会打印每轮的累计奖励和平均开销。判断是否正常收敛,看两个信号:一是累计奖励曲线是否从低位震荡逐渐抬升并趋于平稳;二是平均开销是否降到 Edge_only 和 Local_only 之间或更低。如果奖励一直不涨,先检查动作 clip 范围是否合理,再检查奖励尺度是不是太大导致梯度爆炸。
常见做法是每跑 100 轮存一次模型权重,方便后面加载做测试。测试时把training标志设为 False,关掉行为噪声,用确定性策略跑固定轮数,统计平均时延和能耗。
4. 避坑与排查:跑这套代码最容易翻车的五个地方
4.1 现象:训练奖励剧烈震荡,完全不收敛
原因通常是行为噪声标准差设得太大,或者 Critic 学习率远高于 Actor。DDPG 对超参敏感是出了名的玄学,两个网络学习率不匹配时,Critic 评估不准,Actor 就会往错误方向更新。
解决:先把噪声标准差降到 0.1 左右,Actor 学习率设 1e-4,Critic 设 1e-3,跑一轮看曲线。如果还是震荡,把tau从 0.005 降到 0.001,让目标网络更新更慢。还不行就减小网络隐藏层宽度,从 256 降到 128,降低过拟合风险。
4.2 现象:状态归一化后某些维度全是 NaN
原因是标准差计算时除零,或者某个维度的状态值恒定不变导致 std 为 0。state_normalization.py里如果没加1e-8这种小量保护,就会出问题。
解决:检查normalize函数分母有没有加 epsilon。另外确认环境里每个状态维度确实在变化,如果某个维度从头到尾是常数,要么去掉它,要么在归一化时跳过。
4.3 现象:DQN 版本跑得通,DDPG 版本报动作维度不匹配
原因是UAV_env.py里action_space定义和ddpg_algo.py里网络输出维度不一致。DQN 用的是离散动作数,DDPG 用的是连续动作向量长度,两者不能混用同一个环境配置。
解决:确认 DDPG 目录下的UAV_env.py的action_space是Box类型而不是Discrete。如果是从 DQN 目录复制过来的环境文件,记得改动作空间定义和step函数里的动作解析逻辑。
4.4 现象:训练到一半 loss 突然变成 NaN
原因是奖励尺度太大或者梯度爆炸。这套代码里能耗和时延的数值量级可能到几百上千,直接喂给网络容易出问题。
解决:在奖励函数里做缩放,比如把 cost 除以一个常数因子,让奖励落在 [-10, 10] 区间。或者在优化器里加梯度裁剪:
torch.nn.utils.clip_grad_norm_(self.critic.parameters(), max_norm=1.0) torch.nn.utils.clip_grad_norm_(self.actor.parameters(), max_norm=1.0)这两行加在backward()之后、step()之前,能挡掉大部分梯度爆炸导致的 NaN。
4.5 现象:四个基线跑出来的曲线几乎重合
原因是环境随机种子没固定,或者奖励函数里某个权重设成了 0,导致不同策略的开销差异被淹没。
解决:在训练脚本开头固定 numpy 和 torch 的随机种子。然后检查奖励函数,确认w1和w2都不为 0。如果还是重合,把任务大小、信道条件等环境参数调得更极端一些,让卸载决策的差异更明显。有时候场景太简单,所有策略表现都差不多,这时候需要加大任务负载或者拉长无人机到终端的距离。
5. 进阶用法:把消融实验做成论文里的加分项
跑通主实验只是第一步。这套源码真正的价值在于它已经把消融实验的目录拆好了,你可以直接拿来产出论文里「参数敏感性分析」和「模块有效性验证」两节内容。
先说状态归一化的消融。分别跑DDPG/和DDPG_without_state_normalization/,固定其他所有超参和随机种子,各跑 500 轮,记录每 50 轮的平均开销。把两条曲线画在同一张图上,你会看到带归一化的版本在前 100 轮就明显下降,不带归一化的版本可能到 300 轮还在高位震荡。这个对比直接说明归一化层对收敛速度的贡献。
再说行为噪声的消融。DDPG_without_behavior_noise/去掉了探索噪声,理论上会陷入局部最优。跑出来的曲线往往在前期上升很快,但后期明显低于带噪声的版本,因为它没有足够探索去发现更优的卸载策略。这个结果可以支撑你论文里「探索机制必要性」的论述。
还有一个进阶玩法是改奖励权重做 Pareto 前沿。把w1从 0.1 到 0.9 扫一遍,每个权重跑一次训练,记录最终的平均时延和平均能耗,画成散点图。这就是时延-能耗的 Pareto 前沿,比单点对比更有说服力。我一般会跑 9 个权重点,每个点跑 3 个随机种子取平均,虽然费时间,但曲线出来很稳。
验证方法上,除了看训练奖励,一定要做测试阶段的确定性策略评估。加载训练好的模型,关掉噪声,跑 100 个 episode,统计平均开销和标准差。如果测试开销远高于训练末期奖励对应的开销,说明过拟合了,需要加正则或者减少网络容量。
最后说一个我踩过的坑:不要用训练过程中的即时奖励直接当最终性能指标。DDPG 的训练奖励包含噪声带来的随机性,波动很大。一定要单独跑测试脚本,用确定性策略评估。从那以后我每次做强化学习实验,都强制把训练和测试完全分开,训练脚本只负责存模型,测试脚本只负责加载模型跑评估,两边不混。希望帮到你。
本文还有配套的精品资源,点击获取