Smoothing,简单来说,就是将原来独立的高数量类别特征的每个值映射到概率估计上。基本来讲,这个预处理方法将原始的值放置到实际的机器学习模型之前先通过一个简单的特征处理模型(如贝叶斯模型)。
下面以binary target为例进行方法分析:
当target属性 Y∈{0,1}时,假设要处理的特征为X,该特征的每一个不同的值为Xi。我们要做的是, 将高数量类别特征将映射到一个标量Si中,Si代表一个条件概率,即
Xi→Si≅P(Y|X=Xi)−−−(1)
注意到Si代表的是条件概率,那么他的值被归一到了0和1之间,这对于神经网络模型也是一个好的预处理。
下一步就是概率估计的过程。我们假设数据集被分成了nTR个训练集和nTS个测试集。因为这个概率估计成为了模型训练的一部分,所以只有训练集的数据被使用。注意到不是所有的X的可能值都会出现在训练集中,有的值可能只出现在测试集或者新进来的数据中。所以,这个映射过程必须要能够处理这个特征的不可预见性的值。
如果该特征某个值如X=Xi出现的数量足够多,那么这个概率估计可以这样计算:
Si=niYni−−−(2)
这是一个后验概率的计算过程。然而不幸的是,特征的值的数量分布通常是不均匀的,有很多值的数量非常少。所以这种使用P(Y|X=Xi)的直接估计是不太可靠的。
为了减小这种小数量值的影响,Si的计算可能被分成两个概率的组合。后验概率的计算如公式(2),先验概率的计算如P(Y)=nYnTR。整个组合计算公式为:
Si=λ(ni)niYni+(1−λ(ni))nYnTR−−−(3)
nY代表在整个数据集中Y=1的数量。λ(ni)是一个在0-1之间的单调递增函数。
原理:一方面,当特征的某个值的数量很多,即λ≅1时,公式即为(2),计算后验概率。另一方面,当特征的某个值的数量很少时,即λ≅0时,公式前项为0,只计算先验概率。
所以,关键我们怎么选取λ(ni)这个函数呢?有一个典型函数如下:
λ(n)=11+exp−n−kf−−−(4)
这个公式是一个s形状的函数,当 n=k 时值为 0.5 。
参数 f 控制函数在转折处的斜率,决定了先验概率和后验概率之间的平衡。如果f→∞,那么公式(3)变为一个硬间隔,即先验概率和后验概率各占0.5。
参数 k 决定于我们允许的特征值数量的最小值的一半。so important!
我们在来看看经验贝叶斯估计(Empirical Bayes estimation) 的一般公式:
P=Biyi+(1−Bi)y¯¯¯−−−(5)
y¯¯¯是先验概率,yi是经验后验概率。收缩系数Bi根据不同的估计方法有不同的形式。当所有概率分布服从高斯分布的时候:
Bi=niτ2σ2+niτ2−−−(6)
σ2是值的方差,τ2是样本方差。事实上,公式(6)是公式(4)的一般形式。
以下是我在kaggle中看到大佬对该方法的coding实现,借以参考,帮助理解:
def add_noise(series, noise_level): return series * (1 + noise_level * np.random.randn(len(series))) def target_encode(trn_series=None, tst_series=None, target=None, min_samples_leaf=1, smoothing=1, noise_level=0): """ trn_series : training categorical feature as a pd.Series tst_series : test categorical feature as a pd.Series target : target data as a pd.Series min_samples_leaf (int) : minimum samples to take category average into account smoothing (int) : smoothing effect to balance categorical average vs prior """ assert len(trn_series) == len(target) assert trn_series.name == tst_series.name temp = pd.concat([trn_series, target], axis=1) # Compute target mean averages = temp.groupby(by=trn_series.name)[target.name].agg(["mean", "count"]) # Compute smoothing smoothing = 1 / (1 + np.exp(-(averages["count"] - min_samples_leaf) / smoothing)) # Apply average function to all target data prior = target.mean() # The bigger the count the less full_avg is taken into account averages[target.name] = prior * (1 - smoothing) + averages["mean"] * smoothing averages.drop(["mean", "count"], axis=1, inplace=True) # Apply averages to trn and tst series ft_trn_series = pd.merge( trn_series.to_frame(trn_series.name), averages.reset_index().rename(columns={'index': target.name, target.name: 'average'}), on=trn_series.name, how='left')['average'].rename(trn_series.name + '_mean').fillna(prior) # pd.merge does not keep the index so restore it ft_trn_series.index = trn_series.index ft_tst_series = pd.merge( tst_series.to_frame(tst_series.name), averages.reset_index().rename(columns={'index': target.name, target.name: 'average'}), on=tst_series.name, how='left')['average'].rename(trn_series.name + '_mean').fillna(prior) # pd.merge does not keep the index so restore it ft_tst_series.index = tst_series.index return add_noise(ft_trn_series, noise_level), add_noise(ft_tst_series, noise_level)
两个方法比较
smoothing方法可以只要通过对本地数据集的操作就可完成预处理,而clustering方法需要更复杂的算法而且可能导致信息量的减少(因为最后仍然需要进行one-hot编码)。
当然,对于smoothing方法来说, λ函数的选择直接影响到了结果,可能不同的λ函数可以适用于不同的分支领域,这都需要实验的考证。