步态参数化:环境如何把命令整理成四条腿的节律
在四足机器人强化学习(如 Walk These Ways 代码库)的中间任务逻辑里,最关键的一环之一就是步态命令的处理。
很多人初看代码时会存在几个误区,比如认为 p/o/b 是策略网络直接输出的步态参数,或者以为 obs_buf 里塞入了 sin/cos 成对编码,甚至以为接触状态是靠 sin 的正负号硬切出来的。实际上,代码背后的核心思想并不是“让网络直接学会四条腿什么时候抬落”,而是:
先从 curriculum 中采样连续 gait 参数,再把这些参数整理到若干结构化步态家族附近,最后把整理后的相位节律显式送进 observation 和 reward。
这段逻辑主要由两个核心函数完成:
_resample_commands():采样并整理 gait command。_step_contact_targets():把 gait command 推进成四条腿的相位、时钟输入和目标接触状态。
下面,我们将抽丝剥茧,详细梳理这套优雅的步态参数化机制。
一、先有 Gait Command,再有四腿相位
与其让策略直接输出 “FL(左前腿)现在抬腿、FR(右前腿)现在落地”这种离散指令,环境采用了一种更平滑的表示方式:用周期相位来描述每条腿在一个步态循环中走到了哪一段。
我们可以先把一条腿的迈步想成一个长度为 1 的圆环时间轴:
- 0 ~ 1 表示一个完整的步态周期。
- 走到 1 之后并不会结束,而是回到 0 重新开始。
- 所以,它天然适合用 “相位” 来表示,而不是用一次性的时间戳。
在这个框架里,环境先维护了一个统一的“全局节拍器” g。
它不是某条腿自己的状态,而是整个机器人当前步态周期推进到了哪里:
gt+1=(gt+f⋅Δt) mod 1g_{t+1} = (g_t + f \cdot \Delta t) \bmod 1gt+1=(gt+f⋅Δt)mod1
在这里:
- f 决定节拍走得多快,也就是步频。
- Δt 是每一步环境推进的时间。
- mod 1 表示走完一整圈后重新回到起点(即数值被限制在 0 到 1 之间循环)
如果只有这个全局时钟 g,那么四条腿会完全同步,因为大家都跟着同一个节拍走。
但真实步态的关键恰恰不在“节拍本身”,而在于:四条腿相对于这个节拍器各自提前多少、滞后多少。
这就是 p、o、b 出现的原因。它们不是绝对时间,而是相位偏移量。环境通过给不同腿叠加不同的偏移,来制造“哪条腿先动、哪条腿后动”的关系。
在默认 pacing_offset = False 的定义下:
ϕFL=(g+p+o+b) mod 1\phi_{FL} = (g + p + o + b) \bmod 1ϕFL=(g+p+o+b)mod1
ϕFR=(g+o) mod 1\phi_{FR} = (g + o) \bmod 1ϕFR=(g+o)mod1
ϕRL=(g+b) mod 1\phi_{RL} = (g + b) \bmod 1ϕRL=(g+b)mod1
ϕRR=(g+p) mod 1\phi_{RR} = (g + p) \bmod 1ϕRR=(g+p)mod1
这里的理解重点不是死记哪条腿加了哪几个量,而是看出这个结构:
g是四条腿共享的全局节拍。p / o / b是人为施加的相对时差。- 每条腿都在“同一个周期”里,但各自站在这个周期上的不同位置。
所以,“相位”可以直观地理解成:一条腿在当前步态循环中的进度条。
- 比如两条腿的相位如果相差 0.5,就意味着它们正好错开半个周期,一条腿在前半程时,另一条腿在后半程。
- 如果相位差接近 0,那它们就几乎同步起落。
这样一来,环境要表达 Trot(对角步)、Pace(溜步)、Bound(跳步),本质上就不再是硬编码“第几条腿现在接触”,而是只需要规定:四条腿之间应该保持什么样的相位关系。
这也解释了为什么后面的代码操作都在改 p / o / b,而不是直接改“接触标签”。因为一旦四条腿的相位关系被定下来,后续的时钟输入(Clock Input)、目标接触状态、摆动期奖励,都会顺着这个节律自然生成!
二、为什么要把连续参数“投影”到标准步态附近?
如果完全让 p、o、b 在 [0,1) 内自由漂移,四条腿之间就可能形成大量模糊、难学、甚至不稳定的相位组合。为此,_resample_commands() 在采样完连续 gait 参数后,并不会直接原样使用它们,而是通过一组简单但非常有效的映射,把这些参数投影到若干更稳定的结构附近。具体来说,x' = x/2 + 0.25 会把相位差限制在靠近半周期差的区间内,即远离同相区域;
而 x' = (x/2 - 0.25) mod 1 则会把相位差压到周期意义下接近 0 的邻域,也就是趋向同步。于是,这段代码并不是在凭空硬编码几种 gait,而是在把连续采样得到的 gait 参数约束到“接近同相”或“接近反相”的可学习步态结构附近,从而显著降低策略搜索空间的复杂度。
如果 x ∈ [0,1),那么先看不取模之前:
x/2−0.25∈[−0.25,0.25)
这时左半段是负数,所以再做 mod 1 之后:
[-0.25, 0) 会被折回到 [0.75, 1)
[0, 0.25) 保持不变
所以最终范围就是:
x ′∈[0,0.25)∪[0.75,1)
这也正是为什么说它把相位“推向同相区域”:
在周期变量里,0 和 1 是同一个位置
所以 [0,0.25) 和 [0.75,1) 实际上都是“靠近 0 相位”的邻域
三、四种经典步态在代码里是如何整理出来的?
在 gaitwise_curricula=True 时,环境会先把 env 分配到 pronk / trot / pace / bound 四类,再对参数做特定投影:
1. Pronk (跳跃)
p′=(p/2−0.25) mod 1,o′=(o/2−0.25) mod 1,b′=(b/2−0.25) mod 1p' = (p/2 - 0.25) \bmod 1,\quad o' = (o/2 - 0.25) \bmod 1,\quad b' = (b/2 - 0.25) \bmod 1p′=(p/2−0.25)mod1,o′=(o/2−0.25)mod1,b′=(b/2−0.25)mod1
三项都被压向“同相”,四腿大体同步。理想化情况下:
ϕFL≈ϕFR≈ϕRL≈ϕRR≈g\phi_{FL} \approx \phi_{FR} \approx \phi_{RL} \approx \phi_{RR} \approx gϕFL≈ϕFR≈ϕRL≈ϕRR≈g
表现为四腿一起起落。
2. Trot (小跑)
p′=p/2+0.25,o′=0,b′=0p' = p/2 + 0.25,\quad o' = 0,\quad b' = 0p′=p/2+0.25,o′=0,b′=0
于是:
ϕFL=g+p′,ϕFR=g,ϕRL=g,ϕRR=g+p′\phi_{FL} = g + p',\quad \phi_{FR} = g,\quad \phi_{RL} = g,\quad \phi_{RR} = g + p'ϕFL=g+p′,ϕFR=g,ϕRL=g,ϕRR=g+p′
当 p′≈0.5p' \approx 0.5p′≈0.5 时,就是对角腿两两同步、两组相差半周期。
3. Pace (溜步)
p′=0,o′=o/2+0.25,b′=0p' = 0,\quad o' = o/2 + 0.25,\quad b' = 0p′=0,o′=o/2+0.25,b′=0
于是:
ϕFL=g+o′,ϕFR=g+o′,ϕRL=g,ϕRR=g\phi_{FL} = g + o',\quad \phi_{FR} = g + o',\quad \phi_{RL} = g,\quad \phi_{RR} = gϕFL=g+o′,ϕFR=g+o′,ϕRL=g,ϕRR=g
在默认 pacing_offset=False 公式下,这更像“前腿一组、后腿一组”相差半周期。
4. Bound (跃步)
p′=0,o′=0,b′=b/2+0.25p' = 0,\quad o' = 0,\quad b' = b/2 + 0.25p′=0,o′=0,b′=b/2+0.25
于是:
ϕFL=g+b′,ϕFR=g,ϕRL=g+b′,ϕRR=g\phi_{FL} = g + b',\quad \phi_{FR} = g,\quad \phi_{RL} = g + b',\quad \phi_{RR} = gϕFL=g+b′,ϕFR=g,ϕRL=g+b′,ϕRR=g
在默认公式下,这更像“左腿一组、右腿一组”相差半周期。
💡 核心提示: 如果在代码中开启
pacing_offset=True,FR 和 RL 的相位公式会发生交换。此时pace会更接近标准的“同侧同步”,而bound会更接近“前后同步”。
四、从相位到 Observation:给策略喂多组正弦时钟
相位 ϕ\phiϕ 本身是模 1 的周期变量,在 0.99→0.000.99 \rightarrow 0.000.99→0.00 处会发生物理突变。为了平滑地传递这个信息,代码并没有直接喂入 ϕ\phiϕ,而是构造了多组正弦时钟:
clocki=sin(2πϕi)\text{clock}_i = \sin(2\pi \phi_i)clocki=sin(2πϕi)
doubletimei=sin(4πϕi)\text{doubletime}_i = \sin(4\pi \phi_i)doubletimei=sin(4πϕi)
halftimei=sin(πϕi)\text{halftime}_i = \sin(\pi \phi_i)halftimei=sin(πϕi)
需要特别注意的两点:
- 代码里默认是 sin-only,并不是有些框架里常用的
sin/cos成对编码。 - 真正进入普通 observation 的默认只有
clock_inputs(即 sin(2πϕ)\sin(2\pi\phi)sin(2πϕ) 这一组)。如果需要,可以显式打开observe_clock_inputs把其他项拼入obs_buf。
更准确地说:策略看到的是环境整理好的“节拍器信号”,而不是让神经网络自己从原始接触中去盲猜周期。
五、从相位到目标接触:Duration 重映射与平滑窗口
这是最容易写错、也最容易被误解的一步。
很多直觉化讲法会说:“当 sin(2πϕ)>0\sin(2\pi\phi) > 0sin(2πϕ)>0 时要求落地,否则要求摆动。”但这份代码处理得更加精细:它先根据 duration(ddd)把原始相位重新拉伸。
- 落在 stance (支撑) 区间 (ϕ<d\phi < dϕ<d):
ϕ′=ϕ⋅0.5d\phi' = \phi \cdot \frac{0.5}{d}ϕ′=ϕ⋅d0.5 - 落在 swing (摆动) 区间 (ϕ>d\phi > dϕ>d):
ϕ′=0.5+(ϕ−d)⋅0.51−d\phi' = 0.5 + (\phi - d)\cdot \frac{0.5}{1-d}ϕ′=0.5+(ϕ−d)⋅1−d0.5
这意味着,不管真实的 stance 占整个周期的比例是多少,代码都会把 stance 压到 [0,0.5)[0, 0.5)[0,0.5),把 swing 压到 [0.5,1)[0.5, 1)[0.5,1)。
完成重映射后,desired_contact_states 也不是硬性的 0/1,而是用正态分布 CDF 拼出了一个平滑窗口。直观理解就是:
- 相位接近 stance 中心时,目标接触值接近 1。
- 相位接近 swing 中心时,目标接触值接近 0。
- 在边界附近平滑过渡,避免突然跳变。
因此,desired_contact_states 的物理含义其实是:“这一刻这条腿应该有多大程度处于接触状态”,而不是粗暴的“非接触即接触”。
六、Reward:围绕目标状态的“软塑形”
接触目标生成之后,Reward 会以此来做塑形(Shaped Reward),但绝不是简单的“接触错了就狠狠扣分”。与 gait 接触最相关的三项奖励:
tracking_contacts_shaped_force:当某条腿本来不该接触时,如果接触力还很大,受罚。tracking_contacts_shaped_vel:当某条腿本来应该接触时,如果脚端速度还很大,受罚。feet_clearance_cmd_linear:在摆动相,鼓励脚的高度接近期望摆动高度。这里显式使用了 (1−desired_contact_states)(1 - \text{desired\_contact\_states})(1−desired_contact_states) 来强调 swing 期的权重。
从训练机制看,环境并未“硬编码”步态,而是:提供一个平滑的参考节律,再用 reward 把策略往这个节律附近推。
七、Curriculum 与过滤机制:给策略“加扶手”
为了防止策略在高维 gait 空间里彻底迷失,代码还额外包了几层限制:
gaitwise_curricula:将 env 分配到四大类步态,并投影到对应结构附近。exclusive_phase_offset:只允许p / o / b里的一项起作用,其余两项强制清零。等同于强迫每次只沿一种主导相位关系变化。balance_gait_distribution:按固定比例分配环境,避免某一类 gait 在训练中“赢家通吃”导致占比失衡。binary_phases:将相位差强制量化为同相/反相关系:
x=round(2x)2 mod 1x = \frac{\mathrm{round}(2x)}{2} \bmod 1x=2round(2x)mod1
此外,对平面速度命令还加了一个非常实用的小 Deadband(死区)过滤:
(vx,vy)←0,if vx2+vy2≤0.2(v_x, v_y) \leftarrow 0,\quad \text{if } \sqrt{v_x^2 + v_y^2} \le 0.2(vx,vy)←0,if vx2+vy2≤0.2
这完美避免了“几乎静止但又不完全为零”的极小速度命令让机器人原地鬼畜抖腿。
总结
这套步态参数化机制,并不是让神经网络用纯黑盒的方式自己去摸索“四条腿各自什么时候抬和落”。它呈现的是一套非常典型的现代 Locomotion 环境设计范式:
- 环境先采样速度和连续 gait command。
- 通过巧妙的数学变换,把参数整理到稳定的结构化步态附近。
- 利用全局时钟推进四条腿的相位。
- 将相位转换为拉伸重映射后的平滑时钟输入和软性接触目标。
- 最终,通过 Observation 和 Shaped Reward 将节律信息传递给策略网络。
一言以蔽之:策略负责在给定节律条件下输出合适的底层扭矩动作,而环境负责把“想走成什么样”整理成可学、稳定、可优化的时序约束。
精简回答:
-
第一步:生成原始相位进度条(定骨架)
环境维护一个统一的全局时钟 ggg,结合指令中的相位偏移参数 p,o,bp, o, bp,o,b,为四条腿分别生成在 000 到 111 之间循环的原始相位 ϕ\phiϕ,这就确立了四条腿的时序骨架。 -
第二步:兵分第一路 —— 给网络当“节拍器”(做 Observation)
为了避免 ϕ\phiϕ 归零时产生跳变,我们将它卷成连续的正弦波 sin(2πϕ)\sin(2\pi\phi)sin(2πϕ) 作为观测值喂给网络。网络只看连续的波形来感知当前的周期节奏,它本身并不知道(也不需要知道)这一刻到底该踩地还是抬腿。 -
第三步:兵分第二路 —— 给环境当“裁判尺”(做 Reward 考核)
环境拿着原始相位 ϕ\phiϕ,结合指令中的支撑期比例(Duration ddd),做一个空间重映射计算,捏出一个虚拟相位 ϕ′\phi'ϕ′。在这个系统里被死死规定:ϕ′<0.5\phi' < 0.5ϕ′<0.5 就是目标踩地,>0.5> 0.5>0.5 就是目标抬腿。环境根据它算出目标接触概率,交给 Reward 函数,精准惩罚机器人不按节律的动作。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)