0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

用家用 CPU 让一具只有骨与肌的人体站立、抓握、喝水、跳舞的全记录 —— 700 条肌肉、吉祥物“evis”,以及诚实的输与赢

0
Last updated at Posted at 2026-07-21

用家用 CPU 让一具只有骨与肌的人体站立、抓握、喝水、跳舞的全记录 —— 700 条肌肉、吉祥物“evis”,以及诚实的输与赢

🌐 其他语言版本: 日本語 / English

适合阅读的人: 对机器人、生物力学、进化计算、MuJoCo、动作捕捉感兴趣的人 / 对“靠肌肉驱动的人体模型”或“在自家让一具解剖学意义上的人体动起来”感兴趣的人 / 比起漂亮的结果,更想读到诚实的内幕拆解,以及从卡住到修好的全过程的人。
前提知识: 能读懂 Python 就足够。控制、生物力学、mocap 的术语会在出现时逐一讲清楚。
所有数值都在笔记本电脑的 CPU 上实测得到。这篇文章不是成功学,而是一段把“一方诚实地输了、另一方赢了”的两种结果用同一把尺子去度量,一旦卡住就不去猜测而是去测量、去修正的、连贯的记录。

这篇文章,是把此前分开写的三篇连作 —— 「站立·抓握·搬运」「喝水(追踪 bug)」「evis 吉祥物化(mocap)」—— 按时间线与因果关系连成一体的决定版。篇幅很长,所以你从感兴趣的章节读起也没问题。


0. 三行总结(先给结论)

  • 由骨、腱和 700 条肌肉构成的全身人体模型(中国 LNS Group 的 MS-Human-700,ICRA 2024,Apache-2.0),被搬上家用 CPU 里的 MuJoCo,用进化一条一条地驱动肌肉,让它去「站立」「抓握」「搬运」「喝水」,最后把它做成吉祥物“evis”,用 mocap 让它跳舞、用筷子吃东西。关键在于:驱动它的不是扭矩电机,而是只能产生收缩力的肌肉
  • 站立诚实地输了,抓握赢了,搬运是部分胜利(三分之一)。一具什么都不做就会在约 1 秒内崩塌的骨架,通过闭环控制把崩塌延迟到了典型的 2.5 秒,但撑不到 6 秒(输)。另一方面,固定住肩部的手臂模型,那只由 81 条肌肉构成的手,成功抓起桌上的水瓶并抬高 0.82m 保持住(赢)。再加上「搬运到指定位置」的任务后,变成部分成功(三条指令中严格意义上成功的只有 1 条 = 三分之一)。为什么任务不同,输赢也随之不同,正是前半部分的主角。
  • 后半部分是“测量再修正”的两连击。以为“喝到了”的那个动作,被人一句「没进到嘴里」而否决,一路追下去,竟落到水瓶穿透地板沉入地下的 bug(keyframe 的编号错位)。只要在一个正确的位置插入一个 0,就修好了。接着把骨架命名为 evis 并移植 mocap,这次又冒出**「腰像老人一样弯着」**的驼背 bug。停止猜测、去测量关节角之后,真凶原来是「把 marker 的高度弄错了」。猜测之前先测量——这就是贯穿全文的通奏低音。

先把诚实的注解放在前面: 站立、抓握、搬运、喝水(手臂)都是由肌肉驱动的真实过程,但 evis 的舞蹈、筷子、以及喝水姿态的一部分,混入了运动学回放(直接给定关节角的演出层)哪里是真实、哪里是演出,会在各处以及最终章中明确标注。

evis 动作集锦

↑ 这篇文章的终点: 走→跑→跳→莎莎舞→踢腿→用筷子吃东西。全部是同一具骨架(evis),把免费的 mocap 移植过来驱动它。走到这一步之前经历的「输」与「bug」,下文都会连同每一个测出来的数字一并留存。


1. 术语(先画一张地图)

这是一篇很长的文章,所以先把这些词做成一张地图。已经熟悉的行可以直接跳过。

术语 通俗解释
肌肉骨骼模型(musculoskeletal model) 不仅有骨骼和关节,还带上肌肉和肌腱的人体仿真。这次是带 700 条肌肉的全身模型
肌肉致动器(muscle actuator) 以「肌肉」为动力源的驱动。和电机不同,它只能产生收缩(拉)的力,力的大小由 0〜1 的活性决定,是个非线性的家伙
自由度(DoF / degrees of freedom) 能够独立运动的关节轴的数量。步行用模型是 36,操作用是 48,全身是 85
冗余(redundant) 肌肉数量远远多于想要驱动的自由度的状态。「正确的肌肉组合」有无穷多个,搜索起来很难
肌肉协同 (muscle synergy) 把 100 条肌肉压缩成少数(K 条)「成组一起动的模式」的表示。据说人脑也是这样把肌肉捆在一起的
闭环控制(closed-loop) 一边观察身体的状态(倾斜、速度、触地)一边调整肌肉的控制。不看就一口咬定的是开环(open-loop)
倒立摆(inverted pendulum) 倒着立起来的棍子。放着不管就会倒。双足站立的本质,就是不停地把它重新扶正
CMA-ES(进化策略) 通过对解的群体做变异、选拔来使其变好的优化方法。这次让控制策略里数百〜上千个数值进行进化
基线(baseline) 学习前、无控制的对照。用来衡量「学习到底带来了多少改善」的出发点
RTF(实时因子 / real-time factor) 仿真比实时快多少。13× 表示 1 秒的物理用大约 0.08 秒算完(强烈依赖 CPU 和并行度)
qpos 把模型的所有关节的位置排成一列的数组。是后半段的关键。一旦增加关节,这个排列就会错位
关键帧 (keyframe) 把「实验开始时的姿势」qpos 的排列顺序原样写下来的初始化数据。由位置(编号)决定是它的要害
温启动 (warm-start) 学习不是「从零开始」,而是从「已经会的策略」开始
动作捕捉 (mocap) 记录了人的动作的关节角时间序列。这里用的是免费的 BVH 文件
动作重定向 (retargeting) 把某个骨架的动作,移植到体型和关节结构都不同的另一个骨架
IK(逆运动学 / Inverse Kinematics) 从「想把手末端放在这里」反算出关节角的计算
运动学回放(kinematic) 不跑物理(力、肌肉),而是直接设置关节角来生成画面。只做出动作的外观
等式约束(equality constraint) 「这个关节由那个关节的函数决定」这样的联动规则。膝盖和肩膀里塞了一大堆(即后文的陷阱)

第 I 部 —— 让一具 700 条肌肉的骨架,在家用 CPU 上站立、抓握、搬运

2. 为什么选“全身骨架”,以及为什么先做调研

在开始一件新事之前,先做调研。这是我的习惯。这次的题材是 MS-Human-700——由中国 LNS Group 制作、在 ICRA 2024 上发表的全身肌肉骨骼模型(Zuo, He, Shao, Sui, Self model for embodied intelligence)。它以 Apache-2.0 公开,并被收录进 MuJoCo 的官方模型集 Menagerie。这是一具包含骨、腱以及 700 条肌肉的、名副其实的“全身骨架”。

读完论文后,我诚实地调整了自己的预期。这篇论文的主旨在于“把 700 条肌肉这样的高维度,用**分层的低维表示(≒肌肉协同,muscle synergy)**来控制,并学习行走”。也就是说,要让它像样地动起来,朴素的方法是不够的——这一点是作者自己说的。

提前给出的教训:“用肌肉让人体站起来”即便投入 GPU 集群和分层强化学习也很难,是一个货真价实的研究课题。而我却打算用笔记本电脑的 CPU 加进化策略(evolution strategy)来做,所以从一开始就要把失败的可能性算进去。输了就写输了——这正是本系列的脊梁。

我看重的并不是“成功让它站起来”这个结果本身。用同一把尺子,诚实地归属出走到了哪里、又在哪里停下——这才是我今天想写的东西。

3. 舞台:把 700 条肌肉的人体,装进家用 CPU

首先,我把 MS-Human-700 加载进了自研的研究运行时 onocollo(一个套在 MuJoCo 之上的、带类型的运行时)。它有三个变种。

变种 自由度 肌肉 用途
全身(full) 85 700 全身所有的肌肉
行走用(locomotion) 36 100 简化上半身。用于站立、行走的实验
操作用(manipulation) 48 81 右臂与精细的手+桌上的水瓶

有几个重要的事实。

  • 致动器全部都是“肌肉”。不像机器人的电机那样能“从 +3 N·m 到 −3 N·m”双向转动,肌肉只能沿收缩(拉)的方向发力。活性值在 0〜1 之间,由一种依赖力‐长度‐速度的非线性 Hill 型模型来决定发出的力。所以要“想把关节往右转就用右侧的肌肉,往左就用左侧的肌肉”,必须成对地准备,而且数量比自由度还多(=冗余)。
  • 膝盖在生物力学上非常精密。它不是简单的合页,而是用多个辅助关节和约束式,再现了股骨与胫骨之间滚动加滑动的运动。
  • 骨盆是“浮动基座(floating base)”——不固定在地面上,可以通过 3 个方向的平移和 3 个方向的旋转自由运动。在初始姿态下,它以 0.95m 的高度直立着。
  • 速度:这个模型在笔记本电脑(Intel Core,Ice Lake 世代的移动 CPU)的单线程、空闲状态下,可达 RTF 约 13 倍(6 秒的物理约 0.46 秒,~6500 步/秒,dt=0.002s)。不过这是独占时的数值,为了进化而用 7 个核心并行跑起来时,每个进程会降到 ~3〜5 倍(核心互相争抢)。RTF 会随 CPU 型号、并行度、积分器设置产生这么大的变化,所以数字务必要连同条件一起读。即便如此,这个速度也确实达到了让“成千上万次 rollout”的进化能够实际跑起来的程度。

MS-Human-700 行走用变种的初始直立姿态。腿上呈红色的即是肌肉和腱。骨盆以 0.95m 的高度站立着

4. 先测一个诚实的基线——然后看着既有控制器"崩掉"

在做新的控制之前,一定要先测一个无学习的基线(baseline)。这样当后面出现异常好看的数字时,我才能回头质疑它——先把出发点钉死在原地。在步行用模型上(带地面),做 6 秒的 rollout,并规定"骨盆掉到 0.6m 以下即视为跌倒"。

控制 跌倒前的时间 结局
无控制(全部肌肉松弛) 0.50 秒 崩塌
全部肌肉最大收缩 0.99 秒 崩塌
每一步随机数 1.00 秒 崩塌

什么都不做,人体骨架大约 1 秒就会瘫倒。把所有肌肉都最大程度地绷紧,刚度上去了,能续命一点点,但依然站不住。这就是站立任务的起点。

而就在这里,我实测到了既有工具会"崩掉"——这是这次在技术上最有意思的一个发现。onocollo 里本来就有一个"让 Menagerie 里机器人的控制进化"的控制器。但它的设计是让每个致动器去读取自己所负责关节的角度再动作。可肌肉并没有"自己负责的那唯一一个关节"(它经由肌腱跨越多个关节)。结果就是,全部 100 条肌肉收到的是相同(为零)的输入,又因为共享权重,这 100 条肌肉给出的活性一模一样

100 条肌肉在每一瞬间都以完全相同的强度收缩——这样绝对不可能做到双足站立。我用数值确认了这种退化(独一无二的活性值种类 = 1)。所以,肌肉模型需要一种新的表示:"看着全身的状态,给每一条肌肉发出不同的指令"。这与论文所说的"分层的低维表示"在必要性上恰好吻合。

5. 站立任务:肌肉协同 (muscle synergy) × 进化 —— 然后诚实地摔倒

我做的控制是这样的。

  1. 读取全身的本体感觉(proprioception,自体感受):骨盆高度的误差、倾斜(前后/左右/扭转)及其角速度、骨盆的平移速度、左右脚的触地——共 14 个对平衡起作用的量。
  2. 落到肌肉协同上:把这个状态线性映射(tanh)到 **K 条"成组一起动的模式"**上,再用一个 K×100 的矩阵把它展开成 100 条肌肉的活性(用 sigmoid 压到 0~1)。K=6 时策略的数值约为 790 个。这就是我对论文所说"低维表示"的自己的实现。
  3. 用 CMA-ES 进化:用进化策略来最优化这些数值。奖励是"站着期间,高度越接近目标、上身越挺直、骨盆速度越小,得分越高,跌倒即终止"。

一次诚实的设计修正:最初的奖励只是"高度接近目标就加分"。于是进化变得投机,靠**『慢慢地、优雅地倒下去』的策略来刷分(因为倒下的途中高度也还算高)。看上去根本没在站。于是我给骨盆速度加了惩罚项**,把它改成"安静地停住不动"要比"慢慢倒下"得分更高。这是对量尺的一次调整,用来封住这种投机。

为了不浪费地跑计算,我还写了一套只把模型加载一次、用 7 个核心并行评估的机制(模型加载约需 2 秒,而一次 rollout 只有几百毫秒,所以每次都重新加载的话,加载就会成为主导开销)。让每个 worker 只加载一次然后反复复用,就能拿到与并行度相当的加速。

结果是——诚实地说,没够到目标。如果只列出最优值(best),就成了"拿一座山峰当代表值",所以我把典型值(进化后半段各世代 best 的中位数)也一并列出。先看单个随机种子下的结果:

控制(单一种子) 策略的数值 典型(中位数) 最优(best) 结局
基线(无控制) 0 0.5~1.0 秒 崩塌
静态肌张力(常数) 100 ~2 秒 3.02 秒 最终跌倒
肌肉协同(闭环) 790~1020 ~2.5 秒 3.71 秒 最终跌倒
肌肉协同+记忆(积分项) 808 ~3.0 秒 4.53 秒 最终跌倒

看着这张表,我一瞬间这么想了——"加上记忆(积分项)后,从 3.71 → 4.53 秒变长了。跟假设一样,平衡确实需要对误差做积分"。平衡控制在理论上本来就该如此(积分控制),所以这套说法讲起来也很漂亮。

差点被 n=1 骗了——把随机种子增加到 3 个,差异就消失了

可 CMA-ES 是随机的。单一随机种子上的差异,也许只是种子的运气好坏。于是我把"有记忆/无记忆"各自跑了 3 个种子并取中位数(做公平的比较)。

控制 best 的中位数(3 种子) best 的离散范围 典型(median-late)的中位数
无记忆(plain) 3.55 秒 3.16~3.80 秒 2.71 秒
有记忆(积分项) 3.56 秒 3.34~4.17 秒 2.39 秒

结论反转了。best 的中位数是 3.55 ≒ 3.56,几乎相同,而典型值上有记忆的反而更低(2.39 < 2.71)。也就是说,单一种子里看到的 4.53 秒只是同一分布中碰巧偏高的一个尖峰,并不能作为记忆项起了作用的证据。我那个"记忆有用"的漂亮假设,在 n=3 下没有得到支持

  • 闭环控制把崩塌从约 1 秒 → 典型 2.5 秒、最优 3.5 秒上下推迟了。它明确超过了基线,而且闭环比静态肌张力更好。倍率的分母保守地取"无控制 ~1.0 秒"(如果拿全部肌肉松弛的 0.50 秒当分母,数字会翻倍,但那是夸大)。在这个范围内,有没有记忆并无显著差异
  • 它没法持续站满 6 秒。即便是最优的策略,最后一帧也是躺在地上。
  • 教训:n=1 的"起作用了!"在随机优化里往往是假的。越是漂亮的假设,越要在取到多个种子的中位数之前不去相信它。这一件事,正是我在发布前让内容过一遍独立进程的 fact-check这一做法实际奏效的例子(fact-check 指出"单一种子上的因果结论下得太重"→ 用 3 个种子验证 → 结论反转)。

学习曲线。站住的时间随世代上升,但在 2~2.7 秒(尖峰处 3.7~4.5 秒)见顶。虚线是无控制的约 1 秒。够不到 6 秒(回合长度)

即便是最优的站立策略,约 3.7 秒后也就成了这副样子——躺到地上。不夸大,把"失败"如实留下

为什么够不到?坦白说,用 CMA-ES 去最优化一个多达约 800 维的线性协同策略,对于倒立摆的恒定稳定化来说力量不足(就算加上简单的积分式记忆,如上所见也救不回来)。这与论文需要用到分层强化学习并不矛盾。在这里把它圆成"站起来了!"很容易,但既然实际上并没有持续站住,那我就不这么写。

6. 抓握任务:换个舞台,就赢了

站立之所以困难,是因为它本质上是一个"倒立摆(inverted pendulum)"。那么,换成一个不必担心摔倒的任务又会如何?——用于操作(manipulation)的那个变体,肩膀以上是固定的,骨盆没有自由关节。也就是说,它根本没有失去平衡的可能。任务纯粹变成了"用手臂和手去做点什么"。

于是,我让一只由 81 条肌肉构成的右手,去抓起桌上的水瓶并把它举起来。手一开始就在离水瓶约 0.1m 的地方,所以真正的任务并不是"靠近"——而是抓住、举起、并保持

这次,我把同一个肌肉协同(muscle synergy)策略,以"水瓶举得越高得分越高"的方式进行进化。结果:

世代 水瓶的举起高度
0(初始) 0.006 m(≈几乎为零)
20 0.39 m
50(收敛) 0.825 m

这只由 81 条肌肉构成的右手,抓住了桌上的水瓶,举起 0.82m 后静止并保持(相当于喝水动作的前一阶段——直到"送到嘴边"之前为止)。

用于操作的全身肌肉模型抓住桌上的水瓶,把它举过头顶并保持。这是保持手掌与水瓶距离不变的"保持",而不是抛掷

诚实地质疑"抛掷"和"兜起来"

以最大化高度为目标的奖励,用"把水瓶往上打飞"这种作弊也能拿到分。所以我做了两个阶段的验证。

(1) 是不是抛掷:举起之后的 3 秒内,手掌(腕骨中央的骨头)与水瓶中心的距离一直保持在 0.052〜0.062m(不发散)。如果是抛掷,距离会发散。二者是一起运动的。

(2) 是不是真的"用手指握住"(而不是兜起来):距离恒定只能说明"一起动了"。为了排除用前臂夹着搬运(兜起来)的可能性,我检查了保持过程中的接触力。触碰水瓶的是9 根手指与手部骨头——拇指(远端为 40N)、食指(掌骨处为 33N)、中指、无名指——并且拇指与其余手指相对(对向)握持。水瓶的重量是 5.3N,所以是以远超其重量的对向力在夹持。这不是兜起来,而是收拢手指的真正抓握(force-closure,力封闭)

诚实披露:这次抓握是在摩擦系数 μ=1.0(接近橡胶的偏高摩擦)的接触条件下成立的。摩擦越高,抓握越容易。如果把 μ 调低,同一个策略完全有可能滑落——我必须写明,"抓住了"是在这个摩擦条件下才成立的说法。

这同时也是对我以前一篇文章里"让 AI 义手拿筷子"时留下的未竟课题——"没有手(是直接操纵筷子的)"这个问题的用真正解剖学意义上的手给出的回答。一只连拇指、蚓状肌、骨间肌都齐全的手,仅凭肌肉的力量抓住了物体。

7. 抓住之后:搬到命令指定的地方(可控操作)

"抓住并举起"其实是一个"往上举、随便哪儿都行"的单向任务。真正的操作(manipulation),是把抓住的物体搬到我们命令指定的地方。于是我给策略看了"目标 − 水瓶"这个向量(把观测增加了 3 维),测试用同一个控制,能否听从 3 个不同的目标(正上方/体侧的喝水姿势/前方略偏上)。如果一个策略能按命令改变搬运的目的地,那就可以说它是"可控的(controllable)"。

然而,朴素地做起来,又再一次掉进了奖励作弊(reward hacking)。如果只设"水瓶越靠近目标得分越高",抓握的梯度就消失了。因为水瓶的初始位置本来就靠近目标点、能薅到最近距离的底分,于是优化就"放松肌肉,把水瓶留在桌上不管",靠不动来拿分。就算跑了 200 世代,到目标的最近距离也丝毫没有改善,一直冻结着——分数在涨,却既没抓也没搬。这是 §5 里"慢慢倒下"那套欺骗的兄弟。

修正的办法有两个:

  • warm-start(温启动):把 §6 中已验证的抓握策略作为初始值。因为观测的维度不同(抓握 19 → 搬运 22),我把新增的命令向量的输入权重全部填零,让初始状态与抓握策略完全一致。这样进化要做的事就变成了"在能动的抓握之上,只加一个命令跟随",从一开始就越过了"放着不管"的那个洼地。
  • 保持抓握的加分 + 终端门控:给手与水瓶的靠近程度加分,而终端奖励只在持续抓住时才生效(掉落/抛掷则 0 分)。

结果,分数从"放着不管"作弊的底分(约 18.9)一跃升到约 1021,内容也变成了真材实料:

命令的目标 最近距离 是否持续抓住? 是否放到目标?(12cm 以内)
正上方 +0.30m 0.103m (最终 0.108m)
体侧·喝水姿势 0.265m
前方略偏上 0.120m 临界

在 3 个命令下都持续抓住了水瓶(手掌〜水瓶 0.043〜0.046m),并朝命令的方向搬运。正上方一直成功到放下为止,前方略偏上是临界。诚实的局限在于"体侧的喝水姿势",差 0.27m 没有够到——从正上方抓握 warm-start 而来的单一线性协同,在把手臂拉回体侧的姿势上容量不足。抓握是完整的,命令跟随则有三分之一严格成功。不夸大,把它作为部分意义上的"可控操作"放在这里。

全身骨骼的右手抓着水瓶,搬到被命令的"正上方"目标并保持。不是抛掷,而是持续抓住着搬运

而这种搬运最有意思的应用,就是"搬到嘴边"——也就是进食、喝水动作。把命令目标切换成"体侧·嘴边",肌肉骨骼的手就会试图把抓住的水瓶朝身体方向拉近(见下面的 GIF)。不过——这恰恰正是上表中差 0.27 m 停住的那个最难目标,在这个时间点上还没能够到嘴。

当对同一个策略下达"往体侧·嘴边"的命令时,把抓住的水瓶朝嘴边拉近的"喝水/进食动作"的萌芽。在这个时间点上,它差 0.27 m 停住,还没能完全够到嘴

当时我几乎要下结论:"够不到的原因,是单一线性协同的容量不足。"——但那是错的。接下来的第 II 部,就是把这个错误测出来、并揭穿它的故事。

8. 为什么一边输了,另一边却赢了

明明是同一个模型、同一种控制表征、同一套优化,站立却输了,抓握却赢了。决定性的差别,我认为在于任务的物理稳定性

  • 站立是不稳定平衡。倒立摆,哪怕只是一点点倾斜,也会呈指数级增大。控制必须"不停地、迅速地、正确地"持续扶正,只要稍有差错就会发散、摔倒。对没有记忆的线性策略来说,这份担子太重了。
  • 抓握(在这个设定下)是稳定的。肩膀被固定,目标(举高)一旦达成就容易维持。控制上的一些粗糙不至于致命。

不过,若断言稳定性是唯一的差别,那就不诚实了。至少存在三个混杂因素:①触及(reach)已经解决了(抓握时手一开始就在离水瓶 0.1m 的地方)、②奖励的直白程度(抓握是"越高越好"这种单调而稠密的奖励/站立是基于存活时间,于是出现了"慢慢倒下"的欺骗)、③自由度的差异(抓握是肩膀固定,没有那个把站立害死的、不稳定的浮动基座 6 自由度)。主因是稳定性,但这些也在起作用——过于漂亮的故事会把单一原因夸大。

而 §7 的搬运(place),是这个判断的第三个数据点。舞台和抓握是同一个稳定的操作变体(不会摔倒),可就在往其中加上"搬到命令指定的地方"这一控制性要求的一瞬间,就从完全胜(抓握)跌到了部分胜(三分之一)。难度不仅取决于任务的稳定性,也取决于所要求控制的"聪明程度"

收获可以这样总结:"AI 能做什么/不能做什么",不仅由策略的聪明程度决定,还由任务本身的稳定性(以及立足点、奖励、自由度、所要求的控制性)决定。同一件工具,选对了擂台就能赢,选不对就会诚实地摔倒。而且在这三个任务里,我都在公开之前用一次确认,揪出了"分数在涨、内容却跟不上"的欺骗(慢慢倒下/把水瓶放着不管)——这就是本文一以贯之的做法。


第 II 部 —— 让它喝水:从一句“根本没进嘴”开始,追踪那只穿地而过的瓶子

在第 I 部的 §7 中,我写道喝水动作“在离目标 0.27m 处停住了,这是容量的极限”。从这里开始,是我通过测量意识到那个结论是错的、并把它修正的过程。这篇文章的主角不是“喝到了”,而是在那之前的、朴素却又是每个碰 MuJoCo 的人都会踩一次的 bug

9. 起点:一句“根本没进嘴”

在第 I 部之后,我把目标点重新放到“真正的嘴”上,然后判定:在同一套控制、同样的容量下,手竟然够到了嘴边。数字上确实够到了(瓶子中心与目标点的距离只有几厘米)。我甚至已经开始动笔写“喝水动作做成了”。

然而,看过渲染出来的视频的那个人(给我指示方向的一方)回给我三条否决意见

  1. “根本没进嘴”——瓶子停在喉咙、锁骨一带,根本没够到真正的下颌(下巴)。
  2. “嘴也没张开”——嘴自始至终是闭着的。看上去根本不像在喝。
  3. “看不出在干什么”——只有一台正面摄像机,读不出手臂在怎么动。

这正是这个系列一直恪守的作法——“一旦出现异常好的结果,在自以为赢了之前先质疑内幕”——借由他人的眼睛发动起来的一刻。我相信了“距离只有几厘米”这个数字,却没有质疑画面。而人看了画面,一眼看穿了数字的谎言。诚实地,从撤回主张开始。

10. 第一处订正:目标原来是“喉咙”的高度

先看 (1)。我当作“嘴”设定的目标点是 (0.07, 0, 1.46)。可是一量真正的下颌几何,下颌底部高度约在 1.50m,嘴唇一带则在 1.55m 附近。我的目标 1.46 比下颌底部还低,也就是喉咙、锁骨的高度

瓶子被搬到的不是“嘴边”而是“喉口”,并在那里停住。距离数字之所以小,是因为它确实准确够到了那个错误的目标——只要目标放错了地方,达成率越高质量反而越差。我把目标改成真正的嘴唇高度 (0.05, 0, 1.55)

这同时也是对我自己在第 I 部 §7 写下的那句“单一线性协同缺乏把手臂拉回体侧的容量(所以够不到)”的自我订正。够不到的真正原因不是容量,而是把目标放错了位置。把责任推给容量,永远是最省事的逃路。不过为免误解——这里被推翻的只是“够不到嘴的原因是容量”这一错误归因,而 §7 中看到的“把手臂精确放到任意一点的精度只有三分之一”这一单一线性协同的容量极限本身,如今依然成立。

预支一条教训:在下结论说“这是算法的极限”之前,先质疑评分表(目标、奖励)是否正确。把停滞归因于能力,通常都为时过早。

11. 可是,这个模型没有嘴

接下来是 (2)“嘴没张开”。这不是我的设定失误,而是模型本身的局限。操作用变种为了把手臂和手做到极其精密,代价是把头部当作单一刚体处理。既没有颌关节,也没有面部肌肉。那 81 条肌肉全都属于手臂、手和躯干,没有任何一条能张开嘴

指示很明确——“改造成能张开下颌的模型”。于是我把原模型(251MB 的资产群)整套复制、原封不动地放着,只编辑其中操作用的躯体定义文件,把头部的下颌几何切分成独立的子刚体 mandible(下颌)+ 铰链关节 jaw_open。给这个关节施加角度,下颌就会下降、嘴就张开。

不过这里从一开始就有一条诚实的划界这个下颌上没有附着肌肉。所以下颌的开合不是用肌肉解出来的动作,而是直接写入位置的脚本(kinematic)手臂的抓握与搬运是那 81 条肌肉真真切切解出来的,而嘴的张开是演出——这个区别我会一路带到最后。

改造后的模型顺利读入了。关节多了一根,自由度从 48 → 49。肌肉数量(81)没变,所以学好的控制理应原样载入。我还分别确认了下颌闭合的画面和张开的画面,开合本身是能动的。——可是,一旦把学好的喝水控制载上去,事情就变得不对劲了。

12. Bug:加了下颌之后,瓶子穿地而过

在改造过的下颌模型上,我回放了已经学好抓握与搬运的控制(§7 的 champion)。期待是“像原来一样抓住、搬到嘴边”。实际发生的是这个。

Bug:在只是加了下颌的模型上回放学好的控制,手臂像在喝水一样抬向嘴边,可手里什么都没有。瓶子穿过地板消失了

肌肉骨骼像要喝水一样把手臂抬向嘴边。可手是空的,台子上的瓶子也消失了。控制试图去抓瓶子本该在的位置,而那里什么都没有,于是手臂扑了个空、逐渐发散。只不过加了一根下颌,抓握就彻底坏了。肌肉的数量、手臂的关节,我一样都没碰过——却是这样。

13. 找犯人:丢掉猜测,先测量

碰到这种情况,靠猜测断定原因、然后打补丁是最危险的。我一开始是这样估摸的——“既然加了一根关节,手臂关节的初始角度就一定错位了,是从一个奇怪的姿势开始的吧。”听上去很有道理。但这个系列的纪律是“与其说‘大概是 X 造成的’,不如说‘我要确认 X,确认方法是 Y’”。

于是我写了一个诊断,把原模型和下颌模型开局那一瞬的状态并排量出来。在两个模型上,比较手臂 7 个主要关节的角度、手掌的世界坐标、瓶子的世界坐标。结果如下。

量出的量 原模型 下颌模型
手臂 reach 关节 7 根的角度 (基准) (相同) 最大 0.0
手掌的世界坐标 (基准) (相同) 0.0
瓶子的高度 z 0.94 m −0.63 m −1.57 m
手~瓶子距离 0.108 m 1.667 m

我的猜测落空了。手臂的关节和手的位置,都与原模型完全一致(初始姿势没有坏)。坏掉的只有瓶子——本该在 z=0.94m 桌面上的瓶子,竟然从 z=−0.63m,也就是穿透了地板的地底开始。手处在正确的初始姿势。只有要抓的对象(瓶子)掉到了地面之下。所以手臂正确地朝“桌上瓶子本该在的位置”移动,而那里什么都没有。扑空的真相就是这个

如果在这里认定“一定是手臂错位了”而去改代码,那不但修不好,还会把本来正确的手臂控制给弄坏。测量,能防止修错地方

14. 真凶:keyframe(关键帧)由“编号”决定,加一根关节,就全都错位

为什么偏偏只有瓶子掉了,而且足足掉了 1.57m 高?这里藏着一个对所有接触 MuJoCo(乃至任何物理引擎)的人都受用的教训。

MuJoCo 会把模型的全部关节位置塞进一个一维数组 qpos。每个关节按照身体树状结构的顺序,在 qpos 中被分配到“从第几个到第几个”这样一个位置(槽位)。而定义“实验开始时姿势”的 keyframe,只是把那个 qpos 原封不动地按值排列写下来的一串数字——就像 qpos="-0.07 0.03 ... 0.94 ..." 这样。第 i 个值进入第 i 个槽位。由位置决定。

然后,我给头部加了一个下颌铰链 jaw_open。MuJoCo 给它分配了第 42 个槽位。于是——从第 42 个往后的关节,全部各自向后错开一位。保存瓶子位置的槽位,也从 42 挪到了 43。可是,keyframe 那串数字仍旧是 48 个,还拖着原模型的排列(加了下颌,却没更新开始姿势的数列)。模型的 qpos 已经增加到 49 个,keyframe 却只有 48 个。MuJoCo 会把缺的那 1 个在末尾用 0 补齐。结果,从第 42 个往后,全部错开一位被读取

画成图就是这样。

槽位编号:  ... 41    42(新)   43       44       45      46      47      48
本应内容:    ... 臂末  [颌=0]   瓶x   瓶y   瓶z   旋转…    旋转…    旋转…
keyframe实际:  ... 臂末   0        0        0        -1.57    0.32    0       0(padding)
                          ↑ 从这里开始,全部读取了前移一位的值

手臂(第 41 个槽位及之前)毫发无损——因为错位是从第 42 个才开始的。但瓶子不同。这里要插入一个重要前提:这个瓶子挂在“以桌面高度(0.94m)为原点的上下滑动关节”上,进入 keyframe 的值表示的是相对于这个原点的相对位移(米)。本来瓶子的上下滑动应当是 0(=就放在桌面上)。

可错开一位之后,那个高度栏里,流进了本该是相邻关节旋转角的值 -1.57。−1.57 本来是以弧度计的旋转角(约 −90 度)的数字。它却被原样当作垂直滑动的位移(−1.57 米)来读取——角度的数字,变成了距离的数字。于是瓶子从桌面高度下沉了 1.57m,出现在 0.94 − 1.57 ≈ −0.63m、穿透地板的地下

「0.94 − 1.57 = −0.63」之所以是相加(相对位移的叠加)而非相减,是因为瓶子的高度是从桌面算起的滑动量,而非绝对坐标——若跳过这一点,就会被质疑“z 不是应该整个被覆写成 −1.57 吗?这种吻合是不是太巧了?”。事实上,替本文核验构成内幕的另一个流程,在通过一手来源确认模型定义(瓶子挂在上下滑动关节上)之前,也一度差点判定“这里是有破绽的”。越是干净吻合的数字,越要回溯到机构本身去核实——这正是本系列的信条。

加一根关节,就等于把 keyframe 的所有槽位都错开一位,而我却让那串数字维持旧样、放着没管。绿色的测试是通过的(模型能加载,关节数和肌肉数也都对)。这是一个测试无法检测的、名为“位置错位”的安静 bug。而揭穿它的,不是测试——是看了视频的人的一句“没喝进嘴里”

15. 修复:把一个 0,放到正确的位置

一旦弄清原因,修法简单得让人扫兴。在原本 48 个的 keyframe 里,朝下颌槽位(第 42 个)插入一个 0(下颌闭合状态=角度 0),凑成 49 个。这样一来,第 42 个往后就会正确地错位到本该属于的槽位,瓶子也回到桌面上。

变更前(48个): ... 0.3141  0 0 0  -1.57 0.32 0
变更后(49个): ... 0.3141  0 0 0 0  -1.57 0.32 0
                          ↑ 插入一个下颌的 0

应用之后重新测量,下颌模型在数值上都与原模型一致

测量项 原模型 下颌模型(修复后)
瓶子起始 z 0.94 m 0.94 m
抬升量 +0.62m(至 z=1.56) +0.62m(至 z=1.56)
是否持续抓握?(手~瓶子) ✓ (0.043m) ✓ (0.043m)
到口(唇)的最终距离 0.050m 0.048m

手~瓶子相对位置向量的差为 0.0(在测量精度 1e-6 以内完全一致)——也就是说,就抓握与搬运而言,下颌模型再现了与原模型无法区分的动作。完全不需要重新学习。因为坏掉的不是控制,而是初始化数据的排列。

16. 喝下去了

修好之后,终于呈现出本来想要的画面。伸手去够台上的瓶子,抓住,搬到嘴边,然后下颌张开。

修复后:肌肉骨骼的右手抓住桌上的水瓶,抬升 0.62m 搬到唇边,口张开。81 条肌肉以闭环驱动手臂(仅下颌的开合为脚本驱动)

“口是否张开”也因为被手臂和瓶子挡住而不易看清,所以只把头部裁切出来,把开始和结束并排放在一起。

头部对比。左=开始时口闭合。右=结束时下颌明显下降、口张开,瓶子来到唇边

左(开始)口是闭合的,右(结束)下颌下降、口张开,瓶子抵达唇边。人给出的三条否决——够不到嘴、口不张开、看不出在做什么——至此全部解决。瓶子被搬到唇边一带(z=1.56m,就在 §10 定义的唇点 1.55m 之上),口张开,从一个斜侧视角能看清手臂的动作。

该带走什么(第 II 部的三条教训):① 在把停滞或失败归因于“能力的极限”之前,先质疑评分表(目标、数据) —— “够不到嘴是因为容量不足”,其实是“因为把目标放在了喉咙的高度”。② 在凭猜测改代码之前,先测量、锁定真凶 —— “手臂错位了”一测就落空,真凶是瓶子的初始位置。③ keyframe 和初始化数据由“编号(位置)”决定。一旦加了结构(关节),就必须重新做一遍 —— 忘了更新基于位置的初始化,测试照样是绿的,只有物体会悄悄掉到地板上。而揭穿这个安静 bug 的,不是测试,而是看了画面的人的违和感。把人留在回路里(Human-in-the-Loop),正是为了这样的瞬间。


第 III 部 —— evis:这具骨架成了吉祥物,又跳舞、又用筷子吃饭

从这里往后,记录的是把上文里「想让它站立、却诚实地摔倒」的那具拥有 700 条肌肉的骨架,做成吉祥物并用 mocap 驱动的过程。主角依然不是成功故事,而是**「一让它走路就变成像老人一样的驼背」——我如何不靠猜测、而是靠测量抓到那个真凶的过程**。

先把诚实的但书写在前面:第 III 部是运动学回放(kinematic replay,直接给出每一帧的关节角),并不是靠肌肉驱动。筷子也只是「看起来握着」的刚体道具(rigid prop),手指并没有真的握住。哪里是真实、哪里是演出,我会在最后明确写清。

17. 出发点 —— evis 这个「看得见内部的人体」

evis 的素体,与第 I 部相同,就是 MS-Human-700。它是一具全身 85 自由度(DoF)、拥有 700 条肌肉的、供生物力学研究使用的人体模型。没有皮肤,骨(白色)与肌肉、肌腱(红色)裸露在外。这副「剥去了皮肉的模样」,直接就成了这个角色的魅力所在。名字定为 evis,由来分为两层:

  • 惠比寿(Ebisu) —— 七福神之一,寓意吉利。
  • eviscerate —— 剖除内脏、去除皮肉之意,正是只剩骨与肌的 evis 的样子本身。

house style 是「白色的骨 + 昏暗的摄影棚 + 像点燃一样发光的红色肌肉 + 滑稽的白眼珠 + 在头顶晃动的呆毛(随头部动作延迟摆动的次生运动)」。肌肉发光的方式其中另有机关:在运动学回放里并没有肌肉活性,所以我是用各部位的「速度」来让它发红点亮的——挥得快的手臂会啪地一下亮起来。这样一来,动作所要求的运动模式就被可视化了出来。

18. 为什么选 mocap,而最初撞上的墙不是「技术」而是「许可证」

就算用手工做的正弦波(sine wave)让手臂挥动,动作也很僵硬。我想移植真正的 mocap。 然而最先撞上的不是技术,而是许可证。包含 evis 在内的 FullSense 采用商业 dual-license,因此 mocap 数据也必须能够商用。把知名的几个数据源查了一遍:

来源 格式 商用 SMPL 依赖 判定
CMU MoCap(cgspeed BVH 转换) BVH 采用
AMASS / AIST++ SMPL 参数 不可 需 SMPL(非商用) 排除
Mixamo FBX 产品内可用 需账号、数据不可再分发
Bandai-Namco / LaFAN1 BVH 不可(NC) 排除

SMPL 是人体网格的标准模型,但它是研究用(非商用)许可证,AMASS 和 AIST++ 都依赖它。它虽被广泛使用,却无法带进商业产品。答案是 CMU Graphics Lab Motion Capture Database。其许可证原文是 "This data is free for use in research and commercial projects worldwide"。它以 BVH 自成一体(不需要 SMPL),可以用 git 自动获取。不过有一点 caveat,就是「数据本身(即便是转换后的形式)也不得转售」。所以分发「已经过重定向的动画」是 OK 的,但我做到了不把 BVH 文件本身随仓库一起附带(需要时用获取脚本下载)。

教训:「技术上最好」和「可以分发」是两回事。如果一个研究演示有可能摇身变成商业产品,那么最好在最开始就以敌对的态度把许可证核实清楚

19. 重定向的机制 —— 移的不是「旋转」而是「位置」

BVH 的骨架(标准人形)与 MS-Human-700,关节的构造完全不同。MS-Human-700 源自 OpenSim,比如骨盆根部不是 quaternion 的自由关节,而是6 个独立的 1 自由度关节,肩部则是夹着「幽灵刚体(ghost body)」的复杂肩带结构。想把旋转角原样搬过去简直是地狱。

于是我改用基于位置的做法。用 forward kinematics 解算 BVH,只取出各关节的「世界坐标(点云)」。旋转一概不移。之后,用**微分 IK(differential IK,Levenberg-Marquardt)**解算关节角,让 evis 对应的身体部位与那个点云重叠。瞄准手、肘、膝、脚、头等约 16 个点,并且从上一帧温启动(warm-start)来求解,所以 1 帧约 30ms,一段 5 秒的片段几秒就能完成重定向。因为不移旋转,那个棘手的肩带,IK 也会自行把它分配成一个漂亮的形状。

这里有一个运动学特有的陷阱。 MS-Human-700 在膝、肩带、手腕上带有 42 个等式约束(equality constraint,「这个幽灵关节由肩部角度的多项式决定」)。然而在直接设置关节角的回放中,这个约束并不会被 MuJoCo 强制执行。放任不管的话,IK 就会把肩胛骨的幽灵刚体拧向莫名其妙的方向,肩部就坏了。所以每一帧,我都自己去评估那个多项式,并把从属关节写回去。这样一来,膝的 screw-home(旋锁)和肩的节律就都能正确运动了。(——正如你已经注意到的,第 II 部的 keyframe bug 里那个「由位置决定的数据得自己去照料」的主题,在这里又换了一副面孔出现了。)

20. 正题 —— 「腰像老人一样弯曲」,把这个真凶测出来、抓住它

先让它走起来。能动。……可是请人来审阅时,只得到一句话:「腰弯得像个老人」。从侧面看确实如此:头往前探、后背拱起,是典型的“驼背”。从这里开始才是本章的主角——不靠猜测、而靠测量把真凶逼到墙角的过程

假设1:头往前伸了? 测量头与骨盆的前后差,是 0.3cm。几乎正上方。排除。

假设2:mocap 源本身就是驼背? 测量原始 BVH 的颈部前倾,是 9.5°(轻微),躯干是 1.6°。几乎笔直。原始数据不是驼背。是动作重定向(retargeting)在放大它。

假设3:姿势正则化太弱? 加了一项把脊椎、颈部向中立位轻轻拉回的项,但视觉上几乎没变化。它被标记点的拉力压过去了。

于是停止猜测,把关节角一个一个实测了一遍。真凶随之现身:

T12_L1_FE      = -28.6°   (可动域 [-29, +29] → 贴死在负向极限)
T1_head_neck_FE = +28.6°  (可动域 [-29, +29] → 贴死在正向极限)

中背(胸腰椎)贴到了一侧极限,颈部贴到相反一侧的极限,两者都饱和了。这就是驼背的真身。那么,IK 为什么会把关节弯到极限?

真凶是标记点高度弄错了。 evis 的 thoracic1(胸椎最上端)位于骨盆上方 0.50m(肩的高度)。可我却把 BVH 的 Spine1(骨盆上方 0.26m=中背)分配给了它。我是在试图把肩膀高度的身体往中背的高度拽下去,结果只有 3 段的脊椎被压缩,关节一路折到了极限。

修正很简单。改成高度相符的对应关系——thoracic12(上方 0.23m)←Spine1(0.26m)。把过度约束的 thoracic1/lumbar5/头顶标记点去掉,让姿势正则化稍稍起点作用。结果,步行变直立了,而且舞蹈里那种表现性的背屈(前屈、后仰)保留了下来——因为那是标记点真正要求的动作。

教训(这是第 III 部我最想说的):如果按「大概是头往前伸了」去修,就会修错。 一旦去测评分的内幕(关节角),真凶竟是意想不到的「标记点高度」。这和第 II 部的「先测量」是完全相同的精神——猜测之前先测量。

21. 把眼睛做成 3D 之后,那些小把戏全都消失了

evis 的表情(>< 或 ◎◎)最初是 2D 的贴画。把头的 3D 位置投影到相机上、求出屏幕上的眼睛坐标,再用深度缓冲判断「这只眼睛现在看得见吗(是不是转到侧面被骨头挡住了)」,然后把眼白和颜文字盖上去……就是这么个机制。动是能动,但留下了一个 bug:侧脸时,较远那只眼睛会被投影到背景上、悬在半空

这时来了一句话:「把眼白建成 3D 的球、跟模型合为一体,直接在眼球上用纹理画出表情不就行了?」 这才是正解。给每种表情烤一张立方体纹理(正面是颜文字、其余面是白色巩膜),嵌进头部做成眼球的球,按每种情绪各备一套,只留要显示的那一组、其余全部透明,这样来做替换。于是:

  • 眼睛会随头一起旋转
  • 骨头(头颅)会自然地挡住眼睛——转到后脑勺,眼睛就消失。既不需要深度判断,也不需要投影计算。
  • 任何角度都正确。跳舞时头转过去、摔倒时朝下看,都会自动变正确。

投影+深度的这套 2D 小把戏被整个删掉了(约 80 行)。情绪有 8 种(neutral 的圆眼 ●●、joy ><、surprise ◎◎、love ♥♥ 是彩色 emoji 纹理、dead ×× ……)+ 任意文字。顺便说一句,全角的 >< 被误判成 emoji、导致画得很淡的 bug,也是在这里修好的(判定范围弄错了)。

evis 的表情

接到物理摔倒的 rollout 上之后,evis 一边倒下、一边把表情从 neutral → ◎◎(下落)→ ××(崩塌) 地变换,还成了一段“现场解说”。

evis 摔倒时的反应

22. 动作库 —— 走、跑、跳、跳舞、用筷子吃饭

只要动作重定向(retargeting)生效,接下来只需换 BVH,它就什么都能跳。从 CMU 收集、移植到 evis 上的有:

  • 步行 / 跑步 / 跳跃(subject 7 / 9 / 13)
  • 现代舞 / 萨尔萨(subject 5 / 60)
  • 足球射门(subject 10)
  • 用筷子吃饭 —— 把 CMU 的「喝苏打水」(把手送到嘴边的动作)做了动作重定向,让 evis 的手握住两根筷子 + 一粒食物。筷子的朝向,是把「手→口」方向实测出来、在手的局部坐标里固定的,所以手臂一抬起,筷尖就朝向嘴。
  • 物理摔倒 + 会反应的脸

evis 步行

↑ 修正姿势后的步行。头抬起来了,脊椎笔直。摆动的腿发红发火。

evis 用筷子吃饭

↑ 用筷子吃饭。手里握着的两根筷子 + 一粒食物,会在手臂抬起时朝向嘴。

evis 动作集锦


第 IV 部 —— 整体的诚实边界,以及下一步

23. 哪些是真实的,哪些是演出(整合版)

诚实披露(honest disclosure)是本系列的核心,所以我把整篇文章的边界集中到一处、明确说清楚。

  • 真实层(研究层·肌肉驱动):第 I 部的站立、抓握、搬运,以及第 II 部喝水动作里的手臂抓握与搬运,都是用进化计算学习 81(或 100)条肌肉得到的闭环肌肉驱动。IK、力封闭(force-closure)、keyframe 一致性的验证也都是真实的。站立的那次“负”,以及在 n=3 下被反证的“记忆没有效果”,也都是我不加夸大保留下来的实测。
  • 演出层(角色层):第 II 部的下颌开合是运动学(kinematic)的(因为没有搭载面部肌肉,所以是脚本驱动),而且是把 qpos 直接写死,开到了超过设计可动范围(+0.05 rad)的 +0.6 rad——我明确标注:这是无视物理的演出。第 III 部 evis 的跳舞、奔跑、筷子都是运动学回放,肌肉那抹红色发光是“速度”的替代量(proxy)(并非肌肉活性),眼睛、呆毛是绑定(rig),筷子是刚体道具(手指并没有真的握住)。
  • 它本身并没有在“喝水”。这并不是在模拟液体的摄入,而是把瓶子搬到张开的嘴边的一个姿态。是喝水的“姿态(gesture)”,而不是喝水的“生理过程(physiology)”。
  • 省略之处:胸廓的肋软骨因为是仅含骨骼的模型而被省略了(这正是正面看起来空荡荡的原因)。我一直查证到解剖图上的正确形状(肋骨 1–7 连到胸骨,8–10 构成肋弓),但作为面向角色的近似来说太难处理,于是默认关掉了它。

总而言之:肌肉真正驱动的,只到“站(不过是诚实地承认失败)、抓、搬、喝水的手臂”为止。“张嘴”“跳舞”“握筷子”,是叠加在其上的外观层。如果把这条界线含糊掉再去展示 GIF,就会变成夸大,所以我用粗体保留下来。

24. 下一回

翻过三座山,evis 已经做到了“想要站立却诚实地摔倒、抓、搬、喝、跳、用筷子吃”。但留下的作业也很明确。

  • 让筷子“真的用手指握住”,不是运动学的延伸,而是用进化学习肌肉的另一座山。要看这只有 81 条肌肉的手,能不能不靠刚体道具,而是真的捏起筷子、把食物送到嘴边。
  • 把站立的那次负扳回来。既然单纯的积分记忆行不通,那么下一步就尝试递归(recurrent)策略、或明确的踝/腰策略、或分层结构。
  • 给评价(eval)本身加上一张脸。这次我把 evis 的表情接到了摔倒上;同样地,如果在步行学习里跌倒就变成 ××,如果进展顺利就用 >< 露出笑容——让它成为一只由仿真自己现场解说的吉祥物。

测量,诚实地把失败保留下来,然后再往上爬。越是得到好结果,就越要质疑其中的内幕;一旦卡住,就不要靠猜测而要去测量——这就是我在本系列里坚守的那一点。只有骨与肌的 evis 的故事,还会继续。


附录:方法(复现备忘)

不给出前提条件就抛数字是不诚实的,所以把前提都列在这里。

  • 模型:MS-Human-700(Apache-2.0)/ MuJoCo 3.10 / 积分步长 dt=0.002s。共有三个变种:全身 85 DoF·700 肌,步行用 36 DoF·100 肌,操作用 48 DoF·81 肌。
  • CPU:Intel Core(Ice Lake 世代移动版),8 核。进化评估为7 worker 并行,单次计测为单线程·空闲
  • 站立:步行用场景(带地面),每回合 6 秒,跌倒阈值=骨盆高度 0.6m(初始 0.95m)。CMA-ES,pop 16〜40,世代 60〜250。策略为肌肉协同(muscle synergy)(K=6 或 8)。有记忆/无记忆的对比各取 3 条随机种子的中位数,确认了单一种子上看似存在的差异(4.53 秒)会消失。
  • 抓握:操作用场景(肩部固定),每回合 3 秒,CMA-ES pop 28·K=8·250 世代中在 gen50 取得最优。接触摩擦 μ=1.0。
  • 搬运(place):同一操作用场景,以 3 个目标点(正上方/体侧/前方稍上)的平均值评分。观测=抓握 19 维+命令向量 3 维。从抓握 champion 做 warm-start(温启动)(命令输入的权重用零填充),sigma0=0.25。在 gen30-50 收敛到 fitness ~1021。
  • 喝水(下颌模型):一个派生模型,把操作用变种的头部下颌几何体分离为子刚体 mandible + 铰链 jaw_open(轴 z,设计可动范围 −0.7〜0.05 rad)。自由度 48→49,肌肉 81 保持不变。渲染时的张颌超出设计可动范围,直到 +0.6 rad 都是用 qpos 直写(不施加 limit),这属于演出层。Bug=下颌铰链落在 qpos 的第 42 位,把之后的所有槽位整体后移一位。keyframe 仍保持 48 个而做 zero-pad→旋转角 −1.57 被塞进了瓶子的上下滑动栏(相对于桌面 0.94m 的相对位移),于是 z=0.94−1.57=−0.63m,瓶子掉落。修复=在原本 48 个 keyframe 的第 42 位插入 0.0,补成 49 个。
import numpy as np
# orig_key48 = 原模型的 keyframe(48 值)
broken = np.concatenate([orig_key48, [0.0]])   # 只在末尾加了个 0 = 相当于 MuJoCo 的自动 padding(会坏)
fixed  = np.insert(orig_key48, 42, 0.0)         # 42 位(0-index)插入下颌的 0 = 正确(49 值)
# reset 之后立即测量瓶子的世界 z:
#   broken -> z = -0.63(地板下)   /   fixed -> z = 0.94(桌面上)= 与原模型一致
  • evis(mocap):mocap = CMU Graphics Lab Motion Capture Database(可商用·不依赖 SMPL)。获取方式为 scripts/fetch_cmu_dance.py --motion(BVH 不可再分发,故不随包附带,按需 on-demand 下载)。基于位置的微分 IK(Levenberg-Marquardt,仅用 numpy + mujoco,无需 scipy,~30ms/frame)。膝、肩带、手腕的 42 个等式约束每帧都会求值多项式并写入从属关节。表情则是把按情绪分类的立方体贴图眼球嵌入头部,用 alpha 做替换。
  • 实现:重定向器 src/onocollo/evolve/mocap_retarget.py,吉祥物 scripts/musculo_mascot.py,肌肉控制 src/onocollo/evolve/musculo.py。输出为 mp4(h264,用于 Qiita 内嵌)/ gif 按扩展名自动切换。
  • 数值的来历:全部取自 out/musculo/*.json 以及对已保存策略的重新运行。本文所有定量主张,在公开前都经过了由独立的另一进程对一次数据·实代码的再核对(fact-check)+编辑审校。RTF 的条件标注、以 n=3 反驳站立的记忆效应、抓握的 force-closure(力封闭)验证、keyframe bug 的机理确认、张颌超出可动范围的披露,都是在收到这些指摘后修正的结果。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?