树模型遇上类别型特征(Python)
在数据挖掘项目的数据中,数据类型可以分为两种:有序的连续数值 和 无序的类别型特征。
对于xgboost等boosting树模型,基学习通常是cart回归树,而cart树的输入通常只支持连续型数值类型的,像年龄、收入等连续型变量Cart可以很好地处理,但对于无序的类别型变量(如 职业、地区等),cart树处理就麻烦些了,如果是直接暴力地枚举每种可能的类别型特征的组合,这样找类别特征划分点计算量也很容易就爆了。
在此,本文列举了 树模型对于类别型特征处理的常用方法,并做了深入探讨~
一、one-hot编码处理
我们可以直接对类别型特征做Onehot处理(这也是最常用的做法),每一类别的取值都用单独一位0/1来表示, 也就是一个“性别”类别特征可以转换为是否为“男”、“女” 或者“其他” 来表示,如下:
display(df.loc[:,['Gender_Code']].head())
# onehot
pd.get_dummies(df['Gender_Code']).head()
但是onehot的重大缺点在于,对于取值很多的类别型特征,可能导致高维稀疏特征而容易导致树模型的过拟合。如之前谈到面对高维稀疏的onehot特征,一旦有达到划分条件,树模型容易加深、切分次数越多,相应每个切分出的子特征空间的统计信息越来越小,学习到的可能只是噪音(即 过拟合)。
使用建议:Onehot天然适合神经网络模型,神经网络很容易从高维稀疏特征学习到低微稠密的表示。当onehot用于树模型时,类别型特征的取值数量少的时候还是可以学习到比较重要的交互特征,但是当取值很多时候(如 大于100),容易导致过拟合,是不太适合用onehot+树模型的。
(注:此外 onehot 还有增加内存开销以及训练时间开销等缺点)
二、 Ordinal Encoder
OrdinalEncoder也称为顺序编码 (与 label encoding,两者功能基本一样),特征/标签被转换为序数整数(0 到 n_categories - 1)
使用建议:适用于ordinal feature ,也就是虽然类别型特征,但它存在内在顺序,比如衣服尺寸“S”,“M”, “L”等特征就适合从小到大进行整数编码。
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
df[col] = encoder.transform(df[col])
三、target encoding
target encoding 目标编码也称为均值编码,是借助各类别特征对应的标签信息做编码(比如二分类 简单以类别特征各取值 的样本对应标签值“0/1”的平均值),是一种常用有监督编码方法(此外还有经典的WoE编码),很适合逻辑回归等弱模型使用。
使用建议 : 当树模型使用目标编码,需加入些正则化技巧,减少Target encoding方法带来的条件偏移的现象(当训练数据集和测试数据集数据结构和分布不一样的时候会出条件偏移问题),主流的方法是使用Catboost编码 或者 使用cross-validation求出target mean或bayesian mean。
# 如下简单的target mean代码。也可以用:from category_encoders import TargetEncoder
target_encode_columns = ['Gender_Code']
target = ['y']
target_encode_df = score_df[target_encode_columns + target].reset_index().drop(columns = 'index', axis = 1)
target_name = target[0]
target_df = pd.DataFrame()
for embed_col in target_encode_columns:
val_map = target_encode_df.groupby(embed_col)[target].mean().to_dict()[target_name]
target_df[embed_col] = target_encode_df[embed_col].map(val_map).values
score_target_drop = score_df.drop(target_encode_columns, axis = 1).reset_index().drop(columns = 'index', axis = 1)
score_target = pd.concat([score_target_drop, target_df], axis = 1)
四、CatBoostEncoder
CatBoostEncoder是CatBoost模型处理类别变量的方法(Ordered TS编码),在于目标编码的基础上减少条件偏移。其计算公式为:
- TargetCount : 对于指定类别特征在target value的总和
- prior:对于整个数据集而言,target值的总和/所有的观测变量数目
- FeatureCount:观测的特征列表在整个数据集中的出现次数。
CBE_encoder = CatBoostEncoder()
train_cbe = CBE_encoder.fit_transform(train[feature_list], target)
test_cbe = CBE_encoder.transform(test[feature_list])
五、CountEncoder
也称为频数编码,将类别特征各取值转换为其在训练集出现的频率,这样做直观上就是会以类别取值的频次为依据 划分高频类别和低频类别。至于效果,还是要结合业务和实际场景。
## 也可以直接 from category_encoders import CountEncoder
bm = []
tmp_df=train_df
for k in catefeas:
t = pd.DataFrame(tmp_df[k].value_counts(dropna=True,normalize=True)) # 频率
t.columns=[k+'vcount']
bm.append(t)
for k,j in zip(catefeas, range(len(catefeas))):# 联结编码
df = df.merge(bm[j], left_on=k, right_index=True,how='left')
六、 神经网络embedding
当类别的取值数量很多时(onehot高维),如果直接onehot,从性能或效果来看都会比较差,这时通过神经网络embedding是不错的方法,将类别变量onehot输入神经网络学习一个低维稠密的向量,如经典的无监督词向量表征学习word2vec 或者 基于有监督神经网络编码。
使用建议:特别适合类别变量取值很多,onehot后高维稀疏,再做NN低维表示转换后应用于树模型。
# word2vec
from gensim.models import word2vec
# 加载数据
raw_sentences = ["the quick brown fox jumps over the lazy dogs","yoyoyo you go home now to sleep"]
# 切分词汇
sentences= [s.encode('utf-8').split() for s in sentences]
# 构建模型
model = word2vec.Word2Vec(sentences,size=10) # 词向量的维数为10
# 各单词学习的词向量
model['dogs']
# array([-0.00449447, -0.00310097, 0.02421786, ...], dtype=float32)
七、lgb类别特征处理
为了解决one-hot编码(one vs many )处理类别特征的不足。lgb采用了Many vs many的切分方式,简单来说,是通过对每个类别取值进行数值编码(类似于目标编码),根据编码的数值寻找较优切分点,实现了类别特征集合的较优切分。
- 具体算法原理:
1 、特征取值数目小于等于4(参数max_cat_to_onehot):直接onehot 编码,逐个扫描每一个bin容器,找出最佳分裂点;
2、 特征取值数目大于4: max bin的默认值是256 取值的个数大于max bin数时,会筛掉出现频次少的取值。再统计各个特征值对应的样本的一阶梯度之和,二阶梯度之和,以一阶梯度之和 / (二阶梯度之和 + 正则化系数)作为该特征取值的编码。将类别转化为数值编码后,从大到小排序,遍历直方图寻找最优的切分点
简单来说,Lightgbm利用梯度统计信息对类别特征编码。我个人的理解是这样可以按照学习的难易程度为依据划分类别特征组,比如某特征一共有【狼、狗、猫、猪、兔】五种类别取值,而【狼、狗】类型下的样本分类难度相当高(该特征取值下的梯度大),在梯度编码后的类别特征上,寻找较优划分点可能就是【狼、狗】|vs|【猫、猪、兔】
使用建议: 通常使用lgb类别特征处理,效果是优于one-hot encoding。
# lgb类别处理:简单转化为类别型特征直接输入Lgb模型训练即可。
for ft in category_list:
train_x[ft] = train_x[ft].astype('category')
clf = LGBMClassifier(**best_params)
clf.fit(train_x, train_y)
经验小结
-
对于取值数量很少(<10)的类别型特征,相应的各取值下的样本数量也比较多,可以直接Onehot编码。
-
对于取值数量比较多(10到几百),这时onehot从效率或者效果,都不及lightgbm梯度编码或catboost目标编码,而且直接使用也很方便。(需要注意的是,个人实践中这两种方法在很多取值的类别特征,还是比较容易过拟合。这时,类别值先做下经验的合并或者尝试剔除某些类别特征后,模型效果反而会更好)
-
当几百上千的类别取值,可以先onehot后(高维稀疏),借助神经网络模型做低维稠密表示。
以上就是主要的树模型对类别特征编码方法。实际工程上面的效果,还需具体验证。计算资源丰富的情况下,可以多试几种编码方法,再做特征选择,选取比较有效的特征,效果杠杠的。
(END)
文章首发公众号“算法进阶”,欢迎关注。公众号阅读原文可访问文章相关代码及资料