python

QML: как легко получать футболки в конкурсах mail.ru по машинному обучению

  • среда, 19 июля 2017 г. в 03:13:18
https://habrahabr.ru/post/333554/
  • Программирование
  • Машинное обучение
  • Python




В субботу завершился месячный конкурс по машинному обучению от mail.ru ML bootcamp 5. я занял в нем 14ое место. Это уже третий мой конкурс, в котором я выиграл одежду и за время участия у меня сформировался фреймворк (который я, недолго думая, назвал QML, сокращение от ника и machine learning) для помощи в подборе решения в подобных соревнованиях. На примере решения ML bootcamp 5 я опишу как им пользоваться.


Как полагается, сперва покажу товар лицом :)


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

Итак, начнем решать конкурс (целиком скрипты решения можно посмотреть здесь).


Для начала склонируем репозиторий фреймворка, установим недостающие пакеты описанные в файле install и укажем настройки в config.py:


Далее создадим нужные таблицы в БД из файла со схемой qml/db/schema.sql, создадим файлы с id train и test set (qml_workdir/data/ids_test.csv, qml_workdir/data/ids_train.csv) и файлы с первой версией данных (qml_workdir/data/v0001_test_x.csv, qml_workdir/data/v0001_train_x.csv, qml_workdir/data/train_y.csv). Как это сделать оставлю за рамками этой статьи.


Теперь создадим файл work.py, в котором запишем код ниже и запустим его


# imports
cv = QCV(qm)
print(cv.cross_val(1, 1))
qm.qpredict(1, 1)

Вывод будет примерно таким:
    1 0.545776154551
    2 0.545405471315
    3 0.543444368229
    4 0.539330473549
    5 0.537107693689
0.542212832267

Тремя строчками кода мы:


  • провели кроссвалидацию по 5 фолдам (хардкод️) модели №1 из файла models.py
  • зафиксировали разбиение на фолды, чтобы в дальнейшем было удобно сравнивать результаты
  • увидели результат каждого из фолдов и усредненный результат по фолдам
  • сохранили результаты кроссвалидации в БД (см таблички qml_results и qml_results_statistic)
  • получили файл с предсказаниями для test set (qml_workdir/data/res/m0000001_d001__tr_70000_ts_30000__h_29c96aaed585f02e30096f265faca72c_s_1000.csv)

Удалим колонку с id и заголовок и отошлем на сайт конкурса. Результат будет примерно таким:




С таким результатом на public board мы были бы на 497 месте, а на private board на 484 месте.


Ну что ж, маловато. Самое время взглянуть на данные.


Видим, что в полях weight, height, ap_hi и ap_lo много непонятных данных, чистим их, добавляем индекс массы тела и классы ИМТ из таблички.


В test set некоторые субъективные признаки были изъяты, посмотрев на распределение значений этих колонок заменим NaN в active на 1, в smoke и alco на 0. Итого у нас уже есть 6ая версия данных.


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


Запустим кроссвалидацию на новых данных и приготовим файл для отправки:
# imports
cv = QCV(qm)
print(cv.cross_val(1, 6))
qm.qpredict(1, 6)

Вывод:
    1 0.545776154551
    2 0.545405471315
    3 0.543444368229
    4 0.539330473549
    5 0.537107693689
0.542212832267

Результат:




477 на public board и 470 на private, С — стабильность :) Но нам стабильности мало, в это жаркое лето нужно ходить в клевых черных футболках.


Оба раза мы прогоняли данные через xgboost с параметрами, поставленными наугад. Подберем оптимальные параметры модели с помощью hyperopt.


Пример подбора параметров. Из интересного в скрипте:


  • DATAS = [6] — можно тюнить сразу на нескольких версиях данных
  • Параметр early_stop_cv останавливает кроссвалидацию с текущими параметрами и возвращает результат текущего фолда, если callback вернет True, удобно вручную отсортировать фолды в сохраненных разбиениях, чтобы первым шел фолд с наибольшим CV score. Тогда можно отсеивать неудачные модели не дожидаясь валидации по всем фолдам
  • num_boost_rounds и eta перебираем с шагом в 1.3
  • model_id = qm.add_by_params(.. добавляем в базу модель с такими параметрами, чтобы ее в дальнейшем можно было загрузить по номеру
  • tree_method='hist' — для хейтеров продуктов Microsoft
  • seed=1000 — прогоняем каждую модель по 8 сидам, чтобы выявить стабильность
  • результаты предсказаний каждого фолда из разбиений для каждого сида сохраняются, что позже нам пригодится для метамоделей

Оставив на ночь подбор параметров утром можно посмотреть, что у нас натюнилось с помощью


mysql запроса
select 
    data_id, 
    model_id,
    max(cv_diff), 
    max(std), 
    sum(sum_sc)/sum(cnt), 
    sum(cnt), 
    r.cv_time, 
    m.cls,
    m.level, 
    m.params
from
    (
        select 
            model_id, 
            fold, 
            data_id, 
            sum(cv_score) as sum_sc, 
            std(cv_score) as std, 
            max(cv_score)-min(cv_score) as cv_diff, 
            count(*) as cnt
        from 
            qml_results_statistic

        group by 
            model_id, 
            data_id, 
            fold

    ) q
    left join qml_results r using (model_id, data_id)
    left join qml_models m using (model_id)
group by 
    data_id, 
    model_id
order by 
    sum(sum_sc)/sum(cnt)

limit 10000;

Примерный результат:




Видим, что модель 1255 лучше по CV, но и разброс одного фолда среди 8 сидов больше. Дальше для проверки добавления признаков будем использовать модель 1395, как наиболее стабильную из найденных.


Т.к. CV score заметно улучшился, попробуем отправить:




О, 161 место на public leaderboard и 164 на private! Лучше, чем Лев Литроводочкин, который был на паблике седьмым, Владимир Омельченко (десятый) и Андей Парков (четырнадцатый).


Самое время обогнать Валерия Бабушкина!


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


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


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


Из интересного:


  • QAvgOneModelData(model_id, 8) — прогоняем кроссвалидацию не исходной модели, а усреднения предсказаний исходной модели с 8ю разными сидами для большей стабильности
  • early_stop_cv — опять не гоняем все разбиения
  • log_file='qml_workdir/logs/feat12.txt' — результат работы пишем в лог файл, отсортировав который можно легко получить лучшую колонку текущего прогона.

В результате выяснили, что добавив колонки ниже (47 версия данных), CV улучшается:


Колонки
  • x__age__gluc_all
  • x__ap_hi__cholesterol_all
  • div6__height__gluc_all__imt1/height/gluc_all*imt
  • plus__age_norm__ap_hi_norm__gluc_all_norm
  • x__age__weight
  • div1__age__weight__cholesterol_allage*weight/cholesterol_all
  • div6__age__weight__cholesterol_all1/age/weight*cholesterol_all
  • plus__height_norm__weight_norm__gluc_all_norm
  • div1__ap_hi__ap_lo__cholesterol_allap_hi*ap_lo/cholesterol_all
  • div6__ap_hi__ap_lo__cholesterol_all1/ap_hi/ap_lo*cholesterol_all
  • plus__age_norm__gluc_all_norm__imt_norm
  • minus6__ap_hi_norm__ap_lo_norm__cholesterol_all_norm
  • minus1__ap_hi_norm__ap_lo_norm__cholesterol_all_norm
  • minus6__age_norm__ap_lo_norm__cholesterol_all_norm
  • minus1__age_norm__ap_lo_norm__cholesterol_all_norm
  • div6__height__weight__ap_lo1/height/weight*ap_lo
  • div2__ap_lo__cholesterol_all__gluc_all
  • x__age__ap_hi__gluc_all
  • div5__ap_lo__cholesterol_all__gluc_all

Хорошо, хоть в этом списке не оказалось колонки imt * height * height.


Прогоним нашу модель на новых данных:




Ооо, 40 место на public и 81 на private. Валерий далеко позади, но впереди маячит нефтяные гипнотизеры чата. Нужно догонять.


Садимся устраивать мозговой штурм:



Реализуем все это (66 версия данных) и еще раз прогоняем тюнинг модели на итоговых данных.


Новая лучшая модель
{"alpha": 0.008, "booster": "gbtree", "colsample_bylevel": 0.6, "colsample_bytree": 1.0, "eta": 0.004, "eval_metric": "logloss", "gamma": 0.2, "max_depth": 4, "num_boost_round": 2015, "objective": "binary:logistic", "subsample": 0.7, "tree_method": "hist"}

После этого поочередно выкидываем по одной колонке и выясняем, что без колонки div6__height__weight__ap_lo CV score лучше и нещадно ее выкидываем (69 версия).


Прогоним новую лучшую одиночную модель на исходных данных:




Public — 26, private — 50. Ура! Гипнотизеров обошли, футболка наша!


Вроде бы пора остановиться, но хочется большего. Отправим усреднение по 8 сидам этой модели:




Public — 21 место (эти усреднения были выбраны в качестве одного из решений, можно найти под №0109872 из дампа), private — 34 место. Гипнотизеров обошли и на паблике.


Хорошо, но мало. Нагенерируем различных моделей и еще чуть-чуть. Для некоторых моделей выполнена нормализация и заполнение NaN.


Усредняем и стэкаем лучшие модели на различных данных. После этого еще раз усредняем лучшие модели второго уровня.


Из интересного:


  • Т.к. результаты кроссвалидации моделей сохраняются, то просчет усреднений происходит
    мгновенно
  • Первый просчет стэкинга для модели занимает какое-то время, дальше сохраненные результаты переиспользуются
  • Kaggle Ensembling Guide
  • Cтекинг (Stacking) и блендинг (Blending)
  • Изначально я в качестве моделей второго уровня брал xgboost и LogisticRegression, но результата это не дало. Потом я вспомнил, что Валерий Бабушкин в чате упоминал гребневую регрессию, когда рассказывал про мерседес, погуглил, нашел статью из пункта выше, стал ее использовать в качестве модели второго уровня и все получилось. А я его обошел одиночным xgboost. Нехорошо получилось.

Моя вторая итоговая модель — это усреднение 7 стэкингов на разном количестве моделей с разными данными. Если интересно расковарять, то это модель №109639 из дампа


На public board эта модель давала результат хуже, чем просто усреднение по 8 сидам различных одиночных xgboost, но я верил кроссвалидации и выбрал ее. Из усреднений xgboost я выбрал такое усреднение, чтобы результаты CV на фолдах отличались от стэкинговой модели, т.е. на некоторых фолдах стэкинговая модель была заметно лучше (-0.0001), на других — заметно хуже


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


1 Никита Чуркин
2 Рим Шаяхметов
3 Иван Филонов
4 Александр Я-МОРТИДО Киселев
9 Иван Тямгин (осторожно, Р-р-р)