File size: 5,541 Bytes
e7543dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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()