梯度提升模型中的分类特征编码策略

机器学习中,处理分类特征是一个重要的步骤。本文将探讨使用不同的编码策略对梯度提升模型性能的影响。将使用Ames Housing数据集,该数据集包含数值和分类特征,目标是预测房屋的销售价格。将评估以下几种策略:

  • 删除分类特征
  • 使用OneHotEncoder进行独热编码
  • 使用OrdinalEncoder将分类特征视为有序的等距量
  • 利用HistGradientBoostingRegressor估计器的原生类别支持

首先,加载Ames Housing数据集作为一个pandas数据框。特征要么是分类的,要么是数值的。将选择X的一个子集,以加快示例的运行速度。

from sklearn.datasets import fetch_openml X, y = fetch_openml(data_id=42165, as_frame=True, return_X_y=True) categorical_columns_subset = ["BldgType", "GarageFinish", "LotConfig", "Functional", "MasVnrType", "HouseStyle", "FireplaceQu", "ExterCond", "ExterQual", "PoolQC"] numerical_columns_subset = ["3SsnPorch", "Fireplaces", "BsmtHalfBath", "HalfBath", "GarageCars", "TotRmsAbvGrd", "BsmtFinSF1", "BsmtFinSF2", "GrLivArea", "ScreenPorch"] X = X[categorical_columns_subset + numerical_columns_subset] X[categorical_columns_subset] = X[categorical_columns_subset].astype("category") categorical_columns = X.select_dtypes(include="category").columns n_categorical_features = len(categorical_columns) n_numerical_features = X.select_dtypes(include="number").shape[1] print(f"Number of samples: {X.shape[0]}") print(f"Number of features: {X.shape[1]}") print(f"Number of categorical features: {n_categorical_features}") print(f"Number of numerical features: {n_numerical_features}")

接下来,将创建一个基线估计器,其中分类特征被删除。然后,将创建一个管道,该管道将对分类特征进行独热编码,其余的数值数据将被传递。

from sklearn.compose import make_column_selector, make_column_transformer from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.pipeline import make_pipeline dropper = make_column_transformer(("drop", make_column_selector(dtype_include="category")), remainder="passthrough") hist_dropped = make_pipeline(dropper, HistGradientBoostingRegressor(random_state=42)) one_hot_encoder = make_column_transformer((OneHotEncoder(sparse_output=False, handle_unknown="ignore"), make_column_selector(dtype_include="category")), remainder="passthrough") hist_one_hot = make_pipeline(one_hot_encoder, HistGradientBoostingRegressor(random_state=42))

然后,将创建一个管道,该管道将把分类特征视为有序的数量,即类别将被编码为0, 1, 2等,并被视为连续特征。

import numpy as np from sklearn.preprocessing import OrdinalEncoder ordinal_encoder = make_column_transformer((OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=np.nan), make_column_selector(dtype_include="category")), remainder="passthrough", verbose_feature_names_out=False) hist_ordinal = make_pipeline(ordinal_encoder, HistGradientBoostingRegressor(random_state=42))

现在,将创建一个HistGradientBoostingRegressor估计器,它将原生处理分类特征。这个估计器不会将分类特征视为有序的数量。设置categorical_features="from_dtype",以便将具有分类dtype的特征视为分类特征。

hist_native = HistGradientBoostingRegressor(random_state=42, categorical_features="from_dtype")

最后,使用交叉验证来评估模型。在这里,将比较模型在mean_absolute_percentage_error和拟合时间方面的表现。

import matplotlib.pyplot as plt from sklearn.model_selection import cross_validate scoring = "neg_mean_absolute_percentage_error" n_cv_folds = 3 dropped_result = cross_validate(hist_dropped, X, y, cv=n_cv_folds, scoring=scoring) one_hot_result = cross_validate(hist_one_hot, X, y, cv=n_cv_folds, scoring=scoring) ordinal_result = cross_validate(hist_ordinal, X, y, cv=n_cv_folds, scoring=scoring) native_result = cross_validate(hist_native, X, y, cv=n_cv_folds, scoring=scoring) def plot_results(figure_title): fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 8)) plot_info = [("fit_time", "Fit times (s)", ax1, None), ("test_score", "Mean Absolute Percentage Error", ax2, None)] x, width = np.arange(4), 0.9 for key, title, ax, y_limit in plot_info: items = [dropped_result[key], one_hot_result[key], ordinal_result[key], native_result[key]] mape_cv_mean = [np.mean(np.abs(item)) for item in items] mape_cv_std = [np.std(item) for item in items] ax.bar(x=x, height=mape_cv_mean, width=width, yerr=mape_cv_std, color=["C0", "C1", "C2", "C3"]) ax.set(xlabel="Model", title=title, xticks=x, xticklabels=["Dropped", "One Hot", "Ordinal", "Native"], ylim=y_limit) fig.suptitle(figure_title) plot_results("Gradient Boosting on Ames Housing") plt.show()

可以看到,独热编码的数据模型是最慢的。这是意料之中的,因为独热编码为每个分类特征的每个类别值创建了一个额外的特征,因此在拟合过程中需要考虑更多的分割点。理论上,预计原生处理分类特征的速度会比将类别视为有序数量('Ordinal')稍慢,因为原生处理需要对类别进行排序。然而,当类别数量较少时,拟合时间应该接近,这在实践中可能并不总是反映出来。

在预测性能方面,删除分类特征会导致性能下降。使用分类特征的三个模型具有可比的错误率,原生处理略胜一筹。

一般来说,当树的深度或节点数量受到限制时,可以预期独热编码数据的预测性能较差:使用独热编码数据时,需要更多的分割点,即更多的深度,才能恢复原生处理中的单一分割点所获得的等效分割。

当类别被视为有序数量时,也是如此:如果类别是A..F,最佳分割是ACF-BDE,独热编码模型将需要3个分割点(每个左节点中的类别一个),而非原生模型将需要4个分割点:1个分割点隔离A,1个分割点隔离F,2个分割点隔离C从BCDE中。

模型在实践中的性能差异将取决于数据集和树的灵活性。为了看到这一点,让使用人为限制总分割数量的欠拟合模型重新运行相同的分析,通过限制树的数量和每棵树的深度来限制总分割数量。

for pipe in (hist_dropped, hist_one_hot, hist_ordinal, hist_native): if pipe is hist_native: pipe.set_params(max_depth=3, max_iter=15) else: pipe.set_params(histgradientboostingregressor__max_depth=3, histgradientboostingregressor__max_iter=15) dropped_result = cross_validate(hist_dropped, X, y, cv=n_cv_folds, scoring=scoring) one_hot_result = cross_validate(hist_one_hot, X, y, cv=n_cv_folds, scoring=scoring) ordinal_result = cross_validate(hist_ordinal, X, y, cv=n_cv_folds, scoring=scoring) native_result = cross_validate(hist_native, X, y, cv=n_cv_folds, scoring=scoring) plot_results("Gradient Boosting on Ames Housing (few and small trees)") plt.show()

这些欠拟合模型的结果证实了之前的直觉:当分割预算受到限制时,原生类别处理策略表现最佳。其他两种策略(独热编码和将类别视为有序值)导致的错误值与仅删除分类特征的基线模型相当。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485