Resume NER — Structured Resume Information Extraction
Fine-tuned DistilBERT model for extracting structured information from resume text. Raw model output is token-level BIO tags. Included post-processing turns those spans into structured resume fields.
Status
Base model: distilbert-base-cased
Task: token classification / NER
Labels: NAME, EMAIL, PHONE, LOCATION, COMPANY, TITLE, DATE, DEGREE, FIELD, INSTITUTION, SKILL, CERT, LANGUAGE
Max context: 512 tokens
PyTorch artifact: model.safetensors
ONNX artifacts: onnx/model.onnx, onnx/model_quantized.onnx
Latest NER benchmark
Latest retrain on RTX 3080 with noise-augmented data + Kaggle silver labels (25 epochs):
entity F1: 97.77%
structured micro F1: 97.88%
clean resume F1: 99.18%
noisy resume F1: 69.24% (OCR/scraped text)
These numbers come from entity-level exact-match evaluation with seqeval and the structured extraction benchmark.
Metric note
Older versions of this repo reported token-level F1. Training now uses entity-level exact-match F1 via seqeval, which is stricter and more useful for NER quality.
Quick start
Transformers
1 from transformers import pipeline
2
3 ner = pipeline ( "token-classification" , model = "oksomu/resume-ner" , aggregation_strategy = "simple" )
4 results = ner ( resume_text )
ONNX Runtime via Optimum
1 from optimum . onnxruntime import ORTModelForTokenClassification
2 from transformers import AutoTokenizer , pipeline
3
4 model = ORTModelForTokenClassification . from_pretrained ( "oksomu/resume-ner" , file_name = "onnx/model.onnx" )
5 tokenizer = AutoTokenizer . from_pretrained ( "oksomu/resume-ner" )
6 ner = pipeline ( "token-classification" , model = model , tokenizer = tokenizer , aggregation_strategy = "simple" )
Full structured extraction
1 import { parseResume , computeATSScore } from "resume-extract" ;
2
3 const result = await parseResume ( resumeText , "./model" ) ;
4 const ats = computeATSScore ( result ) ;
Usage
Model predicts these entity types:
NAME
EMAIL
PHONE
LOCATION
COMPANY
TITLE
DATE
DEGREE
FIELD
INSTITUTION
SKILL
CERT
LANGUAGE
Pre-processing
resume_config.json pre_processing section defines text normalization applied before NER inference:
normalize CRLF, em-dashes, bullet characters
strip Phone:/Email: labels from contact lines
expand flattened two-column skill tables (Category Skills → Category: Skills)
collapse multi-space runs
All rules are config-driven — any language implementation can apply the same preprocessing from the JSON config.
Post-processing
resume_config.json post_processing section contains rule-based cleanup applied after NER:
entity-specific min lengths, exceptions, and blocked words
merge adjacent same-label spans (configurable labels + gap)
group flat spans into experience / education entries
infer seniority from titles + years
infer country from location + phone prefix (317 cities in city_country_map.json)
compute experience years from date ranges
fix company names with gazetteer (companies.json)
normalize skills and certifications via aliases
merge multi-word skills
Section detection (training/section_detector.py) fills entities the NER model missed by using resume section context (SKILLS, CERTIFICATIONS, LANGUAGES sections).
All post-processing rules are config-driven for cross-language portability.
For a detailed implementation walkthrough of the full pipeline, see
docs/implementation_guide.md.
Training data
Current checked-in dataset snapshot in training/data/ was built from mixed sources:
DataTurks annotated resumes
generated examples from datasetmaster/resumes
12 manual resume templates across tech and non-tech domains
50 hand-crafted long resumes (>512 tokens) for chunked inference training
93 gold-labeled resume-resource PDFs (hand-annotated, 100% match rate)
2,483 Kaggle resumes with Gemini-extracted silver labels + BIO tagging
2x noise augmentation for OCR robustness (separator swaps, char corruption, case changes)
Data layout:
training/data/
├── ner_train.json # Main training set
├── ner_val.json # Validation split
├── kaggle_train.json # Kaggle BIO training (silver + noise-augmented)
├── gold/ # Hand-annotated evaluation data
├── sources/ # Raw source data (CSV, JSONL, etc.)
└── resume_resource/ # Source PDFs
Current public rebuild path uses:
training/generate_from_structured.py — orchestrates all sources + augmentation
training/manual_resumes.py — short manual templates
training/build_gold_labels.py — gold-labeled PDF resumes
training/noise_augment.py — noise augmentation for OCR robustness
Training
Use Python 3.11+.
Install training stack:
Create separate ONNX export env:
1 uv venv .venv-onnx-export
2 uv pip install --python .venv-onnx-export/bin/python -r training/requirements-export.txt
Train:
1 python -m training.train_ner
2 python -m training.train_ner --base-model deberta
Rebuild dataset from public/synthetic sources:
python -m training.generate_from_structured
Run tests:
python -m unittest discover -s tests
Run structured benchmark:
python -m training.benchmark_structured --model-dir .
Latest internal structured benchmark on current validation set:
overall micro F1: 97.88%
macro field F1: 98.47%
clean-resume micro F1: 99.18%
noisy-resume micro F1: 69.24%
This benchmark uses in-repo structured post-processing for both gold spans and model predictions. Section-aware chunked inference handles resumes exceeding the 512-token context window. These numbers are intended for internal regression tracking, not external leaderboard claims.
Export workflow
Train in main env. Export in separate ONNX env:
1 python -m training.train_ner
2
3 source .venv-onnx-export/bin/activate
4 optimum-cli export onnx --model training/output/resume-ner/distilbert/best --task token-classification onnx/
Quantize ONNX model:
1 python - << 'PY'
2 from optimum.onnxruntime import ORTModelForTokenClassification, ORTQuantizer
3 from optimum.onnxruntime.configuration import AutoQuantizationConfig
4
5 model = ORTModelForTokenClassification.from_pretrained("onnx")
6 quantizer = ORTQuantizer.from_pretrained(model)
7 qconfig = AutoQuantizationConfig.arm64(is_static=False, per_channel=False)
8 quantizer.quantize(save_dir="onnx", quantization_config=qconfig)
9 PY
Full details:
docs/export.md
Artifact layout
root config.json, model.safetensors, tokenizer files — canonical PyTorch / Hugging Face checkpoint
root companies.json — company gazetteer used by post-processing
root city_country_map.json — 317 cities for country inference
label_config.json — explicit label metadata used by training/inference tooling
onnx/model.onnx — exported ONNX model
onnx/model_quantized.onnx — quantized ONNX model for smaller CPU deployment
resume_config.json — pre-processing, post-processing, and inference rules (single config for all languages)
ONNX validation
Current ONNX export was validated against PyTorch on same sample input:
PyTorch vs ONNX logits: allclose=True (max diff: 0.000013)
PyTorch vs ONNX predictions: argmax_equal=True
PyTorch vs quantized ONNX predictions: minor diff (expected for INT8)
This is main safety check when training and export happen in separate environments.
Repo layout
training/train_ner.py — training loop
training/generate_from_structured.py — dataset builder orchestrating all sources + augmentation
training/build_gold_labels.py — gold-labeled PDF resume builder
training/manual_resumes.py — short manual templates
training/noise_augment.py — noise augmentation for OCR robustness
training/labels.py — shared label definitions
training/dataset_utils.py — dedupe / split / manifest helpers
training/tagging.py — span tagging helpers
training/text_preprocess.py — config-driven text preprocessing
training/structured_postprocess.py — config-driven post-processing and grouping
training/section_detector.py — section-aware entity extraction
training/benchmark_structured.py — structured benchmark with chunked inference
training/analyze_structured_errors.py — per-resume error analysis
training/synthetic_assets.py — synthetic source assets/helpers
training/synthetic_formats.py — 8 resume format builders (A-H)
training/export_onnx.py — CLI wrapper for ONNX export
training/validate_onnx.py — PyTorch vs ONNX parity check
training/quantize_onnx.py — ONNX quantization helper
training/requirements-export.txt — separate ONNX export env
run_inference.py — single-resume inference script
docs/implementation_guide.md — detailed pre/post processing implementation guide
docs/export.md — ONNX export and validation notes
Limitations
English resumes only
max 512 tokens per chunk (section-aware chunking handles longer resumes)
image-based/scanned PDFs require OCR before text extraction
two-column PDF layouts may flatten during text extraction
License
Apache 2.0