上周把这个项目推到GitHub的时候,顺手给鸭子录了一段完整的走路视频。一只巴掌大的微小型双足鸭形机器人,没有遥控器、没有预先编排的动作,在桌面上摇摇晃晃地往前走——所有平衡和步态,都来自一套在仿真环境里用强化学习训练出来的策略网络。训练在MuJoCo里完成,最终跑在ESP32上一段不到10KB的C语言推理代码里。这个项目把硬件结构、电气拓扑、训练脚本、部署固件全部开源了,所以这篇复盘写得尽量完整:我会从机械选型讲到奖励函数设计,再讲到Sim-to-Real过程中那些让人头大的问题。如果你是想入门机器人加深度强化学习,或者手头正好有个小舵机平台不知道怎么让它站稳,这篇应该能给你一条能直接照做的路径。
1. 项目缘起:为什么非要做一只会走路的鸭子
1.1 双足平衡是强化学习最好的试验田
很多朋友一开始不理解,问为什么要做一个鸭子形状的双足机器人,而不是四足或者轮式。我的理由其实很简单:双足平衡问题在强化学习里就是一个经典的连续控制benchmark,自由度少、动作空间小,但具备了完整的"感知—决策—控制"闭环。相比人形机器人,微小型双足的硬件成本低、迭代快,一个晚上就能打样新结构,非常适合拿来验证算法。
另一个原因是鸭子的身体结构本身就有优势。真实鸭子的重心低、躯干宽,走路时左右摇摆,这在工程上意味着被动稳定性更好。我们的鸭子机身内部空间足够放主控、IMU和电池,外部又保留了明显的头颈和尾羽轮廓,视觉上天然讨喜,拿去演示和上课都很好用。
1.2 先定技术指标,再谈架构
项目启动前,我先给自己定了几个硬指标,后续所有选型都围绕这些数字来:
| 项目 | 目标参数 |
|---|---|
| 整机重量 | 500g以内 |
| 站立高度 | 约18cm |
| 自由度 | 6(每条腿3个) |
| 控制频率 | 策略50Hz,底层PD 200Hz |
| 主控 | ESP32-S3双核240MHz |
| 续航 | 2S 300mAh电池,约15分钟 |
微小型平台最难受的地方在于,所有东西都要在重量和空间里取舍。每多一个舵机,就多十几克负重,仿真里的动力学参数也会跟着变。所以我一开始就把自由度压缩到了极限:每条腿只有髋关节偏航、髋关节俯仰和膝关节俯仰三个舵机。踝关节没有主动驱动,用脚掌形状和被动柔顺来凑。
1.3 这个项目不是论文,是一条完整的参考线
我在做之前翻了不少开源项目,发现一个普遍问题:很多仓库要么只有仿真训练代码,要么只有硬件图纸,很少有人把"仿真训练—模型导出—MCU推理—实机调试"整条链路串起来。这个鸭子项目从一开始就定位成一个端到端的参考实现,每部分都能单独复用。对想入门强化学习的人来说,你不需要先看懂全栈,按仓库里的Quickstart跑一遍,就能看到一只鸭子在自己电脑上学会走路。
2. 硬件平台拆解:在巴掌大的空间里塞下一套运动系统
2.1 自由度配置:为什么每条腿只留3个舵机
很多人看到双足机器人第一反应是"自由度越多越高级",但在这个尺度上,自由度越多反而越难搞。每条腿3个舵机的布局是这样的:髋关节偏航管左右平衡,髋关节俯仰管前后摆动,膝关节俯仰管抬腿和步幅。三自由度腿在运动学上是冗余度最低的组合,每一步动作都直接对应一个明确的物理意图,这对奖励函数设计特别友好——你不需要额外惩罚关节乱动,因为根本就没几个关节可动。
这个选择背后还有一层考虑:仿真到实物的迁移。仿真里的关节默认是理想位置源,反馈完全准确;但实机舵机有死区、有回差、有响应延迟,自由度越多,这些非理想因素的叠加越严重。六自由度已经是我能在实机上调住的上限了。
2.2 舵机选型:9g金属齿舵机的扭矩余量
舵机是这个小鸭子最核心的执行器。我用的是一类典型的9g金属齿数字舵机,6V供电下峰值扭矩大约2.2 kg·cm,重量9g,响应速度约0.1s/60°。选它不是因为参数亮眼,而是在这个重量级别里可选择的金属齿方案本身就不多,塑料齿舵机在步态冲击下用不了几天就扫齿。
静态扭矩校核很重要。鸭子重心大约在脚掌上方8cm处,总质量500g,按单腿承重250g估算,静态保持扭矩大概是 0.25kg×9.8×0.08m ≈ 0.2 N·m,换算下来约2 kg·cm。也就是说,舵机静态余量只有10%左右。动态行走时惯性力通常要按2到3倍余量算,所以这个选型非常紧张。解决办法是两条腿并行:让两条腿交替承重,减少单舵机瞬时负载,同时尽量压低机体重心。
这个计算过程建议每个做小机器人的朋友都做一遍,我见过太多人装完才发现舵机扭矩不够,走路像喝醉了一样抖。舵机选型手册里的峰值扭矩是短时值,连续堵转根本扛不住,余量一定要留足。
2.3 主控、IMU与电气拓扑
主控选用ESP32-S3,原因很直接:双核240MHz跑一个两层256节点的MLP推理只需要几毫秒,BLE和WiFi还能把实机状态实时发回电脑,调试时能看到IMU数据和策略输出,省了反复插线烧录的麻烦。
IMU用的MPU6050,挂在I2C总线上。IMU驱动层我用200Hz读取原始数据,然后做陀螺仪零偏校准和加速度计低通滤波,最后把姿态投影到重力方向作为策略网络的一部分输入。这里有个必须强调的细节:IMU的安装坐标系必须和仿真里的机体坐标系严格对齐,否则策略在实机上会得到方向和仿真不一致的观测,表现会非常诡异。
说到电气拓扑,很多刚做机器人的朋友不太重视,觉得把线接上就行了,实际上这一块能决定项目的生死。我的供电拓扑是:
| 电源域 | 方案 |
|---|---|
| 动力域 | 2S锂电7.4V直供舵机 |
| 逻辑域 | 5V buck降压给ESP32、IMU和PWM驱动板 |
| 信号域 | PWM信号线,与动力线分离走线 |
关键点在于舵机堵转瞬间电流非常大,6个舵机同时动作时瞬态电流能到3A以上,如果逻辑域和动力域共地不稳,舵机一抖,主控就重启。我在舵机供电轨上并了470uF电解电容和100nF陶瓷电容,动力线用22AWG硅胶线,逻辑地单独拉一条星形地线回电池负极,从此再没出现过复位问题。
2.4 结构材料与装配细节
机身和腿是用PETG打印的,腿部零件全部用100%填充,因为这个位置要承受步态冲击。髋关节和膝关节处我嵌入了微型滚珠轴承,这可能是整个机械部分最值得的投资:仿真里的关节是零摩擦零回差的理想旋转副,实机如果直接用螺丝穿打印件,关节摩擦和磨损会让策略输出和实际动作完全对不上。加了轴承之后,关节摩擦大幅降低,Sim-to-Real的差距明显缩小。
还有一个小细节:鸭子尾巴不是摆设。我把电源开关放在了尾巴末端,顺手把线缆整理进躯干空腔,这样调试时不需要掀开外壳。头部留了空心结构,里面可以塞配重块来调节重心,方便做域随机化里的质量偏移实验。
3. 强化学习训练系统的搭建:算法、仿真与奖励设计
3.1 为什么是PPO,以及怎么理解它在干的事
我在项目里用的算法是PPO,Clipped Proximal Policy Optimization。这几乎是当前连续控制任务里最稳的默认选择。简单理解,PPO的核心思路是让策略每次更新别迈太大步子,通过裁剪目标函数限制新旧策略的差异,这样训练不容易崩。配合GAE做优势估计,加上一个价值网络评估状态好坏,整体框架成熟,调参空间大,踩坑资料也多。
入门的朋友如果对公式不熟,我强烈建议先看David Silver的强化学习课和Sutton那本《Reinforcement Learning: An Introduction》的前几章,把"策略—价值—贝尔曼方程"这几个概念捋清楚,再回来跑代码,效率会高很多。只看代码不看原理,遇到训练不收敛会完全不知道怎么排查。
3.2 仿真环境:MuJoCo、Isaac Gym还是Gazebo
仿真环境我最终用了MuJoCo,配合Gymnasium接口。选它的理由很简单:轻量、精确、跨平台。如果你只有CPU,MuJoCo的向量化环境也能跑出不错的吞吐量,一个粗糙的步态在本地几十分钟就能看到雏形。有NVIDIA显卡的话可以上Isaac Gym或Isaac Lab,开4096个并行环境,训练速度能快一个数量级。
Gazebo也是一个选项,尤其当你需要和ROS生态结合的时候。但Gazebo跑强化学习有个天生的劣势:渲染和物理引擎重,并行度低,迭代速度慢。我是把Gazebo留给部署前做整机仿真验证用的,日常训策略还是MuJoCo为主。
仿真里最关键的建模不是外形,而是执行器模型。我在实机上给舵机扫了阶跃响应,拟合出一个带延迟和死区的模型:延迟大约10ms,死区约0.5度,然后把这个模型写进仿真。这一步太重要了,如果仿真里的舵机是理想位置源,训练出来的策略到实机上会疯狂振荡,因为实机舵机根本不是那样的响应特性。
3.3 奖励函数设计:分层权重与反奖励欺骗
奖励函数我按优先级分了三层:任务层、姿态层、约束层。任务层只有一项,让鸭子跟踪目标速度;姿态层奖励保持躯干直立;约束层惩罚动作变化率和扭矩消耗。具体权重如下:
| 奖励项 | 公式示意 | 权重 |
|---|---|---|
| 速度跟踪 | exp(-2*误差^2) | 1.0 |
| 直立姿态 | 基于躯干倾角的exp项 | 0.5 |
| 动作平滑 | 对相邻动作差的惩罚 | 0.01 |
| 扭矩消耗 | 对关节扭矩平方的惩罚 | 0.0001 |
这里我要重点说"奖励欺骗"的问题。第一次训练时,鸭子学会了往前一扑,利用躯干触地反弹来获得前进速度,仿真里不会摔倒,但实机上一扑就翻车。解决办法是在终止条件里加入"躯干接触地面"判定,一旦躯干触地立即终止回合,同时给一个较高的存活奖励,逼着策略保持稳定站立。
另一个常见的坑是:如果终止条件和时间惩罚设计不当,策略会发现"主动摔倒结束回合"比"继续站下去挨罚"更划算。我的原则是,终止条件只用来终止物理上不合理的状态,所有惩罚项都尽量用连续惩罚而不是硬截断。
3.4 训练稳定化的实用设置
训练过程我用了几个很实用的技巧,这里直接贴出来:
- 观测归一化:所有观测维度用运行均值和方法归一化到零均值单位方差,能显著加速收敛。
- 奖励归一化:训练前期奖励尺度波动大,归一化之后价值网络好训练很多。
- 命令课程学习:目标速度从0.2m/s开始,等鸭子站稳了再逐步提高,避免一上来就要求跑太快导致策略只学到摔倒。
- 学习率调度:PPO的学习率从5e-4开始,训练中期衰减到1e-4,收敛更干净。
观测空间大概是这样的:重力在机体坐标系的投影、机体角速度、6个关节位置、6个关节速度、上次动作、目标速度命令。动作空间就是6个关节的目标位置,策略网络输出后交给底层PD控制器去执行。
训练到reward曲线平稳之后,我会冻结checkpoint,然后在固定随机种子下跑50局部署评估,统计成功率。这个部署评估分数才是真正决定用哪个模型的依据,而不是训练reward,因为训练reward可能被奖励欺骗污染过。
4. Sim-to-Real:从仿真鸭子到真能走路的鸭子
4.1 域随机化:让策略"皮实"起来
Sim-to-Real是整个项目里最考验工程判断的一段。我的核心手段是域随机化,让策略在各种各样的环境参数下训练,这样它学到的步态不依赖某一组特定参数,实机遇到偏差也能扛住。我随机化的参数和范围如下:
| 参数 | 随机范围 |
|---|---|
| 各部件质量 | ±20% |
| 关节摩擦 | ±30% |
| 地面摩擦系数 | 0.3~1.2 |
| 舵机扭矩 | ±15% |
| 控制延迟 | 0~20ms |
| IMU噪声 | 额外高斯噪声 |
光随机还不够,我开始时犯过一个错误:随机范围太大,导致策略学到的是"什么都不敢做"的保守行为,仿真里能走但很慢。后来把随机范围逐步从0开始增大,找到一个"练得动又不至于过度保守"的区间,实机效果明显好转。
4.2 策略部署:从PyTorch到ESP32上的C语言推理
训练用的是PyTorch,部署到ESP32上不能用Python,流程是:先把PyTorch模型导出为ONNX,再转成C语言权重数组,在固件里手写一个矩阵乘法加激活函数的前向推理。模型很简单,两层MLP加ReLU,每层256个节点,转出来的权重数组不到10KB,浮点推理一次不到2ms,跑50Hz控制频率完全没压力。
控制循环的结构是:ESP32以200Hz读取IMU和关节数据,做姿态解算和滤波,然后用一个定时器在50Hz触发策略推理,得到6个目标关节角。底层再以200Hz跑一个PD控制器把关节逼近目标角度。策略输出和PD之间我加了一层指数滑动平均,平滑系数0.7。这层平滑极其重要,直接滤掉了策略输出里的高频抖动,让实机动作干净很多。
4.3 实物调试里逃不掉的问题
实物调试阶段几乎把能踩的坑都踩了一遍,先列几个最常见的:
- 电池电压对舵机速度的影响。2S锂电从满电8.4V掉到7.2V的过程中,舵机响应速度明显变慢,同一个策略在满电时走得好好的,低电量时就开始踉跄。我临时用电压测量值作为策略观测之一,效果有改善,但根治还是得靠更充沛的电流供应。
- 舵机死区导致的静态误差。PID里的积分项可以解决一部分,但积分一大就容易振荡,我的做法是加一个带死区的积分,只在误差超过死区阈值时才积分。
- IMU安装零偏。每次开机做一次陀螺仪零偏校准,几十秒的静态采样取平均值,不要省这一步。
5. 开源架构复盘:仓库怎么组织才有人愿意复现
5.1 仓库目录设计与模块划分
这个开源项目我希望做到"别人照着README能完整复现",仓库目录是这样划分的:
duckbot/ ├── hardware/ # 3D模型、PCB、BOM清单 ├── sim/ # MuJoCo仿真环境定义 ├── train/ # PPO训练脚本、奖励参数配置 ├── deploy/ # ESP32固件、模型导出工具 ├── docs/ # 装配说明、调试笔记 └── configs/ # 所有YAML配置把硬件、仿真、训练、部署拆成四个独立模块,依赖关系是单向的:硬件不依赖任何代码,仿真依赖硬件参数,训练依赖仿真接口,部署依赖训练产物。这样一来,只看其中一个模块的人不需要理解其他模块的细节。
5.2 训练-评估-部署的数据闭环
开源仓库里最重要的不是代码本身,而是让代码跑起来能得到和作者一样的结果。所以我把训练日志、模型权重、评估脚本全部绑在一起。训练时用WandB记录奖励曲线、价值损失、熵系数这些指标,并自动保存最优checkpoint。模型导出脚本会在每次评估通过后自动把PyTorch权重转成C数组,并生成对应的头文件,固件编译时直接引用。
所有配置用YAML管理,一个配置里同时定义仿真参数、奖励权重、训练超参数和随机化范围。改实验只需要改配置文件,不需要动代码,这对复现者非常友好。版本锁死也很重要,requirements.txt里固定所有依赖版本,连MuJoCo的小版本号都钉死。
5.3 给复现者的精简路线
考虑到不是所有人都想从零搭一遍硬件,我整理了三条复现路径:
- 纯仿真路线:装好依赖,跑train脚本,在MuJoCo里看到鸭子学会走路,全程不需要硬件,适合强化学习入门。
- 训练加部署路线:训练完成后导出模型,烧进ESP32,用现成硬件套件跑真机。
- 完整自制路线:按硬件目录打样结构件,买齐BOM清单里的零件,从装配到调试全程走一遍。
这三条路线在README里有各自的标签,按需选择。我实测过,纯仿真路线在一台普通CPU笔记本上大概半小时能看到雏形步态,GPU机器上更快。
6. 踩坑实录与排错方法
6.1 训练不收敛:一套标准排查流程
第一个版本训练了整整一个晚上,reward曲线纹丝不动,鸭子永远趴在地上。那段时间我养成了一个习惯:遇到不收敛,先按固定顺序排查,不要乱改参数。
我的排查顺序是:先看观测归一化是否生效,再看奖励尺度是否差太多,接着检查终止条件是否过于密集,最后查动作空间是否超出了物理限制。这个项目最后定位到的问题很有意思:动作空间的scale设大了,策略一上来就输出超过舵机物理极限的角度,仿真里的鸭子疯狂抽筋,终止条件频繁触发,采集到的全都是垃圾轨迹。把动作scale缩到合理范围后,十几个小时不动的reward曲线立刻开始上升。
6.2 实机抖动、发热与续航
实机调试的第一天,鸭子站在原地颤抖,像帕金森病人一样。我排查了一圈,原因是PD控制器增益太高,加上策略输出直接作用于关节,形成了自激振荡。解决方法是把PD增益降下来,给策略输出做平滑滤波,同时降低控制频率到50Hz——更高频率对普通舵机没有意义,因为舵机本身的响应带宽根本跟不上。
舵机发热是另一个真问题。鸭子站立时六个舵机同时保持负载,电流持续消耗,金属外壳烫手。后来我在站立逻辑里加入了一个很实用的设计:当目标速度为零时,放宽死区范围,让舵机在允许范围内"放松",而不是死死顶住,发热立刻下降,续航也从10分钟提升到了15分钟。
6.3 一次典型的Sim-to-Real失败复盘
最难忘的一次失败是:训练好的策略在仿真里完美走路,上实机后鸭子一直往左后方转圈。我花了半天时间检查代码,最后发现在仿真里定义机体坐标系时,Z轴和实机IMU的Z轴方向相反。重力投影观测完全对不上,策略自然不知道哪里是"下",它学到的平衡行为在实机上全乱了。
这个例子值得记住:Sim-to-Real的第一步永远不是调参,而是校准坐标系。我后来在仿真里加了一个调试脚本,输入一个已知姿态,打印策略的观测值,再和实机日志对比,五分钟就能发现坐标系问题。这个工具我已经写进了开源仓库的tools目录,强烈建议每个做机器人的朋友都备一个。
还有一次失败是鸭子学会了用尾巴支撑身体。仿真里尾巴和地面的摩擦系数被我设高了,策略发现撑尾巴比控制双腿简单,走出来的步态是拖着尾巴滑行的。实机尾巴摩擦力没那么大,一上来就摔。后来我在终止条件里加入了尾巴接触地面的判定,同时在域随机化里把尾巴摩擦系数独立随机,这个作弊行为才被压制住。
7. 下一步:从鸭子到更多形态的探索
项目现在稳定能走了,但我没有停在原地。我现在在尝试两件事:一是把离线强化学习(比如IQL)加进来,把之前训练过程中积累的大批量经验数据利用起来,这样新版本策略可以不用重新训练太久;二是在尝试基于模型的强化学习,让策略对自身动力学有更明确的建模,也许能进一步缩小Sim-to-Real差距。
因果强化学习(CRL)也是我在关注的方向。它的核心思路是把因果推断工具嵌入强化学习流程,让智能体不只学"怎么做"的关联,还学"为什么有效"的因果机制。对鸭子这种小型机器人来说,如果策略能识别出哪些观测维度真正影响平衡,那么在硬件参数变化时,它的适应能力应该会更强,泛化性也会更好。
至于形态扩展,这套架构完全可以平移到一个微小型四足或者带翅膀的扑翼机器人上。我一开始选择鸭子,就是因为它把"非人形"的仿生形象和标准的双足控制问题结合在了一起,外壳只是表达方式,内核的逻辑是通用的。任何做小机器人实验的人,都应该在自己感兴趣的物理形态上把这套闭环跑一遍,那种在仿真和实机之间反复迭代的经验,是看再多教程都换不来的。
最后再分享一个干活的小习惯:每次改完仿真的物理参数,我都会先跑一次"实机对标"实验——把仿真里的鸭子放在和实机一样的初始状态下,对比一条10秒内的IMU轨迹。两者偏差越小,说明仿真可信度越高,后续训练出来的策略上实机成功的概率也就越大。这个习惯帮我省下的调试时间,比任何高级算法都多。