☰
DDPG深度强化学习移动机器人导航:从环境搭建到训练避坑指南
2026/9/26 13:51:42 网站建设 项目流程

简介:面向计算机、自动化、电子信息等专业的毕业设计或课程大作业,一套基于TensorFlow与Gazebo的DDPG端到端移动机器人导航项目完整可用。代码均经过测试并通过运行验证,拿来即可使用,既能支撑答辩与项目展示,也可作为深度学习新手从理论到实践的进阶资料。压缩包共28个文件,以Python源码、演示动画GIF、项目配置XML和说明文档MD为核心,整体约50MB,结构紧凑、模块划分清晰,方便按需查阅。目前已有55人学习/下载,资源中特意保留的action_dim=2失败与action_dim=1成功对比实验,直观展示了连续控制中动作维度对训练效果的影响。结合源码、说明与演示动画,读者可以深入理解DDPG网络构建、Gazebo仿真环境交互以及端到端导航的训练思路,并在此基础上继续改造,适配课程设计、毕业设计等实际场景。

1. 为什么说DDPG导航项目的重头不在训练而在环境

做连续控制的移动机器人导航,最棘手的地方反而不是DDPG神经网络本身,而是仿真环境一次次在细节处把训练流程击穿。这份基于TensorFlow与Gazebo的DDPG深度强化学习导航资源,把源码、说明文档、论文和数据集整理成了一套可以从零复现的毕业设计闭环。它解决的是移动机器人从激光雷达观测到速度指令的端到端映射问题——连续动作空间正好是DDPG的强项,而Gazebo提供可重复的碰撞代价和随机化场景。适合做毕业设计、课程大作业,或者想快速把手里的ROS仿真环境跑起来验证强化学习效果的人。接下来我会把网络设计、奖励函数、超参设置以及我实际复现时踩过的坑逐条拆给你看。

2. DDPG选型与Gazebo环境搭建:从连续动作到仿真通信

2.1 DDPG不是黑匣子:确定性策略梯度在导航里的定位

先明白DDPG在导航任务里的定位。移动机器人导航归根结底是一个马尔可夫决策过程:每个时刻读取机器人状态,输出一个动作,动作造成状态转移,环境给出奖励。但这里的动作不是离散的按键,而是连续的线速度和角速度(v, w)。如果用DQN,动作空间必须先离散化,比如把角速度分成-1.0、-0.5、0、0.5、1.0五档,档位越细精度越高,但Q值输出维度也跟着成倍增加,训练效果很难看。

DDPG的关键在于用确定性策略梯度求解。Actor网络直接输出一个连续的动作值,Critic网络去评估这个动作在当前状态下到底好不好。Actor的学习信号从Critic的梯度中来,而不是像传统Policy Gradient那样靠采样一大片动作来估算期望回报。这样做的优点是方差低,适合控制频率高的机器人场景;缺点是对超参数敏感,奖励函数稍有偏差就容易不收敛,这一点我会在第5章重点展开。DDPG项目在TensorFlow 2.x下面实现时,用的都是Keras的Model子类,把Actor和Critic定义成两个独立的网络,再挂一个目标网络做软更新。

还有一个必须理解的概念是“经验回放”。Gazebo里采样的每个(状态s,动作a,奖励r,下一状态s')会存进一个很大的循环队列,训练时随机抽一批出来更新网络。这样做的原因是把时间上强相关的样本打散,避免网络因为连续几十步都撞墙而把模型带偏。如果你的训练脚本只采样不存buffer,每一步的经验都会直接丢弃,学习过程就会变成“机械记忆最近的动作”,梯度更新方向极其不稳定。

选型理由用几句话概括就是:动作连续、控制频率快、需要离线复用历史经验,这三点都指向DDPG。而Gazebo在这个链路里的角色是数据源和碰撞裁判,它输出激光雷达和里程计数据,同时用物理引擎计算碰撞、摩擦和电机响应。把真实世界换成仿真环境的好处是,你可以在一张地图上随机初始化机器人位置跑几百次,这种随机化在真实实验里很难做到。TensorFlow负责网络定义和梯度计算,Gazebo负责数据生成和交互反馈,两者通过ROS话题通信,这是这套资源的基本架构。

2.2 Gazebo环境搭建:模型、话题和传感器数据流

Gazebo环境搭建在毕业设计里最容易卡死的点是版本匹配。常见做法是装ROS Noetic配Gazebo 11,或者ROS Melodic配Gazebo 9。这套资源对应的环境按论文里的说明来装,如果你机器上已经装过别的ROS版本,一定要先查版本兼容表,否则launch文件启动后会提示找不到gazebo_ros插件,这是第一道坎。

这里给出我一般会走的环境验证流程:

# 1. 启动仿真世界(这里以TurtleBot3为例,换成资源包里的launch文件即可) roslaunch turtlebot3_gazebo turtlebot3_world.launch # 2. 另开终端,确认话题已经发布 rostopic list | grep -E "scan|odom|cmd_vel" # 3. 直接看一下激光雷达数据有没有刷新,5秒内应有连续数据 rostopic echo /scan --noarr -n 5

代码逻辑说明:第一步启动仿真世界,本质上是把机器人URDF模型、激光雷达传感器和物理引擎串起来;第二步用rostopic list检查话题,/scan是激光雷达、/odom是里程计、/cmd_vel是速度指令话题,这三条构成了DDPG训练的数据闭环;第三步是看数据刷新频率,如果/scan长时间没有新数据,多半是传感器插件没有加载成功,需要回头查launch文件。

常见做法里还有一个必查项是模型路径。Gazebo默认要从GAZEBO_MODEL_PATH读取模型库,我遇到过好几次启动后场景一片空白的情况,原因就是模型库没有下载完整,或者环境变量没有写进~/.bashrc。检查方法很简单:

echo $GAZEBO_MODEL_PATH ls /usr/share/gazebo-*/models | head -20

如果资源包里自带world文件,还要确认world里引用的模型名字和实际model目录里的名字完全一致,大小写不一致也会导致加载失败。这一步看似基础,但几乎所有第一次跑Gazebo的人都会在它上面花掉半天时间。

传感器数据流这部分,我建议把训练代码直接订阅ROS话题而不是从Gazebo内部拿数据。这样做的边界很清楚:数据格式、坐标系、时间戳都由ROS层处理,后续要换真机时,算法代码不用改,只要把话题来源从Gazebo换成真机的传感器节点。下面这张表整理的是这套导航系统最常用的几个话题和数据结构,写论文时也可以直接引用:

话题消息类型内容在导航里的用途
/scansensor_msgs/LaserScan激光雷达的距离数组观测状态输入
/odomnav_msgs/Odometry机器人位置与速度状态与目标计算
/cmd_velgeometry_msgs/Twist线速度和角速度动作输出
/base_linktf坐标系变换把目标点转到机器人坐标系

其中/scan的angle_min、angle_max和angle_increment决定了激光雷达的视野角度和分辨率,Gazebo默认是180度,但不同厂家的真机传感器可能是270度。状态空间设计时要和这个参数保持一致,不然换真机时间维度对不上。另外要检查激光雷达装在哪个高度、能不能扫到障碍物底部,雷达高度太高的话低矮障碍物会直接漏检,训练出的策略在真实场景里会有盲区。这个细节放在选型章来讲,是为了让你在一开始就建立“把Gazebo当成真机替身”的意识。下面进入状态空间、动作空间和奖励的设计,这些才是DDPG能否收敛的核心。

3. 端到端导航实现:状态、动作、奖励与Actor-Critic网络

3.1 状态空间设计:激光雷达降采样和目标坐标组合

端到端导航的重头戏在第一层:观测状态到底怎么组织。有人误以为“端到端”就是把激光雷达原始数据全部塞进网络,其实工程里很少这么做。Gazebo的激光雷达一个周期能出几百个点,如果全塞进网络,Actor和Critic的输入维度会变得巨大,而且相邻角度上的点在物理上高度冗余。常见做法是先降采样之后再拼上目标信息。

我一般会把180度的激光数据下采样到36个点,每5度一个,再加3个目标相关量:目标距离、目标相对于机器人朝向的角度差、上一时刻的动作。为什么一定要加目标信息?因为纯激光数据只有障碍物分布,没有“我要去哪”的意图,导航就变成了无头苍蝇乱撞。

import numpy as np def build_obs(laser_ranges, laser_angle_min, laser_angle_max, goal_pose, robot_pose): """ 从激光雷达和机器人/目标位姿构建DDPG观测向量 laser_ranges: 原始激光距离数组 goal_pose / robot_pose: 世界坐标系下的位姿 """ # 1. 激光数据过滤无效值,归一化到0~1 ranges = np.clip(np.nan_to_num(laser_ranges, nan=3.5), 0.0, 3.5) ranges = ranges / 3.5 # 2. 均匀采样36个点,从原始点数组里按索引抽 idx = np.linspace(0, len(ranges) - 1, 36, dtype=int) obs_lidar = ranges[idx] # 3. 目标坐标转到机器人坐标系 dx = goal_pose[0] - robot_pose[0] dy = goal_pose[1] - robot_pose[1] dist = np.sqrt(dx**2 + dy**2) theta = np.arctan2(dy, dx) - robot_pose[2] # 4. 拼接成最终观测 obs = np.concatenate([obs_lidar, [dist, theta / np.pi]]) return obs.astype(np.float32)

这段代码的逻辑有四个要点:首先把激光原始数据里的nan和超出量程的值统一裁剪到3.5米,再除以3.5实现归一化,避免距离值量纲差异影响网络输入;然后用等距取索引的方式把激光点数从几百压到36,这一步直接把网络输入维度降了一个量级;第三是把目标位置从世界坐标转到机器人坐标系,theta除以pi是为了让角度变化范围落在[-1,1]区间;最后把激光和两个目标标量拼起来,得到一个38维的观测向量。

参数说明:lidar分辨率、max_range、目标距离的截断值都是可调的。如果机器人速度比较快,我会把36改成48或64,让网络看得更远;如果环境里障碍物很密,3.5米的最大探测距离可以缩到2.5米,让网络更关注近处的避障。但记得所有改动都要同步改网络输入层维度,这点很多初学者会漏掉,一改观测维度就忘了改Actor和Critic的Input层,直接报维度不匹配。

3.2 动作空间与速度约束:避免原地打转的限幅细节

动作空间是(线速度v,角速度w)两维,看起来简单,但限幅很有讲究。底盘的物理约束一般是线速度上限0.22m/s、角速度上限2.75rad/s,这是TurtleBot型底盘常见参数。如果直接把Actor网络输出不加限制,训练初期网络会疯狂输出大角速度,机器人变成原地陀螺,永远不会前进,碰撞次数和训练时长都会爆表。

def action_to_twist(action, v_max=0.22, w_max=2.75): """ 把Actor输出的连续量映射到底盘速度指令 action: 网络输出, shape=(2,), 范围理论上在(-1,1)之间 """ v = np.clip(action[0], -v_max, v_max) w = np.clip(action[1], -w_max, w_max) # 常见做法:倒车速度限一半,防止训练初期频繁倒退碰撞 v = np.clip(v, -v_max * 0.5, v_max) # 原地旋转最小角速度阈值,低于这个值直接置0,避免机器人缓慢抖动 if abs(w) < 0.05: w = 0.0 twist = Twist() twist.linear.x = v twist.angular.z = w return twist

这段代码在速度映射上做了两层保险。第一层:倒车限幅到正转速度的一半,因为导航任务中绝大多数情况需要前进,倒退只用于脱困,限幅能显著降低碰撞频率;第二层:设置0.05rad/s的角速度死区,避免机器人因为微小角速度输出而一直缓慢转动,这在Gazebo里会表现为“画圈前进”,看起来像漂移。实际调参过程中,如果机器人还是打转,我会把v_max降到0.18,让前进和转弯的比例更平衡。

动作空间设计还有一个被忽略的细节:Actor网络的输出是tanh激活,值域天然在(-1,1),但如果你把动作直接当作速度用,0.22和2.75这两个限幅就完全没意义。必须经过上述映射代码还原成物理量纲。而且限幅参数要和你使用的机器人模型对齐,不同底盘参数差别很大,Gazebo世界文件里的两轮差速模型和真车的最大速度可能完全不同,迁移真机前一定要重新查底盘手册。

3.3 奖励函数设计:稀疏奖励在Gazebo里根本学不动

这是整个项目中最玄学的部分。资源包里的reward设置如果直接用,很可能训练两三个小时reward曲线都不动一下。原因很直白:稀疏奖励下,机器人要随机撞到目标点才得到一次正反馈,在连续动作空间里这个概率低到可以忽略。所以必须做奖励塑形。

我的默认奖励构成是四项叠加:到达目标加10分、碰撞罚-5分、距离下降给正向小奖励每步加0.1、角速度过猛罚-0.1。所有距离量都尽量放到机器人坐标系下计算,不要直接用世界坐标距离,否则机器人转弯时距离计算也会有波动。

def compute_reward(dist_now, dist_last, collision, reached, omega): """ 塑形后的奖励函数 dist_now/dist_last: 机器人到目标的当前距离与上一时刻距离 collision: 是否碰撞 reached: 是否到达目标 omega: 当前角速度, 用于限制原地打转 """ if reached: return 10.0 if collision: return -5.0 # 距离接近奖励: 比上一时刻更近就给正奖励 delta = dist_last - dist_now reward = 0.0 if delta > 0: reward += 0.1 # 角速度惩罚: 抑制原地旋转行为 reward -= 0.1 * min(abs(omega), 1.0) return reward

奖励函数里最关键的是距离接近奖励的方向判断。注意delta = dist_last - dist_now,只有“现在距离比上一时刻近”才加0.1,这意味着机器人每往前走一小步只要方向正确,就能持续获得正向反馈,形成稳定的学习信号。碰撞惩罚-5和到达奖励10的比例也很重要,我调过很多版本,如果碰撞惩罚小于-3,机器人会倾向“冲过去撞了再倒车”,训练过程反复横跳;如果大于-8,机器人会变得极度保守,在障碍物前很远就停下来,永远到不了目标。

角速度惩罚这一项经常被忽略,但它对训练质量影响很大。不加这一项时,机器人学到的最优策略可能是原地快速旋转扫描目标方向,然后直接冲过去,看似有效但动作非常暴躁,转真机时电机根本承受不了。加了惩罚后,策略会自动收敛到平缓的转弯。参数0.1可以调整,如果发现转弯还是太猛可以提到0.2,但要小心别让机器人不敢转弯,变成全程直线行驶然后频繁撞墙。

提示:奖励函数的输出要在同一个量级内,到达奖励、碰撞惩罚和逐帧奖励不能差太多,否则大数值奖励会主导梯度,让网络忽略小奖励携带的方向信息。

3.4 Actor与Critic网络:TensorFlow Keras的两种结构

网络结构在毕业设计里不必追求花哨,MLP加ReLU足够完成这个任务。Actor要输出连续动作,所以最后一层用tanh把值映射到(-1,1);Critic要把“动作好不好”评出来,所以输入是状态和动作拼在一起的向量。这里有一个细节:Critic最好把动作在第二层才拼进来,比第一层直接拼接效果更稳,这也是DDPG原论文实现里的常见做法。

import tensorflow as tf def make_actor(obs_dim, action_dim=2): obs_input = tf.keras.Input(shape=(obs_dim,)) x = tf.keras.layers.Dense(256, activation="relu")(obs_input) x = tf.keras.layers.Dense(256, activation="relu")(x) action = tf.keras.layers.Dense(action_dim, activation="tanh")(x) return tf.keras.Model(obs_input, action) def make_critic(obs_dim, action_dim=2): obs_input = tf.keras.Input(shape=(obs_dim,)) action_input = tf.keras.Input(shape=(action_dim,)) x = tf.keras.layers.Dense(256, activation="relu")(obs_input) # 动作在第二层拼接,Critic能先提取状态特征再融合动作信息 x = tf.keras.layers.Concatenate()([x, action_input]) x = tf.keras.layers.Dense(256, activation="relu")(x) q_value = tf.keras.layers.Dense(1)(x) return tf.keras.Model([obs_input, action_input], q_value)

Actor和Critic的隐藏层都取256加256,对38维输入、2维输出的任务规模来说已经足够。如果训练时发现欠拟合,可以先加到512再观察,不要一上来就堆大网络;如果加了容量还不见涨,那大概率是奖励函数的问题而不是网络容量不够,把这个判断顺序记住能省很多时间。

Critic把动作放在第二层连接,背后的逻辑是让Critic先独立理解状态,再去看动作在这个状态下的价值。参考DDPG原论文的对比实验,这种做法比第一层直接拼接整体表现要好。另外注意Actor的激活函数是tanh,输出天然在(-1,1),配合3.2节的动作映射代码把数值还原到真实速度范围,整个动作链路才算完整。

4. 训练主循环与关键超参:从回放采样到软更新

4.1 训练回环:经验回放、Critic更新和软更新tau

训练主循环的骨架很简单,但每一步都要知道在做什么。先从replay buffer里随机采样一小批经验,这批经验包括当前状态s、动作a、奖励r、下一个状态s'、是否结束done。然后用目标网络计算下一个状态的最大Q值,这是一个“未来价值”的估计值,因为当前Critic更新需要一个目标标签,但未来价值无法精确知道,所以用目标网络来近似。

核心训练代码里必须包含三个更新:Critic更新、Actor更新、目标网络软更新。这里给一个浓缩版的核心循环:

batch_size = 128 gamma = 0.99 tau = 0.005 # 从replay buffer随机采样一个batch states, actions, rewards, next_states, dones = replay_buffer.sample(batch_size) with tf.GradientTape() as tape: # Critic的预测Q值 q_values = critic([states, actions], training=True) # 目标网络计算下一状态的动作与Q值 next_actions = target_actor(next_states) next_q_values = target_critic([next_states, next_actions]) # 贝尔曼方程的目标值 targets = rewards + gamma * (1 - dones) * next_q_values critic_loss = tf.reduce_mean(tf.square(targets - q_values)) critic_grads = tape.gradient(critic_loss, critic.trainable_variables) critic_optimizer.apply_gradients(zip(critic_grads, critic.trainable_variables)) # 更新Actor:目标是最大化Critic给当前状态动作的Q值 with tf.GradientTape() as tape: cur_actions = actor(states, training=True) cur_q_values = critic([states, cur_actions]) actor_loss = -tf.reduce_mean(cur_q_values) actor_grads = tape.gradient(actor_loss, actor.trainable_variables) actor_optimizer.apply_gradients(zip(actor_grads, actor.trainable_variables)) # 软更新:让目标网络慢慢贴近当前网络 for target_param, param in zip(target_actor.weights, actor.weights): target_param.assign(tau * param + (1 - tau) * target_param) for target_param, param in zip(target_critic.weights, critic.weights): target_param.assign(tau * param + (1 - tau) * target_param)

逻辑说明:Critic的loss是目标Q值和预测Q值的均方差,目标Q值由奖励加上折扣后的下一状态Q值得来。dones是一个关键掩码,如果这一step已经结束(比如碰撞),就不能再加入下一状态的价值,否则网络会学到“碰撞后还有未来价值”的错误逻辑。Actor的loss取负的Critic Q值均值,因为梯度下降方向是让loss变小,而我们要让Q值变大,所以取负号。软更新公式是目标参数 = tau * 当前参数 + (1-tau) * 目标参数,tau取0.005意味着每次只向当前网络移动0.5%,这是为了让目标值稳定变化,不会因为当前网络突然大幅更新而导致训练震荡。

训练循环里还有一个容易被忽略的边界条件,就是episode结束的判断。碰撞和超时都要算结束。我一般把单回合步数上限设为150步,超过就结束并给一个额外的小惩罚,不然机器人会在迷宫里绕圈绕到天荒地老,缓冲区里全是绕圈数据,网络被污染得很厉害。超时惩罚要单独记录日志,训练后期如果超时频率降下来,说明策略确实在逼近目的地,而不是在撞墙和绕圈之间来回切换。

4.2 超参抄作业表:学习率、噪声和缓冲区参数

这份资源包里默认的超参可以直接跑通,但在不同Gazebo场景下,有几个参数需要手动调。我把一套在TurtleBot3地图上能稳定收敛的初始参数列出来,后面调参以这个为基准,每改一个参数只动一个变量,不要同时改多个:

参数初始值调参方向与经验
actor学习率1e-4太大容易震荡,太小收敛慢;换复杂地图先降到3e-5
critic学习率1e-3比actor高一个量级,可以让Q值先稳定下来
gamma(折扣因子)0.99接近1表示看重长期收益,路径规划任务常用0.95~0.99
tau(软更新系数)0.0050.001~0.01之间,越小目标网络越稳定
buffer容量200000Gazebo跑数据慢,建议不少于10万条经验
batch_size12864~256之间,显存不够先降到64
探索噪声std0.1训练前5万步用0.2,之后降到0.05
单episode步数上限150地图变大时按对角线长度增加

参数说明拆几个重点:actor学习率不能照搬critic学习率,经验是critic学习率比actor高一个量级,critic先把“状态-动作值”估计准确,actor的梯度才可信。探索噪声std指加在动作上的高斯噪声标准差,强化学习需要随机性来探索未知区域,但噪声过大会导致动作在限幅边缘反复横跳,所以训练中段要线性递减。buffer容量这个参数最容易被忽视,Gazebo仿真一小时内采样不到两万条经验,如果buffer只有五万,很快就存满,旧经验被挤掉,前面学到的东西会被冲掉。

注意:DDPG的目标网络必须在训练启动前复制一份当前网络的初始权重,而不是随机初始化。如果漏了这一步,critic_loss初期会爆炸,reward曲线第一轮冲到很高然后迅速归零。

多提一个容易被忽略的细节:保存模型时要同时保存当前网络和目标网络。很多同学训练完只保存actor,下次加载时直接用,忘了目标网络也需要重建。如果直接从空白的target_actor开始做软更新,前几百步的目标Q值是错的,训练效果会明显退化。

4.3 训练监控:reward曲线之外的三个实操信号

很多新手盯着reward曲线,reward一波动就慌。真实经验是reward曲线噪声极大,因为单次episode里碰撞惩罚和到达奖励差异悬殊,一两步之间reward就可能跳好几分。我一般用三个更稳定的信号判断训练健康状况:平均episode长度、碰撞率、到达率。这三个信号比纯reward更平滑,且物理含义清晰。

操作上可以写一个简单的记录器,在每个episode结束后把length、collision_num、arrive_num写进CSV,然后用matplotlib画图。不需要一开始就上TensorBoard,等训练跑到一半再逐步把loss、q值、平均reward加进去。中期看critic_loss是否持续下降,后期看到达率是否突破0.5,这两个才是真正的收敛信号。

# 每隔100个episode算一次平均回合长度和碰撞次数 tail -n 100 train_log.csv | awk -F',' '{sum+=$1; n++} END {print "avg episode length:", sum/n}' awk -F',' '{if($2>0) coll++} END {print "collision episodes:", coll}' train_log.csv

这个命令在训练跑起来以后,每100个episode计算一次平均长度和碰撞频率。平均长度下降说明避障能力提升,因为机器人更快到达或更快失败;但如果长度持续下降而到达率不涨,说明策略陷入局部最优,比如学会了提前撞墙结束episode来避免长时间绕圈。这时候不是继续等,而是回去调奖励函数或者重置网络,这就是第5章要展开的坑。

5. 复现中最容易翻车的五个坑

5.1 Gazebo启动后空白场景:模型库路径没生效

现象:roslaunch一切正常,但Gazebo主界面一片灰白,机器人模型也没出现,终端没有任何报错。

原因:GAZEBO_MODEL_PATH环境变量没设置,或者模型库没有下载完整。资源包里的world文件引用模型时,如果模型找不到,Gazebo只会静默跳过而不是报错,最终展示一个空白场景。

解决:把资源自带的models目录路径写进~/.bashrc,再重新source:

echo "export GAZEBO_MODEL_PATH=$HOME/.gazebo/models" >> ~/.bashrc echo "export GAZEBO_MODEL_PATH=${GAZEBO_MODEL_PATH}:$PWD/models" >> ~/.bashrc source ~/.bashrc

解决后重新启动launch。如果还是空白,用gz model --list查看模型有没有被加载。另外注意world文件里的mesh引用路径不能是相对路径,否则换目录启动照样加载不出来。这个坑80%的Gazebo新手都会遇到,不要怀疑代码问题,先查模型库。

5.2 reward长时间不涨:奖励值域没做归一化

现象:训练两个小时,reward一直在很小的负值附近波动,没有上升趋势,critic_loss也不下降。

原因:奖励函数量纲和观测量纲没有统一。比如碰撞惩罚给-5、到达奖励给10,而激光雷达观测归一化在0~1之间,目标距离却没有归一化,输入量级差异巨大。网络输入层被大数值维度主导,梯度更新方向被带偏。

解决:把所有观测和奖励都做归一化。观测侧,dist要除以max_range;奖励侧,我习惯把碰撞惩罚改成-1.0、到达奖励改成2.0、接近奖励0.02,让总奖励大致落在[-1,2]区间。改完归一化之后,reward曲线立刻开始有波动,说明梯度更新方向变正常了。这是我从一个五万步学费里换来的经验:强化学习的输入输出量纲不统一,比网络结构错误还致命。

5.3 机器人原地打转:角速度限幅和噪声参数过激

现象:训练到中期,机器人到了目标旁边却不停下来,一直原地转圈,或者一个大转角把自己带偏,平均episode长度只有30步。

原因:角速度限幅太宽或者探索噪声太大。DDPG在探索阶段会往动作上加高斯噪声,如果噪声标准差0.2且角速度上限2.75,噪声累加之后角速度很容易触到2.75边界,机器人每次不是左转就是右转,根本没有直线行驶的机会。

解决:把探索噪声std在训练初期降到0.1,角速度上限降到1.5,同时加角速度死区。把3.2节代码里的死区从0.05提高到0.1,让微小角度误差不触发转弯,机器人就能更稳定地逼近目标。我遇到这个问题时的做法是:把打转时的episode长度打印出来,发现几乎都是50步以内结束,说明还没走几步就转晕了。降低噪声后平均回合长度很快涨到80以上,才开始真正学避障。

5.4 从仿真转真机话题对不上:激光雷达角度范围不一致

现象:仿真里训练好的模型拿到真机上完全失灵,机器人一进走廊就往墙上撞,甚至一启动就在原地螺旋。

原因:Gazebo默认雷达180度、分辨率每度一个点,真机雷达可能是270度、分辨率0.36度。网络输入层是定长的,如果真机扫描点数不是36个降采样后的形状,网络直接报维度错误;即使reshape成功,角度范围和起始角不同也会导致观测语义错位,网络拿到的“第5个点”在仿真里和真机上指向完全不同的方向。

解决:状态构建层要单独写一个适配函数,把真机数据先裁剪成和仿真一致的角度范围。最有效的做法是提前在代码注释里写明雷达角度范围和分辨率,把lidar_range_deg和downsample_points这两个常量集中定义,换平台只改一处。这也是资源包里值得优先看的部分,它把仿真和真机的适配接口单独抽出来了,改起来不用翻整个训练脚本。

5.5 物理时间与训练速度严重不匹配

现象:训练跑了一整晚,实际只训了一万步,仿真里机器人连一圈都没走完,日志显示每一步都卡在等待传感器数据上。

原因:Gazebo的物理仿真步长和训练脚本同步太紧。训练代码每次都要等待/scan更新,如果/scan发布频率是20Hz,那么训练频率最高也只有20Hz,再加上仿真负载波动,实际频率可能只有5Hz。

解决:把replay buffer的填充和网络训练拆成两个线程,Gazebo只负责采样,训练线程在buffer积累到一定量后再开始。另一个常见做法是提升Gazebo的仿真速度,把real time factor调大,但物理精度会下降,碰撞判断相对不可靠,我一般不动它。更推荐的做法是单独设一个高频的cmd_vel发布节点,和DDPG的训练步长解耦,避免每步都等待Gazebo物理仿真一圈的时间。拆成双线程之后,训练速度能提升3到5倍,这是投入产出比最高的一项改动。

6. 把训练好的模型拿去做评测与改进

6.1 评测流程:成功率、固定case和动作回放

训练完模型后,别急着说“能导航了”。我每次都会强制走一遍固定评测流程:单独启动一个评测脚本,在地图里固定摆放20个起点和10个目标点,生成200组起点-目标对,每个episode设定最大步数上限为200,统计到达率。到达率超过70%才敢往下做真机迁移。评测时把机器人的速度、角速度和激光扫描数据都录成日志,跑完之后按时间轴回放,专门观察“哪里有犹豫、哪里有漂移”,这些主观判断比单一数字更容易看出策略短板。

评测环节最能发现的问题是训练场景和评测场景的差异。如果训练时机器人只从固定起点出发,评测时换个起点就找不到目标,说明网络记住了路径而不是学会了导航。所以评测地图一定要和训练地图错开布局,最好把起点、目标点、障碍物位置全部重新随机化。

一个低成本高价值的改进是在奖励函数上动手脚。上面第3章的奖励函数都是围绕“到达目标”做正向引导,如果想让机器人学避障不绕远路,可以把奖励改成到达奖励10加碰撞惩罚-10加每步惩罚-0.01,让机器人倾向于尽快到达而不拖沓。想要更平滑的轨迹,可以再加入一个步间角速度变化惩罚,让相邻两步的角速度差越小奖励越高。这些塑形项加完之后,训练出来的轨迹从“锯齿状”变成“平滑弧线”,后面转真机时电机跟随效果会好很多。

我自己的教训是第一次跑通时完全没做评测,直接拿模型在Gazebo里跑,结果在某些角落反复卡住。后来我花了一整天把评测脚本和日志回放跑顺,才真正看出来问题是奖励函数对近距离障碍物没有惩罚梯度,机器人会在“还敢往前吗”和“撞上去算了”之间反复横跳。从那以后,我每次修改奖励函数或网络结构,都强制自己先跑一遍200组固定评测case,用数据说话,不再凭感觉判断收敛。希望帮到你。

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

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

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

立即咨询