ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于PyTorch的DQN无人机避障实战:从仿真环境到智能体训练

基于PyTorch的DQN无人机避障实战:从仿真环境到智能体训练 做无人机避障项目时我被一个问题困扰了很久雷达数据明明能测到障碍物程序也知道目标在哪可真要让无人机自己飞过去规则怎么写都不对劲。后来我把这个问题完全交给强化学习处理用Python搭仿真环境、用PyTorch实现DQN算法从随机乱撞开始训练最终得到一个能主动躲开障碍、顺利到达目标点的智能体。整个过程从环境搭建到实战训练大概花了两周中间踩了不少坑。这篇文章把完整思路和可运行代码都整理出来适合刚接触强化学习、想用DQN解决实际控制问题的朋友。1. 先把问题想清楚避障任务为什么适合用DQN解决1.1 无人机避障的本质是一个序列决策问题无人机从起点飞到终点每一时刻都要做一次选择直行、左转还是右转。这个选择会影响后续所有状态——可能在两秒后撞上障碍物也可能绕出一条更远但安全的路。这种“当前动作影响未来结果”的任务本质上是马尔可夫决策过程而强化学习处理的正是这一类问题。和普通监督学习不同避障任务里没有现成的“正确动作”标签。比如无人机离障碍物3米时该往左还是往右没有一个标准答案取决于目标方向、飞行速度、周围障碍物分布等多种因素。单纯靠人工标注数据来训练一个分类器既难覆盖所有场景也很难处理连续决策中的时序关联。强化学习的策略是不断试错用奖励信号来告诉智能体“这次做得好不好”然后逐步调整策略。1.2 DQN相比规则法和传统Q-Learning的优势手写规则的思路是探测到障碍物距离小于某个阈值就转弯。听起来简单实际做起来很痛苦。障碍物在左边还是右边距离多近该转转弯角度多大密集成片的障碍物怎么处理每条规则只能覆盖一种情况规则之间还容易冲突。我在初期就写过一版规则避障逻辑分支几十条换一个场景就失灵完全不可维护。DQN走的是另一条路把“当前状态”映射到“每个动作的价值”。Q-Learning本身就能做这件事但传统Q表需要离散化状态。无人机的传感器数据是连续值如果强行分箱状态空间会爆炸假设距离值分成20档、角度分成20档组合起来就几百上千个状态实际存不下也学不好。DQN的关键是用深度神经网络代替Q表直接从连续状态中拟合Q值效果和扩展性都好得多。1.3 仿真先行为什么先在二维环境验证我强烈建议第一步不要直接上真机甚至不要先上三维仿真先在二维平面里把算法跑通。二维环境里状态表达直观、训练速度快一个episode通常几秒就结束方便反复调试奖励函数和网络结构。等DQN完全收敛、避障行为稳定后再把逻辑迁移到Gazebo或AirSim这类三维仿真平台。二维环境看起来简单但它保留了强化学习的核心难点状态连续性、时延决策、稀疏奖励。算法能在二维问题里学会避障迁移到三维只是改状态输入和动作接口的问题。2. 环境搭建Anaconda虚拟环境与PyTorch安装的版本细节2.1 创建独立的Python虚拟环境我建议用Anaconda管理项目环境不要直接装在系统Python里。一个项目一套环境是基本原则避免后面装其他库时互相冲突版本。创建命令如下conda create -n uav_dqn python3.9 conda activate uav_dqnPython版本选3.9比较稳妥兼容PyTorch全系列版本。如果你机器上已经装了Anaconda直接用上面的命令创建环境即可。不想装Anaconda的话用python -m venv uav_dqn建立虚拟环境也完全可行只是后面管理CUDA相关依赖时conda会更省心。2.2 PyTorch的CPU版与GPU版安装安装PyTorch之前先确认机器有没有独立NVIDIA显卡再用nvidia-smi查看CUDA版本。我的机器是RTX 3060笔记本显存6GB所以装了CUDA版本的PyTorch。# CPU版本 pip install torch torchvision # GPU版本以CUDA 11.8为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这里有一个反复出现的坑很多人安装了GPU版PyTorch后torch.cuda.is_available()返回False。原因通常是显卡驱动版本太低或者安装的PyTorch对应的CUDA版本与驱动不兼容。排查顺序很简单先看驱动支不支持目标CUDA版本再确认安装命令里指定的CUDA版本号是否正确。注意如果你只是想在本文环境里跑通DQNCPU版本完全够用。DQN网络本身很小CPU训练这个避障任务也就慢20%左右没必要为了这点性能去折腾CUDA。2.3 依赖库清单与安装验证除了PyTorch还需要几个基础库numpy负责数值计算matplotlib负责绘制训练曲线。安装命令pip install numpy matplotlib安装完成后用下面这段命令验证环境是否正常import torch import numpy as np print(PyTorch version:, torch.__version__) print(CUDA available:, torch.cuda.is_available()) print(Numpy version:, np.__version__)确认能正常打印版本号环境这步就算完成了。整个过程里唯一容易出问题的就是GPU版本如果发现torch.cuda.is_available()为False直接把PyTorch降级为CPU版本不要在上面浪费太多时间。3. 仿真环境设计状态空间、动作空间与奖励函数3.1 场景与坐标系定义我设计的仿真场景是一个100×100的平面空间无人机从左下角出发目标是右上角中间随机分布6个圆形障碍物。坐标轴定义很简单x轴向右y轴向上无人机位置用二维坐标表示朝向用与x轴正方向的夹角表示单位是弧度。环境类的核心职责是维护无人机和障碍物的位置执行动作后更新无人机位置计算传感器读数判断是否碰撞或到达目标。定义如下class UAVEnv: def __init__(self, width100.0, height100.0, sensor_num8, sensor_range25.0): self.width width self.height height self.sensor_num sensor_num self.sensor_range sensor_range self.obstacle_radius 8.0 self.collision_dist 2.0 self.arrive_dist 3.0 self.max_steps 200 self.start_pos np.array([8.0, 8.0]) self.target_pos np.array([width - 8.0, height - 8.0]) rng np.random.default_rng(42) self.obstacles [] for _ in range(6): while True: x rng.uniform(20, self.width - 20) y rng.uniform(20, self.height - 20) if (np.linalg.norm([x - self.start_pos[0], y - self.start_pos[1]]) 25 and np.linalg.norm([x - self.target_pos[0], y - self.target_pos[1]]) 25): self.obstacles.append((x, y, self.obstacle_radius)) break障碍物生成采用拒绝采样先随机选坐标如果落在起点或终点附近25单位范围内就重新采样保证每个episode的初始状态是合理的。固定随机种子42的好处是训练结果可复现方便排查问题。后面如果想增加环境随机性把default_rng(42)改成default_rng()即可。3.2 激光雷达式的状态感知设计状态空间是整个环境设计里最关键的环节直接决定算法能不能学到有效策略。我参考了激光雷达的工作原理从无人机当前位置向8个方向发射射线每个方向间隔45度返回该方向上最近障碍物的距离。为什么选8个方向而不是4个或16个我做过对比实验4个方向感知太粗糙无人机经常“看”不到侧后方的障碍物16个方向信息量更足但状态维度翻倍训练收敛变慢而避障效果提升不明显。8个方向在感知精度和训练效率之间比较平衡。除了障碍物距离信息状态里还需要包含目标相关信息目标相对距离和目标相对角度。否则无人机只知道哪边有障碍不知道往哪边飞才能到终点。完整状态向量如下状态分量维度说明归一化方式传感器读数88个方向的障碍物最近距离除以最大探测距离25目标相对距离1无人机到目标的欧氏距离除以场地尺寸200目标相对角度1目标方向与当前朝向的夹角除以π所有状态分量都归一化到大致[-1, 1]或[0, 1]区间这个细节很重要。神经网络对输入数值范围敏感如果原始距离值从0到100直接喂进去数值大的维度会主导梯度更新导致收敛极慢甚至不收敛。状态计算的核心是射线与圆形障碍物的求交。我实现的_raycast方法逻辑如下把无人机位置作为射线起点从起点指向障碍物圆心得到向量计算这个向量在射线方向上的投影。如果投影为负说明障碍物在无人机身后忽略。然后计算圆心到射线的垂直距离如果垂直距离大于障碍物半径说明射线不会穿过障碍物。否则用勾股定理求出交点距离。def _get_state(self): sensor_readings [] for i in range(self.sensor_num): angle self.uav_heading i * (2.0 * math.pi / self.sensor_num) sensor_readings.append(self._raycast(angle) / self.sensor_range) target_dx self.target_pos[0] - self.uav_pos[0] target_dy self.target_pos[1] - self.uav_pos[1] target_dist math.hypot(target_dx, target_dy) / (self.width self.height) target_angle math.atan2(target_dy, target_dx) angle_diff (target_angle - self.uav_heading math.pi) % (2.0 * math.pi) - math.pi angle_diff / math.pi return np.array(sensor_readings [target_dist, angle_diff], dtypenp.float32) def _raycast(self, angle): direction np.array([math.cos(angle), math.sin(angle)]) min_dist self.sensor_range for ox, oy, r in self.obstacles: oc np.array([ox, oy]) - self.uav_pos proj np.dot(oc, direction) if proj 0: continue perpendicular np.linalg.norm(oc - proj * direction) if perpendicular r: dist proj - math.sqrt(max(r * r - perpendicular * perpendicular, 0)) if 0 dist min_dist: min_dist dist return min_dist3.3 动作空间与转向模型动作空间我选择了5个离散动作直行、左转15度、右转15度、左转30度、右转30度。每次执行动作后无人机先转向再沿当前朝向直线前进2个单位长度。这里有一个设计权衡为什么不把转向角设为连续值理论上DQN能输出连续动作但连续动作空间的探索效率极低算法需要大量样本才能找到合适的精确角度对于二维避障来说没有必要。离散动作简单直接5个动作足够覆盖“微调方向”和“快速转向”两种需求训练难度也低很多。为什么不直接用360度的转向转向步长太大会导致无人机左右摇摆轨迹呈锯齿状而且容易“跳过”正确方向。15度和30度两个档位分别负责精细调整和快速避障实际效果比单一档位好。def _apply_action(self, action): if action 0: delta 0.0 elif action 1: delta math.radians(15) elif action 2: delta -math.radians(15) elif action 3: delta math.radians(30) elif action 4: delta -math.radians(30) else: raise ValueError(fInvalid action: {action}) self.uav_heading delta self.uav_pos[0] 2.0 * math.cos(self.uav_heading) self.uav_pos[1] 2.0 * math.sin(self.uav_heading) self.uav_pos[0] min(max(self.uav_pos[0], 0.0), self.width) self.uav_pos[1] min(max(self.uav_pos[1], 0.0), self.height)3.4 奖励函数设计的细节与演进过程奖励函数是强化学习里最需要用心的地方它直接决定智能体学到的行为模式。我最初的版本很简单碰撞给-100到达给100其余步骤奖励为0。结果训练了几百轮智能体完全学不会避障因为绝大多数状态下反馈都是0它根本不知道哪些动作是有意义的。后来我加入了“距离变化引导”和“步数惩罚”奖励函数变成事件奖励值设计原因到达目标100最终目标大额正向奖励碰撞障碍-100强烈惩罚危险行为每步基础惩罚-0.01鼓励用最短路径到达距离缩短奖励0.5 × 距离减少量引导无人机朝目标前进距离缩短奖励是关键。每执行一步动作计算移动前后无人机到目标点距离的变化距离缩短了就给予正奖励变远了则给负奖励。相当于把稀疏的大目标奖励“铺”到了每一步让智能体即使没有到达终点也能感受到当前动作是好是坏。这个技术叫Reward Shaping奖励塑形。但奖励权重需要调不能太大。我曾经把距离奖励权重设为2.0结果无人机确实一路冲向目标但完全无视障碍物因为高速接近目标带来的单步奖励约2到3远大于未来碰撞的远期惩罚-100被折扣系数稀释成小值学出来是个“莽夫”。把权重降到0.5后避障行为明显改善。完整的step方法如下def step(self, action): prev_dist np.linalg.norm(self.uav_pos - self.target_pos) self._apply_action(action) self.step_count 1 current_dist np.linalg.norm(self.uav_pos - self.target_pos) reward -0.01 terminated False truncated False if self._check_collision(): reward -100.0 terminated True elif current_dist self.arrive_dist: reward 100.0 terminated True else: # 距离减少量归一化到步长2对应的尺度 reward 0.5 * (prev_dist - current_dist) / 2.0 if not terminated and self.step_count self.max_steps: truncated True return self._get_state(), reward, terminated, truncated注意step方法返回的terminated表示episode因碰撞或到达而结束truncated表示因步数超限被强制截断。这两者的概念在DQN训练里要区分计算目标Q值时只有terminated才需要把未来奖励置0truncated情况下一步还会继续。碰撞检测的实现也比较直观def _check_collision(self): for ox, oy, r in self.obstacles: if np.linalg.norm(self.uav_pos - np.array([ox, oy])) self.collision_dist r: return True return False3.5 完整环境代码整合把上述片段组合起来就是完整的UAVEnv类。reset方法负责把无人机放回起点、重置朝向和步数计数并返回初始状态import math import numpy as np class UAVEnv: def __init__(self, width100.0, height100.0, sensor_num8, sensor_range25.0): self.width width self.height height self.sensor_num sensor_num self.sensor_range sensor_range self.obstacle_radius 8.0 self.collision_dist 2.0 self.arrive_dist 3.0 self.max_steps 200 self.start_pos np.array([8.0, 8.0]) self.target_pos np.array([width - 8.0, height - 8.0]) rng np.random.default_rng(42) self.obstacles [] for _ in range(6): while True: x rng.uniform(20, self.width - 20) y rng.uniform(20, self.height - 20) if (np.linalg.norm([x - self.start_pos[0], y - self.start_pos[1]]) 25 and np.linalg.norm([x - self.target_pos[0], y - self.target_pos[1]]) 25): self.obstacles.append((x, y, self.obstacle_radius)) break def reset(self): self.uav_pos self.start_pos.copy() self.uav_heading 0.0 self.step_count 0 return self._get_state() def step(self, action): prev_dist np.linalg.norm(self.uav_pos - self.target_pos) self._apply_action(action) self.step_count 1 current_dist np.linalg.norm(self.uav_pos - self.target_pos) reward -0.01 terminated False truncated False if self._check_collision(): reward -100.0 terminated True elif current_dist self.arrive_dist: reward 100.0 terminated True else: reward 0.5 * (prev_dist - current_dist) / 2.0 if not terminated and self.step_count self.max_steps: truncated True return self._get_state(), reward, terminated, truncated def _apply_action(self, action): if action 0: delta 0.0 elif action 1: delta math.radians(15) elif action 2: delta -math.radians(15) elif action 3: delta math.radians(30) elif action 4: delta -math.radians(30) else: raise ValueError(fInvalid action: {action}) self.uav_heading delta self.uav_pos[0] 2.0 * math.cos(self.uav_heading) self.uav_pos[1] 2.0 * math.sin(self.uav_heading) self.uav_pos[0] min(max(self.uav_pos[0], 0.0), self.width) self.uav_pos[1] min(max(self.uav_pos[1], 0.0), self.height) def _get_state(self): sensor_readings [] for i in range(self.sensor_num): angle self.uav_heading i * (2.0 * math.pi / self.sensor_num) sensor_readings.append(self._raycast(angle) / self.sensor_range) target_dx self.target_pos[0] - self.uav_pos[0] target_dy self.target_pos[1] - self.uav_pos[1] target_dist math.hypot(target_dx, target_dy) / (self.width self.height) target_angle math.atan2(target_dy, target_dx) angle_diff (target_angle - self.uav_heading math.pi) % (2.0 * math.pi) - math.pi angle_diff / math.pi return np.array(sensor_readings [target_dist, angle_diff], dtypenp.float32) def _raycast(self, angle): direction np.array([math.cos(angle), math.sin(angle)]) min_dist self.sensor_range for ox, oy, r in self.obstacles: oc np.array([ox, oy]) - self.uav_pos proj np.dot(oc, direction) if proj 0: continue perpendicular np.linalg.norm(oc - proj * direction) if perpendicular r: dist proj - math.sqrt(max(r * r - perpendicular * perpendicular, 0)) if 0 dist min_dist: min_dist dist return min_dist def _check_collision(self): for ox, oy, r in self.obstacles: if np.linalg.norm(self.uav_pos - np.array([ox, oy])) self.collision_dist r: return True return False4. DQN核心代码拆解Q网络、经验回放与目标网络4.1 Q网络结构设计DQN的神经网络结构不用太复杂三全连接层足够处理这个10维状态输入的任务。第一层128个神经元第二层128个神经元输出层5个神经元对应5个动作的Q值。import torch import torch.nn as nn import torch.optim as optim import numpy as np import random from collections import deque class DQNNetwork(nn.Module): def __init__(self, state_dim10, action_dim5, hidden_dim128): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim) ) def forward(self, x): return self.net(x)网络结构这里有个经验之谈不是网络越大越好。我试过把隐藏层加到256×256训练速度明显变慢但最终效果和128相当。对于这种低维输入的决策问题一层网络很难拟合复杂非线性关系两层是性价比较高的选择。层数继续增加到4层在当前的任务规模下只会增加过拟合风险。4.2 经验回放的设计与作用经验回放是DQN相比传统Q-Learning的重要改进。在Q-Learning中转移样本(state, action, reward, next_state, done)是逐个更新模型的前后样本之间存在强相关性网络参数更新时容易被连续相关的数据带偏训练不稳定。经验回放的做法是把样本存进一个缓冲区训练时随机抽取一批样本更新网络打破数据相关性。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 np.array([x[0] for x in batch], dtypenp.float32) actions np.array([x[1] for x in batch], dtypenp.int64) rewards np.array([x[2] for x in batch], dtypenp.float32) next_states np.array([x[3] for x in batch], dtypenp.float32) dones np.array([x[4] for x in batch], dtypenp.float32) return ( torch.FloatTensor(states), torch.LongTensor(actions).unsqueeze(1), torch.FloatTensor(rewards), torch.FloatTensor(next_states), torch.FloatTensor(dones), ) def __len__(self): return len(self.buffer)缓冲区容量我设为10万条。容量太小早期样本很快被覆盖智能体会“遗忘”刚开始的探索经验容量过大则取样时容易抽到过时数据导致策略更新滞后。10万条对应大约几百个episode的样本量在这个项目里刚好合适。4.3 目标网络为什么需要以及怎么用目标网络的引入是为了解决Q值“自举”的问题——训练时用目标Q值来更新当前Q值如果目标Q值本身就是网络自己预测的容易形成正反馈循环导致Q值估值越来越大训练发散。解决办法是复制一份网络参数不实时更新每隔固定步数才同步一次。当前网络负责输出预测Q值目标网络负责计算目标Q值两者之间存在“时间差”避免了自举带来的不稳定性。class DQNAgent: def __init__(self, state_dim10, action_dim5, lr1e-3, gamma0.99, capacity100000, batch_size64, target_sync_interval100): self.action_dim action_dim self.gamma gamma self.batch_size batch_size self.target_sync_interval target_sync_interval self.eval_net DQNNetwork(state_dim, action_dim) self.target_net DQNNetwork(state_dim, action_dim) self.target_net.load_state_dict(self.eval_net.state_dict()) self.target_net.eval() self.optimizer optim.Adam(self.eval_net.parameters(), lrlr) self.memory ReplayBuffer(capacity) self.train_step 0 def choose_action(self, state, epsilon0.05): if np.random.rand() epsilon: return np.random.randint(self.action_dim) state_tensor torch.FloatTensor(state).unsqueeze(0) with torch.no_grad(): q_values self.eval_net(state_tensor) return int(torch.argmax(q_values).item()) def update(self): if len(self.memory) self.batch_size: return 0.0 states, actions, rewards, next_states, dones self.memory.sample(self.batch_size) q_values self.eval_net(states).gather(1, actions).squeeze(1) with torch.no_grad(): next_q_values self.target_net(next_states).max(1)[0] targets rewards self.gamma * next_q_values * (1.0 - dones) loss nn.MSELoss()(q_values, targets) self.optimizer.zero_grad() loss.backward() self.optimizer.step() self.train_step 1 if self.train_step % self.target_sync_interval 0: self.target_net.load_state_dict(self.eval_net.state_dict()) return loss.item()target_sync_interval100是我多次实验后的选择。同步太频繁比如每步都同步目标网络就失去了“时间差”意义和单网络没区别同步太慢比如1000步目标Q值会严重滞后影响收敛速度。100步在这个项目中大约是1到2个episode既能保持目标稳定又能跟上策略变化。4.4 训练主循环与环境交互流程训练主循环的逻辑很清晰每个episode先重置环境拿到初始状态然后反复执行“选择动作→执行动作→存入经验→采样更新”的流程直到episode结束。这里需要引入epsilon-greedy探索策略——以一定概率随机选择动作以保证初始阶段能尽可能多地探索环境。def train(episodes1000, batch_size64): env UAVEnv() agent DQNAgent(state_dim10, action_dim5, batch_sizebatch_size) epsilon 1.0 epsilon_min 0.02 epsilon_decay 0.995 rewards_history [] losses_history [] for episode in range(episodes): state env.reset() total_reward 0.0 episode_loss 0.0 update_count 0 while True: action agent.choose_action(state, epsilon) next_state, reward, terminated, truncated env.step(action) done terminated or truncated agent.memory.push(state, action, reward, next_state, done) loss agent.update() if loss 0: episode_loss loss update_count 1 state next_state total_reward reward if done: break epsilon max(epsilon_min, epsilon * epsilon_decay) rewards_history.append(total_reward) losses_history.append(episode_loss / max(update_count, 1)) if (episode 1) % 50 0: avg_reward np.mean(rewards_history[-50:]) print(fEpisode {episode 1}, Average Reward: {avg_reward:.2f}, Epsilon: {epsilon:.3f}) return agent, rewards_history, losses_history训练时有一个容易忽略的问题epsilon的衰减节奏。经典写法是每个episode乘一个衰减系数但这样会造成一个偏差——如果某个episode特别长比如200步才结束这个episode里收集的经验非常多epsilon变化却很慢如果某个episode特别短比如几步就撞了epsilon又降得过快。更合理的方式是按总步数衰减。我在项目里最终选择了按episode衰减因为当前环境每个episode的步数差异不大但对更复杂的场景建议改成按全局步数衰减实现起来也简单把衰减逻辑挪到训练循环内部即可。epsilon的初始值设为1.0即完全随机探索。衰减到0.02就停止保留2%的随机性防止策略陷入局部最优。5. 训练实测与调参记录从奖励不收敛到稳定避障5.1 超参数配置与收敛过程我的最终超参数配置如下超参数数值说明Episode数1000训练轮数批量大小64每次采样更新网络的样本数学习率1e-3Adam优化器默认配置可跑通折扣因子γ0.99重视长期收益目标网络同步间隔100步每100步同步一次目标网络经验池容量100000最大存储样本数epsilon初始值1.0完全随机探索epsilon最小值0.02保留一定随机性epsilon衰减率0.995/episode每个episode乘一次用这套参数训练前100个episode的reward曲线几乎是平的偶尔出现几个负的尖峰这是因为epsilon很大无人机还在大量随机探索经常碰撞。200个episode之后reward开始缓慢攀升此时epsilon降到0.37左右智能体开始学会利用已有经验。400到600个episode之间reward出现明显增长平均reward从负转正。700个episode以后基本稳定在正数区间说明智能体已经能稳定避开障碍物。训练完成后我单独跑了一轮评估用epsilon0完全贪婪策略连续测试10个episode平均reward约75分其中有8次成功到达目标2次在密集障碍区域发生碰撞。对于二维仿真环境这个表现已经算合格了。5.2 三个我实际踩过的坑及排查过程第一个坑是训练不收敛reward始终在0以下徘徊。当时我怀疑是网络结构问题试过加深加宽网络没有效果。后来打印每个episode的平均Q值才发现Q值的绝对值在持续变大说明是目标网络同步频率过低导致Q值自举发散。把同步间隔从1000步改成100步后训练逐渐恢复正常。这个问题的排查思路是reward不涨不一定代表没在学习需要看Q值是否稳定、loss是否异常。第二个坑是无人机学会“绕远路”。具体表现是到达目标时间很长reward能拿正数但偏低。原因分析步数惩罚设置不合理当时每步惩罚是-0.5而距离缩短奖励只有0.5×(距离减少量/2)。操作下来无人机宁可绕开所有障碍物也不走折线抄近路因为每一次靠近障碍物的动作都会带来负的距离变化惩罚。解决办法是调大距离奖励权重同时把步数惩罚降到-0.01让无人机在“安全”和“快速”之间找到平衡。第三个坑是训练后期reward震荡得很厉害。这其实是学习率和batch_size联合导致的learning rate3e-3时梯度步长太大Q值更新容易幅度过猛特别是在批量样本中存在少量碰撞样本时loss会突然增大。把learning rate降回1e-3震荡幅度明显减小。提示如果训练过程中出现了lossnan优先检查reward是否出现了极大值比如超过1e6以及网络里有没有做数值不稳定的操作。可以在update方法中加上torch.nn.utils.clip_grad_norm_(self.eval_net.parameters(), 10.0)做梯度裁剪防患于未然。5.3 评估模型不只是看reward很多人训练完只看奖励曲线就下了结论我不建议这么做。奖励曲线只能反映整体趋势无法体现避障行为的具体质量。我习惯做三类评估第一类是成功率评估。连续跑100个episode统计成功到达目标的次数占比。这个指标最直接成功率70%以上说明策略已经可用了。第二类是轨迹可视化。把无人机每个episode的飞行轨迹画出来观察是否有明显异常行为比如原地打转、频繁急转弯、贴着障碍物飞等。轨迹图能暴露很多reward曲线反映不出的问题。import matplotlib.pyplot as plt def evaluate_and_plot(agent, env, renderTrue): state env.reset() positions [env.uav_pos.copy()] total_reward 0.0 while True: action agent.choose_action(state, epsilon0.0) state, reward, terminated, truncated env.step(action) positions.append(env.uav_pos.copy()) total_reward reward if terminated or truncated: break print(fTest reward: {total_reward:.2f}) if render: positions np.array(positions) fig, ax plt.subplots(figsize(6, 6)) for ox, oy, r in env.obstacles: circle plt.Circle((ox, oy), r, fillTrue, colorgray, alpha0.5) ax.add_patch(circle) ax.plot(positions[:, 0], positions[:, 1], b-, linewidth2) ax.scatter([env.start_pos[0]], [env.start_pos[1]], cgreen, s80, labelstart) ax.scatter([env.target_pos[0]], [env.target_pos[1]], cred, s80, labeltarget) ax.set_xlim(0, env.width) ax.set_ylim(0, env.height) ax.legend() plt.show()第三类是泛化测试。用固定随机种子42生成的障碍物只有一种布局模型很可能只是“背下”了这个特定布局。我会重新生成新的障碍物布局测试模型看它能否在没见过的场景里同样避障。这个测试非常关键——它能说明模型学到的是通用的“避障策略”而不是“记住地图”。我在实验中把default_rng(42)改成default_rng()生成新布局测试成功率从80%掉到65%说明模型有一定泛化能力但仍然受训练场景分布限制。解决办法是在训练时让每个episode随机生成障碍物布局相当于变相增加训练样本多样性。修改后的环境类只需把reset方法里的障碍物生成逻辑移到每次reset时重新执行即可这也是环境设计上最值得做的一项改进。6. 从仿真到真机差距在哪下一步往哪走6.1 仿真环境与真实环境的差异二维仿真里跑通的策略不能直接部署到真机上。真实无人机面临几个明显的差异传感器有噪声激光雷达读数不是精确的射线距离而是带误差的测量值状态反馈存在延迟从传感器采集到动作执行有几十毫秒的时延无人机本身有动力学约束不能瞬间转向到指定角度而是有一个加速、减速、转弯的过程。应对办法是在仿真里加入这些“不理想因素”。比如给传感器读数加高斯噪声给动作加随机扰动把单步转向改成渐进转向。强化学习的一个优势是只要环境建模得够真实训练出的策略天然能适应这些扰动。我建议在二维仿真中加入一个小幅度噪声sensor_readings np.random.normal(0, 0.02, size...)再观察模型表现往往会有惊喜。6.2 算法层面的改进方向DQN本身有已知的缺点Q值过估计导致策略过于激进。经典改进方案是Double DQN核心改动只有几行代码——在计算目标Q值时用当前网络选动作用目标网络算Q值而不是直接取目标网络的最大Q值。# Double DQN的目标计算方式 with torch.no_grad(): next_actions self.eval_net(next_states).argmax(dim1, keepdimTrue) next_q_values self.target_net(next_states).gather(1, next_actions).squeeze(1) targets rewards self.gamma * next_q_values * (1.0 - dones)这个改动能明显缓解训练中期的Q值虚高现象。另一个值得尝试的是Dueling DQN把Q值拆成状态价值和动作优势两部分在动作空间较大时能更快收敛。如果你想让避障策略更接近真实无人机的感知方式可以考虑用视觉图像作为状态输入。方法是在环境里渲染出一张俯视图用CNN网络提取特征再交给强化学习网络输出动作。这样状态空间就从10维变成了图像矩阵训练难度会大幅增加但更贴近真实场景中“用相机避障”的需求。AirSim这类仿真平台可以直接输出第一人称或俯视相机图像搭配上面的环境逻辑改造即可。6.3 从二维仿真到三维仿真的迁移路径三维仿真推荐从Gazebo或AirSim入手。Gazebo配合PX4飞控可以从激光雷达的/scan话题读取障碍物距离这个数据格式和本文设计的8维传感器向量高度相似只是维度更多、更新频率更高。控制层面把离散转向动作翻译成cmd_vel话题的速度指令即可。迁移时要做好两类适配工作。一是状态空间维度变了传感器从8路变成360路需要重新设计网络输入层二是时间步长不同仿真平台的决策周期是固定频率比如10Hz需要把每个动作在一个周期内匀速执行而不是像二维仿真那样瞬间完成。核心的DQN训练逻辑、经验回放、目标网络机制都不需要改动这也是在二维环境里把算法基础打扎实的价值所在。我做这个项目最大的体会是DQN难的不是搭网络而是让智能体学会“会避障”和“会到达”这两件事的平衡。奖励函数权重调一次行为模式就变一次epsilon衰减节奏差一点训练曲线就差一大截。如果你照着这套代码训练时发现模型不收敛不要急着改网络结构先检查奖励函数设计、epsilon衰减节奏和经验回放参数这三项大部分问题都出在这三个环节。这套代码的所有核心逻辑已经完整给出跑通一次之后你就可以按自己的想法去改状态表达、换奖励函数尝试Double DQN和Dueling DQN一步步把避障智能体做得更实用。
返回列表