10 функций Sklearn, которые упускают из виду 99% онлайн-курсов
Я написал более двух десятков статей о Sklearn, но эта библиотека не всегда была моей любимой. На самом деле, из-за ужасного онлайн-курса, который я прошел, я сначала ненавидел её.
Но, в конце концов, я ознакомился со всей документацией настолько внимательно, что нашёл столько изящных фич, которых не видел ни в одном онлайн-курсе. Моя первая подборка функций Sklearn была больше сосредоточена на трюках для редких случаев.
Эта статья будет гораздо более практична и применима к широкому спектру рабочих процессов.
Давайте начинать!
1️. FunctionTransformer
Самый простой способ создать собственный трансформатор – это импортировать FunctionTransformer из sklearn.preprocessing. FunctionTransformer отправляет X и опционально y (массив данных и массив меток), а так же пользовательские аргументы в указанную функцию и возвращает результат.
Несмотря на то, что в Sklearn есть много преобразователей предварительной обработки, которые вы можете поместить в конвейер, их недостаточно для каждого возможного сценария предварительной обработки. Даже если у вас есть один шаг, который не находится внутри конвейера, вся идея конвейеров с одним вызовом рушится.
Вот почему вы должны обернуть все свои пользовательские функции предварительной обработки внутрь FunctionTransformer
, что может преобразовать их в Sklearn-совместимый преобразователь. Единственное требование состоит в том, чтобы функция принимала массив признаков (X) и необязательный целевой массив и возвращала их после предварительной обработки.
💻Код
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import FunctionTransformer
def reduce_memory(X: pd.DataFrame, y=None):
"""Simple function to reduce memory usage by casting numeric columns to float32."""
num_cols = X.select_dtypes(incluce=np.number).columns
for col in num_cols:
X[col] = X.astype("float32")
return X, y
ReduceMemoryTransformer = FunctionTransformer(reduce_memory)
# Plug into a pipeline
>>> make_pipeline(SimpleImputer(), ReduceMemoryTransformer)
Pipeline(steps=[('simpleimputer', SimpleImputer()),
('functiontransformer', ReduceMemoryTransformer()])
📚Документация: FunctionTransformer — ссылка
Чтобы создать более сложную конструкцию, можно реализовать собственный класс, а в нем три базовых метода scikit-learn — fit() (должен возвращать self), transform() (собственно сам трансформер) и fit_transform(). Для всех трех методов обязательным аргументом является X. Если унаследовать класс от TransformerMixin, то fit_transformer() можно не задавать, а если добавить в качестве базового класса BaseEstimator и не реализовывать *args, **kwargs в конструкторе, то будут доступны методы get_params() и set_params()
2️. Пользовательские преобразователи
Одна из самых распространенных операций при очистке — масштабирование выбросов, чтобы они были нормально распределены. Обычно люди используют логарифмические преобразователи типа PowerTransformer
или np.log
, но у них есть подвох. Если функция содержит нули, базовая функция логарифмирования не сможет их обработать и выдаст ошибку.
В качестве обходного пути люди добавляют 1 к вектору признаков, а затем выполняют преобразование. Если им нужен исходный вектор, они вызывают экспоненциальную функцию для признака и вычитают 1 — проблема решена.
Конечно, эта операция не встроена в Sklearn, и вы не можете выполнить её внутри простой функции Python. Вот где вы элегантно решаете эту дилемму с помощью пользовательских преобразователей. Ниже приведен преобразователь, который делает именно то, что я описал:
💻 Демо
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.preprocessing import PowerTransformer
class CustomLogTransformer(BaseEstimator, TransformerMixin):
def __init__(self):
self._estimator = PowerTransformer() # init a transformer
def fit(self, X, y=None):
X_copy = np.copy(X) + 1 # add one in case of zeroes
self._estimator.fit(X_copy)
return self
def transform(self, X):
X_copy = np.copy(X) + 1
return self._estimator.transform(X_copy) # perform scaling
def inverse_transform(self, X):
X_reversed = self._estimator.inverse_transform(np.copy(X))
return X_reversed - 1 # return subtracting 1 after inverse transform
Класс должен наследовать от BaseEstimator
и TransformerMixin
классы, которые позволяют подключить его к конвейеру. Вы выигрываете от того, что пишете меньше кода, и снижается вероятность утечки данных.
📚Документация
BaseEstimator — ссылка .
TransformerMixin — ссылка .
3️. TransformedTargetRegressor
Иногда даже для целевого массива y
требуются дополнительные шаги предварительной обработки, которые нельзя включить в конвейер. Типичным сценарием является масштабирование числовых целей, чтобы сделать их нормально распределенными. Вы делаете это вне конвейера, часто более одного раза.
Разве не было бы здорово, если бы у вас был конвейер, который заботился бы как о целевом массиве y, так и о массиве функций X
? Оказывается, он существует! (Только для регрессии).
TransformedTargetRegressor — это класс, который принимает как конвейер регрессора для функций x, так и отдельную функцию предварительной обработки или преобразователь для целевого массива y
.
💻 Демо
from sklearn.compose import TransformedTargetRegressor
reg_lgbm = lgbm.LGBMRegressor()
final_estimator = TransformedTargetRegressor(
regressor=reg_lgbm, transformer=CustomLogTransformer()
)
final_estimator.fit(X_train, y_train)
TransformedTargetRegressor(regressor=LGBMRegressor(),
transformer=CustomLogTransformer())
Параметр regressor
принимает как регрессоры, так и конечные конвейеры. У него также есть параметр transformer
, для которого вы передаете класс преобразователя, который будет применен к цели y
. Если преобразователь — это функция, например np.log
, вы можете передать ее в аргумент func
.
Подробнее об этом в документации.
📚Документация: TransformedTargetregressor — ссылка .
4️. HTML-представление
Если ваш конвейер состоит из нескольких шагов или вложенных конвейеров, их отображение IPython убьёт всю эстетику. Вот как это будет выглядеть, если вы случайно отобразите их в Jupyter:
>>> giant_pipeline
Pipeline(steps=[('columntransformer',
ColumnTransformer(transformers=[('cat_pipe',
Pipeline(steps=[('impute',
SimpleImputer(strategy='most_frequent')),
('oh',
OneHotEncoder())]),
<sklearn.compose._column_transformer.make_column_selector object at 0x000001B6D8BD9310>),
('num_pipe',
Pipeline(steps=[('impute',
SimpleImputer(strategy='median')),
('transform',
QuantileTransformer())]),
<sklearn.compose._column_transformer.make_column_selector object at 0x000001B6D8BD9160>)])),
('lgbmregressor',
LGBMRegressor(device_type='gpu', learning_rate=0.01,
n_estimators=10000))])
К счастью, Sklearn предлагает HTML-представление своих оценок, чтобы сделать их более удобными для пользователя и радовать нас:
💻Демо
from sklearn import set_config
set_config(display="diagram")
>>> giant_pipeline
Установив для параметра display config значение diagram
, вы получите интерактивное HTML-представление вашего конвейера в IPython.
📚Документация
sklearn.set_config — ссылка .
sklearn.utils.estimator_html_repr — ссылка
5️. Квадратичный дискриминантный анализ
В конкурсе мгновенного вознаграждения Kaggle классификатор квадратичного дискриминантного анализа достиг впечатляющего показателя ROC AUC 0,965 даже без настройки гиперпараметров, превзойдя большинство древовидных моделей, включая XGBoost и LightGBM.
Итак, почему вы никогда раньше не слышали об этом алгоритме, если он может превзойти современные модели? Ну, его вариант использования ограничен. Функции для обучения QDA должны быть строго нормально распределены, чтобы QDA легко вычисляла и подгоняла форму эллипсоида вокруг распределения.
Ещё одним преимуществом QDA является его молниеносная скорость — для его обучения на наборе данных из миллиона строк требуется всего несколько секунд:
💻Демо
%%time
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
# Generate 1M samples and 100 features
X, y = make_classification(n_samples=1000000, n_features=100)
qda = QuadraticDiscriminantAnalysis().fit(X, y)
Wall time: 13.4 s
В этой записной книжке Крис Деотт показывает, как он достигает показателя ROC AUC 0,965, комбинируя 512 моделей QDA.
В конкурсе TPS (сентябрь 2021 г.) мне также удалось набрать около 0,8 ROC AUC с QDA, в то время как у лучших решений было около 0,81. Разница в 2% — это компромисс, который вы можете рассмотреть между наиболее точными древовидными моделями, которые намного медленнее, и моделью, которая работает молниеносно за счет более низкой точности.
📚Документация: QuadraticDiscriminantAnalysis — ссылка .
6️. Voting Classifier/Regressor
В одном проекте вы часто получаете несколько настроенных моделей со схожими характеристиками. Именно здесь ансамбль voting может совместно использовать эти модели, чтобы еще больше повысить производительность. Классификатор принимает большинство голосов нескольких классификаторов в качестве окончательного прогноза. Если классы являются вероятностями или прогнозы непрерывны, прогнозы усредняются.
Причина, по которой этот метод работает так хорошо, заложена в теории вероятностей. Короче говоря, три классификатора с точностью 0,6, 0,7 и 0,8 в конечном итоге превзойдут три классификатора с точностью 0,8 при объединении. Если вы мне не верите, прочитайте эту статью от MLWave.
Sklearn предоставляет два удобных класса, которые реализуют эту операцию. Вам нужно передать список классификаторов или регрессоров, и он позаботится об их объединении.
💻Демо
from sklearn.ensemble import VotingClassifier
X, y = make_classification(n_samples=1000)
ensemble = VotingClassifier(
estimators=[
("xgb", xgb.XGBClassifier(eval_metric="auc")),
("lgbm", lgbm.LGBMClassifier()),
("cb", cb.CatBoostClassifier(verbose=False)),
],
voting="soft",
# n_jobs=-1,
)
_ = ensemble.fit(X, y)
Я установил voting
значение soft, указав, что хочу, чтобы прогнозы были вероятностными. Существует также аргумент weights
, который вы можете использовать для назначения различных коэффициентов для более точных моделей. Ознакомьтесь с документами для более подробной информации.
📚Документация
VotingClassifier— ссылка .
VotingRegressor— ссылка .
7️. Stacking Classifier/Regressor
Другой метод объединения, более мощный, чем voting, — это stacking.
Допустим, у вас есть пять моделей. Чтобы сложить их выходные данные, вы помещаете их все в обучающие данные один за другим и генерируете прогнозы. У вас будет пять наборов прогнозов, которые вы объедините в один набор данных с целевым массивом обучающего набора. Это приводит к новому набору данных с пятью прогнозами в виде столбцов и в y
качестве целевого массива.
Затем вы выбираете окончательную, совершенно другую модель для обучения на этом «наборе данных прогнозов» и делаете прогнозы на наборе задержек.
Идея стекирования заключается в том, что модели первого уровня, которые вы выбираете, должны быть как можно более разнообразными. Разные модели изучают информацию обучающей выборки с разных точек зрения, охватывая все информационное пространство.
Другими словами, различные модели, такие как деревья, линейные модели, аппроксиматоры поверхностей, модели на основе соседей, байесовские и гауссовские модели, максимизируют обучающий потенциал, а их совокупный результат уменьшает систематическую ошибку и предотвращает переоснащение.
Большинство выигрышных решений в табличных соревнованиях Kaggle — это несколько моделей, сложенных вместе.
💻Демо
from sklearn.ensemble import StackingClassifier, StackingRegressor
from sklearn.linear_model import LogisticRegression
X, y = make_classification(n_samples=1000)
ensemble = StackingClassifier(
estimators=[
("xgb", xgb.XGBClassifier(eval_metric="auc")),
("lgbm", lgbm.LGBMClassifier()),
("cb", cb.CatBoostClassifier(verbose=False)),
],
final_estimator=LogisticRegression(),
cv=5,
passthrough=False
# n_jobs=-1,
)
_ = ensemble.fit(X, y)
Несмотря на то, что это звучит сложно, вы быстро освоитесь со Sklearn. Реализация такая же интуитивно понятная, как и Voting Classifier/Regressor.
📚Документация
StackingClassifier — ссылка .
StackingRegressor — ссылка .
8️. LocalOutlierFactor
Выбросы — серьезная проблема в машинном обучении. Они искажают целевую функцию моделей и могут привести либо к чрезмерно оптимистичным, либо к пессимистичным результатам.
Поиск выбросов не является проблемой для небольших наборов данных. Настоящая проблема начинается с наборов данных, содержащих более 50–100 признаков. Вам нужен алгоритм, который будет быстрым и точным при обнаружении выбросов высокой размерности. Тем не менее, для наборов данных с сотнями функций и миллионами строк выполнение необработанных алгоритмов может занять несколько часов.
Вот где вам нужно объединить алгоритм уменьшения размерности с надежным детектором выбросов. Комбинация, которая мне недавно понравилась, — это UMAP и LocalOutlierFactor .
UMAP работает исключительно при уменьшении размеров набора данных и сохранении как можно большего количества информации. Что касается LocalOutlierFactor, то это алгоритм на основе соседей, предназначенный для быстрой работы с большими наборами данных.
💻Демо
%%time
import umap # pip install umap
from sklearn.neighbors import LocalOutlierFactor
X, y = make_classification(n_samples=5000, n_classes=2, n_features=10)
X_reduced = umap.UMAP(n_components=2).fit_transform(X, y)
lof = LocalOutlierFactor()
labels = lof.fit_predict(X_reduced, y)
Wall time: 17.8 s
>>> np.where(labels == -1)
(array([ 119, 155, 303, 331, 333, 407, 418, 549, 599, 664, 795,
3092, 3262, 3271, 3280, 3289, 3311, 3477, 3899, 3929, 3975, 4301,
4358, 4442, 4522, 4561, 4621, 4631, 4989], dtype=int64),)
📚Документация
LocalOutlierFactor — ссылка .
9️. QuantileTransformer
Иногда вы будете сталкиваться с сумасшедшими распределениями с дикими формами, которые преобразователи логов или масштабаторы не могут преобразовать в нормальное распределение.
Если у вас есть такие бимодальные, тримодальные или n-модальные распределения, лучше всего сделать их как можно более нормально распределенными с помощью QuantileTransformer
. Он использует надежные статистические показатели, такие как квартили и медиана, для центрирования и масштабирования распределения.
💻Демо
import pandas as pd
from sklearn.preprocessing import QuantileTransformer
qt = QuantileTransformer().fit(crazy_distributions)
crazy_feature_names = ["f18", "f31", "f61"]
crazy_distributions = pd.DataFrame(qt.transform(crazy_distributions), columns=crazy_feature_names)
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for ax, f_name in zip(axes.flatten(), crazy_feature_names):
sns.kdeplot(crazy_distributions[f_name], ax=ax, color="#E50914")
📚Документация
QuantileTransformer — ссылка .
1️0️. PCA + tSNE/UMAP
Больше данных не обязательно означает лучшие модели. Некоторые наборы данных слишком велики, и вы могли бы преуспеть, не используя их в полной мере. Но если вам неудобно откладывать часть данных в сторону, я предлагаю использовать методы уменьшения размерности, чтобы спроецировать данные в более низкое пространство.
Повышение производительности модели не гарантируется, но в долгосрочной перспективе вы сможете проводить гораздо больше экспериментов с меньшим набором данных, поскольку у вас будет меньше использования оперативной памяти, а время вычислений будет намного короче.
Но проблема в том, что снижение размерности качества может занять слишком много времени, если в наборе данных много признаков. У вас не получится с первой попытки, поэтому дальнейшие эксперименты будут еще более затратными по времени.
Вот почему документация Sklearn предлагает сочетать алгоритмы уменьшения размерности с PCA (анализ основных компонентов).
PCA работает быстро для любого количества измерений, что делает его идеальным для редукции на первом этапе. Рекомендуется проецировать данные на разумное количество измерений, например, 30–50 с PCA, а затем использовать другие алгоритмы для еще большего уменьшения, например tSNE или UMAP.
Ниже представлена комбинация PCA и tSNE:
💻Демо
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
df = dt.fread("data/large.csv").to_pandas()
>>> df.shape
(1000000, 287)
X, y = df.drop("target", axis=1), df[["target"]].values.flatten()
%%time
manifold_pipe = make_pipeline(QuantileTransformer(), PCA(n_components=30), TSNE())
reduced_X = manifold_pipe.fit_transform(X, y)
------------------------------------------
Wall time: 4h 27min 46s
В синтетическом наборе данных с 1 млн строк и примерно 300 объектов проецирование данных на первые 30 измерений, а затем на два измерения заняло 4,5 часа. К сожалению, результаты не очень порадовали:
>>> plt.scatter(reduced_X[:, 0], reduced_X[:, 1], c=y, s=0.05);
Вот почему я рекомендую использовать UMAP. Это намного быстрее, чем tSNE, и лучше сохраняет локальную структуру данных:
%%time
manifold_pipe = make_pipeline(QuantileTransformer(), PCA(n_components=30))
X_pca = manifold_pipe.fit_transform(X, y)
embedding = umap.UMAP(n_components=2).fit(X_pca, y)
Wall time: 14min 27s
>>> plt.scatter(embedding.embedding_[:, 0], embedding.embedding_[:, 1], c=y, s=0.05);
UMAP удалось найти четкое различие между целевыми классами, и он сделал это в 20 раз быстрее, чем tSNE.
📚Документация
ППШ — ссылка .
tSNE — ссылка .
УМАП — ссылка .
Заключение
В эпоху ИИ легко ускользнуть от причудливых моделей, таких как языковые преобразователи. Но они предназначены только для обработки естественного языка — классическое машинное обучение все еще находится в руках таких гигантов, как Scikit-learn.
Функции библиотеки, описанные в этой статье, служат дополнительным повышением общей функциональности для повышения производительности, удобства чтения и сокращения кода.
Спасибо за чтение!