Привет! Меня зовут Владимир Морозов, я старший дата-сайентист в отделе автоматической модерации Авито.
Раньше мы блокировали объявления, которые нарушают правила публикации, а теперь исправляем — с помощью ML-системы. Так мы сохраняем количество контента, сокращаем стоимость модерации и улучшаем пользовательский опыт.
В статье подробно расскажу обо всех этапах внедрения новой ML-механики: от идеи и исследования подходов до оптимизации нейронок и вывода в продакшен.

О чём статья:
Дано: зачем Авито модерация изображений
Проблемы классической модерации: долго, дорого, неудобно
Идея №1: не удалять, а блюрить изображения
Планируем архитектуру и обучаем ML-модель
Идея №2: не блюрить, а inpainting
Дано: зачем Авито модерация изображений
Мы часто сталкиваемся с нарушениями правил публикации. Чаще всего люди оставляют ссылки на другие площадки или свои контакты на фото, но могут быть и случаи несоблюдения законодательства РФ. Основная задача модерации — находить эти нарушения.
До нашего проекта по автомодерации в Авито применяли три сценария для контента:
уверены, что всё в порядке — публиковали фото;
не уверены на 100% — направляли на ручную модерацию;
уверены, что есть нарушения — блокировали или просили заменить фото в зависимости от того, насколько критичный случай.
Проблемы классической модерации: долго, дорого, неудобно
На Авито публикуют много объявлений, поэтому модерация для нас очень важна. Но у описанных выше сценариев есть минусы:
ручная модерация стоит дорого. Её нужно качественно валидировать, а ещё она может длиться часами. Автомодерация же в среднем занимает секунды;
блокировка и отклонения уменьшают количество контента. Не все публикуют объявления с исправлениями, а это проблема: чем меньше публикаций на площадке, тем хуже опыт пользователей.
Идея №1: не удалять, а блюрить изображения
В идеале нам бы хотелось, чтобы модерация работала так: если в публикации есть нарушения, то мы автоматически их исправляем и публикуем контент.

Изначально мы рассматривали самый простой способ — удалять фото с нарушением. Но в таком случае возникает проблема: если в объявлении всего одна фотография, то мы теряем всю визуальную часть публикации, что очень плохо.

Поэтому мы стали думать, как удалять не всю фотографию, а только нарушения на ней. Например, такого результата можно достичь с помощью блюра. Так мы сохраним визуальную информацию и ничего не заблокируем — все довольны.

Планируем архитектуру и обучаем ML-модель
Мы решили начать с контактов на фото как с некритичной причины. Часто люди даже не задумываются, что это нарушение правил публикации. На Авито примерно 1% фотографий содержит контакты — это очень много.
В одном объявлении в среднем пять изображений. Если переводить это в нагрузку на модерационные сервисы, то в пиковые моменты она достигает 10 000 RPS. В среднем — 1 500 RPS, а мы должны модерировать где-то за секунду, из этого вытекают ограничения.
Шаг 1. Планируем архитектуру. С одной стороны, наша система должна быть быстрой, но с другой, у нас не так уж много фотографий с контактами. Поэтому мы выбрали решение из двух частей:
Классификатор отсеивает большинство потока — фотографии, где нет контактов. Он должен быть довольно быстрым, потому что у нас жёсткие требования по скорости автомодерации. Мы прикинули, что предпроцессинг и сам инференс модели должны занимать меньше 100 миллисекунд. Кроме того, классификатор не должен ничего пропускать. Метрика, на которую будем смотреть в первую очередь — это recall.
Детектор выделяет боксы контактов, к которым мы применяем блюр. Он уже может быть не такой быстрый, потому что классификатор срезал основной поток. Здесь смотрим на метрику precision.
Шаг 2. Собираем данные для обучения модели. На Авито много объявлений, поэтому проблем с данными не было. Для обучения мы взяли фотографии с нарушениями и без. Фотографии с нарушениями разметили на контакты: QR-коды, логины, телефоны и email. Сразу разметили и боксы, чтобы потом использовать для детектора. Из публикаций без нарушений получили синтетику: сгенерировали телефоны, email и другие контакты.

Всего для классификатора собрали 33 000 фотографий, для детектора меньше: там более сложная разметка. Синтетики взяли 5 000.

Бывают случаи, где люди прям постарались. Такое мы пытаемся отлавливать классификатором и отправлять на ручную модерацию. Детектор, скорее всего, здесь ничего не найдёт.

Шаг 3. Обучаем модель. На наших данных для классификатора свёртки показали себя лучше по качеству и скорости, поэтому мы остановились на EfficientNetV2 версии small. По метрикам у неё лучший precision и AUC-ROC.
У нас был опыт внедрения детекторов на основе YOLO. Поэтому для детекции взяли последнюю версию этой архитектуры и обучили на наших данных.

В итоге архитектура ML-сервиса получилась такой:

В классификатор поступает около 1 500 RPS. Он срезает практически всё, а детектор ещё где-то половину: он не всегда находит контакты — некоторые кейсы уходят на ручную модерацию или в блок.
Ускоряем работу сервиса
Переходим к внедрению моделей и реализации. Конечно, всегда можно закинуться миллионами GPU, но у нас их не так много, поэтому хотелось это всё ускорить.
Среднестатистический ML-сервис, который работает с картинками, выглядит как-то так:

Сначала есть какой-то CacheStorage, потом блок, внутри которого происходит обработка изображения, потом снова CacheStorage. Подробнее про каждый блок в нашем случае:
CacheStorage. Хранит предсказания по Image_ID, чтобы не проверять дубли в фотографиях. Они встречаются часто: кто-то загрузил две одинаковых картинки или немного поменял текст в описании, что также триггерит модерацию.
Aqueduct (акведук) — это open-source библиотека Авито для эффективного серверного инференса ML-моделей. Она позволяет с минимальными усилиями добиться лёгкого батчевания. Подробнее про Aqueduct рассказал мой коллега Иван Санин в youtube-видео «Разгоняем ML в проде».

Если говорить просто, Aqueduct — это очередь, которая накапливает таски и обрабатывает их батчами в хендлерах, всего их четыре:
DownloadHandler — простой HTTPX-клиент, который загружает изображения по url.
PreprocessorHandler и PostprocessorHandler. Во время обучения для повышения робастности моделей часто используют аугментацию. Самое базовое решение — взять трансформации из TorchVision. Но у него «под капотом» Pillow, который по скорости значительно проигрывает преобразованиям из OpenCV, особенно на больших разрешениях. Поэтому мы стараемся использовать другие библиотеки: на этапе обучения это Albumentations, а в продакшене — OpenCV.

Обратите внимание: если вы обучали модель с помощью трансформаций из TorchVision, а в продакшене используете OpenCV, то могут возникнуть проблемы. На картинке ниже видно, что одно и то же преобразование с использованием Pillow и OpenCV даёт разный результат. Для каких-то моделей это может быть не страшно, для других — добавляет существенный шум. Поэтому будьте аккуратнее.

ModelHandler. Обычно мы обучаем на PyTorch/Lighting, но в продакшене нам не нужны многие операции из торча, которые могут тормозить инференс. Так что в сервисах мы конвертируем модели для более быстрой работы.

Самое большое ускорение получается с TensorRT, но его невозможно использовать для всех случаев, так же как и ONNX. Например, когда у вашей модели есть кастомные слои или операции, которые сильно зависят от входных данных и обрабатываются классическими функциями Python. Дело в том, что ONNX и TensorRT могут работать только со статичными графами.
Разберём небольшой пример:

Слева — модель, которая при конвертации в ONNX будет выдавать статичный граф, но с фиксированными походами по if. Значит, при другой природе данных мы всё равно пойдём в ту ветку, по которой пошли при конвертации.
Чтобы этого избежать, можно заменить операции Python на маску из торча. В таком случае уже нет ветвления и граф получается полностью статичным. Это вариант справа.
То есть конвертировать в ONNX и TensorRT возможно, но на это нужно потратить время, которое не всегда есть. Поэтому иногда проще сразу остановиться на TorchScript, особенно на старте.
YOLO и EfficientNetV2, которые мы берём для задачи блюра контактов, легко конвертируются в TensorRT, что мы и делаем.
В итоге получили адекватное значение задержки. В классификаторе предпроцессинг и инференс модели занимают около 100 миллисекунд, со скачиванием выходит 240. В детекторе задержка ещё больше, но это не критично. Блок блюра занимает больше всего времени, поскольку занимается обновлением фотографий в публикации.

На этом этапе мы довольны результатом: было много срабатываний и мало жалоб. Значительно срезался поток, который шёл на ручную модерацию.

Идея №2: не блюрить, а inpainting
После успешного запуска на контактах мы решили работать и с другими отклонениями. Например, с матом или триггерными словами, которые вводят пользователей в заблуждение. Их находим с помощью OCR — Optical Character Recognition, распознавание текста на фотографии. Нарушение авторских прав выявляем с помощью детектора логотипов.
Все эти модели прекрасно ложатся в схему блюра, так как они занимаются детекцией и могут выдавать боксы для блюра нарушения.

У коллег в Авито Услугах была проблема с сегментом «Частный мастер». Некоторые компании маскируются под частников, чем вводят пользователей в заблуждение. Поэтому мы решили убирать с изображений подобный текст с помощью блюра. На тесте результат получился так себе:

После рефлексии мы вспомнили, что есть такая сфера компьютерного зрения, которая восстанавливает изображения — inpainting. Это когда на вход подаётся фотография, на которой маска закрывает часть пикселей. Задача модели — восстановить первоначальное изображение. Если полностью маскировать объект — это выглядит так, будто модель его удаляет. Попробовали на наших данных — получилось хорошо:


Внедряем inpainting. Мы изучили GitHub, нас заинтересовал репозиторий IOPaint с зоопарком моделей для редактирования фото. Мы составили список потенциальных кандидатов и отфильтровали их по качеству из paperwithcode. В итоге остановились на трёх моделях: LaMa, ZITS и MAT.
Шаг 1. Выбираем модель для инпейнтинга. Любую метрику для задачи inpainting можно взломать, поэтому часто обращают внимание на совокупность разных метрик и глазами отсматривают результат. Мы решили взять метрики: PSNR, SSIM, FID и LPIPS. Все они работают на парах: сравнивают оригинальное и обработанное изображение.
PSNR — peak signal-to-noise ratio. Чтобы получить PSNR, сначала считают MSE попиксельно. Затем нужно взять максимальную активацию пикселя в оригинальном изображении, поделить на MSE и вычислить из этого логарифм. Чем больше MSE, тем меньше PSNR, а значит, мы должны растить эту метрику.

PSNR не берёт во внимание близость между пикселями, которая важна для восприятия глазом человека. Для этого нужна следующая метрика:
SSIM — structure similarity, учитывает взаимосвязь близких пикселей.

SSIM смотрит на дисперсию — в окне проходит по всей фотографии. Получается чуть лучше, но и SSIM можно взломать. Есть много примеров, где значения получаются одинаковыми.
FID — frechet inception distance. Метрика учитывает глобальные признаки изображения, которые извлекают с помощью нейронки. Сейчас для этого в основном применяют модель CLIP. С её помощью получают векторное представление оригинального изображения, после чего считают расстояние между гауссианами.

FID учитывает разницу только на одном слое, а нам бы хотелось видеть изменения относительно первоначального изображения на признаках разного размера. Для этого используем финальную метрику:
LPIPS — learned perceptual image patch similarity. Чтобы получить эту метрику, нужно взять признаки с разных слоёв изображения, посмотреть между ними разницу и усреднить.

Но и с этой метрикой есть много примеров, где фото разного качества, при этом LPIPS одинаковая. Идеальной метрики нет, поэтому смотрим на результаты в совокупности.
Новая модель ZITS++ показала себя лучше всех. Потом идёт ZITS, чуть хуже LaMa, а MAT сильно отстаёт. На картинках с рандомной маской видно, что MAT местами создаёт странные артефакты. LaMa в целом работает нормально, хотя может немного размывать фото.

Однако метрик качества нам недостаточно, модель должна быть быстрой, так что мы сравнили и производительность. Здесь уже LaMa гораздо быстрее других, а при небольшом отставании по метрикам от ZITS она вырывается вперёд. Кроме того, у LaMa более качественный GitHub-репозиторий и российские авторы — мы решили поддержать соотечественников.

Шаг 2. Оптимизируем выбранную модель. Внутри LaMa используются быстрые свёртки Фурье, которые нужны, чтобы увеличить receptive field. Это количество пикселей в первоначальном изображении, на которое смотрит внутренний слой нейронки.

Преобразования Фурье — нетипичная операция для нейронок, которая плохо конвертируется в ONNX и TensorRT. Хотя при такой конвертации граф получается динамическим и без Фурье. Поэтому по схеме из оптимизаций моделей мы остановились на TorchScript, получили небольшой бесплатный прирост. Для изображения 1920×1440 и GPU A100 достигли таких значений:

Что получили в итоге
Мы перевели на inpainting часть причин, по которым раньше отклоняли или блокировали объявления.

Нагрузка немного выросла: в этой системе добавляется OCR, которая в среднем выдаёт 16 RPS. Inpainting тоже работает немного дольше, чем блюр.

В результате ML-система помогает не терять количество контента — убирает нарушения без потери качества фотографий. Вот примеры, где изображение не заблюрено, а восстановлено:



Конечно, бывают случаи, где что-то идёт не по плану: здесь модель не смогла выделить весь текст:

Это связано с тем, что бокс не полностью попал на буквы: модель не заинпейнтила их фрагменты. Проблему можно решить с помощью предпроцессинга с сегментацией.
Вместо стандартного бокса от модели нужно подать его как промпт в сегментатор, который более качественно выделит объект для инпейнтинга. Для этого можно использовать SAM — Segmentation Anything Model. В таком случае мы сначала подаём бокс, затем сегментируем его и отдаём в LaMa уже более правильную маску, которая удаляет объект. Это решение мы внедряем в Авито прямо сейчас.
Резюме
Классические методы модерации обходится дорого, а простой бан фотографий с нарушениями снижает количество контента. Чтобы улучшить модерацию, мы внедрили ML-модель, которая блюрит нарушения на фото.
Блюр может добавлять дефекты на изображения, поэтому мы перешли на inpainting. Результат инпейнтинга оказался лучше. Теперь мы убираем нарушения без потери качества фотографий и количества контента, а значит, и денег.
Кроме таких автоисправлений, у нас в команде есть другие интересные проекты. Например, блокировка дублей, внедрение LLM, few-shot подходы, модерация сложных доменов, таких как видео, и всё это — в высоконагруженной системе с большим влиянием на всю компанию.
Спасибо за уделённое время! На любые вопросы об опыте в модерации я с радостью отвечу в комментариях или личке:
Telegram: @vladitm
LinkedIn: Vladimir Morozov
Больше о том, какие задачи решают инженеры Авито, — на нашем сайте и в телеграм-канале AvitoTech. А вот здесь — свежие вакансии в нашу команду.
Комментарии (6)
PereslavlFoto
16.02.2025 17:25Некоторые компании маскируются под частников, чем вводят пользователей в заблуждение.
Более того, они требуют вести переговоры в вотсапе или по телефону, для этого сразу же говорят или пишут (в чате) свой телефон. Вы будете наказывать таких продавцов?
sgjurano
16.02.2025 17:25Отличная статья, спасибо! А как пользователи реагируют на редактирование их контента? Не было ли негативной реакции?
vladitmor Автор
16.02.2025 17:25Спасибо!
В большинстве случаев пользователи либо не замечают редактирования, либо относятся к нему спокойно. Иногда, конечно, кто-то пытается обойти правило, но в целом, когда выбор стоит между публикацией с небольшими правками или отклонением, большинство понимает ситуацию и предпочитает первый вариант)
kiff2007200
16.02.2025 17:25А когда собираетесь бороться с проблемой, когда в обьявлении одна цена, а внутри оказывается совсем другая) Или для этого тоже нужна LLM?)
PereslavlFoto
16.02.2025 17:25Выше уже звучал этот вопрос. Можно подавать жалобы на такие объявления, однако владельцы сайта никак не реагируют на такие жалобы. Им же выгодна движуха.
PereslavlFoto
У вас полно объявлений с фальшивой ценой, однако вы нисколько их не исправляете, даже после жалоб.