Instructions to use wjstx/pick_tube_05_tactile_decoder_deploy with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use wjstx/pick_tube_05_tactile_decoder_deploy with LeRobot:
# See https://github.com/huggingface/lerobot?tab=readme-ov-file#installation for more details git clone https://github.com/huggingface/lerobot.git cd lerobot pip install -e .[smolvla]
# Launch finetuning on your dataset python lerobot/scripts/train.py \ --policy.path=wjstx/pick_tube_05_tactile_decoder_deploy \ --dataset.repo_id=lerobot/svla_so101_pickplace \ --batch_size=64 \ --steps=20000 \ --output_dir=outputs/train/my_smolvla \ --job_name=my_smolvla_training \ --policy.device=cuda \ --wandb.enable=true
# Run the policy using the record function python -m lerobot.record \ --robot.type=so101_follower \ --robot.port=/dev/ttyACM0 \ # <- Use your port --robot.id=my_blue_follower_arm \ # <- Use your robot id --robot.cameras="{ front: {type: opencv, index_or_path: 8, width: 640, height: 480, fps: 30}}" \ # <- Use your cameras --dataset.single_task="Grasp a lego block and put it in the bin." \ # <- Use the same task description you used in your dataset recording --dataset.repo_id=HF_USER/dataset_name \ # <- This will be the dataset name on HF Hub --dataset.episode_time_s=50 \ --dataset.num_episodes=10 \ --policy.path=wjstx/pick_tube_05_tactile_decoder_deploy - Notebooks
- Google Colab
- Kaggle
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`。当前离线结果只证明数值误差,不证明真实机器人在线链路或成功率。
|