anhtld commited on
Commit
d8ce858
·
verified ·
1 Parent(s): b3666b2

Auto-sync: 2026-06-28 07:13:10 (part 2)

Browse files
scripts/export_retrieval_residual_policy_targets.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from collections import Counter, defaultdict
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+
13
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
14
+ if str(PROJECT_ROOT) not in sys.path:
15
+ sys.path.insert(0, str(PROJECT_ROOT))
16
+
17
+ from dovla_cil.data.datasets import CILDataset # noqa: E402
18
+ from dovla_cil.eval.lattice_eval import _validation_group_ids # noqa: E402
19
+ from dovla_cil.eval.maniskill_policy_rollout import ( # noqa: E402
20
+ _nearest_retrieval_entries,
21
+ _numeric_action_values,
22
+ _select_action_chunk,
23
+ )
24
+ from dovla_cil.models.dovla import ( # noqa: E402
25
+ DoVLAConfig,
26
+ DoVLAModel,
27
+ load_model_state,
28
+ vectorize_toy_observation,
29
+ )
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ parser = argparse.ArgumentParser(
34
+ description=(
35
+ "Export continuous BC targets from train-state counterfactual residual "
36
+ "retrieval scored by a trained DoVLA field."
37
+ )
38
+ )
39
+ parser.add_argument("--checkpoint", type=Path, required=True)
40
+ parser.add_argument("--dataset", type=Path, required=True)
41
+ parser.add_argument("--out", type=Path, required=True)
42
+ parser.add_argument("--device", default="auto")
43
+ parser.add_argument("--split", choices=("train", "val", "all"), default="all")
44
+ parser.add_argument("--retrieval-neighbors", type=int, default=1)
45
+ parser.add_argument("--retrieval-metric", choices=("raw", "zscore"), default="raw")
46
+ parser.add_argument("--retrieval-residual-scale", type=float, default=0.35)
47
+ parser.add_argument(
48
+ "--exclude-types",
49
+ default="residual_random_negative,residual_wrong_direction,residual_near_miss",
50
+ help="Comma-separated residual candidate types to mask before field selection.",
51
+ )
52
+ parser.add_argument(
53
+ "--no-leave-one-out",
54
+ action="store_true",
55
+ help="Allow a train target to retrieve residuals from its own source group.",
56
+ )
57
+ parser.add_argument("--max-groups", type=int, default=None)
58
+ parser.add_argument("--clip-action-low", type=float, default=-1.0)
59
+ parser.add_argument("--clip-action-high", type=float, default=1.0)
60
+ parser.add_argument("--no-clip-actions", action="store_true")
61
+ args = parser.parse_args(argv)
62
+
63
+ if args.retrieval_neighbors <= 0:
64
+ parser.error("--retrieval-neighbors must be positive")
65
+ if args.retrieval_residual_scale < 0:
66
+ parser.error("--retrieval-residual-scale must be non-negative")
67
+ if args.max_groups is not None and args.max_groups <= 0:
68
+ parser.error("--max-groups must be positive when provided")
69
+ if args.clip_action_low >= args.clip_action_high:
70
+ parser.error("--clip-action-low must be smaller than --clip-action-high")
71
+
72
+ try:
73
+ import torch
74
+ except ImportError as exc: # pragma: no cover
75
+ raise ImportError("export_retrieval_residual_policy_targets.py requires torch") from exc
76
+
77
+ device = _resolve_device(args.device)
78
+ checkpoint = torch.load(args.checkpoint, map_location=device, weights_only=False)
79
+ model_config = DoVLAConfig(**checkpoint["model_config"])
80
+ if model_config.observation_mode != "state":
81
+ raise ValueError("retrieval-residual target export currently supports state observations")
82
+ model = DoVLAModel(model_config).to(device)
83
+ load_model_state(model, checkpoint)
84
+ model.eval()
85
+
86
+ dataset = CILDataset(args.dataset)
87
+ trainer_config = checkpoint.get("trainer_config", {})
88
+ val_ids = set(
89
+ _validation_group_ids(
90
+ dataset.group_ids,
91
+ val_fraction=float(trainer_config.get("val_fraction", 0.2)),
92
+ seed=int(trainer_config.get("seed", 0)),
93
+ )
94
+ )
95
+ train_ids = [group_id for group_id in dataset.group_ids if group_id not in val_ids]
96
+ if args.split == "train":
97
+ target_group_ids = list(train_ids)
98
+ elif args.split == "val":
99
+ target_group_ids = [group_id for group_id in dataset.group_ids if group_id in val_ids]
100
+ else:
101
+ target_group_ids = list(dataset.group_ids)
102
+ if args.max_groups is not None:
103
+ target_group_ids = target_group_ids[: args.max_groups]
104
+
105
+ bank = _build_residual_bank(
106
+ dataset,
107
+ train_ids,
108
+ obs_dim=model_config.obs_dim,
109
+ )
110
+ excluded = {item.strip() for item in args.exclude_types.split(",") if item.strip()}
111
+ action_low = action_high = None
112
+ if not args.no_clip_actions:
113
+ action_low = torch.full(
114
+ (1, 1, model_config.action_dim),
115
+ float(args.clip_action_low),
116
+ dtype=torch.float32,
117
+ device=device,
118
+ )
119
+ action_high = torch.full(
120
+ (1, 1, model_config.action_dim),
121
+ float(args.clip_action_high),
122
+ dtype=torch.float32,
123
+ device=device,
124
+ )
125
+
126
+ targets: dict[str, dict[str, Any]] = {}
127
+ counts: Counter[str] = Counter()
128
+ source_counts: Counter[str] = Counter()
129
+ skipped: dict[str, str] = {}
130
+ with torch.no_grad():
131
+ for group_id in target_group_ids:
132
+ records = dataset.get_group(group_id)
133
+ if not records:
134
+ skipped[group_id] = "empty_group"
135
+ continue
136
+ task_ids = {record.task_id for record in records}
137
+ if len(task_ids) != 1:
138
+ skipped[group_id] = "multi_task_group"
139
+ continue
140
+ task_id = next(iter(task_ids))
141
+ entries = bank.get(task_id, [])
142
+ if args.no_leave_one_out:
143
+ candidates = entries
144
+ else:
145
+ candidates = [entry for entry in entries if entry[0] != group_id]
146
+ if not candidates:
147
+ candidates = entries
148
+ if not candidates:
149
+ skipped[group_id] = "no_train_bank_for_task"
150
+ continue
151
+
152
+ query = np.asarray(
153
+ vectorize_toy_observation(
154
+ records[0].observation_inline or {},
155
+ obs_dim=model_config.obs_dim,
156
+ ),
157
+ dtype=np.float32,
158
+ )
159
+ nearest = _nearest_retrieval_entries(
160
+ candidates,
161
+ query,
162
+ retrieval_neighbors=args.retrieval_neighbors,
163
+ retrieval_metric=args.retrieval_metric,
164
+ )
165
+ source_group_ids: list[str] = []
166
+ residuals: list[list[list[float]]] = []
167
+ candidate_types: list[str] = []
168
+ for source_group_id, _feature, source_residuals, source_types in nearest:
169
+ source_group_ids.append(source_group_id)
170
+ residuals.extend(source_residuals)
171
+ candidate_types.extend(source_types)
172
+
173
+ allowed = [candidate_type not in excluded for candidate_type in candidate_types]
174
+ if not any(allowed):
175
+ allowed = [True] * len(candidate_types)
176
+ obs = torch.tensor(
177
+ [
178
+ vectorize_toy_observation(
179
+ records[0].observation_inline or {},
180
+ obs_dim=model_config.obs_dim,
181
+ )
182
+ ],
183
+ dtype=torch.float32,
184
+ device=device,
185
+ )
186
+ action_residuals = torch.tensor(
187
+ [residuals],
188
+ dtype=torch.float32,
189
+ device=device,
190
+ )
191
+ candidate_mask = torch.tensor([allowed], dtype=torch.bool, device=device)
192
+ selected, selected_index = _select_action_chunk(
193
+ model,
194
+ obs,
195
+ [records[0].instruction],
196
+ torch=torch,
197
+ selection_mode="retrieval_residual",
198
+ num_candidates=1,
199
+ candidate_sigma=0.0,
200
+ selection_seed=0,
201
+ retrieval_residual_scale=args.retrieval_residual_scale,
202
+ action_low=action_low,
203
+ action_high=action_high,
204
+ action_candidates=action_residuals,
205
+ candidate_mask=candidate_mask,
206
+ )
207
+ index = int(selected_index[0])
208
+ selected_type = candidate_types[index] if index < len(candidate_types) else "unknown"
209
+ action_values = selected[0].detach().cpu().tolist()
210
+ counts[selected_type] += 1
211
+ for source_group_id in source_group_ids:
212
+ source_counts[source_group_id] += 1
213
+ targets[group_id] = {
214
+ "action_values": action_values,
215
+ "selected_candidate_type": selected_type,
216
+ "candidate_source_group_id": ";".join(source_group_ids),
217
+ "task_id": task_id,
218
+ "retrieval_neighbors": args.retrieval_neighbors,
219
+ "retrieval_metric": args.retrieval_metric,
220
+ "retrieval_residual_scale": args.retrieval_residual_scale,
221
+ "excluded_candidate_types": sorted(excluded),
222
+ "leave_one_out": not args.no_leave_one_out,
223
+ }
224
+
225
+ payload = {
226
+ "target_type": "retrieval_residual_action_values",
227
+ "checkpoint": str(args.checkpoint),
228
+ "dataset": str(args.dataset),
229
+ "split": args.split,
230
+ "train_bank_groups": len(train_ids),
231
+ "num_groups": len(target_group_ids),
232
+ "num_targets": len(targets),
233
+ "num_skipped": len(skipped),
234
+ "retrieval_neighbors": args.retrieval_neighbors,
235
+ "retrieval_metric": args.retrieval_metric,
236
+ "retrieval_residual_scale": args.retrieval_residual_scale,
237
+ "excluded_candidate_types": sorted(excluded),
238
+ "leave_one_out": not args.no_leave_one_out,
239
+ "clip_actions": not args.no_clip_actions,
240
+ "clip_action_low": None if args.no_clip_actions else args.clip_action_low,
241
+ "clip_action_high": None if args.no_clip_actions else args.clip_action_high,
242
+ "selected_candidate_type_counts": dict(counts),
243
+ "num_source_groups_used": len(source_counts),
244
+ "skipped": skipped,
245
+ "targets": targets,
246
+ }
247
+ args.out.parent.mkdir(parents=True, exist_ok=True)
248
+ args.out.write_text(json.dumps(payload, indent=2) + "\n")
249
+ print(json.dumps({key: value for key, value in payload.items() if key != "targets"}, indent=2))
250
+ print(f"Wrote {args.out}")
251
+ return 0
252
+
253
+
254
+ def _build_residual_bank(
255
+ dataset: CILDataset,
256
+ group_ids: list[str],
257
+ *,
258
+ obs_dim: int,
259
+ ) -> dict[str, list[tuple[str, np.ndarray, list[list[list[float]]], list[str]]]]:
260
+ bank: dict[str, list[tuple[str, np.ndarray, list[list[list[float]]], list[str]]]] = (
261
+ defaultdict(list)
262
+ )
263
+ for group_id in group_ids:
264
+ records = dataset.get_group(group_id)
265
+ if not records:
266
+ continue
267
+ task_ids = {record.task_id for record in records}
268
+ if len(task_ids) != 1:
269
+ continue
270
+ anchor = next((record for record in records if record.candidate_type == "expert"), records[0])
271
+ anchor_action = np.asarray(_numeric_action_values(anchor), dtype=np.float32)
272
+ residuals: list[list[list[float]]] = [np.zeros_like(anchor_action).tolist()]
273
+ candidate_types = ["policy_residual"]
274
+ for record in records:
275
+ if record.record_id == anchor.record_id:
276
+ continue
277
+ residual = np.asarray(_numeric_action_values(record), dtype=np.float32) - anchor_action
278
+ residuals.append(residual.tolist())
279
+ candidate_types.append(f"residual_{record.candidate_type}")
280
+ feature = np.asarray(
281
+ vectorize_toy_observation(records[0].observation_inline or {}, obs_dim=obs_dim),
282
+ dtype=np.float32,
283
+ )
284
+ bank[next(iter(task_ids))].append((group_id, feature, residuals, candidate_types))
285
+ return bank
286
+
287
+
288
+ def _resolve_device(device: str) -> str:
289
+ if device != "auto":
290
+ return device
291
+ try:
292
+ import torch
293
+ except ImportError: # pragma: no cover
294
+ return "cpu"
295
+ return "cuda" if torch.cuda.is_available() else "cpu"
296
+
297
+
298
+ if __name__ == "__main__":
299
+ raise SystemExit(main())