简介:这份资源面向机器人、自动化与人工智能方向的学生及开发者,提供基于深度强化学习、ROS与TensorFlow的移动机器人导航避障完整源码,适合课程设计、期末大作业或毕设参考,也便于研究不同强化学习算法在避障任务中的实现差异。压缩包共约2000个文件,体积5.47MB,以cmake与make构建文件为主,配合139个Python脚本、101个txt说明、launch与msg等ROS配置,以及world、xacro、dae等仿真模型资源,覆盖从算法训练到Gazebo仿真的完整工程链路。资源中可见dueling DQN、dueling DDQN等算法实现痕迹,并包含tf2相关消息与坐标变换模块,便于理解多算法对比与机器人感知决策流程。目前已有349人学习下载,可作为强化学习导航避障的实践起点,帮助读者快速跑通环境、梳理代码结构并在此基础上调试与扩展功能。
1. 移动机器人导航避障:为什么深度强化学习值得你花时间
如果你正在做 ROS 机器人开发,大概率遇到过这样的场景:机器人在走廊里跑得好好的,前面突然多了一把椅子,传统局部规划器直接原地打转或者贴着椅子边蹭过去,运气不好就撞上。基于深度强化学习的导航避障方案,核心思路就是让机器人在仿真环境里反复试错,自己学出一套从激光雷达数据到速度指令的映射策略,不再依赖人工调参的代价地图和膨胀半径。这个方向适合两类人:一类是已经跑通过 ROS 导航栈、想用学习类方法替代或增强局部规划器的工程师;另一类是手上有仿真环境、想快速验证 DRL 算法在连续控制任务上表现的算法同学。TensorFlow 在这里的角色是策略网络的训练和推理框架,ROS 负责传感器数据采集和底层运动控制。整套方案能不能落地,关键看你能不能把训练环境和实际部署之间的鸿沟填上。
2. 从零搭一套 DRL 导航训练环境:ROS 与 TensorFlow 的接口怎么接
2.1 仿真环境选型与机器人模型配置
常见做法是用 Gazebo 作为物理仿真后端,机器人模型用 TurtleBot3 或者自己拼一个差速底盘。激光雷达选 2D 单线还是 3D 多线,直接决定状态空间维度和训练难度。我一般建议新手从 2D 激光入手,24 线或者 36 线的扫描数据已经够用,状态维度控制在 100 以内,训练收敛速度肉眼可见地快。
机器人模型文件里需要确认几个关键参数:轮距、轮半径、最大线速度和最大角速度。这些参数在后面的动作空间设计里会直接用到。如果你用的是现成的 URDF 模型,先跑一遍roslaunch确认激光话题和里程计话题正常发布,不然后面写环境包装器的时候会浪费大量时间排查话题名。
<!-- 机器人差速驱动插件关键参数 --> <plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so"> <left_joint>left_wheel_joint</left_joint> <right_joint>right_wheel_joint</right_joint> <wheel_separation>0.160</wheel_separation> <!-- 轮距,单位米 --> <wheel_diameter>0.066</wheel_diameter> <!-- 轮径,单位米 --> <max_wheel_torque>20</max_wheel_torque> <max_wheel_acceleration>1.0</max_wheel_acceleration> <command_topic>cmd_vel</command_topic> <!-- 速度指令话题 --> <odometry_topic>odom</odometry_topic> <!-- 里程计话题 --> </plugin>轮距和轮径这两个参数必须和实际模型一致,否则仿真里学出来的策略迁移到真机会出现转向角度偏差。最大轮扭矩影响加速性能,如果设得太小,机器人响应会迟钝,训练时容易学到保守策略。
2.2 用 Python 写一个 Gym 风格的 ROS 环境包装器
TensorFlow 的训练循环需要一个标准接口,最省事的做法是把 ROS 话题订阅和发布封装成step()和reset()方法。下面是一个最小可用的环境类骨架,依赖rospy、gym和numpy。
import rospy import gym import numpy as np from sensor_msgs.msg import LaserScan from geometry_msgs.msg import Twist from nav_msgs.msg import Odometry class RosNavEnv(gym.Env): def __init__(self): super(RosNavEnv, self).__init__() # 状态空间:36 线激光 + 目标相对位置(x,y) + 当前速度(v,w) self.observation_space = gym.spaces.Box( low=-np.inf, high=np.inf, shape=(40,), dtype=np.float32) # 动作空间:线速度 [0, 0.3] m/s,角速度 [-1.0, 1.0] rad/s self.action_space = gym.spaces.Box( low=np.array([0.0, -1.0]), high=np.array([0.3, 1.0]), dtype=np.float32) self.scan_data = np.ones(36) * 3.5 # 默认最大距离 3.5m self.odom_data = None self.cmd_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1) rospy.Subscriber('/scan', LaserScan, self._scan_cb) rospy.Subscriber('/odom', Odometry, self._odom_cb) def _scan_cb(self, msg): # 将激光数据降采样到 36 维,取每段最小值 ranges = np.array(msg.ranges) ranges = np.clip(ranges, 0.0, 3.5) # 截断无效值 self.scan_data = ranges.reshape(36, -1).min(axis=1) def _odom_cb(self, msg): self.odom_data = msg def step(self, action): twist = Twist() twist.linear.x = float(action[0]) twist.angular.z = float(action[1]) self.cmd_pub.publish(twist) rospy.sleep(0.1) # 控制周期 10Hz obs = self._get_obs() reward, done = self._compute_reward(obs) return obs, reward, done, {} def _get_obs(self): # 拼接激光、目标相对位置和当前速度 goal_rel = self._get_goal_relative() vel = np.array([self.odom_data.twist.twist.linear.x, self.odom_data.twist.twist.angular.z]) return np.concatenate([self.scan_data, goal_rel, vel]).astype(np.float32)step()里的rospy.sleep(0.1)决定了控制频率,10Hz 对差速底盘来说足够,再高的话仿真步进可能跟不上。激光降采样用reshape(36, -1).min(axis=1)是为了保留每个扇区内的最近障碍距离,比直接等间隔采样更安全。奖励函数的设计是另一个关键点,后面单独讲。
2.3 奖励函数设计:稀疏、稠密还是混合
奖励函数直接决定策略能不能收敛。纯稀疏奖励只在到达目标时给 +1,撞障碍给 -1,其他时候给 0,训练初期几乎学不到东西。纯稠密奖励用距离缩减的差值做引导,容易让机器人学会绕圈刷分。我一般用混合方案:距离引导 + 碰撞惩罚 + 到达奖励 + 时间惩罚。
def _compute_reward(self, obs): goal_dist = np.linalg.norm(obs[36:38]) # 目标相对距离 min_scan = np.min(obs[:36]) # 最近障碍距离 reward = -0.01 # 每步时间惩罚,催促尽快到达 reward += (self.prev_goal_dist - goal_dist) * 2.0 # 靠近目标给正奖励 if min_scan < 0.25: # 碰撞判定 reward -= 10.0 return obs, reward, True if goal_dist < 0.3: # 到达目标 reward += 50.0 return obs, reward, True self.prev_goal_dist = goal_dist return obs, reward, False距离引导系数 2.0 和时间惩罚 -0.01 需要根据场景尺度调整。走廊场景里目标距离变化慢,系数可以大一点;空旷场景里距离变化快,系数太大会导致策略震荡。碰撞阈值 0.25 米要结合机器人半径和激光安装位置来定,设得太小会撞,设得太大机器人不敢走窄通道。
3. 不同 DRL 算法在导航避障上的表现差异:DQN、DDPG 还是 PPO
3.1 离散动作 vs 连续动作:先想清楚你的底盘怎么控
DQN 只能输出离散动作,比如「前进、左转、右转、后退」四个档位。这种方案在简单避障场景里能跑通,但动作切换不连续,机器人走起来一顿一顿的。DDPG 和 PPO 直接输出连续的速度指令,运动平滑,更适合真实底盘。如果你的机器人只做低速巡逻,DQN 够用;如果要跟踪动态目标或者走窄通道,直接上连续动作算法。
TensorFlow 2.x 里实现 DDPG 需要四个网络:Actor 在线网络、Actor 目标网络、Critic 在线网络、Critic 目标网络。目标网络用软更新,更新系数 τ 一般取 0.001 到 0.005。PPO 相对简单,只需要 Actor 和 Critic 两个网络,用裁剪的替代目标函数限制策略更新幅度。
3.2 用 TensorFlow 2 搭 PPO 策略网络:输入输出与超参设置
PPO 在导航任务上比 DDPG 稳定,对超参没那么敏感,适合作为第一个跑通的算法。下面是一个策略网络的定义,输入 40 维状态,输出动作的均值和标准差。
import tensorflow as tf from tensorflow.keras import layers class PPOActor(tf.keras.Model): def __init__(self, action_dim): super(PPOActor, self).__init__() self.fc1 = layers.Dense(128, activation='tanh') self.fc2 = layers.Dense(128, activation='tanh') self.mu = layers.Dense(action_dim, activation='tanh') # 均值,范围 [-1,1] self.log_std = tf.Variable(tf.zeros(action_dim), trainable=True) # 可学习标准差 def call(self, state): x = self.fc1(state) x = self.fc2(x) mu = self.mu(x) # 将动作映射到实际范围:线速度 [0, 0.3],角速度 [-1, 1] mu_scaled = tf.concat([ (mu[:, 0:1] + 1.0) * 0.15, # [0, 0.3] mu[:, 1:2] * 1.0 # [-1, 1] ], axis=1) std = tf.exp(tf.clip_by_value(self.log_std, -2.0, 0.5)) return mu_scaled, std隐藏层用tanh激活而不是relu,是因为导航任务的状态和动作都在有界范围内,tanh的输出更平滑。log_std设成可学习变量,让策略自己调整探索噪声,比固定噪声更灵活。标准差裁剪到[-2.0, 0.5]对应[0.135, 1.648],防止探索噪声过大或过小。
3.3 训练循环里的三个必调参数:学习率、折扣因子、GAE 系数
学习率对 PPO 的影响最大。Actor 学习率一般取 3e-4 到 1e-3,Critic 学习率可以稍大一点,取 1e-3 到 3e-3。折扣因子 γ 在导航任务里通常取 0.99,如果目标距离远、需要多步才能到达,可以取 0.995。GAE 系数 λ 取 0.95 是常见起点,调大到 0.98 会降低方差但增加偏差。
# PPO 训练循环关键片段 optimizer_actor = tf.keras.optimizers.Adam(learning_rate=3e-4) optimizer_critic = tf.keras.optimizers.Adam(learning_rate=1e-3) gamma = 0.99 lam = 0.95 clip_ratio = 0.2 for epoch in range(num_epochs): states, actions, rewards, next_states, dones = buffer.sample() # 计算 GAE 优势 values = critic(states) next_values = critic(next_states) deltas = rewards + gamma * next_values * (1 - dones) - values advantages = compute_gae(deltas, gamma, lam) # 策略损失:裁剪替代目标 with tf.GradientTape() as tape: mu, std = actor(states) dist = tfp.distributions.Normal(mu, std) log_probs = dist.log_prob(actions) ratio = tf.exp(log_probs - old_log_probs) clipped = tf.clip_by_value(ratio, 1 - clip_ratio, 1 + clip_ratio) actor_loss = -tf.reduce_mean(tf.minimum(ratio * advantages, clipped * advantages)) grads = tape.gradient(actor_loss, actor.trainable_variables) optimizer_actor.apply_gradients(zip(grads, actor.trainable_variables))clip_ratio取 0.2 是 PPO 原论文的推荐值,导航任务里可以试 0.1 到 0.3。优势函数标准化在 batch 内做,减均值除标准差,能显著提升训练稳定性。Critic 损失用均方误差,有时候加一个价值裁剪也能防止值函数发散。
4. 避坑指南:训练不收敛、仿真迁移翻车、奖励震荡的排查清单
4.1 训练初期奖励不升反降
现象:前几百个 episode 奖励曲线往下走,机器人学会原地转圈或者直接撞墙。原因通常是探索噪声太大,策略网络输出的动作幅度超出底盘执行能力,机器人频繁触发碰撞终止。解决方法是把动作空间的上限先调小,线速度限制在 0.15 m/s 以内,角速度限制在 0.5 rad/s 以内,等策略稳定后再逐步放开。另外检查激光数据的归一化,如果输入范围是 0 到 3.5 米,最好除以 3.5 映射到 [0,1],否则网络第一层权重更新会很不均匀。
4.2 仿真里跑得好,真机上直接撞
现象:仿真环境里成功率 90% 以上,部署到真机后机器人反应迟钝或者直接撞障碍。原因有三个:一是仿真激光和真实激光的噪声模型不一致,仿真里激光太干净,策略没学会处理噪声;二是控制延迟,仿真里rospy.sleep(0.1)是精确的,真机上从发布指令到轮子响应有几十毫秒延迟;三是里程计漂移,仿真里里程计是完美的,真机上跑几分钟位置就偏了。解决办法是在仿真里给激光加高斯噪声,给动作加随机延迟,里程计加漂移,让策略提前适应不完美观测。
4.3 奖励曲线周期性震荡
现象:奖励时高时低,周期大概几十个 episode,策略在「激进」和「保守」之间反复横跳。原因是距离引导奖励和碰撞惩罚的权重比例不合适。距离引导系数太大,机器人为了靠近目标不惜贴障碍走;碰撞惩罚太大,机器人学到保守策略不敢动。调整方法是把碰撞惩罚从 -10 降到 -5,同时把距离引导系数从 2.0 降到 1.0,让两个信号的量级更接近。另外检查 GAE 的 λ 是不是设得太高,λ 超过 0.98 会让优势估计方差变大,训练容易震荡。
4.4 激光数据预处理踩坑
现象:机器人对正前方障碍反应正常,对侧面障碍视而不见。原因是激光降采样的时候用了reshape(36, -1).min(axis=1),如果原始激光是 360 线,每 10 线取一个最小值,侧面障碍可能落在两个扇区之间被平均掉了。解决办法是改用滑动窗口最小值,窗口大小取 5 到 10 线,保证每个方向上的最近障碍都能被捕捉到。另外注意激光的安装角度偏移,如果激光不是正前方朝前,需要在预处理里做角度补偿。
4.5 TensorFlow 图执行与 ROS 回调的线程冲突
现象:训练跑一段时间后程序卡死,或者报TF_GetExecutionPlan之类的错误。原因是 ROS 的回调函数在单独线程里执行,TensorFlow 的会话在另一个线程里跑,两个线程同时访问 GPU 或者计算图导致资源竞争。解决办法是把 ROS 回调里的数据先存到线程安全的队列里,训练循环从队列取数据,所有 TensorFlow 操作都在主线程里做。如果用的是 TensorFlow 2.x 的 eager 模式,这个问题会少很多,但tf.function装饰的函数仍然要注意线程安全。
5. 从仿真到真机:策略部署与在线微调的实操技巧
策略网络训练好之后,部署到真机上有两种做法。一种是直接把 TensorFlow 模型导出成 SavedModel 格式,在 ROS 节点里加载做推理,控制频率能跑到 20Hz 以上。另一种是把模型转成 TensorFlow Lite 或者 ONNX,用 C++ 推理引擎跑,延迟更低但转换过程容易出算子兼容问题。我一般先用 SavedModel 跑通,确认策略行为符合预期后再考虑优化推理速度。
# 导出 SavedModel 并在 ROS 节点里加载推理 model = PPOActor(action_dim=2) model.load_weights('./checkpoints/ppo_actor_5000') model.save('./saved_model/ppo_nav', save_format='tf') # ROS 节点里加载 loaded = tf.saved_model.load('./saved_model/ppo_nav') infer = loaded.signatures['serving_default'] def control_loop(): rate = rospy.Rate(20) # 20Hz 控制频率 while not rospy.is_shutdown(): obs = get_observation() # 从话题获取最新观测 obs_tensor = tf.constant(obs[np.newaxis, :], dtype=tf.float32) action = infer(obs_tensor)['output_0'].numpy()[0] publish_cmd(action[0], action[1]) rate.sleep()在线微调是另一个实用技巧。真机部署后,如果发现策略在某些场景下表现不好,不用重新训练,只需要在真机上采集几百条成功和失败的轨迹,用较小的学习率(1e-5 到 1e-4)对策略网络做几轮微调。微调的时候冻结 Critic 网络,只更新 Actor,防止值函数过拟合到少量真机数据。微调数据里失败轨迹的权重可以调高一点,让策略更快修正错误行为。
验证策略是否值得部署,我一般看三个指标:一是仿真环境里随机初始位姿下的到达率,至少要到 85% 以上;二是最小障碍距离的分布,不能有太多低于 0.15 米的样本;三是动作平滑度,相邻两步的角速度差值不能太大,否则真机上会抖。这三个指标都过了,再上真机跑长时间测试。我自己踩过最深的坑是仿真里激光太干净,真机上策略对噪声过敏,后来在仿真里加了噪声训练,迁移效果好了很多。希望帮到你。
本文还有配套的精品资源,点击获取