简介:强化学习作为机器学习的重要分支,正逐步从游戏与棋盘场景走向物理世界的机器人控制。其核心原理是通过智能体与环境的持续交互,以奖励信号为引导,优化策略网络的动作输出。在机器人领域,这一技术让机械臂能够自主习得抓取、搬运等复杂操作技能,无需显式编程。仿真训练成为降低试错成本、加速算法迭代的关键手段,借助PyBullet等物理引擎与Stable-Baselines3等算法库,工程师可在虚拟环境中快速验证PPO、SAC等深度强化学习算法的效果,并将策略迁至真实机械臂。这一流程广泛应用于工业分拣、仓储物流与协作机器人场景。本文以法奥机械臂为例,详细拆解了基于PyBullet与Stable-Baselines3的仿真环境搭建、Gym接口封装、奖励设计及Sim-to-Real迁移实践,为机械臂抓取任务提供了一套可复现的技术方案。 搞了好几个礼拜,总算是把“基于 pybullet 和 stable-baselines3 的法奥机械臂强化学习抓取训练”这套流程完整跑通了。从最开始的 URDF 模型整理,到 Gym 环境封装,再到 PPO 和 SAC 的训练调参,中间踩的坑比想象中多得多。这篇博文就把整个项目的实现思路、核心细节、踩坑记录和可复现的配置全部分享出来,写给正在或者准备做机械臂抓取强化学习的同学,少走点弯路。
先交代一下这个项目是干什么的:在一个完全仿真的 pybullet 环境里,用法奥机械臂的 URDF 模型,配合 stable-baselines3 库,训练一个强化学习智能体,让它学会把散落在工作台上的目标物体抓起来、移动到指定位置。仿真跑通之后,再把训练好的策略导出、部署到实体法奥机械臂上做验证。整个过程涉及四块核心内容:仿真环境搭建、智能体接口封装、训练算法选择和 Sim-to-Real 迁移设计。
如果你手里刚好有一台法奥机械臂,或者你正在用 pybullet 做机器人强化学习,又或者你只是对“仿真训练 + 实机部署”这个链路感兴趣,这篇博文应该能给你一个相对完整的参考。下面开始拆解。
1. 项目整体设计与思路拆解
1.1 为什么选 pybullet + stable-baselines3
机械臂强化学习的第一道选择题就是仿真环境。目前主流的选择无非是 pybullet、MuJoCo、CoppeliaSim 这几种,各有各的生态位。我这次选 pybullet,核心原因有三个。
第一个原因是轻量。pybullet 是纯粹的 Python 接口,可以通过 pip 直接安装,不需要额外的图形界面,在服务器上也能跑无头模式。这对训练来说太重要了,因为强化学习动辄几十万步,如果环境本身太重,训练效率会非常低。
第二个原因是它和 OpenAI Gym 的兼容性非常好。pybullet_envs 本身就是一个 Gym 生态的成员,自定义机器人环境时只需要继承gym.Env,把 pybullet 的底层物理仿真封装成step()和reset(),就能直接喂给 stable-baselines3 的算法,链路非常顺。
第三个原因是 URDF 支持完善。法奥机械臂官方提供的 URDF 模型可以直接导入 pybullet,关节、碰撞体、视觉 mesh 都能正确加载,不需要额外写模型转换脚本。
stable-baselines3 这边,它的优点就是“训练脚本极简”。同样是 PPO,自己写一套要处理 GAE、mini-batch、学习率调度等一堆细节,用 stable-baselines3 只需要model = PPO("MlpPolicy", env, ...)加一行model.learn(total_timesteps=...)。而且它支持Monitor回调、EvalCallback、CheckpointCallback,实验管理比较方便。
1.2 法奥机械臂的建模与导入方式
法奥机械臂在行业里算是一个比较常见的协作机械臂品牌,有不同的负载和轴数型号。我这里用的是六轴协作机型,官方会提供 URDF 文件,里面已经包含了每个 link 的惯性参数、visual mesh 和 collision geometry。
在 pybullet 里加载 URDF 的代码其实非常简单:
import pybullet as p # 连接物理引擎 physics_client = p.connect(p.GUI) # 训练时改 p.DIRECT p.setGravity(0, 0, -9.81) # 加载机械臂 arm_id = p.loadURDF( "franka/urdf/fa_arm.urdf", basePosition=[0, 0, 0.8], useFixedBase=True, flags=p.URDF_USE_SELF_COLLISION_EXCLUDE_PARENT, )这里有几个必须注意的细节:
useFixedBase=True是必需的。默认情况下 pybullet 会把 base link 当成自由物体,机械臂会直接掉下去。固定底座之后,机械臂才能作为稳定的操作主体。- 碰撞检测的 flag 要慎重。
URDF_USE_SELF_COLLISION_EXCLUDE_PARENT可以避免相邻 link 之间误报碰撞,但如果你想要更真实的自碰撞检测,需要自己配置setCollisionFilterGroupMask。 - 法奥官方 URDF 里的 mesh 文件可能有 STL 和 DAE 两种格式。pybullet 对 DAE 纹理的支持一般,如果加载出现紫色/白色模型,不影响物理计算,但影响视觉调试。建议统一转成 STL,或者只保留 collision mesh,visual mesh 用简化模型。
加载完 URDF 之后,要检查一下关节信息,确认关节类型和运动范围。法奥六轴一般是六个旋转关节,可以用p.getNumJoints()和p.getJointInfo()逐个打印。特别是关节的jointLowerLimit和jointUpperLimit,直接决定了后面动作空间怎么裁剪。
1.3 抓取任务如何建模成强化学习问题
机械臂抓取在强化学习里是一个非常典型的“稀疏奖励 + 高维连续控制”任务。我们要明确三点:状态空间、动作空间、奖励函数。
状态空间我采用了“机械臂关节角 + 末端位姿 + 目标物位置 + 目标物姿态”的组合。法奥六轴的关节角是 6 维,末端位姿取位置和欧拉角共 6 维,目标物位置 3 维,目标物姿态用四元数 4 维,再加上末端速度 3 维和经验性的二指夹爪开合度 1 维,总共 23 维。
动作空间有两种设计思路。第一种是关节空间控制,智能体直接输出 6 个关节的目标角度,然后由底层 PID 跟踪;第二种是笛卡尔空间控制,智能体输出末端在 x、y、z 方向的目标位置增量,再通过逆解算成关节角。我实际测下来,在 pybullet 里直接用关节空间控制更容易收敛,因为逆解本身会引入额外的误差和延迟。
奖励函数是所有强化学习项目里最“玄学”的部分,但也是决定项目成败的部分。我一开始用的是纯稀疏奖励:抓到了给 +10,其他情况 0。结果训练了半天几乎不收敛,原因就是“成功样本太难出现”,智能体在前期完全得不到有效梯度。
后来我改成了“稀疏 + 密集引导”的混合奖励,具体设计在后面章节展开。
2. 环境搭建与工具链配置
2.1 Python 环境与版本控制
这个项目对 Python 版本有一定的要求。我用的版本组合是:
- Python 3.9
- pybullet 3.2.5
- stable-baselines3 2.1.0
- gymnasium 0.29.1
- numpy 1.24.3
为什么强调版本,因为 stable-baselines3 从 2.0 开始全面转向gymnasium,而 pybullet 的 Gym 接口在某些老版本里还是gym。如果混用,会出现env.action_space类型不匹配或者env.reset()返回值格式不一致的报错。
我建议用conda创建独立环境,不要直接装在系统 Python 里:
conda create -n rl_grasp python=3.9 conda activate rl_grasp pip install pybullet==3.2.5 stable-baselines3==2.1.0 gymnasium==0.29.1 numpy==1.24.32.2 pybullet 安装与验证
pybullet 的安装一般很顺利,没有复杂的依赖。装完之后先跑一个最小验证脚本,确保物理引擎正常:
import pybullet as p p.connect(p.DIRECT) p.setGravity(0, 0, -9.81) assert p.isConnected() == 1 print("pybullet OK")这里p.DIRECT是不启动图形界面的模式,适合服务器训练;p.GUI会弹出一个可视化窗口,适合调试。训练时一定要用DIRECT模式,否则渲染会占用大量 CPU 资源,训练速度打折。调试时要切到GUI模式,否则你根本看不到机械臂在干嘛。
2.3 stable-baselines3 的核心接口理解
stable-baselines3 的抽象层级非常清晰。最核心的类就是PPO、SAC、TD3这些算法类,它们接受一个 Gym 环境,然后内部通过MlpPolicy或CnnPolicy构建策略网络。
使用的时候,环境需要满足几个约定:
env.reset()返回值是(obs, info)元组,obs 是一个 numpy 数组,info 是一个字典。env.step(action)返回值是(obs, reward, terminated, truncated, info)五元组。env.action_space和env.observation_space必须是gymnasium.spaces里的类型。
如果你的环境是从老版 gym 移植过来的,需要手动调整 reset 和 step 的返回值格式。这个坑非常常见,我会在后面的排查章节详细说。
2.4 法奥机械臂 URDF 模型的准备工作
拿到官方法奥 URDF 之后,不要直接塞给 pybullet,先做三件事。
第一件事,确认所有 mesh 路径是绝对路径还是相对路径。URDF 文件里的 mesh 标签通常用package://协议,pybullet 不认识这个协议,需要改成相对路径或绝对路径。最简单的做法是把 URDF 文件和 mesh 文件夹放在同一个根目录下,然后统一修复。
第二件事,检查joint的origin是否对齐。法奥官方模型的 joint origin 通常是对齐的,但有些协作臂为了适配自家控制器会做坐标偏移。抓取任务对末端精度要求高,这个偏移会导致末端位姿判断出错。可以在 pybullet 里加载后,用p.getLinkState(arm_id, 6)查看末端实际位置,和理论值对比。
第三件事,给机械臂末端添加一个夹爪模型。如果官方 URDF 里没有夹爪,需要在末端 link 上额外加载一个简单的二指夹爪。我这边用一个简易的平行夹爪模型,两个 finger link 分别可以控制开合,这样抓取动作才有效果。
3. 仿真环境核心设计与实现
3.1 Gym 环境封装:observation、action、reward 的完整定义
整个项目最核心的代码就是自定义的 Gym 环境。我把它命名为FaArmGraspEnv,继承gymnasium.Env。
初始化阶段做这些事:
- 创建 pybullet 连接。
- 加载机械臂 URDF 和工作台平面。
- 设置机械臂初始关节角度。
- 创建目标物体,随机生成位置。
- 设置动作空间和观测空间。
- 初始化控制接口。
核心代码如下:
import gymnasium as gym from gymnasium import spaces import numpy as np import pybullet as p class FaArmGraspEnv(gym.Env): metadata = {"render_modes": ["human", "rgb_array"]} def __init__(self, render_mode=None, reward_type="hybrid"): super().__init__() self.reward_type = reward_type if render_mode == "human": self.physics_client = p.connect(p.GUI) else: self.physics_client = p.connect(p.DIRECT) p.setGravity(0, 0, -9.81) p.setTimeStep(1.0 / 240) # 加载机械臂 self.arm_id = p.loadURDF("urdf/fa_arm.urdf", useFixedBase=True) self.joint_ids = [] for j in range(p.getNumJoints(self.arm_id)): info = p.getJointInfo(self.arm_id, j) if info[2] == p.JOINT_REVOLUTE: self.joint_ids.append(j) # 加载工作台 self.table_id = p.loadURDF("urdf/table.urdf", basePosition=[0.5, 0, 0]) # 加载夹爪 self.gripper_id = p.loadURDF("urdf/simple_gripper.urdf", basePosition=[0.5, 0, 0.5]) # 目标物体 self.object_id = None # 动作空间:6个关节角增量 + 1个夹爪开合 self.action_space = spaces.Box( low=np.array([-0.1] * 6 + [-1.0]), high=np.array([0.1] * 6 + [1.0]), dtype=np.float64 ) # 观测空间:23维 self.observation_space = spaces.Box( low=-np.inf, high=np.inf, shape=(23,), dtype=np.float64 ) self.render_mode = render_mode def _get_obs(self): joint_states = p.getJointStates(self.arm_id, self.joint_ids) joint_pos = np.array([s[0] for s in joint_states]) joint_vel = np.array([s[1] for s in joint_states]) # 末端位姿 link_state = p.getLinkState(self.arm_id, 6) end_pos = np.array(link_state[0]) end_orn = np.array(link_state[1]) # 物体位姿 obj_pos, obj_orn = p.getBasePositionAndOrientation(self.object_id) obj_pos = np.array(obj_pos) obj_orn = np.array(obj_orn) # 夹爪开合 gripper_state = p.getJointState(self.gripper_id, 0)[0] obs = np.concatenate([ joint_pos, joint_vel, end_pos, end_orn, obj_pos, obj_orn, np.array([gripper_state]) ]) return obs def reset(self, seed=None, options=None): p.resetSimulation() p.setGravity(0, 0, -9.81) # 重新加载机械臂 self.arm_id = p.loadURDF("urdf/fa_arm.urdf", useFixedBase=True) # 重置夹爪 # 生成目标物体 self._place_object() return self._get_obs(), {} def step(self, action): # 关节角度增量控制 current_joint_pos = [] for j in self.joint_ids: state = p.getJointState(self.arm_id, j) current_joint_pos.append(state[0]) target_pos = np.array(current_joint_pos) + action[:6] for i, j in enumerate(self.joint_ids): p.setJointMotorControl2( self.arm_id, j, p.POSITION_CONTROL, targetPosition=target_pos[i], force=200.0, ) # 夹爪控制 gripper_cmd = 0.5 if action[6] > 0 else 0.0 p.setJointMotorControl2( self.gripper_id, 0, p.POSITION_CONTROL, targetPosition=gripper_cmd, force=50.0, ) p.stepSimulation() obs = self._get_obs() reward = self._compute_reward() terminated = self._check_success() truncated = False info = {"success": terminated} return obs, reward, terminated, truncated, info3.2 机械臂控制方式:位置控制 vs 力矩控制
在 pybullet 里控制机械臂主要有两种方式:POSITION_CONTROL和TORQUE_CONTROL。强化学习里这两种都有人用,但效果差别很大。
POSITION_CONTROL是让机械臂追踪一个目标关节角度,pybullet 内部会通过一个内置的 PID 计算力矩。这个模式下,智能体不需要关心动力学参数,只要输出目标角度就行,所以更容易训练。但缺点是,在仿真里如果目标角度变化太快,机械臂看起来会“瞬移”,这是不真实的。
TORQUE_CONTROL是直接给每个关节施加力矩,智能体输出的是力矩值。这更接近真实机器人控制,但训练难度会高不少,因为智能体必须同时学会动力学补偿和运动规划。
我试验之后的选择是:训练阶段用位置控制,动作空间输出的是关节角度的增量,保证运动平滑;部署到实机前,再把学到的策略转换到位置控制模式,因为法奥机械臂本身的底层控制器也支持关节位置指令,这样 sim-to-real 的映射成本最低。
3.3 目标物生成与抓取判定
目标物体不能每次都放在同一个位置,否则智能体只会“背板”,学不会泛化能力。我在 reset 里把物体放在一个 40cm x 40cm 的区域内,同时随机旋转物体的初始朝向:
def _place_object(self): x = np.random.uniform(0.3, 0.7) y = np.random.uniform(-0.2, 0.2) z = 0.2 orn = p.getQuaternionFromEuler([0, 0, np.random.uniform(0, 2 * np.pi)]) self.object_id = p.loadURDF("urdf/cube.urdf", basePosition=[x, y, z], baseOrientation=orn)抓取判定是整个环境的核心逻辑。我用的判定条件是:
- 夹爪两个 finger 之间的距离小于物体尺寸的一半,说明夹爪已经闭合。
- 物体在 z 方向的位移超过 5cm,说明被夹起来了。
- 连续 10 个仿真步满足上述条件,才算一次成功的抓取。
这个“连续 10 步”非常关键。如果只是单步判定,会有很多“瞬移”抓取的假阳性。比如物体被夹爪推了一下,恰好在这一帧位置变了,单步判定就会误判为成功。
def _check_success(self): gripper_state = p.getJointState(self.gripper_id, 0)[0] obj_pos, _ = p.getBasePositionAndOrientation(self.object_id) if gripper_state < 0.3 and obj_pos[2] > 0.25: self._success_steps += 1 else: self._success_steps = 0 return self._success_steps >= 103.4 奖励函数设计的实战思路
奖励设计是我这个项目里试错最多的地方。最后用的是“稀疏 + 密集”混合奖励,具体公式如下:
- 机械臂末端朝向目标物移动:每一步奖励
+0.1 * (previous_distance - current_distance)。这给智能体一个“往目标走是对的”的梯度。 - 末端与目标物距离小于 10cm:额外奖励
+0.5,鼓励末端足够接近。 - 夹爪成功抓住物体:奖励
+5.0。 - 成功将物体举离桌面并保持:奖励
+10.0。 - 每步施加一个小的时间惩罚
-0.01,鼓励智能体不要无限拖延。
这个设计的核心逻辑是“用密集奖励把智能体引导到动作附近,再用稀疏奖励做精确判断”。如果全部用稀疏奖励,智能体在庞大的动作空间里几乎不可能随机找到成功路径,训练会一直停滞在探索阶段。
4. 训练流程与算法调参实践
4.1 选 PPO 还是 SAC
stable-baselines3 支持不少算法,机械臂连续控制最常用的就是 PPO 和 SAC。这两者的区别我用一句大白话说:PPO 是“一步一步小心试”,SAC 是“大胆尝试,回报最大”。
PPO 的优点是训练稳定、超参数敏感度低,特别适合仿真环境里“样本生成便宜但想要稳定收敛”的场景。缺点是对样本利用效率不够高,需要更多的探索步数。
SAC 的优点是样本利用效率高,能从过去的经验池里反复学习,适合复杂连续控制问题。缺点是调参比较烦,学习率、熵系数、网络结构都会显著影响训练结果。
我的最终选择是 PPO。原因很简单:在这个抓取任务里,动作维度只有 7 维,状态空间 23 维,PPO 完全能处理,而且 PPO 更稳定。SAC 我也跑了几个实验,虽然有时候能更快找到好的策略,但偶尔会突然发散,需要更多精力盯训练。
如果你用的是 7 自由度或者更复杂的机械臂模型,或者你的物体形状不规则,我建议试试 SAC,因为任务难度上去之后,PPO 的样本效率会显得不够用。
4.2 训练脚本核心实现
训练脚本本身并不复杂,核心代码大概 50 行:
import time from stable_baselines3 import PPO from stable_baselines3.common.callbacks import EvalCallback, CheckpointCallback from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv from env import FaArmGraspEnv def make_env(): def _init(): env = FaArmGraspEnv(render_mode=None) return env return _init if __name__ == "__main__": # 使用 4 个并行环境加速数据采样 env = SubprocVecEnv([make_env() for _ in range(4)]) model = PPO( "MlpPolicy", env, learning_rate=3e-4, n_steps=2048, batch_size=256, n_epochs=10, gamma=0.99, gae_lambda=0.95, clip_range=0.2, ent_coef=0.01, vf_coef=0.5, max_grad_norm=0.5, seed=42, verbose=1, tensorboard_log="./tb_logs/", ) # 回调:定期评估与保存模型 eval_env = DummyVecEnv([make_env()]) eval_callback = EvalCallback( eval_env, best_model_save_path="./models/best/", log_path="./eval_logs/", eval_freq=10000, n_eval_episodes=10, deterministic=True, ) checkpoint_callback = CheckpointCallback( save_freq=50000, save_path="./models/checkpoints/", name_prefix="fa_grasp" ) model.learn( total_timesteps=2_000_000, callback=[eval_callback, checkpoint_callback], progress_bar=True, ) model.save("./models/final_model.zip")4.3 训练参数选择与原因分析
上面的超参数不是随便写的,每一个都经过实测调整。
n_steps=2048:这是每轮更新前收集的样本量。对于 7 维动作空间,2048 步的环境交互足够,不需要太大。如果n_steps太大,学习更新频率会降低,收敛变慢。
batch_size=256:这是每次梯度更新的样本量。256 是一个比较平衡的值。太大会让梯度更稳定但计算慢,太小则噪声大。
n_epochs=10:这是每次收集完样本后,用这批样本重复学习的轮数。PPO 的一个特点是可以多学几轮,但过多会导致过拟合当前 batch。10 是我试下来比较合理的选择。
gamma=0.99:折扣因子。因为抓取任务是稀疏奖励+密集奖励混合,未来奖励的权重不能太低。0.99 比较合理,太低会让智能体只顾眼前。
ent_coef=0.01:熵系数。控制探索程度。如果设成 0,智能体可能过早收敛到局部最优;太大又会让策略太随机,难以收敛。0.01 是一个比较微妙的平衡点。
clip_range=0.2:PPO 的裁剪范围。这个值控制每轮更新步长。0.2 是默认值,通常不需要大改。如果训练不稳定,可以降到 0.1。
4.4 训练监控与评估技巧
训练时间是个大问题。我第一次跑了 200 万步,用了大概 10 个小时才稳定收敛。后来我把 pybullet 的setTimeStep调大,从1/240改到1/120,速度翻了一倍,而且对最终策略影响不大。
监控训练过程我强烈建议用 TensorBoard。在训练脚本里设置tensorboard_log之后,stable-baselines3 会自动记录rollout/ep_rew_mean、loss、entropy等指标。
tensorboard --logdir ./tb_logs/重点关注rollout/ep_rew_mean这个曲线。如果它持续上升,说明策略在改善;如果震荡剧烈,说明学习率可能太大或者熵系数太高。loss曲线也有参考价值,但不要只看 loss,loss 下降不代表策略变好,因为 PPO 的 loss 和奖励不是完全线性相关。
评估策略的时候,不要只看平均奖励,要看真实的成功率。我在EvalCallback里设置了n_eval_episodes=10,它会每 10000 步评估 10 轮。再在环境里加一个success字段,统计这 10 轮里成功了多少次,这才是真正有用的指标。
5. Sim-to-Real 迁移的工程化思考
5.1 仿真到实机的主要挑战
仿真里能抓取,不代表实机也能抓取。这个问题做机器人强化学习的人都绕不开。主要挑战有三个:
第一是动力学误差。pybullet 里的摩擦系数、惯性参数、关节阻尼都是理想化的,实机上的电机摩擦、减速器背隙、控制延迟都会让同样一个策略失效。
第二是观测噪声。仿真里关节角度和物体位置是精确已知的,但实机上的关节编码器有噪声,相机位姿估计也有误差。
第三是控制频率差异。pybullet 里stepSimulation()是理想循环,实机控制器的响应频率、通信延迟都会让策略的决策跟不上。
最常用的应对手段就是 Domain Randomization。在训练阶段,每次 reset 环境时都随机化物体的质量、摩擦系数、关节阻尼,甚至随机化控制延迟。这样学到的策略就不会过度依赖某个精确的物理参数。
5.2 Domain Randomization 的实现方案
在 pybullet 里实现 domain randomization 并不复杂。核心是在 reset 时对物理参数做随机扰动:
def _randomize_physics(self): # 随机化物体质量 mass = np.random.uniform(0.05, 0.2) p.changeDynamics(self.object_id, -1, mass=mass) # 随机化物体摩擦 lateral_friction = np.random.uniform(0.5, 1.5) p.changeDynamics(self.object_id, -1, lateralFriction=lateral_friction) # 随机化机械臂关节阻尼 for j in self.joint_ids: damping = np.random.uniform(0.1, 1.0) p.changeDynamics(self.arm_id, j, jointDamping=damping)我试过在训练时加入这些随机化,最终策略对物体质量变化的鲁棒性确实明显增强。但代价是训练时间变长了,因为环境变“难”了。
5.3 法奥机械臂实机部署流程
从 pybullet 到法奥机械臂实机部署,主要分三步。
第一步,确认控制接口。法奥机械臂支持标准的 TCP/IP 或 Modbus 控制协议,可以发送关节位置指令。我通过法奥的 SDK 写了一个简单的 Python 控制类,把仿真里训练好的策略输出的关节位置直接发送给实机。
第二步,做关节映射。训练好的策略输出的是 pybullet 模型里的关节角度,实机的关节角度可能有坐标系差异和零位偏移。需要在实机上逐个关节校准,做线性映射。
第三步,安全验证。在任何自动控制之前,先用遥控模式让机械臂到一个安全位置,然后逐关节小幅度移动,确认关节方向正确,再开始完整策略验证。
这个阶段我强烈建议在实机旁边准备一个急停按钮,毕竟强化学习的策略在实机上表现未知,安全第一。
6. 常见问题与排查技巧实录
6.1 pybullet 连接失败或者 GUI 卡死
这个问题主要出现在环境切换的时候。比如你之前用p.GUI调试,后面忘了断开连接,又去跑新的脚本,就会连接失败。
解决办法是每次脚本开头检查连接,或者利用try-finally结构保证断开连接:
import pybullet as p try: p.connect(p.GUI) # do stuff finally: p.disconnect()如果 GUI 界面卡住不动,很可能是物理步数和渲染步数不同步。pybullet 默认的setRealTimeSimulation(0)模式下,你需要手动调用p.stepSimulation()才能推进物理仿真。渲染线程和物理线程不同步就会表现为画面卡顿。
6.2 训练出现 NaN loss 或策略发散
NaN loss 是强化学习训练里最让人头疼的问题。原因通常有两个:一是状态输入里有 NaN,二是学习率太大导致梯度爆炸。
排查思路:
- 先检查观测值里有没有 NaN。可以在
_get_obs里加一个断言,或者直接打印输出。 - 检查动作输出是否超出合理范围。有些机械臂位置控制如果输入了过大的目标角度,pybullet 内部可能会计算出奇异值。
- 降低学习率,从
3e-4降到1e-4,看是否缓解。
我实际排查过一次,发现是目标物体偶尔会掉到工作台下面,导致getBasePositionAndOrientation返回了 NaN。后来在_place_object里加了位置合法性检查,问题解决。
6.3 机械臂关节运动不自然或震荡
这通常是因为位置控制的力太小。pybullet 里setJointMotorControl2的force参数决定了最大输出力矩。如果力不够,机械臂会“软绵绵”的,跟不上目标位置,导致震荡。
解决办法是把force调大,同时适当增大positionGain。我用的力是 200N,对于法奥这种小型协作臂已经足够。
另一方面,如果动作空间输出的关节增量太大,机械臂每步都会“猛冲”,看起来很假。把动作空间的高低位从0.1降到0.05,运动就会平滑很多,训练稳定性也会提升。
6.4 stable-baselines3 版本兼容问题
稳定基线 3 更新比较频繁,2.0 之后很多 API 有变化。最典型的是env.reset()的返回值。老版本 gym 是直接返回obs,新版本 gymnasium 是返回(obs, info)。
如果你用的是 stable-baselines3 1.x,需要配合老版 gym;如果你用 2.x,必须用 gymnasium。两者混用会直接报错。
6.5 抓取判定不准确,频繁报成功
这个问题我在调试早期经常遇到。原因是物体很小,夹爪闭合的那一帧,物体刚好被夹爪的碰撞体弹飞了,z 坐标瞬间高于判定阈值,导致误判。
解决办法是在_check_success里加入连续步数判定,并且增加一个“夹持力”的判断:不仅要看夹爪位置,还要看物体是否真的在夹爪的正中心。
def _check_success(self): gripper_state = p.getJointState(self.gripper_id, 0)[0] obj_pos, _ = p.getBasePositionAndOrientation(self.object_id) # 检查物体是否在夹爪中心附近 dist_to_center = np.linalg.norm( np.array(obj_pos) - np.array(self._gripper_center) ) if gripper_state < 0.3 and obj_pos[2] > 0.25 and dist_to_center < 0.08: self._success_steps += 1 else: self._success_steps = 0 return self._success_steps >= 107. 项目后续扩展方向
这套抓取训练框架跑通之后,往下的扩展空间还挺大的。我目前正在尝试的方向大概有这么几个,也分享一下。
一是把单物体抓取扩展成多物体分拣。核心改动是把目标物体的数量从 1 增加到 3-5 个,状态空间里加上所有物体的位置信息,奖励函数里改成“每次成功抓取一个物体 + 固定奖励”。这个改动会让学习难度明显上升,因为智能体需要学会“先抓哪个、再抓哪个”的决策能力。
二是增加视觉观测。目前的状态输入是“上帝视角”的精确物体位置,实机上这需要额外的视觉识别模块。更接近实机的做法是加入相机渲染,把 RGB 图像作为 CNN 的输入。stable-baselines3 支持CnnPolicy,pybullet 也可以用p.getCameraImage获取仿真渲染画面。这个方向对硬件要求高,训练时间会成倍增加,但泛化能力会好很多。
三是从 PPO 换成 SAC 做对比实验。如果你想深入理解不同算法在机械臂控制上的表现差异,完全可以在同一套环境上跑 PPO 和 SAC 的对比,用 TensorBoard 的曲线看谁收敛更快、谁更稳定。这组实验跑下来,你对算法特性会有比看十篇论文更直观的认识。
四是用 Domain Randomization 把策略做得更鲁棒。可以逐步加入随机物体形状、随机光照、随机相机位姿、随机控制延迟等。每增加一个随机化维度,训练难度和训练时间都会上涨,但实机成功率也会提升。
最后,从我个人这段实操经历来说,最想强调的一点是:强化学习在机器人上的落地,真正花时间的不是调算法超参数,而是把环境定义对。尤其是奖励函数和抓取成功判定这两个东西,你如果一开始没想清楚,后面所有训练结果都是不可信的。所以如果你准备自己动手做,我建议先从可视化调试开始,在GUI模式下用随机策略跑几百步,看看机械臂和物体的交互是否合理,再去接 stable-baselines3 训练。这一步做到位了,后面就会顺不少。
本文还有配套的精品资源,点击获取