AI-извлечение данных из счетов-фактур и инвойсов
Ручная обработка инвойсов — типичная точка боли: бухгалтерия тратит 3–8 минут на каждый документ, ошибки при ручном вводе 1–3%, при объёме 500+ инвойсов в месяц это ощутимо. Document AI-решение снижает время до 5–15 секунд на документ с точностью поля >98% на стандартных форматах.
Архитектура Document AI для инвойсов
Три подхода в порядке сложности и точности:
1. Rule-based + OCR — для фиксированных шаблонов (один поставщик, всегда одинаковый layout). Быстро, дёшево, ломается на любом изменении шаблона.
2. LayoutLM/DocTR — учитывает пространственное расположение текста, работает на вариативных шаблонах.
3. Multimodal LLM (GPT-4V, Claude Vision, Gemini) — понимает произвольные форматы, высокая точность, но стоимость per-document выше.
LayoutLMv3 — производственное решение
from transformers import (
LayoutLMv3Processor,
LayoutLMv3ForTokenClassification
)
import torch
from PIL import Image
# Метки для invoice extraction
LABEL_LIST = [
'O', # не поле
'B-INVOICE_NUMBER',
'I-INVOICE_NUMBER',
'B-INVOICE_DATE',
'I-INVOICE_DATE',
'B-DUE_DATE',
'B-VENDOR_NAME',
'I-VENDOR_NAME',
'B-VENDOR_ADDRESS',
'I-VENDOR_ADDRESS',
'B-TOTAL_AMOUNT',
'I-TOTAL_AMOUNT',
'B-TAX_AMOUNT',
'I-TAX_AMOUNT',
'B-LINE_ITEM_DESC',
'I-LINE_ITEM_DESC',
'B-LINE_ITEM_AMOUNT',
]
LABEL2ID = {l: i for i, l in enumerate(LABEL_LIST)}
ID2LABEL = {i: l for l, i in LABEL2ID.items()}
class InvoiceExtractor:
def __init__(self, model_path: str):
self.processor = LayoutLMv3Processor.from_pretrained(
model_path, apply_ocr=True # встроенный OCR через Tesseract
)
self.model = LayoutLMv3ForTokenClassification.from_pretrained(
model_path,
num_labels=len(LABEL_LIST),
id2label=ID2LABEL,
label2id=LABEL2ID
).eval().cuda()
@torch.no_grad()
def extract(self, image_path: str) -> dict:
image = Image.open(image_path).convert('RGB')
encoding = self.processor(
image,
return_tensors='pt',
truncation=True,
max_length=512
).to('cuda')
outputs = self.model(**encoding)
predictions = outputs.logits.argmax(dim=-1).squeeze().cpu()
# Декодирование токенов → поля
tokens = self.processor.tokenizer.convert_ids_to_tokens(
encoding['input_ids'].squeeze().cpu()
)
boxes = encoding['bbox'].squeeze().cpu().numpy()
pred_ids = predictions.numpy()
fields = {}
current_field = None
current_tokens = []
for token, pred_id in zip(tokens, pred_ids):
if token in ['[CLS]', '[SEP]', '[PAD]']:
continue
label = ID2LABEL[pred_id]
if label.startswith('B-'):
if current_field and current_tokens:
fields[current_field] = self._tokens_to_text(current_tokens)
current_field = label[2:]
current_tokens = [token]
elif label.startswith('I-') and current_field:
current_tokens.append(token)
else:
if current_field and current_tokens:
fields[current_field] = self._tokens_to_text(current_tokens)
current_field = None
current_tokens = []
return fields
def _tokens_to_text(self, tokens: list) -> str:
text = self.processor.tokenizer.convert_tokens_to_string(tokens)
return text.strip()
Постобработка и валидация
Сырой вывод модели требует нормализации: суммы с разными разделителями, форматы дат, ИНН/VAT number.
import re
from datetime import datetime
from decimal import Decimal
class InvoiceFieldValidator:
def validate_and_normalize(self, raw_fields: dict) -> dict:
validated = {}
# Сумма: '1.234,56 €' → Decimal('1234.56')
if 'TOTAL_AMOUNT' in raw_fields:
validated['total_amount'] = self._parse_amount(
raw_fields['TOTAL_AMOUNT']
)
# Дата: разные форматы → ISO 8601
if 'INVOICE_DATE' in raw_fields:
validated['invoice_date'] = self._parse_date(
raw_fields['INVOICE_DATE']
)
# Номер инвойса — минимальная валидация (не пустой, алфанумерик)
if 'INVOICE_NUMBER' in raw_fields:
inv_num = re.sub(r'\s+', '', raw_fields['INVOICE_NUMBER'])
validated['invoice_number'] = inv_num if inv_num else None
return validated
def _parse_amount(self, text: str) -> Decimal | None:
# Убираем валютные символы и пробелы
cleaned = re.sub(r'[€$£₽\s]', '', text)
# Нормализуем разделители
if re.match(r'^\d{1,3}(\.\d{3})*,\d{2}$', cleaned):
# Европейский формат: 1.234,56
cleaned = cleaned.replace('.', '').replace(',', '.')
elif re.match(r'^\d{1,3}(,\d{3})*\.\d{2}$', cleaned):
# Американский: 1,234.56
cleaned = cleaned.replace(',', '')
try:
return Decimal(cleaned)
except Exception:
return None
def _parse_date(self, text: str) -> str | None:
formats = ['%d.%m.%Y', '%d/%m/%Y', '%Y-%m-%d',
'%d %b %Y', '%B %d, %Y', '%d.%m.%y']
for fmt in formats:
try:
return datetime.strptime(text.strip(), fmt).date().isoformat()
except ValueError:
continue
return None
Точность по типам полей (производственные данные)
| Поле | LayoutLMv3 | GPT-4V | Rule-based |
|---|---|---|---|
| Номер инвойса | 97.3% | 99.1% | 99.8%* |
| Дата | 96.8% | 98.7% | 98.2%* |
| Итоговая сумма | 95.1% | 98.4% | 96.5%* |
| Позиции (line items) | 88.4% | 94.2% | 40%* |
| Адрес поставщика | 91.2% | 96.8% | 72%* |
*только для фиксированных шаблонов
Сроки
| Задача | Срок |
|---|---|
| Настройка DocTR/AWS Textract для стандартных форматов | 1–2 недели |
| Fine-tuning LayoutLMv3 на корпоративный набор | 4–6 недель |
| Полная система с ERP-интеграцией | 6–10 недель |







