AI-видобуток даних з інвойсів та рахунків
Ручна обробка інвойсів — типова проблема: бухгалтерія витрачає 3–8 хвилин на кожен документ, помилки при ручному введенні 1–3%, при обсягу 500+ інвойсів на місяць це помітно. Рішення Document AI скорочує час до 5–15 секунд на документ з точністю поля >98% на стандартних форматах.
Архітектура Document AI для інвойсів
Три підходи в порядку складності та точності:
1. Rule-based + OCR — для фіксованих шаблонів (один поставщик, завжди однаковий макет). Швидко, дешево, ламається на будь-якій зміні шаблону.
2. LayoutLM/DocTR — враховує просторове розташування тексту, працює на варіативних шаблонах.
3. Multimodal LLM (GPT-4V, Claude Vision, Gemini) — розуміє довільні формати, висока точність, але вартість за документ вища.
LayoutLMv3 — виробниче рішення
from transformers import (
LayoutLMv3Processor,
LayoutLMv3ForTokenClassification
)
import torch
from PIL import Image
# Мітки для видобутку інвойсу
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 номери.
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 тижнів |







