简介:面向无人机通信与强化学习研究者的实用源码包,聚焦无人机基站的飞行轨迹优化问题。方案融合深度强化学习与动态规划算法,内置MADQN、DyMADNQ、MADNQ等多智能体实现,可同时考虑多机协同、信号覆盖、能耗与安全性等因素,适合开展蜂窝网络吞吐量提升类仿真实验。压缩包共77个文件,包含38个Python脚本、24个txt说明、6个xml配置、4个md文档、2个mat数据及ipynb示例等,整体仅157KB,结构清晰、便于快速部署。目前已有140人学习下载,尤其适合正在研究深度强化学习应用或无人机基站部署的开发者参考。通过源码即可复现轨迹优化完整流程,理解动态规划与多智能体DRL的结合逻辑,还能依据注释与目录结构修改参数、扩展新场景,为后续算法改进提供了可直接运行的基线。
1. 深度强化学习设计无人机基站轨迹:Python 源码落地前先把问题边界算清楚
无人机背着基站飞到天上给地面用户补盲,这个场景本身不新鲜,真正让方案从“能飞”变成“能服务”的,是它的飞行轨迹到底怎么规划。轨迹优化牵扯到通信覆盖、能耗约束和时隙分配三个目标,常规的启发式算法只能做到“看起来合理”,无法逼近最优。用 Python 做深度强化学习方法加动态规划来解无人机基站轨迹问题,是目前源码实现成本最低、迭代速度最快的路径,也是通信和 AI 两个方向交叉时最容易产生实际结果的切入点。这篇文章写给两类人:刚接触强化学习的通信方向研究生,以及需要在仿真里完整跑通一条轨迹优化链路的工程人员。下面按建模、环境搭建、算法实现、训练避坑、验证产出五个部分展开,每个环节都给可照抄的代码和参数。
2. 从动态规划到深度强化学习:无人机基站轨迹问题的建模路径
2.1 问题形式化:轨迹优化的四个核心要素
把无人机基站轨迹问题写成可以计算的形式,需要四个要素:任务时隙、无人机状态、用户分布和约束条件。经典做法是把任务时间 T 切成长度相等的时隙,令 t = 1,2,…,T 表示时隙序号,无人机在二维水平面的位置记为 (x(t), y(t))。飞行高度 h 在典型部署中固定为 100 至 120 米,这个高度既能避开低层建筑物遮挡,又不至于让自由空间路径损耗过大。
地面用户的位置可以在每一轮训练开始时随机撒点,也可以按照某个真实地形分布预置。用户 i 在时隙 t 是否被无人机服务,取决于它是否落在无人机覆盖半径 R 内。这里有个常见的建模分歧:直接用理想圆形覆盖模型,还是用带衰落的三维路径损耗模型。我的经验是,第一版源码先用理想圆形把训练流程跑通,后面再切换更精细的信道模型——因为奖励函数和算法调参的复杂度已经够高,一开始引入太多物理细节只会让问题像黑匣子,出了问题根本不知道是模型错还是算法错。
目标函数通常是最大化加权吞吐量,也就是让更多用户被服务,且单用户速率不被拉低。用户速率用香农公式计算,表现为 log2(1 + SINR)。决策变量是无人机每个时隙的速度方向和位置序列,但这个序列和奖励、能耗是强耦合的。换句话说,这不是一个“选几个点飞过去”的静态问题,而是一个动态时序决策问题。很多人一开始把它当路径规划做,用 A* 或者遗传算法找一条最短路径,结果发现覆盖率和能耗完全不对,原因就在这里:轨迹优化的本质是“每一刻都在权衡覆盖数量和移动成本”,不是找一条静态路径。
2.2 动态规划的模型原理:网格离散与 Bellman 方程
动态规划的模型原理在这个问题上非常直观。把空间切成网格,状态定义为“无人机当前所在网格 + 剩余时隙数 + 用户位置快照”,动作定义为“向四个相邻网格移动或者原地悬停”。在确定性运动模型下,转移概率就是 1,奖励函数等于该时隙被服务用户数减去飞行能耗。Bellman 方程写作 V(s) = max_α [ r(s,α) + γ·V(s′) ],从最后一时隙开始后向迭代,最终得到初始状态对应的最优轨迹。
这套在小规模下表现很好。我记得第一次实验用 10×10 网格、8 个时隙,状态总数大概一万个,迭代不到十轮就收敛了。但同样的问题,把网格放大到 40×40、时隙拉到 30,状态空间直接涨到百万级,单次求解需要数分钟,而且结果还是离线算出来的——用户位置一变,就要重新跑一轮。
更深层的瓶颈不在网格大小,而在动态规划要求环境转移模型完全已知。用户一旦具备移动统计特性,P(s′|s,α) 就得重新估计,整个求解器又要重跑。这跟 01 背包问题动态规划解法不同——背包的物品种类和容量固定,状态转移不会随时间改变,而轨迹优化里每个时隙的“剩余电量”和“用户相对位置”都在变,状态空间是一个随时间演化的决策树。对比下来,DP 在这个问题里的角色更像“小规模基准答案生成器”,而不是真正可部署的求解器。
2.3 状态空间爆炸与连续动作需求:为什么切换到深度强化学习
当无人机位置从网格变成连续坐标,动作从“四个方向”变成“在 [-v_max, v_max] 内选速度矢量”,动态规划那套离散状态表示就彻底失效了。深度强化学习不存状态转移矩阵,而是用神经网络去近似 Q 值或者策略。它接收的观测向量可以是无人机当前位置、剩余电量、所有用户相对坐标、已服务用户标志、当前时隙,输出动作则取决于你选择离散控制还是连续控制。
这里有一个关键技术选型:动作空间离散且维度小时用 DQN,动作连续时用 DDPG 或 TD3。飞行轨迹天然是连续控制问题,因为无人机的转弯半径和能耗模型都要求动作具有连续性。如果你强行把动作离散成 9 个方向,训练出来的轨迹会充满折线,实际飞控很难执行。
切换到深度强化学习之后还有一个隐性收益:训练好的策略网络在每次做决策时只需要一次前向推理,时延是毫秒级,而 DP 或全局优化需要在每个决策点重新求解。这个差异在部署阶段会体现得非常明显——一个适合做实时在线轨迹调整,另一个只能做离线规划。
3. 用 Python 搭建无人机基站环境:状态、动作与奖励的参数化实现
3.1 通信模型与覆盖判断的工程取舍
先交代运行环境:Python 3.10 或 3.11 均可,深度学习框架用 PyTorch 2.0 以上,不需要额外安装 ROS 或复杂的通信仿真工具包。如果你还没把 Python 环境配好,按官方安装包装完后记得把 pip 加入环境变量,否则后面 import torch 时会莫名失败,这个细节在新手那里出现频率极高。
环境建模的第一步是通信模型。最省事的做法是假定理想圆形覆盖,用户和无人机水平距离小于 R 就算可服务。但我在实际调试当中发现,纯几何覆盖模型会导致一个典型问题:无人机只关心“覆盖了多少用户”,完全不在乎边缘用户的实际速率,最终学出来的轨迹经常是在用户群中心画一个极小的圈,运动范围比预设小得多。
修正办法是给每个用户加一个信道衰落系数,按三维空间距离计算路径损耗并折算到速率公式里。速率 r_i = B·log2(1 + P_tx·G / (N0 + I)),G 是信道增益,距离越远越小。覆盖判据从“几何距离是否小于 R”改成“速率是否高于阈值”,这样无人机必须靠近用户群中心才能获得更高奖励,而不是在覆盖半径边缘试探。这个改动会让奖励稀疏度增加,训练早期更慢,但最终学出来的轨迹质量明显更合理。
3.2 UAVBaseStationEnv:一个最小可运行的 Python 环境类
下面这个环境类覆盖 reset、step、状态拼接和奖励计算四个核心接口,风格接近 gym,可以直接对接后续的 DDPG 训练循环。
import numpy as np class UAVBaseStationEnv: def __init__(self, n_users=8, grid_size=500, h=100, r_cover=80, max_steps=50, v_max=20): self.n_users = n_users self.grid_size = grid_size # 水平面边长,单位:米 self.h = h # 固定飞行高度,米 self.r_cover = r_cover # 覆盖判定半径,简化模型 self.max_steps = max_steps self.v_max = v_max # 每个时隙最大水平位移,米 self.uav_pos = np.array([grid_size / 2, grid_size / 2]) self.users = np.zeros((n_users, 2)) self.time = 0 def reset(self): self.time = 0 self.users = np.random.rand(self.n_users, 2) * self.grid_size self.uav_pos = np.array([self.grid_size / 2, self.grid_size / 2]) return self._get_state() def _get_state(self): # 状态向量:归一化无人机位置 + 相对用户坐标 + 时隙进度 relative = (self.users - self.uav_pos).flatten() time_phase = [self.time / self.max_steps] return np.concatenate([ [self.uav_pos[0] / self.grid_size, self.uav_pos[1] / self.grid_size], relative / self.grid_size, time_phase ]) def step(self, action): # 动作是 [-1,1] 的连续速度分量 dx = float(action[0]) * self.v_max dy = float(action[1]) * self.v_max self.uav_pos[0] = np.clip(self.uav_pos[0] + dx, 0, self.grid_size) self.uav_pos[1] = np.clip(self.uav_pos[1] + dy, 0, self.grid_size) self.time += 1 reward = self._compute_reward() done = bool(self.time >= self.max_steps) return self._get_state(), reward, done, {}逻辑说明:状态向量把无人机位置和所有用户的相对坐标拼接在一起,各维度都做了归一化,神经网络不用重新适配不同物理量纲。动作输入是两个 [-1,1] 的连续值,乘以 v_max 后得到该时隙的实际位移。边界用 np.clip 限制在仿真区域内部,防止无人机飞出地图导致奖励计算失效。time_phase 是容易被忽略的一维状态,但它的作用很关键——网络需要知道“当前处于整个任务的前半段还是后半段”,才能在最后几个时隙决定是继续覆盖新用户还是返回充电点。没有时隙进度,网络学到的策略往往会忽略任务结束时间,导致轨迹“有头无尾”。
参数建议从 n_users=8 起步,太少问题过于简单,学出来的轨迹没有参考价值;太多则初期探索极其缓慢。grid_size=500、r_cover=80 时,覆盖半径与区域边长的比值约 1/6,训练难度适中。v_max=20 配合 max_steps=50,无人机理论最大飞行距离为 1000 米,而区域对角线约 700 米,这意味着无人机有足够能力到达任何位置,但必须做出取舍——它不可能在 50 个时隙里同时照顾所有角落,这种约束正是训练出“智能轨迹”的前提。
3.3 奖励函数的分阶段设计:稀疏与密度怎么平衡
奖励计算单独拆出来讲,因为它是整套源码里对收敛影响最大的模块。我第一次实现的奖励是“覆盖用户数 + 0.1×剩余电量比例”,结果训练 200 个 episode 后轨迹乱飞,核心原因是每时隙奖励差异太小,网络无法分辨哪个动作更优。
修正方案是引入速率项和“阴影区”惩罚。每个时隙的奖励计算如下:
def _compute_reward(self): reward = 0.0 served = 0 for i in range(self.n_users): dist = np.linalg.norm(self.users[i] - self.uav_pos) # 水平距离加固定高度构成三维空间距离 dist_3d = np.sqrt(dist ** 2 + self.h ** 2) if dist < self.r_cover: served += 1 rate = np.log2(1 + 1.0 / dist_3d ** 2) reward += rate # 覆盖半径外沿的用户产生微惩罚,引导轨迹重心 elif dist < self.r_cover * 1.5: reward -= 0.05 # 能耗项先给一个极小的权重,后期再放大 reward += 0.01 * self.current_battery reward -= 0.001 * (float(action[0]) ** 2 + float(action[1]) ** 2) return reward这段代码里我用 1/d² 近似信噪比,距离越近速率越高,边缘用户贡献极低。这样无人机会自然向用户密度高的区域移动,而不是待在中心不动。覆盖半径 1.5 倍以内的“阴影区”给予 -0.05 惩罚,目的是防止轨迹反复在覆盖边界试探。能耗项在早期设置 0.001 的小权重,几乎不影响训练,等奖励曲线基本稳定之后再逐步加到 0.01——这个顺序很关键,如果一开始就把能耗权重设高,无人机会为了省电原地悬停,这在后面避坑章节会具体展开。
4. DDPG 实现与训练循环源码:连续动作轨迹怎么收敛
4.1 为什么选 DDPG:连续动作空间与确定性策略的契合度
轨迹问题里动作是飞行速度的两个分量,本质上是连续变量。DQN 只能输出离散动作,最多把方向切成 8 或 9 档,训练出来的轨迹明显带折线,实际飞控系统很难跟踪。DDPG 通过确定性策略网络直接输出连续动作,配合目标网络和 OU 噪声做探索,在单智能体连续控制问题里是相对稳妥的基础方案。
TD3 在 DDPG 基础上加入了双 Q 网络和延迟更新,稳定性更好,但多出来的网络结构会增加调参负担。我的建议是第一版先跑 DDPG,确认环境建模没问题之后,再切换 TD3 做对比实验。PPO 也可以做连续控制,但它本质是随机策略迭代,在轨迹优化这种样本获取成本较高的问题上,效率低于 DDPG 和 TD3。下面的实现全部基于 DDPG。
4.2 Actor-Critic 网络定义与初始化
import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim), nn.Tanh() ) def forward(self, x): return self.net(x) class Critic(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1) ) def forward(self, s, a): return self.net(torch.cat([s, a], dim=-1))Actor 的输出层用 Tanh 把动作限制在 [-1,1],正好对应环境 step 接口的动作范围。Critic 把状态和动作拼接后输入,输出一个 Q 值。隐藏层 256 维对这个场景足够,用户数超过 20 之后再考虑拓宽到 512。网络深度不建议超过三层,轨迹优化问题的状态维度通常不超过几十,过深的网络容易过拟合训练分布,导致验证阶段奖励曲线突然掉下去。
4.3 回放缓冲区与主训练循环
回放缓冲区用双端队列实现,容量设为 20000。轨迹问题有个特点:同一个 episode 内相邻时隙的样本相关性非常强,如果样本缓冲区太小,每次采样都会集中在最近几条轨迹上,训练很容易被局部牵制。
from collections import deque import random class ReplayBuffer: def __init__(self, capacity=20000): self.buffer = deque(maxlen=capacity) def push(self, s, a, r, s2, d): self.buffer.append((s, a, r, s2, d)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) s, a, r, s2, d = map(np.stack, zip(*batch)) return (torch.FloatTensor(s), torch.FloatTensor(a), torch.FloatTensor(r).unsqueeze(1), torch.FloatTensor(s2), torch.FloatTensor(d).unsqueeze(1))主训练循环里有一个关键参数——每 episode 更新次数与采样步数的比例。标准 DDPG 通常每一步做一次更新,但轨迹问题里一个 episode 只有 50 步,样本利用率太低,所以我把更新比调到 1:5,即每收集完一个 episode 的样本后,连续做 5 次批量更新。
def train(env, actor, critic, n_episodes=500, batch_size=128, gamma=0.99, tau=0.005, lr_a=3e-4, lr_c=3e-4, seed=0): torch.manual_seed(seed) np.random.seed(seed) random.seed(seed) buffer = ReplayBuffer() target_actor = Actor(env.observation_space_dim, 2) target_critic = Critic(env.observation_space_dim, 2) target_actor.load_state_dict(actor.state_dict()) target_critic.load_state_dict(critic.state_dict()) opt_a = torch.optim.Adam(actor.parameters(), lr=lr_a) opt_c = torch.optim.Adam(critic.parameters(), lr=lr_c) for ep in range(n_episodes): s = env.reset() ep_reward = 0.0 for _ in range(env.max_steps): with torch.no_grad(): a = actor(torch.FloatTensor(s)).numpy() a += np.random.normal(0, 0.1, size=2) # 探索噪声 a = np.clip(a, -1, 1) s2, r, done, _ = env.step(a) buffer.push(s, a, r, s2, done) s = s2 ep_reward += r if done: break if len(buffer.buffer) >= 1000: for _ in range(5): # 更新比 1:5 s_b, a_b, r_b, s2_b, d_b = buffer.sample(batch_size) with torch.no_grad(): next_a = target_actor(s2_b) target_q = target_critic(s2_b, next_a) y = r_b + gamma * (1 - d_b) * target_q q = critic(s_b, a_b) loss_c = nn.functional.mse_loss(q, y) opt_c.zero_grad() loss_c.backward() opt_c.step() policy_loss = -critic(s_b, actor(s_b)).mean() opt_a.zero_grad() policy_loss.backward() opt_a.step() for tp, p in zip(target_actor.parameters(), actor.parameters()): tp.data.copy_(tau * p.data + (1 - tau) * tp.data) if ep % 20 == 0: print(f"ep {ep}: reward={ep_reward:.2f}")参数说明:gamma=0.99 适合 50 时隙的任务长度,如果把 max_steps 拉到 100 以上,建议改成 0.99 以下,否则远距离奖励的衰减过慢。tau=0.005 控制目标网络更新速度,这个值一般不需要动。学习率 actor 和 critic 统一用 3e-4,没有采用分层学习率,因为实验结果显示在这个环境里调高 critic 学习率并不会带来更快收敛。噪声标准差 0.1 是初始值,后面避坑章节会讲如何让噪声随训练进度衰减。
另一个值得强调的细节是随机种子。轨迹优化任务对初始用户分布高度敏感,如果不固定 seed,每轮训练结果差异会非常大,很难判断一个改动到底是有效还是运气好。所有实验都固定 seed=0,这是保证实验结果可复现的最低成本操作。
4.4 训练曲线中值得看的三个信号
第一,平均 episode reward 是否收敛到稳定区间。正常情况会在 100 个 episode 内出现明显上升,300 个 episode 以后波动收窄。如果 reward 前后不升,优先检查奖励函数而不是调网络结构。第二,actor 的 policy loss 是否持续增长但增速放缓——这是 DDPG 的正常行为,如果 policy loss 变成剧烈振荡,说明 critic 估值不稳定,需要调低 critic 学习率。第三,单条轨迹里用户覆盖时隙的分布。如果无人机在最后 20% 的时隙里飞行距离突然增大,说明网络学到了“时间快结束前赶紧飞一圈覆盖更多用户”的投机策略,这时候需要在奖励函数里加一个轨迹平滑项或者能耗惩罚来抑制。
5. 无人机基站轨迹训练避坑:五个翻车场景和处理记录
5.1 奖励函数过早加入能耗项,无人机原地悬停
现象:训练 200 个 episode,reward 曲线一路下跌到负值,输出的轨迹显示无人机始终停在初始点附近。
原因:能耗项权重过高,网络发现“动一下奖励降得比覆盖收益还快,原地不动则持平”,于是选择了驻停策略。这是轨迹优化里最常见的奖励工程翻车。
解决:第一阶段把能耗项权重置 0,只保留覆盖和速率奖励,等平均 reward 稳定在正值区间后,再以 0.001 的小系数引入能耗项,每 50 个 episode 观察一次曲线走势,逐步提高系数直到轨迹既覆盖用户又不过度飞行。改完之后原地悬停现象基本消失。
5.2 状态归一化遗漏,训练几乎不收敛
现象:reward 曲线波动剧烈,同样的超参数重复跑两次,结果差异非常大。
原因:状态向量里混入了未经归一化的绝对坐标和用户相对坐标,不同维度的量纲差距让神经网络梯度在训练早期来回摆动,收敛极不稳定。这在把多个物理量直接拼接进状态向量时非常容易发生。
解决:把所有距离量除以 grid_size,时隙计数除以 max_steps,让状态向量的每一维都在 [0,1] 区间。改完之后,同一个 seed 下收敛曲线的可重复性大幅提升,不同 seed 之间的曲线包络也变窄了。
5.3 探索噪声固定方差,策略陷入局部最优
现象:训练后期 reward 卡在某个上限不再增长,轨迹只在区域的一小块范围内反复飞。
原因:高斯噪声的方差固定为 0.1,训练后期策略已经收敛到某个精细动作范围,0.1 的噪声幅度反而远大于策略本身的调整量,网络无法走出当前的局部最优点。
解决:把噪声方差改为随 episode 线性衰减,从 0.3 衰减到 0.02。前 50 个 episode 保持高噪声充分探索,后面逐渐降低让策略精细化。如果衰减太慢会拉长训练时间,太快则会提前收敛到局部最优。也可以直接切换到 TD3 算法,用目标策略平滑正则来替代噪声衰减。
5.4 Actor 输出 Tanh 之后未做二次限幅,轨迹频繁触边
现象:无人机轨迹频繁在仿真区域边界来回往返,看起来像“蹭墙”飞行。
原因:Actor 输出是 [-1,1] 的连续值,在环境内部乘上 v_max 后虽然做了边界 clip,但没有限制最小转弯半径。当无人机到达边界时,前一帧动作让它往右飞,后一帧为了让速度归零又得大反向动作,于是产生了边界往返跳变。
解决:在环境 step 内部对 dx/dy 做二次限幅,同时加入最小位移约束——位移量小于 0.5 米时直接视为悬停,动作输出置零。这个改动对轨迹平滑性改善明显,而且不影响覆盖收益,因为悬停本身不产生额外能耗。
5.5 验证集与训练环境分布不一致,部署效果打对折
现象:训练环境固定用户在 500 米×500 米全域随机撒点,验证时改成用户集中在角落 250 米×250 米区域,发现 reward 掉到原来的一半左右。
原因:状态归一化参数是按训练全域计算的,验证时用户集中在角落后,相对坐标集中在 0.1 到 0.2 区间,网络输入的分布已经完全偏移,学到的策略自然失效。
解决:验证集沿用训练时的归一化参数,但把用户分布半径从全域随机改成半域随机,观察网络能否学到位置无关的覆盖策略。如果半域验证时 reward 显著下降,说明网络过拟合到了“全域撒点”这种特定场景。应该在训练阶段就用不同撒点半径做混合采样,提高策略的泛化能力。
6. 闭环验证飞行轨迹:可视化检查、基准对比与工程文件管理
轨迹训练完成后,第一步是画出学习到的轨迹与用户位置的关系图。把无人机每个时隙的位置按时间顺序连成折线,叠加用户散点作为背景,检查三个特征:轨迹是否平滑,是否优先经过用户密集区域,末端是否回到任务初始点。第三步在很多文献里被忽略,但如果任务是“连续服务型”而不是“单次巡航”,无人机的回收位置必须在训练奖励里显式建模,否则学出来的轨迹会忽略返航动作。
第二步是和动态规划的基准对比。小规模场景(网格 10×10、时隙 15)下 DP 能算出近似最优解。用同一组用户分布分别跑 DP 和 DRL,对比总吞吐量和平均时隙覆盖数。DRL 与 DP 的差距在 15% 以内属于可接受范围——DP 做的是全局离线优化,DRL 做的是在线近似决策,这点性能差换来了部署时的实时性,是划算的置换。
工程化落到源码层面,我会做三件事:把所有超参数和 reward 权重写进每次实验的配置字典并随结果一起存储,训练结束后把轨迹保存成 JSON 文件,字段包括时隙序号、位置、速度和覆盖用户 ID。这样后续做对比实验时,脚本能直接读取历史轨迹计算覆盖率统计,不必重新跑训练。另一个我反复犯过又改掉的教训是:每次实验都要在输出文件名里带上 reward 权重、噪声衰减率、seed 这三个关键信息,否则一周之后翻目录,你会发现十个 exp_result_500 文件里根本分不清谁是谁。说出来有点丢人,但这个习惯至少帮我省了三次重复实验的钱。
这套方法的价值不在做到全网最优,而在于给你一个确定性足够高的起点:环境可以跑、算法收敛、轨迹可验证、坑有人替你踩过。希望帮到你。
本文还有配套的精品资源,点击获取