简介:这是一份基于深度强化学习的车联网通信资源分配优化系统毕设项目,采用Python实现,源码附有详细注释,目标用户是正在准备毕业设计、课程设计或期末大作业的计算机、通信相关专业学生。项目围绕车联网中的频谱、功率等通信资源分配问题,引入DDPG、MADDPG、MADQN等多种深度强化学习算法,构建了多智能体环境、交互逻辑、回放内存与训练评估等完整模块,能够清晰体现从状态感知、决策到奖励反馈的闭环过程,同时提供随机环境等实验配置,方便进行算法对比与二次开发。压缩包内共21个文件,其中13个.py源码文件构成算法主体,6个.pyc为编译后的缓存文件,另含1份项目说明.md和1个zip附加包,整体仅82KB,轻量紧凑,下载解压后即可导入开发环境运行。项目已通过本地验证,无需复杂配置,配合说明文档可快速掌握代码结构。当前已有961人学习使用,适合通信与强化学习方向的学生借鉴完成毕设或课程项目。
1. 车联网资源分配为什么越调越难:DRL 正好接住这个麻烦
做过车联网仿真的人都会有同感:在 C-V2X 场景里给每个 V2V 链路分配合适的时频资源和发射功率,比想象中难得多。车辆在移动、信道在快速衰落、相邻链路的干扰互相纠缠,传统做法要么建模成组合优化问题硬解,要么靠半静态调度拍脑袋配置,结果就是城市场景下算不动、配不准、抖得厉害。基于深度强化学习的车联网通信资源分配优化系统,本质上是把这个分配过程改造成一个序贯决策问题:让智能体观察信道状态和队列状态,在每一个调度周期输出资源块选择和功率等级,再用奖励函数引导它学会“尽量不干扰别人、又把自己的数据发出去”。这套方案在毕设里的价值是显而易见的:既能体现深度学习的能力,又能落到通信仿真里,适合做算法、做实验、做可视化,也适合写出一篇结构完整的毕业设计。这篇博文会带着你把这条链路从建模讲到复现,再把容易翻车的地方一次说清。
2. 把通信资源分配改写成深度强化学习的马尔可夫决策过程
2.1 先选场景:C-V2X 模式与链路构成
车联网通信资源分配,首先要明确你在分什么资源。按 3GPP C-V2X 的标准语境,V2V 链路用 sidelink 传输,资源在时域上是子帧、在频域上是 RB(Resource Block,通常一个 RB 是 180kHz);每次传输除了选 RB,还要选调制编码方式和发射功率。常见做法是在 LTE-V2X 模式下做集中式资源调度:RSU 或路边单元收集车辆上报的信道状态信息(CSI),然后决定每个 V2V 链路的 RB 和功率。到深度强化学习这里,这个调度过程就变成了一个典型的 MDP 问题:某个时隙的状态、决策动作、即时奖励、转移到下一时隙。
我一般会建议毕设先不要把场景铺得太大。不要一上来就是十字路口加多车道加几十辆车,信道模型也用最复杂的 3GPP 城区模型,那样状态空间会迅速失控,训练很久都学不会。常见做法是:单 RSU 覆盖范围约 500 米,6 到 8 对 V2V 链路并发,每对链路有固定源车和目的车,资源池里有 10 个 RB,功率分成 3 到 4 档。这样既保留了车联网的动态特性,又能让 DRL 智能体在可接受的时间内收敛,论文里也可以再补一个“扩展到更多车辆”的对比实验。
2.2 状态空间、动作空间与奖励函数的编码方式
状态空间的设计是整个系统的地基。不能只把信道增益丢进去,智能体需要知道“谁在干扰谁”。常见的编码方式是把状态拆成三块:各链路在当前时隙的信道增益矩阵(V2V 链路自身链路增益以及它到其他接收端的干扰链路增益)、各链路的队列积压长度、上一时隙的资源分配结果。注意不要把原始数值直接灌进网络,增益要取 dB 值再做归一化,否则训练初期数值震荡足以让模型学不动。状态维度可以控制在几十维以内,比如 8 条 V2V 链路、每条考虑自身信道和两跳邻域干扰,凑成 40 到 60 维向量是合理的。
动作空间有两种做法:一是直接输出离散动作,把 RB 编号和功率档位组合成动作序号,比如 10 个 RB 乘以 3 档功率就是 30 个动作,好处是可以用 DQN 这类经典算法,坏处是链路多时组合爆炸;二是按链路维度拆分成多个子动作,每条链路独立选择 RB 和功率,再用多头结构输出。功率建议用离散档位而不是连续值,因为通信系统里的功率控制本来就是以 dBm 为步进调整的,离散化不会损失实际性能,反而能降低训练难度。
奖励函数要顺着优化目标来设计。通信资源分配一般关注两类指标:V2V 链路的传输成功率,以及 V2I(车到基础设施)链路的吞吐量。设计时要把这两者拼成一个标量奖励。我的常用写法是:
- 每条 V2V 链路在时隙结束时如果 SINR 高于解调门限,则记录一次成功传输,对应一个正向奖励;
- 所有 V2I 链路的吞吐量求和后取 log 或线性值,作为次要奖励分量;
- 如果两个 V2V 链路使用了同一 RB 且相互干扰很强,也就是强干扰碰撞,扣除一个专门的惩罚项。
这样的奖励结构比单纯给“吞吐量”要明确得多,智能体不会出现“为了吞吐量疯狂提高功率导致互相干扰”的钻空子行为。
2.3 仿真环境:从零搭一个最小可跑的 Python 环境骨架
这块要写代码了。常见的做法是参考 OpenAI Gym 的接口,写一个自定义环境类ResourceAllocEnv。最小版本只需要实现几件事:维护车辆位置和信道参数、根据当前分配计算 SINR 和吞吐量、生成下一时隙状态、返回奖励。下面这个骨架是可以直接接着写的:
import numpy as np import gym from gym import spaces class ResourceAllocEnv(gym.Env): """ 车联网资源分配仿真环境(简化版) 链路数目: 8 对 V2V 资源池: 10 个 RB 功率档位: 3 档(低/中/高) """ def __init__(self, n_links=8, n_rbs=10, n_power=3): super().__init__() self.n_links = n_links self.n_rbs = n_rbs self.n_power = n_power # 动作: 每条链路选择一个 (RB, 功率) 组合 # 展平后是一个长度为 n_links 的整数数组 self.action_space = spaces.MultiDiscrete([n_rbs * n_power] * n_links) # 状态: 自身信道增益 + 邻域干扰增益 + 队列积压长度 self.obs_dim = n_links * (n_rbs + 1) + n_links self.observation_space = spaces.Box(low=0.0, high=1.0, shape=(self.obs_dim,), dtype=np.float32) # 车辆位置与信道初始化 self.positions = None self.queue_length = None self.current_obs = None self.reset() def reset(self): # 随机撒点,生成车辆位置(简化为一维道路坐标) self.positions = np.random.uniform(0, 500, size=(2 * self.n_links,)) self.queue_length = np.random.randint(100, 500, size=self.n_links) # 根据相对距离计算信道增益 self.current_obs = self._compute_channel_gain() return self.current_obs def _compute_channel_gain(self): # 简化路径损耗模型: PL(d) = 128.1 + 37.5 * log10(d/1000) # 返回归一化信道增益矩阵 obs_parts = [] for i in range(self.n_links): tx_pos = self.positions[2 * i] rx_pos = self.positions[2 * i + 1] dist = max(abs(tx_pos - rx_pos), 1.0) pl = 128.1 + 37.5 * np.log10(dist / 1000.0) gain_db = -pl obs_parts.append(np.array([gain_db])) # 简化: 用固定的干扰基线扩展成完整观测 return np.concatenate(obs_parts).astype(np.float32) def step(self, actions): # 根据动作计算 SINR 和奖励 rewards = 0.0 done = False info = {} # 此处需要实现干扰计算,简化版直接按随机信道返回 # 正式使用时可参考 Vienna 5G 或自写 SINR 计算 sinr = np.random.uniform(-5, 25, size=self.n_links) success = sinr > 10 # 简化解调门限 rewards += np.sum(success) * 0.5 rewards += np.mean(self.queue_length) / 500.0 # 惩罚项: 检测到强干扰碰撞时扣分 rewards -= 0.1 # 更新下一时隙状态 self.queue_length = np.maximum(self.queue_length - 200 * success.astype(int), 0) \ + np.random.randint(0, 50, size=self.n_links) self.current_obs = self._compute_channel_gain() info = {"success_rate": np.mean(success)} return self.current_obs, rewards, done, info这个代码逻辑里有一个很关键的取舍:step()里的 SINR 计算被简化成了随机分布,实际使用时必须换成正向信道模型,否则奖励没有物理含义。路径损耗公式128.1 + 37.5 * log10(d/1000)出自 3GPP TR 36.885,是车联网仿真里的常用默认值。信道增益这里我用了手工矩阵,正式跑实验时可以改用pycomv或自行实现快衰落。MultiDiscrete动作空间的长度是n_links,意思是每条链路独立选动作,这也暴露了你后面要选哪种算法:如果一个动作代表“所有链路同时分配”,那网络输出维度就是n_links * n_rbs * n_power,显然难度上升一个级别。
3. 选对深度强化学习算法:DQN 与 PPO 在资源分配里的适配性
3.1 DQN 对离散资源分配的优势与隐患
DQN 是许多毕设的首选,因为这题动作空间天然离散。把 RB 看作频域上的选择,把功率档位看作离散集合,直接用 Q 网络拟合“当前状态下每个动作的期望累计奖励”。做毕设时 DQN 最大的吸引力是收敛曲线直观、调参经验多,踩坑资料遍地都是。
但 DQN 在车联网场景里有几个躲不开的坑。第一是过估计问题,经典 DQN 用max操作选目标 Q 值,天然会高估,在动态环境下会导致训练曲线看起来在涨、实际测试性能极差;解决手法是 Double DQN,这个改动非常小,但效果明显。第二是经验回放池里存了太多“旧策略”下的样本,信道状态变化快时,旧样本会污染训练分布,要适当调小回放池容量,或者引入优先级采样。第三也是最容易忽略的:DQN 只输出动作价值,不做随机策略采样,一旦状态里有多个等价动作,Q 值的微小差异就会让策略在网络更新中抖动。也就是说,动作之间没有显式概率分布的约束,学习过程很不稳定。
3.2 PPO 在连续动作上的优势与训练流程
如果想把功率也放进动作空间做成连续量,或者干脆把功率档位放宽到 10 档以上,PPO 会是更稳妥的选择。PPO 的思想是在每次更新时限制新旧策略的差异程度,用clip系数把一步更新步长按住。对车联网这种输入是信道快衰落、输出是离散调度决策的场景,PPO 对超参数的敏感度比 DQN 低,训练也更稳,这也是为什么很多毕业设计到后期会从 DQN 转 PPO。
下面是一个用 stable-baselines3 训练流程的最小示例。按我自己做实验的习惯,环境写好后先用 RandomPolicy 跑几百步看奖励上下界,再切到 PPO:
import gym from stable_baselines3 import PPO from stable_baselines3.common.env_checker import check_env from resource_alloc_env import ResourceAllocEnv # 1. 校验环境是否符合 gym 规范 env = ResourceAllocEnv(n_links=8, n_rbs=10, n_power=3) check_env(env, warn=True) # 2. 包一层向量环境,方便并行采样 from stable_baselines3.common.vec_env import DummyVecEnv, VecNormalize vec_env = DummyVecEnv([lambda: ResourceAllocEnv(n_links=8, n_rbs=10, n_power=3)]) vec_env = VecNormalize(vec_env, norm_obs=True, norm_reward=False) # 3. 定义 PPO 智能体 model = PPO( "MlpPolicy", vec_env, learning_rate=3e-4, n_steps=2048, batch_size=64, gamma=0.95, clip_range=0.2, ent_coef=0.01, verbose=1 ) # 4. 训练并保存 model.learn(total_timesteps=200_000) model.save("v2x_resource_alloc_ppo")参数里有两个细节值得细看。norm_obs=True意味着观测值会被滚动标准化,这个对信道增益这种波动很大的输入尤其重要,不开启的话模型很容易在前几万个 timestep 里被大数值输入带偏。norm_reward=False是因为奖励本身已经是人工设计的量纲,标准化后反而会丢失惩罚项的物理尺度。gamma=0.95而不是默认的 0.99,原因是车联网调度更关注近期收益,链路质量变化快,过大的折扣因子会让模型过分关心远期累计奖励,反而学不到即时避碰的行为。
如果要换回 DQN,stable-baselines3 里也有 DQN 实现,但需要把动作空间改成Discrete,也就是把所有链路的联合动作做成一个一维整数。这个改动看起来简单,实际上会导致动作空间直接膨胀成(10 * 3)^8级别,训练难度是指数级上升。所以做实际实验时,多数人会要么在 DQN 上做“一维动作拆分”:每条链路一个 DQN,轮流决策;要么直接用 PPO 的MultiDiscrete支持,一步到位。从我的血泪经验来说,毕设优先级排序是 PPO 大于 Double DQN 大于普通 DQN。
3.3 怎么验证算法真的学会了分配而不是死记硬背
训练结束后想让人信服,不能只放一张 loss 下降图。一个接地气的做法是把模型放到“未见过的车流密度”里测试。训练时车的位置只在 0 到 500 米之间随机,测试时把范围扩到 600 米,或者在车辆数量上加几辆,看模型是否还能维持成功率。另一种做法是做策略可视化:把某一时刻的信道增益和模型输出的功率选择画出来,看看是否和直觉一致——近距离的链路用低功率、远距离链路分配给不相邻的 RB。这套验证思路放到论文里就是泛化实验,能让导师一眼看出你不是在背训练集。
4. 训练车联网资源分配模型:超参设置与训练观测要点
4.1 关键超参数设计与表格
训练 DRL 模型最怕玄学调参。下表是我在做类似系统时默认使用并在多个环境里验证过的一组参数,可以当起点用。
| 参数 | 默认值 | 作用 | 调参方向 |
|---|---|---|---|
| gamma | 0.95 | 折扣因子 | 信道变化快时调小到 0.9;追求长期吞吐可调到 0.99 |
| learning_rate | 3e-4 | 策略更新步长 | 训练震荡时降到 1e-4;收敛太慢可提到 5e-4 |
| n_steps | 2048 | 每次采样步数 | 环境随机性强时可增大到 4096 |
| batch_size | 64 | 更新批量 | 样本方差大时调大 |
| ent_coef | 0.01 | 探索熵系数 | 模型过早收敛时调大到 0.05,保持探索 |
| clip_range | 0.2 | PPO 裁剪范围 | 不稳定时调小到 0.1 |
| 经验回放池大小 | 10000 | 样本缓冲 | DQN 时减小到 5000,防止旧样本污染 |
这里有两条通用规律。第一:gamma是通信场景里最值得调的超参。车联网信道是强时变的,当前时隙的分配结果影响的主要是接下来几个时隙的干扰格局,远期奖励的参考价值很低;所以 gamma 不要照搬 Atari 游戏的 0.99 或 0.999,0.93 到 0.96 是一个合理区间。第二:ent_coef是救命的参数。如果训练到后期策略几乎变成确定性输出,而测试环境里车辆位置分布和训练时略有差异,模型的泛化能力会瞬间崩掉。保留一点熵,让策略在分配边缘保持随机性,反而能提高成功率。
4.2 训练过程中看什么指标才能判断模型在学
很多初学者只盯奖励均值曲线,这是不够的。因为奖励函数里包含了多个子目标,均值上升可能只是某一部分在涨。我的习惯是在step()的 info 字段里额外塞几个指标:V2V 链路成功传输率、平均频谱效率、碰撞次数。然后在训练脚本里用Callback把这些量记录下来。
from stable_baselines3.common.callbacks import BaseCallback import numpy as np class MetricsCallback(BaseCallback): def __init__(self, verbose=0): super().__init__(verbose) self.episode_success = [] self.episode_reward = [] def _on_step(self): # infos 是 vector 环境下发来的 info 字典列表 infos = self.locals["infos"] for info in infos: if "success_rate" in info: self.episode_success.append(info["success_rate"]) # 每 1000 步打印一次 if self.num_timesteps % 1000 == 0: avg_succ = np.mean(self.episode_success[-200:]) print(f"Step {self.num_timesteps}: avg_success_rate={avg_succ:.3f}") return True这个回调不能直接看出策略质量,但你会发现一个规律:奖励在涨、成功率不一定在涨。这种情况通常意味着智能体找到了“多发包刷奖励”的漏洞,而不是学会分配资源。发现这种背离的第一反应不是调奖励权重,而是先检查干扰项是不是没起作用,也就是惩罚项数值太小。换个说法:如果在训练日志里看到奖励涨但成功率不涨,几乎总是奖励函数内各个分量的尺度失衡。把成功奖励从 0.5 提到 1.0,把干扰惩罚从 0.1 提到 0.5,很快就正常了。
4.3 从训练曲线到收敛判断:什么时候可以停车
DRL 训练不像监督学习那样有一个明确的验证集 loss,判断什么时候停更多靠经验。我的判断标准有三个:成功率曲线在最近 2 万步内不再创新高、奖励曲线波动幅度小于早期的一半、策略输出在同一个测试场景里重复推理多次的结果一致。第三个标准最容易被忽视,但最有用。做法很简单:固定一帧信道状态,用训练好的模型推理 10 次,如果 10 次选择的 RB 和功率完全相同,说明策略已经收敛到确定性;如果每次都不同,说明要么熵系数太大、要么还没训透。继续训练前可以先把ent_coef减半试试,如果减半后确定性提升,说明模型已经学了差不多,只是在做无谓探索。
5. 毕设排错:车联网资源分配里最常见的五个坑与排查方法
5.1 奖励曲线一直徘徊不涨,信道输入没做归一化
现象:训练了 5 万个 timestep,奖励均值上下抖动,没有任何上升趋势。
原因:信道增益的数值范围极不稳定。路径损耗公式算出的 dB 值在近距离时可能接近 -70,远距离时低到 -140,直接作为神经网络输入会让激活函数饱和,梯度几乎消失。观测空间我在前面定义时写了Box(low=0.0, high=1.0),正式实现时必须把增益压缩到这个范围里。我给环境加一段最小最大归一化:gain_norm = (gain_db / 100.0 + 1.0) / 2.0,保证大部分数值落在 0.1 到 0.9 之间。如果归一化后曲线依然不涨,再检查是否有的观测分量没有被 ReLU 激活覆盖到。
5.2 PPO 训练时跳出 NaN,奖励中出现 inf
现象:训练到第 2 万步左右,日志里出现 NaN loss,然后环境全部崩掉。
原因:多半是奖励函数里出现了除零或 log 零。计算吞吐量时会用到np.log2(1 + sinr),如果sinr因为干扰计算出现负值或 -1,log 就出问题了。另一个常见来源是队列长度计算里出现负数后取np.maximum(0, x),但有些位置忘记取保护,导致状态变成负值后VecNormalize里的滚动方差出现负值开根号异常。解决方式:在环境step()里所有除法、log 之前加上np.clip(x, a_min=1e-6, a_max=None),并且奖励一律用np.float32,不要混用 Python 内置 float。
5.3 测试时成功率远低于训练时,动作分布过拟合
现象:训练集上成功率 90%,换一个随机种子初始化车辆位置后掉到 60%。
原因:模型记住了训练时特定的干扰格局,而不是学会“根据信道状态分配互不干扰的资源”。这本质上是策略泛化没做好。我的排查顺序是先看观测里是否包含了足够的位置信息。如果只给链路自身增益而不给邻域干扰增益,模型根本不知道“我选了 RB 3 会和谁撞”,那再换场景自然抓瞎。第二个常见原因是熵系数最终被调到 0,策略变成纯贪心,失去了对未见场景的适应能力。解决:在训练结束前 5000 步把ent_coef从 0.01 调回 0.03,让模型保留一点随机决策能力。
5.4 训练步数极多但显存和 CPU 内存不够,车数一多就卡死
现象:8 条链路训得好好的,加到 16 条链路后环境每一步都要算 16×16 的干扰矩阵,训练时长从 2 小时变成 15 小时,内存还爆了。
原因:这属于复杂度预估没做好。干扰矩阵的计算复杂度是 O(N²),车数翻倍就是四倍计算量;经验回放池里存储的观测维度也在涨,内存占用线性上升。解决方式不是换机器,而是重构设计。常见做法是把干扰计算从“每对链路都算”改成“只计算同 RB 内的链路对”:“每条链路先按功率等级粗筛,只有落在同一 RB 上且距离小于干扰半径的链路才进入精确 SINR 计算”。这样 16 条链路的情形下,实际执行精确计算的链路对数往往只有 1/3 到 1/4,训练速度恢复大半。若还是慢,就把n_steps从 2048 下调到 1024,牺牲一点稳定性换速度。
5.5 模型在训练中学会了“只用最小功率”,成功率低但不自知
现象:奖励已经收敛且不再抖动,但实际统计成功传输率只有 40%,模型像是在混日子。
原因:奖励函数里给成功传输的奖励太低,给功率的隐形惩罚太高,或者碰撞惩罚没有生效。智能体发现与其花力气去选 RB 避碰,不如直接把功率调到最小挡位——虽然发不出去,但也不会因为高功率干扰别人被扣分。这种情况下模型非常稳定地收敛到局部最优,你甚至看不出问题,因为奖励曲线很平。解决:把奖励各分量拆开打日志,单独看“成功奖励累计/惩罚累计”的比例,如果惩罚项占比过高,就要把功率从动作空间里暂时拿掉,先只分配 RB,让模型学会选资源,再放回功率。这是我在调这类系统时最常用的一种降难度手法。
6. 泛化验证怎么做:把训练好的模型扔进两倍车流密度里测
模型训练完不意味着结束,真正被导师认可的关键一步是“换环境验证”。这里的技巧是:不改变模型任何参数,只改变仿真环境的车流密度和随机种子,把训练环境里 8 对 V2V 链路提升到 12 对或 16 对,观察成功率下降幅度。如果 8 对时成功率 90%,12 对时降到 70%,说明模型学到的是合理的资源分配策略;如果降到 40% 以下,大概率是模型靠“稀疏资源池”混过去的,没有真正学会避碰。
我的评估做法是写一个评估脚本,固定测试场景,用model.predict(obs, deterministic=True)跑 100 个独立 episode,统计三个指标:V2V 成功率、V2I 平均吞吐量、平均碰撞次数。为什么要用deterministic=True?因为训练时策略有随机性,评估时去掉随机性才能看到模型真实的决策水平。这个细节很多人容易忽略,导致评估结果虚高或者偏低不稳定。
对比实验也可以做成表格:随机分配、贪心算法(选 SINR 最好的 RB)、DQN、PPO 四行,指标列成功率、吞吐量、收敛时间。这一套做完,论文的“实验与分析”章节基本可以直接用。做这个方向最大的收获不是学会调用 stable-baselines3,而是理解马尔可夫决策过程如何投射到通信系统里,以及一个奖励函数的设计是怎么影响整条训练曲线的。我在第一次调通时也被“奖励涨但成功率不涨”卡了整整一周,最后老老实实拆奖励项打日志才定位到干扰项权重过高。所以也希望这套拆解能让你少走一些弯路,也希望帮到你。
本文还有配套的精品资源,点击获取