Введение: Проблема ручного контроля

На работе одним из постоянных и важных процессов является проверка чеков на подлинность. Их поток достаточно большой (порядка нескольких сотен каждый день) и при этом каждый документ разбирается вручную - это может занимать до нескольких минут на один файл. На дистанции получается достаточно много. К тому же ручная проверка это медленно, дорого, и зачастую с ошибками из-за усталости аналитиков.
Потратив некоторое время на поиск готового решения нашей проблемы я нашел самописные гитхабные репы, которые максимум распознавали текст на картинке, даже не на PDF, платных решений я также не нашел, банки их не светят и естественно не продают.

Я ни разу не технарь (хотя по образованию инженер, ха), но благодаря опыту в антифроде я знал точно, что нужно проверять и как. Поэтому я подумал, что было бы прикольно (а в перспективе и полезно) сделать простенькую ML-ку и потихоньку ее обучать, пет проект который если стрельнет, то принесет пользу, а если не стрельнет - я потрачу время с удовольствием.
Я написал детальный промпт для курсора и вайбкодинг помог создать первую версию системы. А потом я итеративно улучшал ее на основе реальных данных.

Первая версия модели

Интерфейс загрузки чеков в модель, можно выбрать эталон для обучения (и загрузить свой чек из шавухи)
Интерфейс загрузки чеков в модель, можно выбрать эталон для обучения (и загрузить свой чек из шавухи)

Модель начала с анализа изображений на уровне пикселей. Логика была следующей: подделанный чек отличается от оригинала на уровне графики.
Модель считала:

JPEG артефакты (0-25 баллов)
Когда чек сохраняют много раз через JPEG, появляются артефакты сжатия - видны блоки пикселей с шумом. Реальный чек обычно сохраняется 1-2 раза.

Уровень сжатия (0-15 баллов) 
Модель проверяла коэффициент JPEG сжатия. Реальные чеки обычно хорошего качества, потому что их выгружают в виде файла или скриншота.

Пиксельные аномалии (0-20 баллов)
Модель искала пиксели, которые отличаются от соседних. Редактирование в Photoshop оставляет микроскопические артефакты - пиксели по краям текста выглядят странно, есть следы инструментов размытия или клонирования.

Нечеткие границы (0-12 баллов)
Если края чека обрезаны неровно, или видны полосы обработки - модель это ловит. Реальный чека имеет естественные края, подделка часто выглядит обрезанной в фотошопе.

Цветовая палитра (0-8 баллов)
Подделки часто имеют странное распределение цветов. Реальные чеки от одного источника похожи друг на друга - одна и та же цветовая схема, одинаковые шрифты.

Метаданные EXIF (0-3 балла)
Если метаданные отсутствуют - то это странно. Реальное фото или выгрузка содержит много информации, в том числе об устройстве, времени съёмки, геолокацию. Подделка часто сохранена из интернета - метаданных нет.

Размер файла (0-5 баллов) 
Очень маленькие файлы (10KB) или очень большие (50MB) выглядят подозрительно и нереалистично.

Всё это складывалось в риск скор систему, которая потом RandomForest интерпретировал в fraud/legitimate.
Результаты выглядели хорошо в тестах. Но на реальных данных система ломалась.Рандомный скриншот рабочего стола получал 41% риска - как подлинный чек. Выгрузка пдфки с банка показывала 72% - хотя это 100% подлинный документ.
Система не понимала, что она вообще анализирует. Она просто смотрела на пиксели и гадала.

<i>Определение реального чека из банка</i>
Определение реального чека из банка

Семантика

Я понял, что анализ только технических метрик не решает мою проблему совсем. Реальный чек всегда имеет определённую структуру текста. В нём всегда есть кириллица, ключевые слова, суммы в специфичном формате.Подделать пиксели легче, чем подделать правильный текст с правильной структурой. Система должна сначала понять: “ЭТО ЧЕК?” И только потом проверять технические детали.

Вторая версия модели

Переделал архитектуру. Теперь система работает поэтапно и состоит из следующих уровней:

Уровень 1: Это чек или выписка? Система сначала извлекает текст через OCR и проверяет три вещи.

<i>Расшифровка OCR</i>
Расшифровка OCR

Кириллица должна составлять минимум 15%

Почему 15%? Потому что даже если чек наполовину состоит из английских букв (название сети “METRO” или “HENDERSON”), в нём всё равно будут русские слова - адрес, “итого”, “спасибо”. 15% это порог ниже которого это точно не российский документ. Рандомный скриншот даст 0%. Иностранный чек даст 5-10%. Подлинный чек даст 70-90%.

Ключевые слова чека или выписки

Для чека система ищет: “итого”, “сумма”, “товар”, “цена”, “чек”, “кассовый”, “спасибо за покупку”, “дата”, “время”.
Для выписки ищет: “выписка”, “счёт”, “баланс”, “операция”, “платёж”, “бик”, “расчётный счёт”.
Если найдено менее 2 ключевых слов - это не чек. Рандомный скриншот даст 0 слов. Подлинный чек даст 5-8 слов.

Суммы в формате XXX.XX или XXX,XX

Если нет вообще ни одной суммы - это не чек. Система ищет паттерн: цифры, точка или запятая, ровно 2 цифры после. Подлинный чек будет иметь минимум 1-2 суммы (цена товара, итого).
Если что-то из этого не прошло - система сразу возвращает fraud 100% и останавливается. Никаких дальнейших проверок не идёт. Экономим ресурсы на явных фейках.

<i>Скриншот рабочего стола, работает</i>
Скриншот рабочего стола, работает

Уровень 2: Структура документа

Проверяет: есть ли даты, есть ли время, есть ли итоговая сумма, выглядит ли как реальный документ.Это вспомогательная проверка - есл�� структура странная, добавляется небольшой риск (0-4 балла). Но это не критично.

Уровень 3: Документ РФ или нет

Простая логика: если в документе есть доллары ($) или евро (€), но нет рублей (₽, РУБ, руб.) - это не наш чек. Fraud 100% и стоп.
Если есть рубли - ок, документ из России, продолжаем.

Уровень 4: Технические метрики (только если прошли уровни 1-3)

Теперь система считает всё: JPEG артефакты, сжатие, пиксельные аномалии, границы, цвета, размер файла. Но теперь эти метрики имеют контекст - система знает, что анализирует реальный документ.

Поправка 30% на технические метрики

Если документ прошёл уровни 1-3, это значит в нём есть правильный текст, правильная структура и рубли. Это уже даёт нам уверенность.

Техметрики у подлинного чека на смартфон всегда будут какими-то: JPEG артефакты от камеры, небольшое сжатие, пиксельный шум. Это нормально. Это не значит что чек подделан.

Без контекста система может подумать: “N балла риска - это может быть фрод”. С контекстом же: “если текст правильный и структура правильная, то N балла это просто артефакты от камеры, нормально”.

Поэтому ввел поправку в 30%: финальный риск = технический риск × 0.7 .
Это снижает технический риск на треть. Если модель посчитала 50 баллов, то с поправкой это 35 баллов. С семантикой подлинного чека - это интерпретируется как “подлинный”.

Техничка: как работает модель

Архитектура

Модель это RandomForestClassifier из scikit-learn. Она работает локально на компе - никакого облака, все данные у меня.

Когда загружается чек:

  1. Система сохраняет его в БД (SQLite)

  2. Извлекает текст через OCR (Tesseract)

  3. Считает 8 признаков (JPEG, сжатие, пиксели и т.д.)

  4. Отправляет признаки в RandomForest

  5. RandomForest возвращает ответ: фрод или не фрод

Модель сохраняется в двоичный файл scikit-learn, в нём лежат все параметры обученной модели.

Как модель учится

Когда пользователь загружает чек как “эталон” (100% подлинный), система:

  1. Сохраняет его в БД с меткой «эталон»

  2. Считает его 8 признаков

  3. Добавляет строку (X, y=0) в обучающий набор, где X это признаки, y это «подлинный»

Когда собирается 10-20 эталонов, система переобучает модель на всех собранных примерах.

RandomForest это ensemble - он состоит из 100 деревьев решений. Каждое дерево анализирует признаки независимо и выводит свой прогноз: фрод или не фрод. Когда примеров становится больше, деревья начинают видеть закономерности в данных.

Первое дерево может выучить правило: “если JPEG артефакты меньше чем 10 баллов И пиксельные аномалии меньше чем 5 баллов, то это подлинный чек”. Это дерево смотрит на качество изображения.

Второе дерево выучивает другое: “если размер файла больше 100KB И метаданные EXIF присутствуют, то это подлинный”. Это дерево смотрит на метаинформацию.

Третье дерево выучивает: “если уровень JPEG сжатия выше 50%, это признак fraud”. Это дерево смотрит на артефакты сжатия.

Когда модели нужно вывести ответ - все 100 деревьев голосуют. Каждое дерево говорит свой результат. Если 70 деревьев говорят “подлинный” и только 30 говорят “фрод” - финальный ответ модели будет “подлинный” с уверенностью 70%.Это работает потому что разные деревья “ловят” разные паттерны. Если у чека аномалия в пикселях, одно дерево это заметит. Если у чека подлинные метаданные, другое дерево это заметит. Вместе они дают более точный результат, чем каждое отдельно.

Почему качество растёт

Чем больше примеров - тем лучше деревья видят паттерны.
Разнообразие тоже важно. Если все эталоны это чеки Пятёрочки с одинаковыми артефактами - модель только на них и выучится. Новый чек из другого магазина может получить неправильный скор.
Поэтому нужны эталоны разных магазинов, разных форматов (PDF, JPG), разного качества. Чем разнообразнее данные - тем лучше модель обобщает.

Результаты: V1.0 vs V2.0
Данные основаны на наборе из 200 реальных чеков (которые я ручками поочередно выгружал из всех банков и подделок =))

Рандомный скриншот рабочего стола: 
• V1.0: 41% риска (система думала что может быть подлинный)
• V2.0: 100% ФРОД (не прошёл уровень 1 - нет кириллицы)

Подлинный кассовый чек (Пятёрочка, пдфка с телефона):
• V1.0: 35% риска (система колебалась)
• V2.0: 21% риска (прошёл все уровни 1-3, затем техметрики дали 30 баллов × 0.7 = 21 балл, итого 21% - подлинный)

Иностранный чек (на английском, в долларах):
• V1.0: 38% риска (система не знала, что иностранный чек не нужно смотреть)
• V2.0: 100% ФРОД (не прошёл уровень 1 - нет кириллицы, потом не прошёл уровень 3 - есть доллары без рублей)

Сбер (выписка, PDF):
• V1.0: 45% риска (система была неуверена)
• V2.0: 28% риска (прошёл уровень 1 - есть русский текст и ключевые слова “выписка”, “счёт”, “баланс”; уровень 3 - валюта RUB; техметрики дали 40 баллов × 0.7 = 28 баллов - подлинный)

На всём наборе из 200 примеров:
• V1.0: точность 62% (много ложных срабатываний)
• V2.0: точность 86% (почти все рандомные картинки отсеяны, подлинные чеки признаны подлинными)

<i>Итоги</i>
<i>Итоги</i>

Хорошо это или плохо - не знаю, время я провел с удовольствием и пользой, для дальнейшего развития нужно больше чеков и свободного времени :-)

Без призыва к действию =)
https://t.me/fraud_ops