简介:一份关于深度强化学习在机器人路径规划应用研究的PDF文献,面向深度学习、机器人控制、智能导航等方向的研究者与学习者,针对传统路径规划算法依赖先验地图、运行时需频繁更新地图且计算资源消耗大的痛点,系统介绍了基于深度Q学习(DQN)的端到端路径规划方法。文献内容详实,先解析TEB算法基于数值优化、考虑障碍物距离与运动学约束的思路,再引出DQN用神经网络逼近Q值函数以解决连续输入问题,并给出回报函数设置、动作空间划分等实现细节;通过在Gazebo仿真环境中设置多个目标点进行对比实验,验证了DQN无需地图信息即可实现实时规划,单步规划时间仅0.001~0.005秒,明显优于TEB,同时客观指出其在路径平滑性上的不足。资源共1个PDF文件,大小约1.35MB,包含算法流程图、公式推导、实验数据与结果分析,便于读者快速掌握深度强化学习解决路径规划问题的完整研究范式。目前已有556人学习,可作为相关课题的参考文献、课程论文素材或入门研读资料。
1. 深度强化学习走到路径规划这一步,到底解决了什么难题
以前做路径规划,本质是图搜索:地图给定、起点终点给定,A*、Dijkstra、RRT 跑一轮就出结果。但一到现场就露馅,AGV 前方突然有人、无人机遇到突发障碍、泊车时车位周围不断有车进出,地图随时在变,搜索算法就得全量重跑,环境一变逻辑就要重构。深度强化学习换了一条路:把路径规划看作「每走一步都根据当前感知做决策、拿即时反馈、并追求未来累积回报最大」的序贯决策问题,用神经网络直接学从感知到动作的映射。它解决的是传统方法在动态、未知、多智能体场景里频繁重规划、无记忆、不看长期收益的短板。这篇笔记写给想在 AGV、动态避障小车、无人机三维路径规划里落地的工程师和学生:MDP 建模怎么拆、最小可跑代码怎么凑、算法怎么选、坑踩在哪,读完能直接动手。
2. 把路径规划重写成马尔可夫决策过程:状态、动作、奖励三张表怎么定
2.1 为什么路径规划要先谈 MDP,而不是先谈神经网络
深度强化学习的训练对象是策略,但策略学得好不好,高度依赖问题被定义成什么样。路径规划传统上定义为「在一张图上找最短路径」,深度强化学习却要求你把问题写成马尔可夫决策过程(MDP),也就是五元组:状态(State)、动作(Action)、状态转移(Transition)、奖励(Reward)、折扣因子(Gamma)。
状态转移在机器人路径规划里是「执行指令后位置怎么变」,通常由运动模型决定,不是我们能改的;折扣因子一般固定 0.99,代表「未来一回合的收益折算到现在值多少」。真正由工程师拍板的只有三个东西:状态怎么描述、动作空间怎么设计、奖励函数怎么给。这三张表定完,才算把「求一条路」改写成了「学一个策略」——前者每次环境变都要重算,后者环境变了照样能输出动作,这才是深度强化学习的价值所在。
很多新手一上来就选 DQN、PPO,先把代码跑起来,后面发现训练不收敛,回头一查,问题几乎都出在这三张表上。状态信息给少了,智能体看不到障碍物;动作空间给得太细,探索效率断崖式下跌;奖励给得含糊,学到原地转圈。所以这一章先把建模讲透,第三章才上代码。
2.2 状态空间:决定智能体「看得到什么」
状态空间是智能体做决策时的输入。做路径规划时,有两类主流做法。
第一类是用全局或局部栅格地图。把地图按分辨率切成网格,用 0 和 1 表示自由和占据,再把这个二维数组给神经网络。弊端是输入的维度大,一个 100x100 的地图展开就是 1 万个输入,网络参数跟着爆炸。我一般在局部窗口上截取,常用 7x7 或 11x11 的窗口,以车为中心,这样输入维度从万级降到百级,训练速度会快一个量级。
第二类是特征向量。这是工程落地里最常见也最省事的做法,把连续场景压缩成几十维。一张典型的状态表长这样:
| 状态分量 | 维度 | 含义 |
|---|---|---|
| 自身坐标 (x, y) | 2 | 当前绝对位置 |
| 目标相对坐标 (dx, dy) | 2 | 目标点相对当前位置的偏移 |
| 当前线速度 (vx, vy) | 2 | 运动状态 |
| 前方、左方、右方障碍物距离 | 3 | 激光雷达或超声波测距结果 |
| 当前朝向角与目标方向夹角 | 1 | 用于转向决策 |
这 10 维状态基本覆盖了动态避障小车路径规划的全部决策依据。注意两点。一是尽可能用相对量,目标坐标写成「相对位置」而不是「全局位置」,模型就不用自己学坐标变换,收敛快得多;二是障碍物距离不要给原始点云,给「最近距离」这类聚合特征就行,原始数据维度高又容易过拟合。你做的如果是无人机三维路径规划,把 x、y 换成 x、y、z,再加一个高度差分量,维度和思路完全一致。
三维路径规划还有一种常用补充:把连续几帧的障碍物距离拼成一小段历史,时间维度上给智能体「障碍物正在靠近还是远离」的信息,动态场景里特别管用。
2.3 动作空间:离散、连续还是混合
动作空间决定智能体每一步能「动什么」,它的设计直接决定你后面选哪类算法。
栅格地图上最常见的是离散动作。四方向(上、下、左、右)和八方向(加四个斜向)在 AGV 路径规划里用得最多。离散动作的好处是 Q 值表小、探索简单,但缺点是走出来的路径带有锯齿感,不符合实际车辆的运动学约束。如果你想控制车辆转角,可以用带角度的离散动作,比如 17 个动作:保持直行 0 度、左转 15/30/45 度、右转 15/30/45 度,再加上前进和后退。拍板依据是:真实底盘听不听你「斜着走一步」这种指令,不听就老老实实加转角。
连续动作空间一般用在无人机三维路径规划和泊车路径规划算法里。比如无人机的位置增量 \delta x、\delta y、\delta z 三个连续值,或者速度控制量,动作就是一个三维向量。连续空间的优点是控制精细、路径平滑,缺点是需要用 PPO、SAC 这类策略梯度算法才能训练,难度明显高于离散动作。
混合动作空间(方向盘角离散、速度连续)理论最优,但工程上我一般不建议一上来就上。混合空间的探索复杂度几乎是指数上升的,常用做法是先离散化再用 DQN 类算法,或把转向和速度拆成两个策略网络,而不是在一个网络里硬学联合动作。
2.4 奖励函数:把「求一条路」改写成「学一个策略」
奖励函数是整个 MDP 建模里最玄学、也最影响成败的一块。奖励给得稀疏,比如「到达终点 +1,其余 0」,智能体在一个大场景里几百步摸不到一次正反馈,训练基本不收敛;奖励给得太密,每走一步都给一堆打分,智能体又会钻奖励函数的空子,学到奇怪行为。
我通常用四段式稠密奖励:时间成本 + 距离变化 + 碰撞惩罚 + 平滑惩罚。公式是:
r = -w1 * dt - w2 * (dist_prev - dist_now) + collision * (-w3) + angular_change * (-w4)
其中 dist_prev 和 dist_now 是上一步和当前步到目标点的欧氏距离。dt 是步长时间,让智能体养成「少绕路」的习惯。距离变化项是一个势能引导项:只要比上一步离目标更近,就给一点正奖励,这是最有效的引导。碰撞惩罚设置成较大值,比如 -10,让智能体把「活着」排在「走捷径」前面。平滑惩罚惩罚相邻两步动作的变化幅度,避免 zigzag 路径,在无人机三维路径规划里这条特别重要,因为飞行器受不了剧烈机动。
在救援路径规划场景里还要额外加一项「单位时间到达人数」或者「时间紧迫度」的权重,把 w1 调大,智能体才会选择尽快到达而不是绕远保安全。奖励项权重没有万能数值,我的习惯是先从量纲上估:时间惩罚 0.1、距离势能 1.0、碰撞惩罚 10、平滑 0.5,跑一轮看行为,再按现象调,而不是盯着 loss 曲线调。
3. 用 Gymnasium 从零搭一个动态避障小车:DQN 最小可复现代码
3.1 环境搭建:十分钟跑通一个动态避障场景
先搭一个最简环境:10x10 的栅格平面,小车从左上角出发,走到右下角,中间有一个会缓慢移动的障碍物。障碍物动起来,这是和传统路径规划最大的区别——环境不是静态的,搜索算法在这里会失效,而强化学习不需要重规划,策略直接应对。
import math import gymnasium as gym from gymnasium import spaces import numpy as np class DynamicObstacleEnv(gym.Env): """极简动态避障小车环境:车走向目标,障碍物缓慢移动。""" def __init__(self, grid_size=10, max_steps=100): super().__init__() self.grid_size = grid_size self.max_steps = max_steps # 4 个动作:上、下、左、右 self.action_space = spaces.Discrete(4) # 观测:x, y, dx_goal, dy_goal, 最近障碍物距离 self.observation_space = spaces.Box(low=-10, high=10, shape=(5,), dtype=np.float32) self.rng = np.random.default_rng(42) def reset(self, *, seed=None, options=None): super().reset(seed=seed) self.agent_pos = np.array([0, 0], dtype=float) self.goal_pos = np.array([self.grid_size - 1, self.grid_size - 1], dtype=float) self.obstacle_pos = np.array([5, 3], dtype=float) self.steps = 0 return self._get_obs(), {} def step(self, action): # 动作:0上 1下 2左 3右 moves = {0: [0, 1], 1: [0, -1], 2: [-1, 0], 3: [1, 0]} self.agent_pos += np.array(moves[action], dtype=float) # 障碍物做小幅移动,制造动态环境 self.obstacle_pos += 1.0 * np.array([0.05, 0.05]) # 边界处理 self.agent_pos = np.clip(self.agent_pos, 0, self.grid_size - 1) self.steps += 1 dist_now = np.linalg.norm(self.agent_pos - self.goal_pos) reward = -0.1 # 时间成本 done = False if np.linalg.norm(self.agent_pos - self.obstacle_pos) < 1.0: reward -= 10 # 碰撞惩罚 done = True if dist_now < 0.5: reward += 20 # 到达目标 done = True if self.steps >= self.max_steps: done = True return self._get_obs(), reward, done, False, {} def _get_obs(self): dist_obs = np.linalg.norm(self.agent_pos - self.obstacle_pos) dist_goal = np.linalg.norm(self.agent_pos - self.goal_pos) return np.array([ self.agent_pos[0], self.agent_pos[1], self.goal_pos[0] - self.agent_pos[0], self.goal_pos[1] - self.agent_pos[1], dist_obs ], dtype=np.float32)Gymnasium 环境四件套一个都不能少:action_space定义动作范围,observation_space定义观测形状,reset返回初始观测,step返回四元组(next_obs, reward, terminated, truncated)。这四件套不齐,后面的训练循环没法跑。
参数上有几个值得说。grid_size=10是小场景参数,适合本地 CPU 调试;真实 AGV 场景我会把栅格放大到 50 以上,同时把障碍物移动速度从0.05提高到0.2,否则动态性不够,训练出来的策略会退化成静态规划。max_steps=100是回合截断长度,防止智能体在地图里无限乱逛,这个值通常设为栅格尺寸的 8-10 倍。seed=42让实验可复现,后面评估那一章还会再强调它的价值。这个环境故意用最朴素的数值,是因为调参阶段你需要的是「跑得快」,而不是「模拟得真」。
3.2 DQN 训练循环:网络、经验回放、目标网络三件套
环境就绪,接下来是核心训练代码。这个最小实现对初学者有代表性,也保留了深度 Q 学习的全部关键部件。
import torch import torch.nn as nn import torch.optim as optim import random from collections import deque class DQN(nn.Module): def __init__(self, state_dim, n_actions): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, n_actions) ) def forward(self, x): return self.net(x) def train_dqn(env, episodes=800, batch_size=64, gamma=0.99, lr=1e-3, buffer_size=50000, eps_start=1.0, eps_end=0.05, eps_decay=500): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") online_net = DQN(env.observation_space.shape[0], env.action_space.n).to(device) target_net = DQN(env.observation_space.shape[0], env.action_space.n).to(device) target_net.load_state_dict(online_net.state_dict()) optimizer = optim.Adam(online_net.parameters(), lr=lr) replay = deque(maxlen=buffer_size) for ep in range(episodes): obs, _ = env.reset() obs = torch.tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) total_reward = 0.0 while True: # epsilon 贪心:前期多探索,后期多利用 eps = eps_end + (eps_start - eps_end) * math.exp(-ep / eps_decay) if random.random() < eps: action = env.action_space.sample() else: with torch.no_grad(): q_values = online_net(obs) action = q_values.argmax().item() next_obs, reward, terminated, truncated, _ = env.step(action) next_obs_t = torch.tensor(next_obs, dtype=torch.float32, device=device).unsqueeze(0) done = terminated or truncated replay.append((obs, action, reward, next_obs_t, done)) obs = next_obs_t total_reward += reward if len(replay) >= batch_size: # 经验回放:切断样本间的时间相关性 batch = random.sample(replay, batch_size) s, a, r, s_, d = zip(*batch) s = torch.cat(s) a = torch.tensor(a).unsqueeze(1).to(device) r = torch.tensor(r, dtype=torch.float32, device=device) s_ = torch.cat(s_) d = torch.tensor(d, dtype=torch.float32, device=device) q_pred = online_net(s).gather(1, a).squeeze() # 目标网络计算 TD 目标,缓解 Q 值过估计 with torch.no_grad(): q_target = r + gamma * (1 - d) * target_net(s_).max(1).values loss = nn.functional.mse_loss(q_pred, q_target) optimizer.zero_grad() loss.backward() optimizer.step() if done: break # 每 100 回合同步一次目标网络 if ep % 100 == 0: target_net.load_state_dict(online_net.state_dict()) print(f"episode {ep}: reward {total_reward:.1f}")这段代码里有几个关键设计。经验回放队列存了最近的 5 万条转移样本,训练时随机抽样,打散相邻时刻的高度相关性——如果按顺序学,网络会被近期几帧带偏,策略会震荡。目标网络是另一个关键,它不参与梯度更新,每隔 100 回合才从在线网络复制一次权重。为什么要这样而不是每步同步?因为每一步都动目标,Q 值的「靶子」一直在变,网络追来追去追不上,loss 会剧烈震荡。100 回合这个同步频率是经验值,小场景下没问题,大场景可以改成每 500 回合。gamma=0.99 意味着智能体把一回合之后 100 步的收益折损一半以上,距离目标越远,越需要长期眼光。
batch_size=64 是 CPU 和 GPU 性能的折中,调大打训练速度,调小提高稳定性,60-128 区间问题不大。学习率 1e-3 配 Adam 是默认组合,路径规划场景不建议低于此值的十倍,否则本地收敛会非常慢。
3.3 训练过程看什么:别盯着 loss 曲线
新手最容易犯的错是把 loss 当唯一的进度条。深度 Q 学习的 loss 是「预测 Q 与目标 Q 的误差」,在策略改进过程中目标值本身也在变,所以 loss 不降反升完全可能是好事。我一般盯三个指标:单回合总奖励的滑动均值、到达目标的成功率、平均回合步数。
训练稳定后,你会看到大致这样的趋势:头 100 回合奖励基本是负的,车到处乱撞;200 回合后开始有零星的正奖励;500 回合附近成功率跳到七成以上,平均步数从五十几步降到二三十步。如果 800 回合成功率还在三成以下,基本不是代码问题,是第二章的 MDP 建模出了问题——奖励引导不够或状态缺了关键量。
训练完成后调一下环境里的障碍物初始位置,让车在没见过的障碍物布局里重新跑。这步比训练曲线更能说明策略是否真的学到了泛化能力,还是只会背从起点到终点的固定路线。
3.4 加载模型做闭环演示:把「学习」变成「会走」
训练只是把权重存进网络,落地要看推理闭环。下面的代码演示了加载训练好的权重,在小车上做闭环控制。这个「加载-推理-执行」的循环,也正是后面接入 ROS 的雏形,把 step 换成底盘速度指令,obs 换成里程计和激光雷达数据即可。
def run_policy(env, model, max_episodes=5): """加载训练好的模型做闭环演示,渲染每一步位置。""" device = next(model.parameters()).device for ep in range(max_episodes): obs, _ = env.reset() obs = torch.tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) total_reward = 0.0 steps = 0 while steps < env.max_steps: with torch.no_grad(): action = model(obs).argmax().item() # 贪心策略 next_obs, reward, terminated, truncated, _ = env.step(action) total_reward += reward steps += 1 obs = torch.tensor(next_obs, dtype=torch.float32, device=device).unsqueeze(0) print(f"ep {ep + 1} step {steps}: agent={env.agent_pos}, reward={reward:.2f}") if terminated or truncated: break print(f"episode {ep + 1} finished, total reward: {total_reward:.1f}")运行时要注意env.reset()会重新摆放目标点。若目标点随机化,那这段代码测的是泛化能力;若目标点固定,测的是记忆能力。做演示时用小场景没问题,但后面评估章节会专门说,正式验证必须用多个随机种子来跑统计,而不是看一次演示的效果。
4. AGV 选 D3QN、无人机选 PPO:动作空间主导的算法选型对照
4.1 为什么是动作空间先决定算法,而不是场景
很多人在 AGV 路径规划、无人机路径规划里纠结到底用哪个深度强化学习算法,拿着 DQN、D3QN、PPO、SAC 挨个试,试完还得出「效果都差不多」的结论。这个现象的根本原因是:他们先用算法再定义问题。正确顺序是先确认动作空间是离散还是连续,再倒推算法范围。
动作空间是离散的前三章已经搭好了,DQN 是自然选择。动作空间是连续的时候,DQN 的 Q 值输出没法覆盖无穷多个动作,只能用策略梯度类算法,主流是 PPO。中间还有一类「动作离散但任务复杂」的情况,这时候 DQN 的变体 D3QN(Dueling Double DQN)往往比朴素 DQN 稳得多。按这个标准,AGV 路径规划因为动作几乎都是离散转向,优先考虑 D3QN;无人机三维路径规划输出速度增量,优先考虑 PPO;泊车路径规划算法动作介于两者之间,视底盘抽象程度而定,通常先离散化用 D3QN,连续化用 PPO。
4.2 AGV 的动态避障为什么 D3QN 比 DQN 稳
AGV 路径规划有个典型特征:障碍物可能运动,决策必须实时,动作空间小(4 到 8 个方向)。这里我一般直接用 D3QN,而不是原版 DQN,原因是它在两个点上专门修了 DQN 的毛病。
D3QN 里的 D 是 Double,解决 Q 值过估计。原版 DQN 在计算目标时用target_net(s_).max(),这就相当于同一个网络既选动作又给这个动作打分,天然会高估——高估在小场景似乎无害,但在 AGV 调度这种多车环境中,高估会让多台车同时判定一条路安全,造成拥堵。Double DQN 的改法是把动作选择交给在线网络,分数评估交给目标网络,即argmax用online_net,取值用target_net。这样一拆,过估计得到抑制,多车协同场景明显更稳。
D3QN 里的 Dueling 是把 Q 值分解成状态价值 V 和动作优势 A。直观理解是:在一条空旷直道上,往左往右的动作优势差别很小,但「活着到达终点」这个状态价值差别很大;Dueling 结构让网络先学会判断「整局状态好不好」,再学「这个动作比平均好多少」。这个分解在动态避障小车里有个实际收益:当小车离障碍物还很远时,网络不必逐动作判断风险,只要状态价值够高,整体的 Q 值就不会乱跳。
AGV 场景如果还用朴素 DQN,常见现象是训练曲线大起大落,上一回合避障成功,下一回合在同一个位置连续撞墙。换 D3QN 后通常一两个小时内就能看到方差明显收敛。
4.3 连续动作空间:无人机三维路径规划的 PPO 用法
无人机三维路径规划通常输出三维速度或位置增量,动作是连续量。这一类问题不要用 DQN,正确选择是 PPO。PPO 属于策略梯度方法,直接优化策略分布的参数,不用 Q 值引导动作,天然处理连续动作。
PPO 的核心机制是 confidence clamp(置信度裁剪),它限制每次参数更新的幅度,避免策略在一步更新中走太远导致崩溃。这个特性在三维路径规划里特别重要,因为无人机的动力学约束很硬,一次大更新可能让策略突然规划出一条穿山路径,而小步更新虽然慢一些,至少飞行轨迹是平滑可执行的。
一个实操细节:PPO 的状态输入里,我强烈建议把无人机当前速度显式加进观测向量。三维空间动作自由度多,如果状态里只有位置没有速度,策略无法感知惯性,训练出来的轨迹会有很明显的抖振。动作输出端,不少实现直接输出三维速度值,再用一个tanh激活函数把输出限制在最大速度范围内——这个限制是飞行安全的底线,模型探索期间很容易输出离谱的数值。
PPO 的超参里最值得花时间调的是学习率和 clip 范围。学习率 3e-4 是通用起点,在路径规划场景里经常需要降到 1e-4,因为三维状态空间更大,更新太猛容易摧毁前期学到的避障能力。clip 范围默认 0.2,平原低风险场景调到 0.3 加速,地形复杂时我调到 0.1 保稳。
4.4 深度强化学习算法选型实战对照表
| 应用场景 | 动作空间 | 推荐算法 | 关键理由 |
|---|---|---|---|
| AGV 单/多车路径规划 | 离散 4/8 方向 | D3QN | 过估计抑制 + 状态价值分离,多车更稳 |
| 动态避障小车 | 离散方向 + 转角 | DQN / D3QN | 动作少时 DQN 足够,动作多时 D3QN |
| 无人机三维路径规划 | 连续三维速度 | PPO | 连续动作 + 更新范围受限,轨迹平滑 |
| 泊车路径规划 | 转向离散、速度连续 | 先离散化用 D3QN | 混合空间训练难度大,工程上先拆 |
| 救援路径规划 | 离散方向 + 时间权重 | D3QN | 需要快速收敛,且对长期收益敏感 |
这张表给的是选型起点,不是终点。同一个场景里换算法效果差异很大的情况常见,原因通常是 MDP 建模有偏差,换算法解决不了建模问题。换句话说,算法选型帮你排除掉「不该用的」,建模与参数才是帮你跑通「应该用的」的那一环。
5. 避坑:深度强化学习路径规划翻车的 5 个真实原因
5.1 现象:训练几百回合,回合奖励稳定在负值,成功率一直挂零
原因:奖励稀疏是深度强化学习路径规划最隐蔽的杀手。这一章开始时我去查了训练日志,发现 rewards 每回合就是 -0.1 乘以步数,一直到 300 回合都没有任何正反馈。环境里「安全到达」事件太稀少,随机策略几乎碰不到一次正样本,Q 值或策略梯度得不到有意义的梯度信号。
解决:把稀疏奖励改成稠密势能引导,核心做法是引入距离项dist_prev - dist_now,让智能体每靠近目标一步就拿到微小正反馈。同时把碰撞惩罚从 -1 提高到 -10,把到达奖励从 +1 提高到 +20,让三个事件的梯度信号量级拉开,而不是让时间成本 -0.1 主导整个训练过程。改完通常 100 回合内就能看到奖励由负转正。
5.2 现象:训练曲线很好看,但把小车拉到实际地图里一看,它在一个角落原地转圈
原因:这是奖励塑形过头的典型症状。上一节把势能项加得太重,智能体发现「原地蠕动」也能骗到微小的距离下降奖励,于是学会在角落不断来回试探,而不是真正走向目标。奖励函数从来没告诉它「方向对了」和「路径完整」的区别。
解决:给距离势能项加一个步数条件,只有「当前步与上一步的目标距离差超过一个阈值(比如 0.1)」才给正奖励,避免微小抖动刷分。同时把回合截断步数从 100 缩短到 50,反正原地转圈很快撞上限,让它没有机会在角落里一直蹭奖励。这个方案我在动态避障小车场景里试过几次,成功率提升在 20 个百分点以上。
5.3 现象:训练 loss 先降后升,Q 值越学越大,策略反而从稳定变震荡
原因:Q 值过估计。这个现象在 D3QN 被解决之前几乎是无解的。目标网络每 100 回合同步一次,但当环境扰动较大时,100 回合内 Q 值会悄悄膨胀,然后在下一次同步时把膨胀传给在线网络,形成周期性尖峰。你看到 loss 锯齿状上升,基本就是它在发作。
解决:首选换 D3QN,让动作选择与价值评估分离,从结构上抑制过估计;如果坚持用 DQN,把目标网络的同步频率从 100 回合降到 20 回合,并且给每次同步加一个软更新参数,比如tau=0.05,让目标网络渐进靠近在线网络而不是直接复制。后者是工程上常用的后悔药。
5.4 现象:训练环境里成功率九成,换了一个障碍物布局立刻掉到四成
原因:训练与评估指标不一致,或者说训练时的环境太单调。很多人为了快速出结果,把起始点、目标点、障碍物位置全固定,模型学到的其实是「背地图」,不是「避开障碍」。也就是说训练集和验证集完全重叠,评估时换个初始条件就翻车。
解决:在训练阶段就让reset()随机化起点与目标点,障碍物初始位置也随机摆。环境随机化是深度强化学习路径规划泛化的第一课,具体做法是self.goal_pos = self.rng.uniform(...),再用一个正态分布扰动障碍物位置。只有训练分布和评估分布重叠,成功率才有意义。
5.5 现象:仿真跑得欢,上了真机之后频繁撞墙,尤其在 ROS2 里替换路径规划模块后
原因:sim-to-real gap。仿真里的障碍物是完美的圆形或矩形,激光测距是带高斯噪声的理想值,电机响应是离散的格子移动。真机上的传感器噪声、轮子打滑、转向延迟,任何一个都会让训练好的策略在陌生输入上崩掉。ROS2 路径规划里常见做法是把本地 costmap 的膨胀半径调大,但这掩盖不了模型对噪声敏感的根因。
解决:用 domain randomization 做数据增强。训练时给观测加高斯噪声,给动作执行加延迟或随机扰动,让智能体学会在感知不确定下决策。我的习惯是先在仿真里把噪声方差从 0 逐步加到 0.1,看成功率还能不能稳住,再上真机。另一个实用技巧是在仿真里故意把小车的动作空间放宽一格,真机执行时做一个速度与转角限幅,预留误差余量。
6. 训练完先别急着部署:一套 20 次随机种子的评估协议
模型训练完,最危险的时刻是「看到一次漂亮的演示视频就以为成了」。这一步我会用评估协议代替直觉。协议很简单:固定 20 个随机种子,每个种子跑 20 回合,统计成功率、平均回合长度、平均路径长度、碰撞次数四个指标。20 是经验值,种子太少统计不显著,太多本地推理时间翻倍。表格长这样:
| 指标 | 计算公式 | 合格线 |
|---|---|---|
| 成功率 | 到达目标回合数 / 总回合数 | 大于 90% |
| 平均回合长度 | 到达回合的步数均值 | 小于 max_steps 的 60% |
| 平均路径长度 | 路径像素长度 / 起点到目标直线长度 | 小于 1.5 |
| 碰撞次数 | 所有回合碰撞次数之和 | 小于总回合数的 5% |
评估代码不用重新训练,在run_policy基础上包一层种子循环就行:
def evaluate(env, model, seeds=20, episodes_per_seed=20): success = 0 total_steps = 0 collide = 0 total_episodes = seeds * episodes_per_seed for seed in range(seeds): env.reset(seed=seed) for _ in range(episodes_per_seed): obs, _ = env.reset() done = False steps = 0 while not done and steps < env.max_steps: obs_t = torch.tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) action = model(obs_t).argmax().item() obs, _, terminated, truncated, _ = env.step(action) steps += 1 done = terminated or truncated total_steps += steps if terminated: success += 1 if "collision" in env.info: collide += 1 print(f"success rate: {success / total_episodes:.2%}, " f"avg steps: {total_steps / total_episodes:.1f}")这套跑完如果合格,再谈部署。我踩过一个真实的坑:当时只跑了一次随机种子就欢呼「成功率高」,后来按协议跑 20 个种子,成功率只有 43%,原因是障碍物一旦摆到某个角落,小车全撞上去。从那以后我把这个评估函数直接写进训练脚本里,训练完自动执行,没跑完协议不许部署。评估脚本建议留在仓库里,后续每次改参数都重跑,防止调一个细节把原有能力带崩。这个习惯在 AGV 和无人机项目里帮我挡掉了至少三次返工。希望帮到你。
本文还有配套的精品资源,点击获取