最近在做机器学习比赛的时候,遇到了Target Encoding。所谓Target Encoding,是一种特征工程方式,根据训练集中的标签信息生成特征,来提高模型的性能。比较常见的是对于二分类问题(即需要预测的标签是0和1),根据训练集中的某一列特征对训练集进行groupby操作,然后计算每个分组内标签的均值,作为新的特征。
例如下图中,根据原始特征id
,生成了id_target_mean
这一列均值特征。
上图展示的是最朴素的一种办法,这种办法非常直观,但是常常不work,带来的最明显的问题就是过拟合,训练集分数会飙升,而验证集的分数会剧烈下降。但Target Encoding确实是一种非常好的特征工程,只是需要一些额外的操作(即Regularization)来防止过拟合,下面就介绍几种带Regularization的Target Encoding方式。
CV Loop
CV即cross validation,这种方式有点类似于交叉验证,利用交叉验证的思路来进行Target Encoding。具体地:
- 将训练集分成几份(例如5份);
- 对于每一份训练集,该训练集上的均值特征通过在其他份训练集上进行groupby mean等操作得到;
- 对于测试集(或验证集),用全部的训练集进行groupby mean等操作得到。
这种方式能够很好地避免过拟合,并且通常使用4折、5折交叉验证就能够得到很好的效果。下面给出python的实现代码,也非常简单:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37def get_kfold_mean(X_train, X_test, cols, target, folds=5):
"""
Args:
X_train: 训练集
X_test: 测试集或验证集
cols: 特征列表,表示根据这些特征做groupby
target: 标签列名
folds: 交叉验证的折数
Returns:
df: pandas.DataFrame, df.shape[0] = X_train.shape[0] + X_test.shape[0]
包含根据cols生成的target均值特征
"""
skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=918)
train, test = pd.DataFrame(), pd.DataFrame()
for col in cols:
new_col = col + '_' + target + '_mean'
train[new_col] = np.zeros(X_train.shape[0])
for tr_idx, val_idx in skf.split(X_train, X_train[target]):
X_tr, X_val = X_train.iloc[tr_idx], X_train.iloc[val_idx]
for col in cols:
new_col = col + '_' + target + '_mean'
tmp_means = X_val[col].map(X_tr.groupby(col)[target].mean())
train[new_col][val_idx] = tmp_means
prior = X_train[target].mean()
for col in cols:
new_col = col + '_' + target + '_mean'
train[new_col].fillna(prior, inplace=True)
target_map = X_train.groupby(col)[target].mean()
test[new_col] = X_test[col].map(target_map)
test[new_col].fillna(prior, inplace=True)
return pd.concat([train, test], axis=0).reset_index(drop=True)
一个使用的例子:
但是这种方式在极端情况下也存在leakage的风险,例如LOO(Leave One Out),只有5个样本,用5折交叉,就相当于每次都剔除当前行的样本,所以叫Leave One Out。因此也衍生出一种LOO的Target Encoding方式,即剔除当前行的target信息,python实现也很简单,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def get_loo_mean(X_train, X_test, cols, target):
prior = X_train[target].mean()
train, test = pd.DataFrame(), pd.DataFrame()
for col in cols:
new_col = col + '_' + target + '_mean'
target_sum = X_train.groupby(col)[target].transform('sum')
n_objects = X_train.groupby(col)[target].transform('count')
train[new_col] = (target_sum - X_train[target]) / (n_objects - 1)
train[new_col].fillna(prior, inplace=True)
test[new_col] = X_test[col].map(X_train.groupby(col)[target].mean())
test[new_col].fillna(prior, inplace=True)
return pd.concat([train, test], axis=0).reset_index(drop=True)
Add Random Noise
对Target Encoding出的特征加入随机噪声,这个不太好控制,噪声太大会让特征失效,噪声太小还是会过拟合。这种方法通常与LOO一起使用。
Smoothing
这种方式通过超参数$\alpha$来控制regularization的程度,如果$\alpha$等于0,相当于没有regularization,如果$\alpha$等于正无穷,则相当于用global mean。通常需要与其他regularization方法一起使用。
1 | def get_smooth_mean(X_train, X_test, cols, target, m=300): |
Expanding mean
根据累积sum和累积count来生成均值特征。这种方式的好处在于不需要调整超参,但是生成的均值特征的质量可能有高有低,而且取决于数据的顺序(因为是用的累积sum和累积count),具体实现可看代码。CatBoost中内置了这种算法。
1 | def get_expanding_mean(X_train, X_test, cols, target): |
总结
在实际使用过程中,没有哪一种方式是Silver Bullet,有时间可以都试一试。但总的来说,还是推荐使用CV loop或者Expanding mean。