复现实施方案 - VN02/Personal-profile GitHub Wiki

复现实施方案:以推理包为宿主,外挂薄训练层

本文目标:给出一条可执行的 Kimodo 复现路线—— 把 NVIDIA 发布的 kimodo 推理包当作不可改动的「已验证模型库」直接 import,只在它之上加一层薄训练代码(dataset + loss + loop + config)。

这样做的决定性理由:kimodo 推理包里训练前向所需的模块(denoiser / motion_rep / diffusion / backbone / constraints / skeleton / stats)已经全齐,且本就是 training-ready 的(twostage_denoiser.forward 里带 self.training 分支)。NVIDIA 只是发布时摘掉了训练 harness。因此我们能做闭环验证:先用发布权重跑通推理、确认前向 100% 正确,再在这个「已验证常量」之上加训练层——训练代码错了能立刻定位。

路线选型的完整论证见 §1。被否的两条路(改单文本扩散框架 / 到处复制拼装)也在 §1 说明。

事实分档:【源码】= 逐行读过 kimodo 推理代码/权重 config;【文档】= 仓库 README / benchmark docs;【报告】= Kimodo technical report 明确给出的训练配方/超参/消融。三档互为印证:报告给宏观配方,源码给精确格式,二者一致时即可照实现。


1. 路线选型:为什么是「宿主 + 薄训练层」

1.1 三条候选路线

路线 做法 判定
A. 改通用单文本扩散框架 在某个开源单阶段 text2motion 训练框架上把模型换成 Kimodo ❌ 器官移植:单阶段 / smplrifke / CLIP / DDPM 模型本体全要扔,只剩训练外壳,却要一直迁就它的抽象
B. 从各仓复制拼装 把 kimodo 的 motion_rep/denoiser 手抄进新工程 ❌ 致命:手抄会引入通道顺序/归一化/6D 约定/detach 位置的微妙偏差,且无法与发布权重对齐验证
C. 宿主 + 薄训练层 import kimodo.* 当库,外挂 dataset/loss/loop ✅ 模型侧是发布原件,可载权重闭环验证;只新写通用且简单的训练层

1.2 路线 C 的关键支撑(均来自精读,见 §3)

  • kimodo 包前向所需模块齐全,且 TwostageDenoiser.forward 本就含训练分支(twostage_denoiser.py:122-130)。
  • 可载入 Kimodo-SOMA-SEED-v1.1/model.safetensors 跑官方推理(load_model,§3.1),作为前向正确性的 ground truth。
  • 训练只需直连 TwostageDenoiser绕开 inference-only 的 Kimodo 顶层类(它全程 torch.inference_mode() + eval(),§3.5)。

1.3 工程形态

kimodo-train/                      # 新建工程,不碰 kimodo 包源码
├── (依赖) import kimodo.*         # 发布原件,当作「已验证常量」
│     TwostageDenoiser / KimodoMotionRep / Diffusion / SOMASkeleton30 / Stats
├── data/seed_dataset.py          # 新写:BVH → 369维特征 + 约束采样   ← §5
├── training/loss.py              # 新写:q_sample 加噪 → 两阶段 forward → 7项加权 smooth-L1(含 FK)  ← §4
├── training/curriculum.py        # 新写:两阶段课程 + text/constraint 采样调度(dual-CFG 训练侧)  ← §4
├── training/loop.py              # 新写:训练循环 + EMA(0.995@10) + Adam-atan2 + ckpt  ← §6
└── configs/                      # 新写:hydra 配置分层  ← §6
  • 当库 import(不可动):整个 kimodo 包——精确部分,逐字节对齐权重。
  • 新写(通用且简单):dataset / loss / curriculum / loop / config。扩散训练 loop 本体约百行。
  • 报告已给的配方:损失权重、优化器/lr/EMA、两阶段课程、约束采样模式、CFG 权重——全部来自 technical report(§4),照搬即可。详见 §6。

1.4 两个先决风险

  1. license:kimodo 包是 Apache-2.0(SPDX 头确认)。import 当训练库合规。
  2. inference-only 硬假设Kimodo._generate/denoising_step 全程 torch.inference_mode()Kimodo.__init__denoiser.eval();text encoder no_grad+逐条编码;量化路径 inference-only。应对:训练直连 TwostageDenoiser、显式 .train()、文本特征离线缓存。详见 §3.5。

2. 背景:Kimodo 的设计决策与为何要从头训练

2.1 任务定位

Kimodo 是一个显式运动扩散模型(在 pose 特征空间直接去噪,非 latent / 非 token 自回归),在 700 小时光学动捕(Bones Rigplay)上训练,目标是同时做到:① 接近动捕的运动质量;② 文本 + 全套运动学约束(全身关键帧 / 稀疏关节位置+旋转 / 2D waypoint / 稠密 2D 路径 / 脚接触)的精确可控;③ 大规模多样化生成。其核心是「精心设计的运动表示 + 两阶段去噪器」,约束通过直接 imputation(覆写 pose 特征)注入,无需 ControlNet 微调 / 测试期梯度引导 / RL。

2.2 与常见 text2motion 模型的关键差异(决定差距)

维度 常见单阶段 text2motion Kimodo-SOMA-SEED-v1.1
骨架 SMPL-H(22 关节) SOMASkeleton30(30 关节,输出 somaskel77)
动作表示 局部/速度型 ~200 维,fps=20,按 heading canonical KimodoMotionRep 369 维,fps=30,全局表示、不按 heading canonical
模型 单阶段 8~16 层 两阶段 interleaved root→body,各 16 层 1024 dim,282M,端到端联合训练
文本编码器 CLIP ViT-B/32(512 维) LLM2Vec(4096 维,1 文本 token + 49 个 zero「register-like」额外 token)
条件 仅文本(或推理期组合) 文本 + 运动学约束训练期联合 imputation
采样 DDPM 100 步 DDIM 100 步,separated dual-CFG(w_text=2, w_constr=2)
后处理 C++ IK 脚滑清理 + 约束硬化 + root 修正
训练代码 通常开源 未开源(但 technical report 给出完整配方,推理代码给出精确格式,见 §3-4)

2.3 为何必须从头训练而非组合/微调

约束跟随是在训练期通过 imputation 学到的(report §4.2-4.3 的两阶段课程:phase-1 纯文本预训练,phase-2 加约束)。这不是推理期能拼出来的能力,必须重训。模型本体、表示、约束注入全部直接 import kimodo 原件复用(§3),只新写薄训练层,因此既能从头训练、又能逐字节对齐发布权重做闭环验证。

2.4 作者谱系(解释技术来源)

Kimodo 由 NVIDIA 出品,一作 Davis Rempe(物理/约束/引导扩散方向)+ 二作 Mathis Petrovich(语义/可控运动生成方向)co-first,合流 + 规模化。

来源为 technical report(research.nvidia.com/labs/sil/projects/kimodo,代码 github.com/nv-tlabs/kimodo,报告日期 2026-3-16),非 arXiv 预印本。引用时按 NVIDIA technical report 标注。


3. 宿主模型库:精确装配与闭环验证【源码】

本节是整条路线的地基:搞清如何把 kimodo 包当库实例化、载权重、跑推理、做对齐验证。全部逐行读过。

3.1 模型装载入口

load_model(...)model/load_model.py:122-233)→ 返回 kimodo.model.Kimodo 实例(.eval())。流程:

  1. 解析模型名 → 权重目录(KIMODO_MODELS_ROOT 环境变量指本地,否则 HF 下载)。
  2. model_path/config.yaml:195-199)。
  3. 注入 checkpoint_dirOmegaConf.merge + resolve(解析掉 ${oc.select:checkpoint_dir} 插值,把权重路径填进 ckpt_pathstats_path)。
  4. instantiate_from_dictcommon.py:33,自定义递归 _target_ 工厂,不是 hydra 运行时)递归实例化整棵树。
  5. eval_mode=Truemodel.eval()

config.yaml 结构(SEED-v1.1):

_target_: kimodo.model.Kimodo
num_base_steps: 1000
cfg_type: separated
denoiser:
  _target_: kimodo.model.twostage_denoiser.TwostageDenoiser
  ckpt_path: ${oc.select:checkpoint_dir}/model.safetensors
  motion_mask_mode: concat
  motion_rep:
    _target_: kimodo.motion_rep.KimodoMotionRep
    fps: 30
    stats_path: ${oc.select:checkpoint_dir}/stats/motion/
    skeleton: {_target_: kimodo.skeleton.SOMASkeleton30}
  latent_dim: 1024, ff_size: 2048, num_layers: 16, num_heads: 8, ...

3.2 权重与 stats 的加载分工

  • 权重:不由 Kimodo 加载,而由 TwostageDenoiser.__init__ 收到 ckpt_path 后自行 load_ckpttwostage_denoiser.py:66-71)。.safetensorssafetensors.torch.load_filestrip 前缀 denoiser.backbone.:70);load_state_dict(strict)。
  • stats:mean/std.npy 是 non-persistent bufferstats.py:62),不在 safetensors 里,由 KimodoMotionRep 实例化时经 stats_path 加载。布局必须是 split:<ckpt>/stats/motion/{global_root,local_root,body}/{mean,std}.npybase.py:72-83)。全局 stats.mean = cat([global_root(5), body(364)]) = 369。

训练保存 ckpt 后仍需单独维护 stats/motion/ 目录;key 命名要能对齐 denoiser.backbone. strip 逻辑,否则无法回灌。

3.3 KimodoModel 顶层构造的子模块

kimodo_model.py:31-61

  • self.denoiser = denoiser.eval() 然后 ClassifierFreeGuidedModel(denoiser, cfg_type) 重新赋值——所以 self.denoiser 最终是 CFG wrapper,内层真正可训练的 denoiser 要 model.denoiser.model
  • self.motion_rep / skeleton / fps(从 denoiser 借)、self.diffusion = Diffusion(1000)self.sampler = DDIMSampler(diffusion)self.text_encoder

3.4 端到端推理调用链(scripts/generate.py

load_model → model(texts, num_frames, constraint_lst, num_denoising_steps, multi_prompt=True, ...)
 └ Kimodo.__call__ → _multiprompt(逐段)
    ├ motion_rep.create_conditions_from_constraints_batched   # 约束→observed_motion+mask
    ├ (非首段) motion_rep.inverse 取上段尾帧 → 建过渡约束
    ├ Kimodo._generate                                        # 采样循环
    │   ├ text_feat = text_encoder(texts)
    │   ├ cur_mot = randn(B,T,369)
    │   ├ diffusion.space_timesteps + calc_diffusion_vars
    │   └ for i reversed(steps): with inference_mode: denoising_step
    │        ├ pred_clean = self.denoiser(cfg_weight,...,cfg_type)   # CFG wrapper
    │        └ x_tm1 = self.sampler(use_timesteps, motion, pred_clean, t)  # DDIM
    ├ motion_rep.unnormalize → (post_processing) post_process_motion
    └ cat 各段 → motion_rep.inverse → output_to_SOMASkeleton77 → numpy

3.5 训练复用:用哪些、绕哪些

构造一次即可训练/推理的对象(训练要复用)

  • TwostageDenoiser(内层真正可训练 nn.Moduleforward 已含训练分支)。
  • KimodoMotionRep/MotionRepBase(编解码 + stats normalize/unnormalize)。
  • Diffusionnum_base_steps=1000,提供 q_sample 加噪与 schedule)。
  • SOMASkeleton30Stats

inference-only(训练要绕过/替换)

  • ClassifierFreeGuidedModel(采样期 guidance wrapper)→ 训练直连内层 denoiser,自己做 cond-dropout。
  • DDIMSampler → 训练不用。
  • text encoder 的服务化/量化路径(TextEncoderAPI / LlamaEmbeddingsAPI / llm2vec_int8)→ 训练用 fp32/bf16 LLM2VecEncoder离线缓存特征

硬编码 inference-only 假设(阻碍点 + 应对)

阻碍 位置 应对
_generate/denoising_step 全程 inference_mode() kimodo_model.py:108,633 不走 Kimodo.__call__/_generate,直连 TwostageDenoiser.forward 自建训练循环
__init__denoiser.eval() + load_model(eval_mode=True) kimodo_model.py:41 训练前显式 denoiser.train()
text encoder no_grad+逐条编码 llm2vec_wrapper.py:226 冻结 + 离线缓存 text_feat
stats 是 non-persistent buffer stats.py:62 训练 ckpt 外单独维护 stats/motion/

3.6 闭环验证步骤(第 0 步,路线成立的地基)

  1. KIMODO_MODELS_ROOTload_model("Kimodo-SOMA-SEED-v1.1", device, eval_mode=True)
  2. seed_everything 固定 → 跑 model(texts, num_frames, num_denoising_steps=100, num_samples=1, multi_prompt=True, return_numpy=True) → 存 output["posed_joints"] 作 ground truth。
  3. 用薄训练层自己构造 TwostageDenoiserckpt_path=None),灌入同一 model.safetensors(strip denoiser.backbone.),.eval() + 关 cond-dropout,确认前向 pred_clean 与官方数值一致。
  4. 校验 stats:<ckpt>/stats/motion/{global_root,local_root,body}/{mean,std}.npy 存在,且拼接长度 == motion_rep.motion_rep_dimbase.py:81 断言)。

通过即证明「前向 + motion_rep + 归一化」100% 正确,之后训练层出错可立即定位。

3.7 模型侧 import 清单与构造顺序

from kimodo.skeleton import SOMASkeleton30
from kimodo.motion_rep import KimodoMotionRep
from kimodo.model.twostage_denoiser import TwostageDenoiser
from kimodo.model.diffusion import Diffusion
from kimodo.model.llm2vec import LLM2VecEncoder          # 冻结/缓存
from kimodo.model import load_checkpoint_state_dict       # 权重对齐
  1. skeleton = SOMASkeleton30()
  2. motion_rep = KimodoMotionRep(skeleton, fps=30, stats_path="<ckpt>/stats/motion/") # 触发 mean/std 加载
  3. denoiser = TwostageDenoiser(motion_rep=motion_rep, motion_mask_mode="concat", ckpt_path=None, latent_dim=1024, ff_size=2048, num_layers=16, num_heads=8, llm_shape=[1,4096], num_text_tokens_override=50, input_first_heading_angle=True, ...)
  4. diffusion = Diffusion(num_base_steps=1000);text 用冻结 LLM2VecEncoder 离线缓存
  5. 训练循环(§4):denoiser.train()motion_rep 编码+normalize GT → diffusion.q_sample 加噪 → denoiser.forward(...) → loss

4. 薄训练层(一):损失与前向规格【源码】

训练 loop 本体简单,但「输入/输出/损失的精确格式」必须与发布权重一致。以下全部从推理代码反推得到,是可照实现的规格。

4.1 扩散数学(model/diffusion.py

  • 噪声调度:cosine,alpha_bar(t)=cos((t+0.008)/1.008·π/2)²max_beta=0.999num_base_steps=1000(report 确认训练用 T=1000)。
  • 前向加噪 q_sample:标准 x_t = sqrt(ᾱ_t)·x0 + sqrt(1-ᾱ_t)·noise训练直接调 diffusion.q_sample
  • 预测目标:x0(DDIMSamplerpred_xstart 当 x0;report §4.3 也是 DDPM 简化损失的 x0 形式)。
  • 损失类型smooth-L1(loss 小用 L2 项、否则 L1,Girshick Fast R-CNN 式),不是纯 MSE

4.1b 损失的精确分项与权重(report §4.3 直接给出,非待定)

损失按运动表示的每个分量分别算 smooth-L1,再加一项 FK 几何项,共 7 项加权:

L = λ1·||r̂_pos − r_pos||      (smoothed root position)
  + λ2·||r̂_head − r_head||    (root heading [cos,sin])
  + λ3·||ĵ_pos − j_pos||      (joint positions)
  + λ4·||ĵ_vel − j_vel||      (joint velocities)
  + λ5·||ĵ_rot − j_rot||      (joint 6D rotations)
  + λ6·||f̂ − f||             (foot contacts)
  + λ7·||FK(ĵ_rot) − j_pos||  (前向运动学一致性)

权重:λ1=λ3=λ5=10.0,λ2=2.0,λ4=3.0,λ6=4.0,λ7=5.0

  • FK 项 FK(ĵ_rot)skeleton.fk 把预测的 6D 旋转转回关节位置,与 GT 位置对齐——直接 import SOMASkeleton30.fk + motion_rep 切片即可实现。
  • 变长 batch:按各样本 length mask 后再求均值。
  • 两阶段的损失分摊到对应分量:root 段贡献 λ1+λ2,body 段贡献 λ3~λ7(见 §4.2 的 369 维切分)。

这一节是确定事实(report 明确列出权重数值),不再是「先用等权起跑再调」。实现时按上述权重直接配置。

4.2 两阶段 denoiser 的训练耦合(twostage_denoiser.py

forward 第 122-152 行直接揭示训练行为,无需猜

convert_ctx = torch.no_grad() if self.training else contextlib.nullcontext()
with convert_ctx:
    root_motion_local = self.motion_rep.global_root_to_local_root(root_motion_pred, ...)
if self.training:
    root_motion_local = root_motion_local.detach()           # 训练时切断梯度
body_x = x[..., self.motion_rep.body_slice]
x_new = torch.cat([root_motion_local, body_x], axis=-1)      # body 输入 = 预测 local root + 当前 body
...
output = torch.cat([root_motion_pred, predicted_body], axis=-1)

确定的事实:

  • 单次联合前向(同一 forwardself.training 分支,非分开训两个模型)。
  • Stage 2 root 输入用 Stage 1 的预测 root(free-running,非 teacher forcing)。
  • 训练时 root→local 转换 .detach()梯度不从 body 回流到 root。root 分支只由自身 loss 训,body 把 root 预测当常数。
  • 输出 = cat([root_pred(5), body_pred(364)]) = 369,损失按 §4.1b 的 7 个分量加权(root 段贡献位置+heading 两项,body 段贡献关节位置/速度/旋转/接触/FK 五项)。
  • 维度:concat 模式下 root stage 输入 = motion_rep_dim*2(x 与 mask 拼接),输出 = global_root_dim(5);body stage 输入 = local_motion_rep_dim + input_dim,输出 = input_dim - global_root_dim(364)。

两段是否分阶段 warmup 由 §4.3 的两阶段课程决定(phase-1 纯文本、phase-2 加约束),不是 loss 权重层面的调度——损失权重全程固定为 §4.1b 的数值。

4.3 训练配方与两阶段课程(report §4.3,确定值)

优化器/调度(report 直接给出)

  • 优化器 Adam-atan2,学习率 2e-5
  • diffusion 训练步数 T=1000;总训练 1,000,000 步
  • batch 2048(最佳模型,16×A100 SXM4-80GB;§6 的消融模型多在 8 GPU / batch 1024 / 20 fps,最佳模型为 16 GPU / 30 fps)。
  • EMA:每 10 步更新,decay 0.995,测试期用 EMA 权重。
  • GT 裁到最长 10 秒;首帧 root 平移到原点上方;首帧 heading 随机化(配合 c_dir,§4.4)。
  • 变长序列同 batch,损失按 length mask(§4.1b)。

两阶段课程

  • Phase 1(前 500k 步):纯 text-to-motion,不喂任何约束;用 dropout 0.1。
  • Phase 2(后 500k 步):text + 运动学约束混合训练;移除 dropout(以免把直接覆写进输入的约束 drop 掉)。
    • 约束从预定义模式随机采样:全身关节位置稀疏关键帧 / 手脚位置+旋转的随机子集稀疏关键帧 / 2D root 位置+heading 稀疏关键帧 / 2D root 位置+heading 稠密路径 / 脚接触配置稀疏关键帧。
    • 2 个约束模式混合的概率 25%完全不给约束(只留文本)的概率 10%
    • 稀疏约束的关键帧最大数从 1 线性增长到 20,且采样偏向更少关键帧(贴近真实使用)。
  • 全程:文本以 10% 概率 drop(CFG)。

消融「No Train Curriculum」(直接 1M 步混训、text-only 与 text+constraint 等概率、无 dropout)文本/质量相当但约束误差上升——两阶段课程是被报告验证过的设计,照搬即可。

4.3b 约束 inpainting 注入(训练期必须联合喂入)

twostage_denoiser.py:98-103motion_mask_mode="concat"):

if motion_mask is None or observed_motion is None:
    motion_mask = torch.zeros_like(x); observed_motion = torch.zeros_like(x)
x = x * (1 - motion_mask) + observed_motion * motion_mask   # 约束位置钉成观测值
x_extended = torch.cat([x, motion_mask], axis=-1)           # mask 作额外输入通道

训练时 observed_motion/motion_maskmotion_rep.create_conditions(§5.3)生成并作为输入通道喂入——这是「约束侧」在训练期就联合学习的实现。

4.4 条件注入方式(model/backbone.py

每个 stage 是 TransformerEncoderBlock,注入为 prefix-token 拼接(非 AdaLN/cross-attn)。固定 num_text_tokens=50 的内部构成(report §4.2 揭示):

  • text:1 个 LLM2Vec 文本嵌入(R⁴⁰⁹⁶)经 Linear(llm_dim→latent) → 1 token。
  • extra/register:49 个全零 token(report 称 c_extra ∈ R^{49×4096},非学习,但起到类似 register token 增容的作用)→ 与文本 token 一起填满 50 的固定长度。消融「No Extra Tokens」显示去掉它会损害 R-precision 与 FID。
  • timestep:TimestepEmbedder(PE 取值 → Linear-SiLU-Linear)→ 1 token。
  • first_heading_angle(c_dir):Linear(2→latent) → 1 token(input_first_heading_angle=True)。配合训练期随机首帧 heading 增广(§4.3),让模型能从任意朝向起手、测试期由 c_dir 指定。
  • 拼接 xseq=cat([text(1)+extra(49), time, (heading), motion]) → 标准 TransformerEncoder → 取 motion 段 → output_linear
  • 位置编码:非学习正弦 PE(max_len=5000),加在整条 xseq 上(含 prefix)。
  • 显式禁用 nested tensor / flash attention(:224,有 bug)。

注意:「50 token」不是 50 条文本 token,而是 1 文本 + 49 零 register。训练离线缓存文本特征时缓存的是单个 4096 维嵌入,零 token 在模型内部补齐,无需缓存。

4.5 dual-CFG 的训练侧条件采样(model/cfg.py + report §4.3)

采样期 separated 三路揭示训练期必须能独立产出「有文本无约束 / 有约束无文本」两路:

text_feat   = cat([text_feat, 0*text_feat, 0*text_feat])       # 路2/3 文本置零
motion_mask = cat([0*motion_mask, motion_mask, 0*motion_mask]) # 路1/3 约束置零(mask清零→不钉值)
# out_text(有文本无约束) / out_constraint(有约束无文本) / out_uncond(都无)
out = out_uncond + w_text*(out_text - out_uncond) + w_constraint*(out_constraint - out_uncond)
  • 文本置空 = 0*text_feat + text pad_mask 全 False;约束置空 = 0*motion_mask(清零→不钉值)。
  • 训练侧的具体调度(report §4.3,确定值,非待定)
    • 文本 dropout 全程 10%(两阶段都做),用于 classifier-free guidance。
    • 约束的「有无」由两阶段课程控制,不是对称的 per-feature dropout:phase-1(前 500k 步)纯文本、不喂约束;phase-2(后 500k 步)才混入约束,且其中 10% 的步数不给约束(只留文本)。
    • phase-1 用 dropout=0.1,phase-2 移除该 dropout(避免把直接覆写进输入的约束 drop 掉)。
  • 因此 training/curriculum.py 要实现的是「两阶段切换 + 约束采样调度(含 10% 无约束)+ 全程 10% 文本 drop」,而非两个独立的 drop 概率。详细约束采样模式见 §4.3 课程与 §5.3。

4.6 训练 step 伪代码(薄训练层核心)

denoiser.train()
feats = motion_rep(local_rot, root_pos, to_normalize=True, lengths=L)     # GT → 369维 (§5)
# 按当前 phase 采样约束:phase-1 无约束;phase-2 按预定义模式采样,10% 步无约束
obs, mask = sample_constraints(motion_rep, feats, phase)                  # §4.3 课程 + §5.3
# 全程 10% drop 文本(CFG);phase-1 额外 0.1 dropout,phase-2 不 drop 约束
text_feat, text_pad = drop_text(cached_text_feat, p=0.10)                 # §4.5
t     = randint(0, 1000, (B,))
noise = randn_like(feats)
x_t   = diffusion.q_sample(feats, t, noise)                               # §4.1
pred  = denoiser(x_t, x_pad, text_feat, text_pad, t, first_heading_angle, mask, obs)  # §4.2
loss  = weighted_smooth_l1_7terms(pred, feats, skeleton)                  # §4.1b(含 FK 项)

5. 薄训练层(二):数据管线 BVH → 369 维特征 + 约束【源码】

训练标签必须按 kimodo 的同一算法生成,否则与权重不兼容。好消息:编解码逻辑全在 KimodoMotionRep直接 import 复用,无需手抄

数据集说明:Kimodo 官方训练集是 Bones Rigplay(700 小时光学动捕,170 人,原生 27 关节,retarget 到 SOMA/G1/SMPL-X),非公开。发布的 SOMA-SEED 系列 checkpoint 即在其 SOMA retarget 版上训练;本方案复现时以 SEED checkpoint 的 SOMA30 表示为对齐基准,自有数据(任意 BVH 动捕)按本节算法编码到同一 369 维空间即可训练。下文「BVH」泛指接入的动捕源。

5.1 动作表示布局(KimodoMotionRep,SOMA30,J=30 → 369 维)

block 形状 通道区间 含义
smooth_root_pos [3] [0:3](x=0,y=1,z=2) 平滑后 root 位置
global_root_heading [2] [3:5] root 朝向 (cos,sin)
local_joints_positions [30,3] [5:95] 相对平滑 root 的关节位置
global_rot_data [30,6] [95:275] 全局关节旋转(6D,矩阵前两列)
velocities [30,3] [275:365] 关节速度
foot_contacts [4] [365:369] 脚接触 [左跟,左趾,右跟,右趾](左侧在前)
  • 369 是算出来的(3+2+90+180+90+4);若用 SOMA77(J=77)则 D=933,布局公式不变。canonical 骨架须从 checkpoint/stats 维度反查,不从源码假设。
  • split:root 段 = [0:5](global_root_heading 是 last_root_feature),body 段 = [5:369];另有 local_root_dim=4(rot_vel 1 + vel_xz 2 + y 1)供 global_root_to_local_root 用(§4.2 的两阶段转换)。

5.2 训练标签的精确生成算法

编码正向 KimodoMotionRep.__call__:FK → compute_heading_angleget_smooth_root_poscompute_vel_xyzfoot_detect_from_pos_and_velmatrix_to_cont6dnormalize

  • foot_contactsfeet.py):(speed < vel_thres) AND (height < height_thresh),speed=3D 速度 L2,height=y 分量。阈值在调用处硬编码 kimodo_motionrep.py:91vel_thres=0.15height_thresh=0.10。每侧只取前 2 个脚关节([:2],丢 ToeEnd)。decode 时 >0.5 二值化。
  • smooth_root_possmooth_root.py):不是高斯/盒式滤波,是 ADMM 最小加速度优化(多重网格)。只平滑 x/z,y 原样透传,输出轴序 [0,2,1]。硬编码:margin 半径 0.06smooth_signal(pos_weight=0, alpha_overrelax=1.8, admm_iters=500)(注意≠类默认 100/1.0)、stencil [-1,2,-1]、ADMM stepsize 0.25·sqrt(diag_max)不可微(内部 .detach().cpu().numpy() 走 SciPy LU)。
  • velocitiesfeature_utils.py: compute_vel_xyz):fps·(pos[1:]-pos[:-1]),末帧复制 padding。
  • 6D 旋转geometry.py):matrix_to_cont6d 取矩阵前两列(列优先);cont6d_to_matrix 用 Gram-Schmidt(固定 X → Z=X×Y_raw → Y=Z×X)。存的是 global 旋转。

smooth_root 不可微不影响训练:它只产 smooth_root_pos 这块 GT 标签(离线常量),梯度只在网络输出↔GT 间流动。但它(CPU 逐 batch),应离线预计算并缓存 369 维特征,不要每个 epoch 重算。

5.3 约束采样(create_conditionskimodo_motionrep.py:222-306

生成稠密 [T,369]observed_motion + bool motion_mask(标记哪些 (帧,通道) 是真约束),batched 后 [B,T,369]。约束→通道映射:

约束 key 写入通道
smooth_root_2d(2D 路径/waypoint) 通道 0(x)、2(z)
root_y_pos 通道 1
global_root_heading [3:5]
global_joints_rots 转 cont6d,按 t*J+j 展平 scatter 进 [95:275]
global_joints_positions 减 smooth-root 局部参考(y=0)转 local,scatter 进 [5:95];要求同帧已约束 smooth_root,否则 ValueError
  • velocities、foot_contacts 从不作约束注入(纯模型预测输出)。
  • 同 (帧,通道) 多约束命中时 get_unique_index_and_data「首次写入者优先」去重(conditioning.py:18-28)。
  • 约束类来自 constraints.pyRoot2DConstraintSet / FullBodyConstraintSet / EndEffectorConstraintSet,各自 update_constraints 填 data/index dict。

训练时约束如何采样:report §4.3 已给出明确模式(见 §4.3 课程)——phase-2 才采样,模式包含全身关节位置 / 手脚位置+旋转子集 / 2D root 位置+heading(稀疏关键帧与稠密路径)/ 脚接触配置;2 模式混合 25%、无约束 10%、关键帧数 1→20 线性增长且偏向更少。把这些模式映射到本节五类约束 + §5.3 的通道写入即可,无需另行猜测稀疏度。

5.4 编解码可逆性(inverse

inversekimodo_motionrep.py:166-220)从 369 维还原 local_rot_mats / global_rot_mats / posed_joints / root_positions / foot_contacts / ...

  • 旋转链 global_rot↔6D↔local_rot 严格可逆;root x/z 经 smooth_root+hips_offset 重组代数可逆,y 直接保留。
  • 有损环节:smooth_root 的高频成分不可反推(但 local_joints 存了相对偏移,root 重建仍闭合);foot_contacts 二值化丢连续值;velocities 在 inverse 中完全未用(冗余特征,可从位置重算)。
  • 训练做几何 loss 时可用 inverse + skeleton.fk 验证。

5.5 坐标系/单位/fps 隐含约定(必须对齐发布权重)

约定 影响
坐标系 Y-up,地面 (x,z) 源 BVH 若 Z-up 必须先转轴
单位 米制(阈值 0.10/0.15/0.06 均裸数值) BVH 常用厘米,需 ÷100
fps 30 速度/角速度全 ×fps,stats 在该 fps 下统计;源数据须重采样到 30 fps
heading (RightLeg,LeftLeg) 髋连线,atan2(dz,-dx) SOMA30 hip 顺序 [right,left]
root root_idx=0 (Hips),世界系全局平移
关节顺序 必须匹配 skeleton.bone_order_names BVH 关节顺序不一致时用 get_skel_slice 重排,T-pose 用 to/from_standard_tpose 对齐

5.6 SOMA30 ↔ SOMA77 转换(definitions.py

  • to_SOMASkeleton77:246-256):以 77 关节 relaxed_hands_rest_pose 为底,填入 30 关节 local rot(缺失关节如手指填 relaxed rest)。
  • output_to_SOMASkeleton77:264-283):扩展 local_rot→77,重跑 77 的 FK,foot_contacts 从 4 通道扩 6 通道(toe-end 复制 toe-base)。推理输出 somaskel77 即经此。
  • 训练在 SOMA30 空间进行;导出/评测时转 77。

5.7 数据管线 import 清单与调用顺序

from kimodo.skeleton import SOMASkeleton30
from kimodo.motion_rep import KimodoMotionRep
# (BVH 解析可用 kimodo.skeleton.bvh / exports.bvh,或自备)
  1. skel = SOMASkeleton30()(自动从 joints.p 载 neutral_joints/parents/root_idx)
  2. BVH → (T,J,3,3) local_rot + (T,3) root_pos(关节顺序对齐 bone_order_names,单位转米,重采样 30fps,Y-up)
  3. rep = KimodoMotionRep(skel, fps=30, stats_path=...)
  4. 编码:feats = rep(local_rot, root_pos, to_normalize=True, lengths=...)[B,T,369]离线缓存
  5. 约束:rep.create_conditions(...)(observed_motion, motion_mask)
  6. 解码/验证:rep.inverse(feats, is_normalized=True)

KimodoMotionRep 已内部聚合 fk / cont6d / heading / vel / smooth_root / foot_detect / Stats,直接调 __call__/inverse/create_conditions 即复用全部逻辑,无需手工拼装几何函数。


6. 薄训练层(三):训练脚手架与数据增广【报告 + 源码】

薄训练层里 kimodo 包没有现成代码的部分。好消息:technical report §3-§4 已给出完整训练配方,这里只需把它落成通用工程代码,无需从外部框架移植算法。

6.1 训练循环与配置脚手架

  • 形态:标准 PyTorch(或 Lightning)训练 module + hydra 分层 config(沿用 kimodo 的 instantiate_from_dict 风格组织模型侧,训练侧另起 config 组)。
  • 必备组件(值见 §4.3):Adam-atan2(lr=2e-5)、EMA(decay 0.995,每 10 步)、变长 batch 的 length-mask collate(padding 到 batch 内最长)、ckpt + stats/motion/ 同步保存(§3.2)。
  • 分布式:报告最佳模型 16×A100 / batch 2048;复现可按显存缩到 §6.3 的 M/S 配置(8 GPU / 1024 或 4 GPU / 512)起步。
  • 训练 step 本体 ≈ §4.6 伪代码,约百行。

6.2 两阶段课程与约束采样调度

  • 直接实现 §4.3 的课程:phase-1(500k 步纯文本,dropout 0.1)→ phase-2(500k 步 text+约束,移除 dropout,约束按预定义模式采样)。
  • 约束采样器把 §4.3 的模式映射到 §5.3 的 create_conditions 五类约束:全身关节位置 / 手脚位置+旋转子集 / 2D root(稀疏 waypoint + 稠密路径)/ 脚接触。实现要点:2 模式混合 25%、无约束 10%、关键帧数 1→20 线性增长且偏向更少。
  • 全程文本 10% drop(CFG)。这些都是确定值,照 §4.3/§4.5 配置即可。

6.3 数据增广(report §3)

  • 文本增广:用 LLM(报告用 Qwen3-32B)把动作描述改写成统一句式(恒以「A [subject]...」开头)、多种详略级别。训练时在原始描述与改写之间按预定分布采样。
  • 运动增广(拼接):随机配对拼接两段动作 clip 以覆盖「多动作组合」prompt;为保证拼接处过渡自然,用在未增广数据上训练的扩散模型自身生成过渡片段(自举式增广)。
  • 训练采样混合:完整 clip / 单个或组合 atomic 子 clip / 拼接增广 clip × 原始文本 / LLM 改写文本,按预设分布采样。
  • 复现建议:先在原始 clip + 文本改写上跑通(拼接增广依赖一个已训好的模型,属第二轮),对应消融「data size」实验也未用拼接增广(§9.2)。

6.4 文本编码器(kimodo 包内已带)

  • kimodo 包 model/llm2vec/ 已含 LLM2VecEncoder + bidirectional_llama,接入侧齐全。
  • 训练:冻结 + 离线缓存 text_feat(单个 4096 维嵌入/条,见 §4.4——49 个零 register token 由模型内部补齐,不需缓存),不参与反传。
  • 复现 Kimodo 不需要重训/微调文本编码器,用现成 LLM2Vec 编码即可。

6.5 后处理(C++,推理/评测期,训练不涉及)

  • 见 §8:MotionCorrection/ C++ IK 脚滑清理 + 约束硬化 + root 修正,本地已有,编译即可。评测建议关闭以测裸模型。

7. 评测套件:精确指标与测试集构建【源码 + 文档】

复现训练后用官方协议验收。指标函数可直接 import 复用。

7.1 三轴指标(无总分,Pareto)

Motion quality(无需 GT)metrics/foot_skate.py,fps=30,dt=1/fps):

  • foot_skate_from_height:仅趾,趾 y<0.05m 视为触地,触地帧趾速度均值(m/s)。
  • foot_skate_from_pred_contacts:用模型 foot_contacts 加权趾速度均值(results.md 显示的 Skate 列,×100 cm/s)。
  • foot_skate_max_vel:触地帧最大滑动速度尖峰。
  • foot_skate_ratio(借鉴 GMD):height_thresh=0.05vel_thresh=0.2m/s,当前+下一帧都触地才算 on-floor,滑动帧占比。
  • foot_contact_consistencyvel=0.15/height=0.10 启发式接触 vs 模型预测的 XOR 一致率,1.0 满分。

Constraint following(无需 GT)metrics/constraints.pyContraintFollowroot_threshold=0.10m,全 L2):

  • constraint_root2d_err/_acc:root XZ vs smooth_root_2d,acc=误差≤0.10 占比;_p95evaluate_folder._to_p95nanquantile(0.95) 聚合。
  • constraint_fullbody_keyframe:关键帧全关节 L2 均值。
  • constraint_end_effector:仅 pos_indices(旋转约束 "EE Rot" 列永远 None,未实现)。

Text alignment(部分需 GT)metrics/tmr.py + model/tmr.py):

  • 相似度 cosine/2+0.5∈[0,1];R-precision R01-R10 = 100·(rank<k),near-dup threshold=0.99
  • FID = TMR 256 维 latent 上的 Fréchet(无独立 inception 网络)。FID/gen_gt需 GT,文献常报)/FID/gen_text(需文本不需 GT)。
  • TMR-SOMA-RP-v1(HF nvidia/TMR-SOMA-RP-v1,经 load_model(default_family="TMR") 加载),内部用 LLM2Vec 编码文本,bf16 默认(v1.1 fp32)。
  • TMR 用的是 TMRMotionRep(位置型、旋转不变:去 heading 的 local 位置+速度+脚接触,无 6D 旋转),与生成用的 KimodoMotionRep 不同。

7.2 测试集构建(benchmark/,五步)

  1. create_benchmark.py:HF 拉 testsuite 元数据 + 读 BONES-SEED BVH → 30fps 子采样 → to_standard_tposeKimodoMotionRep → crop+canonicalize+inverse → 写 gt_motion.npz;有 seed_constraints.json 则派生 constraints.json(~26GB,数小时,文档值未实测)。
  2. generate_eval.py:跑模型 → motion.npz(须含 posed_joints+foot_contacts,per-case seed,batch=1,后处理建议 OFF)。
  3. embed_folder.py:TMR-SOMA 嵌入 motion/gt/text → .npy
  4. evaluate_folder.py:两遍——generated pass(全 6 指标含 TMR)+ GT pass(仅 motion-quality + constraints,TMR 不重算)。SOMA30 自动升 77。
  5. parse_folder.py:按 split×category 加权平均,位置×100→cm,skate×100→cm/s。

7.3 泛化 split(introduction.md

  • content:held-out 语义类别(content_name 训练未见)→ 测文本泛化。
  • repetition:见过类别的新片段(文本已见)→ 测约束跟随泛化。
  • 类别:text-following(overview/timeline_single/timeline_multi)+ constrained(with/without text,各含 root/fullbody/end-effectors/mixture)。全套 22,474 case。

7.4 对标基线(results.md,关键数值,作复现目标)

指标 GT 上界 Kimodo-SEED-v1.1 说明
FID/gen_gt 0.000 ~0.03(content)/ ~0.005(repetition) 越低越好
R@3 (Overview) 89-94% 73-92% 检索上界即 GT
Skate (cm/s) ~1.7-2.1 ~3.3-4.6 越低越好
Contact consistency 1.000 ~0.97-0.98
约束位置误差 (cm) ~0.000 FB ~1.1-1.3 / EE ~1.4-1.8 / 2DRoot ~2.6-3.1 GT 生成了约束故≈0

注:本表为发布的 SEED-v1.1 benchmark(results.md,SOMA 30fps)数值。technical report 的 Tab.1/Tab.2 是在原生 27 关节、20fps 上的消融/scaling 结果(report 明确声明二者不可直接比较);report §2.2 另给 SOMA 30fps 最佳模型的约束误差(FB 3.21cm / EE-pos 3.63cm / EE-rot 6.88° / root 3.63cm),口径与本表不同,勿混用。

7.5 复刻评测需要

  • importkimodo.metricsFootSkate*/FootContactConsistency/ContraintFollow/TMR_EmbeddingMetric + compute_metrics/aggregate_metricskimodo.constraints.load_constraints_lstSOMASkeleton77/30
  • 外部模型:仅 TMR-SOMA-RP-v1(任何 TMR 指标都需要,内部依赖 LLM2Vec)。
  • 数据:GT 由 create_benchmark.py 从 BONES-SEED BVH + benchmark 元数据构建。
  • 无 GT 可算:全部 foot-skate / constraint / FID/gen_text / t2m_R必须 GTFID/gen_gtm2m_*t2m_gt_*、所有 per_motion_mean_gt 上界行。

8. 后处理(推理期,训练后接入)【源码】

postprocess.py: post_process_motion:186)+ C++ 库 MotionCorrection/(C++17 IK + 轨迹校正,Python 绑定):

  • 输入:生成的 local 旋转、root 位置、foot_contacts、约束列表。
  • 功能:脚滑清理(按接触标签锁脚 + IK 反解)、约束硬化(扩散软约束 → 后处理硬性满足)、root 修正(root_margin=0.04,偏离超阈值校正轨迹)。
  • 多段拼接按段后处理(kimodo_model.py:296)。
  • 训练不涉及;本地已有,编译即可,推理/评测时按需启用(评测建议 OFF 以测裸模型)。

9. 剩余空白(分层:已确定 vs 真未知)

9.1 训练侧已完全确定(推理代码 + 报告交叉印证)

  • 噪声调度 cosine(max_beta=0.999, base_steps=1000)、q_sample 加噪、x0 预测(§4.1)。
  • 损失:7 项加权 smooth-L1(含 FK 项),权重 10/2/10/3/10/4/5(§4.1b,report §4.3 直接给出)。
  • 两阶段单次联合前向、Stage 2 用预测 root(free-running)、root→body 梯度 .detach()(§4.2)。
  • 训练超参:Adam-atan2 / lr=2e-5 / 1M 步 / batch 2048(16 GPU) / EMA decay 0.995@10 步 / 裁 10 秒 / 随机首帧 heading(§4.3,report §4.3)。
  • 两阶段课程:phase-1 500k 纯文本(dropout 0.1) → phase-2 500k text+约束(去 dropout);2 模式混 25%、无约束 10%、关键帧 1→20;全程文本 drop 10%(§4.3/§4.5,report §4.3)。
  • text 注入 = 1 文本 token + 49 零 register token + time + heading,prefix 拼接 + 正弦 PE(§4.4,report §4.2)。
  • dual-CFG 采样 w_text=2 / w_constr=2,DDIM 100 步(§4.5,report §4.4)。
  • 表示布局、训练标签算法(foot 0.15/0.10、smooth_root ADMM/margin 0.06)、约束→通道映射、编解码、坐标系/单位/fps(§5)。
  • 模型装载/权重对齐/闭环验证(§3)。

9.2 真正需要实验/数据决定的部分

  1. 数据集本身:官方 Bones Rigplay(700h,非公开)无法直接获得;复现需用自有动捕,规模与多样性直接决定约束跟随上限(report §6.3 显示数据量主导约束精度)。
  2. 数据采样分布细节:完整 clip / 子 clip / 拼接增广 / 原始文本 vs LLM 改写之间的具体采样比例,report 只说「按预定分布」,未给数值——这是少数真未知。
  3. scaling 取舍:受算力约束选 L/M/S 模型与 4/8/16 GPU 配置(report §6.3 给了趋势与对应指标,可据此取舍)。

与早期判断不同:lr/batch/步数/EMA/损失权重/课程/dropout 均非未知,report §4.3 已明确给出,直接照搬。真未知只剩「数据本身 + 采样分布的具体比例 + scaling 取舍」。

9.3 需与发布权重核对的小项

  • 训练 fps 与世界单位(§5.5;config 标 30/米制,须与 stats 口径一致)。
  • canonical 骨架 J=30/77(§5.1;从 checkpoint 维度反查,369 vs 933)。
  • smooth-L1 的 beta(L1↔L2 切换阈值)是否取 PyTorch 默认 1.0(report 未给,按默认起跑)。

10. 精读进度索引

状态 ⬜ 未精读 / 🟡 已概览待精读 / ✅ 已精读。包根:...\NvlabKimodoQuickServer1\kimodo\kimodo\

模型与训练核心(已基本钉死)

状态 文件 结论位置
model/load_model.py + loading.py + registry.py + common.py 装载/权重/stats(§3.1-3.2)
model/kimodo_model.py 顶层构造 + 推理链 + inference-only 假设(§3.3-3.5)
model/twostage_denoiser.py 维度 + forward + 训练耦合(§4.2)
model/diffusion.py cosine + q_sample + x0 + DDIM(§4.1)
model/cfg.py dual-CFG 三路 → 训练期文本 drop + 约束课程采样(§4.5)
model/backbone.py prefix-token 注入 + 正弦 PE(§4.4)
scripts/generate.py 端到端调用链(§3.4)

表示与几何(已基本钉死)

状态 文件 结论位置
motion_rep/reps/kimodo_motionrep.py 布局 + 编解码 + create_conditions(§5.1-5.4)
motion_rep/reps/base.py slice_dict + normalize split(§5.1)
motion_rep/stats.py (x-mean)/sqrt(std²+eps), eps=1e-5(§3.2)
motion_rep/smooth_root.py ADMM 平滑,margin 0.06,不可微(§5.2)
motion_rep/feet.py vel 0.15/height 0.10,每侧前 2 关节(§5.2)
motion_rep/conditioning.py observed/mask 布局 + 首次写入优先(§5.3)
motion_rep/feature_utils.py heading/vel/RotateFeatures(§5.2,5.5)
geometry.py 6D=前两列,Y-up(§5.2,5.5)
skeleton/{kinematics,transforms,definitions,base}.py FK + SOMA30↔77(§5.5-5.6)
🟡 constraints.py 5 类约束已枚举(§5.3);待补 EndEffector 的 bone_index/pos_indices

评测(已读,服务 §7)

状态 文件 结论位置
metrics/{foot_skate,constraints,tmr,base}.py 三轴指标精确算法(§7.1)
model/tmr.py + motion_rep/reps/tmr_motionrep.py TMR 加载 + 表示差异(§7.1)
docs/source/benchmark/{pipeline,results,metrics,introduction}.md 测试集构建 + 对标数值(§7.2-7.4)

后处理(已概览)

状态 文件 结论位置
🟡 postprocess.py + MotionCorrection/ C++ 接口概览(§8);待补 C++ 参数细节
model/llm2vec/* 接入侧齐全;文本特征 = 单个 4096 维嵌入/条(49 零 register 由模型补齐,§4.4),训练用缓存,优先级低

11. 实施路线图(建议顺序)

  1. 第 0 步 闭环验证(§3.6):load_model 跑通官方推理 → 自构 TwostageDenoiser 灌权重 → 确认前向数值一致。这是地基,不过不往下走。
  2. 数据管线(§5):BVH → 对齐坐标系/单位/fps → KimodoMotionRep 编码 → 离线缓存 369 维特征 + LLM2Vec text_feat 缓存。验证:inverse 往返一致。
  3. 薄训练层(§4):实现 loss.py(7 项加权 smooth-L1 含 FK,§4.1b)+ curriculum.py(两阶段课程 + 约束采样 + 10% 文本 drop,§4.3/§4.5)+ loop.py(Adam-atan2 + EMA,§4.3/§6.1)。先按 phase-1 text-only 跑通,再开 phase-2 约束。
  4. 过拟合自检:小批数据上能否过拟合 + 用官方 stats 解码出合理动作。
  5. 评测对齐(§7):建 content/repetition 测试集 → 复刻指标 → 对 §7.4 基线。
  6. 规模化训练(§9.2):按 §4.3 配方(1M 步、batch 2048/16 GPU、两阶段课程)放量;唯一靠实验微调的是数据采样分布与 scaling 取舍。

一句话:模型、表示、训练配方均已可照实现(§3-5 为【源码】,§4.3/§4.1b 训练超参为【报告】明确给出),训练层是通用薄壳。先做闭环验证拿到 ground truth,再逐层往上搭。

⚠️ **GitHub.com Fallback** ⚠️