在这里插入图片描述

朋友在客厅摆了张桌子,想让机器人自己找到桌上的杯子。他问我能不能直接上个 7B 的 VLA。

我说先别。找杯子这个动作,感知占一半,抓取占一半。感知这一步,一个 45 MB 的 ResNet 就够跑,7B 模型一次推理几百毫秒,进不了 30 Hz 的控制回路。先把杯子在桌面上的坐标测准,再谈手怎么伸。

他同意了。条件是我得真跑一遍,不能只给方案。

这篇文章就是那次实验的记录。数据来自 Q1 的 MuJoCo 模型,训练在本机 CPU 上跑,所有数字都是脚本打印出来的,日志和截图都在。先把结论放前面,实验室训出来的模型到客厅,位置误差从 0.95 厘米掉到 10.2 厘米。加 10% 的客厅数据,回到 2.96 厘米。

先说清楚,数据不是真机采的

我没有 Q1 真机。谁要是拿一篇没碰过机器人的文章告诉你真机数据长什么样,那是编的。

我能拿到的是官方的 Q1 MJCF 模型,q1_v3.xml,带完整的 22 关节结构和 head_link 网格。这个模型是厂商发布的,尺寸、关节限位、连杆几何都来自官方 URDF 的转换,不是我手工搭的。我在 head_link 上插了一个相机,位置是头部局部坐标 (0.145, 0, 0.03),朝向桌面中心 (1.05, 0, 0.42),用 look-at 矩阵算的四元数,不是随手填的欧拉角。

渲染用 MuJoCo 的离屏模式,OSMesa 后端,图是 240×180。选这个分辨率有两个原因,一是头部相机的实际视场不需要更高,二是 224×224 的输入反正要缩放,原图太大只是浪费渲染时间。

一张图一个样本,标签三个数。杯子在桌面上的 x、y,以及颜色,红是 0 蓝是 1。x 的实际范围 0.86 到 1.34 米,y 是 -0.20 到 0.20 米,归一化到 0 到 1 之间。桌面本身 72 厘米宽、68 厘米深,杯子圆柱半径 4 厘米、高 11 厘米。

两个域。实验室是亮白桌面、灰白墙、冷白主光、环境光 0.35,桌面干净。客厅是深棕桌面、暖黄主光、环境光 0.16,桌上另外摆了三件杂物,一个方盒、一个扁盒、一个圆柱。杯子颜色两个域都随机,红蓝各半。

在这里插入图片描述

图 1,从 Q1 头部相机渲染到板端部署的链路

两个域的参数不是拍脑袋定的。客厅那组按真实客厅的常见特征调,暖光、暗桌、杂物这三样最影响画面。实验室那组故意调得干净一些,因为很多人的训练数据就是在实验室采的,干净过头是常态。

1920 张图,四个 split

数据集一共 1920 张,生成脚本跑了大约 12 分钟,瓶颈在离屏渲染不在磁盘。

split数量域用途
train1200lab阶段 1 全部训练数据
lab_test300lab实验室测试,与训练不同随机种子
home_test300home客厅测试,全程不参与训练
home_ft120home阶段 2 才加入的微调数据

四个 split 用四个不同的随机种子,杯子位置和颜色都重新采样。这一点很关键。如果 home_ft 和 home_test 用同一个种子,两边的杯子位置会高度重叠,微调之后在客厅测出来的数字就是虚的。这是同源测试集最常见的坑,客厅误差从 10 厘米降到 2 厘米,上真机一测还是 8 厘米,因为测试集和训练集根本是同一批位置。

在这里插入图片描述

图 2,实验室与客厅两个域的头部相机画面

上排是实验室,下排是客厅。同一台机器人,同一个相机位姿,同一张桌子,画面差别这么大。实验室那张桌面几乎全白,杯子像浮在白色背景上,桌沿和桌腿的纹理全被曝掉了。客厅那张桌面是深棕色,暖光把杯子的红色压得偏暗,旁边还有三件杂物。

这两排图放在一起看,10.2 厘米的误差就不奇怪了。模型在实验室学到的是白底上一个红色圆柱,到客厅看到的是棕底上一个暗红圆柱,旁边还多了三个它没见过的形状。

第一轮,只喂实验室数据

模型是 torchvision 的 ResNet18,ImageNet 预训练权重。最后一层全连接从 1000 类改成 3 个输出,前两个是归一化的 x、y,第三个是颜色的 logit。

损失函数两项相加。位置用 SmoothL1,颜色用带 logits 的二元交叉熵,颜色项系数 0.5。优化器 AdamW,学习率 1e-4,权重衰减 1e-4,batch 32,训练 8 轮。

为什么要 SmoothL1 不用 MSE。位置回归里偶尔有离群样本,MSE 会把它们放大,SmoothL1 在误差大于 1 之后转成线性,梯度稳一些。这是回归任务的常规选择,不是什么独门技巧。

硬件是 3 核 Xeon Platinum 8582C 的虚拟机,5 GB 内存,没有 GPU。一轮 1200 张图,batch 32,38 个 batch,耗时 68.6 到 73.5 秒。8 轮跑完 9 分半。

训练 loss 从第一轮的 0.0736 掉到 0.0049,第二轮之后基本贴着 0.001 走。曲线在图 4 里,第一轮的尖峰是预训练权重还没适应新任务,属于正常现象。

在这里插入图片描述

图 3,数据集的真实统计与标签样例

跑完在实验室测试集和客厅测试集上各测一遍。

测试集位置 MAE颜色准确率
lab_test 300 张0.95 厘米100%
home_test 300 张10.2 厘米100%

在这里插入图片描述

图 4,两个阶段的训练 loss

0.95 和 10.2,差在哪

先看 0.95 厘米。桌面 72 厘米宽,0.95 厘米大概是杯口半径的四分之一。这个精度做抓取够用,因为夹爪本身有容差。

再看 10.2 厘米。杯子直径 8 厘米,10.2 厘米意味着预测中心落在杯子外面,夹爪伸过去会抓空。这不是精度下降,是功能失效。

颜色准确率两个域都是 100%。这个结果说明模型没有忘记什么是红色什么是蓝色,它坏掉的是位置。位置依赖的是桌面的纹理、光影、杯子和桌面的边界,这些正好是域偏移最敏感的部分。

如果颜色也掉了,那就是模型整体崩了,得回头查训练。颜色没掉,只是位置崩,说明模型学到了颜色这个强特征,但位置的回归依赖了实验室特有的视觉线索。

这里我要说一句,仿真里的域偏移比真机温和。真实客厅还有反光、阴影、相机自动曝光、白平衡漂移,这些在 MuJoCo 里都没有。所以 10.2 厘米这个数,只能当成一个下界看,真机上大概率更差。

第二轮,加 10% 的客厅数据

第二轮的做法很省。从 120 张客厅图里拿全部,混进原来的 1200 张实验室图,一共 1320 张。客厅数据占 9.1%,口头说 10%。

学习率降到 5e-5,比第一轮低一半。原因是模型已经收敛,学习率太大一上来就把实验室上学到的东西冲掉。训练 5 轮,每轮 75.4 到 77.2 秒,比第一轮慢一点,因为数据多了 10%。

loss 从 0.0055 掉到 0.0013。注意这个起点比第一轮末尾的 0.001 还高,因为混进了客厅图,模型一开始对这批新画面不适应,loss 反弹,然后在 5 轮里压下去。

测出来的结果。

阶段训练数据lab_testhome_test颜色
阶段 11200 lab0.95 厘米10.2 厘米100%
阶段 21200 lab + 120 home2.11 厘米2.96 厘米100%

在这里插入图片描述

图 5,真实训练日志里的两阶段指标

客厅误差从 10.2 掉到 2.96,降了 71%。120 张图,按采集成本算,一个人在客厅摆一晚上桌子就能拍完。这笔投入换回来的是一条能用的功能,不是几个点的指标。

但实验室误差从 0.95 涨到 2.11,翻了一倍多。这个代价必须写出来,不能只报好消息。

那 1.16 厘米的退步

为什么加了客厅数据,实验室反而变差。

第一个解释是容量分配。ResNet18 就那么多参数,第二阶段把一部分容量挪去拟合暖光和杂物,实验室那部分拟合就没那么精细了。5 轮训练,学习率 5e-5,所有权重都在动,不是只调最后几层。

第二个解释是数据比例。1320 张里 120 张是客厅,模型看到的分布不再是纯实验室。它学到的位置映射,是两种光照下的折中。折中在客厅是大赚,在实验室是小亏。

第三个解释可以排除,灾难性遗忘。如果发生了灾难性遗忘,颜色准确率会先掉,实验室的位置误差会暴涨到十几厘米。实际是 2.11 厘米,颜色还是 100%,模型没忘,只是偏了。

这个 trade-off 是微调里最常被忽略的一段。很多人只看目标域,客厅从 10.2 降到 2.96 就发版,回头实验室的演示效果变差,找不到原因。验证集必须两头都测,源域和目标域都摆在天平上。

混合比例怎么定。我的做法是从 10% 起步,同时盯两条曲线。如果目标域还差得远,加到 20% 再测一轮,看源域掉多少。如果源域掉超过一倍,就回来。这个点没有公式,只能一轮一轮试,但每一轮都要两个域的数字,不能只测一头。

六张预测图,看出一个系统偏置

在这里插入图片描述

图 6,客厅测试集上的预测与真值

这张图是阶段 2 的模型在客厅测试集上随机抽的 6 张。每张下面两行字,第一行是预测的归一化坐标和颜色,第二行是真值。

六张的颜色全对。位置看细一点。

第一张预测 (0.14, 0.27),真值 (0.09, 0.24)。第二张 (0.67, 0.01) 对 (0.58, 0.09)。第三张 (0.51, 0.51) 对 (0.43, 0.48)。第四张 (0.85, 0.15) 对 (0.73, 0.11)。第五张 (0.49, 0.63) 对 (0.39, 0.52)。第六张 (0.67, 0.79) 对 (0.54, 0.54)。

六张里,除了第二张的 y 偏小,其余两个轴的预测值全部偏大。x 偏 0.05 到 0.13,y 偏 0.03 到 0.25。两轴同向偏大,这不是随机噪声,随机噪声应该有正有负。这更像整体的尺度和零点问题,不是某个方向的阴影能解释的。

系统的偏置通常有具体原因。可能是归一化的区间和真实分布对不上,也可能是回归头在训练后期被少数边缘样本拉偏。具体是哪一种,得再采一批带标注的诊断数据才能定,靠猜没用。

但这个观察有实际价值。MAE 2.96 厘米里,有一部分是固定偏置贡献的。如果做一个简单的标定,比如在客厅采 20 张图,算预测和真值的平均差,把这个差从输出里减掉,MAE 会立刻降一截。这一步不需要重新训练,只需要一个偏置项。

这类事后标定在工业界很常见。模型负责形状,标定负责零点。两者分开,模型换了标定还能用。

导出 ONNX,44.7 MB

训练完把权重存成 cup_resnet18.pt,再导出 ONNX。opset 17,输入名 images,输出名 logits,batch 维设为动态。

导出后先在客厅测试集上复核一遍,位置 MAE 还是 2.96 厘米,颜色 100%。说明导出没有引入误差,这是必须做的一步,不能默认导出就对。

文件 44.70 MB。ResNet18 的参数量大约 11.2 M,fp32 每个参数 4 字节,算下来 44.8 MB,对得上。

延迟测了三组。PyTorch 直接推理,单张 27.21 毫秒。ONNX Runtime CPU 推理,三次测量分别是 13.18、13.43、13.67 毫秒。ORT 比 PyTorch 快一倍,原因是 ORT 做了图优化和算子融合,PyTorch 的动态图在单张推理上有额外开销。

这个数字决定了它能进什么回路。30 Hz 的控制周期是 33 毫秒,13 毫秒的推理占了 40%,再加上图像采集和预处理,余量不多但够用。如果要做 50 Hz,就得降分辨率或者换更小的骨干。

量化,一个反直觉的结果

板端部署通常要做量化。我用 ONNX Runtime 的动态量化,权重量化成 int8,激活在运行时量化。

文件从 44.70 MB 降到 11.23 MB,正好四分之一。体积这一项完全符合预期。

精度从 2.96 厘米掉到 3.32 厘米,掉了 0.36 厘米,颜色还是 100%。这个代价可以接受,0.36 厘米在杯口半径以内。

延迟从 13 毫秒涨到 89.66 到 98.02 毫秒,慢了大约 7 倍。

在这里插入图片描述

图 7,ONNX 导出件的现场体检

这个结果和很多教程说的不一样。教程里 int8 量化通常带来 2 到 4 倍的加速,前提是硬件有 int8 指令集、运行时用得对、模型足够大。

我这颗 CPU 是 Xeon Platinum 8582C,有 AVX512,但这次跑的是一台 3 核虚拟机。小模型、单张、batch 1 这三个条件凑在一起,量化的固定开销就压过了计算节省。动态量化在每次推理时都要对激活做量化、反量化,这些操作在 batch 1 时摊不开。再加上 PyTorch 和 ORT 的 fp32 路径都走了 oneDNN 的优化 GEMM,本来就快,量化版本的部分算子还可能回退到慢路径。

所以结论要分开说。

体积敏感、存储紧张的场合,int8 值得做,44.7 MB 降到 11.2 MB 是实打实的。

延迟敏感的场合,这颗 x86 CPU 上不该做动态量化。要用 int8 就上静态量化,做校准,让激活的量化参数提前定好,或者干脆换支持 int8 加速的推理框架。

板端 ARM 上的结论可能完全不同。RK3588 这类芯片有专门的 NPU,int8 是它的主场,fp32 是弱项。同一份模型换个硬件,量化的账要重算。

这也是我不爱看别人写量化加速多少倍的原因。硬件、batch、模型规模三个变量一变,结论就翻。自己的模型在自己的板子上跑一遍,才有数。

延迟预算怎么留

13 毫秒只是推理。实际链路上还有几段。

图像采集,如果是 RTSP 流,解码一帧 10 到 20 毫秒。预处理,PIL 缩放加归一化,5 到 10 毫秒。推理 13 毫秒。后处理几乎为零,三个数直接读。加起来 30 到 45 毫秒。

30 Hz 的回路是 33 毫秒,这个链路刚好卡在边缘。要想稳,得把采集和推理流水线化,采集第 N 帧的时候推理第 N-1 帧,不要串行等。

还有一点,这些数字是 x86 上测的。Q1 的运控板和大脑板是 ARM,CPU 主频和缓存都弱一些,同一个 ONNX 模型大概要按 3 到 5 倍估。也就是 40 到 65 毫秒。这个量级下 30 Hz 就紧张了,可能要把输入降到 160×120,或者换 MobileNet 这类更轻的骨干。

这些都得在板子上实测,我不做没有依据的推算。

这套实验没证明什么

这一段必须写,不然前面的数字会被误读。

第一,数据是仿真渲染的,不是真机相机采的。MuJoCo 的光照模型是简化版,没有镜头眩光、没有自动曝光、没有运动模糊。真机上这些都会有,域偏移会比 10.2 厘米更严重。

第二,任务只到感知。输出是桌面上杯子的坐标和颜色,没有关节动作,没有抓取,没有闭环。真正抓起来还需要手眼标定、逆解、力控,这些都不在这套实验里。

第三,桌子是固定的。桌面高度、相机位姿、杯子尺寸都是常量。真机上桌子高度会变,机器人会走动,头部会随腰部转动,视角分布比这里宽得多。

第四,样本量小。1920 张图在仿真里是十几分钟的事,真机上采 1920 张对齐样本,可能要几天。

所以说,这套实验证明的是链路和方法,不是真机指标。链路包括数据生成、训练、双域评测、导出、量化、延迟测量,每一段都跑通了,脚本可以复跑。指标只能在同样的仿真条件下比较,不能直接搬到真机验收。

复跑这套实验

数据生成。

python3 scripts/finetune_dataset.py

输出 1920 张图和一份 labels.csv,跑完打印 dataset done {'train': 1200, ...}。

训练加评测。

python3 scripts/finetune_train.py

两个阶段各训一轮,打印 stage1 和 stage2 的 lab、home 指标,最后写 metrics.json,里面含 loss 历史、ONNX 体积、三种延迟、量化前后的精度。

ONNX 现场体检。

python3 scripts/bench_onnx.py

这个脚本不读 metrics.json 里的旧数字,重新加载两个 ONNX 文件,重新算 300 张客厅图的 MAE,重新测延迟。图 7 就是它跑出来的。

三个脚本都在 实操/scripts/ 下,数据、权重、日志、图在 实操/artifacts/finetune/ 下。依赖是 mujoco、torch 2.5.1+cpu、torchvision 0.20.1、onnxruntime 1.31.1、Pillow、matplotlib,CPU 就能跑,不需要显卡。

我踩过的两个坑

第一个,分类损失用错了。一开始颜色用 CrossEntropyLoss,标签是 0 和 1 的整数,报错说期望浮点类型的 target。改成 BCEWithLogitsLoss,输出一个 logit,标签转 float,问题解决。这种错很低级,但会让你怀疑数据。

第二个,MuJoCo 相机一开始装在头 mesh 内部,渲染出来是一片黑,偶尔能看到 mesh 的内壁。原因是相机位置只写了平移,没考虑朝向,默认朝着 +x,而头部网格是封闭的。改成 look-at 矩阵算四元数,位置往外挪 14.5 厘米,画面才正常。

这两个坑都不大,但都要花时间。写出来是想让复跑的人少走一遍。

检查单

阶段检查项这次的取值
采数前相机位姿固定并记录head_link 局部 (0.145, 0, 0.03)
采数前标签定义写死x/y 归一化 + 颜色 0/1
采数时训练与测试不同种子四个 split 四个 seed
采数时目标域留独立测试集home_test 300 张全程不训
训练前先在源域验证链路8 轮,0.95 厘米,颜色 100%
训练中两个域同时评测每阶段都测 lab 和 home
微调时目标域比例记录120/1320,9.1%
导出后复核精度不掉2.96 厘米,与导出前一致
量化后精度和延迟都测3.32 厘米,延迟反而变慢
上板前延迟按硬件重测x86 13 毫秒,ARM 待测

这张表里最后一行只填了 x86 的数字,ARM 板子上的数字我还没有。谁有 Q1 的板子,把 ONNX 拷过去跑一遍,那个数才有意义。

我的判断

第一,域偏移不是模型容量问题。同一个模型,同一批参数,实验室 0.95 厘米,客厅 10.2 厘米。模型没变,变的是画面。所以遇到效果掉,先看部署现场的画面,别急着换大模型。

第二,目标域数据的边际价值极高。120 张图,占训练量不到一成,把客厅误差降了 71%。采集成本是一晚上,收益是一条能用的功能。这笔账怎么算都划算。

第三,源域的退步要认。实验室从 0.95 退到 2.11,这是混合训练的代价,不是 bug。藏着不报,等演示翻车的时候更难看。

第四,量化不要想当然。44.7 MB 降到 11.2 MB 是真的,延迟慢 7 倍也是真的。体积和延迟是两个账本,部署前分别算。

第五,仿真能验证链路,不能验证指标。这套实验最大的价值是把从渲染到 ONNX 的每一段都跑通了,并且给出了可复跑的数字。真机上的数字,得拿真机采的数据重跑一遍。

我那朋友听完,问了一句,那我现在该干嘛。

我说,先去客厅拍 200 张照片,把你家桌子的光照和杂物都拍进去。模型的事,等你有了这 200 张再说。

他笑,说这不就是采数据。

对,微调这件事,八成的功夫在采数据,两成在训练。很多人把这比例反过来了,在次调参调到半夜,回头发现训练画面和部署画面根本不是一回事。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐