Введение: Проблема ручного контроля
На работе одним из постоянных и важных процессов является проверка чеков на подлинность. Их поток достаточно большой (порядка нескольких сотен каждый день) и при этом каждый документ разбирается вручную - это может занимать до нескольких минут на один файл. На дистанции получается достаточно много. К тому же ручная проверка это медленно, дорого, и зачастую с ошибками из-за усталости аналитиков.
Потратив некоторое время на поиск готового решения нашей проблемы я нашел самописные гитхабные репы, которые максимум распознавали текст на картинке, даже не на 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% подлинный документ.
Система не понимала, что она вообще анализирует. Она просто смотрела на пиксели и гадала.

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

Кириллица должна составлять минимум 15%
Почему 15%? Потому что даже если чек наполовину состоит из английских букв (название сети “METRO” или “HENDERSON”), в нём всё равно будут русские слова - адрес, “итого”, “спасибо”. 15% это порог ниже которого это точно не российский документ. Рандомный скриншот даст 0%. Иностранный чек даст 5-10%. Подлинный чек даст 70-90%.
Ключевые слова чека или выписки
Для чека система ищет: “итого”, “сумма”, “товар”, “цена”, “чек”, “кассовый”, “спасибо за покупку”, “дата”, “время”.
Для выписки ищет: “выписка”, “счёт”, “баланс”, “операция”, “платёж”, “бик”, “расчётный счёт”.
Если найдено менее 2 ключевых слов - это не чек. Рандомный скриншот даст 0 слов. Подлинный чек даст 5-8 слов.
Суммы в формате XXX.XX или XXX,XX
Если нет вообще ни одной суммы - это не чек. Система ищет паттерн: цифры, точка или запятая, ровно 2 цифры после. Подлинный чек будет иметь минимум 1-2 суммы (цена товара, итого).
Если что-то из этого не прошло - система сразу возвращает fraud 100% и останавливается. Никаких дальнейших проверок не идёт. Экономим ресурсы на явных фейках.

Уровень 2: Структура документа
Проверяет: есть ли даты, есть ли время, есть ли итоговая сумма, выглядит ли как реальный документ.Это вспомогательная проверка - есл�� структура странная, добавляется небольшой риск (0-4 балла). Но это не критично.
Уровень 3: Документ РФ или нет
Простая логика: если в документе есть доллары ($) или евро (€), но нет рублей (₽, РУБ, руб.) - это не наш чек. Fraud 100% и стоп.
Если есть рубли - ок, документ из России, продолжаем.
Уровень 4: Технические метрики (только если прошли уровни 1-3)
Теперь система считает всё: JPEG артефакты, сжатие, пиксельные аномалии, границы, цвета, размер файла. Но теперь эти метрики имеют контекст - система знает, что анализирует реальный документ.
Поправка 30% на технические метрики
Если документ прошёл уровни 1-3, это значит в нём есть правильный текст, правильная структура и рубли. Это уже даёт нам уверенность.
Техметрики у подлинного чека на смартфон всегда будут какими-то: JPEG артефакты от камеры, небольшое сжатие, пиксельный шум. Это нормально. Это не значит что чек подделан.
Без контекста система может подумать: “N балла риска - это может быть фрод”. С контекстом же: “если текст правильный и структура правильная, то N балла это просто артефакты от камеры, нормально”.
Поэтому ввел поправку в 30%: финальный риск = технический риск × 0.7 .
Это снижает технический риск на треть. Если модель посчитала 50 баллов, то с поправкой это 35 баллов. С семантикой подлинного чека - это интерпретируется как “подлинный”.
Техничка: как работает модель
Архитектура
Модель это RandomForestClassifier из scikit-learn. Она работает локально на компе - никакого облака, все данные у меня.
Когда загружается чек:
Система сохраняет его в БД (SQLite)
Извлекает текст через OCR (Tesseract)
Считает 8 признаков (JPEG, сжатие, пиксели и т.д.)
Отправляет признаки в RandomForest
RandomForest возвращает ответ: фрод или не фрод
Модель сохраняется в двоичный файл scikit-learn, в нём лежат все параметры обученной модели.
Как модель учится
Когда пользователь загружает чек как “эталон” (100% подлинный), система:
Сохраняет его в БД с меткой «эталон»
Считает его 8 признаков
Добавляет строку (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% (почти все рандомные картинки отсеяны, подлинные чеки признаны подлинными)

Хорошо это или плохо - не знаю, время я провел с удовольствием и пользой, для дальнейшего развития нужно больше чеков и свободного времени :-)
Без призыва к действию =)
https://t.me/fraud_ops
