SNA Hackathon 2019

У лютому-березні 2019 року проходив конкурс із ранжування стрічки соціальної мережі SNA Hackathon 2019, у якому наша команда посіла перше місце. У статті я розповім про організацію конкурсу, методи, які ми спробували, та налаштування catboost для навчання на великих даних.

SNA Hackathon 2019

SNA Hackathon

Хакатон під такою назвою проводиться вже втретє. Організований він соціальною мережею ok.ru, відповідно, завдання та дані мають безпосереднє відношення до цієї соцмережі.
SNA (social network analysis) у разі правильніше розуміти не як аналіз соціального графа, а скоріш як аналіз соціальної мережі.

  • У 2014 році завданням було спрогнозувати кількість лайків, які набере пост.
  • У 2016 році завдання ВВЗ (можливо ви знайомі), більш наближене до аналізу соціального графа.
  • У 2019 році – ранжування стрічки користувача за ймовірністю, що користувач лайкне пост.

Не можу сказати про 2014 рік, але у 2016 та 2019 роках, крім здібностей до аналізу даних, також були потрібні навички роботи з великими даними. Думаю, що саме об'єднання завдань машинного навчання та обробки великих даних мене залучило на ці конкурси, а досвід у цих сферах допоміг здобути перемогу.

mlbootcamp

У 2019 році конкурс було організовано на платформі https://mlbootcamp.ru.

Конкурс розпочався в онлайн режимі 7 лютого та складався з 3 завдань. Усі бажаючі могли зареєструватися на сайті, завантажити базова лінія та завантажити свою машину на кілька годин. Після закінчення онлайн етапу 15 березня топ-15 кожного конкуру було запрошено до офісу Mail.ru на офлайн етап, який проходив з 30 березня до 1 квітня.

Завдання

У вихідних даних надані ідентифікатори користувачів (userId) та ідентифікатори постів (objectId). Якщо користувачеві показували пост, то даних є рядок, що містить userId, objectId, реакції користувача цей пост (feedback) і набір різних ознак чи посилань на картинки і тексти.

ідентифікатор користувача objectId ownerId зворотний зв'язок зображень
3555 22 5677 [liked, clicked] [hash1]
12842 55 32144 [Disliked] [hash2, hash3]
13145 35 5677 [clicked, reshared] [hash2]

Тестовий набір даних містить аналогічну структуру, але немає поля feedback. Завданням є передбачити наявність реакції 'liked' у полі feedback.
Файл сабміта має таку структуру:

ідентифікатор користувача SortedList[objectId]
123 78,13,54,22
128 35,61,55
131 35,68,129,11

Метрика – середній ROC AUC за користувачами.

Більш детальний опис даних можна знайти на сайті згортання. Також там можна завантажити дані, включаючи тести та картинки.

Онлайн етап

На онлайн етапі завдання було розбито на 3 частини

Офлайн етап

На етапі офлайн дані включали всі ознаки, при цьому тексти і зображення були розріджені. Рядок у датасеті, яких і без того було багато, стало в 1,5 раза більше.

Рішення задачі

Так як на роботі займаюся cv, я почав свій шлях у цьому конкурсі із завдання «Зображення». Дані, які були надані, - це userId, objectId, ownerId (група в якій опубліковано пост), часипочатків створення і показу посту і, звичайно, зображення до цього посту.
Після генерації кількох фіч, заснованих на timestamp, наступною ідеєю було взяти передостанній прошарок пройденого на іміджет нейронки і відправити ці ембеддінги в бустинг.

SNA Hackathon 2019

Результати вийшли не вражаючі. Ембеддінги з нейронки нерелевантні, подумав я, треба запиляти свій автоэнкодер.

SNA Hackathon 2019

Це зайняло чимало часу, а результат не покращився.

Генерація фіч

Робота із зображеннями займає багато часу, і я вирішив зайнятися чимось більш простим.
Як одразу видно, у датасеті є кілька категоріальних ознак, і щоб сильно не морочитися, я просто взяв catboost. Рішення було відмінне, без будь-яких налаштувань я відразу потрапив на перший рядок лідерборду.

Даних досить багато і викладені вони у форматі parquet, тому я недовго думаючи взяв scala і почав писати все на spark.

Найпростіші фічі, які дали більше приросту, ніж ембеддінгів зображень:

  • скільки разів зустрічався objectId, userId і ownerId у даних (має корелювати з популярністю);
  • скільки постів userId бачив у ownerId (має корелювати з інтересом користувача до групи);
  • скільки унікальних userId дивилися пости у ownerId (відображає розмір аудиторії групи).

З timestamps можна було отримати час доби, коли користувач дивився стрічку (ранок/день/вечір/ніч). Поєднавши ці категорії, можна продовжувати генерувати фічі:

  • скільки разів userId заходив увечері;
  • коли частіше показують цей пост (objectId) тощо.

Все це поступово покращувало метрику. Але розмір навчального датасета близько 20М записів, тому додавання фічі сильно уповільнювало навчання.

Я переглянув підхід до використання даних. Хоча дані і є time-dependent, явних витоків інформації «в майбутньому» я не бачив, проте про всяк випадок розбив так:

SNA Hackathon 2019

Наданий навчальний набір (лютий і 2 тижні березня) розбив на 2 частини.
На даних останніх N днів навчав модель. Агрегації, які описав вище, будував на всіх даних, включаючи тест. При цьому з'явилися дані, на яких можна будувати різні енкодинг цільової змінної. Найпростіший підхід полягає в тому, щоб перевикористовувати код, який вже створює нові фічі, і просто подати йому дані, на яких не буде проводитись навчання та target = 1.

Таким чином, вийшли подібні фічі:

  • Скільки разів userId бачив пост у групі ownerId;
  • Скільки разів userId лайкнув пост групі ownerId;
  • Відсоток постів, які userId лайкнув у ownerId.

Тобто вийшов mean target encoding на частині датасету з різних комбінацій категоріальних ознак. В принципі, catboost теж будує target encoding і з цієї точки зору вигоди ніякої, але, наприклад, стало можливим порахувати кількість унікальних користувачів, які лайкали пости в цій групі. У той же час досягнуто основної мети — мій датасет зменшився в кілька разів, і можна було продовжувати генерацію фіч.

У той час як catboost може будувати енкодинг тільки за реакцією liked, у feedback є інші реакції: reshared, disliked, unliked, clicked, ignored, енкодинги за якими можна зробити руками. Я перераховував всілякі агрегати і відсівав фічі з низькою важливістю, щоб не роздмухувати датасет.

На той час я був на першому місці з великим відривом. Бентежило лише те, що ембеддінги зображень майже не давали приросту. Прийшла ідея віддати все на відкуп catboost. Кластеризуємо зображення Kmeans та отримуємо нову категоріальну фічу imageCat.

Ось деякі класи після ручної фільтрації та мерджингу кластерів, отриманих від KMeans.

SNA Hackathon 2019

На основі imageCat генеруємо:

  • Нові категоріальні фічі:
    • Який imageCat найчастіше дивився userId;
    • Який imageCat найчастіше показує ownerId;
    • Який imageCat найчастіше лайкал userId;
  • Різні лічильники:
    • Скільки унікальних imageCat дивився userId;
    • Близько 15 подібних фіч плюс target encoding як описано вище.

Тексти

Результати в конкурсі зображень мене влаштовували, і я вирішив спробувати себе в текстах. Раніше я багато не працював із текстами і, по дурості, вбив день на tf-idf та svd. Потім побачив baseline з doc2vec, який робить те, що мені потрібно. Небагато налаштувавши параметри doc2vec, отримав ембеддінги текстів.

А далі просто перевикористовував код для зображень, у якому замінив ембеддінг зображень ембеддингами текстів. В результаті потрапив на 2 місце у конкурсі текстів.

Колаборативна система

Залишався один конкурс, в який я ще не «потикав ціпком», а судячи з AUC на лідерборді, результати саме цього конкурсу найсильніше мали вплинути на офлайн етапі.
Я взяв усі ознаки, які були у вихідних даних, вибрав категоріальні та розрахував ті ж агрегати, що і для зображень, крім фіч за самими зображеннями. Просто вставивши це в catboost, я потрапив на 2 місце.

Перші кроки оптимізації catboost

Одне перше і два другі місця мене радували, але було розуміння того, що нічого особливого я не зробив, а отже, можна очікувати втрати позицій.

Завдання конкурсу – ранжування постів у рамках користувача, а я весь цей час вирішував завдання класифікації, тобто оптимізував не ту метрику.

Наведу простий приклад:

ідентифікатор користувача objectId прогноз основна правда
1 10 0.9 1
1 11 0.8 1
1 12 0.7 1
1 13 0.6 1
1 14 0.5 0
2 15 0.4 0
2 16 0.3 1

Робимо невелику перестановку

ідентифікатор користувача objectId прогноз основна правда
1 10 0.9 1
1 11 0.8 1
1 12 0.7 1
1 13 0.6 0
2 16 0.5 1
2 15 0.4 0
1 14 0.3 1

Отримуємо такі результати:

Модель AUC User1 AUC User2 AUC mean AUC
Варіант 1 0,8 1,0 0,0 0,5
Варіант 2 0,7 0,75 1,0 0,875

Як видно, поліпшення метрики загального AUC не означає поліпшення середньої метрики AUC в рамках користувача.

Catboost вміє оптимізувати метрики ранжування з коробки. Я почитав про ранжируючі метрики, історії успіху при використанні catboost і поставив навчатися YetiRankPairwise на ніч. Результат вийшов не вражаючим. Вирішивши, що я недонавчився, я змінив функцію помилки на QueryRMSE, яка, судячи з документації catboost, швидше сходиться. У результаті отримав ті ж результати, що й при навчанні на класифікацію, але ансамблі цих двох моделей давали добрий приріст, який вивів мене на перші місця у всіх трьох конкурсах.

За 5 хвилин до закриття онлайн етапу у конкурсі «Коллаборативне системи» Сергій Шальнів посунув мене на друге місце. Подальший шлях ми пройшли разом.

Підготовка до офлайн етапу

Перемога в онлайн етапі нам гарантувала по відеокарті RTX 2080 TI, але головний приз у 300 000 рублів і, швидше за все, фінальне перше місце змусили нас попрацювати ці 2 тижні.

Як виявилося, Сергій теж використав catboost. Ми обмінялися ідеями та фічами, і я дізнався про доповідь Ганни Вероніки Дорогуш в якому були відповіді на багато моїх запитань, і навіть на ті, які в мене на той час ще не з'явилися.

Перегляд доповіді привів мене до думки, що треба повернути всі параметри в дефолтне значення, а налаштування займатися дуже акуратно і тільки після фіксації набору ознак. Тепер одне навчання займало близько 15 годин, але вдалося однією моделлю отримати швидше краще, ніж виходив в ансамблі з ранжуванням.

Генерація фіч

У конкурсі «Колоборативні системи» велика кількість ознак оцінюються як важливі для моделі. Наприклад, auditweights_spark_svd — найважливіша ознака, при цьому немає інформації про те, що вона означає. Я подумав, що варто порахувати різні агрегати, ґрунтуючись на важливих ознаках. Наприклад, середній auditweights_spark_svd за користувачем, групою, об'єктом. Те саме можна порахувати за даними, на яких не проводиться навчання і target = 1, тобто середній auditweights_spark_svd за користувачем по об'єктах, які він гавкав. Важливих ознак, крім auditweights_spark_svd, було кілька. Ось деякі з них:

  • auditweightsCtrGender
  • auditweightsCtrHigh
  • userOwnerCounterCreateLikes

Наприклад, середнє значення auditweightsCtrGender по userId виявилося важливою фічею, так само, як і середнє значення userOwnerCounterCreateLikes за userId+ownerId. Це вже мало змусити задуматися про те, що треба розбиратися зі змістом полів.

Також важливими фічами були auditweightsLikesCount и auditweightsShowsCount. Розділивши одне на інше, вийшла ще важливіша фіча.

Витоку даних

Конкурс та продакшн моделі – це дуже різні завдання. При підготовці даних дуже складно врахувати всі деталі і не передати якусь нетривіальну інформацію про цільову змінну на тесті. Якщо ми створюємо продакшн рішення, то намагатимемося уникнути використання витоків даних при навчанні моделі. Але якщо ми хочемо виграти конкурс, то виток даних — це найкращі фічі.

Вивчивши дані, можна помітити, що за objectId значення auditweightsLikesCount и auditweightsShowsCount змінюються, отже відношення максимальних значень цих ознак значно краще відобразить конверсію посту, ніж ставлення на момент показу.

Перший витік, який ми знайшли, - це auditweightsLikesCountMax/auditweightsShowsCountMax.
А якщо подивитися на дані уважніше? Відсортуємо за датою показу та отримаємо:

objectId ідентифікатор користувача auditweightsShowsCount auditweightsLikesCount target (is liked)
1 1 12 3 напевно ні
1 2 15 3 напевне так
1 3 16 4

Дивно було, коли я знайшов перший такий приклад і виявилося, що моє передбачення не збулося. Але, враховуючи той факт, що максимальні значення цих ознак у рамках об'єкта давали приріст, ми не полінувалися і вирішили знайти auditweightsShowsCountNext и auditweightsLikesCountNextтобто значення в наступний момент часу. Додавши фічу
(auditweightsShowsCountNext-auditweightsShowsCount)/(auditweightsLikesCount-auditweightsLikesCountNext) ми зробили різкий стрибок за швидким.
Аналогічні витоку можна було використовувати, якщо знайти наступні значення для userOwnerCounterCreateLikes в рамках userId+ownerId і, наприклад, auditweightsCtrGender у рамках objectId+userGender. Ми знайшли 6 подібних полів із витоками та максимально витягли з них інформацію.

На той момент ми вичавили максимум інформації з колаборативних ознак, але не поверталися до конкурсів зображень та текстів. З'явилася чудова ідея перевірити: а скільки ж дають безпосередньо фічі за зображеннями чи текстами у відповідних конкурсах?

У конкурсах із зображень та текстів не було витоків, але на той час я повернув дефолтні параметри catboost, зачесал код і додав кілька фіч. Разом вийшло:

Рішення скор
Максимум із зображеннями 0.6411
Максимум без зображень 0.6297
Результат другого місця 0.6295

Рішення скор
Максимум із текстами 0.666
Максимум без текстів 0.660
Результат другого місця 0.656

Рішення скор
Максимум у колаборативних 0.745
Результат другого місця 0.723

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

Подальша генерація ознак у колаборативних системах не давала приросту, і ми зайнялися ранжуванням. На онлайн етапі ансамбль класифікації та ранжирування давав мені невеликий приріст, як виявилося тому, що я недонавчав класифікацію. Жодна з функцій помилок, включаючи YetiRanlPairwise, навіть близько не давала того результату, який давав LogLoss (0,745 проти 0,725). Залишалася надія на QueryCrossEntropy, яку вдавалося запустити.

Офлайн етап

На офлайн етапі структура даних залишилася незмінною, але були невеликі зміни:

  • ідентифікатори userId, objectId, ownerId були перерандомізовані;
  • кілька ознак прибрали та кілька перейменували;
  • даних стало приблизно 1,5 разу більше.

Крім перерахованих складнощів був один великий плюс: на команду виділяли великий сервер із RTX 2080TI. Я довго отримав насолоду від htop.
SNA Hackathon 2019

Ідея була одна – просто відтворити те, що вже є. Витративши кілька годин на налаштування оточення на сервері, ми поступово почали перевіряти, що результати відтворюються. Основна проблема, з якою ми зіткнулися, — збільшення обсягу даних. Ми вирішили трохи зменшити навантаження та встановили параметр catboost ctr_complexity=1. Це трохи знижує швидкий, але моя модель почала працювати, результат був добрий - 0,733. Сергій, на відміну від мене, не розбивав дані на 2 частини та навчався на всіх даних, хоча це й давало найкращий результат на онлайн етапі, на офлайн етапі складнощів виявилося багато. Якщо брати всі фічі, які ми нагенерували, і "в лоб" намагатися засунути в catboost, то нічого не вийшло б і на етапі онлайн. Сергій робив оптимізацію типів, наприклад, перетворення типів float64 у float32. В цій статті можна знайти інформацію щодо оптимізації пам'яті в pandas. У результаті Сергій навчився на CPU на всіх даних і вийшло близько 0,735.

Цих результатів було достатньо для перемоги, але ми приховували свій справжній скор і не могли бути впевненими, що інші команди не роблять те саме.

Битва до останнього

Тюнінг catboost

Наше рішення повністю відтворилося, фічі текстових даних та зображень ми додали, тому залишалося лише тюніти параметри catboost. Сергій навчився на CPU з невеликою кількістю ітерацій, а я навчився на ctr_complexity=1. Залишався один день, і якщо просто додати ітерацій або збільшити ctr_complexity, то можна було до ранку отримати ще кращий швидкий і весь день гуляти.

На етапі офлайн скори можна було дуже легко приховувати, просто вибираючи не найкраще рішення на сайті. Ми чекали різких змін у лідерборді в останні хвилини до закриття сабмітів і вирішили не зупинятися.

З відео Ганни я дізнався, що для покращення якості моделі найкраще підбирати такі параметри:

  • швидкість_навчання — Дефолтне значення розраховується за розміром датасета. Learning_rate вимагає збільшення кількості ітерацій.
  • l2_leaf_reg — Коефіцієнт регуляризації, дефолтне значення 3, бажано вибирати від 2 до 30. Зменшення значення веде до збільшення оверфіту.
  • bagging_temperature — додає рандомізацію ваги об'єктів у вибірці. Дефолтне значення 1, при якому ваги вибираються з експонентного розподілу. Зменшення значення веде до збільшення оверфіту.
  • random_strength — Впливає на вибір сплітів на конкретну ітерацію. Чим вищий random_strength, тим вищий шанс у спліта з низькою важливістю бути обраним. На кожній наступній ітерації рандомність знижується. Зменшення значення веде до збільшення оверфіту.

Інші параметри значно менше впливають на кінцевий результат, тому я не намагався підбирати їх. Одна ітерація навчання на моєму датасеті GPU з ctr_complexity=1 займала 20 хвилин, а підібрані параметри на зменшеному датасеті трохи відрізнялися від оптимальних на повному датасеті. У результаті я зробив близько 30 ітерацій на 10% даних, а потім ще близько 10 ітерацій на всіх даних. Вийшло приблизно таке:

  • швидкість_навчання я збільшив на 40% дефолтного;
  • l2_leaf_reg залишив колишньою;
  • bagging_temperature и random_strength зменшив до 0,8.

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

Я був дуже здивований, коли побачив результат на лідерборді:

Модель модель 1 модель 2 модель 3 ансамбль
Без тюнінгу 0.7403 0.7404 0.7404 0.7407
З тюнінгом 0.7406 0.7405 0.7406 0.7408

Я зробив для себе висновок, що якщо не потрібне швидке застосування моделі, то вибір параметрів краще замінити ансамблем декількох моделей на неоптимізованих параметрах.

Сергій оптимізував розмір датасету для запуску його на GPU. Найпростіший варіант - це відрізати частину даних, але це можна зробити кількома способами:

  • поступово прибирати найстаріші дані (початок лютого), поки датасет не почне поміщатися на згадку;
  • прибрати фічі з найнижчою важливістю;
  • прибрати userId, за яким є лише один запис;
  • залишити тільки userId, які є в тесті.

А зрештою — зробити ансамбль із усіх варіантів.

Останній ансамбль

До пізнього вечора останнього дня ми виклали гурт наших моделей, який давав 0,742. На ніч я запустив свою модель із ctr_complexity=2 і замість 30 хвилин вона навчалася 5 годин. Тільки о 4-й ранку вона дорахувалася, і я зробив останній ансамбль, який на публічному лідерборді дав 0,7433.

За рахунок різних підходів до розв'язання задачі наші передбачення не сильно корелювали, що дало гарний приріст в ансамблі. Для отримання гарного ансамблю краще використовувати сирі передбачення моделі predict(prediction_type='RawFormulaVal') та встановити scale_pos_weight=neg_count/pos_count.

SNA Hackathon 2019

На сайті можна побачити фінальні результати на приватному лідерборді.

інші рішення

Багато команд слідували канонам алгоритмів рекомендаційних систем. Я, не будучи експертом у цій галузі, не можу їх оцінити, але запам'яталося 2 цікаві рішення.

  • Рішення Миколи Анохіна. Микола, будучи співробітником Mail.ru, не претендував на призи, тому ставив собі за мету не отримання максимального швидка, а отримання рішення, що легко масштабується.
  • Рішення команди, що отримала приз журі, засноване на цій статті від facebook, дозволило дуже добре кластеризувати зображення без ручної роботи.

Висновок

Що найбільше відклалося у пам'яті:

  • Якщо в даних є категоріальні фічі, і ви знаєте, як правильно робити target encoding, все одно краще спробувати catboost.
  • Якщо ви берете участь у конкурсі, не варто витрачати час на вибір параметрів, крім learning_rate та iterations. Швидше рішення — створити ансамбль кількох моделей.
  • Бустинги можуть навчатися на GPU. Catboost вміє дуже швидко навчатися на GPU, але їсть багато пам'яті.
  • Під час розробки та перевірки ідей краще встановити невеликий rsm~=0.2 (CPU only) та ctr_complexity=1.
  • На відміну від інших команд ансамбль наших моделей дав великий приріст. Ми обмінювалися лише ідеями та писали різними мовами. У нас був різний підхід до розбиття даних і, гадаю, у кожного були свої баги.
  • Незрозуміло, чому оптимізація ранжирування давала результат гірше, ніж оптимізація класифікації.
  • Я отримав невеликий досвід роботи з текстами та розуміння, як робляться рекомендаційні системи.

SNA Hackathon 2019

Дякую організаторам за отримані емоції, знання та призи.

Джерело: habr.com

Додати коментар або відгук