Привет, Хабр! Пару лет назад мы с коллегами из Центра искусственного интеллекта СФУ искали способы набраться практического опыта в задачах компьютерного зрения. Одним из таких форматов оказались хакатоны — соревнования по решению ML-задач на реальных кейсах с жесткими дедлайнами.
За эти пару лет мы успели поучаствовать примерно в десяти хакатонах (Цифровой прорыв, Атомик Хак) и в половине из них доходили до призовых мест. Один из кейсов оказался особенно интересным из-за условий, в которых его пришлось решать. Это хакатон от Норникеля под названием «Интеллектуальные горизонты»

Выбор кейса
Организаторы подготовили три кейса на выбор, среди которых были «Флотомашина времени», «Мультимодальные RAG-модели» и кейс под названием «Грязные дела», который мы и выбрали.
Задача из нашего кейса звучала так «Разработка метода определения степени загрязнения кадра, для обеспечения надежной работы камер на производстве, а также роботов-курьеров и автономных транспортных средств».

По сути, это задача бинарной сегментации изображений (Binary Image Segmentation).
Подробности о том, что это за камера (скорее всего эндоскоп) и условия ее использования не уточнялись, но проблема ясна: подобные загрязнения могут мешать другим моделям ИИ анализировать видеопоток, и нужен был фильтр, который бы говорил, где грязное пятно и на какую область кадра не обращать внимания.
Проблема системы валидации решений
Основная причина, по которой этот хакатон стал для нас необычным — это проблема системы проверки решений. Дело в том, что в обычно для проверки обычно участник отправляет на платформу submission-файл с предсказанными метками для тестовой выборки, а система сверяет их с правильными и выдает score по какой-нибудь метрике (в нашем случае это была mIoU), что и определяет позицию команды на лидерборде.
Однако ключевое отличие этого хакатона заключалось в следующем: нужно было отправить архив с метаданными, скриптом инференса и весами модели, который уже разворачивался и выполнялся на стороне платформы. С теоретической точки зрения это звучит круто, ведь это воспроизводимость результата, но на практике почти все, что отправлялось на сервер не собиралось и не запускалось на стороне платформы, а значит не работало.

И вот тут важная деталь: у платформы был только один фиксированный docker-образ с типовым набором библиотек: ultralytics, torch, torchvision и другое типичное навесное.
В результате стабильно работало фактически только одно решение — бейзлайн от организаторов, основанный на применении YOLOv11 для задачи сегментации. Любые отклонения от предложенного шаблона (другая модель или иная сборка окружения) почти гарантированно приводили к ошибке на стороне сервера.
В итоге все команды оказались в одинаково жестких условиях: менять модель было нельзя, зато оставалась возможность влиять на процесс обучения, а также на пред‑ и постобработку данных. Именно на этом мы и решили сосредоточиться.
Работа с данными в условиях фиксированной модели
Если модель нельзя изменять, то ее можно качественнее обучить и один из самых простых способов — дополнить данные синтетикой.
Всего организаторы дали 250 реальных + 147 синтетических изображений с разметкой контурами, и формально этого достаточно, чтобы дообучить YOLO. Но проблема была в том, что все команды делали ровно то же самое.
Мы решили сделать синтетику, которая похожа на то, что происходит с линзой — размытия, капли, грязевые артефакты и т. д. Поэтому мы написали небольшой скрипт, который:
берет исходное изображение и маску;
случайно применяет набор трансформаций: повороты, флипы, яркость и контраст;
применяет raindrop к оригиналу (с альфой) и к маске.
Таким образом, мы сгенерировали еще 1000 изображений для обучения. Мы не были единственными, кто занимался синтетикой — идея лежала на поверхности. Тем более, что организаторы вместе с бейзлайном приложили похожий, хоть и более простой, скрипт.
Обучив модель на всех данных мы выиграли еще пару пунктов метрики, но все равно находились примерно на уровне большинства решений судя по обсуждениям в чате.
Оптимизация пред- и постобработки
Достаточно очевидно, что подбор гиперпараметров может повлиять на результат работы модели, однако в рамках хакатона не было на это времени.
Рассматривая выводы модели, капитану команды Константину Кожину пришла в голову гипотеза: маски, которые модель выдает на выходе, часто получаются чуть меньше, чем истинные контуры. Мы попробовали это компенсировать тем, что искусственно расширили контуры маски на постобработке, отправили решение на проверку и… ура: метрика стала выше и на обучающей, и на валидационной выборке, и на лидерборде.
Было решено дальше копать в этом направлении, и мы выбрали 5 параметров, которые подбирали, а именно:
яркость,
контраст,
шумоподавление (cv2.bilateralFilter),
масштабирование,
сглаживание контуров.
Все параметры — это числовые коэффициенты, и задача состоит в том, чтобы их подобрать так, чтобы максимизировать метрику модели на обучающих данных. Это задача оптимизации, причем «черного ящика»: мы не можем ни функциональную зависимость выявить, ни все варианты перебрать — просто не успеваем. Тогда у нас остается вариант использовать эволюционные алгоритмы, а именно дифференциальную эволюцию.
Если упростить, то дифференциальная эволюция работает так: мы заводим популяцию наборов параметров, а дальше начинаем их скрещивать и мутировать. Те параметры, которые дают метрику лучше, остаются, а другие отбрасываются. И так несколько поколений подряд.
Общая схема нашего решения состояла в том, чтобы получить обученную модель, а после подобрать оптимальные параметры пред‑ и постобработки с помощью дифференциальной эволюции, и уже с этими параметрами модель выполняет инференс.

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

Возник логичный вопрос: будет ли этот подход работать на тестовых данных организаторов? Как оказалось — да. По финальному скору мы вышли на первое место в лидерборде.

В итоговом зачете, где учитывались не только метрики, но и защита решения, а также качество оформления, мы заняли второе место.

За время участия в подобных соревнованиях мы видели разное: удачные и неудачные организации, сильные и слабые постановки задач, смену условий прямо по ходу соревнований. Но этот хакатон оставил у нас самые позитивные впечатления не только из-за полученного командой результата, а прежде всего из-за необычных условий, которые заставили команды искать нестандартные инженерные решения, а не просто перебирать модели.
Спасибо, что дочитали! Удачи в ваших проектах и побольше действительно интересных задач. Если вас интересуют технические подробности, их можно найти в репозитории нашего решения.
Участники команды
Константин Кожин — Капитан команды, Data Scientist;
Павел Шерстнев — ML-Engineer;
Владислава Жуковская — Дизайнер;
Софья Голубовская — Дизайнер;
Алина Нуриманова — Data Analyst.
