YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

Multi-view SVM reward models (scan_object)

A dense process reward for RL fine-tuning, following Learning Process Rewards via Success Visitation Matching (SVM): a discriminator f(s, a) trained to tell decision steps of successful episodes from those of failed ones. Its logit is log f/(1-f), and the dense reward is

r(s, a) = λ · clip(logit, -β, β)      λ = 0.1, β = 20   (+ outcome reward r_out, added by your RL code)

In both models s is the head + left-wrist + right-wrist camera observation plus the robot state. They differ in a:

model a checkpoint
MVTrackSVMReward 3D point track of the next 16 steps (how each head-camera patch's 3D point moves) https://huggingface.co/Mercury007/mv-track-svm-scan-object
MVActionSVMReward 16-step commanded EEF pose chunk (both arms) https://huggingface.co/Mercury007/mv-action-svm-scan-object

Both are trained on the same RoboTwin scan_object (aloha-agilex) rollouts of the GAP policy and score equally well (see below). The action model needs no tracks, so it is the easier one to plug into an RL loop.

Install

pip install -r requirements.txt     # torch, numpy, opencv-python, safetensors, huggingface_hub

DINOv2 (ViT-L/14 with registers) is loaded from torch.hub (facebookresearch/dinov2) the first time; if you already run a GAP policy, pass its DINOv2 model in instead.

Quick start

from svm import MVActionSVMReward, MVTrackSVMReward, svm_reward

scorer = MVActionSVMReward.from_pretrained("Mercury007/mv-action-svm-scan-object")
# or MVTrackSVMReward.from_pretrained("Mercury007/mv-track-svm-scan-object")
# or MVActionSVMReward("/path/to/local/ckpt_dir", dino=my_dinov2_model)

logits = scorer.logits(obs)          # [B]   log f/(1-f)
reward = svm_reward(logits)          # [B]   0.1 * clip(logit, -20, 20)

Score a whole collector HDF5 part (reference usage and a format check):

python examples/score_episode.py --part scan_wdepth-w7-part0004.hdf5 \
    --model action --ckpt Mercury007/mv-action-svm-scan-object
python examples/score_episode.py --part scan_wdepth-w7-part0004.hdf5 \
    --model track --ckpt Mercury007/mv-track-svm-scan-object

Inputs

obs is a dict of numpy arrays, each with a leading batch dim B (one entry per decision step). For cam in head, left_wrist, right_wrist:

key shape content
{cam}_rgb [B, 240, 320, 3] uint8 RGB image
{cam}_depth [B, 240, 320] float depth in millimetres, 0 where nothing is rendered
{cam}_K [B, 3, 3] pinhole intrinsic
{cam}_E [B, 3, 4] extrinsic world → camera, OpenCV convention (x_cam = R x_world + t)
state [B, 16] [L xyz, L quat wxyz, L gripper, R xyz, R quat wxyz, R gripper], world frame (RoboTwin endpose)
action [B, 16, 16] action model: the 16 commanded poses of the chunk, each in the state format (the collector's action)
track [B, 391, 16, 3] track model: displacement (m, world frame) of each head patch's 3D point from frame t to t+h+1, h = 0..15
track_valid [B, 391] bool track model: patches whose point is tracked (depth-valid and on a scene object)

Optional head_dino=[B, 391, 1024] reuses head DINOv2 patch features the policy already computed.

Patch grid. 391 = 17 × 23 DINOv2 patches: the 240×320 image is resized (bilinear) to 238×322 and cut into 14-px patches, row-major. Patch i of the track corresponds to DINOv2 token i of the head image; its 3D anchor is the head depth at the patch centre (svm/preprocess.py: PATCH_PX) unprojected to the world. The same is done for each wrist camera with its own depth, giving every wrist patch a world-frame 3D position for the RoPE.

One sample = one policy decision. GAP executes all 16 actions of a chunk before observing again, so the model scores the observation at the first frame of each chunk (frames 0, 16, 32, … of an episode) together with the 16-step action chunk / track of that chunk, and you get one reward per chunk. If an episode ends mid-chunk, the chunk holds the last action and the track the last frame's positions for the remaining steps.

Tracks. Training used simulator ground-truth tracks of the executed chunk (object poses replayed in SAPIEN; robot and background points are invalid). Feeding predicted tracks (e.g. GAP's track head) is a distribution shift: check that the score separates success/failure on your data before relying on it.

Normalization (state / action min/max → [-1, 1], tracks / RMS) is stored in config.json and applied inside MVTrackSVMReward; pass everything raw.

Model

svm/model.py, 6 pre-norm layers, d_model 1024, 8 heads; MultiViewTrackSVM 90.7 M parameters, MultiViewActionSVM 91.3 M.

self-attention   track:  [CLS] | state L, R | head DINO ×391 | track ×391         (785 tokens)
                 action: [CLS] | state L, R | action L, R | head DINO ×391        (396 tokens)
cross-attention  left-wrist DINO ×391 | right-wrist DINO ×391 (memory, K/V only)  (782 tokens)
readout          CLS → LayerNorm → MLP(256, 256) → logit
  • Action token (one per arm) = MLP(flatten(16×8 normalized poses)) + arm embedding, at the 3D coordinate of that arm's first commanded xyz.
  • Track token = MLP(flatten(16×3 track)) + learned type embedding (+ invalid embedding where track_valid is false), sitting at the same grid position / 3D coordinate as its head patch.
  • Every token carries a 3D RoPE over world-frame metres (scale 1 cm ≈ 1 rad): state tokens at their arm's EEF xyz, action tokens at their first commanded xyz, head DINO and track tokens at the head patch centre, wrist patches at their unprojected world xyz. In cross-attention q and k are rotated by their own coordinates, so attention depends on the 3D offset between a head-side token and a wrist patch. CLS and depth-invalid patches are not rotated.
  • Plus a 2D sin-cos patch-grid encoding (and a per-wrist view embedding on the memory) added to Q/K.

Training data and results

  • 504 success + 504 failure GAP rollouts of scan_object (step limit 300), with wrist depth recorded; failure episodes capped at their first 250 frames.
  • Split per class: 454 train, last 50 seeds held out (used for both checkpoint selection and the numbers below, so they are mildly optimistic).
  • BCE, AdamW 1e-4, wd 1e-6, batch 64 balanced, bf16; selected by val loss (track at epoch 1, action at epoch 0; both overfit fast after that).
held-out metric (50 + 50 episodes) track action
per-step AUC (success vs failure steps) 0.923 0.917
episode ranking accuracy 0.835 0.814
first-step AUC 0.63 0.61
mean reward, success / failure episodes +0.227 / −0.407 +0.141 / −0.314
fraction of steps hitting the ±β clip 0.0 0.0

Known limitations:

  • The first decision step barely separates the classes since nothing has happened yet; and some failure episodes are scored like successes throughout (e.g. held-out failure seed 870187: track logit > 2 for 17 of its 19 steps; the action model also scores it positive, peaking at 2.4, but drops toward 0 at the end).
  • The action model barely reads its action input. The training rollouts come from one near-deterministic policy, so each state comes with essentially one action and f(s, a) cannot learn action dependence: across 32 GAP candidate chunks for the same observation the logit varies by ~0.004, and 10 cm offsets or chunks from other states move it by ~0.03, vs ~3 across states. Treat it as a state(-visitation) value: fine as a dense RL reward, useless for ranking candidate actions at test time. The track model likewise uses the track mostly as a motion-magnitude cue.

Files

svm/model.py          MultiViewTrackSVM, MultiViewActionSVM (networks only)
svm/rope3d.py         3D RoPE
svm/preprocess.py     patch centres from depth, DINOv2 features, normalization, batch assembly
svm/reward_model.py   MVTrackSVMReward, MVActionSVMReward: load checkpoint, obs -> logits / rewards
examples/score_episode.py

Each Hugging Face repo holds model.safetensors (weights) and config.json (model hyper-parameters, normalizer statistics, training metadata).

Downloads last month
6
Safetensors
Model size
91.3M params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support