一文了解 LightGBM

df804ce83416060ae8d6f48c104f6896.png

来源:我得学城‍‍‍‍‍‍‍‍‍
本文约18000字,建议阅读30+分钟
本文介绍了LightGBM一种基于梯度提升框架的机器学习算法。‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

LightGBM(Light Gradient Boosting Machine)是一种基于梯度提升框架的机器学习算法,专门用于解决分类和回归等问题。它是由微软团队开发的,旨在提供高效、快速和准确的梯度提升算法实现。

与传统的梯度提升方法相比,LightGBM 在许多方面具有优势:

高效性: LightGBM 使用了一种称为 "基于直方图的学习"(Histogram-based Learning)的技术,它能够高效地处理数据,减少内存消耗并提高训练速度。这是通过将数据分成直方图(histogram)来实现的,从而降低了复杂度。

快速: 由于使用了基于直方图的学习和其他一些优化技术,LightGBM 在大规模数据集上训练的速度很快,因此在处理大量数据时特别有用。

准确性: LightGBM 在决策树的生长过程中使用了一种称为 "GOSS"(Gradient-based One-Side Sampling)的技术,以更有效地选择需要分裂的叶子节点。这有助于提高模型的准确性。

支持并行化: LightGBM 支持多线程并行化,在多核处理器上可以更充分地利用计算资源,进一步提高了训练速度。

可扩展性: LightGBM 支持大规模数据集的训练和预测,可以在内存有限的情况下处理大量的特征和样本。

LightGBM 在许多机器学习竞赛和实际应用中取得了出色的表现,成为了许多数据科学家和机器学习从业者的首选算法之一。它的灵活性、高效性和准确性使其成为处理结构化数据的强大工具。

原文:

https://medium.com/@walter_sperat/a-deep-dive-into-lightgbm-part-1-b6856a2e09c6

1. 介绍

2014 年,陈天奇(Tianqi Chen)以首个高效实现梯度提升决策树(Gradient Boosted Decision Trees)的库 XGBoost 的发布席卷了整个世界。在几个月内,使用这个新库的开发者们已经打破了多项性能纪录,包括赢得了几场 Kaggle 竞赛。该实现在处理表格数据时可以轻松胜过当时的 SOTA 方法(主要是随机森林和神经网络)。

是什么使得 XGBoost 如此出色呢?简而言之,有两个原因:

  • 第一个是通过添加严格的正则化改进了典型的 CART 树生长算法;

  • 第二个是它的速度。基础代码(用 C++编写)具有算法级别的优化(例如用于快速预排序数据的直方图方法),以及硬件级别的优化(数据在内存中的存储方式和并行化策略)。

在 2016 年,微软以其典型的方式,窃取了别人的创意,并将资金投入其中,直到它变得庞大,发布了 LightGBM。该库最初在很大程度上取代了 XGBoost,因为它使用了相同的树学习方法,以及数据排序的直方图方法。然而,这个新实现引入了一些有趣的东西,其中大部分都旨在使其更快(以至于在它旁边,甚至 XGBoost 看起来都可能很慢),以及修改了树生长的方式:

  • 与 XGBoost 一样,它实现了数据排序的直方图方法。我个人没有查看过代码,但有理由怀疑他们做了一些不同的事情,以使其更加高效(但请不要引用我说的话)。

  • 基于梯度的单边采样(GOSS)是一种非常聪明的减少梯度计算时间的方法。在计算伪残差时,算法会对那些具有较小残差的元素进行大幅度的子采样,强制模型聚焦于难以学习的元素(即具有较大梯度的元素),就像 AdaBoost 一样。这反过来会使计算梯度时要考虑的元素较少,从而加快计算速度。

  • 独家特征捆绑(EFB)基于费舍尔(Fisher)在 1958 年发表的一篇论文(真的)。本质上,对分类变量进行最佳分割的方法是构建两个组(或分区),并使每个组进入其自己的分支。然而,由于(名义)分类变量在定义上缺乏顺序,诸如独热编码之类的策略往往会使这个过程对树来说过于复杂和混乱,通常需要连续进行几次分割才能得到正确的结果。为了找到最佳分区,LightGBM 使用梯度和海森矩阵的组合来计算分类特征的数值表示,以最佳方式对它们进行排序。这种策略(对于分类特征和高度稀疏的特征组合都适用)既可以提高模型性能,又可以优化训练时间。

  • 叶子导向的树生长是 CART 算法的一种修改,其中在执行分割之前会测试所有可用的叶子节点,并且分割只会在具有最大误差增量的节点上执行。这往往会创建出非常深入、不对称的树,很容易对数据进行过拟合。

  • 对缺失值的固有处理使得理论上不需要进行填充。这里的一般思想是值不是随机生成的,因此缺失值包含一些固有的信息,因此聪明地填充可能实际上会导致性能损失。该实现会学习对于每个分割,缺失值应该朝哪个方向前进(对于分类特征,默认情况下它们会向右前进)。这是一个特别令人惊奇的功能,因为它允许模型从数据集中提取每一个信息单位。有关实现的更多详细信息,请查看这里。

与 XGBoost 和大多数其他机器学习工具一样,LightGBM 主要是用 C++编写的,其中夹杂着一些 C 语言。然后是库的封装部分,有一个是用 C 编写的,一个是用 R 编写的,还有一个是用 Python 编写的。还有用于 GPU 训练的 CUDA 部分...

我是一个非常以 Python 为导向的数据科学家,所以我们将研究三个主要的 Python API:scikit-learn API、数据 API 和训练 API。

正如名称所示,sklearn API 遵循该库的 fit/predict/predict_proba 的方式,以一种非常方便和面向对象的方式封装功能。另一方面,数据和训练 API 相互协作,采用了非常函数式的方法,我认为这种方法更加清晰,尽管它与 scikit-learn 对象的兼容性不太好。

通过这个简洁(我希望如此)且信息密集的介绍,我们已经准备好继续进行一些代码示例。在本文中,我们将介绍通过 scikit-learn API 提供的主要类以及如何使用 lightgbm 的最重要功能。

2. scikit-learn API

就像所有的 scikit-learn 估计器一样,LGBMClassifier 和 LGBMRegressor 分别继承自 sklearn.base.BaseEstimator 以及 sklearn.base.ClassifierMixin 和 sklearn.base.RegressorMixin。这意味着它们必须实现以下方法:

  • set_params,它接受一个字典,并将关键参数设置为其各自的值。

  • get_params,它返回一个包含所有构造器参数的字典。

  • score,它在内部使用 predict 方法来计算经过训练的模型的增益/损失分数。

在内部,scikit-learn API 使用 lightgbm 的本机(函数式)API,因此这些类基本上是以 scikit-learn 友好的方式行为的封装器。

还有第三个符合 sklearn 标准的类,LGBMRanker,专门用于排名问题(比如推荐)。由于 sklearn 没有明确实现任何排名类,这个类也将留待以后讨论。

3. 训练

3.1 使用 numpy 数据

现在,让我们动手操作一下。与所有 sklearn 对象一样,LGBMModel 可以在 numpy 数组、scipy 稀疏矩阵和 pandas 数据帧/序列上进行训练。让我们首先创建一些 numpy 数组:

import numpy as np
from sklearn.datasets import make_classification


categorical_data = np.random.choice(
  a=['a', 'b', 'c'],
  size=(100_000, 1),
)




numerical_data, y = make_classification(
  n_samples=100_000,
  n_features=20,
  n_informative=10,
  n_classes=2,
  random_state=42
  )

现在我们有一些数据存储在两个 numpy 数组中。在这种特定情况下,分类数据与目标没有关联信息,但在实际中,您可能会有一些重要的分类变量。然而,从方法论上讲,基本是一样的。

在建模方面,lightgbm 只能理解分类特征作为正整数,因此我们必须首先进行转换,如下所示:

from sklearn.preprocessing import OrdinalEncoder


encoder = OrdinalEncoder()
encoded_categories = encoder.fit_transform(categorical_data)

这样,我们就可以将数据合并起来并训练模型了:

from lightgbm import LGBMClassifier


X = np.hstack((numerical_data, encoded_categories))


model = LGBMClassifier(random_state=42)
model.fit(X, y, categorical_feature=[20])

从版本 4 开始,会出现一个非常烦人的警告,大致意思是“已在参数中找到 categorical_feature 关键字,将被忽略”。这并不意味着 lightgbm 将把该特征视为普通的数值特征,只是模型在内部覆盖了一个设置为 None 的变量,所以不要像我第一次看到它时那样恐慌。如果您想确切了解正在发生的事情,这是触发警告的代码块。

回到正题,categorical_feature 参数接受一个可迭代对象(比如列表或 numpy 数组),其中包含分类列的索引(在这种情况下是列索引 20)。如果我们有多个分类列,列表会更长。通过这种方式,不管实际数据是否为整数,算法都能够如上所述使用 EFB,忽略编码的绝对值(这正是我们需要的)。

显然,我们可以将所有内容放在 Pipeline ColumnTransformer 组合内:

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer


preprocessor = ColumnTransformer([
  ('encoder', OrdinalEncoder(), [20])
], remainder='passthrough')


model = Pipeline(
  ('encoder', preprocessor),
  ('model', LGBMClassifier(random_state=42))
)


model.fit(X, y, model__categorical_feature=[20])

非常重要的一点是要记住,sklearn 复合估计器使用双下划线表示法来引用其内部对象(model__categorical_feature 是指模型对象的 categorical_feature 参数)。

是不是感觉非常方便?让我告诉您,一旦我们转向 pandas,情况将会变得更加美好。

3.2 基于 pandas 数据

第一步是创建一个数据框(如果您的数据已经以 pandas 形式存在,则不需要这一步):

import pandas as pd


X_pd = pd.DataFrame(X, columns=[f'col_{n}' for n in range(X.shape[1])])

到目前为止,一切都很顺利,在这里没有什么奇怪的,只是一种普通的 pandas 数据帧构建方法。

现在是第一个有趣的部分。首先,我们使用 pandas 的 select_dtypes 方法来识别数值列和分类列:

numerical_columns = X_pd.select_dtypes(exclude=['object']).columns.to_list()
categorical_columns = X_pd.select_dtypes(include=['object']).columns.to_list()

其次,我们将分类列转换为更高效的数据类型:

X_pd[categorical_columns] = X_pd[categorical_columns].astype('category')

如果您不知道的话,pandas 有一种内置的分类类型,允许我们方便地使用字符串,并在内存中具有非常高效的表示方式。巧合的是,lightgbm 完美地理解 pandas 的类别:

 
 
model = LGBMClassifier(random_state=42)
model.fit(X_pd, y)

LGBMClassifier 会识别分类数据类型,并自动配置一切以使用 EFB。就是这样,不需要编码器、流水线或列转换器!

这是为像我这样懒惰的人设计的:

  • 基于决策树,该算法对异常值、偏斜分布和几乎所有情况都具有强大的稳健性。

  • 内置的缺失值学习策略使填充大部分情况下变得不太必要。

  • 内置的分类值处理使得编码是可选的(尽管我仍然建议尝试两种或三种策略)。

总的来说,构建复合估计器不仅可能是不必要的,甚至可能会对性能产生负面影响。

3.3 约束

案例知识通常会影响建模决策,如特征工程和算法选择。此外,我们还可以使用它来为模型添加约束。这里的两个相关参数是

monotone_constraint 和 interaction_constraint。

第一个参数基本上就像名称所示,它强制所选特征与目标具有单调(当然)关系。例如,假设我们正在对房价进行建模,其中之一的变量是房地产的面积。作为人类,我们知道在其他条件相等的情况下,更大的面积意味着更高的价格。这意味着价格可以随着面积增长而上升或保持不变。在模型上强制此约束的方式如下:

model = LGBMClassifier(monotone_constraint=[0, 1, 0, -1, 0])

这意味着第一个、第三和第五个特征(即零)不会应用约束,而目标和第二个特征之间的关系必须是正单调的(即当变量增加时,预测永远不会下降),第四个变量和目标之间的关系是负单调的(即当变量增加时,预测永远不会增加)。

此外,可以使用三种方法强制执行单调性约束:basic、intermediate 和 advanced。basic 强制受约束节点的右叶节点大于(或小于)左叶节点。方法 intermediate 和 advanced 源自这篇论文;intermediate 是一种稍慢但性能更好的启发式方法,而 advanced 可能会显著降低速度,但显著提高性能。

您还可以对模型施加其他约束,如交互约束:

model = LGBMClassifier(interaction_constraints=[[0, 3, 4], [1, 2]])

这意味着包含特征 0、3 或 4 的任何分支不能包含其他特征。换句话说,如果一个分支由特征 3 分割,那么下一个分割要考虑的特征只能是 0 和 4。相反,另一组包含特征 1 和 2,这意味着有两组特征是不允许相互交互的。

每种类型的约束都会强制施加其自己特定的模型简化方式,可以使结果更具解释性,提高泛化能力。

4. 预测

我们现在有了一个训练好的模型。接下来怎么办?就像所有其他的 sklearn 分类器一样,LGBMClassifier 既有 predict 方法,也有 predictproba 方法,可以在测试和生产中用于不同的 X 数据集:

 
 
predictions = model.predict(X_)
scores = model.predict_proba(X_)

第一个方法将返回一个由 0 和 1 组成的 numpy 数组(或在多类情况下,包含从 0 到 k-1 的整数,其中 k 是类的数量)。第二个方法将返回一个二维 numpy 数组,每个类别对应一个列(二元分类有两列等),每个观测对应一行;这些数字对应于每个样本的得分(与概率有很弱的相关性,但绝对不是概率)。

然而,请注意,pandas 的 astype('category') 方法几乎可以处理任何给定的内容。这意味着当您传递新数据时,您需要确保强制使底层数据表示与原始数据集中的相同。为了做到这一点,我们必须从分类列中提取类别表示:

 
 
categorical_dtypes = [X_pd[column].dtype for column in categorical_columns]


X_[categorical_columns] = pd.concat([
  X_[[column]].astype(dtype)
  for column, dtype
  in zip(categorical_columns, categorical_dtypes)
], axis=1)

上述代码将提取原始数据集中存储数据的特定方式,并强制新数据集使用完全相同的表示方式。

值得注意的是,预测方法不需要指出分类特征,因为该信息已经存储在训练好的模型内部。

有了这些预测,您可以绘制一些图表、计算一些指标、决定要联系哪些客户,或者任何您想要的操作。就这样,对吧?还不完全是。

根据您所在的行业,解释预测结果的重要性可能会更大或更小,而 shap 是一个出色的库,可以做到这一点。然而,它的速度相当慢。为了解决这个问题,lightgbm 开发人员添加了一个内置的 Shapley 值特征重要性方法。幸运的是,我们可以非常简单地使用它:

shap_values = model.predict_proba(X_, pred_contrib=True)

考虑到我们的原始数据有 21 列,这个数组将有 22 列:一个用于数据集中的每一列(按照相同的顺序),以及一个额外的列,其中包含数据集的期望值(通常是正类到负类的比例);最后一列只有一个值,通常可以忽略。

现在,您可以以任何您喜欢的方式分析这些重要性,但我建议首先将它们存储在一个数据帧中:

importances = pd.DataFrame(
  shap_values,
  columns=X_pd.columns.to_list()+['expectation']


)

另一个很好而且相当不寻常的事情是,您可以构建一个称为“树嵌入”的东西,它来自于 sklearn 的 RandomTreesEmbedding。这将生成一个样本的向量表示,由整数构建,向量的每个元素表示整个模型中的一个叶节点(这意味着向量的形状将为 n_samples*n_trees),并且将是该特定树的叶子的索引。例如,结果 [3, 5, 18] 表示所选元素落在第一个树的叶子 3,第二个树的叶子 5,以及第三个树的叶子 18。

 
 
embeddings = model.predict_proba(X_, pred_leaf=True)

这些嵌入可以用于研究数据集或生成数据的替代表示,或者几乎可以用嵌入来做的任何事情。

5. 评分和调优

与任何其他回归器或分类器一样,lightgbm 类可以用于进行交叉验证预测和评分:

from sklearn.model_selection import cross_val_predict, cross_val_score


fit_params = {'categorical_feature': [20]}


cv_predictions = cross_val_predict(
  model,
  X_,
  cv=5,
  method='predict_proba',
  fit_params=fit_params
)


cv_scores = cross_val_score(
  model,
  X_,
  y_,
  cv=5,
  fit_params=fit_params


)

这应该基本上是无痛的(除了分类变量的烦人警告之外)。

此外,lightgbm 有许多超参数需要调整,因此我们可以使用 scikit-learn 的调优类:

from sklearn.model_selection import RandomizedSearchCV
from scipy import stats


distributions = {
  'n_estimators': stats.randint(low=50, high=1000),
  'max_depth': stats.randint(low=1, high=20),
  'random_state': [42]
}


search = RandomizedSearchCV(
  model,
  param_distributions=distributions,
  n_iter=100,
  cv=5,
  random_state=42


)

6. 结论

LightGBM 是一个高速的梯度提升决策树实现,比 XGBoost 还要快,能够有效地从缺失值和分类特征中进行学习。

一种非常便捷的使用方式是使用其 LGBMClassifier 和 LGBMRegressor 类,这些类遵循 scikit-learn 的 fit/predict/predict_proba 接口,以及 get_params 和 set_params。这使得 lightgbm 类可以与任何 sklearn 分类器或回归器互换使用,包括交叉验证预测/评分和超参数调整。

编辑:王菁

bd27c403d7ad24b21810b17fe0bb4183.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值