简介:这是一份面向具身智能、视觉语言导航与家务任务规划研究者的基准资源包,适合有一定深度学习基础、希望进入具身智能领域的学生或工程师,用于学习从自然语言指令和第一视角观察映射到长时序家务动作序列,并评估智能体完成真实家居任务的能力。资源共2000个文件,压缩包76.72MB,主要包含8293个JSON场景定义与任务实例、120个NPY状态与特征数据、55个Python训练评估脚本,以及C语言实现的指令解析、状态搜索与规划求解模块,同时提供PDDL任务描述、Dockerfile部署配置和辅助工具,完整覆盖数据、模型、环境与工具链。已有194人浏览学习。借助该包可复现官方实验,深入理解不可逆状态变化、长程指令跟随等核心挑战;也能基于交互式TextWorld环境扩展测试场景,或参考底层C实现改进规划逻辑,从而构建并验证自己的家务指令理解系统,是进入该方向的高密度技术参考。
1. 第一次接触 ALFRED 基准,很容易以为它只是一套动作控制评测,上来就把精力全砸进策略网络。跑过一轮 valid-seen 才会发现,真正让模型翻车的,往往是那句 high-level 指令怎么被解释成子目标序列。ALFRED 是一个围绕日常任务的基础指令基准:把“把杯子放到茶几上”这类指令、室内导航和物体交互揉进同一条评测协议。它适合两类人:一类做具身智能,想在仿真环境里评估视觉语言模型;另一类做任务拆解,希望用标准数据卡住自己对“理解”的判断。这个基准不负责送你漂亮分数,它逼你回答一个问题——模型到底懂没懂那句话。
2. 认识 ALFRED 的任务结构:一条指令如何在数据里拆成可测的子目标
ALFRED 的数据组织方式和常见 VQA 数据集完全不同。它不给你“图片 + 问题 + 答案”的三元组,而是给你一整个带标注的 episode:从初始状态到最终状态,所有动作、帧、子目标、自然语言指令都对齐好了。本地第一次展开一个 episode 目录时,你会看到几十张图片、一份描述任务元数据的 JSON 文件,以及藏在 JSON 里的完整动作序列。这一章先把结构讲清楚,后面所有评估代码都是对这个结构的遍历。
2.1 三个指令层级:high-level、low-level 与 subgoal
ALFRED 里有一套严格的分层指令体系,很多人上来只看最高层那一句话,导致后面评估逻辑全是错的。顶层叫 high-level instruction,就是人类日常说的一句话,比如“将客厅茶几上的马克杯放进微波炉加热后放到餐桌上”。这句话是你最终要模型“解释”的对象。中间层叫 subgoal,是模型必须自己拆出来的语义步骤:拿马克杯、走到微波炉、开门、加热、取杯、走到餐桌、放杯。底层叫 low-level action,是仿真环境里真正执行的 MoveAhead、Rotate、PickupObject、OpenObject 这类原子操作。
我之所以强调这个分层,是因为 ALFRED 的评测核心不是“动作序列像不像”,而是“子目标做没做对”。一个模型可能用完全不同的底层动作完成了同一件事,比如绕过桌子和直接穿过桌子,动作序列差异很大,但 subgoal 语义一致。反过来,动作序列碰巧一致,却可能因为拿错了物体导致任务失败。高层的“基础指令”和底层的“动作流”之间,隔着一个必须显式建模的抽象层,这才是这个基准真正的门槛。
为了在调试时快速理解分层关系,我习惯先用下面这种伪 JSON 结构把一条 episode 的逻辑轮廓画出来:
{ "task": "put a warm mug on the coffee table", "subgoals": [ {"action": "pickup", "obj": "mug"}, {"action": "heat", "obj": "mug"}, {"action": "place", "obj": "mug", "recep": "coffeetable"} ], "actions": [ {"action": "MoveAhead", "step": 1}, {"action": "PickupObject", "obj": "mug", "step": 47}, {"action": "MoveAhead", "step": 48} ] }这段伪代码帮我把“模型该解释出什么”和“模型该执行什么”分开。subgoals 是解释层,actions 是执行层,两者长度没有一一对应关系——一个 subgoal 可能需要几十步底层动作才能完成。调试模型时,如果只盯着 actions 的逐帧匹配,你会被大量噪声淹没;把注意力放到 subgoals 上,才能看到指令理解的真实错误。
2.2 episode 目录里装了什么:帧、JSON 与对齐关系
拆开一个 episode,常见布局是下面这样。注意不同版本可能有命名差异,但大体逃不出这三类内容:
episode_123/ traj_data.json # 任务元数据:指令、子目标、动作、初始状态 png/ # 模型逐帧看到的彩色图,按时间戳命名 depth/ # 有 depth 版本时出现的深度图目录 mask/ # 部分版本提供实例分割 masktraj_data.json 是核心,它至少包含三类信息:goal 对象(high-level 指令、subgoal 标注)、动作列表(每一步的原子操作和参数)、以及初始场景配置。png 目录里的帧图像供视觉模型输入,深度图则给需要空间结构的模型使用。我一般会用 json.load 把 traj_data.json 读进来后,先打印它的顶层 key,而不是一上来就假设字段名称,因为不同渠道拿到的清洗版本字段命名并不完全一致。
为什么强调保留原始帧而不是直接替换成特征文件?因为 ALFRED 的评估标准要求模型在相同观测条件下 compete。如果你在本地把图像预提取成 ResNet 特征,就可能丢掉细粒度视觉信息,比如判断物体开关状态、冷热状态这类视觉线索,而这些恰好是“解释指令”必须依赖的证据。保留原始帧,意味着你的模型随时可以回头看当前画面,和人类执行任务时“看一眼再决定下一步”的行为一致。
2.3 train / valid-seen / valid-unseen:为什么评估必须留一手
ALFRED 的数据划分是我见过的具身基准里最讲究泛化的一档。训练集用于拟合策略;valid-seen 用训练中出现过的房屋布局做验证,主要排查过拟合和代码 bug;valid-unseen 则放进没有见过的房屋、物体组合和指令表述,专门测试模型能不能泛化。很多人只在 valid-seen 上刷分,结果模型换了场景就崩盘,原因是它记住了场景布局,而不是真正理解指令。
| 数据划分 | 场景是否见过 | 适合做什么 | 典型误区 |
|---|---|---|---|
| train | 训练用 | 拟合模型 | 在 valid-seen 上反复调参直到过拟合 |
| valid-seen | 训练场景见过 | 调试链路、排查 bug | 把它当最终成绩 |
| valid-unseen | 场景没见过 | 评判泛化能力 | 跳过不看,只报 seen 分数 |
| test | 未公开 | 最终提交 | 反复用 test 调参导致污染评估 |
我自己的评估顺序固定是:先跑通 valid-seen 的随机基线,确认数据加载和指标计算没问题;再跑 valid-unseen,看模型在陌生场景里是否真的按指令行事。valid-unseen 才是你向别人证明“解释日常任务基础指令”能力的硬指标,valid-seen 分数再好看,也只能说明模型没有把环境搞错。
3. 搭一个最小评估链路:从 episode 加载到跑出随机基线
这一章解决的是落地路径问题。很多入门者卡在“模型还没训练,评估代码先写不出来”这一步,其实可以先不训练,只用 ALFRED 提供的标注数据,把“加载 episode → 调用模型策略 → 输出动作 → 计算指标”这条链路跑通。链路通了,后面换任何模型都只是替换策略函数的问题。
3.1 最小依赖与目录约定:避免评估脚本被环境拖垮
评估 ALFRED 不需要重型框架。我一般环境里只装这几样:Python 3.9 以上、PyTorch(跑视觉模型用)、Pillow(读帧)、NumPy、tqdm(进度条)。如果只是验证数据格式,连 PyTorch 都可以先不装。依赖越少,后面换机器复现越省事。
# 常见做法是建两个目录,一个放数据,一个放预测结果,互不污染 mkdir -p data/valid_seen data/valid_unseen pred/ pip install pillow numpy tqdm参数说明:data/ 存原始 episode,pred/ 存模型输出的动作序列 JSON。这样做的理由是把“数据读取”和“模型输出”解耦,排查问题时能直接对比 pred 里的动作和标注动作,不必重新跑一遍模型。目录约定虽然简单,但能避免后面反复修改绝对路径的麻烦。
3.2 加载 episode 并重建“指令到动作”的对应关系
加载这一步的核心不是 json.load 本身,而是想清楚你要让模型看到什么、输出什么。一个 episode 里,模型该读的是 high-level 指令文本和逐帧图像;该输出的是动作序列。下面是加载 episode 并抓取对应关系的最小脚本:
import json from pathlib import Path def load_episode(json_path: str) -> dict: with open(json_path, "r", encoding="utf-8") as f: return json.load(f) def collect_subgoals(ep) -> list[str]: # subgoals 是模型需要“解释”出来的中间结果 # 统一编码成 "action@obj" 字符串,方便后续做序列比较 return [ g.get("action", "") + "@" + g.get("obj", "NA") for g in ep.get("subgoals", []) ] def collect_actions(ep) -> list[str]: # 模型真正要逐步输出的原子动作,按时间顺序排列 return [a.get("action", "") for a in ep.get("actions", [])] if __name__ == "__main__": ep = load_episode("data/valid_seen/episode_123/traj_data.json") print(collect_subgoals(ep)[:5]) print("总动作数:", len(collect_actions(ep)))逻辑说明:collect_subgoals 把非结构化的 subgoal 列表压平成字符串序列,是为了后面能直接和模型输出的 subgoal 序列做编辑距离或精确匹配;collect_actions 则用来评估底层执行质量。参数上,json_path 指向 traj_data.json,其他字段若缺失用空列表兜底,避免目录不完整时整个脚本崩溃。
有一点要注意:不要在这里把所有帧一次性读进内存。一个 episode 的帧数量可能上百,加上 depth 图会直接把内存吃满。正确做法是留一个帧读取函数,按需从磁盘读当前步的图像,等模型处理完再释放。评估脚本的稳定性往往不是死在模型上,而是死在内存上。
3.3 先跑一个随机基线:让评估管线转起来
写完加载逻辑后,立刻写一个随机策略跑通全流程。这一步非常重要,它确认的是“评估代码本身没有 bug”,而不是模型有没有能力。我见过不少人跳过随机基线,直接上训练好的模型,结果指标低还找不到原因,最后发现是动作对齐少了一位。
import random ACTIONS = ["MoveAhead", "Rotate", "PickupObject", "PlaceObject", "OpenObject"] def random_policy(step: int) -> str: # 随机策略:每步从合法动作里随便挑一个,不考虑画面内容 return random.choice(ACTIONS) def eval_random_baseline(episodes, max_steps: int = 200) -> float: total_matched = 0 total_actions = 0 for ep in episodes: gold_actions = collect_actions(ep)[:max_steps] pred_actions = [random_policy(i) for i in range(len(gold_actions))] total_matched += sum(1 for p, g in zip(pred_actions, gold_actions) if p == g) total_actions += len(gold_actions) return total_matched / max(total_actions, 1) episodes = [load_episode(p) for p in glob.glob("data/valid_seen/*/traj_data.json")] print("随机基线动作命中率:", eval_random_baseline(episodes))逻辑说明:这段代码把模型输出替换成 random_policy,用最朴素的方式把“预测动作 vs 标注动作”的对齐逻辑跑一遍。结果应该接近随机概率,比如动作类别有 10 种时命中率在 10% 左右。如果你的随机基线命中率异常高,说明动作列表里有大量重复标签,或者对齐逻辑有偏置——这比任何模型调参都值得先排查。
参数说明:max_steps 限制每个 episode 评估步数,防止某些长 episode 拖慢调试速度;random_policy 当前忽略 step 参数,正式模型会用 step 来决定是否重新读帧。随机基线不是用来提交结果的,它是一把尺子,量出评估代码本身有没有系统性偏差。
3.4 从指标到结论:看哪些数才算“解释对了”
评估 ALFRED 时,指标要分层看。最粗的是动作命中率,即预测动作与标注动作逐帧一致的占比,但这个指标容易被重复动作稀释。真正有意义的是子目标完成率:模型是否在正确的时间完成了 pick、place、open、heat 这类关键操作。
| 指标 | 计算方式 | 回答的问题 |
|---|---|---|
| 动作命中率 | 逐帧预测动作与标注相同的比例 | 底层策略是否稳定执行 |
| 子目标完成率 | 按顺序完成的 subgoal 数与总 subgoal 数之比 | 指令解释是否到位 |
| 任务成功率 | 最终目标状态是否达成 | 整体任务是否完成 |
| 路径加权成功率 | 成功率除以轨迹长度惩罚 | 是否用最短路径完成 |
我调试时先看子目标完成率。如果这个数低,再去看是哪一个 subgoal 丢了——是没拿对物体,还是没走到位。任务成功率虽然最直观,但它只能告诉你“失败了”,不能告诉你“为什么失败”;子目标完成率则能定位到具体哪一步解释错了。这个习惯能节省大量分析时间。
4. 解释基础指令的落地选型:规则解析与子目标判别的取舍
上一章解决了“怎么评估”,这一章解决“怎么让模型输出可评估的结果”。ALFRED 给的高层指令是一句自然语言,模型不能直接执行,必须先把这句话解释成 subgoal 序列。这个“解释”模块怎么做,直接决定你后续所有指标的上限。
4.1 规则解析 vs 模型映射:两种路径各有各的坑
实现指令到 subgoal 的映射,常见有两条路线。一条是规则解析,用关键词和模板匹配,把“put the mug on the table”映射到 pickup@mug → place@mug@table;另一条是用语言模型直接生成 subgoal 序列,比如把整句指令丢给预训练模型让它输出 JSON 数组。两条路线的取舍非常明确。
| 路线 | 优点 | 缺点 | 适合阶段 |
|---|---|---|---|
| 规则解析 | 可解释、可调试、零成本 | 同义词覆盖有限、组合指令容易漏 | 跑通链路、验证指标 |
| 小模型分类 | 能处理轻微句法变化 | 需要标注数据,且会引入额外误差 | 有一定数据集之后 |
| 大语言模型生成 | 语义理解强、泛化好 | 延迟高、输出不稳定、推理成本大 | 最终业务方案 |
我一般会先用规则解析把链路跑通,再逐步替换成模型。原因很简单:如果规则解析都只能达到 50% 的 subgoal 完成率,说明评估链路有问题,换成大模型也只是把问题掩盖住;反之,规则解析能到 70%,这为后续模型方案提供了一个坚实的对照基线。很多团队一上来就上大模型,结果输出格式不稳定,连指标都统计不出来。
4.2 从 high-level 指令到 subgoal 候选:一个可复现的解析器
下面这个解析器是常见做法的简化版,适合在 ALFRED 数据上做规则解析验证。它把指令文本转成 “action@obj” 序列,关键是把动作触发词和物体别名词典分开维护,调试时能直接看是哪个词典漏了词。
import re OBJECT_ALIASES = { "mug": ["mug", "cup"], "book": ["book", "magazine"], "plate": ["plate", "dish"], "remote": ["remote", "tv remote"], } ACTION_TRIGGERS = { "pickup": ["pick up", "take", "grab", "get"], "place": ["put", "place", "set down", "move"], "heat": ["heat", "warm", "microwave"], "cool": ["cool", "refrigerate", "freeze"], "clean": ["clean", "wash", "rinse"], "open": ["open", "unlock"], } def parse_instruction(text: str) -> list[str]: text = text.lower().rstrip(".") subgoals = [] for action, triggers in ACTION_TRIGGERS.items(): if not any(t in text for t in triggers): continue for obj, aliases in OBJECT_ALIASES.items(): if any(a in text for a in aliases): subgoals.append(f"{action}@{obj}") break # 按物体首次出现的位置排序,尽量还原执行顺序 subgoals.sort(key=lambda sg: text.find(sg.split("@")[1])) return subgoals逻辑说明:这个解析器分两步,先检测指令里的动作触发词,再匹配物体别名,最后按物体在文本中出现的位置排序。排序逻辑基于一个朴素假设——人类描述任务时,先提到的物体大概率先操作。这个假设在 ALFRED 数据里大部分时候成立,但遇到“先把杯子加热再放到茶几上”这类指令时,物体位置排序依然可靠,因为是同一个物体。
参数说明:ACTION_TRIGGERS 和 OBJECT_ALIASES 是两本可扩展词典,你可以根据数据分布不断补词。我调试时会把 text.find(sg.split("@")[1]) 的结果打印出来,如果排出来的顺序和标注 subgoal 差太多,就去检查是不是有介词短语干扰了排序。这个脚本的局限也很明显:无法处理“turn on the lamp after cleaning the mug”这种带逻辑关系的复合指令,但对入门链路已经足够。
4.3 评价信号:用 subgoal 完成率替代逐字匹配
解析器输出 subgoal 序列后,不要拿它和标注做字符串精确比较。自然语言表达多变,模型输出 “pickup@mug” 而标注是 “PickupObject@mug”,语义相同但字符串不同,逐字比较会低估模型能力。正确做法是定义一组等价映射,把同义动作和同义物体归一化后再比较。
| 比较方式 | 实现难度 | 误报率 | 适用场景 |
|---|---|---|---|
| 字符串精确匹配 | 低 | 高 | 快速检查管线 |
| 同义词归一化匹配 | 中 | 中 | 子目标完成率评估 |
| 语义向量相似度 | 高 | 低 | 最终业务级评估 |
我最常用的是同义词归一化:动作层把 “pickup” 和 “PickupObject” 映射到同一 ID,物体层把 “mug” 和 “cup” 映射到同一 ID,然后比较归一化后的序列。这样既保留了解释的语义性,又不会因为命名差异而误杀正确结果。子目标完成率就用归一化后两个序列的最长公共子序列长度除以标注序列长度,这个分数比动作命中率更能反映“模型是否听懂指令”。
5. ALFRED 评测的 5 个避坑记录:为什么你的结果总比预期低一截
这一章是血泪经验的集中区。ALFRED 评测链路环节多,任何一个环节出问题都会让分数莫名其妙掉一个档次。下面 5 条是我调试时反复遇到的典型坑,每一条都按“现象 → 原因 → 解决”记录。
5.1 图像分辨率不一致,导致视觉特征漂移
现象:同一个模型在 A 机器上 valid-seen 完成率 30%,换到 B 机器只有 15%。代码完全没改,数据是同一份。原因:两套环境读取帧图像后没有统一 resize 到相同尺寸,视觉模型输入的张量分布发生了变化。某些预训练视觉编码器对输入分辨率敏感,分辨率漂移会直接改变特征分布。解决:在数据加载函数里强制统一图像处理流程,包括 resize 尺寸、归一化参数、通道顺序,并把处理后的张量形状打印出来作为日志落盘。我习惯在评估开始前先断言所有帧经过预处理后 shape 完全一致,不一致就抛异常,而不是静默通过。
5.2 无效动作掩码被忽略,参数量直接翻车
现象:模型预测了大量环境不允许的“穿墙”动作,成功率却很低。原因:ALFRED 环境里每一步有一个合法动作集合,比如门没开时不能进入房间、物体不在手边时不能 PickupObject。很多评估脚本只比较动作名称,没有屏蔽这些非法动作,导致模型输出了环境不接受的指令。解决:在策略输出后加一个合法动作过滤器,把不可执行的动作替换为 MoveAhead 或随机合法动作。这一步看起来降低了模型分数,实际上让评估逻辑更接近真实世界,因为最终提交时环境会拒绝非法动作。
5.3 词典在 valid 集上拟合,导致报告失真
现象:规则解析器在 valid-seen 上 subgoal 完成率 80%,在 valid-unseen 上掉到 40%。原因:解析器的动作触发词表和物体别名词典是在 valid-seen 的指令上反复调试出来的,里面包含了大量该子集的特殊说法,换到 unseen 集就失效了。解决:把用于调试的 valid-seen 指令和用于调参词典的指令严格分开。正确流程是只用训练集指令来总结关键词,valid-unseen 只作为最终验证。如果你发现某个关键词只存在于 valid-seen 而训练集没有,判定它是过拟合特征,应该删掉。
5.4 子目标完成判定写成“全对才算过”,导致细节错误被放大
现象:模型成功完成了 6 个 subgoal 里的 5 个,任务却判定失败,分数直接归零。原因:评测代码把任务成功率等同于子目标全对,没有区分“部分完成”和“完全失败”。这在早期链路里特别容易踩,因为大家懒得实现逐步判定。解决:把指标拆成两个层级——任务成功率只在最终状态达成时算 1,子目标完成率按实际完成比例累加。这样模型连续完成 5 个子目标能拿到 0.83 的完成率,信息量远大于一个 0。正式提交时再取任务成功率,训练和调参阶段用子目标完成率辅助定位问题。
5.5 帧采样率不一致,动作与观测错位
现象:所有指标都波动剧烈,同样的随机种子跑两次结果不同。原因:episode 里动作列表和图像帧不是一一对应,有的版本每 3 帧采样一次,有的版本每 5 帧采样一次。模型决策时读到的画面和当前动作不在同一时刻,导致视觉信息和动作错位。解决:先明确你拿到的数据版本里动作索引和图像索引的换算关系,在加载时统一采样频率。我习惯在加载后打印“动作数: 137,帧数: 412”这类统计,一眼就能看出比例关系是否合理。如果动作数和帧数相差 5 倍以上,基本可以断定采样率没对齐。
6. 从基准到业务:用手工小验证集卡住指令理解的水位
打完 valid-unseen 之后,很多人就停了。但如果你要把 ALFRED 积累的经验用到自己的业务数据上,还需要一道额外的门槛:手工验证集。因为官方基准再标准,也和你自己的指令分布有差距。我习惯从 valid-unseen 里抽 30 条指令,覆盖 10 种常见的 subgoal 组合,然后人工标注每条指令的标准 subgoal 序列。这个小集合不需要大,关键是稳定和可解释——它用来测量“模型解释指令”的水位线,而不是训练模型。
评估方式用编辑距离最直观——模型输出的 subgoal 序列和人工标注序列差几步,就说明解释质量如何:
def subgoal_edit_distance(pred_seq: list[str], gold_seq: list[str]) -> int: m, n = len(pred_seq), len(gold_seq) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): cost = 0 if pred_seq[i - 1] == gold_seq[j - 1] else 1 dp[i][j] = min( dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost, ) return dp[m][n]逻辑说明:这段代码计算两个 subgoal 序列的最小编辑距离,删除、插入、替换各算一步。参数 pred_seq 是模型解释出的序列,gold_seq 是你人工标注的正确答案。距离为 0 代表完全解释正确,距离越大说明模型漏了步骤或顺序错乱。我一般把距离阈值设为 1,允许模型偶尔多输出一个冗余 subgoal,毕竟真实场景里“顺手关门”这类动作可有可无。
我的习惯是,每调完一版模型,先跑这个 30 条的小验证集,再看 valid-unseen 的大指标。小验证集能快速暴露解释层的退化方向,大指标则确认整体没有回退。两次都稳定,才敢把模型放到业务数据上;如果小验证集都波动明显,那大指标再好看也只是运气。希望这个习惯能帮你在 ALFRED 的坑里少走几步,也帮你在自己的任务里更快判断:模型到底懂没懂那句话。
本文还有配套的精品资源,点击获取