四步流程的设计原理
这是一个典型的 RECAP (Reward-Conditioned Advantage Pretraining) 离线 RL 流程,每个步骤之间存在严格的数据依赖关系,必须按顺序执行。
步骤 1:计算回报 (compute_returns.py)
作用 :为数据集中每个时间步计算 reward 和 return
关键逻辑(compute_returns.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_returns.py#L54-L86)):
- 使用 DynamicReturn 方法:每步 reward=-1,成功时最后一步 reward=0,失败时最后一步 reward=failure_reward
- 通过反向迭代计算 return:
G_t = r_t + gamma * G_{t+1} - 输出:
meta/returns_{tag}.parquet,包含return,reward,prompt
为什么需要单独做?
- 原始数据集通常没有 reward 和 return 标签
- 这是后续所有步骤的数据基础,必须在训练前完成
步骤 2:训练价值模型 (train_value.py)
作用 :训练一个 ValueCriticModel,用于估计状态价值 V(o_t)
关键逻辑(train_value.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/train_value.py#L47-L55)):
- 使用
SFTRunner和FSDPValueSftWorker进行分布式训练 - 输入:图像观测 + 任务描述
- 输出:状态价值估计
为什么需要单独做?
- 优势计算需要价值函数
V(o_t)的估计 - 价值模型是中间产物,需要先训练好才能计算优势
步骤 3:计算优势值 (compute_advantages.py)
作用:使用训练好的价值模型,为每个时间步计算优势标签
关键公式(compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L804)):
A = normalize(r_{t:t+N}) + gamma^N * V(o_{t+N}) - V(o_t)
为什么需要单独做?
- 优势值是衡量数据质量的关键指标
- 正优势表示"好"的状态-动作对,负优势表示"坏"的
- 需要先训练价值模型才能计算,所以必须在步骤 2 之后
步骤 4:CFG RL 训练 (train_cfg.py)
作用:使用计算好的优势值作为标签,训练策略模型
关键逻辑(train_cfg.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/policy_optimization/cfg_rl/train_cfg.py#L45-L56)):
- 使用
FSDPCfgWorker进行 CFG (Classifier-Free Guidance) 训练 - 输入:图像观测 + 任务描述 + 优势标签
- 输出:优化后的策略模型
为什么需要单独做?
- 这是最终的策略优化阶段
- 需要步骤 3 计算好的优势标签作为监督信号
依赖关系图
原始数据集
↓
Step 1: compute_returns → meta/returns.parquet
↓
Step 2: train_value_sft → value_checkpoint
↓
Step 3: compute_advantages → meta/advantages.parquet
↓
Step 4: train_cfg → 最终策略模型
为什么不能合并?
-
计算成本差异大:
- Step 1 是纯 CPU 数据处理,速度快
- Step 2 和 Step 4 是 GPU 训练,耗时较长
- Step 3 是 GPU 推理,中等耗时
-
数据依赖:每个步骤的输出是下一步的输入
-
可调试性:分开执行便于检查每一步的输出是否正确
-
资源调度:不同步骤可能需要不同的硬件资源(CPU vs GPU)
-
增量更新:如果只修改了某个步骤的参数,可以只重新运行该步骤
总结
这个四步流程是 RECAP 算法的标准实现模式:
- 标注回报 → 为离线数据添加 reward/return 标签
- 训练价值模型 → 学会评估状态好坏
- 计算优势 → 区分好/坏的数据点
- 优化策略 → 基于优势信号提升策略
让我深入分析训练价值模型的逻辑。首先查看核心代码文件。
训练价值模型的核心逻辑
一、整体架构
价值模型训练是一个监督学习 过程,目标是学习一个状态价值函数 V(o_t),它能根据当前观测(图像 + 任务描述)预测该状态的长期回报。
核心组件关系:
SFTRunner (训练调度)
↓
FSDPValueSftWorker (训练 Worker)
↓
ValueCriticModel (模型本体)
├── ValueExpert (SigLIP + Gemma3)
└── ValueHead (分类价值预测头)
↓
ValueDataset + ValueDataCollator (数据加载)
二、数据准备阶段
1. 数据源
价值模型需要 Step 1 计算好的 return 数据,来自 meta/returns.parquet(由 compute_returns.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_returns.py) 生成)。
2. 数据归一化
在 fsdp_value_sft_worker.py(file:///Users/mac/projects/robot/RLinf/rlinf/workers/sft/fsdp_value_sft_worker.py#L242-L288) 中,计算全局 return 范围:
python
# 从所有数据集的 stats.json 中读取
global_return_min = min(all_mins) # 通常约 -700
global_return_max = max(all_maxs) # 通常为 0
然后将 return 归一化到 [-1, 0] 区间:
python
def normalize(x):
return_range = global_return_max - global_return_min
return (x - global_return_min) / return_range - 1.0
3. 数据加载器构建
fsdp_value_sft_worker.py(file:///Users/mac/projects/robot/RLinf/rlinf/workers/sft/fsdp_value_sft_worker.py#L141-L467) 中的 build_dataloader() 方法:
- 创建
ValueProcessor:处理图像(224x224,归一化到 -1,1)和文本(模板:Task: {prompt}.) - 创建
ValueDataCollator:负责批处理,将样本转换为模型输入格式 - 创建
ValueDataset:从 LeRobot 数据集中读取观测和目标价值
三、模型架构
1. ValueCriticModel
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L129-L174) 定义了核心模型:
python
class ValueCriticModel(nn.Module):
def __init__(self, config):
# 1. ValueExpert: SigLIP + Gemma3 视觉语言模型
self.value_expert = ValueExpert(
expert_configs={"value": expert_config},
siglip_path=siglip_path,
gemma3_path=gemma3_path,
)
# 2. ValueHead: 分类价值预测头
self.value_head = ValueHead(
hidden_size=expert_config.width,
num_bins=config.num_bins, # 通常 201
v_min=config.v_min, # -1.0
v_max=config.v_max, # 0.0
)
2. ValueHead
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L85-L127) 负责将隐藏状态映射到价值:
python
class ValueHead(nn.Module):
def __init__(self, hidden_size, num_bins, v_min, v_max):
self.cls_embedding = nn.Embedding(1, hidden_size) # 可学习的 CLS token
self.value_proj = nn.Linear(hidden_size, num_bins) # 投影到 bins
self.atoms = torch.linspace(v_min, v_max, num_bins) # 离散化的价值点
关键设计 :使用分类分布来表示价值,而不是直接回归。这有两个好处:
- 处理价值的多模态分布
- 提供不确定性估计
3. 前向传播流程
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L311-L358) 中的 forward() 方法:
输入: observation (图像 + tokenized_prompt)
↓
1. embed_prefix(): 嵌入图像和文本,得到 prefix_embs
↓
2. embed_suffix(): 创建 [CLS] token embedding
↓
3. _forward_expert(): 两阶段前向传播
Phase 1: 预填充 frozen VLM,获取 KV cache
Phase 2: 使用 KV cache 运行 value expert,得到 [CLS] 的隐藏状态
↓
4. _compute_value_from_hidden(): 从隐藏状态计算价值
↓
输出: predicted_values, logits, probs, loss
四、价值估计的计算方式
1. 从隐藏状态到价值
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L466-L471):
python
def _compute_value_from_hidden(self, cls_hidden):
logits = self.value_head(cls_hidden) # [B, num_bins]
probs = F.softmax(logits, dim=-1) # [B, num_bins]
values = (probs * self.value_head.atoms).sum(dim=-1) # [B]
return values, logits, probs
公式 :V(o) = Σ(p_i * z_i),其中 p_i 是第 i 个 bin 的概率,z_i 是该 bin 的价值值。
2. 损失函数
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L473-L519) 使用分类交叉熵损失:
python
def _compute_categorical_loss(self, logits, target_values):
# 将连续目标值投影到离散 bins(Dirac delta projection)
b = (target_values - self.v_min) / self.delta_z
l = b.floor().long().clamp(0, self.num_bins - 1) # 左 bin
u = b.ceil().long().clamp(0, self.num_bins - 1) # 右 bin
# 计算到两个 bin 的距离,分配概率
d_to_l, d_to_u = b - l.float(), u.float() - b
target_probs[batch_idx, l] += d_to_u # 距离远的权重高
target_probs[batch_idx, u] += d_to_l
# 交叉熵损失
loss = -(target_probs * F.log_softmax(logits, dim=-1)).sum(dim=-1)
原理 :将连续的目标价值 y 分解为两个相邻 bin 的加权组合,然后计算预测分布与目标分布的交叉熵。
五、训练循环
1. 核心训练步骤
fsdp_value_sft_worker.py(file:///Users/mac/projects/robot/RLinf/rlinf/workers/sft/fsdp_value_sft_worker.py#L469-L562) 中的 run_training():
python
def run_training(self):
self.model.train()
for idx in range(self.gradient_accumulation): # 梯度累积
batch = next(self.data_iter)
obs, target_values, extra = self._prepare_input(batch)
with self.amp_context: # 混合精度
result = self.model(observation=obs, target_values=target_values)
loss = result.loss
scaled_loss = loss / self.gradient_accumulation
self.grad_scaler.scale(scaled_loss).backward() # 反向传播
grad_norm, lr_list = self.optimizer_step() # 参数更新
self.optimizer.zero_grad()
self.lr_scheduler.step()
return train_metrics
2. 评估指标
训练过程中监控的指标(fsdp_value_sft_worker.py(file:///Users/mac/projects/robot/RLinf/rlinf/workers/sft/fsdp_value_sft_worker.py#L496-L538)):
| 指标 | 含义 |
|---|---|
loss |
分类交叉熵损失 |
predicted_value_mean/std |
预测价值的统计量 |
target_value_mean/std |
目标价值的统计量 |
cat_acc_best |
最佳 bin 预测准确率 |
cat_acc_neighbor |
相邻 bin 预测准确率 |
mae |
平均绝对误差 |
value_spearman |
Spearman 秩相关系数(衡量排序能力) |
六、关键设计总结
| 设计点 | 实现方式 | 优势 |
|---|---|---|
| 价值表示 | 分类分布(201 bins) | 支持多模态价值分布,提供不确定性 |
| 损失函数 | Dirac delta 投影 + 交叉熵 | 平滑处理连续价值的离散化 |
| 视觉编码 | SigLIP 预训练模型 | 强大的视觉特征提取 |
| 语言编码 | Gemma3 预训练模型 | 理解任务描述 |
| 两阶段前向 | KV cache 预填充 | 加速推理,支持长序列 |
| 梯度累积 | 多 micro-batch | 支持大全局 batch size |
| 分布式训练 | FSDP | 多 GPU 高效训练 |
七、训练输出
训练完成后,保存的检查点包含:
- 模型权重(
actor/目录) - 配置文件
- 归一化统计量(
norm_stats.pt)
这些检查点随后被 Step 3 的 compute_advantages.py 加载,用于计算优势值。
为什么说是监督学习?
监督学习的核心特征是:有明确的输入-输出配对(X, Y),模型学习从输入 X 预测输出 Y。
在价值模型训练中:
| 监督学习要素 | 价值模型实现 |
|---|---|
| 输入 X | 观测(图像 + 任务描述) |
| 标签 Y | 该状态的真实 return(由 Step 1 预先计算) |
| 损失函数 | 预测值与真实 return 的分类交叉熵 |
| 训练目标 | 最小化预测误差,学会从观测估计价值 |
对比强化学习 :RL 中没有预先标注的标签,模型通过与环境交互获得奖励,通过策略梯度或价值迭代来学习。而这里的价值模型是离线的、有标签的,完全符合监督学习范式。
模型架构:输入处理与每一步的目的
整体数据流
原始输入 (图像 + 任务文本)
↓ Step 1: 预处理与编码
图像 → SigLIP → 视觉特征 (1152维)
文本 → Gemma3 → 文本特征 (640维)
↓ Step 2: 特征对齐
视觉特征 → 投影层 → 640维 (与文本特征对齐)
↓ Step 3: 前缀嵌入
图像嵌入 + 文本嵌入 → prefix_embs (双向注意力)
↓ Step 4: 两阶段前向传播
Phase A: Gemma3 处理 prefix → KV cache
Phase B: Expert 处理 [CLS] → 利用 KV cache 交叉注意力
↓ Step 5: 价值预测
[CLS] 隐藏状态 → ValueHead → 分类分布 → 期望值
Step 1:预处理与编码
图像编码
value_expert.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/value_expert.py#L142-L154):
python
def embed_image(self, image):
# [B, 3, H, W] → [B, 256, 1152]
feats = self.vision_tower(image).last_hidden_state
return self.multi_modal_proj(feats) # → [B, 256, 640]
目的:
- SigLIP:将原始图像转换为语义丰富的视觉特征(1152维)
- 投影层:将视觉特征从 1152 维降到 640 维,与 Gemma3 的隐藏维度对齐
文本编码
value_expert.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/value_expert.py#L156-L168):
python
def embed_language_tokens(self, tokens):
# [B, L] → [B, L, 640]
return self.gemma3.model.embed_tokens(tokens)
目的:将任务描述文本转换为与视觉特征同维度的文本嵌入
Step 2:特征对齐
value_expert.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/value_expert.py#L100-L106):
python
# 全新初始化的投影层
self.multi_modal_proj = nn.Linear(1152, 640, bias=True)
nn.init.normal_(self.multi_modal_proj.weight, std=0.02)
nn.init.zeros_(self.multi_modal_proj.bias)
目的:
- SigLIP 输出 1152 维,Gemma3 使用 640 维
- 需要一个可训练的投影层,学习如何将视觉特征映射到语言模型的语义空间
- 这是跨模态对齐的关键:让图像和文本在同一语义空间中表示
Step 3:前缀嵌入
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L281-L299):
python
def embed_prefix(self, images, img_masks, lang_tokens, lang_masks):
# 嵌入每个摄像头的图像
for img, img_mask in zip(images, img_masks):
img_emb = self.value_expert.embed_image(img) # [B, 256, 640]
embs.append(img_emb)
# 嵌入语言 tokens
lang_emb = self.value_expert.embed_language_tokens(lang_tokens) # [B, L, 640]
embs.append(lang_emb)
# 拼接成前缀
return torch.cat(embs, dim=1), torch.cat(pad_masks, dim=1)
目的:
- 将多视角图像和文本拼接成一个统一的序列
- 这个序列作为"前缀",后续的 CLS token 需要关注它来预测价值
Step 4:两阶段前向传播
这是模型最核心的设计,分为 Phase A 和 Phase B。
Phase A:Gemma3 处理前缀
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L405-L464):
python
# Mode A: prefix-only --- Gemma3 处理前缀并缓存 KV
(prefix_out, _), past_kv = self.value_expert.forward(
attention_mask=prefix_attn_4d,
inputs_embeds=[prefix_embs, None], # suffix_embs=None → Mode A
use_cache=True,
)
目的:
- 预填充 KV cache:Gemma3 处理前缀序列(图像+文本),将中间结果(键值对)缓存起来
- 双向注意力:前缀内部是完全双向的,图像和文本可以相互关注
- 输出隐藏状态:得到前缀的最后隐藏状态
Phase B:Expert 处理 CLS token
python
# Mode B: suffix-only --- Expert 利用 KV cache 进行交叉注意力
(_, suffix_out), _ = self.value_expert.forward(
attention_mask=full_attn_4d,
inputs_embeds=[None, suffix_embs], # prefix_embs=None → Mode B
past_key_values=past_kv, # 使用 Phase A 的 KV cache
expert_name="value",
)
目的:
- 交叉注意力:CLS token 通过 KV cache 关注整个前缀序列(图像+文本)
- 提取全局信息:CLS token 的隐藏状态聚合了所有输入信息
- 专家网络:使用独立的 GemmaForCausalLM 专家,专门学习价值预测任务
为什么要用两阶段?
| 原因 | 说明 |
|---|---|
| 内存效率 | KV cache 避免重复计算前缀的注意力 |
| 架构约束 | Gemma3 的滑动窗口注意力不支持层间交错 |
| 模块化 | VLM(Phase A)和专家(Phase B)可以独立训练/冻结 |
| 推理加速 | 在线推理时,前缀只需计算一次,后续 token 可复用 KV cache |
Step 5:价值预测
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L466-L471):
python
def _compute_value_from_hidden(self, cls_hidden):
logits = self.value_head(cls_hidden) # [B, 201]
probs = F.softmax(logits, dim=-1) # [B, 201]
values = (probs * self.value_head.atoms).sum(dim=-1) # [B]
return values, logits, probs
目的:
- 分类分布:将 CLS 隐藏状态投影到 201 个 bin 的 logits
- 概率归一化:softmax 得到每个 bin 的概率
- 期望计算 :
V(o) = Σ(p_i * z_i),将分布转换为标量价值
为什么用分类而不是回归?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 分类(当前方案) | 支持多模态价值分布,提供不确定性估计,训练更稳定 | 需要更多参数,计算稍复杂 |
| 直接回归 | 简单,参数少 | 难以处理价值的多峰分布,训练不稳定 |
损失计算:监督学习的核心
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L473-L519):
python
def _compute_categorical_loss(self, logits, target_values):
# 将连续目标值投影到离散 bins
b = (target_values - self.v_min) / self.delta_z
l = b.floor().long().clamp(0, self.num_bins - 1)
u = b.ceil().long().clamp(0, self.num_bins - 1)
# Dirac delta 投影:分配概率到相邻 bins
d_to_l, d_to_u = b - l.float(), u.float() - b
target_probs[batch_idx, l] += d_to_u
target_probs[batch_idx, u] += d_to_l
# 交叉熵损失
loss = -(target_probs * F.log_softmax(logits, dim=-1)).sum(dim=-1)
目的:
- Dirac delta 投影:将连续的真实 return(标签)转换为离散的概率分布
- 交叉熵:衡量预测分布与目标分布的差异
- 监督信号:通过最小化损失,让模型学会从观测预测正确的价值
监督学习视角下的完整流程
训练数据 (X, Y)
├─ X: observation = {images, prompt}
├─ Y: target_values = 归一化后的 return (来自 Step 1)
│
↓ 前向传播
├─ 编码: images → SigLIP → proj → 640维
│ prompt → Gemma3 → 640维
│
├─ 拼接: image_emb + text_emb → prefix_embs
│
├─ Phase A: Gemma3 处理 prefix → KV cache
│
├─ Phase B: [CLS] → Expert → 利用 KV cache → cls_hidden
│
└─ 预测: cls_hidden → ValueHead → predicted_values
│
↓ 损失计算
└─ loss = cross_entropy(predicted_distribution, target_distribution)
│
↓ 反向传播
└─ 更新模型参数,减小预测误差
总结:每一步的设计目的
| 步骤 | 组件 | 目的 |
|---|---|---|
| 1 | SigLIP | 提取图像的高层语义特征 |
| 2 | 投影层 | 将视觉特征对齐到语言模型空间 |
| 3 | Gemma3 嵌入 | 将文本转换为语义嵌入 |
| 4 | 前缀拼接 | 组合多模态信息为统一序列 |
| 5 | Phase A | 预填充 KV cache,双向理解输入 |
| 6 | Phase B | CLS 关注前缀,聚合全局信息 |
| 7 | ValueHead | 将隐藏状态转换为价值分布 |
| 8 | 期望计算 | 从分布得到标量价值 |
| 9 | 交叉熵损失 | 监督学习的核心:最小化预测误差 |
这个架构的本质是:利用预训练的视觉语言模型提取特征,通过可训练的专家网络和价值头,学习从观测到价值的映射函数 ------这是一个典型的多模态监督学习任务。
价值模型本质:数据到分布的映射函数
一、核心定义
价值模型 ValueCriticModel 本质上是一个概率映射函数:
V: Observation → Distribution(return)
| 组成部分 | 数学表示 | 代码实现 |
|---|---|---|
| 输入空间 | o ∈ Observation |
图像 + 任务描述 |
| 输出空间 | `p(· | o) ∈ Δ^K` |
| 映射函数 | V(o) = Σ(p_i(o) * z_i) |
_compute_value_from_hidden(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L466-L471) |
二、为什么是"到分布"的映射?
1. 代码中的分布输出
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L466-L471):
python
def _compute_value_from_hidden(self, cls_hidden):
logits = self.value_head(cls_hidden) # [B, 201] --- 未归一化的分布参数
probs = F.softmax(logits, dim=-1) # [B, 201] --- 归一化的概率分布
values = (probs * self.value_head.atoms).sum(dim=-1) # [B] --- 分布的期望值
return values, logits, probs
关键设计 :模型输出的是一个离散概率分布,而不是单一数值:
输入: 观测 o
↓
输出: p(z|o) = [p(z₁|o), p(z₂|o), ..., p(z₂₀₁|o)]
其中 z_i = -1.0 + i * Δz, Δz = (0 - (-1)) / 200 = 0.005
最终价值: V(o) = E_z[p(z|o)] = Σ(p_i * z_i)
2. 为什么不用直接回归?
| 方案 | 表达能力 | 不确定性 | 训练稳定性 |
|---|---|---|---|
| 分类分布(当前) | 强:支持多峰分布 | 有:方差 = Σ(p_i * (z_i - μ)^2) | 高:交叉熵比 MSE 更稳定 |
| 直接回归 | 弱:只能表达单峰分布 | 无:无法估计不确定性 | 低:极端值会主导损失 |
在机器人强化学习中,同一个观测可能对应多种不同的未来回报(比如不同策略会导致不同结果),分布表示能更好地捕捉这种不确定性。
三、监督学习如何让分布"正确表达"数据?
1. 标签的分布化
modeling_critic.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/value_model/recap/modeling_critic.py#L473-L519) 中的损失计算:
python
def _compute_categorical_loss(self, logits, target_values):
# 将连续标签 y 转换为离散分布
b = (y - v_min) / Δz
l = floor(b), u = ceil(b) # 找到相邻的两个 bins
# Dirac delta 投影:概率分配到相邻 bins
target_probs[l] += distance_to_u
target_probs[u] += distance_to_l
# 交叉熵:最小化预测分布与目标分布的差异
loss = -Σ(target_probs * log(predicted_probs))
核心思想 :将每个训练样本的标签 y(真实 return)转换为一个尖峰分布(Dirac delta),然后让模型学习拟合这个分布。
2. 训练过程的本质
训练目标:min_D KL(target_distribution || predicted_distribution)
其中:
- target_distribution: 以真实 return 为中心的尖峰分布
- predicted_distribution: 模型输出的 201-bin 分布
- KL 散度通过交叉熵损失实现
效果:随着训练进行,模型学会将概率质量集中在真实 return 附近,分布逐渐变窄、峰值逐渐对齐。
四、从分布到标量价值
1. 期望值计算
python
# 分布的期望 = 加权平均
V(o) = Σ(p_i * z_i)
# 分布的方差(可选,衡量不确定性)
Var(o) = Σ(p_i * (z_i - V(o))^2)
2. 为什么需要分布?
在后续的优势计算中(Step 3),compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L804) 使用的是标量价值:
python
advantage = reward_sum + gamma_k * v_next - v_curr
但分布表示在训练阶段有重要作用:
| 阶段 | 使用形式 | 原因 |
|---|---|---|
| 训练时 | 完整分布 | 提供更丰富的监督信号,训练更稳定 |
| 推理时 | 期望值 | 简化计算,符合优势公式的需求 |
五、"正确表达"的含义
1. 统计意义上的正确性
训练目标是让模型的预测分布尽可能接近真实的价值分布:
当训练收敛时:
p_model(z|o) ≈ p_true(z|o)
其中 p_true(z|o) 是观测 o 下真实 return 的分布
2. 实际验证指标
fsdp_value_sft_worker.py(file:///Users/mac/projects/robot/RLinf/rlinf/workers/sft/fsdp_value_sft_worker.py#L81-L98) 中的评估指标:
python
def _compute_spearman(values_a, values_b):
# Spearman 秩相关系数:衡量排序能力
# 完美排序时 = 1.0,完全随机时 ≈ 0
rank_a = argsort(values_a)
rank_b = argsort(values_b)
return correlation(rank_a, rank_b)
| 指标 | 衡量内容 | 理想值 |
|---|---|---|
cat_acc_best |
预测 bin 是否命中最佳 bin | 越高越好 |
cat_acc_neighbor |
预测 bin 是否命中相邻 bin | 越高越好 |
mae |
平均绝对误差 | 越低越好 |
value_spearman |
价值排序能力 | 接近 1.0 |
价值模型"正确表达"的核心标准 :能够准确预测不同观测之间的价值相对大小(排序),而不仅仅是绝对数值。
六、总结:映射函数的完整表达
价值模型 V 是一个"数据 → 分布"的映射函数:
输入: 观测 o = {图像, 任务描述}
↓
特征提取: o → h (SigLIP + Gemma3 + Expert)
↓
分布参数化: h → logits (ValueHead)
↓
概率归一化: logits → p(z|o) (softmax)
↓
期望值: p(z|o) → V(o) = Σ(p_i * z_i)
训练目标: 通过交叉熵损失,让 p_model(z|o) 逼近 p_true(z|o)
最终价值: V(o) 是分布的期望值,表示在观测 o 下的期望长期回报
关键理解:
- 映射函数:是的,V 是从观测到价值的映射函数
- 到分布:模型内部输出的是完整的概率分布,这是"正确表达"的关键
- 期望值:最终用于优势计算的是分布的期望值,兼顾了表达能力和计算效率
第三步:计算优势值(Compute Advantages)
一、核心目标
第三步的目标是为数据集中的每个时间步 计算一个优势值(Advantage) ,用于衡量该时间步的"好坏"程度。这个优势值将作为第四步 CFG RL 训练的监督信号。
二、优势值的定义
核心公式(compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L804)):
A_t = normalize(R_{t:t+N}) + γ^N * V(o_{t+N}) - V(o_t)
| 组成部分 | 含义 | 来源 |
|---|---|---|
R_{t:t+N} |
未来 N 步的折扣奖励和 | Step 1 预计算的 reward |
V(o_t) |
当前状态的价值估计 | 价值模型推理 |
V(o_{t+N}) |
N 步后状态的价值估计 | 价值模型推理 |
normalize(·) |
将奖励归一化到 -1, 0 | 全局 return 范围 |
直观理解:
- 正优势:当前状态比预期更好(好的决策)
- 负优势:当前状态比预期更差(坏的决策)
三、完整流程
Step 1: 加载价值模型和数据
↓
Step 2: Phase 1 --- 批量推理 V(o_t)
↓
Step 3: Phase 2 --- 计算优势值
↓
Step 4: 计算统一阈值
↓
Step 5: 保存优势值到数据集
四、Step 1:加载价值模型和数据
compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L937-L939):
python
value_model = ValueCriticModel.from_checkpoint(
checkpoint_dir=cfg.advantage.value_checkpoint,
device=device,
**model_kwargs
)
关键点:
- 加载 Step 2 训练好的价值模型检查点
- 模型处于评估模式(
model.eval()),使用推理模式 - 加载输入变换(
_input_transform)和处理器(processor)
五、Step 2:Phase 1 --- 批量推理 V(o_t)
这是计算优势值的核心计算阶段,需要对数据集中的每个样本进行价值模型推理。
5.1 数据加载器构建
compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L618-L638):
python
advantage_dataset = ValueInferenceDataset(
dataset,
robot_type,
tasks,
input_transform=value_model._input_transform,
prepare_observation_cpu=worker_cpu_prep,
returns_sidecar=returns_sidecar,
)
设计优化:
- CPU 预处理:图像预处理和 tokenization 在 DataLoader worker 中完成,减少 GPU 等待时间
- 扩展范围 :
extended_end = shard_end + action_horizon,确保能获取o_{t+N}的价值
5.2 批量推理
compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L652-L678):
python
def process_value_batch(obs_list, meta_list):
# 批量推理
batch_results = value_model.infer_batch(
obs_list,
batch_size=batch_size,
pretransformed=cpu_prep_in_workers,
already_cpu_prepared=cpu_prep_in_workers,
)
# 存储结果
for result, meta_info in zip(batch_results, meta_list):
local_idx = int(meta_info["global_idx"]) - shard_start
v_values[local_idx] = float(result["value"]) # V(o_t)
meta_ep_idx[local_idx] = int(meta_info["episode_index"])
meta_frame_idx[local_idx] = int(meta_info["frame_index"])
meta_return[local_idx] = float(meta_info["true_return"])
meta_reward[local_idx] = float(meta_info["reward"])
输出 :得到一个数组 v_values,存储了每个样本的价值估计 V(o_t)
5.3 预取优化
compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L702-L737):
python
with ThreadPoolExecutor(max_workers=1) as prefetch_pool:
pending_future = prefetch_pool.submit(_fetch_next)
for batch_idx in range(batch_count):
batch = pending_future.result() # 获取预取的 batch
if batch_idx + 1 < batch_count:
pending_future = prefetch_pool.submit(_fetch_next) # 预取下一个 batch
process_value_batch(*batch) # GPU 推理
目的 :在 GPU 处理当前 batch 的同时,CPU 预取下一个 batch,实现数据加载和 GPU 计算的流水线并行。
六、Step 3:Phase 2 --- 计算优势值
这是优势计算的核心算法阶段,使用预计算的价值估计和奖励数据计算每个时间步的优势值。
6.1 遍历每个样本
compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L759-L823):
python
for i in range(shard_size):
gidx = shard_start + i
ep_idx = int(meta_ep_idx[i])
frame_idx = int(meta_frame_idx[i])
true_return = float(meta_return[i])
6.2 处理 episode 边界
python
ep_end = int(ep_ends.get(ep_idx, gidx + 1))
next_gidx = gidx + action_horizon
is_next_pad = next_gidx >= ep_end # 是否超出 episode 边界
num_valid = min(action_horizon, ep_end - gidx) # 有效奖励步数
处理逻辑:
- 如果
t + N超出 episode 边界(is_next_pad=True),则V(o_{t+N}) = 0(终止状态价值为 0) num_valid表示实际可用的未来奖励步数
6.3 获取价值估计
python
v_curr = float(v_values[i]) # V(o_t)
if is_next_pad:
v_next = 0.0 # 终止状态价值为 0
else:
next_local_idx = next_gidx - shard_start
v_next = float(v_values[next_local_idx]) # V(o_{t+N})
6.4 计算奖励和
python
if abs(gamma - 1.0) < 1e-8:
# gamma=1.0 时,直接用 return 差
if is_next_pad:
reward_sum_raw = true_return
else:
reward_sum_raw = true_return - float(meta_return[next_local_idx])
else:
# gamma≠1.0 时,计算折扣奖励和
reward_slice = meta_reward[i : i + num_valid]
reward_sum_raw = float(np.sum(gamma_powers[:num_valid] * reward_slice))
公式 :R_{t:t+N} = Σ(γ^i * r_{t+i})
6.5 归一化奖励
python
def normalize(x):
ret_range = ret_max - ret_min # 如 [-700, 0]
return (x - ret_min) / ret_range - 1.0 # 映射到 [-1, 0]
reward_sum = normalize(reward_sum_raw)
目的:将原始奖励和归一化到与价值模型输出相同的范围(-1, 0),确保优势值计算的数值稳定性。
6.6 计算最终优势值
python
gamma_k = gamma**num_valid if discount_next_value else 1.0
advantage = reward_sum + gamma_k * v_next - v_curr
公式回顾:
A_t = normalize(R_{t:t+N}) + γ^N * V(o_{t+N}) - V(o_t)
保存结果:
python
results["advantage"].append(advantage)
results["return"].append(true_return)
results["value_current"].append(v_curr)
results["value_next"].append(v_next)
results["reward_sum"].append(reward_sum)
results["reward_sum_raw"].append(reward_sum_raw)
results["num_valid_rewards"].append(num_valid)
七、Step 4:计算统一阈值
compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L1075-L1078):
python
positive_quantile = cfg.advantage.get("positive_quantile", 0.3)
combined_advantages = np.concatenate(all_advantages)
unified_threshold = quantile_threshold(combined_advantages, positive_quantile)
目的 :将连续的优势值转换为布尔标签(好/坏),用于 CFG RL 训练。
方法:
- 将所有数据集的优势值合并
- 取第
(1 - positive_quantile)百分位数作为阈值 - 默认
positive_quantile=0.3,即选择 top 30% 的样本作为"好"样本
示例:
python
# 假设有 10000 个优势值
# positive_quantile=0.3 → 取第 7000 个分位数作为阈值
# 大于等于阈值的样本标记为 positive=True
八、Step 5:保存优势值
compute_advantages.py(file:///Users/mac/projects/robot/RLinf/examples/offline_rl/advantage_labeling/recap/process/compute_advantages.py#L864-L915):
python
def save_advantages_to_dataset(dataset_path, advantages_df, threshold, ...):
save_df = advantages_df.copy()
save_df.rename(columns={"advantage": "advantage_continuous"}, inplace=True)
# 转换为布尔标签
save_df["advantage"] = save_df["advantage_continuous"] >= threshold
# 保存到 meta/advantages.parquet
adv_filename = f"advantages_{tag}.parquet" if tag else "advantages.parquet"
save_df.to_parquet(meta_dir / adv_filename, index=False)
输出文件 :meta/advantages.parquet,包含:
episode_index:所属 episodeframe_index:帧索引advantage_continuous:连续优势值advantage:布尔标签(好/坏)value_current:当前价值估计value_next:未来价值估计reward_sum:归一化奖励和
九、关键设计总结
| 设计点 | 实现方式 | 优势 |
|---|---|---|
| 两阶段计算 | Phase 1 批量推理,Phase 2 逐个计算 | 避免重复推理,提升效率 |
| 扩展范围 | extended_end = shard_end + N |
确保能获取 V(o_{t+N}) |
| 预取优化 | ThreadPoolExecutor 预取数据 | GPU/CPU 流水线并行 |
| 内存管理 | 定期刷盘到临时 parquet | 避免 OOM |
| 统一阈值 | 跨数据集百分位数 | 保证不同数据集的公平性 |
| 分布式支持 | torchrun 多 GPU | 加速大规模数据集处理 |
十、为什么优势值能衡量"好坏"?
优势值的直觉理解:
A_t = 实际回报 - 预期回报
实际回报 = normalize(R_{t:t+N}) + γ^N * V(o_{t+N})
预期回报 = V(o_t)
如果 A_t > 0: 实际回报 > 预期回报 → 好决策
如果 A_t < 0: 实际回报 < 预期回报 → 坏决策
在第四步 CFG RL 训练中,模型会学习:
- 当
advantage=True(好样本)时,模仿数据中的动作 - 当
advantage=False(坏样本)时,避免数据中的动作
这样就能从混合的成功/失败数据中,提取高质量的训练信号。
第四步:CFG RL 训练
一、核心目标
第四步的目标是使用第三步计算好的优势标签 ,通过 CFG(Classifier-Free Guidance) 技术训练策略模型,使其学会模仿"好"动作,避免"坏"动作。
二、什么是 CFG?
CFG 是一种源自扩散模型的技术,核心思想是:
推理时:v = (1 - w) * v_uncond + w * v_cond
训练时:分别训练无条件分支(v_uncond)和条件分支(v_cond)
| 分支 | 输入 | 训练目标 |
|---|---|---|
| 无条件分支 | 原始任务描述 | 学习基础动作分布 |
| 条件分支 | 带优势标签的任务描述 | 学习受优势信号引导的动作分布 |
在推理时,通过调整 w(guidance_scale),可以控制模型更倾向于生成"好"动作还是"坏"动作。
三、完整流程
Step 1: 加载数据集(含优势标签)
↓
Step 2: 预处理(生成正负引导 prompt)
↓
Step 3: 样本路由(决定走条件分支还是无条件分支)
↓
Step 4: 前向传播(计算 flow loss)
↓
Step 5: 反向传播(更新模型参数)
四、Step 1:加载数据集
4.1 AdvantagePreservingDataset
cfg_model.py(file:///Users/mac/projects/robot/RLinf/rlinf/data/datasets/recap/cfg_model.py#L34-L130):
python
class AdvantagePreservingDataset:
def __init__(self, base_dataset, transformed_dataset, advantages_lookup):
self._transformed_dataset = transformed_dataset
self._advantage_by_index = self._build_advantage_index(
base_dataset, advantages_lookup
)
def __getitem__(self, idx):
sample = self._transformed_dataset[idx]
sample["advantage"] = self._advantage_by_index[idx] # 布尔值:True=好样本
return sample
关键设计:
- 从
meta/advantages.parquet加载优势标签(第三步的输出) - 建立
(episode_index, frame_index) → bool的映射 - 确保每个样本都带有
advantage字段
4.2 数据加载器
fsdp_cfg_worker.py(file:///Users/mac/projects/robot/RLinf/rlinf/workers/sft/fsdp_cfg_worker.py#L251):
python
data_loader = CFGDataLoaderImpl(data_config, torch_data_loader)
CFGDataLoaderImpl 每次迭代返回 (observation, actions, advantage) 三元组。
五、Step 2:预处理(生成正负引导 Prompt)
5.1 TokenizePromptWithGuidance
cfg_model.py(file:///Users/mac/projects/robot/RLinf/rlinf/data/datasets/recap/cfg_model.py#L149-L188):
python
class TokenizePromptWithGuidance:
def __call__(self, data):
prompt = data.pop("prompt")
# 生成原始 prompt 的 token
tokens, token_masks = self.tokenizer.tokenize(prompt, state)
# 生成正负引导 prompt 的 token
positive_prompt = f"{prompt}\nAdvantage: positive"
negative_prompt = f"{prompt}\nAdvantage: negative"
positive_tokens, positive_masks = self.tokenizer.tokenize(positive_prompt, state)
negative_tokens, negative_masks = self.tokenizer.tokenize(negative_prompt, state)
return {
"tokenized_prompt": tokens,
"tokenized_positive_guidance_prompt": positive_tokens,
"tokenized_negative_guidance_prompt": negative_tokens,
...
}
示例:
原始 prompt: "Pick up the block and place it on the table"
正引导 prompt: "Pick up the block and place it on the table\nAdvantage: positive"
负引导 prompt: "Pick up the block and place it on the table\nAdvantage: negative"
六、Step 3:样本路由
6.1 compute_cfg_routing_masks
openpi_cfg_action_model.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/openpi_cfg/openpi_cfg_action_model.py#L106-L160):
python
def compute_cfg_routing_masks(advantage, positive_only_conditional, unconditional_prob):
positive_mask = advantage
negative_mask = ~positive_mask
# 决定哪些样本走条件分支,哪些走无条件分支
if positive_only_conditional:
# 只有正样本才可能走条件分支
positive_conditional_mask = positive_mask & (random_values > unconditional_prob)
negative_conditional_mask = torch.zeros_like(positive_mask)
else:
# 正负样本都可能走条件分支
guidance_mask = random_values > unconditional_prob
positive_conditional_mask = positive_mask & guidance_mask
negative_conditional_mask = negative_mask & guidance_mask
return {
"positive_conditional_mask": positive_conditional_mask,
"positive_unconditional_mask": positive_mask & ~positive_conditional_mask,
"negative_conditional_mask": negative_conditional_mask,
"negative_unconditional_mask": negative_mask & ~negative_conditional_mask,
}
路由逻辑:
unconditional_prob = 0.1(默认)
对于正样本:
- 90% 概率走条件分支(使用正引导 prompt)
- 10% 概率走无条件分支(使用原始 prompt)
对于负样本(positive_only_conditional=True 时):
- 100% 走无条件分支(使用原始 prompt)
6.2 为什么需要无条件分支?
| 原因 | 说明 |
|---|---|
| 稳定性 | 无条件分支防止模型过度依赖引导信号 |
| 多样性 | 保持基础动作分布的多样性 |
| CFG 推理 | 推理时需要无条件输出作为基线 |
七、Step 4:前向传播
7.1 Prompt 选择
openpi_cfg_action_model.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/openpi_cfg/openpi_cfg_action_model.py#L512-L543):
python
if self.config.positive_only_conditional:
# 正样本条件分支用正引导 prompt,其他用原始 prompt
final_lang_tokens = torch.where(
positive_conditional_mask.unsqueeze(-1),
positive_guidance_lang_tokens, # 正引导
lang_tokens, # 原始
)
else:
# 条件分支:正样本用正引导,负样本用负引导
guidance_lang_tokens = torch.where(
positive_mask.unsqueeze(-1),
positive_guidance_lang_tokens,
negative_guidance_lang_tokens,
)
final_lang_tokens = torch.where(
conditional_mask.unsqueeze(-1),
guidance_lang_tokens, # 引导 prompt
lang_tokens, # 原始 prompt
)
7.2 Flow Loss 计算
openpi_cfg_action_model.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/openpi_cfg/openpi_cfg_action_model.py#L363-L444):
python
def _compute_flow_losses(self, images, img_masks, state, actions, lang_tokens, ...):
# 1. 采样时间步和噪声
time = self.sample_time(actions.shape[0], device)
noise = self.sample_noise(actions.shape, device)
# 2. 构造扩散过程中的中间状态
x_t = time * noise + (1 - time) * actions # 噪声+动作的插值
u_t = noise - actions # 目标速度
# 3. 嵌入前缀(图像+文本)
prefix_embs, prefix_pad_masks, prefix_att_masks = self.embed_prefix(
images, img_masks, lang_tokens, lang_masks
)
# 4. 嵌入后缀(状态+动作+时间)
suffix_embs, suffix_pad_masks, suffix_att_masks, adarms_cond = (
self.embed_suffix(state, x_t, time)
)
# 5. 前向传播
suffix_out = self.paligemma_with_expert.forward(
inputs_embeds=[prefix_embs, suffix_embs],
...
)[0][1]
# 6. 预测速度
v_t = self.action_out_proj(suffix_out)
# 7. 计算 MSE 损失
per_element_loss = F.mse_loss(u_t, v_t, reduction="none")
flow_loss = per_element_loss.mean()
return flow_loss, per_element_loss.detach().mean(dim=(-1, -2))
核心公式:
扩散过程:x_t = t * noise + (1 - t) * actions
目标速度:u_t = noise - actions
预测速度:v_t = model(x_t, t, observation)
损失:L = MSE(u_t, v_t)
这是一个标准的**流匹配(Flow Matching)**损失,用于训练扩散模型。
八、Step 5:反向传播
fsdp_cfg_worker.py(file:///Users/mac/projects/robot/RLinf/rlinf/workers/sft/fsdp_cfg_worker.py#L339-L457):
python
def run_training(self):
self.model.train()
for idx in range(self.gradient_accumulation):
# 获取 batch:(observation, actions, advantage)
observation, actions, advantage = next(self.data_iter)
# 前向传播
loss, metrics_data = self.model(
data={
"observation": observation,
"actions": actions,
"advantage": advantage,
}
)
# 反向传播(梯度累积)
loss = loss / self.gradient_accumulation
self.grad_scaler.scale(loss).backward()
# 参数更新
self.optimizer_step()
self.optimizer.zero_grad()
self.lr_scheduler.step()
return train_metrics
九、推理时的 CFG 引导
9.1 sample_actions
openpi_cfg_action_model.py(file:///Users/mac/projects/robot/RLinf/rlinf/models/embodiment/openpi_cfg/openpi_cfg_action_model.py#L692-L816):
python
def sample_actions(self, observation, noise=None):
guidance_type = self.config.guidance_type # "positive"(默认)
# 1. 计算无条件输出
v_t_uncond = self.denoise_step(
state, prefix_pad_masks_uncond, past_key_values_uncond, x_t, time
)
# 2. 计算条件输出(使用正引导 prompt)
if guidance_type == "positive":
v_t_cond = self.denoise_step(
state, prefix_pad_masks_cond, past_key_values_cond, x_t, time
)
# 3. CFG 融合
v_t = (1 - guidance_scale) * v_t_uncond + guidance_scale * v_t_cond
# 4. 扩散采样
x_t = x_t + dt * v_t
return {"actions": x_t}
CFG 公式:
v_t = (1 - w) * v_uncond + w * v_cond
其中 w = cfgrl_guidance_scale(默认 1.0)
直观理解:
w=0:只使用无条件输出,不考虑优势信号w=1:完全使用条件输出,最大化优势引导w>1:放大条件信号(类似扩散模型的分类器引导)
十、关键设计总结
| 设计点 | 实现方式 | 优势 |
|---|---|---|
| 正负引导 Prompt | {prompt}\nAdvantage: positive/negative |
让模型理解优势信号 |
| 样本路由 | positive_only_conditional=True |
只对正样本进行条件引导,避免学习坏动作 |
| Flow Matching 损失 | MSE(u_t, v_t) | 稳定训练扩散模型 |
| 梯度累积 | 多 micro-batch | 支持大全局 batch size |
| FSDP 分布式 | 自动混合精度 + 梯度检查点 | 高效训练大模型 |
十一、四步流程的完整闭环
Step 1: compute_returns
原始数据 → reward/return 标签
Step 2: train_value_sft
数据 + return → ValueCriticModel(学习状态价值估计)
Step 3: compute_advantages
数据 + ValueCriticModel → advantage 标签(衡量动作好坏)
Step 4: train_cfg_rl
数据 + advantage → PolicyModel(学会模仿好动作,避免坏动作)
训练时:
正样本 → 条件分支(正引导 prompt)→ 学习好动作
负样本 → 无条件分支(原始 prompt)→ 保持基础分布
推理时:
CFG 融合:v = (1-w)*v_uncond + w*v_cond
只使用正引导,让模型偏向生成好动作
十二、为什么 CFG RL 有效?
| 机制 | 效果 |
|---|---|
| 优势标签过滤 | 从混合数据中提取高质量训练信号 |
| 条件分支 | 让模型在正样本上学习更好的动作 |
| 无条件分支 | 保持模型的泛化能力和多样性 |
| CFG 融合 | 推理时灵活控制生成方向 |
这种方法的核心思想是:利用监督学习训练价值模型,然后用价值模型为离线数据打标签,最后用 CFG 技术让策略模型学会区分好/坏动作。