Скоро відкриття ML Boot Camp III



15 лютого стартує Machine Learning Boot Camp III — третє змагання з машинного навчання та аналізу даних від Mail.Ru Group. Сьогодні розповідаємо про минулий контесті і відкриваємо таємниці нового! Отже, в ході майбутнього конкурсу потрібно буде вгадати, чи залишиться учасник в онлайн-грі або піде з неї. Вибірки для завдання побудовані на дванадцяти ігрових ознаках для 25000 користувачів. Природно, всі дані анонимизированы.

Самі ознаки:

  • maxPlayerLevel — максимальний рівень гри, який пройшов гравець;
  • numberOfAttemptedLevels — кількість рівнів, які спробував пройти гравець;
  • attemptsOnTheHighestLevel — число спроб, зроблених на самому високому рівні;
  • totalNumOfAttempts — загальне число спроб;
  • averageNumOfTurnsPerCompletedLevel — середня кількість ходів, виконаних на успішно пройдених рівнях;
  • doReturnOnLowerLevels — робив гравець повернення до гри на вже пройдених рівнях;
  • numberOfBoostersUsed — кількість використаних бустерів;
  • fractionOfUsefullBoosters — кількість бустерів, використаних під час успішних спроб (гравець пройшов рівень);
  • totalScore — загальну кількість набраних очок;
  • totalBonusScore — загальна кількість набраних бонусних очок;
  • totalStarsCount — загальна кількість набраних зірок;
  • numberOfDaysActuallyPlayed — кількість днів, коли користувач грав у гру.
Тестову вибірку ми розіб'ємо випадковим чином в співвідношенні 40/60. Результат на перших 40 % буде визначати положення учасників у рейтинговій таблиці на всьому протязі конкурсу. Результат на 60 % стане відомий після закінчення конкурсу, і саме він визначить фінальну розстановку учасників.

Власникові кращого рішення ми подаруємо MacBook Air. Другого і третього місця дістанеться Apple iPad. Четвертого, п'ятого та шостого — Apple iPod Nano. За традицією, 50 кращих учасників отримають пам'ятні футболки з символікою чемпіонату. Крім того, кращих з кращих ми запросимо Mail.Ru Group для співбесіди на позиції, пов'язані з аналізом даних. Зареєструватися на чемпіонат можна тут.

Machine Learning Boot Camp II
Щоб учасники краще розуміли, що їм належить, представляємо завдання минулого чемпіонату і краще рішення від переможця.

Завдання. Учасники другого контесту зіткнулися із завданням «Оцінка продуктивності». Ми запропонували їм навчити комп'ютер передбачати час множення двох матриць розміру
mxk
та
kxn
на тестовій обчислювальній системі. Учасники знали, скільки часу ця задача вирішувалася на інших системах, розміри матриць і параметри систем.

В якості критерію якості вирішення задачі ми використовували найменшу середню відносну помилку (MAPE, в деяких джерелах згадується як MRE) для реалізацій, що працюють довше однієї секунди:

$MAPE = \frac{1}{N}\sum_{i=1}^{N}\frac{|y_{i} - g_{i}|}{y_{i}}, y_{i} > 1$

де N — кількість об'єктів у вибірці, yі — справжнє час роботи в i-му експерименті, gі — передбачений час. Перше місце в конкурсі зайняв Михайло Карачун, про його методику рішення — далі з його слів.

Історія переможця
Загальна методика розв'язання подібних задач описана в доданому до змагання документ. Основна ідея полягає в тому, щоб прогнозувати не час обчислення time, а величину time/(m*n*k). Причому ця величина в рамках однієї системи коливається незначно, і при навчанні моделі на декількох системах відразу краще використовувати нелінійні методи, наприклад, випадковий ліс.

Крок нульовий — common

Для вирішення використовувався python, перевірка результату локально відбувалася за допомогою методу kfold з 5 разбиениями. При підготовці даних категоріальні ознаки пройшли перетворення «одне значення ознака — одна колонка» (one hot

Крок перший — model selection

Для вибору параметрів моделі використовувався модуль hyperopt. Він працює краще простого пошуку по сітці, так як не просто перебирає параметри, а намагається їх оптимізувати.

Приклад
# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.cross_validation import KFold
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
import random
random.seed(1)
def mean_absolute_percentage_error(y_true, y_pred):
ind = y_true > -1
return np.mean(np.abs((y_true[ind] - y_pred[ind]) / y_true[ind]))
def loss_func(y_true, y_pred):
return mean_absolute_percentage_error(y_true,y_pred)
all_train = pd.read_csv('~/Projects/DataMining/Bimbo/data/train1.csv')
all_target = pd.read_csv('~/Projects/DataMining/Bimbo/data/y_train.csv')
all_train['TARGET'] = all_target['time']
cols_to_drop = ['ID','TARGET']
cols = list(set(all_train.columns)-set(cols_to_drop))
print(len(cols))
def hyperopt_train_test(hpparams):
all_results = []
kf = KFold(len(all_train['TARGET'].values),n_folds=5,random_state=1, shuffle=True)
for train_index, test_index in kf:
train = all_train.ix[train_index,:]
test = all_train.ix[test_index,:]
X_train = train[cols].values
y_train_c = train['n'].values*train['m'].values*train['k'].values
y_train = train['TARGET'].values
X_test = test[cols].values
y_test_c = test['n'].values*test['m'].values*test['k'].values
y_test = test['TARGET'].values
params_est = {'n_estimators':int(hpparams['n_estimators']),
'learning_rate':hpparams['eta'],
'max_depth':hpparams['max_depth'],
'min_samples_split':hpparams['min_samples_split'],
'min_samples_leaf':hpparams['min_samples_leaf'],
'loss':hpparams['loss'],
'alpha':hpparams['alpha'],
'subsample':hpparams['subsample'],
'random_state':1}
bst = GradientBoostingRegressor(**params_est)
bst.fit(X_train, np.log(y_train/y_train_c))
y_test_pred = np.exp(bst.predict(X_test))*y_test_c
current_res = loss_func(y_test, y_test_pred)
all_results.append(current_res)
return np.mean(all_results)
space4dt = {
'min_samples_split': hp.quniform('min_samples_split', 3, 14, 1),
'min_samples_leaf': hp.quniform('min_samples_leaf', 1, 7, 1),
'subsample': hp.quniform('subsample', 0.6, 0.99, 0.001),
'eta': hp.quniform('eta', 0.07,0.2, 0.001),
'n_estimators': hp.quniform('n_estimators', 10, 1000, 10),
'max_depth': hp.choice('max_depth', (4,5,6,7,8,9,10)),
'alpha': hp.quniform('alpha', 0.01, 0.99, 0.01),
'loss':hp.choice('loss', ('ls', 'lad', 'huber', 'quantile')),
}
def f(params):
acc = hyperopt_train_test(params)
print(acc)
print(params)
return {'loss': acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4dt, algo=tpe.suggest, max_evals=2000, trials=trials)
print 'best:'
print best

Після першого прогону з невеликою кількістю дерев найкраще себе показав GradientBoostingRegressor(loss='lad').

Крок другий — feature engineering

На другому етапі було поставлено завдання відсіяти зайві ознаки, так як всього їх виявилося ~ 1100. Для цього був використаний метод рекурсивного відбору. Він полягає в послідовному виключенні N ознак за оцінкою на основі крос-валідації. На виході алгоритм в параметрі ranking_ зберігається етап, на якому був відсіяний ознака: 1 — значить він залишився до кінця, чим більше — тим гірше. Параметр support_ зберігає маску обраних ознак, тобто тих, що в ranking_ з одиницею. Потрібно відзначити, що не завжди остаточний варіант кращий, іноді для рішень можна використовувати ознаки, які були відсіяні ближче до кінця відбору. Ця процедура відбувалась досить довго, наприклад, на моєму ноутбуці з середньою продуктивністю вона зайняла більше 12 годин.

Приклад
# -*- coding: utf-8 -*-
from sklearn.feature_selection.rfe import RFECV
from sklearn.ensemble import GradientBoostingRegressor
import numpy as np
bst = GradientBoostingRegressor(**params_est)
selector = RFECV(bst, step=50, cv=5)
selector.fit(all_train[cols], target)
print(list(selector.ranking_ ))
print(np.asarray(cols)[selector.support_ ])

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

Приклад
df.ix[:, 'cpuExtra1'] = 0
df.ix[df['cpuFull'] == 'Intel® Core(TM) i3-2310M CPU @ 2.10 GHz', 'cpuExtra1'] = 1
df.ix[:, 'cpuExtra2'] = 0
df.ix[df['cpuFull'] == 'Intel® Atom(TM) CPU N550 @ 1.50 GHz', 'cpuExtra2'] = 1
# це нові вдалі ознаки
df.ix[:, 'm_div_n'] = df['m'] / df['n']
df.ix[:, 'magic'] = df['k'] * df['m'] * df['n'] / (df['cpuCount'] * df['cpuCount'])
cols = [
'n',
'Sequential_read_128B_by128',
'k',
'Random_write_3MB_by128',
'cpuCount',
'Sequential_write_32kB_by128',
'Random_read_9MB_by128',
'm',
'SeqRead_20kB_by256',
'cpuCores',
'Sequential_read_48MB_by128',
'Random_read_4MB_by32',
'Random_write_32MB_by128',
'Random_read_2MB_by32',
'SeqCopy10MB_by128',
'BMI',
'm_div_n',
'magic',
'cpuExtra1',
'cpuExtra2',
'Random_write_bypassing_cache_6kB_by128',
'Sequential_read_192kB_by32',
]

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

Крок третій — ensembling

Ансамблі. Якщо об'єднати дерева рішень, навчені на різних подмножествах ознак, то результат перевершує по ефективності окремо взяте дерево — так працює random forest. Але якщо взяти кілька різних ансамблів і об'єднати їх вирішення, то це теж може допомогти. Як показує загальна практика і мій особистий досвід, якщо ви маєте декілька моделей з приблизно однаковим результатом, то їх середня майже завжди краще. І чим більше відрізняються ці моделі за логікою побудови — тим краще. Наприклад, якщо ви вирішите взяти два random forest з однаковими параметрами, але різною кількістю дерев — це навряд чи допоможе. А якщо взяти random forest і gradient boosting regressor майже завжди виходить краще, іноді це саме те що потрібно, якщо мова йде про двох або трьох знаків після коми.

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

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

Приклад
# це три основні моделі
params_est = {'n_estimators': 370,
'subsample': 0.961,
'learning_rate': 0.076,
'min_samples_split': 18.0,
'max_depth': 6,
'min_samples_leaf': 8.0,
'random_state':1,
'loss':'lad',}
bst1 = GradientBoostingRegressor(**params_est)
bst1.fit(X_train, y_train/y_train_c1)
params_est = {'n_estimators': 680,
'subsample': 0.902,
'learning_rate': 0.076,
'min_samples_split': 14.0,
'alpha': 0.29,
'max_depth': 9,
'min_samples_leaf': 5.0,
'loss':'quantile',
'random_state':1}
bst2 = GradientBoostingRegressor(**params_est)
bst2.fit(X_train, y_train/y_train_c1)
params_est = {'n_estimators': 430,
'subsample': 0.978,
'learning_rate': 0.086,
'min_samples_split': 19.0,
'max_depth': 6,
'min_samples_leaf': 10.0,
'loss':'lad',
'random_state':1}
bst3 = GradientBoostingRegressor(**params_est)
bst3.fit(X_train, y_train/y_train_c1)

Крок четвертий — we need to go deeper

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

Першою ідеєю було скоригувати ваги об'єктів при навчанні, збільшивши їх для даної ос, але результат тільки погіршився. Так як даних для цієї ос досить мало (<100), то окрему модель теж не налаштувати — переобучится. Допомогло проміжне рішення. Я взяв окрему модель, навчив її на всій вибірці, але з вагами, отдававшими пріоритет даної ос. При обчисленні підсумкової результат ця модель використовувалася тільки для однієї ос. Тобто основні моделі навчалися на всій вибірці без ваг і передбачали результат для всіх ос крім однієї. Одна модель навчалася на всій вибірці з вагами, і прогнозувала лише для однієї ос. Тут вперше виникли труднощі з крос-валідацією — локальне поліпшення не завжди підтверджувалося публічною оцінкою. Це пов'язано з тим, що кількість прикладів для однієї ос досить мало, і якщо вони ще розбиваються на декілька частин для перевірки, то стабільного результату чекати не доведеться.

Приклад
# це ваги для окремої моделі
all_train['w'] = 1
all_train['w'][all_train['os'] == 15] = 4
# це окрема модель для складної os = 15
params_est = {'n_estimators': 480,
'subsample': 0.881,
'learning_rate': 0.197,
'min_samples_split': 3.0,
'max_depth': 7,
'min_samples_leaf': 2.0,
'loss':'lad',
'random_state':1}
bst4 = GradientBoostingRegressor(**params_est)
bst4.fit(X_train, np.log(y_train/y_train_c), sample_weight=train['w'])

Крок п'ятий — last step

Маючи досить непоганий набір моделей було зроблено ще одне припущення, яке принесло досить хороший результат. Якщо потрібно мінімізувати відносне відхилення, чому б саме його і не передбачати? Тут постараюся докладніше. Є кілька варіантів, що подавати моделі в якості еталонного прикладу:

  1. Навчаємо модель, подаючи їй в якості виходу безпосередньо time — час обчислення. Це те, від чого ми відмовилися відразу, послухавши пораду авторів статті наведеної на початку.
  2. Навчаємо модель, подаючи на вихід time/(m*n*k) — тобто кутовий коефіцієнт, розрахований для кожної обчислювальної системи. Це те, що ми робили до цього моменту.
  3. Навчаємо модель, подаючи на вхід значення time/reg_k*(m*n*k). Тобто ми припускаємо, що для кожної обчислювальної системи залежність часу від m*n*k лінійна з кутовим коефіцієнтом reg_k і навчаємо модель прогнозувати відносне відхилення від цієї моделі.
В якості reg_k була взята медіана time/(m*n*k), в якості ідентифікатора обчислювальної системи — ознаки os+cpuFull, так як саме на цьому поєднанні лінійна модель з медіаною давала кращий результат.

Приклад
all_train.ix[:, 'c1'] = all_train['TARGET'] / (all_train['m'] * all_train['n'] * all_train['k'])
all_train_median = all_train[['c1', 'os', 'cpuFull']].groupby(['os', 'cpuFull'], as_index=False).median()
def preprocess_data(df):
# це прогнозне час
df = pd.merge(df, all_train_median, on=['os', 'cpuFull'], how='left', suffixes=(", '_med'))
df.ix[:, 'test_mdeian'] = df['c1_med']*df['m']*df['n']*df['k']
return df

Крок шостий — rules rule

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

Загальні враження

Хочеться відзначити, що по ходу всіх поліпшень, описаних у статті, локальна оцінка рішення, оцінка public score, а як потім з'ясувалося, і в private score, завжди змінювалися в бік поліпшення. Ось тут можна подивитися підсумковий робочий скрипт.

Спробуйте себе!
Як завжди, ми пропонуємо на порталі повчальну статтю для новачків і серйозне завдання для експертів. До речі, на порталі можна потренуватися у вирішенні попередніх конкурсів — всі вони відкрито в режимі пісочниці. Приєднуйтесь до нас на реєстрації!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.