Add training scripts for all-future and next-step supervision with DeepHealth
- Implement `train_all_future.py` for training with query-conditioned all-future supervision. - Implement `train_next_step.py` for training with next-token/next-time-point supervision. - Introduce `train_util.py` for shared utility functions including logging, dataset splitting, and model checkpointing. - Enhance argument parsing for both training scripts to accommodate new parameters. - Update loss functions and model configurations to support the new training paradigms.
This commit is contained in:
97
dataset.py
97
dataset.py
@@ -93,6 +93,7 @@ def _cache_file_path(
|
||||
split: str | None = None,
|
||||
min_history_events: int | None = None,
|
||||
min_future_events: int | None = None,
|
||||
validation_query_seed: int | None = None,
|
||||
) -> str:
|
||||
event_path = f"{data_prefix}_event_data.npy"
|
||||
basic_path = f"{data_prefix}_basic_info.csv"
|
||||
@@ -122,6 +123,7 @@ def _cache_file_path(
|
||||
str(int(include_no_event_in_uts_target)),
|
||||
"" if min_history_events is None else str(int(min_history_events)),
|
||||
"" if min_future_events is None else str(int(min_future_events)),
|
||||
"" if validation_query_seed is None else str(int(validation_query_seed)),
|
||||
]
|
||||
|
||||
for path in (event_path, basic_path, other_path, cate_types_path, labels_file):
|
||||
@@ -305,7 +307,11 @@ class _ExpoBaseDataset(Dataset):
|
||||
"other_time": (rows[:, 4].astype(np.float32) / DAYS_PER_YEAR),
|
||||
}
|
||||
|
||||
def _iter_patient_events(self) -> Iterable[tuple[int, np.ndarray, np.ndarray]]:
|
||||
def _iter_patient_events(
|
||||
self,
|
||||
*,
|
||||
impute_no_event_gaps: bool,
|
||||
) -> Iterable[tuple[int, np.ndarray, np.ndarray]]:
|
||||
unique_eids, starts = np.unique(self.event_data[:, 0], return_index=True)
|
||||
ends = np.concatenate([starts[1:], [len(self.event_data)]])
|
||||
for eid_raw, start, end in zip(unique_eids, starts, ends):
|
||||
@@ -313,7 +319,19 @@ class _ExpoBaseDataset(Dataset):
|
||||
rows = self.event_data[start:end]
|
||||
times_days_raw = rows[:, 1].astype(np.float32)
|
||||
labels_raw = rows[:, 2].astype(np.int64)
|
||||
|
||||
disease_mask = labels_raw != CHECKUP_IDX
|
||||
times_days_raw = times_days_raw[disease_mask]
|
||||
labels_raw = labels_raw[disease_mask]
|
||||
if len(labels_raw) == 0:
|
||||
yield eid, times_days_raw, labels_raw
|
||||
continue
|
||||
|
||||
labels_raw = np.where(labels_raw >= NO_EVENT_IDX, labels_raw + 1, labels_raw)
|
||||
if not impute_no_event_gaps:
|
||||
yield eid, times_days_raw, labels_raw
|
||||
continue
|
||||
|
||||
times_days, labels = _insert_gap_no_event_tokens(
|
||||
times_days_raw,
|
||||
labels_raw,
|
||||
@@ -374,7 +392,7 @@ class NextStepHealthDataset(_ExpoBaseDataset):
|
||||
- UniqueTimeSetExponentialLoss: readout_mask, target_dt_unique, target_multi_hot
|
||||
"""
|
||||
|
||||
CACHE_VERSION = 1
|
||||
CACHE_VERSION = 2
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -406,7 +424,9 @@ class NextStepHealthDataset(_ExpoBaseDataset):
|
||||
)
|
||||
|
||||
self.samples: List[Dict] = []
|
||||
for eid, times_days, labels in self._iter_patient_events():
|
||||
for eid, times_days, labels in self._iter_patient_events(
|
||||
impute_no_event_gaps=True,
|
||||
):
|
||||
if len(labels) < 2:
|
||||
continue
|
||||
|
||||
@@ -463,10 +483,12 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
||||
targets.
|
||||
|
||||
Train samples one query time per patient at each __getitem__ call.
|
||||
Valid/test use deterministic pre-event query points.
|
||||
Valid/test use random-but-fixed query points. For each patient with N real
|
||||
disease events, N - 2 query points are sampled from the eligible observed
|
||||
time range, with at least one future event after every query.
|
||||
"""
|
||||
|
||||
CACHE_VERSION = 1
|
||||
CACHE_VERSION = 4
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -477,6 +499,7 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
||||
include_no_event_in_uts_target: bool = False,
|
||||
min_history_events: int = 1,
|
||||
min_future_events: int = 1,
|
||||
validation_query_seed: int = 42,
|
||||
extra_info_types: Iterable[int] | None = None,
|
||||
) -> None:
|
||||
if split not in {"train", "valid", "test"}:
|
||||
@@ -492,6 +515,7 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
||||
split=split,
|
||||
min_history_events=min_history_events,
|
||||
min_future_events=min_future_events,
|
||||
validation_query_seed=validation_query_seed if split in {"valid", "test"} else None,
|
||||
)
|
||||
cached_state = self._load_cache(cache_path, self.CACHE_VERSION)
|
||||
if cached_state is not None:
|
||||
@@ -509,10 +533,17 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
||||
self.split = split
|
||||
self.min_history_events = int(min_history_events)
|
||||
self.min_future_events = int(min_future_events)
|
||||
self.validation_query_seed = int(validation_query_seed)
|
||||
self.patients: List[Dict] = []
|
||||
self.valid_queries: List[Tuple[int, float]] = []
|
||||
validation_rng = None
|
||||
if split in {"valid", "test"}:
|
||||
split_offset = 0 if split == "valid" else 1_000_003
|
||||
validation_rng = np.random.RandomState(self.validation_query_seed + split_offset)
|
||||
|
||||
for eid, times_days, labels in self._iter_patient_events():
|
||||
for eid, times_days, labels in self._iter_patient_events(
|
||||
impute_no_event_gaps=False,
|
||||
):
|
||||
times_years = (times_days / DAYS_PER_YEAR).astype(np.float32)
|
||||
unique_times = np.unique(times_years)
|
||||
if len(labels) < 2 or len(unique_times) < 2:
|
||||
@@ -534,13 +565,18 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
||||
self.patients.append(patient)
|
||||
|
||||
if split in {"valid", "test"}:
|
||||
for target_time in unique_times[1:]:
|
||||
t_query = float(target_time) - ONE_DAY_YEARS
|
||||
if self._is_valid_query(patient, t_query):
|
||||
self.valid_queries.append((pidx, t_query))
|
||||
if validation_rng is None:
|
||||
raise RuntimeError("validation_rng was not initialized")
|
||||
self.valid_queries.extend(
|
||||
(pidx, t_query)
|
||||
for t_query in self._sample_fixed_validation_queries(
|
||||
patient,
|
||||
validation_rng,
|
||||
)
|
||||
)
|
||||
|
||||
if split in {"valid", "test"} and not self.valid_queries:
|
||||
raise ValueError("No valid deterministic query points were built.")
|
||||
raise ValueError("No random-but-fixed validation query points were built.")
|
||||
|
||||
self._save_cache(cache_path, self.CACHE_VERSION)
|
||||
|
||||
@@ -554,6 +590,45 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
||||
and patient["t_obs"] > t_query
|
||||
)
|
||||
|
||||
def _sample_fixed_validation_queries(
|
||||
self,
|
||||
patient: Dict,
|
||||
rng: np.random.RandomState,
|
||||
) -> List[float]:
|
||||
times = np.asarray(patient["times"], dtype=np.float32)
|
||||
labels = np.asarray(patient["labels"], dtype=np.int64)
|
||||
real_event_mask = ~np.isin(
|
||||
labels,
|
||||
np.array([PAD_IDX, CHECKUP_IDX, NO_EVENT_IDX], dtype=np.int64),
|
||||
)
|
||||
real_times = np.sort(times[real_event_mask].astype(np.float32, copy=False))
|
||||
n_real_events = int(real_times.size)
|
||||
n_queries = max(0, n_real_events - 2)
|
||||
if n_queries == 0:
|
||||
return []
|
||||
|
||||
min_hist = int(self.min_history_events)
|
||||
min_future = int(self.min_future_events)
|
||||
if n_real_events < min_hist + min_future:
|
||||
return []
|
||||
|
||||
left = float(real_times[min_hist - 1])
|
||||
right_event_time = float(real_times[n_real_events - min_future])
|
||||
right = np.nextafter(np.float32(right_event_time), np.float32(-np.inf))
|
||||
if not np.isfinite(left) or not np.isfinite(right) or float(right) <= left:
|
||||
return []
|
||||
|
||||
queries: List[float] = []
|
||||
max_attempts = max(100, n_queries * 50)
|
||||
for _ in range(max_attempts):
|
||||
if len(queries) >= n_queries:
|
||||
break
|
||||
t_query = float(rng.uniform(left, float(right)))
|
||||
if self._is_valid_query(patient, t_query):
|
||||
queries.append(t_query)
|
||||
|
||||
return queries
|
||||
|
||||
def _sample_train_query(self, patient: Dict) -> float:
|
||||
unique_times = np.unique(patient["times"])
|
||||
if len(unique_times) < 2:
|
||||
|
||||
Reference in New Issue
Block a user