☰
PyTorch强化学习机器人路径规划:动态策略训练与部署实战
2026/10/12 1:04:37 网站建设 项目流程

简介:这份PDF文档面向机器人路径规划与强化学习方向的学习者和研究者,系统讲解如何借助PyTorch在复杂环境中训练动态策略。文档共43页,支持目录章节跳转、阅读器左侧大纲显示与章节快速定位,文字、图表、目录等元素显示正常,压缩包内为1个PDF文件,大小约2.04MB,便于在电脑或阅读器上随时查阅。内容从路径规划基本概念、常见图搜索与采样算法讲起,逐步过渡到强化学习基础、PyTorch张量、自动求导与计算图、策略网络与价值网络构建,并深入复杂环境建模、策略梯度与价值迭代算法、代码实现与案例分析、实验结果评估等模块,目录结构完整、条理清晰。已有45人学习,适合希望把PyTorch与强化学习结合到机器人路径规划项目中的读者,可据此理解动态策略训练的整体流程、算法对比思路与实验评估方法,并作为学习参考使用。

1. 机器人路径规划遇上 PyTorch 强化学习:复杂环境里动态策略到底怎么训

去年帮一个仓储 AGV 团队调路径规划,他们原本用 A* 加动态窗口法,在货架密集、人车混行的场景里,拐角处经常出现「原地抖动」——规划器每 200ms 重算一次,路径在两条几乎等价的通道之间反复横跳,车体来回摆。换成 PyTorch 强化学习做动态策略训练之后,抖动没了,但新问题来了:训练 300 万步不收敛,reward 曲线像心电图。这不是个例。复杂环境下的机器人路径规划,难的不是「能不能用强化学习」,而是「动态策略怎么训得稳、训完怎么部署」。

这篇笔记面向两类人:一是已经会用 PyTorch 搭网络、但没把强化学习真正落到机器人路径规划上的工程师;二是正在做多 AGV、移动机器人调度,想从传统规划器切到学习型策略的从业者。我会把环境建模、网络结构、训练循环、奖励设计、仿真到实机的迁移这几个环节拆开讲,每个环节给出可复现的代码骨架和参数含义。不堆公式,重点说清楚「为什么这么设」和「设错了会怎样」。

2. 把路径规划建模成强化学习问题:状态、动作、奖励怎么定

2.1 为什么路径规划适合用强化学习而不是纯规划器

传统路径规划(A*、RRT、DWA)本质是在已知或在线构建的代价地图上做搜索或优化,它们假设环境模型是准确的、代价函数是手工设计的。一旦环境里有动态障碍、传感器噪声、执行误差,手工代价函数就很难覆盖所有情况。强化学习的价值在于:把「怎么走」这件事交给策略网络从交互中自己学,代价函数只需要给出稀疏或半稀疏的奖励信号。

但这里有个常见误解:不是所有路径规划都该上强化学习。如果环境是静态的、地图完全已知、机器人运动学简单,A* 加平滑就够了,上强化学习是杀鸡用牛刀。真正适合强化学习的场景是:动态障碍物频繁出现、环境部分可观测、多机器人之间存在交互、传统规划器参数调不过来。仓储 AGV、服务机器人、自动驾驶的局部避障,都属于这一类。

从建模角度,路径规划问题可以写成马尔可夫决策过程:状态 s 包含机器人位姿、速度、激光或深度观测、目标相对位置;动作 a 是线速度与角速度,或者离散的前进/左转/右转;奖励 r 要同时鼓励接近目标、避开障碍、保持运动平滑。这个建模方式不新鲜,但每个量的具体定义方式直接决定训练能不能收敛。

2.2 状态空间设计:激光雷达数据怎么进网络

状态设计是第一个翻车高发区。我见过最常见的错误是把原始激光数据直接拉平成一个几百维向量丢进全连接层,结果网络学不动。正确做法是先做降采样和特征提取。

以 2D 激光雷达为例,假设一圈 360 个点,先按角度分箱降到 72 维,每 5 度取一个扇区的最小距离。这样既保留了方向信息,又降低了维度。如果用的是深度相机,常见做法是过一个小型 CNN 提特征,再和位姿向量拼接。

import torch import torch.nn as nn class StateEncoder(nn.Module): def __init__(self, laser_bins=72, pose_dim=4, feature_dim=128): super().__init__() # 激光分支:一维卷积提取局部障碍模式 self.laser_conv = nn.Sequential( nn.Conv1d(1, 16, kernel_size=5, stride=2, padding=2), nn.ReLU(), nn.Conv1d(16, 32, kernel_size=3, stride=2, padding=1), nn.ReLU(), nn.AdaptiveAvgPool1d(16) # 压缩到固定长度 ) # 位姿分支:机器人当前 x, y, yaw, 线速度 self.pose_fc = nn.Sequential( nn.Linear(pose_dim, 32), nn.ReLU() ) # 融合层 self.fusion = nn.Sequential( nn.Linear(32 * 16 + 32, feature_dim), nn.ReLU() ) def forward(self, laser, pose): # laser: (B, 1, laser_bins) pose: (B, pose_dim) l = self.laser_conv(laser).flatten(1) p = self.pose_fc(pose) return self.fusion(torch.cat([l, p], dim=1))

这段代码里,laser_bins=72是降采样后的激光维度,pose_dim=4对应 x、y、yaw、当前线速度。卷积核大小 5 和 3 是经验值,太大容易过拟合,太小感受野不够。AdaptiveAvgPool1d(16)把任意长度的卷积输出压到 16 维,这样换激光雷达型号时不用改网络结构。融合层把两路特征拼起来,输出 128 维状态嵌入。

参数上,feature_dim建议在 64 到 256 之间。太小表达能力不够,太大会拖慢训练且容易过拟合。如果环境里障碍物密集,激光分支的卷积核可以适当加大到 7,增强对连续障碍的感知。

2.3 动作空间与奖励函数:连续控制还是离散决策

动作空间的选择取决于机器人底盘类型。差速底盘常用连续动作:线速度 v 和角速度 ω,范围分别是 [0, 0.5] m/s 和 [-1.0, 1.0] rad/s。全向底盘可以加横向速度。离散动作空间适合教学和快速验证,比如 {前进, 左转, 右转, 停止},但实际部署时离散动作会导致运动不平滑,需要额外做插值。

奖励函数是训练成败的关键。我一般用三层结构:接近目标给正向奖励,碰撞给大负奖励,每步给微小负奖励鼓励尽快到达。具体形式:

def compute_reward(prev_dist, curr_dist, collision, reached, action, dt=0.1): # 距离奖励:靠近目标为正 r_dist = (prev_dist - curr_dist) * 5.0 # 碰撞惩罚 r_collision = -10.0 if collision else 0.0 # 到达奖励 r_reached = 20.0 if reached else 0.0 # 时间惩罚:每步扣一点,鼓励短路径 r_time = -0.05 # 动作平滑惩罚:角速度变化大就扣分 r_smooth = -0.1 * abs(action[1]) return r_dist + r_collision + r_reached + r_time + r_smooth

r_dist的系数 5.0 是放大距离变化信号,太小会被时间惩罚淹没,太大会导致策略只盯着目标不看障碍。r_collision=-10和r_reached=20的比例关系很重要,碰撞惩罚必须显著大于到达奖励,否则策略会学会「撞过去更快」。r_smooth抑制角速度抖动,系数 0.1 是调出来的,太大机器人会变得迟钝。

提示:奖励函数不要一开始就写复杂。先用距离加碰撞两项跑通,确认策略能到达目标,再逐步加平滑、能耗等项。一次性堆太多奖励项,出了问题根本不知道是哪一项导致的。

3. 用 PyTorch 搭 PPO 训练循环:从环境交互到策略更新

3.1 为什么选 PPO 而不是 DQN 或 SAC

路径规划的强化学习算法选型,常见候选是 DQN、PPO、SAC、TD3。DQN 只适合离散动作,且样本效率低,在连续控制场景基本不用考虑。SAC 和 TD3 是 off-policy 算法,样本效率高,但训练不稳定,对超参数敏感,调参成本高。PPO 是 on-policy 算法,样本效率低一些,但训练稳定、实现简单、超参数鲁棒,适合作为路径规划的第一版方案。

我一般建议:先用 PPO 跑通整个流程,确认环境建模和奖励设计没问题,再考虑换 SAC 提升样本效率。直接上 SAC,很可能环境有问题但误以为是算法调参问题,浪费大量时间。

PPO 的核心是裁剪目标函数,限制每次策略更新的幅度。PyTorch 实现时,关键是正确计算优势函数和裁剪损失。下面是一个精简但可运行的 PPO 训练循环骨架。

import torch import torch.nn as nn import torch.optim as optim from torch.distributions import Normal class ActorCritic(nn.Module): def __init__(self, state_dim=128, action_dim=2): super().__init__() self.shared = nn.Sequential( nn.Linear(state_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU() ) self.actor_mean = nn.Linear(256, action_dim) self.actor_logstd = nn.Parameter(torch.zeros(action_dim)) self.critic = nn.Linear(256, 1) def forward(self, state): x = self.shared(state) mean = torch.tanh(self.actor_mean(x)) # 动作归一化到 [-1,1] std = self.actor_logstd.exp().expand_as(mean) value = self.critic(x) return mean, std, value def act(self, state): mean, std, value = self.forward(state) dist = Normal(mean, std) action = dist.sample() log_prob = dist.log_prob(action).sum(dim=-1) return action, log_prob, value

state_dim=128对应上一节 StateEncoder 的输出。action_dim=2是线速度和角速度。actor_logstd用可学习参数而不是固定值,让策略自己调整探索程度。tanh把动作压到 [-1,1],部署时再映射到实际速度范围。

训练循环里,PPO 需要收集一批轨迹,计算回报和优势,然后做多轮更新。关键参数是clip_ratio(一般 0.2)、gamma(0.99)、lambda(0.95)、ppo_epochs(10 左右)。

def ppo_update(policy, optimizer, states, actions, old_log_probs, returns, advantages, clip_ratio=0.2, ppo_epochs=10): for _ in range(ppo_epochs): mean, std, values = policy(states) dist = Normal(mean, std) new_log_probs = dist.log_prob(actions).sum(dim=-1) ratio = (new_log_probs - old_log_probs).exp() # 裁剪目标 surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip_ratio, 1 + clip_ratio) * advantages actor_loss = -torch.min(surr1, surr2).mean() # 价值损失 critic_loss = nn.MSELoss()(values.squeeze(), returns) # 熵奖励鼓励探索 entropy = dist.entropy().sum(dim=-1).mean() loss = actor_loss + 0.5 * critic_loss - 0.01 * entropy optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(policy.parameters(), max_norm=0.5) optimizer.step()

clip_ratio=0.2是 PPO 论文的默认值,路径规划场景一般不用改。0.5 * critic_loss里的 0.5 是价值损失权重,如果 value loss 震荡厉害可以降到 0.25。0.01 * entropy是熵系数,太小策略会过早收敛到局部最优,太大策略一直随机探索。clip_grad_norm_的 0.5 防止梯度爆炸,这个在路径规划里很关键,因为奖励稀疏时梯度容易突然变大。

3.2 并行环境采样:怎么把训练速度提上来

单环境采样是 PPO 训练慢的主要原因。路径规划一个 episode 动辄几百步,串行采样效率极低。常见做法是用多个环境实例并行采样,比如 16 或 32 个。

如果用的是 Gazebo 或 Isaac Sim,可以启动多个仿真实例,每个实例跑一个机器人。如果用的是轻量级自研仿真,可以用 Python 多进程。下面是一个多进程采样的简化示例:

import multiprocessing as mp import numpy as np def worker(env_fn, policy_state_dict, num_steps, seed, queue): env = env_fn() policy = ActorCritic() policy.load_state_dict(policy_state_dict) policy.eval() torch.manual_seed(seed) states, actions, rewards, log_probs, dones = [], [], [], [], [] state = env.reset() for _ in range(num_steps): with torch.no_grad(): s_tensor = torch.FloatTensor(state).unsqueeze(0) action, log_prob, _ = policy.act(s_tensor) next_state, reward, done, _ = env.step(action.squeeze().numpy()) states.append(state) actions.append(action.squeeze().numpy()) rewards.append(reward) log_probs.append(log_prob.item()) dones.append(done) state = env.reset() if done else next_state queue.put((states, actions, rewards, log_probs, dones))

每个 worker 跑固定步数后把数据放进队列,主进程收集后统一做 PPO 更新。num_steps一般设 256 或 512,太小优势估计不准,太大单次更新数据量过多。worker 数量建议等于 CPU 核心数,GPU 训练时 8 到 16 个 worker 通常够用。

注意:多进程采样时,策略网络参数在采样前要同步到每个 worker。如果 worker 里用的是旧参数,而主进程已经更新了多轮,会导致 on-policy 假设被破坏,训练不稳定。常见做法是每轮采样前广播一次参数。

3.3 训练过程监控:哪些指标必须盯着

训练路径规划策略,光看 reward 曲线不够。我一般同时监控四个指标:平均回报、平均 episode 长度、碰撞率、到达率。

平均回报反映整体策略质量,但容易被个别高回报 episode 拉偏,所以要配合中位数一起看。平均 episode 长度在路径规划里很关键:如果长度突然变短,可能是策略学会了「快速撞墙结束」;如果一直变长但不收敛,可能是机器人在原地打转。碰撞率和到达率是最直接的业务指标,碰撞率高于 20% 基本不能部署,到达率低于 80% 说明策略还没学会。

用 TensorBoard 记录这些指标是常规做法。关键是设置合理的日志频率,每个 episode 都记太密,每 100 个 episode 记一次又太稀。我一般每 10 个 episode 记一次,同时保存最近 100 个 episode 的滑动平均。

from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter(log_dir="./runs/ppo_path_planning") def log_metrics(writer, episode, avg_return, avg_length, collision_rate, success_rate): writer.add_scalar("train/avg_return", avg_return, episode) writer.add_scalar("train/avg_length", avg_length, episode) writer.add_scalar("train/collision_rate", collision_rate, episode) writer.add_scalar("train/success_rate", success_rate, episode)

如果 collision_rate 在前 500 个 episode 里没有下降趋势,不要急着调学习率,先检查奖励函数里碰撞惩罚是否足够大、状态里是否包含障碍物距离信息。这两个问题占了训练不收敛原因的一大半。

4. 复杂环境下的动态策略调优:域随机化与课程学习

4.1 域随机化:让策略在仿真里见过足够多的「意外」

仿真训练的策略直接搬到实机,最常见的翻车是:仿真里激光雷达没有噪声、地面没有打滑、障碍物位置精确已知,实机里这些全都有。域随机化就是在训练时随机化环境参数,让策略学会应对不确定性。

需要随机化的参数包括:激光雷达噪声(高斯噪声,标准差 0.01 到 0.05 米)、机器人运动学参数(轮径误差 ±5%、轮距误差 ±3%)、障碍物位置和数量(每 episode 随机生成)、地面摩擦系数(0.5 到 1.0)、控制延迟(0 到 2 个时间步)。

class DomainRandomizer: def __init__(self): self.laser_noise_std = 0.02 self.wheel_radius_error = 0.05 self.friction_range = (0.5, 1.0) self.max_delay_steps = 2 def randomize(self, env): env.laser_noise_std = np.random.uniform(0.01, 0.05) env.wheel_radius *= np.random.uniform( 1 - self.wheel_radius_error, 1 + self.wheel_radius_error) env.friction = np.random.uniform(*self.friction_range) env.control_delay = np.random.randint(0, self.max_delay_steps + 1) return env

随机化范围要逐步扩大。一开始用较小的噪声,策略学会基本避障后再加大。如果一上来就加很大噪声,策略可能什么都学不到。laser_noise_std从 0.01 开始,每 200 个 episode 增加 0.005,上限 0.05。摩擦系数范围也是从窄到宽。

4.2 课程学习:从简单场景逐步过渡到复杂场景

复杂环境直接训练,稀疏奖励下策略很难获得正反馈。课程学习的思想是:先让机器人在空旷环境里学会到达目标,再逐步增加障碍物密度、缩小通道宽度、增加动态障碍。

具体课程设计我一般分四个阶段:

阶段障碍物数量通道宽度动态障碍目标距离训练 episode
10-2> 2.0m无3-5m0-1000
23-51.5-2.0m无5-8m1000-3000
35-81.0-1.5m1 个8-12m3000-6000
48-120.8-1.0m2-3 个10-15m6000-10000

阶段切换的判据是到达率连续 100 个 episode 高于 90%。如果没达到就继续训练当前阶段,不要强行切换。我见过有人按固定 episode 数切换,结果阶段 3 还没学会就进阶段 4,策略直接崩掉,reward 掉到负值再也起不来。

课程学习实现上,可以在环境 reset 时根据当前阶段采样场景参数:

def reset_with_curriculum(env, episode): if episode < 1000: stage = 1 elif episode < 3000: stage = 2 elif episode < 6000: stage = 3 else: stage = 4 config = { 1: {"n_obstacles": (0, 2), "corridor_width": (2.0, 3.0), "n_dynamic": 0, "goal_dist": (3, 5)}, 2: {"n_obstacles": (3, 5), "corridor_width": (1.5, 2.0), "n_dynamic": 0, "goal_dist": (5, 8)}, 3: {"n_obstacles": (5, 8), "corridor_width": (1.0, 1.5), "n_dynamic": 1, "goal_dist": (8, 12)}, 4: {"n_obstacles": (8, 12), "corridor_width": (0.8, 1.0), "n_dynamic": 3, "goal_dist": (10, 15)}, }[stage] env.set_obstacles(np.random.randint(*config["n_obstacles"])) env.set_corridor_width(np.random.uniform(*config["corridor_width"])) env.set_dynamic_obstacles(config["n_dynamic"]) return env.reset(goal_distance=np.random.uniform(*config["goal_dist"]))

4.3 策略网络轻量化:从训练到部署的模型压缩

训练时可以用大网络,部署时往往要跑在算力有限的嵌入式平台。PyTorch 训练完的模型,常见做法是导出 ONNX 再转 TensorRT,或者直接量化。

先导出 ONNX:

dummy_input = torch.randn(1, 128) torch.onnx.export( policy, dummy_input, "policy.onnx", input_names=["state"], output_names=["action_mean", "action_std", "value"], dynamic_axes={"state": {0: "batch"}}, opset_version=11 )

导出时注意opset_version选 11 或以上,低版本对某些算子支持不好。dynamic_axes设置 batch 维度动态,方便部署时调整 batch size。

量化方面,PyTorch 支持动态量化和静态量化。路径规划策略网络一般用动态量化就够了,精度损失小,实现简单:

quantized_model = torch.quantization.quantize_dynamic( policy, {nn.Linear}, dtype=torch.qint8 ) torch.save(quantized_model.state_dict(), "policy_quantized.pt")

{nn.Linear}指定量化线性层,卷积层一般不量化,因为对精度影响较大。量化后模型大小通常缩小到原来的 1/4,推理速度提升 2 到 3 倍。但要注意,量化后的策略输出会有微小偏差,部署前要在仿真里验证到达率和碰撞率没有明显下降。

5. 避坑与排查:路径规划强化学习训练中最容易翻车的五件事

5.1 现象:reward 一直不涨,episode 长度越来越短

原因:碰撞惩罚不够大,策略学会了「快速撞墙结束拿负奖励」比「慢慢探索拿时间惩罚」更划算。这是路径规划里最隐蔽的坑,因为 reward 曲线看起来在「收敛」,实际上是收敛到了最差策略。

解决:把碰撞惩罚提高到到达奖励的 2 到 3 倍,同时检查时间惩罚是否过大。如果每步时间惩罚是 -0.05,一个 500 步的 episode 累计 -25,而碰撞只扣 -10,策略当然选择撞。我一般把碰撞惩罚设为 -20 到 -50,到达奖励 20 到 30,时间惩罚 -0.01 到 -0.02。

5.2 现象:训练前期 reward 上升,中期突然崩掉

原因:学习率过大导致策略更新步子太大,或者 PPO 的 clip_ratio 失效。常见于用了自适应学习率但没有设上限,或者优势函数计算时没有做标准化。

解决:检查 advantage 是否做了标准化(减均值除标准差),检查学习率是否在 3e-4 到 1e-4 之间。如果用的是 Adam,把 eps 从默认 1e-8 调到 1e-5,防止极端梯度导致参数更新过大。另外确认clip_grad_norm_的 max_norm 不要超过 1.0。

5.3 现象:仿真里到达率 95%,实机上原地打转或撞墙

原因:仿真和实机的观测分布不一致。最常见的是激光雷达安装角度偏差、里程计累积误差、控制延迟。仿真里激光雷达假设是水平安装、无延迟,实机上雷达可能倾斜 2 度,控制指令有 50ms 延迟。

解决:在仿真里加入观测噪声和控制延迟,做域随机化。实机部署前,先用手动遥控让机器人走几圈,记录激光和里程计数据,和仿真数据对比分布。如果差异大,要么校准传感器,要么在仿真里加对应的噪声模型。

5.4 现象:多 AGV 场景下,单车策略直接复制导致死锁

原因:单车训练时没有考虑其他机器人的存在,策略把其他 AGV 当成静态障碍物,两车相遇时互相等待,形成死锁。

解决:训练时在环境里加入其他机器人,可以是固定策略的脚本机器人,也可以是历史版本的策略。状态里要包含其他机器人的相对位置和速度。奖励函数里加入「死锁惩罚」:如果连续 N 步位置变化小于阈值,扣分。N 一般取 20 到 50,取决于控制频率。

5.5 现象:换了激光雷达型号后策略完全失效

原因:状态编码器对激光输入维度敏感。如果原来用 72 维,新雷达输出 360 维,直接输入网络会维度不匹配;即使做了降采样,分箱方式不同也会导致特征分布偏移。

解决:状态编码器里用AdaptiveAvgPool1d或自适应池化层,让网络对输入长度不敏感。同时固定分箱逻辑:不管原始点数多少,都按角度均分到固定数量的 bin,每个 bin 取最小值。这样换雷达只需要改预处理代码,网络不用动。

6. 从仿真到实机的最后一公里:策略验证与安全兜底

训练完的策略,在仿真里到达率 95% 以上,不代表能直接上实机。我一般会做三层验证。

第一层是仿真压力测试:用训练时没见过的场景配置跑 500 个 episode,包括极端窄通道、密集动态障碍、传感器噪声拉满。到达率低于 85% 就不往下走。

第二层是硬件在环:把策略部署到实际控制器上,但机器人架空,轮子不接触地面。输入用录制的真实传感器数据回放,看策略输出的动作是否合理、推理延迟是否满足控制周期。路径规划的控制周期一般 10 到 20 Hz,推理延迟要控制在 20ms 以内。

第三层是低速实机测试:在空旷场地,限速 0.2 m/s,周围有人监护。先跑直线到达,再跑简单避障,最后跑完整场景。每一层通过后再进下一层。

安全兜底方面,不管策略多好,都要有一个独立的紧急停止逻辑。常见做法是:策略输出动作后,过一个安全检查模块,如果激光检测到障碍物距离小于 0.3 米且策略还在输出前进指令,直接覆盖为停止。这个模块用传统规则实现,不依赖学习策略。

def safety_shield(action, min_laser_dist, safe_dist=0.3): v, omega = action if min_laser_dist < safe_dist: # 距离过近,强制停止前进,只允许旋转 v = 0.0 omega = np.clip(omega, -0.5, 0.5) return np.array([v, omega])

safe_dist=0.3是安全距离,根据机器人尺寸和制动距离调整。这个兜底逻辑看起来简单,但能避免绝大多数实机碰撞。我自己的习惯是:任何学习型策略上线前,安全兜底模块必须先单独测试通过,而且兜底逻辑的优先级永远高于策略输出。

最后说一个我踩过的坑:不要用训练时的 reward 曲线来判断策略能不能部署。reward 高只说明策略在优化目标上表现好,不代表行为安全、平滑、符合业务预期。一定要看实际轨迹,把机器人走过的路径画出来,肉眼检查有没有贴墙、抖动、绕远路。这一步花的时间,比调参多得多,但省下来的实机调试时间,更多。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询