#курс
Всем привет :)
Почти 2 месяца ничего не постил.
А это потому, что был очень занят - писал лучший курс по машинному обучению (и другие курсы ему завидуют :)
А если серьезно, то аналогов такому курсу и в правду не встречал.
Курс посвящен реализации всех классических алгоритмов машинного обучения с нуля. На чистом питоне + нампай и пандас.
Упор в курсе будет делаться именно на алгоритмы. Хотя и немного математики там есть.
Курс бесплатный :) На степике.
Пока реализовал 6 модулей: два вида линейных моделей, деревьев решений и случайного леса.
Остальные будут добавляться по мере готовности.
Сейчас ищу 3-4 человека, которые помогут протестировать задания перед публикацией в открытый доступ.
От кандидатов жду понимания основ МЛ, умения писать на питоне и знания нампай и пандас. Плюс свободное время.
Кто хочет - пишите в личку.
#Tip31 #Train
Обычно оптуну воспринимают как инструмент оптимизации гипер-параметров модели.
Но ее возможности несколько шире. Например, можно повесить на нее и препроцессинг :)
Вот небольшой пример. Допустим, у вас есть датасет. В датасете есть колонки с пропусками. Колонки, в которых очень много пропусков - бесполезны и по-хорошему от них нужно избавиться. Но какой процент считать бесполезным? Выкидывать колонки, в которых более 90% пропусков? Более 80%? 70%? Можно пробовать выяснять это вручную, а можно поручить это опутне. Примерно так:
nulls_perc = (X.isna().sum() / X.shape[0] * 100).astype(int).sort_values(ascending=False)Здесь мы в начале подсчитываем процент пропусков в каждой колонке. Затем внутри цикла генерируем кол-во пропусков, которое хотим оставить (от 10% до 100%). Отбираем колонки по этому показателю и формируем датасеты для обучения и валидации.
def objective(trial):
params = {
'objective': 'regression',
'metric': 'mse',
'learning_rate': trial.suggest_float('learning_rate', 1e-5, 0.7, log=True),
'num_leaves': trial.suggest_int('num_leaves', 20, 150),
'verbose': -1
}
nulls_max = trial.suggest_int('nulls_max', 10, 100)
f_cols = list(nulls_perc[nulls_perc < nulls_max].index)
X_train = train[f_cols]
X_test = test[f_cols]
model = lgbm.train(
params = params,
train_set = lgbm.Dataset(X_train, y_train),
valid_sets = lgbm.Dataset(X_test, y_test),
verbose_eval = None
)
score = model.best_score['valid_0']['l2']
print(score, len(f_cols))
return score
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=3000, timeout=300)
#Tip30 #Train
В отличии от других бустингов в LGBM деревья растут асимметрично (по листьям). В то время как в CatBoost и XGBoost рост происходит по уровням. И это декларируется разработчиками LGBM как одно из основных преимуществ.
И тут закралась ловушка: если вы ограничите глубину дерева в LGBM (а для бустингов рекомендуются не глубокие деревья), то вы по сути превратите его в симметричное урезанное дерево!
Именно поэтому по умолчанию в LGBM глубина деревьев (max_depth) = -1 (что значит - бесконечная), а в CatBoost и XGBoost глубина = 6.
Но бесконечно расти тоже не вариант - так мы просто запомним все данные. Поэтому, чтобы ограничить глубину деревьев используйте такие параметры как:
num_leaves — максимальное количество листьев в одном дереве
min_data_in_leaf — минимальное количество данных в одном листе
min_data_in_bin — минимальное количество данных внутри одного бина
min_data_per_group — минимальное количество данных на одно значение категориальной фичи
min_gain_to_split — минимальный прирост, которое дает разделение
min_sum_hessian_in_leaf — минимальная сумма гессиана в одном листе
Считается, что асимметричные деревья более склонны к переобучению, особенно на маленьких датасетах. В этом случае рекомендуется задействовать max_depth. Также стоит использовать max_depth на очень больших датасетах - иначе модель будет слишком долго учиться.
З.Ы. Недавно поведение аналогичное LGBM и XGBoost реализовали в катбусте.
#Tip29 #Train
Случайный лес не переобучается! Но с оговоркой... :)
Лес переобучается, но он не переобучается от увеличения кол-ва деревьев (при прочих разумных условиях).
Есть у Случайного леса такое свойство, что по мере увеличения кол-ва деревьев кривые обучения выходят на плато и дальше на их предсказание влияет только шум. Для бустинга же бездумное увеличение кол-ва деревьев это прямой путь к оверфиту.
Но если вы зададите для леса глубину скажем 1000, то он конечно же переобучится: просто запомнит данные, вместо того чтобы выучить закономерности.
Интуитивно это можно понять так... Представим себе генеральную совокупность на 10 000 000 экземпляров. Возьмем из нее два случайных экземпляра и посчитаем их среднее. Очевидно, что это среднее будет сильно отличаться от среднего генеральной совокупности. Также и для трех, четыре, пяти... экземпляров. Но если мы возьмем 1 000 000 случайных экземпляров, то среднее будет очень близко к генеральной совокупности. И добавление миллион первого, второго, третьего экземпляра сильно на среднее не повлияет.
Лес точно также усредняет предсказания своих деревьев. И в определенный момент кол-во деревьев становится достаточным, и добавление новых уже ни на что не виляет (если не считать шум).
#Tip28 #Train
Nested Cross-Validation - самая дорогая из "классических" схем валидации.
Состоит из двух вложенных кросс-валидаций: внешней и внутренней. Внутренняя используется для подбора гипер-параметров/выбора модели, а внешняя - для оценки модели.
При обучении модели вам нужно решить две задачи: подобрать гипер-параметры и оценить модель. Если использовать для них только одну кросс-валидацию, то это может привести к чрезмерно оптимистичной оценке модели. Поскольку одни и те же данные используются и для подбора гипер-параметров и для оценки модели. Обычно в этом случае для финальной оценки выделяют тестовую часть, которая никаким образом в обучении не участвует. Но тогда мы теряем часть данных для обучения. С помощью Nested Cross-Validation можно задействовать все данные для обучения и получить корректную оценку.
Алгоритм такой:
1. Делим весь датасет на фолды
2. Для каждого фолда:
- Делим часть для обучения на свои фолды
- Подбираем гипер-параметры
- Обучаем одну модель на всей тренировочной части на лучших гипер-параметрах
- Оцениваем модель на тестовой части
3. Усредняем скоры по всем внешним фолдам
На выходе, помимо скоров, у нас будут N обученных моделей. Для предсказания используются все три, результаты усредняются.
Т.к. такая схема очень дорогая по времени, то применять ее следует в двух случаях:
1. У вас очень маленький датасет.
2. У вас есть своя майнинг-ферма :)
З.Ы. Для временного CV все аналогично просто оба цикла будут со сдвигом.
Ловите еще одну статью :)
На этот раз посвященную Evidently - библиотеке для мониторинга качества данных и моделей в проде.
https://habr.com/ru/post/692272/
Лайки приветствуются :)
Написал на Хабре туториал про Dagster - оркестратор предназначенный для организации конвейеров обработки данных: ETL, проведение тестов, формирование отчетов, обучение ML-моделей.
https://habr.com/ru/post/690342/
Заходим, не стесняемся, лайкаем :)
З.Ы. Туториал написан в рамках прохождения курса ML System Design (можете еще присоединиться): https://ods.ai/tracks/ml-system-design-22
#Tip26 #EDA
Partial dependence plots (PDP) — это график, который позволяет понять, как различные значения конкретной фичи влияют на предсказания модели.
Под капотом PDP меняет исходные значения целевой фичи и смотрит как изменяются предсказания модели.
По сути PDP показывает ожидаемое среднее значение таргета при различных значения интересующей нас фичи.
Строятся PDP в двух вариантах - для одной фичи и для пары фичей. Обычно для графика выбирают наиболее значимые для модели фичи.
Т.к. PDP работает с уже обученной моделью, то это еще один способ приоткрыть завесу черного ящика (наряду с SHAP и пр.).
PDP можно построить либо с помощью скалерна (но на вход принимаются только родные для скалерна модели):
fig, ax = plt.subplots(figsize=(18.5,4.5))
features = ['bahrooms','sqf_living','yr_buil']
PartialDependenceDisplay.from_estimator(clf, X, features, ax=ax);
features_to_plot = ['sqf_living','bahrooms']
inter1 = pdp.pdp_interact(model=model, dataset=X, model_features=fcols, features=features_to_plot)
pdp.pdp_interact_plot(pdp_interact_out=inter1, feature_names=features_to_plot, plot_type='contour')
plt.show()
#Tip24 #Train
Для честной проверки все методы валидации включают в себя тестовую выборку. Тест при этом никак не используется для обучения. Но в нем может содержаться полезная информация.
В самом простом случаи можете просто объединить трейн и тест и обучить модель заново с лучшими гипер-праметрами. Но они были подобраны для другого датасета, и могут быть не оптимальными для нового.
Поэтому для градиентного бустинга придумали такой фокус:
1. Разбиваете датасет на трейн и тест.
2. Обучаете модель и подбираете наилучшие гипер-параметры. Проверяете модель на тесте.
3. Объединяете трейн и тест.
4. Заново обучаете модель на полном датасете с найденными гиперпарамтрами, но только увеличиваете в них количество итераций на % размера теста.
Т.е. если у вас получилось 100 итераций, а тест составляет 20% от размера трейна, то для нового обучения выставляете кол-во итераций в 120.
5. Делаете предсказание с новой моделью. При этом протестировать ее сможете только на лидерборде.
Данный фокус не имеет под собой научного обоснования, но иногда докидывает...
#Tip22 #Evaluation
Object Importance - это метод, который позволяет определить, насколько каждая запись тренировочного датасета влияет на оптимизируемую метрику.
А значит, он позволяет найти и избавится от бесполезных записей (шум, выбросы и т.д.).
Метод реализован в библиотеке Catboost:
train_pool = Pool(X_train, y_train)
val_pool = Pool(X_val, y_val)
cb = CatBoost({'eval_metric': 'RMSE'})
cb.fit(train_pool)
indices, scores = cb.get_object_importance(
val_pool,
train_pool,
importance_values_sign='Positive' # Negative, All
)
#Tip20 #Train
Псевдо-лейблинг (pseudo-labeling) это метод обучения на не размеченных данных.
В соревнованиях он может использоваться так:
1. Обучаете модель на трейне
2. Делаете предсказание на тесте
3. Объединяете трейн+тест и заново обучаете модель. И тут супер важно валидироваться только на данных из трейна, поскольку в тесте будут и ошибочно размеченные данные.
4. Заново предсказать тест.
Очевидно, что при таком подходе модель увидит больше данных и закономерностей. А чем больше (хороших) данных тем точнее модель.
#Tip18 #TimeSeries
Интересный пример подмены задачи.
Допустим Вам дана задача регрессия на временных рядах.
Вы можете решать ее напрямую. А можете предсказывать не абсолютные значения, а прирост значения относительно предыдущего.
Из очевидных преимуществ - меньше разброс значений целевой переменной, а значит меньше выбросов - а модели выбросы не любят :)
#Tip16 #Evaluation
Способ повысить стабильность модели.
На курсах учат подбирать гиперпараметры на кросс-валидации. Тут все правильно.
Но как выбрать наилучшие гиперпараметры. Обычно берут средний скор по фолдам.
Но тут еcть подвох. У двух одинаковых усредненных скоров есть важное отличие - стандартное отклонение.
Чем меньше стандартное отклонение - тем лучше.
Поэтому отбираем модель так:
mean(скоры_по_фолдам) - std(скоры_по_фолдам)Таким образом мы будем стремиться отбирать модель с меньшим разбросом по скорам между фолдами. Читать полностью…
#Tip14 #Preprocessing
Есть такая функция в пандас - transform - которую незаслуженно обходят вниманием на курсах.
Допустим у нас есть данные по продажам в разрезе клиентов.
df[['client_id','sales']]И мы хотим посчитать сумму/среднее (или любой другой агрегат) по продажам, чтобы затем подставить его в ту же самую таблицу.
temp_df = df.groupby('client_id')['sales'].mean().rename('sales_mean')С функцией трансформ это делается в одну строку:
df = pd.merge(df, temp_df, on='client_id')
df['sales_mean'] = df.groupby('client_id')['sales'].transform('mean')Читать полностью…
#Tip12.1 #Preprocessing
У логарифмов 👆 есть некоторые недостатки, в частности они плохо борются с недостаточно длинными хвостами.
Поэтому придумали немного более сложный метод - Бокса-Кокса, который зачастую дает более правильное нормально распределение.
В отличии от логарифма, его нужно обучить на трейна и применить к тесту (если речь о преобразовании фичей):
from scipy.stats import boxcoxОбратное преобразование производится так:
train_price, fitted_lambda = boxcox(df['price'])
train['price_boxcox'] = train_price
test['price_boxcox'] = boxcox(test['price_boxcox'], fitted_lambda)
from scipy.special import inv_boxcoxЧитать полностью…
inv_boxcox(test['price_predict'],fitted_lambda)
#Tip32 #Train
Самый простой способ борьбы с выбросами (о котором не расскажут на курсах :)
1. Обучаете модель.
2. Делаете предсказание на трейне.
3. Объекты, у которых самые большие ошибки выкидываете из трейна.
З.Ы. Для задачи регрессии возможно стоит попробовать метрику MAE, поскольку MSE сильнее штрафует за большие ошибки (а значит лучше борется с выбросами).
Написал небольшой эпос о библиотеке transformers.
И хотя текста много это все равно только лишь введение. Функционала в библиотеке слишком много.
https://habr.com/ru/post/704592/
Для тех кто не в курсе: библиотека Transformers предоставляет доступ к куче современных предобученных DL-моделей.
И на текущий момент является чуть ли не аналогом скалерна в мире Deep Learning.
Залил на Хабр статью про библиотеку Voilà (читается "вуаля"), которая позволяет конвертировать ноутбуки в веб-приложения.
https://habr.com/ru/post/698662/
Написал туториал про фреймворк Hydra
https://habr.com/ru/post/696820/
Гидра предназначена для управления файлами конфигурации в ML-проектах.
#Tip27 #Train
При K-fold валидации прогнозы делаются на тестовых частях, которые не участвуют в обучении и эти прогнозы называются Out-of-Fold (OOF) Predictions.
Обычно они используются по прямому назначению - оценки модели.
Но можно их задействовать и более интересным способом - для стэкинга.
Стэкинг это способ построения ансамбля моделей, в котором есть базовые модели и есть мета модель.
Базовые модели обучаются на исходных данных, а мета-модель на предсказаниях базовых моделей.
Обычно для построения стекинга исходный датасет делится на несколько крупных частей из-за чего все модели увидят меньше данных. Но благодаря Out-of-Fold Predictions можно реализовать обучение на всех (почти) данных.
В соревновательном DS алгоритм в целом такой:
1. Разбиваем датасет с метками на трейн и эвал + у нас уже есть тестовый датасет от организаторов.
2. Трейн разбиваем на фолды.
3. Для каждого фолда:
- Обучаем модель на тренировочной части.
- Выполняем предсказание для тестовой части фолда и сохраняем предсказание в отдельный вектор, каждый элемент которого соответствует строке тренировочного датасета.
- Выполняем предсказание для валидационного датасета.
- Выполняем предсказание для тестового датасета.
4. Собранные предсказания на фолдах передаем в мета-модель как фичу.
5. Усредняем предсказания полученные для валидационного и тестового датасета.
6. С помощью мета-модели делаем предсказание для усредненных значений валидационного и тестового датасета.
7. Значения полученные для валидационного датасета используем для оценки, а значения полученные для тестового датасета заворачиваем в сабмит.
Реализуется данный алгоритм обычно вручную.
Варианты для изменений:
- На фолдах можно обучить несколько видов базовых моделей, тогда на вход мета-модели пойдет несколько фичей.
- На шаге обучения мета-модели можно присоединить фичи-предсказания к исходным фичам.
Для большей эффективности ансамбля для базовых и мета-модели стараются задействовать разные алгоритмы (линейные/KNN/деревья).
Написал на Хабре еще один туториал (в рамках курса ML System Design).
На этот раз посвященный ClearML - системе трекинга Ml-экспериментов.
https://habr.com/ru/post/691314/
Заходим, читаем и не забываем ставить лайки :)
#Tip26.1 #EDA
Для понимания как строится PDP попробуем построить его в ручную.
Алгоритм такой:
- Выбираем какую-нибудь фичу
- Вычисляем все ее уникальные значения (для непрерывной фичи можно взять сколько-то квантильных значений)
- Проходимся по каждому из них и:
- В исходном датасете для нашей фичи заменяем все значения на одно - текущее
- Делаем предсказание
- Усредняем полученные предсказания
И так для каждого уникального значения. В итоге получаем набор пар: (уникальное значение - средний предикт)
По смыслу мы как бы спрашиваем модель: а что если бы фича принимала бы только одно значение из возможного диапазона (при этом другие фичи не трогаем) - какими тогда стали бы предсказания?
- Выводим их на графике.
З.Ы. Также для демонстрации влияния на отдельные примеры выведем случайные 50 записей. Такой график уже будет называться Individual Conditional Expectation (ICE). Он позволяет увидеть какие-либо аномалии - как отдельные записи отклоняются от среднего значения.
# Считаем PDPЧитать полностью…
col = 'grade'
uniq_values = sorted(X[col].unique())
preds = {}
for v in uniq_values:
X_copy = X.copy()
X_copy[col] = v
preds[v] = model.predict(X_copy)
preds = pd.DataFrame(preds)
pdp = preds.mean()
# Выбираем случайные 50 записей
samp = preds.sample(50).copy()
samp = samp.reset_index().melt('index', var_name='cols', value_name='vals')
# Строим графики
plt.figure(figsize=(13,6))
sns.lineplot(data=samp, x='cols', y='vals', hue='index', legend=False, palette='GnBu', alpha=0.2)
sns.lineplot(x=pdp.index, y=pdp, color='red', linewidth=2)
plt.xlabel(col)
plt.ylabel('PDP');
#Tip25 #TimeSeries #Valid
TimeSeriesSplit - известный способ разбиения временных данных для кросс-валидации из скалерна.
Метод разбиения, который в нем реализован называется Expanding window.
Но наука о временных рядах знает гораздо больше способов разбить временные данные. Основных два:
- Expanding Window
- Sliding Window
А дальше включается воображение:
- Нахлесты между периодами
- Пробел между трейном/валидом/тестом
- Способ выделения тестовой выборки
Как уже было сказано, Expanding Window можно найти в скалерне. Для Sliding Window и других ухищрений нужно либо писать свой код (основные ML-библиотеки принимают готовые сплиты в виде индексов строк), либо поискать на гитхабе:
https://github.com/WenjieZ/TSCV
https://github.com/AaronOS0/Time-series-Cross-validation
https://github.com/marmurar/jano
и прочие...
#Tip23 #Preprocessing
pipe - это метод реализации паплайна в датафреймах пандас посредством последовательной цепочки преобразований.
З.Ы. Не путать с Pipeline из скалерна - это разные вещи.
Предположим, у нас есть ряд функций, каждая из которых отвечает за отдельный вид преобразований.
Тогда в пандасе пайплайн можно реализовать так:
def normilize(df):Читать полностью…
...
return df
def fill_null(df, method):
...
return df
def parse_time(df):
...
return df
new_df = (df
.pipe(normilize)
.pipe(fill_null, method='median')
.pipe(parse_time)
)
#Tip21 #Train
Планировщик (scheduler) в нейронных сетях довольно обыденная вещь - он позволяет уменьшать шаг обучения (learniтп rate) с каждой эпохой (ну или раз в несколько эпох).
В бустингах такая вещь тоже встречается, но не часто ее увидишь.
Например, в LGBM динамический learning rate можно реализовать так:
model = lgb.train(Читать полностью…
params,
train_df,
learning_rates = lambda iter: 0.5 * (0.85 ** iter),
valid_sets = eval_df)
#Tip19 #Evaluation
Когда вы делаете предсказание для задачи классификации, то, помимо метрики, также визуально оцениваете качество по матрице ошибок.
А когда делаете регрессию, куда смотреть?
Тут я строю два графика:
Первый - самый очевидный - просто отложить на двух осях фактические и предсказанные значения.
Второй - подсмотрел у Дьконова - изменить координаты на True-Pred и True+Pred. По сути, тот же вариант что и первый, но растянут по одной оси и сужен по второй. Позволяет посмотреть на график под другим углом. Говорят иногда бывает полезно.
#Tip17 #Models
Перед построением какой-ниубдь сложной модели рекомендуется сначала построить простой бейзлайн, чтобы было с чем сравнивать повышение качества. Но какую модель выбрать? А ничего выбирать не надо - есть специальные пакеты, которые сразу строят кучу моделей.
Я знаю два таких:
- PyCaret - https://github.com/pycaret/pycaret
- Lazy Predict - https://github.com/shankarpandala/lazypredict
Но второй требует более старой версии пандаса, да и вообще похоже уже не развивается.
А вот первый довольно интересный (скрин с него). В документации вы найдете туториалы.
#Tip15 #Evaluation
Построили вы модель, вывели feature importance. Допустим фичей достаточно много. И вам нужно выкинуть какое-то кол-во фичей. В идеале бесполезных.
Есть такой старый фокус: перед построением модели добавляете в датасет три случайно сгенерированные фичи:
- Бинарную (0 или 1)
- Непрерывную, между 0 и 1
- Целочисленную (например, между 1 и 10_000)
Далее обучаете модель и выводите feature importance - все фичи, которые ниже случайно сгенерированных - выкидываете.
Эти новые фичи по сути шум и все что ниже их - бесполезный мусор.
#Tip13 #Preprocessing
Некоторые данные по своей природе имеют циклическую структуру: часы, минуты, месяцы и т.д.
Рассмотрим часы. С т.з. математики число 22 больше 1, но с т.з. времени 1 час сегодняшнего дня больше 22 часов вчерашнего.
И по-хорошему, модели необходимо сообщить эту информацию (особенно линейным).
Для этого делают косинусо-синусное преобразование:
hours_in_day = 24После этого одна циклическая фича будет представлена двумя непрерывными фичами. Читать полностью…
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / hours_in_day)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / hours_in_day)
#Tip12 #Preprocessing
Зачастую, в задачах линейной регрессии таргет имеет длинный хвост в распределении данных.
А моделям "нравится" когда данные имеют нормальное распределение. Особенно это касается линейных моделей.
Чтобы привести распределение к нормальному, нужно:
1. Прологарифмировать таргет.
df['target_log'] = np.log(df['target'] + np.abs(df['target'].min()) + 1)Обратите внимание, что мы сначала делаем таргет не отрицательным, добавляя константу и >= 1 (чтобы не было отрицательных значений).
df['predict'] = np.expm1(df['predict_log'])Все выше описанное можно провернуть автоматически с помощью класса TransformedTargetRegressor, библиотеки скаленр.