Wilma / scripts /train_v2.py
winifred12's picture
v2 fine tune: length bias fixed, Nigerian accuracy regressed; do not deploy
7e72e1d
Raw History Blame Contribute Delete
3.94 kB
"""
Fine tune DistilBERT on the short-message dataset.
source .venv/bin/activate
python scripts/train_v2.py
Same architecture as v1 so the data is the only变 variable.
"""
import csv, os, random, time
import torch
from torch.utils.data import DataLoader, TensorDataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification
MODEL = "distilbert-base-uncased"
OUTDIR = "models/distilbert_v2"
MAXLEN = 128
BATCH = 32
EPOCHS = 3
LR = 2e-5
SEED = 42
LABELS = {"legitimate": 0, "scam": 1}
random.seed(SEED); torch.manual_seed(SEED)
device = "mps" if torch.backends.mps.is_available() else "cpu"
print("device:", device)
def load(split):
rows = list(csv.DictReader(open("data/processed/%s.csv" % split, newline="", encoding="utf-8")))
return [r["text"] for r in rows], [LABELS[r["label"]] for r in rows]
tok = AutoTokenizer.from_pretrained(MODEL)
def encode(texts, labels):
enc = tok(texts, truncation=True, padding="max_length", max_length=MAXLEN, return_tensors="pt")
return TensorDataset(enc["input_ids"], enc["attention_mask"], torch.tensor(labels))
tr_x, tr_y = load("train")
va_x, va_y = load("val")
te_x, te_y = load("test")
print("train %d val %d test %d" % (len(tr_x), len(va_x), len(te_x)))
train_dl = DataLoader(encode(tr_x, tr_y), batch_size=BATCH, shuffle=True)
val_dl = DataLoader(encode(va_x, va_y), batch_size=64)
test_dl = DataLoader(encode(te_x, te_y), batch_size=64)
n_legit = tr_y.count(0); n_scam = tr_y.count(1); total = len(tr_y)
weights = torch.tensor([total/(2.0*n_legit), total/(2.0*n_scam)], dtype=torch.float).to(device)
print("class weights: legitimate %.3f scam %.3f" % (weights[0], weights[1]))
model = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=2).to(device)
opt = torch.optim.AdamW(model.parameters(), lr=LR)
lossf = torch.nn.CrossEntropyLoss(weight=weights)
def evaluate(dl):
model.eval()
tp = fp = fn = tn = 0
with torch.no_grad():
for ids, mask, y in dl:
ids, mask, y = ids.to(device), mask.to(device), y.to(device)
pred = model(input_ids=ids, attention_mask=mask).logits.argmax(1)
tp += int(((pred == 1) & (y == 1)).sum())
fp += int(((pred == 1) & (y == 0)).sum())
fn += int(((pred == 0) & (y == 1)).sum())
tn += int(((pred == 0) & (y == 0)).sum())
prec = tp / max(1, tp + fp)
rec = tp / max(1, tp + fn)
f1 = 2 * prec * rec / max(1e-9, prec + rec)
acc = (tp + tn) / max(1, tp + tn + fp + fn)
return acc, prec, rec, f1, (tp, fp, fn, tn)
best = -1
for ep in range(1, EPOCHS + 1):
model.train()
t0, run = time.time(), 0.0
for i, (ids, mask, y) in enumerate(train_dl, 1):
ids, mask, y = ids.to(device), mask.to(device), y.to(device)
opt.zero_grad()
loss = lossf(model(input_ids=ids, attention_mask=mask).logits, y)
loss.backward(); opt.step()
run += loss.item()
if i % 50 == 0:
print(" epoch %d batch %d/%d loss %.4f" % (ep, i, len(train_dl), run / i))
acc, prec, rec, f1, _ = evaluate(val_dl)
print("epoch %d done in %.0fs val acc %.4f prec %.4f rec %.4f F1 %.4f" % (
ep, time.time() - t0, acc, prec, rec, f1))
if f1 > best:
best = f1
os.makedirs(OUTDIR, exist_ok=True)
model.save_pretrained(OUTDIR); tok.save_pretrained(OUTDIR)
print(" saved (best so far)")
print("")
model = AutoModelForSequenceClassification.from_pretrained(OUTDIR).to(device)
acc, prec, rec, f1, (tp, fp, fn, tn) = evaluate(test_dl)
print("=" * 60)
print("HELD OUT TEST")
print("=" * 60)
print("accuracy %.4f precision %.4f recall %.4f F1 %.4f" % (acc, prec, rec, f1))
print("true scam %d false alarm %d missed scam %d true legit %d" % (tp, fp, fn, tn))
print("")
print("This number is NOT the Nigerian number. Run the Nigerian evaluation")
print("and the length experiment before believing anything.")