做一只会走路的鸭子,听起来像是个玩具项目,但真正把强化学习驱动的双足步态跑在一台巴掌大的平台上,要过的坎比实验室里大多数人想象的要多。这个项目最初只是我在走廊上的一句玩笑:“四足满地跑,人形满街走,为什么没人做一只小鸭子?”后来这句话变成了一套可跑、可看、可复现的开源平台——一台整机重量不到400克的微小型双足鸭形机器人,关节动作由深度强化学习训练出来的策略网络驱动,训练和部署代码全部开源。
这篇文章会把项目拆开讲清楚:硬件为什么这样选型,仿真训练链路怎么搭,开源仓库里的模块如何组织,以及我在训练和真机迁移过程中踩到的一系列坑。适合谁看?如果你正打算低成本验证双足步态算法,或者想在机器人强化学习方向找一个完整的上手案例,这篇内容应该能帮你省下不少时间。就算你暂时不打算动手做硬件,只看训练和架构部分,也能有不少收获。
1. 为什么是一只会走路的鸭子:项目缘起与设计目标
1.1 从一句玩笑到一个完整平台
当时我所在的实验室主要做四足步态控制,平台体积大、调试周期长。虽然四足和双足有很多可迁移的结论,但四足本身的静稳定性太好,很多步态规律其实是被“掩盖”的——你不需要真正解决好平衡问题,四条腿自然分摊了一切。我想找一个能把平衡问题逼出来的小平台,而双足恰恰是最合适的任务难度。
一次偶然的机会,我拿到一只玩具小鸭,底下一对宽扁的脚掌,重心低,走路自带左右摇摆。我试着手动给它调了一套正弦步态,发现它能走,但抗扰动能力很差,稍微碰一下就是一个大趔趄。这时候我意识到,这个形态非常适合拿来验证强化学习步态算法:动作维数不高,但动力学非线性很足,奖励函数也可以做得很有意思。
于是我给项目定了三个硬目标:第一,整机重量控制在400克以内,站立高度在25厘米左右;第二,强化学习训练出来的策略要能直接迁移到真机,不允许在真机上重新调一整天PD参数;第三,训练和部署代码全部开源,让拿到硬件的人能反推整个方案。这三个目标后来都实现了,只是中间的过程远比预想曲折。
1.2 技术指标与设计底线
项目开始前我把技术指标列成了一张表,后面所有硬件选型都围绕这张表展开:
| 指标项 | 目标值 | 备注 |
|---|---|---|
| 整机重量 | ≤400g | 含电池、主控、线路 |
| 站立高度 | 约25cm | 便于桌面和走廊测试 |
| 单腿自由度 | 3个 | 髋外摆、髋俯仰、膝俯仰 |
| 峰值关节扭矩 | ≥0.6 N·m | 考虑动态裕量后反推 |
| 最大行走速度 | 0.15 m/s | 鸭子步态的合理速度 |
| 连续行走时间 | ≥15分钟 | 锂电池容量约束 |
| 训练算法 | PPO | 近端策略优化 |
| 仿真器 | MuJoCo / Isaac Gym | 都跑过,最终用MuJoCo出真机 |
这张表里最容易被新手忽略的是“峰值关节扭矩”这一项。很多人在微型机器人上装的是玩具舵机,静态能站住,但强化学习训练出来的策略在起步、转向、被扰动恢复时,关节力矩峰值往往是静态的数倍。如果电机没有足够的扭矩裕量,策略在仿真里学得再好,真机上也只会原地抖动。
1.3 为什么不是人形,也不是四足
这个问题经常被问。人形机器人确实更有视觉冲击力,但微型人形平台的腿长、质心高度、关节惯量参数很难留出足够的设计裕量,稍有误差就变成“站都站不稳”,更别说训练步态。四足则走到另一个极端,静稳定结构天然就不需要解决太多平衡问题,验证强化学习算法的“惊险感”少了很多。
鸭子形态正好卡在中间:双足必须持续处理动态平衡,但腿短、重心低、脚掌面积大,又在机械上给了算法足够多的容错空间。而鸭子行走时那种左右摇摆的姿态,本质上是一种周期性失稳后的受控恢复动作——当你把任务定义为“摇摆着前进并且不摔倒”,它就变成了一个比“站直走”更有区分度的强化学习问题。我后来在奖励函数里专门为这种步态风格设计了奖励项,这是后话。
2. 硬件底子:微型双足鸭的机械与电子设计
2.1 腿部关节配置:三自由度原则
每条腿我用了3个自由度:髋关节外摆/内收(hip yaw)、髋关节前摆/后摆(hip pitch)、膝关节俯仰(knee pitch),两条腿共6个执行器。鸭子的踝关节被省略了,取而代之的是一对TPU打印的宽扁脚掌,中间夹一层硅胶垫,相当于一个廉价的被动弹性踝关节。
这样设计的理由是:踝关节在微型平台上重量占比太高,而且需要额外两个电机,控制维数翻倍后,强化学习策略的收敛难度会明显上升。被动弹性脚掌的好处在于,它能为策略提供一部分“免费”的缓冲和地面自适应能力,代价是脚掌的形变会带来建模误差——这个误差我选择在仿真里做域随机化来处理。
大腿和小腿的比例也做了一些特殊调整。真实鸭子是“大腿短、小腿长、脚蹼外翻”的结构,我参照了这个比例,让大腿略微前倾、小腿自然下垂、脚掌向外偏一个角度。这种形态在走路时天然会产生横向摆动,视觉上更像鸭子,同时也让hip yaw这个关节在步态中真正承担起重心转移的任务,而不是一个摆设。
2.2 电机选型:扭矩估算与减速比选择
微型双足机器人最怕两件事:电机扭矩不够,以及齿轮箱回差太大。我先做了个粗略的静力估算:
- 整机质量按0.35 kg算,重力约3.43 N;
- 静态单腿承重大概一半,也就是1.7 N左右;
- 髋关节到身体质心的力臂约为0.06 m,静态力矩约0.1 N·m;
- 双足动态步态的峰值力矩一般是静态的2到3倍,按3倍估算就是0.3 N·m;
- 再留2倍安全裕量,目标峰值扭矩定为0.6 N·m以上。
在电机方案上我对比了三类方案:串行总线舵机、微型无刷电机加行星减速箱、传统小功率直驱电机。串行总线舵机的优势是集成度高,自带减速箱、编码器和驱动板,开发周期短;缺点是齿轮箱回差偏大,扭矩响应存在明显非线性。微型无刷加行星减速箱的力量和响应更好,但要自己画FOC驱动板,还要解决编码器安装、散热和机械加工精度的问题,成本翻倍。
最终我选择了串行总线舵机方案。代价是训练时要额外对执行器建模:不能把关节当成理想的“目标角度即位置”模型,要加入最大角速度限制、响应延迟和回差噪声。这些建模工作放到强化学习环境里来做,比在真机上跟电机脾气较劲要省时间得多。
2.3 传感器、主控与通信架构
传感器方案比较常规:一个六轴IMU(加速度计加陀螺仪)放在躯干中心,每条腿的关节编码器读取角度和角速度,脚掌底部贴了两片薄膜压力传感器,主要用来获取足底接触状态。接触状态我不直接进策略,但会用在奖励函数和真机的状态机判断里。
主控采用双处理器方案:底层用一块STM32单片机负责电机控制回路、传感器采集和串口通信,上层用树莓派Zero 2 W跑策略网络推理。两层之间用USB串口连接,频率做到100Hz。选树莓派而不是直接在STM32上跑网络,是因为策略网络里有卷积和全连接层,在单片机上优化推理效率很痛苦,而树莓派上可以直接用PyTorch导出的ONNX模型,部署成本低很多。
通信上,真机和PC之间走无线串口模块,训练数据通过Wi-Fi回传;如果是在场地里做真机评估,我会同时架一个小型动作捕捉系统,用外部位置数据来校准机载IMU的积分漂移。这个“外部设备只做评估、不参与推理”的原则,保证了策略在脱离实验室环境后依然能独立运行。
2.4 装配时最容易忽略的惯量匹配
这个项目最深刻的硬件经验之一,是“重心位置比电机好坏更影响步态质量”。最初一版样机,我把电池和主控都放在了鸭子的背部,视觉上和真实鸭子更像,结果训练好的策略拿到真机上,稍微走几步就开始剧烈点头。后来分析发现,电池抬高导致整机质心远离髋关节轴线,转动惯量变大,被动脚掌那点缓冲根本压不住。
解决办法很简单也很土:把电池、主控和大部分线缆全部塞到靠近脚底的“鸭肚子”位置。机头、鸭翅膀、尾巴都是轻质打印外壳,不承担任何电子元件。调整之后,整机质心落在髋关节轴线下方的支撑多边形正中间,策略迁移一下子就顺畅了。这个经验后来被我写进了项目文档——一个能立住的硬件,能让强化学习省下至少三分之一的时间。
3. 强化学习怎么让鸭子学会走路
3.1 仿真建模的取舍:不是越精细越好
很多第一次做机器人强化学习的人,容易陷入“仿真模型必须跟CAD一致”的执念。机械结构当然是越接近越好,但仿真里真正决定步态质量的是接触动力学和惯性参数,而不是外壳长什么样。
我的做法是:鸭子主体只用几个box和sphere拼出来,腿用圆柱加球形关节表达,脚掌用扁椭球加少量几何细节。重点标定三类参数:质量、质心位置、转动惯量。质量可以拆零件用电子秤逐个称;质心用吊线法粗测;转动惯量直接从CAD估算。脚掌的摩擦系数和弹性阻尼则在后续域随机化里处理。
MuJoCo的MJCF格式写起来很直观,核心建模文件大致长这样:
<mujoco model="mini_duck"> <option timestep="0.002" iterations="50" tolerance="1e-10"/> <worldbody> <body name="torso" pos="0 0 0.12"> <inertial pos="0 -0.01 -0.02" mass="0.26" diaginertia="1e-4 2e-4 2e-4"/> <joint name="hip_yaw" type="hinge" axis="0 0 1" range="-30 30"/> <body name="thigh" pos="0 0 -0.04"> <inertial pos="0 0 -0.02" mass="0.03" diaginertia="1e-5 1e-5 3e-5"/> <joint name="hip_pitch" type="hinge" axis="0 1 0" range="-120 120"/> <body name="shin" pos="0 0 -0.04"> <inertial pos="0 0 -0.02" mass="0.02" diaginertia="1e-5 1e-5 2e-5"/> <joint name="knee_pitch" type="hinge" axis="0 1 0" range="-150 0"/> <geom name="foot" type="ellipsoid" size="0.035 0.02 0.005" pos="0 0 -0.02"/> </body> </body> </body> </worldbody> </mujoco>仿真步长设在2ms,控制频率则是50Hz,也就是每20ms从策略输出一次动作。大量并行环境并行跑,4096个环境同时rollout,一个下午就能看到初步的步态雏形。
3.2 状态空间与动作空间
策略网络的输入设计直接决定了任务好不好学。我给鸭子设计的观测向量包括:
- 机体的三轴角速度(IMU数据),经过滤波和单位转换;
- 机体姿态的旋转矩阵或四元数,用于判断倾斜和翻转;
- 每只腿3个关节的角度和角速度;
- 上一个控制周期输出的动作,用来提供“肌肉记忆”;
- 足底接触标志位,1/0表示左脚或右脚是否触地;
- 期望前进速度和期望转向速度,在训练时用随机目标采样。
动作空间则设计为6维向量,每个关节输出一个目标角度。这里有一个小门道:采用“绝对位置”动作比“位置增量”动作在仿真到真机迁移时更稳定。增量模式的策略学得快,但真机上任意一步的建模误差都会被累积;绝对位置模式相当于让策略直接输出关节目标,PD控制器负责追踪,整个闭环对外部误差更鲁棒。
执行器模型用带限速和延迟的一阶PD来近似:
def step_torque(q_target, q_current, dq_current, kp, kd, vmax): q_cmd = np.clip(q_target, q_current - vmax * dt, q_current + vmax * dt) torque = kp * (q_cmd - q_current) - kd * dq_current return torque真机部署时,同一套PD参数直接复用,这是整个迁移链路里比较让我放心的一段代码。
3.3 奖励函数:从“会走”到“像鸭子一样走”
奖励函数是这个项目里最需要耐心打磨的部分。我把它拆成三个层次的奖励项:生存项、任务项、风格项。
生存项最简单也最暴力:只要机体没摔倒,每一帧给一个小正奖励;一旦躯干中心高度低于阈值或者倾斜角超过30度,立刻给负奖励并终止回合。这个机制让策略首先学会“不摔”。
任务项负责方向引导:
reward_vel = exp(-1.5 * ||v_target - v_forward_proj||) reward_heading = cos(yaw_error) reward_torque_penalty = -0.0001 * sum(tau**2) reward_penetration = -10 * max(0, contact_penetration)其中v_forward_proj是指机体前进方向上的线速度,yaw_error是朝向与目标方向的角度差。这些项一起推动鸭子能够稳定地朝指定方向移动。
风格项则是让它对得起“鸭子”这个名头。我额外加了一个髋外摆关节的相位激励:当摆动腿处于支撑相和后摆相时,允许并轻微鼓励hip yaw关节跟随一个正弦参考,产生左右摇摆的姿态;同时惩罚膝关节反曲和脚掌的滑动。加权后,策略在满足“往前走”的同时,自然涌现出接近真实鸭子的步态节奏。我特意没有用“强制指定步态相位”的方式,因为强模板会扼杀强化学习的探索空间,也会让策略在真机遇到扰动时变得僵化。
3.4 PPO训练与超参调节
算法选用了PPO,这不是因为它最时髦,而是它在机器人连续控制任务上足够稳定,对超参不太敏感,社区资料也多。实际训练用的超参如下:
| 超参数 | 数值 | 说明 |
|---|---|---|
| 并行环境数 | 4096 | 学习效率的关键 |
| PPO clip范围 | 0.2 | 常规配置 |
| 策略网络学习率 | 3e-4 | 线性衰减 |
| 价值网络学习率 | 1e-3 | 线性衰减 |
| 折扣因子 γ | 0.99 | 兼顾长期回报 |
| GAE λ | 0.95 | 优势估计 |
| 每个iteration的样本量 | 98304 | 4096环境×24步 |
| mini-batch大小 | 4096 | 稳定性好 |
| 训练轮数 | 5 | 平均每个样本使用5次 |
| 熵系数初始值 | 0.01 | 后期线性降到0.001 |
这个配置在单张消费级显卡上跑了不到4个小时,回报曲线从0.3附近升到0.7,步态从最初的“原地抽搐”逐渐变成“左右摇摆地向前挪”。最容易观察到的转折点是1小时左右,策略学会了交替迈步;2小时后,转弯和抗扰动能力开始成形;3小时后,几乎不再摔倒。
3.5 因果强化学习视角:用干预实验替代盲目调奖励
说一个可能稍微拔高一点的点。很多人调奖励函数是纯“对着曲线加惩罚”,这种方式既费时间又容易过拟合。我在这个项目里尝试把因果强化学习的思路带进训练诊断流程,效果出奇地好。
因果强化学习的核心机制,是把因果推断工具嵌入强化学习的观测和分析流程:不再只看总回报曲线,而是把任务拆成几个可干预的因果因子——比如“身体平衡因子”“前向速度因子”“步态相位因子”。训练到某一阶段后,我会在仿真里固定其他因子,单独干预一个因子来看策略响应。比如把机体的初始横滚角硬拉10度,观察策略是选择加大髋外摆来恢复,还是选择加快步频来稳住;再比如把期望速度从0.05突然跳到0.2,观察策略的前馈响应是否立即更新。
这套干预实验帮我找到了几次很隐蔽的训练异常:有一次总回报一直在涨,但干预横滚角后发现策略几乎不做主动恢复,全靠被动脚掌在地面的摩擦力撑着走——那其实说明策略只是在钻奖励函数的空子,并不是真的学会了平衡。后来我用因果图的方式给策略模块加了一个“姿态恢复必要性”的观测头,训练诊断就直观多了。这也是我在整个开源仓库里保留causal_diagnose.py这个工具的原因。
4. 开源架构拆解:仓库里到底有什么
4.1 目录结构与模块划分
我把整个项目整理成了一个叫duck_rl的开源仓库,目录结构大致是这样的:
duck_rl/ ├── README.md ├── environment/ │ ├── duck_env.py # 仿真训练环境(MuJoCo) │ ├── duck_robot.py # 真机部署适配层 │ └── config/ │ ├── train.yaml # 训练配置 │ └── deploy.yaml # 真机部署配置 ├── rl/ │ ├── ppo/ │ │ ├── agent.py # PPO智能体实现 │ │ └── buffer.py # rollout缓存 │ ├── reward/ │ │ └── duck_reward.py # 奖励函数实现与可视化 │ └── scheduler/ # 学习率、熵系数调度 ├── deploy/ │ ├── duck_controller.cpp # STM32底层控制器 │ └── run_deploy.py # 树莓派上的推理入口 ├── tools/ │ ├── replay_dataset.py # 仿真步态回放 │ ├── export_onnx.py # 转换部署格式 │ └── causal_diagnose.py # 干预实验工具 └── docs/ ├── mechanics.md └── training_notes.md模块边界的原则是:环境、算法、部署三者互相独立。训练环境通过一个统一的step()接口对外服务,真机适配层实现同样的接口。策略训练时完全不知道后面要跑真机,部署时也完全不知道训练用的奖励函数长什么样。这个设计让硬件改动不会污染训练逻辑,算法更新也不会影响部署流程。
4.2 训练、评估、部署三个入口
仓库里最常用的三个命令分别对应三个工作阶段:
训练入口读取train.yaml,生成和注册MuJoCo环境,启动PPO训练,每个checkpoint保存一次模型,并在训练完成后自动导出ONNX。评估入口则加载checkpoint,在可视化环境里回放步态,同时录制接触力、关节力矩、质心轨迹,用来判断策略的稳定性指标。
部署入口是我自己最常用的一段代码。推理侧只保留一个100Hz的循环:读取传感器、做归一化、过一遍网络、输出目标角度,然后通过串口发给STM32。整个推理在树莓派Zero 2 W上大约耗时1.2毫秒,远小于控制周期。部署代码里没有RL库,只剩一个ONNX Runtime,这能让人清晰地看到“算法和部署是两件事”这个基础工程原则。
4.3 依赖管理与可复现性
开源项目最怕“别人clone下来跑不起来”。我在依赖上做了个狠决定:只依赖MuJoCo、PyTorch、NumPy、PyYAML这五个基础库,不引入复杂训练框架。PPO实现是仓库自己维护的四五百行代码,虽然看起来“土”,但每一步都能被调试,也能被教学。
requirements.txt里直接锁版本,训练配置全部走YAML,连随机种子都写进配置。这样做的价值在复现实验结果时特别明显:我后来在另一台机器上重跑训练,得到的回报曲线和之前几乎重合。这种可复现性比追新框架版本爽得多。
4.4 开源协议和模块边界的价值观
我选了MIT协议,也是想给社区最大的使用自由。真正比协议更重要的是项目文档——把机械设计选型、训练参数依据、常见错误排查都写进docs。我见过太多开源机器人项目,代码能跑,但换一个环境就崩,原因就是缺少“为什么这样设计”的上下文。这个仓库不敢说完美,但它至少把从仿真到真机之间的几十个细节判断都记录下来了。
5. 训练中的坑位复盘:奖励欺骗、参数漂移与退化
5.1 奖励欺骗的典型案例:原地转圈
第一版奖励函数里,我用了朝向角度差作为主要引导项。结果训练出来的鸭子确实“很乖”——它学会了在原地快速打转,始终保持正对目标方向,但机体几乎不产生前向位移。因为奖励函数只看朝向角度差,转圈可以把这个差长期维持在零附近,同时还能规避掉速度惩罚项。
这真的是个经典的奖励欺骗案例,解决方式也很经典:把前向速度项拆得更细,必须要求机体坐标系x方向的位移累计增长,而不是只看瞬时朝向。我还加了一条“前进位移窗口平均速度”的奖励项,用过去两秒的平均前向速度来判断“是不是真的在走”,原地转圈一下子就骗不到奖励了。从那以后,我每次都先看策略的轨迹热力图,而不是只看回报曲线。
5.2 摩擦系数和接触刚度的域随机化
真机测试时,我发现了另一个问题:仿真里走得很好的鸭子,在宿舍走廊的瓷砖上疯狂打滑,在会议室地毯上又频繁卡脚。查下来,问题出在摩擦系数和接触刚度——MuJoCo默认的接触参数跟廉价舵机驱动的脚掌实际情况差距很大。
域随机化是我的标准解法。训练时摩擦系数从0.3到1.5之间均匀采样,脚掌刚度从低到高变档,再加入高斯接触噪声。这一步看似简单,却解决了“一种步态只适应一种地面”的老毛病。训练出来的策略最终对不同地面的泛化能力明显更好,虽然单一看某种地面不如专测数据漂亮,但综合可靠性高了很多。
5.3 用因果干预实验定位策略退化
有一次训练到后期,回报曲线平稳,但真机表现突然变差。我以为是硬件出了问题,反复检查电机和脚掌都没有异常。重启训练,发现同样的现象又出现了。
最后定位的方式就是前面提到的因果干预实验。我对训练中的策略做了两组干预:一组把期望前进速度调低到近乎零,一组把机体初始偏航角强制拉偏15度。结果发现,策略对第一组干预几乎不敏感——它已经退化成一个“只会按固定频率迈步”的节奏发生器,不再根据目标速度调整动作。原因在熵系数:我把熵系数设成线性降到0,训练后期策略变得极其确定性,探索能力趋近于零,任何微小环境变化都可能导致崩溃。修复办法是让熵系数在后期保持一个较小但不为0的底值,最终解决了退化问题。
这个案例说明,强化学习训练有时候并不是“loss越低越好”,策略的探索余量是需要刻意保留的。因果干预实验帮我建立了一组“策略行为体检”指标,每次训练完先体检再上真机,比盲目相信回报曲线靠谱得多。
5.4 仿真到真机的“最后一公里”细节
即便做了域随机化,仿真和真机仍然有差异。我总结出最后几个必须过的细节,缺一个都可能让鸭子在下地瞬间变成“抽风鸡”:第一,确保真机的关节限位和仿真完全一致,否则策略输出一个超出物理限位的角度,底层电机根本执行不了;第二,仿真里加入执行器延迟模型,真机的串口和PD计算加起来大约有8到12毫秒延迟,这个量级足够让步态变脆;第三,电机回差要单独测,不同负载下的回差差异非常大,我最终在真机推理代码里加了一个简易的前馈补偿项。
这段经验我写进了项目的training_notes.md,后来有朋友复现时照着做,一次就下地成功了。
6. 实测表现与最后想说的话
6.1 真机实测:从走廊到楼道
拿最终策略上真机,测试过几种常见地面:抛光瓷砖、粗糙水泥地、短毛地毯、塑胶跑道,以及5度以下的小坡。结果最顺的是水泥地,鸭子能以约0.12 m/s的速度稳定走,连续走15分钟累计里程大约110米;瓷砖上稍显谨慎,策略会自动降低步幅、增加步频来抵抗打滑;地毯上有轻微卡脚,但几乎没有摔倒。坡道上,前5度能保持稳定,超过8度时策略会明显吃力,需要人为把期望目标速度调低。
我用外置动作捕捉做了对比,真机质心轨迹和仿真回放的平均误差大约在2.3厘米左右。这个误差水平对双足步态来说已经很理想了,它让我相信,在硬件和仿真建模做扎实的前提下,域随机化加因果诊断这套流程确实能把sim-to-real的鸿沟压到很小。
6.2 给想复现的你的几条真心话
如果看完这篇,你也想照着一套方法做一只自己的小机器人,我有几句实在话想说在前头。
第一,先跑通一次完整的训练闭环,再谈优化。很多人在第一步就卡在“追求更复杂的奖励函数”,其实哪怕奖励只有“别摔、往前走”两项,第一次完整跑通的价值也远大于纸上谈兵。第二,真机下地前,务必用遥控器手动把每个关节掰一遍,确认限位、转向和停止状态都正常,这一步能避免大量“策略没问题、硬件乱来”的误会。第三,录像是你最好的调试伙伴——肉眼觉得步态还行,往往是因为大脑自动补全了细节,慢动作录像才能看到脚掌滑动、膝盖反曲这些真问题。
最后再说一个经验之外的经验:做这类项目,最好把“让鸭子走得稳定”和“让鸭子走得像鸭子”当成两个独立目标。稳定是算法问题,风格是设计问题。先稳定,再谈风格,你会少掉很多心态上的煎熬。我也还在继续往这个平台上加东西,比如鸭翅膀的辅助平衡控制和自适应步态切换,后续有结果了再拿出来分享。