Generating Diverse and Natural 3D Human Motions from Text
HumanML3D:从文本生成多样且自然的 3D 人体动作
📅 阅读日期: 2026-10-11
🏷️ 板块: 14 Human Motion · 文本生成动作 · 数据集 · 时序 VAE · 评测协议 · CVPR 2022
ℹ️ 笔记对照 CVPR 2022 正文(含 Table 1–4)、补充材料(附录 A–K),以及官方仓库 EricGuo5513/HumanML3D(数据处理)与 EricGuo5513/text-to-motion(模型与评测)整理,两个仓库都取的
main。这篇论文没有 arXiv 版本:旧版笔记里写的 arXiv 2204.09419 实际是一篇天体物理论文(超亮超新星的中红外研究),已删除。
📋 基本信息
| 项目 | 链接 |
|---|---|
| 论文 | Generating Diverse and Natural 3D Human Motions from Text |
| 会议 | CVPR 2022(pp. 5152–5161) |
| 论文 PDF | CVF OpenAccess |
| 补充材料 | Supplemental PDF |
| 作者 | Chuan Guo, Shihao Zou, Xinxin Zuo, Sen Wang, Wei Ji, Xingyu Li, Li Cheng |
| 机构 | University of Alberta |
| 发布时间 | 2022 年 6 月(CVPR 2022) |
| 项目主页 | ericguo5513.github.io/text-to-motion |
| 数据集仓库 | EricGuo5513/HumanML3D |
| 代码 | EricGuo5513/text-to-motion |
🎯 一句话总结
HumanML3D 把 AMASS 与 HumanAct12 的动捕统一到 20 fps、同一副骨架与朝向,再请人每段写 3 句话,得到 14,616 段动作 / 44,970 条描述 / 28.59 小时;论文同时给出两阶段生成方法(text2length 采长度 → 时序 VAE 在 snippet code 上逐段生成)和一套基于对比学习特征的评测器(R-Precision、FID、MultiModal Dist 等)。后来的文本生成动作论文大多在这套数据和评测器上报数。
🎮 本文内嵌 1 段讲解动画、1 段配音视频和 3 个交互演示(不用装任何东西): 十二幕动画:HumanML3D 全流程 —— 约 194 秒串完「一句话生成动作卡在哪(论文图 1)→ 动作从哪来 → 每段配三句话(标注界面)→ 一帧 263 维(片段 012314 的真实骨架)→ 每 4 帧一个 snippet code → Text2Length(补充材料图 6 的长度分布)→ 文本编码器与词注意力 → 先验、后验、生成器 → 三项损失与课程学习 → 评测器与 R-Precision → 结果(图 4、用户研究)→ 消融、局限与遗产(图 6、失败案例)」。空格播放/暂停,← → 换幕,也可以直接点分幕标签跳着看。 配音讲解视频 —— 同样十二幕,加中文配音与字幕,10 分 59 秒竖屏,可下载 片段 012314 的 263 维 —— 拖帧号、改触地阈值、勾「镜像」,看真实骨架、触地标签和镜像扩增 Text2Length 采样 —— 补充材料图 6 的八条长度分布(读图值),拖累积概率、随机采样,看同一句话能生成多长的动作 R-Precision 玩具 —— 32 条描述里排第几:生成偏差和「描述本身有歧义」各让分数掉多少
🚶 具体实例拿官方仓库自带的一段真实片段
012314(AMASS 里 Eyes_Japan 的一段网球一发,170 帧特征)从头走一遍:三条描述怎么分词打标、263 维逐段拆开、触地阈值换算、Z-score 放大、压成 42 个 snippet code、补充材料图 6 的长度分布与交叉熵、时序 VAE 每一步的维度账、课程学习里它参加哪几个阶段,最后列出论文与代码的出入。
📌 核心数据集参数
| 维度 | 数值 | 出处 / 备注 |
|---|---|---|
| 动作段数 | 14,616 | 论文 Table 1;官方 index.csv 正好 14,616 行 |
| 文本描述数 | 44,970 | 论文 Table 1;平均每段 44,970 / 14,616 ≈ 3.08 条 |
| 总时长 | 28.59 h | 论文 Table 1;单段 2–10 s,论文写平均 7.1 s |
| 词表 | 5,371 个不同词 | 论文 Table 1;描述平均 12 词、中位数 10 词 |
| 帧率 | 20 fps | 超过 10 s 的随机裁成 10 s |
| 骨架 | 22 个关节(SMPL 的前 22 个关节) | 存的是统一骨架下的关节位置与旋转特征,不是 SMPL 的 pose / shape 参数 |
| 每帧特征 | 263 维 | 根节点 4 + 局部位置 63 + 6D 旋转 126 + 局部速度 66 + 触地 4 |
| 来源 | AMASS 17 个子集 13,425 段 + HumanAct12 1,191 段 | 按官方 index.csv 的 source_path 数出来(18 个来源);最大的是 AMASS 里的 KIT 子集(4,648 段) |
| 扩增与切分 | 镜像后 29,232 段;train : test : val = 0.8 : 0.15 : 0.05 | 三个 split 文件实际合计 29,228 行(train 23,384 / test 4,384 / val 1,460),少的 4 行是 index.csv 里两段只有 1 帧的退化片段(009707、011059)和它们的镜像 |
🎬 十二幕动画:HumanML3D 全流程
(本节含动画演示,需要启用 JavaScript)
📺 配音讲解视频(可下载)
(本节含讲解视频播放器,需要启用 JavaScript;也可以直接下载 mp4(11.7 MB))
📖 动画之后的正文默认全部折叠:动画覆盖到的那几节(要解决什么问题、方法详解、实验结果)按小节收起,再往后的具体实例、源码对照、工程价值、讨论、面试与参考来源各整块收起。想细读哪一块就点开对应的折叠条,内容一字未删;流程图和三个交互演示留在外面,目录里的标题依旧可以直接点,会自动展开所在折叠块,左侧目录顶部还有「展开全部文字」一键铺开。
动画第 3–5、7–9 幕里的片段
012314(骨架、263 维、触地、42 个 snippet code、注意力示意、课程阶段),第 6 幕从补充材料图 6 量出来的两条长度分布,和三个交互演示、下文「🚶 具体实例」是同一组数字。
❓ 要解决什么问题
the figure rises from a lying position
and walks in a counterclockwise circle ..."] --> OLD["已有方法
Seq2Seq / Language2Pose / Text2Gesture"] OLD --> P1["确定性一对一"] OLD --> P2["长度固定"] OLD --> P3["只有 KIT-ML
3,911 段,偏走路"] TXT --> C1["① 长度可变"] TXT --> C2["② 一句多解"] TXT --> C3["③ 文本有短有长"] style OLD fill:#fdebd0,stroke:#e67e22 style C1 fill:#e8f4fd,stroke:#1f78b4 style C2 fill:#e8f4fd,stroke:#1f78b4 style C3 fill:#e8f4fd,stroke:#1f78b4
已有方法的四个短板
📖 展开文字:输入短、确定性(动作趋于僵住)、长度固定、只有 KIT-ML
论文第 1 节列出当时(2022 年以前)文本生成动作的共同问题:
- 输入短:通常只处理一个短句;
- 确定性:任务被写成确定性的 sequence-to-sequence,一句话只能生成一段动作,生成结果「趋于静止、没有生气」(stationary and lifeless);
- 长度固定:生成长度要事先给定。评测时基线直接用真实长度,这句出处是 Table 2 的表注和补充材料附录 C(三个确定性基线还额外拿到了真值初始姿态:”These methods requires initial pose as well as ground truth motion length during inference”);
- 数据少:唯一可用的数据集 KIT Motion-Language 规模小、以走路为主。论文正文第 1 节写 3,010 段,Table 1 与第 2 节写 3,911 段 / 6,278 条描述——两处数字不一致,官方数据集仓库 README 用的是 3,911 / 6,278。
三个内在难点
📖 展开文字:同一句话 → 长短不一、做法不一;文本本身从短语到长句
- 长度可变:同一个模型生成的动作长度应当不同(「挥手」可长可短);
- 一句多解:同一句描述通常有多种合理的做法(论文图 1 同一句话的两次生成);
- 文本形式多样:描述可能是一个短语,也可能是一串带先后顺序的复杂长句。
论文的回应分两部分:方法上是 text2length + text2motion 两阶段,中间表示换成 motion snippet code;数据上构建 HumanML3D,覆盖日常、运动、杂技、舞蹈等动作,不止走路。
🔧 方法详解
263 维 / 帧"] -->|"2 层 Conv1d k=4 s=2"| CS["snippet code c_s^1..c_s^T
T = T'/4,512 维"] CS -->|"2 层 ConvTranspose1d"| PH["重建姿态 + 触地"] end subgraph INF["(c) 推理"] X["文本"] --> TL["text2length
50 类长度分布"] TL -->|"采样 T"| VAE["时序 VAE
先验采 z_t,生成器滚 T 步"] X --> TE["文本编码器
GloVe + 词性 → Bi-GRU"] TE -->|"句向量 s / 词特征 w"| VAE VAE -->|"ĉ_s^1..ĉ_s^T"| DEC["运动解码器 D"] DEC --> OUT["4T 帧动作"] end style CS fill:#e8f4fd,stroke:#1f78b4 style VAE fill:#eafaf1,stroke:#27ae60
1. 数据集构建:从动捕到「每段三句话」
📖 展开文字:20 fps、≤10 s、统一骨架与朝向、AMT 标注、镜像扩增
论文第 4 节与补充材料附录 D:
- 来源:HumanAct12 与 AMASS,两者都是公开动捕,覆盖日常(walking、jumping)、运动(swimming、karate)、杂技(cartwheel)、艺术(dancing),但都没有文字描述。官方
index.csv一共 18 个来源:17 个 AMASS 子集 13,425 段(KIT 4,648、CMU 2,913、BMLmovi 1,839、Eyes_Japan 1,465 ……)加 HumanAct12 1,191 段。 - 归一化:缩放到 20 FPS;超过 10 秒的随机裁成 10 秒;重定向到一副默认人体骨架模板;初始朝向统一转到 Z+。代码里「统一骨架」是三步:按腿长比例缩放 → 逆运动学求关节旋转 → 套上模板(样例
000021)的骨长再正运动学(uniform_skeleton())。 - 标注:在 Amazon Mechanical Turk 上招募英语母语、平均通过率 >92% 的标注者,每段至少写 5 个词,每段收集 3 条来自不同标注者的描述;界面(补充材料图 3)鼓励写清动作类型、方向、身体部位、速度、轨迹、相对位置与风格,避免过于笼统(a man walks)或过于具体(绝对距离、角度),说明不用描述开头的 T-Pose、也不要写相对地面的绝对位置;动作太复杂时允许只描述其中一段并标出起止秒数;不合格描述人工剔除。
- 统计:14,616 段 / 44,970 条描述 / 5,371 个词 / 28.59 小时;平均 7.1 秒,最短 2 秒、最长 10 秒;描述平均 12 词、中位数 10 词。
- 扩增与切分(第 5 节):把动作左右镜像,同时替换描述里的关键词(left ↔ right,仓库 README 还举了 clockwise ↔ counterclockwise),数据量翻倍;按 0.8 : 0.15 : 0.05 切成训练 / 测试 / 验证。
几个小核对(我们在官方 texts.zip 与 index.csv 上数的):
- 28.59 h × 3600 ÷ 14,616 段 = 7.04 s,与论文写的「平均 7.1 s」同一量级(差异可能来自取整或统计口径,这是我的推测);
- 14,616 个非镜像文本里一共 43,692 行描述,平均 12.37 词、中位数 10;1,617 条(3.70%)只描述了一个子区间(起止秒数不是 0.0),333 条不足 5 个词。
2. 姿态表示:一帧 263 维
📖 展开文字:根节点存角速度、xz 速度和绝对高度,关节存相对根的位置 / 6D 旋转 / 速度,再加 4 维触地
论文第 5 节把一帧定义为元组 $(\dot r^a, \dot r^x, \dot r^z, r^y, j^p, j^v, j^r, c^f)$:
| 分量 | 含义 | HumanML3D 维数(代码) |
|---|---|---|
| $\dot r^a$ | 根节点绕 Y 轴的角速度 | 1 |
| $\dot r^x, \dot r^z$ | 根节点在 XZ 平面的线速度 | 2 |
| $r^y$ | 根节点高度(绝对值,不是速度) | 1 |
| $j^p$ | 关节位置(ric):水平方向相对根节点、转到根节点的朝向下,高度仍是离地的绝对值 | $3 \times 21 = 63$ |
| $j^r$ | 关节的 6D 连续旋转 | $6 \times 21 = 126$ |
| $j^v$ | 关节在根坐标系下的速度 | $3 \times 22 = 66$ |
| $c^f$ | 脚跟 / 脚尖速度阈值化得到的触地标签 | 4 |
合计 $1+2+1+63+126+66+4 = 263$;KIT-ML 只有 21 个关节,是 251 维(补充材料附录 A)。论文还写了两句容易漏的:训练时所有动作都裁成 4 的倍数帧(这是 $T = T’/4$ 成立的前提),姿态处理沿用文献 [13](Holden 等人的 PFNN)的做法。
设计意图(论文没展开,下面是我们的理解):根节点在水平方向只存速度、关节的水平位置都以根为参照,全局平移和水平朝向就不进特征;高度存绝对值,是因为蹲下、跳起需要它。6D 旋转避免四元数 / 欧拉角的不连续——它是代码从关节位置反解逆运动学得到的,不是 SMPL 的 pose 参数;触地标签是由速度阈值得到的 0 / 1,不是接触力,用来让解码器预测脚是否着地、压制脚滑。
(本节含交互演示:片段 012314 的 263 维,需要启用 JavaScript)
3. 运动自编码器:每 4 帧一个 snippet code
📖 展开文字:两层 kernel 4 / stride 2 卷积,L1 重建 + 稀疏 + 平滑,触地只给解码器
论文 3.1 节。编码器 $E$ 在时间轴上做一维卷积,把姿态序列 $P=(p_1,\dots,p _ {T’})$ 变成 snippet code 序列 $C_s=(c_s^1,\dots,c_s^T)$,解码器 $D$ 用反卷积还原:
\[C_s = E(P), \qquad \hat P = D(C_s)\]损失(式 2):
\[\mathcal{L} _ {E,D} = \sum _ {t'} \lVert \hat p _ {t'} - p _ {t'} \rVert_1 + \lambda _ {spr} \sum_t \lVert c_s^t \rVert_1 + \lambda _ {smt} \sum_t \lVert c_s^t - c_s^{t-1} \rVert_1\]- 两层卷积、kernel 4、stride 2,所以 $T = T’/4$;code 是 512 维(附录 A)。代码里编码器每层卷积后接 Dropout(0.2) 和 LeakyReLU(0.2),解码器不带 Dropout。
- 触地标签不喂给编码器,只让解码器预测,用来防脚滑。
- $\lambda _ {spr} = \lambda _ {smt} = 0.001$(附录 A):稀疏项压 code 的幅度,平滑项让相邻 code 连续。
- 论文说一个 code 的感受野是 8 帧、约 0.5 秒;按两层 $k=4, s=2$ 卷积严格算是 $4 + 3 \times 2 = 10$ 帧,20 fps 下正好 0.5 秒(8 帧只有 0.4 秒;10 帧是我的推算)。
- 自编码器单独先训:官方命令
--window_size 24,即每次取 24 帧(6 个 code)的窗口。
为什么要这一层:相比逐帧姿态,snippet code 携带局部时间语义,生成器只需走 $T’/4$ 步。Table 4 消融里去掉它(w/o SnC),R-Precision top-1 从 0.455 掉到 0.370;论文图 6 观察到去掉它之后,动作开头看着合理、贴合上下文,越往后越跟不上文字描述(论文归因于缺少时间上的刻画)。
4. Text2Length:从文本得到长度分布
📖 展开文字:50 个离散长度类、交叉熵训练、推理时按分布采样;补充材料图 6 的八条分布
论文 3.2 节:把「动作多长」当成条件密度估计 $p(T \mid x_1,\dots,x_M)$,论文说沿用 pixelCNN 的做法。文本编码器得到句向量,接 MLP + softmax,输出离散长度上的多项分布;每增加 1 等于 4 帧,$T _ {max}=50$ 对应 200 帧 = 10 秒;用交叉熵训练。推理时从这个分布采样,同一句话因此能生成不同时长的动作。代码里这是一个独立的 MotionLenEstimatorBiGRU(不和时序 VAE 共用文本编码器),MLP 是 1024 → 512 → 256 → 128 → 50。
补充材料附录 F 与图 6:训练时丢掉不足 40 帧(2 秒)的动作,所以图里画的是 10 到 50;概率高的长度离真实值都不远;周期动作(如「挥左手」)的分布比非周期动作(如「绊一下、向右弯腰」)更平。我们把图 6 的八张子图按像素量了一遍(读图近似值,零线附近 1–2 像素被坐标轴挡住,误差约 ±0.001):
- 八张里有 3 张的众数正好等于真实长度(34 / 49 / 40);偏得最远的是「左右各踏一步」,众数 21、真实 25;
- 「挥左手」是双峰(16 和 44 附近),「跑去搬东西再走回」也是双峰(30 和 49 附近);
- 按读图值算熵:「挥左手」3.28、「绊一下」2.29(40 类均匀分布的上限是 3.69)——周期动作更平,和论文的说法一致;
- 图里 k = 50 那一格八张都是空的:代码是
num_classes = 200 // 4 = 50,类下标 0–49。
(本节含交互演示:Text2Length 采样,需要启用 JavaScript)
5. Text2Motion:时序 VAE
📖 展开文字:词注意力、先验 / 后验 / 生成器三个 GRU、到达倒计时位置编码、ELBO
论文 3.3 节。文本编码器给出词特征 $w _ {1:M}$ 与句向量 $s$;时序 VAE 由生成器 $F_\theta$、后验 $F_\phi$、先验 $F_\psi$ 三个循环网络组成,逐个生成 snippet code。
- 文本编码器:GloVe 300 维词向量 + 词性(POS)标签;另外人工整理一个动作相关词典,把方向(DIR)、身体部位(BOD)、物体(OBJ)、动作(ACT)四类词单独标出来,标签经嵌入后加到词向量上,再进双向 GRU(补充材料图 2(a))。句向量用来初始化 VAE 各 GRU 的隐状态,词特征在每一步经注意力进入。
- 局部词注意力(式 4):查询来自生成器上一步隐状态,键 / 值来自词特征;每一步只算一次,三个网络共用这一个注意力向量:
- 到达倒计时位置编码(式 5):每一步加上 $\mathrm{PE}(T-t)$,$\mathrm{PE} _ {T-t, 2i} = \sin\big((T-t)/10000^{2i/d}\big)$、$\mathrm{PE} _ {T-t, 2i+1} = \cos\big((T-t)/10000^{2i/d}\big)$,$d = 1024$;加在「输入拼接 → MLP」之后、进 GRU 之前。论文的动机是让网络知道「现在在哪、还要走多远」;Table 4 里去掉它 top-1 只掉 0.011(0.455 → 0.444),论文自己也说它和词性标签的影响相对小。
- 三个网络的输入:后验看 $[c_s^{t-1}, c_s^t, w _ {att}^t]$,输出 $\mathcal{N}(\mu_\phi(t), \sigma_\phi(t))$;先验看 $[c_s^{t-1}, w _ {att}^t]$,输出 $\mathcal{N}(\mu_\psi(t), \sigma_\psi(t))$;生成器看 $[c_s^{t-1}, w _ {att}^t, z_t]$,重建 $\hat c_s^t$。训练时 $z_t$ 从后验采,推理时从先验采。三个 GRU 都是单层、隐层 1,024(补充材料附录 A),$z$ 是 128 维。
- 先验不是固定的 $\mathcal{N}(0, I)$,而是学出来的 $p_\psi(z_t \mid c_s^{1:t-1}, c)$。整个模型最大化一个带权的变分下界(式 3):
条件里的 $1:t$ 靠 GRU 的循环记忆实现;下面式 6 的 L1 损失是它的实用版。
- 最后用预训练的解码器 $D$ 把 code 序列还原成姿态;text2motion 训练时 $D$ 一起微调。
6. 训练方案:三项损失 + 课程学习 + teacher forcing
📖 展开文字:λ_KL = 0.01、从短到长逐个加 code、p_tf = 0.4、c^0 是平均姿态
总损失(式 6):
\[\mathcal{L} = \mathcal{L}^{code} _ {rec} + \lambda _ {mot}\,\mathcal{L}^{mot} _ {rec} + \lambda _ {KL}\,\mathcal{L} _ {KL}\]其中 $\mathcal{L}^{code} _ {rec} = \sum_t \lVert \hat c_s^t - c_s^t \rVert_1$,$\mathcal{L}^{mot} _ {rec} = \sum _ {t’} \lVert \hat p _ {t’} - p _ {t’} \rVert_1$(生成的 code 经正在微调的解码器 $D$ 还原后,和真值姿态比),$\mathcal{L} _ {KL}$ 是逐步的后验—先验 KL。附录 A:$\lambda _ {mot}=1$,$\lambda _ {KL}$ 在 HumanML3D 上取 0.01、KIT-ML 上取 0.005(附录 J 的表里写成 0.05(K),是笔误,官方 README 的 KIT 命令也是 0.005);Adam,学习率 $2\times10^{-4}$,解码器 $D$ 的学习率小 10 倍($2\times10^{-5}$)。
- 课程学习:先只学前 $T _ {cur}$ 个 code,只用长度不少于 $T _ {cur}$ 的动作;验证集重建损失开始上升就多加一个 code,直到 $T _ {max}$(附录 A:$T _ {max}=50$、$T _ {cur}=8$;代码从 10 起步,见「🚶 第 8 步」)。
- teacher forcing / scheduled sampling:以概率 $p _ {tf}=0.4$ 对整条目标序列用真值 code 作下一步输入,否则用自己生成的。
- 边界条件:$c_s^0$ 是编码器 $E$ 对平均姿态的输出。
- 附录 A 还提到:Z-score 归一化后,把 $(\dot r^a, \dot r^x, \dot r^z, r^y, c^f)$ 的幅度放大 5 倍以强调它们(官方脚本实际是 25 倍,见「🚶 第 4 步」);训练时以抛硬币的概率砍掉输入序列最后 4 帧,增加随机性。
7. 评测指标
📖 展开文字:对比学习特征提取器 + R-Precision / MultiModal Dist / FID / Diversity / MultiModality
没有现成的「动作版 Inception」,论文(补充材料附录 B 与图 2(b))自己训一对特征提取器:文本提取器与文本编码器同结构,动作先经预训练编码器 $E$ 变成 snippet code,再进隐层 1,024 的双向 GRU;两边的特征都是 512 维,$E$ 冻结、只训两个提取器。用对比损失训练:
\[D _ {s,m} = \lVert s - m \rVert_2,\qquad \mathcal{L} _ {Cta} = (1-y)\,D _ {s,m}^2 + y\,\max(0,\ m - D _ {s,m})^2\]$y=0$ 表示配对,margin $m=10$。测试数据不参与这一步。代码里的负样本是把整个 batch 的文本循环移位(shift 随机取)。
- R-Precision(本文新提出):每个生成动作,配上它的真值描述和测试集里随机 31 条不相关描述,共 32 条;按特征欧氏距离排序,统计真值落在 top-1 / 2 / 3 的比例。随机猜的 top-1 是 $1/32 \approx 3.1\%$,top-3 是 $3/32 \approx 9.4\%$。代码里这 32 条就是一个 batch(
batch_size = 32)。 - MultiModal Dist(本文新提出):生成动作特征与其描述特征的平均欧氏距离。
- FID / Diversity / MultiModality:沿用 Action2Motion 的定义。FID 是生成动作与真实动作特征分布的 Fréchet 距离;Diversity 从所有生成动作里随机抽两组各 $S_d=300$ 个,算成对平均距离(越接近真实动作的值越好);MultiModality 对同一描述生成多次,每句抽两组各 $S_m=10$ 个,算平均距离。
- 每个实验重复 20 次,报 95% 置信区间(代码是 $1.96\,\sigma/\sqrt{20}$)。
(本节含交互演示:R-Precision 玩具,需要启用 JavaScript)
📊 实验结果
HumanML3D 测试集(Table 2)
📖 展开文字:top-1 0.455 vs 真实 0.511 vs Language2Pose 0.246;FID 1.087,约为 Language2Pose 的 1/10
| 方法 | Top-1 ↑ | Top-2 ↑ | Top-3 ↑ | FID ↓ | MM Dist ↓ | Diversity → | MModality ↑ |
|---|---|---|---|---|---|---|---|
| 真实动作 | 0.511 | 0.703 | 0.797 | 0.002 | 2.974 | 9.503 | - |
| Seq2Seq | 0.180 | 0.300 | 0.396 | 11.75 | 5.529 | 6.223 | - |
| Language2Pose | 0.246 | 0.387 | 0.486 | 11.02 | 5.296 | 7.676 | - |
| Text2Gesture | 0.165 | 0.267 | 0.345 | 7.664 | 6.030 | 6.409 | - |
| MoCoGAN | 0.037 | 0.072 | 0.106 | 94.41 | 9.643 | 0.462 | 0.019 |
| Dance2Music | 0.033 | 0.065 | 0.097 | 66.98 | 8.116 | 0.725 | 0.043 |
| Ours w/ real length | 0.457 | 0.639 | 0.740 | 1.067 | 3.340 | 9.188 | 2.090 |
| Ours | 0.455 | 0.636 | 0.736 | 1.087 | 3.347 | 9.175 | 2.219 |
(表中置信区间略去;所有基线都直接用真实长度,「Ours」用 text2length 采样的长度。论文里加粗的是每列最好的结果——除 MModality 外都是「Ours w/ real length」那一行,「Ours」是下划线的第二名。)
几个现算的比例:
- Ours 的 top-1 是真实动作的 $0.455 / 0.511 = 89.0\%$,top-3 是 $0.736 / 0.797 = 92.3\%$;
- FID 是 R-Precision 最好的基线 Language2Pose 的 $1.087 / 11.02$,约 1/10($11.02 / 1.087 \approx 10.1$);FID 最好的基线其实是 Text2Gesture(7.664),本文约为它的 1/7;
- 用采样长度代替真实长度,MultiModality 从 2.090 升到 2.219(+6.2%),top-1 只差 0.002;不过两者的 95% 置信区间重叠(2.090 + 0.083 = 2.173 > 2.219 − 0.074 = 2.145),KIT-ML 上的差别(1.482 → 2.052,+38.5%,区间不重叠)更能说明采样长度带来了多样性;
- Diversity 一列是越接近真实值(9.503)越好,Ours(9.175)反而比 w/ real length(9.188)略远,所以「更多样」只有 MultiModality 撑得住;
- MoCoGAN、Dance2Music 的 top-1(0.037 / 0.033)几乎就是随机猜的 $1/32 \approx 0.031$。
KIT-ML 测试集(Table 3)与用户研究
📖 展开文字:KIT-ML 上 top-1 0.361 vs 真实 0.424;约 72% 的生成动作被用户排进前二
| 行 | Top-1 | Top-2 | Top-3 | FID | MM Dist | Diversity | MModality |
|---|---|---|---|---|---|---|---|
| 真实动作 | 0.424 | 0.649 | 0.779 | 0.031 | 2.788 | 11.08 | - |
| Language2Pose(最好的基线) | 0.221 | 0.373 | 0.483 | 6.545 | 5.147 | 9.073 | - |
| Ours w/ real length | 0.370 | 0.569 | 0.693 | 2.770 | 3.401 | 10.91 | 1.482 |
| Ours | 0.361 | 0.559 | 0.681 | 3.022 | 3.488 | 10.72 | 2.052 |
(Seq2Seq 一行的 ± 在 pdftotext 里错了位,以 PDF 渲染为准:0.103 / 0.178 / 0.241 / 24.86 / 7.960 / 6.744。)
用户研究(正文 §5.1.2 与补充材料附录 C、D、图 4):AMT、master 资格,测试集随机 50 条描述;每条描述把 7 段火柴人动画(5 个基线 + 本文 + 真实动作,顺序打乱)给 5 个用户排序,评判标准是自然程度、和描述是否对得上、连贯与真实感,30 秒内交卷的自动拒收。本文最受偏好,约 72% 的生成动作被排进前二;按论文图 5 的堆叠柱读,是第 1 名 21.3% + 第 2 名 50.7%,也就是有两成多的时候用户把本文排在了真实动作前面(真实动作自己是第 1 名 71.0%、第 2 名 19.8%)。Language2Pose 第二受偏好;两个非确定性基线最差(最不受欢迎一档里 Dance2Music 40.5%、MoCoGAN 38.6%)。
消融(Table 4)
📖 展开文字:去掉 snippet code 或词注意力掉得最多,词性与位置编码影响较小;局限分三类
| 变体 | Top-1 | Top-2 | Top-3 | FID |
|---|---|---|---|---|
| Ours | 0.455 | 0.636 | 0.736 | 1.087 |
| w/o SnC(snippet code) | 0.370 | 0.538 | 0.642 | 1.200 |
| w/o Att(词注意力) | 0.396 | 0.570 | 0.674 | 1.833 |
| w/o PoS(词性标签) | 0.443 | 0.622 | 0.723 | 1.157 |
| w/o PoE(位置编码) | 0.444 | 0.627 | 0.729 | 1.229 |
top-1 的掉幅:SnC −0.085、Att −0.059、PoS −0.012、PoE −0.011。论文说「去掉 SnC 或 Att,R-Precision 下降超过 6%」,又说词性和位置编码「下降约 2%」——两句话都按相对掉幅读才对得上(口径是我按表算的):
| 去掉 | top-1 相对掉幅 | top-2 相对掉幅 |
|---|---|---|
| SnC | 18.7% | 15.4% |
| Att | 13.0% | 10.4% |
| PoS | 2.6% | 2.2% |
| PoE | 2.4% | 1.4% |
FID 的排序和 R-Precision 不一样:去掉词注意力 FID 升到 1.833(+68.6%),最差;去掉位置编码的 1.229 比去掉 snippet code 的 1.200 还差。
局限(附录 I 与补充材料图 7、8),论文分开写了三类:
- 数据集外的罕见动作:补充材料图 7 里「stomp(跺脚)」之后的「step back」没做出来;
- 太复杂的动作:补充材料图 8 里的「scratch(挠头)」「pitching baseball(投棒球)」超出了方法的能力;
- 细粒度的身体部位:对「左腿 / 右腿」这类描述不敏感(图 8 的「lightly kicks … with their left leg」)。
另外,超长文本和与环境的交互都还没覆盖。
🚶 具体实例:官方片段 012314 从数据到生成的每一步
📖 展开全文(9 节):环境设定 / 第 1 步:三条描述与分词 / 第 2 步:263 维逐段拆开 / 第 3 步:触地阈值 / 第 4 步:Z-score 与放大 / 第 5 步:42 个 snippet code / 第 6 步:长度分布与交叉熵 / 第 7 步:时序 VAE 每一步的维度账 / 第 8 步:课程学习里的这一段
⚠️ 本节的统计量是我们用 numpy 读官方仓库自带的
HumanML3D/new_joint_vecs/012314.npy、new_joints/012314.npy、Mean.npy/Std.npy、index.csv和texts.zip算的;网络结构与超参来自text-to-motion仓库的代码;第 6 步的长度分布是从补充材料图 6 量出来的读图近似值。我们没有下载 AMASS、也没有跑训练或推理。标「示意」的数字(注意力打分、KL 算例)是构造的,用来演示机制。动画第 3–9 幕和三个交互演示用的是同一组数字。
环境设定
- 片段:
012314,官方仓库为了让使用者核对处理结果而自带的那一段(motion_representation.ipynb用它做 double check)。按index.csv第 12316 行,它来自 AMASS 的 Eyes_Japan 子集,源文件是tennis-15-first service-yamaoka_poses.npy的第 149–320 帧——一段网球一发。它在train.txt里(镜像版M012314也在),属于训练集。 - 特征:
new_joint_vecs/012314.npy形状 $(170, 263)$;关节位置new_joints/012314.npy形状 $(170, 22, 3)$;20 fps,$170/20 = 8.5$ 秒。 - 速度要相邻两帧相减,所以
process_file输出的特征比原始帧少一帧:原始片段是 $320 - 149 = 171$ 帧。
第 1 步:三条描述与分词
texts/012314.txt 的三行(原句#分词/词性#起#止):
a person appears to be playing tennis and shoots the ball with the racket in his right hand#a/DET person/NOUN appear/VERB to/PART be/AUX play/VERB tennis/NOUN and/CCONJ shoot/VERB the/DET ball/NOUN with/ADP the/DET racket/NOUN in/ADP his/DET right/ADJ hand/NOUN#0.0#0.0
walking sideways and almost falling.#walk/VERB sideways/ADV and/CCONJ almost/ADV fall/VERB#0.0#0.0
a person lunges and hits something, then backing up.#a/DET person/NOUN lunge/VERB and/CCONJ hit/VERB something/PRON then/ADV back/VERB up/ADP#0.0#0.0
- 起止都是 0.0,表示描述整段;三个人对同一段动作的理解差别不小(打网球 / 侧着走、险些摔一跤 / 前冲击打再后退),这正是「一句多解」的反面——一段动作也有多种说法。镜像版
M012314的第一句把 right 换成了 left。 text_process.py用 spaCy 分词,名词、动词取原形(playing → play),去掉非字母 token;两个细节:分词前先replace('-', ''),left不做词形还原(免得被还原成 leave)。第一句得到 18 个 token,加上sos/eos是 20 个,再用unk补到max_text_len + 2 = 22。WordVectorizer把词性变成 15 维 one-hot(9 个词性 + 5 个关键词类 + OTHER)。关键词类取代原词性:ball在物体表里 →Obj_VIP,right在方向表里 →Loc_VIP,hand在身体部位表里 →Body_VIP;第二句的walk→Act_VIP,第三句的back、up→Loc_VIP。play、shoot不在动作词表里;PART、CCONJ不在 15 类里,记作OTHER。
第 2 步:263 维逐段拆开
| 下标区间 | 分量 | 维数 | 片段 012314 上的读数 |
|---|---|---|---|
| [0, 1) | 根角速度 | 1 | 存的是每帧转角的一半(取相邻帧根四元数 y 分量的 arcsin):170 行累加 −22.8°,乘 2 才是转角,约 −45.6°(按 recover_root_rot_pos 只累加前 169 行是 −45.3°) |
| [1, 3) | 根 xz 线速度 | 2 | 水平累计走了 5.46 m:先侧向冲出约 2.3 m(第 49 帧离起点 2.34 m),第 50–60 帧停在那里,再退回起点,首尾只差 6 cm |
| [3, 4) | 根高度 | 1 | 绝对高度:平均 0.88 m,最高 1.06 m(第 27 帧,发球起跳),最低 0.70 m(第 41 帧,落地弓步) |
| [4, 67) | 21 个关节的局部位置 | 63 | 右腕最高 1.94 m(第 27 帧),比头部关节最高点 1.52 m 还高 |
| [67, 193) | 21 个关节的 6D 旋转 | 126 | 由关节位置反解逆运动学得到 |
| [193, 259) | 22 个关节的局部速度 | 66 | 右腕峰值 10.0 m/s(第 28 帧,1.4 s),左腕峰值 3.7 m/s |
| [259, 263) | 触地 | 4 | 见第 3 步 |
右腕在第 27 帧举到 1.94 m、第 28 帧速度冲到 10 m/s,骨盆也在第 27 帧到最高点——这就是发球时的起跳挥拍;左腕只有 3.7 m/s,和「右手持拍」的描述对得上。侧向冲出再退回,正好对应第二、三条描述(walking sideways / lunges … then backing up)。
第 3 步:触地阈值
foot_detect(positions, 0.002):某只脚的关节相邻两帧位移平方和小于 0.002 m²,就记作触地。
四个通道依次是左踝(7)、左脚(10)、右踝(8)、右脚(11)。片段 012314 上触地占比分别是 79.4%、81.2%、74.7%、74.7%(135 / 138 / 127 / 127 帧,共 170);全数据集的均值(Mean.npy)是 84.8%、86.1%、84.9%、86.1%——这段比平均更「离地」,符合发球的起跳和垫步。用同一公式从 new_joints 重算,前 169 帧逐帧一致(只有 169 对相邻帧,占比是 79.3% / 81.1% / 74.6% / 74.6%,第一个交互演示用的就是这个重算版)。
第 4 步:Z-score 与放大
cal_mean_variance.ipynb 把每一组特征的标准差取组内平均;训练时 Text2MotionDataset 再把根节点 4 维和触地 4 维的标准差除以 feat_bias = 5,等于把它们的数值放大 5 倍(附录 A 说的「放大 5 倍以强调」)。
以触地通道 259 为例,均值 0.848、组标准差 0.356:
| 原始值 0 | 原始值 1 | |
|---|---|---|
| 不放大 $(x-\mu)/\sigma$ | −2.38 | +0.43 |
| 放大 5 倍 $(x-\mu)/(\sigma/5)$ | −11.9 | +2.13 |
| 官方脚本实际 $(x-\mu)/(\sigma/25)$ | −59.6 | +10.7 |
最后一行是我们读代码推出来的(没有用发布的 checkpoint 核对):train_comp_v6.py 只读一次 Std.npy,再用同一个数组先后构造训练集和验证集两个 Text2MotionDataset;构造函数里的 std[0:1] = std[0:1] / opt.feat_bias 是原地赋值,所以这几维被除了两次,等于放大 25 倍。保存下来的 meta/std.npy 也是除过 25 的版本,评测时读的就是它,所以训练和评测前后一致。不管 5 倍还是 25 倍,「脚离地」在 L1 损失里的分量都比普通关节通道大一个量级,网络会优先把它学对。
第 5 步:42 个 snippet code
- 裁到 4 的倍数:评测用的
Text2MotionDatasetV2与Text2MotionDataset的非训练分支都以 2/3 概率取single($\lfloor 170/4 \rfloor \times 4 = 168$ 帧)、1/3 概率取double(再少 4 帧,164 帧),窗口起点再在 0–2 帧(single)或 0–6 帧(double)里随机。这就是附录 A「抛硬币砍掉最后 4 帧」在代码里的样子,只不过概率是 1/3、起点也是随机的。 - 编码器输入去掉 4 维触地:$168 \times 259$。
Conv1d(259→512, k=4, s=2, p=1):长度 $\lfloor (168+2-4)/2 \rfloor + 1 = 84$;再一层Conv1d(512→512):$84 \to 42$;最后Linear(512→512)。结果是 42 个 512 维 code。- 数据量:$168 \times 263 = 44{,}184$ 个数 → $42 \times 512 = 21{,}504$ 个数,少 2.05 倍;更重要的是生成器只需走 42 步。
- 参数量(我按层数的):编码器 $259\cdot512\cdot4+512 + 512\cdot512\cdot4+512 + 512\cdot512+512 = 1{,}842{,}688$;解码器
ConvTranspose1d(512→512)、ConvTranspose1d(512→263)、Linear(263→263)共 1,657,407。 - 窗口起点为 0 时,右腕峰值所在的第 28 帧落在第 8 个 code(帧 28–31)里;两层卷积叠起来,第 8 个 code 看得到第 25–34 帧(10 帧)。起点取 1 或 2 时,原第 28 帧会落进第 7 个 code。
第 6 步:长度分布与交叉熵
train_length_est.py:num_classes = 200 // 4 = 50,标签是 m_lens // 4。这段裁成 168 帧时标签是 42,裁成 164 帧时是 41。
论文没有给片段 012314 的长度分布,我们用补充材料图 6 里量出来的两条(读图近似值,归一化后)走一遍;动画第 6 幕和第二个交互演示是同一组数:
| 「Person stumbles and bends to their right side」 | 「A person waves with their left hand」 | |
|---|---|---|
| 类型 | 非周期 | 周期 |
| 真实长度 | 25 个 code(100 帧,5.0 s) | 45 个 code(180 帧,9.0 s) |
| 众数 / 最高概率 | 26 / 0.233 | 44 / 0.108 |
| $p(\text{真实})$ / 交叉熵 $-\ln p$ | 0.168 / 1.79 | 0.043 / 3.14 |
| 真实长度 ±2 以内的概率 | 80.7% | 23.7% |
| 熵(nats,均匀分布上限 3.69) | 2.29 | 3.28 |
| 累积概率 0.2 / 0.5 / 0.9 取到 | 23 / 25 / 27 个 code | 15 / 21 / 44 个 code |
| 对应时长 | 4.6 / 5.0 / 5.4 s | 3.0 / 4.2 / 8.8 s |
推理时 torch.multinomial 采样,若采到不足 10 个 code,comp_v6_model_dataset.py 会再采(最多再采两次)。周期动作的分布摊得开,同一句话三次采样能从 3 秒采到 8.8 秒。
第 7 步:时序 VAE 每一步的维度账
部件(networks/modules.py) |
输入 → 输出 |
|---|---|
TextEncoderBiGRU |
GloVe 300 + pos_emb: Linear(15→300) 相加 → Linear(300→512) → 双向 GRU(512):每个词 1024 维特征,句向量 1024 |
AttLayer |
查询 $h_\theta^{t-1}$(1024)→ W_q;键 / 值 = 词特征(1024)→ W_k(无 bias)/ W_v;输出 $w _ {att}^t$ 512 维,缩放 $\sqrt{512} \approx 22.6$ |
先验 TextDecoder |
$[c^{t-1}, w _ {att}^t]$ = 512 + 512 = 1,024 → GRUCell(1024) → $\mu, \log\sigma^2$ 各 128 |
后验 TextDecoder |
$[c^{t-1}, c^t, w _ {att}^t]$ = 1,536 → 同上 |
生成器 TextVAEDecoder |
$[c^{t-1}, w _ {att}^t, z_t]$ = 512 + 512 + 128 = 1,152 → GRUCell(1024) → Linear(1024→1024) → Linear(1024→512):$\hat c^t$ |
这三个输入维数和补充材料 Table 1 里 embedding 层的 1024 / 1536 / 1152 对得上。三个 GRU 的初始隐状态都由句向量经 z2init: Linear(1024→1024) 得到;位置编码维数 1024,加在 emb 的输出上。词特征按句子实际长度打包编码(pack_padded_sequence),这一句单独编码是 20 × 1024;只有和更长的句子同一个 batch 时才补零到 22 行,而 AttLayer 没有 mask,补零的位置打分为 0、仍会分到一点注意力权重。
- 注意力示意(打分是编的):对 play / tennis / shoot / ball / racket 五个词,第 2 步打分 $[1.8, 1.5, 0.2, 0.1, 0.4]$,softmax 后权重 0.422 / 0.312 / 0.085 / 0.077 / 0.104;第 8 步(挥拍那个 code)打分 $[0.2, 0.4, 2.0, 1.3, 1.1]$,权重 0.073 / 0.089 / 0.440 / 0.219 / 0.179。
- 到达倒计时:这段 $T = 42$,论文记号里第 $t=8$ 步还剩 $T-t = 34$ 个 code。代码里循环下标 $i$ 从 0 开始,
tta = m_lens // 4 - i,第 8 步($i=7$)传入的是 35,比论文记号多 1。 - KL 示意:代码的
kl_criterion按 $\log\frac{\sigma_\psi}{\sigma_\phi} + \frac{\sigma_\phi^2 + (\mu_\phi-\mu_\psi)^2}{2\sigma_\psi^2} - \frac12$ 逐维求和。若某维后验 $\mathcal{N}(0.5, 0.8^2)$、先验 $\mathcal{N}(0, 1)$,这一维是 $\ln 1.25 + (0.64 + 0.25)/2 - 0.5 = 0.168$;128 维都这样就是 21.5,乘 $\lambda _ {KL}=0.01$ 后计入总损失 0.215。代码最后对「batch × 步数」取平均(kld.sum() / mu1.shape[0]),不是论文式 6 写的对步数求和。
第 8 步:课程学习里的这一段
CompTrainerV6.train() 对 HumanML3D 从 schedule_len = 10 起步,每阶段 +1,schedule_len > 49 时结束,共 40 个阶段;每阶段最多 50 个 sub-epoch。验证损失不再创新低时计数加 1,计到 3 之后再不创新低(也就是连续第 4 次)就进入下一阶段;比最低值高出 0.1 也直接进入下一阶段。
- 每阶段只取长度 ≥
schedule_len × 4帧的样本(数据按长度排序后用searchsorted定位)。这段 170 帧,从 $T _ {cur}=10$(40 帧)一直参加到 $T _ {cur}=42$(168 帧),$T _ {cur}=43$(172 帧)起被排除:参加 33 个阶段。 - 在 $T _ {cur}=10$ 阶段,
__getitem__只截 40 帧(10 个 code)的窗口来训练,窗口起点只在第 0–2 帧(single)或第 0–6 帧(double)里随机;但传给网络的长度是 168 或 164 帧,所以到达倒计时从 42(或 41)开始数——网络一开始就知道这段「总共多长」,只是先学开头(这是我读代码的理解)。 - teacher forcing:每个 batch 抛一次硬币,40% 整条序列喂真值 code,60% 喂自己生成的 code。
📁 源码对照
📖 展开文字:两个仓库的目录、论文 ↔ 代码对照表、训练与评测命令、论文与代码的出入
两个仓库
| 仓库 | 作用 | 关键文件 |
|---|---|---|
| EricGuo5513/HumanML3D | 从 AMASS 复现数据集(AMASS 不允许直接分发) | raw_pose_processing.ipynb(SMPL+H 前向得到关节、降采样、裁剪、镜像)、motion_representation.ipynb(统一骨架、263 维特征)、cal_mean_variance.ipynb(Mean / Std)、text_process.py(spaCy 分词)、index.csv(14,616 段的来源与起止帧)、paramUtil.py(运动链) |
| EricGuo5513/text-to-motion | 模型、训练与评测 | networks/modules.py、networks/trainers.py、data/dataset.py、utils/word_vectorizer.py、train_decomp_v3.py、train_length_est.py、train_comp_v6.py、train_tex_mot_match.py、final_evaluations.py、utils/metrics.py |
论文 ↔ 代码对照
| 论文 | 代码 |
|---|---|
| 降到 20 FPS(§4) | amass_to_pose():down_sample = int(fps / ex_fps),ex_fps = 20,按整数步长抽帧 |
| 裁剪与去头 | index.csv 的 start_frame / end_frame;另对 Eyes_Japan、HDM05 去掉前 3 秒,TotalCapture、MPI_Limits 去掉前 1 秒,Transitions 去掉前 0.5 秒 |
| 镜像扩增(§5) | swap_left_right():x 取反并交换左右链,存成 M 前缀的文件 |
| 统一骨架、面朝 Z+ | uniform_skeleton() 按腿长比缩放 → IK → 用目标骨架(样例 000021)的骨长 FK;process_file() 落地、根节点 xz 归零、用髋与肩的横向量把初始朝向转到 Z+ |
| 263 维特征(§5) | process_file() 的拼接顺序:root_data(4) + ric_data(63) + rot_data(126) + local_vel(66) + feet_l, feet_r(4);root_data 是 [r_velocity, l_velocity, root_y],高度不做差分 |
| 运动自编码器(§3.1) | MovementConvEncoder / MovementConvDecoder;DecompTrainerV3.backward():L1 重建 + lambda_sparsity·mean|code| + lambda_smooth·L1(相邻 code) |
| Text2Length(§3.2) | MotionLenEstimatorBiGRU(自己一套文本编码,MLP 1024 → 512 → 256 → 128 → 50);LengthEstTrainer 用 CrossEntropyLoss,标签 m_lens // unit_length |
| 文本编码器 | TextEncoderBiGRU;词性 one-hot 来自 WordVectorizer(POS_enumerator 15 类,VIP 词表覆盖原词性) |
| 局部词注意力(式 4) | AttLayer:W_q、W_k(无 bias)、W_v,除以 $\sqrt{512}$ |
| 先验 / 后验 / 生成器 | TextDecoder(先验与后验)、TextVAEDecoder(生成器),都是 GRUCell + PositionalEncoding |
| 到达倒计时 PE(式 5) | CompTrainerV6.forward():tta = m_lens // unit_length - i |
| 总损失 | CompTrainerV6.backward_G():动作重建 + code 重建 + lambda_kld·KL |
| 课程学习 + teacher forcing | CompTrainerV6.train():schedule_len 10 → 49,tf_ratio = 0.4,early_stop_count = 3 |
| 评测提取器(附录 B) | TextEncoderBiGRUCo、MotionEncoderBiGRUCo、ContrastiveLoss;--negative_margin 10.0 |
| R-Precision / MM Dist / FID / Diversity / MultiModality | final_evaluations.py + utils/metrics.py:batch_size = 32、diversity_times = 300、mm_num_samples = 100、mm_num_repeats = 30、mm_num_times = 10、replication_times = 20 |
跑起来(README 实录)
# 运动自编码器
python train_decomp_v3.py --name Decomp_SP001_SM001_H512 --gpu_id 0 --window_size 24 --dataset_name t2m
# text2length
python train_length_est.py --name length_est_bigru --gpu_id 0 --dataset_name t2m
# text2motion(HumanML3D 用 λ_KL = 0.01)
python train_comp_v6.py --name Comp_v6_KLD01 --gpu_id 0 --lambda_kld 0.01 --dataset_name t2m
# 评测用的文本 / 动作提取器
python train_tex_mot_match.py --name text_mot_match --gpu_id 1 --batch_size 8 --dataset_name t2m
# 用采样长度生成并画出动画
python eval_comp_v6.py --name Comp_v6_KLD01 --est_length --repeat_time 3 --num_results 10 --ext default --gpu_id 1
论文与代码的出入(我们核对到的)
- 姿态维数的写法:论文写 $j^p \in \mathbb{R}^{3j}$、$j^r \in \mathbb{R}^{6j}$、$j^v \in \mathbb{R}^{3j}$,$j=22$ 时会得到 $4 + 66 + 132 + 66 + 4 = 272$;代码里位置与旋转都去掉了根节点($j-1 = 21$),所以是 263,和附录 A 写的 263 一致。
- 补充材料 Table 1 的卷积维数:标题写「on dataset HumanML3D」,但编码器输入 247、解码器输出 251、隐层 384 是 KIT-ML 的数($251-4=247$);HumanML3D 的代码是 $259 \to 512 \to 512$(模型名
H512)。 - 课程起点:附录 A 写 $T _ {cur}=8$、$T _ {max}=50$;
CompTrainerV6.train()对 HumanML3D 从 10 起步(KIT-ML 从 6),到 49 停。数据集也只收不足 200 帧的动作,max_motion_length = 196。 - 长度类:论文写 ${1, \dots, T _ {max}}$,补充材料说分布画在 10 到 50;代码是
num_classes = 200 // 4 = 50,类下标 0–49,补充材料图 6 里 50 那一格八张都是空的。训练 text2length 时数据集的max_length一直停在 20(LengthEstTrainer从不调用reset_max_len),按Text2MotionDataset的裁剪规则,40–43 帧的动作抽到double会被裁成 $20 + 4 \times 4 = 36$ 帧,所以实际训练标签落在 9–49,不是 10–49(按index.csv粗估,训练集里有 49 段是 40–43 帧)。 - 「抛硬币」砍 4 帧:代码是从
['single', 'single', 'double']里随机选,double的概率是 1/3,不是 1/2;窗口起点也随机(single在 0–2 帧,double在 0–6 帧)。 - 重建损失:论文写 L1;
CompTrainerV6里名为l1_criterion的其实是SmoothL1Loss(DecompTrainerV3用的是真 L1)。backward_G()里lambda_rec_mov与lambda_rec_mot乘反了,两者默认都是 1,不影响结果。KL 对「batch × 步数」取平均,不是对步数求和。 - λ_KL 默认值:
train_options.py默认 0.005,HumanML3D 的 README 命令显式传--lambda_kld 0.01(模型名Comp_v6_KLD01),与附录 A 一致;附录 J 的表里 KIT-ML 写成 0.05,是笔误(附录 A 与 README 都是 0.005)。 - 关键词类:论文说词典分四类(方向、身体部位、物体、动作),代码有五个 VIP 表(多一个描述词表 slowly / happily 等),而且 VIP 类替换原词性,而不是另加一个标签——补充材料图 2(a) 的「doing/VERB jumping/ACT jacks/NOUN」也是这么画的。
- R-Precision 的方向:论文写「对每个生成动作,在 32 条描述里排序」;
evaluate_matching_score()是在 batch(32)内算文本 × 动作的距离矩阵、对每条文本给 32 个动作排序。两种方向在一个 32 × 32 的 batch 里口径接近,但并不完全相同(这是我读代码的理解)。测试集 4,384 条是 2,192 段原始动作加各自的镜像,一段动作和它的镜像碰巧进同一个 batch 的概率约 0.7%,影响很小。 - 降采样:
int(fps / 20)取整步长,源帧率不是 20 的整数倍时(例如 250 fps 会得到步长 12、约 20.8 fps),实际帧率会略偏(例子是我按公式推的,没有逐个核对 AMASS 子集的帧率)。 - 描述条数:仓库
texts.zip里 14,616 个非镜像文件我们数到 43,692 行描述(14,434 个文件正好 3 行),比论文的 44,970 少 1,278 条;按 lemma 去重的词表是 5,331(论文 5,371)。差异的原因我们没有找到出处。 - feat_bias 被除了两次:附录 A 说根节点与触地特征放大 5 倍;
train_comp_v6.py用同一个std数组构造训练集与验证集,Text2MotionDataset里是原地除法,实际放大 25 倍(见「🚶 第 4 步」,按代码推断)。 - 根角速度存的是半角:通道 0 是相邻帧根四元数 y 分量的 arcsin,即每帧转角的一半;
recover_root_rot_pos把累加值直接当四元数的半角用,所以还原是对的,但直接把通道 0 当转角读会少一半(见「🚶 第 2 步」)。另外skeleton.py求朝向时髋部横向量与肩部横向量的方向是反的(face_joint_idx = [2, 1, 17, 16]解包成l_hip, r_hip, sdr_r, sdr_l);按这个写法复现才和通道 0 对得上。 - 其余模块的学习率:附录 A 只写了 $2\times10^{-4}$;
train_options.py里自编码器、text2length、评测提取器都是 $1\times10^{-4}$(batch 分别 128 / 64 / 32,README 的提取器命令传 8)。
🤖 工程价值
📖 展开文字:对人形机器人是「语言到动作的先验」,不是可直接执行的参考
- 文本到参考动作:先由文本生成人体动作,再映射(retarget)到 G1 / H1 等人形骨架(如 GMR),作为跟踪策略的参考。
- 动作先验:HumanML3D 常和 AMASS、BABEL、Motion-X 一起用来训练更大的运动先验或运动 tokenizer。注意重叠:HumanML3D 有 13,425 段来自 AMASS,BABEL 也是 AMASS 上的标注,混在一起用时要按源文件去重。
- 标准评测:后续 MDM、T2M-GPT、MoMask 等都在 HumanML3D 上沿用本文的 263 维特征与评测器(R-Precision、FID、MM Dist、Diversity、MultiModality)报数。
- 注意:HumanML3D 存的是统一骨架下的关节位置与旋转特征,没有 SMPL 的 shape 参数;6D 旋转是从关节位置反解出来的,4 维触地是速度阈值得到的 0 / 1,没有接触力这类物理量。直接拿来做机器人参考,需要处理骨长差异、脚滑、关节限位与动力学可行性。
在发布清单里,它排在 ToddlerBot 之后、GMR 之前:前者讲机器人自己怎样采数据,这一篇讲人类动作数据从哪里来、怎样配上文字,下一篇讲怎样把人的动作搬到机器人身上。
💬 讨论记录
📖 展开文字:retargeting 与物理过滤是关键中间层;描述噪声与一段多说法
- HumanML3D 对机器人不是「可直接执行的数据集」,而是语言到人体动作先验的来源。
- 机器人使用时最关键的中间层是 retargeting 和 physics filtering。没有这层,文本生成的动作可能视觉上合理但动力学不可行。
- 描述质量参差:片段 012314 的三条描述分别是打网球、侧着走险些摔一跤、前冲击打再后退(见具体实例第 1 步),评测时 R-Precision 的上限(真实动作 top-1 也只有 0.511)一部分就来自这种标注分歧(这是我们的推测,论文没有分析;R-Precision 玩具里把「描述歧义 q」调大就能看到这种上限)。
- 后续 text-to-humanoid 工作往往会把 HumanML3D 与 AMASS、BABEL、Motion-X 等数据一起使用,训练更大的 motion prior。
🎤 面试高频问题 & 参考回答
📖 展开文字:数据怎么来、263 维里存了什么、为什么要 snippet code 与 text2length、R-Precision 怎么算
- HumanML3D 的数据是怎么来的?
- 现成动捕(AMASS 17 个子集 + HumanAct12)统一到 20 fps、≤10 s、同一副骨架与初始朝向,再在 AMT 上每段收 3 条描述(≥5 词),最后左右镜像(描述里 left / right 一起换)把数据翻倍。
- 263 维特征里根节点存了什么?
- 绕竖轴的角速度、水平面的两个线速度分量——水平方向只存速度;第 4 维是根节点的绝对高度,不做差分。这样特征与全局平移、水平朝向无关(这是常见的解读,论文只说沿用 PFNN 的处理),模型学的是「怎么动」而不是「在哪」;生成后再把速度积分回全局轨迹(
recover_from_ric)。
- 绕竖轴的角速度、水平面的两个线速度分量——水平方向只存速度;第 4 维是根节点的绝对高度,不做差分。这样特征与全局平移、水平朝向无关(这是常见的解读,论文只说沿用 PFNN 的处理),模型学的是「怎么动」而不是「在哪」;生成后再把速度积分回全局轨迹(
- HumanML3D 用的是 SMPL 吗?
- 关节来自 SMPL+H 前向得到的前 22 个关节,并统一到一副骨架;数据里存的是关节位置 / 旋转 / 速度特征,6D 旋转是从关节位置反解出来的,不是 SMPL 的 pose 与 shape 参数。要驱动 SMPL 网格或机器人,还要额外拟合或 retarget。
- 为什么要 snippet code?
- 4 帧压一个 code,生成器步数降到 1/4,每个 code 带局部时间语义;消融里去掉它 top-1 从 0.455 掉到 0.370,论文图 6 里越往后越跟不上文字。
- 为什么单独做 text2length?
- 让长度也成为随机变量:从 $p(T \mid \text{text})$ 采样,同一句话能生成不同时长。HumanML3D 上用采样长度的 MultiModality 比用真实长度高 6.2%(置信区间重叠),KIT-ML 上高 38.5%(不重叠),R-Precision 几乎不变;补充材料图 6 里周期动作的长度分布更平。
- R-Precision 怎么算?随机水平是多少?
- 1 条真值描述 + 31 条随机描述共 32 条,按评测器特征的欧氏距离排序,看真值是否在 top-k;随机猜 top-1 是 $1/32 \approx 3.1\%$,top-3 是 $9.4\%$。代码里 32 条就是一个 batch。
📎 附录
📖 展开文字:参考来源
A. 参考来源
- CVPR 2022 OpenAccess:论文页
- CVPR 2022 OpenAccess:正文 PDF
- CVPR 2022 OpenAccess:补充材料 PDF
- 项目主页
- GitHub:EricGuo5513/HumanML3D
- GitHub:EricGuo5513/text-to-motion
- 后续沿用评测器的工作(官方代码仓库):MDM、T2M-GPT、MoMask
- 上游收录:YanjieZe/awesome-humanoid-robot-learning(以 CVPR 2022 链接收录,无 arXiv)
- 动画与交互演示里的论文图(正文图 1、4、6,补充材料图 3、6、8)只做讲解引用,版权归原作者;片段 012314 的骨架与特征热图按官方仓库的数据画。