本文原为LightGBM的技巧指南,但终稿时已成机器学习重点回顾……这说明从一而终的确是件难事。

特性

Ichigo,你能不能用浅显易懂的话让我理解LightGBM相较于XGBoost的改进点?
嗯~,想象一下,LightGBM和XGBoost都是种树的高手,但是他们种树的方法和策略有些不同。如果把这个过程简化成游戏的话:

  1. 种树的速度:LightGBM就像是开启了加速模式的种树高手,它比XGBoost种树更快。这是因为LightGBM在选择分裂点时用了一种聪明的技巧,只在数据的一部分上寻找最佳点,而不是每个可能的点都检查。这就像是玩寻宝游戏时,有提示哪些地方可能藏有宝藏,而不是每寸土地都挖一遍。
  2. 处理大数据:当数据很大时,XGBoost可能会觉得有点吃力,需要很长时间来处理。而LightGBM就像是拥有更高级别背包的探险者,能更有效地携带和处理大量数据。
  3. 树的生长方式:XGBoost的树好比是一层层地慢慢长大,每次都是先长高再长宽,我们叫做“水平分裂”。而LightGBM的树则是边长高边长宽,它采用的是“垂直分裂”的方式,这意味着它会优先增加树的深度,然后再平衡,这样做可以更快地处理数据。

当然,每种方法都有它的优势和场景。就像不同的游戏策略,关键在于找到最适合当前挑战的那一个。希望这个解释让你对LightGBM和XGBoost有了更直观的理解!

根据https://zhuanlan.zhihu.com/p/99069186,LightGBM相较于之前大多数的机器学习工具,具有以下突出的工程优化点:

  • 直接支持类别特征:LightGBM是第一个直接支持类别特征的GBDT工具,可以直接输入类别特征,解决了one-hot编码难以处理的“维度灾难”。
  • 支持高效并行:LightGBM基于分散规约(reduce scatter),将直方图合并的任务分摊到不同的机器,降低了通信量;而基于投票(voting based)的并行则进一步优化了通信代价。
  • Cache命中率优化:所有的特征都采用相同的方式获得梯度,只需要对梯度进行排序即可实现连续访问,消除了Cache Miss问题。
嗯……经过研究,“直接支持类别特征”似乎需要确保对应类别特征已经是category类型而非object类型,这样可以使得LightGBM更好地理解哪些列是类别特征,从而应用适当的优化算法。

Python
import pandas as pd
df['category'] = df['category'].astype('category')

不过,似乎最保险的方法是在此基础上同时设置categorical_feature参数。不过这个参数在LightGBM的Scikit-learn风格接口中很难用,就不作进一步说明了。注意类别特征均为category类型就好。

而根据LightGBM的原理,其可能具有的缺点为:

  • 可能会长出比较深的决策树,产生过拟合。因此LightGBM在Leaf-wise之上增加了一个最大深度限制,在保证高效率的同时防止过拟合;
  • Boosting族是迭代算法,每一次迭代都根据上一次迭代的预测结果对样本进行权重调整,所以随着迭代不断进行,误差会越来越小,模型的偏差(bias)会不断降低。由于LightGBM是基于偏差的算法,所以会对噪点较为敏感;
  • 在寻找最优解时,依据的是最优切分变量,没有将最优解是全部特征的综合这一理念考虑进去。


用例

在Python中,利用LightGBM原生接口和Scikit-learn接口进行LightGBM模型的训练和预测,有什么差异呢?
嗯~,用简单的话来说,LightGBM的原生接口和Scikit-learn接口就像是两种不同的控制器来玩同一个游戏。虽然游戏是一样的,但是操作起来会有点不同。

  1. 设置方式:使用LightGBM的原生接口时,你像是直接和游戏的内部代码交流,需要更精确地控制参数和数据的格式。而通过Scikit-learn接口,就像是有了一个更友好的用户界面,能让你以更熟悉的方式设置参数,尤其是如果你已经习惯了Scikit-learn的环境。
  2. 功能范围:原生接口提供了一些高级功能和更细致的控制,这就像是游戏的高级设置,可以让你调整更多的选项来优化性能。Scikit-learn接口简化了这个过程,使得操作更加直观易懂,但可能无法使用LightGBM的所有高级功能。
  3. 数据格式:使用LightGBM的原生接口,你需要手动处理数据格式,比如将数据转换成LightGBM特有的Dataset格式。而如果通过Scikit-learn接口,它会自动帮你处理这些,让你可以更容易地使用Pandas DataFrame等更熟悉的数据格式。
  4. 交叉验证和网格搜索:如果你喜欢使用Scikit-learn的交叉验证和网格搜索来调整参数,Scikit-learn接口会让这个过程更加简单。原生接口也能做到这一点,但需要你写更多的代码来实现。

总之,选择哪个接口取决于你对功能的需求以及你对这两个框架的熟悉程度。如果需要更专业、更细致的控制,原生接口可能是更好的选择。如果你追求易用性和快速上手,Scikit-learn接口可能会更适合你。

以下用LightGBM的Scikit-learn风格接口演示其用法。

LGBMClassifier()

数据:经典的鸢尾花数据集。

Python
import lightgbm as lgb
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

# 初始化
iris = load_iris()
train_X, test_X, train_y, test_y = train_test_split(iris.data, iris.target, test_size=0.2)

# 训练
model = lgb.LGBMClassifier(num_leaves=31, learning_rate=0.05, n_estimators=20)
model.fit(train_X, train_y, eval_set=[(test_X, test_y)], early_stopping_rounds=5)

# 预测与评估
pred_y = model.predict(test_X, num_iteration=model.best_iteration_)
print('Accuracy Score:', accuracy_score(test_y, pred_y))
lgb.plot_importance(model)
plt.show()

LGBMRegressor()

数据:https://www.kaggle.com/c/house-prices-advanced-regression-techniques/data

Python
import pandas as pd
from sklearn.model_selection import train_test_split
import lightgbm as lgb
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.impute import SimpleImputer
import numpy as np
import matplotlib.pyplot as plt

# 初始化
df_sample = pd.read_csv('example_data/train.csv')
X = df_sample.drop(['SalePrice'], axis=1).select_dtypes(exclude=['object'])
y = df_sample['SalePrice']
train_X, test_X, train_y, test_y = train_test_split(X.values, y.values, test_size=0.25)

# 一个新知识点:空值处理
imp = SimpleImputer(missing_values=np.nan, strategy='mean')  # 一般填充的strategy都是取均值
train_X = imp.fit_transform(train_X)  # 相当于fit()和transform()连用,先利用train_X对imp进行fit,再处理train_X的空值
test_X = imp.transform(test_X)  # 这里直接transform,即使用train_X的值指导test_X的空值填充

# 训练
model = lgb.LGBMRegressor(objective='regression', num_leaves=31, learning_rate=0.05, n_estimators=20, verbosity=2)
model.fit(train_X, train_y, verbose=False)

# 预测与评估
pred_y = model.predict(test_X)
print('Mean Absolute Error:', mean_absolute_error(pred_y, test_y))  # 平均绝对误差
print('Root Mean Squared Error:', np.sqrt(mean_squared_error(pred_y, test_y)))  # 均方根误差
lgb.plot_importance(model)
plt.show()


调参

先导

需要重点关注的性能参数:

  • max_depth: 限制树的最大深度。较深的树可以增加模型的复杂度,但也容易过拟合。默认值为-1(表示不做限制)。
  • num_leaves: 决定树模型复杂度的主要参数。数值越大,模型越复杂,训练越容易过拟合,但也可以提高模型的准确性。默认值为31(更改了max_depth时,该值应小于2^max_depth)。
  • learning_rate: 学习率。较小的值意味着需要更多的树来进行训练,通常与更好地模型性能相关,但也会增加训练时间。默认值为0.1。
  • n_estimators: 树的个数。增加树的数量可以提高模型的准确性,但同时也会增加模型的复杂度和训练时间。默认值为100。
  • min_child_samples: 叶子节点最少的样本数,可以帮助防止过拟合,默认值为20。
  • subsmplecolsample_bytree: 控制行(样本)采样和列(特征)采样比例,有助于提高模型的泛化能力和训练速度。默认值均为1。
subsamplecolsample_bytree都是用来防止过拟合的重要参数,在调整它们时,可以帮助模型更好地泛化到未知数据上。

subsample参数控制了用于训练模型的样本的比例。这个参数的值在0和1之间,假设你有一个很大的数据集,通过设置subsample小于1的值,LightGBM在每一轮迭代时会随机选择一部分数据(比如70%的数据,如果subsample设置为0.7)来进行训练,而不是使用全部的数据。这样做的好处是可以减少过拟合的风险,同时也可以减少每一轮迭代的计算量,加快训练速度。不过,设置得太小可能会导致模型学习不足,因此需要通过交叉验证来找到一个平衡点。

colsample_bytree参数控制了每棵树随机选择的特征的比例。同样地,这个参数的值也是在0和1之间。例如,如果你将colsample_bytree设置为0.8,那么LightGBM将会在构建每棵树时,从所有的特征中随机选择80%的特征来进行训练。通过减少每棵树可以使用的特征数量,colsample_bytree不仅帮助模型避免过拟合,还可以让模型在不同的特征子集上学习,增加模型的多样性。

在使用这些参数时,一般建议先设置一个初始值(如subsample=1colsample_bytree=1,即不进行任何抽样),然后通过交叉验证逐渐调整,观察模型的性能变化。通常,开始时可以逐渐降低这两个参数的值,观察模型表现如何变化。如果发现降低参数值有助于提高模型的泛化能力,可以继续调整,直到找到最佳的参数组合。

需要注意的是,subsamplecolsample_bytree参数的调整需要与学习率(learning_rate)和树的数量(n_estimators)相结合使用。较小的subsamplecolsample_bytree值可能需要配合较低的学习率和较多的树的数量来达到最佳效果,这是因为使用了更加保守的训练方式,需要更多的迭代来学习数据的规律。

希望这能帮你更好地理解subsamplecolsample_bytree参数的作用和调整方法!

常用思路:网格搜索

即使用sklearn.model_selection.GridSearchCV()来辅助调参。

还是延续上面的LGBMRegressor()的用例,假设现在需要优化max_depth, learning_rate, subsamplecolsample_bytree这几个参数,则代码修改为:

Python
import pandas as pd
from sklearn.model_selection import train_test_split, GridSearchCV
import lightgbm as lgb
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.impute import SimpleImputer
import numpy as np
import matplotlib.pyplot as plt


def grid_optimize(train_X, train_y):
    parameters = {
        'max_depth': [15, 20, 25, 30, 35],
        'learning_rate': [0.01, 0.02, 0.05, 0.1, 0.15],
        'subsample': [0.6, 0.7, 0.8, 0.9, 1],
        'colsample_bytree': [0.6, 0.7, 0.8, 0.9, 1]
    }
    proto_model = lgb.LGBMRegressor(objective='regression')
    gs = GridSearchCV(  # 顺便解释一下GridSearchCV()中的参数
        estimator=proto_model,  # 模型
        param_grid=parameters,  # 待优化参数
        scoring='neg_mean_squared_error',  # 评价标准,默认为None(即使用模型的误差估计方法)
        n_jobs=-1,  # 并行数,默认为1,-1表示使用所有处理器
        cv=3  # 交叉验证参数,默认为5(即五折交叉验证)
    )
    gs.fit(train_X, train_y)
    best_parameters = gs.best_estimator_.get_params()
    for param_name in sorted(parameters.keys()):
        print(f'\n{param_name}: {best_parameters[param_name]}')


def lgb_example(mode='test'):
    # 初始化
    df_sample = pd.read_csv('example_data/train.csv')
    X = df_sample.drop(['SalePrice'], axis=1).select_dtypes(exclude=['object'])
    y = df_sample['SalePrice']
    train_X, test_X, train_y, test_y = train_test_split(X.values, y.values, test_size=0.25)

    # 一个知识点:空值处理
    imp = SimpleImputer(missing_values=np.nan, strategy='mean')  # 一般填充的strategy都是取均值
    train_X = imp.fit_transform(train_X)  # 相当于fit()和transform()连用,先利用train_X对imp进行fit,再处理train_X的空值
    test_X = imp.transform(test_X)  # 这里直接transform,即使用train_X的值指导test_X的空值填充

    if mode == 'test':
        # 参数网格搜索
        grid_optimize(train_X, train_y)
    else:
        # 训练
        model = lgb.LGBMRegressor(  # 这里已经参考了网格搜索的优化结果
            objective='regression',
            num_leaves=31,
            n_estimators=20,
            verbosity=2,
            colsample_bytree=0.7,
            learning_rate=0.05,
            max_depth=25,
            subsample=0.6
        )
        model.fit(train_X, train_y, verbose=False)

        # 预测与评估
        pred_y = model.predict(test_X)
        print('Mean Absolute Error:', mean_absolute_error(pred_y, test_y))  # 平均绝对误差
        print('Root Mean Squared Error:', np.sqrt(mean_squared_error(pred_y, test_y)))  # 均方根误差
        lgb.plot_importance(model)
        plt.show()


if __name__ == '__main__':
    # lgb_example('test')
    lgb_example('operate')

关于GridSearchCV()中的参数,Ichigo还有话想说:

嗯~scoring参数在GridSearchCV中非常关键,它决定了模型的评估方法。根据你的模型类型(分类还是回归),你可以选择不同的评分标准。如果你不指定scoring参数,它会使用模型默认的评分标准,但是根据你的具体需求调整scoring参数通常会得到更适合问题的模型。

对于分类模型,常见的scoring参数取值有:

  • 'accuracy':准确率,适用于大多数分类问题。
  • 'f1'、'f1_weighted'、'f1_macro'、'f1_micro':用于不平衡类别问题,F1分数考虑了精确度和召回率。
  • 'roc_auc':接收者操作特征曲线下的面积,适用于二分类问题。
  • 'precision'、'recall':在关心正类的精确识别时非常有用。

对于回归模型,常见的scoring参数取值包括:

  • 'neg_mean_squared_error':负均方误差,这是回归问题中常用的损失度量,值越小表示模型越好。
  • 'r2':R^2值,表示模型的解释能力,最高为1。
  • 'neg_mean_absolute_error':负平均绝对误差,同样,这个值越小表示模型越好。

选择哪个取决于你的具体需求和问题。有时候,默认的评分标准已经足够好,但如果你对模型的特定表现有要求,比如更关心精确率而不是召回率,或者你的数据极度不平衡,那么选择一个更适合你需求的评分标准会更好。

嗯~cv参数代表交叉验证的策略,主要用于评估模型的泛化能力。具体采用几折交叉验证(比如3折、5折或10折)确实有一定的讲究,但也没有一成不变的规则,这主要取决于你的数据集大小和模型训练的计算负担。如果你的数据集很大,使用较少的折数(比如3折)就可能足够了,因为每一折都提供了足够的数据来有效估计模型性能。但如果数据集较小,使用更多的折数(比如10折或留一法)可以让评估更加准确和稳定,尽管这会增加计算成本。

然而,在一些特殊情况下,比如数据极度不平衡,可能需要采用分层交叉验证(StratifiedKFold)来确保每一折中各类别的比例大致相同。

StratifiedKFold实例:https://blog.csdn.net/weixin_44110891/article/details/95240937

其他思路:遗传算法

对于调参这一规划问题,遗传算法显然也能发挥其快速求解的特性,尤其是在待优化参数矩阵很大的时候不过真遇到非常棘手的调参问题,你可能需要转向商业求解器Gurobi等

由于这一方法并没有和scikit-learn良好集成,此处便不再赘述,具体原理和编码方法见本站过往文章。