Второе почетное. Заметки участника конкурса Dstl Satellite Imagery Feature Detection



    Недавно закончилось соревнование по машинному обучению Dstl Satellite Imagery Feature Detection в котором приняло участие аж трое сотрудников Avito. Я хочу поделиться опытом участия от своего лица и рассказать о решении.

    Описание задачи


    Задача заключалась в разработке алгоритма сегментации объектов на спутниковых снимках.



    Даны мультиспектральные снимки, сделанные в разных диапазонах длин волн. Снимки также отличаются разрешением и из-за особенностей съемки некоторые каналы немного смещены относительно друг друга.

    Предсказывать нужно объекты 10 классов:

    Class 1: Строения (просто строения, ничего примечательного)
    Class 2: Структуры (заборы, которые даже на самых больших снимках имеют ширину в несколько пикселей)
    Class 3: Дороги (асфальтовые дороги и шоссе)
    Class 4: Грунтовые дороги
    Class 5: Деревья (могут расти как одиночно, так и группами)
    Class 6: Поля (отличаются тем, что на них что-то выращивают)
    Class 7: Быстрая вода (реки, фрагменты моря, крупные водоемы)
    Class 8: Медленная вода (озера, пересыхающие реки)
    Class 9: Большой транспорт (грузовики, автобусы)
    Class 10: Небольшой транспорт (автомобили и мотоциклы).

    Метрика — усредненный по всем классам индекс Джаккарда (Average Jaccard Index), который определяется как отношение площади пересечения к площади объединения. В данном случае пересечение и объединение относятся к маскам классов. При этом каждый класс вносил одинаковый вклад в общую метрику, но площади каждого класса сильно отличались. На гистограмме показана доля площади каждого класса относительно всей площади. Для класса 6 (поля) доля площади составляла 0.6, а для двух последних классов 0.0001.



    Отдельно стоит отметить количество данных. Нам были выданы 25 размеченных областей. В то время как в тесте их было 425. Из них public leaderboard строился по 19%, а private по оставшимся 81%. Как водится public служил по сути для того, чтобы проверить свое решение и сравнить результат с другими участниками. А финальный рейтинг составлялся только по private части. Однако мне было очевидно, что раз в public’e 81 снимок, то эта проверка куда надежней, чем любая моя локальная валидация по 25 снимкам, и если я получал расхождение между метрикой на локальной проверке и лидербордом, то доверял лидерборду. Как же я ошибался… Но об этом ближе к концу.

    Лирическое отступление


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

    • коллега по работе Дмитрий Цыбулевский уже участвовал и рассказывал, что конкурс интересный, позволяет попробовать много трюков и поэкспериментировать с архитектурой нейросетей (ну и даже решать проблемы с конвертацией масок это интереснее, чем просто обучать xgboost поверх обфусцированных фичей);

    • также Владимир Игловиков в ODS Slack’e агрессивно агитировал участвовать и аргументировал это тем, что тут будет борьба за что-то осмысленное, а не за 4-й знак после запятой в метрике (как же он ошибался...);

    • ну и в итоге окончательное решение я принял, когда Константин Лопухин выложил скрипт, который решал последнюю проблему с конвертацией масок в полигоны.

    Baseline


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

    Соответственно, я выбрал скрипты, выложенные на форуме, и собрал из них следующий пайплайн:



    Предпроцессинг и подготовка данных


    Я взял все изображения из M-бэнда, отмасштабировал их к одному размеру для удобства (при этом рейсайз был не больше 1%) и собрал из них большую мозаику 5х5. Тоже самое сделал с масками. Затем из одинаковых для изображения и маски рандомных мест я вырезал кропы размером под вход и выход сети (128 х 128 пикселей), формируя тем самым очередь для обучения. Однако брались не все сэмплы, а только те, у которых доля площади маски класса от общей площади была выше порога. Пороги я подобрал для каждого класса руками так, чтобы сэмплирование происходило более-менее равномерно. Затем изображения в каждой очереди приводились к диапазону (-1, 1). Также для валидации я вырезал небольшую полоску из картинки, в которой содержались все классы и заменил ее копией соседнего куска картинки в трейне.

    Обучение нейросети


    В качестве нейросети я выбрал U-net. Эта нейросеть по структуре похожая на обычный энкодер-декодер, но с пробросами фичей из энкодер-части в декодер на соответствующих по размеру стадиях.



    U-net себя хорошо показал в другом конкурсе на кэггле с сегментацией нервов. К тому же я нашел код с ней для Keras’a. В качестве функции потерь использовалось приближение целевой метрики. Оптимайзер был взят Adam, потому что как правило у меня получались с ним неплохие результаты в задачах, когда непонятно, по какому критерию уменьшать Learning rate.

    Предсказания


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

    В итоге кэггл его даже принял. Я решил, что это успех и побежал оформлять свой код в виде kernel. На тот момент никто не выложил полный pipeline от начала и до конца, поэтому скрипт был принят очень позитивно. И потом в течение нескольких дней я видел, как на лидерборде появляются новые люди с примерно моим скором. Ну и кернел набрал больше всех голосов и комментов в этом соревновании.



    Однако радость моя был недолгой. У меня был объективно низкий скор, и я не мог понять причину. Ее нашел Расим Ахунзянов. Я зачем-то после того, как приводил все изображения к диапазону (0, 1), переводил float’ы в int’ы. Этот факт отметили в комментариях к скрипту и с того момента любой желающий мог получить работающую версию скрипта.

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

    Примером успешного использования скрипта является результат Миши Каменщикова. В Avito он занимается рекомендациями и я решил подсадить его на DL, продав ему этот конкурс. Миша исправил баги, за выходные обучил свою модель с небольшими доработками и воспользовался другим публичным скриптом для воды. Это позволило ему финишировать на 46 месте и получить первую серебряную медаль.

    Мое решение


    Вообще говоря, мое решение не сильно отличалось от Baseline в плане общего похода, однако я внес ряд существенных улучшений.

    1. Conv + ReLu → Conv + BN + ReLu.
      В ванильном U-net не использовался слой батч нормализации. Я его добавил. И так делали почти все участники. У всех он улучшал сходимость.
    2. Аугментации: поворот на произвольный угол, вертикальный и горизонтальный флип. Очевидная вещь, без которой сейчас не обходится обучений картиночных нейросетей.
    3. Hard Negative mining.

    Тут стоит остановиться поподробнее. Я отказался от сэмплирования патчей с долей маски класса выше порога и решил брать все сэмплы. Но после обучения нейросети на наборе сэмплов, я предсказывал маски на нем. И для каждого класса выбирал топ 100 сэмплов, где присутствует маска класса и где метрика наихудшая. И затем добавлял эти сэмплы в следующую итерацию. Тем самым я убивал двух зайцев: Теперь в каждой эпохе были сэмплы каждого класса и в каждой эпохе были «сложные» сэмплы. Я не проводил прямого сравнения, но по ощущениям сеть сходилась намного быстрее.

    С такими улучшениями я обучил еще две сети, которые вошли в финальный ансамбль. Но лучшая, на мой взгляд, находка состояла в сети с тремя входами:



    Эта концепция родилась в процессе обсуждения с Дмитрием Цыбулевским. Проблема с картинками в этом конкурсе в том, что разные каналы имеют сильно разное разрешение и если хочется использовать всю информацию, то нужно приводить все к одному размеру. Но если уменьшать, то теряется разрешение и такие классы как рукотворные структуры вовсе исчезнут. Ну а если приводить все к самому большому размеру, то нужно увеличивать, скажем, А-бэнд в 20 раз, что выглядит крайне неразумно. С другой стороны изображение при прохождении через нейросеть после каждого слоя с пуллингом становится эффективно меньшего размера.

    Соответственно, родилась идея добавлять новые картинки по мере уменьшения размера основного бранча энкодер-части U-net. Для этого на первый выход подавались RGB и P-бэнды в размере 224х224, после двух пуллингов к основному бранчу конкатинировался вход с М-бэндом и затем еще через два пуллинга А-бэнд. При этом, на всех бэндах была одинаковая физическая область. Для этого все-таки пришлось в 1.5 раза увеличить самый маленький канал, но это приемлемо. Каждый канал конкатинировался после слоя свертки. Это было сделано, чтобы не конкатенировать представление сетки и сырые картинки как фичи разной природы. В этом плане я получил справедливую критику, что этого все равно не достаточно, ведь картинка в основном бранче прошла куда больше слоев сверток, чем картинка с второго и тем более третьего входа. И, скорее всего, это правда, и архитектура с большим числов сверток для каждого последующего входа будет работать лучше, но я не занимался такого рода оптимизацией гиперпараметров. Полная схема

    Особенности обучения


    Как правило, процесс обучения глубоких сверточных нейросетей для распознавания изображения довольно понятная вещь. Особенно после того как были разработаны и выложены веса State of the Art архитектуры с Imagenet. Однако в этой задаче не выходило просто взять и обучить нейросеть так, чтобы она гладенько залетела в глобальный минимум. Во-первых, долгое время в ODS Slack’e никто не мог предложить надежную схему локальной валидации, которая бы давала однозначное соответствие с лидербордом и в конце мы узнали почему. Во-вторых, сеть довольно сильно шатало от эпохи к эпохе и порой соседние эпохи для одной архитектуры по качеству предсказаний отличались сильнее, чем предсказания другой нейросети.  

    Если построить лог обучения на локальной валидации, то он выглядит так:



    На графиках джаккард для первых 7 классов и справа снизу средняя метрика по всем классам. Видно, что разные классы обучаются за разное количество итераций даже с выравниванием сэмплов, поскольку все равно площади одних классов больше других. Также видны провалы и шатание. Мне интересно было узнать, что же происходит с предсказаниями, и я для каждой эпохи визуализировал их для валидации. Из этих предсказаний Роман Соловьев собрал прекрасный ролик:



    В ролике показано как меняются предсказания каждого класса от эпохи к эпохе. Цветами отмечено: синий — маска класса (False Negative), зеленое — неправильные предсказания (False Positive), голубое — правильные предсказания (пересечение маски и предсказаний) (True Positive). Мне лично очень нравятся моменты, когда сеть сначала предсказывает что-то нерелевантное, а потом уточняет свои предсказания, как, например, с классами быстрой воды и шоссейных дорог. Также на всех классах видно шатание в предсказаниях. Еще можно заметить артефакты, связанные с тем, что вся маска предсказывается скользящим окном. Ну и главный вывод, который я для себя сделал, — визуальный анализ результатов решает.

    Поэтому я сделал такую штуку.



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

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

    Финальный пайплайн до объединения с Романом выглядит так:



    Но затем мне написал Роман и предложил объединиться. Мне показалось, что это крайне разумно, ведь мы оба использовали Keras с Theano в качестве бэкэнда и прямое объединение наших результатов с лучшими классами давал топ2 результат даже без усреднения или объединения предсказаний по отдельным классам. Про свое решение, я надеюсь, Роман напишет отдельный пост.

    Послесловие


    Про данные. Каково же было мое удивление и удивление всех участников ODS Slack’a, когда уже после окончания конкурса орги сообщили, что на самом деле они разметили только 32 снимка из теста. Остальные были просто фейком. Ну и соответственно public это 6 снимков, а private 26. Именно из-за этого можно считать, что конкурс имел в какой-то степени характер лотереи. Ну и это также объясняет, частые расхождения локальной валидации и паблика, ведь часто они были сопоставимы по размеру.

    Про организаторов. По описанию конкурс должен был закончиться 7го марта в 3 ночи по Москве. Этого момента лично я ждал намного больше, чем любого нового года. И вы можете представить уровень пригара участников ODS Slack'a, когда результаты не опубликовали, а на форуме появилось сообщение, что мы все молодцы и результаты будут через неделю. Мол, связано с тем, что заказчики готовят пресс-релиз. Через неделю появился пресс-релиз в котором сообщалось, что данные уже анализируются (ага, конечно, ведь решение никто еще не отправил). И еще Dstl так понравился формат соревнований, что они решили запилить свой кэггл с беспилотниками и topic modelling. На этой площадке уже запущены два конкурса, но не спешите натирать радиаторы видеокарт и импортировать xgboost'ы. На призы в этом конкурсе могут претендовать только граждане стран, у которых индекс коррупции выше 37 (Transparency International's Corruption Perceptions Index 2014). Соответственно, граждане Замбии и Буркина-Фасо могут участвовать, а россияне — нет. У любых соревнований есть своя изюминка, и, похоже, Dstl её нашли — это пригар.

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

    Запись с тренировки:



    Презентация доклада здесь.

    UPD:
    В ожидании поста от Романа, решил сделать картинку с нашим общим пайплайном:
    image

    UPD2:
    Пост Владимира о том же соревновании
    • +58
    • 9,5k
    • 8
    Avito 312,60
    Avito – сайт объявлений №1 в России
    Поделиться публикацией

    Вакансии компании Avito

    Комментарии 8
    • +5
      Это было просто приятно прочитать! Отличная история и понятная даже человеку, далёкому от нейросетей.
      • +1

        Шикарно! Планируете ли вы коммерческое применение получившихся результатов? Если да, то какое? Будете ли предлагать результаты работы компаниям которые занимаются ДЗЗ?

        • +3
          У меня сложилось впечатление, что Dstl не рассчитывает всерьез применять эти алгоритмы, чтобы наводить ракеты на Нигерию. Я лично не хотел бы заниматься этой деятельностью, поскольку у нее есть двойное применение. Также, к сожалению, мы не можем выложить полный код своего решения, поскольку это одно из условий получения приза. Однако, Сергей выложил мой исправленный и доработанный кернел. Он уже решает задачу end-to-end, и если собрать все трюки с форума, то заинтересованная компания может довольно быстро получить State of the art на своих данных.
        • 0
          Безумная идея от никогда не работавшего с нейросетями:

          Разнородность данных подталкивает в поиску общего в них:
          Возможно, стоит попробовать представить данные не в виде отмасштабированных картинок, а в виде набора точек? Это избавит от мыла, вносимого масштабированием (изменение на размера 1% в этом плане просто ужасно )
          • 0
            В изменении размера на и 1% и на 50% нет ничего ужасного, на мой взгляд, если использовать подходящую интерполяцию. Ужас начинается, когда увеличивают в 5+ раз. А про представление изображения я не понял. Оно и так по сути является набором точек и в нейросеть подается в виде матрицы размером (n_channels, weight, height). В чем идея?
          • 0
            Прошу помочь, задача немногим схожая. От нарисованной текстуры на изображении зависят качественные характеристики предмета. допустим, я 0 в программировании и у меня есть 2-3 года на решение этой задачи. Каким образом я могу достичь цели? Грубо говоря, нужны пункты, которые необходимо изучить. Что вы можете посоветовать? Ответ: занимайся, парень, лучше своими делами — тоже устроит. С математикой у меня ок, магистр техники. Привлекают 2 языка программирования: python и golang (ну еще асм интересен для кое-чего)
            • +2
              Чтобы изучить нейронки нужно изучать нейронки (а они сейчас почти всё могут зарулить, для остального есть xgboost и симуляции). Лучший гайд по нейронкам, там несколько вариантов на выбор, пара курсов, пара книг: http://yerevann.com/a-guide-to-deep-learning/
            • +2
              Если честно, я не понял вопрос. Если хочется разобраться в машинном обучении и глубоком обучении, то лучше всего просто взять и начать решать какие-то задачи, разбираясь с непонятными вещами по ходу. Либо из реальной жизни (если работа позволяет), либо решая конкурсы по тематике. Сейчас, кстати, на кэггле куча годных конкурсов разной степени сложности и мы скоро запустим конкурсе по рекомендациям. Если вопрос про язык программирования, то для глубокого обучения единственный выбор, на мой взгляд, это python. В питоне для решения задачи классификации текстур по изображению (если я правильно понял вопрос) легче всего будет вникнуть в keras (для DL), skimage/PIL (для работы с картинками), matplotlib (для визуализации), ну и без numpy/scipy никуда.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое