Align disease attribution batching with survival evaluator
This commit is contained in:
@@ -196,48 +196,6 @@ def write_summary_csv(
|
|||||||
return len(rows)
|
return len(rows)
|
||||||
|
|
||||||
|
|
||||||
def concat_padded_tensor_batches(chunks: list[Dict[str, torch.Tensor]]) -> Dict[str, torch.Tensor]:
|
|
||||||
if not chunks:
|
|
||||||
raise ValueError("Cannot concatenate an empty chunk list")
|
|
||||||
|
|
||||||
fill_values = {
|
|
||||||
"event_seq": PAD_IDX,
|
|
||||||
"time_seq": 0.0,
|
|
||||||
"readout_mask": False,
|
|
||||||
"padding_mask": False,
|
|
||||||
"other_type": 0,
|
|
||||||
"other_value": 0.0,
|
|
||||||
"other_value_kind": 0,
|
|
||||||
"other_time": 0.0,
|
|
||||||
}
|
|
||||||
out: Dict[str, torch.Tensor] = {}
|
|
||||||
for key in chunks[0]:
|
|
||||||
tensors = [chunk[key] for chunk in chunks]
|
|
||||||
shapes = [tuple(t.shape) for t in tensors]
|
|
||||||
if len(set(shapes)) == 1:
|
|
||||||
out[key] = torch.cat(tensors, dim=0)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if any(t.ndim == 0 for t in tensors):
|
|
||||||
raise ValueError(f"Cannot concatenate scalar tensor key={key!r} with mismatched shapes")
|
|
||||||
max_shape = list(shapes[0])
|
|
||||||
for shape in shapes[1:]:
|
|
||||||
if len(shape) != len(max_shape):
|
|
||||||
raise ValueError(f"Cannot concatenate key={key!r} with shapes {shapes}")
|
|
||||||
max_shape = [max(a, b) for a, b in zip(max_shape, shape)]
|
|
||||||
|
|
||||||
padded: list[torch.Tensor] = []
|
|
||||||
fill = fill_values.get(key, 0)
|
|
||||||
for tensor in tensors:
|
|
||||||
target_shape = [int(tensor.shape[0]), *max_shape[1:]]
|
|
||||||
padded_tensor = tensor.new_full(target_shape, fill)
|
|
||||||
slices = tuple(slice(0, int(size)) for size in tensor.shape)
|
|
||||||
padded_tensor[slices] = tensor
|
|
||||||
padded.append(padded_tensor)
|
|
||||||
out[key] = torch.cat(padded, dim=0)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def build_disease_ablated_slice(
|
def build_disease_ablated_slice(
|
||||||
batch: Dict[str, torch.Tensor],
|
batch: Dict[str, torch.Tensor],
|
||||||
row_indices: torch.Tensor,
|
row_indices: torch.Tensor,
|
||||||
@@ -469,6 +427,12 @@ def parse_args() -> argparse.Namespace:
|
|||||||
default=1e-7,
|
default=1e-7,
|
||||||
help="Small lower bound for ratio denominators.",
|
help="Small lower bound for ratio denominators.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--shard_rows",
|
||||||
|
type=int,
|
||||||
|
default=200_000,
|
||||||
|
help="Approximate number of detailed rows to buffer before writing one .npz shard.",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -561,6 +525,8 @@ def main() -> None:
|
|||||||
raise ValueError("attribution_batch_size must be positive")
|
raise ValueError("attribution_batch_size must be positive")
|
||||||
if float(args.ratio_eps) <= 0:
|
if float(args.ratio_eps) <= 0:
|
||||||
raise ValueError("--ratio_eps must be positive")
|
raise ValueError("--ratio_eps must be positive")
|
||||||
|
if int(args.shard_rows) <= 0:
|
||||||
|
raise ValueError("--shard_rows must be positive")
|
||||||
|
|
||||||
num_workers = int(cfg_get(args, cfg, "num_workers", 4))
|
num_workers = int(cfg_get(args, cfg, "num_workers", 4))
|
||||||
loader = DataLoader(
|
loader = DataLoader(
|
||||||
@@ -611,51 +577,27 @@ def main() -> None:
|
|||||||
shards: list[dict[str, Any]] = []
|
shards: list[dict[str, Any]] = []
|
||||||
summary_accumulator: dict[tuple[Any, ...], dict[str, float]] = {}
|
summary_accumulator: dict[tuple[Any, ...], dict[str, float]] = {}
|
||||||
row_base_cache: dict[int, dict[str, Any]] = {}
|
row_base_cache: dict[int, dict[str, Any]] = {}
|
||||||
pending_batch_chunks: list[Dict[str, torch.Tensor]] = []
|
shard_frames: list[pd.DataFrame] = []
|
||||||
pending_meta_chunks: list[list[dict[str, Any]]] = []
|
shard_buffer_rows = 0
|
||||||
pending_orig_risk_chunks: list[torch.Tensor] = []
|
|
||||||
pending_orig_hazard_chunks: list[torch.Tensor] = []
|
|
||||||
pending_n = 0
|
|
||||||
|
|
||||||
def flush_pending() -> None:
|
def flush_shard_buffer() -> None:
|
||||||
nonlocal written_rows, shard_index, pending_batch_chunks, pending_meta_chunks
|
nonlocal written_rows, shard_index, shard_frames, shard_buffer_rows
|
||||||
nonlocal pending_orig_risk_chunks, pending_orig_hazard_chunks, pending_n
|
if shard_buffer_rows == 0:
|
||||||
if pending_n == 0:
|
|
||||||
return
|
return
|
||||||
|
table = pd.concat(shard_frames, ignore_index=True).reindex(columns=OUTPUT_COLUMNS)
|
||||||
|
shard_name = f"part-{shard_index:06d}.npz"
|
||||||
|
shard_path = output_dir / shard_name
|
||||||
|
shard_rows = write_compressed_npz_table(shard_path, table)
|
||||||
|
shards.append({"file": shard_name, "rows": int(shard_rows)})
|
||||||
|
shard_index += 1
|
||||||
|
written_rows += int(shard_rows)
|
||||||
|
shard_frames = []
|
||||||
|
shard_buffer_rows = 0
|
||||||
|
|
||||||
ablated_batch = concat_padded_tensor_batches(pending_batch_chunks)
|
def write_result_rows(meta_rows: list[dict[str, Any]], value_block: np.ndarray) -> None:
|
||||||
meta_rows = [row for chunk in pending_meta_chunks for row in chunk]
|
nonlocal shard_buffer_rows
|
||||||
orig_risk = torch.cat(pending_orig_risk_chunks, dim=0).to(device)
|
if not meta_rows:
|
||||||
orig_hazard = torch.cat(pending_orig_hazard_chunks, dim=0).to(device)
|
return
|
||||||
with torch.no_grad():
|
|
||||||
ablated_risk = death_risk_for_batch(
|
|
||||||
model=model,
|
|
||||||
batch=ablated_batch,
|
|
||||||
device=device,
|
|
||||||
model_target_mode=model_target_mode,
|
|
||||||
readout_name=readout_name,
|
|
||||||
readout_reduce=readout_reduce,
|
|
||||||
dist_mode=dist_mode,
|
|
||||||
tau=tau,
|
|
||||||
)
|
|
||||||
ablated_hazard = mortality_hazard_from_risk(ablated_risk)
|
|
||||||
attr_prob = orig_risk - ablated_risk
|
|
||||||
attr_hazard = orig_hazard - ablated_hazard
|
|
||||||
ratio_prob = safe_ratio(orig_risk, ablated_risk, eps=float(args.ratio_eps))
|
|
||||||
ratio_hazard = safe_ratio(orig_hazard, ablated_hazard, eps=float(args.ratio_eps))
|
|
||||||
value_block = torch.stack(
|
|
||||||
[
|
|
||||||
orig_risk,
|
|
||||||
orig_hazard,
|
|
||||||
ablated_risk,
|
|
||||||
ablated_hazard,
|
|
||||||
attr_prob,
|
|
||||||
attr_hazard,
|
|
||||||
ratio_prob,
|
|
||||||
ratio_hazard,
|
|
||||||
],
|
|
||||||
dim=1,
|
|
||||||
).detach().cpu().numpy()
|
|
||||||
|
|
||||||
for i, row in enumerate(meta_rows):
|
for i, row in enumerate(meta_rows):
|
||||||
row["death_risk"] = float(value_block[i, 0])
|
row["death_risk"] = float(value_block[i, 0])
|
||||||
@@ -669,17 +611,10 @@ def main() -> None:
|
|||||||
|
|
||||||
table = pd.DataFrame(meta_rows).reindex(columns=OUTPUT_COLUMNS)
|
table = pd.DataFrame(meta_rows).reindex(columns=OUTPUT_COLUMNS)
|
||||||
update_summary_accumulator(summary_accumulator, table)
|
update_summary_accumulator(summary_accumulator, table)
|
||||||
shard_name = f"part-{shard_index:06d}.npz"
|
shard_frames.append(table)
|
||||||
shard_path = output_dir / shard_name
|
shard_buffer_rows += len(table)
|
||||||
shard_rows = write_compressed_npz_table(shard_path, table)
|
if shard_buffer_rows >= int(args.shard_rows):
|
||||||
shards.append({"file": shard_name, "rows": int(shard_rows)})
|
flush_shard_buffer()
|
||||||
shard_index += 1
|
|
||||||
written_rows += len(meta_rows)
|
|
||||||
pending_batch_chunks = []
|
|
||||||
pending_meta_chunks = []
|
|
||||||
pending_orig_risk_chunks = []
|
|
||||||
pending_orig_hazard_chunks = []
|
|
||||||
pending_n = 0
|
|
||||||
|
|
||||||
def get_row_base(row_idx: int) -> dict[str, Any]:
|
def get_row_base(row_idx: int) -> dict[str, Any]:
|
||||||
cached = row_base_cache.get(row_idx)
|
cached = row_base_cache.get(row_idx)
|
||||||
@@ -756,8 +691,7 @@ def main() -> None:
|
|||||||
|
|
||||||
pair_offset = 0
|
pair_offset = 0
|
||||||
while pair_offset < int(pair_indices.shape[0]):
|
while pair_offset < int(pair_indices.shape[0]):
|
||||||
capacity = int(attribution_batch_size) - pending_n
|
pair_stop = min(int(pair_indices.shape[0]), pair_offset + int(attribution_batch_size))
|
||||||
pair_stop = min(int(pair_indices.shape[0]), pair_offset + capacity)
|
|
||||||
pair_chunk = pair_indices[pair_offset:pair_stop]
|
pair_chunk = pair_indices[pair_offset:pair_stop]
|
||||||
local_rows = pair_chunk[:, 0].long()
|
local_rows = pair_chunk[:, 0].long()
|
||||||
disease_token_ids = scanned_disease_tensor[pair_chunk[:, 1]].long()
|
disease_token_ids = scanned_disease_tensor[pair_chunk[:, 1]].long()
|
||||||
@@ -766,6 +700,37 @@ def main() -> None:
|
|||||||
row_indices=local_rows,
|
row_indices=local_rows,
|
||||||
token_ids=disease_token_ids,
|
token_ids=disease_token_ids,
|
||||||
)
|
)
|
||||||
|
with torch.no_grad():
|
||||||
|
ablated_risk = death_risk_for_batch(
|
||||||
|
model=model,
|
||||||
|
batch=ablated_chunk,
|
||||||
|
device=device,
|
||||||
|
model_target_mode=model_target_mode,
|
||||||
|
readout_name=readout_name,
|
||||||
|
readout_reduce=readout_reduce,
|
||||||
|
dist_mode=dist_mode,
|
||||||
|
tau=tau,
|
||||||
|
)
|
||||||
|
orig_risk = death_risk_tensor[local_rows]
|
||||||
|
orig_hazard = death_hazard_tensor[local_rows]
|
||||||
|
ablated_hazard = mortality_hazard_from_risk(ablated_risk)
|
||||||
|
attr_prob = orig_risk - ablated_risk
|
||||||
|
attr_hazard = orig_hazard - ablated_hazard
|
||||||
|
ratio_prob = safe_ratio(orig_risk, ablated_risk, eps=float(args.ratio_eps))
|
||||||
|
ratio_hazard = safe_ratio(orig_hazard, ablated_hazard, eps=float(args.ratio_eps))
|
||||||
|
value_block = torch.stack(
|
||||||
|
[
|
||||||
|
orig_risk,
|
||||||
|
orig_hazard,
|
||||||
|
ablated_risk,
|
||||||
|
ablated_hazard,
|
||||||
|
attr_prob,
|
||||||
|
attr_hazard,
|
||||||
|
ratio_prob,
|
||||||
|
ratio_hazard,
|
||||||
|
],
|
||||||
|
dim=1,
|
||||||
|
).detach().cpu().numpy()
|
||||||
|
|
||||||
meta_chunk: list[dict[str, Any]] = []
|
meta_chunk: list[dict[str, Any]] = []
|
||||||
row_ids = batch_dev["row_idx"][local_rows].detach().cpu().numpy().astype(np.int64)
|
row_ids = batch_dev["row_idx"][local_rows].detach().cpu().numpy().astype(np.int64)
|
||||||
@@ -810,18 +775,10 @@ def main() -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if meta_chunk:
|
write_result_rows(meta_chunk, value_block)
|
||||||
pending_batch_chunks.append(ablated_chunk)
|
|
||||||
pending_meta_chunks.append(meta_chunk)
|
|
||||||
pending_orig_risk_chunks.append(death_risk_tensor[local_rows].detach())
|
|
||||||
pending_orig_hazard_chunks.append(death_hazard_tensor[local_rows].detach())
|
|
||||||
pending_n += len(meta_chunk)
|
|
||||||
pair_offset = pair_stop
|
pair_offset = pair_stop
|
||||||
|
|
||||||
if pending_n >= int(attribution_batch_size):
|
flush_shard_buffer()
|
||||||
flush_pending()
|
|
||||||
|
|
||||||
flush_pending()
|
|
||||||
if not shards:
|
if not shards:
|
||||||
empty_path = output_dir / "part-000000.npz"
|
empty_path = output_dir / "part-000000.npz"
|
||||||
write_compressed_npz_table(empty_path, pd.DataFrame(columns=OUTPUT_COLUMNS))
|
write_compressed_npz_table(empty_path, pd.DataFrame(columns=OUTPUT_COLUMNS))
|
||||||
|
|||||||
Reference in New Issue
Block a user