Найти и выбрать квартиру в современном мире — что может быть проще? Берёшь смартфон, скачиваешь приложение и находишь подходящий вариант. Так же просто разместить объявление о продаже или аренде недвижимости. Пользователи смотрят десятки квартир в поисках подходящей — качество ремонта для них очень важно. Вот было бы классно, если бы существовал фильтр, который может правдиво оценить новизну ремонта и отсортировать…
Меня зовут Ирина Говорова, и сейчас я расскажу, как во время моей стажировки в Циан наша команда разработала фильтр «бабушкин ремонт», способный распознавать и классифицировать фотографии помещений.
От собеседования на стажировку до реального проекта
Хочу сделать небольшое лирическое отступление и рассказать, как я вообще попала в Циан. Надеюсь, что моя история поможет студентам и начинающим специалистам.
Всё началось с того, что в мае 2021 года на одном из ресурсов ODS (Open Data Science) я увидела объявление о наборе стажёров в Циан и отправила заявку. А дальше обычная история: от «Google Форм» и тестирования до собеседования-интервью, где также было тестовое задание.
В общем, это стандартные тесты, по содержанию похожие на тесты для стажёров в других компаниях. Там были вопросы по базовым терминам: что такое случайный лес, как работает backpropagation и т. д. На них сможет ответить любой, кто целенаправленно изучает data science.
На тот момент я училась на первом курсе магистерской программы «Науки о данных» ВШЭ. Именно там на одном из курсов я впервые погрузилась в deep learning, и мне понравилась эта область. Ранее я проходила различные онлайн-курсы по классическому ML, участвовала в хакатоне в треке Data Science.
Примерно через неделю после собеседования пришёл ответ, что меня приняли на должность стажёра. Ура! Уже в июне я приступила к работе.
Поначалу было тяжело. На момент прихода в проект у меня было немного опыта в глубоком обучении. За короткое время в ходе работы я узнала море новой информации, особенно технических деталей. Но главное, что у меня было и есть по сей день, — это вовлеченность в проект, поэтому все сложности делали работу только интереснее. Мы с коллегами очень гордимся нашим детищем, а теперь я даже пишу о нём статью.
Что мы должны были разработать? Описание задачи
Выхожу на работу — теперь я в команде разработчиков Циан. Наша задача — обучить нейронную сеть, которая будет фильтровать объявления, публикуемые пользователями. Тут легче объяснить на примере.
Допустим, клиент выкладывает объявление с фотографиями квартиры. Наш фильтр в режиме реального времени анализирует прикрепленные изображения, чтобы определить бабушкин ремонт. Самый яркий признак бабушкиного ремонта — старый ковёр на стене спальни (об остальных критериях расскажу позже). Если объявление имеет достоверные теги, оно получает метку «не бабушкин ремонт».
Если обобщать, то бабушкин ремонт выглядит в нашем понимании примерно так:
Именно такие ремонты фильтр должен называть бабушкиными. Благодаря нашему фильтру вам будет проще найти квартиру с новым и хорошим ремонтом или наоборот — не переплачивать за чужой ремонт при покупке жилья.
Давайте покажу, как этот фильтр работает с точки зрения юзера, и постараюсь объяснить, как он устроен изнутри.
Бабушкин ремонт на практике: как работает фильтр в приложении на iOS и Android
Первое, что видит пользователь, когда заходит в поиск Циан, — большая карта, в правом углу которой есть значок настроек отображения. Именно там можно проставить интересующие параметры и выбрать фильтр.
Для примера поищем в Санкт-Петербурге и окрестностях квартиры стоимостью 3–7 млн рублей. Не забываем выбрать фильтр «бабушкин ремонт».
Результаты всей ленты показать не получится, но часть из них, которая поместилась в монитор, выглядит так:
Вот она, работа фильтра! На фотографиях мы видим непрезентабельную отделку, видавшую виды мебель и тоску — полный набор критериев старой квартиры.
Теперь главное, чтобы в категории «хороший ремонт» не было фотографий со старыми коврами и ветхой мебелью. Смотрим:
Отлично! Среди объявлений, которые выдал поисковик, нет фотографий с бабушкиным ремонтом.
Итак, мы эмпирическим путём выяснили, что фильтр работает должным образом.
Архитектура и фреймворк фильтра
Под руководством ментора и тимлида я занималась как раз сборкой датасета и обучением нейросети. Творческие задачи мне по душе, и я обязательно расскажу об их реализации. Но сначала поговорим об используемых технологиях.
В самом начале работы над проектом надо было определиться, какую архитектуру мы будем использовать. Решили остановиться на двух семействах архитектур: EfficientNet и ResNet. Они популярны на рынке, довольно «лёгкие» и неплохо себя зарекомендовали.
Мы выбрали свёрточные сети EfficientNet, потому что на наших данных в тесте они показывали лучшие результаты по сравнению с ResNet. По показателям экономии ресурсов и скорости на инференсе нам подходила EfficientNet версии B4. На ней и остановились.
В качестве фреймворка мы использовали PyTorch, потому что он используется в моей команде для других проектов, и я его знала. Также он оказался весьма удобным при конвертировании модели в Open Neural Network eXchange (ONNX) — onnxruntime через torch.onnx.export.
Конвертация понадобилась потому, что нам потребовался быстрый инференс на CPU, а обучение мы построили на PyTorch на GPU. Теперь мы могли использовать OpenVINO, который максимизировал производительность сетки на процессорах Intel, установленных в наших машинах. В OpenVINO нельзя напрямую конвертировать PyTorch-модель, зато можно конвертировать из PyTorch в ONNX, а уж из него — в OpenVINO. Кстати, сама конвертация модели в OpenVINO — крайне простой процесс (зато сколько ресурсов позволяет сэкономить!).
На этапе конвертации и инференса OpenVINO-модели возникали сложности, которые оказались для меня совершенно новыми. Вместе с первыми попытками выпуска модели в прод мы выяснили, что используемый метод инференса .infer() работает только с одним потоком и только с батчем фиксированного размера. Каждый запрос (а это либо отдельная картинка в батче картинок, либо отдельный тред в мультитреде) должен был обрабатываться в своём экземпляре реквеста. Для адекватной работы модели с батчами различных размеров (или вообще с мультитредом) требовалось настроить асинхронный инференс.
Асинхронный инференс
from openvino.inference_engine import IECore, StatusCode
ie = IECore()
net = self.ie.read_network(model='model.xml', weights='model.bin’)
net.batch_size = 1
exec_net = self.ie.load_network(network=net, device_name='CPU', num_requests=20)
input_name = next(iter(exec_net.input_info))
output_name = next(iter(exec_net.outputs))
requests_dict = {}
for i in range(len(transformed_images)):
request_id = self.exec_net.get_idle_request_id()
exec_net.requests[request_id].async_infer({input_name:input_images[i].unsqueeze(0)})
requests_dict[i] = request_id
done_requests = set()
while True:
for i in range(len(input_images)):
infer_status = self.exec_net.requests[requests_dict[i]].wait(0)
if infer_status != StatusCode.OK or i in done_requests:
continue
prediction_openvino=exec_net.requests[requests_dict[i]].output_blobs[output_name].buffer
done_requsts.add(i)
if len(done_requests) == len(input_images):
break
Расшифровка. При загрузке модели из файлов с помощью load_network в параметрах указывается num_requests, который показывает, сколько изображений одновременно может процесситься моделью. Затем для каждой картинки из батча определяется свой номер потока, в котором она будет процесситься через get_idle_request_id(). Отдельно в потоках запускается асинхронный инференс картинок с помощью async_infer(). Как только аутпуты потоков готовы, просходит загрузка в итоговый список с выходными данными для всего батча.
С учётом того, что я впервые пользовалась фреймворком OpenVINO, в само́м Циан до нас его никто не юзал, и отсутствовали какие-либо наработки, — на мой взгляд, получилось весьма неплохо.
Выборка, «Яндекс.Толока» и аугментации. Формирование датасета
Архитектура и фреймворки — это хорошо. Но без грамотно размеченных данных, аугментаций, препроцессинга изображений, сбалансированной выборки и должного обучения нейросети — в общем, без качественной предварительной работы — результаты будут соответствующие.
Я занималась как раз сборкой датасета и обучением нейронки, так что могу кое-что рассказать о подводных камнях в потоке данных. Приступим!
1. Откуда брать картинки для датасета
Каждая конкретная задача в некотором смысле уникальна, и наша — не исключение. Казалось бы, найди себе готовый и размеченный датасет на каком-нибудь Kaggle или Dataset Search и пользуйся на здоровье, но нет. Не нашлось ни одного агрегатора, где отыскался бы датасет по типу CelebA с полностью готовыми изображениями бабушкиных квартир и разметкой. Пришлось всё делать ручками.
Первой идеей сбора изображений было парсить картинки по ключевым запросам: «бабушкин ремонт», «хороший ремонт», «евроремонт» — в общем, все возможные виды ремонтов. Увы, адекватных результатов этот метод не дал, потому как действительно релевантная выдача была в радиусе первых трёх десятков картинок, а дальше — сплошной мусор.
Но нет худа без добра. Мы нашли по ключевым запросам в поисковиках и самостоятельно отобрали примеры и уже на их основе отправились искать похожие фотографии в базе Циан. Вот уж действительно, где ещё искать фотографии самых разных интерьеров, если не тут? И наконец на нас посыпались куда более релевантные результаты.
Однако по-прежнему не было гарантии, что даже из небольшой выборки (допустим, около сотни фотографий) все изображения будут подходящими. Фотографии всё равно пришлось отсматривать вручную.
2. Толокисты и разметка данных
Для начала нужно было определиться с набором критериев, согласно которым мы могли бы классифицировать изображения как «бабушкин ремонт» или «не бабушкин ремонт».
Плюс ещё такой момент: при размещении объявления пользователь, очевидно, публикует не только фотографии гостиной или спальни. Как правило, публикации о продаже или аренде содержат набор фотографий: от холла до туалета и кладовой.
Но, например, та же кладовая зачастую не имеет никакой дизайнерской отделки. К тому же такое помещение есть не в каждой квартире. Если обрабатывать все фотографии квартиры подряд, это меняет вероятность бабушкиного ремонта. Иными словами, использование неинформативных фотографий может ухудшить результаты обучения и работу фильтра в дальнейшем. Мы решили выделить четыре типа комнат: гостиную, санузел, кухню и спальню. Это было началом нашей стандартизации.
Далеко не каждую фотографию можно с уверенностью отнести к бабушкиному или не бабушкиному ремонту. Одно и то же фото разные люди отнесут к разным классам. Поэтому приняли решение сформировать конкретный список критериев бабушкиного ремонта: ковёр на стене, советская отделка, старый кухонный гарнитур, бывалая плитка на стенах и т. д.
Например, совершенно не «бабушкина» спальня (p = 0,0629):
А вот фотография комнаты, которая подходит под критерии «бабушкин ремонт» по всем параметрам (p = 0,9401):
На этой кухне ремонт современный (p = 0,1046):
А вот кухня с «бабушкиным ремонтом» (p = 0,9809):
Понятно, что формирование и разметка датасета — дело небыстрое, а времени у нас не было. Так что мы решили воспользоваться «Яндекс.Толокой», пользователи которой за небольшую плату анализируют и размечают данные. Всего на «Толоке» мы разметили 1000 фотографий, взятых из нашей базы.
За выполненные задачи толокисты получают денежное вознаграждение, но они также могут ошибаться. Проблема в том, что само восприятие бабушкиного ремонта — субъективный момент. Невозможно формализовать задачу на 100%: все варианты ремонтов просто нельзя учесть. Но мы просматривали результаты разметки и исправляли самые явные ошибки. В спорных моментах доверялись толокистам.
На основании агрегированных ответов мы собрали датасет, содержащий два класса: «бабушкин ремонт» и «не бабушкин ремонт». Но это ещё не было решением нашей задачи.
3. Должен царствовать баланс…
Выборка получилась несбалансированной, бабушкиных ремонтов оказалось слишком мало, да и размер датасета был недостаточным для качественного обучения. Ведь изначально мы отбирали 1000 изображений произвольно и только потом размечали. На такой выборке обучить нейросеть нужным образом проблематично.
Как же мы выкрутились? Обучили нейронку! Сейчас поясню. Мы решили обучить её на имеющихся данных. Полученная нейросеть, очевидно, не давала нужных результатов, чтобы использовать ее как готовое решение, но зато хорошо подходила для сбора дополнительных изображений, чтобы увеличить и сбалансировать датасет.
Мы отобрали примерно 1000 случайных картинок, которые имеют вероятность p ⩾ 0,3 и потенциально могут относиться к классу «бабушкин ремонт», но среди них была небольшая доля не бабушкиных ремонтов. Затем разметили их на «Толоке». У нас получился датасет (более 2000 картинок), на котором мы стали обучать нейронную сеть — первую версию фильтра.
Напоследок мы таким же образом набрали ещё около 3,5 тыс. картинок — дополнительный набор изображений, где доля бабушкиных ремонтов была больше, чем не бабушкиных.
Получилась вполне сбалансированная выборка, примерно 50 на 50. Итоговый обучающий датасет вырос до 5,5 тыс. 4400 изображений пошло в обучающую выборку, а 1100 — в тестовую.
Препроцессинг изображений
Конечно, нейронке нельзя скармливать изображения просто так: они должны пройти этап препроцессинга. При тестировании первой версии фильтра выдавались неплохие результаты, а precision был равен ≈0,78. Но нас такая точность не удовлетворяла. Помните, как важен для обучения хорошо обработанный датасет? Вот и над картинками для фильтра нам пришлось поработать, ведь далеко не всегда проблема именно в модели: бывает, что данные слишком разношёрстные.
Мы использовали классический набор для преобразования изображений: поворот на малый угол, horizontal flip и регулировки яркости и контрастности.
Немного аугментаций — отличный способ искусственно увеличить выборку и сделать сеть более устойчивой к небольшим изменениям фотографий. Всё это помогло добиться precision ≈0,86, а recall — ≈0,82.
Отдельно надо сказать о ресайзе. Модель OpenVINO принимает на вход картинки фиксированного размера. В примерах инференса, которые я использовала, применялся ресайз из OpenCV, который работает с картинками в виде NumPy-массивов. Да и OpenVINO-модель принимает на вход NumPy-массив. Мы заменили ресайз OpenCV на ресайз модуля Pillow (оба дефолтные). В своей основе эти два ресайза (Pillow и OpenCV) используют отличные друг от друга алгоритмы: на выходе сжатые картинки воспринимаются нейронной сетью по-разному, хотя для человеческого глаза разницы нет. Применение к изображениям ресайза, который изначально использовался при обучении, дало нам адекватный инференс и полное совпадение результатов OpenVINO-модели и оригинальной модели на PyTorch. Я ответственно заявляю: мелочи важны!
Что у нас получилось
После длительной сборки датасета (поиска изображений, их разметки и препроцессинга) и обучения сетки на внушительной итоговой выборке мы добились показателей precision ≈0,98 и recall ≈0,5 при пороге классификации 0,95. Неплохо, не так ли?
Поясню, почему такой маленький recall. Мы решили, что в первой версии фильтра не будем ставить низкий порог для бабушкиного ремонта, чтобы пользователи видели больше вариантов объявлений. Поэтому отбираем только самый типичный, самый хардкорный бабушкин ремонт.
Фильтр получился более чем работоспособным. Об этом говорят не только метрики, но и первые оценки от пользователей приложения на iOS. Это популярный фильтр арендных квартир, и он очень востребован у тех, кто ищет «бабушкины» квартиры для покупки. Уже сейчас каждый желающий, у кого есть смартфон на iOS или Android, может самостоятельно протестировать фильтр «бабушкин ремонт» и оставить свой отзыв о нём (например, в комментариях к посту).
Напутствие для инициативных
Каждый сервис, если есть такая возможность, должен оперативно разрабатывать технологии, которые помогут клиентам сберечь время и деньги. Задачей нашего проекта было улучшить Циан так, чтобы каждый человек мог ещё проще найти квартиру своей мечты. Я очень надеюсь, что наш фильтр зайдёт простым юзерам, а может, сподвигнет и вас на создание новых решений. Призываем пробовать, открывать и тестировать. Нам очень важен фидбэк, так что смелее!
Тех, кого больше интересует техническая реализация, у кого есть вопросы или интересное мнение, — приглашаем в комментарии!