Practical Implementation of NER for Resume Processing: From Data Annotation to Production Service
Named Entity Recognition (NER) is a critical component of modern NLP systems, especially in HR automation. In this article, we'll break down the technical aspects of implementing an NER solution for extracting structured data from resumes, with a focus on Russian-language texts and the specifics of production deployment.
Defining the Task Boundaries
Before starting development, it's essential to clearly formalize the requirements. For an HR system, the key entities are:
- Personal data (full name, email, phone)
- Professional skills (technologies, tools)
- Financial parameters (expected salary with currency)
It's important to note that skills in resumes are often listed without separators (e.g., "Python SQL Docker"). This requires using the BIO tagging scheme instead of simple IO so the model can correctly separate adjacent entities. For Russian, handling case forms and abbreviations ("JS" instead of "JavaScript") is critical.
Define precise rules for each entity:
- SKILL: only professional terms (excluding soft skills)
- SALARY: amount + currency ("150 thousand. rubles.", "$2500")
- PHONE: all number formats with country code
Data Preparation: Strategies and Tools
Searching for Ready-Made Datasets
First stage — check existing resources:
- Hugging Face Datasets
Filter by: Task=Token Classification, Language=Russian. As of this article, no specialized datasets for resumes were found. English analogs (e.g., Resume Entities Dataset) require adaptation via translation and fine-tuning.
- Kaggle and Zenodo
Search by keywords "resume NER", "CV entities". Found datasets often contain:
* Incomplete annotation (only skills)
* Lack of Russian-language examples
* Different tagging schemes (BIOES vs BIO)
- Synthetic Generation
When data is scarce, use libraries like nlpaug for:
* Replacing real data with analogs ("Ivanov" → "Petrov")
* Introducing typos in contact data
* Generating variants of skill names ("PyTorch" → "pytorch")
Manual Annotation in Label Studio
When ready-made data isn't enough, create a custom dataset. Key steps:
- Preparing PDF Documents
Use pdfplumber to extract text while preserving structure:
```python
with pdfplumber.open('resume.pdf') as pdf:
text = '\n\n'.join([page.extract_text() for page in pdf.pages])
```
- Converting to JSON for Label Studio
Script to convert PDF to the platform's format:
```python
import json
import os
def convert_to_ls_format(pdf_dir):
tasks = [{"text": open(f, 'r').read()}
for f in os.listdir(pdf_dir) if f.endswith('.txt')]
with open('import.json', 'w') as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)
```
- Setting Up the Annotation Interface
In Label Studio, create a template with tags:
```xml
<View>
<Text name="text" value="$text"/>
<Labels name="ner" toName="text">
<Label value="SKILL" background="#ffcc00"/>
<Label value="SALARY" background="#66ccff"/>
</Labels>
</View>
```
Consistent annotation is critically important: establish rules for ambiguous cases (e.g., "C++" — one skill or two). To improve quality, involve two annotators and calculate Cohen's Kappa metric.
Model Selection and Training
Base Architectures
For Russian-language texts, we recommend the following approaches:
- Fine-tuning Pretrained Models
* RuBERT (DeepPavlov) — optimal for Russian
* mBERT — for multilingual support if needed
* Key parameter: max_seq_length=512 (resumes often have long descriptions)
- Customizing Tokenization
Add specific terms to the vocabulary:
```python
tokenizer.add_tokens(["CI/CD", "ETL", "MLflow"])
model.resize_token_embeddings(len(tokenizer))
```
- Handling Class Imbalance
Use a weighted loss function:
```python
class_weights = compute_class_weight('balanced',
classes=np.unique(labels),
y=labels)
criterion = nn.CrossEntropyLoss(weight=torch.tensor(class_weights))
```
Evaluation Metrics
Standard accuracy isn't suitable for NER. Track:
- Token-level F1 — primary metric
- Entity-level precision/recall — via the
seqevallibrary - Processing speed — tokens per second on CPU
Pay special attention to recall for the SALARY entity — a drop here will lead to losing critically important data.
Production Deployment
Service Architecture
Recommended integration scheme:
- Preprocessing
* Convert PDF/DOCX to text (via antiword and pdf2text)
* Normalize text (remove extra spaces, encode currencies)
- NER Core
```python
class NERServer:
def __init__(self):
self.model = AutoModelForTokenClassification.from_pretrained('ner_model')
def extract_entities(self, text):
inputs = tokenizer(text, return_tensors='pt', truncation=True)
outputs = self.model(**inputs)
# Process logits into entities
```
- Postprocessing
* Group tokens into entities by BIO tags
* Validate formats (regex for email/phone)
* Normalize skills ("JS" → "JavaScript") via mapping
Performance Optimization
- Caching — for repeated requests
- Batch Processing — combine multiple resumes into a batch
- Quantization — convert model to FP16 via
torch.quantization
For high-load systems, implement a task queue via RabbitMQ to avoid timeouts when processing large files.
Key Takeaways
- Domain Adaptation is critical: a model trained on news will fail on resumes
- Annotation Quality determines 80% of the final result — invest in annotators
- BIO Scheme is mandatory for lists of skills without separators
- Validation on Edge Cases — test handling of non-standard formats ("100k $", "~2000 evro")
- Data Drift Monitoring — regularly recalculate metrics on new resumes
— Editorial Team
No comments yet.