当进行监督学习时, 经常要处理离散变量. 也就是说, 变量没有一个自然的数值表示. 问题是大多数机器学习算法要求输入的数据是数值的. 在某种程度上, 数据科学需要将离散变量转换为数值变量。
>>
分类特征编码有许多方式:
- Label encoding(标签编码)
- One-hot encoding(独热码)
- Vector representation(向量表示法)
- Optimal binning(最佳合并? 不知道怎么翻)
- Target Encoding(目标编码)
每种方法都有其优缺点, 通常取决于数据和需求. 如果一个变量有很多类别, 那么独热码将产生许多冗余列, 这可能会导致内存问题. 根据我的经验, LightGBM/CatBoost自带的编码方式很不错. 标签编码没什么用. 不过, 如果离散变量恰好是有序的, 可以用递增的数字来表示它(比如说, “cold”->0,“mild”->1,“hot”->2). 词嵌入(word2vec)也还不错, 但是需要一些微调, 有点麻烦.
目标编码比较简单实用, 本质非常简单, 当你有一个分类变量$x$和目标label $y$, label可以是离散值也可以是连续值, 对于$x$里的每个不同的样本, 你要计算对应的$y$的平均值, 然后用相应的均值替换$x_i$. 在python和python的pandas库里很容易实现.
来试试
随意生成一些数据样本.1
2
3
4
5
6
7import pandas as pd
df = pd.DataFrame({
'x_0': ['a'] * 5 + ['b'] * 5,
'x_1': ['a'] * 9 + ['b'] * 1,
'y': [1, 1, 1, 1, 0, 1, 0, 0, 0, 0]
})
得到的数据帧是这样的:
$x_0$ | $x_1$ | $y$ |
---|---|---|
a | c | 1 |
a | c | 1 |
a | c | 1 |
a | c | 1 |
a | c | 0 |
b | c | 1 |
b | c | 0 |
b | c | 0 |
b | c | 0 |
b | d | 0 |
计算$x_0$列的均值:1
means = df.groupby('x_0')['y'].mean()
得到一个字典:1
2
3
4{
'a': 0.8,
'b': 0.2
}
然后用对应的值替换掉$x_0$的值:1
df['x_0'] = df['x_0'].map(means)
得到的数据帧:
$x_0$ | $x_1$ | $y$ |
---|---|---|
0.8 | c | 1 |
0.8 | c | 1 |
0.8 | c | 1 |
0.8 | c | 1 |
0.8 | c | 0 |
0.2 | c | 1 |
0.2 | c | 0 |
0.2 | c | 0 |
0.2 | c | 0 |
0.2 | d | 0 |
OK, 接下里对$x_1$列做同样的事:1
df['x_1'] = df['x_1'].map(df.groupby('x_1')['y'].mean())
$x_0$ | $x_1$ | $y$ |
---|---|---|
0.8 | 0.444 | 1 |
0.8 | 0.444 | 1 |
0.8 | 0.444 | 1 |
0.8 | 0.444 | 1 |
0.8 | 0.444 | 0 |
0.2 | 0.444 | 1 |
0.2 | 0.444 | 0 |
0.2 | 0.444 | 0 |
0.2 | 0.444 | 0 |
0.2 | 0 | 0 |
目标编码的好处就在于, 它选择的值具有可解释性. 在这个简单的例子里, 变量$x_0$的$a$值有平均目标值0.8, 这可以帮助后续机器学习算法的学习.
然而目标编码也带来一些问题, 比如过拟合. 实际上. 当平均值的数值很小时, 依赖平均值编码会产生不好的效果. 需要注意的一点是, 需要训练的数据集必须具有一定数量的样本(不能太少). 否则, 运用目标编码会带来严重的过拟合问题.
在这个例子里, $x_1$列的$d$仅有一个样本, 对应的$y$是0, 在这种情况下, 说明编码方式过拟合. 因为这里没有足够的样本说明所有$d$对应的$y$都是0, 因此直接取平均值是不合适的.
把目标编码的数学表达式写一下:
$m$是唯一需要选择的参数, 意思是$m$越高, 越依赖于$w$的总体均值, 如果$m$等于0, 你只需要计算估计均值, 也就是:
也就是说, $m=0$时不做任何平滑处理(smoothing).
写成代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14def calc_smooth_mean(df, by, on, m):
# Compute the global mean
mean = df[on].mean()
# Compute the number of values and the mean of each group
agg = df.groupby(by)[on].agg(['count', 'mean'])
counts = agg['count']
means = agg['mean']
# Compute the "smoothed" means
smooth = (counts * means + m * mean) / (counts + m)
# Replace each value by the according smoothed mean
return df[by].map(smooth)
$m=10$时, 编码的结果:1
2df['x_0'] = calc_smooth_mean(df, by='x_0', on='y', m=10)
df['x_1'] = calc_smooth_mean(df, by='x_1', on='y', m=10)
$x_0$ | $x_1$ | $y$ |
---|---|---|
0.6 | 0.526316 | 1 |
0.6 | 0.526316 | 1 |
0.6 | 0.526316 | 1 |
0.6 | 0.526316 | 1 |
0.6 | 0.526316 | 0 |
0.4 | 0.526316 | 1 |
0.4 | 0.526316 | 0 |
0.4 | 0.526316 | 0 |
0.4 | 0.526316 | 0 |
0.4 | 0.454545 | 0 |
每个编码都很接近, 这是因为$m=10$对于我们的数据集有点太高了:
这种编码方式非常快速, 只需要调整一个参数$m$.
这只是一种非常简单基础的目标编码, 不建议直接用, 建议用贝叶斯目标编码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23>>> import pandas as pd
>>> import xam
>>> X = pd.DataFrame({'x_0': ['a'] * 5 + ['b'] * 5, 'x_1': ['a'] * 9 + ['b'] * 1})
>>> y = pd.Series([1, 1, 1, 1, 0, 1, 0, 0, 0, 0])
>>> encoder = xam.feature_extraction.BayesianTargetEncoder(
... columns=['x_0', 'x_1'],
... prior_weight=3,
... suffix=''
... )
>>> encoder.fit_transform(X, y)
x_0 x_1
0 0.6875 0.541667
1 0.6875 0.541667
2 0.6875 0.541667
3 0.6875 0.541667
4 0.6875 0.541667
5 0.3125 0.541667
6 0.3125 0.541667
7 0.3125 0.541667
8 0.3125 0.541667
9 0.3125 0.375000