Use precomputed exposure embeddings
This commit is contained in:
19
README.md
19
README.md
@@ -68,9 +68,8 @@ python train_exposure_autoencoder.py \
|
|||||||
--val_eid_file ukb_val_eid.csv
|
--val_eid_file ukb_val_eid.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
The best checkpoint contains both `model_state_dict`, an `encoder_state_dict`
|
The best checkpoint contains the encoder and normalization statistics needed
|
||||||
compatible with the default gated `TimesNetExposureEncoder`, and the channel
|
to generate fixed exposure embeddings.
|
||||||
normalization statistics needed when the encoder is attached to DeepHealth.
|
|
||||||
Multi-GPU pretraining follows the main trainer interface: add
|
Multi-GPU pretraining follows the main trainer interface: add
|
||||||
`--data_parallel --gpu_ids 0,1,2,3`.
|
`--data_parallel --gpu_ids 0,1,2,3`.
|
||||||
For efficient multi-GPU training, launch one process per GPU with DDP:
|
For efficient multi-GPU training, launch one process per GPU with DDP:
|
||||||
@@ -84,13 +83,21 @@ torchrun --standalone --nproc_per_node=4 train_exposure_autoencoder.py \
|
|||||||
In DDP mode, `--batch_size` is the global batch size and must be divisible by
|
In DDP mode, `--batch_size` is the global batch size and must be divisible by
|
||||||
the number of processes.
|
the number of processes.
|
||||||
|
|
||||||
The end-to-end next-step trainer supports the same DDP launch pattern:
|
Encode every cached exposure window once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python encode_exposure_cache.py \
|
||||||
|
--exposure_cache_dir ukb_exposure_cache \
|
||||||
|
--checkpoint runs/exposure_ae_RUN/best.pt
|
||||||
|
```
|
||||||
|
|
||||||
|
The next-step trainer reads `exposure_embeddings.npy` directly and does not
|
||||||
|
run TimesNet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
torchrun --standalone --nproc_per_node=4 train_next_step.py \
|
torchrun --standalone --nproc_per_node=4 train_next_step.py \
|
||||||
--exposure_cache_dir ukb_exposure_cache \
|
--exposure_cache_dir ukb_exposure_cache \
|
||||||
--batch_size 128 \
|
--batch_size 128
|
||||||
--d_model 64
|
|
||||||
```
|
```
|
||||||
Training-channel statistics are cached at
|
Training-channel statistics are cached at
|
||||||
`<exposure_cache_dir>/train_channel_stats.npz`; use
|
`<exposure_cache_dir>/train_channel_stats.npz`; use
|
||||||
|
|||||||
117
dataset.py
117
dataset.py
@@ -50,7 +50,11 @@ def _load_readonly_npy(path: Path) -> np.ndarray:
|
|||||||
class ExposureCache:
|
class ExposureCache:
|
||||||
"""Eid-sequence-aligned exposure windows from prepare_exposure_cache.py."""
|
"""Eid-sequence-aligned exposure windows from prepare_exposure_cache.py."""
|
||||||
|
|
||||||
def __init__(self, cache_dir: str | Path):
|
def __init__(
|
||||||
|
self,
|
||||||
|
cache_dir: str | Path,
|
||||||
|
embeddings_file: str | Path | None = None,
|
||||||
|
):
|
||||||
cache_dir = Path(cache_dir)
|
cache_dir = Path(cache_dir)
|
||||||
self.cache_dir = cache_dir
|
self.cache_dir = cache_dir
|
||||||
manifest_path = cache_dir / "exposure_manifest.json"
|
manifest_path = cache_dir / "exposure_manifest.json"
|
||||||
@@ -101,6 +105,11 @@ class ExposureCache:
|
|||||||
self.eid_start = _load_readonly_npy(eid_start_path)
|
self.eid_start = _load_readonly_npy(eid_start_path)
|
||||||
self.daily = _load_readonly_npy(daily_path)
|
self.daily = _load_readonly_npy(daily_path)
|
||||||
self.monthly = _load_readonly_npy(monthly_path)
|
self.monthly = _load_readonly_npy(monthly_path)
|
||||||
|
self.embeddings = (
|
||||||
|
_load_readonly_npy(Path(embeddings_file))
|
||||||
|
if embeddings_file is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
quality_path = cache_dir / "exposure_quality.npy"
|
quality_path = cache_dir / "exposure_quality.npy"
|
||||||
self.quality = _load_readonly_npy(quality_path) if quality_path.is_file() else None
|
self.quality = _load_readonly_npy(quality_path) if quality_path.is_file() else None
|
||||||
|
|
||||||
@@ -132,6 +141,16 @@ class ExposureCache:
|
|||||||
raise ValueError("exposure_eid_start.npy must have len(eid_index) + 1")
|
raise ValueError("exposure_eid_start.npy must have len(eid_index) + 1")
|
||||||
if len(self.eid_start) and int(self.eid_start[-1]) != n_rows:
|
if len(self.eid_start) and int(self.eid_start[-1]) != n_rows:
|
||||||
raise ValueError("Last exposure eid offset must equal exposure row count")
|
raise ValueError("Last exposure eid offset must equal exposure row count")
|
||||||
|
if self.embeddings is not None:
|
||||||
|
if self.embeddings.ndim != 2:
|
||||||
|
raise ValueError(
|
||||||
|
"Exposure embeddings must have shape (N, D), got "
|
||||||
|
f"{self.embeddings.shape}"
|
||||||
|
)
|
||||||
|
if max_window_index >= len(self.embeddings):
|
||||||
|
raise ValueError(
|
||||||
|
"Exposure row index points past exposure embeddings"
|
||||||
|
)
|
||||||
|
|
||||||
self._eid_to_pos = {
|
self._eid_to_pos = {
|
||||||
int(eid): idx
|
int(eid): idx
|
||||||
@@ -203,6 +222,20 @@ class ExposureCache:
|
|||||||
def monthly_windows(self, indices: np.ndarray) -> np.ndarray:
|
def monthly_windows(self, indices: np.ndarray) -> np.ndarray:
|
||||||
return self._windows("monthly", indices)
|
return self._windows("monthly", indices)
|
||||||
|
|
||||||
|
def embedding_windows(self, indices: np.ndarray) -> np.ndarray:
|
||||||
|
if self.embeddings is None:
|
||||||
|
raise RuntimeError("Exposure embeddings are not enabled")
|
||||||
|
indices = np.asarray(indices, dtype=np.int64)
|
||||||
|
out = np.zeros(
|
||||||
|
(len(indices), self.embeddings.shape[1]), dtype=np.float32
|
||||||
|
)
|
||||||
|
valid_pos = np.nonzero(indices >= 0)[0]
|
||||||
|
if len(valid_pos):
|
||||||
|
out[valid_pos] = np.asarray(
|
||||||
|
self.embeddings[indices[valid_pos]], dtype=np.float32
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
def _windows(
|
def _windows(
|
||||||
self,
|
self,
|
||||||
kind: Literal["daily", "monthly"],
|
kind: Literal["daily", "monthly"],
|
||||||
@@ -288,18 +321,21 @@ class _ExpoBaseDataset(Dataset):
|
|||||||
no_event_interval_years: float = 5.0,
|
no_event_interval_years: float = 5.0,
|
||||||
include_no_event_in_uts_target: bool = False,
|
include_no_event_in_uts_target: bool = False,
|
||||||
exposure_cache_dir: str | Path | None = None,
|
exposure_cache_dir: str | Path | None = None,
|
||||||
mask_onset_exposure: bool = False,
|
exposure_embeddings_file: str | Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.data_prefix = data_prefix
|
self.data_prefix = data_prefix
|
||||||
self.labels_file = labels_file
|
self.labels_file = labels_file
|
||||||
self.no_event_interval_years = float(no_event_interval_years)
|
self.no_event_interval_years = float(no_event_interval_years)
|
||||||
self.include_no_event_in_uts_target = bool(include_no_event_in_uts_target)
|
self.include_no_event_in_uts_target = bool(include_no_event_in_uts_target)
|
||||||
self.exposure_cache = (
|
self.exposure_cache = (
|
||||||
ExposureCache(exposure_cache_dir)
|
ExposureCache(exposure_cache_dir, exposure_embeddings_file)
|
||||||
if exposure_cache_dir is not None
|
if exposure_cache_dir is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
self.mask_onset_exposure = bool(mask_onset_exposure)
|
if exposure_embeddings_file is not None and exposure_cache_dir is None:
|
||||||
|
raise ValueError(
|
||||||
|
"exposure_cache_dir is required with exposure_embeddings_file"
|
||||||
|
)
|
||||||
|
|
||||||
self.label_code_to_id, self.label_id_to_code = load_label_vocab(
|
self.label_code_to_id, self.label_id_to_code = load_label_vocab(
|
||||||
labels_file,
|
labels_file,
|
||||||
@@ -427,24 +463,14 @@ class _ExpoBaseDataset(Dataset):
|
|||||||
age_days=input_times_days,
|
age_days=input_times_days,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_exposure_windows(self, exposure_index: np.ndarray) -> tuple[torch.Tensor, torch.Tensor]:
|
def _load_exposure_embeddings(
|
||||||
|
self, exposure_index: np.ndarray
|
||||||
|
) -> torch.Tensor:
|
||||||
if self.exposure_cache is None:
|
if self.exposure_cache is None:
|
||||||
raise RuntimeError("Exposure cache is not enabled")
|
raise RuntimeError("Exposure cache is not enabled")
|
||||||
|
return torch.from_numpy(
|
||||||
daily = self.exposure_cache.daily_windows(exposure_index).astype(
|
self.exposure_cache.embedding_windows(exposure_index)
|
||||||
np.float32,
|
).float()
|
||||||
copy=False,
|
|
||||||
)
|
|
||||||
monthly = self.exposure_cache.monthly_windows(exposure_index).astype(
|
|
||||||
np.float32,
|
|
||||||
copy=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.mask_onset_exposure:
|
|
||||||
daily[:, 0, :] = np.nan
|
|
||||||
monthly[:, 0, :] = np.nan
|
|
||||||
|
|
||||||
return torch.from_numpy(daily).float(), torch.from_numpy(monthly).float()
|
|
||||||
|
|
||||||
class NextStepHealthDataset(_ExpoBaseDataset):
|
class NextStepHealthDataset(_ExpoBaseDataset):
|
||||||
"""
|
"""
|
||||||
@@ -465,7 +491,7 @@ class NextStepHealthDataset(_ExpoBaseDataset):
|
|||||||
no_event_interval_years: float = 5.0,
|
no_event_interval_years: float = 5.0,
|
||||||
include_no_event_in_uts_target: bool = False,
|
include_no_event_in_uts_target: bool = False,
|
||||||
exposure_cache_dir: str | Path | None = None,
|
exposure_cache_dir: str | Path | None = None,
|
||||||
mask_onset_exposure: bool = False,
|
exposure_embeddings_file: str | Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
data_prefix=data_prefix,
|
data_prefix=data_prefix,
|
||||||
@@ -473,7 +499,7 @@ class NextStepHealthDataset(_ExpoBaseDataset):
|
|||||||
no_event_interval_years=no_event_interval_years,
|
no_event_interval_years=no_event_interval_years,
|
||||||
include_no_event_in_uts_target=include_no_event_in_uts_target,
|
include_no_event_in_uts_target=include_no_event_in_uts_target,
|
||||||
exposure_cache_dir=exposure_cache_dir,
|
exposure_cache_dir=exposure_cache_dir,
|
||||||
mask_onset_exposure=mask_onset_exposure,
|
exposure_embeddings_file=exposure_embeddings_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.samples: List[Dict] = []
|
self.samples: List[Dict] = []
|
||||||
@@ -531,9 +557,9 @@ class NextStepHealthDataset(_ExpoBaseDataset):
|
|||||||
"target_multi_hot": torch.from_numpy(s["target_multi_hot"]).bool(),
|
"target_multi_hot": torch.from_numpy(s["target_multi_hot"]).bool(),
|
||||||
}
|
}
|
||||||
if "exposure_index" in s:
|
if "exposure_index" in s:
|
||||||
daily, monthly = self._load_exposure_windows(s["exposure_index"])
|
out["exposure_embedding"] = self._load_exposure_embeddings(
|
||||||
out["exposure_daily"] = daily
|
s["exposure_index"]
|
||||||
out["exposure_monthly"] = monthly
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -561,7 +587,7 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
|||||||
min_future_events: int = 1,
|
min_future_events: int = 1,
|
||||||
validation_query_seed: int = 42,
|
validation_query_seed: int = 42,
|
||||||
exposure_cache_dir: str | Path | None = None,
|
exposure_cache_dir: str | Path | None = None,
|
||||||
mask_onset_exposure: bool = False,
|
exposure_embeddings_file: str | Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if split not in {"train", "valid", "test"}:
|
if split not in {"train", "valid", "test"}:
|
||||||
raise ValueError(f"split must be train/valid/test, got {split!r}")
|
raise ValueError(f"split must be train/valid/test, got {split!r}")
|
||||||
@@ -572,7 +598,7 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
|||||||
no_event_interval_years=no_event_interval_years,
|
no_event_interval_years=no_event_interval_years,
|
||||||
include_no_event_in_uts_target=include_no_event_in_uts_target,
|
include_no_event_in_uts_target=include_no_event_in_uts_target,
|
||||||
exposure_cache_dir=exposure_cache_dir,
|
exposure_cache_dir=exposure_cache_dir,
|
||||||
mask_onset_exposure=mask_onset_exposure,
|
exposure_embeddings_file=exposure_embeddings_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.split = split
|
self.split = split
|
||||||
@@ -719,9 +745,9 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
|||||||
input_times_days=times_days[hist],
|
input_times_days=times_days[hist],
|
||||||
)
|
)
|
||||||
if exposure_index is not None:
|
if exposure_index is not None:
|
||||||
daily, monthly = self._load_exposure_windows(exposure_index)
|
out["exposure_embedding"] = self._load_exposure_embeddings(
|
||||||
out["exposure_daily"] = daily
|
exposure_index
|
||||||
out["exposure_monthly"] = monthly
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
@@ -745,19 +771,20 @@ def _collate_common_static(batch: List[Dict]) -> Dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _pad_exposure(batch: List[Dict], key: str, shape: tuple[int, int]) -> torch.Tensor:
|
def _pad_exposure_embedding(batch: List[Dict]) -> torch.Tensor:
|
||||||
max_len = max(int(s["event_seq"].numel()) for s in batch)
|
max_len = max(int(s["event_seq"].numel()) for s in batch)
|
||||||
out = torch.full(
|
embedding_dim = next(
|
||||||
(len(batch), max_len, shape[0], shape[1]),
|
int(s["exposure_embedding"].size(1))
|
||||||
float("nan"),
|
for s in batch
|
||||||
dtype=torch.float32,
|
if "exposure_embedding" in s
|
||||||
|
)
|
||||||
|
out = torch.zeros(
|
||||||
|
len(batch), max_len, embedding_dim, dtype=torch.float32
|
||||||
)
|
)
|
||||||
for idx, sample in enumerate(batch):
|
for idx, sample in enumerate(batch):
|
||||||
value = sample.get(key)
|
value = sample.get("exposure_embedding")
|
||||||
if value is None:
|
if value is not None:
|
||||||
continue
|
out[idx, :value.size(0)] = value
|
||||||
seq_len = int(value.size(0))
|
|
||||||
out[idx, :seq_len] = value
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -809,9 +836,8 @@ def next_step_collate_fn(batch: List[Dict]) -> Dict:
|
|||||||
"target_multi_hot": target_multi_hot,
|
"target_multi_hot": target_multi_hot,
|
||||||
}
|
}
|
||||||
out.update(_collate_common_static(batch))
|
out.update(_collate_common_static(batch))
|
||||||
if any("exposure_daily" in s for s in batch):
|
if any("exposure_embedding" in s for s in batch):
|
||||||
out["exposure_daily"] = _pad_exposure(batch, "exposure_daily", DAILY_EXPOSURE_SHAPE)
|
out["exposure_embedding"] = _pad_exposure_embedding(batch)
|
||||||
out["exposure_monthly"] = _pad_exposure(batch, "exposure_monthly", MONTHLY_EXPOSURE_SHAPE)
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -847,9 +873,8 @@ def all_future_collate_fn(batch: List[Dict]) -> Dict:
|
|||||||
"exposure": torch.stack([s["exposure"] for s in batch]),
|
"exposure": torch.stack([s["exposure"] for s in batch]),
|
||||||
}
|
}
|
||||||
out.update(_collate_common_static(batch))
|
out.update(_collate_common_static(batch))
|
||||||
if any("exposure_daily" in s for s in batch):
|
if any("exposure_embedding" in s for s in batch):
|
||||||
out["exposure_daily"] = _pad_exposure(batch, "exposure_daily", DAILY_EXPOSURE_SHAPE)
|
out["exposure_embedding"] = _pad_exposure_embedding(batch)
|
||||||
out["exposure_monthly"] = _pad_exposure(batch, "exposure_monthly", MONTHLY_EXPOSURE_SHAPE)
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
147
encode_exposure_cache.py
Normal file
147
encode_exposure_cache.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""Encode cached exposure windows once for embedding-only model training."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from torch.utils.data import DataLoader, Dataset
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from backbones import TimesNetExposureEncoder
|
||||||
|
from dataset import ExposureCache
|
||||||
|
from train_util import configure_torch_for_training, resolve_device
|
||||||
|
|
||||||
|
|
||||||
|
class ExposureEncodingDataset(Dataset):
|
||||||
|
def __init__(self, cache: ExposureCache):
|
||||||
|
self.cache = cache
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.cache.daily)
|
||||||
|
|
||||||
|
def __getitem__(self, index: int) -> tuple[int, torch.Tensor, torch.Tensor]:
|
||||||
|
return (
|
||||||
|
index,
|
||||||
|
torch.from_numpy(
|
||||||
|
np.array(self.cache.daily[index], dtype=np.float32, copy=True)
|
||||||
|
),
|
||||||
|
torch.from_numpy(
|
||||||
|
np.array(self.cache.monthly[index], dtype=np.float32, copy=True)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Precompute exposure embeddings from an autoencoder checkpoint"
|
||||||
|
)
|
||||||
|
parser.add_argument("--exposure_cache_dir", required=True)
|
||||||
|
parser.add_argument("--checkpoint", required=True)
|
||||||
|
parser.add_argument("--output_file", default=None)
|
||||||
|
parser.add_argument("--batch_size", type=int, default=64)
|
||||||
|
parser.add_argument("--num_workers", type=int, default=4)
|
||||||
|
parser.add_argument("--device", default="cuda")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output_dtype", choices=["float16", "float32"], default="float16"
|
||||||
|
)
|
||||||
|
parser.add_argument("--overwrite", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.batch_size <= 0:
|
||||||
|
parser.error("--batch_size must be positive")
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
device = resolve_device(args.device)
|
||||||
|
configure_torch_for_training(device)
|
||||||
|
cache_dir = Path(args.exposure_cache_dir)
|
||||||
|
output_path = (
|
||||||
|
Path(args.output_file)
|
||||||
|
if args.output_file
|
||||||
|
else cache_dir / "exposure_embeddings.npy"
|
||||||
|
)
|
||||||
|
if output_path.exists() and not args.overwrite:
|
||||||
|
raise FileExistsError(f"{output_path} exists; pass --overwrite")
|
||||||
|
|
||||||
|
checkpoint_data = torch.load(args.checkpoint, map_location="cpu")
|
||||||
|
model_cfg = checkpoint_data["model_config"]
|
||||||
|
normalization = checkpoint_data["normalization"]
|
||||||
|
encoder = TimesNetExposureEncoder(
|
||||||
|
n_embd=int(model_cfg["n_embd"]),
|
||||||
|
d_model=model_cfg["d_model"],
|
||||||
|
n_layers=int(model_cfg["n_layers"]),
|
||||||
|
top_k=int(model_cfg["top_k"]),
|
||||||
|
n_backbone_blocks=int(model_cfg["n_backbone_blocks"]),
|
||||||
|
backbone_kernel_size=int(model_cfg["backbone_kernel_size"]),
|
||||||
|
backbone_expansion=float(model_cfg["backbone_expansion"]),
|
||||||
|
dropout=float(model_cfg["dropout"]),
|
||||||
|
use_gate=True,
|
||||||
|
)
|
||||||
|
encoder.load_state_dict(checkpoint_data["encoder_state_dict"], strict=True)
|
||||||
|
encoder.to(device).eval()
|
||||||
|
|
||||||
|
cache = ExposureCache(cache_dir)
|
||||||
|
loader_kwargs = {
|
||||||
|
"batch_size": args.batch_size,
|
||||||
|
"shuffle": False,
|
||||||
|
"num_workers": args.num_workers,
|
||||||
|
"pin_memory": device.type == "cuda",
|
||||||
|
"persistent_workers": args.num_workers > 0,
|
||||||
|
}
|
||||||
|
loader = DataLoader(ExposureEncodingDataset(cache), **loader_kwargs)
|
||||||
|
output_dtype = np.float16 if args.output_dtype == "float16" else np.float32
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output = np.lib.format.open_memmap(
|
||||||
|
output_path,
|
||||||
|
mode="w+",
|
||||||
|
dtype=output_dtype,
|
||||||
|
shape=(len(cache.daily), int(model_cfg["n_embd"])),
|
||||||
|
)
|
||||||
|
stats = {
|
||||||
|
key: torch.as_tensor(normalization[key], device=device).view(1, 1, -1)
|
||||||
|
for key in ("daily_mean", "daily_std", "monthly_mean", "monthly_std")
|
||||||
|
}
|
||||||
|
|
||||||
|
with torch.inference_mode():
|
||||||
|
for indices, daily, monthly in tqdm(loader, desc="Encoding exposure"):
|
||||||
|
daily = daily.to(device, non_blocking=True)
|
||||||
|
monthly = monthly.to(device, non_blocking=True)
|
||||||
|
daily_mask = torch.isfinite(daily)
|
||||||
|
monthly_mask = torch.isfinite(monthly)
|
||||||
|
has_observation = (
|
||||||
|
daily_mask.flatten(1).any(dim=1)
|
||||||
|
| monthly_mask.flatten(1).any(dim=1)
|
||||||
|
)
|
||||||
|
daily = (
|
||||||
|
torch.nan_to_num(daily) - stats["daily_mean"]
|
||||||
|
) / stats["daily_std"]
|
||||||
|
monthly = (
|
||||||
|
torch.nan_to_num(monthly) - stats["monthly_mean"]
|
||||||
|
) / stats["monthly_std"]
|
||||||
|
daily = daily * daily_mask
|
||||||
|
monthly = monthly * monthly_mask
|
||||||
|
encoded = encoder(daily, monthly, daily_mask, monthly_mask)
|
||||||
|
encoded = encoded * has_observation.unsqueeze(1)
|
||||||
|
output[indices.numpy()] = encoded.float().cpu().numpy().astype(
|
||||||
|
output_dtype, copy=False
|
||||||
|
)
|
||||||
|
output.flush()
|
||||||
|
metadata = {
|
||||||
|
"checkpoint": str(Path(args.checkpoint).resolve()),
|
||||||
|
"output_file": str(output_path.resolve()),
|
||||||
|
"rows": len(cache.daily),
|
||||||
|
"embedding_dim": int(model_cfg["n_embd"]),
|
||||||
|
"dtype": args.output_dtype,
|
||||||
|
}
|
||||||
|
output_path.with_suffix(".json").write_text(
|
||||||
|
json.dumps(metadata, indent=2), encoding="utf-8"
|
||||||
|
)
|
||||||
|
print(f"Wrote {len(cache.daily):,} embeddings to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
57
eval_data.py
57
eval_data.py
@@ -8,8 +8,6 @@ import torch
|
|||||||
from torch.nn.utils.rnn import pad_sequence
|
from torch.nn.utils.rnn import pad_sequence
|
||||||
|
|
||||||
from dataset import (
|
from dataset import (
|
||||||
DAILY_EXPOSURE_SHAPE,
|
|
||||||
MONTHLY_EXPOSURE_SHAPE,
|
|
||||||
AllFutureHealthDataset,
|
AllFutureHealthDataset,
|
||||||
HealthDataset,
|
HealthDataset,
|
||||||
)
|
)
|
||||||
@@ -32,7 +30,7 @@ class AllFutureSequenceEvalDataset:
|
|||||||
min_history_events: int = 1,
|
min_history_events: int = 1,
|
||||||
min_future_events: int = 1,
|
min_future_events: int = 1,
|
||||||
exposure_cache_dir: str | Path | None = None,
|
exposure_cache_dir: str | Path | None = None,
|
||||||
mask_onset_exposure: bool = False,
|
exposure_embeddings_file: str | Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
base = AllFutureHealthDataset(
|
base = AllFutureHealthDataset(
|
||||||
data_prefix=data_prefix,
|
data_prefix=data_prefix,
|
||||||
@@ -41,7 +39,7 @@ class AllFutureSequenceEvalDataset:
|
|||||||
min_history_events=min_history_events,
|
min_history_events=min_history_events,
|
||||||
min_future_events=min_future_events,
|
min_future_events=min_future_events,
|
||||||
exposure_cache_dir=exposure_cache_dir,
|
exposure_cache_dir=exposure_cache_dir,
|
||||||
mask_onset_exposure=mask_onset_exposure,
|
exposure_embeddings_file=exposure_embeddings_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.base = base
|
self.base = base
|
||||||
@@ -91,9 +89,9 @@ class AllFutureSequenceEvalDataset:
|
|||||||
"sex": torch.tensor(s["sex"], dtype=torch.long),
|
"sex": torch.tensor(s["sex"], dtype=torch.long),
|
||||||
}
|
}
|
||||||
if "exposure_index" in s:
|
if "exposure_index" in s:
|
||||||
daily, monthly = self.base._load_exposure_windows(s["exposure_index"])
|
out["exposure_embedding"] = self.base._load_exposure_embeddings(
|
||||||
out["exposure_daily"] = daily
|
s["exposure_index"]
|
||||||
out["exposure_monthly"] = monthly
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -107,7 +105,7 @@ def load_sequence_eval_dataset(
|
|||||||
min_history_events: int,
|
min_history_events: int,
|
||||||
min_future_events: int,
|
min_future_events: int,
|
||||||
exposure_cache_dir: str | Path | None = None,
|
exposure_cache_dir: str | Path | None = None,
|
||||||
mask_onset_exposure: bool = False,
|
exposure_embeddings_file: str | Path | None = None,
|
||||||
):
|
):
|
||||||
mode = str(model_target_mode).lower()
|
mode = str(model_target_mode).lower()
|
||||||
if mode == "next_token":
|
if mode == "next_token":
|
||||||
@@ -117,7 +115,7 @@ def load_sequence_eval_dataset(
|
|||||||
no_event_interval_years=no_event_interval_years,
|
no_event_interval_years=no_event_interval_years,
|
||||||
include_no_event_in_uts_target=include_no_event_in_uts_target,
|
include_no_event_in_uts_target=include_no_event_in_uts_target,
|
||||||
exposure_cache_dir=exposure_cache_dir,
|
exposure_cache_dir=exposure_cache_dir,
|
||||||
mask_onset_exposure=mask_onset_exposure,
|
exposure_embeddings_file=exposure_embeddings_file,
|
||||||
)
|
)
|
||||||
if mode == "all_future":
|
if mode == "all_future":
|
||||||
return AllFutureSequenceEvalDataset(
|
return AllFutureSequenceEvalDataset(
|
||||||
@@ -126,7 +124,7 @@ def load_sequence_eval_dataset(
|
|||||||
min_history_events=min_history_events,
|
min_history_events=min_history_events,
|
||||||
min_future_events=min_future_events,
|
min_future_events=min_future_events,
|
||||||
exposure_cache_dir=exposure_cache_dir,
|
exposure_cache_dir=exposure_cache_dir,
|
||||||
mask_onset_exposure=mask_onset_exposure,
|
exposure_embeddings_file=exposure_embeddings_file,
|
||||||
)
|
)
|
||||||
raise ValueError(f"Unknown model_target_mode: {model_target_mode!r}")
|
raise ValueError(f"Unknown model_target_mode: {model_target_mode!r}")
|
||||||
|
|
||||||
@@ -156,34 +154,17 @@ def sequence_eval_collate_fn(batch: List[Dict[str, torch.Tensor]]) -> Dict[str,
|
|||||||
"readout_mask": readout_mask,
|
"readout_mask": readout_mask,
|
||||||
"sex": torch.stack([s["sex"] for s in batch]),
|
"sex": torch.stack([s["sex"] for s in batch]),
|
||||||
}
|
}
|
||||||
if any("exposure_daily" in s for s in batch):
|
if any("exposure_embedding" in s for s in batch):
|
||||||
out["exposure_daily"] = _pad_eval_exposure(
|
embedding_dim = next(
|
||||||
batch,
|
int(s["exposure_embedding"].size(1))
|
||||||
"exposure_daily",
|
for s in batch if "exposure_embedding" in s
|
||||||
DAILY_EXPOSURE_SHAPE,
|
|
||||||
)
|
)
|
||||||
out["exposure_monthly"] = _pad_eval_exposure(
|
encoded = torch.zeros(
|
||||||
batch,
|
len(batch), event_seq.size(1), embedding_dim, dtype=torch.float32
|
||||||
"exposure_monthly",
|
|
||||||
MONTHLY_EXPOSURE_SHAPE,
|
|
||||||
)
|
)
|
||||||
return out
|
for idx, sample in enumerate(batch):
|
||||||
|
value = sample.get("exposure_embedding")
|
||||||
|
if value is not None:
|
||||||
def _pad_eval_exposure(
|
encoded[idx, :value.size(0)] = value
|
||||||
batch: List[Dict[str, torch.Tensor]],
|
out["exposure_embedding"] = encoded
|
||||||
key: str,
|
|
||||||
shape: tuple[int, int],
|
|
||||||
) -> torch.Tensor:
|
|
||||||
max_len = max(int(s["event_seq"].numel()) for s in batch)
|
|
||||||
out = torch.full(
|
|
||||||
(len(batch), max_len, shape[0], shape[1]),
|
|
||||||
float("nan"),
|
|
||||||
dtype=torch.float32,
|
|
||||||
)
|
|
||||||
for idx, sample in enumerate(batch):
|
|
||||||
value = sample.get(key)
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
out[idx, : int(value.size(0))] = value
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -323,14 +323,9 @@ def build_model_from_dataset(args: argparse.Namespace, cfg: Dict[str, Any], data
|
|||||||
target_mode=model_target_mode,
|
target_mode=model_target_mode,
|
||||||
dist_mode=str(cfg_get(args, cfg, "dist_mode", "exponential")),
|
dist_mode=str(cfg_get(args, cfg, "dist_mode", "exponential")),
|
||||||
dropout=float(cfg_get(args, cfg, "dropout", 0.0)),
|
dropout=float(cfg_get(args, cfg, "dropout", 0.0)),
|
||||||
use_exposure_encoder=bool(cfg_get(args, cfg, "use_exposure_encoder", False)),
|
use_exposure_embeddings=bool(
|
||||||
exposure_d_model=cfg_get(args, cfg, "d_model", 64),
|
cfg_get(args, cfg, "use_exposure_embeddings", False)
|
||||||
exposure_n_layers=int(cfg_get(args, cfg, "exposure_n_layers", 2)),
|
),
|
||||||
exposure_top_k=int(cfg_get(args, cfg, "exposure_top_k", 2)),
|
|
||||||
exposure_n_backbone_blocks=int(cfg_get(args, cfg, "exposure_n_backbone_blocks", 1)),
|
|
||||||
exposure_backbone_kernel_size=int(cfg_get(args, cfg, "exposure_backbone_kernel_size", 5)),
|
|
||||||
exposure_backbone_expansion=float(cfg_get(args, cfg, "exposure_backbone_expansion", 2.0)),
|
|
||||||
exposure_use_gate=bool(cfg_get(args, cfg, "exposure_use_gate", True)),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -531,14 +526,9 @@ def infer_readout_hidden(
|
|||||||
sex=batch_dev["sex"][active],
|
sex=batch_dev["sex"][active],
|
||||||
padding_mask=padding_mask[active],
|
padding_mask=padding_mask[active],
|
||||||
t_query=time_seq[active, pos],
|
t_query=time_seq[active, pos],
|
||||||
exposure_daily=(
|
exposure_embedding=(
|
||||||
batch_dev["exposure_daily"][active]
|
batch_dev["exposure_embedding"][active]
|
||||||
if "exposure_daily" in batch_dev
|
if "exposure_embedding" in batch_dev
|
||||||
else None
|
|
||||||
),
|
|
||||||
exposure_monthly=(
|
|
||||||
batch_dev["exposure_monthly"][active]
|
|
||||||
if "exposure_monthly" in batch_dev
|
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
target_mode="all_future",
|
target_mode="all_future",
|
||||||
@@ -551,8 +541,7 @@ def infer_readout_hidden(
|
|||||||
time_seq=time_seq,
|
time_seq=time_seq,
|
||||||
sex=batch_dev["sex"],
|
sex=batch_dev["sex"],
|
||||||
padding_mask=padding_mask,
|
padding_mask=padding_mask,
|
||||||
exposure_daily=batch_dev.get("exposure_daily"),
|
exposure_embedding=batch_dev.get("exposure_embedding"),
|
||||||
exposure_monthly=batch_dev.get("exposure_monthly"),
|
|
||||||
target_mode="next_token",
|
target_mode="next_token",
|
||||||
)
|
)
|
||||||
ro = readout(
|
ro = readout(
|
||||||
@@ -1337,7 +1326,7 @@ def main() -> None:
|
|||||||
min_history_events=int(cfg.get("all_future_min_history_events", 1)),
|
min_history_events=int(cfg.get("all_future_min_history_events", 1)),
|
||||||
min_future_events=int(cfg.get("all_future_min_future_events", 1)),
|
min_future_events=int(cfg.get("all_future_min_future_events", 1)),
|
||||||
exposure_cache_dir=cfg.get("exposure_cache_dir", None),
|
exposure_cache_dir=cfg.get("exposure_cache_dir", None),
|
||||||
mask_onset_exposure=bool(cfg.get("mask_onset_exposure", False)),
|
exposure_embeddings_file=cfg.get("exposure_embeddings_file", None),
|
||||||
)
|
)
|
||||||
validate_dataset_metadata(dataset, cfg)
|
validate_dataset_metadata(dataset, cfg)
|
||||||
|
|
||||||
|
|||||||
153
models.py
153
models.py
@@ -7,7 +7,6 @@ import torch.nn.functional as F
|
|||||||
from backbones import (
|
from backbones import (
|
||||||
AgeSinusoidalEncoding,
|
AgeSinusoidalEncoding,
|
||||||
GPTBlock,
|
GPTBlock,
|
||||||
TimesNetExposureEncoder,
|
|
||||||
)
|
)
|
||||||
from targets import PAD_IDX
|
from targets import PAD_IDX
|
||||||
|
|
||||||
@@ -30,16 +29,7 @@ class DeepHealth(nn.Module):
|
|||||||
target_mode: str = "next_token", # "next_token" or "all_future"
|
target_mode: str = "next_token", # "next_token" or "all_future"
|
||||||
dist_mode: str = "exponential", # "exponential", "weibull" or "mixed"
|
dist_mode: str = "exponential", # "exponential", "weibull" or "mixed"
|
||||||
dropout: float = 0.0,
|
dropout: float = 0.0,
|
||||||
use_exposure_encoder: bool = False,
|
use_exposure_embeddings: bool = False,
|
||||||
exposure_daily_input_dim: int = 4,
|
|
||||||
exposure_monthly_input_dim: int = 2,
|
|
||||||
exposure_d_model: int | None = 64,
|
|
||||||
exposure_n_layers: int = 2,
|
|
||||||
exposure_top_k: int = 2,
|
|
||||||
exposure_n_backbone_blocks: int = 1,
|
|
||||||
exposure_backbone_kernel_size: int = 5,
|
|
||||||
exposure_backbone_expansion: float = 2.0,
|
|
||||||
exposure_use_gate: bool = True,
|
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if target_mode not in ["next_token", "all_future"]:
|
if target_mode not in ["next_token", "all_future"]:
|
||||||
@@ -53,26 +43,9 @@ class DeepHealth(nn.Module):
|
|||||||
2, n_embd) # Assuming binary gender
|
2, n_embd) # Assuming binary gender
|
||||||
self.target_mode = target_mode
|
self.target_mode = target_mode
|
||||||
self.dist_mode = dist_mode
|
self.dist_mode = dist_mode
|
||||||
self.use_exposure_encoder = use_exposure_encoder
|
self.use_exposure_embeddings = bool(use_exposure_embeddings)
|
||||||
self.n_embd = n_embd
|
self.n_embd = n_embd
|
||||||
self.vocab_size = vocab_size
|
self.vocab_size = vocab_size
|
||||||
self.exposure_encoder = (
|
|
||||||
TimesNetExposureEncoder(
|
|
||||||
n_embd=n_embd,
|
|
||||||
daily_input_dim=exposure_daily_input_dim,
|
|
||||||
monthly_input_dim=exposure_monthly_input_dim,
|
|
||||||
d_model=exposure_d_model,
|
|
||||||
n_layers=exposure_n_layers,
|
|
||||||
top_k=exposure_top_k,
|
|
||||||
n_backbone_blocks=exposure_n_backbone_blocks,
|
|
||||||
backbone_kernel_size=exposure_backbone_kernel_size,
|
|
||||||
backbone_expansion=exposure_backbone_expansion,
|
|
||||||
dropout=dropout,
|
|
||||||
use_gate=exposure_use_gate,
|
|
||||||
)
|
|
||||||
if use_exposure_encoder
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
nn.init.normal_(self.token_embedding.weight, mean=0.0, std=0.02)
|
nn.init.normal_(self.token_embedding.weight, mean=0.0, std=0.02)
|
||||||
nn.init.zeros_(self.token_embedding.weight[0])
|
nn.init.zeros_(self.token_embedding.weight[0])
|
||||||
nn.init.normal_(self.gender_embedding.weight, mean=0.0, std=0.02)
|
nn.init.normal_(self.gender_embedding.weight, mean=0.0, std=0.02)
|
||||||
@@ -121,95 +94,6 @@ class DeepHealth(nn.Module):
|
|||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
).masked_fill(~valid, -1e4)[:, None, :, :]
|
).masked_fill(~valid, -1e4)[:, None, :, :]
|
||||||
|
|
||||||
def _encode_event_exposure(
|
|
||||||
self,
|
|
||||||
exposure_daily: torch.Tensor | None,
|
|
||||||
exposure_monthly: torch.Tensor | None,
|
|
||||||
exposure_daily_mask: torch.Tensor | None,
|
|
||||||
exposure_monthly_mask: torch.Tensor | None,
|
|
||||||
event_shape: tuple[int, int],
|
|
||||||
) -> torch.Tensor | None:
|
|
||||||
if self.exposure_encoder is None:
|
|
||||||
if exposure_daily is not None or exposure_monthly is not None:
|
|
||||||
raise ValueError(
|
|
||||||
"Exposure tensors were provided but use_exposure_encoder=False"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
if exposure_daily is None or exposure_monthly is None:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure_daily and exposure_monthly are required when "
|
|
||||||
"use_exposure_encoder=True"
|
|
||||||
)
|
|
||||||
|
|
||||||
batch_size, event_len = event_shape
|
|
||||||
if exposure_daily.shape[:2] != event_shape:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure_daily must have shape (B, L, T_daily, C_daily), got "
|
|
||||||
f"{tuple(exposure_daily.shape)} for event shape {event_shape}"
|
|
||||||
)
|
|
||||||
if exposure_monthly.shape[:2] != event_shape:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure_monthly must have shape (B, L, T_monthly, C_monthly), got "
|
|
||||||
f"{tuple(exposure_monthly.shape)} for event shape {event_shape}"
|
|
||||||
)
|
|
||||||
if exposure_daily.dim() != 4 or exposure_monthly.dim() != 4:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure_daily and exposure_monthly must both be 4D tensors"
|
|
||||||
)
|
|
||||||
|
|
||||||
def flatten_mask(mask: torch.Tensor | None, ref: torch.Tensor) -> torch.Tensor | None:
|
|
||||||
if mask is None:
|
|
||||||
return None
|
|
||||||
if mask.shape[:2] != event_shape:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure mask must start with shape (B, L), got "
|
|
||||||
f"{tuple(mask.shape)}"
|
|
||||||
)
|
|
||||||
if mask.dim() not in {3, 4}:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure mask must have shape (B, L, T) or (B, L, T, C), "
|
|
||||||
f"got {tuple(mask.shape)}"
|
|
||||||
)
|
|
||||||
if mask.shape[2] != ref.shape[2]:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure mask time dimension does not match exposure tensor"
|
|
||||||
)
|
|
||||||
if mask.dim() == 4 and mask.shape[3] != ref.shape[3]:
|
|
||||||
raise ValueError(
|
|
||||||
"exposure mask channel dimension does not match exposure tensor"
|
|
||||||
)
|
|
||||||
return mask.reshape(batch_size * event_len, *mask.shape[2:])
|
|
||||||
|
|
||||||
daily = exposure_daily.reshape(
|
|
||||||
batch_size * event_len,
|
|
||||||
exposure_daily.size(2),
|
|
||||||
exposure_daily.size(3),
|
|
||||||
)
|
|
||||||
monthly = exposure_monthly.reshape(
|
|
||||||
batch_size * event_len,
|
|
||||||
exposure_monthly.size(2),
|
|
||||||
exposure_monthly.size(3),
|
|
||||||
)
|
|
||||||
daily_mask = flatten_mask(exposure_daily_mask, exposure_daily)
|
|
||||||
monthly_mask = flatten_mask(exposure_monthly_mask, exposure_monthly)
|
|
||||||
|
|
||||||
exposure_device = exposure_daily.device
|
|
||||||
exposure_dtype = self.token_embedding.weight.dtype
|
|
||||||
daily = daily.to(device=exposure_device, dtype=exposure_dtype)
|
|
||||||
monthly = monthly.to(device=exposure_device, dtype=exposure_dtype)
|
|
||||||
if daily_mask is not None:
|
|
||||||
daily_mask = daily_mask.to(device=exposure_device)
|
|
||||||
if monthly_mask is not None:
|
|
||||||
monthly_mask = monthly_mask.to(device=exposure_device)
|
|
||||||
|
|
||||||
exposure_emb = self.exposure_encoder(
|
|
||||||
daily=daily,
|
|
||||||
monthly=monthly,
|
|
||||||
daily_mask=daily_mask,
|
|
||||||
monthly_mask=monthly_mask,
|
|
||||||
)
|
|
||||||
return exposure_emb.reshape(batch_size, event_len, self.n_embd)
|
|
||||||
|
|
||||||
def _forward_shared(
|
def _forward_shared(
|
||||||
self,
|
self,
|
||||||
event_seq: torch.LongTensor,
|
event_seq: torch.LongTensor,
|
||||||
@@ -218,10 +102,7 @@ class DeepHealth(nn.Module):
|
|||||||
mode: str,
|
mode: str,
|
||||||
padding_mask: torch.Tensor | None = None,
|
padding_mask: torch.Tensor | None = None,
|
||||||
t_query: torch.FloatTensor | None = None,
|
t_query: torch.FloatTensor | None = None,
|
||||||
exposure_daily: torch.Tensor | None = None,
|
exposure_embedding: torch.Tensor | None = None,
|
||||||
exposure_monthly: torch.Tensor | None = None,
|
|
||||||
exposure_daily_mask: torch.Tensor | None = None,
|
|
||||||
exposure_monthly_mask: torch.Tensor | None = None,
|
|
||||||
return_output: bool = False,
|
return_output: bool = False,
|
||||||
**unused_kwargs,
|
**unused_kwargs,
|
||||||
) -> torch.Tensor | DeepHealthOutput:
|
) -> torch.Tensor | DeepHealthOutput:
|
||||||
@@ -240,16 +121,24 @@ class DeepHealth(nn.Module):
|
|||||||
|
|
||||||
event_len = event_seq.size(1)
|
event_len = event_seq.size(1)
|
||||||
h_disease = self.token_embedding(event_seq)
|
h_disease = self.token_embedding(event_seq)
|
||||||
h_exposure = self._encode_event_exposure(
|
if self.use_exposure_embeddings:
|
||||||
exposure_daily=exposure_daily,
|
if exposure_embedding is None:
|
||||||
exposure_monthly=exposure_monthly,
|
raise ValueError(
|
||||||
exposure_daily_mask=exposure_daily_mask,
|
"exposure_embedding is required when "
|
||||||
exposure_monthly_mask=exposure_monthly_mask,
|
"use_exposure_embeddings=True"
|
||||||
event_shape=(event_seq.size(0), event_len),
|
)
|
||||||
)
|
if exposure_embedding.shape != h_disease.shape:
|
||||||
if h_exposure is not None:
|
raise ValueError(
|
||||||
h_exposure = h_exposure.to(device=event_seq.device, dtype=h_disease.dtype)
|
"exposure_embedding must have shape "
|
||||||
h_disease = h_disease + h_exposure
|
f"{tuple(h_disease.shape)}, got {tuple(exposure_embedding.shape)}"
|
||||||
|
)
|
||||||
|
h_disease = h_disease + exposure_embedding.to(
|
||||||
|
device=h_disease.device, dtype=h_disease.dtype
|
||||||
|
)
|
||||||
|
elif exposure_embedding is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"exposure_embedding provided but use_exposure_embeddings=False"
|
||||||
|
)
|
||||||
t_disease = time_seq
|
t_disease = time_seq
|
||||||
h_disease = h_disease * padding_mask.unsqueeze(-1).to(h_disease.dtype)
|
h_disease = h_disease * padding_mask.unsqueeze(-1).to(h_disease.dtype)
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ MODEL_INPUT_KEYS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
EXPOSURE_INPUT_KEYS = (
|
EXPOSURE_INPUT_KEYS = (
|
||||||
"exposure_daily",
|
"exposure_embedding",
|
||||||
"exposure_monthly",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -174,8 +173,7 @@ class NextStepTrainingModel(nn.Module):
|
|||||||
sex: torch.Tensor,
|
sex: torch.Tensor,
|
||||||
padding_mask: torch.Tensor,
|
padding_mask: torch.Tensor,
|
||||||
readout_mask: torch.Tensor | None = None,
|
readout_mask: torch.Tensor | None = None,
|
||||||
exposure_daily: torch.Tensor | None = None,
|
exposure_embedding: torch.Tensor | None = None,
|
||||||
exposure_monthly: torch.Tensor | None = None,
|
|
||||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
hidden = self.model(
|
hidden = self.model(
|
||||||
event_seq=event_seq,
|
event_seq=event_seq,
|
||||||
@@ -183,8 +181,7 @@ class NextStepTrainingModel(nn.Module):
|
|||||||
sex=sex,
|
sex=sex,
|
||||||
padding_mask=padding_mask,
|
padding_mask=padding_mask,
|
||||||
target_mode="next_token",
|
target_mode="next_token",
|
||||||
exposure_daily=exposure_daily,
|
exposure_embedding=exposure_embedding,
|
||||||
exposure_monthly=exposure_monthly,
|
|
||||||
)
|
)
|
||||||
if not isinstance(hidden, torch.Tensor):
|
if not isinstance(hidden, torch.Tensor):
|
||||||
raise TypeError("DeepHealth forward must return a hidden-state tensor")
|
raise TypeError("DeepHealth forward must return a hidden-state tensor")
|
||||||
@@ -224,19 +221,15 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--n_hist_layer", type=int, default=12)
|
parser.add_argument("--n_hist_layer", type=int, default=12)
|
||||||
parser.add_argument("--dropout", type=float, default=0.0)
|
parser.add_argument("--dropout", type=float, default=0.0)
|
||||||
parser.add_argument("--exposure_cache_dir", type=str, default=None)
|
parser.add_argument("--exposure_cache_dir", type=str, default=None)
|
||||||
parser.add_argument("--mask_onset_exposure", action="store_true")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--d_model",
|
"--exposure_embeddings_file",
|
||||||
type=int,
|
type=str,
|
||||||
default=64,
|
default=None,
|
||||||
help="Internal TimesNet channel dimension for exposure encoding.",
|
help=(
|
||||||
|
"Precomputed exposure embeddings. Defaults to "
|
||||||
|
"<exposure_cache_dir>/exposure_embeddings.npy."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument("--exposure_n_layers", type=int, default=2)
|
|
||||||
parser.add_argument("--exposure_top_k", type=int, default=2)
|
|
||||||
parser.add_argument("--exposure_n_backbone_blocks", type=int, default=1)
|
|
||||||
parser.add_argument("--exposure_backbone_kernel_size", type=int, default=5)
|
|
||||||
parser.add_argument("--exposure_backbone_expansion", type=float, default=2.0)
|
|
||||||
parser.add_argument("--no_exposure_gate", action="store_true")
|
|
||||||
parser.add_argument("--target_mode", type=str, default="uts",
|
parser.add_argument("--target_mode", type=str, default="uts",
|
||||||
choices=["delphi2m", "uts"])
|
choices=["delphi2m", "uts"])
|
||||||
parser.add_argument("--readout_name", type=str, default=None,
|
parser.add_argument("--readout_name", type=str, default=None,
|
||||||
@@ -311,8 +304,10 @@ def parse_args() -> argparse.Namespace:
|
|||||||
raise ValueError("prefetch_factor must be positive when num_workers > 0")
|
raise ValueError("prefetch_factor must be positive when num_workers > 0")
|
||||||
if args.exposure_locality_buffer_size < 0:
|
if args.exposure_locality_buffer_size < 0:
|
||||||
raise ValueError("exposure_locality_buffer_size must be non-negative")
|
raise ValueError("exposure_locality_buffer_size must be non-negative")
|
||||||
if args.d_model <= 0:
|
if args.exposure_embeddings_file and not args.exposure_cache_dir:
|
||||||
raise ValueError("--d_model must be positive")
|
raise ValueError(
|
||||||
|
"--exposure_cache_dir is required with --exposure_embeddings_file"
|
||||||
|
)
|
||||||
if args.target_mode == "uts":
|
if args.target_mode == "uts":
|
||||||
args.readout_name = args.readout_name or "same_time_group_end"
|
args.readout_name = args.readout_name or "same_time_group_end"
|
||||||
args.include_no_event_in_uts_target = True
|
args.include_no_event_in_uts_target = True
|
||||||
@@ -440,14 +435,7 @@ def build_model(args: argparse.Namespace, dataset: HealthDataset) -> DeepHealth:
|
|||||||
target_mode="next_token",
|
target_mode="next_token",
|
||||||
dist_mode="exponential",
|
dist_mode="exponential",
|
||||||
dropout=args.dropout,
|
dropout=args.dropout,
|
||||||
use_exposure_encoder=args.exposure_cache_dir is not None,
|
use_exposure_embeddings=args.exposure_embeddings_file is not None,
|
||||||
exposure_d_model=args.d_model,
|
|
||||||
exposure_n_layers=args.exposure_n_layers,
|
|
||||||
exposure_top_k=args.exposure_top_k,
|
|
||||||
exposure_n_backbone_blocks=args.exposure_n_backbone_blocks,
|
|
||||||
exposure_backbone_kernel_size=args.exposure_backbone_kernel_size,
|
|
||||||
exposure_backbone_expansion=args.exposure_backbone_expansion,
|
|
||||||
exposure_use_gate=not args.no_exposure_gate,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -499,9 +487,8 @@ def compute_next_step_loss(
|
|||||||
"padding_mask": batch["padding_mask"],
|
"padding_mask": batch["padding_mask"],
|
||||||
"readout_mask": batch["readout_mask"],
|
"readout_mask": batch["readout_mask"],
|
||||||
}
|
}
|
||||||
if "exposure_daily" in batch:
|
if "exposure_embedding" in batch:
|
||||||
model_kwargs["exposure_daily"] = batch["exposure_daily"]
|
model_kwargs["exposure_embedding"] = batch["exposure_embedding"]
|
||||||
model_kwargs["exposure_monthly"] = batch["exposure_monthly"]
|
|
||||||
logits, current_times, output_readout_mask = model(**model_kwargs)
|
logits, current_times, output_readout_mask = model(**model_kwargs)
|
||||||
non_blocking = device.type == "cuda"
|
non_blocking = device.type == "cuda"
|
||||||
targets = {
|
targets = {
|
||||||
@@ -631,16 +618,9 @@ def build_metadata(
|
|||||||
"dataset_metadata": {
|
"dataset_metadata": {
|
||||||
"vocab_size": int(dataset.vocab_size),
|
"vocab_size": int(dataset.vocab_size),
|
||||||
},
|
},
|
||||||
"use_exposure_encoder": args.exposure_cache_dir is not None,
|
"use_exposure_embeddings": args.exposure_embeddings_file is not None,
|
||||||
"exposure_cache_dir": args.exposure_cache_dir,
|
"exposure_cache_dir": args.exposure_cache_dir,
|
||||||
"mask_onset_exposure": bool(args.mask_onset_exposure),
|
"exposure_embeddings_file": args.exposure_embeddings_file,
|
||||||
"d_model": int(args.d_model),
|
|
||||||
"exposure_n_layers": int(args.exposure_n_layers),
|
|
||||||
"exposure_top_k": int(args.exposure_top_k),
|
|
||||||
"exposure_n_backbone_blocks": int(args.exposure_n_backbone_blocks),
|
|
||||||
"exposure_backbone_kernel_size": int(args.exposure_backbone_kernel_size),
|
|
||||||
"exposure_backbone_expansion": float(args.exposure_backbone_expansion),
|
|
||||||
"exposure_use_gate": not bool(args.no_exposure_gate),
|
|
||||||
"num_workers": int(args.num_workers),
|
"num_workers": int(args.num_workers),
|
||||||
"prefetch_factor": int(args.prefetch_factor),
|
"prefetch_factor": int(args.prefetch_factor),
|
||||||
"exposure_locality_buffer_size": int(args.exposure_locality_buffer_size),
|
"exposure_locality_buffer_size": int(args.exposure_locality_buffer_size),
|
||||||
@@ -659,6 +639,10 @@ def build_metadata(
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
if args.exposure_cache_dir and args.exposure_embeddings_file is None:
|
||||||
|
args.exposure_embeddings_file = str(
|
||||||
|
Path(args.exposure_cache_dir) / "exposure_embeddings.npy"
|
||||||
|
)
|
||||||
device, rank, local_rank, world_size = init_distributed(args)
|
device, rank, local_rank, world_size = init_distributed(args)
|
||||||
set_seed(args.seed + rank)
|
set_seed(args.seed + rank)
|
||||||
configure_torch_for_training(device)
|
configure_torch_for_training(device)
|
||||||
@@ -670,6 +654,7 @@ def main() -> None:
|
|||||||
logger.info(f"Device: {device}")
|
logger.info(f"Device: {device}")
|
||||||
logger.info(f"readout={args.readout_name}, target_mode={args.target_mode}")
|
logger.info(f"readout={args.readout_name}, target_mode={args.target_mode}")
|
||||||
logger.info(f"exposure_cache_dir={args.exposure_cache_dir}")
|
logger.info(f"exposure_cache_dir={args.exposure_cache_dir}")
|
||||||
|
logger.info(f"exposure_embeddings_file={args.exposure_embeddings_file}")
|
||||||
logger.info(
|
logger.info(
|
||||||
"DataLoader IO: "
|
"DataLoader IO: "
|
||||||
f"num_workers={args.num_workers}, "
|
f"num_workers={args.num_workers}, "
|
||||||
@@ -683,8 +668,15 @@ def main() -> None:
|
|||||||
no_event_interval_years=args.no_event_interval_years,
|
no_event_interval_years=args.no_event_interval_years,
|
||||||
include_no_event_in_uts_target=args.include_no_event_in_uts_target,
|
include_no_event_in_uts_target=args.include_no_event_in_uts_target,
|
||||||
exposure_cache_dir=args.exposure_cache_dir,
|
exposure_cache_dir=args.exposure_cache_dir,
|
||||||
mask_onset_exposure=args.mask_onset_exposure,
|
exposure_embeddings_file=args.exposure_embeddings_file,
|
||||||
)
|
)
|
||||||
|
if dataset.exposure_cache is not None:
|
||||||
|
embedding_dim = int(dataset.exposure_cache.embeddings.shape[1])
|
||||||
|
if embedding_dim != args.n_embd:
|
||||||
|
raise ValueError(
|
||||||
|
f"Exposure embedding dim {embedding_dim} must equal "
|
||||||
|
f"--n_embd={args.n_embd}"
|
||||||
|
)
|
||||||
if args.train_eid_file and args.val_eid_file and args.test_eid_file:
|
if args.train_eid_file and args.val_eid_file and args.test_eid_file:
|
||||||
train_subset, val_subset, test_subset = split_dataset_by_eid_files(
|
train_subset, val_subset, test_subset = split_dataset_by_eid_files(
|
||||||
dataset=dataset,
|
dataset=dataset,
|
||||||
|
|||||||
Reference in New Issue
Block a user