aitrainingdatabroker commited on
Commit
b9bb1a9
·
verified ·
1 Parent(s): 0f6210e

Add source/train.py

Browse files
Files changed (1) hide show
  1. source/train.py +159 -0
source/train.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train one of the five models on the graphics card and write its test scores (2026-10-29).
2
+
3
+ python models/train.py <name> e.g. business-pii-detector
4
+ Reads models/<name>/data/{train,test}.jsonl (models/build.py), fine-tunes distilroberta-base, saves the model to
5
+ models/<name>/out/ (not committed; uploaded to Hugging Face) and the scores to models/<name>/scores.json.
6
+ Span models (rows with "entities") are scored by exact entity match; classifiers by accuracy and macro F1.
7
+ """
8
+
9
+ import json
10
+ import sys
11
+ from collections import Counter
12
+ from pathlib import Path
13
+
14
+ import torch
15
+ from torch.utils.data import DataLoader
16
+ from transformers import (AutoModelForSequenceClassification, AutoModelForTokenClassification, AutoTokenizer,
17
+ get_linear_schedule_with_warmup)
18
+
19
+ BASE = "distilroberta-base"
20
+ ROOT = Path(__file__).resolve().parent
21
+ name = sys.argv[1]
22
+ folder = ROOT / name
23
+ read = lambda s: [json.loads(l) for l in open(folder / "data" / f"{s}.jsonl", encoding="utf-8")]
24
+ train_rows, test_rows = read("train"), read("test")
25
+ spans = "entities" in train_rows[0]
26
+ dev = "cuda" if torch.cuda.is_available() else "cpu"
27
+ torch.manual_seed(1029)
28
+ tok = AutoTokenizer.from_pretrained(BASE, add_prefix_space=True)
29
+
30
+ if spans:
31
+ types = sorted({e["label"] for r in train_rows + test_rows for e in r["entities"]})
32
+ labels = ["O"] + [f"{p}-{t}" for t in types for p in ("B", "I")]
33
+ else:
34
+ labels = sorted({r["label"] for r in train_rows + test_rows})
35
+ lid = {l: i for i, l in enumerate(labels)}
36
+ MAX = 256 if not spans else 160
37
+
38
+
39
+ def encode_spans(r):
40
+ enc = tok(r["text"], truncation=True, max_length=MAX, return_offsets_mapping=True)
41
+ tags = []
42
+ for s, e in enc["offset_mapping"]:
43
+ if s == e:
44
+ tags.append(-100)
45
+ continue
46
+ tag = "O"
47
+ for ent in r["entities"]:
48
+ if s >= ent["start"] and e <= ent["end"] + 1 and s < ent["end"]:
49
+ tag = ("B-" if s <= ent["start"] else "I-") + ent["label"]
50
+ break
51
+ tags.append(lid[tag])
52
+ enc.pop("offset_mapping")
53
+ enc["labels"] = tags
54
+ return enc
55
+
56
+
57
+ def encode_cls(r):
58
+ enc = tok(r["text"], truncation=True, max_length=MAX)
59
+ enc["labels"] = lid[r["label"]]
60
+ return enc
61
+
62
+
63
+ def collate(batch):
64
+ n = max(len(b["input_ids"]) for b in batch)
65
+ out = {"input_ids": [], "attention_mask": [], "labels": []}
66
+ for b in batch:
67
+ pad = n - len(b["input_ids"])
68
+ out["input_ids"].append(b["input_ids"] + [tok.pad_token_id] * pad)
69
+ out["attention_mask"].append(b["attention_mask"] + [0] * pad)
70
+ out["labels"].append(b["labels"] + [-100] * pad if spans else b["labels"])
71
+ return {k: torch.tensor(v) for k, v in out.items()}
72
+
73
+
74
+ encode = encode_spans if spans else encode_cls
75
+ train = [encode(r) for r in train_rows]
76
+ Model = AutoModelForTokenClassification if spans else AutoModelForSequenceClassification
77
+ model = Model.from_pretrained(BASE, num_labels=len(labels), id2label=dict(enumerate(labels)), label2id=lid).to(dev)
78
+ EPOCHS = 8 if spans else 6
79
+ loader = DataLoader(train, batch_size=16, shuffle=True, collate_fn=collate)
80
+ opt = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.01)
81
+ sched = get_linear_schedule_with_warmup(opt, len(loader) * EPOCHS // 10, len(loader) * EPOCHS)
82
+ model.train()
83
+ for ep in range(EPOCHS):
84
+ total = 0.0
85
+ for batch in loader:
86
+ with torch.autocast(dev, dtype=torch.float16, enabled=dev == "cuda"):
87
+ loss = model(**{k: v.to(dev) for k, v in batch.items()}).loss
88
+ loss.backward()
89
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
90
+ opt.step(), sched.step(), opt.zero_grad()
91
+ total += loss.item()
92
+ print(f"epoch {ep + 1}: loss {total / len(loader):.4f}")
93
+ model.eval()
94
+
95
+
96
+ def predict_spans(text):
97
+ enc = tok(text, truncation=True, max_length=MAX, return_offsets_mapping=True, return_tensors="pt")
98
+ offs = enc.pop("offset_mapping")[0].tolist()
99
+ with torch.no_grad():
100
+ ids = model(**{k: v.to(dev) for k, v in enc.items()}).logits[0].argmax(-1).tolist()
101
+ found, cur = [], None
102
+ for (s, e), i in zip(offs, ids):
103
+ if s == e:
104
+ continue
105
+ tag = labels[i]
106
+ if tag == "O":
107
+ cur = None
108
+ continue
109
+ kind = tag[2:]
110
+ if tag.startswith("I-") and cur and cur["label"] == kind:
111
+ cur["end"] = e
112
+ else:
113
+ cur = {"label": kind, "start": s, "end": e}
114
+ found.append(cur)
115
+ for f in found: # offsets from a prefix-space tokenizer can start on the space
116
+ while f["start"] < f["end"] and text[f["start"]] == " ":
117
+ f["start"] += 1
118
+ return found
119
+
120
+
121
+ if spans:
122
+ tp, fp, fn = Counter(), Counter(), Counter()
123
+ for r in test_rows:
124
+ gold = {(e["label"], e["start"], e["end"]) for e in r["entities"]}
125
+ pred = {(e["label"], e["start"], e["end"]) for e in predict_spans(r["text"])}
126
+ for g in gold:
127
+ (tp if g in pred else fn)[g[0]] += 1
128
+ for p in pred - gold:
129
+ fp[p[0]] += 1
130
+ f1 = lambda t, p, n: (2 * t / (2 * t + p + n)) if t else 0.0
131
+ per = {t: {"precision": round(tp[t] / (tp[t] + fp[t]), 3) if tp[t] + fp[t] else 0.0,
132
+ "recall": round(tp[t] / (tp[t] + fn[t]), 3) if tp[t] + fn[t] else 0.0,
133
+ "f1": round(f1(tp[t], fp[t], fn[t]), 3), "support": tp[t] + fn[t]} for t in types}
134
+ T, P, N = sum(tp.values()), sum(fp.values()), sum(fn.values())
135
+ scores = {"metric": "exact entity match", "micro_precision": round(T / (T + P), 3), "micro_recall": round(T / (T + N), 3),
136
+ "micro_f1": round(f1(T, P, N), 3), "per_type": per}
137
+ else:
138
+ pred, gold = [], []
139
+ for r in test_rows:
140
+ enc = tok(r["text"], truncation=True, max_length=MAX, return_tensors="pt")
141
+ with torch.no_grad():
142
+ pred.append(labels[model(**{k: v.to(dev) for k, v in enc.items()}).logits[0].argmax().item()])
143
+ gold.append(r["label"])
144
+ acc = sum(p == g for p, g in zip(pred, gold)) / len(gold)
145
+ per = {}
146
+ for l in labels:
147
+ t = sum(p == g == l for p, g in zip(pred, gold))
148
+ p_ = sum(p == l for p in pred)
149
+ g_ = sum(g == l for g in gold)
150
+ per[l] = {"f1": round(2 * t / (p_ + g_), 3) if p_ + g_ else 0.0, "support": g_}
151
+ scores = {"metric": "accuracy and macro F1", "accuracy": round(acc, 3),
152
+ "macro_f1": round(sum(v["f1"] for v in per.values() if v["support"]) / sum(1 for v in per.values() if v["support"]), 3),
153
+ "per_label": per}
154
+
155
+ scores.update({"base_model": BASE, "train_rows": len(train_rows), "test_rows": len(test_rows), "epochs": EPOCHS, "device": dev})
156
+ (folder / "scores.json").write_text(json.dumps(scores, indent=2) + "\n", encoding="utf-8")
157
+ print(json.dumps({k: v for k, v in scores.items() if k not in ("per_type", "per_label")}))
158
+ model.save_pretrained(folder / "out", safe_serialization=True)
159
+ tok.save_pretrained(folder / "out")