在机器学习中,处理分类特征时,目标编码(Target Encoding)是一种常用的技术,它通过将目标变量的平均值分配给每个类别来编码分类特征。这种方法特别适用于分类特征与目标变量之间存在强关系的情况。为了防止过拟合,目标编码在训练数据的编码过程中采用了内部交叉拟合方案。这种方案涉及将数据分成k份,并使用其他k-1份学习到的编码来编码每一份数据。本文通过一个实例来展示交叉拟合过程在防止过拟合中的重要性。
在这个例子中,构建了一个包含三个分类特征的数据集:一个具有中等基数的信息性特征("informative"),一个具有中等基数的无信息性特征("shuffled"),以及一个具有高基数的无信息性特征("near_unique")。首先,生成信息性特征:
import numpy as np
from sklearn.preprocessing import KBinsDiscretizer
n_samples = 50_000
rng = np.random.RandomState(42)
y = rng.randn(n_samples)
noise = 0.5 * rng.randn(n_samples)
n_categories = 100
kbins = KBinsDiscretizer(n_bins=n_categories, encode="ordinal", strategy="uniform", random_state=rng, subsample=None)
X_informative = kbins.fit_transform((y + noise).reshape(-1, 1))
# 通过排列X_informative的值来移除y和箱索引之间的线性关系:
permuted_categories = rng.permutation(n_categories)
X_informative = permuted_categories[X_informative.astype(np.int32)]
中等基数的无信息性特征是通过排列信息性特征并移除与目标的关系来生成的:
X_shuffled = rng.permutation(X_informative)
高基数的无信息性特征是独立于目标变量的。将展示,没有交叉拟合的目标编码会导致下游回归器的灾难性过拟合。这些高基数特征基本上是样本的唯一标识符,通常应该从机器学习数据集中移除。在这个例子中,生成它们是为了展示目标编码的默认交叉拟合行为如何自动缓解过拟合问题:
X_near_unique_categories = rng.choice(int(0.9 * n_samples), size=n_samples, replace=True).reshape(-1, 1)
最后,组装数据集并执行训练测试拆分:
import pandas as pd
from sklearn.model_selection import train_test_split
X = pd.DataFrame(np.concatenate([X_informative, X_shuffled, X_near_unique_categories], axis=1), columns=["informative", "shuffled", "near_unique"])
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
在本节中,在有和没有编码的数据集上训练岭回归模型,并探索使用和不使用内部交叉拟合的目标编码对模型的影响。首先,看到在原始特征上训练的岭模型性能较低。这是因为排列了信息性特征的顺序,这意味着X_informative在原始状态下并不具有信息性:
import sklearn
from sklearn.linear_model import Ridge
# 配置转换器始终输出DataFrame
sklearn.set_config(transform_output="pandas")
ridge = Ridge(alpha=1e-6, solver="lsqr", fit_intercept=False)
raw_model = ridge.fit(X_train, y_train)
print("Raw Model score on training set: ", raw_model.score(X_train, y_train))
print("Raw Model score on test set: ", raw_model.score(X_test, y_test))
接下来,创建了一个包含目标编码器和岭模型的管道。该管道使用TargetEncoder.fit_transform,它使用交叉拟合。可以看到模型很好地拟合了数据,并泛化到了测试集:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import TargetEncoder
model_with_cf = make_pipeline(TargetEncoder(random_state=0), ridge)
model_with_cf.fit(X_train, y_train)
print("Model with CF on train set: ", model_with_cf.score(X_train, y_train))
print("Model with CF on test set: ", model_with_cf.score(X_test, y_test))
线性模型的系数显示,大部分权重都在第0列的特征上,这是信息性特征。
import matplotlib.pyplot as plt
import pandas as pd
plt.rcParams["figure.constrained_layout.use"] = True
coefs_cf = pd.Series(model_with_cf[-1].coef_, index=model_with_cf[-1].feature_names_in_).sort_values()
ax = coefs_cf.plot(kind="barh")
_ = ax.set(title="Target encoded with cross fitting", xlabel="Ridge coefficient", ylabel="Feature")
虽然TargetEncoder.fit_transform使用内部交叉拟合方案来学习训练集的编码,但TargetEncoder.transform本身并不使用。它使用完整的训练集来学习编码并转换分类特征。因此,可以使用TargetEncoder.fit后跟TargetEncoder.transform来禁用交叉拟合。这种编码然后传递给岭模型。
target_encoder = TargetEncoder(random_state=0)
target_encoder.fit(X_train, y_train)
X_train_no_cf_encoding = target_encoder.transform(X_train)
X_test_no_cf_encoding = target_encoder.transform(X_test)
model_no_cf = ridge.fit(X_train_no_cf_encoding, y_train)
评估了在编码时没有使用交叉拟合的模型,并看到它过拟合了:
print("Model without CF on training set: ", model_no_cf.score(X_train_no_cf_encoding, y_train))
print("Model without CF on test set: ", model_no_cf.score(X_test_no_cf_encoding, y_test))
岭模型过拟合是因为它比使用交叉拟合编码特征时赋予了无信息的极高基数("near_unique")和中等基数("shuffled")特征更多的权重。
coefs_no_cf = pd.Series(model_no_cf.coef_, index=model_no_cf.feature_names_in_).sort_values()
ax = coefs_no_cf.plot(kind="barh")
_ = ax.set(title="Target encoded without cross fitting", xlabel="Ridge coefficient", ylabel="Feature")
这个例子展示了目标编码的内部交叉拟合的重要性。使用目标编码来编码训练数据然后传递给机器学习模型是很重要的。当目标编码器是管道的一部分并且管道被拟合时,管道将正确调用TargetEncoder.fit_transform并在编码训练数据时使用交叉拟合。