目录
1 背景
2 原理
2.1 类别特征处理
2.1.1 传统目标编码: TS
2.1.2 Greedy TS
2.1.3 ordered TS编码
2.1.4 CatBoost处理Categorical features总结
2.2.预测偏移处理
2.2.1 梯度无偏估计
2.3 树的构建
3 优缺点
优点
4 代码
1 背景
终于到了CatBoost,这个模型我在打比赛的时候相对于lightGBM用的少一些,但是我一般都会进行尝试,尤其是类别型特征很多的时候。曾依赖这个模型单模干到了top8,那时候还不懂的继承的优雅。我们开始这个模型介绍吧。
CatBoost是一种基于对称决策树(oblivious trees)为基学习器实现的参数较少、支持类别型变量和高准确性的GBDT框架,主要解决的痛点是高效合理地处理类别型特征,这一点从它的名字中可以看出来,CatBoost是由Categorical和Boosting组成。此外,CatBoost还解决了梯度偏差(Gradient Bias)以及预测偏移(Prediction shift)的问题,从而减少过拟合的发生,进而提高算法的准确性和泛化能力。
上面做了一个概述,那么黑体的名字如何理解呢?
另外,与其提升算法不同,CatBoost使用对称全二叉树(这种树的特点是每一层使用相同的分割特征)。这样一来,树是更简单的结构,我们也就避免了过度拟合的危险。此外,由于我们的基础模型结构简单,我们有更快的预测器。
2 原理
2.1 类别特征处理
CatBoost算法的设计初衷是为了更好的处理GBDT特征中的categorical features(比如性别【男,女】)。在处理 GBDT特征中的categorical features的时候,最简单的方法是用 categorical feature 对应的标签的平均值来替换(target encoding, 这个在比赛中我也是常用,但是存在问题)。在决策树中,标签平均值将作为节点分裂的标准。这种方法被称为 Greedy Target-based Statistics , 简称 Greedy TS;
2.1.1 传统目标编码: TS
用公式来表达就是:
, => groupby('cat')[label].mean()
TS编码有一个缺点,极端情况下当训练集中某类取值只有一个样本、或者没有样本时,计算的编码值就失真了,也就是容易受噪声数据影响。
如果强行用标签的平均值来表示特征的话,当训练数据集和测试数据集数据结构和分布不一样的时候会出条件偏移问题。
2.1.2 Greedy TS
一个标准的改进 TS的方式是添加先验分布项,这样可以减少噪声和低频率类别型数据对于数据分布的影响
直接上公式:
其中p是添加的先验项,a通常是大于0的权重系数。添加先验项是一个普遍做法,针对类别数较少的特征,它可以减少噪声数据。对于回归问题,一般情况下,先验项可取数据集label的均值。对于二分类,先验项是正例的先验概率。
Greedy TS编码也存在一个问题,即目标泄露。也需要训练预测集合数据分布一致;
2.1.3 ordered TS编码
它是catboost的主要思想,依赖于排序,受online learning algorithms的启发得到,对于某一个样本,TS的值依赖于观测历史,为了在离线的数据上应用该思想,我们将数据随机排序,对于每一个样本,利用该样本之前数据计算该样本类别值的TS值。如果仅仅使用一个随机序列,那么计算得到值会有较大的方差,因此我们使用不同的随机序列来计算。
在某种排序状态 𝜎 下,样本 在分类特征 𝑘 下的值为 ,的ordered TS编码值是基于排在其前面的样本计算的,在中计算分类特征 𝑘 下取值与相同的样本的Greedy TS编码值,该值即为的ordered TS编码值。举例说明
在上图中,经过样本排序后,样本的排序情况为{4,3,7,2,6,1,5},计算样本4的ordered TS编码值时,由于没有样本排在其前面,因此其ordered TS编码值计算方式为
计算样本6的ordered TS编码值时,排在其前面的样本为{4,3,7,2},在这4个样本中,特征取值为D的只有样本4,因此其ordered TS编码值计算方式为:
2.1.4 CatBoost处理Categorical features总结
- 首先会计算一些数据的statistics。计算某个category出现的频率,加上超参数,生成新的numerical features。这一策略要求同一标签数据不能排列在一起(即先全是之后全是这种方式),训练之前需要打乱数据集。
- 第二,使用数据的不同排列。在每一轮建立树之前,先扔一轮骰子,决定使用哪个排列来生成树。
- 第三,考虑使用categorical features的不同组合。例如颜色和种类组合起来,可以构成类似于blue dog这样的特征。当需要组合的categorical features变多时,CatBoost只考虑一部分combinations。在选择第一个节点时,只考虑选择一个特征,例如A。在生成第二个节点时,考虑A和任意一个categorical feature的组合,选择其中最好的。就这样使用贪心算法生成combinations。
- 第四,除非向gender这种维数很小的情况,不建议自己生成One-hot编码向量,最好交给算法来处理。
2.2.预测偏移处理
在GBDT算法中,每一棵树都是为了拟合前一棵树上的梯度,构造树时所有的样本都参与了,一个样本参与了建树,然后又用这棵树去估计样本值,这样的估计就不是无偏估计,当测试集和训练集上的样本分布不一致时,模型就会因过拟合而性能不佳,即在测试集上产生了预测偏移。
2.2.1 梯度无偏估计
对于样本 𝑥𝑖 ,如果用一个不包含它的模型去估计它的梯度,估计结果可以视为无偏估计。基于这种思路,CatBoost算法中采用了如下策略:在每一轮迭代时,将样本集排序,然后训练 𝑛 个模型 𝑀𝑖,𝑖=1,...,𝑛 ,𝑛为样本数量,其中 𝑀𝑖 是由前 𝑖 个样本训练得到(基于本轮样本的排序,包含样本𝑖),然后估计样本𝑖的梯度时,使用模型𝑀𝑖−1 来估计,因为𝑀𝑖−1是由不包含样本𝑖的样本训练得到,因此该估计结果是无偏估计。
这种方式尽管得到了无偏估计,但是对于排序靠前的样本,它的梯度估计结果可能并不太准确,具有较大的方差,因为估计它的模型是由较少的样本训练的,而且是基于本轮迭代中的样本排序计算的,为了减少预测方差,CatBoost在每轮迭代中都会对样本进行重新排序,然后按照相同的思路估计本轮中样本的梯度,这样多轮迭代的最终结果就可以获取一个较小的方差。
当然,如果每轮迭代都要训练𝑛 个模型,那是一个比较大的工作量,CatBoost算法对这个过程做了些简化来提升训练速度,详情后面分析树的创建过程时再说明。
2.3 树的构建
得到树结构,也是节点分裂的过程。在GBDT、XGBoost、LightGBM等算法中,节点分类时需要遍历所有候选特征及分裂阈值,CatBoost算法也采用了这种策略,但有两点不同:
- 分类型特征处理;
- 数值型特征的空值处理。在CatBoost算法中,将空值全部转换为最小值(默认),或者最大值;
每一轮迭代都会创建 𝑛 个模型,也就是创建𝑛棵树(在CatBoost算法中,并不是𝑛棵树,而是 [𝑙𝑜𝑔2𝑛] ,这个只是为了减少计算量,因此在阐述CatBoost算法的原理时仍然使用𝑛),训练树是比较耗时的过程,在整个模型训练的时间中占很大的比重,如果每一轮都要重新训练𝑛棵树,那将会非常耗时,CatBoost算法在这点上做了调整,具体做是:
1 在第一轮迭代中,在选定样本排序状态 𝜎 下,分别用前 𝑗 个样本训练模型 𝑀𝜎,𝑗 ,也就是得到 𝑛 棵树。一个树的训练过程包含两部分:第一步,得到树结构;第二步,计算叶子节点的值。第一步中得到的树结构,也即是每一层选用什么分裂特征,分裂阈值是多少
2 得到模型𝑀𝜎,𝑗的树结构后,CatBoost会使将该树结构复用到后续的所有迭代过程中。例如在第一轮迭代中,由前100个样本训练得到了模型 𝑀𝜎,100 的树结构,后续的迭代过程中会直接使用该树结构,然后将对应排序状态 𝜎′ 下的前100个样本直接应用该树结构,将样本划分到对应的叶子节点上,得到完整的模型,而不用再重新遍历样本的特征来寻找最佳划分特征和划分阈值。
那么在每一层中,如何评判用哪种特征分裂最好呢?CatBoost算法采用这样的策略:
- 基本本轮样本排序状态,得到每一棵树上的样本,首先依据前面迭代轮次的结果计算每个样本的梯度,也就是得到根节点中每个样本的梯度向量 𝐺 。
-
遍历候选特征来分裂根节点,得到多种分裂结果,分别计算每个样本的叶子值增量 Δ(𝑖) ,其中 𝑖 指第 𝑖 个样本(这个增量值的含义原论文中也没有明确说明,只是说是一个增量值,这里也参照原文进行说明),Δ(𝑖)的计算方式为:在样本 𝑖 所属的叶子节点上,计算排在样本 𝑖 前面的样本的梯度的平均值,该平均值即为Δ(𝑖)。(这里要说明一下,上面说的叶子节点,是相对于上一层的节点来说的,并不是整棵树的叶子节点),这样就得到每个样本的增量值Δ(𝑖),这些增量值可以组成一个向量 Δ;
-
采用余弦相似度方法,计算各种候选特特征下节点分裂的损失 𝑙𝑜𝑠𝑠(𝐺,Δ) ,选择损失最小的方式来分裂树。余弦相似度的计算方式为:
-
-
其中 𝜔𝑖 标识样本𝑖的权重,这个权重是CatBoost算法中为每个样本随机赋予的,起到样本抽样的效果,目的是为了减少过拟合; 𝑎𝑖 标识样本在树上的输出值,这里也就是Δ(𝑖)值; 𝑔𝑖 即第一步中计算的样本的梯度值。
3 优缺点
优点
- 性能卓越: 在性能方面可以匹敌任何先进的机器学习算法;
- 鲁棒性/强健性: 无需调参即可获得较高的模型质量,采用默认参数就可以获得非常好的结果,减少在调参上面花的时间,减少了对很多超参数调优的需求
- 易于使用: 提供与scikit集成的Python接口,以及R和命令行界面;
- 实用: 可以处理类别型、数值型特征,支持类别型变量,无需对非数值型特征进行预处理
- 可扩展: 支持自定义损失函数;
- 快速、可扩展的GPU版本,可以用基于GPU的梯度提升算法实现来训练你的模型,支持多卡并行提高准确性,
- 快速预测:即便应对延时非常苛刻的任务也能够快速高效部署模型;
缺点:
- 对于类别型特征的处理需要大量的内存和时间;
- 不同随机数的设定对于模型预测结果有一定的影响;
4 代码
import re
import os
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
import sklearn
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve,roc_auc_score
import matplotlib.pyplot as plt
import gc
from bayes_opt import BayesianOptimization
from catboost import Pool, cv
n_fold = 5
folds = KFold(n_splits=n_fold, shuffle=True, random_state=1314)
oof = np.zeros(len(train_df))
prediction = np.zeros(len(test_df))
for fold_n, (train_index, valid_index) in enumerate(folds.split(train_df)):
X_train, X_valid = train_df[features].iloc[train_index], train_df[features].iloc[valid_index]
y_train, y_valid = train_df[label].iloc[train_index], train_df[label].iloc[valid_index]
cate_features=[]
# +['corss_卧室_床的数量', 'corss_床的类型_床的数量',
# 'corss_房产类型_卧室数量', 'corss_房产类型_洗手间数量']
train_pool = Pool(X_train, y_train, cat_features=cate_features)
eval_pool = Pool(X_valid, y_valid, cat_features=cate_features)
cbt_model = catboost.CatBoostRegressor(iterations=600, # 注:baseline 提到的分数是用 iterations=60000 得到的,但运行时间有点久
learning_rate=0.1, # 注:事实上好几个 property 在 lr=0.1 时收敛巨慢。后面可以考虑调大
eval_metric='mse',
# n_estimators=3000,
# reg_lambda=5,
use_best_model=True,
random_seed=42,
logging_level='Verbose',
#task_type='GPU',
devices='0',
gpu_ram_part=0.5)
cbt_model.fit(train_pool,
eval_set=eval_pool,
verbose=1000)
y_pred_valid = cbt_model.predict(X_valid)
print("valid RMSE")
print(print(np.sqrt(np.mean(np.square(y_pred_valid - train_df.loc[valid_index,label])))))
y_pred = cbt_model.predict(test_df[features])
oof[valid_index] = y_pred_valid.reshape(-1, )
prediction += y_pred
prediction /= n_fold
from sklearn.metrics import mean_squared_error
score = mean_squared_error(oof, train_df[label].values)
print(score)
#test['价格'] = prediction
#test[['数据ID', '价格']].to_csv('./{}_sub_cat.csv'.format(np.sqrt(score)), index=None)
ref:
安全验证 - 知乎
安全验证 - 知乎