☰
开源鸭形双足机器人:强化学习驱动与Sim-to-Real迁移全解析
2026/10/6 7:22:44 网站建设 项目流程

说实话,第一次看到这只鸭子摇摇晃晃地在实验室地板上走出稳定步态时,我没有立刻觉得了不起——毕竟仿真里同一套策略已经跑过几百万步。真正让我意外的是,真机落地的门槛比仿真里任何一张摩擦系数表格都苛刻。这个微小型双足鸭形机器人项目,是一套以强化学习驱动、架构完全开源的双足机器人系统:机械设计、仿真环境、训练代码、真机部署固件全部公开可复现。本文会把我从零搭这套系统的完整过程、踩过的坑、以及为什么这样选型的原因全部讲清楚,适合正在做足式机器人强化学习落地、或者打算从零接触双足运动控制的同学参考。

1. 立项思路:小尺寸双足鸭形机器人到底在验证什么

1.1 为什么选“鸭子”而不是人形

人形机器人看起来是最有冲击力的,但小尺寸人形对结构刚性、舵机扭矩、平衡控制的成本要求非常高,以我这种小工作室级别的条件,人形方案只会把大量时间花在修机器而不是训练策略上。

鸭子形态则是一个被低估的好选择。鸭子的腿部短、身体矮胖、质心低,再加上脚掌宽大,天然解决了人形最头疼的侧向平衡问题——侧倾方向上的静态稳定裕度很大,策略不需要在roll轴上有特别精细的力矩控制就能站住。同时,短腿缩小了步幅,对舵机的位置精度要求也相应降低,微型舵机能扛得住。

更重要的是,鸭形机器人的“摇摇摆摆”步态不是我们人为指定的,而是强化学习在奖励函数驱动下自然涌现出来的。鸭子走路摇摆,是因为质心低、腿部短,策略为了在每一帧保持稳定,会主动把躯干重心转移到支撑脚一侧。这种涌现式步态,比手写ZMP轨迹更有说服力,教学展示效果也好。

1.2 这个项目真正要解决的核心问题

这个项目的技术目标可以拆成三层:

第一层,双足运动控制本身。四到六个自由度的关节腿看起来简单,但本质上是欠驱动系统,脚掌与地面的接触约束随时会切换,加上舵机响应慢、存在回差,经典控制方式需要比较精确的动力学模型,而这些模型在微型硬件上往往测不准。

第二层,强化学习策略的仿真到真机迁移,即Sim-to-Real。策略在仿真里能跑得很好,不等于真机也能跑。摩擦、延迟、舵机死区、IMU噪声,每一个都可能导致策略在真机上“原地转圈”或“双腿僵直”。这套项目把域随机化、动作平滑、模型导出这些工程细节全部沉淀下来,这是我想分享的核心价值。

第三层,整体架构的开源组织方式。强化学习训练环境通常跑在GPU机器上,真机部署跑在嵌入式设备上,两者是完全不同的技术栈。如何让一套代码仓库同时兼容训练与推理,如何保证观测向量的顺序在仿真和真机上严格一致,这些工程问题往往被课程和论文忽略,但在实际项目里最重要。

1.3 这套系统适合哪些人参考

如果你是做强化学习算法研究、但一直停留在“CartPole”或“MountainCar”这类玩具环境的,这套鸭形机器人可以给你一个真正的连续控制、接触丰富的测试平台。如果你已经在用Isaac Gym或MuJoCo训练足式机器人,但是苦于迁移到真机总是失败,那第6章的排查记录大概率对你有用。如果你是硬件玩家,想做一个有辨识度的开源作品,这个鸭形双足的成本控制在几百元到两千元区间,完全在发烧友承受范围内。

2. 硬件设计取舍:微型双足的机械与电气细节

2.1 自由度配置与舵机选型

自由度配置我最终定为每条腿3个自由度:髋关节俯仰、膝关节俯仰、踝关节俯仰,共6个舵机。有人会问,要不要加髋关节横滚?鸭子走路时躯干左右摇摆,听起来髋横滚很有用,但实测下来,宽脚掌加低质心已经把侧向问题消化掉了,强行上髋横滚反而会让每条腿增加一个舵机,重量和成本上升,没必要。6个自由度既能走出实验结果明确的步态,又保持结构简单可靠。

舵机是这套系统里最关键的硬件。我对比过三种方案:

  • 玩具级舵机(SG90/MG90S):扭矩1.8到2.2公斤厘米,价格几块钱一个。只能带动300克以内的结构,回差大得离谱,小角度指令经常不响应,只适合做静态结构验证。
  • 总线数字舵机(Feetech STS3215):扭矩约25公斤厘米(7.4V供电),支持角度反馈和串行总线,一个UART就能控制并联的多个舵机。单个重量57克左右,适合600到900克级别的机器。我是最终选了这一档。
  • 高阶智能舵机(Dynamixel XL330):15克重,精度和响应速度非常好,但价格是STS3215的数倍,还需要额外的主控适配,预算充足的可以上。

选择驱动方案时,很多人只看扭矩数据,忽略了“电流和电源”。STS3215堵转时可以拉到3到5安培,两三个舵机同时发力时瞬时电流很大。如果直接用单片机的5V引脚供电,分分钟掉压重启。我最后在电池输出端并联了470微法和1000微法两个电解电容,再经过一个持续3安培的BEC降压到5V给舵机供电,主控单独用LDO供电,隔离干扰。

2.2 感知、主控与功率分配

感知层只需要一个IMU。我选了ICM-20948,比MPU6050的噪声小,包含三轴加速度计、陀螺仪和磁力计。但其实训练时策略主要用陀螺仪角速度和估计出来的roll/pitch,磁力计的yaw信息因为室内干扰大基本不用。IMU安装位置很重要:尽量贴近整机质心,并且用螺丝固定,不能只用双面胶,否则舵机震动会直接污染IMU数据,策略输入噪声一大,真机立刻抖动。

主控分成上下位机两级:

  • 上位机用树莓派Zero 2 W,负责跑强化学习策略推理。Python加ONNX Runtime,50Hz的推理频率完全够用,CPU占用约30%到40%。树莓派的好处是生态成熟,部署PyTorch导出的模型非常方便。
  • 下位机用STM32F103或ESP32,负责舵机控制、IMU读取、状态估计,以及PD控制环。PD环跑500Hz,把上位机50Hz的粗粒度目标位置插值成平滑的舵机指令。

这种上下位机分离的结构,本质上把“慢速智能”和“快速反应”分开。强化学习策略不需要在1毫秒内响应,但舵机PD必须快速收敛到目标角度,否则关节会软塌塌的。

2.3 3D打印结构与装配细节

结构件全部用PLA+或PETG打印,壁厚1.4到2毫米。大腿和小腿是受力最大的部位,我在这两个零件上额外加了加强筋,单纯加厚壁厚反而容易翘曲,受力集中处加三角形筋肋的效果更好。打印时注意:

  • 舵机安装孔用公差0.15毫米的孔位,配合金属舵机盘,不要直接打印塑料轴套去卡舵机输出轴,磨损很快。
  • 踝关节和膝关节的连接螺丝要用M2或者M3的机制螺丝,配合热熔铜螺母嵌入打印件,不能直接往塑料里拧自攻螺丝,几次拆装就会滑丝。
  • 电池放在鸭子腹部最低处,让整机质心更低。这一步对步态稳定性提升非常明显,比调整奖励函数权重见效更快。

装配完毕后,务必做一个完整的零点标定流程:把每个舵机置于物理中位,读回原始角度值,写入配置文件。这个值就是仿真模型里的关节零位,如果标定偏差超过2度,策略一启动就会歪着走。

3. 仿真环境搭建与强化学习算法选型

3.1 仿真器对比与最终选择

我先后试过Gazebo、PyBullet、MuJoCo和Isaac Lab,说下真实感受,不一定都对,但踩出来的经验值得参考:

仿真器并行能力训练速度上手难度适用阶段
Isaac LabGPU上千环境并行极快较高大规模RL训练,主力方案
MuJoCo/MJXGPU并行快低轻量训练与物理调试
PyBulletCPU单进程为主很慢低教学调试,不推荐大规模训练
GazeboCPU,难以并行很慢中ROS集成、传感器闭环验证

很多人第一步就选Gazebo,结果发现训练一集动画都要等几秒,强化学习需要成千上万次采样,这种速度根本跑不动。Gazebo的优势在于传感器仿真细节和ROS生态,但那是策略训练完成之后的验证环节。最终我选择Isaac Lab做主力训练,MuJoCo用于快速原型验证。

选Isaac Lab还有一个重要原因:官方和社区已经沉淀了大量足式机器人的训练范例,四足也好、双足也好,代码模式可以直接参考,不用从零造轮子。对于小团队来说,站在这种开源工具链上,能少走非常多弯路。

3.2 PPO为什么是双足运动控制的首选

算法选型上我做了一轮筛选,最终确定PPO。原因有三:

第一,双足运动控制是连续动作空间问题,DQN这类离散动作算法直接排除。第二,SAC和TD3虽然采样效率更高、超参数相对敏感,在真机数据少的情况下有优势,但在仿真里我们有几千个并行环境,根本不需要极致的采样效率,反而需要稳定性和对奖励函数扰动的鲁棒性。第三,PPO的clipped surrogate objective对步长控制内置了保守机制,训练过程不容易发散,这一点在奖励函数还要反复调整的项目阶段非常宝贵。

实现上我直接用了rsl_rl这个库,它是Isaac Gym生态里最常见的PPO实现,针对足式运动做了优化。策略网络是两层MLP,每层256个隐藏单元,激活函数用tanh。批评家网络可以访问特权信息(真值速度、接触力、摩擦系数等),这是经典的asymmetric actor-critic设计:行动者只看带噪声的真实观测,批评家在训练时拿到全知信息,这样训练出的策略对真机观测噪声更鲁棒。

3.3 奖励函数设计的层次结构

奖励函数是强化学习里最容易被低估的部分。我的设计分层如下:

奖励项公式权重
速度跟踪exp(-0.5 * (v_x - v_cmd)^2)1.0
躯干姿态exp(-2 * (roll^2 + pitch^2))0.5
动作平滑-(a_t - a_{t-1})^20.05
关节限位-(max(0, q - q_lim))^21.0
足底滑动-v_foot_xy
存活奖励1.01.0

速度跟踪用指数函数而不是线性惩罚差距,是因为指数函数在误差附近梯度陡峭,策略会被强制“靠近目标速度”,而不是走一步算一步。躯干姿态项同理。

动作平滑惩罚对真机迁移至关重要,它约束连续两步动作的变化量不能太大,输出动作更平稳,舵机也不会被高频指令折磨得发热。动作变化量的权重从0.02到0.1去扫,过大会让策略不敢动,过小真机上会剧烈抖动。关节限位惩罚是为了防止舵机在训练时被直接撞到机械死点,真机上容易烧舵机。

要注意的是,奖励函数各项的量级差别很大,速度跟踪和姿态保持是指数型的,范围稳定在0到1;动作平滑是平方项,数量级可能到几十上百,所以必须靠权重调节。我一般先固定速度跟踪和存活奖励,再逐步增加其他项的权重,每加一项就重新训练一次,观察步态是否退化。

4. 训练过程中的工程化思考:从跑通到跑稳

4.1 域随机化:策略泛化能力的来源

仿真里跑通的策略直接搬到真机,百分之百会出问题,因为仿真永远是现实的一个近似。域随机化的思路是:既然我们不知道真实世界到底在哪组参数附近,那就把一组参数按随机范围同时塞给仿真环境,让策略在一个“参数区间”内都学会走路,真机落在区间里就能直接工作。

我这里用的随机化项包括:

  • 地面摩擦系数:均匀采样0.3到1.2,覆盖瓷砖、木板、地毯的不同情况。
  • 舵机扭矩缩放:0.8到1.2,模拟电池电压波动带来的扭矩下降。
  • 控制延迟:0到20毫秒随机,模拟上位机推理耗时和通信抖动。
  • 质量与质心偏移:整机质量±10%,质心位置随机偏移2到3毫米。
  • 观测噪声:关节角度加±0.01弧度的白噪声,陀螺仪加±0.05弧度每秒。
  • 随机初始姿态:每个episode开始时,各关节在±5度范围内随机偏置。

还有一个容易被忽视的扰动:训练过程中每隔30到60秒,给鸭子一个随机的横向推力,模拟真实环境里的碰撞和意外。这个简单手段让策略学会了“被撞一下之后重新稳住”,真机演示时有人不小心碰一下也不会当场扑倒。

但域随机化的强度要掌握分寸。范围拉得太大,策略为了在所有参数下都活下来,会输出极度保守的动作,走路速度变慢、姿态僵硬。我会先跑一次小范围随机化,评估速度跟踪是否还算准,再逐轮放大范围。

4.2 训练监控与评估指标

训练过程中只盯着总奖励曲线是不够的,奖励函数由多项组成,总奖励上升但各项内部可能互相掩盖。我的做法是用Weights & Biases记录每一项奖励的原始值,再加上几个独立于奖励的物理指标:

  • 成功完成率:整段episode没有摔倒的占比。
  • 平均前进速度:验证策略是否真的在走,而不是原地抖腿。
  • 速度跟踪RMSE:输出实际速度与指令速度的误差。
  • 步态对称性:左腿和右腿支撑时间的差异,差异过大说明策略在偷懒。
  • 坠落率:在固定时长内的跌倒次数。

训练参数方面,我在RTX 3090上用4096个并行环境,PPO每轮迭代约8000步,跑1000轮,总计800万步,大约四五个小时收敛。如果资源不够,降到1024个环境也能收敛但需要更长墙钟时间。保存训练快照时,一定要把配置文件和代码的commit号一起记下。

4.3 训练失败案例与调参对策

项目里遇到最典型的三个失败模式,值得单独说:

第一种,策略学会了“原地转圈”。鸭子在一个点打转不走直线,速度跟踪奖励照样拿到了很高的值,因为指令方向上的速度可能确实达标了,但yaw角速度很大。这个问题的根因是奖励函数里没有惩罚偏航旋转。对策是加一项负的yaw角速度平方惩罚,权重设为0.1,转圈立刻消失。

第二种,策略“站住不动”。学完训练之后发现鸭子从头到尾站在原地,不摔倒也不前进。原因是存活奖励的权重相对于速度跟踪太高了,原地站着就能拿到稳定回报,策略陷入了局部最优。对策是降低存活奖励、提高速度跟踪权重,同时把episode最大步数设短一点,迫使策略必须在有限步数内走出去。

第三种,训练后期数值不稳定,出现NaN。多数情况是奖励项的量级太大导致梯度爆炸,或者学习率过高。我处理过的一个案例是动作平滑惩罚权重调到了0.2,瞬时动作变化大时这项奖励能打到几十的量级,经过广义优势估计后大幅震荡。对策是把各奖励项统一做归一化或者压缩到合理量级,学习率从1e-4降到3e-5,瞬间稳定。

这些失败案例都不是算法层面的错误,而是强化学习工程里的“奖励黑客”问题。训练时多打印各个奖励项分解曲线,及时看到是哪一项在引导策略跑偏,比训练完之后懊恼再重来要高效得多。

5. 开源架构设计与代码组织

5.1 仓库结构:训练与部署分开后的边界

开源项目的灵魂是让其他人能复现。很多机器人开源仓库把训练代码和部署固件混在一起,结构混乱,别人拿到手根本不知道从哪看起。我的仓库分了六个顶层目录:

duckbot/ ├── sim/ # 仿真环境定义、URDF/MJCF资产、地面地形配置 ├── train/ # 强化学习训练脚本、奖励函数、训练配置 ├── deploy/ # 树莓派端策略推理、STM32固件、串口协议 ├── hardware/ # CAD模型STL、BOM表、接线图 ├── configs/ # 统一的YAML配置:机械参数、训练超参、域随机化范围 ├── scripts/ # ONNX导出、步态分析、可视化工具 └── docs/ # 设计方案文档、踩坑记录、决策记录

训练代码和部署代码分离是我最坚持的一点:训练侧依赖GPU和PyTorch的生态,部署侧依赖ONNX Runtime和嵌入式工具链,两者混合在一起只会让CI和依赖管理崩溃。它们通过两个稳定的“契约”对接:一个是我导出的ONNX策略文件,一个是configs目录下的观测向量顺序定义。

5.2 训练与推理的接口设计

接口设计是整个系统里容易被伪造但至关重要的部分。策略输入必须严格固定顺序,我把它写成了一个yaml文件,训练代码和部署代码都从这个文件生成,杜绝“仿真里一个顺序、真机上另一个顺序”这种灾难。

观测向量定义如下:

序号内容维度
0-5当前关节位置(弧度,归一化到±1)6
6-11当前关节速度(弧度每秒,归一化)6
12-17上一时刻动作(归一化)6
18-20IMU roll/pitch/yaw(弧度)3
21-23陀螺仪角速度(弧度每秒)3
24速度指令(米每秒)1

动作定义直接决定部署难度。我用的动作是“目标关节位置的增量”,范围在-1到1之间,推理时乘以最大步长0.2弧度,然后加到期盼位置上,再交给PD控制器。这种增量动作比直接输出绝对角度更平滑,而且天然兼容低通滤波。

部署侧导出模型用PyTorch自带的方式:

import torch policy.eval() dummy_obs = torch.randn(1, 25) torch.onnx.export( policy, dummy_obs, "duck_policy.onnx", input_names=["obs"], output_names=["action"], dynamic_axes={"obs": {0: "batch"}, "action": {0: "batch"}} )

树莓派上加载ONNX后,用ONNX Runtime跑IO绑定推理,单次推理在Zero 2 W上大概耗时2到4毫秒,远够50Hz的控制周期。注意要固定batch大小为1,不要在推理时动态开新batch,那会增加不可控延迟。

5.3 配置管理与实验追踪

强化学习实验的复现难点在于配置散落各处。我用Hydra来管理所有YAML配置,一个命令启动训练,就会把整套配置和代码版本号记录进日志目录:

python train/train.py --config-name duck_rl \ algorithm.learning_rate=3e-5 \ domain_randomization.friction_min=0.3 \ domain_randomization.friction_max=1.2

训练结束时,自动把最终策略的ONNX导出到带时间戳的目录,并连同配置备份一份。这样每次调完参数,都能准确追溯到“是哪组配置生成了哪个策略文件”,不会出现训练了一周却不知道最后能跑的模型对应哪个版本。开源的仓库里还应该包含一个“失败实验记录”文档,把这篇文章第4.3节说的那些翻车案例都写进去,对后来者比代码更有价值。

6. 仿真到真机迁移的实战记录

6.1 真机部署链路:从策略输出到底层伺服

真机部署链路分六步:

  1. STM32以200Hz读取ICM-20948,运行互补滤波估计roll和pitch。
  2. STM32把姿态和舵机位置打包成UART帧,每5毫秒发给树莓派。
  3. 树莓派在50Hz周期内,把最新观测拼成25维向量,输入ONNX策略。
  4. 策略输出的动作经过一阶低通滤波,时间常数0.15秒,再乘0.2弧度步长,得到关节目标位置。
  5. UART回传树莓派的指令给STM32。
  6. STM32运行500Hz的PD控制器,把舵机收敛到目标角度。

这里有个细节:树莓派上的策略推理是50Hz,但IMU状态是200Hz,二者之间不同步。我在树莓派里用最近一帧状态,不做过多的插值,因为插值延迟反而比直接使用更糟。实测下来50Hz动作更新加上500Hz PD,鸭子走路已经足够顺滑。

通信协议必须带看门狗机制。UART帧格式固定为帧头、序号、6个关节目标、CRC16校验。STM32如果在200毫秒内没有收到有效帧,会立刻进入安全模式——把所有舵机目标设置为当前角度并加上保持力矩,防止鸭子突然跪倒摔坏结构。这个设计在一次树莓派过热卡死意外里救了我的机器。

6.2 我在迁移过程中踩过的坑

仿真到真机,我把所有踩过的坑按“现象、根因、对策”整理成表,这是项目里含金量最高的部分:

现象根因对策
机器启动后整体歪向一侧舵机零点与仿真关节零位不一致专门写标定程序,逐舵机记录零点,写入config
走几十秒后步态越来越慢电池电压跌落,舵机扭矩不足低电量检测,低于7.0V停止行走;用满电电池测评
小步幅动作响应迟钝舵机齿轮死区和回差动作平滑惩罚、加大PD积分项、或换总线数字舵机开反馈
高频颤抖,关节嗡嗡响策略输出本来就有微小抖动,PD放大动作低通滤波,降低PD的微分项增益
在地毯上打滑或绊倒地毯摩擦与仿真rafa范围不匹配扩大摩擦随机范围,脚底贴硅胶防滑垫

其中“零点不一致”是最坑的。仿真里我定义关节角度0代表腿直立,但实际舵机安装时不可能分毫不差地保证中位就是直立位。我的标定程序做法是:把每个舵机接到最小角度,然后逐步增加,同时用IMU的roll/pitch反馈确认大腿垂直程度,最终把三个角度读回值记录为一个offset数组。这套流程每换一次结构件都要重新做。

另一个坑是IMU的相位滞后。训练时我直接给策略roll/pitch真值,但真机上互补滤波输出的roll/pitch有十几毫秒的滞后,高频步态里这个滞后会让策略误判姿态,越想修正越抖。对策有两个:一是把训练时的观测设置成带延迟的roll/pitch,模拟闭环滞后;二是部署时提高IMU输出频率,并且在滤波里让陀螺仪积分占更高权重,换取更低滞后。

6.3 步行实测表现与后续优化方向

完成迁移之后,我做了几组测量。鸭子身高约25厘米,体重约800克,在平整瓷砖地面上稳定前进速度大约0.18到0.25米每秒,速度指令跟踪误差在±0.03米每秒以内。原地转弯和后退没有做,是当前版本的功能边界。续航方面,800毫安时2S电池连续行走大约15到20分钟,舵机温度在连续行走10分钟后稳定在45度左右,没有过热。

负载测试里最惊喜的是抗推能力:在站立状态下从侧面轻轻推一下,策略能主动迈步调整,两三步内恢复平衡。这个能力不是我预设的,是训练时随机横向推力逼出来的泛化行为。

后续我打算做三件事:第一,给鸭子装一个前视摄像头,策略输入加入感知信息,实现视觉引导的直线前进和小障碍跨越,这需要把观测向量扩展到感知维度。第二,把真机上采集的轨迹整理成离线数据集,用离线强化学习(比如IQL)做一次策略微调,让策略更贴合这台机器的真实动力学,这是当前足式机器人社区很值得探索的路线。第三,尝试模型基强化学习,让策略在学习过程中内部建模动力学,减少对大规模采样的依赖,这个方向对计算资源受限的小团队更有吸引力。

我个人最深的体会是:强化学习在足式机器人上的价值不在算法本身的炫酷,而在于它把“未知动力学下生成稳定行为”这个难题,变成了“如何设计好的仿真环境、奖励和迁移策略”的工程问题。仿真训练只占精力的三成,硬件标定、接口对齐、真机排障反而占了七成。如果你也在做类似的东西,记住一句我反复念叨的话:仿真里跑得再好看,都不如真机多走一秒。早点让策略上真机,哪怕数据难看,它告诉你问题的速度也远快于任何仿真调试。

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

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

立即咨询