pick_tube_05_tactile_decoder_deploy / docs /NETWORK_STRUCTURE.md
wjstx's picture
Upload folder using huggingface_hub
adf7c6c verified
|
Raw History Blame Contribute Delete
17.6 kB
# 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`。当前离线结果只证明数值误差,不证明真实机器人在线链路或成功率。