Блог Антона Репушко

Дневник соревнования OpenEDS 2020

Мы со Святославом Скобловым 2 месяца решали OpenEDS 2020 Challenge, где в одном треке заняли первое место, а во втором восьмое. Про второй трек тут не будет, потому что он скучный и не очень интересный. И ещё нас изредка консультировал Вова Михеюшкин по всяким CV-вопросам. Может быть кому-то тоже будет интересно почитать, как проходят соревнования по ML.

Про соревнование

На основе датасета OpenEDS2020, собранного Facebook Reality Labs, запустили два трека. Оба связаны с VR/AR, Oculus и всем таким. Треки шли в рамках воркшопа к ECCV 2020.

Один из тысяч кадров

В первом треке даны последовательности по 100 (трейн) и 55 (валидация) фотографий глаз с gaze-векторами каждого кадра. Представьте, что вы умеет стрелять лазерами из глаз. Вот отнормированный вектор из вашего зрачка до объекта на VR/AR экране и есть gaze-vector.
Частота записи последовательности — 100Гц. В тесте были те же последовательности по 50 кадров, но уже без настоящих векторов. Общая задача — научиться предсказывать по 50 кадрам последовательности в тесте следующие 5 кадров (т.e. 50мс). Нужно это для foveated rendering.

Во втором треке нужно было сегментировать части глаза (бэкграунд, склеру, радужку и зрачок) по кривой разметке 5% данных в каждой последовательности из 200 кадров. Многие (по отзывам других участников) боролись именно с кривой разметкой, мы же начали решать второй трек за 2 недели до конца и не слишком преуспели, хотя разрыв между топом очень маленький. Важнее и интереснее для нас был именно первый трек.

Технические особенности

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

Команда BTSD скрыла свой сабмит

Своего железа у нас было не очень много, поэтому мы время от времени арендовали машины на vast.ai. Потратили на это около 230 долларов за 2 месяца соревнования.

Дневник

1 июня
Создан чат в Телеграме, начали разбираться в предметной области, с платформой, читать правила. Выяснили, как части глаза называются на английском. Создали репозиторий на Гитхабе и настроили всем доступы.

3 июня
Выкачиваются данные, визуально посмотрели gaze-вектора. Появляется идея классическими CV-методами поисков контуров искать зрачок на изображении и смотреть на изменение его положение внутри склеры. Разбираемся в типах движения глаза (саккады, скольжения, статичное положение и т. д.). Наконец-то понимаем вообще в чём суть трека.

4 июня
Нашли pupil-labs, с помощью оборудования которых Facebook генерировал свой датасет. Ничего полезного, но очень интересно.
Научились находить зрачок обычными CV методами. В итоге это потом использовалось только в визуализациях.

Пытались добавить ещё всяких контуров, но ничего не получилось.

Оригинальный кадр
Посчитанный контур

Важное решение: посчитали правильным разделить пайплайн на две части: gaze-estimator (модель, которая по кадру предсказывает его gaze-вектор) и gaze-predictor (модель, которая по истории gaze-векторов предсказывает gaze-вектора следующих 5 кадров).

6 июня
Начали визуализировать вектора, чтобы посмотреть на всю последовательность целиком. Нам нужен gaze-predictor, но насколько сложным он будет? Если в данных в основном статичное положение глаза, то тогда сложная модель тут не нужна (так в итоге и оказалось).

Глаз практически неподвижен
А тут уже двигается

Где-то тут было решено для обучения эстиматора (модели, которая будет по кадру предсказывать gaze-вектор) использовать аугментации. Но проблема в том, что при изменении изображения нужно будет менять и изначальный вектор: вращаешь изображение -> вращаешь вектор. Решили патчить albumentations.

Проекция вектора

11 июня
Начали гонять первые модели, оптимизировать параметры. Стали разбираться с предиктором. Попробовали всякие стандартные штуки для форкаста временных рядов типа prophet, но они предсказуемо не зашли из-за специфики данных: нельзя вытащить сезонность (которой нет), другие фичи вроде дней недели, времени и прочего, что активно эксплуатируется в таких местах.
Обучили первый resnet для эстиматора.

12 июня
Скор первого трека: 0.1556

Сделали первый сабмит (команда baccaddes)

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

15 июня
Скор первого трека: 0.0786

Засабмитили улучшенную модель (VAR поверх хорошего эстиматора).
Это приблизило нас к остальным на ЛБ.

16 июня
Скор первого трека: 0.0747

Написан и запущен Big Brother — бот, который следит за ЛБ. С этого момента до конца соревнования он работал с одним перебоем на полдня, после без ошибок.
Засабмитили скользящее среднее по 5 последним кадрам.

Бот отрабатывает как надо

17 июня

Начали смотреть глазами на глаза (это анимация одной последовательности)

Посмотрели на статические генераторы таких данных (NVGaze и UnityEyes). В итоге для сореванования их так и не использовали.

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

28 июня
Подумали, что было бы классно научиться группировать авторов одних и тех же последовательностей (количество участников при сборе датасета на порядки меньше числа последовательностей), чтобы вытаскивать оттуда какие-то фичи специфичные для конкретного участника. Эту идею так и не доделали.

Начали думать про классическое CV снова: хотели вытаскивать крайние точки глаз и прочее.

Обучили LSTM для предиктора. Сработало чуточку хуже средних по 5 кадрам.

29 июня
Закончили патчить albumentations.

Оригинальный кадр
Сработавший Vertical Flip (и пересчитанный вектор)
Живой ShiftScaleRotate

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

Нафигачили для эстиматора аугментаций.

4 июля
Скор первого трека: 0.0613

Стали думать, как сделать интереснее предиктор. Взяли потыкать darts — это такой враппер над всякими стандартными моделями для форкаста.

Запустили старый метод среднего по кадрам (или какую-то оч простую эвристику) над данными нового эстиматора. Очень сильно улучшились.

5 июля
Скор первого трека: 0.0570

Выучили пачку effnet’ов (до этого был resnet). Начали дробить на фолды и мешать предикты с разных фолдов.

Сняли тачку на vast.ai.

Посмотрели на тестовые данные на основе более-менее нормальных предиктов эстиматора. Оказалось, что какая-то динамика в последних 10 кадрах последовательности есть всего в 600-700 последовательностях из 6400.

Засабмитил старые методы предикта на среднем фолдов effnet’а. Почти до самого конца это было нашим лучшим результатом и первым местом на ЛБ.

9 июля
Обучили mobnet. Стали экспериментировать с предиктором: ExponentialSmoothing, VAR, ARIMA и т. д.
Методы ничего не докинули, а некоторые и ухудшили скор относительно просто среднего.
Скор самого предиктора при этом был очень хороший, около 0.000400+ на кадр по их метрике.

Первый раз открыли данные второго трека, порисовали маски.

10 июля
Засабмитили скользящее среднее по 3 кадрам на куче разных фолдов — не сработало.

Постарались достраивать вектора регрессией. Тоже не сработало.

11 июля
Начали играться с фильтрами над сигналом, чтобы сгладить разницу предиктов эстиматора между соседними кадрами. Получалось хорошо, использовали фильтр Савицки-Голая. Кажется, что всякие неровности эстиматора очень красиво сглаживаются.

12 июля
Разбирались с предиктором. Выяснили, что наша регрессия багованная и искали ошибку в разнице валидаций друг у друга. Нашли. Смотрели глазами на фильтрованные предикты и думали, что делать дальше.

14 июля
Нарисовали красивые картинки градиентов по разным осям между двумя соседними кадрами по предиктам на всём тесте. Выяснили, что у нас действительно всё — статика. Поэтому опять же нет смысла во всяких сложных моделях предиктора.

Выяснили, что локальная метрика эстиматора напрямую коррелирует с результатами на ЛБ и что результат одного хорошего фолда лучше, чем его же со смесью фолдов чуть-чуть хуже. Опять упёрлись в то, что надо дотюнивать эстиматор. Вернулись к resnet’у.

14 июля — 23 июля
Скор первого трека: 0.0552

Разбирались с сегментацией, удивлялись кривой разметке, сложным случая с закрытыми глазами и т. д.

Вытюнили 1 фолд для эстиматора до ошибки 0.000197 (в 3 раза лучше прошлых). В предиктор засунули простую эвристику: если статика, то скользящее среднее по 2 последним кадрам, а если была динамика в последних 5 кадрах, то добавляем градиент дальше до упора (из-за особенностей движения глаза, там бОльшая часть движений — линейная).

23 июля — 29 июля
Тюнили и думали над сегментацией. Смотрели на то, как нам применить синтетические данные в сегментаци. Генерировали синтетику.

Типичный несуществующий глаз

Из интересного: один сабмит пропустили, потому что кое-кто заснул от усталости и не слышал звонков с просьбой прислать данные. Два сабмита в два дня были сделаны за 30 и 20 секунд до сгорания.

Кусочек каких-то сравнений моделей по сегментации

30 июля — 31 июля
Скор первого трека: 0.0537

Смотрели и думали над всякими сложными случаями в сегментации

По первому треку доучили остальные бленды эстиматора, сблендили и засабмитили. Так и осталось нашим лучшим результатом.

По сегментации остались на 8ом месте.

Что хотели попробовать, но не попробовали

  • Не использовали никакую синтетику, а скорее всгео надо было бы. Можно и в обоих треках
  • Надо было учить LSTM на чистых данных (в том числе из тесты и из трейна) и сразу на векторах. Наши эвристики в итоге были по каждой оси отдельно

Как работает очистка данных со stat.gibdd.ru

Год назад написал скрипты для очистки данных статистики ДТП, потому что изначальные данные (именно GPS-координаты) были очень грязными и их практически нельзя было визуализировать. Сорцы на Гитхабе имеются.

Сейчас скрипты временно неработоспособны, так как Яндексовский геокодер теперь требует ключ для использования API, но скоро я их поправлю.

Я расскажу про красивый и аккуратный способ, как нам точку с неправильными координатами аккуратно подвинуть прямо на улицу, где произошло ДТП.

Алгоритм очистки данных

Описаниме по шагам

Изначально нам дана синяя точка слева внизу. Это GPS-координаты, которые мы получили из исходных данных. Наша основная зацепка — адрес. Он заполняется вручную и обычно верный.

Шаг 1. Воспользуемся геокодером. Это такая программа, которая переводит географическое название (город/село/улицу + дом) в географические координаты. Я обычно пользуюсь геокодером Яндекса, потому что он точнее для СНГ, но ещё есть бесплатный от OpenStreetMap. Так мы получаем чистую координату дома по адресу (красная точка на рисунке).

Шаг 2. В OSM хранится граф дорог, при этом они привязаны к географическим координатам (у графа есть точное положение на плоскости). И есть классная особенность: мы можем ввести координаты точки на плоскости и получить кусок графа в радиусе n от этой точки. Общая идея такая:

  • вводим координаты дома после геокодирования -> получаем круг примерно как на картинке,
  • выбираем все дороги, попавшие в радиус (можно считать их просто линиями),
  • строим перпендикуляры из точки до каждой из линий (на рисунке x и y),
  • выбираем кратчайший (в нашем случае x),
  • наша красная точка получает новые координаты (уже на ребре графа из OSM) и становится жёлтой точкой,
  • profit.

Так мы кривые координаты ДТП по одному лишь адресу аккуратно перенесли прямо на улицу. Теперь при визуализации всё будет аккуратно и ровно.

Типы улиц Тулы

Раз уж пошла такая мода (оригинальная идея Эрина Дэвиса, Москва, Астрахань), то и я нарисовал карту типов улиц Тулы.

Интересно выглядят плотные участки, состоящие только из проездов. Вроде как согласно СНиП (Строительные нормы и правила), к проездам меньше требований по ширине полос, допустимым углам поворота, наибольшему допустимому уклону, ширине пешеходной части.

А ещё в Туле есть особенный тип: «обвод». Он всего один (Восточной обвод), но всё таки существует. И целых два проспекта.

Датасет структуры сети Lightning

Ковыряю в свободное время интересную тему и задачу, которую курирует LAMBDA.

Вводная

Есть сеть Лайтнинг, которая является надстройкой над майннетом Биткоина. Придумывалась для более мелких транзакций, которые не позволял делать Биткоин. Например, тебе нужно заплатить за кофе или совершить какую-то маленькую операцию.

Основная терминология:

  • нода — узел сети. Получатель денег в общем,
  • канал — ребро сети. Общий кошелёк в майннете Биткоина между двумя нодами.

В Лайтнинге интересный механизм поиска пути транзакции. Eсли тебе надо перевести деньги из А в С, то не обязательно открывать новый общий кошелёк в майннете Биткоина.

Предположим у тебя уже есть канал А<->B и есть канал B<->C. В таком случае ты можешь сделать перевод A->B->C, где B за проход транзакции через себя возьмёт какую-то комиссию. А можно перевести и предположим по пути A>D->B->C, если такой существует.

Но не всё так просто. Помимо комиссии, на транзакцию накладываются дополнительные условия. У канала есть «ёмкость»: сколько можно переслать денег между двумя нодами без создания нового общего кошелька в майннете. И «ёмкость» А->B и B->A не одно и то же.

Подробнее и больше можно почитать в документации.

Цель

Основная идея ресёча — заменить глупый перебор результатов DFS для проведения транзакции на что-то более умное. Кажется, что это можно сделать, имея статистику «прошедших» транзакций и варианты альтернативных путей. Получиться должно что-то вроде статистического роутинга на основе прошлых транзакций.
Для этого нужно собирать датасет таких транзакций и их альтернатив, но это упирается в некоторые технические сложности и проблемы. Я собственно сейчас пытаюсь их решить, проверяя вообще работоспособность идеи сбора таких данных. Если получится, то будет очень классное и необычное решение.

Датасет

До этого этапа хотелось посмотреть (и посмотрели) просто на динамику сети, как она меняется и насколько стабильна. Написал простенький парсер и в итоге получился датасет на 10 Gb «слепков» сети: рёбра графа и всякая метаинфа нод (ip-адрес, алиас в сети и гео-координаты).

Парсилось каждый час с 10.12.2019 до 04.03.2020 (84 дня), всего 2022 записи. Каждый результат в отдельном файле со своим временем.

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

«Средненько». Проверка гипотезы

Мотивация

Я 3-4 года назад услышал про исследование КБ «Стрелка», в котором они скрапили фото из социальных сетей (Инстаграм и ВК) и рисовали хитмапы на картах по ним. Оказывается, это называется цифровой антропологией.

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

Загорелся идеей сделать нечто похожее, но в сферу каких-то постоянных событий идея оставалась идеей без реализации.

Закрывая гештальт (как Инстаграм своё API для доступа к гео-информации о фотографиях в 2016-ом) достиг успеха.

Реализация

Т. к. API Инстаграма оказалось закрытым, решил воспользоваться Flickr’ом.
Суть идеи:

  • берем 2019ый год,
  • cкрапим фотографии с Flickr для конкретной локации (т.e. города),
  • аккуратно накладываем на карту,
  • видим места, где люди много фотографируют,
  • делаем какие-то выводы.

Например: люди фотографируют -> там что-то интересное -> логично поселиться во время поездки.

Другой например: можно увидеть места, которые недостаточно освещены туристически (с точки зрения наличия фотографий) и можно выбрать наоборот район, где живут местные. На примере Берлина эта теория вроде как работает.

Фотографии в Берлине за 2019ый год

Найденные подводные камни:

  • API Flickr’a частично не завелось из python-обертки, которую я нашёл,
  • фотографий не так много (250к за год), как в Инстаграме. С геопозицией — еще меньше. За 2019ый год только 44к для Берлина,
  • на карте прямыми линиями из фотографий заметны фотопрогулки, когда один человек шел и фотографировал всё, что видел. Так получается много фотографий одного места, хотя это всего лишь от одного человека. В планах написать кастомную функцию хитмапа, которая бы давала больший вес участкам, где фотографии от разных людей. Так получится сильно честнее.

«Средненько»

В процессе возникла идея — сопоставить кучу фотографий одной достопримечательности для получения её «усреднённого» вида. Итог получился сильно лучше, чем я ожидал.

Оригинальные Бранденбургские ворота
Усреднённые 55 фотографий Бранденбургских ворот

Когда-то вероятно продолжу и сделаю для других городов.

Ранее Ctrl + ↓