Auto-sync: 2026-06-28 20:44:54
Browse files
dovla_cil/eval/maniskill_policy_rollout.py
CHANGED
|
@@ -99,9 +99,10 @@ def evaluate_maniskill_policy_rollout(
|
|
| 99 |
|
| 100 |
When ``selection_mode == 'retrieval_residual'`` the evaluator retrieves counterfactual
|
| 101 |
action residuals (candidate minus expert action) from the nearest training-split state(s),
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
| 105 |
|
| 106 |
When ``selection_mode == 'field_optim'`` the evaluator starts from the policy mean plus
|
| 107 |
optional Gaussian multi-start proposals, performs projected gradient ascent on the learned
|
|
@@ -155,9 +156,15 @@ def evaluate_maniskill_policy_rollout(
|
|
| 155 |
raise ValueError("retrieval_metric must be 'raw' or 'zscore'")
|
| 156 |
if retrieval_residual_anchor not in {"expert", "policy"}:
|
| 157 |
raise ValueError("retrieval_residual_anchor must be 'expert' or 'policy'")
|
| 158 |
-
if retrieval_residual_reduce not in {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
raise ValueError(
|
| 160 |
-
"retrieval_residual_reduce must be 'none', 'mean_by_type',
|
|
|
|
| 161 |
)
|
| 162 |
if not 0.0 <= retrieval_type_min_success <= 1.0:
|
| 163 |
raise ValueError("retrieval_type_min_success must be in [0, 1]")
|
|
@@ -217,7 +224,6 @@ def evaluate_maniskill_policy_rollout(
|
|
| 217 |
observation_mode=model_config.observation_mode,
|
| 218 |
retrieval_neighbors=retrieval_neighbors,
|
| 219 |
retrieval_metric=retrieval_metric,
|
| 220 |
-
retrieval_type_min_success=retrieval_type_min_success,
|
| 221 |
)
|
| 222 |
else:
|
| 223 |
cases = _attach_retrieved_residual_candidates(
|
|
@@ -557,7 +563,7 @@ def _attach_retrieved_residual_candidates(
|
|
| 557 |
vectorize_toy_observation(case.observation, obs_dim=obs_dim),
|
| 558 |
dtype=np.float32,
|
| 559 |
)
|
| 560 |
-
nearest =
|
| 561 |
candidates,
|
| 562 |
query,
|
| 563 |
retrieval_neighbors=retrieval_neighbors,
|
|
@@ -566,15 +572,22 @@ def _attach_retrieved_residual_candidates(
|
|
| 566 |
source_group_ids: list[str] = []
|
| 567 |
residuals: list[list[list[float]]] = []
|
| 568 |
candidate_types: list[str] = []
|
| 569 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
source_group_ids.append(source_group_id)
|
| 571 |
residuals.extend(source_residuals)
|
| 572 |
candidate_types.extend(source_candidate_types)
|
|
|
|
| 573 |
if retrieval_residual_reduce != "none":
|
| 574 |
residuals, candidate_types = _reduce_residual_candidates_by_type(
|
| 575 |
residuals,
|
| 576 |
candidate_types,
|
| 577 |
mode=retrieval_residual_reduce,
|
|
|
|
| 578 |
)
|
| 579 |
output.append(
|
| 580 |
replace(
|
|
@@ -592,26 +605,48 @@ def _reduce_residual_candidates_by_type(
|
|
| 592 |
candidate_types: list[str],
|
| 593 |
*,
|
| 594 |
mode: str,
|
|
|
|
| 595 |
) -> tuple[list[list[list[float]]], list[str]]:
|
| 596 |
-
if mode not in {"mean_by_type", "median_by_type"}:
|
| 597 |
-
raise ValueError(
|
|
|
|
|
|
|
| 598 |
if len(residuals) != len(candidate_types):
|
| 599 |
raise ValueError("residuals and candidate_types must have the same length")
|
|
|
|
|
|
|
| 600 |
|
| 601 |
ordered_types = list(dict.fromkeys(candidate_types))
|
| 602 |
reduced_residuals: list[list[list[float]]] = []
|
| 603 |
reduced_types: list[str] = []
|
| 604 |
for candidate_type in ordered_types:
|
| 605 |
-
values = [
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
if residual_type =
|
| 609 |
-
|
|
|
|
|
|
|
|
|
|
| 610 |
if not values:
|
| 611 |
continue
|
| 612 |
stack = np.stack(values, axis=0)
|
| 613 |
if mode == "mean_by_type":
|
| 614 |
reduced = np.mean(stack, axis=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
else:
|
| 616 |
reduced = np.median(stack, axis=0)
|
| 617 |
reduced_residuals.append(reduced.astype(np.float32).tolist())
|
|
@@ -647,11 +682,30 @@ def _nearest_retrieval_entries(
|
|
| 647 |
retrieval_neighbors: int,
|
| 648 |
retrieval_metric: str,
|
| 649 |
) -> list[tuple[Any, np.ndarray, Any, Any]]:
|
| 650 |
-
|
| 651 |
-
|
|
|
|
| 652 |
candidates,
|
| 653 |
-
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
if retrieval_metric != "zscore":
|
| 656 |
raise ValueError("retrieval_metric must be 'raw' or 'zscore'")
|
| 657 |
features = np.stack([np.asarray(item[1], dtype=np.float32) for item in candidates], axis=0)
|
|
@@ -660,8 +714,35 @@ def _nearest_retrieval_entries(
|
|
| 660 |
scale = np.where(std > 1e-6, std, 1.0).astype(np.float32)
|
| 661 |
normalized_features = (features - mean) / scale
|
| 662 |
normalized_query = (np.asarray(query, dtype=np.float32) - mean) / scale
|
| 663 |
-
|
| 664 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
|
| 666 |
|
| 667 |
def _evaluate_task_cases(
|
|
|
|
| 99 |
|
| 100 |
When ``selection_mode == 'retrieval_residual'`` the evaluator retrieves counterfactual
|
| 101 |
action residuals (candidate minus expert action) from the nearest training-split state(s),
|
| 102 |
+
optionally reduces them into type-wise tangent consensus proposals, adds those residuals
|
| 103 |
+
around the current policy mean, scores the resulting local proposal lattice with the learned
|
| 104 |
+
field, and executes the best chunk. This keeps the proposal geometry counterfactual while
|
| 105 |
+
avoiding same-state validation candidates.
|
| 106 |
|
| 107 |
When ``selection_mode == 'field_optim'`` the evaluator starts from the policy mean plus
|
| 108 |
optional Gaussian multi-start proposals, performs projected gradient ascent on the learned
|
|
|
|
| 156 |
raise ValueError("retrieval_metric must be 'raw' or 'zscore'")
|
| 157 |
if retrieval_residual_anchor not in {"expert", "policy"}:
|
| 158 |
raise ValueError("retrieval_residual_anchor must be 'expert' or 'policy'")
|
| 159 |
+
if retrieval_residual_reduce not in {
|
| 160 |
+
"none",
|
| 161 |
+
"mean_by_type",
|
| 162 |
+
"median_by_type",
|
| 163 |
+
"kernel_mean_by_type",
|
| 164 |
+
}:
|
| 165 |
raise ValueError(
|
| 166 |
+
"retrieval_residual_reduce must be 'none', 'mean_by_type', "
|
| 167 |
+
"'median_by_type', or 'kernel_mean_by_type'"
|
| 168 |
)
|
| 169 |
if not 0.0 <= retrieval_type_min_success <= 1.0:
|
| 170 |
raise ValueError("retrieval_type_min_success must be in [0, 1]")
|
|
|
|
| 224 |
observation_mode=model_config.observation_mode,
|
| 225 |
retrieval_neighbors=retrieval_neighbors,
|
| 226 |
retrieval_metric=retrieval_metric,
|
|
|
|
| 227 |
)
|
| 228 |
else:
|
| 229 |
cases = _attach_retrieved_residual_candidates(
|
|
|
|
| 563 |
vectorize_toy_observation(case.observation, obs_dim=obs_dim),
|
| 564 |
dtype=np.float32,
|
| 565 |
)
|
| 566 |
+
nearest = _nearest_retrieval_entries_with_distances(
|
| 567 |
candidates,
|
| 568 |
query,
|
| 569 |
retrieval_neighbors=retrieval_neighbors,
|
|
|
|
| 572 |
source_group_ids: list[str] = []
|
| 573 |
residuals: list[list[list[float]]] = []
|
| 574 |
candidate_types: list[str] = []
|
| 575 |
+
residual_weights: list[float] = []
|
| 576 |
+
source_weights = _kernel_weights_from_distances(
|
| 577 |
+
[distance for _entry, distance in nearest]
|
| 578 |
+
)
|
| 579 |
+
for (entry, _distance), source_weight in zip(nearest, source_weights):
|
| 580 |
+
source_group_id, _feature, source_residuals, source_candidate_types = entry
|
| 581 |
source_group_ids.append(source_group_id)
|
| 582 |
residuals.extend(source_residuals)
|
| 583 |
candidate_types.extend(source_candidate_types)
|
| 584 |
+
residual_weights.extend([float(source_weight)] * len(source_residuals))
|
| 585 |
if retrieval_residual_reduce != "none":
|
| 586 |
residuals, candidate_types = _reduce_residual_candidates_by_type(
|
| 587 |
residuals,
|
| 588 |
candidate_types,
|
| 589 |
mode=retrieval_residual_reduce,
|
| 590 |
+
weights=residual_weights,
|
| 591 |
)
|
| 592 |
output.append(
|
| 593 |
replace(
|
|
|
|
| 605 |
candidate_types: list[str],
|
| 606 |
*,
|
| 607 |
mode: str,
|
| 608 |
+
weights: list[float] | None = None,
|
| 609 |
) -> tuple[list[list[list[float]]], list[str]]:
|
| 610 |
+
if mode not in {"mean_by_type", "median_by_type", "kernel_mean_by_type"}:
|
| 611 |
+
raise ValueError(
|
| 612 |
+
"mode must be 'mean_by_type', 'median_by_type', or 'kernel_mean_by_type'"
|
| 613 |
+
)
|
| 614 |
if len(residuals) != len(candidate_types):
|
| 615 |
raise ValueError("residuals and candidate_types must have the same length")
|
| 616 |
+
if weights is not None and len(weights) != len(residuals):
|
| 617 |
+
raise ValueError("weights and residuals must have the same length")
|
| 618 |
|
| 619 |
ordered_types = list(dict.fromkeys(candidate_types))
|
| 620 |
reduced_residuals: list[list[list[float]]] = []
|
| 621 |
reduced_types: list[str] = []
|
| 622 |
for candidate_type in ordered_types:
|
| 623 |
+
values: list[np.ndarray] = []
|
| 624 |
+
value_weights: list[float] = []
|
| 625 |
+
for index, (residual, residual_type) in enumerate(zip(residuals, candidate_types)):
|
| 626 |
+
if residual_type != candidate_type:
|
| 627 |
+
continue
|
| 628 |
+
values.append(np.asarray(residual, dtype=np.float32))
|
| 629 |
+
if weights is not None:
|
| 630 |
+
value_weights.append(float(weights[index]))
|
| 631 |
if not values:
|
| 632 |
continue
|
| 633 |
stack = np.stack(values, axis=0)
|
| 634 |
if mode == "mean_by_type":
|
| 635 |
reduced = np.mean(stack, axis=0)
|
| 636 |
+
elif mode == "kernel_mean_by_type":
|
| 637 |
+
if value_weights:
|
| 638 |
+
np_weights = np.asarray(value_weights, dtype=np.float32)
|
| 639 |
+
weight_sum = float(np.sum(np_weights))
|
| 640 |
+
if weight_sum > 1e-12:
|
| 641 |
+
np_weights = np_weights / weight_sum
|
| 642 |
+
reduced = np.sum(
|
| 643 |
+
stack * np_weights.reshape((-1,) + (1,) * (stack.ndim - 1)),
|
| 644 |
+
axis=0,
|
| 645 |
+
)
|
| 646 |
+
else:
|
| 647 |
+
reduced = np.mean(stack, axis=0)
|
| 648 |
+
else:
|
| 649 |
+
reduced = np.mean(stack, axis=0)
|
| 650 |
else:
|
| 651 |
reduced = np.median(stack, axis=0)
|
| 652 |
reduced_residuals.append(reduced.astype(np.float32).tolist())
|
|
|
|
| 682 |
retrieval_neighbors: int,
|
| 683 |
retrieval_metric: str,
|
| 684 |
) -> list[tuple[Any, np.ndarray, Any, Any]]:
|
| 685 |
+
return [
|
| 686 |
+
entry
|
| 687 |
+
for entry, _distance in _nearest_retrieval_entries_with_distances(
|
| 688 |
candidates,
|
| 689 |
+
query,
|
| 690 |
+
retrieval_neighbors=retrieval_neighbors,
|
| 691 |
+
retrieval_metric=retrieval_metric,
|
| 692 |
+
)
|
| 693 |
+
]
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
def _nearest_retrieval_entries_with_distances(
|
| 697 |
+
candidates: list[tuple[Any, np.ndarray, Any, Any]],
|
| 698 |
+
query: np.ndarray,
|
| 699 |
+
*,
|
| 700 |
+
retrieval_neighbors: int,
|
| 701 |
+
retrieval_metric: str,
|
| 702 |
+
) -> list[tuple[tuple[Any, np.ndarray, Any, Any], float]]:
|
| 703 |
+
if retrieval_metric == "raw":
|
| 704 |
+
scored = [
|
| 705 |
+
(item, float(np.mean((item[1] - query) ** 2)))
|
| 706 |
+
for item in candidates
|
| 707 |
+
]
|
| 708 |
+
return sorted(scored, key=lambda item: item[1])[:retrieval_neighbors]
|
| 709 |
if retrieval_metric != "zscore":
|
| 710 |
raise ValueError("retrieval_metric must be 'raw' or 'zscore'")
|
| 711 |
features = np.stack([np.asarray(item[1], dtype=np.float32) for item in candidates], axis=0)
|
|
|
|
| 714 |
scale = np.where(std > 1e-6, std, 1.0).astype(np.float32)
|
| 715 |
normalized_features = (features - mean) / scale
|
| 716 |
normalized_query = (np.asarray(query, dtype=np.float32) - mean) / scale
|
| 717 |
+
distances = np.mean((normalized_features - normalized_query) ** 2, axis=1)
|
| 718 |
+
order = np.argsort(distances)
|
| 719 |
+
return [
|
| 720 |
+
(candidates[int(index)], float(distances[int(index)]))
|
| 721 |
+
for index in order[:retrieval_neighbors]
|
| 722 |
+
]
|
| 723 |
+
|
| 724 |
+
|
| 725 |
+
def _kernel_weights_from_distances(distances: list[float]) -> list[float]:
|
| 726 |
+
if not distances:
|
| 727 |
+
return []
|
| 728 |
+
values = np.asarray(distances, dtype=np.float32)
|
| 729 |
+
values = np.maximum(np.nan_to_num(values, nan=np.inf, posinf=np.inf, neginf=0.0), 0.0)
|
| 730 |
+
finite = values[np.isfinite(values)]
|
| 731 |
+
if finite.size == 0:
|
| 732 |
+
return [1.0] * len(distances)
|
| 733 |
+
positive = finite[finite > 1e-12]
|
| 734 |
+
if positive.size == 0:
|
| 735 |
+
return [1.0] * len(distances)
|
| 736 |
+
bandwidth = float(np.median(positive))
|
| 737 |
+
if bandwidth <= 1e-12:
|
| 738 |
+
bandwidth = float(np.max(positive))
|
| 739 |
+
if bandwidth <= 1e-12:
|
| 740 |
+
return [1.0] * len(distances)
|
| 741 |
+
weights = np.exp(-np.minimum(values, bandwidth * 50.0) / bandwidth)
|
| 742 |
+
weights = np.where(np.isfinite(weights), weights, 0.0)
|
| 743 |
+
if float(np.sum(weights)) <= 1e-12:
|
| 744 |
+
return [1.0] * len(distances)
|
| 745 |
+
return weights.astype(np.float32).tolist()
|
| 746 |
|
| 747 |
|
| 748 |
def _evaluate_task_cases(
|