vla / tests /test_losses.py
anhtld's picture
Initial commit: DoVLA-CIL codebase (h=16 breakthrough) (part 2)
20c251e verified
Raw
History Blame Contribute Delete
6.58 kB
from __future__ import annotations
import pytest
from dovla_cil.training.losses import (
CompositeLoss,
behavior_cloning_loss,
causal_contrastive_loss,
effect_prediction_loss,
language_minimal_pair_loss,
lattice_cycle_residual,
lattice_field_loss,
pairwise_ranking_loss,
progress_loss,
regret_loss,
regret_targets,
same_state_pairwise_ranking_loss,
success_loss,
)
def test_ranking_loss_prefers_correct_order() -> None:
rewards = [1.0, 0.0, -1.0]
good_scores = [2.0, 1.0, 0.0]
bad_scores = [0.0, 1.0, 2.0]
good_loss = same_state_pairwise_ranking_loss(good_scores, rewards)
bad_loss = same_state_pairwise_ranking_loss(bad_scores, rewards)
assert good_loss < bad_loss
def test_ranking_loss_ignores_ties() -> None:
assert same_state_pairwise_ranking_loss([0.0, 1.0], [1.0, 1.0]) == 0.0
def test_ranking_loss_checks_shape() -> None:
with pytest.raises(ValueError):
same_state_pairwise_ranking_loss([0.0], [0.0, 1.0])
def test_regret_targets() -> None:
assert regret_targets([1.0, 0.25, -1.0]) == [0.0, 0.75, 2.0]
def test_pairwise_ranking_loss_lower_when_order_is_correct() -> None:
good = pairwise_ranking_loss([2.0, 1.0], [0.0, 0.0], [1.0, 1.0], [0.0, -1.0])
bad = pairwise_ranking_loss([0.0, 0.0], [2.0, 1.0], [1.0, 1.0], [0.0, -1.0])
assert good < bad
def test_regret_loss_zero_when_exact() -> None:
assert regret_loss([0.0, 0.5, 1.0], [0.0, 0.5, 1.0]) == 0.0
def test_behavior_cloning_loss_works() -> None:
assert behavior_cloning_loss([1.0, 2.0], [1.0, 4.0]) == pytest.approx(2.0)
def test_effect_prediction_loss_combines_continuous_and_binary_terms() -> None:
loss = effect_prediction_loss(
{"continuous": [0.0, 1.0], "binary_logits": [0.0]},
{"continuous": [0.0, 3.0], "binary": [1.0]},
)
assert loss > 0.0
def test_success_loss_prefers_correct_logits() -> None:
assert success_loss([3.0], [1.0]) < success_loss([-3.0], [1.0])
def test_progress_loss_works() -> None:
assert progress_loss([0.5], [0.5]) == pytest.approx(0.0)
def test_contrastive_loss_finite() -> None:
loss = causal_contrastive_loss(
[[1.0, 0.0]],
[[1.0, 0.0]],
[[0.0, 1.0]],
temperature=0.1,
)
assert float(loss) >= 0.0
def test_language_minimal_pair_loss_pushes_and_pulls() -> None:
close_different = language_minimal_pair_loss([[0.0, 0.0]], [[0.1, 0.0]], [True], margin=1.0)
far_different = language_minimal_pair_loss([[0.0, 0.0]], [[2.0, 0.0]], [True], margin=1.0)
same_identical = language_minimal_pair_loss([[0.0, 0.0]], [[0.0, 0.0]], [False], margin=1.0)
same_apart = language_minimal_pair_loss([[0.0, 0.0]], [[1.0, 0.0]], [False], margin=1.0)
assert far_different < close_different
assert same_identical < same_apart
def test_composite_returns_components_and_total() -> None:
output = CompositeLoss()(
predictions={
"pred_action": [1.0, 2.0],
"pred_regret": [0.0, 1.0],
"pred_scores_i": [2.0],
"pred_scores_j": [0.0],
},
targets={
"target_action": [1.0, 3.0],
"target_regret": [0.0, 1.0],
"rewards_i": [1.0],
"rewards_j": [0.0],
},
)
assert set(output) >= {"total", "bc", "rank", "regret"}
assert float(output["total"]) >= 0.0
def test_lattice_field_loss_is_invariant_to_state_reward_offsets() -> None:
torch = pytest.importorskip("torch")
potential = torch.tensor([0.2, -0.1, 0.7, 0.0])
utility = torch.tensor([0.8, 0.3, 0.6, 0.1])
effect = torch.tensor([[0.0], [0.2], [0.5], [0.1]])
target_effect = torch.tensor([[0.1], [0.4], [0.6], [0.0]])
group_ids = ["state-a", "state-a", "state-b", "state-b"]
base = lattice_field_loss(potential, utility, effect, target_effect, group_ids)
shifted = lattice_field_loss(
potential,
utility + torch.tensor([17.0, 17.0, -9.0, -9.0]),
effect,
target_effect,
group_ids,
)
assert torch.allclose(base["potential"], shifted["potential"])
assert base["edge_count"] == shifted["edge_count"] == 2
def test_lattice_field_is_zero_under_groupwise_gauge_shifts() -> None:
torch = pytest.importorskip("torch")
utility = torch.tensor([0.8, 0.3, 0.6, 0.1])
target_effect = torch.tensor([[0.1, 0.2], [0.4, 0.0], [0.6, -0.2], [0.0, 0.3]])
group_ids = ["state-a", "state-a", "state-b", "state-b"]
potential = utility + torch.tensor([5.0, 5.0, -2.0, -2.0])
predicted_effect = target_effect + torch.tensor(
[[1.0, -3.0], [1.0, -3.0], [-4.0, 2.0], [-4.0, 2.0]]
)
loss = lattice_field_loss(
potential,
utility,
predicted_effect,
target_effect,
group_ids,
)
assert float(loss["potential"]) == pytest.approx(0.0, abs=1e-7)
assert float(loss["effect"]) == pytest.approx(0.0, abs=1e-7)
def test_lattice_field_orientation_penalizes_reversed_edge_order() -> None:
torch = pytest.importorskip("torch")
utility = torch.tensor([1.0, 0.0])
effect = torch.zeros((2, 2))
correct = lattice_field_loss(
torch.tensor([1.0, 0.0]),
utility,
effect,
effect,
["state", "state"],
)
reversed_order = lattice_field_loss(
torch.tensor([0.0, 1.0]),
utility,
effect,
effect,
["state", "state"],
)
assert float(correct["potential"]) == pytest.approx(0.0, abs=1e-7)
assert float(reversed_order["orientation"]) > 0.0
assert float(reversed_order["potential"]) > float(correct["potential"])
assert float(reversed_order["preference"]) > float(correct["preference"])
def test_lattice_field_preference_is_group_offset_invariant() -> None:
torch = pytest.importorskip("torch")
potential = torch.tensor([0.4, -0.2, 2.0, 1.7])
utility = torch.tensor([0.9, 0.1, 0.8, 0.2])
effect = torch.zeros((4, 2))
group_ids = ["a", "a", "b", "b"]
base = lattice_field_loss(potential, utility, effect, effect, group_ids)
shifted = lattice_field_loss(
potential,
utility + torch.tensor([10.0, 10.0, -4.0, -4.0]),
effect,
effect,
group_ids,
)
assert torch.allclose(base["preference"], shifted["preference"])
def test_scalar_potential_has_zero_cycle_residual() -> None:
torch = pytest.importorskip("torch")
residual = lattice_cycle_residual(torch.tensor([0.3, -1.2, 2.4]), [[0, 1, 2]])
assert float(residual) == pytest.approx(0.0, abs=1e-7)