GR00T N1.7源码学习(一):工程入口、模型结构与动作生成流程解析

1、GR00T N1.7由视觉语言骨干和动作模型组成

GR00T N1.7是一套面向机器人控制的Vision-Language-Action模型。模型接收相机图像、语言指令和机器人当前状态,输出一段连续的机器人动作序列。

GR00T N1.7内部包含两条比较明显的处理链路。第一条链路负责处理图像和语言,得到视觉语言特征;第二条链路负责将机器人状态、带噪动作以及视觉语言特征组合起来,生成连续动作。

模型主类定义在,

复制代码
gr00t/model/gr00t_n1d7/gr00t_n1d7.py

其中最主要的两个类是,

复制代码
class Gr00tN1d7ActionHead(nn.Module)
"""Action head component for flow matching diffusion policy."""

class Gr00tN1d7(PreTrainedModel)
"""Gr00tN1d7: VLA model with Cosmos-Reason2-2B (Qwen3-VL) backbone."""

Gr00tN1d7负责组织完整模型,内部包含视觉语言Backbone和动作头;Gr00tN1d7ActionHead负责根据视觉语言特征、机器人状态和带噪动作预测动作速度。完整的数据流如下,

图像、语言 -> Cosmos-Reason2-2B(基于Qwen3-VL) -> 视觉语言特征

视觉语言特征 + 机器人状态 + 带噪动作 -> DiT -> 动作速度场 -> Euler积分 -> 连续动作序列

这里需要先区分两个经常混在一起的概念。GR00T N1.7的动作网络使用DiT,也就是Diffusion Transformer,但动作的训练目标采用Flow Matching。DiT描述的是网络结构,Flow Matching描述的是训练和采样方法,两者并不冲突。

2、N1.7微调入口完成模型与数据配置

N1.7微调入口位于,

复制代码
gr00t/experiment/launch_finetune.py

命令行参数由tyro根据FinetuneConfig自动生成,

复制代码
ft_config = tyro.cli(FinetuneConfig, description=__doc__)

FinetuneConfig定义在:

复制代码
gr00t/configs/finetune_config.py

该配置类中包含模型路径、数据集路径、机器人类型、训练哪些模块、学习率、Batch Size、保存频率等参数。例如,

复制代码
@dataclass
class FinetuneConfig:
    base_model_path: str
    dataset_path: str
    embodiment_tag: str
    modality_config_path: str | None = None

    tune_llm: bool = False
    tune_visual: bool = False
    tune_projector: bool = True
    tune_diffusion_model: bool = True

    global_batch_size: int = 64
    learning_rate: float = 1e-4
    gradient_accumulation_steps: int = 1
    output_dir: str = "./outputs"

从默认值可以看出,常规微调不会更新完整的语言模型和视觉编码器,而是训练动作头中的状态编码器、动作编码器、动作解码器以及DiT主体。需要注意的是,配置项虽然叫tune_projector,但从后面的源码可以看到,它控制的并不是单独一个传统意义上的多模态投影层,而是动作头前后的多组特征映射模块。

复制代码
tune_llm = False
tune_visual = False
tune_projector = True
tune_diffusion_model = True

这种设置比较符合机器人数据集的实际情况。机器人示范数据的规模通常远小于视觉语言模型的预训练数据,如果直接更新完整VLM,不仅显存开销很高,也容易破坏原有的视觉语言能力。

进入主函数后,代码先解析embodiment_tag,

复制代码
from gr00t.data.embodiment_tags import EmbodimentTag

ft_config.embodiment_tag = EmbodimentTag.resolve(ft_config.embodiment_tag)
embodiment_tag = ft_config.embodiment_tag.value

embodiment_tag可以理解为机器人本体类型标识。不同机器人可能有完全不同的状态维度和动作维度,例如单臂机械臂、双臂机器人、人形机器人以及仿真环境中的机械臂,其关节数量和动作定义都不同。GR00T通过该标识选择对应的数据配置以及类别相关的动作编码器。数据集路径支持传入多个目录,

复制代码
dataset_paths = [
    path for path in ft_config.dataset_path.split(os.pathsep) if path
]

在Linux系统中,os.pathsep通常是冒号,因此可以通过下面的方式传入多个数据集,

复制代码
--dataset-path /data/task_a:/data/task_b

随后代码加载默认配置,并将用户传入的数据集注册到配置中,

复制代码
config = get_default_config().load_dict(
    {
        "data": {
            "download_cache": False,
            "datasets": [
                {
                    "dataset_paths": dataset_paths,
                    "mix_ratio": 1.0,
                    "embodiment_tag": embodiment_tag,
                }
            ],
        }
    }
)

接下来会覆盖N1.7使用的模型参数:

复制代码
config.model.tune_llm = ft_config.tune_llm
config.model.tune_visual = ft_config.tune_visual
config.model.tune_projector = ft_config.tune_projector
config.model.tune_diffusion_model = ft_config.tune_diffusion_model

config.model.load_bf16 = False
config.model.reproject_vision = False
config.model.model_name = "nvidia/Cosmos-Reason2-2B"
config.model.backbone_trainable_params_fp32 = True
config.model.use_relative_action = True

这里有几个配置值得单独说明。model_name被固定为nvidia/Cosmos-Reason2-2B,说明N1.7默认使用Cosmos-Reason2-2B作为视觉语言骨干;use_relative_action=True表示训练时默认使用相对动作;backbone_trainable_params_fp32=True表示VLM中可训练的参数会保留FP32精度,避免低精度训练导致数值不稳定。最后将训练参数写入总配置并调用:

复制代码
run(config)

因此,launch_finetune.py本身并不包含训练循环,它的主要工作是读取命令行参数、构造配置,然后将真正的训练交给gr00t/experiment/experiment.py。

3、Gr00tN1d7主类组织完整的VLA模型

Gr00tN1d7继承自Hugging Face的PreTrainedModel,

复制代码
class Gr00tN1d7(PreTrainedModel):
    config_class = Gr00tN1d7Config
    supports_gradient_checkpointing = True

继承PreTrainedModel以后,模型可以使用Hugging Face提供的权重保存和加载接口,例如,

复制代码
AutoModel.from_pretrained(...)
model.save_pretrained(...)

初始化函数中首先构造Backbone,

复制代码
backbone_cls = get_backbone_cls(config)

self.backbone = backbone_cls(
    model_name=config.model_name,
    tune_llm=config.tune_llm,
    tune_visual=config.tune_visual,
    select_layer=config.select_layer,
    reproject_vision=config.reproject_vision,
    use_flash_attention=config.use_flash_attention,
    load_bf16=config.load_bf16,
    tune_top_llm_layers=config.tune_top_llm_layers,
    trainable_params_fp32=config.backbone_trainable_params_fp32,
    transformers_loading_kwargs=transformers_loading_kwargs,
)

get_backbone_cls(config)会根据配置返回对应的视觉语言模型封装类。N1.7配置中使用的是Cosmos-Reason2-2B,其底层结构属于Qwen3-VL系列,因此后续动作模型拿到的不是原始图片,而是Qwen3-VL编码后的序列特征。动作头的初始化比较直接,

复制代码
self.action_head = Gr00tN1d7ActionHead(config)

此外还会创建数据Collator,

复制代码
from .processing_gr00t_n1d7 import Gr00tN1d7DataCollator

self.collator = Gr00tN1d7DataCollator(
    model_name=config.model_name,
    model_type=config.backbone_model_type,
    transformers_loading_kwargs=transformers_loading_kwargs,
)

Collator负责把一个Batch中的图像和语言整理成VLM能够接受的输入格式,包括input_ids、attention_mask、图像Tensor以及视觉相关的网格信息等。模型的forward函数没有额外的复杂逻辑,

复制代码
def forward(self, inputs: dict) -> BatchFeature:
    backbone_inputs, action_inputs = self.prepare_input(inputs)
    backbone_outputs = self.backbone(backbone_inputs)
    action_outputs = self.action_head(backbone_outputs, action_inputs)

    return action_outputs

这段代码将整个训练过程分成三步,prepare_input -> backbone -> action_head

prepare_input会将同一个输入字典拆成两部分。图像、文本等数据交给Backbone,状态、动作、动作Mask和embodiment_id交给动作头,

复制代码
backbone_inputs = self.backbone.prepare_input(inputs)
action_inputs = self.action_head.prepare_input(inputs)

随后使用统一函数把Tensor移动到模型所在设备:

复制代码
def to_device_with_dtype(x):
    if torch.is_floating_point(x):
        return x.to(self.device, dtype=self.dtype)
    else:
        return x.to(self.device)

浮点Tensor不仅会移动到GPU,还会转换为模型当前使用的数据类型;整数类型的Token ID和Mask只移动设备,不进行浮点转换。

4、动作头由状态编码器、动作编码器和DiT组成

动作头类名为Gr00tN1d7ActionHead,

复制代码
class Gr00tN1d7ActionHead(nn.Module):
    """Action head component for flow matching diffusion policy."""

注释中已经直接写出了flow matching diffusion policy。这个表述容易让人误以为代码同时实现了两套方法,实际上这里只实现一套动作生成流程:使用DiT预测Flow Matching速度场。 动作头首先根据配置创建DiT,

复制代码
if config.use_alternate_vl_dit:
    self.model = AlternateVLDiT(
        **config.diffusion_model_cfg,
        cross_attention_dim=config.backbone_embedding_dim,
        attend_text_every_n_blocks=config.attend_text_every_n_blocks,
    )
else:
    self.model = DiT(
        **config.diffusion_model_cfg,
        cross_attention_dim=config.backbone_embedding_dim,
    )

正常配置下使用DiT,开启use_alternate_vl_dit后使用AlternateVLDiT。两种结构都会将状态和动作特征作为主序列,并通过交叉注意力读取视觉语言特征。下面几个成员负责状态和动作的维度映射,

复制代码
self.state_encoder = CategorySpecificMLP(
    num_categories=config.max_num_embodiments,
    input_dim=config.max_state_dim * config.state_history_length,
    hidden_dim=self.hidden_size,
    output_dim=self.input_embedding_dim,
)

self.action_encoder = MultiEmbodimentActionEncoder(
    action_dim=self.action_dim,
    hidden_size=self.input_embedding_dim,
    num_embodiments=config.max_num_embodiments,
)

self.action_decoder = CategorySpecificMLP(
    num_categories=config.max_num_embodiments,
    input_dim=self.hidden_size,
    hidden_dim=self.hidden_size,
    output_dim=self.action_dim,
)

state_encoder将机器人状态映射到DiT的输入维度;action_encoder将连续动作和时间步编码为动作Token;action_decoder将DiT输出还原为连续动作速度。

这里使用了CategorySpecificMLP和MultiEmbodimentActionEncoder,原因是不同机器人本体的数据分布不同。即使两台机器人都使用7维动作,其中每一维的物理含义也可能不同。通过embodiment_id选择类别相关参数,可以在共享DiT主体的同时,为不同机器人保留各自的输入输出映射。

动作头还会记录以下参数,

复制代码
self.action_dim = config.max_action_dim
self.action_horizon = config.action_horizon
self.num_inference_timesteps = config.num_inference_timesteps

action_dim是补齐后的最大动作维度,action_horizon表示模型一次预测多少步动作,num_inference_timesteps表示推理时执行多少次速度积分。

5、动作头同时控制参数梯度和模块运行模式

动作头通过set_trainable_parameters控制不同模块是否参与训练,

复制代码
def set_trainable_parameters(
    self,
    tune_projector: bool,
    tune_diffusion_model: bool,
    tune_vlln: bool,
):
    for p in self.parameters():
        p.requires_grad = True

    if not tune_projector:
        self.state_encoder.requires_grad_(False)
        self.action_encoder.requires_grad_(False)
        self.action_decoder.requires_grad_(False)

        if self.config.add_pos_embed:
            self.position_embedding.requires_grad_(False)

    if not tune_diffusion_model:
        self.model.requires_grad_(False)

    if not tune_vlln:
        self.vlln.requires_grad_(False)
        self.vl_self_attention.requires_grad_(False)

tune_projector控制状态编码器、动作编码器、动作解码器以及位置编码;tune_diffusion_model控制DiT主体;tune_vlln控制视觉语言特征进入动作头之前的LayerNorm和自注意力层。代码中还实现了set_frozen_modules_to_eval_mode,

复制代码
def set_frozen_modules_to_eval_mode(self):
    if self.training:
        if not self.tune_projector:
            self.state_encoder.eval()
            self.action_encoder.eval()
            self.action_decoder.eval()

        if not self.tune_diffusion_model:
            self.model.eval()

        if not self.tune_vlln:
            self.vlln.eval()
            self.vl_self_attention.eval()

仅设置requires_grad=False并不会自动将模块切换到评估模式。Hugging Face Trainer在训练期间会调用model.train(),被冻结模块中的Dropout等训练态行为仍可能生效,因此源码又在forward开头调用set_frozen_modules_to_eval_mode(),将这些模块重新切换到eval()状态。

6、Flow Matching在噪声和真实动作之间构造训练轨迹

动作头forward函数的输入包括视觉语言特征和动作相关数据,

复制代码
def forward(
    self,
    backbone_output: BatchFeature,
    action_input: BatchFeature,
) -> BatchFeature:

Backbone输出中主要包含,

复制代码
backbone_features:      [B, seq_len, backbone_embedding_dim]
backbone_attention_mask:[B, seq_len]

动作输入中主要包含,

复制代码
state:          [B, state_history, state_dim]
action:         [B, action_horizon, action_dim]
embodiment_id:  [B]
action_mask:    [B, action_horizon, action_dim]

首先对Backbone特征做额外处理,

复制代码
backbone_output = self.process_backbone_output(backbone_output)

vl_embeds = backbone_output.backbone_features

process_backbone_output中包含LayerNorm和可选的视觉语言自注意力层,

复制代码
def process_backbone_output(self, backbone_output):
    backbone_features = backbone_output["backbone_features"]
    backbone_features = self.vlln(backbone_features)
    backbone_features = self.vl_self_attention(backbone_features)
    backbone_output["backbone_features"] = backbone_features

    return backbone_output

接着处理机器人状态。状态历史会先展平,

复制代码
assert action_input.state.shape[1] == self.config.state_history_length

action_input.state = action_input.state.view(
    action_input.state.shape[0],
    1,
    -1,
)

假设状态历史长度为4,每一帧状态维度为32,展平后得到128维向量,再由state_encoder映射为一个状态Token,

复制代码
state_features = self.state_encoder(
    action_input.state,
    embodiment_id,
)

训练阶段还会按概率丢弃完整状态特征,

复制代码
if self.training and self.state_dropout_prob > 0:
    do_dropout = (
        torch.rand(
            state_features.shape[0],
            device=state_features.device,
        )
        < self.state_dropout_prob
    )

    do_dropout = do_dropout[:, None, None].to(
        dtype=state_features.dtype
    )

    state_features = state_features * (1 - do_dropout)

这里不是对状态向量中的单个元素做Dropout,而是对一个样本的整个状态Token置零。这样训练出来的模型不会完全依赖机器人状态,在状态传感器存在噪声或缺失时仍能利用图像和语言生成动作。Flow Matching目标的构造集中在下面几行,

复制代码
actions = action_input.action

noise = torch.randn(
    actions.shape,
    device=actions.device,
    dtype=actions.dtype,
)

t = self.sample_time(
    actions.shape[0],
    device=actions.device,
    dtype=actions.dtype,
)

t = t[:, None, None]

noisy_trajectory = (1 - t) * noise + t * actions
velocity = actions - noise

模型输入noisy_trajectory、时间步以及条件信息,输出该位置上的速度。训练目标不是预测高斯噪声,也不是直接恢复干净动作,而是预测从噪声指向真实动作的速度方向。

时间t并不是均匀采样,而是由Beta分布产生,

复制代码
self.beta_dist = Beta(
    config.noise_beta_alpha,
    config.noise_beta_beta,
)

def sample_time(self, batch_size, device, dtype):
    sample = self.beta_dist.sample([batch_size]).to(
        device,
        dtype=dtype,
    )
    sample = (1 - sample) * self.config.noise_s

    return sample

Beta分布可以控制训练样本更多地落在轨迹的哪个区域。不同于简单的均匀采样,这种方式可以调整模型对高噪声阶段和低噪声阶段的学习比例。

连续时间t随后被离散化,

复制代码
t_discretized = (
    t[:, 0, 0] * self.num_timestep_buckets
).long()

离散时间步会传给动作编码器和DiT,

复制代码
action_features = self.action_encoder(
    noisy_trajectory,
    t_discretized,
    embodiment_id,
)

如果启用了位置编码,还会给动作序列中的每一个时间位置添加独立Embedding,

复制代码
if self.config.add_pos_embed:
    pos_ids = torch.arange(
        action_features.shape[1],
        dtype=torch.long,
        device=device,
    )

    pos_embs = self.position_embedding(pos_ids).unsqueeze(0)
    action_features = action_features + pos_embs

这样DiT可以区分动作块中的第0步、第1步和后续时间步。

7、DiT根据多模态条件预测动作速度并计算损失

状态特征和动作特征会在序列维度拼接,

复制代码
sa_embs = torch.cat(
    (state_features, action_features),
    dim=1,
)

如果状态被编码为1个Token,动作块长度为16,那么拼接后的序列长度就是17,

复制代码
[state_token, action_0, action_1, ..., action_15]

视觉语言特征不会直接拼接进这个序列,而是作为交叉注意力的encoder_hidden_states输入DiT,

复制代码
model_output, _ = self.model(
    hidden_states=sa_embs,
    encoder_hidden_states=vl_embeds,
    encoder_attention_mask=vl_attn_mask,
    timestep=t_discretized,
    return_all_hidden_states=True,
)

这种结构可以看成条件生成模型。主序列是机器人状态和动作,视觉语言特征提供环境和任务条件。每一层DiT都可以根据当前图像和语言指令更新动作Token。

DiT输出经过动作解码器,

复制代码
pred = self.action_decoder(
    model_output,
    embodiment_id,
)

由于输出中还包含状态Token对应的位置,因此只保留最后的动作部分,

复制代码
pred_actions = pred[:, -actions.shape[1]:]

动作损失采用逐元素MSE,

复制代码
action_loss = F.mse_loss(
    pred_actions,
    velocity,
    reduction="none",
)

随后乘以action_mask。动作Mask的作用是忽略补零维度以及无效时间步。GR00T需要支持不同机器人,一些机器人可能只有7维动作,另一些机器人可能有20维动作,模型内部会统一补齐到max_action_dim,但补齐部分不应参与损失计算。

源码随后将逐元素MSE乘以action_mask,再根据有效动作元素的数量计算平均损失,

复制代码
action_loss = action_loss * action_mask
loss = action_loss.sum() / (
    action_mask.sum() + 1e-6
)

分母使用有效元素数量,而不是直接对整个Tensor求平均,可以避免不同动作维度和不同Padding长度对损失尺度造成影响。

8、推理阶段从高斯噪声逐步生成动作序列

训练阶段学习速度场,推理阶段则沿着该速度场进行积分。动作生成入口为,

复制代码
Gr00tN1d7.get_action(...)

主模型先处理输入并计算视觉语言特征,

复制代码
backbone_inputs, action_inputs = self.prepare_input(inputs)
backbone_outputs = self.backbone(backbone_inputs)

action_outputs = self.action_head.get_action(
    backbone_outputs,
    action_inputs,
    options,
)

动作头首先创建一段高斯噪声,

复制代码
actions = torch.randn(
    size=(
        batch_size,
        self.config.action_horizon,
        self.action_dim,
    ),
    dtype=vl_embeds.dtype,
    device=device,
)

然后根据推理步数计算步长,

复制代码
dt = 1.0 / self.num_inference_timesteps

如果num_inference_timesteps=4,每次积分步长就是0.25。

每轮迭代都会对当前动作轨迹重新编码,

复制代码
for t in range(self.num_inference_timesteps):
    t_cont = t / float(self.num_inference_timesteps)
    t_discretized = int(
        t_cont * self.num_timestep_buckets
    )

    timesteps_tensor = torch.full(
        size=(batch_size,),
        fill_value=t_discretized,
        device=device,
    )

    action_features = self.action_encoder(
        actions,
        timesteps_tensor,
        embodiment_id,
    )

随后将状态Token和动作Token拼接,交给DiT,

复制代码
sa_embs = torch.cat(
    (state_features, action_features),
    dim=1,
)

model_output = self.model(
    hidden_states=sa_embs,
    encoder_hidden_states=vl_embeds,
    timestep=timesteps_tensor,
)

动作解码器输出当前轨迹上的速度,

复制代码
pred = self.action_decoder(
    model_output,
    embodiment_id,
)

pred_velocity = pred[:, -self.action_horizon:]

最后使用Euler方法更新动作,

复制代码
actions = actions + dt * pred_velocity * vel_strength
相关推荐
2401_885665192 小时前
从神经元到BP反向传播,零基础吃透神经网络底层原理
人工智能·python·深度学习·神经网络·opencv
山居秋暝LS2 小时前
【无标题】
人工智能·深度学习
Ricky_yyy2 小时前
GLM架构深度解读:清华大模型的核心技术
人工智能·深度学习·glm
月疯3 小时前
torch:view和reshape的区别
pytorch·python·深度学习
好评笔记3 小时前
深度学习面试八股—— GRU(Gated Recurrent Unit)
人工智能·rnn·深度学习·算法·机器学习·gru·校招
AI人工智能+3 小时前
往来港澳通行证识别系统,深度融合计算机视觉与自然语言处理,为“智慧口岸”和“数字政务”提供了强有力的技术支撑
人工智能·深度学习·ocr·往来港澳通行证识别
闻道且行之3 小时前
Hair Segmentation:MediaPipe 头发分割模块 CMake 独立编译
c++·人工智能·深度学习·神经网络·opencv·计算机视觉
高洁013 小时前
知识图谱与推荐系统实战
深度学习·机器学习·transformer·virtualenv·知识图谱
EQUINOX13 小时前
【论文阅读】| ViT精读
论文阅读·人工智能·深度学习·机器学习