简介:面向计算机、人工智能、通信工程等专业本科生毕业设计及课程设计场景,这份资源提供了基于深度强化学习的移动边缘计算(MEC)计算卸载与资源分配的完整Python实现。资源围绕任务卸载决策与计算资源调度核心问题,包含环境建模、DQN算法训练主程序、多组实验结果对比绘图以及一键式Shell运行脚本,可帮助读者从零复现深度强化学习在MEC场景下的应用流程,并支持在此基础上调整状态、动作与奖励设置,开展算法对比或功能扩展。资源包共19个文件,以Python源码、Shell脚本、文本日志、Markdown说明和PNG结果图为主,压缩后仅112KB,目录清晰、轻量易用;内置的日志与绘图文件还能直观展示训练收敛过程与卸载效果。目前已有290人学习下载,适合作为毕设、课设项目演示、期末作业或入门深度强化学习与边缘计算交叉方向的实战参考。
1. 把计算卸载写成深度强化学习问题的第一原则
看起来是“任务扔给谁执行”的选择题,本质上是时变信道下的资源拍卖。MEC计算卸载的难点不在矩阵运算,而在你既要决定当前任务在本地、边缘还是云端执行,又要给每个边缘节点分配CPU份额,而此刻的决策会直接影响下个时隙的队列积压。深度强化学习适合这类场景,是因为它不需要系统模型的精确表达式,只需要在仿真环境里不断试错,就能学到“排队时间短的时候多往边缘卸,信道差的时候保住本地执行”这类策略。这个项目对你真正的价值,不是那几十个python文件,而是把一个含随机信道、异构任务和能量约束的优化问题,成功包装成马尔可夫决策过程并能训练收敛的全过程。能把这套流程走通,通信和AI里的基础概念基本都会被串起来。
2. MEC环境建模:把计算卸载写成MDP的状态、动作与奖励
2.1 为什么先建模MDP而不是先配网络
很多开源代码一上来就搭神经网络,结果训练曲线全程平躺。问题出在场景没有“盒装化”。计算卸载这个MEC问题,核心是把时变的信道增益、任务队列长度、边缘节点负载压缩成一个状态向量,再把卸载节点选择和资源分配压成动作向量。没有这两步,网络看到的只是一堆没法比较的数值。
我会先把环境抽象成MDP,其中时间切成离散时隙,每个时隙生成0到3个任务。状态包含三部分:信道状态、队列状态、资源状态。它的关键属性是“下一个时隙的任务量是随机的”,这正是强化学习比数学优化更适合的原因。这里的“计算卸载”在MEC语境里至少包含两层决策:卸载目标选择和卸载比例控制。若只做0/1卸载到某个边缘节点,状态空间可以压缩到UE数量级别;若还要决定卸载任务的百分比,动作空间就需要连续化或离散化处理。
2.2 状态向量和动作空间的Python定义
先给出一份可以直接跑通的dataclass配置,用它统一管理所有环境参数。参数集中在这份配置里,后续做参数扫描时不需要改动环境类本身。
import numpy as np from dataclasses import dataclass @dataclass class MECConfig: n_ue: int = 10 # 用户设备数 n_mec: int = 3 # MEC节点数 bw: float = 10e6 # 信道带宽(Hz) noise_power: float = 1e-13 # 高斯白噪声功率 ue_power: float = 0.1 # UE发射功率(W) local_cpu: float = 1e9 # 本地CPU频率(cycle/s) mec_cpu: float = 20e9 # 每个MEC节点CPU频率 cpu_per_bit: float = 1000 # 处理1bit任务所需CPU周期这份config决定后面的状态维度和动作口径。n_ue取10、n_mec取3是折中值:状态维度过大时DQN的探索成本会成倍上涨,边界服务器数量超过5个后,收益提升也不再明显。
class MECEnv: """ 单智能体MEC环境:把信道增益、队列长度、计算资源拼成状态向量。 """ def __init__(self, cfg: MECConfig): self.cfg = cfg self.state_dim = 2 * cfg.n_ue + cfg.n_mec self.action_dim = 1 + cfg.n_mec # 0=本地执行,1..n_mec=卸载到对应MEC节点 def get_state(self): # 模拟当前时隙的观测:队列长度随机,信道增益服从指数分布 queue_len = np.random.randint(0, 3, self.cfg.n_ue).astype(float) channel_gain = np.random.exponential(1.0, self.cfg.n_ue) mec_load = np.random.rand(self.cfg.n_mec) state = np.concatenate([queue_len, channel_gain, mec_load]) return state / (state.max() + 1e-6) # 简易归一化,避免大数值特征主导网络状态向量的维度是2 * n_ue + n_mec。前10维表示本地队列长度,中间10维表示用户到MEC节点的信道增益,最后3维表示边缘节点当前负载。归一化这里做的是近似min-max,除以状态最大值,足够让DQN理解相对好坏。
动作解码单独抽成一个函数,方便后续换算法时复用。action_index里每个元素取值范围是0到n_mec,0表示本地执行,其他值表示卸载到第几个MEC节点。
def decode_action(action_index, cfg): target = np.zeros(cfg.n_ue, dtype=int) for i, idx in enumerate(action_index): target[i] = idx return target这种表示天然支持多MEC场景,但要注意动作空间规模是(n_mec + 1) ^ n_ue;当UE数为10时有4^10种组合,穷举和数学规划都已经不现实,这也就是神经网络逼近Q值的意义所在。
2.3 奖励函数里那三个无量纲化处理
常见实现会把时延和能耗直接相加,相加之前必须无量纲化。我建议的奖励结构是同时考虑时延、能耗和队列溢出惩罚,三项都必须缩放回相似数量级。
delay_ratio = total_delay / (task.deadline + 1e-6) # 时延相对截止时间的比例 energy_ratio = total_energy / (max_energy + 1e-6) # 能耗相对最大值比例 queue_penalty = 1.0 if queue_overflow else 0.0 reward = - (0.6 * delay_ratio + 0.3 * energy_ratio + 2.0 * queue_penalty)delay_ratio用截止时间做分母,energy_ratio用单时隙最大能耗做分母,queue_penalty是硬惩罚项。为什么不能直接用毫秒和焦耳相加?因为量纲不同会导致小数量级信号被大数量级淹没,训练曲线看着在下降,实际任务迟到率却很高。调大queue_penalty会让策略优先清空队列,但过大会让网络忽略其他目标,所以2.0是一个对多数场景都不过激的初值。
MDP建模到这里,信息边界就清楚了:观测被压缩成状态,决策被编码成动作,环境反馈被映射成奖励。接下来的DQN只是在这个边界内做近似的长期回报优化。
3. 深度强化学习选型:DQN处理离散卸载决策的Python实现与PPO对比
3.1 为什么毕业设计首选DQN而不是PPO
计算卸载的动作为离散的卸载目标选择,DQN直接把Q价值表用神经网络近似,这种形式对应MEC问题最为直接。PPO的优势在连续动作,比如需要输出“分配到MEC 1的卸载比例是0.35”这类精细资源分配。但如果把资源分配比例离散成档位,DQN同样可以覆盖。
实际做项目时我是这么权衡的:如果环境状态维度和动作空间都不大,DQN的采样效率远高于PPO,训练一轮能看到明显反馈;PPO在连续资源分配上的稳定性更好,但需要调的优势函数和裁剪系数会让毕设阶段的工作量增加不少。有一个更前沿的方向是用图强化学习建模UE之间的干扰关系,相当于给每个用户一个独立的决策头,再把它们的交互打包成动态计算卸载层,这类结构适合论文创新点,但复杂度不适合作为第一个能跑的版本。
3.2 经验回放与目标网络的Python骨架
DQN能不能收敛,一半取决于经验回放缓冲区和目标网络的更新节奏。下面这份代码是毕业设计中最常见的骨架,可以直接替换进自己的环境。
import torch import torch.nn as nn from collections import deque class DQN(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, action_dim), # 输出层不加激活,因为Q值可为负 ) def forward(self, x): return self.net(x)经验回放缓冲区的作用是打断样本间的时序相关性。如果在MEC环境中按原始顺序训练,连续几个时隙的样本会共享同一种信道条件,梯度方向被当前信道带偏;随机抽样之后,一个minibatch里能混合好信道和差信道的样本,策略才能学到“看到好信道就多卸载”的泛化规律。
class ReplayBuffer: def __init__(self, capacity=50000): self.buffer = deque(maxlen=capacity) def add(self, s, a, r, s_next, done): self.buffer.append((s, a, r, s_next, done)) def sample(self, batch_size): idx = np.random.choice(len(self.buffer), batch_size, replace=False) states, actions, rewards, next_states, dones = [], [], [], [], [] for i in idx: s, a, r, sn, d = self.buffer[i] states.append(s) actions.append(a) rewards.append(r) next_states.append(sn) dones.append(d) return ( torch.tensor(states, dtype=torch.float32), torch.tensor(actions, dtype=torch.long), torch.tensor(rewards, dtype=torch.float32), torch.tensor(next_states, dtype=torch.float32), torch.tensor(dones, dtype=torch.float32), )注意dones必须存成float,否则(1 - done)会变成布尔运算,导致终局状态下目标值计算错误。
# 每隔target_update步同步一次目标网络 for step in range(total_steps): s, a, r, sn, done = buffer.sample(batch_size=64) q_pred = dqn(s).gather(1, a.unsqueeze(1)).squeeze() with torch.no_grad(): q_target = r + gamma * (1 - done) * target_dqn(sn).max(dim=1).values loss = nn.MSELoss()(q_pred, q_target) optimizer.zero_grad() loss.backward() optimizer.step() if step % target_update == 0: target_dqn.load_state_dict(dqn.state_dict())gamma取0.9到0.99。0.9在MEC任务中已经偏小,因为决策的收益要跨多个时隙体现;值越接近1,网络越能感知队列积压带来的长期代价。target_update一般取200到300步,太短会导致目标值波动,太长则网络学不到最新Q值变化。
3.3 DQN与PPO的取舍表
| 对比项 | DQN | PPO |
|---|---|---|
| 动作类型偏好 | 离散动作 | 连续动作 |
| 卸载目标选择 | 直接输出Q值选最优 | 输出动作概率分布 |
| 连续资源分配 | 需要离散化成档位 | 原生支持连续值 |
| 训练稳定性 | 依赖经验回放和目标网络 | 稳定性更好,方差较低 |
| 调试成本 | 需要调目标网络更新节奏 | 需要调裁剪系数和熵系数 |
一个比较实用的策略是:先拿DQN把环境调试通,确认奖励设计和状态归一化没有出现数值问题;确认收敛趋势之后,如果论文需要体现连续资源分配创新点,再把DQN替换成PPO,只改动动作解码部分,前期的环境代码和奖励函数完全复用。
4. 深度强化学习训练稳定性的三个必调参数与常见收敛陷阱
4.1 奖励尺度:梯度爆炸的头号元凶
如果你发现loss在训练初期直接变成nan,或者奖励曲线在前几步变成巨大的负数后不再变化,第一个要检查的就是奖励尺度。MEC环境里时延的单位一旦取微秒,奖励数值会达到10的6次方量级,任何神经网络在这种输入下都会梯度爆炸。
我会这么做:训练开始前先随机跑20个episode,统计奖励的均值r_mean和标准差r_std,然后在训练中给奖励除以标准差。这个操作相当于对奖励做了一次标准化,能明显缓解环境量纲带来的波动。
reward = reward / (r_std + 1e-6)r_std来自随机策略的探索结果,它描述的是“什么都不学”时的奖励波动范围。把这个波动范围缩放到1附近,DQN的Q值初始化就不会偏离真实回报太多。
4.2 epsilon衰减与目标网络更新间隔
epsilon-greedy的初值一般从1.0开始,让智能体前几百步完全随机探索,然后逐渐衰减到0.05。衰减速度需要和环境长度对齐:如果每一episode有50个时隙,而epsilon衰减函数被设计成1000步内从1.0掉到0.1,那么只过了20个episode就基本不再探索,很难覆盖各种信道组合。我一般把衰减步长设置为总训练步数的30%,也就是前三分之一时间在探索,后三分之二开始看重利用。若系统里有5个MEC节点,动作空间更大,这一个比例可以适当上调到40%。
目标网络更新间隔target_update在前面已经提到,取200到300步是经验值。判断这个值是否合适,可以观察训练曲线是否出现周期性震荡:如果奖励曲线每几百步就掉一次,多半是目标网络更新太频繁,Q值目标跟着当前网络一起漂移;如果奖励曲线长时间不变,说明更新太慢,目标值一直停留在旧策略上。
4.3 状态特征不归一化的隐性坑
状态向量里队列长度是0到3的整数,信道增益可能小到1e-5,而信道噪声功率可能是1e-13。如果不做任何处理直接把原始数值拼进状态,那么前几层的线性变换会被量级最大的维度主导,小量级的信道增益等于没有进入网络。
最简单的处理办法是用sklearn.preprocessing.MinMaxScaler,但要注意它需要先统计出整个状态空间的上下界;如果环境是持续运行而不是离线生成数据集,快速做法就是像前面代码一样除以状态最大值。更精细的做法是对每个特征维分别统计历史最大绝对值,维护一个滑动窗口的running max。这一步在算法创新上没有任何贡献,但能直接决定DQN是否学得动。
4.4 和随机卸载基线对比检查信号
训练10分钟后,如果DQN的平均奖励长期低于“每个任务都以50%概率随机卸载”的随机策略,请优先检查奖励符号和卸载动作是否真的被执行。很多开源代码会把决策输出直接作为动作编号,但忘记把动作转换成环境里的task.offload_target,网络在学但环境一直在执行默认的本地策略,这样的曲线自然学不出来。
我发现最有用的调试顺序是:
- 先跑本地执行策略,记下平均时延和能耗。
- 再跑随机卸载策略,记下同样两个指标。
- 最后跑DQN,和随机策略比较。
DQN至少要比随机卸载的平均时延低10%才算正常。如果差距不大,问题通常不在算法,而在奖励函数里没有区分“本地执行”和“边缘执行”的代价,网络找不到优化的方向。
5. MEC计算卸载收敛太慢时:先做奖励塑形与课程学习
5.1 从稀疏奖励分解成三个子奖励
有些环境实现会在整个episode结束后才计算一次奖励,对DQN来说这种稀疏奖励会让探索变得非常困难。MEC环境天然适合拆成密集奖励:每个时隙结束时,把时延、能耗和队列长度分别计算成子奖励再合并。我通常会使用下面的分解方式:
sub_reward_delay = -np.clip(avg_delay / delay_max, 0, 5) sub_reward_energy = -np.clip(avg_energy / energy_max, 0, 5) sub_reward_queue = -np.clip(current_queue_len / (cfg.n_ue * 3), 0, 1) reward = 0.5 * sub_reward_delay + 0.3 * sub_reward_energy + 0.2 * sub_reward_queuesub_reward_delay用截止时间做归一化分母,并把超出5倍截止时间的极端情况截断,避免异常时延把奖励压得太低。sub_reward_queue的构造很关键:当队列长度接近n_ue * 3时,它会让奖励明显变负,DQN会主动学会避免任务积压。系数0.5、0.3、0.2不是拍脑袋定的:先跑10episode随机策略,统计三个分量的标准差,然后按标准差倒数归一化,再按比例整成近似权重,收敛会更快。
5.2 课程学习:先固定信道,再随机信道
如果一开始就加入随机信道衰落,DQN很可能长期无法区分不同动作的好坏。一个常见的做法是先固定信道增益为常数,让环境退化成确定性场景,DQN会把“边缘节点空闲时多卸载”这类基本策略学会;然后每隔几个episode增大信道增益的方差,逐步训练在恶劣信道条件也能保住基本性能。实际操作中我用一个系数控制信道方差:
channel_gain = np.random.exponential(scale=channel_variance, size=n_ue)channel_variance从0.2开始,每100个episode加到0.5、0.9、1.6,而不是直接使用最终随机强度。这种方式能让奖励曲线呈阶梯式下降,但每一步都比直接跑随机信道快得多。
5.3 验证卸载率而不是只盯奖励
训练最后,用100个episode统计平均奖励、平均时延和卸载率。卸载率指“选择边缘执行的任务数占总任务数的比例”,它是最能说明计算卸载行为是否形成的指标。DQN如果只是学会了“所有任务都丢给本地CPU”,奖励曲线可能也能收敛,但论文里没有计算卸载的意义。有效的卸载率通常从0.1爬到0.5左右,说明策略真正在利用边缘节点。把奖励曲线用指数移动平均平滑后再画进论文图里,背景保留原始曲线的浅色阴影,既显得真实又不用答辩时解释锯齿波。
本文还有配套的精品资源,点击获取