jefftherover commited on
Commit
34993ac
Β·
verified Β·
1 Parent(s): 16309b8

Upload train_ner_pii.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. train_ner_pii.py +266 -0
train_ner_pii.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # dependencies = [
3
+ # "transformers>=4.48.0",
4
+ # "datasets>=2.20.0",
5
+ # "evaluate>=0.4.0",
6
+ # "seqeval>=1.2.2",
7
+ # "trackio",
8
+ # "numpy<2.0",
9
+ # "accelerate>=0.34.0",
10
+ # ]
11
+ # ///
12
+ """
13
+ ModernBERT PII NER β€” remapped to 11 company policy labels.
14
+
15
+ Trains from answerdotai/ModernBERT-base with a new 23-label classification
16
+ head. Fixes the entity-scan alignment bug: instead of reading char_labels
17
+ only at real_s (the first non-space position), we now scan the entire token
18
+ span [real_s, tok_e) for the first entity character. This ensures entities
19
+ that start after punctuation (e.g. "(Home" or ":John") are correctly labeled
20
+ rather than silently dropped.
21
+
22
+ Run with: uv run train_ner_pii.py
23
+ """
24
+
25
+ import json
26
+ import numpy as np
27
+ import trackio
28
+ from datasets import load_dataset
29
+ from transformers import (
30
+ AutoTokenizer,
31
+ AutoModelForTokenClassification,
32
+ TrainingArguments,
33
+ Trainer,
34
+ DataCollatorForTokenClassification,
35
+ EarlyStoppingCallback,
36
+ )
37
+ import evaluate
38
+
39
+ # ── Label mapping: ai4privacy 56 types β†’ 11 policy categories ────────────────
40
+ # Labels absent from this dict β†’ treated as O (not redacted)
41
+ LABEL_MAP = {
42
+ # PER β€” person names and titles
43
+ "FIRSTNAME": "PER",
44
+ "MIDDLENAME": "PER",
45
+ "LASTNAME": "PER",
46
+ "PREFIX": "PER", # Mr., Dr., Ms.
47
+ # ORG β€” organisation names
48
+ "COMPANYNAME": "ORG",
49
+ # EMAIL
50
+ "EMAIL": "EMAIL",
51
+ # PHONE
52
+ "PHONENUMBER": "PHONE",
53
+ # ADDRESS β€” postal / physical address components
54
+ "BUILDINGNUMBER": "ADDRESS",
55
+ "STREET": "ADDRESS",
56
+ "SECONDARYADDRESS": "ADDRESS", # apt / suite
57
+ "CITY": "ADDRESS",
58
+ "COUNTY": "ADDRESS",
59
+ "STATE": "ADDRESS",
60
+ "ZIPCODE": "ADDRESS",
61
+ # GOV_ID β€” government-issued identity numbers
62
+ "SSN": "GOV_ID",
63
+ # FINANCIAL_ID β€” payment cards, bank & crypto account identifiers
64
+ "CREDITCARDNUMBER": "FINANCIAL_ID",
65
+ "CREDITCARDCVV": "FINANCIAL_ID",
66
+ "IBAN": "FINANCIAL_ID",
67
+ "BIC": "FINANCIAL_ID",
68
+ "BITCOINADDRESS": "FINANCIAL_ID",
69
+ "ETHEREUMADDRESS": "FINANCIAL_ID",
70
+ "LITECOINADDRESS": "FINANCIAL_ID",
71
+ "MASKEDNUMBER": "FINANCIAL_ID",
72
+ # ACCOUNT_ID β€” bank / service account identifiers
73
+ "ACCOUNTNAME": "ACCOUNT_ID",
74
+ "ACCOUNTNUMBER": "ACCOUNT_ID",
75
+ "USERNAME": "ACCOUNT_ID",
76
+ # DEVICE_ID β€” device, vehicle, and network identifiers
77
+ "IP": "DEVICE_ID",
78
+ "IPV4": "DEVICE_ID",
79
+ "IPV6": "DEVICE_ID",
80
+ "MAC": "DEVICE_ID",
81
+ "PHONEIMEI": "DEVICE_ID",
82
+ "USERAGENT": "DEVICE_ID",
83
+ "VEHICLEVIN": "DEVICE_ID",
84
+ "VEHICLEVRM": "DEVICE_ID",
85
+ # DATE_OF_BIRTH β€” birth dates only (generic DATE β†’ O)
86
+ "DOB": "DATE_OF_BIRTH",
87
+ # CREDENTIALS β€” authentication secrets
88
+ "PASSWORD": "CREDENTIALS",
89
+ "PIN": "CREDENTIALS",
90
+ # Everything else (AGE, DATE, GENDER, JOBAREA, etc.) β†’ O
91
+ }
92
+
93
+ TARGET_LABELS = [
94
+ "PER", "ORG", "EMAIL", "PHONE", "ADDRESS",
95
+ "GOV_ID", "FINANCIAL_ID", "ACCOUNT_ID", "DEVICE_ID",
96
+ "DATE_OF_BIRTH", "CREDENTIALS",
97
+ ]
98
+
99
+ label_list = (
100
+ ["O"]
101
+ + sorted(f"B-{l}" for l in TARGET_LABELS)
102
+ + sorted(f"I-{l}" for l in TARGET_LABELS)
103
+ )
104
+ id2label = {i: l for i, l in enumerate(label_list)}
105
+ label2id = {l: i for i, l in id2label.items()}
106
+
107
+ # ── Config ────────────────────────────────────────────────────────────────────
108
+ MODEL_NAME = "answerdotai/ModernBERT-base" # train from base
109
+ DATASET_NAME = "ai4privacy/pii-masking-200k"
110
+ HUB_MODEL_ID = "jefftherover/modernbert-pii-mapped-v2"
111
+ OUTPUT_DIR = "modernbert-pii-mapped-v2"
112
+ MAX_LENGTH = 512
113
+
114
+ print(f"Labels ({len(label_list)}): {label_list}")
115
+
116
+ # ── Dataset ───────────────────────────────────────────────────────────────────
117
+ print("Loading dataset...")
118
+ full = load_dataset(DATASET_NAME, split="train")
119
+ en = full.filter(lambda x: x["language"] == "en")
120
+ print(f"English rows: {len(en)}")
121
+
122
+ splits = en.train_test_split(test_size=0.1, seed=42)
123
+ train_ds = splits["train"]
124
+ eval_ds = splits["test"]
125
+ print(f"Train: {len(train_ds)} Eval: {len(eval_ds)}")
126
+
127
+ # ── Tokenizer ─────────────────────────────────────────────────────────────────
128
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
129
+
130
+ # ── Tokenisation + label alignment ─────────────────────────────���──────────────
131
+ # Identical to v6 logic, with LABEL_MAP applied inside make_char_labels.
132
+
133
+ def make_char_labels(text, raw):
134
+ spans = json.loads(raw) if isinstance(raw, str) else raw
135
+ cl = ["O"] * len(text)
136
+ for span in spans:
137
+ s, e, src_lbl = int(span[0]), int(span[1]), span[2]
138
+ tgt_lbl = LABEL_MAP.get(src_lbl)
139
+ if tgt_lbl is None:
140
+ continue
141
+ for i in range(s, min(e, len(text))):
142
+ cl[i] = f"B-{tgt_lbl}" if i == s else f"I-{tgt_lbl}"
143
+ return cl
144
+
145
+ def tokenize_and_align(examples):
146
+ enc = tokenizer(
147
+ examples["source_text"],
148
+ truncation=True,
149
+ max_length=MAX_LENGTH,
150
+ return_offsets_mapping=True,
151
+ )
152
+ all_labels = []
153
+ for idx in range(len(examples["source_text"])):
154
+ text = examples["source_text"][idx]
155
+ cl = make_char_labels(text, examples["span_labels"][idx])
156
+ offsets = enc["offset_mapping"][idx]
157
+ labels, prev_end = [], None
158
+ for tok_s, tok_e in offsets:
159
+ if tok_s == tok_e:
160
+ labels.append(-100)
161
+ prev_end = None
162
+ continue
163
+ # Space-offset fix: ModernBERT absorbs leading space into token offset
164
+ real_s = tok_s
165
+ while real_s < tok_e and text[real_s] == " ":
166
+ real_s += 1
167
+ is_word_start = prev_end is None or real_s > tok_s
168
+ # Scan the full token span for the first entity character.
169
+ # Fixes the case where an entity begins after punctuation with no
170
+ # preceding space (e.g. "(Home Loan Account") β€” previously real_s
171
+ # landed on "(" (O) and the entity was silently dropped.
172
+ lbl = "O"
173
+ for c in range(real_s, min(tok_e, len(cl))):
174
+ if cl[c] != "O":
175
+ lbl = cl[c]
176
+ break
177
+ if lbl == "O":
178
+ labels.append(label2id["O"] if is_word_start else -100)
179
+ else:
180
+ labels.append(label2id.get(lbl, label2id["O"]))
181
+ prev_end = tok_e
182
+ all_labels.append(labels)
183
+ enc.pop("offset_mapping")
184
+ enc["labels"] = all_labels
185
+ return enc
186
+
187
+ print("Tokenising datasets...")
188
+ cols = train_ds.column_names
189
+ train_tok = train_ds.map(tokenize_and_align, batched=True, remove_columns=cols)
190
+ eval_tok = eval_ds.map(tokenize_and_align, batched=True, remove_columns=cols)
191
+
192
+ # ── Metrics ───────────────────────────────────────────────────────────────────
193
+ seqeval = evaluate.load("seqeval")
194
+
195
+ def compute_metrics(p):
196
+ logits, labels = p
197
+ preds = np.argmax(logits, axis=2)
198
+ true_preds = [[id2label[pp] for pp, ll in zip(pr, la) if ll != -100]
199
+ for pr, la in zip(preds, labels)]
200
+ true_labels = [[id2label[ll] for pp, ll in zip(pr, la) if ll != -100]
201
+ for pr, la in zip(preds, labels)]
202
+ res = seqeval.compute(predictions=true_preds, references=true_labels)
203
+ return {
204
+ "precision": res["overall_precision"],
205
+ "recall": res["overall_recall"],
206
+ "f1": res["overall_f1"],
207
+ "accuracy": res["overall_accuracy"],
208
+ }
209
+
210
+ # ── Model ─────────────────────────────────────────────────────────────────────
211
+ print("Loading model (ModernBERT-base + new 23-label head)...")
212
+ model = AutoModelForTokenClassification.from_pretrained(
213
+ MODEL_NAME,
214
+ num_labels=len(label_list),
215
+ id2label=id2label,
216
+ label2id=label2id,
217
+ )
218
+
219
+ # ── Trackio ───────────────────────────────────────────────────────────────────
220
+ trackio.init(project="modernbert-pii-mapped", name="modernbert-pii-mapped-v2")
221
+
222
+ # ── Training args ─────────────────────────────────────────────────────────────
223
+ args = TrainingArguments(
224
+ output_dir=OUTPUT_DIR,
225
+ num_train_epochs=5,
226
+ per_device_train_batch_size=16,
227
+ per_device_eval_batch_size=32,
228
+ gradient_accumulation_steps=2, # effective batch = 32
229
+ learning_rate=5e-5,
230
+ weight_decay=0.01,
231
+ warmup_ratio=0.1,
232
+ lr_scheduler_type="cosine_with_restarts",
233
+ eval_strategy="steps",
234
+ eval_steps=500,
235
+ save_strategy="steps",
236
+ save_steps=500,
237
+ save_total_limit=3,
238
+ load_best_model_at_end=True,
239
+ metric_for_best_model="f1",
240
+ greater_is_better=True,
241
+ push_to_hub=True,
242
+ hub_model_id=HUB_MODEL_ID,
243
+ hub_private_repo=False,
244
+ hub_strategy="every_save",
245
+ report_to="trackio",
246
+ run_name="modernbert-pii-mapped-v2",
247
+ fp16=True,
248
+ logging_steps=100,
249
+ dataloader_num_workers=2,
250
+ )
251
+
252
+ # ── Train ─────────────────────────────────────────────────────────────────────
253
+ trainer = Trainer(
254
+ model=model,
255
+ args=args,
256
+ train_dataset=train_tok,
257
+ eval_dataset=eval_tok,
258
+ data_collator=DataCollatorForTokenClassification(tokenizer),
259
+ compute_metrics=compute_metrics,
260
+ callbacks=[EarlyStoppingCallback(early_stopping_patience=3)],
261
+ )
262
+
263
+ print("Starting training...")
264
+ trainer.train()
265
+ trainer.push_to_hub()
266
+ print(f"Done! Model pushed to: https://huggingface.co/{HUB_MODEL_ID}")