| """Run the fine-tuned TinyBERT model on new addresses. |
| |
| Standalone script — only needs `transformers`+`torch` (and optionally |
| `gazetteer_lookup.py` + a pincodes CSV for the administrative fields). |
| Unlike the t5/qwen models, this is token classification (BIO tagging), not |
| JSON generation — it always produces a well-formed field dict; there's no |
| "invalid JSON" failure mode to handle. |
| |
| Usage: |
| # Single address |
| python inference_tinybert.py "FLAT NO.32, UTTARA TOWERS, MG ROAD GUWAHATI , Kamrup Unclassified AS 781029" |
| |
| # Batch from stdin (one address per line) |
| cat addresses.txt | python inference_tinybert.py --stdin |
| |
| # Batch from a text file, output JSONL |
| python inference_tinybert.py --file addresses.txt --out results.jsonl |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import sys |
|
|
| MAX_LENGTH = 160 |
|
|
| ALL_FIELDS = ( |
| "houseNumber", "houseName", "poi", "street", "subsubLocality", "subLocality", |
| "locality", "village", "subDistrict", "district", "city", "state", "pincode", |
| ) |
|
|
| LABELS = ["O"] + [f"{prefix}-{field}" for field in ALL_FIELDS for prefix in ("B", "I")] |
| ID2LABEL = {i: label for i, label in enumerate(LABELS)} |
|
|
|
|
| def extract_fields(raw_address: str, offsets: list[tuple[int, int]], pred_label_ids: list[int]) -> dict: |
| """Reconstruct the field dict from per-token BIO predictions by slicing the |
| raw address text at each contiguous B-/I- run's char span — never |
| `tokenizer.decode`, which would lose casing and introduce WordPiece-merge |
| artifacts (e.g. "##" continuation glue).""" |
| result = {f: None for f in ALL_FIELDS} |
| current_field = None |
| current_start = None |
| current_end = None |
|
|
| def flush(): |
| if current_field is not None and result[current_field] is None: |
| result[current_field] = raw_address[current_start:current_end] |
|
|
| for (start, end), label_id in zip(offsets, pred_label_ids): |
| if start == end: |
| continue |
| label = ID2LABEL[label_id] |
| if label == "O": |
| flush() |
| current_field = None |
| continue |
| prefix, field = label.split("-", 1) |
| if prefix == "B" or field != current_field: |
| flush() |
| current_field, current_start, current_end = field, start, end |
| else: |
| current_end = end |
| flush() |
| return result |
|
|
|
|
| def load_model(model_id: str): |
| from transformers import AutoModelForTokenClassification, AutoTokenizer |
|
|
| tokenizer = AutoTokenizer.from_pretrained(model_id) |
| model = AutoModelForTokenClassification.from_pretrained(model_id) |
| model.eval() |
| return model, tokenizer |
|
|
|
|
| def parse_address(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() |
| result = extract_fields(raw_address, offsets, pred_ids) |
| result["_raw_address"] = raw_address |
| return result |
|
|
|
|
| def main(): |
| p = argparse.ArgumentParser(description="Parse Indian addresses using the fine-tuned TinyBERT model") |
| p.add_argument("address", nargs="?", help="Single address to parse") |
| p.add_argument("--stdin", action="store_true", help="Read addresses from stdin, one per line") |
| p.add_argument("--file", help="Read addresses from a text file") |
| p.add_argument("--out", help="Write JSONL output to file (default: stdout)") |
| p.add_argument("--model", default=".", help="Model dir or HF repo id (default: current directory)") |
| p.add_argument("--pincodes", default=None, |
| help="Path to a pincodes CSV (India Post format) — if given, adds " |
| "districtAdministrative/stateAdministrative/cityAdministrative " |
| "fields via deterministic pincode lookup (does not alter the " |
| "model's own district/state/city)") |
| args = p.parse_args() |
|
|
| import os |
|
|
| model, tokenizer = load_model(args.model) |
|
|
| gazetteer = None |
| if args.pincodes and os.path.exists(args.pincodes): |
| from gazetteer_lookup import load_gazetteer |
|
|
| gazetteer = load_gazetteer(args.pincodes) |
|
|
| if args.address: |
| addresses = [args.address] |
| elif args.stdin: |
| addresses = [line.rstrip("\n") for line in sys.stdin if line.strip()] |
| elif args.file: |
| with open(args.file, encoding="utf-8") as f: |
| addresses = [line.rstrip("\n") for line in f if line.strip()] |
| else: |
| p.print_help() |
| return |
|
|
| out_f = open(args.out, "w", encoding="utf-8") if args.out else None |
| for addr in addresses: |
| result = parse_address(model, tokenizer, addr) |
| if gazetteer is not None: |
| from gazetteer_lookup import add_administrative_fields |
|
|
| raw_addr = result.pop("_raw_address") |
| result = add_administrative_fields(result, gazetteer) |
| result["_raw_address"] = raw_addr |
| line = json.dumps(result, ensure_ascii=False) |
| if out_f: |
| out_f.write(line + "\n") |
| else: |
| print(line) |
|
|
| if out_f: |
| out_f.close() |
| print(f"Wrote {len(addresses):,} results to {args.out}", file=sys.stderr) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|