"""Evaluate the fine-tuned TinyBERT model on a held-out test set. Standalone script — only needs `transformers`+`torch`. Expects a JSONL file where each line is `{"messages": [system, user, assistant]}` (same chat format used by the training data on GitHub), with the assistant content being the gold JSON string. See: https://github.com/innerkorehq/indian-address-parser Usage: python evaluate_tinybert.py test.jsonl python evaluate_tinybert.py test.jsonl --n 100 """ from __future__ import annotations import argparse import json from collections import defaultdict from inference_tinybert import ALL_FIELDS, extract_fields, load_model, MAX_LENGTH def predict(model, tokenizer, raw_address: str) -> dict: import torch enc = tokenizer( raw_address, return_tensors="pt", return_offsets_mapping=True, truncation=True, max_length=MAX_LENGTH, ) offsets = enc.pop("offset_mapping")[0].tolist() with torch.no_grad(): out = model(**enc) pred_ids = out.logits[0].argmax(-1).tolist() return extract_fields(raw_address, offsets, pred_ids) def compute_metrics(pairs: list[tuple[dict, dict]]) -> dict: n_total = len(pairs) field_correct = defaultdict(int) field_present_gold = defaultdict(int) field_recalled = defaultdict(int) overall_exact = 0 for gold, pred in pairs: all_match = True for field in ALL_FIELDS: g = (gold.get(field) or "").strip().lower() p = (pred.get(field) or "").strip().lower() if g: field_present_gold[field] += 1 if g == p: field_recalled[field] += 1 else: all_match = False if g == p: field_correct[field] += 1 if all_match: overall_exact += 1 accs = {f: (field_correct[f] / n_total if n_total else 0) for f in ALL_FIELDS} return { "n_total": n_total, "overall_exact": overall_exact, "field_correct": field_correct, "field_present_gold": field_present_gold, "field_recalled": field_recalled, "field_accuracy": accs, "mean_field_accuracy": sum(accs.values()) / len(accs), } def print_report(m: dict): n_total = m["n_total"] print(f"\nResults") print(f"Samples evaluated: {n_total}") print(f"Overall exact match (all present fields): {m['overall_exact']}/{n_total} ({100*m['overall_exact']/max(1,n_total):.1f}%)\n") print(f"{'Field':20s} {'Accuracy':>10s} {'Recall':>10s} {'Gold presence':>14s}") print("-" * 60) for field in ALL_FIELDS: acc = m["field_accuracy"][field] n_gold = m["field_present_gold"][field] rec = m["field_recalled"][field] / n_gold if n_gold else float("nan") pres = n_gold / n_total if n_total else 0 rec_str = f"{100*rec:.1f}%" if n_gold else " n/a" print(f"{field:20s} {100*acc:>9.1f}% {rec_str:>10s} {100*pres:>13.1f}%") print(f"\n{'Mean field accuracy':20s} {100*m['mean_field_accuracy']:>9.1f}%") def main(): p = argparse.ArgumentParser() p.add_argument("test_file", help="JSONL file with {'messages': [system, user, assistant]} per line") p.add_argument("--model", default=".", help="Model dir or HF repo id (default: current directory)") p.add_argument("--n", type=int, default=None) args = p.parse_args() with open(args.test_file, encoding="utf-8") as f: samples = [json.loads(line) for line in f] if args.n: samples = samples[: args.n] model, tokenizer = load_model(args.model) pairs = [] for i, sample in enumerate(samples): msgs = sample["messages"] raw_address = msgs[1]["content"].replace("Parse this address:\n", "", 1) gold = json.loads(msgs[2]["content"]) pred = predict(model, tokenizer, raw_address) pairs.append((gold, pred)) if (i + 1) % 25 == 0: print(f" ...{i + 1}/{len(samples)} evaluated") print_report(compute_metrics(pairs)) if __name__ == "__main__": main()