"""Full supervised training using the pinned HF token-classification recipe.""" import argparse import json import math import random import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import numpy as np import torch from transformers import AutoModelForTokenClassification, AutoTokenizer, DataCollatorForTokenClassification from pii.core import BIO, MODEL_REVISION, load_jsonl, encode_windows def prepare(records, tokenizer, max_length, stride): chunks=[] stats={"documents":len(records),"chunks":0,"tokens":0,"ignored_boundary_tokens":0,"partial_window_entity_tokens":0} for record in records: encoded=encode_windows(tokenizer,record["text"],max_length,stride) for ids, mask, offsets in zip(encoded["input_ids"],encoded["attention_mask"],encoded["offset_mapping"]): content=[(a,b) for a,b in offsets if a!=b] left,right=content[0][0] if content else 0,content[-1][1] if content else 0 labels=[] for a,b in offsets: label=0 if a==b: labels.append(-100) continue for span in record["spans"]: s,e=span["start"],span["end"] if aright: label=-100 stats["partial_window_entity_tokens"]+=1 else: # First representable token of the entity gets B. earlier=any(s<=aa24*3600: raise RuntimeError("Estimated run exceeds user-approved 24h limit; owner decision required") model=AutoModelForTokenClassification.from_pretrained(args.resume / "model" if args.resume else "artifacts/backbone",num_labels=len(BIO),id2label=dict(enumerate(BIO)),label2id={v:k for k,v in enumerate(BIO)},local_files_only=True,attn_implementation="eager").to(args.device) optimizer=torch.optim.AdamW(model.parameters(),lr=config["learning_rate"],weight_decay=config["weight_decay"]) steps_per_epoch=math.ceil(len(prepared["train"])/16) scheduler=torch.optim.lr_scheduler.LambdaLR(optimizer,lambda step:max(0.0,1-step/(steps_per_epoch*config["epochs"]))) collate=DataCollatorForTokenClassification(tokenizer,padding=True,return_tensors="pt") start_epoch,best_loss,global_step=0,float("inf"),0 if args.resume: state=torch.load(args.resume/"state.pt",map_location="cpu",weights_only=False) optimizer.load_state_dict(state["optimizer"]);scheduler.load_state_dict(state["scheduler"]) random.setstate(state["random"]);np.random.set_state(state["numpy"]);torch.set_rng_state(state["torch"]) if args.device=="mps":torch.mps.set_rng_state(state["mps"]) start_epoch,best_loss,global_step=state["epoch"],state["best_loss"],state["step"] history=[] started=time.perf_counter() for epoch in range(start_epoch,config["epochs"]): model.train() order=list(range(len(prepared["train"])));random.shuffle(order) running=0.0 for step,begin in enumerate(range(0,len(order),16)): batch=collate([prepared["train"][i] for i in order[begin:begin+16]]) batch={k:v.to(args.device) for k,v in batch.items()} optimizer.zero_grad(set_to_none=True) loss=model(**batch).loss if not torch.isfinite(loss):raise RuntimeError("nonfinite loss") loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(),1.0,error_if_nonfinite=True) # Trainer default. optimizer.step();scheduler.step() running+=loss.item();global_step+=1 # Persist current progress every batch; no logging interval assumption. (out/"progress.json").write_text(json.dumps({"epoch":epoch+1,"step_in_epoch":step+1,"steps_per_epoch":steps_per_epoch,"global_step":global_step,"loss":loss.item(),"elapsed_seconds":time.perf_counter()-started})+"\n") model.eval();total_loss,total_tokens=0.0,0 with torch.no_grad(): for begin in range(0,len(prepared["validation"]),16): batch=collate(prepared["validation"][begin:begin+16]);batch={k:v.to(args.device) for k,v in batch.items()} count=(batch["labels"]!=-100).sum().item() total_loss+=model(**batch).loss.item()*count;total_tokens+=count val_loss=total_loss/total_tokens selected=val_loss