简介:本资源是论文《Human Motion Diffusion Model》第一作者开源的PyTorch实现,面向计算机视觉、图形学及AI生成方向的研究者与开发者,聚焦于高质量3D人体运动序列的生成任务,适用于动画制作、虚拟现实、动作识别等实际场景。压缩包共125个文件,含84个Python训练与推理脚本、7个Shell工具脚本、6个Markdown文档(含DiP.md等核心说明)、4个PNG/GIF可视化示例(如in_between_edit.gif、upper_body_edit.gif),以及模型参数(.h5、.pkl)、配置(.yaml)和许可证文件,整体仅3.33MB,轻量易部署。目前已有44人学习下载。用户可直接复现扩散模型训练流程、执行3D运动生成与编辑(如上下身局部控制)、调用预训练权重进行推理,并借助清晰的README与模块化代码结构快速理解姿态建模、噪声调度与逆扩散采样等关键技术实现细节。
1. 项目概述:当文本真的能“驱动”一具3D身体
文本生成3D人体运动,这个方向在几年前听起来还像是科幻设定。你输入一句“一个人踉跄着后退两步,用手捂住脸”,模型就得输出一段骨骼动画。而这个领域绕不开的一个里程碑,就是这篇由Guy Tevet等人提出的《Human Motion Diffusion Model》(简称MDM)。标题里提到的“第一作者开源的PyTorch实现”,指的就是这个项目在GitHub上的官方代码库。如果你在找“开源的人体动效生成模型”、“文本到动作的扩散模型实现”,或者单纯想在自己项目里接一段“文字转动作”的能力,这个仓库基本是绕不开的参考范本。
我最初接触这个项目时,并没有把它当成一个普通的“读论文配代码”的示例,而是把它当成一个完整的工业级样本在研究。因为它背后涉及的东西远不止“模型怎么搭”:文本如何编码、动作序列如何表示、扩散模型如何在这个非欧几里得结构上运作、生成的质量如何评估,每一环都有讲究。这篇文章我会按照自己从“读论文、看代码、跑通demo、改造源码”这条完整链路来拆解,把其中的技术设计、实操步骤和踩过的坑一次讲清楚。不管你是刚接触生成模型的学生,还是想在业务里落地的工程师,都应该能从这篇里找到对你有用的部分。
先说结论:MDM的核心创新在于,它把扩散模型用在了“人体骨骼运动序列”这种结构化的时间序列数据上,并且通过一个轻量级的transformer架构和Classifier-Free Guidance策略,在HumanML3D和KIT-ML两个主流数据集上拿到了当时最好的文本到动作生成效果。而这份官方PyTorch实现,完整复现了论文里的训练、采样和评估流程,代码结构清晰,可扩展性也相当不错。
2. 为什么要用扩散模型做动作生成
这一节先不讲代码,我想聊聊“为什么”。你只有理解了MDM为什么选择扩散模型,才能真正看懂代码里那些设计。这也是我在阅读源码时觉得最有价值的部分。
2.1 动作生成问题的本质难点
人体运动本质上是一个高维时序信号。假设你有一个人体骨架,大概22到24个关节点,每个关节点用6D旋转表示(6D rotation),再加上全局根节点的位移和旋转,那么每一帧的特征维度大约是263维。如果生成2秒、60帧的动作,那就是一个263×60的矩阵。如果生成更长的序列,这个维度还会线性膨胀。
所以这个问题的第一难点是:输出空间巨大且具有强结构约束。随便生成一组数,大概率是不像人动作的——关节会扭曲、脚会滑步、身体会穿模。第二个难点是:同一段文本描述可能对应多种合理动作,也就是“一对多”的映射关系。传统的回归模型(比如直接让模型输出动作序列)往往会把这种多模态性“平均”掉,生成的动作会趋于平庸、模糊、没有个性。
2.2 扩散模型的优势:从噪声中“雕刻”运动
扩散模型的基本思路,大家可能已经不陌生了——先定义一个前向过程,逐步给数据加噪声,直到变成纯高斯噪声;然后训练一个神经网络,学习逆向过程,即从噪声中一步步还原出原始数据。生成过程就是从纯噪声出发,通过逐步去噪得到一个样本。
为什么这个机制适合动作生成?我自己的理解有三个层面:
**第一,扩散模型天然支持“一对多”生成。**因为采样过程中有随机噪声注入,同一个文本条件,每次采样都能得到不同的、合理的动作变体。这在动作生成这种“一个文本描述对应多种合理动作”的场景里特别合适。
**第二,扩散模型对高维结构化数据有很强的建模能力。**你不需要像VAE那样担心后验坍缩,也不像GAN那样要小心翼翼地平衡生成器和判别器。只要训练稳定的去噪网络,生成质量就有保障。
**第三,扩散模型的训练目标更直接。**MDM并没有使用标准DDPM里的噪声预测(predict the noise),而是直接让网络预测“去噪后的干净序列”。这个设计细节很关键,我们后面讲代码时会展开。
2.3 MDM相比其他方案的取舍
在MDM之前,动作生成的主流方案包括:
| 方法类型 | 代表思路 | 主要问题 |
|---|---|---|
| 回归模型 | 文本特征直接映射到动作序列 | 生成动作趋于平均化,缺少多样性 |
| VAE系列 | 学习隐空间再解码 | 容易后验坍缩,生成质量不稳定 |
| GAN系列 | 生成器对抗判别器 | 训练不稳定,模式坍塌 |
| 自回归模型 | 逐帧预测 | 误差累积,长序列质量崩溃 |
MDM选择扩散这条路,本质上是拿“更多的采样时间”换“更好的生成质量和多样性”。而且它在架构上刻意保持了轻量——没有用复杂的图神经网络,而是一个纯transformer的encoder-only结构。这个选择带来的好处是:代码约简,易于复现,而且后续如果要改loss、加模块,改造成本很低。
3. 核心实现细节拆解:模型三件套
这一节进入正题。我按照自己阅读代码的路径,把整个项目拆成几个核心模块来解读:文本编码、动作编码、扩散模型主体、训练与采样逻辑。
3.1 整体框架:一个“条件扩散”模型
如果你打开仓库的model/mdm.py,会看到MDM这个类,它本质上是把三样东西组合在一起:
class MDM(nn.Module): def __init__(self, modeltype, njoints, nfeats, num_actions, translation, pose_rep, glob, glob_rot, latent_dim, ff_size, num_layers, num_heads, dropout, activation, data_rep, cond_mode, cond_mask_prob, ...): super().__init__() self.cond_mode = cond_mode ... self.embedding = Embedding(...) self.sequence_pos_encoder = PositionalEncoding(...) self.cond_emb = nn.Sequential(...) # 条件编码 self.model = Transformer(...) # 核心去噪网络 self.rot2xyz = Rotation2xyz(...) # 旋转表示转3D坐标 if self.cond_mode == 'text': self.emb_text = nn.Linear(768, self.latent_dim)这个MDM类就是整个系统的骨架。它的核心参数包括:latent_dim(默认512)、ff_size(前馈层维度,默认1024)、num_layers(transformer层数,默认6)、num_heads(注意力头数,默认8)。这几个参数决定了模型的规模,也是后面你自己训练调参时最常动的地方。
从外部看,它接收两个输入:一个是不带条件的动作序列(加噪后的),一个是文本条件。然后输出的是“预测的干净动作序列”。这里有一个论文里明确说明的创新点:它预测的是$x_0$而不是噪声$\epsilon$。
为什么这样做?在标准的DDPM里,模型预测噪声,然后通过$x_0 = (x_t - \sqrt{1-\bar{\alpha}_t}\epsilon)/\sqrt{\bar{\alpha}_t}$来反推干净样本。但在动作生成场景下,我们希望模型在每一步去噪时都在“思考这个动作长什么样”,而不是去“思考噪声长什么样”。直接预测$x_0$,配合后续的几何损失(geometric loss),可以让模型在训练阶段就关注动作本身的质量,比如关节角度是否合理、骨骼长度是否一致。
3.2 文本条件编码:怎么把一句话变成向量
文本条件处理在model/cmam.py里。核心逻辑是这样的:
class TextCond(nn.Module): def __init__(self, emb_trans_dim, emb_text_dim, emb_type): super().__init__() self.emb_trans_dim = emb_trans_dim # 512 self.emb_text_dim = emb_text_dim # 768 self.emb_type = emb_type if emb_type == 'bigru': self.emb_text = nn.Linear(emb_text_dim, self.emb_trans_dim) self.gru = nn.GRU(self.emb_trans_dim, self.emb_trans_dim, bidirectional=True, batch_first=True) self.linear = nn.Linear(self.emb_trans_dim*2, self.emb_trans_dim) elif emb_type == 'openai': self.emb_text = nn.Linear(self.emb_text_dim, self.emb_trans_dim)这里有两个可选项:bigru和openai。
如果你用openai模式,代码会调用OpenAI的CLIP模型(clip.load("ViT-B/32", device=...)),把文本编码成768维的向量,然后通过一个线性层映射到512维。这是最简单直接的方案。
如果你用bigru模式,则会把文本的word-level embedding(同样来自CLIP)过一层双向GRU,取最后时刻的隐状态作为整句的语义向量。双向GRU的好处是能捕捉词序信息,但实测下来,在这个任务上openai模式已经足够好,而且速度更快、更省显存。
论文里默认使用的是CLIP的text encoder,这也是我推荐的模式。原因在于CLIP训练时对齐了文本和图像特征,它的文本表征空间对“动作描述”这类语义信息已经有了比较好的编码。实际测试中,描述“一个人向前走”和“一个人向后走”,CLIP给出的向量在余弦相似度上就有明显区分。
3.3 动作序列编码:从原始数据到模型输入
人体运动数据不能直接喂给transformer。在进入模型之前,要经过一系列处理。MDM定义了一套数据表示方式,在utils/rotation_conversions.py和data_loaders/humanml/utils/metrics.py里都有体现。
动作数据的一般流程是这样的:
- 原始数据:HumanML3D数据集给的是SMPL参数或者关节旋转(axis-angle)。
- 转换为6D旋转表示:项目把关节旋转从axis-angle转为6D表示(6D rotation),这是为了避免角度表示的周期性不连续问题。
- 加上全局信息:在每一帧的最前面拼接上根节点的位移和旋转信息,最终得到263维的特征。
- 送入线性层:在
Embedding模块中,用nn.Linear(njoints*nfeats, latent_dim)把263维映射到512维。 - 加位置编码:使用
PositionalEncoding给序列加上时序信息。
这里值得多说一句:为什么用6D旋转而不是四元数或者欧拉角?因为在深度学习里,网络的输出往往是连续的实数向量,而欧拉角存在万向锁问题,四元数的单位长度约束又不好直接满足。6D表示是2019年《On the Continuity of Rotation Representations in Neural Networks》提出的方法,它用6个连续数值表示一个旋转矩阵,本质上是把$SO(3)$流形投影到$\mathbb{R}^6$再恢复,既避免了不连续问题,又不会有维度冗余。
3.4 扩散过程的实现:加噪与去噪
MDM的扩散逻辑在diffusion/diffusion.py和diffusion/gaussian_diffusion.py里。我读代码时重点关注了两个部分。
前向加噪(training阶段):
在gaussian_diffusion.py里,q_sample函数实现了从干净数据$x_0$得到第$t$步加噪数据$x_t$的操作:
def q_sample(self, x_start, t, noise=None): noise = default(noise, lambda: torch.randn_like(x_start)) return ( extract_into_tensor(self.sqrt_alphas_cumprod, t, x_start.shape) * x_start + extract_into_tensor(self.sqrt_one_minus_alphas_cumprod, t, x_start.shape) * noise )这是在训练时执行的。流程是:随机采样一个时间步$t$,按公式对干净的输入$x_0$加噪得到$x_t$,然后把$x_t$和时间步$t$、文本条件一起送入模型,让模型预测$x_0$,再用MSE loss计算预测值和真实值的差距。
反向去噪(sample阶段):
在gaussian_diffusion.py的p_sample_loop中:
def p_sample_loop(self, model, shape, noise=None, clip_denoised=True, model_kwargs=None, progress=False): ... for i in reversed(range(0, self.num_timesteps)): t = torch.full((batch_size,), i, device=device, dtype=torch.long) x = self.p_sample(model, x, t, clip_denoised=clip_denoised, model_kwargs=model_kwargs)它的流程是:初始化一个纯噪声(形状为[batch, njoints*nfeats, seq_len]),然后从$T$(论文里默认1000步)开始,一步步预测$x_0$,再根据$x_0$和预定义的方差表重新参数化得到$x_{t-1}$。如此循环,直到$t=0$,得到最终生成的动作序列。
这里需要注意的是,MDM在采样时支持两种方式:一种是ddpm(完整1000步迭代),另一种是ddim(可以大幅减少采样步数,比如50步)。在sample.py和scripts中,你可以通过--diffusion_steps参数来控制。实际用的时候,如果想快速出结果,DDIM配50步就够了;如果追求极致质量,DDPM的1000步在效果上会略好一点。
4. 把代码跑起来:环境配置与快速开始
理论说完,该动手了。这一节讲怎么在本机把MDM跑通。我以Linux系统为主,Windows下大部分步骤也适用,只是某些路径和预训练模型加载可能需要微调。
4.1 环境准备
推荐用conda创建独立环境,避免依赖冲突:
conda create -n mdm python=3.8 conda activate mdm然后安装PyTorch。这里根据你的显卡情况选择CUDA版本,例如CUDA 11.8:
pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117提示:MDM的原版代码比较老,用的PyTorch版本是1.x。如果你用PyTorch 2.x跑,大概率会遇到
torch.cuda.amp相关API变更的问题,建议先按旧版本搭环境。新版PyTorch的weights_only参数默认值变化也会影响预训练模型的加载,这个后面讲。
接着安装项目依赖:
pip install git+https://github.com/openai/CLIP.git pip install hydra-core==1.1.1 pip install einops pip install smplx pip install ema-pytorch pip install numpy==1.23.5这里有两个依赖要特别注意。
第一个是numpy版本。新版numpy(1.24+)移除了很多旧API,MDM代码里用了np.bool、np.int等已废弃的写法,直接用最新numpy会报AttributeError: module 'numpy' has no attribute 'bool'。所以务必锁定numpy版本。
第二个是SMPL模型文件。MDM在评估和可视化阶段需要SMPL人体参数模型。你需要去SMPL官网注册下载SMPL_NEUTRAL.pkl、SMPL_MALE.pkl、SMPL_FEMALE.pkl这三个文件,放到body_models/smpl/目录下。如果没有这些文件,可以训练模型,但没法做可视化渲染,也就看不到生成的动作长什么样。
4.2 下载与准备数据集
要复现论文效果,需要HumanML3D数据集。你可以从HumanML3D项目主页申请下载。下载后把数据集目录放到项目根目录下,或者通过软链接指向:
ln -s /your/path/to/HumanML3D ./dataset/humanmlHumanML3D数据集中包含了动作的文本描述,每条动作对应多段文本标注,这些标注质量参差不齐,但作为训练数据已经足够。论文里还用到了KIT-ML数据集做跨数据集泛化测试,如果是学习用途,先玩HumanML3D一个数据集就够了。
4.3 生成动作:用预训练模型跑通demo
仓库提供了预训练模型权重(save/humanml_only_text_03/等目录),下载后放到save/下。接着编辑sample.py或直接在命令行里指定模型路径:
python -m sample --model_path ./save/humanml_only_text_03/model000200000.pt --text_prompt "a person walks forward" --num_samples 1 --batch_size 1 --output_dir ./outputs这里有几个常用参数说明:
--text_prompt:输入文本描述--num_samples:要对同一个文本生成多少个动作变体--batch_size:一次生成几个,受显存限制--diffusion_steps:去噪步数,默认1000,可以设为50配合DDIM加速--guidance_param:Classifier-Free Guidance的权重,默认2.5
运行完成后,会在output_dir下生成.npy文件,里面是动作序列数据。你可以用render.py将它渲染成视频:
python -m render --input_path ./outputs/sample00_rep0.npy --output_path ./outputs/sample00.mp4如果前面SMPL模型放好了,这里就能输出一个可以直观看到“人在走动”的mp4视频。
4.4 用自己的文本生成动作
项目默认只支持在数据集里出现过的动作类别对应的文本描述。但实际测试下来,只要你的描述不算太离谱,模型还是能给出合理结果。比如:
"a person jumps with both legs" "a person walks in a circle" "a person crouches down then stands up"建议用“主语+动作+方式/方向”这种结构,避免过于抽象的词汇。比如“一个人悲伤地走”这种带情绪的描述,模型并不一定能理解情绪,但“一个人低着头慢慢走”就大概率能得到不错的结果。
实操心得:CLIP文本编码器对“短句”效果最好。如果你的输入描述很长,比如超过20个词,建议精简成核心的动作短语。我在测试时发现,“a person walks forward slowly with hands in pockets”的效果比“a person is walking in a forward direction at a slow pace while keeping both hands inside the pockets of their pants”要好很多。
5. 训练自己的模型:从零开始
跑通demo之后,很多人会想用自己的数据训练。这一节讲完整流程。
5.1 数据处理流程
HumanML3D的原始数据存储格式是.npy文件,每段动作序列加上对应的文本文件。数据预处理的核心目标是:把原始的SMPL参数转成模型输入的特征。
如果你想用自己的动作数据(比如动捕数据、Mixamo导出的FBX动画),需要先转换成SMPL参数格式,然后跑一遍HumanML3D同款预处理管线。这个管线在data_loaders/humanml/scripts/下,主要包括:
- 骨骼对齐:把动作重定向到SMPL骨骼。
- 运动特征提取:计算关节位置、速度、旋转、脚部接触等特征。
- 特征归一化:按数据集的均值和方差做标准化。
- 保存为npy:每个动作保存成一个
.npy文件,形状为[seq_len, njoints*nfeats]。
这个过程比较繁琐,最省力的做法是:先把动作转成SMPL的pose参数(72维,即24个关节×3),再调用项目里的motion_representation相关代码生成特征。如果只是想实验,我建议先用HumanML3D的现有数据训练,等把整个流程摸透了,再考虑换自己的数据。别一上来就想“用自己的数据”,预处理这一关就可能劝退很多人。
5.2 训练命令与关键参数
训练入口是train/training_loop.py,通过train.py启动:
python -m train --save_dir ./save/my_mdm --dataset humanml --cond_mode text --batch_size 64 --lr 1e-4 --num_steps 200000核心参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--batch_size | 64 | 单卡建议32~64,根据显存调整 |
--lr | 1e-4 | AdamW优化器的学习率 |
--num_steps | 200000 | 总训练步数 |
--cond_drop_prob | 0.1 | 条件丢弃概率,Classifier-Free Guidance需要 |
--lambda_rc | 0.01 | 重构损失权重 |
--lambda_vel | 0.1 | 速度损失权重 |
--lambda_fc | 0.01 | 脚部接触损失权重 |
--unconstrained | False | 是否训练无条件生成 |
这里重点讲一下cond_drop_prob。这个参数是Classifier-Free Guidance(CFG)的关键。训练时,有cond_drop_prob概率的样本会把文本条件置为全零向量,让模型学会“无条件生成”。采样时,模型同时预测“有条件的结果”和“无条件的结果”,然后按guidance_param的权重外推:
x = x_uncond + guidance_param * (x_cond - x_uncond)这样做的效果是:生成的样本会更精确地匹配文本条件,同时保留一定的多样性。默认guidance_param=2.5是论文调出来的比较好用的值。
5.3 评估指标
训练完成后,用eval.py评估生成质量:
python -m eval --model_path ./save/my_mdm/model000200000.pt --dataset humanml评估结果会输出几个关键指标:
- FID(Fréchet Inception Distance):衡量生成动作分布与真实动作分布的差异,越低越好。
- R-Precision:衡量文本和动作的匹配度,越高越好。
- Diversity:衡量生成动作的多样性,需要和真实数据的Diversity接近。
- MMDist:文本与动作特征之间的平均距离,越低越好。
论文在HumanML3D上的结果是FID约0.544(无MMAE的原始版本),配上好的数据和模型,复现这个值不难。我的实测经验是,如果FID能跑到1.0以内,视觉上生成的动作已经比较自然了。
6. 常见问题与排查技巧实录
最后这部分,整理一下我在跑这个项目时实际遇到的典型问题,希望能帮你少踩几个坑。
6.1 环境与依赖类问题
问题1:numpy版本导致np.bool报错
这是最常见的问题。解决方法是强制安装numpy 1.23.5:
pip install numpy==1.23.5如果还不行,就去代码里把所有np.bool改成bool、np.int改成int。
问题2:PyTorch 2.x下torch.cuda.amp报错
PyTorch 2.x把torch.cuda.amp.GradScaler改成了torch.amp.GradScaler。如果你非得用新版PyTorch,需要修改train/training_loop.py里的相关调用。但更省事的方案是直接用PyTorch 1.13。
问题3:加载预训练模型时weights_only报错
PyTorch 2.6之后,torch.load的weights_only默认值变成了True,这会导致加载MDM的checkpoint时报错。解决方法是改model_load.py里的torch.load(path, map_location='cpu', weights_only=True),或者简单粗暴地升级项目代码。
问题4:CLIP加载失败
clip.load("ViT-B/32")需要网络下载权重。如果下载超时,可以手动下载ViT-B-32.pt放到~/.cache/clip/目录下。
6.2 数据与训练类问题
问题5:显存不足(OOM)
生成动画时batch_size设1,num_samples设1也会占不少显存,因为transformer的序列长度覆盖了扩散步数。建议:
- 采样时把
--diffusion_steps降到50(配合DDIM) - 训练时把序列长度裁剪短一点(
--max_seq_len 60) - 使用混合精度训练
问题6:生成的动作“漂移”或“滑步”
这通常是因为训练时几何损失的权重不够,或者数据里本来就有滑步的样本。可以尝试加大--lambda_vel到0.2,或者换更高质量的数据。生成之后做一步“脚部接触修正”也会有效果,但这个项目没内置这个模块,需要自己实现。
问题7:文本描述对结果影响不大
如果你发现换不同的文本描述,生成的动作几乎一样,大概率是cond_drop_prob设成了0,或者文本编码没有正确传入模型。另一个可能就是guidance_param设得太小(比如接近0),CFG的效果没发挥出来。
问题8:训练loss下降但生成质量差
这是我在实际训练里遇到的比较诡异的问题。最后定位到原因是:训练时数据归一化的均值和方差与评估时不匹配。MDM的数据管线对特征做了标准化,如果你从不同入口加载数据(比如训练用dataset类,评估用npy直接读),很容易出现统计量不一致的情况。确保训练和评估都用同一套数据加载器。
6.3 我对这个项目的一些个人看法
MDM这份开源实现,优点和缺点都很明显。
优点:
- 代码结构清晰,模块化做得好。
model、diffusion、data_loaders、train四个目录职责分明,就算完全不懂扩散模型的人,顺着代码读一遍也能建立整体认知。 - 训练和采样流程完整,论文里的每个技术点都有对应实现。
- 提供了预训练权重,降低了复现成本。
缺点:
- 代码有一些历史遗留问题,比如对旧版numpy和PyTorch的依赖,新环境适配需要动手改。
- 部分模块写得比较“学术风”,没有太多工程化考虑。比如数据加载没有做缓存,大训练集上每次读盘都是瓶颈。
- 可视化依赖SMPL模型,没有网络的时候配置很麻烦。
如果你不只是想复现,而是想在此基础上做改动,我的建议是关注三个扩展点:
- 换文本编码器:把CLIP换成更强大的语言模型(如BERT、T5),看对生成质量是否有提升。
- 加动作控制:在采样过程中加入关键帧约束或运动轨迹约束,实现“文本+轨迹”联合控制。
- 换扩散形式:试试用Latent Diffusion的思路,把动作先编码到低维隐空间再扩散,可以大幅提速。
我记得之前看相关研究时,有一篇后续工作就是沿着MDM的思路,加了“运动轨迹控制”的支持,实现了类似“让角色走到某个位置再挥手”的效果。这正是MDM留下的扩展空间——它的架构足够简洁,以至于后续改进都能以它为基线去做对比。
从个人体会来说,MDM这个项目最值得学习的地方,不在于它拿了多少个SOTA指标,而在于它把一个相对复杂的研究问题(文本生成动作)组织成了一个结构清晰、可复现、可扩展的工程实现。即使你不想做动作生成方向,把它当作“如何把一个扩散模型思路落地成代码”的教材来读,也会有不少收获。
本文还有配套的精品资源,点击获取