简介:面向机器人导航避障研究与深度强化学习应用开发的完整Python项目,基于DQN、DDQN、Dueling DQN等不同算法,在ROS Melodic与Gazebo仿真环境中实现移动机器人自主避障导航。项目依托catkin_ws与catkin_ws1两个工作空间,将Gazebo启动与TensorFlow强化学习训练分离,避免合在一起运行报错,适合人工智能、机器人、自动化等专业学生开展课程设计、期末大作业或毕业设计。压缩包共2000个文件、约5.19MB,涵盖139个py算法源码、101个txt说明文档、launch启动文件、world仿真场景、msg消息定义及xacro机器人模型,并配有大量cmake与make编译配置,目录结构清晰便于按需查阅。内部包含dueling ddqn、dueling dqn等算法实现,可直接对照不同网络结构的训练效果,分析深度强化学习算法的避障能力差异。目前已有233人学习下载,代码经严格调试,具备一定Python与ROS基础即可上手运行调试。
1. 深度强化学习移动机器人导航避障:这份源码到底解决什么问题
做移动机器人导航避障的课程设计或毕业设计,最难的不是调通某个算法,而是把 Gazebo 仿真、ROS 通信、深度强化学习训练三件事拼在一起。很多人单独跑 demo 都能动,合在一起就出问题——roslaunch 起来后训练进程崩,或者模型训练一晚上,小车在仿真里还是原地转圈。项目给的是完整闭环:DQN、DDQN、Dueling DQN 等多套不同的强化学习算法源码,配上 ROS Melodic 环境下的移动机器人导航避障工程,并用两个工作空间分开的方式避开 ROS 与 TensorFlow 的依赖冲突。适合计科、人工智能、大数据方向正在做课设、期末大作业和毕设的学生,也适合想快速搭一套 DRL 导航实验环境去看训练曲线的学习者。注意它需要 Python 3.5、TensorFlow 1.14.0 和 ROS Melodic,环境偏旧但稳定。
2. 把导航避障建模成 MDP:状态、动作、奖励和算法选型
移动机器人导航避障在强化学习框架下是一个典型马尔可夫决策过程(MDP)。目标不是让机器人去背一条固定路径,而是让它根据传感器观测学习策略 π,也就是从状态到动作的映射。这个映射怎么表达,直接决定了后面网络结构怎么写、训练能不能收敛。
2.1 状态空间:激光雷达距离加目标方位
状态空间是第一件要定的事。避障和导航的区别在于,导航要考虑目标,避障要考虑局部障碍物,所以状态必须同时包含这两种信息。这个项目采用工程里最常见的方案:用激光雷达的距离信息作为主要感知输入,抽取 16 到 20 个方向的距离值(比如每 20 度取一个),做归一化后丢给网络。同时加入目标相对车体的距离和方位角。
| 状态分量 | 维度 | 说明 |
|---|---|---|
| 激光距离 | 16~20 | 各方向归一化距离,传感器量程通常 0~10 m |
| 目标方位角 | 1 | 目标航向与车体航向的夹角,用 sin 平滑表示 |
| 目标距离 | 1 | 归一化到 0~1,辅助网络判断是否接近终点 |
为什么不把完整激光点云直接丢给网络?因为 DQN 这类算法处理高维输入需要更大的网络和更多样本,而避障决策真正依赖的是前方一定扇区内的近距离障碍物分布,20 个方向已经能把障碍轮廓描述清楚。我从经验上觉得,方向数设置到 16~20 是性价比最高的范围;少于 12 个,很难分辨斜前方障碍物,多于 30 个,训练时间明显变长但收益很小。
代码里状态就是这样拼出来的:
import numpy as np def get_state(laser_ranges, goal_angle, goal_dist, max_range=10.0): laser = np.array(laser_ranges, dtype=np.float32) / max_range laser = np.clip(laser, 0.0, 1.0) state = np.concatenate([laser, [np.sin(goal_angle), goal_dist / max_range]]) return state.astype(np.float32)这里有一个很容易翻车的细节:Gazebo 的 laser 数据在测不到物体时经常返回 inf 或极大值,直接除 max_range 会得到 inf,再进神经网络就全是 NaN。所以必须先 clip 再拼接状态。目标角度用 sin 而不是直接用弧度,是因为角度有 2π 周期性,目标在左边 170 度和右边 170 度时,如果直接丢角度值,网络会觉得这两个状态差很远,实际上它们只差 20 度。用 sin 编码就能避免这种突跳。
2.2 动作空间:为什么用离散动作而不是连续控制
动作空间决定了你选哪一类强化学习算法。这份资源里用的是离散动作集,典型配置是 5 个动作:左转、右转、直行、左偏、右偏,每个动作对应一组线速度和角速度。
| 动作编号 | 线速度 m/s | 角速度 rad/s | 行为 |
|---|---|---|---|
| 0 | 0.2 | 0.5 | 左转 |
| 1 | 0.2 | -0.5 | 右转 |
| 2 | 0.4 | 0.0 | 直行 |
| 3 | 0.3 | 0.3 | 左偏 |
| 4 | 0.3 | -0.3 | 右偏 |
DQN 系列算法要求动作空间离散,因为网络输出的是每个动作的 Q 值,然后取 argmax。你当然可以用 DDPG 或 SAC 做连续控制,但连续动作下的策略梯度类算法训练稳定性差得多,需要同时调 actor 和 critic 两个网络的 learning rate、tau 更新系数,对课程设计来说工作量翻倍。
我的建议是:先把离散动作跑通,确认状态设计和奖励函数没有问题之后,再考虑是否升级连续控制。很多避障场景下,离散动作的轨迹已经足够平滑——把直行动作线速度调到 0.4,转向动作线速度调到 0.2,小车的弧线转弯效果并不生硬。
2.3 奖励函数与算法选型:DQN、DDQN 和 Dueling DQN 怎么取舍
奖励函数是影响训练效果最大的部分。这份项目里采用典型稠密奖励:靠近目标给正反馈,远离目标给微弱惩罚,靠近障碍物给惩罚,到达目标给大正奖励,碰撞给更大的负奖励。我一般会把避障惩罚设计成分段:
def compute_reward(laser, prev_goal_dist, goal_dist, reached, collision): r = 0.0 if reached: r += 1.0 elif collision: r -= 1.0 min_dist = min(laser) if min_dist < 0.3: r -= 0.3 elif min_dist < 0.6: r -= 0.1 r += 0.1 * (prev_goal_dist - goal_dist) return r分段惩罚的意义在于,机器人学到的行为是提前减速转向,而不是冲到障碍物跟前再急打方向。如果只在小于 0.3 米时给惩罚,早期探索阶段大部分 step 的奖励都是 0,网络很难从稀疏反馈里学到梯度。加入目标距离变化量,则是给网络一个持续的方向性信号,让它知道往哪个方向走是有利的。
算法选型方面,资源里能切换三套算法:
| 算法 | 核心改动 | 要解决的典型问题 |
|---|---|---|
| DQN | 经验回放 + 目标网络 | 样本相关性导致更新震荡 |
| DDQN | 动作选择与动作评估解耦 | Q 值过估计,机器人过于乐观 |
| Dueling DQN | Q 值拆成 V 和 Advantage | 状态价值接近时动作区分度差 |
对移动机器人避障场景,我默认选 DDQN。原因是在导航任务里,Q 值过估计会直接表现为机器人低估碰撞风险,明明障碍物很近,网络给出的 Q 值还是很高,导致它继续往前走。DDQN 的做法是:用主网络选动作,用目标网络算这个动作的 Q 值,把选择和解耦分开,过估计程度明显下降。Dueling DQN 的改动是网络结构层面的,适合那些不同动作之间结果差异很大的环境,但它的收益在避障任务上不如 DDQN 明显。
3. 搭建环境:ROS Melodic 与 TensorFlow 1.14 的双工作空间
搭建环境是复现时第一次可能翻车的地方。这个项目卡了两个硬性版本:Python 3.5、TensorFlow 1.14.0,配合 ROS Melodic。很多人上来就装 TensorFlow 2.x,结果代码里大量 tf.Session、tf.placeholder 全部报错,这是最典型的误用。先把版本对齐,再谈训练。
3.1 创建 NDDDQN 虚拟环境并安装 TensorFlow
项目说明里直接提到了创建 NDDDQN 虚拟环境,这一步建议用 conda 做,因为 Python 3.5 在系统层面并不好单独管理。
conda create -n NDDDQN python=3.5 conda activate NDDDQN pip install tensorflow-gpu==1.14.0 -i https://pypi.tuna.tsinghua.edu.cn/simple pip install numpy==1.16.6这里把 numpy 锁到 1.16.6 是必要的。TensorFlow 1.14 编译时依赖的是旧版 numpy API,如果直接用最新 numpy,训练时经常会报ModuleNotFoundError: No module named 'numpy.core.multiarray'或者_ARRAY_API not found这类诡异错误。conda 创建环境后用清华源装 TF,可以避免很多网络超时问题。
TensorFlow 1.14.0 对应的是 CUDA 10.0 + cuDNN 7.4。如果你机器上没有 GPU,就装 CPU 版:
pip install tensorflow==1.14.0 -i https://pypi.tuna.tsinghua.edu.cn/simpleCPU 版能跑,但训练速度慢很多,尤其是 20 维状态输入的全连接网络,一局 episode 在 CPU 上可能要慢 3 到 5 倍。做实验验证代码流程没问题,CPU 可以接受,正式训练建议还是上 GPU。
提示:不要在这个虚拟环境里再去 pip 升级 protobuf、grpcio 或 setuptools,TF 1.14 对这些库有版本依赖,升级任何一个都可能让整个训练进程崩溃。
3.2 两个工作空间的划分与编译顺序
这个项目有一个非常规设计:把 Gazebo 仿真启动和 TensorFlow 强化学习训练分成了两个独立文件夹。项目说明里写“有未知问题,需要把小车在 gazebo 中的启动,与 tensorflow 强化学习分开成两个文件夹,合在一起会报错”,这句话是真话,不是多此一举。
原因在 ROS 与 TensorFlow 的底层依赖冲突。ROS Melodic 会往 PYTHONPATH 和 LD_LIBRARY_PATH 里注入它自己的 Python 绑定、消息库和 protobuf 版本。TensorFlow 1.14 也绑定了自己的 protobuf 实现。两者在同一个终端环境里共存时,最常见的就是 protobuf 版本不匹配,导致 import 阶段就段错误,或者训练过程中出现随机崩溃。把两个工作空间分开,就是让仿真环境变量和训练环境变量彻底隔离。
实际操作要分两个终端:
# 终端 A:仿真工作空间 cd ~/catkin_ws catkin_make source devel/setup.bash roslaunch robot_gazebo robot_world.launch# 终端 B:强化学习工作空间 cd ~/catkin_ws1 catkin_make source devel/setup.bash python train_dqn.py --algo ddqn关键点在于终端 B 不要 source 终端 A 的 setup.bash。如果你把两个工作空间的 setup.bash 都写进.bashrc,等于又回到了合在一起的状态,训练时大概率还是崩。我习惯的做法是:在训练终端里只激活 conda 环境,不自动 source 任何 ROS 工作空间,需要发布指令时手动 source catkin_ws1 的 setup.bash。
编译顺序上,两个工作空间可以单独执行catkin_make。遇到编译错误时不要慌,先看是缺失依赖包还是 Python 版本不匹配。ROS Melodic 在 Ubuntu 18.04 下默认 Python 2.7,如果系统 Python 被切换成 3.x,catkin_make 会报一堆奇怪的找不到模块错误。项目代码基于 Python 3.5,需要确保 catkin 生成的消息接口能被 Python 3 调用。
3.3 安装验证:三个命令确认环境可用
环境配好后不要急着跑训练,先花两分钟验证三件事:ROS 主节点是否正常、激光话题有没有数据、TensorFlow 能否读取 GPU。
roscore# 另开终端 rostopic list rostopic echo /scan | head -n 5python -c "import tensorflow as tf; print(tf.__version__); print(tf.test.is_gpu_available())"rostopic echo /scan能打印出激光数据,说明 Gazebo 和小车模型正常。tf.test.is_gpu_available()返回 True 说明 GPU 可用;如果返回 False,也不要急着重装,先用nvidia-smi看驱动是否正常,再看 cuDNN 版本是否匹配。这一步淘汰掉了不少后续训练中莫名其妙的故障。
4. 核心代码拆解:训练循环、经验回放与关键参数
环境通了,接下来进入代码层。这部分我按训练闭环的顺序拆:网络结构、经验回放、主循环、算法切换。你拿到源码后不需要逐行看懂,但要能定位到这几个关键模块,出了问题才有排查头绪。
4.1 网络结构:全连接网络而不是卷积网络
输入状态是 20 维左右的向量,所以网络不需要卷积层。项目里基础版本是两层全连接,隐藏层 64 个神经元,输出维度等于动作数。
import tensorflow as tf def build_q_network(state_dim, action_dim, hidden=64, name='q_net'): inputs = tf.placeholder(tf.float32, [None, state_dim], name=name + '_input') h1 = tf.layers.dense(inputs, hidden, activation=tf.nn.relu, name=name + '_h1') h2 = tf.layers.dense(h1, hidden, activation=tf.nn.relu, name=name + '_h2') q_values = tf.layers.dense(h2, action_dim, name=name + '_q') return inputs, q_values隐藏层 64 是默认值,我发现导航避障这类低维输入任务用 64 到 128 之间差别不大。隐藏层过多反而更容易过拟合到训练环境。这里用tf.placeholder是因为 TF 1.14 没有 Keras 那种便捷的 eager 模式,所有数据都要走 feed_dict,这是老代码的正常写法,不是设计缺陷。
4.2 经验回放与目标网络:稳定训练的基座
强化学习的样本是强相关的,连续几步内机器人的位置、激光数据变化都很小,如果直接用相邻样本做梯度更新,网络会不断被拉向最近的状态分布,训练方差极大。经验回放是先存后抽样,打破这种时间相关性。
from collections import deque import random class ReplayBuffer: def __init__(self, capacity=50000): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones = zip(*batch) return np.array(states), np.array(actions), \ np.array(rewards), np.array(next_states), np.array(dones) def __len__(self): return len(self.buffer)capacity 设为 50000 是常见取值。太小的话,样本很快被覆盖,网络反复看到的是近期状态,容易遗忘早期经验;太大的话,抽样里大量是远古样本,和当前策略分布差距大,训练效率低。动作、奖励、下一个状态都统一转成 numpy 数组,是为了方便后续 tf 的 feed_dict。
目标网络的更新频率也是玄学高发区。常见做法是每 1000 步把主网络参数硬拷贝到目标网络,或者用 soft update:target_params = tau * main_params + (1 - tau) * target_params,tau 取 0.01。固定周期硬拷贝简单直接,对避障任务足够了;soft update 更适合连续动作算法。
4.3 训练主循环与模型保存
主循环的逻辑是多 episode 迭代,每个 episode 里机器人从起点出发,在时间步上限内不断执行“选动作→执行→观察奖励→存经验→训练”。
for episode in range(max_episodes): state = env.reset() total_reward = 0.0 epsilon = max(0.05, epsilon_min + (epsilon_start - epsilon_min) * (0.99 ** episode)) for step in range(max_steps_per_episode): action = choose_action(state, epsilon) next_state, reward, done, _ = env.step(action) replay.push(state, action, reward, next_state, done) state = next_state total_reward += reward if len(replay) > warmup_steps: training_step(batch_size=32) if done: break if episode % save_interval == 0: saver.save(sess, './model/ddqn.ckpt', global_step=episode)几个参数要单独说。epsilon 从 1.0 开始,指数衰减,最终停在 0.05 附近。1.0 意味着前期完全随机探索,这对强化学习是必须的,否则网络永远学不到新策略。warmup_steps 一般设 1000 到 5000,让经验回放池先攒够一批高质量样本再开始训练,避免一开始就从两三个样本里反复抽样。batch size 32 是稳定选择,learning rate 用 1e-4,gamma 0.99,gamma 越大网络越看重未来收益。
4.4 不同算法切换:DDQN 与 Dueling DQN 的 target 差别
前面说项目里有三套算法,切换逻辑集中在 target Q 值的计算部分。DQN 用目标网络直接取最大值,DDQN 换成两步:
if algo == 'dqn': q_target = tf.reduce_max(target_net(next_states), axis=1) elif algo == 'ddqn': # 用主网络选动作,用目标网络估计该动作的价值 best_actions = tf.argmax(q_net(next_states), axis=1) indices = tf.stack([tf.range(batch_size), best_actions], axis=1) q_target = tf.gather_nd(target_net(next_states), indices) elif algo == 'dueling': # 网络输出拆成 V(s) 和 A(s,a) v_out, adv_out = dueling_net(next_states) q_target = tf.reduce_max(v_out + adv_out - tf.reduce_mean(adv_out, axis=1, keepdims=True), axis=1)DDQN 的关键在两行best_actions和q_target。Dueling 的网络结构改动较大,把 Q 值拆成状态价值和优势函数,输出时减去优势均值保证可辨识性。代码量差别不大,但训练效果和稳定性差异明显。如果资源默认跑的是 DQN,建议立刻切到 DDQN 跑一遍对比,通常碰撞率会下降。
5. 复现避坑:五个高频报错与排查方法
这一节是血泪经验。以下五个问题是我拆这类项目时见过最多、也最容易被误判的。
5.1 现象:Gazebo 一启动,TensorFlow 训练进程段错误崩溃
训练终端在 roslaunch 启动后立刻报Segmentation fault,或者Illegal instruction,且没有明显 Python 堆栈。
原因:ROS Melodic 的环境变量污染了 TensorFlow 的依赖库,尤其是 protobuf。启动 roslaunch 的终端和训练终端如果共享同一 ROS 环境变量,TF 在 import 或 tensorflow 执行时加载到了 ROS 自带的 protobuf 版本,直接崩。
解决:训练终端不要 source 仿真工作空间的 setup.bash,两个工作空间严格分终端运行。我一般在训练终端只做两件事:conda activate NDDDQN,然后手动进入 catkin_ws1 目录执行训练脚本。
5.2 现象:catkin_make 时出现 tf2_msgs-msg.asd、tf-msg.asd 这类文件,编译卡住
工作空间里出现tf2_msgs-msg.asd、tf2_msgs-srv.asd、tf-msg.asd这些带 .asd 后缀的“消息文件”,之前编译状态异常中断,或者在编译途中同时 source 了另一个 ROS 工作空间的环境变量。
原因:.asd不是 ROS 标准消息后缀。标准消息是 .msg 和 .srv。这类文件通常是 catkin 在进程被中断、或环境变量混乱时生成的残留产物,也可能是从网盘同步后文件后缀错误。
解决:先删掉这些垃圾文件和编译缓存,重新编译。
rm -rf build devel find . -name "*.asd" -delete catkin_make编译前确认当前终端只 source 当前工作空间的 setup.bash。如果源文件里真的存在 .asd 后缀的参考文件,说明文件同步时坏掉了,重新从原压缩包解压相应文件即可。
5.3 现象:loss 一路下降,但避障成功率一直不涨
训练曲线的 loss 在下降,可是每 episode 的碰撞次数没有明显减少,机器人要么原地转圈,要么直直撞墙。
原因:loss 下降只能说明 TD 误差在减小,不代表策略变好了。更多时候是奖励函数设计出了问题——避障惩罚太弱,接近目标的正奖励太强,网络学到的策略是“冲目标,撞了也无所谓”。另外 epsilon 衰减太快也会导致策略固化在随机行为上。
解决:先检查奖励函数分段。把障碍物距离惩罚的范围从 0.3 米扩大到 0.6 米,并且碰撞惩罚从 -1.0 改到 -2.0。同时把 epsilon 的衰减系数放缓,让网络在训练中期仍有足够的随机探索。如果仍然不涨,建议从 DQN 切到 DDQN,过估计缓解后避障表现通常会好一块。
5.4 现象:TensorFlow 报错找不到 libcudnn.so.7
训练脚本一执行就报Could not load dynamic library 'libcudnn.so.7',或者Failed to get convolution algorithm。
原因:TensorFlow 1.14 是专门针对 cuDNN 7.x 编译的,而很多机器现在装的是 CUDA 11 搭配 cuDNN 8.x,版本对不上。Failed to get convolution algorithm这个报错还可能是显存不足。
解决:确认 CUDA 版本为 10.0、cuDNN 为 7.4。如果项目环境不想动系统全局库,可以在启动训练前单独设置 LD_LIBRARY_PATH 指向项目自带的 cuDNN 7 路径。如果只是显存不足,在代码里设置 TensorFlow 按需增长显存:
gpu_options = tf.GPUOptions(allow_growth=True) sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))5.5 现象:roslaunch 提示 Unable to contact ROS master
启动仿真时报Unable to contact ROS master at [http://localhost:11311],但单独运行 roscore 又是正常的。
原因:新开终端的 ROS_MASTER_URI 没有设置,常见于.bashrc里只 source 了 ROS 的 setup.bash 但没 export master 地址,或者终端从非标准 shell 启动导致环境变量丢失。
解决:在同一终端先执行 roscore,然后重新打开一个终端再测试:
export ROS_MASTER_URI=http://localhost:11311 export ROS_HOSTNAME=localhost把这两行写进.bashrc可以避免以后每次手动设置。仍需注意 ROS_HOSTNAME 不要随便改成内网 IP,多机通信场景下才有必要,单机调试固定 localhost 最省事。
6. 让模型从“会动”到“会避障”:验证信号与调参顺序
训练跑通不等于问题解决。很多人的模型勉强能走到终点,但路径极其诡异,贴着墙绕大圈,或者在空旷地带画弧。这通常说明模型已经学会某种策略,不是最优策略。我建议用三个信号判断模型是否真的可落地。
一是 episode reward 的滑动平均。单次训练曲线抖动很大,直接把最近 30 个 episode 的 reward 做平均,比原始曲线靠谱得多。平均 reward 如果持续保持上升,说明策略在变好;如果在某一平台期停住,说明模型卡在局部最优。
二是碰撞率。统计每次 episode 内机器人是否触发碰撞,计算最近 50 个 episode 的碰撞占比。训练初期 80% 以上碰撞是正常的,训练后期如果还在 30% 以上,优先怀疑奖励函数,而不是网络结构。
三是机器人轨迹是否符合指向目标的直觉。关闭训练模式,把 epsilon 设为 0,让模型纯 greedy 执行,回放仿真录像。如果轨迹频繁贴墙,说明避障惩罚权重不够;如果原地打转,说明目标方位角的状态编码有问题,检查一下 get_state 里的 sin 转换是否有符号错误。
关键超参的调整顺序往往被搞反。我从这套项目里得到的习惯顺序是:先保证动作空间和奖励函数合理,再验证算法可行性,最后才调学习率和网络结构。
| 参数 | 默认值 | 调整方向 | 影响程度 |
|---|---|---|---|
| epsilon 衰减速度 | 0.99 | 衰减太快易早熟,放慢到 0.995 | 高 |
| 奖励惩罚权重 | 避障 -0.3 | 碰撞频发时加大到 -0.5 | 高 |
| batch size | 32 | 增大到 64 能稳定,但训练变慢 | 中 |
| learning rate | 1e-4 | 过大容易发散,已收敛后降到 5e-5 | 中 |
| replay capacity | 50000 | 场景复杂时扩大到 100000 | 低 |
训练记录也是很多人忽略的环节。我建议每轮训练至少记录三个文件的版本:模型权重文件、训练曲线截图、超参配置。不要只用一张终端截图,因为很多问题要到几百 episode 之后才能暴露,没有记录就无法回溯是改哪个参数导致的回归。
这套流程跑完,模型的避障成功率通常能达到 80% 以上,而且最诡异的是它还会表现出一些泛化能力——把障碍物位置换一下,机器人也能绕过去,这就是深度强化学习避障相比传统人工势场法最大的优势。从那以后我每次换机器或换算法,都强制先跑一遍旧模型验证环境,再确认奖励函数,最后才敢调超参,这个习惯帮我少踩了至少十几次环境兼容性的坑。希望帮到你。
本文还有配套的精品资源,点击获取