来自俄罗斯在线搜索公司Yandex的CatBoost快速且易于使用,但同一家公司的研究人员最近发布了一种基于神经网络的新软件包NODE,声称其性能优于CatBoost和所有其他梯度增强方法。 这是真的吗? 让我们找出如何同时使用CatBoost和NODE!

该文章适用于谁?

尽管我是为那些对机器学习特别是表格数据感兴趣的人写这篇博客的,但是如果您熟悉Python和scikit-learn库,并且希望跟随代码一起学习,对您很有帮助。 否则,希望您会发现理论和概念方面都很有趣!

CatBoost简介

CatBoost是我建模表格数据的首选包。这是一个梯度增强决策树的实现,只是做了一些微调,使其与例如xgboost或LightGBM略有不同。它对分类和回归问题都有效。

关于CatBoost的一些好处:

  • 它处理分类特征(虽然不是最优解),所以你不需要担心如何编码它们。
  • 它通常只需要很少的参数调优。
  • 它避免了其他方法可能遭受的某些微妙类型的数据泄漏。
  • 它速度很快,如果你想让它跑得更快,可以在GPU上运行。

这些因素使得CatBoost对我来说,当我需要分析一个新的表格数据集时,首先要做的就是使用它。

CatBoost的技术细节

如果你只是想使用CatBoost,请跳过这一节!

在更技术的层面上,关于CatBoost的实现有一些有趣的事情。如果您对细节感兴趣,我强烈推荐论文Catboost: unbiased boosting with categorical features。我只想强调两件事。

在论文中,作者指出,标准的梯度增强算法会受到一些微妙的数据泄漏的影响,这些泄漏是由模型的迭代拟合方式引起的。同样,最有效的对分类特征进行数字编码的方法(如目标编码)也容易出现数据泄漏和过拟合。为了避免这种泄漏,CatBoost引入了一个人工时间轴,根据训练示例到达的时间轴,这样在计算统计数据时只能使用“以前看到的”示例。

CatBoost实际上并不使用常规决策树,而是使用遗忘的决策树。在这些树中,在树的每一层上,相同的特性和相同的分割标准被到处使用!这听起来很奇怪,但有一些不错的属性。让我们看看这是什么意思。

使用CatBoost和NODE建模表格数据对比测试_神经网络

遗忘的决策树。 每个级别都有相同的拆分。

使用CatBoost和NODE建模表格数据对比测试_决策树_02

常规决策树。 每个级别上都可以存在任何功能或分割点。

在普通的决策树中,要分割的特性和截止值都取决于到目前为止在树中所走的路径。这是有意义的,因为我们可以使用我们已经拥有的信息来决定最有意义的下一个问题。有了健忘决策树,历史就不重要了;我们无论如何都要提出同样的问题。这些树被称为“健忘的”,因为它们总是“忘记”发生过的事情。

为什么这个有用?健忘决策树的一个很好的特性是,一个例子可以非常快速地分类或得分——它总是提出相同的N个二叉问题(其中N是树的深度)。对于许多例子来说,这可以很容易地并行完成。这是CatBoost快速发展的原因之一。另一件要记住的事情是我们这里处理的是一个树集合。作为一种独立的算法,健忘决策树可能没有那么好,但树集合的思想是,由于错误和偏见被“洗掉”,一个弱学习者的联盟经常工作得很好。通常情况下,弱学习者是一棵标准的决策树,而在这里,它甚至更弱,也就是健忘决策树。CatBoost的作者认为,这种特殊的弱学习者在泛化方面工作得很好。

安装CatBoost

安装CatBoost是非常简单的

pip install catboost

我在Mac上有时会遇到这样的问题。在Linux系统上,比如我现在输入的Ubuntu系统,或者在谷歌Colaboratory上,它应该“正常工作”。如果安装时一直有问题,可以考虑使用Docker镜像。

docker pull yandex/tutorial-catboost-clickhouse
docker run -it yandex/tutorial-catboost-clickhouse

在数据集上使用CatBoost

让我们看看如何在表格数据集上使用CatBoost。我们先下载一个稍微预处理过的成人/人口普查收入数据集,下面假设它位于datasets/ Adult .csv中。我选择这个数据集是因为它混合了分类和数字特征,在数以万计的示例中有一个很好的可管理的规模,并且没有太多的特征。它经常用于举例说明算法,例如在谷歌的What-If工具和许多其他地方。

成人人口普查数据包括“年龄”、“工作类别”、“教育程度”、“受教育程度”、“婚姻状况”、“职业”、“关系”、“种族”、“性别”、“资本收益”、“资本损失”、“每周小时”、“本土国家”和“<=50K”。任务是预测最后一列’ <=50K '的值,该列指示相关人员的年收入是否为50,000美元或更少(数据集来自1994年)。我们认为以下特征是分类的而不是数字的:“工人阶级”、“教育”、“婚姻地位”、“职业”、“关系”、“种族”、“性别”、“原住民”。

该代码与scikit-learn非常相似,除了CatBoost用于将数据集的特征值和目标值捆绑在一起,同时在概念上保持它们分离的Pool数据类型之外。

代码可以在Colab上找到(https://colab.research.google.com/drive/1WezJuc3ioEUZYKh_Mm7YVjWcMeYjDNKP)但我将在这里复制以供参考。CatBoost需要知道哪些特性是分类的,然后自动处理它们。在这个代码片段中,我还使用了5倍(分层)交叉验证来估计预测精度。

from catboost import CatBoostClassifier, Pool
from hyperopt import fmin, hp, tpe
import pandas as pd
from sklearn.model_selection import StratifiedKFolddf = pd.read_csv('https://docs.google.com/uc?' + 
                 'id=10eFO2rVlsQBUffn0b7UCAp28n0mkLCy7&' + 
                 'export=download')
labels = df.pop('<=50K')categorical_names = ['workclass', 'education', 'marital-status',
                     'occupation', 'relationship', 'race',
                     'sex', 'native-country']  
categoricals = [df.columns.get_loc(i) for i in categorical_names]nfolds = 5
skf = StratifiedKFold(n_splits=nfolds, shuffle=True)
acc = []for train_index, test_index in skf.split(df, labels):
  X_train, X_test = df.iloc[train_index].copy(), \
                    df.iloc[test_index].copy()
  y_train, y_test = labels.iloc[train_index], \
                    labels.iloc[test_index]
  train_pool = Pool(X_train, y_train, cat_features = categoricals)
  test_pool = Pool(X_test, y_test, cat_features = categoricals)
  model = CatBoostClassifier(iterations=100,
                             depth=8,
                             learning_rate=1,
                             loss_function='MultiClass') 
  model.fit(train_pool)
  predictions = model.predict(test_pool)
  accuracy = sum(predictions.squeeze() == y_test) / len(predictions)
  acc.append(accuracy)mean_acc = sum(acc) / nfolds
print(f'Mean accuracy based on {nfolds} folds: {mean_acc:.3f}')
print(acc)

通过运行此命令(不进行超参数优化的CatBoost),我们往往会获得85%到86%的平均准确度。 在上次运行中,我获得了约85.7%的j结果。

如果我们想尝试优化超参数,可以使用hyperopt(如果您没有,请使用pip install hyperopt进行安装)。 为了使用它,您需要定义一个hyperopt试图最小化的函数。 我们将在此处尝试优化准确性。 最佳化例如 log loss,等

要优化的主要参数可能是迭代次数,学习率和树深度。 还有许多其他与过度拟合相关的参数,例如提前停止回合等。 随意自行探索!

# Optimize between 10 and 1000 iterations and depth between 2 and 12search_space = {'iterations': hp.quniform('iterations', 10, 1000, 10),
                'depth': hp.quniform('depth', 2, 12, 1),
                'lr': hp.uniform('lr', 0.01, 1)
               }def opt_fn(search_space):  nfolds = 5
  skf = StratifiedKFold(n_splits=nfolds, shuffle=True)
  acc = []  for train_index, test_index in skf.split(df, labels):
    X_train, X_test = df.iloc[train_index].copy(), \
                      df.iloc[test_index].copy()
    y_train, y_test = labels.iloc[train_index], \
                      labels.iloc[test_index]
    train_pool = Pool(X_train, y_train, cat_features = categoricals)
    test_pool = Pool(X_test, y_test, cat_features = categoricals)    model = CatBoostClassifier(iterations=search_space['iterations'],
                             depth=search_space['depth'],
                             learning_rate=search_space['lr'],
                             loss_function='MultiClass',
                             od_type='Iter')    model.fit(train_pool, logging_level='Silent')
    predictions = model.predict(test_pool)
    accuracy = sum(predictions.squeeze() == y_test) / len(predictions)
    acc.append(accuracy)  mean_acc = sum(acc) / nfolds
  return -1*mean_accbest = fmin(fn=opt_fn, 
            space=search_space, 
            algo=tpe.suggest, 
            max_evals=100)

当我上次运行此代码时,它花费了5个小时以上,但平均准确度为87.3%。

完整性检查:逻辑回归

在这一点上,我们应该问问自己,这些新奇的方法是否真的有必要。在超参数优化之后,一个好的旧逻辑回归将如何进行开箱即用?

为了简单起见,这里我将省略重新生成代码,但它在Colab笔记本中与以前一样可用。逻辑回归实现的一个细节是,它不像CatBoost处理分类变量的,所以我决定代码使用目标编码,具体分析目标编码,这是节点和一个相当接近中采取的方法虽然不是相同的模拟CatBoost会发生什么。

长话短说,使用这种编码方式的逻辑回归的未调优精度约为80%,在超参数调优后约为81%(在我最近的运行中为80.7%)。这里,一个有趣的替代方法是尝试自动预处理库,如vtreat和Automunge,这个作为后续的优化工作吧!

在尝试NODE之前,到目前为止我们有什么?

Logistic回归 ,未调整 :80.0%

Logistic回归,调整后:80.7%

CatBoost ,未调整:85.7%

CatBoost,调整后:87.2%

NODE:Neural Oblivious Decision Ensembles

Yandex研究人员最近的一份论文描述了CatBoost的一个有趣的神经网络版本,或者至少是一种用于健忘决策树集成的神经网络(如果你想提醒自己这里的“健忘”是什么意思,请参阅上面的技术部分)。这种架构称为NODE,可以用于分类或回归。

摘要中的一项声明是:“通过在大量表格数据集上与领先的GBDT包进行广泛的实验比较,我们展示了提议的节点架构的优势,它在大多数任务上优于竞争对手。”这自然激起了我的兴趣。这个工具会比CatBoost更好吗?

NODE是如何工作的?

你应该去论文上看完整的介绍,但是一些相关的细节是:

entmax激活函数用作常规决策树中拆分的软版本。 正如论文所说,“ entmax能够产生稀疏的概率分布,其中大多数概率恰好等于0。在这项工作中,我们认为entmax也是我们模型中的一个适当的归纳偏差,它允许在内部树节点中构造可微的分裂决策。 直观地讲,entmax可以学习基于小子集的数据特征(最多一个,就像在经典的决策树中一样)来分割决策,从而避免了其他方面的不良影响。” entmax函数允许神经网络模仿决策树类型的系统,同时保持模型的可微性(权重可以基于梯度进行更新)。

作者提出了一种新型的层,即“节点层”,可以在神经网络中使用(使用PyTorch实现)。节点层表示树集成。

可以堆叠多个节点层,从而产生一个分层模型,其中输入一次只能通过一个集成树来提供。输入表示的连续连接可以用来给出一个模型,这让人想起用于图像处理的流行的DenseNet模型,只是专门用于表格数据。

节点模型的参数为:

  1. 学习率(本文均为0.001)
  2. 节点层数(k)
  3. 每层树的数量(m)
  4. 每层树的深度(d)

为什么说NODE与树的集成是相似的?

为了了解神经网络体系结构和决策树集合之间的相似性,这里复制了图1。

使用CatBoost和NODE建模表格数据对比测试_数据挖掘_03

如何选择参数?

论文中没有太多的指导;建议采用超参数优化方法。他们提到他们优化了以下空间:

  1. num层:{2,4,8}
  2. 树总数:{1024,2048}
  3. 树深度:{6,8}
  4. 树输出dim: {2,3}

在我的代码中,我不做网格搜索,而是让hyperopt在一定范围内采样值。我考虑这个问题的方式(可能是错误的)是,每一层都表示一个树集合(比方说CatBoost的单个实例)。对于您添加的每一层,您可能会添加一些表示能力,但是您也会使模型变得更重,难以训练,并且可能会有过拟合的风险。树的总数大致类似于CatBoost/xgboost/random forest中的树的数量,并且有相同的权衡:树很多时,可以表达更复杂的函数,但是模型需要更长的时间来训练,并且有过拟合的风险。同样,树的深度也有同样的权衡。至于输出维度,坦白地说,我不太明白为什么它是一个参数。似乎回归应该等于1,分类应该等于类的数量。

如何使用NODE?

作者在GitHub上发布了代码。它们不提供命令行界面,而是建议用户在提供的Jupyter笔记本中运行它们的模型。在这些笔记本中提供了一个分类示例和一个回归示例。

README页面也强烈建议使用GPU来训练节点模型。(这是支持CatBoost的一个因素。)

我准备了一个合作的笔记本,里面有一些关于如何在NODE上运行分类以及如何用hyperopt优化超参数的示例代码。

https://colab.research.google.com/drive/11VH01T5BNDGEBLlZlG28z_BB_3MIBGWt

这里我将突出显示代码的一些部分。

修改代码的一般问题

我在修改作者的代码时遇到的问题主要与数据类型有关。重要的是输入数据集(X_train和X_val)是浮点32格式的数组(numpy或torch);不是float64或者float和int的混合。标签需要像int64一样编码,用于分类,而float32用于回归。

其他问题与内存有关。这些模型可以快速地消耗GPU内存,特别是在作者的示例笔记本中使用的大批处理尺寸。我简单地解决了这个问题,在我的笔记本电脑(以及后来的Colab)上使用最大的批量大小。

不过,总的来说,让代码正常工作并不难。文档有点少,但足够了。

分类变量处理

与CatBoost不同,NODE不支持分类变量,因此您必须自己将它们准备成数字格式。我们使用来自category_encoders库的LeaveOneOutEncoder,与节点作者使用相同的方法对成人人口普查数据集执行此操作。在这里,出于方便,我们使用常规的训练/测试分割,而不是5倍CV,因为训练NODE需要很长时间(特别是在超参数优化时)。

from category_encoders import LeaveOneOutEncoder
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_splitdf = pd.read_csv('https://docs.google.com/uc' + 
                 '?id=10eFO2rVlsQBUffn0b7UCAp28n0mkLCy7&' + 
                 'export=download')
labels = df.pop('<=50K')
X_train, X_val, y_train, y_val = train_test_split(df,
                                                  labels,
                                                  test_size=0.2)class_to_int = {c: i for i, c in enumerate(y_train.unique())}                                                                                                               
y_train_int = [class_to_int[v] for v in y_train]                                                                                                                            
y_val_int = [class_to_int[v] for v in y_val] cat_features = ['workclass', 'education', 'marital-status',
                'occupation', 'relationship', 'race', 'sex',
                'native-country']
  
cat_encoder = LeaveOneOutEncoder()
cat_encoder.fit(X_train[cat_features], y_train_int)
X_train[cat_features] = cat_encoder.transform(X_train[cat_features])
X_val[cat_features] = cat_encoder.transform(X_val[cat_features])# Node is going to want to have the values as float32 at some points
X_train = X_train.values.astype('float32')
X_val = X_val.values.astype('float32')
y_train = np.array(y_train_int)
y_val = np.array(y_val_int)

模型定义和训练循环

代码的其余部分与作者的回购协议基本相同(hyperopt部分除外)。他们创建了一个名为DenseBlock的Pytorch层,该层实现了节点架构。一个名为Trainer的类保存了关于实验的信息,并且有一个直接的训练循环,它跟踪到目前为止看到的最好的度量标准,并绘制更新后的损失曲线。

结果与结论

通过一些最小的尝试和错误,我能够找到一个验证精度约为86%的模型。在使用hyperopt进行超参数优化后(它本应在Colab的GPU上通宵运行,但实际上,经过40次迭代后就超时了),最佳性能达到87.2%。在其他几轮中,我的成绩为87.4%。换句话说,在进行了hyperopt调优之后,NODE的表现确实优于CatBoost,尽管只是略微优于CatBoost。

然而,准确性并不是一切。必须对每个数据集进行代价高昂的优化还是不太方便。

NODE和CatBoost的优点:

  1. 似乎可以得到稍微好一点的结果(基于论文和本次测试;我一定会尝试许多其他数据集!)

CatBoost与NODE的优点:

  1. 快得多
  2. 少需要超参数优化
  3. 没有GPU运行良好
  4. 支持分类变量

实际项目会用哪一个?也许CatBoost仍将是我的首选工具,但如果是kaggle这种线上的比赛NODE是一个不错的尝试

同样重要的是要认识到性能是依赖于数据的,成人人口普查收入数据集并不能代表所有场景。或许更重要的是,分类特征的预处理在NODE中也是相当重要的一个问题。

作者:Mikael Huss

deephub翻译组