项目文件夹

文件
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

153 行
6.4 KiB
Python

"""Unit tests for ContrastiveAlignmentLoss (Phase 6.4.2)."""
from __future__ import annotations
import math
import pytest
import torch
from ludwig.modules.contrastive_alignment import ContrastiveAlignmentLoss
class TestContrastiveAlignmentLoss:
def test_shape_and_scalar(self):
torch.manual_seed(0)
loss_fn = ContrastiveAlignmentLoss({"a": 8, "b": 12}, projection_dim=16)
batch = {
"a": torch.randn(4, 8),
"b": torch.randn(4, 12),
}
loss = loss_fn(batch)
assert loss.ndim == 0
assert torch.isfinite(loss)
def test_perfect_alignment_near_zero_loss(self):
"""When two feature embeddings are identical and projection is identity-ish, loss should be very small
because the diagonal dominates the similarity matrix."""
torch.manual_seed(0)
# Use a large projection_dim and initialize projections to near-identity so paired
# examples map to near-identical points in the aligned space.
loss_fn = ContrastiveAlignmentLoss({"a": 16, "b": 16}, projection_dim=16, learnable_temperature=False)
with torch.no_grad():
for name in ["a", "b"]:
loss_fn.projections[name].weight.copy_(torch.eye(16))
loss_fn.projections[name].bias.zero_()
x = torch.randn(32, 16)
loss = loss_fn({"a": x, "b": x.clone()})
# With identical features and an identity projection the contrastive loss is minimised.
assert loss.item() < 0.1
def test_misalignment_increases_loss(self):
"""Pairing example i with i works better than random pairing."""
torch.manual_seed(0)
loss_fn = ContrastiveAlignmentLoss({"a": 16, "b": 16}, projection_dim=16, learnable_temperature=False)
with torch.no_grad():
for name in ["a", "b"]:
loss_fn.projections[name].weight.copy_(torch.eye(16))
loss_fn.projections[name].bias.zero_()
x = torch.randn(32, 16)
aligned = loss_fn({"a": x, "b": x.clone()})
# Shuffle feature b so its positive is no longer at position i.
perm = torch.randperm(32)
misaligned = loss_fn({"a": x, "b": x[perm].clone()})
assert misaligned.item() > aligned.item()
def test_pair_symmetry(self):
"""Swapping two feature names should leave the loss value unchanged (up to floating-point)."""
torch.manual_seed(0)
loss_fn = ContrastiveAlignmentLoss({"a": 8, "b": 8}, projection_dim=16, learnable_temperature=False)
with torch.no_grad():
# Copy feature-a projection weights into feature-b so the two features become
# interchangeable.
loss_fn.projections["b"].weight.copy_(loss_fn.projections["a"].weight)
loss_fn.projections["b"].bias.copy_(loss_fn.projections["a"].bias)
x = torch.randn(16, 8)
y = torch.randn(16, 8)
ab = loss_fn({"a": x, "b": y})
ba = loss_fn({"a": y, "b": x})
assert torch.allclose(ab, ba, atol=1e-5)
def test_three_features(self):
"""Loss should accommodate any number >= 2 of features and average over pairs."""
torch.manual_seed(0)
loss_fn = ContrastiveAlignmentLoss({"a": 4, "b": 5, "c": 6}, projection_dim=8)
embeddings = {
"a": torch.randn(4, 4),
"b": torch.randn(4, 5),
"c": torch.randn(4, 6),
}
loss = loss_fn(embeddings)
assert torch.isfinite(loss)
def test_rejects_single_feature(self):
with pytest.raises(ValueError, match="at least 2 input features"):
ContrastiveAlignmentLoss({"only_one": 8})
def test_rejects_missing_feature_in_batch(self):
loss_fn = ContrastiveAlignmentLoss({"a": 4, "b": 4}, projection_dim=8)
with pytest.raises(KeyError, match="expected feature 'b'"):
loss_fn({"a": torch.randn(2, 4)})
def test_learnable_vs_fixed_temperature(self):
fixed = ContrastiveAlignmentLoss({"a": 4, "b": 4}, projection_dim=8, learnable_temperature=False)
learnable = ContrastiveAlignmentLoss({"a": 4, "b": 4}, projection_dim=8, learnable_temperature=True)
assert not fixed.log_temperature.requires_grad
assert learnable.log_temperature.requires_grad
# Both should start at log(1/0.07).
expected = math.log(1.0 / 0.07)
assert abs(float(fixed.log_temperature) - expected) < 1e-5
assert abs(float(learnable.log_temperature) - expected) < 1e-5
def test_backward_populates_encoder_grads(self):
"""The loss gradient must flow into the per-feature encoder inputs so an upstream encoder is actually
updated during pre-alignment."""
loss_fn = ContrastiveAlignmentLoss({"a": 8, "b": 8}, projection_dim=16)
a = torch.randn(4, 8, requires_grad=True)
b = torch.randn(4, 8, requires_grad=True)
loss = loss_fn({"a": a, "b": b})
loss.backward()
assert a.grad is not None and torch.isfinite(a.grad).all()
assert b.grad is not None and torch.isfinite(b.grad).all()
class TestContrastivePretrainSchema:
def test_default_values(self):
from ludwig.schema.model_config import ModelConfig
cfg = ModelConfig.from_dict(
{
"input_features": [
{"name": "a", "type": "number"},
{"name": "b", "type": "number"},
],
"output_features": [{"name": "y", "type": "binary"}],
}
)
assert cfg.trainer.contrastive_pretrain_epochs == 0
assert cfg.trainer.contrastive_pretrain_temperature == 0.07
assert cfg.trainer.contrastive_pretrain_projection_dim == 128
def test_explicit_values(self):
from ludwig.schema.model_config import ModelConfig
cfg = ModelConfig.from_dict(
{
"input_features": [
{"name": "a", "type": "number"},
{"name": "b", "type": "number"},
],
"output_features": [{"name": "y", "type": "binary"}],
"trainer": {
"contrastive_pretrain_epochs": 3,
"contrastive_pretrain_temperature": 0.1,
"contrastive_pretrain_projection_dim": 64,
},
}
)
assert cfg.trainer.contrastive_pretrain_epochs == 3
assert cfg.trainer.contrastive_pretrain_temperature == 0.1
assert cfg.trainer.contrastive_pretrain_projection_dim == 64