
简介这是基于 TensorFlow 与 Gazebo 的 DDPG 深度强化学习连续控制项目聚焦端到端移动机器人导航适合计算机、自动化、电子信息等专业的毕业设计或课程大作业。项目通过 DDPG 算法进行连续动作控制实验展示了 action_dim2 失败与 action_dim1 成功的对比对理解连续动作空间设计很有帮助。包内共 28 个文件包含 14 个 Python 源码、6 个编译缓存、3 个 XML 配置、3 个 GitHub 演示 GIF 及 README 说明文档压缩包约 50.25MB代码已测试运行成功可直接用于答辩演示或二次开发。资源还附带论文与数据集的一站式资料已有 55 人学习浏览。对于想在机器人导航方向快速上手深度强化学习的读者这套从源码、配置说明到运行效果演示的完整方案能有效降低入门门槛值得下载参考。1. 为什么选 TensorFlow Gazebo 做端到端 DDPG 导航做移动机器人导航的毕设最容易卡住的不是算法而是“拿什么证明算法有效”。传统路线先 SLAM 再路径规划调一整个流程的软件包就要一个学期而端到端导航让小车“看到什么就输出什么速度”DDPG 又是连续控制里最成熟的深度强化学习方案。用 TensorFlow 写 DDPG在 Gazebo 里建一个能反复重置的训练场再随机生成障碍和起点终点就能在没有实车的情况下把导航任务做成可复现、可演示、可画曲线的完整成果。这套流程适合计算机、自动化、电子信息类需要快速出结果、论文又能讲清楚的同学。2. 搭建 Gazebo 训练环境让小车和传感器先“活”起来2.1 版本搭配先解决 Ubuntu、Gazebo 与 TensorFlow 的兼容动手写 DDPG 之前先把环境焊死。我不建议在 Ubuntu 22.04 上硬啃因为“gazebo 安装 ros 环境 ubuntu22”之后你还会遇见 Gazebo 和 ros_control 的 ABI 冲突以及在 VMware 里打开 Gazebo 屏幕闪烁的问题。反过来用 Ubuntu 20.04 ROS Noetic Gazebo 11 TensorFlow 2.10 这套组合会顺利很多。Noetic 自带 Gazebo 11ROS 和 Gazebo 不会因为版本打架TensorFlow 2.10 是目前和 ROS 的 protobuf 兼容最好的一个版本再往上走就会出现避坑章节里说的 protobuf 冲突。# ROS Noetic 安装完成后自带 Gazebo 11 sudo sh -c echo deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main /etc/apt/sources.list.d/ros-latest.list wget -O - https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - sudo apt update sudo apt install -y ros-noetic-desktop-full python3-pip sudo apt install -y ros-noetic-turtlebot3 ros-noetic-turtlebot3-simulations # TensorFlow 安装锁死 2.10 是后面少踩坑的关键 pip3 install tensorflow2.10.0 protobuf3.20.3命令逻辑很简单第一段把 ROS 源写进去并装完整桌面版第二段补上 TurtleBot3 的车模和 Gazebo 插件。TensorFlow 那行之所以要单独锁 protobuf是因为 ROS 的 Python 包默认依赖 protobuf 3.x而 TensorFlow 2.11 以上会拉动 protobuf 4.x两者一撞后面 import tensorflow 就崩。装完记得在 bashrc 里加source /opt/ros/noetic/setup.bash再把source ~/catkin_ws/devel/setup.bash按自己工作空间地址补上。如果你实在只能装 Ubuntu 22.04建议把 TensorFlow 和 ROS 的 Python 依赖分开放进两个 virtualenv 里Gazebo 用 snap 版否则你会发现在解决“gazebo 安装 ros 环境 ubuntu22”之后还要面对一堆 OpenGL 渲染问题。物理机跑比虚拟机省心但我个人见过很多同学的台式机条件一般虚拟机也能跑只是要按第 5 章 5.2 节的方法处理显示。2.2 搭建一个能反复重置的随机障碍训练场固定地图对 DDPG 没有意义小车背下的是地图而不是导航能力。所以训练场必须每一局都重新生成。Gazebo 里最方便的做法是启动一个只有地面和太阳的空世界然后用/gazebo/spawn_sdf_model服务往场景里随机撒箱子。# 先启动一个空世界为下面脚本提供可操作场景 roslaunch turtlebot3_gazebo turtlebot3_empty_world.launch然后运行一个随机障碍生成脚本#!/usr/bin/env python3 # 训练场景生成器每次重置后运行一次得到不同布局 import rospy, random from gazebo_msgs.srv import SpawnModel rospy.init_node(spawn_obstacles) rospy.wait_for_service(/gazebo/spawn_sdf_model) spawn rospy.ServiceProxy(/gazebo/spawn_sdf_model, SpawnModel) for i in range(14): x, y random.uniform(-5, 5), random.uniform(-5, 5) if abs(x) 1.0 and abs(y) 1.0: # 车在原点出生周围留空避免开局就撞 continue size random.uniform(0.3, 1.0) sdf fsdf version1.6 model nameobs_{i} pose{x} {y} 0 0 0 {random.uniform(0, 3.14)}/pose link namelink collision namecboxsize{size} {size} 0.6/size/box/collision visual namevboxsize{size} {size} 0.6/size/box/visual /link /model/sdf spawn(fobs_{i}, sdf, , , rospy.Time.now(), /map)这段代码每次运行会生成 14 个位置、大小、朝向完全随机的箱子。关键参数是spawn服务的六个参数模型名、SDF 内容、命名空间、相对初始位姿、生成时间和参考坐标系。我这里参考坐标系写的是/map这样小车和障碍物都在同一个坐标系下对齐后面做目标点计算不用额外做坐标变换。运行时如果发现某局开局就能看到前面一堵墙把abs(x)1.0的禁区半径调大到 1.5 即可。还可以在每次 reset 时先调用/gazebo/delete_model服务把上一局的障碍物清掉再生成不然障碍物会越攒越多最后整个场景被箱子塞满。这个方法在 Gazebo 教程里很少讲清楚实际训练时却非常关键。2.3 传感器与坐标系端到端控制的第一步端到端导航的“端”对大多数移动机器人来讲就是激光雷达的/scan话题和里程计坐标系。TurtleBot3 Burger 用的是 360 度单线雷达话题输出是sensor_msgs/LaserScan。训练策略之前先用一个探测脚本确认激光话题在线并且小车真的能收到速度指令。#!/usr/bin/env python3 import rospy, numpy as np from sensor_msgs.msg import LaserScan from geometry_msgs.msg import Twist class SensorProbe: def __init__(self): self.ranges None rospy.Subscriber(/scan, LaserScan, self._cb) self.pub rospy.Publisher(/cmd_vel, Twist, queue_size1) def _cb(self, msg): self.ranges np.array(msg.ranges, dtypenp.float32) def drive_and_probe(self): t Twist(); t.linear.x 0.15 for _ in range(100): self.pub.publish(t) rospy.sleep(0.02) if self.ranges is not None and np.min(self.ranges) 0.30: t.linear.x 0.0 self.pub.publish(t) print(前方最近 %.2f m已刹车 % np.min(self.ranges)) return if __name__ __main__: rospy.init_node(sensor_probe) SensorProbe().drive_and_probe()这个脚本的意义是验证三件事/scan数据确实在发布、/cmd_vel的速度控制能驱动小车动起来、激光雷达的min_range和前方检测方向是否符合预期。如果打开发布后小车没动优先查/cmd_vel有没有被其他节点抢占或者机器人模型有没有加载。如果动了但不减速把/scan的话题名称换成自己车上的实际名字即可。端到端训练中错误话题名是最常见的“假翻车”——代码没写错就是听不到传感器。传感器话题确认之后再看坐标系是否完整。在终端里运行rosrun tf view_frames会生成一个frames.pdf里面会列出odom、base_link、base_scan之间的变换关系。DDPG 的状态量里用了目标点的goal_angle如果 base_scan 和 base_link 的 tf 关系缺失激光数据坐标就会飘。我见过有人状态里用了 20 个激光值目标角度却直接用arctan2和旧坐标算结果模型再怎么调也学不会最后发现是激光系和车体系对不上。这个问题 Gazebo 不会报错它只会在物理仿真里默默让你撞墙。经常有人问能不能用 RGB 相机替代激光雷达端到端导航用相机不是不行但 RGB 输入一个 84x84x3 的图像DDPG 的 Critic 网络很难在其中稳定 激光雷达是稀疏、低维信号更容易在有限样本里收敛。如果你毕设要求必须识别物体再考虑在激光状态上叠加一个小的视觉特征别一上来就端到端图像那个训练周期不是一两个月能压完的。3. 从传感器到速度指令设计 DDPG 的状态、动作和奖励函数3.1 端到端不玄学状态、动作怎么定义才不像“黑匣子”很多人一听端到端就以为“原始传感器数据直接当状态”其实完全不做处理模型根本无法收敛。我一般把状态设计成[20 束激光距离 目标方向角 目标距离]这是一个 22 维的连续向量。选 20 束激光而不是完整 360 束原因有两个。一是 LaserScan 通常有 360 或 720 个点直接塞进全连接 DDPG 会让参数爆炸训练速度慢到没法毕业。二是导航决策不需要知道每个角度的精确距离20 束已经能表达“前方有一堵墙”和“左边空隙够不够过”这两件事。把 360 度雷达降采样成 20 段每段取最小值避免个别噪声点把距离测近。def build_state(ranges, goal_angle, goal_dist): # ranges 是 /scan 输出形状通常为 (360,) # goal_angle 为目标相对车头方向角goal_dist 为目标距离 step len(ranges) // 20 beams [np.min(ranges[i*step:(i1)*step]) for i in range(20)] state np.concatenate([np.clip(beams, 0.0, 3.0), [goal_angle, goal_dist]]).astype(np.float32) return statenp.clip把大于 3 米的距离都截到 3 米目的是让网络把“远”和“很远”看成同一类减少输入方差。goal_angle这里直接用一个角度值但我在实际项目里会把它拆成[sin(goal_angle), cos(goal_angle)]两维。因为角度有 0 到 2π 跳变的问题sin/cos 能平滑表达连续性。如果你也把角度包在[-pi, pi]里那就要保证预处理函数里做一次np.arctan2归一化不然角度从-pi跳到pi时网络会以为目标位置瞬间移动了 360 度。动作空间则是[v, ω]分别是线速度和角速度。线速度范围设为[0, 0.5]角速度范围[-1.0, 1.0]。注意线速度下界设为 0不让小车倒着走。DDPG 的 Actor 输出会先经过 tanh 映射到[-1, 1]再乘以动作范围得到实际值所以这个范围在训练前就写死在环境里。3.2 TensorFlow 2 实现 Actor-Critic 网络结构、参数与软更新DDPG 使用一组 Actor 网络输出动作一组 Critic 网络评估“状态动作”的 Q 值。和常见 DQN 不同这里的 Actor 是一个连续策略网络不是动作分类器。TensorFlow 2 用 Keras 写这两套网络非常直接。import tensorflow as tf from tensorflow.keras import layers def build_actor(state_dim, action_dim, action_bound): inp layers.Input(shape(state_dim,)) x layers.Dense(256, activationrelu)(inp) x layers.Dense(256, activationrelu)(x) out layers.Dense(action_dim, activationtanh)(x) out out * action_bound # 把 tanh 输出缩放到实际动作范围 return tf.keras.Model(inp, out) def build_critic(state_dim, action_dim): s layers.Input(shape(state_dim,)) a layers.Input(shape(action_dim,)) x layers.Concatenate()([s, a]) x layers.Dense(256, activationrelu)(x) x layers.Dense(256, activationrelu)(x) out layers.Dense(1)(x) return tf.keras.Model([s, a], out)这段代码里有两个关键点。第一Actor 输出的out * action_bound这里的action_bound是个 numpy 数组[0.5, 1.0]乘法会逐维缩放保证线速度和角速度各自落在自己范围内。第二Critic 没有像传统 DQN 那样只给状态而是把动作也拼进去这样 Critic 能学习“在这个状态下每个动作到底值多少”。DDPG 还分别需要目标 Actor 和目标 Critic。目标网络不是周期性拷贝而是做软更新我一般把 tau 设成 0.005tau 0.005 for target, source in zip(target_net.variables, source_net.variables): target.assign(tau * source (1.0 - tau) * target)这个软更新相当于给目标网络加了一个一阶低通滤波避免训练早期 Q 值剧烈波动。如果你把 tau 调到 0.01 以上训练速度会变快一些但 Critic 很容易发散如果调到 0.001训练会稳定但收敛慢。第一次跑直接抄 0.005 就好。超参数表可以直接照抄第一次调参方向超参数推荐值第一次调参方向Actor 学习率1e-4不稳定时再降到 5e-5Critic 学习率1e-3Critic 发散时和大 Actor 一起降到 1e-4折扣因子 gamma0.99目标距离远可 0.995软更新系数 tau0.005稳定后可调 0.001batch size64显存够用可以 128经验池容量100000太大没意义训练反而慢每 episode 最大步数300小车跑不出去了调大3.3 奖励函数让小车学会靠近目标而不是原地转圈奖励函数是 DDPG 里最“玄学”的部分但也是有公式的。我的经验是不要用稀疏奖励只给“到达 10、碰撞 -10”会让小车在地图里瞎逛几百步几乎学不会。要把每一步的朝向变化转换成密集奖励def step_reward(prev_dist, now_dist, collision, reached, ang_vel): if collision: return -10.0 if reached: return 10.0 r 2.0 * (prev_dist - now_dist) # 靠近目标正奖励 r r - 0.1 * abs(ang_vel) # 惩罚原地打转 r r - 0.01 * now_dist # 避免小车只看局部奖励而忽略长距离 return r这个函数的含义是每一步如果离目标近了就按距离减少量给正奖励如果角速度很大说明在绕圈每步扣分。prev_dist - now_dist的单位是米所以当一步前进 0.1 米时奖励约为 0.2而原地转一圈角速度约为 6.28那一步要扣 0.63这样小车不会一直转圈。注意这里的reached判断要写在碰撞之前否则小车到达目标但激光盲区误撞目标边缘会拿到负奖励对学习是巨大干扰。我在实际项目中把“到达”定义为目标距离小于 0.3 米且线速度小于 0.05 米/秒防止“路过目标”也算到达。goal_angle和goal_dist从哪里来我在环境里用一个固定的世界坐标点来表示目标每次重置随机生成。这样一开始目标就不在小车正前方逼它学习“转弯再前进”的完整动作。如果你把目标放在小车正前方 3 米处早期训练会显得奖励很高但换一个方向起点就全瞎属于自己骗自己。4. 把 DDPG 跑进 Gazebo训练循环、探索噪声和数据集记录4.1 桥接 TensorFlow 与 Gazebo一个可复用的 step/reset 封装训练 DDPG 最核心的代码是环境封装。它要做三件事接收 Actor 网络给出的动作、把动作转换成 ROS 的cmd_vel消息并发布、从/scan读取下一次状态并计算奖励。下面是一个简化版封装类你可以直接抄进训练脚本class GazeboNavEnv: def __init__(self): self.pub rospy.Publisher(/cmd_vel, Twist, queue_size1) self.laser None rospy.Subscriber(/scan, LaserScan, self._scan_cb) self.target np.array([3.0, 3.0]) # 每次 reset 可随机生成 def _scan_cb(self, msg): self.laser np.array(msg.ranges, dtypenp.float32) def reset(self): rospy.wait_for_service(/gazebo/set_model_state) set_state rospy.ServiceProxy(/gazebo/set_model_state, SetModelState) state ModelState() state.model_name turtlebot3_burger state.pose.position.x 0.0 state.pose.position.y 0.0 state.pose.orientation.z 0.0 set_state(state) rospy.sleep(0.5) self.laser None while self.laser is None: rospy.sleep(0.05) return self._build_state() def step(self, action): twist Twist() twist.linear.x float(action[0]) twist.angular.z float(action[1]) self.pub.publish(twist) rospy.sleep(self.control_dt) # 控制周期一般 0.1 秒 state self._build_state() reward self._reward(action) done self._is_done() return state, reward, done这个封装背后的关键点是reset。很多人直接在训练里用rospy.wait_for_message(/scan, LaserScan)拿初始状态但 Gazebo 虽然重置了模型激光消息还是有时间戳延迟经常拿到上一局的最后一帧。所以我在reset里把self.laser先置空然后用while self.laser is None等待下一帧真正到来这才算同步完成。control_dt是步长我用 0.1 秒也就是小车每 0.1 秒做一次决策和 Gazebo 的仿真步长匹配。还有一个细节SetModelState服务只改模型的物理位姿不改传感器发布频率。如果 Gazebo 里雷达是 10Hz而你control_dt设成 0.05 秒那么一个 step 里可能等不到新激光拿到的还是上一次的旧数据。所以我建议把control_dt对齐到雷达发布周期或至少是它的一半并且每次 step 结束后清空一次 laser 缓存。4.2 探索噪声与超参数第一次收敛的配置DDPG 离不开探索噪声。在 Gazebo 里我推荐在高斯噪声和 Ornstein-Uhlenbeck 噪声中选后者。OU 噪声的特点是带有“惯性”不会每一步都像白噪声那样剧烈跳变更适合让小车“转着弯走”。class OUNoise: def __init__(self, mu0.0, sigma0.2, theta0.15): self.mu, self.sigma, self.theta mu, sigma, theta self.reset() def reset(self): self.state self.mu def __call__(self): self.state self.state self.theta * (self.mu - self.state) \ self.sigma * np.random.randn() return self.state训练循环里的整体结构是经典 DDPG 更新先采样一个 episode把 transition 推进经验池再从池子里随机采一个小批量。下面这个训练骨架可以直接替换成自己的激光状态from collections import deque import random replay_buffer deque(maxlen100000) for episode in range(500): state env.reset() noise.reset() ep_reward 0 done False step 0 while not done and step max_steps: action actor(state[np.newaxis, :]).numpy()[0] action noise() next_state, reward, done env.step(action) replay_buffer.append((state, action, reward, next_state, done)) if len(replay_buffer) batch_size: batch random.sample(replay_buffer, batch_size) update_ddpg(batch) # 内部实现用 3.2 节介绍的梯度计算 state next_state ep_reward reward step 1 print(fep {episode}: reward {ep_reward:.2f})第一次跑参数表你可以直接盯着这个配置抄Actor 学习率 1e-4Critic 学习率 1e-3gamma 0.99tau 0.005batch 64buffer 100000max_steps 300OU 噪声 sigma 从 0.2 衰减到 0.02。Actor 的学习率比 Critic 低一个量级是因为 Critic 更新比较频繁如果两个网络同速策略会朝一个不稳定 Q 值猛冲。OU 噪声中的 sigma 在训练后期要手动降下来否则动作一直抖收敛后成功率也上不去。不是说到第 200 个 episode 才降而是看奖励曲线平稳了再降。4.3 用数据集反向辅助训练把 transition 存下来并回放毕业设计的项目包里一定会要求“源码说明论文数据集”很多人把数据集理解成图片其实 DDPG 项目的数据集就是训练过程中产生的大量子状态-动作-奖励转移样本。在 ROS 里你可以用 rosbag 把激光和速度指令录下来也可以直接在代码里把 transition 存成 numpy 的 npz 文件。# 在训练循环里顺手存一条样本 np.savez( fdata/transition_{episode:04d}_{step:03d}.npz, statestate.astype(np.float32), actionaction.astype(np.float32), rewardnp.float32(reward), next_statenext_state.astype(np.float32), donenp.bool_(done), )这样保存的数据有两个用途。一是论文里画曲线你可以统计每一百个 episode 的平均奖励和碰撞次数这就是数据集的价值。二是离线重放如果模型在 Gazebo 里跑崩了你可以用这些数据重新离线训练一段不必再让 Gazebo 反复跑真仿真。我在项目里就把这些 npz 文件交给一个DataLoader训练时随机抽样本相当于把仿真产生的经验变成可复用的离线数据集。需要说明的是不要把全部转移样本都存下来。我只存那些奖励有明显变化或doneTrue的关键样本普通行走样本存多了数据集中“前方无障碍、直行就行”的场景占绝大多数训练会偏移。这也是最常见误区数据集越存越大但有效信息反而被稀释了。你可以用一个简单计数判断reward ! 0或done为真的样本保留其余按 10% 概率采样这样数据集的信噪比会高很多。5. 避坑DDPG Gazebo 项目里我踩过的五个坑5.1 TensorFlow 和 ROS 的 protobuf 冲突import 直接崩现象训练脚本第一行import tensorflow之后立刻报错ImportError: Something is wrong with the protobuf installation或者google.protobuf.descriptor_pool找不到文件。原因ROS Noetic 自带 Python 的 protobuf 3.x而 TensorFlow 2.11 以上会把 protobuf 升级到 4.x两套 proto 描述符打起来。解决锁版本pip3 install tensorflow2.10.0 protobuf3.20.3。装完不要再用pip3 install --upgrade tensorflow一升级就翻车。如果你的环境是 Ubuntu 22.04 还涉及 snap 版 Gazebo这种情况先把 ROS 的 Python 包和 TensorFlow 分开装进 virtualenv别混在系统 Python 里。5.2 Gazebo 在虚拟机里闪烁到没法看现象VMware 或 VirtualBox 里打开 Gazebo画面疯狂闪烁或者 GPU 渲染不出来只能看到一片黑窗口。原因虚拟机没有直通 NVIDIA 显卡时Gazebo 默认走 OpenGL 硬渲染驱动接口不稳定。解决给 Gazebo 的启动加软渲染环境变量export LIBGL_ALWAYS_SOFTWARE1 export GAZEBO_RESOURCE_PATH/opt/ros/noetic/share/gazebo_ros roslaunch turtlebot3_gazebo turtlebot3_empty_world.launch如果还不行在 VMware 设置里打开 3D 图形加速并把显存调大。这个方法能解决 90% 的“VMware 打开 Gazebo 屏幕闪烁”问题。如果你跑的是云服务器无桌面环境直接用roslaunch的时候加headless:true参数不要用虚拟屏幕硬开。5.3 训练前期奖励曲线不动小车只会原地画圈现象训练到第 30 个 episode小车不是朝目标走而是不停原地绕车轴转圈奖励一直在零附近徘徊。原因奖励函数中角速度惩罚系数太小同时目标方向角没有进状态小车靠“绕圈的同时偶尔扫到目标”得到了几乎稳定的奖励没有动力去学习向前走。解决把状态里的goal_angle改成[sin(goal_angle), cos(goal_angle)]两维并在奖励里加上0.1*abs(angular_vel)的惩罚。另外把linear.x的动作下界设成 0.05保证每一步至少有一点前进。如果你看到小车绕圈但线速度不为 0说明是噪声太大把 OUNoise 的 sigma 从 0.2 降到 0.1。还有一个经验判断如果小车绕圈方向固定说明 Critic 已经欠拟合把 Critic 学习率调回 1e-3 试试。5.4 Gazebo 重置后小车“有记忆”传感器缓存导致状态错乱现象每个 episode 重置完第一帧状态是上一局末尾的状态小车还没动奖励却和上一局最后一步一样。原因set_model_state复位模型位置后/scan话题的发布频率比代码执行循环慢订阅缓存里还留着一帧旧激光。如果不处理每次训练的起点状态就会串。解决在reset()里把self.laser None然后用一个 while 循环等待新激光到达。如果等不到再单独检查/scan话题的发布频率把/gazebo/set_model_state之后的 sleep 时间加长到 1 秒。这是机器人 RL 里最常见的时序坑不比算法本身简单。我见过有人调了两周算法最后发现只是 reset 没等传感器模型学的全是上一条轨迹的残影。5.5 数据集“假平衡”单场景数据让 DDPG 过拟合现象训练时奖励曲线很漂亮成功率 95%换一张新地图跑成功率掉到 30% 以下。原因你只在同一个 Gazebo 世界训练数据集中 90% 的激光布局都长得差不多DDPG 的 Critic 很快背下了这套布局换成新障碍位置就失效。解决训练时让目标点和障碍布局每一局都随机化。具体做法是每个 episode 重置后用 2.2 节给的 spawn 服务清掉旧障碍物、生成新障碍物。同时把数据集的存储改为跨场景混合存储把不同随机种子下产生的 transition 混在一起再做 npz 保存。论文里写“训练集覆盖多种布局测试集用未见布局”这条就已经够到了毕业设计的核心创新点。6. 从训练到演示用一张陌生地图验证 DDPG 并固化成 ROS 节点训练出来的 actor 模型如果不封装直接写论文会被认为“算法没落地”。我有一个自己的固定收尾动作把 actor 权重保存成 h5 文件训练完再启动一个全新的 Gazebo 世界让模型在这个没见过的场景里跑一遍端到端导航并录下速度指令和激光数据。# 验证脚本的关键几行 model tf.keras.models.load_model(actor_final.h5) rospy.init_node(nav_ddpg_eval) pub rospy.Publisher(/cmd_vel, Twist, queue_size1) laser_sub rospy.Subscriber(/scan, LaserScan, ...) rate rospy.Rate(10) while not rospy.is_shutdown(): state build_state(np.array(laser_msg), goal_angle, goal_dist) action model(state[np.newaxis, :]).numpy()[0] pub.publish(make_twist(action))我把目标固定在距离起点 4 米的一个点然后记录三个指标是否撞墙、到达耗时、路径长度。如果成功率过了 90%说明这个模型是真的会导航如果只是训练曲线好看这步一测就露馅。你还可以把每个第 50 个 episode 的 actor 都保存一份这样不用回测也能找到一个可用的中间权重。有一个习惯救过我很多次训练过程中每遇一次“不见长进”的崩溃先在 Gazebo 里手动遥控小车走两步确认传感器数据没问题再回头改 DDPG 参数否则就会把环境 bug 当成算法问题反复调参也白搭。导航这个方向仿真里跑通后再上实车实车跑不动的权重先回来看看是不是速度控制太陡。希望帮到你也希望你能把这一整套流程用到自己的毕设里。本文还有配套的精品资源点击获取