import unittest import numpy as np import torch import torch.nn.functional as F from torch.utils.data import Subset from dataset import AllFutureHealthDataset from losses import ExponentialLoss, WeibullLoss from models import DeepHealth from train_util import fit_all_future_baseline def _inverse_softplus(value: torch.Tensor) -> torch.Tensor: return torch.log(torch.expm1(value)) class AllFutureLikelihoodTests(unittest.TestCase): def test_exponential_uses_event_specific_first_onset_exposure(self): desired_rate = torch.tensor( [[0.01, 0.01, 0.01, 0.20, 0.30, 0.40]], dtype=torch.float64, ) logits = _inverse_softplus(desired_rate) criterion = ExponentialLoss(ignored_idx={0, 1, 2}, eps=1e-12) loss = criterion( logits=logits, targets=torch.tensor([[4, 0]]), exposure=torch.tensor([5.0], dtype=torch.float64), dt=torch.tensor([[2.0, 0.0]], dtype=torch.float64), history=torch.tensor([[3, 0]]), ) rate = F.softplus(logits) + criterion.eps expected = -(rate[0, 4].log()) + rate[0, 4] * 2.0 + rate[0, 5] * 5.0 torch.testing.assert_close(loss, expected) def test_weibull_uses_event_time_and_excludes_prevalent_outcome(self): desired_rate = torch.tensor( [[0.01, 0.01, 0.01, 0.20, 0.30, 0.40]], dtype=torch.float64, ) logits = _inverse_softplus(desired_rate) rho = torch.tensor( [[1.0, 1.0, 1.0, 1.2, 1.5, 0.8]], dtype=torch.float64, ) criterion = WeibullLoss(ignored_idx={0, 1, 2}, eps=1e-12) loss = criterion( logits=logits, weibull_rho=rho, targets=torch.tensor([[4, 0]]), dt=torch.tensor([[2.0, 0.0]], dtype=torch.float64), exposure=torch.tensor([5.0], dtype=torch.float64), history=torch.tensor([[3, 0]]), ) rate = F.softplus(logits) + criterion.eps log_hazard = ( rate[0, 4].log() + rho[0, 4].log() + (rho[0, 4] - 1.0) * torch.tensor(2.0).log() ) expected = ( -log_hazard + rate[0, 4] * torch.pow(torch.tensor(2.0), rho[0, 4]) + rate[0, 5] * torch.pow(torch.tensor(5.0), rho[0, 5]) ) torch.testing.assert_close(loss, expected) class AllFutureQueryDistributionTests(unittest.TestCase): @staticmethod def _dataset() -> AllFutureHealthDataset: dataset = AllFutureHealthDataset.__new__(AllFutureHealthDataset) dataset.min_history_events = 1 dataset.min_future_events = 1 return dataset @staticmethod def _patient(): return { "times": np.asarray([1.0, 2.0, 3.0, 4.0], dtype=np.float32), "labels": np.asarray([3, 4, 5, 6], dtype=np.int64), "t_obs": 4.0, } def test_train_validation_and_test_share_one_query_sampler(self): dataset = self._dataset() patient = self._patient() patient["query_intervals"] = dataset._eligible_query_intervals(patient) self.assertEqual(len(patient["query_intervals"]), 3) rng = np.random.RandomState(123) fixed = dataset._sample_fixed_validation_queries(patient, rng) self.assertEqual(len(fixed), 1) self.assertTrue(dataset._is_valid_query(patient, fixed[0])) seen_intervals = set() rng = np.random.RandomState(456) for _ in range(200): query = dataset.sample_query(patient, rng) self.assertTrue(dataset._is_valid_query(patient, query)) seen_intervals.add(int(np.floor(query))) self.assertEqual(seen_intervals, {1, 2, 3}) class AllFutureBaselineTests(unittest.TestCase): def test_training_baseline_matches_first_onset_exposure(self): dataset = AllFutureHealthDataset.__new__(AllFutureHealthDataset) dataset.vocab_size = 7 dataset.min_history_events = 1 dataset.min_future_events = 2 patient = { "times": np.asarray([1.0, 3.0, 5.0], dtype=np.float32), "labels": np.asarray([3, 4, 5], dtype=np.int64), "t_obs": 5.0, "query_intervals": [(1.0, 3.0)], } dataset.patients = [patient] subset = Subset(dataset, [0]) stats = fit_all_future_baseline(dataset, subset, seed=17) query = dataset.sample_query(patient, np.random.RandomState(17)) self.assertEqual(stats.event_count[3], 0) self.assertEqual(stats.at_risk_exposure[3], 0.0) self.assertEqual(stats.event_count[4], 1) self.assertEqual(stats.event_count[5], 1) self.assertAlmostEqual(stats.at_risk_exposure[4], 3.0 - query, places=5) self.assertAlmostEqual(stats.at_risk_exposure[5], 5.0 - query, places=5) self.assertAlmostEqual( float(F.softplus(torch.tensor(stats.bias[4]))), float(stats.rate[4]), places=6, ) def test_model_starts_exactly_at_fitted_output_baseline(self): baseline_rate = torch.tensor([0.0, 0.0, 0.0, 0.02, 0.05, 0.10]) baseline_bias = torch.zeros_like(baseline_rate) baseline_bias[3:] = _inverse_softplus(baseline_rate[3:]) model = DeepHealth( vocab_size=6, n_embd=4, n_head=1, n_layer=1, n_types=1, n_cont_types=0, n_categories=1, cont_type_ids=[], target_mode="all_future", time_mode="absolute", dist_mode="weibull", model_architecture="transformer_ffn_v1", risk_head_bias=True, risk_head_bias_init=baseline_bias, ) torch.testing.assert_close(model.risk_head.weight, torch.zeros_like(model.risk_head.weight)) output_rate = F.softplus(model.risk_head(torch.randn(3, 4))) torch.testing.assert_close(output_rate[:, 3:], baseline_rate[None, 3:].expand(3, -1)) torch.testing.assert_close( F.softplus(model.rho_head.bias), torch.ones_like(model.rho_head.bias), atol=2e-5, rtol=0.0, ) if __name__ == "__main__": unittest.main()