CatBoost 是如何自动高级处理类别特征的?

Posted 我爱Python数据挖掘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CatBoost 是如何自动高级处理类别特征的?相关的知识,希望对你有一定的参考价值。

CatBoost是一个开放源码的梯度提升库。CatBoost和其他梯度提升库之间的一个区别是它对类别型特征的高级处理(实际上包名中的“Cat”不是代表猫,而是代表“categorical”)。前面我们已经对CatBoost基本原理和应用做了简单的介绍,可以点击查看。喜欢本文记得收藏、关注、点赞。

注:完整版代码、技术交流,如下方式获取。

目前开通了技术交流群,群友已超过2000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友

  • 方式、添加微信号:dkl88191,备注:来自CSDN
  • 方式、微信搜索公众号:Python学习与数据挖掘,后台回复:加群
  • 机器学习中的类别型特征

  • CatBoost中的类别型特征处理

  • 类别型特征设置如何影响预测旧车价格的准确性

机器学习中的类别型特征

Сategorical特征是一种具有一组称为类别的离散值的特征,它们之间不能通过<>进行比较。在实际数据集中,我们经常需要处理类别型数据。一个类别型特征的基数,在具有不同的特征的数据集之间,特征可以使用的不同值,且其数量也有很大的差异——从几个不同的值到成千上万个不同的值不等。

类别型特征的值可以近似均匀分布,也可能存在类别型特征的值出现的频率相差多个数量级的情况。要在梯度增强模型中使用类别型特征,需要将其转换为某种可以由决策树处理的形式,例如数值。在下一节中,我们将简要介绍机器学习中将类别型特征值转换为数值的常用方法,以及类别型特征预处理的常用方法。

One-hot Encoding 独热编码

包括为每个类别创建一个二分类特征。在 类别型特征基数比较低(low-cardinality features) 时,即该特征的所有值去重后构成的集合元素个数比较少,一般利用One-hot编码方法将特征转为数值型。One-hot编码可以在数据预处理时完成,也可以在模型训练的时候完成,从训练时间的角度,后一种方法的实现更为高效,CatBoost对于基数较低的类别型特征也是采用后一种实现。这种编码方式的主要问题是,高基数类别型特征(high cardinality features) 当中,比如 user ID会产生大量新的特征,造成维度灾难。

Label Encoding 标签编码

映射每个类别,即一个类别型特征可以带入一个随机数的值。虽然这样处理看似比较合理,但它在实践中效果真的很一般。

Hash Encoding 哈希编码

使用哈希函数将字符串类型特征转换为固定维向量。

Frequency Encoding 频率编码

是用数据集中类别的频率替换类别特征值。

Target Encoding 目标编码

将类别型特征的值替换为一个数字,对于分类变量的特定值,从目标值的分布中计算出来的数字。最直接的方法是使用属于该类别的对象上的目标的平均值,在决策树中,标签平均值将作为节点分裂的标准,这种方法被称为Greedy Target Encoding,简称 Greedy TS,用公式来表达就是:

然而,这种方法有一个显而易见的缺陷,就是通常特征比标签包含更多的信息,如果强行用标签的平均值来表示特征的话,当训练数据集和测试数据集数据结构和分布不一样的时候会出条件偏移问题。一个标准的改进 Greedy TS的方式是添加先验分布项,这样可以减少噪声和低频率类别型数据对于数据分布的影响:

其中 是添加的先验项, 通常是大于 0 的权重系数。添加先验项是一个普遍做法,针对类别数较少的特征,它可以减少噪声数据。对于回归问题,一般情况下,先验项可取数据集label的均值。对于二分类,先验项是正例的先验概率。利用多个数据集排列也是有效的,但是,如果直接计算可能导致过拟合。CatBoost利用了一个比较新颖的计算叶子节点值的方法,这种方式(oblivious trees,对称树)可以避免多个数据集排列中直接计算会出现过拟合的问题。

Target Encoding 目标编码方法还会导致目标泄漏和过拟合。这些问题的一个可能的解决方案是holout Target Encoding——训练数据集的一部分用于计算每个类别的目标统计数据,然后对其余的训练数据进行训练。它解决了目标泄漏问题,但需要我们牺牲部分宝贵的训练数据。

因此,在实践中最流行的解决方案是使用 K-Fold Target Encoding 和 Leave-One-Out Target Encoding。K-Fold Target Encoding 背后的思想与 k折交叉验证非常相似——我们将训练数据分成几个fold,在每个fold中,我们将类别型特征值替换为在其他fold中计算出的类别的目标统计数据。Leave-One-Out Target Encoding 是K- fold Encoding 的一种特殊情况,其中K等于训练数据的长度。

K-Fold Target Encoding 和 Leave-One-Out Target Encoding 也会导致过拟合。例如:在一个训练数据集中,我们有一个具有单个值的单一类别型特征和5个类0对象和6个类1对象。显然,只有一个可能值的特征是无用的,但是,如果我们使用均值函数的Leave-One-Out 目标编码,对于所有的0类对象,特征编码值将被编码为0.6,而对于所有的1类对象,特征编码值将被编码为0.5。决策树分类器将在0.55处选择一个切分点,这样就导致了在训练集上的准确率达100%。

CatBoost中的类别型特征处理

CatBoost支持一些传统的分类数据预处理方法,如独热编码和频率编码。然而,这个包的特征之一是它的类别型特征编码的original解决方案。

CatBoost类别型特征预处理背后的核心思想是Ordered Target Encoding:对数据集进行随机排列,然后仅使用放置在当前对象之前的对象对每个样本进行某种类型的目标编码(例如,仅计算该类对象的目标均值)。

在CatBoost中将类别型特征转化为数值特征一般包括以下步骤:

  1. 随机排列训练数据集。

  2. Quantization:即根据任务类型将目标值从浮点数转换为整数:

  • Classification —— 目标值可能取值为“0”(不属于指定的目标类)和“1”(属于指定的目标类)。

  • Multiclassification —— 目标值是目标类的整数标识符(从“0”开始)。

  • Regression —— 在标签值上进行分箱,分箱方式和箱子的数量在初始化函数参数中设置。所有位于一个箱子内的值都被分配一个标签值类——一个在公式定义的范围内的整数:<bucketID - 1>

  1. 对分类特征值进行编码。

CatBoost为训练数据集创建四种排列,并为每一种排列训练一个单独的模型。三个模型用于树的结构选择,第四个模型用于计算保存的最终模型的叶子值。在每次迭代中,随机选择三个模型中的一个,该模型用于选择新的树结构来生成树,并计算所有四种模型的叶子值。

采用多种模型进行树结构选择,增强了类别型特征编码的鲁棒性。

CatBoost 可以结合现有的类别型特征创建新的类别型特征。除非你明确告诉它不要这样做。原始特征和创建的特征的处理可以分别通过设置参数 simple_ctr 和 combination_ctr 控制。

类别型特征实战

旧车价格预测

数据集

这里使用kaggle中数据集 https://www.kaggle.com/lepchenkov/usedcarscatalog。该数据集由旧车描述及其特征组成——既有数值型特征,如里程、生产年份等,也有类别型特征,如颜色、制造商名称、型号名称等。

我们的目标是解决回归任务,即预测旧车的价格。

df = pd.read_csv('cars.csv')

看下类别型特征的情况。

categorical_features_names = ['manufacturer_name', 'model_name', 'transmission', 
                              'color', 'engine_fuel', 'engine_type',  'body_type',
                              'state', 'drivetrain','location_region']
df[categorical_features_names].nunique()
manufacturer_name      55
model_name           1118
transmission            2
color                  12
engine_fuel             6
engine_type             3
body_type              12
state                   3
drivetrain              3
location_region         6
dtype: int64

接下来看下目标变量的分布状况。

ax = sns.distplot(df.price_usd.values)
np.median(df.price_usd.values)

首先,我们要粗略估计树的数量和足够完成这个任务所需的学习速率。

拆分训练集和测试集,并定义为pool类。

from catboost import CatBoost, CatBoostRegressor, Pool
df_ = df.sample(frac=1., random_state=0)
df_train = df_.iloc[: 2 * len(df) // 3]
df_test = df_.iloc[2 * len(df) // 3 :]
train_pool = Pool(df_train.drop(['price_usd'], 1), 
                  label=df_train.price_usd,
                  cat_features=categorical_features_names)
test_pool = Pool(df_test.drop(['price_usd'], 1),
                 label=df_test.price_usd,
                 cat_features=categorical_features_names)
                 
model = CatBoostRegressor(
          custom_metric= ['R2', 'RMSE'],
          learning_rate=0.1,
          n_estimators=5000)
model.fit(train_pool, 
          eval_set=test_pool, 
          verbose=2000, plot=True)
0:	learn: 5935.7603510	test: 6046.0339243
    best: 6046.0339243 (0)	
    total: 53ms	remaining: 4m 25s
2000:	learn: 1052.8405096	test: 1684.8571308
      best: 1684.8571308 (2000)	
      total: 55.3s	remaining: 1m 22s
4000:	learn: 830.0093394	test: 1669.1267503
      best: 1668.7626148 (3888)	
      total: 1m 55s	remaining: 28.8s
4999:	learn: 753.5299104	test: 1666.7826842
      best: 1666.6739968 (4463)	
      total: 2m 27s	remaining: 0us

bestTest = 1666.673997
bestIteration = 4463

Shrink model to first 4464 iterations.

现在我们将编写一个简单的函数,在给定参数的情况下测试 CatBoost 在 3 折交叉验证上的性能,并返回最后一个模型的完整参数列表(model.get_all_params())。此函数将模型的指标与使用默认分类特征参数训练的模型的结果进行比较。

我们将估计器的数量固定为 4500,学习率固定为 0.1。

kf = KFold(n_splits=3, shuffle=True)
DEFAULT_PARAMETERS = 'n_estimators' : 4500, 'learning_rate' : 0.1
DEFAULT_MODEL_METRICS = 

def score_catboost_model(catboost_parameters, update_defaults=False):
    r2_values = []
    rmse_values = []
    catboost_parameters.update(DEFAULT_PARAMETERS)
    for train_index, test_index in kf.split(df):
        train_pool = Pool(df.iloc[train_index].drop(['price_usd'], 1),
                          label=df.iloc[train_index].price_usd,
                          cat_features=categorical_features_names)
        test_pool = Pool(df.iloc[test_index].drop(['price_usd'], 1),
                        label=df.iloc[test_index].price_usd,
                        cat_features=categorical_features_names)
        model = CatBoost(catboost_parameters)
        model.fit(train_pool, verbose=False)
        
        r2_values.append(r2_score(df.iloc[test_index].price_usd.values, model.predict(test_pool)))
        rmse_values.append(mean_squared_error(df.iloc[test_index].price_usd.values,
                                              model.predict(test_pool),
                                              squared=False)) 
    if update_defaults:
        DEFAULT_MODEL_METRICS['R2'] = np.mean(r2_values)
        DEFAULT_MODEL_METRICS['RMSE'] = np.mean(rmse_values)
        print('R2 score: :.4f(:.4f)'.format(np.mean(r2_values), np.std(r2_values)))
        print('RMSE score: :.0f(:.0f)'.format(np.mean(rmse_values), np.std(rmse_values)))  

    else:
        DEFAULT_MODEL_R2 = DEFAULT_MODEL_METRICS['R2']
        DEFAULT_MODEL_RMSE = DEFAULT_MODEL_METRICS['RMSE']
        r2_change = 100 * (np.mean(r2_values) - DEFAULT_MODEL_R2) / DEFAULT_MODEL_R2
        rmse_change = 100 * (np.mean(rmse_values) - DEFAULT_MODEL_RMSE) / DEFAULT_MODEL_RMSE
        print('R2 score: :.4f(:.4f) :+.1f% compared to default parameters'.format(
            np.mean(r2_values), np.std(r2_values), r2_change))
        print('RMSE score: :.0f(:.0f) :+.1f% compared to default parameters'.format(
            np.mean(rmse_values), np.std(rmse_values), rmse_change))  
        
    return model.get_all_params()

CatBoost 中的类别型特征编码参数

CatBoost 中与类别型特征处理相关的参数数量非常庞大。完整参数列表如下:

  • one_hot_max_size (int) —— 对具有小于或等于给定参数值的多个不同值的所有类别型特征使用 one-hot 编码。没有对这些特征执行复杂的编码。回归任务的默认值为 2。

  • model_size_reg (float from 0 to inf) —— 模型尺寸正则化系数。该值越大,模型尺寸越小。只有具有类别型特征的模型(其他模型很小)才需要这种正则化。如果类别型特征具有很多值,则具有类别型特征的模型的权重可能会达到数十 GB 或更多。如果正则化器的值不为零,则使用具有大量值的类别型特征或特征组合会受到惩罚,因此在生成的模型中使用较少的特征。默认值为 0.5

  • max_ctr_complexity —— 可以组合的最大特征数。每个结果组合由一个或多个类别型特征组成,并且可以选择包含以下形式的二元特征:“numeric feature > value”。对于 CPU 上的回归任务,默认值为 4。

  • has_time (bool) —— 如果为True,则不执行类别型特征处理的第一步,即排列。当数据集中的对象按时间排序时,该参数很有用。默认值为False

  • simple_ctr —— 简单类别型特征的量化设置。

  • combination_ctr —— 类别型特征组合的量化设置。

  • per_feature_ctr —— 类别型特征的每特征量化设置。

  • counter_calc_method 决定是否使用验证数据集(通过 fit 方法的参数 eval_set 提供)使用 Counter 估计类别频率。默认情况下,它是 Full 并且使用来自验证数据集的对象;传递 SkipTest 值以忽略验证集中的对象。

  • ctr_target_border_count —— 用于需要它的类别型特征的目标量化的最大边界数。回归任务的默认值为 1。

  • ctr_leaf_count_limit —— 具有类别型特征的最大叶子数。默认值为无,即没有限制。

  • store_all_simple_ctr —— 如果之前的参数 ctr_leaf_count_limit 在某个点梯度提升树不能再按类别型特征进行分割。默认值 False 限制适用于原始类别型特征和 CatBoost 通过组合不同特征创建的特征。如果此参数设置为 True,则仅限制对组合特征进行的拆分次数。

三个参数 simple_ctr、combinations_ctr 和 per_feature_ctr 是控制类别型特征处理的第二步和第三步的复杂参数。我们将在下一节中讨论它们。

默认参数

首先,我们测试开箱即用的 CatBoost 类别型处理。

last_model_params = score_catboost_model(, True)
R2 score: 0.9340(0.0015)
RMSE score: 1652(17)

为了进行对比测试,这里将使用默认参数来处理类别型特征的模型保存,作为baseline模型,待后续使用。

One-Hot 编码最大尺寸

参数:one_hot_max_size

首先让 CatBoost 对所有的类别型特征使用 one-hot 编码(我们数据集中的最大类别型特征基数是 1118 < 2000)。从CatBoost文档中了解到,对于使用 one-hot 编码的特征,不会计算其他编码。

该参数在不同场景下会有不同的默认值:

  • N/A:在 Pairwise 评分模式下,在 CPU 上执行训练

  • 255:在 GPU 上进行训练,并且所选的 Ctr 类型需要在训练期间不可用的目标数据

  • 10:在排名模式下进行训练

  • 2:以上条件都不满足

model_params = score_catboost_model(
        'one_hot_max_size' : 2000)
R2 score: 0.9392(0.0029) +0.6% compared to default parameters
RMSE score: 1584(28) -4.5% compared to default parameters

从我们的数据集可以看出,该参数效果较好。然而,在开始我们也提到,one-hot 编码根本不可能用于具有非常大基数的类别型特征。

模型尺寸正则化系数

参数:model_size_reg

如果训练数据具有类别型特征,则此参数会影响模型大小。

有关类别型特征的信息对模型的最终大小有很大贡献。为模型中使用的每个分类特征存储从类别型特征值哈希到某些统计值的映射。特定特征的此映射大小取决于此特征采用的唯一值的数量。

因此,在树中选择一个拆分点来减小模型的最终大小时,可以在最终模型中考虑类别型特征的潜在大小。选择最佳分割时,计算所有分割分数,然后选择具有最佳分数的分割。但在选择得分最高的分组之前,所有分数都按照以下公式变化:

是被某些类别型特征或组合特征分割的新分数, 是特征分割的旧分数, 是特征的唯一值的数量, 是所有特征中所有 值的最大值,是 model_size_reg 参数的值。

这种正则化在 GPU 上的工作方式略有不同:特征组合的正则化比在 CPU 上更积极。对于 CPU 花费在组合上的成本等于训练数据集中存在的该组合中不同特征值的数量。即在 GPU 上,组合的成本等于该组合所有可能的不同值的数量。例如,如果组合包含两个 categories 特征 c1 和 c2,则成本将为 c1 中的 #categories 乘以 c2 中的 #categories,即使此组合中的许多值可能不存在于数据集中。我们将模型大小正则化系数设置为 0 —— 允许模型使用尽可能多的 category 特征及其组合。

model_params = score_catboost_model('model_size_reg': 0)
# ---
model_params = score_catboost_model('model_size_reg': 1)
R2 score: 0.9360(0.0014) +0.3% compared to default parameters
RMSE score: 1626(26) -2.0% compared to default parameters
--- 
R2 score: 0.9327(0.0020) -0.1% compared to default parameters
RMSE score: 1667(30) +0.5% compared to default parameters

为了检查模型的大小如何受此设置的影响,我们将编写一个函数,给定参数dict将训练模型,将其保存在一个文件中,并返回模型的权重:

from pathlib import Path
def weight_model(catboost_parameters):
    catboost_parameters.update(DEFAULT_PARAMETERS)
    model = CatBoost(catboost_parameters)
    model.fit(train_pool, verbose=False)
    model.save_model('model_tmp')
    model_size = Path('model_tmp').stat().st_size
    return model_size
weight_model('model_size_reg': 0)/weight_model('model_size_reg': 1)
12.689550532622183

我们可以看到,具有强正则化的模型几乎比没有正则化的模型小13倍。

组合的特征数量

参数:max_ctr_complexity

值得注意的是几个类别型特征的任意组合都可视为新的特征。例如,在音乐推荐应用中,我们有两个类别型特征:用户ID和音乐流派。如果有些用户更喜欢摇滚乐,将用户ID和音乐流派转换为数字特征时,根据上述这些信息就会丢失。结合这两个特征就可以解决这个问题,并且可以得到一个新的强大的特征。然而,组合的数量会随着数据集中类别型特征的数量成指数增长,因此不可能在算法中考虑所有组合。为当前树构造新的分割点时,CatBoost会采用贪婪的策略考虑组合。对于树的第一次分割,不考虑任何组合。对于下一个分割,CatBoost将当前树的所有组合、类别型特征与数据集中的所有类别型特征相结合,并将新的组合类别型特征动态地转换为数值型特征。CatBoost还通过以下方式生成数值型特征和类别型特征的组合:树中选定的所有分割点都被视为具有两个值的类别型特征,并像类别型特征一样被进行组合考虑。

虽然文档中没有提到,但这个参数值必须是 ≤15 。

score_catboost_model('max_ctr_complexity': 6)
# ---
score_catboost_model('max_ctr_complexity': 0)
R2 score: 0.9335(0.0016) +0.0% compared to default parameters
RMSE score: 1657(24) -0.2% compared to default parameters
---
R2 score: 0.9286(0.0041) -0.5% compared to default parameters
RMSE score: 1716(30) +3.4% compared to default parameters

我们可以看到,在我们的数据集中,模型的准确性差异不显著。为了检查模型的大小如何受到影响,我们将使用对模型进行加权的函数。

weight_model('max_ctr_complexity': 6)/weight_model('max_ctr_complexity': 0)
6.437194589788451

可以看出,可以组合多达6个特征的模型大小是完全不组合特征的模型大小的6倍。

Has time

参数:has_time

设置该参数为True时,我们不会在将类别型特征转换为数值型特征期间执行随机排列。当我们的数据集的对象已经按时间排序时,需要保持数据集中的时间序列信息,这就可能会很有用。如果输入数据中存在时间戳类型列,则该参数可用于确定对象的顺序。

model_params = score_catboost_model('has_time': True)
R2 score: 0.9174(0.0029) -1.7% compared to default parameters
RMSE score: 1847(29) +11.3% compared to default parameters

simple_ctr & combinations_ctr

simple_ctr 和combinations_ctr 都是复杂的参数,提供分类特征编码类型的规则。simple_ctr 负责处理数据集中最初存在的分类特征,combinations_ctr 影响新特征的编码,CatBoost 通过组合现有特征创建。simple_ctr 和combinations_ctr 的可用编码方法和可能取值是相同的,所以我们不打算分开看。当然,可以随时根据任务单独调整它们。

没有Target quantization的编码

Target quantization 使用一些边界值将浮点目标值转换为整数目标值。首先考虑不需要这种转换的目标编码方法。

FloatTargetMeanValue(仅限 GPU)

第一个是 FloatTargetMeanValue 是最直接的方法。category特征的每个值都替换为目标在当前对象之前放置的同一类别的对象的平均值。

score_catboost_model('simple_ctr' : 'FloatTargetMeanValue',
                      'combinations_ctr' : 'FloatTargetMeanValue',
                      'task_type' : 'GPU')
R2 score: 0.9183(0.0022) -1.6% compared to default parameters
RMSE score: 1837(32) +10.7% compared to default parameters
FeatureFreq(仅限 GPU)

第二个是 FeatureFreq,category特征值替换为数据集中该类别的频率。同样,仅使用放置在当前对象之前的对象。

score_catboost_model('simple_ctr' : 'FeatureFreq',
                      'combinations_ctr' : 'FeatureFreq',
                      'task_type' : 'GPU')
R2 score: 0.9170(0.0019) -1.8% compared to default parameters
RMSE score: 1852(12) +11.6% compared to default parameters
Counter

Counter 计数器方法与传统频率编码非常相似,由以下公式定义:

是当前categories的对象数量, 是最频繁categories的对象数量,是由参数prior定义数量。

score_catboost_model('simple_ctr' : 'Counter',
                      'combinations_ctr' : 'Counter')
R2 score: 0.9270(0.0033) -0.7% compared to default parameters
RMSE score: 1736(23) +5.1% compared to default parameters
CtrBorderCount

假设我们已经计算了category 特征的编码。这些编码是浮点数,它们是可比较的:在 Counter 的情况下,较大的编码值对应于更频繁的 category。但是,如果我们有大量 categories,并且相近的 categories 编码之间的差异可能是由噪声引起的,此时我们并不希望模型区分这些相近的 categories。出于这个原因,我们将float编码转换为 int 编码 。默认情况下 CtrBorderCount=15,设置意味着 。其实我们可以尝试使用更大的值:

score_catboost_model('combinations_ctr': 
                      ['Counter:CtrBorderCount=40:Prior=0.5/1'],
                      'simple_ctr':
                      ['Counter:CtrBorderCount=40:Prior=0.5/1'])
R2 score: 0.9337(0.0013) -0.0% compared to default parameters
RMSE score: 1655(13) +0.2% compared to default parameters
BinarizedTargetMeanValue

第二种方法 BinarizedTargetMeanValue 与目标编码非常相似,除非我们使用 bean 值的总和而不是精确目标值的总和。对应于以下公式:

Where

  • 是该类别型特征的标签值整数之和与最大标签值整数之比。

  • 是具有与当前特征值匹配的特征值的对象总数。

  • 是由起始参数定义的数字(常数)。

model_params = score_catboost_model('combinations_ctr': 'BinarizedTargetMeanValue',
                                     'simple_ctr': 'BinarizedTargetMeanValue')
k:v for k, v in model_params.items() if k in ctr_parameters
R2 score: 0.9312(0.0008) -0.2% compared to default parameters
RMSE score: 1685(20) +1.6% compared to default parameters

'combinations_ctr': ['BinarizedTargetMeanValue:
  CtrBorderCount=15:CtrBorderType=Uniform:
  TargetBorderCount=1:TargetBorderType=MinEntropy:
  Prior=0/1:Prior=0.5/1:Prior=1/1'],
 'simple_ctr': ['BinarizedTargetMeanValue:
 CtrBorderCount=15:CtrBorderType=Uniform:
 TargetBorderCount=1:TargetBorderType=MinEntropy:
 Prior=0/1:Prior=0.5/1:Prior=1/1']

在使用 BinarizedTargetMeanValue 方法时,我们还可以微调 Prior 和 CtrBorderCount&#x

以上是关于CatBoost 是如何自动高级处理类别特征的?的主要内容,如果未能解决你的问题,请参考以下文章

30 分钟看懂 CatBoost(Python代码)

30 分钟看懂 CatBoost(Python代码)

catboost原理以及Python代码

如何使用 sklearn 管道跟踪 catboost 的分类索引

XGBoost/CatBoost中具有大量类别的分类变量

XGBoost/CatBoost中具有大量类别的分类变量