Реализация извлечения структурированных данных из сканов документов (Document AI)
Document AI — задача преобразования сканов и фотографий документов в структурированные данные: JSON, CSV, записи в базе данных. Это уровень выше простого OCR: система понимает семантику документа и маппирует извлечённый текст на поля схемы данных.
LayoutLM и его производные
Ключевое отличие Document AI от обычного NLP: модель обрабатывает не только текст, но и визуальное расположение элементов (координаты слов) и изображение документа. LayoutLMv3 объединяет все три модальности.
from transformers import LayoutLMv3Processor, LayoutLMv3ForTokenClassification
from PIL import Image
import torch
class DocumentFieldExtractor:
def __init__(self, model_path: str, labels: list[str]):
self.processor = LayoutLMv3Processor.from_pretrained(model_path)
self.model = LayoutLMv3ForTokenClassification.from_pretrained(
model_path,
num_labels=len(labels) * 2 + 1 # BIO tagging
)
self.labels = labels
self.model.eval()
@torch.no_grad()
def extract(self, image_path: str) -> dict:
image = Image.open(image_path).convert('RGB')
# Processor сам запускает OCR (через Tesseract) и нормализует координаты
encoding = self.processor(
image,
return_tensors='pt',
truncation=True,
padding='max_length',
max_length=512
)
outputs = self.model(**encoding)
predictions = outputs.logits.argmax(-1).squeeze().tolist()
# Извлечение сущностей по BIO меткам
return self._decode_entities(encoding, predictions)
Построение кастомного экстрактора
Для каждого типа документа (инвойс, договор, накладная) обучаем отдельный экстрактор на размеченном датасете:
Разметка данных: Label Studio с поддержкой Document AI. Разметчик выделяет области на скане и назначает тип поля. Минимум 200–500 размеченных документов для базового качества.
Fine-tuning LayoutLMv3:
from transformers import TrainingArguments, Trainer
from datasets import load_dataset
training_args = TrainingArguments(
output_dir='./invoice_extractor',
num_train_epochs=20,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
learning_rate=5e-5,
warmup_steps=100,
weight_decay=0.01,
fp16=True,
evaluation_strategy='epoch',
save_strategy='best',
metric_for_best_model='f1'
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
compute_metrics=compute_ner_metrics
)
trainer.train()
Пример: универсальный экстрактор инвойсов
class InvoiceExtractor:
"""Извлечение данных из инвойсов любого формата"""
FIELDS = [
'invoice_number', 'invoice_date', 'due_date',
'vendor_name', 'vendor_address', 'vendor_tin',
'buyer_name', 'buyer_address', 'buyer_tin',
'line_item_description', 'line_item_quantity',
'line_item_unit_price', 'line_item_amount',
'subtotal', 'tax_rate', 'tax_amount', 'total_amount',
'payment_terms', 'bank_account'
]
def extract(self, scan_path: str) -> dict:
# 1. OCR
ocr_result = self.ocr.extract_text(scan_path)
# 2. LayoutLMv3 извлечение полей
fields = self.layoutlm.extract(scan_path)
# 3. Постобработка и валидация
processed = self._postprocess(fields)
# 4. Проверка контрольных сумм (subtotal + tax = total)
processed['validation'] = self._validate_amounts(processed)
return processed
def _postprocess(self, fields: dict) -> dict:
# Нормализация дат
if fields.get('invoice_date'):
fields['invoice_date'] = normalize_date(fields['invoice_date'])
# Нормализация сумм (удаление пробелов, замена запятых)
for amount_field in ['total_amount', 'tax_amount', 'subtotal']:
if fields.get(amount_field):
fields[amount_field] = parse_amount(fields[amount_field])
return fields
Качество на реальных датасетах
| Датасет | Поле | F1 (LayoutLMv3) |
|---|---|---|
| FUNSD (формы) | Entity extraction | 92.0% |
| CORD (чеки) | Поля транзакций | 95.5% |
| SROIE (инвойсы) | 4 ключевых поля | 96.6% |
| DocVQA (QA на документах) | ANLS | 83.5% |
| Тип проекта | Срок |
|---|---|
| Один тип документа, 500+ размеченных сканов | 4–6 недель |
| 3–5 типов документов | 8–12 недель |
| Универсальный экстрактор с дообучением | 12–18 недель |







