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
Download docs/NETWORK_STRUCTURE.md from wjstx/pick_tube_05_tactile_decoder_deploy: direct link, hf CLI and curl.
- Browser
- Download file 17.6 kB
-
https://huggingface.co/wjstx/pick_tube_05_tactile_decoder_deploy/resolve/main/docs/NETWORK_STRUCTURE.md
- Command line
-
hf download hf://wjstx/pick_tube_05_tactile_decoder_deploy/docs/NETWORK_STRUCTURE.md
-
curl -L -o NETWORK_STRUCTURE.md https://huggingface.co/wjstx/pick_tube_05_tactile_decoder_deploy/resolve/main/docs/NETWORK_STRUCTURE.md
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 节 | 可训练 | 是 |
该实验不是端到端联合训练,而是两个阶段:
- 离线运行冻结 producer,生成不可变数值 cache。
- 只读取 cache,训练小型动作 decoder。
训练程序接收冻结资产路径是为了验证 SHA256 provenance;它不会在训练 loop 中再次加载或运行 SmolVLA/触觉 encoder。
3. 整体数据流
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_01adapter 有效配置) - 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 实验中以上所有模块均冻结
加载后执行:
PeftModel.from_pretrained(..., is_trainable=False)
for parameter in policy.parameters():
parameter.requires_grad_(False)
policy.eval()
SmolVLA 前向还在 torch.inference_mode() 下运行。
5.3 粗动作生成
每个 anchor 使用固定噪声:
seed = 0
external noise = [B,20,20]
internal padding = [B,20,32]
flow steps = 10
内部动作最大维度为 32,最终只保留前 20 个有效动作维度:
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 四路输入顺序
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:
- 转为 NCHW float32。
- 保持宽高比缩放到不超过
224x224。 - 缩小时使用 area,放大时使用 bilinear。
- 中心补零到
224x224。 - 除以 255,得到
[0,1]。 - 不使用 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:
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 编码
[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
输入:
coarse_action [B,20,20]
共享线性投影并加入可学习时间位置:
[ q_t=W_a a_t+b_a+p_t, ]
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
输入:
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, ]
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 配置:
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。
每层:
动作 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]
两层后:
[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 的目标:
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 优化配置
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:
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. 前向伪代码
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. 部署/在线推理边界
部署时需要重新组合三个模块:
当前 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。当前离线结果只证明数值误差,不证明真实机器人在线链路或成功率。