from __future__ import annotations import argparse import json import math import os from pathlib import Path import random import sys sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import numpy as np import torch from torch.optim import AdamW from torch.utils.data import DataLoader from transformers import AutoModelForSequenceClassification, AutoTokenizer, get_linear_schedule_with_warmup from train_candidate_verifier import VerifierDataset, build_examples, score def load_jsonl(path: Path) -> list[dict]: return [json.loads(line) for line in path.open(encoding="utf-8") if line.strip()] def main() -> None: parser = argparse.ArgumentParser(description="Train a full-data phenotype candidate verifier and score deployment candidates.") parser.add_argument("--train-data", type=Path, required=True) parser.add_argument("--train-candidates", type=Path, required=True) parser.add_argument("--predict-data", type=Path, required=True) parser.add_argument("--predict-candidates", type=Path, required=True) parser.add_argument("--model", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--epochs", type=int, default=3) parser.add_argument("--batch-size", type=int, default=16) parser.add_argument("--learning-rate", type=float, default=2e-5) parser.add_argument("--max-length", type=int, default=256) parser.add_argument("--seed", type=int, default=2026) parser.add_argument("--device", default="cuda") args = parser.parse_args() os.environ.setdefault("HF_HUB_OFFLINE", "1") os.environ.setdefault("TRANSFORMERS_OFFLINE", "1") random.seed(args.seed) np.random.seed(args.seed) torch.manual_seed(args.seed) train_documents = load_jsonl(args.train_data) predict_documents = load_jsonl(args.predict_data) train_candidates = json.loads(args.train_candidates.read_text(encoding="utf-8")) predict_candidates = json.loads(args.predict_candidates.read_text(encoding="utf-8")) train_examples = build_examples(train_documents, train_candidates) predict_examples = build_examples(predict_documents, predict_candidates) tokenizer = AutoTokenizer.from_pretrained(args.model, local_files_only=True, use_fast=True) train_loader = DataLoader( VerifierDataset(train_examples, tokenizer, args.max_length), batch_size=args.batch_size, shuffle=True, generator=torch.Generator().manual_seed(args.seed), num_workers=2, pin_memory=True, ) predict_loader = DataLoader( VerifierDataset(predict_examples, tokenizer, args.max_length), batch_size=args.batch_size, shuffle=False, num_workers=2, pin_memory=True, ) model = AutoModelForSequenceClassification.from_pretrained( args.model, local_files_only=True, num_labels=2, ignore_mismatched_sizes=True ).to(args.device) positives = sum(example.label for example in train_examples) class_weights = torch.tensor( [1.0, (len(train_examples) - positives) / max(1, positives)], device=args.device ) optimizer = AdamW(model.parameters(), lr=args.learning_rate, weight_decay=0.01) updates = len(train_loader) * args.epochs scheduler = get_linear_schedule_with_warmup(optimizer, max(1, updates // 10), updates) history: list[dict[str, float | int]] = [] for epoch in range(1, args.epochs + 1): model.train() total_loss = 0.0 for batch in train_loader: batch.pop("index") labels = batch.pop("labels").to(args.device) inputs = {key: value.to(args.device, non_blocking=True) for key, value in batch.items()} with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=args.device.startswith("cuda")): logits = model(**inputs).logits loss = torch.nn.functional.cross_entropy(logits.float(), labels, weight=class_weights) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad(set_to_none=True) total_loss += float(loss.item()) report = {"epoch": epoch, "train_loss": total_loss / max(1, len(train_loader))} history.append(report) print(json.dumps(report), flush=True) scores = score(model, predict_loader, args.device, len(predict_examples)) predictions = [ { "pmc_id": example.pmc_id, "start": example.start, "end": example.end, "text": example.text, "score": value, **example.metadata, } for example, value in zip(predict_examples, scores, strict=True) ] result = { "configuration": vars(args) | { "train_data": str(args.train_data), "train_candidates": str(args.train_candidates), "predict_data": str(args.predict_data), "predict_candidates": str(args.predict_candidates), "model": str(args.model), "output": str(args.output), }, "train_examples": len(train_examples), "train_positives": positives, "predict_examples": len(predict_examples), "history": history, "predictions": predictions, } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8") print(json.dumps({"train_examples": len(train_examples), "predict_examples": len(predict_examples), "output": str(args.output)})) if __name__ == "__main__": main()