You're working with domain texts—medical, legal, financial. Standard NER models (e.g., spaCy or Stanford NER) miss up to 30% of specific entities: license numbers, drug codes, references to legislative acts. A custom TokenClassification model solves the problem—we train it with F1 up to 97%. Our NER model training service starts at €3,500, and clients typically save over $5,000 in data correction costs. With extensive experience in NLP and over 30 projects, we've learned to navigate common pitfalls such as unbalanced annotation and label alignment.
Named Entity Recognition (NER) is the task of extracting named entities. Fine-tuning NER on custom types is standard practice, but there are many pitfalls: from IOB2 annotation errors to incorrect loss calculation on special tokens. We offer expert fine-tuning NER services for domain adaptation.
What Problems We Solve
- Chaotic annotation: IOB2 tags with omissions and overlaps reduce F1 by 10–15%. We audit, fix inconsistencies, and standardize the format. One client saved over $1.8k–2.6k on error correction after our audit.
- Rare entities: The "License Number" type may appear 20 times in 10,000 documents. Without oversampling or augmentation, the model simply won't learn it. We use entity replacement from a predefined dictionary—boosting F1 by up to 8%.
- Model selection: DeepPavlov/rubert-base-cased gives 94% on PER but may drop to 70% on custom legal entities. We test 3–4 pre-trained models (ruBERT, XLM-R, mBERT) and pick the best for your domain. DeepPavlov/rubert is 1.5× more accurate than mBERT on Russian custom entities. Our NLP model outperforms default spaCy NER by 3× on legal entity extraction.
Example IOB2 annotation for a medical text
[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O] Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O] Prescribed [O] paracetamol [B-DRUG] . [SEP]"}
Wait, the instruction says to restore proper newlines inside a single code block that was flattened. But this is a single line of tokens, not a multi-line structure. It seems like it's a sequence of tokens for NER. Possibly it should be split into sentences? But the instruction says "For ASCII diagrams (arrows ââââ or boxes [ ]): each logical row on its own line." Here we have brackets but they are token labels, not boxes. Actually the tokens are like [B-PER] etc. But the instruction says "Do NOT alter any character other than adding
and leading indentation." So we cannot change anything else. The original is one line. Maybe it should be split at sentence boundaries? But there are periods. However, the instruction says "restore proper newlines inside a single code block that was flattened to one line by an upstream LLM." So we need to infer where newlines were originally. Possibly it was originally multiple lines, each line being a sentence or a segment. But we don't have context. The example given is a single line. Perhaps it should be split at each period? But that would add newlines after periods. However, the instruction says "For Python/YAML/JSON/bash/JS/TS: restore idiomatic line breaks (imports, def/class, control blocks, YAML keys, list items)." This is not any of those. It's a sequence of tokens. Maybe it's a CoNLL format? But it's not tab-separated. It's space-separated with tags. Possibly it should be one token per line? But that would be a huge change. The instruction says "Do NOT alter any character other than adding
and leading indentation." So we can only add newlines. We cannot split tokens. So we need to decide where to put newlines. Perhaps it's a single sentence, so no newlines needed. But the instruction says "restore proper newlines" implying there were some. Maybe the original had each sentence on a new line? But there is only one sentence here. Actually there are multiple sentences: "Patient Ivanov complains of headache ." "Diagnosis : migraine ." "Prescribed paracetamol ." So we could split at periods. But the periods are tokens with [O] tags. So we can add newlines after each period token? But we need to preserve the tokens. Let's see the string: "[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O] Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O] Prescribed [O] paracetamol [B-DRUG] . [SEP]"
We can split at the periods that are followed by a space and then a token that starts a new sentence? But we cannot remove spaces. We can add newlines. So we could put a newline after each period token? But then we have to consider the tags. Actually the period is a token, and then there is a space and then [O] which is the tag for the period? Wait, the format seems to be token followed by tag? Actually it's like "Patient [B-PER]" meaning token "Patient" has tag B-PER. But then "Ivanov [I-PER]" etc. But then we have "headache [B-SYMPTOM] [I-SYMPTOM]" - that suggests that "headache" has two tags? That doesn't make sense. Actually it might be that the tokens are separated by spaces and each token is followed by its tag in brackets? But then "headache [B-SYMPTOM] [I-SYMPTOM]" would mean headache has two tags? Possibly it's a sequence of token-tag pairs, but sometimes a token can have multiple tags? Or maybe it's a different format. Let's parse: The string starts with "[CLS]" which is a special token. Then "Patient" then "[B-PER]" - so that is token "Patient" with tag B-PER. Then "Ivanov" then "[I-PER]" - token Ivanov with tag I-PER. Then "complains" then "[O]" - token complains with tag O. Then "of" then "[O]" - token of with tag O. Then "headache" then "[B-SYMPTOM]" then "[I-SYMPTOM]" - that would be token headache with two tags? That is odd. Possibly it's a mistake. Maybe it should be "headache [B-SYMPTOM]" and then the next token is something else? But there is no space between [B-SYMPTOM] and [I-SYMPTOM]? Actually there is a space: "headache [B-SYMPTOM] [I-SYMPTOM]" so it's three tokens: "headache", "[B-SYMPTOM]", "[I-SYMPTOM]"? But then the tags are not attached to tokens. Actually in typical NER, we have token and tag separated by space, but here they are interleaved. Possibly it's a sequence of tokens and tags, but each token is followed by its tag, but sometimes a token can have multiple tags? That doesn't make sense. Alternatively, it might be that the tokens are the words and the tags are in brackets, but they are separate tokens. For example, the sequence is: [CLS], Patient, [B-PER], Ivanov, [I-PER], complains, [O], of, [O], headache, [B-SYMPTOM], [I-SYMPTOM], ., [O], Diagnosis, [O], :, [O], migraine, [B-DIAGNOSIS], ., [O], Prescribed, [O], paracetamol, [B-DRUG], ., [SEP]
So it's a sequence of tokens, some are words, some are tags in brackets, some are punctuation. So it's a flat list. The instruction says "For ASCII diagrams (arrows ââââ or boxes [ ]): each logical row on its own line." But here we have brackets but they are not boxes, they are tags. So that doesn't apply. So perhaps we should not add any newlines because it's a single sequence. But the instruction says "restore proper newlines" implying there were some. Maybe the original had each sentence on a new line? But we don't know. Possibly it was originally a multi-line format where each line is a sentence? But we have only one line. Could it be that the original had each token on a new line? That would be a typical CoNLL format, but here tokens are space-separated. So maybe we need to split at sentence boundaries. The periods indicate sentence boundaries. So we can add newlines after each period token? But we have to be careful with the tags. Let's see: after the first period, there is a space and then [O] which is the tag for the period? Actually in the sequence, we have ". [O]" so the period is a token and [O] is its tag? But then we have "Diagnosis [O]" - that would be token Diagnosis with tag O? But then we have ":" and then "[O]" - so colon with tag O? Then "migraine [B-DIAGNOSIS]" - token migraine with tag B-DIAGNOSIS. Then "." and "[O]" - period with tag O. Then "Prescribed [O]" - token Prescribed with tag O. Then "paracetamol [B-DRUG]" - token paracetamol with tag B-DRUG. Then "." and "[SEP]" - period with tag SEP? Actually [SEP] is a special token, not a tag. So it's confusing.
Maybe the format is: each token is followed by its tag, but sometimes a token can have multiple tags? Actually in the string, we see "headache [B-SYMPTOM] [I-SYMPTOM]" - that suggests that the token "headache" has two tags? That is not standard. Possibly it's a mistake and it should be "headache [B-SYMPTOM]" and then the next token is something else? But there is no space between [B-SYMPTOM] and [I-SYMPTOM]? Actually there is a space. So it's three tokens: "headache", "[B-SYMPTOM]", "[I-SYMPTOM]". So the tags are separate tokens. So the sequence is a mix of words and tags. So it's not a token-tag pair format. It's just a sequence of tokens. So perhaps it's a sequence of tokens for a BERT input, where special tokens like [CLS] and [SEP] are included, and the tags are also tokens? That doesn't make sense.
Given the instruction, we are to restore newlines. Since it's a single line, maybe we should not add any newlines. But the instruction says "restore proper newlines" implying there were some. Perhaps the original had each sentence on a new line. So we can split at the periods that are followed by a space and then a capital letter? But we cannot remove spaces. We can add newlines after the period token and its tag? Actually we need to decide where to put newlines. Let's look at the structure: There are three sentences: "Patient Ivanov complains of headache ." "Diagnosis : migraine ." "Prescribed paracetamol ." So we can put newlines after the period token and its associated tag? But the period token is followed by a space and then [O] which is its tag? Actually in the sequence, we have ". [O]" so the period is a token and [O] is another token. So we could put a newline after the [O] that follows the period? But then we have "Diagnosis" which starts a new sentence. So we can put a newline before "Diagnosis" and before "Prescribed". But we need to preserve the tokens. So we can add newlines at those points.
Let's identify the sentence boundaries. The periods are tokens. After each period, there is a space and then a tag token [O] or [SEP]. So we can put a newline after the tag that follows the period? But we have to be careful: the first period is followed by [O], then space, then "Diagnosis". So we can put a newline after [O]? But then we have "Diagnosis" on a new line. Similarly, the second period is followed by [O], then space, then "Prescribed". So we can put a newline after that [O]. The third period is followed by [SEP] which is the end. So we can put a newline after [SEP]? But that would be the end.
But we also have the beginning [CLS] and then "Patient". So we can put a newline after [CLS]? Possibly not.
Maybe the original had each sentence on a separate line, with the [CLS] at the beginning of the first line and [SEP] at the end of the last line. So we can split into three lines:
Line 1: [CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O]
Line 2: Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O]
Line 3: Prescribed [O] paracetamol [B-DRUG] . [SEP]
But note that in line 1, we have "headache [B-SYMPTOM] [I-SYMPTOM]" - that is three tokens. That might be correct. So we can add newlines after the [O] that follows the first period, and after the [O] that follows the second period. But we need to ensure we don't break any token pairs. Since the tokens are space-separated, we can just insert newlines at those points.
Let's write the original string with spaces:
"[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O] Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O] Prescribed [O] paracetamol [B-DRUG] . [SEP]"
We want to add newlines after the first "[O]" that follows the first period? Actually the first period is followed by "[O]" then space then "Diagnosis". So we can put a newline after that "[O]". Similarly, the second period is followed by "[O]" then space then "Prescribed". So we can put a newline after that "[O]". And the third period is followed by "[SEP]" so we can put a newline after "[SEP]" but that would be the end.
So the reformatted code would be:
[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O]
Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O]
Prescribed [O] paracetamol [B-DRUG] . [SEP]
But we need to ensure that we don't have extra spaces at the beginning of lines. The original has no leading spaces. So we just add newlines.
But the instruction says "Do NOT alter any character other than adding
and leading indentation." So we can add newlines. We cannot remove spaces. So we just insert newline characters at those positions.
So the output JSON should have the code with real newlines.
Let's construct the string with newlines. We'll use
to represent newline.
So the code is:
"[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O]
Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O]
Prescribed [O] paracetamol [B-DRUG] . [SEP]"
But we need to check if there is any leading indentation required. The instruction says "Preserve indentation (4 spaces for Python, 2 spaces for YAML)." But this is not Python or YAML. So no indentation needed. So we just have the lines as above.
But wait, the instruction says "For ASCII diagrams (arrows ââââ or boxes [ ]): each logical row on its own line." Here we have brackets but they are not boxes, they are tags. So that doesn't apply.
So I think this is the correct reformatting.
But let's double-check: The original had no newlines. We added newlines after the first [O] that follows the first period, and after the second [O] that follows the second period. But we need to ensure that we don't break the sequence incorrectly. For example, after the first period, we have ". [O]" so the period and [O] are on the same line. Then we put a newline after [O], so the next token "Diagnosis" starts a new line. That seems fine.
Similarly, after the second period, we have ". [O]" then newline then "Prescribed". That is fine.
The third period is followed by "[SEP]" so we put a newline after [SEP]? Actually we don't need to put a newline at the end because it's the end of the string. But we can put a newline after [SEP] if we want, but it's not necessary. The instruction says "restore proper newlines" so we should put newlines where they were originally. Since we don't know, we can assume each sentence is on its own line. So we have three lines. The last line ends with [SEP] and no newline after it? Typically we might have a newline at the end, but it's not required. We'll just have the three lines without a trailing newline.
So the code string is as above.
Now we need to output JSON with {"code": "..."} where the code contains real newlines. In JSON, we need to escape newlines as
. So we'll write the string with
.
Thus the output is:
{"code": "[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O]
Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O]
Prescribed [O] paracetamol [B-DRUG] . [SEP]"}
But we need to ensure that we don't have any extra spaces. The original had spaces between tokens. We preserved them.
Let's verify the original string: "[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O] Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O] Prescribed [O] paracetamol [B-DRUG] . [SEP]"
We split after the first "[O]" that follows the first period. That is after the token "[O]" that comes after the period. So we have:
Line1: "[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O]"
Then newline.
Line2: "Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O]"
Then newline.
Line3: "Prescribed [O] paracetamol [B-DRUG] . [SEP]"
Yes.
So that is the answer.
But wait, the instruction says "For ASCII diagrams (arrows ââââ or boxes [ ]): each logical row on its own line." Here we have brackets but they are not boxes, they are tags. So that doesn't apply.
So I'll output that.
But let's consider if there is any other interpretation. Maybe the original had each token on a new line? That would be a typical NER format. But then we would have many lines. However, the instruction says "restore proper newlines" and we don't know the original. Given that it's a single line, it's likely that it was originally multiple lines with each sentence on a line. So I'll go with that.
Thus the final JSON.<ï½endâofâthinkingï½>{"code": "[CLS] Patient [B-PER] Ivanov [I-PER] complains [O] of [O] headache [B-SYMPTOM] [I-SYMPTOM] . [O]
Diagnosis [O] : [O] migraine [B-DIAGNOSIS] . [O]
Prescribed [O] paracetamol [B-DRUG] . [SEP] How We Do It
Consider a case: medical protocols where entities "Symptom", "Diagnosis", and "Drug" need extraction. Initial annotation—500 sentences in IOB2. Our entity extraction pipeline:
- Fix errors: in 10% of sentences, confusion between B-Diagnosis and I-Diagnosis.
- Augment: replace drug names from a dictionary of analogues, expanding the sample to 1500 sentences.
- Fine-tune on DeepPavlov/rubert-base-cased: 10 epochs, batch size 16, learning rate 5e-5.
- Evaluate using seqeval: F1 per entity—symptom 91%, diagnosis 94%, drug 96%.
Fine-tuning process for TokenClassification:
from transformers import AutoModelForTokenClassification, TrainingArguments, Trainer
from transformers import DataCollatorForTokenClassification
label_list = ["O", "B-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC"]
label2id = {l: i for i, l in enumerate(label_list)}
id2label = {i: l for l, i in label2id.items()}
model = AutoModelForTokenClassification.from_pretrained(
"DeepPavlov/rubert-base-cased",
num_labels=len(label_list),
id2label=id2label,
label2id=label2id
)
data_collator = DataCollatorForTokenClassification(tokenizer)
training_args = TrainingArguments(
output_dir="./ner_model",
num_train_epochs=10, # NER requires more epochs than classification
per_device_train_batch_size=16,
learning_rate=5e-5,
weight_decay=0.01,
)NER metrics: seqeval library (BIO format)
import evaluate
seqeval = evaluate.load("seqeval")
def compute_metrics(p):
predictions, labels = p
predictions = np.argmax(predictions, axis=2)
true_predictions = [
[label_list[p] for (p, l) in zip(pred, label) if l != -100]
for pred, label in zip(predictions, labels)
]
true_labels = [
[label_list[l] for (p, l) in zip(pred, label) if l != -100]
for pred, label in zip(predictions, labels)
]
results = seqeval.compute(predictions=true_predictions, references=true_labels)
return {
"f1": results["overall_f1"],
"precision": results["overall_precision"],
} Process of Work
- Analytics: Study your domain, entity list, data volume.
- Audit & annotation preparation: Check IOB2, augment, fix errors.
- Model selection: Test 3 architectures on a validation set.
- Fine-tuning with hyperparameters: learning rate, epochs, scheduling.
- Testing: On a held-out set with per-type evaluation.
- Deployment: Conversion to ONNX, integration via REST API or library.
What's Included
- Final report with metrics per entity type.
- Fine-tuning and inference code (Jupyter notebook or Python script).
- Model in ONNX or TorchScript format for production.
- Brief documentation on parameters and supported entities.
- 2 weeks of support after delivery—we answer questions on further tuning.
How to Choose a Model Architecture?
| Model | PER F1 | ORG F1 | Custom Entities | Inference Speed |
|---|---|---|---|---|
| DeepPavlov/rubert | 94–97% | 88–93% | 80–92% | 15 ms / token |
| XLM-RoBERTa | 93–96% | 87–92% | 78–90% | 20 ms / token |
| mBERT | 91–95% | 85–90% | 75–88% | 12 ms / token |
DeepPavlov/rubert gives the best F1 on Russian for standard entities, but for custom ones with small data, XLM-RoBERTa may be more stable. For domain adaptation, we test multiple pretrained models.
Comparison of Augmentation Methods
| Method | F1 Gain | Applicability |
|---|---|---|
| Oversampling | 2–5% | Rare types (≤50 examples) |
| Dictionary replacement | 5–8% | Entities with known analogues |
| Back-translation | 3–7% | Any types, requires extra compute |
Why Quality Annotation Matters
IOB2 annotation is key. Errors like missing B-tag or entity overlap reduce final F1 by 10–20%. We guarantee that after our annotation audit and fix, you'll gain at least 5% accuracy improvement with the same data volume. A robust NLP model relies on precise token alignment.
Common Fine-Tuning Mistakes
- Too few epochs: 3 epochs is insufficient for NER—need 8–12.
- Unbalanced batch: if one entity type appears in 90% of sentences, use weighted loss or undersampling.
- Ignoring special tokens: don't forget to mask [CLS], [SEP], and padding when calculating loss.
Metrics and Guarantee
We rely on seqeval for transparent evaluation. Typical F1 after our tuning: PER 94–97%, ORG 88–93%, custom domain entities 80–92%. We guarantee stable metrics on the test set: if F1 falls 5% or more below the stated threshold, we fine-tune for free. Our medical NLP solutions are tailored for clinical texts.
Contact us to assess your case—we'll recommend the optimal architecture and annotation volume. Order your NER model training today.







