简介:面向强化学习与智能机器人方向的开发者与研究者,这份资源围绕深度确定性策略梯度(DDPG)算法,提供了一个完整的机器人导航系统实现。项目覆盖路径规划、环境交互、奖励机制、状态空间与动作空间建模,并包含神经网络训练、经验回放、目标网络与探索策略等核心模块,可直接用于自动驾驶、无人机航拍、工厂自动化等连续控制场景的方案设计与代码学习。压缩包共60个文件,核心为Python源码(含环境、智能体、网络结构与训练脚本)、训练与测试CSV数据,以及可运行编译文件、配置文件、说明文档和附赠资源,包体大小约6.59MB。目录划分清晰,便于按“算法实现—数据记录—文档说明”路径快速上手。已有140人学习下载,适合正在研究连续动作空间控制、需要参考完整DDPG导航项目或准备扩展改进算法的中高级学习者。
1. 基于DDPG的机器人导航:为什么连续动作空间里它比DQN更顺手
一个装有激光雷达的差速小车要从 A 点走到 B 点,中间有墙、有障碍物,你不想手动布置路点,也不想写一堆 if-else 避障逻辑,而是想让机器人在仿真环境里自己试错、自己学会“看到障碍就减速转向”这种策略——这就是基于深度确定性策略梯度(DDPG)算法做机器人导航要解决的核心问题。DDPG 属于强化学习里的 actor-critic 家族,它最大的特点是能直接输出连续动作,比如“线速度 0.3 m/s + 角速度 0.5 rad/s”,而不是像 DQN 那样只能从预设的离散动作里挑一个。对于移动机器人导航,连续的速度指令才是真实控制接口,所以 DDPG 在实际落地中比 DQN 更顺手。这套方案的完整链路包括环境交互、状态空间与动作空间设计、奖励机制、神经网络训练、经验回放、目标网络稳定策略,最后在仿真环境或实体小车上验证路径规划效果。
2. DDPG 的核心机制拆解:Actor-Critic、经验回放与目标网络在导航里各自干了什么
2.1 为什么机器人导航不能用 DQN:离散动作是死穴
DQN 在 Atari 游戏上的成功让很多人误以为它可以直接迁移到机器人控制,但真做导航时你会发现一个根本矛盾:DQN 输出的是离散动作索引。你可以把动作空间定义为“前进、左转、右转、后退”四选一,但小车实际底盘接收的是连续速度指令。如果你想用 DQN,只能把连续空间离散化,比如把角速度从 -1.0 到 1.0 按步长 0.2 切成 10 档,线速度再切 5 档,组合出来 50 个动作。这个做法有两个问题:一是动作粒度太粗,机器人走出来的轨迹是折线,转弯时机往往刚好错过;二是动作数量增多后,DQN 的价值网络要同时评估 50 个动作的 Q 值,训练样本效率急剧下降。实际项目里我见过有人把 DQN 的离散动作压到 8 个,结果小车在窄走廊里来回撞墙,根本原因是它永远等不到“刚好对准门缝”的那个动作。
DDPG 直接把这个问题绕过去了。它的策略网络(actor)输入当前状态,输出一个多维连续向量,例如二维就是 [线速度, 角速度],每个维度经过 tanh 激活函数限制在 [-1, 1],再乘上你设置的速度上限。这样机器人能输出 0.3 m/s 的线速度配合 0.52 rad/s 的角速度这种任意组合,控制细腻程度和 PID 手动调参差不多了。所以如果你要做的是“让机器人在连续动作空间里学习控制策略”,DDPG 是比 DQN 合理得多的起点,而这也是这个项目标题把 DDPG 算法单独拎出来的原因。
2.2 Actor-Critic 架构:策略网络与价值网络的分工
DDPG 里有两个角色:Actor(演员)负责决策,Critic(评论家)负责评价。Actor 是一个参数为 θμ 的神经网络,输入状态 s,输出动作 a;Critic 是一个参数为 θQ 的神经网络,输入状态 s 和动作 a,输出一个 Q 值,表示“在这个状态下执行这个动作,长期回报期望是多少”。
训练时 Critic 的工作是尽量准确地预测长期回报,它的损失函数是 TD error:
LQ = E[(r + γ Q'(s', a') - Q(s, a))²]
其中 r 是即时奖励,γ 是折扣因子,Q' 是目标 critic 网络的输出,a' 是目标 actor 网络在 s' 状态下输出的动作。这个公式的核心意思是:当前 Q 值应该逼近“即时奖励 + 下一状态的估计 Q 值”,也就是把未来的回报逐步往回传。
Actor 的更新用的则是策略梯度。为了让 Q 值尽量大,Actor 朝着“当前状态对应的 Q 值上升最快”的方向调整参数。具体实现里,梯度通过链式法则从 critic 的输出反传到 actor 的输出,不需要像 REINFORCE 那样依赖完整回合的回报累计。这也是 DDPG 能在线持续学习的关键——每一步交互完都能立即更新两组网络,不必等到一个 episode 结束。
在导航任务里,Actor 的输入通常是激光雷达距离数据、目标点相对位置、当前速度;Critic 的输入除了状态还要拼上 Actor 给出的动作。不要小看这个“拼上动作”的细节:Critic 只有同时看到状态和动作才能合理打分,否则它无法判断“这个状态下动作 a1 比 a2 好在哪里”。我第一次实现时把 Critic 的输入只写成状态,结果 Q 值永远不收敛,后来才发现网络结构错了。
2.3 经验回放与目标网络:DDPG 中两个稳定训练的关键机制
强化学习在线训练的一个天然问题是样本相关性太强。机器人这一秒的状态和下一秒的状态只差了一个控制周期,如果直接按到达顺序训练,网络会反复拟合最近几帧的数据,把之前学到的经验忘掉。DDPG 解决这个问题的办法是建一个经验回放缓冲区(replay buffer),每次交互产生的 (s, a, r, s', done) 五元组都存进去,训练时随机采样一批不相关的样本。相当于把机器人走过的路全部录下来,训练时打乱重放。
缓冲区大小一般设 50 万到 100 万条。导航任务每个 episode 可能产生 200 到 500 条经验,100 万条意味着能覆盖大约 2000 到 5000 个完整轨迹,这对让机器人记住“走廊尽头的分叉处如何选择”这种稀疏出现的场景很重要。采样批次大小我一般设 64 或 128,太大则更新方向过于平均,探索性弱;太小则梯度噪声大。
目标网络是另一个稳定化手段。DDPG 里有两套网络:在线网络和目标网络。在线网络实时更新,目标网络则通过“软更新”缓慢跟进:
θ_target ← τ θ_online + (1 - τ) θ_target
τ 一般取 0.001 到 0.01。这样做的好处是计算 TD target 时用的 Q 值来自一个变化很慢的网络,不会因为当前网络一步跳变过大而导致训练发散。打个比方,你在教一个新员工判断工作好坏,如果评价标准每天变,他会疯掉;目标网络就是那个“标准相对稳定”的导师。
3. 状态空间、动作空间与奖励机制:导航任务建模的细节
3.1 状态空间怎么定义:激光雷达、目标点与速度信息如何组织
导航任务的状态空间设计直接决定算法能不能学出来。我常用的方案是把三部分信息拼接成一个一维向量:激光雷达测距值、目标点相对位姿、当前速度。
激光雷达部分,不是把所有扫描线全部塞进网络。比如 360 度、1 度分辨率的雷达一次返回 360 个数值,直接输入会让网络参数暴涨,而且相邻角度的距离值高度相关,训练效率反而低。常见做法是降采样到 24 到 36 个等间距角度,覆盖机器人四周 360 度。另一个做法是使用 180 度前向扫描加尾部两个测距点,因为差速小车一般不倒车。我自己的习惯是取 24 维均匀分布在 360 度上的距离值,做一个截断,超过 3 米的统一置为 3 米,这样网络不需要区分“远处 4 米”和“远处 10 米”的差异,都能视为无障碍。
目标点相对位姿一般用机器人为参考系,而不是全局坐标。因为神经网络对绝对坐标不敏感,但对“目标在我的左前方 45 度、距离 2 米”这种相对描述学得很快。计算方式是用目标全局坐标减去机器人全局坐标,再旋转到机器人坐标系:
dx_local = (dx_global · cos(θ) + dy_global · sin(θ)) dy_local = (-dx_global · sin(θ) + dy_global · cos(θ))
然后把 dx_local、dy_local 或者距离和角度拼进状态向量。当前速度(线速度、角速度)也必须包含,否则策略网络不知道小车现在是在往前走还是在打转,输出的动作会缺乏连续性,轨迹容易出现抖动。
最终状态向量形如:state = [laser_24维, dx_local, dy_local, v, ω],共 29 维。这里维度不宜太高,300 维以上的状态向量会让样本效率急剧下降,仿真环境里还算能忍,真机上训练成本就完全无法接受了。
3.2 动作空间:连续速度指令的约束与归一化
动作空间比状态空间简单,但约束条件不比状态少。差速机器人的控制输入常见是 (v, ω),线速度和角速度。DDPG 的 actor 输出层用 tanh 激活,输出范围天然是 [-1, 1],你需要把它映射到实际物理限制:
v_real = v_tanh · v_max ω_real = ω_tanh · ω_max
举个例子,你设 v_max = 0.5 m/s,ω_max = 1.0 rad/s,那 actor 输出的 (0.3, -0.6) 对应实际指令是 (0.15 m/s, -0.6 rad/s)。这里有个容易被忽略的细节:差速小车执行 (v, ω) 指令时,如果 v 太小而 ω 太大,机器人基本上在原地打转,里程计积分误差会快速累积。所以有的实现会对动作空间加一个约束,比如 |ω| ≤ k · (v + ε),限制原地旋转。这个约束可以在 actor 输出之后、环境执行之前做一次处理,或者直接在动作向量里加一个惩罚项。
另外要注意,动作空间不能直接使用原始速度值而不做归一化。原因在于 actor 网络最后一层 tanh 的梯度特征,输入范围过大时误差反向传播的梯度数值不稳定。把动作限制在 [-1, 1] 再做缩放,既能匹配激活函数的敏感区间,又让超参数(比如噪声方差)与机器人的物理速度上限解耦。改天你想让同一套训练代码跑在不同速度上限的小车上,只需要改 v_max 和 ω_max 两个常数,网络和噪声参数都不用动。
3.3 奖励机制:稀疏与稠密怎么结合,以及“贪快”的反效果
奖励机制是导航训练里最“玄学”也最影响成败的部分,没有之一。太稀疏的奖励——比如只有到达终点给 +10,撞墙给 -10,其余每步 0——理论上是可行的,但实践中 DDPG 很难从零开始探索到终点,尤其当起点和目标距离超过 5 米时,随机策略基本找不到目标,整个过程就是随机游走,训练十几个小时曲线纹丝不动。
所以你需要稠密奖励来引导探索。最常见的做法是让每一步的奖励与“到目标点的距离变化”挂钩:
r_step = α · (d_prev - d_now)
其中 d_prev 是上一步到目标的距离,d_now 是当前步到目标的距离。只要这一步离目标更近,就得到正奖励。这个设计思路很直觉,但你很快会踩坑:机器人可能学会原地转圈或左右横跳,因为只要某一帧“碰巧”让距离变小就获得正奖励,整体路径根本不高效。因此还要叠加一个时间惩罚:
r_step = α · (d_prev - d_now) - β
β 是一个小正数,比如 0.01,每一活步都扣一点,逼机器人尽快到达。如果任务地图较大,β 太大机器人会抄近路撞墙,太小又会慢慢悠悠绕远路,这个参数需要反复调。
最终的奖励函数我的经验写法是分段组合:
- 到达目标:+50,并结束当前 episode
- 碰撞障碍物(激光雷达最小读数低于安全阈值):-20,并结束
- 正常步进:α·(d_prev - d_now) - β,α 取 2.0,β 取 0.01
这里 α 和 β 的比值要控制好。另一种常见错误是只给“接近目标”的正奖励而不惩罚碰撞距离,结果机器人学会紧贴墙边滑行——因为贴着墙走往往能让距离观测值逐渐下降,但行为显然是错的。考虑到激光雷达有测量噪声,碰撞判定不要只看单一测距点,我会取最近的三根激光束的最小值做滤波,避免小车视觉上“贴着墙”但雷达偶发读到更远的斜向距离导致漏判。
4. 训练实现:从网络结构到训练循环
4.1 网络结构:两个全连接网络,BatchNorm 与激活函数的选择
DDPG 的 actor 和 critic 网络结构都不复杂,复杂的是怎么让它们在导航任务里稳定训练。以 29 维状态输入、2 维动作输出为例,我常用的 actor 网络如下:
import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, state_dim=29, action_dim=2, hidden=256): super(Actor, self).__init__() self.fc1 = nn.Linear(state_dim, hidden) self.bn1 = nn.BatchNorm1d(hidden) self.fc2 = nn.Linear(hidden, hidden) self.bn2 = nn.BatchNorm1d(hidden) self.fc3 = nn.Linear(hidden, action_dim) def forward(self, state): x = F.relu(self.bn1(self.fc1(state))) x = F.relu(self.bn2(self.fc2(x))) # tanh 把动作压到 [-1, 1],之后在环境侧缩放为实际速度 return torch.tanh(self.fc3(x))BatchNorm1d 在这里很重要。激光雷达距离值虽然做了截断归一化,但不同场景下数据分布仍然差异很大,比如空旷房间和狭窄走廊的雷达读数方差完全不同。没有 BatchNorm,网络很容易在训练中后期因为输入的分布漂移而出现梯度不稳定。需要注意的是,推理时 BatchNorm 要切换成 eval 模式,否则它还会用训练 batch 的均值方差做归一化,造成行为不一致。
Critic 网络结构略有不同,它的输入是状态拼接动作:
class Critic(nn.Module): def __init__(self, state_dim=29, action_dim=2, hidden=256): super(Critic, self).__init__() # 先单独编码状态 self.fc1 = nn.Linear(state_dim + action_dim, hidden) self.bn1 = nn.BatchNorm1d(hidden) self.fc2 = nn.Linear(hidden, hidden) self.bn2 = nn.BatchNorm1d(hidden) self.fc3 = nn.Linear(hidden, 1) def forward(self, state, action): x = torch.cat([state, action], dim=1) x = F.relu(self.bn1(self.fc1(x))) x = F.relu(self.bn2(self.fc2(x))) return self.fc3(x)隐藏层宽度我选 256。太窄(比如 64)拟合不了激光雷达观测与最优动作之间的复杂映射;太宽(比如 1024)在样本量不够的导航场景里很容易过拟合,表现为训练集上 Q 值一直涨,但仿真测试时小车一进新环境就乱撞。中间层的激活函数建议 ReLU,输出层 actor 用 tanh、critic 不用激活,这是 DDPG 的标准配置,一般不需要创新。
4.2 训练循环:采样、更新、软更新的完整节奏
训练循环是整篇代码里最需要照抄的部分,节奏不对网络就学不动。我按顺序拆解如下:
# 超参数 lr_actor = 1e-4 # actor 学习率,过大会导致策略跳变 lr_critic = 1e-3 # critic 学习率,通常比 actor 大 5 到 10 倍 tau = 0.005 # 目标网络软更新系数 gamma = 0.99 # 折扣因子 batch_size = 64 # 采样批次大小 buffer_size = 500000 # 经验回放缓冲容量 noise_std = 0.1 # 探索噪声标准差 noise_decay = 0.9995 # 噪声衰减系数 # 初始化网络与目标网络 actor = Actor(state_dim, action_dim) critic = Critic(state_dim, action_dim) actor_target = Actor(state_dim, action_dim) critic_target = Critic(state_dim, action_dim) # 把在线网络参数复制到目标网络 actor_target.load_state_dict(actor.state_dict()) critic_target.load_state_dict(critic.state_dict()) # 优化器 optim_actor = torch.optim.Adam(actor.parameters(), lr=lr_actor) optim_critic = torch.optim.Adam(critic.parameters(), lr=lr_critic) # 经验回放缓冲区 replay_buffer = deque(maxlen=buffer_size) for episode in range(max_episodes): state = env.reset() # 重置到随机起点和目标点 total_reward = 0 done = False for step in range(max_steps_per_episode): # 1. 动作选择加探索噪声(OU 噪声或高斯噪声) action = actor(state) noise = np.random.normal(0, noise_std, size=action.shape) action = np.clip(action + noise, -1.0, 1.0) # 2. 与环境交互 next_state, reward, done, info = env.step(action) # 3. 存入经验回放 replay_buffer.append((state, action, reward, next_state, done)) # 4. 经验足够后开始训练 if len(replay_buffer) > batch_size: batch = random.sample(replay_buffer, batch_size) states, actions, rewards, next_states, dones = zip(*batch) states = torch.FloatTensor(np.array(states)) actions = torch.FloatTensor(np.array(actions)) rewards = torch.FloatTensor(np.array(rewards)).unsqueeze(1) next_states = torch.FloatTensor(np.array(next_states)) dones = torch.FloatTensor(np.array(dones)).unsqueeze(1) # 计算 TD target:r + γ * target_Q(s', target_actor(s')) next_actions = actor_target(next_states) target_Q = critic_target(next_states, next_actions) y = rewards + gamma * (1 - dones) * target_Q # 更新 critic current_Q = critic(states, actions) critic_loss = F.mse_loss(current_Q, y.detach()) optim_critic.zero_grad() critic_loss.backward() optim_critic.step() # 更新 actor:目标是最大化当前 Q 值 actor_loss = -critic(states, actor(states)).mean() optim_actor.zero_grad() actor_loss.backward() optim_actor.step() # 软更新目标网络 for target_param, param in zip(actor_target.parameters(), actor.parameters()): target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data) for target_param, param in zip(critic_target.parameters(), critic.parameters()): target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data) state = next_state total_reward += reward if done: break # 每 20 个 episode 衰减一次探索噪声 if episode % 20 == 0: noise_std = max(0.02, noise_std * noise_decay)这段循环里需要注意几个细节。dones 的处理很关键,当 terminal 发生时 target Q 不能再往后传,否则目标值会包含超出 episode 的虚假回报,代码里用 (1 - dones) 做掩码实现。actor 的损失在 PyTorch 里写成负的期望 Q 值,最大化 Q 等价于最小化 -Q。两个网络共用一个状态输入但图层不同,需要确认梯度不会穿到对方网络的参数上,所以 actor 的 loss 只反传到 actor 的参数,critic 的 loss 只反传到 critic 的参数,PyTorch 的优化器设计天然保证了这一点。
4.3 超参数:学习率、噪声、回放缓冲区的配对建议
超参数里第一个要强调的是 actor 和 critic 学习率的比例。我见过太多人把两个网络的学习率都设成 1e-3,结果 critic 学得飞快,actor 跟不上,策略每更新一步就变化很大,训练曲线反复振荡。经验做法是 critic 用 1e-3,actor 用 1e-4 或者更低的 3e-5。理由很简单:actor 的更新要依赖 critic 提供的梯度信号,如果这个信号本身不稳,actor 的低学习率就是一个天然的滤波器。
探索噪声的标准差建议初始 0.1 到 0.2,随训练进行衰减到 0.02 左右。注意不要衰减到 0,因为 DDPG 的确定性策略一旦完全失去噪声,就容易陷入局部最优,比如永远沿着某一条固定路径走,换一个起点就不会了。我一般保留 0.02 到 0.05 的残余噪声作为持续探索。
经验回放缓冲区的大小也直接影响训练效果。缓冲区过小(低于 10 万条)会让数据分布偏向最近的轨迹,样本相关性上升;缓冲区过大(超过 200 万条)会让旧经验占据主导,而旧经验里大量是早期随机探索产生的无效数据,拖慢后期学习。导航场景我常用 50 万条,同时每 episode 结束后把当天新数据优先参与采样。
5. DDPG 导航训练避坑:4 个高频问题与排查方法
5.1 训练半天 loss 不降或者 reward 曲线平躺
现象:训练 500 个 episode,total reward 始终在 -100 到 -50 之间波动,没有上升趋势。
原因:最常见的是探索噪声太小,导致机器人从第一个 episode 开始就只做小幅动作,永远撞不上墙也到不了目标,学不到任何有效梯度信号;或者是奖励函数里的距离变化项 α 太小,正负奖励几乎抵消,网络分辨不出哪些动作更好。
解决:先检查噪声。把噪声标准差临时提高到 0.3,观察机器人是否开始“乱窜”。如果它都不怎么动,说明动作输出被 tanh 限制后乘以最大速度还是太小,检查速度映射。再检查奖励曲线里每一档的占比——我常用的办法是打印最近 100 步的平均奖励等指标,看奖励是否稀疏:如果只有到达和碰撞两种非零奖励,就要把距离差项调大。α 可以从 2.0 往上加,直到机器人明显表现出“朝向目标走时奖励变高”的行为。
5.2 轨迹抖动:小车走出的路径在目标点周围来回打转
现象:训练后机器人能到达目标点附近,但始终定不下来,在目标点周围画出半径 0.3 米左右的圈,或者反复前后蠕行。
原因:到达目标的判定距离太苛刻,比如设置为 0.1 米,而机器人实际控制精度达不到,于是只能在附近反复徘徊;也可能是速度映射比例失调,最小速度指令已经超过了精度所需的控制分辨率,导致机器人永远过冲。
解决:把到达判定阈值放宽到 0.15 到 0.3 米,取决于机器人尺寸和定位精度。如果这个阶段训练曲线已经很高,说明策略本身没问题,是任务定义与控制分辨率的匹配问题。另一个做法是在距离小于 0.5 米时把线速度上限从 v_max 降为 0.3·v_max——相当于一个简单的“低速接近段”,可以显著消除尾部抖动。这个技巧不需要改网络,改环境封装即可。
5.3 仿真里跑得好,真机就翻车:sim-to-real 的观测噪声问题
现象:在 gazebo 仿真里测试成功率 90%,部署到真实小车后前几步就撞墙,或者朝目标方向走的过程中突然偏转。
原因:仿真环境里的激光雷达读数没有噪声,状态转换是确定性的;真机上雷达有镜面反射、混叠、盲区,里程计积分也会漂移。仿真里网络可能过度依赖“雷达值超过 3 米”这种干净特征,真机上这个条件会因为玻璃或低反射物体失效。
解决:在仿真训练时给激光雷达观测加高斯噪声,方差约为测量值的 2% 到 5%,给里程计位姿加累积漂移。另一个更有效的做法是随机化起始位姿和地图障碍物布局。每次 reset 时,把障碍物位置随机挪动 10 到 20 厘米,目标点随机变化,逼网络学习“通用的避障导航策略”而不是某一张地图的记忆。我踩过的坑是用了固定地图训练,仿真测试一次通过率 95%,真机一换场地立刻翻车,加随机化后这个问题明显缓解。真机测试前还要确认速度上限是否和仿真一致,两个环境的 v_max 差 0.1 m/s 都可能让策略的行为边界失效。
5.4 训练崩溃:Q 值突然爆炸变负数
现象:训练正常进行到几百个 episode,critic loss 突然从小幅波动变成上万,然后所有 Q 值输出变成很大的负数,之后再不恢复。
原因:最常见的是奖励没有做尺度控制。如果奖励函数里最大奖励是 +50,碰撞惩罚是 -20,而 Q 网络的初始输出接近 0,目标值在训练初期会在 [-20, 50] 之间跳变,梯度很容易让某些层的权重更新幅度过大,进入“死亡 ReLU”或梯度爆炸状态。另一个原因是 batch 采样到了连续同一时刻的样本,导致 TD target 自相关过高,引发连锁发散。
解决:一是把奖励整体 scale 到 [-1, 1] 区间,比如到达 +1.0、碰撞 -0.5、步进惩罚 -0.01,这些值能学,而大数值反而降低稳定性。二是检查采样是否均匀——确保随机采样,不要按 episode 顺序取样。三是给 critic 的梯度加 clip,torch.nn.utils.clip_grad_norm_(critic.parameters(), 1.0),防止单个 batch 的异常样本把网络打飞。这个坑的特点是一旦崩溃基本救不回来,只能 rerun,所以要在训练脚本里周期性保存模型检查点,至少每 100 个 episode 存一次。
5.5 维度不匹配:state 与 action 拼接时的隐性错误
现象:程序不报错,训练也能跑,但效果极差。看了网络结构发现 actor 输出维度是 2,critic 输入维度是 state_dim + action_dim,似乎没错。
原因:可能是状态向量里混入了未归一化的原始距离值,比如激光雷达返回 0.08 到 10 米不等的原始值,而其他特征都在 0 到 1 之间,导致网络输入分布严重倾斜。距离值需要先除以雷达量程,把范围压到 [0, 1]。另一种可能是在拼接时用了错误的数据类型,action 是 numpy float64,state 是 torch float32,torch.cat 直接报类型错,但如果你用 tensorflow 反而不会报错,只会静默做类型转换,训练效果就悄悄变差。
解决:统一在采样后做一次torch.as_tensor(state, dtype=torch.float32)和torch.as_tensor(action, dtype=torch.float32)。把状态向量的每个维度做 min-max 归一化,范围全部压到 0 到 1 之间。归一化手段不必复杂,但一定要有。这里可以先把雷达距离除以测量上限,把 dx_local 和 dy_local 除以最大距离常数,速度除以 v_max,然后拼接,就能发现训练稳定性上一个台阶。
6. 评估与验证:怎么确认导航系统真的“学会”了
6.1 三个核心评估指标:到达率、路径长度比、平均速度
训练结束时不能只看 total reward,那个数值包含了太多奖励权重的主观因素。我习惯在固定的测试场景里跑固定数量的 episode(比如 50 个,起点和目标随机),统计三个客观指标。
到达率是最直观的,定义为成功到达目标点的 episode 数量除以总 episode 数量。对 5 米以内的近距离导航,我认为低于 80% 都说明策略还不稳;对 10 米以上的复杂环境,60% 以上可以接受。
路径长度比是实际行驶轨迹长度除以两点间直线距离,这个比值越接近 1 越好。1.0 到 1.3 说明策略路径接近最优,1.5 以上说明绕路明显,可能原因是奖励函数里距离差项权重不足。要注意碰撞结束的 episode 不参与这个统计,否则路径长度会被截断拉低数值,产生“撞墙了但路径比很高”的假象。
平均速度只统计成功到达的 episode,衡量机器人在实际导航中的效率。单位是 m/s,一般低于 v_max 的 50% 说明策略过于保守。三个指标放在一起看才能判断系统的真实水平——比如到达率 95% 但路径长度比 1.8,机器人可能只是学会了“安全地绕远路”,并不会让你满意。
6.2 可视化复盘:轨迹叠加与 Q 值曲线怎么读
代码跑完只能看到 reward 曲线太抽象,我强烈建议做两个可视化。第一个是把每个测试 episode 的机器人轨迹画到地图上,用不同颜色区分成功和失败轨迹。失败的轨迹会告诉你策略在哪类障碍物附近出问题——是总在直角转弯处撞墙,还是到了目标点附近就开始乱转,还是被凹形障碍物困住。这些信息比任何 loss 曲线都直接,是调参最快的方式。
第二个是看 Q 值曲线。把同一个状态下(比如起点处)critic 对不同动作的打分差异可视化。一个合格的 critic 应该在“朝向目标且避障”的动作上打出明显高于“朝向墙”的动作的分数。如果两者差距不显著,说明 critic 还没有学会分辨好动作和坏动作,actor 自然也学不好。这个检查可以直接用训练好的模型在测试状态上做一次前向推理,打印 Q 值。
最后说一个我自己的习惯:从记录的训练检查点里,挑出过去 20 个 episode 平均 reward 最高的那个模型,而不是训练结束时的最终模型。因为强化学习训练过程本身就带着波动,最后一帧未必是最优策略。把最好的检查点导出、跑完整测试集、保存轨迹可视化结果,这个流程才算完整闭环。整个 DDPG 导航系统的实现,核心不是把代码跑通,而是建立起“策略—仿真—评估—调整”的正反馈,每一次踩坑都让奖励函数和观测归一化更贴近机器人实际。希望帮到你。
本文还有配套的精品资源,点击获取