File size: 17,574 Bytes
adf7c6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
# Direct Tactile Action Decoder:完整网络结构与实验协议

本文档描述 VB3 当前 `DirectTactileActionDecoder` 实验的**实际代码结构**,用于模型复现、论文讨论和跨团队交接。

## 1. 一句话概述

该系统先用冻结的 SmolVLA 离线生成一个归一化粗动作块,用冻结的触觉 ResNet18 离线生成四个当前帧触觉 token,再训练一个 471,828 参数的两层 Transformer Decoder,直接重新生成完整动作块:

\[
f_\theta:
\left(
A_{\text{SmolVLA}}\in\mathbb{R}^{20\times20},
T\in\mathbb{R}^{4\times512}
\right)
\rightarrow
\hat A\in\mathbb{R}^{20\times20}.
\]

输出是完整新动作,不是 `coarse_action + residual`。

## 2. 关键实验边界

| 模块 | 资产 | 状态 | Decoder 训练时是否运行 |
|---|---|---:|---:|
| SmolVLA base | `lerobot/smolvla_base`,revision `c83c3163...` | 冻结 | 否 |
| SmolVLA PEFT adapter | `KaiyueChen/pick_tube_01`,revision `c2bb4296...` | 冻结 | 否 |
| 触觉 encoder | `KaiyueChen/encoder_ckpt_0809` 转换后的 ResNet18 | 冻结、永久 eval | 否 |
| DirectTactileActionDecoder | 本文第 8 节 | 可训练 | 是 |

该实验不是端到端联合训练,而是两个阶段:

1. 离线运行冻结 producer,生成不可变数值 cache。
2. 只读取 cache,训练小型动作 decoder。

训练程序接收冻结资产路径是为了验证 SHA256 provenance;它不会在训练 loop 中再次加载或运行 SmolVLA/触觉 encoder。

## 3. 整体数据流

```mermaid
flowchart TB
    subgraph A[阶段 A:离线冻结特征生成]
        RGB[两路 RGB + 20D state + task] --> PRE[SmolVLA saved preprocessor]
        PRE --> SVLA[冻结 SmolVLA base + pick_tube_01 PEFT]
        NOISE[固定 seed=0 flow noise] --> SVLA
        SVLA --> COARSE[coarse_action\nB x 20 x 20\n归一化空间]

        TOUCH[当前帧四路触觉 RGB] --> TPRE[uint8预处理\n224x224 / 255]
        TPRE --> R18[共享冻结 ResNet18]
        R18 --> TRMS[per-sensor RMS normalize]
        TRMS --> TEMB[tactile_embedding\nB x 4 x 512]

        EXPERT[Pick_tube_05 专家动作] --> NORM[adapter action mean/std]
        NORM --> TARGET[expert_action B x 20 x 20\naction_pad_mask B x 20]
    end

    subgraph B[阶段 B:只训练 Decoder]
        COARSE --> APATH[action_in 20->128\n+ action position]
        TEMB --> TPATH[RMS normalize\ntactile_in 512->128\n+ sensor identity]
        APATH --> TD[2-layer Transformer Decoder]
        TPATH -->|cross-attention memory| TD
        TD --> OUT[LayerNorm + Linear 128->20]
        OUT --> PRED[prediction B x 20 x 20]
        PRED --> LOSS[Masked Smooth L1]
        TARGET --> LOSS
    end
```

## 4. 离线 cache 的每行内容

| 字段 | dtype | shape | 含义 |
|---|---|---:|---|
| `episode_index` | int64 | `[]` | 原始 episode ID |
| `frame_index` | int64 | `[]` | episode 内 anchor 帧 |
| `split_id` | int64 | `[]` | train=0,val=1,test=2 |
| `coarse_action` | float32 | `[20,20]` | 冻结 SmolVLA 的归一化动作块 |
| `tactile_embedding` | float32 | `[4,512]` | 四路当前帧触觉特征 |
| `expert_action` | float32 | `[20,20]` | 归一化专家动作块 |
| `action_pad_mask` | bool | `[20]` | 哪些未来时间步有效 |

Cache 不包含原始 RGB、原始触觉图像、state、task token、SmolVLA hidden state 或 ResNet feature map。

## 5. 冻结 SmolVLA 粗动作 producer

### 5.1 有效输入

每个 anchor 使用一个当前观测步:

| 输入 | shape |
|---|---:|
| 两路 RGB | 每路 `[3,224,224]`,processor 后进入有效 camera1/camera2 |
| `observation.state` | `[20]` |
| task | 字符串,token 长度固定为 48 |

原始 `camera0/camera1` 由 adapter processor 映射为 SmolVLA 的有效 `camera1/camera2`。触觉图像绝不进入 SmolVLA preprocessor。

运行时强制使用实际 20D state、20D action、两路相机和 `empty_cameras=0`;不能直接把 adapter `config.json` 中陈旧的 6D/额外相机 metadata 当成有效输入契约。

### 5.2 模型与 PEFT

- VLM:`HuggingFaceTB/SmolVLM2-500M-Video-Instruct`
- VLM layers:16
- Action expert layers:16(来自 `pick_tube_01` adapter 有效配置)
- PEFT:LoRA,`r=16`,`alpha=16`,dropout=0
- LoRA target:VLM text layers self-attention 的 `q_proj` 和 `v_proj`
- 额外保存/训练过的 adapter 模块:language expert、state/action projection 和 action-time MLP
- 当前 decoder 实验中以上所有模块均冻结

加载后执行:

```python
PeftModel.from_pretrained(..., is_trainable=False)
for parameter in policy.parameters():
    parameter.requires_grad_(False)
policy.eval()
```

SmolVLA 前向还在 `torch.inference_mode()` 下运行。

### 5.3 粗动作生成

每个 anchor 使用固定噪声:

```text
seed             = 0
external noise   = [B,20,20]
internal padding = [B,20,32]
flow steps       = 10
```

内部动作最大维度为 32,最终只保留前 20 个有效动作维度:

```text
frozen SmolVLA output [B,20,32]
              unpad ↓
coarse_action        [B,20,20]
```

`coarse_action` 没有经过 postprocessor,仍处于 adapter 保存的 mean/std 归一化空间。

## 6. 冻结触觉 encoder

当前 direct-decoder 实验使用的是 `encoder_ckpt_0809` 中的 Flax ResNet18 转换结果,不是 AnyTouch2,也不是两帧触觉 VideoMAE。

### 6.1 四路输入顺序

```text
0. observation.images.tactile_left_0
1. observation.images.tactile_right_0
2. observation.images.tactile_left_1
3. observation.images.tactile_right_1
```

四路都是当前帧;该实现不使用首帧背景差分和触觉历史。

### 6.2 图像预处理

输入必须是 uint8 RGB,支持 NHWC 或 NCHW:

1. 转为 NCHW float32。
2. 保持宽高比缩放到不超过 `224x224`。
3. 缩小时使用 area,放大时使用 bilinear。
4. 中心补零到 `224x224`。
5. 除以 255,得到 `[0,1]`。
6. 不使用 ImageNet mean/std。

原始 Pick_tube_05 触觉图像已经是 `224x224`,因此实际主要执行通道转换和 `/255`。

### 6.3 ResNet18 逐层结构

所有卷积使用与 Flax/TensorFlow 一致的动态 SAME padding。

| 阶段 | 结构 | 输出 shape |
|---|---|---:|
| 输入 | RGB float32 | `[N,3,224,224]` |
| Stem | Conv `7x7`, 3→64, stride 2;BN;ReLU | `[N,64,112,112]` |
| Pool | MaxPool `3x3`, stride 2 | `[N,64,56,56]` |
| Layer 1 | 2×BasicBlock,64→64 | `[N,64,56,56]` |
| Layer 2 | 2×BasicBlock,首块 stride 2,64→128 | `[N,128,28,28]` |
| Layer 3 | 2×BasicBlock,首块 stride 2,128→256 | `[N,256,14,14]` |
| Layer 4 | 2×BasicBlock,首块 stride 2,256→512 | `[N,512,7,7]` |
| 输出 | 空间维度 global average | `[N,512]` |

BasicBlock:

```text
input ───────────────────────────────┐
  │                                  │ identity / 1x1 Conv+BN
  └→ Conv3x3 → BN → ReLU → Conv3x3 → BN
                                      +
                                      ↓
                                    ReLU
```

Layer 2–4 的首个 block 使用 stride-2 的 `1x1 Conv + BN` residual projection。模型没有分类 FC 层。

### 6.4 四路 batch 编码

```text
[B,4,3,224,224]
       flatten sensor dimension
[B*4,3,224,224]
       shared frozen ResNet18
[B*4,512]
       RMS normalize
[B*4,512]
       reshape
[B,4,512]
```

RMS normalization:

\[
z=\frac{f}{\sqrt{\operatorname{mean}(f^2)+10^{-6}}}.
\]

四路在 encoder 内互不交互;它们只在后续 Transformer cross-attention 中融合。

### 6.5 冻结与参数量

| 模块 | 参数量 |
|---|---:|
| conv1 | 9,408 |
| bn1 | 128 |
| layer1 | 147,968 |
| layer2 | 525,568 |
| layer3 | 2,099,712 |
| layer4 | 8,393,728 |
| **总参数** | **11,176,512** |
| **可训练参数** | **0** |

Encoder 构造时就执行 `eval()` 和 `requires_grad_(False)`,并重写 `train()`,所以外部调用 `train(True)` 也不会更新 BatchNorm 统计量。

## 7. Decoder 输入 token 构造

固定配置:

| 字段 | 数值 |
|---|---:|
| chunk/action horizon | 20 |
| action dimension | 20 |
| tactile token count | 4 |
| tactile embedding dimension | 512 |
| `d_model` | 128 |
| attention heads | 4 |
| head dimension | 32 |
| decoder layers | 2 |
| FFN hidden dimension | 256 |
| dropout | 0.1 |

### 7.1 动作 query token

输入:

```text
coarse_action [B,20,20]
```

共享线性投影并加入可学习时间位置:

\[
q_t=W_a a_t+b_a+p_t,
\]

```text
Linear(20→128)          [B,20,128]
action_position         [20,128]
动作 query token         [B,20,128]
```

`action_position` 用 `Normal(std=0.02)` 初始化。

### 7.2 触觉 memory token

输入:

```text
tactile_embedding [B,4,512]
```

Decoder 会对 cache embedding 再做一次防御性 per-token RMS normalization,然后使用一个共享 Linear:

\[
m_i=W_t\operatorname{RMSNorm}(t_i)+b_t+s_i,
\]

```text
RMS normalize            [B,4,512]
Linear(512→128)          [B,4,128]
sensor_identity          [4,128]
触觉 memory token         [B,4,128]
```

这里没有额外两层 MLP、激活函数或 tactile positional encoding。`sensor_identity` 表示四个物理传感器身份,用 `Normal(std=0.02)` 初始化。

触觉 token 不与动作 token 直接拼接,而是作为 Transformer Decoder 的 `memory`。

## 8. 两层 Transformer Decoder

PyTorch 配置:

```python
nn.TransformerDecoderLayer(
    d_model=128,
    nhead=4,
    dim_feedforward=256,
    dropout=0.1,
    activation="relu",
    batch_first=True,
    norm_first=True,
)
```

未传入 causal mask、target padding mask 或 memory mask。因此:

- 20 个动作 token 之间是非因果全连接 self-attention。
- 每个动作 token 都能 cross-attend 全部四个触觉 token。

每层:

```text
动作 token [B,20,128]
   ↓ Pre-Norm self-attention:20个动作时间步相互建模
   ↓ residual
   ↓ Pre-Norm cross-attention:Q来自动作,K/V来自4个触觉memory
   ↓ residual
   ↓ Pre-Norm FFN:Linear 128→256 → ReLU → Dropout → Linear 256→128
   ↓ residual
输出 [B,20,128]
```

两层后:

```text
[B,20,128]
    ↓ LayerNorm(128)
    ↓ Linear(128→20)
[B,20,20]
```

输出层没有激活函数。输出本身就是最终归一化动作块。

## 9. Decoder 精确参数量

| 模块 | 参数 shape / 映射 | 参数量 |
|---|---|---:|
| `action_in` | Linear 20→128 | 2,688 |
| `action_position` | `[20,128]` | 2,560 |
| `tactile_in` | Linear 512→128 | 65,664 |
| `sensor_identity` | `[4,128]` | 512 |
| Transformer layer 0 | self-attn + cross-attn + FFN + 3 norms | 198,784 |
| Transformer layer 1 | self-attn + cross-attn + FFN + 3 norms | 198,784 |
| `action_out` | LayerNorm + Linear 128→20 | 2,836 |
| **总计** |  | **471,828** |

每个 Transformer layer:

| 子模块 | 参数量 |
|---|---:|
| action self-attention | 66,048 |
| tactile cross-attention | 66,048 |
| FFN | 65,920 |
| 3×LayerNorm | 768 |
| **每层总计** | **198,784** |

两层 Transformer 合计 397,568 参数。

## 10. Action+tactile 与 Action-only

两个模式使用相同类、相同名义参数量,但分别训练独立 checkpoint。

| 项目 | Action+tactile | Action-only |
|---|---|---|
| coarse action | 使用 | 使用 |
| 真实 tactile embedding | 使用 | 忽略 |
| memory 输入 | 当前样本 `[B,4,512]` | 内部全零 `[B,4,512]` |
| projection 后 memory | `Linear(RMS(tactile))+sensor_id` | `Linear(0)+sensor_id` |
| memory 含义 | 样本相关触觉 token | 四个可学习常量 token |
| 参数量 | 471,828 | 471,828 |

Action-only 中:

\[
m_i=b_t+s_i.
\]

`tactile_in.weight` 的输入恒为零,因此对 forward 没有数据相关贡献;`tactile_in.bias` 和 `sensor_identity` 仍会通过 cross-attention 学习。所有名义参数都被放入 AdamW,但不应声称 action-only 中每个参数都收到非零数据梯度。

## 11. 训练目标

### 11.1 Episode-level split

| split | episodes | anchors | 用途 |
|---|---:|---:|---|
| train | 280 | 106,802 | 更新 decoder |
| validation | 35 | 13,294 | 选择 `best.pt` |
| test | 35 | 13,264 | 最终一次报告 |

同一 episode 不跨 split。

### 11.2 Expert chunk 与 mask

每个 anchor 的目标:

```text
expert_action   [B,20,20]
valid mask      [B,20]
```

规则:

- chunk 不跨 episode。
- episode 尾部越界位置填零并 mask 掉。
- 每个 episode 最后一帧全零 terminal action 被 mask 掉。
- terminal 行自身没有有效第一步,因此不作为 cache anchor。
- target 使用与冻结 adapter 相同的 action mean/std 归一化。

### 11.3 Loss

预测:

\[
\hat A=f_\theta(A_{\text{coarse}},T).
\]

损失是归一化空间的 masked Smooth L1(PyTorch 默认 `beta=1`):

\[
L=\frac{\sum_{b,t,d}M_{b,t}\,\rho(\hat A_{b,t,d}-A^{expert}_{b,t,d})}{20\sum_{b,t}M_{b,t}},
\]

\[
\rho(x)=
\begin{cases}
\frac{1}{2}x^2,&|x|<1\\
|x|-\frac{1}{2},&|x|\ge1.
\end{cases}
\]

该 loss 监督完整动作,不监督 residual。

### 11.4 优化配置

```text
seed                  = 0
global batch size     = 256
epochs                = 50
optimizer             = AdamW
learning rate         = 3e-4
weight decay          = 1e-4
AdamW betas           = (0.9, 0.999), PyTorch default
AdamW epsilon         = 1e-8, PyTorch default
scheduler             = none
gradient clipping     = none
explicit AMP          = none
```

Train loader shuffle;validation loader 不 shuffle。训练时 dropout 开启,验证时 decoder `eval()`、dropout 关闭。

每个 epoch 计算 validation masked Smooth L1;严格更低时复制参数。最后保存 validation loss 最低的 `best.pt`,并在 CPU 上严格 reload 检查 `[1,20,20]` finite 输出。

实际 checkpoint:

| 模式 | best epoch | best validation loss |
|---|---:|---:|
| Action-only | 30 | 0.0735953188 |
| Action+tactile | 3 | 0.0762285802 |

## 12. 四条件最终测试

所有条件使用同一 held-out test split、相同有序 key、coarse action、expert target 和 mask:

```text
Frozen SmolVLA:
    prediction = coarse_action

Action-only:
    action_only_checkpoint(coarse_action, constant memory)

Action+tactile:
    action_tactile_checkpoint(coarse_action, synchronized tactile)

Shuffled-tactile:
    同一个 action_tactile checkpoint
    coarse/target/mask 保持不变
    tactile 替换成其他 episode 的 embedding
```

Shuffled-tactile 不是第四个训练模型,而是 action+tactile checkpoint 的错误输入条件。固定 seed=0,并保证 donor episode 与目标 episode 不同。

离线评估对完整有效 20-step chunk 计分,不截断到配置中的 `execute_steps=10`。

当前正式 test 结果:

| Condition | Normalized Smooth L1 | Physical MAE | Physical RMSE | MAE - baseline | Improved chunks |
|---|---:|---:|---:|---:|---:|
| Frozen SmolVLA | 0.11852655 | 0.000834609 | 0.001771636 | 0 | 0% |
| Action-only | **0.07258066** | **0.000636407** | **0.001334380** | **-0.000198201** | **91.2922%** |
| Action+tactile | 0.07536559 | 0.000685921 | 0.001395565 | -0.000148688 | 83.0066% |
| Shuffled-tactile | 0.09412303 | 0.000893633 | 0.001818145 | +0.000059024 | 42.4608% |

这些是离线动作误差,不等于真实机器人成功率。

## 13. 前向伪代码

```python
class DirectTactileActionDecoder(nn.Module):
    def __init__(self):
        self.action_in = nn.Linear(20, 128)
        self.action_position = nn.Parameter(torch.randn(20, 128) * 0.02)

        self.tactile_in = nn.Linear(512, 128)
        self.sensor_identity = nn.Parameter(torch.randn(4, 128) * 0.02)

        layer = nn.TransformerDecoderLayer(
            d_model=128,
            nhead=4,
            dim_feedforward=256,
            dropout=0.1,
            activation="relu",
            batch_first=True,
            norm_first=True,
        )
        self.decoder = nn.TransformerDecoder(layer, num_layers=2)
        self.action_out = nn.Sequential(
            nn.LayerNorm(128),
            nn.Linear(128, 20),
        )

    def forward(self, coarse_action, tactile_embedding, mode):
        # coarse_action: [B,20,20]
        action_tokens = self.action_in(coarse_action) + self.action_position

        if mode == "action_tactile":
            # tactile_embedding: [B,4,512]
            tactile = tactile_embedding.float()
            rms = tactile.square().mean(-1, keepdim=True).sqrt()
            tactile = tactile / rms.clamp_min(torch.finfo(torch.float32).eps)
        elif mode == "action_only":
            tactile = torch.zeros(
                coarse_action.shape[0], 4, 512,
                device=coarse_action.device,
                dtype=torch.float32,
            )
        else:
            raise ValueError(mode)

        tactile_tokens = self.tactile_in(tactile) + self.sensor_identity
        decoded = self.decoder(
            tgt=action_tokens,       # [B,20,128]
            memory=tactile_tokens,  # [B,4,128]
        )
        prediction = self.action_out(decoded)  # [B,20,20]
        return prediction
```

## 14. 部署/在线推理边界

部署时需要重新组合三个模块:

```text
当前 RGB/state/task
    → frozen SmolVLA
    → normalized coarse action [B,20,20]

当前四路触觉
    → frozen ResNet18
    → tactile embedding [B,4,512]

coarse + tactile
    → trained decoder best.pt
    → fine normalized action [B,20,20]

fine normalized action
    → adapter postprocessor
    → physical action [B,20,20]
```

配置计划执行前 10 步:预测 horizon 为 20,`execute_steps=10`。当前离线结果只证明数值误差,不证明真实机器人在线链路或成功率。