aitrainingdatabroker commited on
Commit
b18fe8f
·
verified ·
1 Parent(s): 2b825d7

Add source/train.py

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