前面文章已经把openpi论文及代码解析(A Vision-Language-Action Flow Model for General Robot Control) (一)技术做了梳理, 今天我把之前看的Pi0-Fast, 两者都是Physical Intelligence公司作主导研发.也进行回顾总结下.
论文地址: FAST: Efficient Action Tokenization for Vision-Language-Action Models
一、 文章阅读
之前的机器人策略采用按时间步进行分享方案的策略即将连续的机器人动作映射为离散的动作token, 但是这种动作在执行高频控制的灵巧技能的时候, 方法效果不佳. ⭐️ 不要选择频率过高的动作预测, 否则动作token之间的相似性很高, 说简单点就是复制最近的token一样简单, 不利于模型训练, 容易使得模型陷入局部最优
. 作者采用的策略是在训练之前需要压缩机器人动作信号,以减少连续令牌之间的相关, 这里作者选择DCT(discrete cosine transform)编码
, 对连续信号进行转化.该方法被广泛应用于压缩图像等连续信号(如JEPG压缩). ⭐️作者提出了一种基于时间序列压缩技术的新的FAST token化方案,允许在高频数据上训练自回归VLA.
1. 高效分词策略
机器人动作信号在训练前需要进行压缩,以减少连续token之间的相关性, 他们借鉴了基于压缩的分词策略,例如语言模型中常用的字节对编码方法「byte-pairencoding,BPE」——通过合并频繁出现的token序列为新token来压缩输入文本. 这里发明的FAST名字的由来是由频域动作序列分词『Frequency-spaceAction Sequence Tokenization,FAST』
得名.
且基于FAST,作者还开发了
FAST+
,这是一种通用的机器人动作分词器,在覆盖多种机器人结构、动作空间和控制频率的100 万条真实机器人动作轨迹上进行训练他们证明了FAST+ 分词器能够有效地对从单臂到双臂及移动机器人等多种机器人动作序列进行分词,并且是训练自回归VLA 模型的即插即用分词器. 当与π0 VLA 集成时,基于FAST 的自回归VLA 能够扩展到在10k 小时的机器人数据上进行训练,并在多种任务中实现与基于扩散的VLA 相当的性能,同时将训练时间最多缩短至5 倍如下图所示当使用简单的逐时间步标记化方案时,随着训练信号控制频率的增加,该边际信息趋近于零:对于平滑信号,随着时间步变短,每个时间步的变化成比例减小。这极大地减缓了训练期间的收敛速度,并可能使拟合复杂的高频数据集变得困难。高频动作轨迹中的冗余如何导致每个动作标记的边际信息较低,从而导致训练性能不佳。为解决这个问题,我们需要一种将高度冗余的动作信号压缩为更少高信息标记的标记化方法。作者在这项工作使用基于离散余弦变换(DCT)的压缩算法。DCT 是一种频域变换,将连续信号表示为各种频率的余弦元素之和。低频捕获信号的整体形状,而高频分量反映急剧变化。DCT 是压缩算法(如图像 JPEG 压缩)的常用变换,因其简单性和计算效率,以及对实际图像的强压缩特性:由于像素通常平滑变化,DCT 通常可以仅用几个系数表示输入信号的大部分信息。信号可以通过省略低权重的频率分量来压缩。与基于向量量化的学习压缩方法相比,基于 DCT 的压缩是一种解析方法,因此极其简单和快速.
根据下图可以看到, 采样率对预测性能的影响。在教学插值任务上训练了一个小型的自回归 transformer 模型,其中网络必须预测给定四个圆圈的黑色虚线曲线。我们发现,由于高频下连续标记之间的强相关性,随着我们增加底层信号的采样频率,使用先前 VLA中使用的分箱标记化方法训练的模型产生的预测越来越差。我们的 FAST 分词化方法基于所有采样率的高质量预测。(下图左侧是以前的自回归 VLA(如 RT-2、RT-2-X和OpenVLA)使用的每维度分箱方案进行比较——那种方案被称native tokenization, 下图右侧是FAST所采取的tokenization*), 同时也可以看出在不同采样频率下训练的自回归模型的平均预测均方误差(MSE), 但是在DCT压缩目标token上训练自回归模型在各种采样频率下始终保持较低的预测误差. 而如果是使用分箱tokenization的模型,则虽然在低采样率下实现了良好的预测性能(即较低的MSE),但随着采样率的增加,预测误差急剧增加,最终模型仅仅复制了第一个动作,
高频下,相邻token几乎相同,模型只需抄上一句即可“蒙混过关”。导致只复制第一个 如下图图左所示.
学习信号 = “预测下一个 token 时收到的误差反馈(梯度)”。
边际信息内容 = “在给定历史上下文的前提下,新 token 出现的“意外”程度
学习信号越大,说明误差越大,新的token的“意外”程度就越大,促使模型更大幅度地修正以便下次更好地预测。
- 当使用简单的逐时刻分词方案时,随着训练信号的控制频率增加,这一边际信息趋近于零
- 对于平滑信号,随着时间步变短,每个时间步的变化也按比例减小。这极大地减缓了训练过程中的收敛速度,并可能导致难以拟合复杂的高频数据集
- 先前的研究中已经观察到了此类挑战。例如,OpenVLA 在低频的BridgeV2 和RT-1 数据集上表现良好,但在拟合高频的DROID 数据集时遇到了困难
我们接下来看下FAST Tokenization算法
给定一段归一化后的动作块,我们首先对其应用离散余弦变换(DCT),将时域信号转换到频域。接着对 DCT 系数量化,并使用字节对编码(BPE)将各维度的 DCT 系数展平成一维序列后压缩,得到最终的动作 token 序列
首先对输入动作进行标准化:将训练集中每个动作维度的第1到第99百分位映射到[−1,1]区间。这个初始标准化步骤有助于将数据带入指定范围,并且还使得具有不同动作尺度的跨实体数据集的token化更为容易,且使用分位数来应对大型机器人数据集中偶尔出现的异常动作。
在数据标准化后,作者对每个动作维度分别应用离散余弦变换DCT,变成频域系数对DCT系数进行压缩时,可以简单的省略不重要的系数,通过“缩放+四舍五入”来实现,其中缩放比例是一个超参数,控制token化的损失率和压缩率之间的平衡。
在舍入操作之后,DCT系数矩阵通常是稀疏的,大多数条目为零,每个动作维度仅保留少数几个重要系数
为了真正实现压缩,必须将这个稀疏矩阵转换为一系列密集的token先将矩阵展平成一维整数向量,按先低频后高频并在维度间交错的顺序拼接,然后训练字节对编码(BPE)分词器,对其进行无损压缩,得到紧凑的动作token。
BPE步骤会“压扁”所有零值分量,并合并跨维度的高频组合。之所以选用BPE来压缩DCT矩阵,因为它有众多高效实现,还能生成固定大小的输出词表,便于与现有视觉-语言模型词表无缝集成。其他无损压缩算法,如哈夫曼编码或Lempel-Ziv方法(gzip底层算法)也可替代,但留待后续研究。
注意,在进行BPE编码前,如何扁平化 |A|×H 的DCT系数矩阵,对策略训练效果影响很大。有两种展平方式:列优先——先拼接每个维度的低频分量;行优先——先拼接单个维度的所有频率分量。列优先让模型先学整体趋势,行优先则先学某个维度全貌,各有得失, 这里选择列优先,因为实验发现,在自回归预测时先预测低频分量(即输出序列的整体形状),能带来更稳定的策略执行。如此,tokenization流程中的所有操作都易于逆转,允许快速解码预测的动作。
该tokenizer只有两个超参数:用于DCT系数舍入前的缩放比例,以及BPE步骤的词汇表大小。作者发现这两个参数都不太敏感,并且在所有的单数据集tokenization实验中使用了相同的值(舍入比例10,BPE词汇量大小1024)。这与依赖于矢量量化的端到端学习压缩模块形成对比(那些模块往往动辄数十个超参),需要为每个数据集精心调参才能获得良好的重构效果。
通用机器人动作tokenizer: FAST+
FAST分词器的唯一学习组件是 BPE 编码器的词汇表,这需要为每个新数据集进行训练,以便应用该分词器,虽然该训练过程很快(通常仅需几分钟),但仍为使用FAST引入了额外的门槛。
因此,作者的目标是再训练一个通用动作分词器,能对来自任意机器人的动作块进行编码。为此,作者使用上述流程在一个大型跨形态机器人动作数据集上训练分词器,该数据集由大约一百万个1秒的动作chunk组成,涵盖单臂、双手和移动操作机器人,具有关节和末端执行器控制动作空间以及各种控制频率——他们在附录A中详细列出了用于训练通用分词器的数据混合集。对于许多数据集,他们包括了多个动作空间参数化版本:联合空间、末端执行器世界框架和末端执行器摄像头框架,以确保生成的分词器的通用性。Open X-Embodiment 、DROID 和 Bridge V2 以其原始形式包含在内。
一旦训练完成,通用动作分词器FAST+即可以作为一个黑箱分词器,应用于任何机器人设置的1秒动作序列。且实验评估显示,它与为单个数据集调优的分词器具有竞争力。作者将预训练好的通用动作分词器 FAST+ 封装为 HuggingFace 的 AutoProcessor 类,用户只需三行代码即可在任意新动作块上调用。
from transformers import AutoProcessor
tokenizer = AutoProcessor.from_pretrained(
"physical-intelligence/fast",
trust_remote_code=True
)
tokens = tokenizer(action_chunk)
# 为获得最佳压缩效果,建议如4.2所述,先将输入动作通过分位数标准化到[−1,1]后,再以1秒为单位进行token化。这个模块同样支持在给定动作块数据集上快速训练新的FAST分词器:
from transformers import AutoProcessor
tokenizer = AutoProcessor.from_pretrained(
"physical-intelligence/fast",
trust_remote_code=True
)
new_tokenizer = tokenizer.fit(action_dataset)
打个比方
自回归:像写作文,一字一句接着写,写到哪儿就决定哪儿。
扩散式:像整体给你一块泥,用雕塑刀逐步修整,最后一次性出炉一尊精致雕像。
扩散式 VLA 就是用去噪的方式并行地“雕刻”全序列动作,再量化成离散 token,而不是像大模型那样一句一句地自左向右生成。
这里面给到的FastTokenizer代码如下所示:
class FASTTokenizer:
def __init__(self, max_len: int = 256, fast_tokenizer_path: str = "physical-intelligence/fast"):
self._max_len = max_len
# Download base PaliGemma tokenizer
path = download.maybe_download("gs://big_vision/paligemma_tokenizer.model", gs={"token": "anon"})
with path.open("rb") as f:
self._paligemma_tokenizer = sentencepiece.SentencePieceProcessor(model_proto=f.read())
# Instantiate FAST tokenizer
self._fast_tokenizer = AutoProcessor.from_pretrained(fast_tokenizer_path, trust_remote_code=True)
self._fast_skip_tokens = 128 # Skip last 128 tokens in PaliGemma vocab since they are special tokens
def tokenize(
self, prompt: str, state: np.ndarray, actions: np.ndarray | None
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
cleaned_text = prompt.lower().strip().replace("_", " ")
# Convention: state gets discretized into 256 discrete bins (assumed range after normalization: [-1, 1])
discretized_state = np.digitize(state, bins=np.linspace(-1, 1, 256 + 1)[:-1]) - 1
# Convention: prefix includes prompt and string-representation of state, followed by ';'
state_str = " ".join(map(str, discretized_state))
prefix = f"Task: {cleaned_text}, State: {state_str};\n"
prefix_tokens = self._paligemma_tokenizer.encode(prefix, add_bos=True)
if actions is not None:
# Tokenize actions with FAST tokenizer --> map to last tokens in PaliGemma vocab
action_tokens = self._fast_tokenizer(actions[None])[0]
action_tokens_in_pg = self._act_tokens_to_paligemma_tokens(action_tokens)
# Convention: postfix contains 'Action:' followed by FAST tokens, followed by '|'
postfix_tokens = (
self._paligemma_tokenizer.encode("Action: ")
+ action_tokens_in_pg.tolist()
+ self._paligemma_tokenizer.encode("|", add_eos=True)
)
else:
postfix_tokens = []
# Create output token sequence & masks
# AR mask is 0 on prefix (bidirectional attention) and 1 on postfix (causal attention to all previous tokens)
tokens = prefix_tokens + postfix_tokens
token_mask = [True] * len(tokens)
ar_mask = [0] * len(prefix_tokens) + [1] * len(postfix_tokens)
loss_mask = [False] * len(prefix_tokens) + [True] * len(postfix_tokens) # Loss on postfix only
# Pad tokens to max length
tokens_len = len(tokens)
if tokens_len < self._max_len:
padding = [False] * (self._max_len - tokens_len)
tokens = tokens + padding
token_mask = token_mask + padding
ar_mask = ar_mask + padding
loss_mask = loss_mask + padding
else:
if len(tokens) > self._max_len:
logging.warning(
f"Token length ({len(tokens)}) exceeds max length ({self._max_len}), truncating. "
"Consider increasing the `max_token_len` in your model config if this happens frequently."
)
tokens = tokens[: self._max_len]
token_mask = token_mask[: self._max_len]
ar_mask = ar_mask[: self._max_len]
loss_mask = loss_mask[: self._max_len]
return np.asarray(tokens), np.asarray(token_mask), np.asarray(ar_mask), np.asarray(loss_mask)
def extract_actions(self, tokens: np.ndarray, action_horizon: int, action_dim: int) -> np.ndarray:
# Decode predicted output tokens
decoded_tokens = self._paligemma_tokenizer.decode(tokens.tolist())
# Extract actions from FAST model outputs
if "Action: " not in decoded_tokens:
return np.zeros((action_horizon, action_dim), dtype=np.float32)
# Extract actions from decoded tokens
raw_action_tokens = np.array(
self._paligemma_tokenizer.encode(decoded_tokens.split("Action: ")[1].split("|")[0].strip())
)
action_tokens = self._act_tokens_to_paligemma_tokens(raw_action_tokens)
return self._fast_tokenizer.decode(
[action_tokens.tolist()], time_horizon=action_horizon, action_dim=action_dim
)[0]
def _act_tokens_to_paligemma_tokens(self, tokens: np.ndarray | list[int]) -> np.ndarray:
if isinstance(tokens, list):
tokens = np.array(tokens)
return self._paligemma_tokenizer.vocab_size() - 1 - self._fast_skip_tokens - tokens
参考:
[1] 自回归版π0-FAST——打造高效Tokenizer:比扩散π0的训练速度快5倍但效果相当(含π0-FAST源码剖析)
[2] π0-FAST-针对VLA模型的高效动作token化技术-2025.1.16-开源