ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于深度强化学习的双目标动态感知路径规划:避障与效率的平衡实践

基于深度强化学习的双目标动态感知路径规划:避障与效率的平衡实践

简介:本资源为基于深度强化学习的双目标动态感知路径规划方法Python源码包,面向计算机、人工智能、自动化、通信工程等专业的在校学生、教师及企业员工,可用于毕业设计、课程设计、大作业或项目初期立项演示,也适合具备一定基础的学习者进阶研究。压缩包共44个文件,约325KB,以29个py源码文件为核心,辅以10个pyc编译文件、2个md说明文档、1个txt日志、1个license及1个log运行记录,涵盖仿真环境、算法模块与多组风险参数下的导航匹配实验脚本,目录结构清晰,便于按模块阅读与二次开发。项目代码均经过测试运行成功,功能完整,读者可据此理解双目标动态感知路径规划的整体实现思路,掌握深度强化学习在路径决策中的应用方式,并在此基础上修改扩展以实现其他功能。目前已有719人学习下载,适合作为学习参考与项目实践素材。

1. 双目标动态感知路径规划:为什么“绕开障碍”和“少走弯路”总是打架

做移动机器人或者 AGV 调度的人,大概率都遇到过这个场景:小车在仓库里跑,前面突然出现一个临时堆放的托盘,你让它绕开,它绕了,但绕完之后路线歪得离谱,原本 20 秒能到的点位跑了 35 秒。更气人的是,下一次同样的障碍物出现在同样的位置,它还是绕成那个鬼样子。这不是代码写错了,而是路径规划里两个目标在互相拉扯——避障的实时性和路径的全局最优性。

基于深度强化学习的双目标动态感知路径规划,要解决的就是这件事。所谓“双目标”,通常指路径长度最短和动态障碍物避让成功率最高这两个目标同时优化;所谓“动态感知”,是指环境里的障碍物不是静态地图上画好的,而是会动、会突然出现的。传统 A* 或者 Dijkstra 在这类场景下要么重规划太慢,要么绕出来的路根本没法看。深度强化学习(DRL)把这套逻辑变成“感知-决策”的闭环,让智能体自己学会什么时候该贴着障碍物边缘走、什么时候该提前变道。

这套方法适合谁?如果你在做 AGV 集群调度、无人机三维路径规划、泊车路径规划算法,或者任何需要“边走边看边调整”的 Python 工程,这篇笔记里的东西可以直接拿去改。但如果你只是想让小车在固定地图上从 A 走到 B,那用不着 DRL,A* 加个膨胀层就够了,别为了技术而技术。

下面我会按“先搞清楚双目标怎么建模 → 再搭一个能跑的最小 DRL 环境 → 然后调参和避坑 → 最后讲怎么验证它真的比传统方法好”这条线来写。中间会给出可复现的 Python 代码块,参数怎么改、改完看什么指标,都会说清楚。

2. 双目标动态感知的建模:状态、动作、奖励怎么定才不翻车

2.1 状态空间:别把原始激光雷达数据直接塞进网络

很多刚上手的人喜欢把激光雷达的 360 度原始点云直接拉平成一个几千维的向量,然后扔给 DRL 网络。这么做不是不行,但训练慢、泛化差,而且动态障碍物的速度信息完全丢了。我一般会做两层处理:第一层是局部栅格化,以智能体为中心切一个 20×20 的局部地图,每个格子用 0/1 表示有没有障碍物;第二层是动态特征提取,把最近 3 帧的栅格做差分,得到障碍物的运动趋势。

状态向量的典型构成如下表:

分量维度含义备注
局部栅格40020×20 障碍物占用0 空闲,1 占用
运动差分400前后帧栅格变化反映动态障碍物方向
目标相对位置2目标点相对于智能体的 (dx, dy)归一化到 [-1, 1]
当前速度2线速度、角速度连续控制时用
上一动作2上一步的 (v, ω)让策略考虑平滑性

这样状态维度控制在 800 左右,比原始点云小一个数量级,训练效率会高很多。注意,局部栅格的分辨率不要设得太细,0.1 米一格在室内场景够用,再细会让状态维度爆炸,而且动态障碍物的差分特征会变得很稀疏。

2.2 动作空间:离散还是连续,取决于你的底盘

如果你的机器人是差速底盘,动作空间通常是连续的 (v, ω),v 是线速度,ω 是角速度。但连续动作空间的 DRL 训练难度比离散大不少,尤其是双目标优化时,策略容易在“转圈”和“直冲”之间震荡。我的经验是:先离散化跑通,再上连续控制。

离散动作可以设计成 9 个:

import numpy as np # 离散动作空间:线速度 3 档 × 角速度 3 档 V_LEVELS = [0.0, 0.5, 1.0] # m/s W_LEVELS = [-1.0, 0.0, 1.0] # rad/s def get_discrete_actions(): actions = [] for v in V_LEVELS: for w in W_LEVELS: actions.append(np.array([v, w])) return actions # 共 9 个动作

这段代码定义了一个 3×3 的离散动作网格。V_LEVELS里的 0.0 表示可以原地停,0.5 是慢速调整,1.0 是正常前进。W_LEVELS里的 -1.0 和 1.0 分别对应左转和右转的最大角速度。实际部署时,你需要根据底盘的最大速度和最大角速度来改这三个值,不要直接抄。比如你的 AGV 最大线速度是 1.5 m/s,那V_LEVELS可以改成[0.0, 0.75, 1.5]。

离散化的好处是 DQN 或者 Double DQN 就能直接跑,不用折腾 SAC 或者 TD3 的调参。缺点是动作不够平滑,走出来的路径会有轻微抖动。如果你对平滑性要求高,可以在奖励函数里加一个动作变化惩罚,这个后面会讲。

2.3 双目标奖励函数:把“绕开”和“少走弯路”写成可计算的数

奖励函数是双目标动态感知路径规划里最容易翻车的地方。很多人只写一个“到达目标 +100,撞障碍 -100”,结果训练出来的策略要么不敢动,要么贴着障碍物蹭。双目标意味着你要在奖励里同时体现路径效率和避障安全。

我常用的奖励结构是:

def compute_reward(state, action, next_state, done, info): reward = 0.0 # 目标1:路径效率——鼓励靠近目标 dist_old = np.linalg.norm(state['goal'] - state['pos']) dist_new = np.linalg.norm(next_state['goal'] - next_state['pos']) reward += (dist_old - dist_new) * 10.0 # 靠近目标给正奖励 # 目标2:避障安全——惩罚危险距离 min_obs_dist = info['min_obstacle_distance'] if min_obs_dist < 0.3: reward -= (0.3 - min_obs_dist) * 50.0 # 越近罚越狠 # 碰撞终止 if info['collision']: reward -= 100.0 done = True # 到达目标 if dist_new < 0.2: reward += 200.0 done = True # 动作平滑惩罚(可选) reward -= 0.1 * np.linalg.norm(action - state['last_action']) return reward, done

逐段解释一下。第一段(dist_old - dist_new) * 10.0是路径效率项,系数 10.0 是我试出来的,太小了策略不在乎绕远,太大了会鼓励它直冲障碍物。第二段是避障安全项,min_obs_dist是智能体到最近障碍物的距离,小于 0.3 米就开始惩罚,系数 50.0 比效率项大,意思是“安全比省时间更重要”。碰撞惩罚 -100 和到达奖励 +200 是终止条件,这两个数不用太纠结,保持 2:1 左右的比例就行。最后的动作平滑惩罚系数 0.1 很小,只是让路径别太抖,不影响主目标。

提示:奖励函数里的系数没有万能值,但有一个判断标准——训练 1000 轮后,如果智能体在空旷场景下平均步数比最优步数多 30% 以上,说明效率项系数太小;如果它经常贴着障碍物 0.1 米走,说明安全项系数不够。

2.4 动态障碍物的感知与预测:别把“会动的东西”当静态处理

动态感知的核心是让智能体知道障碍物在动。最简单的做法是在环境里维护一个障碍物列表,每个障碍物有位置和速度,每一步更新位置。状态里的“运动差分”就是前后两帧栅格的差,这个差能告诉网络“哪个方向有东西在靠近”。

但这里有个坑:如果障碍物速度很快,比如 1.5 m/s,而你的控制频率只有 5 Hz,那两帧之间障碍物移动了 0.3 米,在 0.1 米分辨率的栅格上就是 3 格,差分特征会很明显,但智能体可能来不及反应。我的做法是把控制频率提到 10 Hz,同时把障碍物速度限制在 1.0 m/s 以内。如果实际场景里障碍物更快,那就需要加预测模块,比如用简单的匀速模型预测未来 0.5 秒的位置,把预测位置也放进状态里。

def predict_obstacle_positions(obstacles, dt=0.5): """匀速模型预测障碍物未来位置""" predicted = [] for obs in obstacles: pred_x = obs['x'] + obs['vx'] * dt pred_y = obs['y'] + obs['vy'] * dt predicted.append((pred_x, pred_y)) return predicted

这个函数很简单,但效果立竿见影。把预测位置画在局部栅格上,智能体会提前开始避让,而不是等到障碍物快贴脸了才急转。dt取 0.5 秒是个经验值,太短了预测没意义,太长了预测不准。

3. 用 Python 搭一个能跑的最小 DRL 训练环境

3.1 环境接口设计:兼容 Gym 风格,但别被它绑死

我建议环境接口按 Gym 的reset()和step()来写,这样能直接复用 Stable-Baselines3 这类库。但不要完全照搬,因为动态障碍物的场景里,step()需要返回额外的info字典,里面放min_obstacle_distance、collision这些奖励函数要用的信息。

import gym from gym import spaces import numpy as np class DynamicPathEnv(gym.Env): def __init__(self, grid_size=20, max_steps=200): super().__init__() self.grid_size = grid_size self.max_steps = max_steps self.action_space = spaces.Discrete(9) # 状态:局部栅格 400 + 运动差分 400 + 目标 2 + 速度 2 + 上一动作 2 self.observation_space = spaces.Box( low=-1.0, high=1.0, shape=(806,), dtype=np.float32 ) self.reset() def reset(self): self.step_count = 0 self.agent_pos = np.array([0.0, 0.0]) self.agent_vel = np.array([0.0, 0.0]) self.goal = np.array([5.0, 5.0]) self.obstacles = self._init_obstacles() self.last_action = np.array([0.0, 0.0]) return self._get_obs() def _init_obstacles(self): # 初始化 3 个动态障碍物,随机位置和速度 obstacles = [] for _ in range(3): obstacles.append({ 'x': np.random.uniform(-2, 7), 'y': np.random.uniform(-2, 7), 'vx': np.random.uniform(-0.5, 0.5), 'vy': np.random.uniform(-0.5, 0.5) }) return obstacles def _get_obs(self): # 这里省略栅格化细节,返回 806 维向量 obs = np.zeros(806, dtype=np.float32) # ... 填充局部栅格、差分、目标、速度、上一动作 return obs def step(self, action_idx): self.step_count += 1 # 动作映射 v_levels = [0.0, 0.5, 1.0] w_levels = [-1.0, 0.0, 1.0] v = v_levels[action_idx // 3] w = w_levels[action_idx % 3] action = np.array([v, w]) # 更新智能体位置(简化运动学) dt = 0.1 theta = np.arctan2(self.agent_vel[1], self.agent_vel[0]) if np.linalg.norm(self.agent_vel) > 0.01 else 0.0 self.agent_pos[0] += v * np.cos(theta) * dt self.agent_pos[1] += v * np.sin(theta) * dt self.agent_vel = np.array([v * np.cos(theta), v * np.sin(theta)]) # 更新障碍物位置 for obs in self.obstacles: obs['x'] += obs['vx'] * dt obs['y'] += obs['vy'] * dt # 边界反弹 if abs(obs['x']) > 10: obs['vx'] *= -1 if abs(obs['y']) > 10: obs['vy'] *= -1 # 计算奖励和终止条件 info = self._get_info() reward, done = compute_reward( {'pos': self.agent_pos, 'goal': self.goal, 'last_action': self.last_action}, action, {'pos': self.agent_pos, 'goal': self.goal}, False, info ) self.last_action = action if self.step_count >= self.max_steps: done = True return self._get_obs(), reward, done, info def _get_info(self): min_dist = float('inf') for obs in self.obstacles: d = np.linalg.norm(self.agent_pos - np.array([obs['x'], obs['y']])) min_dist = min(min_dist, d) return { 'min_obstacle_distance': min_dist, 'collision': min_dist < 0.2 }

这段代码是一个最小可运行的环境骨架。__init__里定义了动作空间为 9 个离散动作,观测空间是 806 维。reset()初始化智能体在原点,目标在 (5,5),三个动态障碍物随机分布。step()里做了三件事:更新智能体位置、更新障碍物位置、计算奖励。注意_get_obs()里的栅格化细节我省略了,实际写的时候需要把智能体周围 20×20 的区域映射成 0/1 栅格,再和上一帧做差得到运动差分。

注意:障碍物的边界反弹逻辑很简单,但实际场景里障碍物可能走出边界就消失了。如果你的场景是这样,记得在step()里把超出边界的障碍物重置到另一侧,而不是反弹。

3.2 用 Stable-Baselines3 跑 DQN:训练脚本与关键超参

环境搭好后,训练部分我一般直接用 Stable-Baselines3 的 DQN,省得自己写经验回放和目标网络。安装命令:

pip install stable-baselines3[extra] gymnasium

训练脚本:

from stable_baselines3 import DQN from stable_baselines3.common.callbacks import EvalCallback from stable_baselines3.common.monitor import Monitor # 创建环境 env = Monitor(DynamicPathEnv(grid_size=20, max_steps=200)) eval_env = Monitor(DynamicPathEnv(grid_size=20, max_steps=200)) # DQN 超参 model = DQN( policy="MlpPolicy", env=env, learning_rate=1e-4, # 学习率,别超过 1e-3 buffer_size=100000, # 经验回放池大小 learning_starts=1000, # 前 1000 步只收集不训练 batch_size=64, # 批大小 gamma=0.99, # 折扣因子 train_freq=4, # 每 4 步训练一次 target_update_interval=500, # 目标网络更新间隔 exploration_fraction=0.3, # 前 30% 步数用于探索 exploration_final_eps=0.05, # 最终探索率 verbose=1, tensorboard_log="./dqn_path_logs/" ) # 评估回调,每 5000 步评估一次 eval_callback = EvalCallback( eval_env, best_model_save_path="./best_model/", log_path="./eval_logs/", eval_freq=5000, deterministic=True, render=False ) model.learn(total_timesteps=200000, callback=eval_callback) model.save("dqn_dynamic_path_final")

逐行说明关键参数。learning_rate=1e-4是我试过比较稳的值,DQN 对学习率很敏感,1e-3 容易震荡,1e-5 收敛太慢。buffer_size=100000对于 806 维状态和 9 个动作来说够用,再大内存吃紧。learning_starts=1000让智能体先随机跑 1000 步,把经验池填一填再开始训练,不然早期样本太单一。gamma=0.99是标准值,如果你的场景步数很少(比如 50 步内必须到达),可以降到 0.95。exploration_fraction=0.3表示前 60000 步(200000×0.3)探索率从 1.0 线性降到 0.05,这个比例不能太小,否则策略会过早收敛到局部最优。

训练过程中用 TensorBoard 看两个曲线:rollout/ep_rew_mean和eval/mean_reward。如果ep_rew_mean一直在涨但eval/mean_reward不涨,说明过拟合了,需要增大buffer_size或者加正则化。如果两个都不涨,先检查奖励函数是不是写错了,尤其是碰撞惩罚和到达奖励的比例。

3.3 训练结果怎么看:三个必须监控的指标

训练跑起来后,别只盯着奖励曲线。双目标动态感知路径规划要看三个指标:

指标含义健康范围
平均路径长度到达目标的步数 × 每步距离比 A* 最优路径长 15% 以内
碰撞率碰撞回合数 / 总回合数低于 5%
平均最小障碍距离每回合中离障碍物最近的距离大于 0.25 米

如果路径长度比 A* 长 50% 以上,说明效率项系数太小,或者探索不够。如果碰撞率高于 10%,说明安全项系数不够,或者障碍物速度太快。如果平均最小障碍距离小于 0.15 米,说明智能体在“蹭”障碍物,虽然没撞但很危险,实际部署时传感器噪声一抖就撞了。

我一般会在训练 50000 步、100000 步、200000 步时各跑 100 个测试回合,统计这三个指标。如果 200000 步时碰撞率还在 10% 以上,那就别继续训了,先回去改奖励函数或者降低障碍物速度。

4. 避坑与排查:双目标 DRL 路径规划里最容易翻车的 5 个地方

4.1 现象:训练奖励震荡,一会儿高一会儿低

原因:DQN 的目标网络更新间隔太短,或者学习率太大。双目标奖励里效率项和安全项的尺度差异大,如果安全项系数是效率项的 5 倍以上,Q 值的方差会很大,目标网络跟不上。

解决:把target_update_interval从 500 提到 1000 或 2000,学习率降到 5e-5。另外检查奖励函数,效率项和安全项的系数比不要超过 1:10。如果安全项必须很大,那就把效率项也等比例放大,让 Q 值落在差不多的范围。

4.2 现象:智能体学会“原地转圈”拿安全奖励

原因:安全项只惩罚“离障碍物太近”,但没有惩罚“不前进”。如果障碍物恰好离起点很近,智能体发现原地不动就不会靠近障碍物,安全项一直是 0,而效率项因为没动也没扣分。

解决:在奖励里加一个“时间惩罚”,每步固定扣 0.1 分。这样原地不动也会慢慢扣分,逼它往前走。或者把效率项改成“到达目标的进度”,而不是“距离变化”,进度为 0 时也给负奖励。

4.3 现象:换一个地图布局,策略完全失效

原因:状态里的局部栅格是固定 20×20,但训练时只在一个小区域里跑,网络没见过障碍物在远处的场景。另外,目标相对位置归一化到 [-1,1] 时,如果目标距离超过 10 米,归一化后接近 1,网络分不清 10 米和 20 米的区别。

解决:训练时随机化起点和目标位置,范围要覆盖实际部署的最大区域。目标相对位置不要简单归一化,可以用对数缩放或者分段归一化。另外,局部栅格的分辨率可以自适应,距离远时用粗分辨率,距离近时用细分辨率。

4.4 现象:仿真里跑得好,真机上直接撞

原因:仿真里的障碍物位置是精确的,真机上激光雷达有噪声,障碍物边缘会抖动。另外,仿真控制频率是 10 Hz,真机可能只有 5 Hz,动态障碍物的差分特征对不上。

解决:训练时在状态里加高斯噪声,模拟传感器误差。控制频率尽量和真机对齐,如果真机只有 5 Hz,那仿真也用 5 Hz,别用 10 Hz 训完直接部署。另外,真机部署时把安全距离阈值从 0.3 米提到 0.5 米,留出反应余量。

4.5 现象:训练了 50 万步,路径还是比 A* 长很多

原因:DQN 的离散动作空间太粗,9 个动作里没有“微调”选项。比如智能体需要走一条很窄的通道,但离散动作只有 0.5 m/s 和 1.0 m/s 两档速度,没有 0.3 m/s,它只能要么冲过去要么停下来。

解决:要么把动作空间细化到 5×5=25 个动作,要么换成 SAC 或 TD3 做连续控制。连续控制的训练难度大,但路径质量会好很多。如果不想换算法,可以在离散动作里加一个“慢速微调”档,比如V_LEVELS = [0.0, 0.2, 0.5, 1.0],变成 4×3=12 个动作。

5. 验证双目标是否真的生效:一个对比实验和两个调参技巧

训练完了,怎么证明你的双目标 DRL 真的比传统方法好?别只看奖励曲线,那东西会骗人。我一般会做一个对比实验:在同一组动态障碍物场景下,跑 100 个回合,对比 DRL 策略、A* 加动态重规划、以及 RRT* 加动态重规划。对比指标就是前面说的三个:平均路径长度、碰撞率、平均最小障碍距离。

def evaluate_policy(model, env, n_episodes=100): """评估 DRL 策略在动态环境下的表现""" metrics = { 'path_length': [], 'collision': [], 'min_obs_dist': [] } for _ in range(n_episodes): obs = env.reset() done = False total_dist = 0.0 min_dist = float('inf') collided = False while not done: action, _ = model.predict(obs, deterministic=True) obs, reward, done, info = env.step(action) total_dist += 0.1 # 假设每步 0.1 米 min_dist = min(min_dist, info['min_obstacle_distance']) if info['collision']: collided = True metrics['path_length'].append(total_dist) metrics['collision'].append(1 if collided else 0) metrics['min_obs_dist'].append(min_dist) print(f"平均路径长度: {np.mean(metrics['path_length']):.2f} m") print(f"碰撞率: {np.mean(metrics['collision'])*100:.1f}%") print(f"平均最小障碍距离: {np.mean(metrics['min_obs_dist']):.3f} m") return metrics

这个评估函数跑 100 个回合,输出三个指标。如果 DRL 的碰撞率比 A* 低但路径长度比 A* 长 30% 以上,说明双目标没平衡好,需要回去调奖励函数。如果碰撞率和路径长度都比 A* 差,那这套 DRL 方案在这个场景下就不值得做,老老实实用 A* 加动态窗口法。

两个调参技巧。第一个是奖励系数自适应:训练初期安全项系数设小一点,让智能体先学会往目标走;训练到 50000 步后,把安全项系数翻倍,逼它学会避障。这个可以用回调函数实现,每 10000 步检查一次碰撞率,如果碰撞率高于 15% 就增大安全项系数。第二个是动作空间退火:训练初期用 9 个离散动作,训练到 100000 步后,把动作空间细化到 25 个,让策略在已经学会大致方向的基础上微调路径。这个技巧对路径长度的改善很明显,我试过能把路径长度从比 A* 长 25% 降到长 12%。

最后说一个我自己的习惯:每次改完奖励函数或者动作空间,先跑 20000 步看看趋势,别一上来就训 50 万步。20000 步如果碰撞率还在 30% 以上,那后面大概率也救不回来,不如早点改。这套东西我踩过的最大坑就是“奖励函数写得太复杂”,一开始加了五六个惩罚项,结果智能体什么也没学会。后来砍到只剩效率项、安全项、终止奖励三项,反而收敛得又快又好。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进