Летом прошлого года закончилось соревнование на площадке kaggle, которое было посвящено классификации спутниковых снимков лесов Амазонки. Наша команда заняла 7 место из 900+ участников. Не смотря на то, что соревнование закончилось давно, почти все приемы нашего решения применимы до сих пор, причём не только для соревнований, но и для обучения нейросетей для прода. За подробностями под кат.
tldr.py
import kaggle
from ods import albu, alno, kostia, n01z3, nizhib, romul, ternaus
from dataset import x_train, y_train, x_test
oof_train, oof_test = [], []
for member in [albu, alno, kostia, n01z3, nizhib, romul, ternaus]:
for model in member.models:
model.fit_10folds(x_train, y_train, config=member.fit_config)
oof_train.append(model.predict_oof_tta(x_train, config=member.tta_config))
oof_test.append(model.predict_oof_tta(x_test, config=member.tta_config))
for model in albu.second_level:
model.fit(oof_train)
y_test = model.predict_proba(oof_test)
y_test = kostia.bayes_f2_opt(y_test)
kaggle.submit(y_test)
Описание задачи
Planet подготовила набор спутниковых снимков в двух форматах:
- TIF — 16 bit RGB + N, где N — Near Infra Red
- JPG — 8bit RGB, которые производные от TIF и которые были предоставлены для уменьшения порога входа в задачу, а также для упрощения визуализации. В предыдущем соревновании на Kaggle, необходимо было работать с мультиспектральными изображениями. невизуальные, то есть инфракрасный, а также каналы с большей длиной волны очень сильно улучшали качество предсказания, причем как сети, так и unsupervised методы.
Географически данные были взяты с территории бассейна реки Амазонки, и с территорий стран Бразилия, Перу, Уругвай, Колумбия, Венесуэла, Гайана, Боливия и Эквадор, в которых были выбраны интересные участки поверхности, снимки с которых и были предложены участникам.
После того, как из tif создавались jpg, все сцены резались на мелкие куски размером 256x256. И по полученным jpg силами работников Planet из Берлинского и Сан Францисского офиса, а также через платформу Crowd Flower, производилась маркировка.
Перед участниками ставилась задача для каждого 256x256 тайла предсказать одну из взаимоисключающих погодных меток:
Cloudy, Partly cloudy, Haze, Clear
А также 0 или более непогодных: Agriculture, Primary, Selective Logging, Habitation, Water, Roads, Shifting Cultivation, Blooming, Conventional Mining
Итого 4 погодных и 13 не погодных, причем погодные взаимоисключающие, а непогодные нет, но при этом если на картинке метка cloudy, то других меток там не должно быть.
Точность модели оценивалась по метрике F2:
Причем все метки имели одинаковый вес и сначала высчитывалось F2 по каждой картинке, а потом шло общее усреднение. Обычно делают чуть по другому, то есть высчитывается некая метрика по каждому классу, а потом усредняется. Логика в том, что последний вариант более интерпретируем, так как позволяет ответить на вопрос как модель себя ведет на каждом конкретном классе. В данном случае организаторы пошли по первому варианту, что, видимо, связано со спецификой их бизнеса.
Всего в трейне 40к сэмплов. В тесте 40к. Из-за небольшого размера датасета, но большого размера картинок, можно сказать, что это “MNIST на стероидах”
Лирическое отступление
Как видно из описания, задача довольно понятна и решение не представляет из себя рокет саенс: нужно просто зафайнтюнить сетку. А с учетом специфики кэггла, еще и настакать кучу моделей поверх. Однако, чтобы получить золотую медаль, необходимо не просто как-то обучить кучу моделей. Крайне важно иметь много базовых разнообразных моделей, каждая из которых сама по себе показывает выдающийся результат. И уже поверх этих моделей можно накручивать стэккинг и другие хаки.
member | net | 1crop | TTA | diff, % |
---|---|---|---|---|
alno | densenet121 | 0.9278 | 0.9294 | 0.1736 |
nizhib | densenet169 | 0.9243 | 0.9277 | 0.3733 |
romul | vgg16 | 0.9266 | 0.9267 | 0.0186 |
ternaus | densenet121 | 0.9232 | 0.9241 | 0.0921 |
albu | densenet121 | 0.9294 | 0.9312 | 0.1933 |
kostia | resnet50 | 0.9262 | 0.9271 | 0.0907 |
n01z3 | resnext50 | 0.9281 | 0.9298 | 0.1896 |
В таблице приведены F2 score моделей всех участников для single crop и TTA. Как видно, разница невелика для реального использования, однако это важно для режима соревнования.
Командное взаимодействие
Александр Буслаев albu
На момент участия в соревновании руководил всем мл направлением в компании Geoscan. Но с тех пор затащил кучу соревнований, стал отцов вся ODS по семантической сегментации и уехал в Минск, грести в Mapbox, про что вышла статья
Алексей Носков alno
Универсальный мл боец. Работал в Evil Martians. Сейчас перекатился в Yandex.
Константин Лопухин kostialopuhin
Работал и продолжает работать в Scrapinghub. С тех пор Костя успел получить еще несколько медалей и без 5 минут Kaggle Grandmaster
Артур Кузин n01z3
На момент участия в этом соревновании я работал в Avito. Но в районе нового года перекатился в блокчейн стартап Dbrain на должность Lead Data Scientist. Надеюсь, скоро порадуем комьюнити своими конкурсами с докерами и ламповой разметкой.
Евгений Нижибицкий @nizhib
Lead Data Scientist в Rambler & Co. С этого соревнования Женя открыл в себе тайное умение находить лики в картиночных конкурсах. Что помогло ему затащить пару соревнований на платформе Topcoder. Об одном из них я рассказывал.
Руслан Байкулов romul
Занимается трекингом спортивных событий в компании Constanta.
Владимир Игловиков ternaus
Мог вам запомниться по остросюжетным статье о притеснении британской разведкой. Работал в TrueAccord, но затем перекатился в модный-молодежный Lyft. Где занимается Computer Vision для Self-Driving car. Продолжается тащить соревнования и недавно получил Kaggle Grandmaster.
Наше объединение и формат участия можно назвать типичным. Решение объединиться было связано с тем, что у нас всех были близкие реузльтаты на лидерборде. И каждый из нас пилил свой независимый пайплайн, которые представлял полностью автономное решение от начала и до конца. Также после объединения несколько участников занимались стэккингом.
Первое, что мы сделали — это общие фолды. Мы сделали так, чтобы распределение классов в каждом фолде было такое же как во всем датасете. Для этого сначала выбрали самый редкий класс, стратифицировали по нему, потому оставшиеся картинки стратифицировали по второму по популярности классу и так далее пока картинок не осталось.
Гистограмма классов по фолдам:
Также у нас был общий репозиторий, где у каждого члена команды была своя папка, в рамках который он организовывал код как хотел.
И еще мы договорились о формате предиктов, поскольку это была единственная точка взаимодействия по объединению наших моделей.
Обучение нейросетей
Поскольку у каждого из нас была независимый пайплайн, то мы представляли собой гридсерч оптимального процесса обучения распараллеленный по людям.
Общий подход
Картинка из github.com/tornadomeet/ResNet
Типичный процесс обучения представлен на графике обучения Resnet нейросетей на imagenet’e. Стартуют с рандомно инициализированных весов с SGD (lr 0.1 Nesterov Momentum 0.0001 WD 0.9) и затем через 30 эпох понижают learning rate в 10 раз.
Концептуально каждый из нас использовал такой же подход, однако чтобы не состариться пока обучается каждая сеть, понижение LR происходило если на валидации лосс не падал 3-5 эпох подряд. Либо некоторые участники просто сократили количество эпох на каждом уроне LR и понижали по расписанию.
Аугментации
Выбор правильных аугментаций очень важен при обучении нейросетей. Аугментации должны отражать вариативность природы данных. Условно аугментации можно разделить на два типа: те, которые вносят смещение в данные, и те, которые не вносят. Под смещением можно понимать разные низкоуровневые статистики, типа гистограмм цветов или характерный размер. В этом плане, скажем, HSV аугментации и скейл — вносят смещение, а рандомный кроп — нет.
На первых стадиях обучения сети можно сильно переборщить с аугментациями и использовать очень жесткий набор. Однако ближе к концу обучения необходимо либо выключить аугментации, либо оставить только те, которые не вносят смещения. Это позволяет нейросети немного оверфитнутся под трейн и показать чуть лучший результат на валидации.
Замораживание слоев
В подавляющем большинстве задач нет смысла обучать нейросеть с нуля, намного эффективней файнтюнить с предобученных сетей, скажем с Imagenet. Однако можно пойти дальше и не просто менять полносвязный слой под слой с нужным количеством классов, а обучать сначала его с заморозкой всех сверток. Если не замораживать свертки и обучать сразу всю сеть с рандомно инициализированными весами полносвязного слоя, то веса сверток корраптятся и финальный перфоманс нейросети будет ниже. На этой задаче это было особенно заметно из-за небольшого размера обучающей выборки. На других соревнованиях с большим объемом данных типа cdiscount можно было отмораживать не всю нейросеть, а группы сверток начиная с конца. Таким образом можно было сильно ускорить обучение, поскольку для замороженных слоев не считались градиенты.
Циклический отжиг
Этот процесс выглядит так. После завершений базового процесса обучения нейросети берутся лучшие веса и процесс обучения повторяется. Но стартует он с более низкого Learning rate и происходит за короткое время, скажем 3-5 эпох. Это позволяет нейросети спуститься в более низкий локальный минимум и показать лучший перформанс. Этот поход стабильного улучшает результат в довольно широком числе конкурсов.
Более подробно про два приема здесь
Test time augmentations
Поскольку речь идет о соревновании и у нас нет формального ограничения на время инференса, то можно использовать аугментации во время теста. Выглядит это так, что картинка искажается также, как это происходило во время обучения. Скажем, отражается по вертикали, по горизонтали, поворачивается на угол и т.д. Каждая аугментация дает новую картинку от которой получаем предсказания. Затем предсказания таких искажений одной картинки усредняются (как правило геометрическим средним). Это также дает профит. В других конкурсах я экспериментировал также с рандомными аугментациями. Скажем, можно применять не по одной, а просто уменьшить амплитуду для случайных поворотов, контрастов и цветовых аугментаций в два раза, зафиксировать сид и сделать несколько таких рандомно искаженных картинок. Это тоже давало прирост.
Snapshot Ensembling (Multicheckpoint TTA)
Идеей отжига можно развить дальше. На каждой стадии отжига нейросеть залетает в немного разные локальные минимумы. А это значит, что это по сути немного разные модели, которые можно усреднить. Таким образом во время предсказаний теста можно взять три лучших чекпоинта и усреднить их предсказания. Я также пробовал брать не три лучших, а три самых разнообразных из топ10 чекпоинтов — было хуже. Ну и для продакшена такой трюк не применим и я пробовал усреднять веса моделей. Это давало очень незначительный, но стабильный прирост.
Подходы каждого члена команды
Соответственно в той или иной степени каждый член нашей команды использовал разную комбинацию вышеописанных приемов.
epoch |
|||||
---|---|---|---|---|---|
albu | 3 | SGD | 15 epoch LR decay, Circle 13 epochs |
D4, Scale, Offset, Distortion, Contrast, Blur |
D4 |
alno | 3 | SGD | LR decay | D4, Scale, Offset, Distortion, Contrast, Blur, Shear, Channel multiplier |
D4 |
n01z3 | 2 | SGD | Drop LR, patient 10 | D4, Scale, Distortion, Contrast, Blur |
D4, 3 checkpoint |
- | Adam | Cyclic LR (1e-3: 1e-6) | D4, Scale, Channel add, Contrast |
D4, random crop |
|
nizhib | - | Adam | StepLR, 60 epochs, 20 per decay | D4, RandomSizedCrop |
D4, 4 corners, center, scale |
kostia | 1 | Adam | D4, Scale, Distortion, Contrast, Blur |
D4 | |
romul | - | SGD | base_lr: 0.01 — 0.02 lr = base_lr * (0.33 ** (epoch / 30)) Epoch: 50 |
D4,Scale | D4, Center crop, Corner crops |
Стеккинг и хаки
Каждую модель с каждым набором параметров мы обучили на 10 фолдах. И затем на out of fold (OOF) предиктах обучили модели второго уровня: Extra Trees, Linear Regression, Neural Network и просто усреднение моделей.
И уже на OOF предиктах моделей второго уровня подобрали веса для смешивания. Более подробно про стэккинг можно почитать тут и тут.
В реальном продакшене как ни странно такой подход тоже имеет место. Например, когда есть разномодальные данные (картинки, текст, категории и т.д.) и хочется объединить предсказания моделей. Можно просто усреднить вероятности, но обучение модели второго уровня дает лучший результат.
Оптимизация F2 по Баесу
Также финальные предсказания немного тюнились используя оптимизацию по байесу. Предположим, что у нас есть идеальные вероятности, тогда F2 с наилучшим мат ожиданием (т.е. типа оптимальный) получается по такой формуле:
Что это означает? Надо перебрать все комбинации (т.е. для каждого лейбла 0 и 1), посчитать вероятность каждой комбинации, и домножить на F2 — получаем ожидаемое F2. Для какой комбинации оно лучше, да и даст оптимальный F2. Вероятности считались просто перемножением вероятностей отдельных лейблов (если у лейбла 0, берем 1 — p), а чтобы не перебирать 2 в 17 вариантов, шатались только лейблы у которых вероятность от 0.05 до 0.5 — таких было 3-7 в строке, так что вариантов немного (сабмит делался за пару минут). По идее, было бы круто получать вероятность комбинации лейблов не просто перемножая индивидуальные вероятности (т.к. лейблы не независимы), но это не зашло.
что это дало? когда модели стали хорошими, подбор порогов после ансамбля перестал работать, а эта штука давала небольшой но стабильный прирост и на валидации, и на public/private.
Послесловие
В итоге мы обучили 48 разных моделей, каждую на 10 фолдах, т.е. 480 моделей первого уровня. Такой человеческий гридсерч позволил попробовать разные приемы при обучении глубоких сверточных нейросетей, которые я до сих пор использую в работе и соревнованиях.
Можно ли было обучить меньше моделей и получить такой же или лучший результат? Да, вполне. Наши соотечественники с 3го места Станислав stasg7 Семенов и Роман ZFTurbo Соловьев обошлись меньшим числом моделей первого уровня и компенсировали 250+моделями второго уровня. Про решение можно посмотреть разбор и почитать пост.
Первое место занял таинственный bestfitting. Вообще этот парень очень крут, и сейчас стал топ1 рейтинга кэггла, затащив много картиночных конкурсов. Он долго оставался анонимусом, пока Nvidia не сорвала покровы, взяв у него интервью. В котором он признался, что ему репортят 200 подчиненных… Про решение также есть пост.
Еще из интересного: широко известный в узких кругах Jeremy Howard, отец fastai финишировал 22м. И если подумали, что он просто по фану отправил пару сабмитов, то не угадали. Он участвовал в команде и отправил 111 посылок.
Также аспиранты Стэнфорда, которые проходили в то время легендарный курс CS231n, и которым разрешили эту задачу использовать как курсовой проект, всем коллективом закончили в середине лидерборда.
В качестве бонуса, я выступал в Mail.ru с материалом этого поста и вот еще презентация Владимира Игловикова с митапа в Долине.