SmartMailHack. История победителей в задаче Name Entity Recognition

в 14:33, , рубрики: data mining, python, машинное обучение, хакатон, это действительно кто-то читает?

На прошедших выходных (20-22 апреля) в офисе Mail.ru Group прошел студенческий хакатон по машинному обучению. Хакатон объединил студентов разных ВУЗов, разных курсов и, что самое любопытное, разных направлений: от программистов до безопасников.

SmartMailHack. История победителей в задаче Name Entity Recognition - 1

От Почты Mail.ru было предоставлено три задачи:

  1. Распознавание и классификация логотипов компаний. Эта задача полезна в антиспаме для детектирования фишинговых писем.
  2. Требовалось по тексту письма предсказать какие из его частей относятся к определенным категориям. Задача распознавания именованных сущностей (Named Entity Recognition, NER)
  3. Последняя задача являлась свободной в реализации. Необходимо было придумать и сделать прототип новой полезной функции для Почты. Критериями оценки являлись полезность, качество реализации, применение ML и хайповость фичи.


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

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

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

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

Письмо парсится построчно, каждая строка разбивается на токены и каждому токену ставится метка(label).

Пример размеченных данных

# [](http://t.adidas-news.adidas.com/res/adidas-t/spacer.gif) Итого к оплате
39 []( OUT
39 http OUT
39 :// OUT
39 t OUT
39 . OUT
39 adidas OUT
39 - OUT
39 news OUT
39 . OUT
39 adidas OUT
39 . OUT
39 com OUT
39 / OUT
39 res OUT
39 / OUT
39 adidas OUT
39 - OUT
39 t OUT
39 / OUT
39 spacer OUT
39 . OUT
39 gif OUT
39 ) OUT
39 Итого B-PRICE
39 к PRICE
39 оплате PRICE

В размеченном файле сначала идет распаршенная строка, начинающаяся с символа "#", затем результата парсинга в виде трех колонок: (номер строки, токен, метка класса).

Метки бывают следующих типов:

  • Артикул товара: B-ARTICUL, ARTICUL
  • Заказ и его номер: B-ORDER, ORDER
  • Итоговая сумма заказа: B-PRICE, PRICE
  • Заказанные товары: B-PRODUCT, PRODUCT
  • Тип товара: B-PRODUCT_TYPE, PRODUCT_TYPE
  • Продавец: B-SHOP, SHOP
  • Все остальные токены: OUT

Префикс «B-» обозначает начало токена в предложение, обязателен. Для оценки модели использовалась f1-метрика по всем меткам кроме OUT.

Скор для логрега как бейзлайна

Training time 157.34269189834595 s
================================================TRAIN======================================
+++++++++++++++++++++++ ARTICUL +++++++++++++++++++++++
Tokenwise precision: 0.0 Tokenwise recall: 0.0 Tokenwise f-measure: 0.0
+++++++++++++++++++++++ ORDER +++++++++++++++++++++++
Tokenwise precision: 0.7981220657276995 Tokenwise recall: 0.188470066518847 Tokenwise f-measure: 0.30493273542600896
+++++++++++++++++++++++ PRICE +++++++++++++++++++++++
Tokenwise precision: 0.9154929577464789 Tokenwise recall: 0.04992319508448541 Tokenwise f-measure: 0.09468317552804079
+++++++++++++++++++++++ PRODUCT +++++++++++++++++++++++
Tokenwise precision: 0.6538461538461539 Tokenwise recall: 0.0160075329566855 Tokenwise f-measure: 0.03125000000000001
+++++++++++++++++++++++ PRODUCT_TYPE +++++++++++++++++++++++
Tokenwise precision: 0.5172413793103449 Tokenwise recall: 0.02167630057803468 Tokenwise f-measure: 0.04160887656033287
+++++++++++++++++++++++ SHOP +++++++++++++++++++++++
Tokenwise precision: 0.0 Tokenwise recall: 0.0 Tokenwise f-measure: 0.0
+++++++++++++++++++++++ CORPUS MEAN METRIC +++++++++++++++++++++++
Tokenwise precision: 0.7852941176470588 Tokenwise recall: 0.05550935550935551 Tokenwise f-measure: 0.1036893203883495
================================================TEST=======================================
+++++++++++++++++++++++ ARTICUL +++++++++++++++++++++++
Tokenwise precision: 0.0 Tokenwise recall: 0.0 Tokenwise f-measure: 0.0
+++++++++++++++++++++++ ORDER +++++++++++++++++++++++
Tokenwise precision: 0.8064516129032258 Tokenwise recall: 0.205761316872428 Tokenwise f-measure: 0.3278688524590164
+++++++++++++++++++++++ PRICE +++++++++++++++++++++++
Tokenwise precision: 0.8666666666666667 Tokenwise recall: 0.05263157894736842 Tokenwise f-measure: 0.09923664122137404
+++++++++++++++++++++++ PRODUCT +++++++++++++++++++++++
Tokenwise precision: 0.4 Tokenwise recall: 0.0071174377224199285 Tokenwise f-measure: 0.013986013986013988
+++++++++++++++++++++++ PRODUCT_TYPE +++++++++++++++++++++++
Tokenwise precision: 0.3333333333333333 Tokenwise recall: 0.011627906976744186 Tokenwise f-measure: 0.02247191011235955
+++++++++++++++++++++++ SHOP +++++++++++++++++++++++
Tokenwise precision: 0.0 Tokenwise recall: 0.0 Tokenwise f-measure: 0.0
+++++++++++++++++++++++ CORPUS MEAN METRIC +++++++++++++++++++++++
Tokenwise precision: 0.7528089887640449 Tokenwise recall: 0.05697278911564626 Tokenwise f-measure: 0.10592885375494071

Распределение меток в обучающем датасете выглядит следующим образом:

SmartMailHack. История победителей в задаче Name Entity Recognition - 2
Меток класса OUT почти 580k

Виден сильный дисбаланс классов, а метку OUT вообще не имеет смысла учитывать для оценки качества модели. Чтобы учесть дисбаланс классов в лес был добавлен параметр class_weight='balanced', что хоть и уменьшило скор на трейне и на тесте ( с 0.27 и 0.15 до 0.09 и 0.08), однако позволило избавиться от переобучения (уменьшилась разница между этими показателями).

Модели

Для представления слов в виде вектора использовался fastText'овский word embedding для русского языка, что позволило представить токен в виде вектора из 300 значений. Пока часть команды пыталась написать нейронку, были опробованы стандартные алгоритмы классификации, такие логрег, случайный лес, knn и xgboost. По итогам проб был выбран случайный лес в качестве запасного варианта на случай, если нейронка не взлетит. Выбор был обоснован во многом из-за хорошей скорости обучения и предсказания модели (что сильно спасло под конец соревнования) при удовлетворительном качестве модели на фоне других моделей.

Имея опыт прохождения курса mlcourse_open от ODS и понимая, что значительно повысить скор
могут только качественные фичи, на генерацию которых и было направлено оставшееся время. Первое, что пришло на ум — добавить простые признаки, такие как индекс токена в предложении, длина токена, является ли токен буквенно-числовым, состоит только из прописных или строчных букв и т.п. Это дало прирост метрики f1 до 0.21 на тестовой выборке. При дальнейшем изучении датасета был сделан вывод, что важен контекст, и в зависимости от него два одинаковых токена могли иметь различные метки класса. Чтобы учесть контекст было взято окно — к вектору признаков добавлялись предыдущий и последующий токены. Что увеличило скор уже до 0.55 на трейне и 0.43 на тесте. Последнюю ночь хакатона мы пытались увеличить окно и впихнуть больше признаков в 12 гигабайт оперативки ноута. Как оказалось оно не впихивается. Бросив эти попытки начали думать какие еще признаки можно добавить в модель. Обратились к библиотеки pymorphy2, но прикрутить ее должным образом не успели.

Сабмит

До выдачи тестового датасета и первого сабмита оставалось пару часов. После выдачи датасета давался час на то, чтобы сделать предсказания и отправить организаторам — это был первый сабмит. После этого давался еще час на вторую попытку. Итак, пришло время начать делать препроцессинг и обучить лес на всей выборке. Также нас все еще не покидала вера в нейронку. Препроцессинг и обучение леса из 50-ти деревьев прошло на удивление быстро: минут 10 на препроцессинг (вместе с 5-ти минутной загрузкой словаря для эмбеддинга) и еще 10 минут на обучение леса на матрице размером (609101, 906), что не могло нас не радовать. Ведь это говорило о том, что мы сможем быстро поправить модель ко второму сабмиту и заново ее обучить. Обученный лес показал 0.59 скора на всей выборке. Учитывая предыдущие тестирования модели на отложенной выборке мы надеялись показать результат не меньше 0.4 на лидерборде, ну, или хотя бы не меньше 0.3.

Получив тестовый датасет из ~300000 токенов и имея уже обученную модель мы быстро сделали предсказание, буквально, за 2 минуты. Стали первыми и получили скор, равный 0.2997. Ожидая результаты других команд и обдумывая планы по улучшению собственной модели, к нам пришла идея добавить к обучающей выборке только что размеченную тестовой. Во-первых, это не противоречило правилам, так как запрещалась ручная разметка, во-вторых нам самим стало интересно что из этого получится. В это время узнали результаты других команд — они все оказались позади нас, чему мы приятно удивились. Однако ближайшей к нам результат был равен 0.28, что давало соперникам шанс нас обойти. Также мы не были уверены, что у соперников не было припасено козырей в рукаве. Второй час прошел напряженно, команды держали сабмиты до последнего, а наша идея с увеличением обучающей выборки провалилась — ноуту не понравилась идея впихнуть в его память в 1.5 раза больше данных и в знак своего протеста отвечал зависаниями и MemoryError. Когда время истекло появился окончательный лидерборд, некоторые команды улучшили результат, а некоторых результат ухудшился, а мы по-прежнему были на первом месте. Однако предстояла валидация ответа — необходимо было показать рабочую модель и сделать предсказание перед организаторами, а у нас только что перезагрузившийся ноут, на которым и обучались деревья, а модель не была сохранена. Что делать? Тут нам на встречу пошли организаторы и согласились подождать, пока модель обучится и сделает предсказания. Однако была загвоздка, лес то случайный, SEED мы не зафиксировали, а точность предсказания нужно было подтвердить в точности до последней цифры. Мы надеялись на лучшее, а также запустили обучение на втором ноуте, где уже был загружен словарь. В это время организаторы тщательно исследовали данные и код и расспрашивали о модели, фичах. Ноут не переставал действовать на нервы и иногда подвисал на несколько минут так, что даже время не менялось. Когда обучение завершилось сделали повторный предикт и отправили организаторам. В это время обучение завершил второй ноут, а предсказания на обучающей выборке на обоих ноутах совпали идеально, что повысило нашу веру в успешную валидацию результата. И вот, спустя несколько секунд, организаторы поздравляют с победой и жмут руки :)

Итог

Мы отлично провели выходные в офисе Mail.ru, послушали интересные доклады на тему ML и DL от команды Почты. Наслаждались бесконечным запасами печенек, молочного шоколада (молоко, однако, оказалось ресурсом конечным, но восполняемым) и пиццой, а также незабываемой атмосферой и общением с интересными людьми.

Если рассмотреть саму задачу и наш опыт, то можно сделать следующие выводы:

  • Не стоит гнаться за модой и сразу применять DL, возможно, классические модели могут неплохо взлететь.
  • Генерируйте больше полезных фич, а уже потом оптимизируйте модель и подбирайте гиперпараметры.
  • Фиксируйте SEED и сохраняйте свои модели, а также делайте бекапы :)

SmartMailHack. История победителей в задаче Name Entity Recognition - 3
Команда «EpicTeam» — победители в задаче NER. Слева направо:

  • Леонид Жариков, МГТУ им. Баумана, студент ТехноПарка.
  • Андрей Атаманюк, МГТУ им. Баумана, студент ТехноПарка.
  • Милия Фатхутдинова, РЭУ им. Плеханова.
  • Андрей Пашков, НИЯУ «МИФИ», студент ТехноАтома.

Автор: Mahpella

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js