Проблема, которая сводит с ума
Хотите обучить LLM работать с финансовыми документами? Счета, накладные, банковские выписки? Забудьте. Реальные данные вы никогда не получите. Банки не отдают. Клиенты не подписывают согласия. Юристы пугаются слова "ИИ". Остаётся одно — сидеть и ждать чуда.
Или генерировать синтетику. Но тут новая проблема: синтетические данные часто выглядят как детский рисунок. LLM их распознаёт сразу — и начинает галлюцинировать на реальных документах. Получается замкнутый круг: нет данных → плохая модель → нельзя использовать → нет данных.
Синтетические данные, которые не похожи на реальные, хуже, чем отсутствие данных. Они учат модель неправильным паттернам.
DocuLite: не идеальное, но рабочее решение
DocuLite — это подход, а не магия. Суть простая: вместо того чтобы генерировать документы с нуля, мы берём реальные шаблоны и наполняем их синтетическими данными. Получается гибрид: структура настоящая, содержание — вымышленное.
Почему это работает? Потому что LLM в первую очередь смотрят на layout документа. Столбцы, таблицы, расположение полей, шрифты. Содержание — вторично. Если layout реалистичный, модель считает документ "настоящим".
1Собираем реальные шаблоны
Первое, что нужно сделать — найти реальные финансовые документы. Не содержимое, а именно шаблоны. PDF-ки, которые можно скачать с сайтов компаний. Пустые бланки. Формы.
Где искать:
- Сайты крупных ритейлеров (у них всегда есть образцы счетов)
- Госуслуги (шаблоны налоговых деклараций)
- Банковские сайты (пустые формы заявлений)
- GitHub репозитории с открытыми документами
Важно: не скачивайте документы с персональными данными. Только пустые формы. Если нет — делайте скриншоты и зачищайте всё, что может идентифицировать человека.
Юридический нюанс: даже пустые бланки могут быть защищены авторским правом. Используйте их только для внутренних экспериментов. Для продакшена — создавайте свои шаблоны с нуля, вдохновляясь существующими.
2InvoicePy: генератор, который не стыдно показать
InvoicePy — это Python-библиотека, которая превращает ваши шаблоны в реалистичные документы. Не просто PDF, а именно то, что вы получили бы от реального поставщика.
Как это выглядит в коде:
from invoicepy import InvoiceGenerator
from invoicepy.templates import RetailTemplate
# Создаём генератор
generator = InvoiceGenerator(
template=RetailTemplate(),
locale="ru_RU", # Русские названия, форматы дат
currency="RUB" # Рубли
)
# Генерируем синтетический счёт
invoice = generator.generate(
items=[
{"name": "Ноутбук Dell XPS 15", "quantity": 1, "price": 145000.00},
{"name": "Мышь беспроводная", "quantity": 2, "price": 3500.00},
],
vendor_name="ООО ТехноСити",
customer_name="ИП Петров А.В.",
add_errors=True # Добавляем реалистичные ошибки: опечатки, смещение текста
)
# Сохраняем в разных форматах
invoice.save_pdf("synthetic_invoice.pdf")
invoice.save_image("synthetic_invoice.png") # Для OCR пайплайнов
Ключевой параметр — add_errors=True. Реальные документы никогда не бывают идеальными. Текст съезжает. OCR ошибается. Люди опечатываются. Если ваша синтетика будет стерильно чистой — модель не научится работать с реальным миром.
3TemplatePy: когда нужна полная кастомизация
InvoicePy хорош для стандартных документов. Но что если у вас специфичный шаблон? Своя форма отчёта? Нестандартная таблица?
TemplatePy — следующий уровень. Это движок для создания собственных шаблонов с точным контролем над каждым пикселем.
from templatepy import DocumentTemplate, Field, Table, Style
# Создаём шаблон с нуля
template = DocumentTemplate(
width=210, # A4 в мм
height=297,
margins={"top": 20, "bottom": 20, "left": 15, "right": 15}
)
# Добавляем логотип (синтетический, конечно)
template.add_image(
path="synthetic_logo.png",
x=15, y=20, width=40, height=40
)
# Поле для номера счёта
template.add_field(
Field(
name="invoice_number",
x=150, y=25,
width=50, height=10,
style=Style(font_size=12, bold=True),
generator=lambda: f"INV-{random.randint(10000, 99999)}"
)
)
# Таблица с товарами
table = Table(
x=15, y=100,
columns=[
{"name": "name", "width": 100, "header": "Наименование"},
{"name": "quantity", "width": 30, "header": "Кол-во"},
{"name": "price", "width": 40, "header": "Цена"},
{"name": "total", "width": 40, "header": "Сумма"},
],
row_height=12,
num_rows=lambda: random.randint(3, 10) // Случайное количество строк
)
template.add_table(table)
# Генерируем 1000 документов
for i in range(1000):
doc = template.generate()
doc.save_pdf(f"invoices/invoice_{i}.pdf")
Прелесть TemplatePy в том, что вы контролируете всё: от расположения полей до вероятности ошибок в каждом конкретном поле. Хотите, чтобы в 5% случаев номер счёта был напечатан криво? Легко. Чтобы в 10% случаев одна из колонок таблицы съезжала? Без проблем.
Как НЕ надо делать: три фатальные ошибки
Прежде чем покажу работающий пайплайн, давайте разберём ошибки, которые гарантированно приведут к провалу.
| Ошибка | Почему это плохо | Как исправить |
|---|---|---|
| Использовать только идеальные данные | Модель переобучится на стерильные данные и сломается на реальных | Добавлять шум, ошибки OCR, опечатки в 15-20% документов |
| Генерировать мало вариаций | Модель запомнит конкретные шаблоны вместо обучения общим паттернам | Минимум 1000 уникальных документов на каждый тип, меняя всё: шрифты, цвета, layout |
| Игнорировать domain knowledge | В финансах есть жёсткие правила (сумма прописью, НДС, реквизиты) | Привлекать эксперта или использовать готовые библиотеки с бизнес-логикой |
Рабочий пайплайн: от шаблона до обученной модели
Вот полная схема, которую мы использовали в реальном проекте. От начала до конца.
1Подготовка шаблонов
Собрали 15 реальных шаблонов счетов от разных компаний. Не содержимое, а пустые формы. Конвертировали в HTML-шаблоны (да, TemplatePy работает с HTML).
# Конвертируем PDF шаблоны в HTML
python -m templatepy convert \
--input templates/*.pdf \
--output templates_html/ \
--format html
2Генерация датасета
Сгенерировали 50 000 документов. По 3-4 тысячи на каждый шаблон. Каждый документ — уникальная комбинация данных, шрифтов, ошибок.
import multiprocessing
from invoicepy.dataset import SyntheticDataset
# Создаём конфиг генерации
dataset_config = {
"num_documents": 50000,
"variations_per_template": 3000,
"error_rates": {
"ocr_errors": 0.15, # 15% документов с ошибками OCR
"typos": 0.10, # 10% с опечатками
"layout_shifts": 0.05, # 5% со съехавшим layout'ом
},
"output_formats": ["pdf", "png", "json"], # JSON с разметкой
}
# Генерируем параллельно
with multiprocessing.Pool(processes=8) as pool:
dataset = SyntheticDataset.generate_parallel(
config=dataset_config,
templates_dir="templates_html/",
output_dir="dataset/",
pool=pool
)
3Подготовка к обучению
Разметили данные автоматически (прелесть синтетики — разметка уже есть). Создали три набора: train (40k), validation (5k), test (5k).
Важный момент: в тестовый набор добавили 500 реальных документов (с обезличенными данными). Чтобы проверять не на синтетике, а на том, с чем модель будет работать в продакшене.
4Обучение модели
Использовали OpenChat 3.5 7B — хороший баланс между качеством и требовательностью. Fine-tuning на извлечение структурированных данных из документов.
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
# Загружаем предобученную модель
model = AutoModelForCausalLM.from_pretrained(
"openchat/openchat_3.5",
torch_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained("openchat/openchat_3.5")
# LoRA для эффективного fine-tuning
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# Специальный формат промптов для документов
training_prompts = []
for doc in train_dataset:
prompt = f"""Извлеки данные из счёта:
{doc['text']}
Извлеки в JSON:
- vendor_name (поставщик)
- customer_name (клиент)
- invoice_number (номер счёта)
- items (массив товаров с полями name, quantity, price, total)
- total_amount (общая сумма)
- date (дата)
JSON:"""
training_prompts.append(prompt)
Результаты: цифры, а не слова
После 8 эпох обучения (примерно 12 часов на 4x A100) получили такие результаты:
| Метрика | Без синтетики (zero-shot) | С синтетикой (наш метод) | Улучшение |
|---|---|---|---|
| F1-score (извлечение полей) | 0.412 | 0.937 | +0.525 |
| Accuracy (номер счёта) | 0.67 | 0.98 | +0.31 |
| Recall (все товары) | 0.38 | 0.95 | +0.57 |
| Время обработки (сек/док) | 3.2 | 1.8 | -44% |
Улучшение F1 на 0.525 — это не "немного лучше". Это переход от "не работает" к "работает в продакшене". Модель, которая раньше пропускала 6 из 10 товаров в накладной, теперь находит 19 из 20.
Где этот метод ломается (и как чинить)
DocuLite не панацея. Есть случаи, где он работает плохо, и нужно это понимать заранее.
Рукописные документы. Если у вас чеки, заполненные от руки, — забудьте. Генерация реалистичного рукописного текста — отдельная сложная задача. Здесь лучше использовать открытые датасеты рукописного текста и дообучать на них.
Очень сложные таблицы. Многоуровневые заголовки, объединённые ячейки, вложенные таблицы. TemplatePy с ними справляется, но генерация осмысленных данных для таких таблиц — нетривиальная задача. Придётся писать кастомные генераторы для каждого типа таблицы.
Документы с графиками и диаграммами. Финансовые отчёты часто содержат визуализации. Генерация реалистичных графиков — отдельная область. Можно использовать библиотеки типа matplotlib с добавлением шума, но идеального результата не ждите.
Интеграция в RAG-пайплайн
Обученная модель — это только половина дела. Её нужно встроить в работающую систему. Вот как мы это делали:
- Предобработка документов: OCR (если PDF не текстовый), определение типа документа, сегментация на блоки.
- Извлечение структурированных данных: Наша модель получает текст документа и возвращает JSON с полями.
- Валидация и исправление: Логический детектор проверяет непротиворечивость данных (сумма равна сумме товаров, дата в правильном формате).
- Индексация в векторную БД: И текст документа, и структурированные данные индексируются для поиска.
- Ответы на вопросы: Пользователь спрашивает "Сколько мы потратили на офисную технику в мае?", RAG ищет соответствующие документы и использует структурированные данные для точного ответа.
Ключевое преимущество: поскольку у нас есть структурированные данные, мы можем отвечать на сложные аналитические запросы. Не просто "найди документ", а "посчитай статистику по всем документам".
Что делать, если нет GPU?
A100 — это прекрасно, но не у всех они есть. Есть несколько обходных путей:
- Использовать меньшую модель: Qwen2.5 3B или даже 1.5B справляются с извлечением данных из документов, если их правильно обучить. Качество будет ниже, но для многих задач достаточно.
- Облачные инстансы: Арендовать GPU на несколько часов только для обучения. 8-часовой инстанс g5.xlarge на AWS обойдётся в $5-10.
- Коллабы: Google Colab Pro даёт A100 на 24 часа за $10 в месяц. Наши эксперименты показывали, что для fine-tuning 7B модели на 50k примеров этого достаточно.
- Поэтапное обучение: Сначала обучить на маленьком датасете (5k документов), проверить качество, потом докупать ресурсы для полного обучения.
Самое дорогое в этом пайплайне — не обучение, а генерация данных. 50 000 документов в высоком качестве — это десятки часов CPU-времени. Но эту задачу можно распараллелить на дешёвых CPU-инстансах.
Что дальше? Эксперименты, которые стоит провести
Метод DocuLite работает. Но есть куда развиваться. Вот что мы тестируем сейчас:
Адаптивная генерация ошибок. Вместо случайных ошибок — анализ реальных OCR-ошибок и их воспроизведение. Если Tesseract часто путает "1" и "l", наша синтетика должна делать то же самое.
Мультиязычные документы. Счета на английском, китайском, арабском. Layout отличается, правила форматирования — тоже. Нужно создавать отдельные шаблоны для каждого языка.
Генерация документов цепочками. Не просто счёт, а полный цикл: заказ → накладная → счёт → акт → платёжное поручение. Чтобы модель училась понимать связи между документами.
Использование аблитерированных моделей для генерации содержимого. Вместо случайных товаров — реалистичные названия, описания, цены. Модель, у которой "стёрли" ограничения, может генерировать любые данные без риска утечки реальной информации.
Самый перспективный эксперимент: использовать обученную на синтетике модель для разметки реальных (обезличенных) документов, а потом дообучать на этой разметке. Получается самоусиливающаяся петля.
Финальный совет: не гонитесь за совершенством
Самая большая ошибка — пытаться создать идеальную синтетику. Потратить месяцы на тонкую настройку генератора. Добавлять всё больше реалистичных деталей.
На практике закон убывающей отдачи срабатывает быстро. Первые 80% реалистичности дают 95% улучшения качества модели. Последние 20% реалистичности — это месяцы работы за 1-2% улучшения метрик.
Начните с простого. Возьмите 1-2 шаблона. Сгенерируйте 1000 документов. Обучите маленькую модель (3B параметров). Проверьте на реальных данных. Если работает — масштабируйте. Если нет — поймёте, что нужно улучшить.
Финансовые данные — самая охраняемая территория в мире ИИ. Синтетика — единственный способ её освоить. Не идеальный, но единственный.