Remove legacy event and mixed distribution paths
This commit is contained in:
18
README.md
18
README.md
@@ -6,7 +6,9 @@
|
|||||||
疾病序列 stream + 统一的额外信息 token stream
|
疾病序列 stream + 统一的额外信息 token stream
|
||||||
```
|
```
|
||||||
|
|
||||||
疾病、死亡、checkup 事件保存在预处理事件文件中;性别单独保存在 `basic_info`;其他体检、暴露、生活方式等信息统一整理成 `(type, value, value_kind, time)` token。dataset 仅在实验选择了至少一种 extra-info type 时保留 checkup;显式传入空列表时,模型输入是没有 checkup 的纯疾病历史。
|
疾病和死亡事件保存在预处理事件文件中;性别单独保存在 `basic_info`;其他体检、暴露、生活方式等信息统一整理成 `(type, value, value_kind, time)` token。每个额外信息 token 自带测量时间并直接与疾病 token 拼接,不再生成或使用 assessment/checkup 事件 token。dataset 会无条件清除旧预处理文件中遗留的 `label=1` 事件。
|
||||||
|
|
||||||
|
所有连续额外信息强制使用训练子集拟合的 RobustScale(median/IQR)。center 和 scale 保存为模型 buffer,并用于验证集、测试集和推理;代码不提供未标准化模式,缺少 scaler buffer 的连续变量 checkpoint 不受支持。该规则同时适用于 `next_token` 和 `all_future`。
|
||||||
|
|
||||||
## 数据准备
|
## 数据准备
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ python prepare_data.py
|
|||||||
- `ukb_event_data.npy`
|
- `ukb_event_data.npy`
|
||||||
- 形状为 `(N, 3)`
|
- 形状为 `(N, 3)`
|
||||||
- 每行是 `(eid, days, label)`
|
- 每行是 `(eid, days, label)`
|
||||||
- 包含疾病、死亡、checkup 事件
|
- 包含疾病和死亡事件
|
||||||
|
|
||||||
- `ukb_basic_info.csv`
|
- `ukb_basic_info.csv`
|
||||||
- index 为 `eid`
|
- index 为 `eid`
|
||||||
@@ -64,7 +66,7 @@ python prepare_data.py
|
|||||||
|
|
||||||
- `AllFutureHealthDataset`
|
- `AllFutureHealthDataset`
|
||||||
- 用于 query-conditioned all-future 监督
|
- 用于 query-conditioned all-future 监督
|
||||||
- 对应 `ExponentialLoss`、`WeibullLoss`、`MixedLoss`
|
- 对应 `ExponentialLoss`、`WeibullLoss`
|
||||||
|
|
||||||
为了兼容旧训练入口:
|
为了兼容旧训练入口:
|
||||||
|
|
||||||
@@ -215,7 +217,6 @@ all-future / query-conditioned 监督:
|
|||||||
|
|
||||||
- `ExponentialLoss`
|
- `ExponentialLoss`
|
||||||
- `WeibullLoss`
|
- `WeibullLoss`
|
||||||
- `MixedLoss`
|
|
||||||
|
|
||||||
all-future 训练只读出 `t_query` 对应的 query hidden。展开的 extra-info tokens 作为主序列上下文输入,但不会被单独读出,也不会被纳入 loss 监督。
|
all-future 训练只读出 `t_query` 对应的 query hidden。展开的 extra-info tokens 作为主序列上下文输入,但不会被单独读出,也不会被纳入 loss 监督。
|
||||||
|
|
||||||
@@ -232,7 +233,7 @@ all-future 训练只读出 `t_query` 对应的 query hidden。展开的 extra-in
|
|||||||
- `train_all_future.py`
|
- `train_all_future.py`
|
||||||
- 使用 `AllFutureHealthDataset`
|
- 使用 `AllFutureHealthDataset`
|
||||||
- 不使用 readout,直接对 query hidden 计算风险
|
- 不使用 readout,直接对 query hidden 计算风险
|
||||||
- `--dist_mode exponential/weibull/mixed` 分别搭配 `ExponentialLoss`、`WeibullLoss`、`MixedLoss`
|
- `--dist_mode exponential/weibull` 分别搭配 `ExponentialLoss`、`WeibullLoss`
|
||||||
- 展开的 extra-info tokens 只作为 query 上下文,不单独监督
|
- 展开的 extra-info tokens 只作为 query 上下文,不单独监督
|
||||||
|
|
||||||
当前 `train_next_step.py` / `train_all_future.py` 支持所有已有训练目标定义的组合:
|
当前 `train_next_step.py` / `train_all_future.py` 支持所有已有训练目标定义的组合:
|
||||||
@@ -242,7 +243,6 @@ all-future 训练只读出 `t_query` 对应的 query hidden。展开的 extra-in
|
|||||||
| `next_token` | `absolute` | `target_mode=delphi2m`, `dist_mode=exponential` | `Delphi2MLoss` + `token` |
|
| `next_token` | `absolute` | `target_mode=delphi2m`, `dist_mode=exponential` | `Delphi2MLoss` + `token` |
|
||||||
| `all_future` | `relative`, `absolute` | `dist_mode=exponential` | `ExponentialLoss`,无 readout |
|
| `all_future` | `relative`, `absolute` | `dist_mode=exponential` | `ExponentialLoss`,无 readout |
|
||||||
| `all_future` | `relative`, `absolute` | `dist_mode=weibull` | `WeibullLoss`,无 readout |
|
| `all_future` | `relative`, `absolute` | `dist_mode=weibull` | `WeibullLoss`,无 readout |
|
||||||
| `all_future` | `relative`, `absolute` | `dist_mode=mixed` | `MixedLoss`,无 readout |
|
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ python evaluate_auc.py \
|
|||||||
- 为每个患者和 landmark age 构造 landmark query 样本。
|
- 为每个患者和 landmark age 构造 landmark query 样本。
|
||||||
- 根据模型模式插入 `<NO_EVENT>` token 或直接传 `t_query`,取 landmark/query hidden。
|
- 根据模型模式插入 `<NO_EVENT>` token 或直接传 `t_query`,取 landmark/query hidden。
|
||||||
- 对疾病 token 分块投影到 `risk_head`;`score_mode="risk"` 时会根据 `dist_mode` 把线性输出转换为固定 horizon 风险概率。
|
- 对疾病 token 分块投影到 `risk_head`;`score_mode="risk"` 时会根据 `dist_mode` 把线性输出转换为固定 horizon 风险概率。
|
||||||
- 分布转换规则与 all-future 训练损失一致:`exponential` 使用 `1 - exp(-rate * horizon)`;`weibull` 使用 `1 - exp(-rate * horizon ** rho)`;`mixed` 中普通疾病使用 exponential,死亡 endpoint 使用 Weibull death rho。
|
- 分布转换规则与 all-future 训练损失一致:`exponential` 使用 `1 - exp(-rate * horizon)`;`weibull` 使用 `1 - exp(-rate * horizon ** rho)`。
|
||||||
- `score_mode="eta"` 是诊断用排序分数,不使用 `rho`,因此不区分不同分布的风险曲线。
|
- `score_mode="eta"` 是诊断用排序分数,不使用 `rho`,因此不区分不同分布的风险曲线。
|
||||||
- 按疾病、性别、landmark age、horizon 计算 incident disease AUC。
|
- 按疾病、性别、landmark age、horizon 计算 incident disease AUC。
|
||||||
- 可选择排除 horizon 内先于目标疾病发生的死亡竞争风险。
|
- 可选择排除 horizon 内先于目标疾病发生的死亡竞争风险。
|
||||||
@@ -402,7 +402,7 @@ python evaluate_auc_v2.py \
|
|||||||
| 评估口径 | next-step/token-level 预测点 | landmark fixed-horizon incident risk |
|
| 评估口径 | next-step/token-level 预测点 | landmark fixed-horizon incident risk |
|
||||||
| 查询位置 | next-token 用满足 offset 条件的最新 readout token;all-future 直接用该预测点年龄作为 `t_query` | next-token 用人工插入的 `<NO_EVENT>` landmark token;all-future 直接用 `t_query` |
|
| 查询位置 | next-token 用满足 offset 条件的最新 readout token;all-future 直接用该预测点年龄作为 `t_query` | next-token 用人工插入的 `<NO_EVENT>` landmark token;all-future 直接用 `t_query` |
|
||||||
| 时间参数 | `offsets`:预测点至少早于目标事件多少年 | `landmark_*` 和 `horizons`:固定年龄点与未来窗口 |
|
| 时间参数 | `offsets`:预测点至少早于目标事件多少年 | `landmark_*` 和 `horizons`:固定年龄点与未来窗口 |
|
||||||
| score 与分布 | 使用 eta/logit 排序分数;不按 `dist_mode` 转换风险概率 | `score_mode="risk"` 按 `dist_mode` 区分 exponential / Weibull / mixed;`score_mode="eta"` 不区分分布 |
|
| score 与分布 | 使用 eta/logit 排序分数;不按 `dist_mode` 转换风险概率 | `score_mode="risk"` 按 `dist_mode` 区分 exponential / Weibull;`score_mode="eta"` 不区分分布 |
|
||||||
| 病例定义 | target table 中出现目标疾病的患者/事件 | landmark 后 horizon 内首次发生目标疾病 |
|
| 病例定义 | target table 中出现目标疾病的患者/事件 | landmark 后 horizon 内首次发生目标疾病 |
|
||||||
| 对照定义 | 从未出现该疾病的患者的 eligible target occurrence | landmark 时未患病,且 horizon 内未发病并有足够随访 |
|
| 对照定义 | 从未出现该疾病的患者的 eligible target occurrence | landmark 时未患病,且 horizon 内未发病并有足够随访 |
|
||||||
| 分层 | sex + age bracket + offset | sex + landmark age + horizon |
|
| 分层 | sex + age bracket + offset | sex + landmark age + horizon |
|
||||||
@@ -450,4 +450,4 @@ python evaluate_auc_v2.py \
|
|||||||
- landmark fixed-horizon incident disease AUC 评估
|
- landmark fixed-horizon incident disease AUC 评估
|
||||||
- next-token 模型通过插入 `<NO_EVENT>` landmark token 查询固定年龄点风险
|
- next-token 模型通过插入 `<NO_EVENT>` landmark token 查询固定年龄点风险
|
||||||
- all-future 模型直接通过 `t_query` 查询固定年龄点风险
|
- all-future 模型直接通过 `t_query` 查询固定年龄点风险
|
||||||
- `score_mode="risk"` 按 exponential / Weibull / mixed 分布计算固定 horizon 风险
|
- `score_mode="risk"` 按 exponential / Weibull 分布计算固定 horizon 风险
|
||||||
|
|||||||
20
dataset.py
20
dataset.py
@@ -10,10 +10,10 @@ from torch.nn.utils.rnn import pad_sequence
|
|||||||
from torch.utils.data import Dataset
|
from torch.utils.data import Dataset
|
||||||
|
|
||||||
from targets import (
|
from targets import (
|
||||||
CHECKUP_IDX,
|
|
||||||
DAYS_PER_YEAR,
|
DAYS_PER_YEAR,
|
||||||
NO_EVENT_IDX,
|
NO_EVENT_IDX,
|
||||||
PAD_IDX,
|
PAD_IDX,
|
||||||
|
RESERVED_IDX,
|
||||||
build_next_token_targets,
|
build_next_token_targets,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -188,12 +188,12 @@ def load_label_vocab(
|
|||||||
) -> Tuple[Dict[str, int], Dict[int, str]]:
|
) -> Tuple[Dict[str, int], Dict[int, str]]:
|
||||||
label_id_to_code: Dict[int, str] = {
|
label_id_to_code: Dict[int, str] = {
|
||||||
PAD_IDX: "<PAD>",
|
PAD_IDX: "<PAD>",
|
||||||
CHECKUP_IDX: "<CHECKUP>",
|
RESERVED_IDX: "<RESERVED>",
|
||||||
}
|
}
|
||||||
if include_no_event:
|
if include_no_event:
|
||||||
label_id_to_code[NO_EVENT_IDX] = "<NO_EVENT>"
|
label_id_to_code[NO_EVENT_IDX] = "<NO_EVENT>"
|
||||||
|
|
||||||
offset = NO_EVENT_IDX + 1 if include_no_event else CHECKUP_IDX + 1
|
offset = NO_EVENT_IDX + 1 if include_no_event else RESERVED_IDX + 1
|
||||||
label_code_to_id: Dict[str, int] = {}
|
label_code_to_id: Dict[str, int] = {}
|
||||||
with open(labels_file, encoding="utf-8") as f:
|
with open(labels_file, encoding="utf-8") as f:
|
||||||
for i, line in enumerate(f):
|
for i, line in enumerate(f):
|
||||||
@@ -416,12 +416,10 @@ class _ExpoBaseDataset(Dataset):
|
|||||||
times_days_raw = rows[:, 1].astype(np.float32)
|
times_days_raw = rows[:, 1].astype(np.float32)
|
||||||
labels_raw = rows[:, 2].astype(np.int64)
|
labels_raw = rows[:, 2].astype(np.int64)
|
||||||
|
|
||||||
# CHECKUP is the assessment landmark for selected extra-info tokens.
|
# Label 1 was emitted as a CHECKUP event by older prepared files.
|
||||||
# An explicitly empty selection represents a disease-only history,
|
# It is now an unused reserved slot and must never enter either the
|
||||||
# so retaining CHECKUP in that case would introduce an empty
|
# next-token or all-future disease sequence.
|
||||||
# landmark token that is not part of the disease sequence.
|
keep = labels_raw != RESERVED_IDX
|
||||||
if not self.extra_info_types:
|
|
||||||
keep = labels_raw != CHECKUP_IDX
|
|
||||||
times_days_raw = times_days_raw[keep]
|
times_days_raw = times_days_raw[keep]
|
||||||
labels_raw = labels_raw[keep]
|
labels_raw = labels_raw[keep]
|
||||||
|
|
||||||
@@ -615,7 +613,7 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
|||||||
labels = patient["labels"]
|
labels = patient["labels"]
|
||||||
real_event_mask = ~np.isin(
|
real_event_mask = ~np.isin(
|
||||||
labels,
|
labels,
|
||||||
np.array([PAD_IDX, CHECKUP_IDX, NO_EVENT_IDX], dtype=np.int64),
|
np.array([PAD_IDX, RESERVED_IDX, NO_EVENT_IDX], dtype=np.int64),
|
||||||
)
|
)
|
||||||
n_hist = int((times <= t_query).sum())
|
n_hist = int((times <= t_query).sum())
|
||||||
n_future = int(((times > t_query) & real_event_mask).sum())
|
n_future = int(((times > t_query) & real_event_mask).sum())
|
||||||
@@ -634,7 +632,7 @@ class AllFutureHealthDataset(_ExpoBaseDataset):
|
|||||||
labels = np.asarray(patient["labels"], dtype=np.int64)
|
labels = np.asarray(patient["labels"], dtype=np.int64)
|
||||||
real_event_mask = ~np.isin(
|
real_event_mask = ~np.isin(
|
||||||
labels,
|
labels,
|
||||||
np.array([PAD_IDX, CHECKUP_IDX, NO_EVENT_IDX], dtype=np.int64),
|
np.array([PAD_IDX, RESERVED_IDX, NO_EVENT_IDX], dtype=np.int64),
|
||||||
)
|
)
|
||||||
real_times = np.sort(times[real_event_mask].astype(np.float32, copy=False))
|
real_times = np.sort(times[real_event_mask].astype(np.float32, copy=False))
|
||||||
n_real_events = int(real_times.size)
|
n_real_events = int(real_times.size)
|
||||||
|
|||||||
38
eval_data.py
38
eval_data.py
@@ -151,6 +151,33 @@ def build_model_from_dataset(
|
|||||||
f"{model_target_mode!r}"
|
f"{model_target_mode!r}"
|
||||||
)
|
)
|
||||||
model_architecture = resolve_model_architecture(cfg, state_dict)
|
model_architecture = resolve_model_architecture(cfg, state_dict)
|
||||||
|
continuous_value_center = None
|
||||||
|
continuous_value_scale = None
|
||||||
|
if dataset.n_cont_types > 0:
|
||||||
|
scaling = str(cfg.get("continuous_value_scaling", "")).lower()
|
||||||
|
if scaling != "robust":
|
||||||
|
raise RuntimeError(
|
||||||
|
"Continuous-variable checkpoints must declare "
|
||||||
|
"continuous_value_scaling='robust'; unscaled checkpoints are "
|
||||||
|
"not supported"
|
||||||
|
)
|
||||||
|
if state_dict is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"A checkpoint state_dict is required to restore RobustScale buffers"
|
||||||
|
)
|
||||||
|
center_key = "tokenizer.continuous_value_center"
|
||||||
|
scale_key = "tokenizer.continuous_value_scale"
|
||||||
|
missing = [
|
||||||
|
key for key in (center_key, scale_key)
|
||||||
|
if key not in state_dict
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Checkpoint is missing required RobustScale buffers: "
|
||||||
|
+ ", ".join(missing)
|
||||||
|
)
|
||||||
|
continuous_value_center = state_dict[center_key]
|
||||||
|
continuous_value_scale = state_dict[scale_key]
|
||||||
return DeepHealth(
|
return DeepHealth(
|
||||||
vocab_size=dataset.vocab_size,
|
vocab_size=dataset.vocab_size,
|
||||||
n_embd=int(cfg_get(args, cfg, "n_embd", 120)),
|
n_embd=int(cfg_get(args, cfg, "n_embd", 120)),
|
||||||
@@ -161,9 +188,8 @@ def build_model_from_dataset(
|
|||||||
n_categories=dataset.n_categories,
|
n_categories=dataset.n_categories,
|
||||||
cont_type_ids=dataset.cont_type_ids,
|
cont_type_ids=dataset.cont_type_ids,
|
||||||
n_bins=int(cfg_get(args, cfg, "n_bins", 16)),
|
n_bins=int(cfg_get(args, cfg, "n_bins", 16)),
|
||||||
continuous_value_scaling=str(
|
continuous_value_center=continuous_value_center,
|
||||||
cfg_get(args, cfg, "continuous_value_scaling", "none")
|
continuous_value_scale=continuous_value_scale,
|
||||||
),
|
|
||||||
extra_pool_reduce=str(
|
extra_pool_reduce=str(
|
||||||
cfg_get(args, cfg, "extra_pool_reduce", "mean")
|
cfg_get(args, cfg, "extra_pool_reduce", "mean")
|
||||||
),
|
),
|
||||||
@@ -251,9 +277,9 @@ class AllFutureSequenceEvalDataset:
|
|||||||
Eval-only sequence view for all-future checkpoints.
|
Eval-only sequence view for all-future checkpoints.
|
||||||
|
|
||||||
All-future training uses the observed history without reusing the
|
All-future training uses the observed history without reusing the
|
||||||
next-step view that contains imputed <NO_EVENT> gap tokens. CHECKUP is
|
next-step view that contains imputed <NO_EVENT> gap tokens. Legacy label-1
|
||||||
retained only when the experiment selects at least one extra-info type;
|
assessment events are removed by the shared base dataset for every
|
||||||
an explicitly empty selection is a disease-only history.
|
extra-info selection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ from eval_data import (
|
|||||||
)
|
)
|
||||||
from model_architectures import resolve_model_architecture
|
from model_architectures import resolve_model_architecture
|
||||||
from models import DeepHealth
|
from models import DeepHealth
|
||||||
from targets import PAD_IDX, CHECKUP_IDX, NO_EVENT_IDX
|
from targets import NO_EVENT_IDX, PAD_IDX, RESERVED_IDX
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -168,7 +168,7 @@ def get_auc_delong_var(control_scores: np.ndarray, case_scores: np.ndarray) -> T
|
|||||||
# Disease selection
|
# Disease selection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
SPECIAL_TOKENS = {PAD_IDX, CHECKUP_IDX, NO_EVENT_IDX}
|
SPECIAL_TOKENS = {PAD_IDX, RESERVED_IDX, NO_EVENT_IDX}
|
||||||
|
|
||||||
|
|
||||||
def _get_death_token_ids(dataset: HealthDataset) -> List[int]:
|
def _get_death_token_ids(dataset: HealthDataset) -> List[int]:
|
||||||
@@ -249,27 +249,20 @@ def load_checkpoint_state_dict(checkpoint_path: str, map_location: str | torch.d
|
|||||||
|
|
||||||
def resolve_dist_mode_for_checkpoint(cfg_dist_mode: str, state_dict: Dict[str, Any]) -> str:
|
def resolve_dist_mode_for_checkpoint(cfg_dist_mode: str, state_dict: Dict[str, Any]) -> str:
|
||||||
mode = str(cfg_dist_mode).lower()
|
mode = str(cfg_dist_mode).lower()
|
||||||
|
if mode not in {"exponential", "weibull"}:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported dist_mode={mode!r}; expected exponential or weibull."
|
||||||
|
)
|
||||||
has_rho_head = any(str(k).startswith("rho_head.")
|
has_rho_head = any(str(k).startswith("rho_head.")
|
||||||
for k in state_dict.keys())
|
for k in state_dict.keys())
|
||||||
has_rho_death_head = any(str(k).startswith("rho_death_head.")
|
if mode == "weibull" and not has_rho_head:
|
||||||
for k in state_dict.keys())
|
raise RuntimeError(
|
||||||
|
"Weibull checkpoint is missing rho_head parameters."
|
||||||
if has_rho_head and mode != "weibull":
|
)
|
||||||
print(
|
if mode == "exponential" and has_rho_head:
|
||||||
"[WARN] Checkpoint contains rho_head weights; overriding dist_mode to 'weibull' for evaluation.")
|
raise RuntimeError(
|
||||||
return "weibull"
|
"Exponential checkpoint unexpectedly contains rho_head parameters."
|
||||||
if has_rho_death_head and mode != "mixed":
|
)
|
||||||
print(
|
|
||||||
"[WARN] Checkpoint contains rho_death_head weights; overriding dist_mode to 'mixed' for evaluation.")
|
|
||||||
return "mixed"
|
|
||||||
if (not has_rho_head) and mode == "weibull":
|
|
||||||
print(
|
|
||||||
"[WARN] dist_mode is 'weibull' but checkpoint has no rho_head weights; overriding dist_mode to 'exponential'.")
|
|
||||||
return "exponential"
|
|
||||||
if (not has_rho_death_head) and mode == "mixed":
|
|
||||||
print(
|
|
||||||
"[WARN] dist_mode is 'mixed' but checkpoint has no rho_death_head weights; overriding dist_mode to 'exponential'.")
|
|
||||||
return "exponential"
|
|
||||||
return mode
|
return mode
|
||||||
|
|
||||||
|
|
||||||
@@ -977,7 +970,7 @@ def evaluate_auc_pipeline(
|
|||||||
sex_items = [("female", 0), ("male", 1)]
|
sex_items = [("female", 0), ("male", 1)]
|
||||||
all_rows: List[Dict[str, Any]] = []
|
all_rows: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
valid_target_min_id = CHECKUP_IDX if NO_EVENT_IDX >= dataset.vocab_size else CHECKUP_IDX
|
valid_target_min_id = RESERVED_IDX
|
||||||
# If NO_EVENT exists and should not be a disease/control target, require target > NO_EVENT_IDX.
|
# If NO_EVENT exists and should not be a disease/control target, require target > NO_EVENT_IDX.
|
||||||
if NO_EVENT_IDX in dataset.label_id_to_code and dataset.label_id_to_code.get(NO_EVENT_IDX) == "<NO_EVENT>":
|
if NO_EVENT_IDX in dataset.label_id_to_code and dataset.label_id_to_code.get(NO_EVENT_IDX) == "<NO_EVENT>":
|
||||||
valid_target_min_id = NO_EVENT_IDX
|
valid_target_min_id = NO_EVENT_IDX
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Evaluate landmark fixed-horizon incident disease AUC for DeepHealth.
|
"""Evaluate landmark fixed-horizon incident disease AUC for DeepHealth.
|
||||||
|
|
||||||
This script supports DeepHealth fixed-horizon risk scores for exponential,
|
This script supports DeepHealth fixed-horizon risk scores for exponential and
|
||||||
Weibull, and mixed all-future distributions.
|
Weibull all-future distributions.
|
||||||
|
|
||||||
The default horizons are 0.1, 1, 5, and 10 years. As in Delphi2M, 0.1 years
|
The default horizons are 0.1, 1, 5, and 10 years. As in Delphi2M, 0.1 years
|
||||||
is reported as the no-gap evaluation.
|
is reported as the no-gap evaluation.
|
||||||
@@ -53,9 +53,9 @@ from eval_data import (
|
|||||||
)
|
)
|
||||||
from model_architectures import resolve_model_architecture
|
from model_architectures import resolve_model_architecture
|
||||||
from models import DeepHealth
|
from models import DeepHealth
|
||||||
from targets import CHECKUP_IDX, NO_EVENT_IDX, PAD_IDX
|
from targets import NO_EVENT_IDX, PAD_IDX, RESERVED_IDX
|
||||||
|
|
||||||
SPECIAL_TOKENS = {PAD_IDX, CHECKUP_IDX, NO_EVENT_IDX}
|
SPECIAL_TOKENS = {PAD_IDX, RESERVED_IDX, NO_EVENT_IDX}
|
||||||
|
|
||||||
|
|
||||||
def parse_int_list(value: Any) -> Optional[List[int]]:
|
def parse_int_list(value: Any) -> Optional[List[int]]:
|
||||||
@@ -137,29 +137,19 @@ def load_checkpoint_state_dict(checkpoint_path: Path, map_location: str | torch.
|
|||||||
|
|
||||||
def resolve_dist_mode_for_checkpoint(cfg_dist_mode: str, state_dict: Dict[str, Any]) -> str:
|
def resolve_dist_mode_for_checkpoint(cfg_dist_mode: str, state_dict: Dict[str, Any]) -> str:
|
||||||
mode = str(cfg_dist_mode).lower()
|
mode = str(cfg_dist_mode).lower()
|
||||||
|
if mode not in {"exponential", "weibull"}:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported dist_mode={mode!r}; expected exponential or weibull."
|
||||||
|
)
|
||||||
has_rho_head = any(str(k).startswith("rho_head.")
|
has_rho_head = any(str(k).startswith("rho_head.")
|
||||||
for k in state_dict.keys())
|
for k in state_dict.keys())
|
||||||
has_rho_death_head = any(str(k).startswith("rho_death_head.")
|
if mode == "weibull" and not has_rho_head:
|
||||||
for k in state_dict.keys())
|
raise RuntimeError("Weibull checkpoint is missing rho_head parameters.")
|
||||||
if has_rho_head:
|
if mode == "exponential" and has_rho_head:
|
||||||
if mode != "weibull":
|
raise RuntimeError(
|
||||||
print(
|
"Exponential checkpoint unexpectedly contains rho_head parameters."
|
||||||
"[WARN] Checkpoint contains rho_head weights; overriding dist_mode to 'weibull' for evaluation.")
|
)
|
||||||
return "weibull"
|
return mode
|
||||||
if has_rho_death_head:
|
|
||||||
if mode != "mixed":
|
|
||||||
print(
|
|
||||||
"[WARN] Checkpoint contains rho_death_head weights; overriding dist_mode to 'mixed' for evaluation.")
|
|
||||||
return "mixed"
|
|
||||||
if mode == "weibull":
|
|
||||||
print(
|
|
||||||
"[WARN] dist_mode is 'weibull' but checkpoint has no rho_head weights; overriding dist_mode to 'exponential'.")
|
|
||||||
return "exponential"
|
|
||||||
if mode == "mixed":
|
|
||||||
print(
|
|
||||||
"[WARN] dist_mode is 'mixed' but checkpoint has no rho_death_head weights; overriding dist_mode to 'exponential'.")
|
|
||||||
return "exponential"
|
|
||||||
return mode if mode in {"exponential", "weibull", "mixed"} else "exponential"
|
|
||||||
|
|
||||||
|
|
||||||
def load_model_state(model: DeepHealth, state_dict: Dict[str, Any]) -> None:
|
def load_model_state(model: DeepHealth, state_dict: Dict[str, Any]) -> None:
|
||||||
@@ -290,7 +280,7 @@ def _metadata_count_map(dataset: HealthDataset, labels_meta: Optional[pd.DataFra
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _get_death_token_ids(dataset: HealthDataset, labels_meta: Optional[pd.DataFrame]) -> List[int]:
|
def _get_death_token_ids(dataset: HealthDataset) -> List[int]:
|
||||||
return [int(dataset.vocab_size) - 1]
|
return [int(dataset.vocab_size) - 1]
|
||||||
|
|
||||||
|
|
||||||
@@ -418,7 +408,7 @@ class LandmarkDataset(Dataset):
|
|||||||
prefix_times = full_time[prefix_mask]
|
prefix_times = full_time[prefix_mask]
|
||||||
|
|
||||||
valid_history_mask = ~np.isin(prefix_events, np.array(
|
valid_history_mask = ~np.isin(prefix_events, np.array(
|
||||||
[PAD_IDX, CHECKUP_IDX, NO_EVENT_IDX], dtype=np.int64))
|
[PAD_IDX, RESERVED_IDX, NO_EVENT_IDX], dtype=np.int64))
|
||||||
if valid_history_mask.sum() < self.min_history_events:
|
if valid_history_mask.sum() < self.min_history_events:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -671,24 +661,11 @@ def project_distribution_chunk(
|
|||||||
device=device, dtype=compute_dtype)
|
device=device, dtype=compute_dtype)
|
||||||
rho_weight = None
|
rho_weight = None
|
||||||
rho_bias = None
|
rho_bias = None
|
||||||
death_rho_weight = None
|
|
||||||
death_rho_bias = None
|
|
||||||
mixed_death_cols: List[int] = []
|
|
||||||
death_idx = int(getattr(model, "death_idx", getattr(model, "vocab_size", 0) - 1))
|
|
||||||
|
|
||||||
if dist_mode == "weibull":
|
if dist_mode == "weibull":
|
||||||
rho_weight = model.rho_head.weight[disease_ids].detach().to(
|
rho_weight = model.rho_head.weight[disease_ids].detach().to(
|
||||||
device=device, dtype=compute_dtype)
|
device=device, dtype=compute_dtype)
|
||||||
rho_bias = model.rho_head.bias[disease_ids].detach().to(
|
rho_bias = model.rho_head.bias[disease_ids].detach().to(
|
||||||
device=device, dtype=compute_dtype)
|
device=device, dtype=compute_dtype)
|
||||||
elif dist_mode == "mixed":
|
|
||||||
mixed_death_cols = [j for j, token in enumerate(disease_ids)
|
|
||||||
if int(token) == death_idx]
|
|
||||||
if mixed_death_cols:
|
|
||||||
death_rho_weight = model.rho_death_head.weight.detach().to(
|
|
||||||
device=device, dtype=compute_dtype)
|
|
||||||
death_rho_bias = model.rho_death_head.bias.detach().to(
|
|
||||||
device=device, dtype=compute_dtype)
|
|
||||||
|
|
||||||
out_parts: List[np.ndarray] = []
|
out_parts: List[np.ndarray] = []
|
||||||
rho_parts: List[np.ndarray] = []
|
rho_parts: List[np.ndarray] = []
|
||||||
@@ -703,14 +680,6 @@ def project_distribution_chunk(
|
|||||||
if dist_mode == "weibull":
|
if dist_mode == "weibull":
|
||||||
assert rho_weight is not None and rho_bias is not None
|
assert rho_weight is not None and rho_bias is not None
|
||||||
rho = F.softplus(torch.matmul(h, rho_weight.t()) + rho_bias) + 1e-6
|
rho = F.softplus(torch.matmul(h, rho_weight.t()) + rho_bias) + 1e-6
|
||||||
elif dist_mode == "mixed" and mixed_death_cols:
|
|
||||||
assert death_rho_weight is not None and death_rho_bias is not None
|
|
||||||
rho = torch.ones_like(logits)
|
|
||||||
death_rho = F.softplus(
|
|
||||||
torch.matmul(h, death_rho_weight.t()).squeeze(-1) + death_rho_bias.squeeze(0)
|
|
||||||
) + 1e-6
|
|
||||||
for col in mixed_death_cols:
|
|
||||||
rho[:, int(col)] = death_rho
|
|
||||||
|
|
||||||
out_parts.append(logits.float().cpu(
|
out_parts.append(logits.float().cpu(
|
||||||
).numpy().astype(np.float32, copy=False))
|
).numpy().astype(np.float32, copy=False))
|
||||||
@@ -747,7 +716,6 @@ def _init_worker(
|
|||||||
exclude_death_competing: bool,
|
exclude_death_competing: bool,
|
||||||
death_token_ids: np.ndarray,
|
death_token_ids: np.ndarray,
|
||||||
dist_mode: str,
|
dist_mode: str,
|
||||||
model_death_idx: int,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
||||||
os.environ.setdefault("MKL_NUM_THREADS", "1")
|
os.environ.setdefault("MKL_NUM_THREADS", "1")
|
||||||
@@ -772,7 +740,6 @@ def _init_worker(
|
|||||||
"exclude_death_competing": bool(exclude_death_competing),
|
"exclude_death_competing": bool(exclude_death_competing),
|
||||||
"death_token_ids": set(int(x) for x in np.asarray(death_token_ids, dtype=np.int64).tolist()),
|
"death_token_ids": set(int(x) for x in np.asarray(death_token_ids, dtype=np.int64).tolist()),
|
||||||
"dist_mode": str(dist_mode).lower(),
|
"dist_mode": str(dist_mode).lower(),
|
||||||
"model_death_idx": int(model_death_idx),
|
|
||||||
"first_time_cache": {},
|
"first_time_cache": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -798,8 +765,6 @@ def _score_to_probability(
|
|||||||
score_mode: str,
|
score_mode: str,
|
||||||
horizon: float,
|
horizon: float,
|
||||||
dist_mode: str,
|
dist_mode: str,
|
||||||
token: int,
|
|
||||||
death_idx: int,
|
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
if score_mode == "eta":
|
if score_mode == "eta":
|
||||||
return logits.astype(np.float64, copy=False)
|
return logits.astype(np.float64, copy=False)
|
||||||
@@ -811,11 +776,6 @@ def _score_to_probability(
|
|||||||
raise RuntimeError("Weibull risk scoring requires rho parameters.")
|
raise RuntimeError("Weibull risk scoring requires rho parameters.")
|
||||||
exposure = np.power(np.float32(horizon), rho.astype(np.float32, copy=False))
|
exposure = np.power(np.float32(horizon), rho.astype(np.float32, copy=False))
|
||||||
return (-np.expm1(-rate * exposure)).astype(np.float64, copy=False)
|
return (-np.expm1(-rate * exposure)).astype(np.float64, copy=False)
|
||||||
if dist_mode == "mixed" and int(token) == int(death_idx):
|
|
||||||
if rho is None:
|
|
||||||
raise RuntimeError("Mixed death risk scoring requires death rho parameters.")
|
|
||||||
exposure = np.power(np.float32(horizon), rho.astype(np.float32, copy=False))
|
|
||||||
return (-np.expm1(-rate * exposure)).astype(np.float64, copy=False)
|
|
||||||
return (-np.expm1(-rate * np.float32(horizon))).astype(np.float64, copy=False)
|
return (-np.expm1(-rate * np.float32(horizon))).astype(np.float64, copy=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -832,7 +792,6 @@ def _eval_token(task: Tuple[int, int, str]) -> List[Dict[str, Any]]:
|
|||||||
rho_chunk = _WORKER["rho_chunk"]
|
rho_chunk = _WORKER["rho_chunk"]
|
||||||
rho_token = None if rho_chunk is None else rho_chunk[:, int(j)]
|
rho_token = None if rho_chunk is None else rho_chunk[:, int(j)]
|
||||||
dist_mode = _WORKER["dist_mode"]
|
dist_mode = _WORKER["dist_mode"]
|
||||||
model_death_idx = int(_WORKER["model_death_idx"])
|
|
||||||
|
|
||||||
first_time_patient = _first_time_by_patient(token)
|
first_time_patient = _first_time_by_patient(token)
|
||||||
is_death_target = token in _WORKER["death_token_ids"]
|
is_death_target = token in _WORKER["death_token_ids"]
|
||||||
@@ -891,8 +850,6 @@ def _eval_token(task: Tuple[int, int, str]) -> List[Dict[str, Any]]:
|
|||||||
score_mode=score_mode,
|
score_mode=score_mode,
|
||||||
horizon=horizon,
|
horizon=horizon,
|
||||||
dist_mode=dist_mode,
|
dist_mode=dist_mode,
|
||||||
token=token,
|
|
||||||
death_idx=model_death_idx,
|
|
||||||
)
|
)
|
||||||
control_scores = _score_to_probability(
|
control_scores = _score_to_probability(
|
||||||
logits_token[idx[control_idx]],
|
logits_token[idx[control_idx]],
|
||||||
@@ -900,8 +857,6 @@ def _eval_token(task: Tuple[int, int, str]) -> List[Dict[str, Any]]:
|
|||||||
score_mode=score_mode,
|
score_mode=score_mode,
|
||||||
horizon=horizon,
|
horizon=horizon,
|
||||||
dist_mode=dist_mode,
|
dist_mode=dist_mode,
|
||||||
token=token,
|
|
||||||
death_idx=model_death_idx,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
auc, auc_var = get_auc_delong_var(case_scores, control_scores)
|
auc, auc_var = get_auc_delong_var(case_scores, control_scores)
|
||||||
@@ -1019,8 +974,6 @@ def evaluate_landmark_auc(
|
|||||||
death_token_ids=np.asarray(
|
death_token_ids=np.asarray(
|
||||||
landmark_dataset.death_token_ids, dtype=np.int64),
|
landmark_dataset.death_token_ids, dtype=np.int64),
|
||||||
dist_mode=dist_mode,
|
dist_mode=dist_mode,
|
||||||
model_death_idx=int(getattr(
|
|
||||||
model, "death_idx", getattr(model, "vocab_size", 1) - 1)),
|
|
||||||
)
|
)
|
||||||
nested = [_eval_token(t) for t in tqdm(
|
nested = [_eval_token(t) for t in tqdm(
|
||||||
tasks, desc=f"AUC chunk {chunk_idx}", leave=False, dynamic_ncols=True)]
|
tasks, desc=f"AUC chunk {chunk_idx}", leave=False, dynamic_ncols=True)]
|
||||||
@@ -1048,8 +1001,6 @@ def evaluate_landmark_auc(
|
|||||||
np.asarray(landmark_dataset.death_token_ids,
|
np.asarray(landmark_dataset.death_token_ids,
|
||||||
dtype=np.int64),
|
dtype=np.int64),
|
||||||
dist_mode,
|
dist_mode,
|
||||||
int(getattr(
|
|
||||||
model, "death_idx", getattr(model, "vocab_size", 1) - 1)),
|
|
||||||
),
|
),
|
||||||
) as ex:
|
) as ex:
|
||||||
nested = list(
|
nested = list(
|
||||||
@@ -1253,10 +1204,6 @@ def main() -> None:
|
|||||||
|
|
||||||
state_dict = load_checkpoint_state_dict(model_ckpt_path, map_location="cpu")
|
state_dict = load_checkpoint_state_dict(model_ckpt_path, map_location="cpu")
|
||||||
dist_mode = resolve_dist_mode_for_checkpoint(dist_mode_cfg, state_dict)
|
dist_mode = resolve_dist_mode_for_checkpoint(dist_mode_cfg, state_dict)
|
||||||
if dist_mode not in {"exponential", "weibull", "mixed"}:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unsupported dist_mode={dist_mode!r}; expected exponential, weibull, or mixed."
|
|
||||||
)
|
|
||||||
|
|
||||||
if score_mode == "eta":
|
if score_mode == "eta":
|
||||||
print(
|
print(
|
||||||
@@ -1302,7 +1249,7 @@ def main() -> None:
|
|||||||
"Please use a checkpoint trained with the same no-event vocabulary configuration."
|
"Please use a checkpoint trained with the same no-event vocabulary configuration."
|
||||||
)
|
)
|
||||||
|
|
||||||
death_token_ids = _get_death_token_ids(dataset, labels_meta)
|
death_token_ids = _get_death_token_ids(dataset)
|
||||||
min_history_events = int(cfg_get(args, cfg, "min_history_events", 1))
|
min_history_events = int(cfg_get(args, cfg, "min_history_events", 1))
|
||||||
landmark_dataset = LandmarkDataset(
|
landmark_dataset = LandmarkDataset(
|
||||||
dataset=dataset,
|
dataset=dataset,
|
||||||
|
|||||||
@@ -23,12 +23,11 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import contextlib
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import math
|
|
||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -67,7 +66,7 @@ from evaluate_auc_v2 import (
|
|||||||
)
|
)
|
||||||
from losses import build_loss
|
from losses import build_loss
|
||||||
from model_architectures import resolve_model_architecture
|
from model_architectures import resolve_model_architecture
|
||||||
from targets import CHECKUP_IDX, PAD_IDX
|
from targets import PAD_IDX, RESERVED_IDX
|
||||||
from train_util import load_eid_file
|
from train_util import load_eid_file
|
||||||
|
|
||||||
|
|
||||||
@@ -971,8 +970,6 @@ def _risk_probability_matrix(
|
|||||||
rho: Optional[np.ndarray],
|
rho: Optional[np.ndarray],
|
||||||
horizons: np.ndarray,
|
horizons: np.ndarray,
|
||||||
dist_mode: str,
|
dist_mode: str,
|
||||||
token: int,
|
|
||||||
death_idx: int,
|
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""Convert one token's logits to all horizon risks at once."""
|
"""Convert one token's logits to all horizon risks at once."""
|
||||||
logits = np.asarray(logits, dtype=np.float32)
|
logits = np.asarray(logits, dtype=np.float32)
|
||||||
@@ -982,13 +979,7 @@ def _risk_probability_matrix(
|
|||||||
+ np.maximum(logits, np.float32(0.0))
|
+ np.maximum(logits, np.float32(0.0))
|
||||||
+ np.float32(1e-8)
|
+ np.float32(1e-8)
|
||||||
)
|
)
|
||||||
use_weibull = (
|
use_weibull = str(dist_mode).lower() == "weibull"
|
||||||
str(dist_mode).lower() == "weibull"
|
|
||||||
or (
|
|
||||||
str(dist_mode).lower() == "mixed"
|
|
||||||
and int(token) == int(death_idx)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if use_weibull:
|
if use_weibull:
|
||||||
if rho is None:
|
if rho is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -1047,7 +1038,6 @@ def _evaluate_calibration_token(
|
|||||||
label_id_to_code: Dict[int, str],
|
label_id_to_code: Dict[int, str],
|
||||||
dist_mode: str,
|
dist_mode: str,
|
||||||
horizons: np.ndarray,
|
horizons: np.ndarray,
|
||||||
death_index: int,
|
|
||||||
min_cases: int,
|
min_cases: int,
|
||||||
min_controls: int,
|
min_controls: int,
|
||||||
max_ipcw_weight: float,
|
max_ipcw_weight: float,
|
||||||
@@ -1114,8 +1104,6 @@ def _evaluate_calibration_token(
|
|||||||
),
|
),
|
||||||
horizons=horizons,
|
horizons=horizons,
|
||||||
dist_mode=dist_mode,
|
dist_mode=dist_mode,
|
||||||
token=token,
|
|
||||||
death_idx=death_index,
|
|
||||||
)
|
)
|
||||||
results = compute_ipcw_horizons(
|
results = compute_ipcw_horizons(
|
||||||
probabilities=probabilities,
|
probabilities=probabilities,
|
||||||
@@ -1200,9 +1188,6 @@ def evaluate_landmark_calibration(
|
|||||||
|
|
||||||
patient_count = len(landmark_dataset.subset_indices)
|
patient_count = len(landmark_dataset.subset_indices)
|
||||||
death_tokens = set(int(value) for value in landmark_dataset.death_token_ids)
|
death_tokens = set(int(value) for value in landmark_dataset.death_token_ids)
|
||||||
death_index = int(
|
|
||||||
getattr(model, "death_idx", getattr(model, "vocab_size", 1) - 1)
|
|
||||||
)
|
|
||||||
strata = _build_calibration_strata(
|
strata = _build_calibration_strata(
|
||||||
row_arrays["sex"],
|
row_arrays["sex"],
|
||||||
row_arrays["landmark_age"],
|
row_arrays["landmark_age"],
|
||||||
@@ -1265,7 +1250,6 @@ def evaluate_landmark_calibration(
|
|||||||
),
|
),
|
||||||
"dist_mode": dist_mode,
|
"dist_mode": dist_mode,
|
||||||
"horizons": horizons,
|
"horizons": horizons,
|
||||||
"death_index": death_index,
|
|
||||||
"min_cases": min_cases,
|
"min_cases": min_cases,
|
||||||
"min_controls": min_controls,
|
"min_controls": min_controls,
|
||||||
"max_ipcw_weight": max_ipcw_weight,
|
"max_ipcw_weight": max_ipcw_weight,
|
||||||
@@ -1498,19 +1482,12 @@ def build_calibration_summary(metrics: pd.DataFrame) -> pd.DataFrame:
|
|||||||
|
|
||||||
def _build_point_process_criterion(
|
def _build_point_process_criterion(
|
||||||
dist_mode: str,
|
dist_mode: str,
|
||||||
death_index: int,
|
|
||||||
) -> Any:
|
) -> Any:
|
||||||
ignored = {PAD_IDX, CHECKUP_IDX}
|
ignored = {PAD_IDX, RESERVED_IDX}
|
||||||
if dist_mode == "exponential":
|
if dist_mode == "exponential":
|
||||||
return build_loss("exponential", ignored_idx=ignored)
|
return build_loss("exponential", ignored_idx=ignored)
|
||||||
if dist_mode == "weibull":
|
if dist_mode == "weibull":
|
||||||
return build_loss("weibull", ignored_idx=ignored)
|
return build_loss("weibull", ignored_idx=ignored)
|
||||||
if dist_mode == "mixed":
|
|
||||||
return build_loss(
|
|
||||||
"mixed",
|
|
||||||
death_idx=death_index,
|
|
||||||
ignored_idx=ignored,
|
|
||||||
)
|
|
||||||
raise ValueError(f"Unsupported dist_mode: {dist_mode!r}")
|
raise ValueError(f"Unsupported dist_mode: {dist_mode!r}")
|
||||||
|
|
||||||
|
|
||||||
@@ -1523,16 +1500,7 @@ def evaluate_point_process_nll(
|
|||||||
device: torch.device,
|
device: torch.device,
|
||||||
use_amp: bool,
|
use_amp: bool,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
criterion = _build_point_process_criterion(
|
criterion = _build_point_process_criterion(dist_mode)
|
||||||
dist_mode,
|
|
||||||
death_index=int(
|
|
||||||
getattr(
|
|
||||||
model,
|
|
||||||
"death_idx",
|
|
||||||
int(getattr(model, "vocab_size", 1)) - 1,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
model.eval().to(device)
|
model.eval().to(device)
|
||||||
total_nll = 0.0
|
total_nll = 0.0
|
||||||
query_count = 0
|
query_count = 0
|
||||||
@@ -1587,13 +1555,7 @@ def evaluate_point_process_nll(
|
|||||||
exposure=batch_device["exposure"],
|
exposure=batch_device["exposure"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
loss = criterion(
|
raise ValueError(f"Unsupported dist_mode: {dist_mode!r}")
|
||||||
logits=logits,
|
|
||||||
death_rho=model.calc_death_rho(hidden),
|
|
||||||
targets=batch_device["future_targets"],
|
|
||||||
dt=batch_device["future_dt"],
|
|
||||||
exposure=batch_device["exposure"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not torch.isfinite(loss):
|
if not torch.isfinite(loss):
|
||||||
raise RuntimeError("Non-finite point-process NLL encountered.")
|
raise RuntimeError("Non-finite point-process NLL encountered.")
|
||||||
@@ -1601,7 +1563,7 @@ def evaluate_point_process_nll(
|
|||||||
total_nll += float(loss.detach().cpu()) * batch_size
|
total_nll += float(loss.detach().cpu()) * batch_size
|
||||||
query_count += batch_size
|
query_count += batch_size
|
||||||
valid_targets = batch["future_targets"] > PAD_IDX
|
valid_targets = batch["future_targets"] > PAD_IDX
|
||||||
valid_targets &= batch["future_targets"] != CHECKUP_IDX
|
valid_targets &= batch["future_targets"] != RESERVED_IDX
|
||||||
future_event_count += int(valid_targets.sum().item())
|
future_event_count += int(valid_targets.sum().item())
|
||||||
exposure_sum += float(batch["exposure"].sum().item())
|
exposure_sum += float(batch["exposure"].sum().item())
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,13 @@
|
|||||||
|
|
||||||
补充现有四级 extra-information 证据链:
|
补充现有四级 extra-information 证据链:
|
||||||
|
|
||||||
1. `disease_only`:疾病事件、相对患病时间和 sex;无 CHECKUP、无 extra-info token。
|
1. `disease_only`:疾病事件、相对患病时间和 sex;无 extra-info token。
|
||||||
2. `smoking_alcohol_bmi`:疾病史、sex、CHECKUP、smoking/alcohol/BMI。
|
2. `smoking_alcohol_bmi`:疾病史、sex、smoking/alcohol/BMI token。
|
||||||
3. `assessment_only`:疾病史、sex、CHECKUP、65项常规体格、肺功能、血液、尿液和生化指标。
|
3. `assessment_only`:疾病史、sex、65项常规体格、肺功能、血液、尿液和生化指标 token。
|
||||||
4. `all`:疾病史、sex、CHECKUP、全部265项体检和暴露信息。
|
4. `all`:疾病史、sex、全部265项体检和暴露信息 token。
|
||||||
|
|
||||||
|
所有配置均不使用 CHECKUP。额外信息以独立 token 注入,并使用各自的 assessment 时间。
|
||||||
|
所有连续变量均强制使用训练子集拟合的 RobustScale;训练与评估不设置未标准化对照或兼容模式。
|
||||||
|
|
||||||
目标是区分:
|
目标是区分:
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ assessment_only − disease_only
|
|||||||
all − assessment_only
|
all − assessment_only
|
||||||
```
|
```
|
||||||
|
|
||||||
回答生活方式、社会经济、心理和环境暴露是否在常规体检之后仍有增量价值。这是新增实验中最干净的主要比较,因为两组都保留 CHECKUP,Landmark 和随访边界应一致。
|
回答生活方式、社会经济、心理和环境暴露是否在常规体检之后仍有增量价值。两组使用完全相同的疾病事件、Landmark 和随访边界,仅 extra-info token 集合不同。
|
||||||
|
|
||||||
### 4.3 全体检相对紧凑变量集
|
### 4.3 全体检相对紧凑变量集
|
||||||
|
|
||||||
@@ -130,15 +133,9 @@ smoking_alcohol_bmi − disease_only
|
|||||||
- Brier、NLL及绝对校准偏差越低越好;
|
- Brier、NLL及绝对校准偏差越低越好;
|
||||||
- 不以单个 seed 或单个 horizon 决定模型。
|
- 不以单个 seed 或单个 horizon 决定模型。
|
||||||
|
|
||||||
## 7. disease_only 比较的评估限制
|
## 7. disease_only 比较的评估边界
|
||||||
|
|
||||||
`disease_only` 按设计删除 CHECKUP,其他三组保留 CHECKUP。当前评估实现会使两类模型的随访终点和 `n_at_risk` 略有差异。
|
所有配置均从同一疾病/死亡事件流构造历史、查询点、随访终点和 censoring,并且都不使用 CHECKUP。不同配置只改变 extra-info token,因此可以在共同支持集上把差异解释为额外信息的增量价值。
|
||||||
|
|
||||||
因此:
|
|
||||||
|
|
||||||
- `assessment_only`、`smoking_alcohol_bmi`、`all` 三者之间可以直接比较;
|
|
||||||
- 它们与 `disease_only` 的比较应使用固定的原始随访终点、Landmark 和 censoring;
|
|
||||||
- 在共享风险集评估完成前,不能把与 `disease_only` 的全部差异严格归因于 extra-info 数值。
|
|
||||||
|
|
||||||
## 8. 决策规则
|
## 8. 决策规则
|
||||||
|
|
||||||
|
|||||||
78
losses.py
78
losses.py
@@ -8,7 +8,7 @@ import torch.nn.functional as F
|
|||||||
|
|
||||||
|
|
||||||
PAD_IDX = 0
|
PAD_IDX = 0
|
||||||
CHECKUP_IDX = 1
|
RESERVED_IDX = 1
|
||||||
NO_EVENT_IDX = 2
|
NO_EVENT_IDX = 2
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class Delphi2MLoss(nn.Module):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.t_min = float(t_min)
|
self.t_min = float(t_min)
|
||||||
self.ignored_tokens = (
|
self.ignored_tokens = (
|
||||||
[PAD_IDX, CHECKUP_IDX]
|
[PAD_IDX, RESERVED_IDX]
|
||||||
if ignored_tokens is None
|
if ignored_tokens is None
|
||||||
else [int(x) for x in ignored_tokens]
|
else [int(x) for x in ignored_tokens]
|
||||||
)
|
)
|
||||||
@@ -150,7 +150,7 @@ class ExponentialLoss(nn.Module):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ignored_idx: Iterable[int] = (PAD_IDX, CHECKUP_IDX),
|
ignored_idx: Iterable[int] = (PAD_IDX, RESERVED_IDX),
|
||||||
eps: float = 1e-8,
|
eps: float = 1e-8,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -183,7 +183,7 @@ class WeibullLoss(nn.Module):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ignored_idx: Iterable[int] = (PAD_IDX, CHECKUP_IDX),
|
ignored_idx: Iterable[int] = (PAD_IDX, RESERVED_IDX),
|
||||||
eps: float = 1e-8,
|
eps: float = 1e-8,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -232,78 +232,14 @@ class WeibullLoss(nn.Module):
|
|||||||
return (-observed + penalty).mean()
|
return (-observed + penalty).mean()
|
||||||
|
|
||||||
|
|
||||||
class MixedLoss(nn.Module):
|
|
||||||
"""Exponential diseases plus one Weibull death endpoint."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
death_idx: int,
|
|
||||||
ignored_idx: Iterable[int] = (PAD_IDX, CHECKUP_IDX),
|
|
||||||
eps: float = 1e-8,
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.death_idx = int(death_idx)
|
|
||||||
self.ignored_idx = tuple(int(i) for i in ignored_idx)
|
|
||||||
self.eps = eps
|
|
||||||
|
|
||||||
def forward(
|
|
||||||
self,
|
|
||||||
logits: torch.Tensor,
|
|
||||||
death_rho: torch.Tensor,
|
|
||||||
targets: torch.Tensor,
|
|
||||||
dt: torch.Tensor,
|
|
||||||
exposure: torch.Tensor,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
_, vocab_size = logits.shape
|
|
||||||
dtype = logits.dtype
|
|
||||||
rate = F.softplus(logits) + self.eps
|
|
||||||
|
|
||||||
if death_rho.dim() == 2:
|
|
||||||
death_rho = death_rho.squeeze(-1)
|
|
||||||
death_rho = death_rho.to(device=logits.device, dtype=dtype).clamp_min(self.eps)
|
|
||||||
|
|
||||||
valid_vocab = _valid_vocab_mask(vocab_size, self.ignored_idx, logits.device)
|
|
||||||
valid_disease_vocab = valid_vocab.clone()
|
|
||||||
valid_disease_vocab[self.death_idx] = False
|
|
||||||
|
|
||||||
t_exp = exposure.to(dtype).clamp_min(self.eps)
|
|
||||||
disease_penalty = t_exp * rate[:, valid_disease_vocab].sum(dim=-1)
|
|
||||||
death_rate = rate[:, self.death_idx]
|
|
||||||
death_penalty = death_rate * torch.pow(t_exp, death_rho)
|
|
||||||
penalty = disease_penalty + death_penalty
|
|
||||||
|
|
||||||
target_valid = torch.ones_like(targets, dtype=torch.bool, device=logits.device)
|
|
||||||
for idx in self.ignored_idx:
|
|
||||||
target_valid &= targets != idx
|
|
||||||
|
|
||||||
disease_event_mask = target_valid & (targets != self.death_idx)
|
|
||||||
safe_targets = targets.clamp(min=0, max=vocab_size - 1)
|
|
||||||
disease_log_rate = rate.log().gather(1, safe_targets)
|
|
||||||
observed_disease = (disease_log_rate * disease_event_mask.to(dtype)).sum(dim=-1)
|
|
||||||
|
|
||||||
death_event_mask = target_valid & (targets == self.death_idx)
|
|
||||||
death_observed = death_event_mask.any(dim=1)
|
|
||||||
death_dt = (dt.to(dtype).clamp_min(self.eps) * death_event_mask.to(dtype)).sum(dim=1)
|
|
||||||
death_log_intensity = (
|
|
||||||
death_rate.log()
|
|
||||||
+ death_rho.log()
|
|
||||||
+ (death_rho - 1.0) * death_dt.clamp_min(self.eps).log()
|
|
||||||
)
|
|
||||||
observed_death = death_log_intensity * death_observed.to(dtype)
|
|
||||||
|
|
||||||
return (-observed_disease - observed_death + penalty).mean()
|
|
||||||
|
|
||||||
|
|
||||||
def build_loss(name: str, **kwargs) -> nn.Module:
|
def build_loss(name: str, **kwargs) -> nn.Module:
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
if name == "delphi2m":
|
if name == "delphi2m":
|
||||||
return Delphi2MLoss(**kwargs)
|
return Delphi2MLoss(**kwargs)
|
||||||
if name in {"exponential", "query_exponential"}:
|
if name == "exponential":
|
||||||
return ExponentialLoss(**kwargs)
|
return ExponentialLoss(**kwargs)
|
||||||
if name in {"weibull", "query_weibull"}:
|
if name == "weibull":
|
||||||
return WeibullLoss(**kwargs)
|
return WeibullLoss(**kwargs)
|
||||||
if name in {"mixed", "query_mixed"}:
|
|
||||||
return MixedLoss(**kwargs)
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unknown loss {name!r}. Available: delphi2m, exponential, weibull, mixed."
|
f"Unknown loss {name!r}. Available: delphi2m, exponential, weibull."
|
||||||
)
|
)
|
||||||
|
|||||||
46
models.py
46
models.py
@@ -37,7 +37,6 @@ class OtherInfoTokenizer(nn.Module):
|
|||||||
cont_type_ids: list[int],
|
cont_type_ids: list[int],
|
||||||
n_value_kinds: int = 3,
|
n_value_kinds: int = 3,
|
||||||
n_bins: int = 16,
|
n_bins: int = 16,
|
||||||
continuous_value_scaling: str = "none",
|
|
||||||
continuous_value_center: torch.Tensor | list[float] | None = None,
|
continuous_value_center: torch.Tensor | list[float] | None = None,
|
||||||
continuous_value_scale: torch.Tensor | list[float] | None = None,
|
continuous_value_scale: torch.Tensor | list[float] | None = None,
|
||||||
):
|
):
|
||||||
@@ -57,13 +56,6 @@ class OtherInfoTokenizer(nn.Module):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"n_value_kinds must be > {self.CATE_KIND}, got {n_value_kinds}"
|
f"n_value_kinds must be > {self.CATE_KIND}, got {n_value_kinds}"
|
||||||
)
|
)
|
||||||
continuous_value_scaling = str(continuous_value_scaling).lower()
|
|
||||||
if continuous_value_scaling not in {"none", "robust"}:
|
|
||||||
raise ValueError(
|
|
||||||
"continuous_value_scaling must be either 'none' or 'robust', "
|
|
||||||
f"got {continuous_value_scaling!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.type_emb = nn.Embedding(n_types, n_embd, padding_idx=0)
|
self.type_emb = nn.Embedding(n_types, n_embd, padding_idx=0)
|
||||||
self.kind_emb = nn.Embedding(n_value_kinds, n_embd, padding_idx=0)
|
self.kind_emb = nn.Embedding(n_value_kinds, n_embd, padding_idx=0)
|
||||||
self.cont_value_encoder = (
|
self.cont_value_encoder = (
|
||||||
@@ -80,18 +72,20 @@ class OtherInfoTokenizer(nn.Module):
|
|||||||
n_embd,
|
n_embd,
|
||||||
padding_idx=0,
|
padding_idx=0,
|
||||||
)
|
)
|
||||||
self.continuous_value_scaling = continuous_value_scaling
|
if n_cont_types > 0:
|
||||||
if continuous_value_scaling == "robust" and n_cont_types > 0:
|
if continuous_value_center is None or continuous_value_scale is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Continuous values require train-split RobustScale center "
|
||||||
|
"and scale statistics"
|
||||||
|
)
|
||||||
center = self._coerce_scaler_buffer(
|
center = self._coerce_scaler_buffer(
|
||||||
continuous_value_center,
|
continuous_value_center,
|
||||||
n_cont_types=n_cont_types,
|
n_cont_types=n_cont_types,
|
||||||
default=0.0,
|
|
||||||
name="continuous_value_center",
|
name="continuous_value_center",
|
||||||
)
|
)
|
||||||
scale = self._coerce_scaler_buffer(
|
scale = self._coerce_scaler_buffer(
|
||||||
continuous_value_scale,
|
continuous_value_scale,
|
||||||
n_cont_types=n_cont_types,
|
n_cont_types=n_cont_types,
|
||||||
default=1.0,
|
|
||||||
name="continuous_value_scale",
|
name="continuous_value_scale",
|
||||||
)
|
)
|
||||||
if not torch.isfinite(center).all():
|
if not torch.isfinite(center).all():
|
||||||
@@ -130,11 +124,10 @@ class OtherInfoTokenizer(nn.Module):
|
|||||||
value: torch.Tensor | list[float] | None,
|
value: torch.Tensor | list[float] | None,
|
||||||
*,
|
*,
|
||||||
n_cont_types: int,
|
n_cont_types: int,
|
||||||
default: float,
|
|
||||||
name: str,
|
name: str,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
if value is None:
|
if value is None:
|
||||||
return torch.full((n_cont_types,), float(default), dtype=torch.float32)
|
raise ValueError(f"{name} is required")
|
||||||
tensor = torch.as_tensor(value, dtype=torch.float32).detach().clone()
|
tensor = torch.as_tensor(value, dtype=torch.float32).detach().clone()
|
||||||
if tensor.shape != (n_cont_types,):
|
if tensor.shape != (n_cont_types,):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -184,12 +177,11 @@ class OtherInfoTokenizer(nn.Module):
|
|||||||
"cont_type_ids"
|
"cont_type_ids"
|
||||||
)
|
)
|
||||||
cont_value = other_value[cont_pos].to(type_emb.dtype)
|
cont_value = other_value[cont_pos].to(type_emb.dtype)
|
||||||
if self.continuous_value_scaling == "robust":
|
|
||||||
if (
|
if (
|
||||||
self.continuous_value_center is None
|
self.continuous_value_center is None
|
||||||
or self.continuous_value_scale is None
|
or self.continuous_value_scale is None
|
||||||
):
|
):
|
||||||
raise RuntimeError("Robust continuous-value scaler buffers are missing")
|
raise RuntimeError("RobustScale buffers are missing")
|
||||||
center = self.continuous_value_center[cont_idx].to(type_emb.dtype)
|
center = self.continuous_value_center[cont_idx].to(type_emb.dtype)
|
||||||
scale = self.continuous_value_scale[cont_idx].to(type_emb.dtype)
|
scale = self.continuous_value_scale[cont_idx].to(type_emb.dtype)
|
||||||
cont_value = (cont_value - center) / scale
|
cont_value = (cont_value - center) / scale
|
||||||
@@ -221,12 +213,11 @@ class DeepHealth(nn.Module):
|
|||||||
cont_type_ids: list[int],
|
cont_type_ids: list[int],
|
||||||
n_value_kinds: int = 3,
|
n_value_kinds: int = 3,
|
||||||
n_bins: int = 16,
|
n_bins: int = 16,
|
||||||
continuous_value_scaling: str = "none",
|
|
||||||
continuous_value_center: torch.Tensor | list[float] | None = None,
|
continuous_value_center: torch.Tensor | list[float] | None = None,
|
||||||
continuous_value_scale: torch.Tensor | list[float] | None = None,
|
continuous_value_scale: torch.Tensor | list[float] | None = None,
|
||||||
target_mode: str = "next_token", # "next_token" or "all_future"
|
target_mode: str = "next_token", # "next_token" or "all_future"
|
||||||
time_mode: str = "absolute", # next_token requires absolute
|
time_mode: str = "absolute", # next_token requires absolute
|
||||||
dist_mode: str = "exponential", # "exponential", "weibull" or "mixed"
|
dist_mode: str = "exponential", # "exponential" or "weibull"
|
||||||
extra_pool_reduce: str = "mean",
|
extra_pool_reduce: str = "mean",
|
||||||
dropout: float = 0.0,
|
dropout: float = 0.0,
|
||||||
model_architecture: str | None = None,
|
model_architecture: str | None = None,
|
||||||
@@ -243,9 +234,9 @@ class DeepHealth(nn.Module):
|
|||||||
"next_token is reserved for Delphi2M reproduction and "
|
"next_token is reserved for Delphi2M reproduction and "
|
||||||
"requires time_mode='absolute'"
|
"requires time_mode='absolute'"
|
||||||
)
|
)
|
||||||
if dist_mode not in ["exponential", "weibull", "mixed"]:
|
if dist_mode not in ["exponential", "weibull"]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"dist_mode must be either 'exponential', 'weibull' or 'mixed'")
|
"dist_mode must be either 'exponential' or 'weibull'")
|
||||||
if extra_pool_reduce not in {"mean", "sum"}:
|
if extra_pool_reduce not in {"mean", "sum"}:
|
||||||
raise ValueError("extra_pool_reduce must be either 'mean' or 'sum'")
|
raise ValueError("extra_pool_reduce must be either 'mean' or 'sum'")
|
||||||
if n_layer < 1:
|
if n_layer < 1:
|
||||||
@@ -262,7 +253,6 @@ class DeepHealth(nn.Module):
|
|||||||
cont_type_ids=cont_type_ids,
|
cont_type_ids=cont_type_ids,
|
||||||
n_value_kinds=n_value_kinds,
|
n_value_kinds=n_value_kinds,
|
||||||
n_bins=n_bins,
|
n_bins=n_bins,
|
||||||
continuous_value_scaling=continuous_value_scaling,
|
|
||||||
continuous_value_center=continuous_value_center,
|
continuous_value_center=continuous_value_center,
|
||||||
continuous_value_scale=continuous_value_scale,
|
continuous_value_scale=continuous_value_scale,
|
||||||
)
|
)
|
||||||
@@ -270,7 +260,6 @@ class DeepHealth(nn.Module):
|
|||||||
self.time_mode = time_mode
|
self.time_mode = time_mode
|
||||||
self.dist_mode = dist_mode
|
self.dist_mode = dist_mode
|
||||||
self.extra_pool_reduce = extra_pool_reduce
|
self.extra_pool_reduce = extra_pool_reduce
|
||||||
self.continuous_value_scaling = str(continuous_value_scaling).lower()
|
|
||||||
self.model_architecture = model_architecture
|
self.model_architecture = model_architecture
|
||||||
self.n_layer = n_layer
|
self.n_layer = n_layer
|
||||||
self.n_embd = n_embd
|
self.n_embd = n_embd
|
||||||
@@ -283,12 +272,6 @@ class DeepHealth(nn.Module):
|
|||||||
nn.init.zeros_(self.rho_head.weight)
|
nn.init.zeros_(self.rho_head.weight)
|
||||||
nn.init.constant_(self.rho_head.bias, 0.5413)
|
nn.init.constant_(self.rho_head.bias, 0.5413)
|
||||||
|
|
||||||
if dist_mode == "mixed":
|
|
||||||
self.death_idx = vocab_size - 1
|
|
||||||
self.rho_death_head = nn.Linear(n_embd, 1)
|
|
||||||
nn.init.zeros_(self.rho_death_head.weight)
|
|
||||||
nn.init.constant_(self.rho_death_head.bias, 0.5413)
|
|
||||||
|
|
||||||
if time_mode == "absolute":
|
if time_mode == "absolute":
|
||||||
self.age_encoding = AgeSinusoidalEncoding(n_embd)
|
self.age_encoding = AgeSinusoidalEncoding(n_embd)
|
||||||
self.blocks = nn.ModuleList([
|
self.blocks = nn.ModuleList([
|
||||||
@@ -551,10 +534,3 @@ class DeepHealth(nn.Module):
|
|||||||
f"calc_weibull_rho called with dist_mode={self.dist_mode!r}"
|
f"calc_weibull_rho called with dist_mode={self.dist_mode!r}"
|
||||||
)
|
)
|
||||||
return F.softplus(self.rho_head(x)) + 1e-6
|
return F.softplus(self.rho_head(x)) + 1e-6
|
||||||
|
|
||||||
def calc_death_rho(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
if self.dist_mode != "mixed":
|
|
||||||
raise RuntimeError(
|
|
||||||
f"calc_death_rho called with dist_mode={self.dist_mode!r}"
|
|
||||||
)
|
|
||||||
return F.softplus(self.rho_death_head(x)).squeeze(-1) + 1e-6
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This script converts raw UK Biobank CSV exports into the artefacts consumed by
|
|||||||
DeepHealth:
|
DeepHealth:
|
||||||
|
|
||||||
* ``ukb_event_data.npy``: ``(N, 3)`` uint32 array of ``(eid, days, label)``
|
* ``ukb_event_data.npy``: ``(N, 3)`` uint32 array of ``(eid, days, label)``
|
||||||
disease/death/checkup events sorted by patient then time.
|
disease/death events sorted by patient then time.
|
||||||
* ``ukb_basic_info.csv``: basic patient table indexed by ``eid`` with ``sex``.
|
* ``ukb_basic_info.csv``: basic patient table indexed by ``eid`` with ``sex``.
|
||||||
* ``ukb_other_info.npy``: ``(M, 5)`` float64 array of
|
* ``ukb_other_info.npy``: ``(M, 5)`` float64 array of
|
||||||
``(eid, type, value, value_kind, time)`` rows. ``type=0`` is reserved for
|
``(eid, type, value, value_kind, time)`` rows. ``type=0`` is reserved for
|
||||||
@@ -219,7 +219,8 @@ with open(labels_file, encoding="utf-8") as f: # Open labels file
|
|||||||
for idx, line in enumerate(f): # Enumerate to assign incremental label IDs
|
for idx, line in enumerate(f): # Enumerate to assign incremental label IDs
|
||||||
parts = line.strip().split(" ") # Split by space
|
parts = line.strip().split(" ") # Split by space
|
||||||
if parts and parts[0]: # Guard against empty lines
|
if parts and parts[0]: # Guard against empty lines
|
||||||
# Start labels from 1 to reserve 0 for padding, 1 for checkup
|
# Keep raw disease ids at 2+ so existing prepared data and model
|
||||||
|
# vocabulary indices remain stable; raw id 1 is unused.
|
||||||
label_dict[parts[0]] = idx + 2
|
label_dict[parts[0]] = idx + 2
|
||||||
|
|
||||||
# Pre-build lookup: ICD/Death column name -> integer label for fast per-column extraction
|
# Pre-build lookup: ICD/Death column name -> integer label for fast per-column extraction
|
||||||
@@ -327,19 +328,6 @@ for ukb_chunk in tqdm.tqdm(ukb_iterator, desc="Processing UK Biobank data"):
|
|||||||
if cancer_frames:
|
if cancer_frames:
|
||||||
event_list.append(np.vstack(cancer_frames))
|
event_list.append(np.vstack(cancer_frames))
|
||||||
|
|
||||||
# Add checkup events with label=1 using date_of_assessment (already in days from dob)
|
|
||||||
if "date_of_assessment" in ukb_chunk.columns:
|
|
||||||
doa_series = ukb_chunk["date_of_assessment"].dropna()
|
|
||||||
if not doa_series.empty:
|
|
||||||
checkup_data = np.column_stack(
|
|
||||||
(
|
|
||||||
doa_series.index.values,
|
|
||||||
doa_series.values.astype(int),
|
|
||||||
np.ones(len(doa_series), dtype=int),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
event_list.append(checkup_data)
|
|
||||||
|
|
||||||
# Combine tabular chunks
|
# Combine tabular chunks
|
||||||
final_tabular = pd.concat(tabular_list, axis=0, ignore_index=False)
|
final_tabular = pd.concat(tabular_list, axis=0, ignore_index=False)
|
||||||
final_tabular.index.name = "eid" # Ensure index named consistently
|
final_tabular.index.name = "eid" # Ensure index named consistently
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ with exactly three fields:
|
|||||||
token int32
|
token int32
|
||||||
|
|
||||||
``token`` follows the existing ``labels.csv`` convention used by
|
``token`` follows the existing ``labels.csv`` convention used by
|
||||||
``prepare_data.py``: padding=0, checkup=1 (not emitted here), and the first
|
``prepare_data.py``: padding=0, token 1 is reserved and unused, and the first
|
||||||
label in ``labels.csv`` receives token 2. Each ``(eid, token)`` is deduplicated
|
label in ``labels.csv`` receives token 2. Each ``(eid, token)`` is deduplicated
|
||||||
to the first known event date.
|
to the first known event date.
|
||||||
|
|
||||||
The output is intended for calendar-indexed temperature and air-pollution
|
The output is intended for calendar-indexed temperature and air-pollution
|
||||||
queries. It contains no date of birth, sex, covariates, or checkup events.
|
queries. It contains no date of birth, sex, covariates, or assessment events.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import numpy as np
|
|||||||
|
|
||||||
|
|
||||||
PAD_IDX = 0
|
PAD_IDX = 0
|
||||||
CHECKUP_IDX = 1
|
RESERVED_IDX = 1
|
||||||
NO_EVENT_IDX = 2
|
NO_EVENT_IDX = 2
|
||||||
DAYS_PER_YEAR = 365.25
|
DAYS_PER_YEAR = 365.25
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ def build_next_token_targets(
|
|||||||
target_events: [x1, x2, ..., xN-1]
|
target_events: [x1, x2, ..., xN-1]
|
||||||
target_times_years: [t1, t2, ..., tN-1] / 365.25
|
target_times_years: [t1, t2, ..., tN-1] / 365.25
|
||||||
|
|
||||||
This function does not ignore PAD/CHECKUP/NO_EVENT. Ignoring belongs to
|
This function does not ignore PAD/RESERVED/NO_EVENT. Ignoring belongs to
|
||||||
the loss function because different objectives may use different ignore ids.
|
the loss function because different objectives may use different ignore ids.
|
||||||
"""
|
"""
|
||||||
labels = _as_numpy_1d(labels, "labels", np.int64)
|
labels = _as_numpy_1d(labels, "labels", np.int64)
|
||||||
|
|||||||
@@ -207,19 +207,15 @@ class IPCWCalibrationMetricTests(unittest.TestCase):
|
|||||||
rho = np.asarray([0.8, 1.0, 1.2, 1.5], dtype=np.float32)
|
rho = np.asarray([0.8, 1.0, 1.2, 1.5], dtype=np.float32)
|
||||||
horizons = np.asarray([0.1, 1.0, 5.0], dtype=np.float32)
|
horizons = np.asarray([0.1, 1.0, 5.0], dtype=np.float32)
|
||||||
|
|
||||||
for dist_mode, token, death_idx, selected_rho in (
|
for dist_mode, selected_rho in (
|
||||||
("exponential", 4, 9, None),
|
("exponential", None),
|
||||||
("weibull", 4, 9, rho),
|
("weibull", rho),
|
||||||
("mixed", 9, 9, rho),
|
|
||||||
("mixed", 4, 9, None),
|
|
||||||
):
|
):
|
||||||
actual = _risk_probability_matrix(
|
actual = _risk_probability_matrix(
|
||||||
logits=logits,
|
logits=logits,
|
||||||
rho=selected_rho,
|
rho=selected_rho,
|
||||||
horizons=horizons,
|
horizons=horizons,
|
||||||
dist_mode=dist_mode,
|
dist_mode=dist_mode,
|
||||||
token=token,
|
|
||||||
death_idx=death_idx,
|
|
||||||
)
|
)
|
||||||
expected = np.vstack(
|
expected = np.vstack(
|
||||||
[
|
[
|
||||||
@@ -229,8 +225,6 @@ class IPCWCalibrationMetricTests(unittest.TestCase):
|
|||||||
score_mode="risk",
|
score_mode="risk",
|
||||||
horizon=float(horizon),
|
horizon=float(horizon),
|
||||||
dist_mode=dist_mode,
|
dist_mode=dist_mode,
|
||||||
token=token,
|
|
||||||
death_idx=death_idx,
|
|
||||||
)
|
)
|
||||||
for horizon in horizons
|
for horizon in horizons
|
||||||
]
|
]
|
||||||
@@ -271,7 +265,6 @@ class IPCWCalibrationMetricTests(unittest.TestCase):
|
|||||||
"label_id_to_code": {4: "D4", 5: "D5"},
|
"label_id_to_code": {4: "D4", 5: "D5"},
|
||||||
"dist_mode": "exponential",
|
"dist_mode": "exponential",
|
||||||
"horizons": np.asarray([1.0, 5.0], dtype=np.float32),
|
"horizons": np.asarray([1.0, 5.0], dtype=np.float32),
|
||||||
"death_index": 9,
|
|
||||||
"min_cases": 1,
|
"min_cases": 1,
|
||||||
"min_controls": 1,
|
"min_controls": 1,
|
||||||
"max_ipcw_weight": 0.0,
|
"max_ipcw_weight": 0.0,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import torch
|
|||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
from torch.utils.data import Subset
|
from torch.utils.data import Subset
|
||||||
|
|
||||||
from models import OtherInfoTokenizer
|
from eval_data import build_model_from_dataset
|
||||||
|
from models import DeepHealth, OtherInfoTokenizer
|
||||||
from train_util import fit_continuous_robust_scaler
|
from train_util import fit_continuous_robust_scaler
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +60,17 @@ class ContinuousValueScalingTests(unittest.TestCase):
|
|||||||
np.testing.assert_allclose(stats.center, np.asarray([2.0, 10.0]))
|
np.testing.assert_allclose(stats.center, np.asarray([2.0, 10.0]))
|
||||||
np.testing.assert_allclose(stats.scale, np.asarray([2.0, 1.0]))
|
np.testing.assert_allclose(stats.scale, np.asarray([2.0, 1.0]))
|
||||||
|
|
||||||
|
def test_fit_supports_next_step_sample_storage(self):
|
||||||
|
dataset = _ToyAllFutureDataset()
|
||||||
|
dataset.samples = dataset.patients
|
||||||
|
del dataset.patients
|
||||||
|
train_subset = Subset(dataset, np.asarray([0, 1, 2, 3, 4]))
|
||||||
|
|
||||||
|
stats = fit_continuous_robust_scaler(dataset, train_subset)
|
||||||
|
|
||||||
|
np.testing.assert_allclose(stats.center, np.asarray([2.0, 10.0]))
|
||||||
|
np.testing.assert_allclose(stats.scale, np.asarray([2.0, 1.0]))
|
||||||
|
|
||||||
def test_tokenizer_standardizes_only_continuous_values(self):
|
def test_tokenizer_standardizes_only_continuous_values(self):
|
||||||
tokenizer = OtherInfoTokenizer(
|
tokenizer = OtherInfoTokenizer(
|
||||||
n_embd=4,
|
n_embd=4,
|
||||||
@@ -66,7 +78,6 @@ class ContinuousValueScalingTests(unittest.TestCase):
|
|||||||
n_cont_types=2,
|
n_cont_types=2,
|
||||||
n_categories=3,
|
n_categories=3,
|
||||||
cont_type_ids=[1, 3],
|
cont_type_ids=[1, 3],
|
||||||
continuous_value_scaling="robust",
|
|
||||||
continuous_value_center=[10.0, 100.0],
|
continuous_value_center=[10.0, 100.0],
|
||||||
continuous_value_scale=[2.0, 20.0],
|
continuous_value_scale=[2.0, 20.0],
|
||||||
)
|
)
|
||||||
@@ -89,7 +100,6 @@ class ContinuousValueScalingTests(unittest.TestCase):
|
|||||||
n_cont_types=2,
|
n_cont_types=2,
|
||||||
n_categories=2,
|
n_categories=2,
|
||||||
cont_type_ids=[1, 3],
|
cont_type_ids=[1, 3],
|
||||||
continuous_value_scaling="robust",
|
|
||||||
continuous_value_center=[2.0, 10.0],
|
continuous_value_center=[2.0, 10.0],
|
||||||
continuous_value_scale=[1.5, 4.0],
|
continuous_value_scale=[1.5, 4.0],
|
||||||
)
|
)
|
||||||
@@ -103,7 +113,8 @@ class ContinuousValueScalingTests(unittest.TestCase):
|
|||||||
n_cont_types=2,
|
n_cont_types=2,
|
||||||
n_categories=2,
|
n_categories=2,
|
||||||
cont_type_ids=[1, 3],
|
cont_type_ids=[1, 3],
|
||||||
continuous_value_scaling="robust",
|
continuous_value_center=[0.0, 0.0],
|
||||||
|
continuous_value_scale=[1.0, 1.0],
|
||||||
)
|
)
|
||||||
restored.load_state_dict(state, strict=True)
|
restored.load_state_dict(state, strict=True)
|
||||||
|
|
||||||
@@ -116,27 +127,99 @@ class ContinuousValueScalingTests(unittest.TestCase):
|
|||||||
torch.tensor([1.5, 4.0]),
|
torch.tensor([1.5, 4.0]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_legacy_none_mode_keeps_old_state_dict_schema(self):
|
def test_continuous_tokenizer_rejects_missing_scaler_statistics(self):
|
||||||
tokenizer = OtherInfoTokenizer(
|
with self.assertRaisesRegex(ValueError, "require train-split RobustScale"):
|
||||||
|
OtherInfoTokenizer(
|
||||||
n_embd=4,
|
n_embd=4,
|
||||||
n_types=4,
|
n_types=4,
|
||||||
n_cont_types=2,
|
n_cont_types=2,
|
||||||
n_categories=2,
|
n_categories=2,
|
||||||
cont_type_ids=[1, 3],
|
cont_type_ids=[1, 3],
|
||||||
)
|
)
|
||||||
state = tokenizer.state_dict()
|
|
||||||
|
|
||||||
self.assertNotIn("continuous_value_center", state)
|
def test_evaluation_rejects_unscaled_continuous_checkpoint(self):
|
||||||
self.assertNotIn("continuous_value_scale", state)
|
dataset = type(
|
||||||
restored = OtherInfoTokenizer(
|
"DatasetMetadata",
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
"vocab_size": 8,
|
||||||
|
"n_types": 4,
|
||||||
|
"n_cont_types": 2,
|
||||||
|
"n_categories": 2,
|
||||||
|
"cont_type_ids": [1, 3],
|
||||||
|
},
|
||||||
|
)()
|
||||||
|
cfg = {
|
||||||
|
"model_target_mode": "all_future",
|
||||||
|
"target_mode": "all_future",
|
||||||
|
"model_architecture": "transformer_ffn_v1",
|
||||||
|
"n_layer": 1,
|
||||||
|
"time_mode": "absolute",
|
||||||
|
"dist_mode": "exponential",
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "unscaled checkpoints are not supported"):
|
||||||
|
build_model_from_dataset(
|
||||||
|
None,
|
||||||
|
cfg,
|
||||||
|
dataset,
|
||||||
|
state_dict={"blocks.0.mlp.w1.weight": torch.zeros(1)},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_evaluation_restores_required_scaler_buffers(self):
|
||||||
|
dataset = type(
|
||||||
|
"DatasetMetadata",
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
"vocab_size": 8,
|
||||||
|
"n_types": 4,
|
||||||
|
"n_cont_types": 2,
|
||||||
|
"n_categories": 2,
|
||||||
|
"cont_type_ids": [1, 3],
|
||||||
|
},
|
||||||
|
)()
|
||||||
|
source = DeepHealth(
|
||||||
|
vocab_size=8,
|
||||||
n_embd=4,
|
n_embd=4,
|
||||||
|
n_head=1,
|
||||||
|
n_layer=1,
|
||||||
n_types=4,
|
n_types=4,
|
||||||
n_cont_types=2,
|
n_cont_types=2,
|
||||||
n_categories=2,
|
n_categories=2,
|
||||||
cont_type_ids=[1, 3],
|
cont_type_ids=[1, 3],
|
||||||
|
continuous_value_center=[2.0, 10.0],
|
||||||
|
continuous_value_scale=[1.5, 4.0],
|
||||||
|
target_mode="all_future",
|
||||||
|
time_mode="absolute",
|
||||||
|
dist_mode="exponential",
|
||||||
|
model_architecture="transformer_ffn_v1",
|
||||||
)
|
)
|
||||||
|
state = source.state_dict()
|
||||||
|
cfg = {
|
||||||
|
"model_target_mode": "all_future",
|
||||||
|
"target_mode": "all_future",
|
||||||
|
"model_architecture": "transformer_ffn_v1",
|
||||||
|
"n_embd": 4,
|
||||||
|
"n_head": 1,
|
||||||
|
"n_layer": 1,
|
||||||
|
"n_bins": 16,
|
||||||
|
"time_mode": "absolute",
|
||||||
|
"dist_mode": "exponential",
|
||||||
|
"continuous_value_scaling": "robust",
|
||||||
|
}
|
||||||
|
|
||||||
|
restored = build_model_from_dataset(None, cfg, dataset, state_dict=state)
|
||||||
restored.load_state_dict(state, strict=True)
|
restored.load_state_dict(state, strict=True)
|
||||||
|
|
||||||
|
torch.testing.assert_close(
|
||||||
|
restored.tokenizer.continuous_value_center,
|
||||||
|
torch.tensor([2.0, 10.0]),
|
||||||
|
)
|
||||||
|
torch.testing.assert_close(
|
||||||
|
restored.tokenizer.continuous_value_scale,
|
||||||
|
torch.tensor([1.5, 4.0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from dataset import _ExpoBaseDataset
|
|
||||||
from targets import CHECKUP_IDX
|
|
||||||
from train_util import load_extra_info_types_file
|
|
||||||
|
|
||||||
|
|
||||||
class CheckupSelectionTests(unittest.TestCase):
|
|
||||||
@staticmethod
|
|
||||||
def _base(extra_info_types):
|
|
||||||
dataset = _ExpoBaseDataset.__new__(_ExpoBaseDataset)
|
|
||||||
dataset.extra_info_types = list(extra_info_types)
|
|
||||||
dataset.event_data = np.asarray(
|
|
||||||
[
|
|
||||||
[101, 10, CHECKUP_IDX],
|
|
||||||
[101, 20, 2],
|
|
||||||
[101, 30, 3],
|
|
||||||
],
|
|
||||||
dtype=np.float64,
|
|
||||||
)
|
|
||||||
return dataset
|
|
||||||
|
|
||||||
def test_explicit_empty_extra_info_removes_checkup(self):
|
|
||||||
project_root = Path(__file__).resolve().parents[1]
|
|
||||||
selected_types = load_extra_info_types_file(
|
|
||||||
str(project_root / "extra_info_types_none.txt")
|
|
||||||
)
|
|
||||||
self.assertEqual(selected_types, [])
|
|
||||||
dataset = self._base(selected_types)
|
|
||||||
|
|
||||||
rows = list(dataset._iter_patient_events(impute_no_event_gaps=False))
|
|
||||||
|
|
||||||
self.assertEqual(len(rows), 1)
|
|
||||||
eid, times, labels = rows[0]
|
|
||||||
self.assertEqual(eid, 101)
|
|
||||||
np.testing.assert_array_equal(times, np.asarray([20, 30], dtype=np.float32))
|
|
||||||
self.assertNotIn(CHECKUP_IDX, labels.tolist())
|
|
||||||
|
|
||||||
def test_selected_extra_info_keeps_checkup(self):
|
|
||||||
dataset = self._base([11])
|
|
||||||
|
|
||||||
rows = list(dataset._iter_patient_events(impute_no_event_gaps=False))
|
|
||||||
|
|
||||||
self.assertEqual(len(rows), 1)
|
|
||||||
_, times, labels = rows[0]
|
|
||||||
np.testing.assert_array_equal(
|
|
||||||
times,
|
|
||||||
np.asarray([10, 20, 30], dtype=np.float32),
|
|
||||||
)
|
|
||||||
self.assertEqual(labels[0], CHECKUP_IDX)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
44
tests/test_dataset_reserved_event.py
Normal file
44
tests/test_dataset_reserved_event.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from dataset import _ExpoBaseDataset
|
||||||
|
from targets import RESERVED_IDX
|
||||||
|
|
||||||
|
|
||||||
|
class ReservedEventFilteringTests(unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def _base(extra_info_types):
|
||||||
|
dataset = _ExpoBaseDataset.__new__(_ExpoBaseDataset)
|
||||||
|
dataset.extra_info_types = list(extra_info_types)
|
||||||
|
dataset.event_data = np.asarray(
|
||||||
|
[
|
||||||
|
[101, 10, RESERVED_IDX],
|
||||||
|
[101, 20, 2],
|
||||||
|
[101, 30, 3],
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
return dataset
|
||||||
|
|
||||||
|
def _assert_reserved_event_removed(self, extra_info_types):
|
||||||
|
dataset = self._base(extra_info_types)
|
||||||
|
|
||||||
|
rows = list(dataset._iter_patient_events(impute_no_event_gaps=False))
|
||||||
|
|
||||||
|
self.assertEqual(len(rows), 1)
|
||||||
|
eid, times, labels = rows[0]
|
||||||
|
self.assertEqual(eid, 101)
|
||||||
|
np.testing.assert_array_equal(times, np.asarray([20, 30], dtype=np.float32))
|
||||||
|
np.testing.assert_array_equal(labels, np.asarray([3, 4], dtype=np.int64))
|
||||||
|
self.assertNotIn(RESERVED_IDX, labels.tolist())
|
||||||
|
|
||||||
|
def test_empty_extra_info_removes_legacy_reserved_event(self):
|
||||||
|
self._assert_reserved_event_removed([])
|
||||||
|
|
||||||
|
def test_selected_extra_info_removes_legacy_reserved_event(self):
|
||||||
|
self._assert_reserved_event_removed([11])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -37,7 +37,7 @@ from model_architectures import (
|
|||||||
SUPPORTED_MODEL_ARCHITECTURES,
|
SUPPORTED_MODEL_ARCHITECTURES,
|
||||||
)
|
)
|
||||||
from models import DeepHealth
|
from models import DeepHealth
|
||||||
from targets import CHECKUP_IDX, PAD_IDX
|
from targets import PAD_IDX, RESERVED_IDX
|
||||||
from train_util import (
|
from train_util import (
|
||||||
ContinuousRobustScalerStats,
|
ContinuousRobustScalerStats,
|
||||||
configure_torch_for_training,
|
configure_torch_for_training,
|
||||||
@@ -106,22 +106,12 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--n_head", type=int, default=10)
|
parser.add_argument("--n_head", type=int, default=10)
|
||||||
parser.add_argument("--n_layer", type=int, default=12)
|
parser.add_argument("--n_layer", type=int, default=12)
|
||||||
parser.add_argument("--n_bins", type=int, default=16)
|
parser.add_argument("--n_bins", type=int, default=16)
|
||||||
parser.add_argument(
|
|
||||||
"--continuous_value_scaling",
|
|
||||||
type=str,
|
|
||||||
default="robust",
|
|
||||||
choices=["none", "robust"],
|
|
||||||
help=(
|
|
||||||
"Continuous extra-info scaling. 'robust' fits the median and IQR "
|
|
||||||
"on the complete training subset and stores them in the checkpoint."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument("--extra_pool_reduce", type=str, default="mean",
|
parser.add_argument("--extra_pool_reduce", type=str, default="mean",
|
||||||
choices=["mean", "sum"])
|
choices=["mean", "sum"])
|
||||||
parser.add_argument("--time_mode", type=str, default="relative",
|
parser.add_argument("--time_mode", type=str, default="relative",
|
||||||
choices=["relative", "absolute"])
|
choices=["relative", "absolute"])
|
||||||
parser.add_argument("--dist_mode", type=str, default="exponential",
|
parser.add_argument("--dist_mode", type=str, default="exponential",
|
||||||
choices=["exponential", "weibull", "mixed"])
|
choices=["exponential", "weibull"])
|
||||||
parser.add_argument("--dropout", type=float, default=0.0)
|
parser.add_argument("--dropout", type=float, default=0.0)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--model_architecture",
|
"--model_architecture",
|
||||||
@@ -188,19 +178,14 @@ def parse_args() -> argparse.Namespace:
|
|||||||
def build_model(
|
def build_model(
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
dataset: AllFutureHealthDataset,
|
dataset: AllFutureHealthDataset,
|
||||||
scaler_stats: ContinuousRobustScalerStats | None = None,
|
scaler_stats: ContinuousRobustScalerStats,
|
||||||
) -> DeepHealth:
|
) -> DeepHealth:
|
||||||
if (
|
if tuple(int(x) for x in dataset.cont_type_ids) != scaler_stats.cont_type_ids:
|
||||||
args.continuous_value_scaling == "robust"
|
|
||||||
and dataset.n_cont_types > 0
|
|
||||||
and scaler_stats is None
|
|
||||||
):
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Robust continuous-value scaling requires statistics fitted on the "
|
"RobustScale statistics are not aligned with dataset.cont_type_ids"
|
||||||
"training subset"
|
|
||||||
)
|
)
|
||||||
center = None if scaler_stats is None else scaler_stats.center
|
center = scaler_stats.center if dataset.n_cont_types > 0 else None
|
||||||
scale = None if scaler_stats is None else scaler_stats.scale
|
scale = scaler_stats.scale if dataset.n_cont_types > 0 else None
|
||||||
return DeepHealth(
|
return DeepHealth(
|
||||||
vocab_size=dataset.vocab_size,
|
vocab_size=dataset.vocab_size,
|
||||||
n_embd=args.n_embd,
|
n_embd=args.n_embd,
|
||||||
@@ -211,7 +196,6 @@ def build_model(
|
|||||||
n_categories=dataset.n_categories,
|
n_categories=dataset.n_categories,
|
||||||
cont_type_ids=dataset.cont_type_ids,
|
cont_type_ids=dataset.cont_type_ids,
|
||||||
n_bins=args.n_bins,
|
n_bins=args.n_bins,
|
||||||
continuous_value_scaling=args.continuous_value_scaling,
|
|
||||||
continuous_value_center=center,
|
continuous_value_center=center,
|
||||||
continuous_value_scale=scale,
|
continuous_value_scale=scale,
|
||||||
extra_pool_reduce=args.extra_pool_reduce,
|
extra_pool_reduce=args.extra_pool_reduce,
|
||||||
@@ -223,18 +207,12 @@ def build_model(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_criterion(args: argparse.Namespace, dataset: AllFutureHealthDataset):
|
def build_criterion(args: argparse.Namespace):
|
||||||
ignored_idx = {PAD_IDX, CHECKUP_IDX}
|
ignored_idx = {PAD_IDX, RESERVED_IDX}
|
||||||
if args.dist_mode == "exponential":
|
if args.dist_mode == "exponential":
|
||||||
return build_loss("exponential", ignored_idx=ignored_idx)
|
return build_loss("exponential", ignored_idx=ignored_idx)
|
||||||
if args.dist_mode == "weibull":
|
if args.dist_mode == "weibull":
|
||||||
return build_loss("weibull", ignored_idx=ignored_idx)
|
return build_loss("weibull", ignored_idx=ignored_idx)
|
||||||
if args.dist_mode == "mixed":
|
|
||||||
return build_loss(
|
|
||||||
"mixed",
|
|
||||||
death_idx=dataset.vocab_size - 1,
|
|
||||||
ignored_idx=ignored_idx,
|
|
||||||
)
|
|
||||||
raise ValueError(f"Unknown dist_mode: {args.dist_mode}")
|
raise ValueError(f"Unknown dist_mode: {args.dist_mode}")
|
||||||
|
|
||||||
|
|
||||||
@@ -247,7 +225,7 @@ def compute_all_future_loss(
|
|||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
required_keys = set(MODEL_INPUT_KEYS)
|
required_keys = set(MODEL_INPUT_KEYS)
|
||||||
required_keys.update(("future_targets", "exposure"))
|
required_keys.update(("future_targets", "exposure"))
|
||||||
if args.dist_mode in {"weibull", "mixed"}:
|
if args.dist_mode == "weibull":
|
||||||
required_keys.add("future_dt")
|
required_keys.add("future_dt")
|
||||||
batch = move_batch_to_device(
|
batch = move_batch_to_device(
|
||||||
{key: batch[key] for key in required_keys},
|
{key: batch[key] for key in required_keys},
|
||||||
@@ -282,13 +260,7 @@ def compute_all_future_loss(
|
|||||||
exposure=batch["exposure"],
|
exposure=batch["exposure"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
loss = criterion(
|
raise ValueError(f"Unknown dist_mode: {args.dist_mode}")
|
||||||
logits=logits,
|
|
||||||
death_rho=model.calc_death_rho(hidden),
|
|
||||||
targets=batch["future_targets"],
|
|
||||||
dt=batch["future_dt"],
|
|
||||||
exposure=batch["exposure"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not torch.isfinite(loss):
|
if not torch.isfinite(loss):
|
||||||
raise RuntimeError(f"Loss is not finite: {float(loss.detach().cpu())}")
|
raise RuntimeError(f"Loss is not finite: {float(loss.detach().cpu())}")
|
||||||
@@ -352,15 +324,8 @@ def build_metadata(
|
|||||||
train_subset,
|
train_subset,
|
||||||
val_subset,
|
val_subset,
|
||||||
test_subset,
|
test_subset,
|
||||||
scaler_stats: ContinuousRobustScalerStats | None,
|
scaler_stats: ContinuousRobustScalerStats,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
scaler_metadata: Dict[str, Any]
|
|
||||||
if scaler_stats is None:
|
|
||||||
scaler_metadata = {
|
|
||||||
"method": "none",
|
|
||||||
"fitted_on": None,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
scaler_metadata = scaler_stats.as_metadata()
|
scaler_metadata = scaler_stats.as_metadata()
|
||||||
return {
|
return {
|
||||||
"run_name": run_name,
|
"run_name": run_name,
|
||||||
@@ -370,6 +335,8 @@ def build_metadata(
|
|||||||
"model_architecture": args.model_architecture,
|
"model_architecture": args.model_architecture,
|
||||||
"model_target_mode": "all_future",
|
"model_target_mode": "all_future",
|
||||||
"target_mode": "all_future",
|
"target_mode": "all_future",
|
||||||
|
"event_stream_version": "disease_death_only_v1",
|
||||||
|
"uses_assessment_event_token": False,
|
||||||
"dist_mode": args.dist_mode,
|
"dist_mode": args.dist_mode,
|
||||||
"disease_history_mode": args.disease_history_mode,
|
"disease_history_mode": args.disease_history_mode,
|
||||||
"all_future_min_history_events": int(args.min_history_events),
|
"all_future_min_history_events": int(args.min_history_events),
|
||||||
@@ -381,6 +348,7 @@ def build_metadata(
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
||||||
|
"continuous_value_scaling": "robust",
|
||||||
"continuous_value_scaler": scaler_metadata,
|
"continuous_value_scaler": scaler_metadata,
|
||||||
"dataset_metadata": {
|
"dataset_metadata": {
|
||||||
"vocab_size": int(dataset.vocab_size),
|
"vocab_size": int(dataset.vocab_size),
|
||||||
@@ -389,6 +357,8 @@ def build_metadata(
|
|||||||
"n_categories": int(dataset.n_categories),
|
"n_categories": int(dataset.n_categories),
|
||||||
"cont_type_ids": [int(x) for x in dataset.cont_type_ids],
|
"cont_type_ids": [int(x) for x in dataset.cont_type_ids],
|
||||||
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
||||||
|
"event_stream_version": "disease_death_only_v1",
|
||||||
|
"uses_assessment_event_token": False,
|
||||||
},
|
},
|
||||||
"split_sizes": {
|
"split_sizes": {
|
||||||
"train": int(len(train_subset)),
|
"train": int(len(train_subset)),
|
||||||
@@ -425,7 +395,7 @@ def main() -> None:
|
|||||||
logger.info(f"Model architecture: {args.model_architecture}")
|
logger.info(f"Model architecture: {args.model_architecture}")
|
||||||
logger.info(f"Disease history mode: {args.disease_history_mode}")
|
logger.info(f"Disease history mode: {args.disease_history_mode}")
|
||||||
logger.info(f"extra_info_types: {format_extra_info_types(args.extra_info_types)}")
|
logger.info(f"extra_info_types: {format_extra_info_types(args.extra_info_types)}")
|
||||||
logger.info(f"Continuous value scaling: {args.continuous_value_scaling}")
|
logger.info("Continuous value scaling: RobustScale (required)")
|
||||||
|
|
||||||
logger.info("Loading all-future datasets...")
|
logger.info("Loading all-future datasets...")
|
||||||
train_dataset = AllFutureHealthDataset(
|
train_dataset = AllFutureHealthDataset(
|
||||||
@@ -489,8 +459,7 @@ def main() -> None:
|
|||||||
f"Patients/queries: train={len(train_subset)}, val={len(val_subset)}, test={len(test_subset)}"
|
f"Patients/queries: train={len(train_subset)}, val={len(val_subset)}, test={len(test_subset)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
scaler_stats = None
|
if train_dataset.n_cont_types > 0:
|
||||||
if args.continuous_value_scaling == "robust" and train_dataset.n_cont_types > 0:
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Fitting continuous RobustScaler on the complete training subset: "
|
"Fitting continuous RobustScaler on the complete training subset: "
|
||||||
f"patients={len(train_subset):,}, features={train_dataset.n_cont_types}"
|
f"patients={len(train_subset):,}, features={train_dataset.n_cont_types}"
|
||||||
@@ -499,6 +468,7 @@ def main() -> None:
|
|||||||
train_dataset,
|
train_dataset,
|
||||||
train_subset,
|
train_subset,
|
||||||
)
|
)
|
||||||
|
if train_dataset.n_cont_types > 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Continuous RobustScaler fitted: "
|
"Continuous RobustScaler fitted: "
|
||||||
f"observations={int(scaler_stats.observation_count.sum()):,}, "
|
f"observations={int(scaler_stats.observation_count.sum()):,}, "
|
||||||
@@ -550,7 +520,7 @@ def main() -> None:
|
|||||||
betas=tuple(args.betas),
|
betas=tuple(args.betas),
|
||||||
weight_decay=args.weight_decay,
|
weight_decay=args.weight_decay,
|
||||||
)
|
)
|
||||||
criterion = build_criterion(args, train_dataset)
|
criterion = build_criterion(args)
|
||||||
adaptive_lr = args.base_lr * math.sqrt(args.batch_size / 128)
|
adaptive_lr = args.base_lr * math.sqrt(args.batch_size / 128)
|
||||||
|
|
||||||
train_metadata = build_metadata(
|
train_metadata = build_metadata(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# The matrix contains:
|
# The matrix contains:
|
||||||
# 1. FFN + Delphi2M next-token reproduction
|
# 1. FFN + Delphi2M next-token reproduction
|
||||||
# 2. FFN/TrajMixer x absolute/relative x exponential/Weibull/mixed
|
# 2. FFN/TrajMixer x absolute/relative x exponential/Weibull
|
||||||
#
|
#
|
||||||
# Every task uses one GPU. Each selected GPU runs its assigned tasks
|
# Every task uses one GPU. Each selected GPU runs its assigned tasks
|
||||||
# sequentially, while different GPUs run in parallel.
|
# sequentially, while different GPUs run in parallel.
|
||||||
@@ -47,6 +47,7 @@ Options:
|
|||||||
Fixed experiment settings:
|
Fixed experiment settings:
|
||||||
batch_size 256
|
batch_size 256
|
||||||
extra_info_types extra_info_types_smoking_alcohol_bmi.txt
|
extra_info_types extra_info_types_smoking_alcohol_bmi.txt
|
||||||
|
continuous scaling required train-split RobustScale
|
||||||
model size Defaults from the individual training entrypoints
|
model size Defaults from the individual training entrypoints
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
@@ -180,7 +181,7 @@ add_job \
|
|||||||
|
|
||||||
for architecture in transformer_ffn_v1 traj_mixer_v5; do
|
for architecture in transformer_ffn_v1 traj_mixer_v5; do
|
||||||
for time_mode in absolute relative; do
|
for time_mode in absolute relative; do
|
||||||
for dist_mode in exponential weibull mixed; do
|
for dist_mode in exponential weibull; do
|
||||||
add_job \
|
add_job \
|
||||||
"${architecture}_all_future_${time_mode}_${dist_mode}" \
|
"${architecture}_all_future_${time_mode}_${dist_mode}" \
|
||||||
"train_all_future.py" \
|
"train_all_future.py" \
|
||||||
|
|||||||
@@ -236,7 +236,6 @@ run_job() {
|
|||||||
--time_mode relative
|
--time_mode relative
|
||||||
--dist_mode weibull
|
--dist_mode weibull
|
||||||
--disease_history_mode timed
|
--disease_history_mode timed
|
||||||
--continuous_value_scaling robust
|
|
||||||
--extra_info_types_file "$EXTRA_INFO_TYPES_FILE"
|
--extra_info_types_file "$EXTRA_INFO_TYPES_FILE"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ Options:
|
|||||||
Fixed experiment settings:
|
Fixed experiment settings:
|
||||||
batch_size 256
|
batch_size 256
|
||||||
extra_info_types extra_info_types_smoking_alcohol_bmi.txt
|
extra_info_types extra_info_types_smoking_alcohol_bmi.txt
|
||||||
|
continuous scaling required train-split RobustScale
|
||||||
model size Defaults from the training entrypoints
|
model size Defaults from the training entrypoints
|
||||||
tasks per seed 6
|
tasks per seed 6
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ from model_architectures import (
|
|||||||
SUPPORTED_MODEL_ARCHITECTURES,
|
SUPPORTED_MODEL_ARCHITECTURES,
|
||||||
)
|
)
|
||||||
from models import DeepHealth, DeepHealthOutput
|
from models import DeepHealth, DeepHealthOutput
|
||||||
from targets import CHECKUP_IDX, PAD_IDX
|
from targets import PAD_IDX, RESERVED_IDX
|
||||||
from train_util import (
|
from train_util import (
|
||||||
|
ContinuousRobustScalerStats,
|
||||||
configure_torch_for_training,
|
configure_torch_for_training,
|
||||||
create_unique_run_dir,
|
create_unique_run_dir,
|
||||||
|
fit_continuous_robust_scaler,
|
||||||
format_extra_info_types,
|
format_extra_info_types,
|
||||||
get_lr,
|
get_lr,
|
||||||
get_model_parameter_counts,
|
get_model_parameter_counts,
|
||||||
@@ -120,7 +122,17 @@ def parse_args() -> argparse.Namespace:
|
|||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
def build_model(args: argparse.Namespace, dataset: HealthDataset) -> DeepHealth:
|
def build_model(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
dataset: HealthDataset,
|
||||||
|
scaler_stats: ContinuousRobustScalerStats,
|
||||||
|
) -> DeepHealth:
|
||||||
|
if tuple(int(x) for x in dataset.cont_type_ids) != scaler_stats.cont_type_ids:
|
||||||
|
raise ValueError(
|
||||||
|
"RobustScale statistics are not aligned with dataset.cont_type_ids"
|
||||||
|
)
|
||||||
|
center = scaler_stats.center if dataset.n_cont_types > 0 else None
|
||||||
|
scale = scaler_stats.scale if dataset.n_cont_types > 0 else None
|
||||||
return DeepHealth(
|
return DeepHealth(
|
||||||
vocab_size=dataset.vocab_size,
|
vocab_size=dataset.vocab_size,
|
||||||
n_embd=args.n_embd,
|
n_embd=args.n_embd,
|
||||||
@@ -131,6 +143,8 @@ def build_model(args: argparse.Namespace, dataset: HealthDataset) -> DeepHealth:
|
|||||||
n_categories=dataset.n_categories,
|
n_categories=dataset.n_categories,
|
||||||
cont_type_ids=dataset.cont_type_ids,
|
cont_type_ids=dataset.cont_type_ids,
|
||||||
n_bins=args.n_bins,
|
n_bins=args.n_bins,
|
||||||
|
continuous_value_center=center,
|
||||||
|
continuous_value_scale=scale,
|
||||||
extra_pool_reduce=args.extra_pool_reduce,
|
extra_pool_reduce=args.extra_pool_reduce,
|
||||||
target_mode="next_token",
|
target_mode="next_token",
|
||||||
time_mode="absolute",
|
time_mode="absolute",
|
||||||
@@ -143,7 +157,7 @@ def build_model(args: argparse.Namespace, dataset: HealthDataset) -> DeepHealth:
|
|||||||
def build_next_step_loss(args: argparse.Namespace):
|
def build_next_step_loss(args: argparse.Namespace):
|
||||||
return build_loss(
|
return build_loss(
|
||||||
"delphi2m",
|
"delphi2m",
|
||||||
ignored_tokens={PAD_IDX, CHECKUP_IDX},
|
ignored_tokens={PAD_IDX, RESERVED_IDX},
|
||||||
t_min=args.t_min,
|
t_min=args.t_min,
|
||||||
max_exp_input=args.max_exp_input,
|
max_exp_input=args.max_exp_input,
|
||||||
ce_weight=args.ce_weight,
|
ce_weight=args.ce_weight,
|
||||||
@@ -244,7 +258,6 @@ def build_augmented_next_step_targets(
|
|||||||
|
|
||||||
|
|
||||||
def compute_next_step_loss(
|
def compute_next_step_loss(
|
||||||
args: argparse.Namespace,
|
|
||||||
model: DeepHealth,
|
model: DeepHealth,
|
||||||
criterion,
|
criterion,
|
||||||
batch: Dict[str, torch.Tensor],
|
batch: Dict[str, torch.Tensor],
|
||||||
@@ -309,7 +322,7 @@ def run_epoch(
|
|||||||
for batch_idx, batch in enumerate(progress):
|
for batch_idx, batch in enumerate(progress):
|
||||||
try:
|
try:
|
||||||
loss, parts = compute_next_step_loss(
|
loss, parts = compute_next_step_loss(
|
||||||
args, model, criterion, batch, device
|
model, criterion, batch, device
|
||||||
)
|
)
|
||||||
if is_train:
|
if is_train:
|
||||||
if optimizer is None:
|
if optimizer is None:
|
||||||
@@ -352,6 +365,7 @@ def build_metadata(
|
|||||||
train_subset,
|
train_subset,
|
||||||
val_subset,
|
val_subset,
|
||||||
test_subset,
|
test_subset,
|
||||||
|
scaler_stats: ContinuousRobustScalerStats,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"run_name": run_name,
|
"run_name": run_name,
|
||||||
@@ -361,6 +375,8 @@ def build_metadata(
|
|||||||
"model_architecture": args.model_architecture,
|
"model_architecture": args.model_architecture,
|
||||||
"model_target_mode": "next_token",
|
"model_target_mode": "next_token",
|
||||||
"target_mode": "delphi2m",
|
"target_mode": "delphi2m",
|
||||||
|
"event_stream_version": "disease_death_only_v1",
|
||||||
|
"uses_assessment_event_token": False,
|
||||||
"time_mode": "absolute",
|
"time_mode": "absolute",
|
||||||
"dist_mode": "exponential",
|
"dist_mode": "exponential",
|
||||||
"extra_info_types_file": (
|
"extra_info_types_file": (
|
||||||
@@ -369,6 +385,8 @@ def build_metadata(
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
||||||
|
"continuous_value_scaling": "robust",
|
||||||
|
"continuous_value_scaler": scaler_stats.as_metadata(),
|
||||||
"dataset_metadata": {
|
"dataset_metadata": {
|
||||||
"vocab_size": int(dataset.vocab_size),
|
"vocab_size": int(dataset.vocab_size),
|
||||||
"n_types": int(dataset.n_types),
|
"n_types": int(dataset.n_types),
|
||||||
@@ -376,6 +394,8 @@ def build_metadata(
|
|||||||
"n_categories": int(dataset.n_categories),
|
"n_categories": int(dataset.n_categories),
|
||||||
"cont_type_ids": [int(x) for x in dataset.cont_type_ids],
|
"cont_type_ids": [int(x) for x in dataset.cont_type_ids],
|
||||||
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
"extra_info_types": [int(x) for x in dataset.extra_info_types],
|
||||||
|
"event_stream_version": "disease_death_only_v1",
|
||||||
|
"uses_assessment_event_token": False,
|
||||||
},
|
},
|
||||||
"split_sizes": {
|
"split_sizes": {
|
||||||
"train": int(len(train_subset)),
|
"train": int(len(train_subset)),
|
||||||
@@ -406,6 +426,7 @@ def main() -> None:
|
|||||||
logger.info(f"Device: {device}")
|
logger.info(f"Device: {device}")
|
||||||
logger.info(f"Model architecture: {args.model_architecture}")
|
logger.info(f"Model architecture: {args.model_architecture}")
|
||||||
logger.info(f"extra_info_types: {format_extra_info_types(args.extra_info_types)}")
|
logger.info(f"extra_info_types: {format_extra_info_types(args.extra_info_types)}")
|
||||||
|
logger.info("Continuous value scaling: RobustScale (required)")
|
||||||
logger.info("time_mode=absolute, readout=token, target_mode=delphi2m")
|
logger.info("time_mode=absolute, readout=token, target_mode=delphi2m")
|
||||||
|
|
||||||
dataset = HealthDataset(
|
dataset = HealthDataset(
|
||||||
@@ -441,6 +462,20 @@ def main() -> None:
|
|||||||
f"Samples: train={len(train_subset)}, val={len(val_subset)}, test={len(test_subset)}"
|
f"Samples: train={len(train_subset)}, val={len(val_subset)}, test={len(test_subset)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if dataset.n_cont_types > 0:
|
||||||
|
logger.info(
|
||||||
|
"Fitting continuous RobustScaler on the complete training subset: "
|
||||||
|
f"patients={len(train_subset):,}, features={dataset.n_cont_types}"
|
||||||
|
)
|
||||||
|
scaler_stats = fit_continuous_robust_scaler(dataset, train_subset)
|
||||||
|
if dataset.n_cont_types > 0:
|
||||||
|
logger.info(
|
||||||
|
"Continuous RobustScaler fitted: "
|
||||||
|
f"observations={int(scaler_stats.observation_count.sum()):,}, "
|
||||||
|
f"min_per_feature={int(scaler_stats.observation_count.min()):,}, "
|
||||||
|
f"max_per_feature={int(scaler_stats.observation_count.max()):,}"
|
||||||
|
)
|
||||||
|
|
||||||
train_loader = DataLoader(
|
train_loader = DataLoader(
|
||||||
train_subset,
|
train_subset,
|
||||||
batch_size=args.batch_size,
|
batch_size=args.batch_size,
|
||||||
@@ -472,7 +507,7 @@ def main() -> None:
|
|||||||
prefetch_factor=2 if args.num_workers > 0 else None,
|
prefetch_factor=2 if args.num_workers > 0 else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
model = build_model(args, dataset).to(device)
|
model = build_model(args, dataset, scaler_stats).to(device)
|
||||||
parameter_counts = get_model_parameter_counts(model)
|
parameter_counts = get_model_parameter_counts(model)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Model parameters: "
|
"Model parameters: "
|
||||||
@@ -489,7 +524,13 @@ def main() -> None:
|
|||||||
adaptive_lr = args.base_lr * math.sqrt(args.batch_size / 128)
|
adaptive_lr = args.base_lr * math.sqrt(args.batch_size / 128)
|
||||||
|
|
||||||
train_metadata = build_metadata(
|
train_metadata = build_metadata(
|
||||||
args, dataset, run_name, train_subset, val_subset, test_subset
|
args,
|
||||||
|
dataset,
|
||||||
|
run_name,
|
||||||
|
train_subset,
|
||||||
|
val_subset,
|
||||||
|
test_subset,
|
||||||
|
scaler_stats,
|
||||||
)
|
)
|
||||||
train_metadata.update(parameter_counts)
|
train_metadata.update(parameter_counts)
|
||||||
save_config(
|
save_config(
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ContinuousRobustScalerStats:
|
|||||||
|
|
||||||
|
|
||||||
def fit_continuous_robust_scaler(
|
def fit_continuous_robust_scaler(
|
||||||
dataset: AllFutureHealthDataset,
|
dataset: HealthDataset | AllFutureHealthDataset,
|
||||||
subset: Subset,
|
subset: Subset,
|
||||||
*,
|
*,
|
||||||
quantile_range: tuple[float, float] = (25.0, 75.0),
|
quantile_range: tuple[float, float] = (25.0, 75.0),
|
||||||
@@ -90,13 +90,22 @@ def fit_continuous_robust_scaler(
|
|||||||
)
|
)
|
||||||
type_to_column[type_id] = column
|
type_to_column[type_id] = column
|
||||||
|
|
||||||
|
if hasattr(dataset, "patients"):
|
||||||
|
records = dataset.patients
|
||||||
|
elif hasattr(dataset, "samples"):
|
||||||
|
records = dataset.samples
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
"dataset must expose patient records through .patients or .samples"
|
||||||
|
)
|
||||||
|
|
||||||
values = np.full(
|
values = np.full(
|
||||||
(int(subset_indices.size), n_cont_types),
|
(int(subset_indices.size), n_cont_types),
|
||||||
np.nan,
|
np.nan,
|
||||||
dtype=np.float32,
|
dtype=np.float32,
|
||||||
)
|
)
|
||||||
for row, patient_index in enumerate(subset_indices.tolist()):
|
for row, patient_index in enumerate(subset_indices.tolist()):
|
||||||
patient = dataset.patients[int(patient_index)]
|
patient = records[int(patient_index)]
|
||||||
other_type = np.asarray(patient["other_type"], dtype=np.int64)
|
other_type = np.asarray(patient["other_type"], dtype=np.int64)
|
||||||
other_value = np.asarray(patient["other_value"], dtype=np.float32)
|
other_value = np.asarray(patient["other_value"], dtype=np.float32)
|
||||||
other_kind = np.asarray(patient["other_value_kind"], dtype=np.int64)
|
other_kind = np.asarray(patient["other_value_kind"], dtype=np.int64)
|
||||||
|
|||||||
Reference in New Issue
Block a user