Python-和-LightGBM-机器学习-一-

125 阅读1小时+

Python 和 LightGBM 机器学习(一)

原文:annas-archive.org/md5/fec3be57ef79d1371f9bec4f04b9ee9c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读《使用 LightGBM 和 Python 进行机器学习:开发生产就绪机器学习系统的实践指南》。在这本书中,你将踏上一段丰富的旅程,从机器学习的基础原理到高级的 MLOps 领域。我们探索的基础是 LightGBM,这是一个强大且灵活的梯度提升框架,可以用于各种机器学习挑战。

本书专为任何热衷于利用机器学习的力量将原始数据转化为可操作见解的人量身定制。无论你是渴望动手实践的机器学习新手,还是寻求掌握 LightGBM 复杂性的经验丰富的数据科学家,这里都有适合你的内容。

数字时代为我们提供了丰富的数据宝藏。然而,挑战往往在于从这些数据中提取有意义的见解,并在生产环境中部署可扩展、高效和可靠的模型。本书将引导你克服这些挑战。通过深入研究梯度提升、数据科学生命周期以及生产部署的细微差别,你将获得一套全面的技能,以应对机器学习领域的不断变化。

每一章都是基于实用性设计的。穿插着理论见解的现实案例研究确保你的学习建立在实际应用的基础上。我们专注于 LightGBM,它有时会被更主流的算法所掩盖,提供了一个独特的视角来欣赏和应用梯度提升在各种场景中的应用。

对于那些好奇这本书与众不同的地方,那就是我们的实用方法。我们自豪地超越了对算法或工具的简单解释。相反,我们将优先考虑实际应用、案例研究和现实世界的挑战,确保你不仅是在阅读,而且也在“实践”机器学习。

随着我们穿越章节,请记住,机器学习的世界是广阔且不断演变的。虽然本书内容全面,但它是你在机器学习领域终身学习和探索旅程中的一个基石。在你导航 LightGBM、数据科学、MLOps 等领域时,保持你的思维开放,好奇心旺盛,并准备好动手编码。

本书面向的对象

《使用 LightGBM 和 Python 进行机器学习:开发生产就绪机器学习系统的实践指南》专为那些热衷于通过机器学习利用数据力量的广泛读者群体量身定制。本书的目标受众包括以下人群:

  • 机器学习初学者:刚刚踏入机器学习世界的人会发现这本书非常有帮助。它从基础机器学习原理开始,并使用 LightGBM 介绍梯度提升,对于新入门者来说是一个极好的起点。

  • 经验丰富的数据科学家和机器学习从业者:对于那些已经熟悉机器学习领域但希望深化对 LightGBM 和/或 MLOps 的了解的人,本书提供了高级见解、技术和实际应用。

  • 希望学习更多数据科学的软件工程师和架构师:对从数据科学转型或将其集成到他们的应用程序中的软件专业人士来说,本书将非常有价值。本书从理论和实践两方面探讨机器学习,强调动手编码和现实世界应用。

  • MLOps 工程师和 DevOps 专业人士:在 MLOps 领域工作或希望了解生产环境中机器学习模型部署、扩展和监控的个人将受益于本书中关于 MLOps、管道和部署策略的章节。

  • 学者和学生:教授机器学习、数据科学或相关课程的教师以及追求这些领域的学生将发现本书既是一本信息丰富的教科书,也是一本实用的指南。

熟悉 Python 编程是必要的。熟悉 Jupyter 笔记本和 Python 环境是加分项。不需要具备机器学习的前置知识。

事实上,任何对数据有热情、有 Python 编程背景、并渴望使用 LightGBM 探索机器学习多面世界的读者都将发现本书是他们的宝贵资源。

本书涵盖内容

第一章介绍机器学习,通过软件工程的视角开启我们对机器学习的探索之旅。我们将阐述该领域核心概念,如模型、数据集和各种学习范式,并通过使用决策树的实际示例确保概念的清晰性。

第二章集成学习 – Bagging 和 Boosting,深入探讨集成学习,重点关注应用于决策树的 bagging 和 boosting 技术。我们将探讨随机森林、梯度提升决策树等算法,以及更高级的概念,如Dropout meets Additive Regression Trees(DART)。

第三章Python 中 LightGBM 概述,探讨了 LightGBM,这是一个基于树的学习的高级梯度提升框架。突出其独特的创新和增强集成学习的改进,我们将引导您了解其 Python API。使用 LightGBM 的综合建模示例,结合高级验证和优化技术,为深入数据科学和生产系统机器学习奠定基础。

第四章, 比较 LightGBM、XGBoost 和深度学习,将 LightGBM 与两种主要的表格数据建模方法——XGBoost 和深度神经网络DNNs),特别是 TabTransformer 进行比较。我们将通过评估两个数据集来评估每种方法的复杂性、性能和计算成本。本章的精髓是确定 LightGBM 在更广泛的机器学习领域的竞争力,而不是对 XGBoost 或 DNNs 进行深入研究。

第五章, 使用 Optuna 进行 LightGBM 参数优化,专注于关键任务的超参数优化,介绍了 Optuna 框架作为强大的解决方案。本章涵盖了各种优化算法和策略,以修剪超参数空间,并通过一个实际示例指导你如何使用 Optuna 来细化 LightGBM 参数。

第六章, 使用 LightGBM 解决现实世界的数据科学问题,系统地分解了数据科学过程,并将其应用于两个不同的案例研究——一个回归问题和分类问题。本章阐明了数据科学生命周期的每个步骤。你将亲身体验使用 LightGBM 进行建模,并结合全面的理论。本章还作为使用 LightGBM 进行数据科学项目的蓝图。

第七章, 使用 LightGBM 和 FLAML 进行 AutoML,深入探讨了自动化机器学习AutoML),强调了其在简化并加速数据工程和模型开发中的重要性。我们将介绍 FLAML,这是一个值得注意的库,它通过高效的超参数算法自动化模型选择和微调。通过一个实际案例研究,你将见证 FLAML 与 LightGBM 的协同作用以及零样本 AutoML 功能的变革性,这使得调优过程变得过时。

第八章, 使用 LightGBM 进行机器学习管道和 MLOps,从建模的复杂性转向生产机器学习的世界。它介绍了机器学习管道,确保一致的数据处理和模型构建,并探讨了 MLOps,这是 DevOps 和 ML 的结合,对于部署弹性机器学习系统至关重要。

第九章, 使用 AWS SageMaker 进行 LightGBM MLOps,引领我们踏上亚马逊 SageMaker 的旅程,这是亚马逊云服务(Amazon Web Services)提供的一套全面的解决方案,用于构建和维护机器学习(ML)解决方案。我们将通过深入研究如偏差检测、模型的可解释性和自动化、可扩展部署的细微差别等高级领域,来深化我们对 ML 管道的理解。

第十章使用 PostgresML 的 LightGBM 模型,介绍了 PostgresML,这是一个独特的 MLOps 平台和 PostgreSQL 数据库扩展,它通过 SQL 直接促进 ML 模型开发和部署。这种方法虽然与我们所采用的 scikit-learn 编程风格形成对比,但展示了数据库级 ML 的优势,尤其是在数据移动效率和更快推理方面。

第十一章使用 LightGBM 进行分布式和基于 GPU 的学习,深入探讨了训练 LightGBM 模型的广阔领域,利用分布式计算集群和 GPU。通过利用分布式计算,您将了解如何显著加速训练工作负载并管理超出单机内存容量的数据集。

要充分利用本书

本书假定您对 Python 编程有一定的了解。本书中的 Python 代码并不复杂,因此即使只理解 Python 的基础知识,也应该足以让您通过大多数代码示例。

在所有章节的实践示例中使用了 Jupyter 笔记本。Jupyter Notebooks 是一个开源工具,允许您创建包含实时代码、可视化和 Markdown 文本的代码笔记本。有关如何开始使用 Jupyter Notebooks 的教程可在realpython.com/jupyter-notebook-introduction/plotly.com/python/ipython-notebook-tutorial/找到。

本书涵盖的软件/硬件操作系统要求
Python 3.10Windows, macOS, or Linux
Anaconda 3Windows, macOS, or Linux
scikit-learn 1.2.1Windows, macOS, or Linux
LightGBM 3.3.5Windows, macOS, or Linux
XGBoost 1.7.4Windows, macOS, or Linux
Optuna 3.1.1Windows, macOS, or Linux
FLAML 1.2.3Windows, macOS, or Linux
FastAPI 0.103.1Windows, macOS, or Linux
Amazon SageMaker
Docker 23.0.1Windows, macOS, or Linux
PostgresML 2.7.0Windows, macOS, or Linux
Dask 2023.7.1Windows, macOS, or Linux

我们建议在设置自己的环境时使用 Anaconda 进行 Python 环境管理。Anaconda 还捆绑了许多数据科学包,因此您无需单独安装它们。可以从www.anaconda.com/download下载 Anaconda。值得注意的是,本书附有 GitHub 仓库,其中包含创建运行本书中代码示例所需环境的 Anaconda 环境文件。

如果您使用的是本书的电子版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将有助于您避免与代码的复制和粘贴相关的任何潜在错误

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了几个文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“代码几乎与我们的分类示例相同 - 我们使用DecisionTreeRegressor作为模型,而不是分类器,并计算mean_absolute_error而不是 F1 分数。”

代码块设置如下:

import numpy as np 
import pandas as pd 
from matplotlib import pyplot as plt 
import seaborn as sns 
from sklearn.linear_model import LinearRegression 
from sklearn.metrics import mean_absolute_error 

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

model = DecisionTreeRegressor(random_state=157, max_depth=3, min_samples_split=2)
model = model.fit(X_train, y_train)
mean_absolute_error(y_test, model.predict(X_test))

任何命令行输入或输出都应如下编写:

conda create -n your_env_name python=3.9

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“因此,数据准备和清理是机器学习过程中的关键部分。”

小贴士或重要注意事项

出现在这些块中。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送给我们,邮箱地址为 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您向我们报告。请访问www.packtpub.com/support/err…并填写表格。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将非常感激您提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

读完《使用 LightGBM 和 Python 进行实用机器学习》后,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

你喜欢在路上阅读,但无法携带你的印刷书籍到处走吗?你的电子书购买是否与你的选择设备不兼容?

别担心,现在每本 Packt 书籍都附赠一本无 DRM 的 PDF 版本,无需额外费用。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不会就此结束,您还可以获得独家折扣、时事通讯和每天收件箱中的精彩免费内容。

按照以下简单步骤获取福利:

  1. 扫描下面的二维码或访问以下链接!二维码图片

packt.link/free-ebook/…

  1. 提交您的购买证明

  2. 就这些!我们将直接将您的免费 PDF 和其他福利发送到您的电子邮件

第一部分:梯度提升和 LightGBM 基础

在这部分,我们将通过向您介绍机器学习的基本概念来开始我们的探索,这些概念从基本术语到复杂的算法如随机森林。我们将深入探讨集成学习,强调决策树结合时的强大功能,然后转向梯度提升框架,LightGBM。通过 Python 中的实际示例和与 XGBoost 和深度神经网络等技术的比较分析,您将在机器学习领域,特别是 LightGBM 方面获得基础理解和实践能力。

本部分将包括以下章节:

  • 第一章*,介绍机器学习*

  • 第二章*,集成学习 – Bagging 和 Boosting*

  • 第三章*,Python 中 LightGBM 概述*

  • 第四章*,比较 LightGBM、XGBoost 和深度学习*

第一章:介绍机器学习

我们的旅程从机器学习的介绍和本书中我们将使用的根本概念开始。

我们将从软件工程的角度提供一个机器学习的概述。然后,我们将介绍机器学习和数据科学领域使用的核心概念:模型、数据集、学习范式以及其他细节。这个介绍将包括一个实际例子,清楚地说明了讨论的机器学习术语。

我们还将介绍决策树,这是一个至关重要的机器学习算法,是我们理解 LightGBM 的第一步。

完成本章后,您将在机器学习和机器学习技术的实际应用方面打下坚实的基础。

本章将涵盖以下主要主题:

  • 什么是机器学习?

  • 介绍模型、数据集和监督学习

  • 决策树学习

技术要求

本章包括简单的机器学习算法示例,并介绍了使用 scikit-learn。您必须安装一个带有 scikit-learn、NumPy、pandas 和 Jupyter Notebook 的 Python 环境。本章的代码可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-1找到。

什么是机器学习?

机器学习是更广泛的人工智能领域的一部分,涉及允许计算机“学习”特定任务而无需明确编程的方法和技术。

机器学习只是另一种从数据中自动编写程序的方式。抽象地说,一个程序是一系列将输入转换为特定输出指令。程序员的任务是理解计算机程序的所有相关输入,并开发一套指令以产生正确的输出。

然而,如果输入超出了程序员的认知范围怎么办呢?

例如,让我们考虑创建一个程序来预测大型零售店的总体销售额。程序的输入将是可能影响销售的各种因素。我们可以想象的因素包括历史销售数据、即将到来的公共假日、库存可用性、商店可能进行的任何特别优惠,甚至包括天气预报或与其他商店的邻近程度等因素。

在我们的商店例子中,传统的方法是将输入分解成可管理的、可理解的(由程序员理解)部分,也许可以咨询一位商店销售预测方面的专家,然后制定手工定制的规则和指令来尝试预测未来的销售。

虽然这种方法当然可行,但它也很脆弱(从程序可能需要经历关于输入因素的广泛变化的角度来看)并且完全基于程序员(或领域专家)对问题的理解。面对可能成千上万的因素和数十亿个示例,这个问题变得难以承受。

机器学习为我们提供了这种方法的替代方案。不是创建规则和指令,我们反复向计算机展示我们需要完成的任务的示例,然后让它自己找出如何自动解决这些问题。

然而,我们之前有一组指令,现在我们有一个训练好的模型而不是编程的模型。

这里的一个关键认识,尤其是如果你来自软件背景,是我们的机器学习程序仍然像一个常规程序一样运行:它接受输入,有处理它的方法,并产生输出。像所有其他软件程序一样,机器学习软件必须经过正确性测试,集成到其他系统中,部署、监控和优化。所有这些共同构成了机器学习工程这一领域。我们将在后面的章节中涵盖所有这些方面以及更多内容。

机器学习范式

广义而言,机器学习有三个主要范式:监督学习、无监督学习和强化学习。

监督学习中,模型在标记数据上训练:数据集中的每个实例都有其关联的正确输出,或标签,对于输入示例。模型预计会学习预测未见输入示例的标签。

无监督学习中,数据集中的示例是无标签的;在这种情况下,模型预计会在数据中发现模式和关系。无监督方法的例子包括聚类算法、异常检测和降维算法。

最后,强化学习涉及一个模型,通常称为代理,与特定环境交互,并通过接收特定行为的惩罚或奖励来学习。目标是让代理执行最大化其奖励的行为。强化学习在机器人学、控制系统或训练计算机玩游戏方面得到了广泛应用。

LightGBM 和本书后面讨论的大多数其他算法是监督学习技术的例子,也是本书的重点。

以下章节将深入探讨本书中我们将使用的机器学习术语以及机器学习过程的细节。

介绍模型、数据集和监督学习

在上一节中,我们介绍了一个模型作为替代一组指令的构建,这组指令通常构成一个程序以执行特定任务。本节更详细地介绍了模型和其他核心机器学习概念。

模型

更正式地说,模型是对执行特定任务的特定过程的数学或算法表示。机器学习模型通过使用训练算法数据集上训练来学习特定任务。

注意

训练的另一个术语是拟合。从历史上看,拟合起源于统计学领域。当模型被训练时,我们说模型“拟合数据”。在这本书中,我们将这两个术语交替使用。

存在许多不同类型的模型,它们都使用不同的数学、统计或算法技术来模拟训练数据。机器学习算法的例子包括线性回归、逻辑回归、决策树、支持向量机和神经网络。

在模型类型和该模型的训练实例之间做出了区分:大多数机器学习模型都可以训练以执行各种任务。例如,决策树(一种模型类型)可以训练来预测销售、识别心脏病和预测足球比赛结果。然而,每个这些任务都需要一个不同的实例的决策树,该决策树是在不同的数据集上训练的。

一个特定模型做什么取决于模型的参数。参数有时也被称为权重,在技术上,它们是模型参数的特定类型。

训练算法是用于找到特定任务最合适的模型参数的算法。

我们使用目标函数来确定拟合质量,即模型的表现如何。这是一个数学函数,它衡量给定输入的预测输出和实际输出之间的差异。目标函数量化了模型的表现。根据我们正在解决的问题,我们可能寻求最小化或最大化目标函数。目标通常在训练过程中作为我们试图最小化的错误来衡量。

我们可以将模型训练过程总结如下:训练算法使用数据集的数据来优化模型参数以完成特定任务,这是通过目标函数来衡量的。

超参数

当一个模型由参数组成时,训练算法有其自己的参数,称为超参数。超参数是一个可控的值,它会影响训练过程或算法。例如,考虑找到一个抛物线函数的最小值:我们可以先猜测一个值,然后朝着最小化函数输出的方向迈出小步。步长必须选择得当:如果我们的步子太小,找到最小值将需要过长的时间。如果步长太大,我们可能会超过最小值并错过它,然后继续在最小值周围振荡(来回跳跃):

图 1.1 – 使用过大的步长(左)和过小的步长(右)的影响

图 1.1 – 使用过大的步长(左侧)和过小的步长(右侧)的影响

在这个例子中,步长将是我们的最小化算法的超参数。步长的影响在图 1**.1中得到了说明。

数据集

如前所述,机器学习模型是使用数据集进行训练的。数据是机器学习过程的核心,数据准备通常是占用最多时间的流程部分。

在本书的整个过程中,我们将与 表格型 数据集一起工作。表格型数据集在现实世界中非常常见,由行和列组成。行通常被称为样本、示例或观察,而列通常被称为特征、变量或属性。

重要的是,列中的数据类型没有限制。特征可以是字符串、数字、布尔值、地理空间坐标,或编码格式,如音频、图像或视频。

数据集也 rarely 完美定义。数据可能不完整、有噪声、不正确、不一致,并包含各种格式。

因此,数据准备和清洗是机器学习过程中的关键部分。

数据准备涉及处理数据使其适合机器学习,通常包括以下步骤:

  1. 收集和验证:一些数据集最初可能太小或表示问题不佳(数据不是从其抽取样本的实际数据总体有代表性)。在这些情况下,从业者必须收集更多数据,并进行验证以确保数据代表问题。

  2. 检查系统错误和偏差:检查并纠正收集和验证过程中可能导致的任何系统错误,这些错误可能导致数据集偏差至关重要。在我们的销售示例中,系统收集错误可能仅从城市商店收集数据,而排除农村商店。仅使用城市商店数据训练的模型在预测商店销售时将存在偏差,并且当模型用于预测农村商店的销售时,我们可能会期望性能不佳。

  3. 数据清洗:任何格式或值范围的不一致性都必须得到解决。任何缺失值也需要以不引入偏差的方式进行处理。

  4. 特征工程:某些特征可能需要转换以确保机器学习模型能够从中学习,例如将一个单词句子进行数值编码。此外,可能需要从现有特征中准备新的特征,以帮助模型检测模式。

  5. 归一化和标准化:特征的相关范围必须进行归一化和标准化。归一化和标准化确保没有任何一个特征对整体预测有不成比例的影响。

  6. 平衡数据集:在数据集不平衡的情况下——也就是说,它包含一个类或预测的示例比另一个多得多——数据集需要被平衡。平衡通常是通过过度采样少数示例来实现的,以平衡数据集。

第六章**,使用 LightGBM 解决现实世界数据科学问题中,我们将通过整个数据准备过程来展示前面的步骤是如何在实际中应用的。

注意

一个值得记住的谚语是“垃圾进,垃圾出”。模型从它所给出的任何数据中学习,包括数据中包含的任何缺陷或偏差。当我们用垃圾数据训练模型时,结果就是一个垃圾模型。

关于数据集,还有一个需要理解的概念是训练、验证和测试数据集。我们在数据准备步骤完成后将数据集分为这三个子集:

  • 训练集是最重要的子集,通常由 60%到 80%的数据组成。这些数据用于训练模型。

  • 验证集与训练数据分开,并在整个训练过程中用于评估模型。拥有独立的验证数据确保模型是在它之前未见过的数据上评估的,也称为其泛化能力。超参数调整,在第五章**,Optuna 进行 LightGBM 参数优化中详细介绍的流程,也使用验证集。

  • 最后,测试集是一个可选的保留集,类似于验证集。它用于过程的最后,以评估模型在训练或调整过程中未参与的数据上的性能。

验证集的另一个用途是监控模型是否过度拟合数据。让我们更详细地讨论一下过度拟合。

过度拟合与泛化

要理解过度拟合,我们首先必须定义我们所说的模型泛化是什么意思。如前所述,泛化是模型准确预测它之前未见过的数据的能力。与训练准确率相比,泛化准确率作为模型性能估计更为重要,因为这表明我们的模型在生产中的表现。泛化有两种形式,插值外推

  • 插值指的是模型预测两个已知数据点之间值的能力——换句话说,就是在训练数据范围内进行泛化。例如,假设我们用 1 月到 7 月的月度数据来训练我们的模型。在插值时,我们会要求模型对 4 月某一天进行预测,这是一个在我们训练范围内的日期。

  • 外推,正如你可能推断的那样,是模型预测训练数据定义范围之外值的能力。外推的一个典型例子是预测——即预测未来。在我们的上一个例子中,如果我们要求模型在十二月进行预测,我们期望它从训练数据中外推。

在两种泛化类型中,外推更具挑战性,可能需要特定类型的模型来实现。然而,在两种情况下,模型都可能过度拟合数据,失去准确插值或外推的能力。

过拟合是一种现象,其中模型对训练数据拟合得太紧密,失去了泛化到未见数据的能力。模型不是学习数据中的潜在模式,而是记住了训练数据。更技术地说,模型拟合了训练数据中包含的噪声。这个术语“噪声”来源于数据包含信号噪声的概念。信号指的是我们试图预测的数据中捕获的潜在模式或信息。相比之下,噪声指的是数据点的随机或不相关的变化,这些变化掩盖了信号。

例如,考虑一个数据集,我们试图预测特定位置的降雨量。数据中的信号将是降雨的一般趋势:冬季或夏季降雨增加,或相反的其他位置。噪声将是我们在数据集中每个月和每个位置的降雨量测量的微小变化。

下面的图表说明了过拟合的现象:

图 1.2 – 展示过拟合的图表。模型过度拟合并完美预测了训练数据,但失去了泛化到实际信号的能力

图 1.2 – 展示过拟合的图表。模型过度拟合并完美预测了训练数据,但失去了泛化到实际信号的能力

前面的图表显示了信号和噪声之间的差异:每个数据点都是从实际信号中采样的。数据遵循信号的总体模式,有轻微的、随机的变化。我们可以看到模型是如何过度拟合数据的:模型完美地拟合了训练数据,但以泛化为代价。我们还可以看到,如果我们使用模型通过预测 4 的值来进行插值,我们得到的结果比实际信号(6.72 比 6.2)高得多。此外,还显示了模型外推失败的情况:对 12 的预测远低于信号的预测(7.98 比 8.6)。

在现实中,所有现实世界的数据集都包含噪声。作为数据科学家,我们的目标是准备数据,尽可能多地去除噪声,使信号更容易检测。数据清洗、归一化、特征选择、特征工程和正则化是去除数据中噪声的技术。

由于所有真实世界的数据都包含噪声,过拟合是无法完全消除的。以下条件可能导致过拟合:

  • 过于复杂的模型:对于我们所拥有的数据量来说过于复杂的模型,会利用额外的复杂性来记住数据中的噪声,从而导致过拟合。

  • 数据不足:如果我们没有足够的训练数据用于模型,这类似于一个过于复杂的模型,它会过度拟合数据。

  • 特征过多:具有过多特征的集合很可能包含无关的(噪声)特征,这会降低模型的泛化能力。

  • 过度训练:对模型进行过长时间的训练,使其能够记住数据集中的噪声。

由于验证集是模型尚未见过的训练数据的一部分,我们使用验证集来监控过拟合。我们可以通过观察训练和泛化误差随时间的变化来识别过拟合的点。在过拟合的点,验证误差增加。相比之下,训练误差持续改善:模型正在拟合训练数据中的噪声,并失去了泛化的能力。

防止过拟合的技术通常旨在解决我们之前讨论的导致过拟合的条件。以下是一些避免过拟合的策略:

  • 提前停止:当我们看到验证误差开始增加时,我们可以停止训练。

  • 简化模型:具有较少参数的简单模型将无法学习训练数据中的噪声,从而更好地泛化。

  • 获取更多数据:收集更多数据或增强数据是防止过拟合的有效方法,因为它给模型提供了更好的机会来学习数据中的信号,而不是在较小数据集中的噪声。

  • 特征选择和降维:由于某些特征可能对要解决的问题不相关,我们可以丢弃我们认为冗余的特征,或者使用主成分分析等技术来降低维度(特征)。

  • 添加正则化:较小的参数值通常会导致更好的泛化,这取决于模型(神经网络就是一个例子)。正则化向目标函数添加一个惩罚项,以阻止参数值过大。通过将参数值驱动到更小(或零)的值,它们对预测的贡献更小,从而有效地简化了模型。

  • 集成方法:结合多个较弱模型的预测可以导致更好的泛化,同时提高性能。

重要的是要注意,过拟合以及防止过拟合的技术是针对我们模型的特定问题。我们的目标始终应该是最小化过拟合,以确保对未见数据的泛化。一些策略,如正则化,可能对某些模型不起作用,而其他策略可能更有效。还有一些针对特定模型的定制策略,我们将在讨论决策树中的过拟合时看到一个例子。

监督学习

店铺销售额的例子是监督学习的一个实例——我们有一个由特征组成的数据库,并且正在训练模型来预测一个目标。

监督学习问题可以分为两大类问题:分类问题回归问题

分类与回归

分类问题中,模型需要预测的标签是分类的或定义了一个类别。一些类别的例子包括垃圾邮件非垃圾邮件、以及糖尿病患者非糖尿病患者。这些都是二元分类的例子:只有两个类别。

多类分类也是可能的;例如,电子邮件可以被分类为重要促销杂乱垃圾邮件;云朵的图片可以被分类为卷云积云层云雨层云

回归问题中,目标是预测一个连续的、数值的值。例子包括预测收入、销售额、温度、房价和人群数量。

机器学习艺术中很大一部分是正确地将问题定义为分类或回归问题(或者可能是无监督或强化学习)。后面的章节将涵盖这两种类型问题的多个端到端案例研究。

模型性能指标

让我们简要讨论一下我们如何衡量我们模型的表现。模型性能指的是机器学习模型根据给定的输入做出准确预测或生成有意义输出的能力。一个评估指标量化了模型对新、未见数据的泛化程度。高模型性能表明模型有效地学习了数据中的潜在模式,并且可以在它未见过的数据上做出准确的预测。当与监督学习问题(无论是分类还是回归问题)一起工作时,我们可以根据已知的目标来衡量模型的表现。

重要的是,我们衡量模型在分类任务和回归任务上的表现方式不同。scikit-learn 有许多内置的指标函数,可以用于分类或回归问题(scikit-learn.org/stable/modu…)。让我们回顾这些中最常见的。

可以用模型做出的正面和负面预测来定义分类指标。以下定义可以用来计算分类指标:

  • 真正正例 (TP): 一个正例被正确地分类为正例

  • 真正负例 (TN): 一个负例被正确地分类为负例

  • 假阳性 (FP): 一个负例被错误地分类为正例

  • 假阴性 (FN): 一个正例被错误地分类为负例

根据这些定义,最常见的 分类 指标如下:

  • 准确率: 准确率是最直接的分类指标。准确率是正确预测的数量除以总预测数量。然而,准确率容易受到数据不平衡的影响。例如,假设我们有一个包含 8 个垃圾邮件示例和 2 个非垃圾邮件示例的电子邮件数据集,并且我们的模型只预测垃圾邮件。在这种情况下,模型的准确率为 80%,尽管它从未正确分类非垃圾邮件。从数学上讲,我们可以如下定义准确率:

    准确率 =  TP + TN ______________  TP + FP + TN + FN

  • 精确率: 精确率是获取对分类性能更深入理解的一种方式。精确率是真正正例预测(正确预测)与所有正例预测(真正正例和假阳性)的比例。换句话说,精确率指标表明模型在预测正例时的精确度。在我们的垃圾邮件示例中,仅预测垃圾邮件的模型精确度不高(因为它将所有非垃圾邮件分类为垃圾邮件),并且具有较低的精确率。以下公式可以用来计算精确率:

    精确率 =  TP _ TP + FP

  • 召回率: 召回率是精确率的对立面。召回率衡量模型有效地找到(或召回)所有真正正例的能力。召回率是真正正例预测与所有正例(真正正例和假阴性)的比例。在我们的垃圾邮件示例中,仅预测垃圾邮件的模型具有完美的召回率(它可以找到所有垃圾邮件)。我们可以这样计算召回率:

    召回率 =  TP _ TP + FN

  • F1 分数: 最后,我们有 F1 分数。F1 分数是精确率和召回率的调和平均数。F1 分数平衡了精确率和召回率,给出了一个总结分类器性能的单个值。以下公式可以用来计算 F1 分数:

    F 1 =  2 × 精确率 × 召回率 _______________  精确率 + 召回率  =  2 × TP _____________  2 × TP + FP + FN

上述分类指标是最常见的,但还有很多。尽管 F1 分数在分类问题中常用(因为它总结了精确率和召回率),但选择最佳指标取决于你解决的问题。通常,可能需要特定的指标,但有时必须根据经验和你对数据的理解来选择。我们将在本书的后面部分查看不同指标的一些示例。

以下是一些常见的 回归 指标:

  • 均方误差MSE):MSE 是预测值和实际值之间平方差异的平均值。MSE 因其一个关键数学特性而常用:MSE 是 可微的,因此适用于与基于梯度的学习方法一起使用。然而,由于差异被平方,MSE 对大误差的惩罚比对小误差更重,这可能或可能不适合要解决的问题。

  • 平均绝对误差MAE):与平方差异不同,MAE 是预测值和实际值之间绝对差异的平均值。通过避免误差的平方,MAE 对误差的大小更稳健,对异常值比均方误差(MSE)更不敏感。然而,MAE 不可微,因此不能与基于梯度的学习方法一起使用。

与分类指标一样,选择最合适的回归指标取决于你试图解决的问题。

指标与目标

我们将训练模型定义为找到最合适的参数以最小化一个 目标函数。需要注意的是,特定问题的目标函数和指标可能不同。一个很好的例子是决策树,在构建树时使用不纯度(熵)作为目标函数。然而,我们仍然计算之前解释的指标来确定树在数据上的性能。

在我们对基本指标有了理解之后,我们可以结束对机器学习概念的介绍。现在,让我们通过一个例子来回顾我们讨论过的术语和概念。

一个建模例子

考虑以下按月销售的以下数据(单位:千):

JanFebMarAprMayJun
4,1404,8507,3406,8908,27010,060
JulAugSeptOctNovDec
8,11011,67010,45011,54013,40014,420

表 1.1 – 按月样本销售数据,单位:千

这个问题很简单:只有一个特征,即月份,目标是销售数量。因此,这是一个监督回归问题的例子。

注意

你可能已经注意到这是一个时间序列问题的例子:时间是主要变量。时间序列也可以使用更高级的时间序列特定算法(如方差分析)进行预测,但在这个部分我们将使用一个简单的算法进行说明。

我们可以将我们的数据绘制成每月销售的图表,以更好地理解它:

图 1.3 – 显示按月商店销售的图表

图 1.3 – 显示按月商店销售的图表

在这里,我们使用直线模型,也称为简单线性回归,来模拟我们的销售数据。直线的定义如下公式:

y = mx + c

在这里,m 是直线的斜率,c 是 Y 轴截距。在机器学习中,直线是模型,而 m 和 c 是模型参数。

为了找到最佳参数,我们必须衡量我们的模型对于特定参数集的数据拟合程度如何 – 也就是说,我们输出的错误。我们将使用 MAE 作为我们的指标:

MAE =  ∑ i=1 n  | ˆ y  − y| _ n

在这里,ˆy 是预测输出,y 是实际输出,n 是预测次数。我们通过为每个输入进行预测,然后根据公式计算 MAE 来计算 MAE。

拟合模型

现在,让我们将我们的线性模型拟合到我们的数据上。我们的拟合线的过程是迭代的,我们从这个过程开始,通过猜测 m 和 c 的值,然后从那里迭代。例如,让我们考虑 m = 0.1,c = 4:

图 1.4 – 显示 m = 0.1 和 c = 4 的线性模型预测的图表

图 1.4 – 显示 m = 0.1 和 c = 4 的线性模型预测的图表

使用这些参数,我们达到了4,610的错误率。

我们的猜测值太低了,但这没关系;我们现在可以更新参数,尝试改进错误率。实际上,更新模型参数是通过使用梯度下降等训练算法算法化完成的。我们将在第二章**,集成学习 – Bagging 和 Boosting中讨论梯度下降。

在这个例子中,我们将使用我们对直线的理解以及直觉来手动更新每个迭代的参数。我们的线太浅,截距太低;因此,我们必须增加这两个值。我们可以通过选择步长来控制我们每次迭代所做的更新。我们必须通过添加步长来更新 m 和 c 值。对于步长为 0.1 的结果,请参阅表 1.2

猜测#mcMAE
10.144.61
20.24.13.89
30.34.23.17
40.34.32.5
50.44.41.83

表 1.2 – 逐步猜测直线的斜率(m)和 y 截距(c)以拟合我们的数据。拟合质量是通过 MAE 来衡量的

在我们的例子中,步长是我们训练过程中的一个超参数

我们最终得到的错误率为1.83,这意味着平均来说,我们的预测错误不超过2,000

现在,让我们看看如何使用 scikit-learn 解决这个问题。

使用 scikit-learn 进行线性回归

我们可以不手动建模,而是使用 scikit-learn 构建线性回归模型。由于这是我们第一个例子,我们将逐行解释代码,说明正在发生什么。

首先,我们必须导入我们将要使用的 Python 工具:

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error

有三组导入:我们首先导入numpypandas。导入 NumPy 和 pandas 是开始所有数据科学笔记本的常用方法。此外,请注意短名称nppd,这是在处理numpypandas时的标准约定。

接下来,我们导入一些标准的绘图库,我们将使用这些库来绘制一些图表:来自matplotlibpyplotseaborn。Matplotlib 是一个广泛使用的绘图库,我们通过 pyplot Python 接口访问它。Seaborn是建立在 Matplotlib 之上的另一个可视化工具,它使得绘制专业外观的图表变得更加容易。

最后,我们到达了 scikit-learn 导入的部分。在 Python 代码中,scikit-learn 库被称为sklearn。从其linear_model包中,我们导入LinearRegression。scikit-learn 实现了许多预定义的度量,在这里,我们将使用mean_absolute_error

现在,我们准备设置我们的数据:

months = np.array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12])
sales = np.array([4.14,  4.85,  7.34,  6.89,  8.27, 10.06,  8.11, 11.67, 10.45, 11.54, 13.4 , 14.42])
df = pd.DataFrame({"month": months, "sales": sales})

在这里,我们定义了一个新的numpy数组,用于月份和相应的销售,为了使它们更容易处理,我们将这两个数组收集到一个新的pandas DataFrame 中。

数据就绪后,我们到达了代码的有趣部分:使用 scikit-learn 进行建模。代码很简单:

model = LinearRegression()
model = model.fit(df[["month"]], df[["sales"]])

首先,我们通过构造LinearRegression的实例来创建我们的模型。然后,我们使用model.fit和从我们的 DataFrame 中传递的月份和销售数据来拟合我们的模型。这两行代码就足以拟合一个模型,正如我们将在后面的章节中看到的,即使是复杂的模型也使用相同的配方来实例化和训练模型。

我们现在可以通过为我们的数据创建预测并将预测和实际目标传递给度量函数来计算我们的MAE

predicted_sales = model.predict(df[["month"]])
mean_absolute_error(predicted_sales, df[["sales"]])

我们得到一个0.74的错误,这比我们的猜测略低。我们还可以检查模型系数和截距(mc,来自之前的内容):

print(f"Gradient: ${model.coef_}")
print(f"Intercept: ${model.intercept_}")

scikit-learn 已经使用系数为0.85和截距为3.68的模型进行了拟合。我们的猜测在正确的范围内,但可能需要一些时间才能得到最优值。

这就结束了我们对 scikit-learn、建模和机器学习基础介绍的介绍。在我们的玩具示例中,我们没有将数据分成单独的数据集,优化模型超参数,也没有应用任何确保模型不过拟合的技术。在下一节中,我们将查看分类和回归示例,我们将应用这些和其他最佳实践。

决策树学习

本节介绍了决策树学习,这是理解 LightGBM 所必需的机器学习算法。我们将通过使用 scikit-learn 构建决策树的示例来进行分析。本节还将提供一些构建决策树的数学定义;理解这些定义不是必需的,但它将帮助我们理解我们对决策树超参数的讨论。

决策树是基于树的学习者,通过连续对数据进行提问以确定结果。沿着树路径向下,使用一个或多个特征对输入做出决策。路径在叶节点终止,它代表预测的类别或值。决策树可用于分类或回归。

下图是 Iris 数据集上拟合的决策树示意图:

图 1.5 – 使用 Iris 数据集建模的决策树

图 1.5 – 使用 Iris 数据集建模的决策树

Iris 数据集是一个分类数据集,其中使用 Iris 花的萼片和花瓣尺寸来预测 Iris 花的类型。每个非叶节点使用一个或多个特征来缩小数据集中的样本:根节点开始于所有 150 个样本,然后根据花瓣宽度进行分割,<= 0.8。我们继续沿着树向下,每个节点进一步分割样本,直到我们达到包含预测类(versicolor、virginica 或 setosa)的叶节点。

与其他模型相比,决策树有许多优点:

  • 特征可以是数值或分类的:可以使用数值特征(通过分割范围)或分类特征来分割样本,而无需我们对其进行编码。

  • 减少数据准备需求:决策分割对数据范围或大小不敏感。许多其他模型(例如,神经网络)需要将数据进行归一化到单位范围内。

  • 可解释性:如前所述,解释树做出的预测是直接的。在需要向决策者解释预测的情况下,可解释性非常有价值。

这些只是使用基于树的模型的一些优点。然而,我们还需要意识到与决策树相关的一些缺点:

  • 过拟合:决策树非常容易过拟合。在拟合决策树时设置正确的超参数是至关重要的。决策树中的过拟合将在后面详细讨论。

  • 较差的外推能力:由于决策树的预测不是连续的,并且实际上由训练数据所限制,因此决策树在外推能力方面较差。

  • 不平衡数据:当在不平衡数据上拟合树模型时,高频类别会主导预测。需要准备数据以消除不平衡。

关于决策树的优缺点有更详细的讨论,请参阅scikit-learn.org/stable/modules/tree.xhtml

熵和信息增益

首先,在查看构建(或拟合)决策树的算法之前,我们需要对熵和信息增益有一个基本的理解。

熵可以被视为衡量系统无序或随机性的方法。熵衡量特定输入或事件的结果可能有多令人惊讶。考虑一副洗好的牌:从牌堆顶部抽取可能会给我们任何一张牌(每次都是令人惊讶的结果);因此,我们可以说洗好的牌堆具有高熵。从有序牌堆的顶部抽取牌不会令人惊讶;我们知道下一张牌是什么。因此,有序牌堆的熵较低。另一种解释熵的方法是数据集的纯度:低熵数据集(整齐有序)比高熵数据集的纯度低。

信息增益,反过来,是修改或观察底层数据时所获得的信息量。信息增益涉及在观察之前减少熵。在我们的牌堆示例中,我们可能将洗好的牌堆分成四个较小的牌堆,按花色(黑桃、红心、方块和梅花)。如果我们从小牌堆中抽取,结果就不会那么令人惊讶:我们知道下一张牌来自同一花色。通过按花色分割牌堆,我们已经减少了小牌堆的熵。在特征(花色)上分割牌堆与决策树中的分割非常相似;每次分割都试图最大化信息增益——也就是说,它们在分割后最小化熵。

在决策树中,有两种常见的测量信息增益或纯度损失的方法:

  • 吉尼指数

  • 对数损失或熵

详细解释可在scikit-learn.org/stable/modules/tree.xhtml#classification-criteria找到。

使用 C4.5 构建决策树

C4.5 是从数据集构建决策树的算法[1]。该算法是递归的,并从以下基本案例开始:

  1. 如果子数据集中的所有样本都属于同一类,则在树中创建一个选择该类的叶节点。

  2. 如果使用任何特征分割无法获得信息(数据集不能再进一步分割),则创建一个叶节点,预测子数据集中包含的最频繁的类别。

  3. 如果子数据集中达到最小样本阈值,则创建一个叶节点,预测子数据集中包含的最频繁的类别。

然后,我们可以应用该算法:

  1. 检查任何三种基本情况,如果任何一种适用于数据集,则停止分割。

  2. 对于数据集的每个特征或属性,计算在该特征上分割数据集所获得的信息量。

  3. 通过在具有最高信息增益的特征上分割数据集来创建决策节点。

  4. 根据决策节点将数据集分割成两个子数据集,并递归地对每个子数据集应用算法。

一旦树构建完成,就会应用剪枝。在剪枝过程中,信息增益相对较低的决策节点会被移除。移除节点可以避免过度拟合训练数据并提高树的泛化能力。

分类和回归树

你可能已经注意到,在前面的解释中,我们只使用了类别来使用决策节点分割数据集;这并非偶然,因为经典的 C4.5 算法仅支持分类树。分类和回归树CART)扩展了 C4.5 以支持数值目标变量——即回归问题 [2]。使用 CART,决策节点也可以分割连续的数值输入变量以支持回归,通常使用阈值(例如,x <= 0.3)。当达到叶节点时,剩余数值范围的均值或中位数通常被用作预测值。

在构建分类树时,仅使用不纯度来确定分割。然而,对于回归树,不纯度会与其他标准结合来计算最佳分割:

  • 均方误差(MSE)或平均绝对误差(MAE)

  • 半泊松偏差

每个细节的数学解释都可以在 scikit-learn.org/stable/modules/tree.xhtml#regression-criteria 找到。

scikit-learn 使用 CART 的优化版本来构建决策树。

决策树中的过度拟合

决策树最显著的缺点之一是它们容易过度拟合。如果没有适当的超参数选择,C4.5 和其他训练算法会创建过于复杂和深的树,几乎完全符合训练数据。管理过度拟合是构建决策树的关键部分。以下是一些避免过度拟合的策略:

  • 剪枝:如前所述,我们可以移除贡献信息增益不多的分支;这减少了树的复杂性并提高了泛化能力。

  • 最大深度:限制树的深度也可以避免过度复杂的树并避免过度拟合。

  • 最大叶节点数:与限制深度类似,限制叶节点数可以避免过度具体的分支并提高泛化能力。

  • 每个叶节点的最小样本数:设置每个叶节点可能包含的样本数的最小限制(当子数据集达到最小大小时停止分割)也可以避免过度具体的叶节点。

  • 集成方法:集成学习是一种结合多个模型以改善单个模型预测的技术。多个模型的预测平均值也可以减少过度拟合。

这些策略可以通过设置适当的超参数来应用。现在我们了解了如何构建决策树以及避免过度拟合的策略,让我们看看如何在 scikit-learn 中构建决策树。

使用 scikit-learn 构建决策树

是时候检验我们如何通过使用 scikit-learn 训练分类和回归树来应用决策树了。

对于这些示例,我们将使用 scikit-learn 中包含的玩具数据集。与真实世界数据相比,这些数据集较小,但易于处理,使我们能够专注于决策树。

乳腺癌分类

我们将使用乳腺癌数据集(scikit-learn.org/stable/data…

使用 scikit-learn,我们可以用五行代码解决这个分类问题:

dataset = datasets.load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(dataset.data, dataset.target, random_state=157)
model = DecisionTreeClassifier(random_state=157, max_depth=3, min_samples_split=2)
model = model.fit(X_train, y_train)
f1_score(y_test, model.predict(X_test))

首先,我们使用load_breast_cancer函数加载数据集。然后,我们使用train_test_split将数据集分为训练集和测试集;默认情况下,25%的数据用于测试集。像之前一样,我们实例化DecisionTreeClassifier模型,并使用model.fit在训练集上训练它。在实例化模型时传递的两个超参数值得注意:max_depthmin_samples_split。这两个参数都控制过拟合,将在下一节中更详细地讨论。我们还指定了训练-测试分割和模型的random_state。通过固定随机状态,我们确保结果可重复(否则,scikit-learn 将为每次执行创建一个新的随机状态)。

最后,我们使用f1_score来衡量性能。我们的模型实现了 0.94 的 F1 分数和 93.7%的准确率。F1 分数是 1.0 的分数,因此我们可以得出结论,该模型表现非常好。如果我们分解我们的预测,模型在测试集的 143 个样本中只错了一个预测:7 个假阳性和 2 个假阴性。

预测糖尿病进展

为了说明使用决策树解决回归问题,我们将使用糖尿病数据集(scikit-learn.org/stable/datasets/toy_dataset.xhtml#diabetes-dataset)。这个数据集有 10 个特征(年龄、性别、体重指数等),模型的任务是预测一年后疾病进展的定量指标。

我们可以使用以下代码构建和评估回归模型:

dataset = datasets.load_diabetes()
X_train, X_test, y_train, y_test = train_test_split(dataset.data, dataset.target, random_state=157)
model = DecisionTreeRegressor(random_state=157, max_depth=3, min_samples_split=2)
model = model.fit(X_train, y_train)
mean_absolute_error(y_test, model.predict(X_test))

我们的模型实现了 45.28 的 MAE。代码几乎与我们的分类示例相同:我们使用DecisionTreeRegressor作为模型,而不是分类器,并计算mean_absolute_error而不是 F1 分数。scikit-learn 中用于解决不同类型模型的各种问题的 API 的一致性是设计上的,它展示了机器学习工作中的一条基本真理:尽管数据、模型和度量会变化,构建机器学习模型的整体过程仍然保持不变。在接下来的章节中,我们将扩展这一通用方法,并在构建机器学习管道时利用过程的这种一致性。

决策树超参数

我们在先前的分类和回归示例中使用了一些决策树超参数来控制过拟合。本节将探讨 scikit-learn 提供的最关键的决策树超参数:

  • max_depth: 树允许达到的最大深度。更深的树允许更多的分割,从而导致更复杂的树和过拟合。

  • min_samples_split: 分割节点所需的最小样本数。仅包含少量样本的节点会导致数据过拟合,而增加最小样本数可以提高泛化能力。

  • min_samples_leaf: 允许在叶子节点中的最小样本数。类似于分割中的最小样本数,增加该值会导致更简单的树,减少过拟合。

  • max_leaf_nodes: 允许的最大叶子节点数。叶子节点越少,树的大小和复杂性就越小,这可能会提高泛化能力。

  • max_features: 确定分割时考虑的最大特征数。丢弃一些特征可以减少数据中的噪声,从而提高过拟合。特征是随机选择的。

  • criterion: 确定分割时使用的杂质度量,可以是ginientropy/log_loss

正如你可能已经注意到的,大多数决策树超参数都涉及通过控制树的复杂性来控制过拟合。这些参数提供了多种方法来实现这一点,找到最佳参数及其值的组合并非易事。找到最佳超参数被称为超参数调整,本书后面将详细讨论。

完整的超参数列表可以在以下位置找到:

现在,让我们总结本章的关键要点。

摘要

在本章中,我们介绍了机器学习作为一种通过学习从数据集中执行任务来创建软件的方法,而不是依靠手动编程指令。我们通过 scikit-learn 的示例,重点介绍了机器学习的核心概念,并展示了它们的应用。

我们还介绍了决策树作为机器学习算法,并讨论了它们的优缺点,以及如何通过超参数控制过拟合。我们通过在 scikit-learn 中使用决策树解决分类和回归问题的示例来结束本章。

本章为我们提供了机器学习的基础理解,使我们能够更深入地了解数据科学过程和 LightGBM 库。

下一章将专注于决策树中的集成学习,这是一种将多个决策树的预测结果结合起来以提高整体性能的技术。特别是梯度提升将详细介绍。

参考文献

*[*1]J. R. Quinlan, 《C4.5:机器学习程序》,Elsevier 出版社,2014 年。
*[*2]R. J. Lewis, 《分类与回归树分析(CART)简介》,发表于旧金山学术急诊医学年会,加利福尼亚,2000 年。

第二章:集成学习 – Bagging 和 Boosting

在上一章中,我们介绍了机器学习(ML)的基础知识,包括数据处理和模型,以及过拟合和监督学习(SL)等概念。我们还介绍了决策树,并展示了如何在 scikit-learn 中实际应用它们。

本章,我们将学习集成学习以及两种最重要的集成学习类型:Bagging 和 Boosting。我们将涵盖将集成学习应用于决策树的理论和实践,并通过关注更高级的 Boosting 方法来结束本章。

到本章结束时,你将很好地理解集成学习以及如何通过 Bagging 或 Boosting 实际构建决策树集成。我们还将准备好深入研究 LightGBM,包括其更高级的理论方面。

我们将涵盖的主要主题如下:

  • 集成学习

  • Bagging 和随机森林

  • 梯度提升决策树(GBDT

  • 高级提升算法—Dropouts meet Multiple Additive Regression Trees(DART

技术要求

本章包括简单机器学习算法的示例,并介绍了如何使用 scikit-learn。你必须安装一个带有 scikit-learn、NumPy、pandas 和 Jupyter 的 Python 环境。本章的代码可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-2找到。

集成学习

集成学习是将多个预测器或模型组合起来创建一个更稳健模型的实践。模型可以是同一类型的(同质集成)或不同类型的(异质集成)。此外,集成学习不仅限于决策树,还可以应用于任何机器学习技术,包括线性模型、神经网络(NNs)等。

集成学习背后的核心思想是通过聚合多个模型的预测结果,来弥补单个模型的弱点。

当然,在相同的数据上训练相同的模型在集成中是没有帮助的(因为模型会有相似的预测)。因此,我们追求模型之间的多样性。多样性指的是集成中每个模型差异的程度。高多样性集成具有广泛不同的模型。

我们有几种方法可以确保集成中的多样性。一种方法是在训练数据的不同子集上训练模型。每个模型都会接触到训练数据中的不同模式和噪声,从而增加训练模型的多样性。

同样,我们可以在训练数据的不同特征子集上训练每个模型。一些特征比其他特征更有价值,一些可能是不相关的,导致模型预测的多样性。

我们还可以用不同的超参数训练每个模型,导致不同复杂性和能力的模型。超参数的影响在决策树的情况下尤为明显,因为超参数显著影响树的结构,导致非常不同的模型。

最后,我们可以通过使用不同类型的模型来多样化集成。每个模型都有其独特的优势和劣势,从而导致集成中的多样性。

集成学习方法指的是我们如何通过指定如何训练成员模型以及如何组合模型结果来引入集成模型中的多样性。最常用的集成方法如下:

  • 自助聚合(Bagging):这些方法是在训练数据的子集(样本或特征)上训练模型,并将预测结果进行聚合。

  • 提升:这涉及到迭代地在先前模型的错误上训练模型。最终预测是通过结合链中所有模型的预测来完成的。

  • 堆叠:这涉及到训练多个基模型,然后训练一个更高阶的模型(称为元模型),以从基模型的预测中学习并做出最终预测。

  • 混合:这与堆叠非常相似。然而,元模型是在基模型对保留集(基学习器未训练过的训练数据的一部分)上的预测上训练的,而不是在整个训练集上。

集成学习方法的目的在于提高我们的预测性能,并且有几种方法可以改善集成相对于单个模型的性能,如下所述:

  • 提高准确性:通过结合预测,我们增加了最终预测准确的可能性,因为从总体上看,模型犯的错误更少。

  • 提高泛化能力和避免过拟合:通过聚合预测,我们减少了最终预测中的方差,提高了泛化能力。此外,在某些集成方法中,模型无法访问所有数据(Bagging 集成),这减少了噪声和过拟合。

  • 提高预测稳定性:预测的聚合减少了单个预测的随机波动。集成对异常值不太敏感,成员模型的异常预测对最终预测的影响有限。

决策树非常适合集成学习,并且存在专门用于集成学习的决策树特定算法。下一节将讨论决策树中的袋装集成,重点关注随机森林

Bagging 和随机森林

袋装是一种集成方法,其中多个模型在训练数据的子集上训练。模型的预测被组合起来做出最终预测,通常是通过取数值预测的平均值(对于回归)或对类别的多数投票(对于分类)。在训练每个模型时,我们从原始训练数据集中选择一个数据子集,并带有替换——也就是说,特定的训练模式可以是多个子集的成员。由于每个模型只接触到训练数据的一个样本,因此没有单个模型可以“记住”训练数据,这减少了过拟合。以下图表说明了袋装过程:

图 2.1 – 描述袋装过程的示意图;每个独立的分类器在训练数据的随机子样本上训练,并通过汇总所有分类器的预测来做出最终预测

图 2.1 – 描述袋装过程的示意图;每个独立的分类器在训练数据的随机子样本上训练,并通过汇总所有分类器的预测来做出最终预测

袋装集成中的每个模型仍然是一个完整的模型,能够独立存在。因此,袋装与强大的模型结合得最好——也就是说,在决策树的情况下,深或宽的决策树。

虽然之前的例子说明了从训练集中抽取样本模式,但也可以为每个模型从数据集中抽取随机特征。在创建训练集时随机选择特征被称为随机子空间方法或特征袋装。特征袋装防止了特定属性可能主导预测或误导模型的情况,并进一步减少了过拟合。

在决策树中,一个同时应用样本袋装和特征袋装的流行算法是随机森林。现在让我们来看看这个算法。

随机森林

随机森林是一种针对决策树的特定袋装集成学习方法[1]。正如其名称所暗示的,它不是构建单个决策树,而是使用袋装训练多个决策树:每棵树要么在随机样本上训练,要么在训练数据中的随机特征上训练,或者两者都训练。随机森林支持分类和回归。

随机森林中单个树的训练方法与单个决策树相同,正如之前所解释的,每棵树都是一棵完整的树。对于预测,森林的最终预测是通过取所有树的算术平均值来实现的;对于分类,是通过多数投票来实现的。

关于性能,随机森林学习产生了一个更稳健的模型,具有更高的准确性,并且倾向于避免过拟合(因为单个模型无法对所有训练数据进行过拟合)。

随机森林超参数

在 scikit-learn 中,正如预期的那样,随机森林的可用超参数与训练决策树的可用超参数相同。我们可以指定max_depthmin_samples_splitmax_leaf_nodes等,然后用于训练单个树。然而,有三个值得注意的附加参数,如下所示:

  • n_estimators:控制森林中的树的数量。通常,树越多越好。然而,通常会达到收益递减的点。

  • max_features:确定在分割节点时用作子集的最大特征数。将max_features=1.0设置为允许在随机选择中使用所有特征。

  • bootstrap决定是否使用袋装法。如果bootstrap设置为False,则所有树将使用整个训练集。

scikit-learn 中所有可用参数的列表在此处提供:scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.xhtml

ExtraTrees

极随机树ExtraTrees)是构建随机决策树的相关方法。使用 ExtraTrees 时,在构建决策节点时,会随机创建多个候选分割,而不是使用Gini 指数信息增益指标来计算最佳分割[2]。然后从所有随机分割中选择最佳分割用于节点。ExtraTrees 的方法可以应用于单个决策树,也可以与随机森林结合使用。scikit-learn 将 ExtraTrees 作为随机森林的扩展实现(scikit-learn.org/stable/modu…

在 scikit-learn 中,ExtraTrees 的实现与随机森林具有相同的超参数。

使用 scikit-learn 训练随机森林

现在,我们将查看如何使用 scikit-learn 中的随机森林。

在本例中,我们将使用森林覆盖类型数据集(archive.ics.uci.edu/ml/datasets/Covertype),该数据集在 scikit-learn(scikit-learn.org/stable/modu… 581,012 个样本,具有 54 个维度(特征数)。特征描述了美国 30x30m 的森林区域(例如,海拔、坡向、坡度和到水文点的距离)。我们必须构建一个分类器,将每个区域分类为描述森林覆盖类型的七个类别之一。

除了训练RandomForestClassifier之外,我们还将训练一个独立的DecisionTreeClassifier和一个ExtraTreesClassifier,并比较算法的性能。

RandomForestClassifierExtraTreesClassifier位于sklearn.ensemble包中。除了我们的常规导入外,我们还从那里导入分类器,如下所示:

from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score

scikit-learn 数据集包提供了森林覆盖数据集。我们可以使用 scikit-learn 获取数据集,并将其拆分为我们的训练集和测试集,如下所示:

dataset = datasets.fetch_covtype()
X_train, X_test, y_train, y_test = train_test_split(dataset.data, dataset.target, random_state=179)

最后,我们可以训练我们的分类器,并将它们各自与测试集进行评估:

tree = DecisionTreeClassifier(random_state=179, min_samples_leaf=3, min_samples_split=6)
tree = tree.fit(X_train, y_train)
print(f1_score(y_test, tree.predict(X_test), average="macro"))
forest = RandomForestClassifier(random_state=179, min_samples_leaf=1, min_samples_split=2, n_estimators=140)
forest = forest.fit(X_train, y_train)
print(f1_score(y_test, forest.predict(X_test), average="macro"))
extra_tree = ExtraTreesClassifier(random_state=179, min_samples_leaf=1, min_samples_split=2, n_estimators=180)
extra_tree = extra_tree.fit(X_train, y_train)
print(f1_score(y_test, extra_tree.predict(X_test), average="macro"))

我们已经为模型设置了适合问题的超参数。额外的一步是优化算法超参数,以发现最佳参数值。参数优化将在后面的章节中详细讨论。

运行前面的代码,我们得到以下每个算法的 F1 分数:

模型F1 分数
决策树0.8917
随机森林0.9209
ExtraTrees0.9231

表 2.1 – Forest CoverType 数据集上每个算法的 F1 分数

ExtraTrees 模型略优于随机森林模型,两者都比决策树分类器表现更好。

在本节中,我们概述了 bagging 和随机森林,这是一种基于 bagging 的决策树集成学习方法,它相对于标准决策树提供了一些优势。下一节将探讨另一种集成学习方法:梯度提升。

阶梯提升决策树

梯度提升是一种集成学习方法,它通过顺序组合多个模型来产生一个更稳健的集成模型。与 bagging 不同,在 bagging 中使用了多个强大的模型(并行使用),而在 boosting 中,训练了多个弱学习器,每个学习器都从前一个学习器的错误中学习,以构建一个更准确和更稳健的集成模型。与 bagging 的另一个显著区别是,每个模型都使用整个数据集进行训练。

注意

如下文所述,梯度提升始终构建一系列回归树作为集成的一部分,无论解决的是回归问题还是分类问题。梯度提升也称为多重加性回归树MART)。

抽象地说,boosting 过程从弱基学习器开始。在决策树的情况下,基学习器可能只有一个分割(也称为决策树桩)。然后计算误差残差(预测值与实际目标之间的差异)。然后,在先前学习器的误差残差上训练新的学习器,以最小化错误。最终的预测是所有学习器预测的总和。以下图示说明了迭代梯度提升过程:

图 2.2 – 阶梯提升过程的示意图;在每次迭代中,都会添加一个新的回归树来补偿前一次迭代的误差残差

图 2.2 – 梯度提升过程的示意图;在每次迭代中,都会添加一个新的回归树来补偿前一次迭代的误差残差

一个关键问题之一是,我们如何确定减少误差残差的变化。梯度提升通过应用一个广泛使用的优化问题来解决误差最小化问题:梯度下降。

梯度下降

梯度下降是一种优化算法,试图找到最小化损失函数的最优参数。参数通过在损失函数负梯度的方向上采取小步迭代更新(从而减少函数值)。损失函数在概念上类似于误差函数,但有两个重要的属性,如下所述:

  • 损失函数产生一个数值,量化了模型的性能或模型表现不佳的精确程度。一个好的损失函数对于不同性能的模型会产生显著不同的输出。一些误差函数也可以用作损失函数——例如,均方 误差MSE)。

  • 第二个属性是损失函数必须是可微分的,特别是在梯度下降的上下文中。一个不可微分的误差函数是 F1 分数。F1 分数可能产生一个表示模型性能的数值,但它不可微分,不能用作损失函数。

梯度下降的过程可以定义为以下。假设我们有一个针对参数 x 定义的损失函数 L。对于一组初始参数,损失计算为 L(x_0)。梯度下降迭代进行以最小化损失函数:

L(x_{n+1}) < L(x_n)

为了更新参数,我们朝着 L 的负梯度方向迈出一步。我们可以将梯度下降更新规则指定如下:

x_{n+1} = x_n − γ_n ∇L(x_n)

在这里,γn 是学习率,它定义了步长,而∇L(xn)是 L 在 xn 处的梯度。

图 2.3中的图表说明了梯度下降过程:

图 2.3 – 显示寻找函数最小值的梯度下降过程的图表

图 2.3 – 显示寻找函数最小值的梯度下降过程的图表

选择合适的学习率对梯度下降的成功至关重要。如果学习率太低,优化过程会非常缓慢,可能无法在允许的迭代次数内达到最小值。学习率太小也可能导致过程陷入局部最小值:步长太小以至于无法逃脱。相反,假设学习率太大。在这种情况下,我们可能会跳过最小值而完全错过它,或者陷入在最小值周围振荡(不断跳来跳去但从未下降到最优值)。

梯度提升

现在我们已经了解了梯度下降的工作原理,我们可以看到它在梯度提升中的应用。我们将通过一个小例子详细地讲解整个梯度提升算法。在我们的例子中,我们将使用回归树,因为它比分类情况更容易理解。

梯度提升算法

梯度提升算法定义如下,其中 M 是提升树的数量 [3]:

  1. 给定训练数据 {(x i, y i)} i=1 n ,包含 n 个训练样本(由特征 x i 和目标 y i 定义)和一个可微的损失函数 L(y i, F(x)),其中 F(x) 是模型 F 的预测。

  2. 使用常数预测值初始化模型 F 0(X) = argmin γ ∑ i=1 n  L( y i, γ)

  3. 对于 m = 1 到 M:

    计算伪残差 r im = − [∂ L(y i, F(x i)) _ ∂ F(x i) ] F(X)=F m−1(X) 对于 i = 1, … , n

    将回归树拟合到 r im 值,并为 j = 1…J m 创建终端区域 R jm

    对于 j = 1…J m 计算 γ jm = argmin γ ∑ x i∈R ij L( y i, F m−1(x i) + γ)

    更新 F m(x) = F m−1(x) + ν∑ j=1 J m  γ jm I(x ∈ R jm)

  4. 结果:F M(x)

虽然算法和特别是数学可能看起来令人畏惧,但实际上它比看起来要简单得多。我们将一步一步地讲解算法。考虑以下玩具数据集:

性别空腹 血糖腰围BMILDL 胆固醇
男性10511029.3170
女性85802190
男性959326113

表 2.2 – 由患者的物理测量和测量的低密度脂蛋白(LDL)胆固醇组成的示例数据集

给定物理测量值,我们旨在预测患者的低密度脂蛋白胆固醇。

上述表格定义了我们的训练数据 {(x i, y i)} i=1 n ,其中 x 是特征(血糖、腰围、BMI)和 y 是目标:低密度脂蛋白胆固醇。

我们需要一个可微的损失函数,为了简化本例中的一些数学推导,我们选择以下损失函数,它与均方误差函数类似:

L =  1 _ 2  ∑ i=0 n (y i − γ i) 2

我们现在将详细地讲解每个算法步骤,以了解梯度提升树是如何产生的。

第一步是找到 F 0(x) = argmin γ ∑ i=1 n  L( y i, γ),其中 y i 是我们的目标值,γ 是我们的初始预测值。我们的初始预测是恒定的,简单来说是目标值的平均值。但让我们看看为什么。

F 0(x) 的方程表明我们需要找到一个 γ 的值,以最小化我们的损失函数。为了找到最小值,我们取损失函数关于 γ 的导数:

∂ L _ ∂ γ  =  2 _ 2 (∑ i=0 n (y i − γ)) × − 1

然后,将其设为 0 并解以下方程:

− (∑ i=0 n (y i − γ)) = 0

−  1 _ n  ∑ i=0 n y i+ γ = 0

γ =  1 _ n  ∑ i=0 n y i

该方程简化为计算目标值的平均值。

将预测更新到表中,我们得到以下内容:

性别F. 血糖W. Circum.BMILDL 胆固醇预测 F 0(x)
10511029.3170125
85802190125
959326113125

表 2.3 – 我们对每个患者的 LDL 胆固醇预测( F 0(x))的初始预测是恒定的

我们重复以下 M 次,其中 M 是我们选择构建的树的数量。

现在,我们需要计算伪残差 r im = − [∂ L(y i, F(x i)) _ ∂ F(x i) ] F(X)=F m−1(X)。这个 r im 方程表明我们使用预测的损失函数的负偏导数来计算伪残差。这部分梯度提升算法与梯度下降相关:我们取负梯度以最小化残差。幸运的是,我们已经计算了此导数:

− [ ∂ L(y i, F(x i)) _ ∂ F(x i) ] = −  ∂ _ ∂ F(x i) (  1 _ 2  (y i − F(x i)) 2) =  2 _ 2 (y i− F(x i)) = ( y i − F(x i))

在这里,F(x i)是预测值。换句话说,该方程简化了目标值和预测值之间的差异。我们可以将残差添加到表中,如下所示:

性别F. 血糖W. Circum.BMILDL 胆固醇预测 F 0(x)F 0(x) 的残差
10511029.317012545
85802190125-35
959326113125-12

表 2.4 – 根据我们的初始预测,我们可以计算每个患者的残差,如 Residuals for F 0(x) 列所示

下一步很简单:我们构建一个回归树来预测残差。我们不直接使用回归树的预测。相反,我们使用终端区域来计算我们的更新预测。终端区域指的是树的叶节点。

对于这个例子,我们假设以下简单的回归树已被构建:

图 2.4 – 预测残差的回归树

图 2.4 – 预测残差的回归树

在我们的回归树构建并定义了叶节点后,我们可以进行下一步。我们需要计算 γ jm,它最小化我们的损失函数,并考虑之前的预测,γ jm = argmin γ ∑ x i∈R ij L( y i, F m−1(x i) + γ)。这正是我们在步骤 1中所做的,我们展示了由于我们选择的损失函数,方程简化为预测值的平均值。在这里,这意味着取每个叶节点中残差的平均值。因此,我们有 γ 1,1 = − 35 − 12 _ 2  = − 23.5 和 γ 2,1 = 45 _ 1  = 45。

最后,我们现在可以计算我们的下一个预测,F1(x),其定义为:Fm(x) = Fm-1(x) + ν∑j=1Jmγjm I(x ∈ Rjm),这意味着我们的下一个预测由先前预测加上在步骤 2.3中计算的γ值,并乘以学习率ν。这里的求和意味着如果一个样本属于多个叶节点,我们取γ值的总和。让我们使用学习率 0.1 计算数据集的第一个样本的 F1(x)。根据步骤 2.2中的回归树,我们的样本(BMI > 26)映射到γ2,1。因为它只映射到一个叶子节点,所以我们不需要方程中的求和部分。因此,方程看起来是这样的:

F1(x) = F0(x) + νγ2,1 = 125 + 0.1(45) = 129.5

如预期,我们的预测在目标值的方向上有所改进。对其他样本做同样的处理,我们得到以下结果:

性别空腹血糖腰围BMILDL 胆固醇预测 F0(x)F0(x)的残差预测 F1(x)
男性10511029.317012545129.5
女性85802190125-35122.65
男性959326113125-12122.65

表 2.5 – 在遵循步骤 2.1 到 2.4 之后,我们根据初始预测和残差计算一个新的预测,F1(x)

学习率的目的在于限制每棵树对整体预测的影响:通过小步骤改进我们的预测,我们最终得到一个整体更准确的模型。

步骤 2然后重复,直到我们得到最终的预测 Fm(x)。

总结来说,我们的梯度提升集成由一系列回归树的预测加权求和组成(权重由学习率决定),每个回归树预测先前预测的伪残差(相对于先前预测的错误梯度),从而最小化先前预测的错误以产生准确的最终预测。

梯度提升用于分类

我们之前给出的梯度提升解释使用了回归问题作为例子。由于算法相同,我们不会详细说明分类的例子。然而,我们不是处理连续的预测值,而是使用与逻辑回归相同的技巧[(en.wikipedia.org/wiki/Logist…

p(x) = 1 / (1 + e^(-(x-μ)/s))

伪残差是观测值(对于类的 1 或 0)与预测值(逻辑函数的概率)之间的差异。最终的差异是损失函数。我们不仅可以使用如 MSE 之类的函数,还可以使用交叉熵作为损失,如下所示:

H p(q) = −  1 _ N  ∑ i=1 N y ilog(p(y i)) + (1 − y i)log(1 − p(y i))

梯度提升决策树超参数

除了标准决策树训练的参数外,scikit-learn 还提供了以下新超参数,专门针对梯度提升树:

  • n_estimators:控制集成中树的数量。一般来说,树越多越好。然而,通常会出现收益递减的点,当树的数量过多时,就会发生过拟合。

  • learning_rate:控制每棵树对集成贡献的大小。较低的学习率会导致更长的训练时间,可能需要构建更多的树(n_estimators的较大值)。将learning_rate设置得非常大可能会导致优化错过最佳点,并且必须与较少的树结合使用。

可以在scikit-learn.org/stable/modules/generated/sklearn.ensemble.GradientBoostingClassifier.xhtml找到 scikit-learn 梯度提升超参数的完整列表。

scikit-learn 中的梯度提升

梯度提升的细节是数学的且复杂;幸运的是,通过 scikit-learn,该算法与其他算法一样易于访问。以下是一个 scikit-learn 中GradientBoostingClassifier类的示例,再次使用我们在本章前面使用的Forest CoverType数据集来训练随机森林分类器。

分类器也像这样从ensemble包中导入:

from sklearn.ensemble import GradientBoostingClassifier

我们像以前一样获取并分割数据,然后拟合模型,如下所示:

dataset = datasets.fetch_covtype()
X_train, X_test, y_train, y_test = train_test_split(dataset.data, dataset.target, random_state=179)
booster = GradientBoostingClassifier(random_state=179, min_samples_leaf=3, min_samples_split=3, learning_rate=0.13, n_estimators=180)
booster = booster.fit(X_train, y_train)
print(f1_score(y_test, booster.predict(X_test), average="macro"))

运行前面的代码应该会产生一个 F1 分数为 0.7119,这个分数比标准的决策树还要差得多。我们可以花时间优化超参数以提高性能。然而,有一个更严重的问题。与 ExtraTrees 相比,之前的代码执行时间非常长——在我们的硬件上大约需要 45 分钟——而 ExtraTrees 只需要大约 3 分钟。

LightGBM 解决了我们与梯度提升树相关的问题,并以更短的时间构建了一个性能显著更好的梯度提升树。

在下一节中,我们将简要介绍与梯度提升相关的高级算法:DART。

高级提升算法 – DART

DART是前一小节中讨论的标准 GBDT 算法的扩展[4]。DART 采用了dropout,这是一种来自深度学习DL)的技术,通过决策树集成来避免过拟合。这个扩展很简单,包括两个部分。首先,在拟合下一个预测树 M n+1(x),它由所有先前树的缩放总和 M n…M 1 组成时,使用先前树的随机子集,而不是从和中删除其他树。p drop 参数控制先前树被包含的概率。DART 算法的第二部分是对新树的贡献进行额外的缩放。设 k 为新树 M n+1 计算时删除的树的数目。由于 M n+1 是在更新我们的预测 F n+1(包括所有树)时没有这些 k 棵树的贡献下计算的,因此预测会超出。因此,新树通过一个 1 _ k 的因子进行缩放以补偿。

DART 已被证明在性能上优于标准的 GBDT,同时显著减少了过拟合。

Scikit-learn 没有为 GBDT 实现 DART,但 DART 已包含在 LightGBM 中。

摘要

总结来说,本章探讨了决策树集成学习的两种最常见方法:bagging 和 boosting。我们研究了随机森林和 ExtraTrees 算法,它们使用 bagging 构建决策树集成。

本章还通过逐步介绍 GBDT 算法,详细概述了决策树中的 boosting,说明了梯度提升是如何应用的。我们涵盖了 scikit-learn 中随机森林、ExtraTrees 和 GBDT 的实用示例。

最后,我们探讨了如何使用 DART 算法将 dropout 应用于 GBDT。我们现在彻底理解了决策树集成技术,并准备好深入研究 LightGBM。

下一章将详细介绍 LightGBM 库,包括该库的理论进步及其实际应用。我们还将探讨如何使用 Python 和 LightGBM 解决机器学习问题。

参考文献

*[*1]L. Breiman, “随机森林,”机器学习,第 45 卷,第 5-32 页,2001 年。
*[*2]P. Geurts, D. Ernst 和 L. Wehenkel, “Extremely randomized trees,”机器学习,第 63 卷,第 3-42 页,2006 年。
*[*3]J. H. Friedman, “Greedy function approximation: a gradient boosting machine,”统计年鉴,第 1189-1232 页,2001 年。
*[*4]R. K. Vinayak 和 R. Gilad-Bachrach, “Dart: Dropouts meet multiple additive regression trees,”在人工智能与 统计学,2015 年。

第三章:Python 中 LightGBM 的概述

在上一章中,我们探讨了决策树的集成学习方法。详细讨论了 bootstrap aggregationbagging)和梯度提升,并提供了如何在 scikit-learn 中应用这些技术的实际示例。我们还展示了 梯度提升决策树GBDTs)训练速度慢,在某些问题上可能表现不佳。

本章介绍了 LightGBM,这是一个使用基于树的学习者的梯度提升框架。我们探讨了 LightGBM 对集成学习方法的创新和优化。还提供了使用 Python 实际应用 LightGBM 的详细信息和示例。最后,本章包含了一个使用 LightGBM 的建模示例,结合了更高级的模型验证和参数优化技术。

到本章结束时,您将对 LightGBM 的理论和实际特性有深入的了解,这将使我们能够更深入地探讨在数据科学和生产系统中使用 LightGBM。

本章的主要主题如下:

  • 介绍 LightGBM

  • 在 Python 中开始使用 LightGBM

  • 构建 LightGBM 模型

技术要求

本章包含示例和代码片段,说明如何在 Python 中使用 LightGBM。有关设置本章所需环境的完整示例和说明可在 github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-3 找到。

介绍 LightGBM

LightGBM 是一个开源的基于树的集成梯度提升框架 (github.com/microsoft/LightGBM)。LightGBM 专注于速度、内存使用和改进的准确性,特别是在高维度和大数据量问题上。

LightGBM 首次在论文 LightGBM: A Highly Efficient Gradient Boosting Decision Tree [1] 中介绍。

LightGBM 的效率和精度是通过针对标准集成学习方法的几个技术和理论优化实现的,特别是 GBDTs。此外,LightGBM 支持通过优化网络通信和基于 GPU 的树集成训练进行分布式训练。

LightGBM 支持许多 机器学习ML)应用:回归、二分类和多分类、交叉熵损失函数以及通过 LambdaRank 进行排序。

LightGBM 算法也可以通过其超参数进行高度定制。它支持许多指标和功能,包括 Dropouts meet Multiple Additive Regression TreesDART)、bagging(随机森林)、连续训练、多个指标和早期停止。

本节回顾了 LightGBM 使用的理论和实践优化,包括控制 LightGBM 特征的超参数的详细概述。

LightGBM 优化

在其核心,LightGBM 实现了我们在上一章中讨论的相同集成算法。然而,LightGBM 通过理论和技术的优化来提高性能和准确性,同时显著减少内存使用。接下来,我们将讨论 LightGBM 中实施的最显著的优化。

GBDT 中的计算复杂度

首先,我们必须理解构建 GBDT 中的低效性来源,才能理解 LightGBM 如何提高 GBDT 的效率。GBDT 算法中最计算复杂的部分是每次迭代的回归树训练。更具体地说,找到最优分割是非常昂贵的。基于预排序的算法是寻找最佳分割的最流行方法之一[2],[3]。一种简单的方法要求对每个决策节点按特征对数据进行排序,算法复杂度为 O(#data × #feature)。基于预排序的算法在训练前对数据进行一次排序,这降低了构建决策节点的复杂度到 O(#data) [2]。即使有预排序,当寻找决策节点的分割时,复杂度对于大型数据集来说仍然太高。

基于直方图的采样

预排序的另一种方法涉及为连续特征构建直方图[4]。在构建这些特征直方图时,连续值被添加到离散的箱中。在计算决策节点的分割时,我们不再直接使用数据,而是现在可以使用直方图箱。构建直方图的复杂度为 O(#data)。然而,构建决策节点的复杂度现在降低到 O(#bins),由于箱的数量远小于数据量,这显著加快了构建回归树的过程,如下面的图所示:

图 3.1 – 从连续特征创建特征直方图允许使用箱边界值来计算决策节点的分割,而不是必须对每个数据点进行采样,这显著降低了算法的复杂性,因为#bins << #data

图 3.1 – 从连续特征创建特征直方图允许使用箱边界值来计算决策节点的分割,而不是必须对每个数据点进行采样,这显著降低了算法的复杂性,因为#bins << #data

由使用直方图产生的二级优化是“直方图减法”,用于构建叶子的直方图。我们不需要为每个叶子计算直方图,而是可以从父直方图中减去叶子的邻居直方图。选择数据量较小的叶子会导致第一个叶子的 O(#data)复杂度较小,由于直方图减法,第二个叶子的 O(#bin)复杂度较小。

LightGBM 使用直方图应用的一种第三种优化是减少内存成本。特征预排序需要为每个特征提供一个支持数据结构(一个字典)。在构建直方图时不需要这样的数据结构,从而降低了内存成本。此外,由于#bins 很小,可以使用较小的数据类型,如uint8_t来存储训练数据,从而减少内存使用。

关于构建特征直方图算法的详细信息可在论文《CLOUDS:用于大型数据集的决策树分类器》[4]中找到。

独家功能捆绑

独家功能捆绑EFB)是 LightGBM 在处理稀疏数据(稀疏数据在高维数据集中普遍存在)时应用的一种基于数据的优化。当特征数据稀疏时,通常会发现许多特征是相互排斥的,这意味着它们永远不会同时呈现非零值。考虑到这种排他性,将这些特征组合成一个单一的特征通常是安全的。EFB 在以下图中展示:

图 3.2 – 从两个相互排斥的特征构建特征捆绑

图 3.2 – 从两个相互排斥的特征构建特征捆绑

将相互排斥的特征捆绑在一起,可以构建与单个特征相同的特征直方图[1]。这种优化将构建特征直方图的复杂度从 O(#数据 × #特征)降低到 O(#数据 × #捆绑)。对于存在许多相互排斥特征的数据库,这显著提高了性能,因为#捆绑远小于#特征。EFB 的详细算法及其正确性的证明可在[1]中找到。

基于梯度的单侧采样

LightGBM 框架中可用的最后一种基于数据的优化是基于梯度的单侧采样GOSS)[1]。GOSS 是一种丢弃不再对训练过程有显著贡献的训练数据样本的方法,从而有效地减少了训练数据的大小并加快了过程。

我们可以使用每个样本的梯度计算来确定其重要性。如果梯度变化很小,这表明训练误差也很小,我们可以推断出树对特定数据实例拟合得很好[1]。一个选择是丢弃所有梯度小的实例。然而,这改变了训练数据的分布,减少了树泛化的能力。GOSS 是一种选择保留在训练数据中的实例的方法。

为了保持数据分布,GOSS 按照以下方式应用:

  1. 数据样本按其梯度的绝对值排序。

  2. 然后选择前 a × 100%的实例(梯度大的实例)。

  3. 然后从剩余的数据中随机抽取 b × 100%的实例样本。

  4. 在损失函数(对于这些实例)中添加一个因子以放大其影响:1 − a _ b,从而补偿小梯度数据的代表性不足。

因此,GOSS 从具有大梯度的实例中采样大量实例,并从具有小梯度的实例中随机采样一部分实例,在计算信息增益时放大小梯度的影响。

GOSS 启用的下采样可以显著减少训练过程中处理的数据量(以及 GBDT 的训练时间),尤其是在大型数据集的情况下。

最佳优先树增长

构建决策树最常见的方法是按层次增长(即,一次增长一个层次)。LightGBM 采用了一种替代方法,通过叶节点或最佳优先的方式增长树。叶节点方法选择具有最大损失变化的现有叶节点,并从那里构建树。这种方法的一个缺点是,如果数据集很小,树很可能会过拟合数据。必须设置最大深度来抵消这一点。然而,如果构建的叶节点数量是固定的,叶节点树构建已被证明优于层次算法[5]。

L1 和 L2 正则化

LightGBM 在集成中训练回归树时支持目标函数的 L1 和 L2 正则化。从 第一章 介绍机器学习 中,我们回忆起正则化是控制过拟合的一种方法。在决策树的情况下,更简单、更浅的树过拟合较少。

为了支持 L1 和 L2 正则化,我们通过添加正则化项扩展了目标函数,如下所示:

obj = L(y, F(x)) + Ω(w)

在这里,L(y, F(x)) 是在第二章中讨论的损失函数,集成学习 – Bagging 和 Boosting,而 Ω(w) 是定义在 w 上的正则化函数,即叶得分(叶得分是根据 GBDT 算法中定义的 步骤 2.3 计算的叶输出,该算法在第二章中讨论,集成学习 – Bagging 和 Boosting)。

正则化项有效地向目标函数添加了惩罚,我们的目标是惩罚更复杂的树,这些树容易过拟合。

Ω 有多个定义。决策树中这些项的典型实现如下:

Ω(w) = α∑ i n |w i| + λ∑ i n w i 2

在这里,α∑ i n |w i| 是由参数 α 控制的 L1 正则化项,0 ≤ α ≤ 1,而 λ∑ i n w i 2 是由参数 λ 控制的 L2 正则化项。

L1 正则化通过惩罚具有大绝对输出的叶节点,将叶得分驱动到零。较小的叶输出对树的预测影响较小,从而有效地简化了

L2 正则化类似,但由于输出取平方,对异常值叶节点有更大的影响。

最后,当构建较大的树(具有更多叶节点,因此具有较大的 w 向量)时,Ω(w) 的两个求和项都会增加,从而增加目标函数的输出。因此,较大的树会受到惩罚,从而减少过拟合。

LightGBM 优化总结

总结来说,LightGBM 通过以下方式改进了标准集成算法:

  • 实现基于直方图的采样特征以减少寻找最优分割的计算成本

  • 通过计算独家特征包来减少稀疏数据集中的特征数量

  • 应用 GOSS 以在不损失准确性的情况下对训练数据进行下采样

  • 以叶节点的方式构建树以提高准确性

  • 通过 L1 和 L2 正则化以及其他控制参数可以控制过拟合

结合优化,这些优化将 LightGBM 的计算性能提高了与标准 GBDT 算法相比的数量级OOM)。此外,LightGBM 是用 C++实现的,具有 Python 接口,这使得代码比基于 Python 的 GBDT(如 scikit-learn)快得多。

最后,LightGBM 还支持改进的数据并行和特征并行分布式训练。分布式训练和 GPU 支持将在后面的第十一章**,使用 LightGBM 的分布式和基于 GPU 的学习中讨论。

超参数

LightGBM 公开了许多参数,可用于自定义训练过程、目标和性能。接下来,我们将讨论最显著的参数以及它们如何用于控制特定现象。

注意

核心 LightGBM 框架是用 C++开发的,但包括用于在 C、Python 和 R 中与 LightGBM 一起工作的 API。本节讨论的参数是框架参数,并且每个 API 以不同的方式暴露。以下章节将讨论使用 Python 时可用参数。

以下是用以控制优化过程和目标的核心框架参数

  • 目标:LightGBM 支持以下优化目标,包括但不限于——回归(包括具有 Huber 和 Fair 等损失函数的回归应用),二元(分类),多类(分类),交叉熵,以及用于排序问题的lambdarank

  • boosting:提升参数控制提升类型。默认情况下,此参数设置为gbdt,即标准 GBDT 算法。其他选项是dartrf,用于随机森林。随机森林模式不执行提升,而是构建随机森林。

  • num_iterations(或n_estimators):控制提升迭代次数,因此也控制构建的树的数量。

  • num_leaves:控制单个树中的最大叶节点数。

  • learning_rate:控制学习或收缩率,即每个树对整体预测的贡献。

LightGBM 还提供了许多参数来控制学习过程。我们将讨论这些参数相对于它们如何用于调整训练的特定方面。

以下控制参数可用于提高准确性

  • boosting:使用dart,这已被证明优于标准 GBDT。

  • learning_rate:学习率必须与num_iterations一起调整以获得更好的准确率。较小的学习率与较大的num_iterations值相结合,可以在牺牲优化速度的情况下提高准确率。

  • num_leaves:较大的叶子数量可以提高准确率,但可能导致过拟合。

  • max_bin:在构建直方图时,将特征分桶的最大数量。较大的max_bin大小会减慢训练速度并使用更多内存,但可能提高准确率。

以下学习控制参数可用于处理过拟合

  • bagging_fractionbagging_freq:设置这两个参数可以启用特征袋装。除了提升之外,还可以使用袋装,并且不强制使用随机森林。启用袋装可以减少过拟合。

  • early_stopping_round:启用早期停止并控制用于确定是否停止训练的迭代次数。如果在early_stopping_round设置的迭代中任何指标没有改进,则停止训练。

  • min_data_in_leaf:叶子中允许的最小样本数。较大的值可以减少过拟合。

  • min_gain_to_split:执行分割所需的最小信息增益量。较高的值可以减少过拟合。

  • reg_alpha:控制 L1 正则化。较高的值可以减少过拟合。

  • reg_lambda:控制 L2 正则化。较高的值可以减少过拟合。

  • max_depth:控制单个树的最大深度。较浅的树可以减少过拟合。

  • max_drop:控制使用 DART 算法(仅在boosting设置为dart时使用)时丢弃的最大树的数量。较大的值可以减少过拟合。

  • extra_trees:启用极端随机树ExtraTrees)算法。LightGBM 将为每个特征随机选择一个分割阈值。启用 Extra-Trees 可以减少过拟合。该参数可以与任何提升模式一起使用。

这里讨论的参数仅包括 LightGBM 中可用参数的一部分,并专注于提高准确性和防止过拟合。完整的参数列表可在以下链接中找到:https://lightgbm.readthedocs.io/en/latest/Parameters.xhtml

LightGBM 的局限性

LightGBM 被设计得比传统方法更高效和有效。它特别以其处理大数据集的能力而闻名。然而,与任何算法或框架一样,它也有其局限性和潜在缺点,包括以下内容:

  • 对过拟合敏感:LightGBM 可能对过拟合敏感,尤其是在小或噪声数据集上。在使用 LightGBM 时,应小心监控和控制过拟合。

  • 最佳性能需要调整:如前所述,LightGBM 有许多超参数需要适当调整,以从算法中获得最佳性能。

  • 缺乏表示学习:与擅长从原始数据中学习的 深度学习DL)方法不同,LightGBM 在学习之前需要应用特征工程到数据上。特征工程是一个耗时且需要领域知识的过程。

  • 处理序列数据:LightGBM 本身并不是为处理序列数据(如时间序列)而设计的。为了使用 LightGBM 处理时间序列数据,需要应用特征工程来创建滞后特征并捕捉时间依赖性。

  • 复杂交互和非线性:LightGBM 是一种以决策树为驱动的方法,可能无法捕捉复杂的特征交互和非线性。需要应用适当的特征工程来确保算法能够建模这些。

虽然这些是使用该算法的潜在局限性,但它们可能并不适用于所有用例。在适当的情境下,LightGBM 经常是一个非常有效的工具。与任何模型一样,理解权衡对于为您的应用程序做出正确的选择至关重要。

在下一节中,我们将探讨如何使用 Python 的各种 LightGBM API 开始使用。

在 Python 中开始使用 LightGBM

LightGBM 使用 C++ 实现,但提供了官方的 C、R 和 Python API。本节讨论可用于与 LightGBM 一起工作的 Python API。LightGBM 提供了三个 Python API:标准的 LightGBM API、与其它 scikit-learn 功能完全兼容的 scikit-learn API,以及用于与 Dask 一起工作的 Dask API。Dask 是在第 第十一章* 中讨论的并行计算库,分布式和基于 GPU 的学习 (www.dask.org/)。

在本书的其余部分,我们主要使用 LightGBM 的 scikit-learn API,但让我们首先看看标准的 Python API。

LightGBM Python API

深入了解 Python API 的最佳方式是通过动手示例。以下是从代码列表中摘录的片段,说明了 LightGBM Python API 的使用。完整的代码示例可在 github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-3 找到。

需要导入 LightGBM。导入通常简写为 lgb

import lightgbm as lgb

LightGBM 提供了一个 Dataset 包装类来处理数据。Dataset 支持多种格式。通常,它用于包装 numpy 数组或 pandas DataFrame。Dataset 还接受 CSV、TSV、LIBSVM 文本文件或 LightGBM Dataset 二进制文件的 Path。当提供路径时,LightGBM 会从磁盘加载数据。

在这里,我们从 sklearn 加载我们的 Forest Cover 数据集,并将 numpy 数组包装在 LightGBM 的 Dataset 中:

dataset = datasets.fetch_covtype()
X_train, X_test, y_train, y_test = train_test_split(dataset.data, dataset.target, random_state=179)
training_set = lgb.Dataset(X_train, y_train - 1)
test_set = lgb.Dataset(X_test, y_test - 1)

我们从y_trainy_test数组中减去 1,因为sklearn提供的类别标签在范围[1, 7]内,而 LightGBM 期望零索引的类别标签在范围[0, 7]内。

我们无法设置训练的参数。我们将使用以下参数:

params = {
    'boosting_type': 'gbdt',
    'objective': 'multiclass',
    'num_classes': '7',
    'metric': {'auc_mu'},
    'num_leaves': 120,
    'learning_rate': 0.09,
    'force_row_wise': True,
    'verbose': 0
}

我们使用标准的 GBDT 作为提升类型,并将目标设置为七类的多分类。在训练过程中,我们将捕获auc_mu指标。AU C μ是多类版本的受试者工作特征曲线下面积AUC),如 Kleiman 和 Page [6]所定义。

我们将num_leaveslearning_rate设置为适合该问题的合理值。最后,我们指定force_row_wiseTrue,这是大型数据集的一个推荐设置。

LightGBM 的训练函数也支持回调。回调是训练过程中的一个钩子,在每个提升迭代中执行。为了说明它们的目的,我们将使用以下回调:

metrics = {}
callbacks = [
    lgb.log_evaluation(period=15),
    lgb.record_evaluation(metrics),
    lgb.early_stopping(15),
    lgb.reset_parameter(learning_rate=learning_rate_decay(0.09, 0.999))
]

我们使用log_evaluation回调,周期为 15,它每 15 次提升迭代将我们的指标记录(打印)到标准输出。我们还设置了一个record_evaluation回调,它将我们的评估指标捕获在metrics字典中。我们还指定了一个early_stopping回调,停止轮次设置为 15。如果经过指定的停止轮次后没有验证指标改进,early_stopping回调将停止训练。

最后,我们还使用reset_parameter回调来实现学习率衰减。衰减函数定义如下:

def learning_rate_decay(initial_lr, decay_rate):
    def _decay(iteration):
        return initial_lr * (decay_rate ** iteration)
    return _decay

reset_parameter回调接受一个函数作为输入。该函数接收当前迭代次数并返回参数值。学习率衰减是一种技术,随着时间的推移降低学习率的值。学习率衰减提高了整体准确度。理想情况下,我们希望初始树对纠正预测错误有更大的影响。相比之下,后期我们希望减少额外树的影响,并让它们对错误进行微小调整。我们在整个训练过程中实施了一种轻微的指数衰减,将学习率从 0.09 降低到 0.078。

现在,我们已经准备好进行训练。我们使用lgb.train来训练模型:

gbm = lgb.train(params, training_set, num_boost_round=150, valid_sets=test_set, callbacks=callbacks)

我们使用 150 次提升轮次(或提升树)。结合较低的学习率,拥有许多提升轮次应该可以提高准确度。

训练完成后,我们可以使用lgb.predict来获取测试集的预测并计算 F1 分数:

y_pred = np.argmax(gbm.predict(X_test, num_iteration=gbm.best_iteration), axis=1)
f1_score(y_test - 1, y_pred, average="macro")

LightGBM 的预测函数输出一个激活数组,每个类别一个。因此,我们使用np.argmax来选择具有最高激活的类别作为预测类别。LightGBM 也支持一些绘图函数。例如,我们可以使用plot_metric来绘制我们在metrics中捕获的 AU C μ结果:

lgb.plot_metric(metrics, 'auc_mu')

这些结果在图 3**.3中显示。

图 3.3 – 使用 lgb.plot_metric 创建的每个训练迭代的 AU C <?AID d835?><?AID df41?> metric 的绘图

图 3.3 – 使用 lgb.plot_metric 创建的每个训练迭代的 AU C 𝝁指标的绘图

运行前面的代码应该会产生一个 LightGBM GBDT 树,其 F1 分数大约为 0.917,与随机森林和 Extra-Trees 算法在第二章**,集成学习 – Bagging 和 Boosting中实现的分数一致。然而,LightGBM 在达到这些准确度方面要快得多。在我们的硬件上,LightGBM 仅用了 37 秒就完成了训练:这比在相同问题和硬件上运行 Extra-Trees 快 4.5 倍,比我们在测试中使用的 scikit-learn 的GradientBoostingClassifier快 60-70 倍。

LightGBM scikit-learn API

现在我们来看看 scikit-learn Python API 的 LightGBM。scikit-learn API 提供了四个类:LGBMModelLGBMClassifierLGBMRegressorLGBMRanker。这些类都提供了与 LightGBM Python API 相同的功能,但具有我们之前使用过的相同方便的 scikit-learn 接口。此外,scikit-learn 类与 scikit-learn 生态系统的其余部分兼容和可互操作。

让我们使用 scikit-learn API 重复之前的示例。

数据集的加载方式与之前完全相同。scikit-learn API 不需要将数据包装在Dataset对象中。我们也不需要为目标类进行零索引,因为 scikit-learn 支持任何标签的类:

dataset = datasets.fetch_covtype()
X_train, X_test, y_train, y_test = train_test_split(dataset.data, dataset.target, random_state=179)

scikit-learn API 还支持 LightGBM 回调;因此,我们使用与之前相同的回调:

metrics = {}
callbacks = [
    lgb.log_evaluation(period=15),
    lgb.record_evaluation(metrics),
    lgb.early_stopping(15),
    lgb.reset_parameter(learning_rate=learning_rate_decay(0.09, 0.999))
]

然后,我们创建LGBMClassifier的方式与创建任何其他 scikit-learn 模型的方式完全相同。在创建分类器时,我们还设置了参数:

model = lgb.LGBMClassifier(
    boosting_type='gbdt',
    n_estimators=150,
    num_leaves=120,
    learning_rate=0.09,
    force_row_wise=True
)

注意,我们不需要指定类的数量;scikit-learn 会自动推断。然后我们在模型上调用fit,传递训练数据和测试数据以及我们的回调:

model = model.fit(X_train, y_train, eval_set=(X_test, y_test), eval_metric='auc_mu', callbacks=callbacks)

最后,我们使用 F1 分数评估我们的模型。我们不需要在预测上使用np.argmax,因为这是由 scikit-learn API 自动完成的:

f1_score(y_test, model.predict(X_test), average="macro")

总体来看,我们可以看到通过 scikit-learn API 使用 LightGBM 比使用标准的 Python API 更简单。在我们的硬件上,scikit-learn API 也比 LightGBM API 快约 40%。本节探讨了使用 LightGBM 的各种 Python API 的优缺点。下一节将探讨使用 scikit-learn API 训练 LightGBM 模型。

构建 LightGBM 模型

本节提供了一个使用 LightGBM 解决实际问题的端到端示例。我们更详细地查看问题的数据准备,并解释如何为我们的算法找到合适的参数。我们使用多个 LightGBM 变体来探索相对性能,并将它们与随机森林进行比较。

交叉验证

在深入探讨解决问题之前,我们需要讨论一种更好的验证算法性能的方法。在训练模型时,将数据分成两个或三个子集是标准做法。训练数据用于训练模型,验证数据是用于在训练期间验证数据的保留集,测试数据用于在训练后验证性能。

在之前的例子中,我们只进行了一次这种分割,构建了一个单独的训练和测试集来训练和验证模型。这种方法的问题是我们模型可能会“幸运”。如果我们测试集偶然与训练数据非常接近,但并不代表现实世界数据,我们可能会报告一个很好的测试误差,尽管我们无法对我们的模型性能有信心。

另一种方法是多次进行数据集分割并多次训练模型,每次分割训练一次。这种方法称为交叉验证

交叉验证最常见的应用是k折交叉验证。在 k 折交叉验证中,我们选择一个值k,并将(随机打乱的)数据集分成k个子样本(或折)。然后我们重复训练过程k次,使用不同的子集作为验证数据,所有其他子集作为训练数据。模型性能的计算是所有折的平均(或中位数)分数。以下图表说明了这个过程:

图 3.4 – k 折交叉验证,k = 3;原始数据集被随机打乱并分成 3 个相等的部分(或折);对每个子样本数据的组合重复训练和验证,并报告平均性能

图 3.4 – k 折交叉验证,k = 3;原始数据集被随机打乱并分成 3 个相等的部分(或折);对每个子样本数据的组合重复训练和验证,并报告平均性能

使用高值的k可以降低模型偶然表现出良好性能的可能性,并表明模型在现实世界中的可能表现。然而,整个训练过程需要为每个折重复,这可能会非常耗费计算资源和时间。因此,我们需要平衡可用的资源与验证模型的需求。k的典型值是 5(scikit-learn 的默认值),也称为 5 折交叉验证。

分层 k 折验证

在 k 折交叉验证中可能出现的问题之一是,由于偶然性,一个折可能只包含来自单个类的样本。分层抽样通过在创建折时保留每个类的样本百分比来解决此问题。这样,每个折都具有与原始数据集相同的类别分布。当应用于交叉验证时,这种技术称为分层 k 折交叉验证。

参数优化

参数优化,也称为参数调整,是寻找针对特定问题的模型和训练过程的好超参数的过程。在之前的训练模型示例中,我们一直是基于直觉和最小实验来设置模型和训练算法的参数。没有保证参数选择对优化问题是最佳的。

但我们如何找到最佳参数选择呢?一种天真策略是尝试一个参数的广泛范围值,找到最佳值,然后对下一个参数重复此过程。然而,参数通常是相互依赖的。当我们改变一个参数时,另一个参数的最佳值可能会不同。GBDTs 中相互依赖的一个优秀例子是提升轮数和学习率。拥有较小的学习率需要更多的提升轮数。因此,独立优化学习率和提升轮数不太可能产生最佳结果。这两个参数必须同时优化

网格搜索

考虑参数相互依赖的方法是网格搜索。在网格搜索中,设置一个参数网格。网格由我们正在优化的每个参数要尝试的值范围组成。然后执行穷举搜索,在每个参数组合上训练和验证模型。

这里是一个三个参数的参数网格示例:

grid = {
    'learning_rate': [0.001, 0.01, 0.1, 0.2, 0,5],
    'num_rounds': [20, 40, 60, 80, 100],
    'num_leaves': [2, 16, 32, 64, 128, 256],
}

每个参数都指定了一个可能的值范围。之前的网格需要 150 次尝试来搜索。

由于网格搜索是穷举的,它有一个优点,即保证在指定的范围内找到最佳参数组合。然而,网格搜索的缺点是成本。尝试每个可能的参数组合非常昂贵,并且对于许多参数和大的参数范围很快就会变得难以处理。

Scikit-learn 提供了一个实用类来实现网格搜索并同时执行交叉验证。GridSearchCV 接收一个模型、一个参数网格和交叉验证折数作为参数。GridSearchCV 然后开始搜索网格以找到最佳参数,使用交叉验证来验证每个参数组合的性能。我们将在下一节中展示 GridSearchCV 的使用方法。

参数优化是建模过程中的关键部分。为模型找到合适的参数可能是成功或失败过程的关键。然而,正如之前所讨论的,参数优化在时间和计算复杂度上通常也非常昂贵,这需要在成本和性能之间进行权衡。

预测学生学术成功

我们现在继续到我们的例子。我们构建了一个模型,基于一系列社会和经济因素使用 LightGBM [7] (archive-beta.ics.uci.edu/dataset/697/predict+students+dropout+and+academic+success) 来预测学生的辍学率。数据以 CSV 格式提供。我们首先从探索数据开始。

探索性数据分析

任何数据集的最基本属性之一是形状:我们的数据由行和列组成。这也是验证数据读取是否成功的一个很好的方法。在这里,我们的数据由 4,424 行和 35 列组成。随机抽取数据样本让我们对列及其值有了概念:

df = pd.read_csv("students/data.csv", sep=";")
print(f"Shape: {df.shape}")
df.sample(10)

接下来,我们可以运行df.info()来查看所有列、它们的非空计数和它们的数据类型:

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4424 entries, 0 to 4423
Data columns (total 35 columns):
 #   Column                                          Non-Null Count  Dtype
---  ------                                          --------------  -----
 0   Marital status                                  4424 non-null   int64
 1   Application mode                                4424 non-null   int64
 2   Application order                               4424 non-null   int64
…

运行前面的代码显示,大多数列是整数类型,除了Target列,中间有一些浮点数。Target列被列为object类型;如果我们查看样本中的值,我们可以看到Target列由GraduateDropoutEnrolled字符串组成。LightGBM 不能处理字符串作为目标,所以我们将在训练模型之前将这些映射到整数值。

我们还可以运行df.describe()来获取每个列的统计描述(平均值、标准差、最小值、最大值和分位数)。计算描述性统计有助于检查数据的界限(与决策树模型一起工作时不是大问题)并检查异常值。对于这个数据集,没有数据界限或异常值问题。

接下来,我们需要检查重复和缺失值。我们需要删除包含缺失值的行,或者如果有任何缺失值,需要用适当的替代值进行插补。我们可以使用以下代码检查缺失值:

df.isnull().sum()

运行前面的代码显示,这个数据集没有缺失值。

为了定位重复项,我们可以运行以下代码:

df.loc[df.duplicated()]

数据集中也没有重复项。如果有任何重复数据,我们会删除额外的行。

我们还需要检查目标类的分布,以确保它是平衡的。在这里,我们展示了一个直方图,表明目标类的分布。我们使用 Seaborn 的countplot()方法创建直方图,如下所示:

sns.countplot(data=df, x='Target')

图 3.5 – 学术成功数据集中目标类的分布

图 3.5 – 学术成功数据集中目标类的分布

尽管目标分布并不完全平衡,但它也没有过度偏向任何一类,我们不需要执行任何补偿操作。

到目前为止,我们发现我们的数据集适合建模(我们仍然需要重新映射 Target)和清洗(它不包含缺失或重复的值,并且平衡良好)。现在我们可以更深入地查看一些特征,从特征相关性开始。以下代码绘制了一个相关性热图。成对皮尔逊相关性是通过 df.corr() 计算的。随后的截图显示了使用成对皮尔逊相关性构建的相关性热图:

sns.heatmap(df.corr(), cmap='coolwarm')

图 3.6 – 学术成功数据集的成对皮尔逊特征相关性

图 3.6 – 学术成功数据集的成对皮尔逊特征相关性

我们可以看到三种相关性的模式:第一学期学分、入学、评估和批准都是相关的。这些特征的第一学期和第二学期的值也是相关的。这些相关性表明,一旦学生入学,他们倾向于看到整个学年而不是在学期中辍学。尽管存在相关性,但这些相关性并不足够强,以至于考虑删除任何特征。

第三种相关性模式是 NacionalityInternational 之间的,它们高度相关。

注意

单词 Nacionality 指的是 nationality。我们在这里也保留了原始数据集中的拼写,以保持一致性。

仔细观察 Nacionality 特征会发现,几乎所有行只有一个值:数据集收集的国家。强烈的相关性意味着 International 特征也是这样:

nationalities = df.groupby(['Nacionality', 'Target']).size().reset_index().pivot(columns='Target', index='Nacionality', values=0)
nationalities_total = nationalities.sum(axis=1)
nationalities_total = nationalities_total.sort_values(ascending=True)
nationalities.loc[nationalities_total.index].plot(kind='barh', stacked=True)

以下截图显示了国籍的堆叠条形图:

图 3.7 – 学术成功数据集中 ‘Nacionality’ 特征的分布,显示几乎所有行只有一个值

图 3.7 – 学术成功数据集中 ‘Nacionality’ 特征的分布,显示几乎所有行只有一个值

'Nacionality''International' 的分布意味着它们不是很有信息量(几乎所有行都有相同的值),因此我们可以从数据集中删除它们。

最后,我们注意到 'Gender' 特征。当处理性别信息时,总是好的做法检查是否存在偏见。我们可以使用直方图来可视化 'Gender' 特征相对于目标类别的分布。结果展示在随后的代码片段之后的截图里:

sns.countplot(data=df, x='Gender', hue='Target', hue_order=['Dropout', 'Enrolled', 'Graduate'])
plt.xticks(ticks=[0,1], labels=['Female','Male'])

图 3.8 – 学术成功数据集中 ‘Gender’ 特征的分布

图 3.8 – 学术成功数据集中 ‘Gender’ 特征的分布

对女性学生的轻微偏见,但不足以引起关注。

建模

现在我们可以为建模准备我们的数据集。我们必须将我们的 Target 值映射到整数,并删除 NacionalityInternational 特征。我们还需要删除特征名称中的空格。LightGBM 不能处理名称中的空格;我们可以用下划线替换它们:

df.columns = df.columns.str.strip().str.replace(' ', '_')
df = df.drop(columns=["Nacionality", "International"], axis=1)
df["Target"]=df["Target"].map({
    "Dropout":0,
    "Enrolled":1,
    "Graduate":2
})
X = df.drop(columns=["Target"], axis=1)
y = df["Target"]

我们训练并比较了四个模型:一个 LightGBM GBDT,一个 LightGBM DART 树,一个带有 GOSS 的 LightGBM DART 树,以及一个 scikit-learn 随机森林。

我们将使用 5 折交叉验证和GridSearchCV进行参数优化,以确保模型有良好的性能。

以下代码设置了 GBDT 的参数优化。其他模型遵循类似的模式,可以在源代码中看到:

def gbdt_parameter_optimization():
    params = {
        "max_depth": [-1, 32, 128],
        "n_estimators": [50, 100, 150],
        "min_child_samples": [10, 20, 30],
        "learning_rate": [0.001, 0.01, 0.1],
        "num_leaves": [32, 64, 128]
    }
    model = lgb.LGBMClassifier(force_row_wise=True, boosting_type="gbdt", verbose=-1)
    grid_search = GridSearchCV(estimator=model, param_grid=params, cv=5, verbose=10)
    grid_search.fit(X, y)
    return grid_search
results = gbdt_parameter_optimization()
print(results.best_params_)
print(results.best_score_)

运行前面的代码需要一些时间,但一旦完成,它将打印出找到的最佳参数以及最佳模型的分数。

在所有模型训练完成后,我们可以使用 F1 评分对每个模型进行评估,取 5 折交叉验证的平均值,使用找到的最佳参数。以下代码演示了如何对 GBDT 模型进行此操作:

model = lgb.LGBMClassifier(force_row_wise=True, boosting_type="gbdt", learning_rate=0.1, max_depth=-1, min_child_samples=10, n_estimators=100, num_leaves=32, verbose=-1)
scores = cross_val_score(model, X, y, scoring="f1_macro")
scores.mean()

[每个模型的参数优化 Jupyter 笔记本可在 GitHub 仓库中找到:github.com/PacktPublis…]

下表总结了找到的最佳参数值和每个模型的交叉验证 F1 分数:

模型学习率最大深度最小子样本数估计器数叶子数最小叶子样本数最小分割样本数F1 分数
GBDT0.1-1010032N/AN/A0.716
DART0.112830150128N/AN/A0.703
DART (GOSS)0.112830150128N/AN/A0.703
随机森林N/AN/AN/A150N/A10200.665

表 3.1 – 每个模型找到的最佳参数及其对应的 F1 分数

从表中我们可以看出,LightGBM 模型的表现远优于 scikit-learn 随机森林。两个 DART 模型实现了几乎相同的 F1 分数,GOSS 的 F1 分数略低(表中的值已四舍五入到三位数字)。

这就结束了我们探索数据集并为其构建优化模型(使用参数网格搜索)的端到端示例。在接下来的章节中,我们将查看更复杂的数据集,并深入分析模型性能。

摘要

本章介绍了 LightGBM 作为训练提升机的库。我们探讨了构建 GBDT 的复杂性的来源,以及 LightGBM 中解决这些问题的特性,例如基于直方图的采样、特征捆绑和 GOSS。我们还回顾了 LightGBM 最重要的超参数。

我们还详细概述了在 Python 中使用 LightGBM 的方法,包括 LightGBM Python API 和 scikit-learn API。然后我们使用 LightGBM 构建了第一个调整后的模型来预测学生的学术表现,利用交叉验证和基于网格搜索的参数优化。

在下一章中,我们比较 LightGBM 与另一个流行的梯度提升库 XGBoost 以及表格数据的 DL 技术。

参考文献

*[*1]G. Ke, Q. Meng, T. Finley, T. Wang, W. Chen, W. Ma, Q. Ye 和 T.-Y. Liu, “LightGBM: A Highly Efficient Gradient Boosting Decision Tree,” in Advances in Neural Information Processing Systems, 2017.
*[*2]M. Mehta, R. Agrawal 和 J. Rissanen, “SLIQ: A fast scalable classifier for data mining,” in Advances in Database Technology—EDBT’96: 5th International Conference on Extending Database Technology Avignon, France, March 25-29, 1996 Proceedings 5, 1996.
*[*3]J. Shafer, R. Agrawal, M. Mehta 和其他人, “SPRINT: A scalable parallel classifier for data mining,” in Vldb, 1996.
*[*4]S. Ranka 和 V. Singh, “CLOUDS: A decision tree classifier for large datasets,” in Proceedings of the 4th Knowledge Discovery and Data Mining Conference, 1998.
*[*5]H. Shi, “Best-first decision tree learning,” 2007.
*[*6]R. Kleiman 和 D. Page, “Aucμ: A performance metric for multi-class machine learning models,” in International Conference on Machine Learning, 2019.
*[*7]V. Realinho, J. Machado, L. Baptista 和 M. V. Martins, Predicting student dropout and academic success, Zenodo, 2021.

第四章:比较 LightGBM、XGBoost 和深度学习

上一章介绍了用于构建梯度提升决策树(GBDTs)的 LightGBM。在本章中,我们将 LightGBM 与两种用于建模表格数据的其他方法进行比较:XGBoost,另一个用于构建梯度提升树的库,以及深度神经网络(DNNs),一种最先进的机器学习技术。

我们在两个数据集上比较了 LightGBM、XGBoost 和 DNNs,重点关注复杂性、数据集准备、模型性能和训练时间。

本章面向高级读者,需要一些对深度学习的了解。然而,本章的主要目的不是详细了解 XGBoost 或 DNNs(这两种技术都不会在后续章节中使用)。相反,到本章结束时,你应该对 LightGBM 在机器学习领域中的竞争力有所了解。

主要内容包括:

  • XGBoost 概述

  • 深度学习和 TabTransformers

  • 比较 LightGBM、XGBoost 和 TabTransformers

技术要求

本章包括示例和代码片段,说明如何在 Python 中训练 LightGBM、XGBoost 和 TabTransformer 模型。完整的示例和设置本章所需环境的说明可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-4找到。

XGBoost 概述

XGBoost,即极端梯度提升,是一个广受欢迎的开源梯度提升库,其目标和功能与 LightGBM 类似。XGBoost 比 LightGBM 更早,由陈天奇开发,并于 2014 年首次发布*[1]*。

在其核心,XGBoost 实现了梯度提升决策树(GBDTs),并支持高效地构建它们。XGBoost 的一些主要特性如下:

  • 正则化:XGBoost 结合了 L1 和 L2 正则化,以避免过拟合

  • 稀疏性感知:XGBoost 高效地处理稀疏数据和缺失值,在训练过程中自动学习最佳插补策略

  • 并行化:该库采用并行和分布式计算技术同时训练多个树,显著减少了训练时间

  • 提前停止:如果模型性能没有显著提高,XGBoost 提供了一个选项来停止训练过程,从而提高性能并防止过拟合

  • 跨平台兼容性:XGBoost 支持多种编程语言,包括 Python、R、Java 和 Scala,使其能够服务于多样化的用户群体

多年来,由于 XGBoost 支持各种应用以及库的易用性和效率,它在机器学习社区中获得了广泛的认可。

比较 XGBoost 和 LightGBM

XGBoost 和 LightGBM 在功能上有很多重叠。两个库都实现了 GBDTs 和 DART,并支持构建随机森林。它们都有类似的避免过拟合、处理缺失值和自动处理稀疏数据的技术。

然而,XGBoost 和 LightGBM 之间的一些差异如下:

  • 树增长策略:XGBoost 采用按层级的树增长方法,即逐层构建树,而 LightGBM 使用按叶子的树增长策略,专注于通过选择具有最高 delta 损失的叶子来增长树。这种增长策略的差异通常使得 LightGBM 在大型数据集上运行更快。

  • 速度和可扩展性:LightGBM 在设计上更注重内存使用和计算效率,使其成为大规模数据集或训练时间至关重要的场景下的更好选择。然而,这种速度优势有时可能会以模型预测中更高的方差为代价。

  • 处理分类特征:LightGBM 内置了对分类特征的支持,这意味着它可以直接处理它们,而无需进行 one-hot 编码或其他预处理技术。另一方面,XGBoost 要求用户在将特征输入模型之前对分类特征进行预处理。

  • 早期停止:XGBoost 提供了一个选项,如果模型性能没有显著改进,则停止训练过程。LightGBM 没有内置此功能,尽管可以通过回调手动实现,如前几章所示。

总结来说,LightGBM 和 XGBoost 提供了类似的功能。LightGBM 在具有许多特征的大型数据集上表现更好,而 XGBoost 可能在小型或中型数据集上提供更稳定和准确的结果。

Python XGBoost 示例

XGBoost 提供了一个基于 scikit-learn 的接口来构建模型。以下示例展示了如何在 Forest Cover 数据集上使用 XGBoost:

from xgboost import XGBClassifier
...
dataset = datasets.fetch_covtype()
X_train, X_test, y_train, y_test = train_test_split(
    dataset.data, dataset.target, random_state=179
    )
y_train = y_train - 1
y_test = y_test – 1
xgb = XGBClassifier(
    n_estimators=150, max_leaves=120, learning_rate=0.09
    )
xgb = xgb.fit(X_train, y_train)
f1_score(y_test, xgb.predict(X_test), average="macro")

在这个阶段,scikit-learn 的接口应该对你来说已经很熟悉了。前面的代码显示 XGBoost 支持与训练基于 LightGBM 模型时使用的类似超参数。完整的参数列表可在xgboost.readthedocs.io/en/stable/parameter.xhtml找到。

XGBoost 作为 LightGBM 的直接替代品,是另一个梯度提升库。在下一节中,我们将探讨深度学习,这是一种完全不同但极其流行的学习技术,以及它与梯度提升在表格学习问题上的比较。

深度学习和 TabTransformers

我们现在来看一种使用深度学习解决基于表格的数据问题的方法。近年来,由于基于深度学习的模型性能出色,深度学习获得了巨大的普及。基于深度学习的技术,如 AlphaZero、Stable Diffusion 和 GPT 系列语言模型,在游戏、艺术生成和基于语言推理方面实现了人类或超人类的性能。

深度学习是什么?

深度学习是更广泛的机器学习领域中人工神经网络的一个子领域。人工神经网络是数学上模拟人脑的,由相互连接的节点层(或生物学术语中的“神经元”)组成,这些节点处理和传输信息。

简单的人工神经网络只包含几个层。深度学习中的“深度”一词指的是使用具有许多更多层的神经网络,每个层可能包含数千个神经元。这些层以层次结构组织,输入层在底部,输出层在顶部,隐藏层在中间。每个层在数据通过网络时提取和细化特征,使模型能够学习复杂的模式和表示。

下面的图展示了名为多层感知器的简单神经网络,它具有一个隐藏层。

图 4.1 – 具有一个隐藏层和输出层的多层感知器。层之间是完全连接的

图 4.1 – 具有一个隐藏层和输出层的多层感知器。层之间是完全连接的

每个神经元从其他神经元接收输入,执行数学运算,然后将结果传递给下一层的神经元。

数学运算涉及两个主要步骤 – 加权求和和激活函数:

  1. 加权求和: 神经元接收输入(输入数据或前一个神经元的输出),将每个输入与其对应的权重相乘,然后将它们相加。通常还会添加一个偏差项到加权和中,以更好地控制神经元的输出。从数学上讲,这可以表示如下:

    z j = ∑ i (w ij x i) + b j

    在这里,x i 代表神经元的所有输入,w ij 是与第 i 个输入相关的权重,b j 是神经元的偏差。

  2. 激活函数: 加权求和随后通过一个激活函数,确定神经元的输出。激活函数的目的是将非线性引入数学运算中。非线性使得神经网络能够模拟输入和输出之间的非线性关系,因此能够模拟复杂关系。存在各种激活函数,如sigmoid(对数函数)、双曲正切tanh)和修正线性单元ReLU),每个都有其自身的特性和用例。这可以表示为:

    a j = σ( z j)

    其中,a j 是神经元输出,σ 是激活函数。

结合这两个步骤,神经网络中的神经元处理输入数据,使网络能够学习和建模复杂模式。

神经网络通过调整与神经元相关的权重进行训练。算法可以概括如下:

  1. 权重被初始化为小的随机值。

  2. 执行前向传播:对于批次中的每个示例,将输入特征传递通过整个网络(在每个神经元计算总和和激活)以在输出层产生预测。

  3. 然后通过比较批次中每个示例的实际输出和输出来计算损失。像 GBDTs 一样,损失函数必须是可微分的,标准损失函数包括均方误差(MSE)和交叉熵损失。

  4. 反向传播被执行:使用微积分链式法则计算损失函数相对于权重的梯度。这个过程从输出层开始,反向通过网络进行。

  5. 然后使用梯度下降或其现代变体(如 Adam)根据反向传播的梯度更新权重。

  6. 该过程会重复进行一定数量的 epoch(每个 epoch 遍历整个数据集)以最小化损失函数。

神经网络的一个独特属性是,神经网络已被证明是通用函数逼近器。深度神经网络(DNNs)具有理论上的能力,在给定足够的隐藏神经元和适当的激活函数的情况下,可以将任何连续函数逼近到所需的精度水平。这一属性基于通用逼近定理,该定理已被证明适用于各种类型的神经网络。

这意味着神经网络可以学会表示输入和输出数据之间的复杂关系,无论这些关系多么复杂或非线性。这种能力是神经网络,尤其是 DNNs,成功解决不同领域广泛问题的一个原因。然而,这种保证是理论上的。在实践中,找到正确的网络架构、超参数和训练技术以实现所需的逼近水平可能具有挑战性。这个过程通常需要实验、专业知识和巨大的计算资源。

深度学习的优缺点

考虑到 DNNs 的能力,我们可能会认为它们应该是我们解决所有机器学习问题的首选。使用 DNNs 的主要优势在于它们在非常复杂的领域中的高精度:在广泛复杂任务、自然语言处理、生成式 AI、图像识别和语音识别等领域的当前最先进性能都是通过 DNNs 实现的,这得益于它们在大数据集中学习复杂和隐藏模式的能力。

另一个优点是自动特征提取。有了正确的架构,DNN 可以自动提取复杂或高阶特征,减轻了数据科学家进行特征工程的需求。

最后,深度神经网络(DNNs)还可以进行迁移学习:预先训练的深度学习模型可以在较小的数据集上进行微调,以完成特定任务,利用初始训练期间获得的知识。迁移学习可以显著减少新任务的训练时间和数据需求。

然而,深度学习并非解决所有机器学习问题的万能药。使用 DNNs 的一些缺点包括以下内容:

  • 计算资源:深度学习模型在训练时通常需要大量的计算能力和内存,尤其是在处理大型数据集和复杂架构时。

  • 大型数据集:DNNs 在大型数据集上进行训练时通常表现良好,但它们在小型数据集上的性能可能会下降。当数据集过小时,DNN 会对训练数据进行过度拟合,无法泛化到未见过的数据。

  • 可解释性:由于 DNNs 具有复杂的架构和大量参数,它们通常被视为“黑盒”。这种复杂性使得理解模型如何做出决策变得困难,这可能对需要透明度或合规性应用造成担忧。

  • 超参数调整:DNNs 涉及许多超参数,如网络架构、学习率和激活函数。结合较长的训练时间和资源需求,找到这些超参数的最佳组合可能既昂贵又耗时。

介绍 TabTransformers

我们希望将深度学习应用于表格数据,因为大多数实际机器学习问题都有表格数据。为此,我们使用一种新的深度学习架构,称为TabTransformer:一种专门设计用于处理表格数据的深度神经网络模型。

与 DNNs 的 GPT 系列类似,TabTransformer 基于 Vaswani 等人最初提出的 transformer 架构*[2]。TabTransformer 将 transformer 架构调整为有效地处理表格数据,为这类数据提供了其他机器学习模型(如决策树和梯度提升机)的替代方案[3]*。

图 4.2 – 在 Keras 中实现的 TabTransformer 架构[3]

图 4.2 – 在 Keras 中实现的 TabTransformer 架构[3]

TabTransformer 的模型架构如图图 4.2 所示。使用 TabTransformer,表格数据中的每个特征都被视为一个标记,类似于在自然语言处理中如何将单词视为标记。该模型应用自注意力机制来学习输入数据中特征之间的复杂交互和依赖关系。标记嵌入和注意力机制允许模型捕捉特征之间的全局和局部关系。

TabTransformer 模型有几个关键组件:标记嵌入、位置编码、Transformer 层、池化和输出层。标记嵌入将每个特征值转换为连续向量表示,并通过位置编码结合位置信息。

图 4**.2中展示的层中移动,我们可以看到分类特征和数值特征是分开的。

分类特征首先通过嵌入层进行处理,如 Keras 中的layers.Embedding实现,然后传递到 Transformer 块。可以实施可变数量的 Transformer 块(使用超参数设置),但每个块都包含一个layers.MultiHeadAttention层和一个带有Dropoutlayers.Dense层。输出值在通过注意力和密集层后相加并归一化。

由于它们的性能,Transformer 前馈层在我们的实现中使用了高斯误差线性单元GELU)激活函数。然而,也可能使用其他激活函数*[5]*。

数值特征通过一个归一化层(归一化数值输入范围)传递,然后与 Transformer 的输出连接。

连接的结果通过一个Dropout层。我们的实现使用缩放指数线性单元SELU),这导致激活自我归一化*[6]*。

最后,MLP 块的输出传递到损失函数,其实现取决于学习问题(分类或回归)。

TabTransformers 的实现和训练比梯度提升树复杂得多。与其他深度神经网络(DNNs)一样,TabTransformers 比梯度提升树需要更多的数据准备和计算能力。

除了 TabTransformers 之外,本节还介绍了深度学习及其优缺点。在下一节中,我们将通过一个实际例子比较不同的方法,包括与 TabTransformers 一起工作的复杂性。

比较 LightGBM、XGBoost 和 TabTransformers

在本节中,我们比较了 LightGBM、XGBoost 和 TabTransformers 在两个不同数据集上的性能。我们还探讨了针对不平衡类别、缺失值和分类数据的数据准备技术。

预测人口普查收入

我们使用的第一个数据集是人口普查收入数据集,该数据集根据教育、婚姻状况、职业等属性预测个人收入是否会超过$50,000*[4]*。该数据集有 48,842 个实例,正如我们将看到的,一些缺失值和不平衡的类别。

数据集可以从以下 URL 获取:archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data。数据已经被分为训练集和测试集。一旦加载,我们就可以采样数据:

train_data.sample(5)[["age", "education", "marital_status", "hours_per_week", "income_bracket"]]

选择列的数据样本在 表 4.1 中显示。

年龄教育程度婚姻状况每周工作小时数收入区间
1239034部分大学从未结婚40<=50K
2016941专科-学术已婚平民配偶45>50K
1713435博士从未结婚60>50K
2345249高中毕业已婚平民配偶40>50K
2237231高中毕业分居45<=50K

表 4.1 – 来自人口普查收入数据集的样本数据

表 4.1 显示我们拥有混合的数据类型:一些特征是数值型,而其他是文本型。值得注意的是,数据集中的一些列是分类特征:基于字符串的特征,具有一组固定的值。接下来,我们看看如何对这些特征进行编码以用于机器学习。

编码分类特征

大多数机器学习算法需要将基于字符串的特征编码为数字;在某些情况下,这可以自动完成。我们在第六章**,使用 LightGBM 解决现实世界的数据科学问题中讨论了 LightGBM 的自动编码。在这个例子中,我们编码特征以了解这意味着什么。

我们需要将每个分类值映射到一个唯一的数字;因此,我们首先为每个特征构建一个所有值的词汇表:

CATEGORICAL_FEATURES_WITH_VOCABULARY = {
    "workclass": sort_none_last(list(train_data["workclass"].unique())),
    "education": sort_none_last(list(train_data["education"].unique())),
    "marital_status": sort_none_last(list(train_data["marital_status"].unique())),
    "occupation": sort_none_last(list(train_data["occupation"].unique())),
    "relationship": sort_none_last(list(train_data["relationship"].unique())),
    "race": sort_none_last(list(train_data["race"].unique())),
    "gender": sort_none_last(list(train_data["gender"].unique())),
    "native_country": sort_none_last(list(train_data["native_country"].unique())),
    "income_bracket": sort_none_last(list(train_data["income_bracket"].unique())),
}

以下代码提取了每列的唯一值到一个列表中,并对列表进行了排序,将null值放在最后。当使用 pandas DataFrame 时,明确地为分类列设置数据类型为category也是很有用的:

for c in CATEGORICAL_FEATURES_WITH_VOCABULARY.keys():
    for dataset in [train_data, test_data]:
        dataset[c] = dataset[c].astype('category')
        dataset[c] = dataset[c].astype('category')

使用我们的词汇表,我们现在可以更新每列的值,用表示其类别的数字来表示(使用词汇表列表中的索引作为数值):

def map_to_index(val, vocab):
    if val is None:
        return None
    return vocab.index(val)
for dataset in (train_data, test_data):
    for feature, vocab in CATEGORICAL_FEATURES_WITH_VOCABULARY.items():
        dataset[feature] = dataset[feature].map(lambda val: map_to_index(val, vocab))

结果是一个 DataFrame,其中所有特征现在都是数值型的:

年龄教育程度婚姻状况每周工作小时数收入区间
1854537112400
2611051140601
2190536115320
14963210431
314847152400

表 4.2 – 来自人口普查收入数据集的编码分类数据

我们现在已将分类特征编码,可以继续进行进一步的数据清理。

缺失值和重复值

我们需要检查缺失值、重复值和异常值。我们可以使用以下代码:

train_data.isnull().sum()
train_data.drop_duplicates(inplace=True)
train_data.describe()

我们删除了重复数据,并查看describe的输出显示没有显著的异常值。然而,数据集中存在缺失值。LightGBM 和 XGBoost 可以自动处理缺失值,这是基于树的算法的一个显著优势。然而,对于 TabTransformers,我们需要实现特定的逻辑来处理缺失值,正如我们接下来将要看到的。

数据不平衡

该数据集也存在偏差:每个类别的示例数量不平衡。我们可以使用以下代码计算偏差:

counts = np.bincount(train_data["income_bracket"])
class_weight = {
    0: counts[0] / train_data.shape[0],
    1: counts[1] / train_data.shape[0]
}

输出显示大约 75%/25%的偏斜倾向负类(0)。处理不平衡类(如果我们有二元类)的最简单方法之一是比负类更加强调正类。因此,在计算损失时,错过正类的预测有更大的影响。

LightGBM 和 XGBoost 都通过scale_pos_weight参数支持这一点,其计算方法如下:

scale_pos_weight = class_weight[0]/class_weight[1]

训练 LightGBM 和 XGBoost 模型

数据清理和准备就绪后,我们现在可以训练我们的模型。训练 LightGBM 和 XGBoost 模型很简单。对于 LightGBM,我们有以下内容:

model = lgb.LGBMClassifier(force_row_wise=True, boosting_type="gbdt", scale_pos_weight=scale_pos_weight)
model = model.fit(X_train, y_train)

对于 XGBoost,我们可以运行以下代码:

model = xgb.XGBClassifier(scale_pos_weight=scale_pos_weight)
model = model.fit(X_train, y_train)

上述代码突出了使用这两个库的简单性。

训练 TabTransformer 模型

现在,我们将构建一个 TabTransformer 模型。我们将使用TensorFlow 的 Keras根据示例代码定义模型:keras.io/examples/structured_data/tabtransformer/

我们的数据集准备保持基本相同,有两个关键区别:我们不编码分类特征,并且必须显式处理缺失值。

我们不编码分类特征,因为 Keras 提供了一个特殊的层来执行字符串查找并将其转换为数值。然而,我们仍然必须提供词汇表。以下代码说明了创建查找层的示例:

lookup = layers.StringLookup(
                vocabulary=vocabulary,
                mask_token=None,
                num_oov_indices=0,
                output_mode="int",
            )

num_oov_indices参数设置为0,这意味着如果没有设置mask_token参数为None,则不使用任何索引,因为我们没有对任何字符串输入进行掩码。

我们需要为数据集中的每一列提供一个默认值来处理缺失值。我们的策略是用默认字符串值NA替换字符串值,对于数值列,我们使用统计平均值来填充缺失值。以下代码创建了一个默认值列表:

train_data_description = train_data.describe()
COLUMN_DEFAULTS = [
    train_data_description[feature_name]["mean"] if feature_name in NUMERIC_FEATURE_NAMES else ["NA"]
    for feature_name in HEADERS
]

实现 TabTransformer 模型的 Keras 代码大约有 100 行长,可在我们的 GitHub 仓库中找到:github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/blob/main/chapter-4/tabtransformer-census-income.ipynb

以下代码设置了我们可以用于 TabTransformer 模型的梯度优化器和数据:

optimizer = tfa.optimizers.AdamW(
    learning_rate=learning_rate,
    weight_decay=weight_decay
)
model.compile(
    optimizer=optimizer,
    loss=keras.losses.BinaryCrossentropy(),
    metrics=[keras.metrics.BinaryAccuracy(name="accuracy"),
             f1_metric,
             precision_metric,
             recall_metric],
)
train_dataset = get_dataset_from_csv(
    train_data_file, batch_size, shuffle=True
)
validation_dataset = get_dataset_from_csv(
    test_data_file, batch_size
)

我们使用带有权重衰减*[7]*的AdamW优化器以及二元交叉熵损失函数来拟合二元分类问题。然后我们可以使用以下代码来训练和评估我们的模型:

callback = keras.callbacks.EarlyStopping(
    monitor='loss', patience=3
)
history = model.fit(
    train_dataset,
    epochs=num_epochs,
    validation_data=validation_dataset,
    class_weight=class_weight,
    callbacks=[callback]
)
model.evaluate(validation_dataset, verbose=0)

我们还通过 Keras 回调添加了提前停止,耐心为3个 epoch。在训练和验证过程中,我们跟踪准确率和 F1 分数。

训练时间显著长于任何梯度提升框架,并且需要 GPU(在 CPU 上训练在技术上可行,但需要不寻常的时间)。

我们现在可以查看三种算法在人口普查收入数据集上的结果。

结果

使用前一章中讨论的网格搜索技术对所有三种算法进行了参数优化。对于两种提升算法,优化了学习率、bin 大小和树的数量。对于 TabTransformer,必须优化参数和架构的各个方面。在参数方面,优化了学习率、权重衰减和 dropout 率,而在架构方面,必须选择 transformer 块的数量和隐藏层(在 MLP 中)的数量。优化后的参数可在源代码中找到。

下表显示了算法的验证集结果。

模型训练时间准确率F1 分数
LightGBM GBDT1.05s84.46%0.71
XGBoost GBDT5.5s84.44%0.72
TabTransformer113.63s77.00%0.64

表 4.3 – 在人口普查收入数据集上训练三种模型的结果

XGBoost 和 LightGBM 在数据集上的表现相似,达到了 84% 的准确率和 0.7 的 F1 分数。TabTransformer 模型的表现较差,准确率和 F1 分数都较低。

关于训练时间,LightGBM 比其他方法快得多。LightGBM 模型的训练速度比 XGBoost 快 5.23 倍,比 TabTransformer 快 108.22 倍。TabTransformer 在一个 8 核 P4000 GPU 上训练了 15 个 epoch。

为了进行另一个比较点,并说明当没有分类特征时 TabTransformer 架构如何进行适配,我们使用三种算法解决第二个问题。

检测信用卡欺诈

我们的第二个任务是检测信用卡交易数据集中的欺诈交易*[8]*。数据集可在 www.kaggle.com/datasets/mlg-ulb/creditcardfraud 获取。该任务是一个二元分类问题,训练数据中的交易被标记为非欺诈(0)和欺诈(1)。数据集仅包含已匿名化的数值特征,以保护机密性。值得注意的是,数据集高度不平衡,欺诈交易仅占数据的 0.17%。

训练 LightGBM 和 XGBoost 模型

由于所有值都是数值型,对于梯度提升模型,所需的数据准备非常少。为了抵消数据集中的不平衡,我们再次计算scale_pos_weight并将其作为参数传递给模型。我们对 LightGBM 和 XGBoost 模型进行网格搜索和交叉验证,以找到良好的超参数。对于 LightGBM,尝试了 DART 和 GBDT 模型,DART 表现更好。与人口普查收入数据集不同,信用卡数据集没有预先分成训练集和测试集。因此,我们应用五折交叉验证来衡量未见数据的表现。以下代码训练了 LightGBM 模型,XGBoost 的代码非常相似:

model = lgb.LGBMClassifier(force_row_wise=True, boosting_type="dart", learning_rate=0.0023, max_bin=384, n_estimators=300, scale_pos_weight=scale_pos_weight, verbose=-1)
scores = cross_val_score(model, X, y, scoring="f1_macro")
print(f"Mean F1-score: {scores.mean()}")

LightGBM 和 XGBoost 的结果以及 TabTransformer 的结果都显示在表 4.4中。

训练 TabTransformer 模型

没有分类特征时,TabTransformer 架构可以显著简化。让我们看看图 4.2 所示的架构。我们可以看到,嵌入层和注意力层不再需要。实际上,模型简化为常规的多层感知器(MLP)(由于根本不使用注意力层,所以仍然称模型为 transformer 是不诚实的)。

除了移除不必要的层之外,架构和过程的其他部分与人口普查收入问题相同。再次使用AdamW作为优化器,我们对模型的超参数和隐藏层数量进行网格搜索优化。与梯度提升模型一样,执行五折交叉验证来衡量性能。

结果

虽然接下来也会报告准确率,但重要的是要注意,在数据不平衡的情况下,它不是一个好的性能指标。在数据集中,99.82%的样本属于一个类别,一个只预测该类别的模型将具有 99.82%的准确率,并且完全无意义。F1 分数不受类别不平衡的影响,在数据不平衡的数据集中仍然是分类性能的好指标。以下表格显示了所有三个算法的五折交叉验证结果。

模型训练时间准确率F1 分数
LightGBM GBDT113 秒99.88%0.80
XGBoost GBDT351 秒98.41%0.82
TabTransformer528.59 秒93.37%0.05

表 4.4 – 在信用卡欺诈数据集上训练三个模型的结果。训练时间包括五折交叉验证

XGBoost 和 LightGBM 在数据集上的表现非常相似,分别获得 F1 分数为 0.82 和 0.80。DNN 在处理这个问题上表现显著不佳,即使使用类权重来补偿不平衡的数据集,F1 分数也只有 0.05。

在深度神经网络(DNN)中调试性能问题非常棘手。由于构建和训练 DNN 模型复杂性和不透明性,微小的变化都可能产生重大影响。

可能导致性能不佳的原因包括以下几方面:

  • 模型架构不足:这是最可能的原因。架构不适合该问题。需要进一步实验以改进架构、层的大小,甚至神经网络的类型。

  • 训练不足:模型可能训练时间不够长。增加训练轮数可以提高性能。然而,在我们的实验中,损失在 10 个轮次后停滞(尽管训练继续到 15 轮)。

  • BinaryCrossentropy 损失函数与类别权重。然而,可以尝试更高级的损失函数,例如焦点损失,[9]

在训练和验证时间方面,与人口普查收入数据集的情况类似。LightGBM 模型训练和验证的速度显著快于其他方法:比 XGBoost 快 3.1 倍,比 DNN 快 4.62 倍。

摘要

在本章中,我们讨论了两种可能用于解决表格学习问题的额外算法:XGBoost,另一个梯度提升框架,以及 TabTransformer,一种深度学习方法。

我们展示了如何在两个数据集上设置和训练 XGBoost 模型和 TabTransformer,以及如何为基于树和神经网络的模型编码分类特征。这两个数据集也具有不平衡的类别,我们在训练过程中必须对此进行补偿。

我们发现 LightGBM 和 XGBoost 产生了类似准确度的模型,但 LightGBM 训练模型的速度更快,效率更高。我们还看到了训练深度神经网络(DNN)的复杂性以及在这些问题上的表现不佳。深度学习是一种极其强大的技术,但在处理表格数据集时,基于树的算法通常更适用。

在下一章中,我们将重点介绍使用名为Optuna的框架进行更有效的 LightGBM 参数优化。

参考文献

*[*1]陈 T. 和 Guestrin C., “XGBoost,” 在第 22 届 ACM SIGKDD 国际知识发现和数据挖掘会议论文集中,2016.
*[*2]瓦斯瓦尼 A., 沙泽尔 N., 帕尔玛 N., 乌斯克雷特 J., 琼斯 L., 戈麦斯 A. N., 凯撒 L. 和 波罗斯库欣 I., 注意力即一切,2017.
*[*3]黄 X., 克坦 A., Cvitkovic M., 和 Karnin Z., TabTransformer:使用上下文嵌入进行表格数据建模,2020.
*[*4]贝克尔 R., Adult,UCI 机器学习库,1996.
*[*5]亨德里斯克 D. 和 吉姆佩尔 K., 高斯误差线性单元(GELUs),2020.
*[*6]克拉姆鲍尔 G., 优特纳 T., 梅尔 A. 和 高切尔瑞特 S., 自归一化神经网络,2017.
*[*7]洛什奇洛夫 I. 和 胡特 R., 解耦权重衰减正则化,2019.
*[*8]达洛佐洛 A., 凯伦 C., 约翰逊 R. 和 博特姆皮 G., “使用欠采样校准不平衡分类的概率,” 2015.
*[*9]T.-Y. 林,P. 戈亚尔,R. 吉里斯,K. 何和 P. 多拉尔,密集目标检测的 Focal Loss,2018.

第二部分:使用 LightGBM 的实用机器学习

第二部分深入探讨了支撑实用机器学习工程的复杂过程,从通过名为Optuna的框架进行高效的超参数优化开始。然后,我们将过渡到对数据科学生命周期的全面探索,展示了从问题定义和数据处理到实际数据科学建模应用的严谨步骤。本部分的结尾,焦点将转向自动化机器学习,重点关注 FLAML 库,该库旨在简化并简化模型选择和调整。在整个部分中,结合案例研究和实际操作示例,将提供一条清晰的路线图,以充分利用这些高级工具的潜力,强调效率和优化的主题。

本部分将包括以下章节:

  • 第五章*,使用 Optuna 进行 LightGBM 参数优化*

  • 第六章*,使用 LightGBM 解决现实世界的数据科学问题*

  • 第七章*,使用 LightGBM 和 FLAML 进行 AutoML*

第五章:使用 Optuna 进行 LightGBM 参数优化

前几章讨论了 LightGBM 的超参数及其对构建模型的影响。构建新模型时的一个基本问题是找到最佳超参数以实现最佳性能。

本章重点介绍使用名为 Optuna 的框架进行参数优化过程。讨论了不同的优化算法以及超参数空间的剪枝。一个实际示例展示了如何将 Optuna 应用于寻找 LightGBM 的最佳参数。还展示了 Optuna 的高级用例。

本章的主要内容包括以下内容:

  • Optuna 和优化算法

  • 使用 Optuna 优化 LightGBM

技术要求

本章包括示例和代码片段,说明如何使用 Optuna 对 LightGBM 进行参数优化研究。完整的示例和设置本章所需环境的说明可在 github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-5 获取。

Optuna 和优化算法

前几章的示例表明,为问题选择最佳超参数对于解决机器学习问题至关重要。超参数对算法的性能和泛化能力有显著影响。最佳参数也特定于所使用的模型和要解决的问题。

其他复杂超参数优化的因素如下:

  • 成本:对于每一组独特的超参数集合(可能有很多),必须执行整个训练过程,通常包括交叉验证。这非常耗时且计算成本高昂。

  • 高维搜索空间:每个参数可以有一个广泛的潜在值范围,使得测试每个值变得不可能。

  • 参数交互:单独优化每个参数通常是不可能的,因为某些参数的值会与其他参数的值相互作用。一个很好的例子是 LightGBM 中的学习率和估计器的数量:更少的估计器需要更大的学习率,反之亦然。这种现象在 图 5.1 中显示。

图 5.1 – 一个并行坐标图,显示了学习率和估计器数量之间的参数交互:拥有更多估计器需要更低的 learning rate,反之亦然

图 5.1 – 一个并行坐标图,显示了学习率和估计器数量之间的参数交互:拥有更多估计器需要更低的 learning rate,反之亦然

图 5.1使用一种称为平行坐标图的技术来可视化参数交互。平行坐标图是一种用于表示高维数据的可视化工具,因此对于可视化超参数优化的结果特别有用。每个维度(在此上下文中,指超参数)被描绘为一条垂直轴,平行排列。每个轴的范围反映了超参数可以假设的值的范围。每个超参数的配置都被描绘为一条穿过所有这些轴的线,每个轴上的交点表示给定配置中该超参数的值。线条还可以根据性能指标,如验证准确率,进行着色编码,以区分哪些超参数组合产生更好的结果。

平行坐标图之美在于它们能够展示多个超参数之间的关系及其对性能的累积影响,例如图 5.1中所示的超参数交互。观察线条的聚类或颜色相似性,我们可以了解超参数之间的趋势和复杂的相互依赖关系。这种可视化多维模式的能力有助于数据科学家确定哪些超参数值或组合最有利于模型性能的最优化。

由于超参数优化的挑战和复杂性,寻找最佳参数的直观方法是手动优化。在手动优化中,人类从业者根据直观理解和经验选择参数。使用这些参数训练模型,然后重复此过程,直到找到令人满意的参数。手动优化易于实现,但由于过程中涉及人类,因此非常耗时。人类的直觉也可能出错,并且很容易错过好的参数组合。

注意

寻找最佳参数的过程通常被称为参数研究。研究中测试的每个配置(参数组合)被称为试验

在前几章的示例中,我们迄今为止使用的方法是网格搜索。使用网格搜索,我们设置一个参数网格,包括每个参数和潜在值的范围,并彻底测试每个可能的组合以找到最佳值。

网格搜索很好地解决了参数交互问题:由于每个可能的组合都进行了测试,每个交互都被考虑在内。

然而,使用网格搜索的缺点是成本。由于我们彻底测试了每个参数组合,试验次数迅速变得难以承受,尤其是在添加更多参数的情况下。例如,考虑以下网格:

params = {"learning_rate": [0.001, 0.01, 0.1],
          "num_leaves": [10, 20, 50, 100],
          "num_estimators": [100, 200, 500]}

对此网格进行的优化研究需要 36 次试验。仅添加一个具有两个可能值的额外参数就会将研究成本翻倍。

需要的是一个算法和框架,能够在有限的试验次数内智能地优化我们控制的参数。为此目的,存在几个框架,包括用于调整机器学习模型的 Python 库 SHERPA;另一个用于在复杂搜索空间中进行参数优化的 Python 库 Hyperopt;以及专门针对 Keras 的工具 Talos。然而,在下一节以及本章的其余部分,我们将探讨Optuna,这是一个旨在自动化调整机器学习模型的框架。

介绍 Optuna

Optuna 是一个开源的超参数优化HPO)框架,旨在自动化寻找机器学习模型的最佳超参数 (optuna.org/)。它用 Python 编写,可以轻松集成到各种机器学习库中,包括 LightGBM。

Optuna 提供了高效的优化算法,以更有效地搜索超参数空间。除了优化算法之外,Optuna 还提供了剪枝策略,通过剪枝表现不佳的试验来节省计算资源和时间。

除了优化和剪枝算法之外,Optuna 还提供了一个易于使用的 API,用于定义参数类型(整数、浮点或分类),创建和自动化可恢复的优化研究,以及可视化优化运行的结果。在本章的后面部分,我们将看到如何实际使用该 API。

优化算法

Optuna 提供了几个高效的优化算法。在本节中,我们重点关注两种可用的算法:树结构帕累托估计器TPE)和协方差矩阵自适应进化策略CMA-ES)算法。

TPE

要理解 TPE,我们首先必须知道什么是帕累托估计器。

帕累托估计器,或核密度估计器KDE),是一种用于估计一组数据点概率分布的技术。它是一种非参数方法,这意味着它不假设数据有任何特定的潜在分布。相反,它试图根据观察到的数据点“学习”分布。

假设你有一组数据点,并想知道数据是如何分布的。一种方法是在每个数据点上放置小的“山丘”(核函数)。这些“山丘”可以有不同的形状,例如高斯(钟形)或均匀(箱形)。任何点的“山丘”高度代表新数据点落在该位置的可能性。帕累托估计器通过将这些“山丘”相加来创建一个平滑的地形,代表数据的估计概率分布。

在 TPE 的情况下,我们关心的数据点是参数组合,概率分布是一组参数被认为是“好”或“坏”的可能性 [1],[2]。

TPE 首先采样一些随机组合的超参数,并评估每个组合的模型性能。基于这些初步结果,TPE 将超参数组合分为两组:良好(那些导致更好性能的组合)和不良(那些导致更差性能的组合):

  • l(x):良好配置的概率密度函数

  • g(x) : 不良配置的概率密度函数

TPE 随后使用 Parzen 估计技术估计良好不良两组超参数组合的概率分布。

在概率分布估计可用的情况下,TPE 计算超参数配置的期望改进EI)。EI 可以计算为两个密度之间的比率:l(x) _ g(x)。每次试验,算法都会采样新的超参数配置,以最大化 EI。

TPE 中的树结构来源于算法在超参数搜索空间内处理参数交互的能力,其中特定超参数的相关性取决于其他超参数的值。为了处理这种情况,TPE 构建了一个层次结构,捕捉不同超参数之间的关系,并相应地调整采样过程。

总结来说,TPE 估计良好不良参数的分布,并利用它们通过最大化新试验的期望改进来寻找最佳参数。由于 TPE 可以近似分布并以最优方式(非穷举方式)搜索更好的参数,因此它具有成本效益。TPE 还可以处理参数交互。

Optuna 提供的另一种算法是 CMA-ES 算法,我们将在下文中讨论。

CMA-ES

CMA-ES 是另一种可以用来寻找最佳超参数的优化算法[3]。与 TPE 相比,CMA-ES 非常适合涉及连续变量以及搜索空间非线性且非凸的情况。

CMA-ES 是进化算法EA)的一个例子。EA 是一种受自然进化过程启发的优化算法。它通过模拟自然界通过选择、繁殖、突变和遗传进化物种的方式,旨在找到问题的最佳解决方案。进化算法从候选解决方案的种群开始,并在每一代中修改候选方案以更接近最佳解决方案。这种代际过程在图 5.2中展示。

图 5.2 – 候选解决方案(红色 x 标记)随每一代进化以近似全局最优(位于每个景观的顶部和中心)。在 CMA-ES 的上下文中,每个候选解决方案代表一组超参数值,算法的性能决定了最优解

图 5.2 – 一个二维图示,展示了候选解(红色 x 标记)随着每一代后续演变,以逼近全局最优解(位于每个景观的顶部和中心)。在 CMA-ES 的上下文中,每个候选解代表一组超参数值的组合,算法的性能决定了最优解。

CMA-ES 的进化过程的核心是协方差矩阵。协方差矩阵是一个表示变量对(在 CMA-ES 的情况下,是超参数)之间协方差的正方形、对称矩阵,提供了它们之间关系的洞察。矩阵的对角元素代表单个变量的方差,而矩阵的非对角元素代表变量对的协方差。当存在正协方差时,它表示变量通常在同一方向上移动,要么增加要么减少。相反,负协方差指向一种关系,其中一个变量上升时,另一个变量倾向于下降,反之亦然。协方差为零表示变量之间没有线性关系。

当优化超参数时,CMA-ES 应用以下进化原则:

  1. 在超参数搜索空间内,初始化平均值和协方差矩阵。

  2. 重复进化过程:

    1. 使用平均值和协方差矩阵从搜索空间生成候选解。每个候选解代表一组超参数值的组合。

    2. 评估候选解的适应度。适应度指的是候选解的质量或它解决优化问题的程度。在 CMA-ES 中,这意味着使用候选超参数在数据集上训练模型,并在验证集上评估性能。

    3. 从种群中选择最佳候选解。

    4. 从最佳候选解更新平均值和协方差矩阵。

    5. 重复进行试验,直到达到最大试验次数或种群适应度不再提高。

CMA-ES 在复杂搜索空间中表现良好,并智能地采样搜索空间,由协方差矩阵引导。当超参数搜索空间复杂且非线性,或者验证数据的评估有噪声(例如,当指标是一个不一致的性能指标)时,这很有益。

TPE 和 CMA-ES 都解决了超参数优化相关的问题:两种算法都有效地搜索高维搜索空间。两种算法都捕捉参数交互。两种算法都让我们对成本有了控制:我们可以决定我们的优化预算,并将搜索限制在那个范围内。

TPE 和 CMA-ES 之间的主要区别在于它们整体的方法。TPE 是一个具有顺序搜索策略的概率模型,与基于群体的 CMA-ES 相比,CMA-ES 会并行评估解决方案。这通常意味着 TPE 在搜索中更具探索性,而 CMA-ES 通过群体控制机制平衡探索和利用。然而,TPE 通常比 CMA-ES 更有效率,尤其是在参数数量较少的情况下。

Optuna 为剪枝无效试验的搜索过程提供了进一步的优化。接下来,我们将讨论一些剪枝策略。

剪枝策略

剪枝策略是指通过剪枝这些试验来避免在无望的试验上浪费优化时间的方法。剪枝与模型训练过程同步发生:在训练过程中检查验证误差,如果算法表现不佳,则停止训练。这样,剪枝类似于早期停止

中值剪枝

Optuna 提供了多种剪枝策略,其中最简单的一种是中值剪枝。在中值剪枝中,每个试验在 n 步之后报告一个中间结果。然后取中间结果的平均值,并停止任何在相同步骤中低于先前试验中值的结果。

连续减半和 Hyperband

一种更复杂的策略被称为连续减半[4]。它采取了一种更全局的方法,并将相同的小预算的训练步骤分配给所有试验。连续减半然后迭代进行:在每个迭代中,评估每个试验的性能,并选择候选者中的上半部分进入下一轮,而下半部分被剪枝。下一轮的训练预算加倍,然后重复此过程。这样,优化预算被花在最有希望的候选者上。因此,一小部分优化预算被用于消除表现不佳的候选者,而更多的资源被用于寻找最佳参数。

Hyperband 是另一种剪枝技术,它通过结合随机搜索和多括号资源分配策略来扩展连续减半[5]。虽然连续减半通过迭代剪枝表现不佳的候选配置并分配更多资源给剩余的有希望的配置,从而有效地缩小候选配置集,但它依赖于一个固定的初始配置集和单一的资源分配方案。

Hyperband 而是使用多区间资源分配策略,将总计算预算分成几个区间,每个区间代表不同级别的资源分配。在每个区间内,应用连续减半来迭代消除表现不佳的配置,并将更多资源分配给剩余的有希望的配置。在每个区间的开始,使用随机搜索采样一个新的超参数配置集,这允许 Hyperband 更广泛地探索超参数空间并降低错过良好配置的风险。这种并发过程使 Hyperband 能够在搜索过程中自适应地平衡探索和利用,最终导致更高效和有效的超参数调整。

Optuna 对优化算法及其相应的剪枝策略进行了实证研究github.com/optuna/optuna/wiki/Benchmarks-with-Kurobako)。实证研究表明,Hyperband 是最佳的 TPE 或 CMA-ES 优化策略

本节概述了 Optuna 所使用的理论和算法,重点关注 TPE、CMA-ES 和高级剪枝策略。在下一节中,我们将实际应用 Optuna 到一个与 LightGBM 相关的机器学习问题上。

使用 Optuna 优化 LightGBM

我们将使用分类示例来演示如何应用 Optuna。我们将要建模的问题是为电信提供商预测客户流失(/)。数据集可以从github.com/IBM/telco-customer-churn-on-icp4d/tree/master/data获取。数据描述了每个客户使用提供商可用的数据——例如,性别、客户是否支付互联网服务费、是否有无纸化账单、是否支付技术支持费以及他们的月度费用。数据包括数值和分类特征。数据已经过清洗且平衡,这使我们能够专注于参数优化研究。

我们首先定义参数研究的目标。objective 函数为每个试验调用一次。在这种情况下,我们希望在数据上训练一个 LightGBM 模型并计算 F1 分数。Optuna 将一个 trial 对象传递给 objective 函数,我们可以使用它来设置特定试验的参数。以下是一个示例代码片段,展示了如何定义带有参数的 objective 函数:

def objective(trial):
        boosting_type = trial.suggest_categorical(
            "boosting_type", ["dart", "gbdt"])
        lambda_l1= trial.suggest_float(
            'lambda_l1', 1e-8, 10.0, log=True),
...
        min_child_samples= trial.suggest_int(
            'min_child_samples', 5, 100),
        learning_rate = trial.suggest_float(
            "learning_rate", 0.0001, 0.5, log=True),
        max_bin = trial.suggest_int(
            "max_bin", 128, 512, 32)
        n_estimators =  trial.suggest_int(
            "n_estimators", 40, 400, 20)

在这里,我们可以看到我们如何使用 trial 提供的方法来设置超参数。对于每个参数,优化算法在指定的范围内建议一个值。我们可以使用 trial.suggest_categorical 建议分类变量(如 boosting 类型所示),并分别使用 suggest_intsuggest_float 建议整数和浮点参数。在建议浮点数或整数时,指定一个范围,可选地还可以指定步长:

n_estimators =  trial.suggest_int(
            name="n_estimators", low=40, high=400, step=20)

设置步长意味着优化算法不会在范围内建议任何任意值,而是将建议限制在上下限之间的步长(40, 60, 80, 100, …, 400)。

我们还可以选择通过为数值参数传递 log=True 来对可能值的范围进行对数缩放。对数缩放参数范围的效果是,在范围的下限附近测试更多的值,而在上限附近(对数地)测试较少的值。对数缩放特别适合学习率,因为我们希望关注较小的值,并通过指数增加测试值直到上限。

要在训练 LightGBM 模型时应用剪枝,Optuna 提供了一个专门定制的回调函数,该回调函数与优化过程集成:

pruning_callback = optuna.integration.LightGBMPruningCallback(trial, "binary")

在创建回调函数时,我们必须指定一个错误度量标准,在我们的情况下,我们为二进制错误指定 "binary"

在设置好超参数后,我们可以像平常一样拟合,传递参数和回调函数:

model = lgb.LGBMClassifier(
    force_row_wise=True,
    boosting_type=boosting_type,
    n_estimators=n_estimators,
    lambda_l1=lambda_l1,
    lambda_l2=lambda_l2,
    num_leaves=num_leaves,
    feature_fraction=feature_fraction,
    bagging_fraction=bagging_fraction,
    bagging_freq=bagging_freq,
    min_child_samples=min_child_samples,
    learning_rate=learning_rate,
    max_bin=max_bin,
    callbacks=[pruning_callback],
    verbose=-1)
scores = cross_val_score(model, X, y, scoring="f1_macro")
return scores.mean()

我们使用五折交叉验证和 F1 宏分数作为评分标准来训练模型。最后,objective 函数返回 F1 分数的平均值作为试验评估。

我们已经准备好使用定义的 objective 函数开始一个优化研究。我们创建一个采样器、剪枝器和研究本身,然后调用 optimize 并传递我们的 objective 函数:

sampler = optuna.samplers.TPESampler()
pruner = optuna.pruners.HyperbandPruner(
    min_resource=10, max_resource=400, reduction_factor=3)
study = optuna.create_study(
    direction='maximize', sampler=sampler,
    pruner=pruner
)
study.optimize(objective(), n_trials=100, gc_after_trial=True, n_jobs=-1)

我们使用 TPE 优化算法作为采样器,与 Hyperband 剪枝一起使用。Hyperband 剪枝器指定的最小和最大资源控制了每个试验训练的最小和最大迭代次数(或估计器)。在应用剪枝时,缩减因子控制每个减半回合中提升的试验数量。

通过指定优化方向(maximizeminimize)来创建研究。在这里,我们正在优化 F1 分数,因此我们希望最大化这个值。

我们随后调用 study.optimize 并设置我们的优化预算:n_trials=100。我们还执行了一个内存优化设置,gc_after_trial=True。执行 n_jobs=-1 将并行运行与 CPU 核心数量相同的试验。

在运行优化后,我们可以通过调用以下代码来获取最佳试验和参数:

print(study.best_trial)

上述示例展示了如何有效地将 Optuna 应用于寻找 LightGBM 超参数。接下来,我们将探讨 Optuna 框架的一些高级特性。

高级 Optuna 特性

当优化大型机器学习问题的超参数时,优化过程可能需要持续数天或数周。在这些情况下,保存优化研究并在以后恢复它有助于防止数据丢失或将研究迁移到不同的机器之间。

保存和恢复优化研究

Optuna 支持两种方式来保存和恢复优化研究:内存中和使用远程数据库(RDB)。

当在内存中运行研究时,可以应用标准的 Python 序列化对象的方法。例如,可以使用joblibpickle。我们使用joblib来保存研究:

joblib.dump(study, "lgbm-optuna-study.pkl")

为了恢复和继续研究,我们需要反序列化study对象并继续优化:

study = joblib.load("lgbm-optuna-study.pkl")
study.optimize(objective(), n_trials=20, gc_after_trial=True, n_jobs=-1)

运行研究在内存中的替代方法是使用关系数据库。当使用关系数据库时,研究的中间(试验)和最终结果将持久化在 SQL 数据库后端。RDB 可以托管在单独的机器上。可以使用 SQLAlchemy 支持的任何 SQL 数据库(docs.sqlalchemy.org/en/20/core/…

在我们的示例中,我们使用 SQLite 数据库作为关系数据库(RDB):

study_name = "lgbm-tpe-rdb-study"
storage_name = f"sqlite:///{study_name}.db"
study = optuna.create_study(
    study_name=study_name,
    storage=storage_name,
    load_if_exists=False,
    sampler=sampler,
    pruner=pruner)

Optuna 管理着与关系数据库(RDB)的连接和结果的持久化。在设置连接后,优化可以像往常一样进行。

从 RDB 后端恢复研究很简单;我们指定相同的storage并将load_if_exists设置为True

study = optuna.create_study(study_name=study_name, storage=storage_name, load_if_exists=True)

理解参数影响

在许多情况下,在解决特定问题时更好地理解超参数的影响也是很有价值的。例如,n_estimators参数直接影响到模型的计算复杂度。如果我们知道该参数不太重要,我们可以选择较小的值来提高我们模型的运行时性能。Optuna 提供了几种可视化方法,以深入了解研究的结果并洞察超参数。

一种直接的可视化方法可以绘制每个参数的重要性:每个参数对训练结果的影响程度。我们可以创建一个重要性图如下:

fig = optuna.visualization.plot_param_importances(study)
fig.show()

我们研究的参数重要性图如下所示:

图 5.3 – 一个参数重要性图,显示了每个超参数对目标值(F1 分数)的重要性

图 5.3 – 一个参数重要性图,显示了每个超参数对目标值(F1 分数)的重要性

图 5.3中,我们可以看到学习率是影响试验成功最关键的参数。叶子和估计器的数量紧随其后。利用这些信息,我们可能会决定在未来的研究中更加重视寻找最优的学习率。

我们创建一个并行坐标图如下,指定它应包含的参数。该图帮助我们可视化超参数之间的交互:

fig = optuna.visualization.plot_parallel_coordinate(study, params=["boosting_type", "feature_fraction", "learning_rate", "n_estimators"])
fig.show()

这里是生成的图表:

图 5.4 – 我们研究的并行坐标图。每条水平线是单个试验的配置。较暗的线条表示更成功的试验(更高的 F1 分数)

图 5.4 – 我们研究的并行坐标图。每条水平线是单个试验的配置。较暗的线条表示更成功的试验(更高的 F1 分数)

并行坐标图显示,最佳试验都使用了 DART 作为提升类型,学习率略低于 0.1,并且有超过 200 个估计器。我们还可以直观地看到一些参数交互:GBDT 模型与略高的学习率相关。当估计器数量很多时,所需的叶子节点数量就很少,因为拥有许多估计器和大量叶子节点会导致过拟合。

多目标优化

在之前显示的优化研究中,我们关注单一优化目标:最大化我们的 F1 分数。然而,在某些情况下,我们希望优化两个可能相互竞争的目标。例如,假设我们想要创建尽可能小的 GBDT(梯度提升决策树)(最少的叶子节点)同时获得良好的 F1 分数。减少叶子节点数量可能会对我们的性能产生负面影响,因此存在权衡。

Optuna 通过使用objective函数并指定优化方向来支持解决这类问题。

例如,考虑学习率和性能之间的权衡。我们希望尽可能快地训练我们的模型,这需要一个高的学习率。然而,我们知道使用小的学习率和多次迭代可以达到最佳性能。

我们可以使用 Optuna 来优化这种权衡。我们定义一个新的objective函数,将所有其他参数固定为之前找到的最佳值。我们返回两个评估结果:学习和交叉验证的 F1 分数。我们希望最大化这两个值:

def moo_objective(trial):
    learning_rate = trial.suggest_float("learning_rate", 0.0001, 0.5, log=True),
    model = lgb.LGBMClassifier(
        force_row_wise=True,
        boosting_type='gbdt',
        n_estimators=200,
        num_leaves=6,
        bagging_freq=7,
        learning_rate=learning_rate,
        max_bin=320,
    )
    scores = cross_val_score(model, X, y, scoring="f1_macro")
    return learning_rate[0], scores.mean()

当调用optimize时,我们为两个评估的优化设置方向:

study = optuna.create_study(directions=["maximize", "maximize"])
study.optimize(moo_objective, n_trials=100)

在执行 MOO(多目标优化)时,并不总是存在一个最佳结果:目标之间往往存在权衡。因此,我们希望可视化研究结果以探索权衡并选择在两个目标上都能表现良好的参数值。这种可视化称为帕累托前沿,可以按以下方式创建:

图 5.5 – 显示 MOO 研究帕累托前沿的散点图

图 5.5 – 显示 MOO 研究帕累托前沿的散点图

图 5.5所示,如果学习率太低,F1 分数会很差,而当学习率达到 0.01 时,F1 分数会迅速提升。F1 分数在 0.12 时达到峰值,随着学习率的增加而缓慢下降。我们现在有了必要的信息来决定我们的权衡:我们可以选择更高的学习率以加快训练速度,牺牲最小的分类性能。

摘要

本章介绍了 Optuna 作为 HPO 的框架。我们讨论了寻找最佳超参数的问题以及如何使用 HPO 算法高效地找到合适的参数。

我们讨论了 Optuna 中可用的两种优化算法:TPE 和 CMA-ES。这两种算法都允许用户为优化设置一个特定的预算(要执行的试验次数)并在约束条件下寻找合适的参数。此外,我们还讨论了剪枝无望的优化试验以节省额外资源和时间。讨论了中值剪枝以及更复杂但有效的连续减半和 Hyperband 剪枝技术。

然后,我们继续展示如何在实际示例中执行 LightGBM 的 HPO 研究。我们还展示了 Optuna 的高级功能,这些功能可用于保存和恢复研究,了解参数的影响,并执行 MOO。

下一章重点介绍使用 LightGBM 的两个案例研究,其中详细讨论并应用了数据科学流程。

参考文献

*[*1]J. Bergstra, R. Bardenet, Y. Bengio 和 B. Kégl, “超参数优化的算法,”载于神经信息处理系统进展,2011 年。
*[*2]J. Bergstra, D. Yamins 和 D. Cox, “使模型搜索成为一门科学:视觉架构在数百维度的超参数优化,”载于第 30 届国际机器学习会议论文集,亚特兰大,2013 年。
*[*3]N. Hansen 和 A. Ostermeier, “在进化策略中调整任意正态变异分布:协方差矩阵调整,”载于 IEEE 国际进化计算会议论文集,1996 年。
*[*4]K. Jamieson 和 A. Talwalkar, 非随机最佳臂识别和超参数优化,2015 年。
*[*5]L. Li, K. Jamieson, G. DeSalvo, A. Rostamizadeh 和 A. Talwalkar, Hyperband:一种基于 Bandit 的新的超参数优化方法,2018 年。