Python-和-LightGBM-机器学习-全-
Python 和 LightGBM 机器学习(全)
原文:
annas-archive.org/md5/fec3be57ef79d1371f9bec4f04b9ee9c译者:飞龙
前言
欢迎阅读《使用 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.10 | Windows, macOS, or Linux |
| Anaconda 3 | Windows, macOS, or Linux |
| scikit-learn 1.2.1 | Windows, macOS, or Linux |
| LightGBM 3.3.5 | Windows, macOS, or Linux |
| XGBoost 1.7.4 | Windows, macOS, or Linux |
| Optuna 3.1.1 | Windows, macOS, or Linux |
| FLAML 1.2.3 | Windows, macOS, or Linux |
| FastAPI 0.103.1 | Windows, macOS, or Linux |
| Amazon SageMaker | |
| Docker 23.0.1 | Windows, macOS, or Linux |
| PostgresML 2.7.0 | Windows, macOS, or Linux |
| Dask 2023.7.1 | Windows, 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/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将非常感激您提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
读完《使用 LightGBM 和 Python 进行实用机器学习》后,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
你喜欢在路上阅读,但无法携带你的印刷书籍到处走吗?你的电子书购买是否与你的选择设备不兼容?
别担心,现在每本 Packt 书籍都附赠一本无 DRM 的 PDF 版本,无需额外费用。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不会就此结束,您还可以获得独家折扣、时事通讯和每天收件箱中的精彩免费内容。
按照以下简单步骤获取福利:
- 扫描下面的二维码或访问以下链接!
![二维码图片]()
https://packt.link/free-ebook/9781800564749
-
提交您的购买证明
-
就这些!我们将直接将您的免费 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中得到了说明。
数据集
如前所述,机器学习模型是使用数据集进行训练的。数据是机器学习过程的核心,数据准备通常是占用最多时间的流程部分。
在本书的整个过程中,我们将与 表格型 数据集一起工作。表格型数据集在现实世界中非常常见,由行和列组成。行通常被称为样本、示例或观察,而列通常被称为特征、变量或属性。
重要的是,列中的数据类型没有限制。特征可以是字符串、数字、布尔值、地理空间坐标,或编码格式,如音频、图像或视频。
数据集也 rarely 完美定义。数据可能不完整、有噪声、不正确、不一致,并包含各种格式。
因此,数据准备和清洗是机器学习过程中的关键部分。
数据准备涉及处理数据使其适合机器学习,通常包括以下步骤:
-
收集和验证:一些数据集最初可能太小或表示问题不佳(数据不是从其抽取样本的实际数据总体有代表性)。在这些情况下,从业者必须收集更多数据,并进行验证以确保数据代表问题。
-
检查系统错误和偏差:检查并纠正收集和验证过程中可能导致的任何系统错误,这些错误可能导致数据集偏差至关重要。在我们的销售示例中,系统收集错误可能仅从城市商店收集数据,而排除农村商店。仅使用城市商店数据训练的模型在预测商店销售时将存在偏差,并且当模型用于预测农村商店的销售时,我们可能会期望性能不佳。
-
数据清洗:任何格式或值范围的不一致性都必须得到解决。任何缺失值也需要以不引入偏差的方式进行处理。
-
特征工程:某些特征可能需要转换以确保机器学习模型能够从中学习,例如将一个单词句子进行数值编码。此外,可能需要从现有特征中准备新的特征,以帮助模型检测模式。
-
归一化和标准化:特征的相关范围必须进行归一化和标准化。归一化和标准化确保没有任何一个特征对整体预测有不成比例的影响。
-
平衡数据集:在数据集不平衡的情况下——也就是说,它包含一个类或预测的示例比另一个多得多——数据集需要被平衡。平衡通常是通过过度采样少数示例来实现的,以平衡数据集。
在第六章**,使用 LightGBM 解决现实世界数据科学问题中,我们将通过整个数据准备过程来展示前面的步骤是如何在实际中应用的。
注意
一个值得记住的谚语是“垃圾进,垃圾出”。模型从它所给出的任何数据中学习,包括数据中包含的任何缺陷或偏差。当我们用垃圾数据训练模型时,结果就是一个垃圾模型。
关于数据集,还有一个需要理解的概念是训练、验证和测试数据集。我们在数据准备步骤完成后将数据集分为这三个子集:
-
训练集是最重要的子集,通常由 60%到 80%的数据组成。这些数据用于训练模型。
-
验证集与训练数据分开,并在整个训练过程中用于评估模型。拥有独立的验证数据确保模型是在它之前未见过的数据上评估的,也称为其泛化能力。超参数调整,在第五章**,Optuna 进行 LightGBM 参数优化中详细介绍的流程,也使用验证集。
-
最后,测试集是一个可选的保留集,类似于验证集。它用于过程的最后,以评估模型在训练或调整过程中未参与的数据上的性能。
验证集的另一个用途是监控模型是否过度拟合数据。让我们更详细地讨论一下过度拟合。
过度拟合与泛化
要理解过度拟合,我们首先必须定义我们所说的模型泛化是什么意思。如前所述,泛化是模型准确预测它之前未见过的数据的能力。与训练准确率相比,泛化准确率作为模型性能估计更为重要,因为这表明我们的模型在生产中的表现。泛化有两种形式,插值和外推:
-
插值指的是模型预测两个已知数据点之间值的能力——换句话说,就是在训练数据范围内进行泛化。例如,假设我们用 1 月到 7 月的月度数据来训练我们的模型。在插值时,我们会要求模型对 4 月某一天进行预测,这是一个在我们训练范围内的日期。
-
外推,正如你可能推断的那样,是模型预测训练数据定义范围之外值的能力。外推的一个典型例子是预测——即预测未来。在我们的上一个例子中,如果我们要求模型在十二月进行预测,我们期望它从训练数据中外推。
在两种泛化类型中,外推更具挑战性,可能需要特定类型的模型来实现。然而,在两种情况下,模型都可能过度拟合数据,失去准确插值或外推的能力。
过拟合是一种现象,其中模型对训练数据拟合得太紧密,失去了泛化到未见数据的能力。模型不是学习数据中的潜在模式,而是记住了训练数据。更技术地说,模型拟合了训练数据中包含的噪声。这个术语“噪声”来源于数据包含信号和噪声的概念。信号指的是我们试图预测的数据中捕获的潜在模式或信息。相比之下,噪声指的是数据点的随机或不相关的变化,这些变化掩盖了信号。
例如,考虑一个数据集,我们试图预测特定位置的降雨量。数据中的信号将是降雨的一般趋势:冬季或夏季降雨增加,或相反的其他位置。噪声将是我们在数据集中每个月和每个位置的降雨量测量的微小变化。
下面的图表说明了过拟合的现象:

图 1.2 – 展示过拟合的图表。模型过度拟合并完美预测了训练数据,但失去了泛化到实际信号的能力
前面的图表显示了信号和噪声之间的差异:每个数据点都是从实际信号中采样的。数据遵循信号的总体模式,有轻微的、随机的变化。我们可以看到模型是如何过度拟合数据的:模型完美地拟合了训练数据,但以泛化为代价。我们还可以看到,如果我们使用模型通过预测 4 的值来进行插值,我们得到的结果比实际信号(6.72 比 6.2)高得多。此外,还显示了模型外推失败的情况:对 12 的预测远低于信号的预测(7.98 比 8.6)。
在现实中,所有现实世界的数据集都包含噪声。作为数据科学家,我们的目标是准备数据,尽可能多地去除噪声,使信号更容易检测。数据清洗、归一化、特征选择、特征工程和正则化是去除数据中噪声的技术。
由于所有真实世界的数据都包含噪声,过拟合是无法完全消除的。以下条件可能导致过拟合:
-
过于复杂的模型:对于我们所拥有的数据量来说过于复杂的模型,会利用额外的复杂性来记住数据中的噪声,从而导致过拟合。
-
数据不足:如果我们没有足够的训练数据用于模型,这类似于一个过于复杂的模型,它会过度拟合数据。
-
特征过多:具有过多特征的集合很可能包含无关的(噪声)特征,这会降低模型的泛化能力。
-
过度训练:对模型进行过长时间的训练,使其能够记住数据集中的噪声。
由于验证集是模型尚未见过的训练数据的一部分,我们使用验证集来监控过拟合。我们可以通过观察训练和泛化误差随时间的变化来识别过拟合的点。在过拟合的点,验证误差增加。相比之下,训练误差持续改善:模型正在拟合训练数据中的噪声,并失去了泛化的能力。
防止过拟合的技术通常旨在解决我们之前讨论的导致过拟合的条件。以下是一些避免过拟合的策略:
-
提前停止:当我们看到验证误差开始增加时,我们可以停止训练。
-
简化模型:具有较少参数的简单模型将无法学习训练数据中的噪声,从而更好地泛化。
-
获取更多数据:收集更多数据或增强数据是防止过拟合的有效方法,因为它给模型提供了更好的机会来学习数据中的信号,而不是在较小数据集中的噪声。
-
特征选择和降维:由于某些特征可能对要解决的问题不相关,我们可以丢弃我们认为冗余的特征,或者使用主成分分析等技术来降低维度(特征)。
-
添加正则化:较小的参数值通常会导致更好的泛化,这取决于模型(神经网络就是一个例子)。正则化向目标函数添加一个惩罚项,以阻止参数值过大。通过将参数值驱动到更小(或零)的值,它们对预测的贡献更小,从而有效地简化了模型。
-
集成方法:结合多个较弱模型的预测可以导致更好的泛化,同时提高性能。
重要的是要注意,过拟合以及防止过拟合的技术是针对我们模型的特定问题。我们的目标始终应该是最小化过拟合,以确保对未见数据的泛化。一些策略,如正则化,可能对某些模型不起作用,而其他策略可能更有效。还有一些针对特定模型的定制策略,我们将在讨论决策树中的过拟合时看到一个例子。
监督学习
店铺销售额的例子是监督学习的一个实例——我们有一个由特征组成的数据库,并且正在训练模型来预测一个目标。
监督学习问题可以分为两大类问题:分类问题和回归问题。
分类与回归
在分类问题中,模型需要预测的标签是分类的或定义了一个类别。一些类别的例子包括垃圾邮件或非垃圾邮件、猫或狗、以及糖尿病患者或非糖尿病患者。这些都是二元分类的例子:只有两个类别。
多类分类也是可能的;例如,电子邮件可以被分类为重要、促销、杂乱或垃圾邮件;云朵的图片可以被分类为卷云、积云、层云或雨层云。
在回归问题中,目标是预测一个连续的、数值的值。例子包括预测收入、销售额、温度、房价和人群数量。
机器学习艺术中很大一部分是正确地将问题定义为分类或回归问题(或者可能是无监督或强化学习)。后面的章节将涵盖这两种类型问题的多个端到端案例研究。
模型性能指标
让我们简要讨论一下我们如何衡量我们模型的表现。模型性能指的是机器学习模型根据给定的输入做出准确预测或生成有意义输出的能力。一个评估指标量化了模型对新、未见数据的泛化程度。高模型性能表明模型有效地学习了数据中的潜在模式,并且可以在它未见过的数据上做出准确的预测。当与监督学习问题(无论是分类还是回归问题)一起工作时,我们可以根据已知的目标来衡量模型的表现。
重要的是,我们衡量模型在分类任务和回归任务上的表现方式不同。scikit-learn 有许多内置的指标函数,可以用于分类或回归问题(scikit-learn.org/stable/modules/model_evaluation.xhtml)。让我们回顾这些中最常见的。
可以用模型做出的正面和负面预测来定义分类指标。以下定义可以用来计算分类指标:
-
真正正例 (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 不可微,因此不能与基于梯度的学习方法一起使用。
与分类指标一样,选择最合适的回归指标取决于你试图解决的问题。
指标与目标
我们将训练模型定义为找到最合适的参数以最小化一个 目标函数。需要注意的是,特定问题的目标函数和指标可能不同。一个很好的例子是决策树,在构建树时使用不纯度(熵)作为目标函数。然而,我们仍然计算之前解释的指标来确定树在数据上的性能。
在我们对基本指标有了理解之后,我们可以结束对机器学习概念的介绍。现在,让我们通过一个例子来回顾我们讨论过的术语和概念。
一个建模例子
考虑以下按月销售的以下数据(单位:千):
| Jan | Feb | Mar | Apr | May | Jun |
|---|---|---|---|---|---|
| 4,140 | 4,850 | 7,340 | 6,890 | 8,270 | 10,060 |
| Jul | Aug | Sept | Oct | Nov | Dec |
| 8,110 | 11,670 | 10,450 | 11,540 | 13,400 | 14,420 |
表 1.1 – 按月样本销售数据,单位:千
这个问题很简单:只有一个特征,即月份,目标是销售数量。因此,这是一个监督回归问题的例子。
注意
你可能已经注意到这是一个时间序列问题的例子:时间是主要变量。时间序列也可以使用更高级的时间序列特定算法(如方差分析)进行预测,但在这个部分我们将使用一个简单的算法进行说明。
我们可以将我们的数据绘制成每月销售的图表,以更好地理解它:

图 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 的线性模型预测的图表
使用这些参数,我们达到了4,610的错误率。
我们的猜测值太低了,但这没关系;我们现在可以更新参数,尝试改进错误率。实际上,更新模型参数是通过使用梯度下降等训练算法算法化完成的。我们将在第二章**,集成学习 – Bagging 和 Boosting中讨论梯度下降。
在这个例子中,我们将使用我们对直线的理解以及直觉来手动更新每个迭代的参数。我们的线太浅,截距太低;因此,我们必须增加这两个值。我们可以通过选择步长来控制我们每次迭代所做的更新。我们必须通过添加步长来更新 m 和 c 值。对于步长为 0.1 的结果,请参阅表 1.2。
| 猜测# | m | c | MAE |
|---|---|---|---|
| 1 | 0.1 | 4 | 4.61 |
| 2 | 0.2 | 4.1 | 3.89 |
| 3 | 0.3 | 4.2 | 3.17 |
| 4 | 0.3 | 4.3 | 2.5 |
| 5 | 0.4 | 4.4 | 1.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
有三组导入:我们首先导入numpy和pandas。导入 NumPy 和 pandas 是开始所有数据科学笔记本的常用方法。此外,请注意短名称np和pd,这是在处理numpy和pandas时的标准约定。
接下来,我们导入一些标准的绘图库,我们将使用这些库来绘制一些图表:来自matplotlib的pyplot和seaborn。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的错误,这比我们的猜测略低。我们还可以检查模型系数和截距(m和c,来自之前的内容):
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 数据集建模的决策树
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]。该算法是递归的,并从以下基本案例开始:
-
如果子数据集中的所有样本都属于同一类,则在树中创建一个选择该类的叶节点。
-
如果使用任何特征分割无法获得信息(数据集不能再进一步分割),则创建一个叶节点,预测子数据集中包含的最频繁的类别。
-
如果子数据集中达到最小样本阈值,则创建一个叶节点,预测子数据集中包含的最频繁的类别。
然后,我们可以应用该算法:
-
检查任何三种基本情况,如果任何一种适用于数据集,则停止分割。
-
对于数据集的每个特征或属性,计算在该特征上分割数据集所获得的信息量。
-
通过在具有最高信息增益的特征上分割数据集来创建决策节点。
-
根据决策节点将数据集分割成两个子数据集,并递归地对每个子数据集应用算法。
一旦树构建完成,就会应用剪枝。在剪枝过程中,信息增益相对较低的决策节点会被移除。移除节点可以避免过度拟合训练数据并提高树的泛化能力。
分类和回归树
你可能已经注意到,在前面的解释中,我们只使用了类别来使用决策节点分割数据集;这并非偶然,因为经典的 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,我们可以用五行代码解决这个分类问题:
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_depth和min_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: 确定分割时使用的杂质度量,可以是gini或entropy/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 – 描述袋装过程的示意图;每个独立的分类器在训练数据的随机子样本上训练,并通过汇总所有分类器的预测来做出最终预测
袋装集成中的每个模型仍然是一个完整的模型,能够独立存在。因此,袋装与强大的模型结合得最好——也就是说,在决策树的情况下,深或宽的决策树。
虽然之前的例子说明了从训练集中抽取样本模式,但也可以为每个模型从数据集中抽取随机特征。在创建训练集时随机选择特征被称为随机子空间方法或特征袋装。特征袋装防止了特定属性可能主导预测或误导模型的情况,并进一步减少了过拟合。
在决策树中,一个同时应用样本袋装和特征袋装的流行算法是随机森林。现在让我们来看看这个算法。
随机森林
随机森林是一种针对决策树的特定袋装集成学习方法[1]。正如其名称所暗示的,它不是构建单个决策树,而是使用袋装训练多个决策树:每棵树要么在随机样本上训练,要么在训练数据中的随机特征上训练,或者两者都训练。随机森林支持分类和回归。
随机森林中单个树的训练方法与单个决策树相同,正如之前所解释的,每棵树都是一棵完整的树。对于预测,森林的最终预测是通过取所有树的算术平均值来实现的;对于分类,是通过多数投票来实现的。
关于性能,随机森林学习产生了一个更稳健的模型,具有更高的准确性,并且倾向于避免过拟合(因为单个模型无法对所有训练数据进行过拟合)。
随机森林超参数
在 scikit-learn 中,正如预期的那样,随机森林的可用超参数与训练决策树的可用超参数相同。我们可以指定max_depth、min_samples_split、max_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 作为随机森林的扩展实现(https://scikit-learn.org/stable/modules/ensemble.xhtml#extremely-randomized-trees)。
在 scikit-learn 中,ExtraTrees 的实现与随机森林具有相同的超参数。
使用 scikit-learn 训练随机森林
现在,我们将查看如何使用 scikit-learn 中的随机森林。
在本例中,我们将使用森林覆盖类型数据集(archive.ics.uci.edu/ml/datasets/Covertype),该数据集在 scikit-learn(https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_covtype.xhtml#sklearn.data)中可用。与之前使用的玩具数据集相比,该数据集是一个显著的提升。数据集包含 581,012 个样本,具有 54 个维度(特征数)。特征描述了美国 30x30m 的森林区域(例如,海拔、坡向、坡度和到水文点的距离)。我们必须构建一个分类器,将每个区域分类为描述森林覆盖类型的七个类别之一。
除了训练RandomForestClassifier之外,我们还将训练一个独立的DecisionTreeClassifier和一个ExtraTreesClassifier,并比较算法的性能。
RandomForestClassifier和ExtraTreesClassifier位于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 |
| ExtraTrees | 0.9231 |
表 2.1 – Forest CoverType 数据集上每个算法的 F1 分数
ExtraTrees 模型略优于随机森林模型,两者都比决策树分类器表现更好。
在本节中,我们概述了 bagging 和随机森林,这是一种基于 bagging 的决策树集成学习方法,它相对于标准决策树提供了一些优势。下一节将探讨另一种集成学习方法:梯度提升。
阶梯提升决策树
梯度提升是一种集成学习方法,它通过顺序组合多个模型来产生一个更稳健的集成模型。与 bagging 不同,在 bagging 中使用了多个强大的模型(并行使用),而在 boosting 中,训练了多个弱学习器,每个学习器都从前一个学习器的错误中学习,以构建一个更准确和更稳健的集成模型。与 bagging 的另一个显著区别是,每个模型都使用整个数据集进行训练。
注意
如下文所述,梯度提升始终构建一系列回归树作为集成的一部分,无论解决的是回归问题还是分类问题。梯度提升也称为多重加性回归树(MART)。
抽象地说,boosting 过程从弱基学习器开始。在决策树的情况下,基学习器可能只有一个分割(也称为决策树桩)。然后计算误差残差(预测值与实际目标之间的差异)。然后,在先前学习器的误差残差上训练新的学习器,以最小化错误。最终的预测是所有学习器预测的总和。以下图示说明了迭代梯度提升过程:

图 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 – 显示寻找函数最小值的梯度下降过程的图表
选择合适的学习率对梯度下降的成功至关重要。如果学习率太低,优化过程会非常缓慢,可能无法在允许的迭代次数内达到最小值。学习率太小也可能导致过程陷入局部最小值:步长太小以至于无法逃脱。相反,假设学习率太大。在这种情况下,我们可能会跳过最小值而完全错过它,或者陷入在最小值周围振荡(不断跳来跳去但从未下降到最优值)。
梯度提升
现在我们已经了解了梯度下降的工作原理,我们可以看到它在梯度提升中的应用。我们将通过一个小例子详细地讲解整个梯度提升算法。在我们的例子中,我们将使用回归树,因为它比分类情况更容易理解。
梯度提升算法
梯度提升算法定义如下,其中 M 是提升树的数量 [3]:
-
给定训练数据 {(x i, y i)} i=1 n ,包含 n 个训练样本(由特征 x i 和目标 y i 定义)和一个可微的损失函数 L(y i, F(x)),其中 F(x) 是模型 F 的预测。
-
使用常数预测值初始化模型 F 0(X) = argmin γ ∑ i=1 n L( y i, γ)
-
对于 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)
-
结果:F M(x)
虽然算法和特别是数学可能看起来令人畏惧,但实际上它比看起来要简单得多。我们将一步一步地讲解算法。考虑以下玩具数据集:
| 性别 | 空腹 血糖 | 腰围 | BMI | LDL 胆固醇 |
|---|---|---|---|---|
| 男性 | 105 | 110 | 29.3 | 170 |
| 女性 | 85 | 80 | 21 | 90 |
| 男性 | 95 | 93 | 26 | 113 |
表 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. | BMI | LDL 胆固醇 | 预测 F 0(x) |
|---|---|---|---|---|---|
| 男 | 105 | 110 | 29.3 | 170 | 125 |
| 女 | 85 | 80 | 21 | 90 | 125 |
| 男 | 95 | 93 | 26 | 113 | 125 |
表 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. | BMI | LDL 胆固醇 | 预测 F 0(x) | F 0(x) 的残差 |
|---|---|---|---|---|---|---|
| 男 | 105 | 110 | 29.3 | 170 | 125 | 45 |
| 女 | 85 | 80 | 21 | 90 | 125 | -35 |
| 男 | 95 | 93 | 26 | 113 | 125 | -12 |
表 2.4 – 根据我们的初始预测,我们可以计算每个患者的残差,如 Residuals for F 0(x) 列所示
下一步很简单:我们构建一个回归树来预测残差。我们不直接使用回归树的预测。相反,我们使用终端区域来计算我们的更新预测。终端区域指的是树的叶节点。
对于这个例子,我们假设以下简单的回归树已被构建:

图 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
如预期,我们的预测在目标值的方向上有所改进。对其他样本做同样的处理,我们得到以下结果:
| 性别 | 空腹血糖 | 腰围 | BMI | LDL 胆固醇 | 预测 F0(x) | F0(x)的残差 | 预测 F1(x) |
|---|---|---|---|---|---|---|---|
| 男性 | 105 | 110 | 29.3 | 170 | 125 | 45 | 129.5 |
| 女性 | 85 | 80 | 21 | 90 | 125 | -35 | 122.65 |
| 男性 | 95 | 93 | 26 | 113 | 125 | -12 | 122.65 |
表 2.5 – 在遵循步骤 2.1 到 2.4 之后,我们根据初始预测和残差计算一个新的预测,F1(x)
学习率的目的在于限制每棵树对整体预测的影响:通过小步骤改进我们的预测,我们最终得到一个整体更准确的模型。
步骤 2然后重复,直到我们得到最终的预测 Fm(x)。
总结来说,我们的梯度提升集成由一系列回归树的预测加权求和组成(权重由学习率决定),每个回归树预测先前预测的伪残差(相对于先前预测的错误梯度),从而最小化先前预测的错误以产生准确的最终预测。
梯度提升用于分类
我们之前给出的梯度提升解释使用了回归问题作为例子。由于算法相同,我们不会详细说明分类的例子。然而,我们不是处理连续的预测值,而是使用与逻辑回归相同的技巧[(https://en.wikipedia.org/wiki/Logistic_regression)]。因此,单个树预测样本属于类的概率。概率是通过取样本的对数似然并将其转换为概率使用逻辑函数来计算的,如下所示:
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 aggregation(bagging)和梯度提升,并提供了如何在 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 Trees(DART)、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
由使用直方图产生的二级优化是“直方图减法”,用于构建叶子的直方图。我们不需要为每个叶子计算直方图,而是可以从父直方图中减去叶子的邻居直方图。选择数据量较小的叶子会导致第一个叶子的 O(#data)复杂度较小,由于直方图减法,第二个叶子的 O(#bin)复杂度较小。
LightGBM 使用直方图应用的一种第三种优化是减少内存成本。特征预排序需要为每个特征提供一个支持数据结构(一个字典)。在构建直方图时不需要这样的数据结构,从而降低了内存成本。此外,由于#bins 很小,可以使用较小的数据类型,如uint8_t来存储训练数据,从而减少内存使用。
关于构建特征直方图算法的详细信息可在论文《CLOUDS:用于大型数据集的决策树分类器》[4]中找到。
独家功能捆绑
独家功能捆绑(EFB)是 LightGBM 在处理稀疏数据(稀疏数据在高维数据集中普遍存在)时应用的一种基于数据的优化。当特征数据稀疏时,通常会发现许多特征是相互排斥的,这意味着它们永远不会同时呈现非零值。考虑到这种排他性,将这些特征组合成一个单一的特征通常是安全的。EFB 在以下图中展示:

图 3.2 – 从两个相互排斥的特征构建特征捆绑
将相互排斥的特征捆绑在一起,可以构建与单个特征相同的特征直方图[1]。这种优化将构建特征直方图的复杂度从 O(#数据 × #特征)降低到 O(#数据 × #捆绑)。对于存在许多相互排斥特征的数据库,这显著提高了性能,因为#捆绑远小于#特征。EFB 的详细算法及其正确性的证明可在[1]中找到。
基于梯度的单侧采样
LightGBM 框架中可用的最后一种基于数据的优化是基于梯度的单侧采样(GOSS)[1]。GOSS 是一种丢弃不再对训练过程有显著贡献的训练数据样本的方法,从而有效地减少了训练数据的大小并加快了过程。
我们可以使用每个样本的梯度计算来确定其重要性。如果梯度变化很小,这表明训练误差也很小,我们可以推断出树对特定数据实例拟合得很好[1]。一个选择是丢弃所有梯度小的实例。然而,这改变了训练数据的分布,减少了树泛化的能力。GOSS 是一种选择保留在训练数据中的实例的方法。
为了保持数据分布,GOSS 按照以下方式应用:
-
数据样本按其梯度的绝对值排序。
-
然后选择前 a × 100%的实例(梯度大的实例)。
-
然后从剩余的数据中随机抽取 b × 100%的实例样本。
-
在损失函数(对于这些实例)中添加一个因子以放大其影响: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 算法。其他选项是dart和rf,用于随机森林。随机森林模式不执行提升,而是构建随机森林。 -
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_fraction和bagging_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_train和y_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_leaves和learning_rate设置为适合该问题的合理值。最后,我们指定force_row_wise为True,这是大型数据集的一个推荐设置。
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 𝝁指标的绘图
运行前面的代码应该会产生一个 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 提供了四个类:LGBMModel、LGBMClassifier、LGBMRegressor和LGBMRanker。这些类都提供了与 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 个相等的部分(或折);对每个子样本数据的组合重复训练和验证,并报告平均性能
使用高值的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列由Graduate、Dropout和Enrolled字符串组成。LightGBM 不能处理字符串作为目标,所以我们将在训练模型之前将这些映射到整数值。
我们还可以运行df.describe()来获取每个列的统计描述(平均值、标准差、最小值、最大值和分位数)。计算描述性统计有助于检查数据的界限(与决策树模型一起工作时不是大问题)并检查异常值。对于这个数据集,没有数据界限或异常值问题。
接下来,我们需要检查重复和缺失值。我们需要删除包含缺失值的行,或者如果有任何缺失值,需要用适当的替代值进行插补。我们可以使用以下代码检查缺失值:
df.isnull().sum()
运行前面的代码显示,这个数据集没有缺失值。
为了定位重复项,我们可以运行以下代码:
df.loc[df.duplicated()]
数据集中也没有重复项。如果有任何重复数据,我们会删除额外的行。
我们还需要检查目标类的分布,以确保它是平衡的。在这里,我们展示了一个直方图,表明目标类的分布。我们使用 Seaborn 的countplot()方法创建直方图,如下所示:
sns.countplot(data=df, x='Target')

图 3.5 – 学术成功数据集中目标类的分布
尽管目标分布并不完全平衡,但它也没有过度偏向任何一类,我们不需要执行任何补偿操作。
到目前为止,我们发现我们的数据集适合建模(我们仍然需要重新映射 Target)和清洗(它不包含缺失或重复的值,并且平衡良好)。现在我们可以更深入地查看一些特征,从特征相关性开始。以下代码绘制了一个相关性热图。成对皮尔逊相关性是通过 df.corr() 计算的。随后的截图显示了使用成对皮尔逊相关性构建的相关性热图:
sns.heatmap(df.corr(), cmap='coolwarm')

图 3.6 – 学术成功数据集的成对皮尔逊特征相关性
我们可以看到三种相关性的模式:第一学期学分、入学、评估和批准都是相关的。这些特征的第一学期和第二学期的值也是相关的。这些相关性表明,一旦学生入学,他们倾向于看到整个学年而不是在学期中辍学。尽管存在相关性,但这些相关性并不足够强,以至于考虑删除任何特征。
第三种相关性模式是 Nacionality 和 International 之间的,它们高度相关。
注意
单词 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’ 特征的分布,显示几乎所有行只有一个值
'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’ 特征的分布
对女性学生的轻微偏见,但不足以引起关注。
建模
现在我们可以为建模准备我们的数据集。我们必须将我们的 Target 值映射到整数,并删除 Nacionality 和 International 特征。我们还需要删除特征名称中的空格。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 仓库中找到:https://github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-3]
下表总结了找到的最佳参数值和每个模型的交叉验证 F1 分数:
| 模型 | 学习率 | 最大深度 | 最小子样本数 | 估计器数 | 叶子数 | 最小叶子样本数 | 最小分割样本数 | F1 分数 |
|---|---|---|---|---|---|---|---|---|
| GBDT | 0.1 | - | 10 | 100 | 32 | N/A | N/A | 0.716 |
| DART | 0.1 | 128 | 30 | 150 | 128 | N/A | N/A | 0.703 |
| DART (GOSS) | 0.1 | 128 | 30 | 150 | 128 | N/A | N/A | 0.703 |
| 随机森林 | N/A | N/A | N/A | 150 | N/A | 10 | 20 | 0.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 – 具有一个隐藏层和输出层的多层感知器。层之间是完全连接的
每个神经元从其他神经元接收输入,执行数学运算,然后将结果传递给下一层的神经元。
数学运算涉及两个主要步骤 – 加权求和和激活函数:
-
加权求和: 神经元接收输入(输入数据或前一个神经元的输出),将每个输入与其对应的权重相乘,然后将它们相加。通常还会添加一个偏差项到加权和中,以更好地控制神经元的输出。从数学上讲,这可以表示如下:
z j = ∑ i (w ij x i) + b j
在这里,x i 代表神经元的所有输入,w ij 是与第 i 个输入相关的权重,b j 是神经元的偏差。
-
激活函数: 加权求和随后通过一个激活函数,确定神经元的输出。激活函数的目的是将非线性引入数学运算中。非线性使得神经网络能够模拟输入和输出之间的非线性关系,因此能够模拟复杂关系。存在各种激活函数,如sigmoid(对数函数)、双曲正切(tanh)和修正线性单元(ReLU),每个都有其自身的特性和用例。这可以表示为:
a j = σ( z j)
其中,a j 是神经元输出,σ 是激活函数。
结合这两个步骤,神经网络中的神经元处理输入数据,使网络能够学习和建模复杂模式。
神经网络通过调整与神经元相关的权重进行训练。算法可以概括如下:
-
权重被初始化为小的随机值。
-
执行前向传播:对于批次中的每个示例,将输入特征传递通过整个网络(在每个神经元计算总和和激活)以在输出层产生预测。
-
然后通过比较批次中每个示例的实际输出和输出来计算损失。像 GBDTs 一样,损失函数必须是可微分的,标准损失函数包括均方误差(MSE)和交叉熵损失。
-
反向传播被执行:使用微积分链式法则计算损失函数相对于权重的梯度。这个过程从输出层开始,反向通过网络进行。
-
然后使用梯度下降或其现代变体(如 Adam)根据反向传播的梯度更新权重。
-
该过程会重复进行一定数量的 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]](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ml-lgbm-py/img/B16690_04_02.jpg)
图 4.2 – 在 Keras 中实现的 TabTransformer 架构[3]
TabTransformer 的模型架构如图图 4.2 所示。使用 TabTransformer,表格数据中的每个特征都被视为一个标记,类似于在自然语言处理中如何将单词视为标记。该模型应用自注意力机制来学习输入数据中特征之间的复杂交互和依赖关系。标记嵌入和注意力机制允许模型捕捉特征之间的全局和局部关系。
TabTransformer 模型有几个关键组件:标记嵌入、位置编码、Transformer 层、池化和输出层。标记嵌入将每个特征值转换为连续向量表示,并通过位置编码结合位置信息。
在图 4**.2中展示的层中移动,我们可以看到分类特征和数值特征是分开的。
分类特征首先通过嵌入层进行处理,如 Keras 中的layers.Embedding实现,然后传递到 Transformer 块。可以实施可变数量的 Transformer 块(使用超参数设置),但每个块都包含一个layers.MultiHeadAttention层和一个带有Dropout的layers.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 中显示。
| 年龄 | 教育程度 | 婚姻状况 | 每周工作小时数 | 收入区间 | |
|---|---|---|---|---|---|
| 12390 | 34 | 部分大学 | 从未结婚 | 40 | <=50K |
| 20169 | 41 | 专科-学术 | 已婚平民配偶 | 45 | >50K |
| 17134 | 35 | 博士 | 从未结婚 | 60 | >50K |
| 23452 | 49 | 高中毕业 | 已婚平民配偶 | 40 | >50K |
| 22372 | 31 | 高中毕业 | 分居 | 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,其中所有特征现在都是数值型的:
| 年龄 | 教育程度 | 婚姻状况 | 每周工作小时数 | 收入区间 | |
|---|---|---|---|---|---|
| 18545 | 37 | 11 | 2 | 40 | 0 |
| 26110 | 51 | 14 | 0 | 60 | 1 |
| 21905 | 36 | 11 | 5 | 32 | 0 |
| 1496 | 32 | 1 | 0 | 43 | 1 |
| 3148 | 47 | 15 | 2 | 40 | 0 |
表 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 GBDT | 1.05s | 84.46% | 0.71 |
| XGBoost GBDT | 5.5s | 84.44% | 0.72 |
| TabTransformer | 113.63s | 77.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 GBDT | 113 秒 | 99.88% | 0.80 |
| XGBoost GBDT | 351 秒 | 98.41% | 0.82 |
| TabTransformer | 528.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使用一种称为平行坐标图的技术来可视化参数交互。平行坐标图是一种用于表示高维数据的可视化工具,因此对于可视化超参数优化的结果特别有用。每个维度(在此上下文中,指超参数)被描绘为一条垂直轴,平行排列。每个轴的范围反映了超参数可以假设的值的范围。每个超参数的配置都被描绘为一条穿过所有这些轴的线,每个轴上的交点表示给定配置中该超参数的值。线条还可以根据性能指标,如验证准确率,进行着色编码,以区分哪些超参数组合产生更好的结果。
平行坐标图之美在于它们能够展示多个超参数之间的关系及其对性能的累积影响,例如图 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 的上下文中,每个候选解代表一组超参数值的组合,算法的性能决定了最优解。
CMA-ES 的进化过程的核心是协方差矩阵。协方差矩阵是一个表示变量对(在 CMA-ES 的情况下,是超参数)之间协方差的正方形、对称矩阵,提供了它们之间关系的洞察。矩阵的对角元素代表单个变量的方差,而矩阵的非对角元素代表变量对的协方差。当存在正协方差时,它表示变量通常在同一方向上移动,要么增加要么减少。相反,负协方差指向一种关系,其中一个变量上升时,另一个变量倾向于下降,反之亦然。协方差为零表示变量之间没有线性关系。
当优化超参数时,CMA-ES 应用以下进化原则:
-
在超参数搜索空间内,初始化平均值和协方差矩阵。
-
重复进化过程:
-
使用平均值和协方差矩阵从搜索空间生成候选解。每个候选解代表一组超参数值的组合。
-
评估候选解的适应度。适应度指的是候选解的质量或它解决优化问题的程度。在 CMA-ES 中,这意味着使用候选超参数在数据集上训练模型,并在验证集上评估性能。
-
从种群中选择最佳候选解。
-
从最佳候选解更新平均值和协方差矩阵。
-
重复进行试验,直到达到最大试验次数或种群适应度不再提高。
-
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_int 和 suggest_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 剪枝器指定的最小和最大资源控制了每个试验训练的最小和最大迭代次数(或估计器)。在应用剪枝时,缩减因子控制每个减半回合中提升的试验数量。
通过指定优化方向(maximize 或 minimize)来创建研究。在这里,我们正在优化 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 序列化对象的方法。例如,可以使用joblib或pickle。我们使用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 数据库(https://docs.sqlalchemy.org/en/20/core/engines.xhtml#database-urls)。
在我们的示例中,我们使用 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中,我们可以看到学习率是影响试验成功最关键的参数。叶子和估计器的数量紧随其后。利用这些信息,我们可能会决定在未来的研究中更加重视寻找最优的学习率。
我们创建一个并行坐标图如下,指定它应包含的参数。该图帮助我们可视化超参数之间的交互:
fig = optuna.visualization.plot_parallel_coordinate(study, params=["boosting_type", "feature_fraction", "learning_rate", "n_estimators"])
fig.show()
这里是生成的图表:

图 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所示,如果学习率太低,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 年。 |
第六章:使用 LightGBM 解决现实世界的数据科学问题
在前面的章节中,我们逐渐构建了一套工具集,使我们能够解决机器学习问题。我们看到了检查数据、解决数据问题和创建模型的例子。本章正式定义并应用数据科学流程到两个案例研究中。
本章详细概述了数据科学生命周期及其包含的所有步骤。在回归和分类问题背景下,讨论了问题定义、数据探索、数据清洗、建模和报告的概念。我们还探讨了使用所学技术准备数据以及构建优化后的 LightGBM 模型。最后,我们深入探讨了如何利用训练好的模型作为机器学习操作(MLOps)的介绍。
本章的主要内容包括:
-
数据科学生命周期
-
使用 LightGBM 预测风力涡轮机发电量
-
使用 LightGBM 对个人信用评分进行分类
技术要求
本章包含示例和代码片段,展示了如何使用 Optuna 对 LightGBM 进行参数优化研究。关于设置本章所需环境的完整示例和说明可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-6找到。
数据科学生命周期
数据科学已成为一门关键学科,使组织能够从其数据中提取有价值的见解并推动更好的决策。数据科学的核心是数据科学生命周期,这是一个系统、迭代的流程,指导数据驱动的解决问题的各种行业和领域。此生命周期概述了一系列数据科学家遵循的步骤,以确保他们解决正确的问题,并提供可操作见解,以产生实际影响。
数据科学生命周期的第一阶段涉及定义问题,这包括理解业务背景、阐述目标和制定假设。这一关键阶段通过确立明确的方向和范围,为整个项目奠定了基础。生命周期中的后续阶段侧重于数据收集、准备和探索,共同涉及收集相关数据、清洗和预处理它,以及进行探索性数据分析以揭示模式和趋势。
数据分析完成后,数据科学生命周期进入模型选择、训练、评估和调整阶段。这些阶段通过选择最合适的算法、在预处理数据上训练它们并优化其性能,对于开发准确和可靠的预测或描述性模型至关重要。目标是构建一个健壮的模型,能够很好地泛化到未见数据,并有效地解决当前问题。
最后,数据科学生命周期强调将最终模型部署到生产环境中的重要性,监控其性能,并维护它以确保其持续的相关性和准确性。同样重要的是将结果和见解传达给利益相关者,这对于推动明智的决策和实现数据科学全部潜力至关重要。通过遵循数据科学生命周期,组织可以系统地从数据中提取价值,并解锁增长和创新的新机会。
在之前的示例中,我们遵循了一个松散的步骤食谱来处理数据和创建模型。在下一节中,我们将正式定义并讨论数据科学生命周期的步骤。
定义数据科学生命周期
以下是在数据科学生命周期中广泛应用的几个关键步骤。这些步骤也在图 6.1中展示,该图说明了生命周期的循环性质。

图 6.1 – 描述数据科学生命周期的图
这些是关键步骤:
-
定义问题:明确阐述业务问题、目标和目标。这一阶段涉及理解利益相关者的需求、制定假设和确定项目的范围。定义问题还为数据收集设定了舞台,并可能决定我们将如何利用我们的模型。
-
数据收集:从各种来源收集所需数据,例如数据库、API、网络抓取或第三方数据提供商。确保数据具有代表性、准确性和与问题的相关性。记录数据的来源和移动方式对于建立数据血缘很重要。此外,构建一个数据字典来记录数据的格式、结构、内容和意义。重要的是,验证数据收集或抽样过程中可能存在的任何潜在偏差。
-
数据准备:清洁和预处理数据,使其适合分析。这一阶段包括诸如数据清洗(例如,处理缺失值和删除重复项)、数据转换(例如,归一化和编码分类变量)和特征工程(例如,创建新变量或聚合现有变量)等任务。将数据移动和合并到可以进行分析和建模的地方可能也是必要的。
-
数据探索:通过进行探索性数据分析(EDA)来深入了解数据。这一步骤包括可视化数据分布,识别趋势和模式,检测异常值和异常,以及检查特征之间的关系和相关性。
-
模型选择:根据问题类型(例如,回归、分类或聚类)和数据特征选择最合适的数据建模技术。选择多个模型算法来验证数据集上的性能是很重要的。
-
模型训练:使用准备好的数据训练选定的模型。这一步骤包括将数据分为训练集和验证集,设置模型参数(超参数),并将模型拟合到数据中。
-
模型评估:使用适当的评估指标(例如,准确率、精确率、召回率、F1 分数、ROC 曲线下面积(AUC-ROC)或均方根误差(RMSE))评估训练模型的性能,并将它们进行比较以选择性能最佳的模型。进行交叉验证或使用保留测试集以确保无偏评估。
-
模型调优:通过优化超参数、特征选择或结合领域知识来微调选定的模型。这一步骤旨在提高模型性能和泛化到未见数据的能力。对于特定问题调整模型也可能是合适的;例如,在识别面部时,更高的精确度比高召回率更合适。
-
模型部署:如果模型要成为更广泛软件系统的一部分,将其最终模型部署到生产环境中,以便用于做出预测或提供决策信息。部署可能涉及将模型集成到现有系统中,创建 API,或设置监控和维护程序。
-
模型监控和维护:持续监控模型的性能,并在必要时更新它,以确保其保持准确性和相关性。应使用检测模型和数据漂移等技术来确保模型性能。模型维护可能涉及使用新数据重新训练模型,更新特征,或细化问题定义。
-
沟通结果:与利益相关者分享见解和结果,包括基于分析的任何建议或行动。沟通结果可能涉及创建可视化、仪表板或报告,以有效地传达发现。
我们现在考察两个案例研究,以了解数据科学生命周期如何实际应用于现实世界的数据。我们研究了一个回归问题,即预测风力涡轮机发电量,以及一个分类问题,即对个人信用评分进行分类。
使用 LightGBM 预测风力涡轮机发电量
我们的第一个案例研究是一个旨在预测风力涡轮机发电功率的问题。该问题的数据集可以从www.kaggle.com/datasets/mukund23/hackerearth-machine-learning-challenge获取。
我们使用前一部分中定义的步骤来处理这个问题,同时详细说明每个步骤中涉及的内容以及代码片段。完整的最终解决方案可在 https://github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-6/wind-turbine-power-output.ipynb 处找到。
问题定义
数据集包含在特定日期和时间测量的风力涡轮机发电功率(kW/h)测量值。每个测量值旁边都有风力涡轮机的参数,包括风车的物理测量(包括风车高度、叶片宽度和长度)、涡轮机的运行测量(包括电阻(欧姆)、电机扭矩、发电机温度和转子扭矩)以及大气条件(包括风速、温度和压力)。
给定参数集,我们必须构建一个回归模型来预测生成的功率(kW/h)。因此,我们采用回归建模。模型的质量通过均方误差(MSE)和决定系数(R²)来衡量。我们还必须确定哪些因素对发电影响最大。
数据收集
数据集包含在 2018 年 10 月至 2019 年 9 月 11 个月内收集的 22,800 个样本。数据以 CSV 文件形式提供,并作为公共领域数据发布。没有收集额外的数据。
数据准备
我们现在可以查看准备数据以进行清洗和探索。通过从我们的 pandas DataFrame 获取信息,我们可以看到数据集包含 18 个数值特征、2 个分类特征和日期特征:
train_df.info()
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 tracking_id 28200 non-null object
1 datetime 28200 non-null object
2 wind_speed(m/s) 27927 non-null float64
3 atmospheric_temperature(°C) 24750 non-null float64
4 shaft_temperature(°C) 28198 non-null float64
...
20 windmill_height(m) 27657 non-null float64
21 windmill_generated_power(kW/h) 27993 non-null float64
我们可以立即看到数据集中存在缺失值,一些特征少于 28,200 个值。我们可以通过计算统计描述来更好地了解数据分布:
train_df.describe().T.style.bar(subset=['mean'])
这将打印出以下内容:
| 特征 | 计数 | 平均值 | 标准差 | 最小值 | 最大值 |
|---|---|---|---|---|---|
| 风速(m/s) | 27927 | 69.04 | 76.28 | -496.21 | 601.46 |
| 大气温度(°C) | 24750 | 0.38 | 44.28 | -99.00 | 80.22 |
| 轴温度(°C) | 28198 | 40.09 | 27.20 | -99.00 | 169.82 |
| 叶片角度(°) | 27984 | -9.65 | 47.92 | -146.26 | 165.93 |
| 传动箱温度(°C) | 28199 | 41.03 | 43.66 | -244.97 | 999.00 |
| 发动机温度(°C) | 28188 | 42.61 | 6.12 | 3.17 | 50.00 |
| 电机扭矩(N-m) | 28176 | 1710 | 827 | 500 | 3000.00 |
| 发电机温度(°C) | 28188 | 65.03 | 19.82 | 33.89 | 100.00 |
| 大气压力(Pascal) | 25493 | 53185 | 187504 | -1188624 | 1272552 |
| 面积温度(°C) | 28200 | 32.74 | 7.70 | -30.00 | 55.00 |
| 风机本体温度(°C) | 25837 | 20.80 | 54.36 | -999.00 | 323.00 |
| 风向(°) | 23097 | 306.89 | 134.06 | 0.00 | 569.97 |
| 阻抗(欧姆) | 28199 | 1575.6 | 483.33 | -1005.22 | 4693.48 |
| 转子扭矩(N-m) | 27628 | 25.85 | 32.42 | -136.73 | 236.88 |
| 叶片长度(m) | 23107 | 2.25 | 11.28 | -99.00 | 18.21 |
| 叶片宽度(m) | 28200 | 0.40 | 0.06 | 0.20 | 0.50 |
| 风机高度(m) | 27657 | 25.89 | 7.77 | -30.30 | 78.35 |
| 风机发电功率 (kW/h) | 27993 | 6.13 | 2.70 | 0.96 | 20.18 |
表 6.1 – 风机数据集中数值特征的统计描述
观察表 6.1 中特征的统计描述,我们可以看到以下不规则性:
-
许多特征存在异常值:通常,标准差大于平均值可能表明存在异常值。例如,风速、大气温度和大气压力。同样,远离平均值的最大值或最小值可能表明数据中存在异常值。我们可以通过使用直方图可视化数据分布来进一步验证这一点。
-
物理不可能性:数据显示某些测量数据中存在不可能性:长度(以米为单位)小于 0(例如,叶片长度)和温度超出自然范围(体温为-999)。
-
-99.0和-999.0在几个特征中重复。这些值在特征之间自然发生的可能性很小。我们可以推断这些值表明样本中存在缺失或错误的测量。
我们可以可视化特征的分布,以调查异常值。例如,对于大气温度,我们有以下:
sns.histplot(train_df["atmospheric_temperature(°C)"], bins=30)

图 6.2 – 以摄氏度显示的大气温度直方图
图 6.2 展示了数据中发现的问题中的两个:精确值为-99.0的测量值的高频率,表明存在错误。一些异常值也远离平均值。
最后,我们可以使用以下方法检查重复数据:
train_df[train_df.duplicated()]
没有返回行,表示数据集中没有重复项。
数据清洗
我们在数据集中识别出多个问题,这些问题需要作为数据清洗步骤的一部分来解决:
-
异常值:许多特征具有使该特征值分布扭曲的异常值。
-
测量误差:一些特征具有超出物理不可能性范围(长度小于 0 或温度在不可能范围内)的值。
-
-99.0和-999.0被视为缺失值。
我们首先解决异常值和测量误差,因为这会影响我们处理缺失值的方式。
处理异常值
处理数据集中的异常值有两个方面:准确识别异常值和选择适当的替换值。识别异常值的方法如下:
-
可视化: 如前所述,可以使用直方图或其他可视化数据分布的图表(如箱线图或散点图)。
-
领域知识: 就像我们识别测量误差一样,可以利用领域知识来决定值是否异常
-
统计分析: 使用统计方法确定值是否异常的两种流行方法是四分位数间距(IQR)和标准差。
四分位数间距(IQR)是第 25 百分位数和第 75 百分位数之间的差值。距离第 25 或第 75 百分位数超过 1.5 倍 IQR 的值被认为是异常值。
或者,我们可以利用标准差:我们计算数据集的均值和标准差。任何超过均值两倍或三倍的数据值都是异常值。将界限设置为标准差的二倍或三倍取决于基础数据。使用两倍标准差可能导致许多误报,但如果大量数据集中在均值附近,则这是合适的。使用三倍标准差更为保守,并且仅将非常远离均值的值标记为异常值。
当检测到异常值时,我们必须对它们采取行动。通常,我们的选择如下:
-
移除: 如果异常值是由于数据输入、测量或收集中的错误造成的,那么从数据集中移除它可能是合理的。但是,应该谨慎行事,因为移除过多的数据点可能导致信息丢失和结果偏差。在我们的数据集中移除包含异常值的实例将导致近 70%的数据丢失,这不是一个选择。
-
插补: 与缺失值类似,用更具有代表性的值替换异常值,例如变量的均值、中位数或众数,或者使用更复杂的插补方法,如k 近邻或基于回归的插补。
-
截断或限制: 设置一个阈值(可以是上限或下限),并将异常值在该阈值处截断或限制。这种方法保留了数据的原始结构,同时减少了极端值的影响。
对于风力涡轮机数据集,我们使用设置为标准差三倍的范围来检测异常值,并将值映射到np.nan,因此我们可以在以后替换它们:
column_data = frame[feature]
column_data = column_data[~np.isnan(column_data)]
mean, std = np.mean(column_data), np.std(column_data)
lower_bound = mean - std * 3
upper_bound = mean + std * 3
frame.loc[((frame[feature] < lower_bound) | (frame[feature] > upper_bound))] = np.nan
处理测量误差
被检测为测量误差类型的值也可以被认为是异常值,尽管不是在统计意义上的异常值。然而,我们可以用稍微不同的方式处理这些值,而不是用统计意义上的异常值处理。
我们应用我们的领域知识以及一些关于天气的研究,来确定这些特征的适当范围。然后我们将错误值限制在这些范围内:
frame.loc[frame["wind_speed(m/s)"] < 0, "wind_speed(m/s)"] = 0
frame.loc[frame["wind_speed(m/s)"] > 113, "wind_speed(m/s)"] = 113
frame.loc[frame["blade_length(m)"] < 0, "blade_length(m)"] = 0
frame.loc[frame["windmill_height(m)"] < 0, "windmill_height(m)"] = 0
frame.loc[frame["resistance(ohm)"] < 0, "resistance(ohm)"] = 0
在这里,我们将任何负长度、高度和电阻值设置为0。我们还把风速限制在113 m/s,这是记录中的最大阵风速度。
最后,我们可以处理数据集中的缺失值。
处理缺失值
我们在前面章节中讨论了处理缺失值的方法。在此总结一下,我们可以采取的一些潜在方法如下:
-
移除含有缺失值的实例
-
使用描述性统计(均值、中位数或众数)来填补缺失值
-
使用其他机器学习算法,通常是聚类等无监督技术来计算更稳健的统计量
移除缺失值将丢弃我们数据集的很大一部分。在这里,我们决定使用描述性统计来替换缺失值,以尽可能保留数据。
首先,我们将-99.0和-999.0值标记为缺失:
df.loc[frame[f] == -99.0, f] = np.nan
df.loc[frame[f] == 99.0, f] = np.nan
df.loc[frame[f] == -999.0, f] = np.nan
df.loc[frame[f] == 999.0, f] = np.nan
然后我们用均值替换缺失的数值,用众数替换分类值:
if f in numerical_columns:
frame[f].fillna(frame[f].mean(), inplace=True)
else:
frame[f].fillna(frame[f].mode()[0], inplace=True)
通常,在使用均值时我们必须小心,因为均值会受到异常值的影响。然而,由于我们已经将异常值标记为np.nan,它们在计算均值时被排除。当在测试集中替换缺失值时,还有一个额外的注意事项:由于测试集应被视为未见过的数据,我们必须使用训练数据集的均值来替换测试集中的缺失值。
这就完成了我们数据集所需的数据清洗。我们应该通过重新检查缺失值和重新计算描述性统计和数据直方图来验证我们的工作。
数据集清洗完毕后,我们可以进行下一步数据准备:特征工程。
特征工程
特征工程指的是创建新特征或修改现有特征以提升机器学习模型性能的过程。本质上,特征工程是利用领域知识和数据理解来创建使机器学习算法更有效工作的特征。它既是艺术也是科学,需要创造力、直觉和对问题的深刻理解。
特征工程过程通常从探索数据以了解其特征、分布和变量之间的关系开始。这一探索阶段可以揭示创建特征的可能机会,例如交互项、聚合特征或时间特征;例如,如果你正在处理包含客户交易数据的集合,你可能设计出捕捉交易频率、平均交易价值或自上次交易以来时间的特征。
在特征工程中,也有一些标准技术被广泛使用。这些包括对分类变量进行编码、对数值变量进行归一化、创建多项式特征和对连续变量进行分箱。例如,分类变量通常被编码为数值格式(如独热编码或顺序编码)以用于数学模型。同样,数值变量通常被归一化(如最小-最大缩放或标准化)以确保它们处于可比较的尺度上,并防止某些变量仅仅因为其尺度而支配其他变量。
然而,特征工程不是一个一刀切的过程。模型适用的特征可能严重依赖于具体问题、使用的算法和数据性质。因此,特征工程通常需要迭代实验和评估。尽管存在挑战,有效的特征工程可以显著提高模型性能。
注意
正如你可能已经注意到的,特征工程需要理解和探索数据,这取决于工程特征的可用性。这突出了数据科学生命周期中的循环过程:我们在数据准备和探索之间迭代。
例如,我们数据中适合进一步工程的特征是datetime字段。对于未来的预测,测量所采取的具体日期和时间对模型来说并不具有信息性。
然而,如果我们提取年份、月份、月份中的日期和一天中的小时作为新特征,那么模型可以捕捉到电力生成与不同时间周期之间的潜在关系。日期分解允许提出如下问题:年份、季节、特定月份等是否会影响电力生成?或者一天中的时间,早上、中午或晚上是否有任何影响?
我们可以将日期分解为以下新特征:
frame["date_year"] = train_df["datetime"].dt.year
frame["date_month"] = train_df["datetime"].dt.month
frame["date_day"] = train_df["datetime"].dt.day
frame["date_hour"] = train_df["datetime"].dt.hour
frame = frame.drop(columns=["tracking_id", "datetime"], axis=1)
如果在建模之后,我们发现这些特征缺乏信息,我们可以进一步使用时间字段来协助建模。未来要探索的方向包括基于特定时期的聚合或通过排序测量来创建时间序列,以研究电力生成随时间的变化趋势。
我们现在继续进行案例研究的 EDA 部分,以可视化和更好地理解我们的数据。
EDA
我们已经对数据集进行了一些探索性数据分析(EDA),以找到缺失值和异常值。对数据集进行 EDA 没有固定的方法;需要一些经验和创造力来指导这个过程。
除了获取对数据的洞察和理解之外,主要目标是尝试在数据中识别模式和关系。在这里,我们从一个相关性热图开始,以探索特征之间的直接相关性:

图 6.3 – 风力涡轮机数据集的相关性热图
图 6.3中的相关性热图显示了风速和大气温度、发动机温度、发电机温度和电机扭矩等发动机指标之间的一些显著相关性,以及我们日期特征与大气条件之间的较弱相关性。
值得注意的是,电机扭矩和发电机温度之间存在非常强的相关性。直观上,这是有道理的:如果电机产生更多的扭矩,它会产生更多的热量。由于扭矩是因果关系特征,我们可以考虑在建模时忽略发电机温度。
我们还可以看到发电量和发动机指标之间的相关性,包括电阻和风向。我们可以预期这些特征将对模型的性能产生重大影响。
我们还可以探索分类特征与发电量之间的相关性。涡轮机状态似乎对发电量影响很小(单独来看)。然而,云层级别有显著影响。将云层级别与发电量绘制成图,我们得到以下结果:
train_df.groupby("cloud_level")["windmill_generated_power(kW/h)"].mean().plot.bar()

图 6.4 – 不同云层下的平均发电量
如图 6.4所示,极低云层与发电量减少有很强的相关性。在进一步探索数据时,控制云层级别有助于确保云层级别不会主导任何出现的模式。
另一个有助于观察各种特征影响的可视化方法是散点图。绘制每个值使得通过视觉识别数据中的模式和聚类来识别特征变得简单。
接下来,我们提供了一些散点图的例子,这些图揭示了数据中的模式。
为了研究叶片角度可能对发电量产生的影响,我们可以创建以下散点图:
sns.scatterplot(x='blades_angle(°)', y='windmill_generated_power(kW/h)', hue='cloud_level', data=train_df)
在散点图中,我们还添加了云层级别的色调区分,这样我们可以直观地验证任何影响不是仅来自云层级别:

图 6.5 – 发电量(y 轴)与叶片角度(x 轴)的散点图
叶片角度散点图显示在图 6.5中。散点图表明,特定的叶片角度范围与发电量的增加相关:[0, 10]度和[65, 75]度(在另一方向上也是相反的)。基于树的算法也能模拟这种相关性。
另一个说明我们特征工程强大功能的例子是月份与发电量的散点图。我们再次通过不同色调控制云层级别:
sns.scatterplot(x='date_month',y='windmill_generated_power(kW/h)',hue='cloud_level',data=train_df)

图 6.6 – 发电量(y 轴)按月份(x 轴)的散点图
图 6**.6显示,4 月至 9 月与发电量的显著下降相关。我们可以得出结论,这些月份风力涡轮机的位置不太 windy,其他来源将不得不补充电力生产的不足。通过分解我们的日期特征,我们使我们的学习算法能够利用这种相关性。
EDA 没有明确的目标。对于大型、复杂的数据集,分析可以深入到数据中,迭代地探索更深入的方面和细微差别,几乎无限期地进行。然而,有两个有助于确定数据是否已充分探索的合理性检查如下:
-
我们是否充分理解了每个特征的含义及其对模型输出的潜在影响?
-
数据是否已准备好进行建模?据我们所知,特征是否具有信息性,数据是否干净且无偏见,以及格式是否适合在它上进行训练?
现在我们继续对数据进行建模,利用前几章的技术构建一个优化良好的模型。
建模
建模的第一步是模型选择。最好的做法是首先使用简单的算法对数据进行建模,以验证我们的数据准备并建立基线。如果建模失败,使用简单算法更容易调试可能出错的地方或隔离可能引起问题的数据实例。
模型选择
对于我们的风力涡轮机数据,我们使用线性回归模型来建立基线并验证数据建模的适用性。
我们还训练了一个随机森林回归模型,作为与我们的 LightGBM 模型进行比较的基准。如果预算允许,使用不同的学习算法训练多个模型也是一个好的实践,因为特定问题可能更适合特定的算法。
最后,我们训练了一个 LightGBM 回归器作为我们的主要模型。
模型训练和评估
从 EDA 中,我们看到生成器温度是多余的(由于与电机扭矩的相关性)。因此,我们从训练数据中排除了它:
X = train_df.drop(columns=["windmill_generated_power(kW/h)", axis=1)
y = train_df["windmill_generated_power(kW/h)"]
与 LightGBM 不同,线性回归和 scikit-learn 的随机森林回归器都不能自动处理分类特征。
因此,我们使用 pandas 的get_dummies来对特征进行编码以进行训练。get_dummies操作执行一个称为0或1列的过程,因为有唯一值。相应的值用1(独热编码)标记每个模式,其他值用0标记。例如,考虑云层特征:有三个类别(中等、低和极低)。我们的数据集中中等云层的行将被编码为100(三个单独的列)。同样,低云层被编码为010,以此类推。
执行独热编码允许算法,如线性回归,仅支持数值列,以增加额外列的内存使用成本为代价来对数据进行建模。
如问题定义所述,我们将使用两个指标来评估模型:确定系数和 MSE。两者都是使用五折交叉验证计算的。
我们现在可以继续训练我们的线性、随机森林和 LightGBM 回归器:
X_dummies = pd.get_dummies(X)
linear = LinearRegression()
scores = cross_val_score(linear, X_dummies, y)
scores = cross_val_score(linear, X_dummies, y, scoring="neg_mean_squared_error")
forest = RandomForestRegressor()
X_dummies = pd.get_dummies(X)
scores = cross_val_score(forest, X_dummies, y)
scores = cross_val_score(forest, X_dummies, y, scoring="neg_mean_squared_error")
lgbm = lgb.LGBMRegressor(force_row_wise=True, verbose = -1)
scores = cross_val_score(lgbm, X, y)
scores = cross_val_score(lgbm, X_dummies, y, scoring="neg_mean_squared_error")
下表总结了每个模型的性能:
| 算法 | R² | MSE |
|---|---|---|
| 线性回归 | 0.558 | 2.261 |
| 随机森林 | 0.956 | 0.222 |
| LightGBM | 0.956 | 0.222 |
表 6.2 – Wind Turbine 数据集上五折交叉验证的性能指标
LightGBM 和随机森林回归器在四舍五入的 R² 和 MSE 分数上表现出几乎相同的表现。这两种算法都显著优于线性回归。
我们的模式表现非常好,绝对误差约为 471 W/h。然而,如果我们绘制训练模型的特征重要性,问题就很容易被发现。
图 6**.7 展示了每个特征对我们 LightGBM 模型的相对重要性。

图 6.7 – 每个特征相对于我们的 LightGBM 模型的相对特征重要性
如我们从特征的重要性中可以看到,有三个特征突出:blades_angle、motor_torque 和 resistance。然而,对于其中两个特征,motor_torque 和 resistance,我们可以问:这些特征是导致生成的功率提高,还是它们是功率增加的结果?这些特征是目标泄露的例子,如以下所述。
目标泄露
目标泄露,通常称为“泄露”,是设计和训练机器学习模型时常见的陷阱。它发生在模型在训练过程中无意中获得了对目标变量(或目标变量的某些代理)的访问。因此,模型在训练过程中的表现可能看起来很令人印象深刻,但在新的、未见过的数据上表现不佳,因为它在训练过程中实际上“作弊”了。
泄露可能发生的一些常见例子如下:
-
基于时间的泄露:假设你正在尝试预测明天的股价。如果你在训练集中包含明天的数据(可能是无意中),这将导致泄露。同样,仅在事后可用的数据(如所有股票的汇总数据)也是基于时间的泄露的另一个例子。
-
预处理错误:这些发生在你使用包括训练集和测试集的统计数据执行特定操作,如缩放或归一化时。
-
错误的数据分割:对于时间序列数据,使用简单的随机分割可能会导致训练集中出现未来的数据。
-
受污染的验证集:有时,在创建验证集或测试集时,一些数据可能重叠或与训练数据非常接近,导致乐观且不具有代表性的验证分数。
在我们的例子中,motor_torque和resistance是时间相关的泄漏的例子:这两个指标只能在发电后才能测量,这正是我们试图预测的。这也说明了进行基线训练测试的重要性,因为像这些问题可能不容易在事先发现。
我们通过从我们的数据集中删除特征来修复这个错误。然后我们可以继续模型调优以进一步提高模型性能。
模型调优
我们使用 Optuna 来执行我们的参数优化研究。我们将利用 Optuna 的树结构帕累托估计器(TPE)采样算法和 Hyperband 剪枝以提高效率。我们定义我们的目标函数所需的参数、剪枝回调,并测量均方误差:
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),
...
pruning_callback = optuna.integration.LightGBMPruningCallback(trial, "mean_squared_error")
model = lgb.LGBMRegressor(
...
callbacks=[pruning_callback],
verbose=-1)
scores = cross_val_score(model, X, y, scoring="neg_mean_squared_error")
return scores.mean()
我们随后使用 TPE 采样器、Hyperband 剪枝和 200 个试验的优化预算创建我们的 Optuna 研究:
sampler = optuna.samplers.TPESampler()
pruner = optuna.pruners.HyperbandPruner(
min_resource=20, max_resource=400, reduction_factor=3)
study = optuna.create_study(
direction='maximize', sampler=sampler,
pruner=pruner
)
study.optimize(objective, n_trials=200, gc_after_trial=True, n_jobs=-1)
通过 Optuna 找到的优化参数,我们进一步提高了 LightGBM 模型在运行中的性能,将其 R²提高到0.93,均方误差为0.21。由于 Optuna 研究的随机性质,您的结果可能会有所不同。
在训练出一个优化的模型后,我们可以继续数据科学流程的下一阶段:部署和报告。
模型部署
我们现在可以使用我们的训练模型对未见数据做出预测。接下来的章节将重点介绍作为 MLOps 过程一部分的各种部署和监控模型的方法。
然而,使用我们的模型最简单的方法是保存模型并编写一个简单的脚本,该脚本加载模型并做出预测。
我们可以使用标准的 Python 序列化或 LightGBM API 来保存我们的模型。在这里,我们展示了使用标准的 Python 工具:
joblib.dump(model, "wind_turbine_model.pkl")
加载模型并做出预测的简单脚本如下:
def make_predictions(data):
model = joblib.load("wind_turbine_model.pkl")
return model.predict(data)
if __name__ == '__main__':
make_predictions(prepare_data(pd.read_csv("wind-turbine/test.csv")))
重要的是,我们必须为任何我们想要预测的数据重复数据准备,以添加工程化特征并删除未使用的列。
沟通结果
数据科学流程的最后一步是沟通结果。数据科学家通常会编制一份包含显著发现和可视化的报告,向利益相关者展示结果。
报告看起来会与这个案例研究的撰写类似。我们可能会展示我们找到的特征之间的相关性,例如,月份与发电量的相关性。我们还会突出数据中的问题,如异常值和缺失值,以改善未来的数据收集工作。
我们将进一步突出对模型重要的特征,以便风力涡轮机可以优化以最大化发电量。
专注于报告的质量。使用精心设计和详细的可视化以及其他支持材料,而不是仅仅依赖文本。信息图表或交互式图表可能比详细的撰写更有帮助。检查您的写作错误,并在发送之前确保报告经过校对。
报告的内容应解决问题陈述中定义的问题。任何被测试过的假设都必须在报告中回答。但是,报告也强烈依赖于并应针对您的受众进行调整。例如,如果您的受众是商业高管,请包括他们能理解的内容,并回答他们可能有的问题,这些问题将围绕您发现的业务影响为中心。
现在我们来看一个分类问题的案例研究。我们表明,尽管每个数据集都是独特的,并且具有特定的挑战,但整体的数据科学流程仍然是相同的。
使用 LightGBM 对个人信用评分进行分类
我们的第二个案例研究是一个针对个人信用评分分类的问题。数据集可在www.kaggle.com/datasets/parisrohan/credit-score-classification?datasetId=2289007找到。
与前一个问题相比,数据集显著更大,并且存在独特的数据格式问题。为了简洁,我们不会像以前的问题那样详细地介绍解决方案(因为大部分工作都是相同的),但端到端解决方案可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-6/credit-score-classification.ipynb找到。
问题定义
数据集包含 10 万行和 27 列,代表个人的人口和财务信息,包括信用评分评级。数据包括有关个人收入、贷款数量、支付行为和投资的信息。信用评分可能被评为良好、标准或较差。
我们的任务是分析数据并构建一个模型,该模型能够准确地对未见过的个人的信用评分进行分类。预测质量使用分类准确率和 F1 分数来衡量。
数据收集
数据来自一家美国金融机构的客户数据库。14 至 65 岁的个人构成了数据集的一部分。没有记录表明采样特定人口统计特征(低收入群体、年龄或种族群体)存在偏差,但这必须得到验证。没有收集额外的数据。
数据准备
如前所述,我们首先从简单的数据探索任务开始,以确定数据的清洁度。我们首先检查数据结构和类型:
train_df.info()
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ID 100000 non-null object
1 Customer_ID 100000 non-null object
2 Month 100000 non-null object
3 Name 90015 non-null object
4 Age 100000 non-null object
5 SSN 100000 non-null object
...
25 Payment_Behaviour 100000 non-null object
26 Monthly_Balance 98800 non-null object
27 Credit_Score 100000 non-null object
我们注意到某些特征存在缺失值。此外,我们期望许多特征是数值型(如年收入、贷款数量等),但它们被解释为对象而不是整数或浮点数。这些特征必须被强制转换为数值特征。
我们还检查了特征的描述性统计和重复行。发现有两个特征有异常值,年龄和银行账户数量,需要清理。
值得注意的是,Type_of_Loan字段包含一个以逗号分隔的列表,包括连词,列出每个个体的贷款类型。例如,“student_loan, mortgage_loan, and personal_loan”。建模算法无法将贷款类型作为字符串的一部分提取出来。我们必须构建新的字段来启用有效的建模。
数据清理
我们现在可以继续清理数据。总的来说,以下问题需要解决:
-
在适当的地方将对象列强制转换为数值列
-
处理年龄、银行账户数量和月度余额列中的异常值
-
为贷款类型构建新特征
-
处理缺失值和重复行
将列强制转换为数值列
数据集的主要问题之一是应表示数值的列中发现的混合类型,但由于错误值,pandas 将其解释为对象。例如,Annual_Income列包含100000.0_之类的值,然后被解释为字符串。
为了清理并将特征转换为数字,我们首先使用正则表达式删除字符符号:
frame[col] = frame[col].astype(str).str.replace(r'[^\d\.]', '', regex=True)
这使我们能够使用 pandas 将列强制转换为数值特征,将任何错误(空值)转换为np.nan值:
frame[col] = pd.to_numeric(frame[col], errors="coerce")
Credit_History_Age特征需要我们更多的工作。年龄使用自然语言指定,例如“12 年 3 个月”。在这里,我们使用 Python 字符串处理将年和月转换为浮点数:
def clean_credit_age(age):
if age == 'nan':
return np.nan
if not "Years" in age:
return age
years, months = age.split(" Years and ")
months = months.replace(" Months", "")
return int(years) + int(months) / 12
分割分隔符分隔的字符串
如前所述,Type_of_Loan特征是个人拥有的贷款类型的逗号分隔列表。尽管我们可以以多种方式处理这个问题,但最有帮助的技术是解析字段并构建一个布尔数组列,指示个人拥有哪些贷款。
有八种独特的贷款类型,以及如果贷款类型未指定,有一个特定类别。这些是Auto Loan、Credit-Builder Loan、Debt Consolidation Loan、Home Equity Loan、Mortgage Loan、Payday Loan、Personal Loan和Student Loan。
我们的编码策略将按以下方式处理特征。我们创建九个新的列(每个贷款类型和一个未指定的),如果个体有那种贷款类型,则将该列设置为 true。例如,这里我们有三个连接的贷款描述:
"Home Equity Loan, and Payday Loan"
"Payday Loan, Personal Loan"
"Student Loan, Auto Loan, and Debt Consolidation Loan"
表 6.3显示了这些示例的编码结果,其中如果个体有那种类型的贷款,则设置 true 标志。
| Auto | Credit-Builder | Debt Cons. | Home Equity | Mortgage | Payday | Personal | Student | Unspecified |
|---|---|---|---|---|---|---|---|---|
| F | F | F | T | F | T | F | F | F |
| F | F | F | F | F | T | T | F | F |
| T | F | T | F | F | F | F | T | F |
表 6.3 – 贷款类型列编码客户贷款类型
我们可以利用 pandas 的字符串工具来完成前面的编码,如下例所示:
frame["auto_loan"] = frame["Type_of_Loan"].str.lower().str.contains("auto loan").astype(bool)
异常值和缺失值
我们遵循之前相同的一般策略:使用描述性统计来填补缺失值,并在可能的情况下,将异常值设置为使用领域知识定义的边界。
删除重复行。
在异常值方面,年龄、银行账户数量和月余额特征具有异常值(我们通过均值、标准差和分布图进行确认)。我们将这些特征的异常值设置为上限:
frame.loc[frame["Age"] > 65, "Age"] = 65
frame.loc[frame["Num_Bank_Accounts"] > 1000, "Num_Bank_Accounts"] = 1000
frame.loc[frame["Monthly_Balance"] > 1e6, "Monthly_Balance"] = np.nan
在数据清洗后,我们可以验证所有特征都有正确的类型,并且缺失值已得到处理:
train_df.info()
train_df.isnull().sum()
train_df[train_df.duplicated()]
我们可以使用清洗后的数据集进行更彻底的探索性分析。
EDA
接下来,我们强调在 EDA 过程中发现的一些模式。
如问题描述所述,我们需要验证数据中是否存在任何潜在的偏差。
我们首先可视化客户的年龄:
sns.histplot(train_df["Age"], bins=20)

图 6.8 – 按年龄统计客户数量的直方图
图 6.8显示所有年龄段都有数据,数据主要围绕中年人呈正态分布。
我们还检查月收入。低收入群体数据缺失可能表明少数族裔被排除在外:
sns.histplot(train_df["Monthly_Inhand_Salary"], bins=30)

图 6.9 – 按月到手工资统计客户数量的直方图
如图 6.9所示,月收入遵循预期的分布,低收入群体有很好的代表性。
我们再次可视化数值特征的关联热图,以突出和直接关联:

图 6.10 – 信用评分数据集的关联热图
两个显著的关联值得关注:月余额和月工资,以及逾期债务和还款延迟。进一步可视化这些关联表明,客户的月余额随着工资的增加而增加,并且不良的信用评分与较低的余额和工资相关。
逾期债务和信用评分之间存在类似的关联:债务的增加与不良的信用评分相关,反之亦然。分析确认我们的模型应该能够捕捉到这两个关联。
最后,并且非常重要,我们还要检查数据集的类别分布:
sns.histplot(train_df["Credit_Score"], bins=30)

图 6.11 – 信用评分数据集类别分布的直方图
我们的数据集类别分布如图6.11所示。如图所示,存在显著的类别不平衡。在建模之前,我们必须解决这种不平衡问题。
建模
如前所述,我们首先进行模型选择。
模型选择
由于数据集的大小和复杂性,线性模型不太可能表现良好。因此,我们使用常规决策树作为基线模型,并包括随机森林进行比较。
模型训练和评估
我们几乎准备好训练我们的模型了,但一个问题仍然存在:我们必须解决类别不平衡问题。
处理类别不平衡
类别不平衡可能会使任何训练好的模型偏向多数类或多个类别。尽管基于树的算法在处理类别不平衡方面比大多数其他学习算法更擅长,但在建模之前解决类别不平衡问题仍然是最佳实践。
通常,以下策略可以用来处理类别不平衡问题:
-
重采样技术:
-
上采样:这涉及到增加少数类中的样本数量以匹配多数类。一种常见的技术是合成少数类过采样技术(SMOTE),其中基于现有样本创建新的样本。
-
下采样:这涉及到减少多数类中的样本数量以匹配少数类。这种方法的潜在风险是丢失可能有价值的数据。
-
-
class_weight(用于多类)和scale_pos_weight(用于二分类)参数。这些参数的应用示例在第四章,比较 LightGBM、XGBoost 和深度学习中给出。 -
数据增强:这涉及到通过向现有实例添加小的扰动来在数据集中创建新的实例。这种方法在图像分类任务中很常见。
-
使用适当的评估指标:在类别不平衡的情况下,准确率往往具有误导性。相反,如精确率、召回率、F1 分数、ROC 曲线下的面积(AUC-ROC)和混淆矩阵等指标可以提供对模型性能的更全面了解。
在我们的案例研究中,我们已经采用了对类别不平衡具有鲁棒性的评估指标。对于这个问题,我们使用 SMOTE,一种过采样技术,来平衡我们的类别,同时保留我们的数据。
SMOTE
SMOTE 是为了克服简单上采样少数类的某些不足而开发的,这可能导致由于实例的精确复制而过度拟合[1]。SMOTE 不是简单地复制少数样本,而是创建与少数类中现有样本相似但不完全相同的合成或“假”样本。
SMOTE 算法按以下步骤进行。通过选择接近少数类样本的样本之间的点来合成新的样本点,称为合成样本。具体来说,对于每个少数类样本,SMOTE 计算其 k 个最近邻,选择这些邻居中的一个,然后将样本与其所选邻居的特征向量之间的差异乘以 0 到 1 之间的随机数,并将这个值加到原始样本上,以创建一个新的、合成的样本。
通过创建合成示例,SMOTE 提供了一种更稳健的解决方案来解决不平衡问题,鼓励模型绘制更具泛化能力的决策边界。然而,需要注意的是,尽管 SMOTE 可以提高不平衡数据集上模型的性能,但它并不总是最佳选择。例如,如果少数样本在特征空间中不够接近,它可能会引入噪声,导致类别重叠。与所有采样技术一样,使用交叉验证或单独的验证集仔细评估 SMOTE 对模型性能的影响是至关重要的。
SMOTE 过采样在 imblearn Python 库中实现。我们可以如下拟合和重采样我们的数据:
X = train_df.drop(columns=["Credit_Score"], axis=1)
X_dummies = pd.get_dummies(X)
y = train_df["Credit_Score"]
smote = SMOTE(sampling_strategy='auto')
return smote.fit_resample(X_dummies, y)
图 6**.12 展示了使用 SMOTE 重采样后的数据集类别分布。如图所示,类别现在完全平衡。

图 6.12 – 使用 SMOTE 重采样后的信用评分数据集类别分布直方图;类别现在平衡
训练和评估
我们现在可以继续进行建模。下表展示了我们的决策树分类器、随机森林和 LightGBM 模型使用默认参数的运行结果。
| 算法 | 准确率 | F1 分数 |
|---|---|---|
| 决策树 | 59.87% | 0.57 |
| 随机森林 | 69.35% | 0.67 |
| LightGBM | 70.00% | 0.68 |
表 6.4 – 在信用评分数据集上五折交叉验证的性能指标
如 表 6.4 所示,LightGBM 模型的性能最佳,略优于随机森林模型的准确率。两种算法的性能都优于决策树基线。LightGBM 模型的训练速度也最快,比随机森林模型快七倍以上。
我们现在继续进行 LightGBM 模型的参数优化。
模型调优
与之前的案例研究类似,我们使用 Optuna 进行参数优化。我们再次使用 TPE 采样器,优化预算为 50 次试验。
模型部署和结果
我们的模式现在已准备就绪,可以部署到我们选择的平台上。可能的部署选项包括围绕我们的模型构建一个 Web API,使用PostgresML等工具进行部署,或者使用如AWS SageMaker这样的云平台。这些以及其他选项将在接下来的章节中详细讨论。
我们还可以将模型作为数据科学报告的一部分使用。参见上一节,了解撰写良好报告的详细信息。请记住沟通数据科学结果最重要的方面,如下所示:
-
总是以无偏见和公平的方式报告结果
-
考虑你的受众,并关注报告中提供他们最大价值的细节
摘要
本章介绍了两个案例研究,说明了如何使用 LightGBM 应用数据科学流程。详细讨论了数据科学生命周期和典型的组成部分步骤。
以风力涡轮机发电为例,介绍了一个在生命周期中处理数据问题的案例研究。详细讨论了特征工程以及如何处理异常值。进行了一次示例性探索性数据分析,并提供了用于可视化的样本。同时展示了模型训练和调优,以及导出和使用模型作为程序的基本脚本。
还介绍了一个涉及多类信用评分分类的第二个案例研究。再次遵循数据科学流程,特别关注数据清洗和数据集中类别不平衡问题。
下一章讨论了 AutoML 框架 FLAML,并介绍了机器学习管道的概念。
参考文献
| *[1] | N. V. Chawla, K. W. Bowyer, L. O. Hall, and W. P. Kegelmeyer, “SMOTE: Synthetic Minority Over-sampling Technique,” Journal of Artificial Intelligence Research, vol. 16, p. 321–357, 六月 2002. |
|---|
第七章:使用 LightGBM 和 FLAML 进行 AutoML
在上一章中,我们讨论了两个案例研究,展示了如何处理数据科学问题的端到端示例。在典型的数据科学生命周期中,通常最耗时的工作是准备数据、寻找正确的模型以及调整模型。
本章探讨了自动机器学习的概念。自动机器学习系统旨在自动化机器学习生命周期的某些或全部部分。我们将探讨 FLAML,这是一个库,使用高效的超参数优化算法来自动化模型选择和调优步骤。
最后,我们将通过一个案例研究来展示如何使用 FLAML 和另一个名为 Featuretools 的开源工具。我们将讨论和展示 FLAML 的实际应用,并展示 FLAML 的零样本 AutoML 功能,该功能完全绕过了调优过程。
本章的主要内容包括:
-
自动机器学习简介
-
FLAML 用于 AutoML
-
案例研究 – 使用 FLAML 和 LightGBM
技术要求
本章包含了一些示例和代码片段,展示了如何使用 FLAML 和 LightGBM 进行 AutoML 应用。关于设置本章所需环境的完整示例和说明可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-7找到。
自动机器学习
自动机器学习(AutoML)是一个新兴领域,旨在自动化机器学习工作流程的复杂方面,从而更高效、更易于部署机器学习模型。AutoML 的出现反映了人工智能和机器学习技术的日益复杂化,以及它们在各个行业和研究领域的渗透。它旨在减轻数据科学过程中的某些复杂、耗时的工作,使机器学习技术的应用更加广泛和易于获取。
对于熟悉机器学习和数据科学流程的软件工程师来说,机器学习模型的日益复杂性和算法宇宙的不断扩大可能构成重大挑战。构建一个稳健、高性能的模型需要大量的专业知识、时间和计算资源来选择合适的算法、调整超参数以及进行深入的比较。AutoML 作为解决这些挑战的解决方案应运而生,旨在自动化这些复杂且劳动密集型的工作。
AutoML 还有助于民主化机器学习领域。通过抽象化数据工程和模型构建及调优的一些复杂性,AutoML 使得那些在机器学习方面经验较少的个人和组织能够利用这些强大的技术。因此,机器学习可以在更广泛的背景下发挥作用,更多个人和组织能够部署机器学习解决方案。
AutoML 系统在复杂度方面各不相同。尽管所有 AutoML 系统都旨在简化机器学习(ML)工作流程,但大多数系统和工具仅关注工作流程的一部分。通常,数据预处理、特征工程、模型选择和超参数调整等步骤被自动化。这种自动化可以节省时间,并通过系统地探索更全面的选项集来提高机器学习模型的鲁棒性和性能,这些选项可能由于人为偏见或时间限制而被忽视或未探索。
自动化特征工程
如在第第六章《使用 LightGBM 解决现实世界数据科学问题》中所述,数据清洗和特征工程是机器学习工作流程的关键部分。它们涉及处理不可用数据、处理缺失值以及创建可以输入到模型中的有意义的特征。手动特征工程可能特别具有挑战性和耗时。AutoML 系统旨在有效地处理这些任务,实现自动特征提取和转换,从而产生更鲁棒的模型。
数据清洗自动化通常是通过遵循处理异常值和缺失值等问题的特定知名技术来实现的。在之前的章节中,我们手动应用了一些这些技术:异常值可以通过统计测试进行测试,并截断或截顶。缺失值通常使用描述性统计量,如平均值或众数进行插补。AutoML 系统要么使用启发式算法和测试来选择最佳的数据清洗技术,要么通过训练模型来采取多种最佳方法和测试。
自动化特征工程的方法通常相似:将许多可能的转换应用于所有现有特征,并在建模后测试生成的特征的有用性。以下案例研究中可以找到转换如何生成特征的示例。
自动化特征工程的另一种方法是使用基于特征数据类型的规则提取特征。例如,可以从日期字段中提取日期、周、月和年。或者可以计算单词或句子特征的字符数、词元、词干或嵌入。
正如你可能注意到的,特征工程自动化技术的应用依赖于手头特征的技术信息,通常包括数据类型,以及与其他特征的关联和关系。重要的是,在创建新特征时没有应用领域知识。这突显了 AutoML 的一个缺点:它无法处理需要人类专业知识的特定、领域驱动型决策。例如,考虑一个包含空腹血糖特征的糖尿病数据集。医学专业人士(领域专家)知道,空腹血糖在 100 到 125 mg/dL 之间的人被认为是糖尿病前期,任何更高都被认为是糖尿病患者。这个连续特征可以被工程化为特定的类别:正常、糖尿病前期和糖尿病患者,从而简化了建模数据。这种类型的转换是无法通过 AutoML 系统实现的。
自动化模型选择和调整
AutoML 特别有用的领域包括模型选择和超参数调整。鉴于可用的算法众多,为特定数据集和问题选择最佳算法可能会令人望而却步。AutoML 系统使用各种技术,包括贝叶斯优化和元学习,来选择最佳模型。它们还自动调整超参数,以最大化模型性能。
AutoML 系统可以提供自动交叉验证,降低过拟合的风险,并确保模型对未见数据的泛化能力。一旦选定了最佳模型并进行了训练,许多 AutoML 工具还可以帮助部署模型,使其可用于对新数据的推理。
除了初始模型部署之外,一些 AutoML 解决方案在持续模型监控和维护方面也提供了价值。随着现实世界数据的发展,模型可能会出现漂移,其性能可能会下降。AutoML 可以帮助监控模型性能,并在需要时重新训练模型,确保您的机器学习系统在长期内保持有效。
使用 AutoML 系统的风险
如前所述,AutoML 系统通常不使用领域知识来辅助特征工程、模型选择或其他自动化任务。相反,它们采用一种试错的方法。
一些 AutoML 系统的“黑盒”特性也可能使得解释系统做出的决策变得具有挑战性,这使得它们对于需要高解释性的应用不太适合。
因此,仍然非常重要的是要有一个数据科学家或领域专家参与其中,与 AutoML 系统协同工作,以识别并利用领域知识可以带来更好模型的机遇。然而,AutoML 系统有时会阻碍数据科学家,而不是通过在科学家和数据之间增加一个额外的层次来使他们能够发挥作用。
我们已经看到了一个自动化机器学习框架的实际应用。在第五章中讨论的 Optuna,使用 Optuna 进行 LightGBM 参数优化,是一个专注于超参数调整的自动化机器学习框架的例子。在下一节中,我们将讨论另一个自动化机器学习框架:FLAML。
介绍 FLAML
FLAML(快速轻量级自动化机器学习)是由微软研究院开发的 Python 库[1]。它旨在自动生成高质量的机器学习模型,降低计算成本。FLAML 的主要目标是最大限度地减少调整超参数和识别最佳机器学习模型所需的资源,使自动化机器学习更加易于访问和成本效益,尤其是对于预算有限的用户。
FLAML 提供了一些关键特性,使其与众不同。其中之一是它的效率。它为机器学习任务提供了一种快速轻量级的解决方案,最大限度地减少了所需的时间和计算资源。它在不影响所产生模型质量的情况下实现了这一点。FLAML 还强调其在各种机器学习算法和不同应用领域中的多功能性。
FLAML 的高效核心在于其新颖且成本效益高的搜索算法。这些算法智能地探索超参数空间,最初专注于“低成本”配置。随着对搜索空间的深入了解,它逐渐探索更多“高成本”配置。这确保了平衡的探索和利用,在用户指定的时间和资源预算内提供优化的模型。
FLAML 在模型选择过程中也表现出色。它支持各种机器学习算法,包括 XGBoost、LightGBM、CatBoost、随机森林和各种线性模型。该库可以自动为给定数据集选择最佳算法并优化其超参数,为用户提供一个无需大量手动干预的优化模型。
FLAML 提供了一个简单直观的 API,可以无缝集成到现有的基于 Python 的数据科学和机器学习工作流程中。用户指定数据集、时间预算(以秒为单位)和优化任务,FLAML 处理其余部分。这种用户友好性和效率使其成为机器学习初学者和希望加快工作流程的资深从业者的一项实用选择。
FLAML 效率背后的新意来自于其超参数优化(HPO)算法。FLAML 提供了两种 HPO 算法:成本节约优化和BlendSearch。
成本节约优化
成本节约优化(CFO)是一种局部搜索方法,利用随机直接搜索来探索超参数空间[2]。CFO 算法从一个低成本的超参数配置开始(例如,对于 LightGBM,一个低成本配置可能只有少数提升树)。它在超参数空间中随机移动固定次数的迭代,朝着更高成本的参数区域前进。
CFO 的步长是自适应的,这意味着如果连续几次迭代没有改进,算法会降低步长。这样做意味着不会在成本高昂的方向上采取大步长。
CFO 还利用随机重启。作为一个局部搜索算法,CFO 可能会陷入局部最优。如果没有任何进展且步长已经很小,算法会在一个随机点重新启动。
总结来说,CFO 快速(使用大步长)尝试在搜索空间中达到更有希望的领域,尽可能少地使用优化预算(通过在低成本区域开始)。当优化预算允许时,CFO 会继续搜索,如果出现停滞,会在随机区域重新启动。FLAML 允许用户以秒为单位设置优化预算。
BlendSearch
FLAML 为 BlendSearch 中的 CFO 算法提供了一种替代方案。BlendSearch 与 CFO 的不同之处在于,它使用多线程方法[3]同时运行全局和局部搜索过程。
与 CFO 类似,BlendSearch 从一个低成本配置开始,并继续进行局部搜索。然而,与 CFO 不同,BlendSearch 不会等待局部搜索停滞后再探索新的区域。相反,全局搜索算法(如贝叶斯优化)会不断提出新的起始点。起始点会根据它们与现有点的距离进行过滤,并按成本优先排序。
BlendSearch 的每次迭代都会根据前一次迭代的表现来决定是继续进行局部搜索还是从新的全局搜索点开始。与 CFO 类似,全局搜索方法提出的配置会经过可行性验证。
由于 BlendSearch 使用全局优化,因此它不太可能陷入局部最小值。如果超参数搜索空间非常复杂,建议使用 BlendSearch 而不是 CFO。通常,先尝试 CFO,如果 CFO 遇到困难,再切换到 BlendSearch 是个不错的主意。
FLAML 局限性
尽管 FLAML 有其优势,但也存在局限性。该库的自动化流程可能不会始终优于专家的手动调整,尤其是在处理复杂、特定领域的任务时。此外,与其他 AutoML 解决方案一样,生成的模型的可解释性可能具有挑战性,尤其是在处理如提升树或神经网络等模型时。
FLAML 只执行 ML 过程中的模型选择和调整部分。这些是模型开发中最耗时的部分之一,但 FLAML 不提供执行特征工程或数据准备的功能。
以下部分展示了使用 FLAML 与 LightGBM 的案例研究,展示了日常用例、不同的优化算法以及 FLAML 的零样本 AutoML。
案例研究 - 使用 FLAML 与 LightGBM
我们将使用前一章中的风力涡轮机数据集作为案例研究。数据集像以前一样进行了清理,填补了缺失值,并将异常值限制在适当的范围内。然而,我们在特征工程上采取了不同的方法。为了进一步探索 AutoML,我们使用了一个名为 Featuretools 的开源框架。
特征工程
Featuretools (featuretools.alteryx.com/en/stable/#) 是一个用于自动特征工程的开源框架。具体来说,Featuretools 非常适合转换关系数据集和时间序列数据。
如前所述,自动特征工程工具通常使用特征的组合转换来为数据集生成新特征。Featuretools 通过其深度特征合成(DFS)过程支持特征转换。
例如,考虑一个在线客户网络会话的数据集。此类数据集中可能有用的典型特征包括客户访问网站的会话总数,或客户注册的月份。使用 Featuretools 和 DFS,可以通过以下代码实现(感谢featuretools.alteryx.com/):
feature_matrix, feature_defs = ft.dfs(
entityset=es,
target_dataframe_name="customers",
agg_primitives=["count"],
trans_primitives=["month"],
max_depth=1,
)
feature_matrix
这里应用了两种转换:一个是“month”的转换,另一个是“count”的聚合。通过这些转换,会自动从客户的任何日期(如加入日期)中提取月份,并为每个客户(如会话数或交易数)计算聚合计数。Featuretools 提供了一套丰富的转换和聚合功能。完整的列表可在 https://featuretools.alteryx.com/en/stable/api_reference.xhtml 上找到。
让我们看看如何使用 Featuretools 为风力涡轮机数据集的特征进行工程。
使用 Featuretools 和风力涡轮机数据集
我们必须为我们数据集执行两个特征工程任务:为日期时间字段生成特征,并对分类特征进行编码。为了开始,我们为我们的数据创建一个EntitySet:
es = ft.EntitySet(id="wind-turbine")
es = es.add_dataframe(
dataframe_name="wind-turbine",
dataframe=df,
index="tracking_id"
)
EntitySet告诉 Featuretools 框架我们在数据中处理的数据实体和关系。在先前的例子中,客户是一个实体示例;对于这个案例,它是风力涡轮机。然后我们传递数据框和用作索引的列。
然后我们应用dfs和encode_features来为我们数据集的特征进行工程:
feature_matrix, feature_defs = ft.dfs(
entityset=es, target_dataframe_name="wind-turbine",
trans_primitives=["day", "year", "month", "weekday"],
max_depth=1)
feature_matrix_enc, features_enc = ft.encode_features(
feature_matrix, feature_defs)
上述代码提取了每个风力涡轮机测量值的日、年、月和星期几。特征编码随后自动将数据集中的分类特征进行 one-hot 编码,包括新的日期字段。
下面的内容是从数据集列列表的摘录,显示了 Featuretools 创建的一些列:
...
'cloud_level = Low',
'cloud_level = Medium',
'cloud_level = Extremely Low',
'cloud_level is unknown',
'MONTH(datetime) = 1',
'MONTH(datetime) = 2',
...
'MONTH(datetime) = 8',
'MONTH(datetime) = 9',
'MONTH(datetime) = 11',
'MONTH(datetime) is unknown',
...
'YEAR(datetime) = 2019',
'YEAR(datetime) = 2018',
'YEAR(datetime) is unknown'
注意分类特征的独热编码:每个值现在都分割成单独的列。这包括未知值的列(例如,YEAR(datetime) is unknown),这说明了处理分类特征中缺失值的另一种方法。我们不是通过使用诸如众数之类的值来填充值,而是有一个列向模型(true或false)发出信号,表示该值缺失。
自动特征工程将我们的列数从 22 列增加到 66 列。这说明了自动特征工程和 AutoML 的另一个一般性警告:自动化可能导致数据集过于复杂。在第六章《使用 LightGBM 解决现实世界数据科学问题》中,我们可以根据对学习算法的理解有选择性地编码特征。LightGBM 可以自动处理分类特征;因此,如果只使用 LightGBM 作为学习算法,则独热编码是多余的。
此外,日期字段可以以数值方式处理。通过应用我们对问题和算法的了解,我们可以降低学习问题的维度,从而简化它。自动系统的易用性必须与专家特征工程的手动工作相平衡,这可能在以后节省时间。
数据集现在已准备好进行模型开发;我们仅用两行代码就完成了数据集的特征工程。
FLAML AutoML
我们现在将探讨模型选择和调优。我们将使用 FLAML 比较五种不同的模型:LightGBM、RandomForest、XGBoost、ExtraTrees 以及 XGBoost 的有限深度版本。我们还想找到最佳模型的最佳参数。整个流程只需两行代码即可使用 FLAML 完成:
automl = flaml.AutoML()
automl.fit(X, y, task="regression", time_budget=60)
上一段代码在 60 秒的时间预算内拟合了一个最优回归模型。FLAML 自动使用保留集来计算验证结果,然后使用默认的 CFO 作为调优器进行优化。
AutoML 类提供“面向任务的 AutoML”。用户设置学习任务,FLAML 完成剩余工作。支持的任务包括:分类、回归、时间序列预测和时间序列分类、排序以及与 NLP 相关的任务,如摘要和词元分类。
fit函数的调用是可定制的。例如,我们可以这样定制它:
automl = flaml.AutoML()
custom_hp = {
"learning_rate": {
"domain": flaml.tune.loguniform(0.0001, 0.05)
}
}
automl.fit(X, y, task="regression", time_budget=120,
metric="mse",
estimator_list=['lgbm', 'xgboost', 'rf'],
custom_hp={
"lgbm": custom_hp
},
hpo_method="bs")
在这里,我们通过显式声明学习率作为一个在范围内对数缩放的均匀变量来自定义超参数搜索空间。设置参数搜索空间的其它选项包括均匀抽样、随机整数抽样以及用于分类参数的选择性抽样。
此外,我们将估计器列表设置为仅关注三种建模算法:LightGBM、随机森林和 XGBoost。最后,我们可以自定义 HPO 算法,在这里我们将其设置为 BlendSearch,它使用之前讨论过的多线程优化方法。
完整的自定义列表可在 https://microsoft.github.io/FLAML/docs/reference/automl/automl/#automl-objects 找到。
一旦调用 fit,我们可以像使用任何其他模型一样使用 AutoML 训练的模型。FLAML 提供了类似 scikit-learn 的 API 用于预测和基于概率的预测(用于分类问题)。
以下代码从给定数据创建预测并计算指标和特征重要性:
y_pred = automl.predict(X)
print(f"r2: {1 - sklearn_metric_loss_score('r2',
y_pred, y)}")
print(f"MSE: {sklearn_metric_loss_score('mse',
y_pred, y)}")
r2: 0.9878605489721696
MSE: 0.08090827806554425
我们还可以通过调用以下代码来获取获胜模型和每个试验模型的最佳超参数配置:
print(automl.best_config)
print(automl.best_config_per_estimator)
print(automl.time_to_find_best_model)
FLAML 的一个最终显著特点是零样本 AutoML,它完全绕过了模型调整的需求。
零样本 AutoML
零样本 AutoML 是 FLAML 的一个功能,其中不执行超参数优化。相反,通过分析算法在广泛数据集上的性能,离线确定合适的超参数配置。该过程可以描述如下:
-
在构建模型之前:
-
使用 AutoML 在许多数据集上训练模型
-
将所有数据集的超参数配置、评估结果和元数据存储为零样本解决方案。
-
-
当为新的问题构建模型时:
-
使用 FLAML 分析新数据集与零样本解决方案结果,以确定合适的超参数。
-
使用超参数在新数据集上训练模型。
-
第一步对于给定的模型类型(例如 LightGBM)只执行一次。之后,对于任何新的问题都可以构建新的模型,无需调整。解决方案是“零样本”,因为对于新的数据集,在第一次拟合时使用了合适的参数。
FLAML 的零样本 AutoML 方法具有许多优点:
-
如前所述,不涉及调整,在解决新问题时节省了大量计算努力和时间。
-
由于不需要调整,因此也不需要验证数据集,更多的数据可以用于训练。
-
用户需要的参与更少。
-
通常,不需要更改代码,正如我们接下来将要看到的。
当然,为模型类型创建零样本解决方案仍然很困难,需要各种数据集和大量计算来训练许多模型。幸运的是,FLAML 为许多流行的模型提供了预训练的零样本解决方案,包括 LightGBM、XGBoost 和 scikit-learn 的随机森林。
要使用零样本解决方案,将常规的 LightGBM 导入替换为 FLAML 包装的版本:
from flaml.default import LGBMRegressor
zs_model = LGBMRegressor()
zs_model.fit(X, y)
调用 fit 会分析 X 中的数据,选择合适的参数,并使用这些参数训练模型。训练只执行一次,不进行任何调整。
这标志着 FLAML 案例研究的结束。正如我们所见,FLAML 提供了一个直观的 API,用于复杂的模型选择和调优功能,这在处理 ML 问题时可以节省很多精力。
概述
总结来说,本章讨论了 AutoML 系统和它们的用途。我们讨论了自动化特征工程、模型选择和调优的典型方法。我们还提到了使用这些系统可能存在的风险和注意事项。
本章还介绍了 FLAML,这是一个提供自动化模型选择和调优工具的 AutoML 库。我们还介绍了 FLAML 提供的两个高效的超参数优化算法:CFO 和 BlendSearch。
FLAML 的实际应用通过案例研究的形式展示。除了 FLAML,我们还展示了一个开源工具 Featuretools,它提供自动化特征工程的功能。我们展示了如何使用 FLAML 在固定时间内开发优化模型。最后,我们提供了使用 FLAML 零样本 AutoML 功能示例,该功能通过分析数据集与已知问题的配置来确定合适的超参数,从而消除了模型调优的需要。
下一章将讨论围绕 LightGBM 模型构建 ML 管道,重点关注导出、打包和部署 LightGBM 模型以用于生产。
参考文献
| [**1] | 王晨,吴强,魏梅,朱易,“FLAML:一个快速且轻量级的 AutoML 库”,发表于 MLSys,2021. |
|---|---|
| [**2] | 吴强,王晨,黄思,关于成本相关超参数的节约优化,2020. |
| [**3] | 王晨,吴强,黄思,赛义德,“使用混合搜索策略进行经济超参数优化”,发表于 ICLR,2021. |
第三部分:使用 LightGBM 的生产就绪机器学习
在第三部分,我们将深入探讨机器学习解决方案在生产环境中的实际应用。我们将揭示机器学习管道的复杂性,确保系统性地处理数据并构建模型以获得一致的结果。MLOps,DevOps 和 ML 的结合,成为焦点,突出了在现实场景中部署和维护强大 ML 系统的重要性。通过实际案例,我们将探讨在平台(如 Google Cloud、Amazon SageMaker 和创新的 PostgresML)上部署 ML 管道,强调每个平台提供的独特优势。最后,我们将探讨分布式计算和基于 GPU 的训练,展示加速训练过程和管理大数据集的有效方法。本部分将强调将 ML 无缝集成到实际、生产就绪的解决方案中,为读者提供将他们的模型在动态环境中实现的知识。
本部分将包括以下章节:
-
第八章**,使用 LightGBM 的机器学习管道和 MLOps
-
第九章**,使用 AWS SageMaker 进行 LightGBM MLOps
-
第十章**,PostgresML 的 LightGBM 模型
-
第十一章**,基于分布式和 GPU 的 LightGBM 学习
第八章:使用 LightGBM 的机器学习流程和 MLOps
本章将重点从数据科学和建模问题转移到为我们的机器学习解决方案构建生产服务。我们介绍了机器学习流程的概念,这是一种处理数据并构建确保一致性和正确性的模型的系统方法。
我们还介绍了 MLOps 的概念,这是一种结合 DevOps 和 ML 的实践,旨在解决部署和维护生产级 ML 系统的需求。
本章包括使用 scikit-learn 构建机器学习流程的示例,封装数据处理、模型构建和调整。我们展示了如何将流程包装在 Web API 中,暴露一个安全的预测端点。最后,我们还探讨了系统的容器化和部署到 Google Cloud。
本章的主要内容包括以下几方面:
-
机器学习流程
-
MLOps 概述
-
部署客户流失的机器学习流程
技术要求
本章包括创建 scikit-learn 流程、训练 LightGBM 模型和构建 FastAPI 应用的示例。设置环境的要求可以在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-8的完整代码示例旁边找到。
介绍机器学习流程
在第六章《使用 LightGBM 解决现实世界数据科学问题》中,我们详细概述了数据科学生命周期,其中包括训练 ML 模型的各种步骤。如果我们只关注训练模型所需的步骤,给定已经收集的数据,那么这些步骤如下:
-
数据清洗和准备
-
特征工程
-
模型训练和调整
-
模型评估
-
模型部署
在之前的案例研究中,我们在处理 Jupyter 笔记本时手动应用了这些步骤。然而,如果我们把背景转移到长期机器学习项目中,会发生什么呢?如果我们需要在有新数据可用时重复该过程,我们就必须遵循相同的程序来成功构建模型。
同样,当我们想要使用模型对新数据进行评分时,我们必须每次都正确地应用这些步骤,并使用正确的参数和配置。
从某种意义上说,这些步骤形成了一个数据处理流程:数据进入流程,完成时产生一个可部署的模型。
形式上,机器学习流程是一个系统化和自动化的过程,指导机器学习项目的流程。它涉及几个相互关联的阶段,封装了之前列出的步骤。
机器学习管道旨在确保这些任务是有结构的、可重复的且高效的,从而更容易管理复杂的机器学习任务。当处理大型数据集或当将原始数据转换为机器学习模型可用的输入步骤复杂且必须频繁重复时,例如在生产环境中,管道特别有益。
管道中涉及的步骤具有一定的灵活性:根据管道的使用方式,步骤可以添加或删除。一些管道包括数据收集步骤,从各种数据源或数据库中提取数据,并为机器学习建模准备数据。
许多机器学习服务和框架提供功能和方法来实现机器学习管道。Scikit-learn 通过其Pipeline类提供此功能,我们将在下一节中探讨。
Scikit-learn 管道
Scikit-learn 提供Pipeline类作为实现机器学习管道的工具。Pipeline类提供了一个统一的接口来执行一系列与数据和模型相关的任务。管道依赖于 scikit-learn 的标准fit和transform接口来实现操作的链式调用。每个管道由任意数量的中间步骤组成,这些步骤必须是transforms。一个转换必须实现fit和transform,Pipeline类依次对每个转换进行操作,首先将数据传递给fit,然后传递给transform。最后的步骤,通常涉及将模型拟合到数据上,只需要实现fit方法。转换通常是预处理步骤,用于转换或增强数据。
使用 scikit-learn 管道的主要优势是确保工作流程被清晰地实现,并且是可重复的。这有助于避免常见的错误,例如在预处理步骤中将测试数据的统计信息泄露到训练模型中。通过在管道中包含预处理步骤,我们确保在训练期间以及当模型用于预测新数据时,应用相同的步骤。
此外,scikit-learn 管道可以与模型选择和超参数调整工具结合使用,例如网格搜索和交叉验证。我们可以通过定义预处理步骤和最终估计器的参数网格来使用网格搜索自动选择整个管道中最佳参数。这可以显著简化代码并减少复杂机器学习工作流程中的错误。FLAML 等工具也具备与 scikit-learn 管道的集成功能。
例如,可以创建一个简单的管道如下:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
pipeline = Pipeline(
[
('scaler', StandardScaler()),
('linear', LinearRegression())
]
)
在这里,该流程由两个步骤组成:一个缩放步骤,用于标准化数据,以及一个最终步骤,用于拟合线性回归模型。
scikit-learn 的Pipeline的强大之处在于它可以像使用任何其他估计器或模型一样使用:
pipeline.fit(X_train, y_train)
pipeline.predict(X_train, y_train)
这为我们提供了一个统一的接口,用于封装在管道中的所有步骤,并使可重复性变得简单。此外,我们可以像其他 scikit-learn 模型一样导出管道,以便部署管道及其封装的所有步骤:
import joblib
joblib.dump(pipeline, "linear_pipeline.pkl")
我们将在下一节中展示更多使用 scikit-learn 管道的示例。
尽管我们花费了大量时间解决与数据处理、构建和调整模型相关的问题,但我们还没有深入探讨模型训练后的情况。正是在这里,MLOps 的世界发挥了作用。下一节将提供详细的概述。
理解 MLOps
机器学习运维(MLOps)是一种将机器学习和系统运维领域融合的实践。它旨在标准化和简化机器学习模型开发和部署的生命周期,从而提高业务环境中机器学习解决方案的效率和效果。在许多方面,MLOps 可以被视为对将机器学习投入运营所面临的挑战的回应,将 DevOps 原则引入机器学习世界。
MLOps 旨在将数据科学家(他们通常专注于模型创建、实验和评估)和运维专业人员(他们处理部署、监控和维护)聚集在一起。目标是促进这些团队之间的更好协作,从而实现更快、更稳健的模型部署。
MLOps 的重要性通过机器学习系统所提出的独特挑战得到了强调。与传统的软件系统相比,机器学习系统更加动态且难以预测,这可能导致在可靠性、鲁棒性方面出现潜在挑战,尤其是在快速变化的生产环境中。
MLOps 的核心目标是通过自动化机器学习管道来加速机器学习生命周期,从而促进更快地进行实验和部署。这通过自动化多个阶段来实现,包括数据预处理、特征工程、模型训练、模型验证和部署。MLOps 的另一个关键方面是确保可重复性。鉴于机器学习模型的动态特性,精确复制结果可能具有挑战性,尤其是在使用新数据进行模型重新训练时。MLOps 强调代码、数据和模型配置版本控制的重要性,这确保了每个实验都可以精确重现,这对于调试和审计至关重要。
监控也是 MLOps 的一个关键部分。一旦模型部署,监控其性能和持续验证其预测至关重要。MLOps 强调需要强大的监控工具,这些工具可以跟踪模型性能、输入数据质量和其他关键指标。这些指标中的异常可能表明模型需要重新训练或调试。
MLOps 还鼓励使用稳健的 ML 测试实践。ML 测试包括传统的软件测试实践,如单元测试和集成测试,但也包括更多 ML 特定的测试,如验证模型预测的统计属性。
MLOps 还侧重于管理和扩展 ML 部署。在现实世界中,ML 模型可能需要每秒处理数千甚至数百万个预测。在这里,容器化和无服务器计算平台等 DevOps 实践发挥作用,以促进部署和扩展自动化。
重要的是要注意 MLOps 如何融入更广泛的软件生态系统。就像 DevOps 在软件工程中弥合了开发和运维之间的差距一样,MLOps 旨在为 ML 做同样的事情。通过促进共同理解和责任,MLOps 可以导致更成功的 ML 项目。
MLOps 是一个快速发展的领域,随着更多企业采用 ML,其重要性日益增加。通过将 DevOps 的原则应用于 ML 的独特挑战,MLOps 提供了一个从初始实验到稳健、可扩展部署的端到端 ML 生命周期的框架。MLOps 强调标准化、自动化、可重复性、监控、测试和协作,以实现高吞吐量的 ML 系统。
现在,我们将通过一个实际示例来查看使用 scikit-learn 创建 ML 管道并将其部署在 REST API 背后的情况。
部署客户流失的 ML 管道
在我们的实际示例中,我们将使用我们在第五章中使用的电信(telco)客户流失数据集,即使用 Optuna 进行 LightGBM 参数优化。该数据集包含每个客户的描述性信息(例如性别、账单信息和费用),以及客户是否已离开电信提供商(流失为是或否)。我们的任务是构建一个分类模型来预测流失。
此外,我们希望将模型部署在 REST API 后面,以便它可以集成到一个更广泛的软件系统中。REST API 应该有一个端点,用于对传递给 API 的数据进行预测。
我们将使用FastAPI,一个现代、高性能的 Python 网络框架来构建我们的 API。最后,我们将使用 Docker 将我们的模型和 API 部署到 Google Cloud Platform。
使用 scikit-learn 构建 ML 管道
我们将首先使用 scikit-learn 的Pipeline工具集构建一个 ML 管道。我们的管道应该封装数据清洗和特征工程步骤,然后构建和调整适当的模型。
我们将评估两种建模算法:LightGBM 和随机森林,因此,无需对数据进行任何缩放或归一化。然而,数据集为每个客户都有一个唯一的标识符,即customerID,我们需要将其删除。
此外,数据集包括数值和分类特征,我们必须对分类特征实现独热编码。
管道预处理步骤
要在 scikit-learn 管道中执行这些步骤,我们将使用 ColumnTransformer。ColumnTransformer 是一个仅对数据集的子集列操作的 Pipeline 转换器。转换器接受一个元组列表,形式为 (name, transformer, columns)。它将子转换器应用于指定的列,并将结果连接起来,使得所有结果特征都成为同一个结果集的一部分。
例如,考虑以下 DataFrame 和 ColumnTransformer:
df = pd.DataFrame({
"price": [29.99, 99.99, 19.99],
"description": ["Luxury goods", "Outdoor goods",
"Sports equipment"],
})
ct = ColumnTransformer(
[("scaling", MinMaxScaler(), ["price"]),
("vectorize", TfidfVectorizer(), "description")])
transformed = ct.fit_transform(df)
这里,我们有一个包含两列的 DataFrame:price 和 description。创建了一个包含两个子转换器的列转换器:一个缩放转换器和向量器。缩放转换器仅对 price 列应用最小-最大缩放。向量器仅对 description 列应用 TF-IDF 向量化。当调用 fit_transform 时,返回一个 单个 数组,包含缩放后的价格列和表示词向量的列。
注意
TF-IDF,或词频-逆文档频率,只是从文本中提取特征的一种方式。分析和提取文本特征,以及自然语言处理,是机器学习中的一个广泛领域,我们在这里不会深入探讨。我们鼓励您进一步阅读有关该主题的内容,请参阅scikit-learn.org/stable/modules/feature_extraction.xhtml#text-feature-extraction。
我们可以将 Customer Churn 数据集的预处理设置为一个单独的 ColumnTransformer。我们首先定义两个单独的转换器,id_transformer 和 encode_transformer,它们应用于 ID 列和分类特征:
id_transformer = (
"customer_id",
CustomerIdTransformer(id_columns),
id_columns
)
encode_transformer = (
"encoder",
OneHotEncoder(sparse_output=False),
categorical_features
)
然后将单独的转换器组合成 ColumnTransformer:
preprocessor = ColumnTransformer(
transformers=[
id_transformer,
encode_transformer,
],
remainder='passthrough'
)
ColumnTransformer 使用 remainder='passthrough' 参数定义。remainder 参数指定了 ColumnTransformer 不转换的列的处理方式。默认情况下,这些列会被删除,但我们需要将它们原样传递,以便包含在数据集中。
编码转换器创建并应用 OneHotEncoder 到分类特征上。
为了说明目的,我们创建了一个自定义的转换器类来维护 ID 列的列表,并在转换过程中从数据中删除它们。
类在这里展示:
class CustomerIdTransformer(BaseEstimator, TransformerMixin):
def __init__(self, id_columns):
self.id_columns = id_columns
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
return X.drop(columns=self.id_columns, axis=1)
如我们所见,该类扩展了 scikit-learn 基础类 BaseEstimator 和 TransformerMixin,并且必须实现 fit 和 transform 方法。实现这些方法也使其适合作为管道中的转换器。如果需要,实现 fit 是可选的;在我们的情况下,fit 过程中没有任何操作。我们的转换步骤会删除相关列。
注意
在管道中封装删除无关列(在这种情况下,ID 列)的功能很重要。当将管道部署到生产使用时,我们期望在做出预测时将这些列传递给管道。作为管道的一部分删除它们简化了模型消费者对管道的使用,并减少了用户出错的机会。
这完成了预处理所需的转换,我们现在可以继续下一步:拟合和调整模型。
管道建模步骤
对于管道建模部分,我们将使用 FLAML。我们也将利用这个机会展示如何将参数传递给管道内的步骤。首先,我们定义我们的 AutoML 模型的设置:
automl_settings = {
"time_budget": 120,
"metric": "accuracy",
"task": "classification",
"estimator_list": ["lgbm", "rf"],
"custom_hp": {
"n_estimators": {
"domain": flaml.tune.uniform(20, 500)
}
},
"verbose": -1
}
上述代码设置了我们的时间预算、优化指标和分类任务,并将估计器限制在 LightGBM 和随机森林模型。最后,我们通过指定 n_estimators 应该在 20 到 500 之间均匀采样来自定义搜索空间。
管道需要将构成步骤的参数以步骤名称和双下划线为前缀。我们可以在管道中设置一个字典,将这些参数传递给 AutoML 类:
pipeline_settings = {
f"automl__{key}": value for key, value in
automl_settings.items()
}
在这里,automl 是管道中步骤的名称。因此,例如,时间预算和指标的参数分别设置为 automl__time_budget: 120 和 automl__metric: accuracy。
最后,我们可以添加 FLAML 的 AutoML 估计器:
automl = flaml.AutoML()
pipeline = Pipeline(
steps=[("preprocessor", preprocessor),
("automl", automl)]
)
以下图显示了最终的管道:

图 8.1 – 客户流失预测的最终 ML 管道
图 8**.1 显示了一个由两个子转换器组成的 ColumnTransformer,它们将数据输入到 AutoML 估计器中。
模型训练和验证
现在我们已经准备好将管道拟合到我们的数据中,传递我们之前设置的管道设置。管道支持标准的 scikit-learn API,因此我们可以调用管道本身:
pipeline.fit(X, y, **pipeline_settings)
运行 fit 执行所有预处理步骤,然后将数据传递给 AutoML 建模和调整。单个 Pipeline 对象展示了 ML 管道的强大功能:包括训练和调整后的模型在内的整个端到端过程被封装并便携,我们可以像使用单个模型一样使用管道。例如,以下代码对训练数据执行 F1 分数:
print(f"F1: {f1_score(pipeline.predict(X), y,
pos_label='Yes')}")
要导出管道,我们使用 joblib 将模型序列化到文件中:
joblib.dump(pipeline, "churn_pipeline.pkl")
导出管道允许我们在生产代码中重新实例化并使用它。接下来,我们将探讨为我们的模型构建 API。
在这个阶段,我们的管道(它封装了预处理、训练、优化和验证)已经定义,我们准备将其部署到系统中。我们将通过用 FastAPI 包装我们的模型来实现这一点。
使用 FastAPI 构建 ML API
现在我们将探讨围绕我们的流水线构建 REST API,使流水线的消费者可以通过 Web 请求获取预测。为模型构建 Web API 也简化了与其他系统和服务的集成,并且是微服务架构中集成标准的方法。
要构建 API,我们使用 Python 库 FastAPI。
FastAPI
FastAPI 是一个现代、高性能的 Web 框架,用于使用 Python 3.6+构建 API。它从头开始设计,易于使用并支持高性能的 API 开发。FastAPI 的关键特性是其速度和易用性,使其成为开发健壮、生产就绪 API 的绝佳选择。FastAPI 广泛采用 Python 的类型检查,有助于在开发过程中早期捕获错误。它还使用这些类型提示来提供数据验证、序列化和文档,减少了开发者需要编写的样板代码。
FastAPI 的性能是其定义性的特征之一。它与 Node.js 相当,并且比传统的 Python 框架快得多。这种速度是通过其使用 Starlette 进行 Web 部分和 Pydantic 进行数据部分,以及其非阻塞特性来实现的,这使得它适合处理大量并发请求。
FastAPI 提供了自动交互式 API 文档,这在开发复杂 API 时是一个很大的优势。使用 FastAPI,开发者可以通过 Swagger UI 访问自动生成的交互式 API 文档。Swagger UI 还提供了与 REST 资源交互的功能,无需编写代码或使用外部工具。这一特性使 FastAPI 非常适用于开发者,并加速了开发过程。
FastAPI 还支持行业标准的安全协议,如 OAuth2,并提供工具以简化实现。FastAPI 的大部分工具都依赖于其依赖注入系统,允许开发者高效地管理依赖项和处理共享资源。
FastAPI 因其易用性和高性能,非常适合构建用于机器学习模型的 Web API 和微服务,这使得机器学习工程师可以专注于生产机器学习部署的其他众多问题。
使用 FastAPI 进行构建
要使用 FastAPI 创建 REST API,我们可以创建一个新的 Python 脚本并实例化 FastAPI 实例。实例启动后,我们可以从文件中加载我们的模型:
app = FastAPI()
model = joblib.load("churn_pipeline.pkl")
在应用程序启动时加载模型会增加启动时间,但确保 API 在应用程序启动完成后即可准备就绪以处理请求。
接下来,我们需要实现一个 REST 端点以进行预测。我们的端点接受输入数据,并以 JSON 格式返回预测。输入 JSON 是一个 JSON 对象的数组,如下所示:
[
{
"customerID": "1580-BMCMR",
...
"MonthlyCharges": 87.3,
"TotalCharges": "1637.3"
},
{
"customerID": "4304-XUMGI",
...
"MonthlyCharges": 75.15,
"TotalCharges": "3822.45"
}
]
使用 FastAPI,我们通过创建一个接受输入数据作为参数的函数来实现 REST 端点。FastAPI 将前面的 JSON 结构序列化为 Python 字典列表。因此,我们的函数签名实现如下:
@app.post('/predict')
def predict_instances(
instances: list[dict[str, str]]
):
我们使用 FastAPI 的post装饰器装饰该函数,指定端点路径('/predict')。
为了对模型进行实际预测,我们将字典转换为 DataFrame 并执行预测:
instance_frame = pd.DataFrame(instances)
predictions = model.predict_proba(instance_frame)
我们使用predict_proba来获取每个类(是或否)的概率,因为我们希望将此附加信息发送给我们的 API 消费者。在预测的同时返回概率是一种推荐的做法,因为这使 API 消费者在使用预测时拥有更多的控制权。API 消费者可以根据预测的使用方式来决定什么概率阈值对于他们的应用程序来说是足够的。
为了以 JSON 格式返回结果,我们构建一个字典,然后 FastAPI 将其序列化为 JSON。我们使用 NumPy 的argmax来获取最高概率的索引以确定预测的类别,并使用amax来获取最高的概率本身:
results = {}
for i, row in enumerate(predictions):
prediction = model.classes_[np.argmax(row)]
probability = np.amax(row)
results[i] = {"prediction": prediction,
"probability": probability}
return results
前面的代码为输入列表中的每个数据实例生成一个prediction对象,使用列表中的位置作为索引。当端点被调用时,将返回以下 JSON:
{
"0": {
"prediction": "Yes",
"probability": 0.9758797243307111
},
"1": {
"prediction": "No",
"probability": 0.8896770039274629
},
"2": {
"prediction": "No",
"probability": 0.9149225087944103
}
}
我们现在已经构建了 API 端点的核心。然而,我们还得注意非功能性关注点,例如安全性。通常,机器学习工程师会忽视诸如安全性或性能等方面的内容,而只关注机器学习相关的问题。我们不应犯这样的错误,必须确保我们给予这些关注点必要的关注。
保护 API
为了保护我们的端点,我们将使用 HTTP 基本认证。我们使用预置的用户名和密码,这些我们从环境中读取。这允许我们在部署期间安全地传递这些凭证到应用程序中,避免了硬编码凭证等陷阱。我们的端点还需要增强以接受用户的凭证。HTTP 基本认证凭证作为 HTTP 头发送。
我们可以按以下方式实现。我们首先为 FastAPI 设置安全措施并从环境中读取凭证:
security = HTTPBasic()
USER = bytes(os.getenv("CHURN_USER"), "utf-8")
PASSWORD = bytes(os.getenv("CHURN_PASSWORD"), "utf-8")
然后,我们在endpoint函数中添加以下内容:
@app.post('/predict')
def predict_instances(
credentials: Annotated[HTTPBasicCredentials,
Depends(security)],
instances: list[dict[str, str]]
):
authenticate(credentials.username.encode("utf-8"),
credentials.password.encode("utf-8"))
authenticate函数验证接收到的凭证是否与我们从环境中获取的 API 凭证匹配。我们可以使用 Python 的 secrets 库来进行验证:
def authenticate(username: bytes, password: bytes):
valid_user = secrets.compare_digest(
username, USER
)
valid_password = secrets.compare_digest(
password, PASSWORD
)
if not (valid_user and valid_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return username
如果凭证无效,我们将抛出一个带有 HTTP 状态码401的异常,表示消费者未授权。
我们的 API 端点现在已完全实现、安全并准备好部署。为了部署我们的 API,我们将使用 Docker 进行容器化。
容器化我们的 API
我们可以使用以下 Dockerfile 为我们的 API 构建一个 Docker 容器:
FROM python:3.10-slim
RUN apt-get update && apt-get install -y --no-install-recommends apt-utils
RUN apt-get -y install curl
RUN apt-get install libgomp1
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD [ "uvicorn", "telco_churn_api:app", "--host", "0.0.0.0", "--port", "8080" ]
Dockerfile 非常简单:我们从一个基于 Python 3.10 的基础镜像开始,安装 LightGBM 需要的某些操作系统依赖项(libgomp1)。然后我们设置 FastAPI 应用程序:我们复制 Python 的requirements文件,安装所有依赖项,然后复制必要的源文件(使用COPY . .)。
最后,我们运行一个监听所有地址的端口8080的 Uvicorn 服务器。Uvicorn 是 Python 的 ASGI 网络服务器实现,支持异步 I/O,显著提高了网络服务器的吞吐量。我们绑定到端口8080,这是我们的部署平台默认的端口。
我们可以使用以下命令构建和运行 Docker 镜像,传递用户名和密码环境变量:
docker build . -t churn_api:latest
docker run --rm -it -e CHURN_USER=***** -e CHURN_PASSWORD=*********** -p 8080:8080 churn_api:latest
API 现在应该可以在你的本地主机上通过端口8080访问,并受到你在环境变量中提供的凭证的保护。
在我们的应用程序容器化后,我们就可以将应用程序部署到任何支持容器的平台。对于 churn 应用程序,我们将将其部署到 Google Cloud Platform。
将 LightGBM 部署到 Google Cloud
我们将利用Google Cloud Run平台将我们的应用程序部署到 Google Cloud Platform。
Google Cloud Run 是一个无服务器平台,允许你在不担心基础设施管理的情况下开发和运行应用程序。Cloud Run 允许开发者在一个安全、可扩展、零运维的环境中运行他们的应用程序。Cloud Run 是完全管理的,这意味着所有基础设施(如服务器和负载均衡器)都被抽象化,使用户能够专注于运行他们的应用程序。Cloud Run 还支持完全自动扩展,运行中的容器数量会自动增加以响应增加的负载。Cloud Run 也非常经济高效,因为你只有在容器运行并处理请求时才会被收费。
要使用 Cloud Run,你需要一个 Google Cloud 账户,并需要创建一个 Google Cloud 项目,启用计费,并设置和初始化Google Cloud CLI。以下资源将指导你完成这些步骤:
一旦完成 Google Cloud 的设置,我们就可以使用 CLI 部署我们的 API。这可以通过单个命令完成:
gcloud run deploy --set-env-vars CHURN_USER=*****,CHURN_PASSWORD=***********
运行命令会提示你输入服务名称和部署服务所在的区域。我们还会设置用于安全凭证所需的环境变量。对于部署,Cloud Run 会为你创建 Cloud Build,它会自动构建和存储 Docker 容器,然后将其部署到 Cloud Run。
一旦 Cloud Run 命令完成,我们就部署了一个安全、可扩展的 RESTful Web API,该 API 为我们客户的流失 ML 管道提供服务。
摘要
本章介绍了 ML 管道,说明了其在实现 ML 解决方案时的一致性、正确性和可移植性的优势。
对新兴的 MLOps 领域进行了概述,这是一个结合 DevOps 和 ML 以实现经过测试、可扩展、安全且可观察的生产 ML 系统的实践。
此外,我们还讨论了 scikit-learn 的Pipeline类,这是一个使用熟悉的 scikit-learn API 实现 ML 管道的工具集。
还提供了一个实现客户流失 ML 管道的端到端示例。我们展示了如何创建一个 scikit-learn 管道,该管道执行预处理、建模和调优,并且可以导出用于软件系统。然后,我们使用 FastAPI 构建了一个安全的 RESTful Web API,该 API 提供了一个从我们的客户流失管道获取预测的端点。最后,我们使用 Cloud Run 服务将我们的 API 部署到 Google Cloud Platform。
尽管我们的部署是安全的且完全可扩展的,Cloud Run 提供了可观察性、指标和日志,但我们的部署没有解决一些 ML 特定的方面:模型漂移、模型性能监控和重新训练。
在下一章中,我们将探讨一个专门的 ML 云服务——AWS SageMaker,它提供了一个针对特定平台构建和托管基于云的 ML 管道的解决方案。
第九章:使用 AWS SageMaker 进行 LightGBM MLOps
在第八章“使用 LightGBM 的机器学习管道和 MLOps”中,我们使用 scikit-learn 构建了一个端到端的 ML 管道。我们还探讨了将管道封装在 REST API 中,并将我们的 API 部署到云端。
本章将探讨使用Amazon SageMaker开发和部署管道。SageMaker 是 Amazon Web Services(AWS)提供的一套完整的用于开发、托管、监控和维护 ML 解决方案的生产服务。
我们将通过查看高级主题,如检测训练模型中的偏差和自动化部署到完全可扩展的无服务器 Web 端点,来扩展我们的 ML 管道功能。
本章将涵盖以下主要内容:
-
AWS 和 SageMaker 简介
-
模型可解释性和偏差
-
使用 SageMaker 构建端到端管道
技术要求
本章深入探讨了使用 Amazon SageMaker 构建 ML 模型和管道。您需要访问一个 Amazon 账户,并且您还必须配置一种支付方式。请注意,运行本章的示例代码将在 AWS 上产生费用。本章的完整笔记本和脚本可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-9找到。
AWS 和 SageMaker 简介
本节提供了 AWS 的高级概述,并深入探讨了 SageMaker,AWS 的 ML 服务。
AWS
AWS 是全球云计算市场的主要参与者之一。AWS 提供许多基于云的产品和服务,包括数据库、机器学习(ML)、分析、网络、存储、开发工具和企业应用程序。AWS 背后的理念是为企业提供一种经济实惠且可扩展的解决方案,以满足其计算需求,无论其规模或行业如何。
AWS 的一个关键优势是弹性,这意味着服务器和服务可以快速随意地停止和启动,从零台机器扩展到数千台。服务的弹性与其主要定价模式“按使用付费”相辅相成,这意味着客户只需为使用的服务和资源付费,无需任何预付成本或长期合同。这种弹性和定价允许企业根据需要按需和细粒度地扩展计算需求,然后只为他们使用的付费。这种方法已经改变了企业扩展 IT 资源和应用程序的方式,使他们能够快速响应不断变化的企业需求,而无需承担与硬件和软件采购和维护相关的传统高昂成本。
另一个优势是 AWS 的全球覆盖范围。AWS 服务在全球许多地区都可用。区域在地理上是分开的,每个区域进一步划分为可用区。区域-区域设置允许用户创建全球分布和冗余的基础设施,以最大化弹性和为灾难恢复进行设计。区域数据中心还允许用户在靠近最终用户的地方创建服务器和服务,以最小化延迟。
核心服务
核心 AWS 服务提供计算、网络和存储能力。AWS 的计算服务包括 Amazon Elastic Compute Cloud(EC2),为用户提供可配置的虚拟机,以及 AWS Lambda,一个无服务器计算平台,允许您在不需要配置和管理服务器的情况下运行代码。在机器学习中,EC2 实例和 Lambda 函数通常用于通过 API 端点训练、验证或提供服务模型。EC2 服务器的弹性特性允许机器学习工程师将训练服务器扩展到数万个,这可以显著加快训练或参数调整任务。
AWS 的存储和数据库服务,如 Amazon Simple Storage Service(S3)和 Amazon RDS(关系数据库服务),提供可靠、可扩展和安全的存储解决方案。这些服务管理存储基础设施,并提供高级功能,如备份、补丁管理和垂直和水平扩展。S3 是广泛用于数据工程和机器学习的服务。S3 提供低成本、高度冗余的安全存储,可扩展到超过艾字节。
AWS 还提供数据仓库解决方案,即 Amazon Redshift。大型企业经常使用 Redshift 作为仓库或数据湖的基础,这意味着它通常是机器学习解决方案的数据源。
AWS 还提供网络服务,以帮助企业在复杂的网络和隔离需求方面取得成功。AWS Direct Connect 允许客户从客户的站点设置一个专用网络连接到 AWS 云。路由和域名服务器可以使用 Amazon Route 53,一个灵活且可扩展的 域名系统(DNS)服务进行管理。
然而,在众多网络服务中,首要的是 Amazon Virtual Private Cloud(VPC)。VPC 为客户提供配置完全隔离的虚拟网络的能力。客户可以精细配置子网、路由表、地址范围、网关和安全组。VPC 允许用户隔离他们的环境和云资源,并控制进出流量,以增加安全性和隐私性。
安全性
任何基础设施方程中的关键部分是安全性。在安全性方面,AWS 提供一个高度安全、可扩展和灵活的云计算环境。AWS 的安全服务,包括 AWS Identity and Access Management(IAM)和 Amazon Security Hub,通过实施强大的安全措施帮助客户保护他们的数据和应用程序。
AWS 还符合多个国际和行业特定的合规标准,例如 GDPR、HIPAA 和 ISO 27001。此外,在数据治理方面,AWS 使遵守数据驻留和隐私要求变得容易。由于 AWS 的区域结构,数据可以保留在特定国家,同时工程师可以访问 AWS 的完整服务套件。
机器学习
AWS 还提供专注于 ML 和 人工智能(AI)的服务。其中许多是针对特定 ML 任务的完全托管服务。AWS Comprehend 提供了许多 自然语言处理(NLP)服务,例如文档处理、命名实体识别和情感分析。Amazon Lookout 是用于设备、指标或图像异常检测的服务。此外,Amazon Rekognition 提供了用于机器视觉用例的服务,例如图像分类和面部识别。
对我们来说特别感兴趣的是 Amazon SageMaker,这是一个完整的 ML 平台,使我们能够在亚马逊云中创建、训练和部署 ML 模型。下一节将详细讨论 SageMaker。
SageMaker
Amazon SageMaker 是一个端到端的 ML 平台,允许数据科学家与数据一起工作,并开发、训练、部署和监控 ML 模型。SageMaker 完全托管,因此无需配置或管理服务器。
亚马逊 SageMaker 的主要吸引力在于其作为一个平台的全面性。它涵盖了机器学习(ML)过程的各个方面,包括数据标注、模型构建、训练、调优、部署、管理和监控。通过处理这些方面,SageMaker 允许开发者和数据科学家专注于核心的 ML 任务,而不是管理基础设施。
正如我们之前讨论的,ML 生命周期始于数据收集,这通常需要手动数据标注。为此,SageMaker 提供了一个名为 SageMaker Ground Truth 的服务。该服务使高效标注 ML 数据集变得容易。通过使用自动标注工作流程,它可以显著减少与数据标注通常相关的耗时和成本,并且还提供了一支用于手动数据标注任务的工作队伍。此外,SageMaker 还提供了 Data Wrangler 服务,该服务有助于数据准备和 探索性数据分析(EDA)。Data Wrangler 提供了从 S3、Redshift 和其他平台查询数据的功能,然后从单一可视化界面中净化、可视化和理解数据。
SageMaker 为模型训练阶段提供了一个完全管理的服务,可以通过Training Jobs处理大规模、分布式模型训练。该服务旨在灵活和适应性强,使用户能够根据需要优化他们的机器学习模型。用户只需指定其数据的位置,通常是 S3 和机器学习算法,SageMaker 就会负责其余的训练过程。模型训练服务充分利用了底层 AWS 基础设施的弹性:可以快速创建多个服务器来执行训练任务,训练完成后丢弃以节省成本。
这种范式也扩展到了超参数调整。为了简化超参数优化,SageMaker 提供了一个自动模型调优功能。提供了许多调优算法,例如 Optuna 或 FLAML,并且可以在多个服务器上运行调优。
SageMaker 还通过SageMaker Autopilot支持更全面的 AutoML 体验。Autopilot 是一种服务,它能够实现自动模型创建。用户只需提供原始数据并设置目标;然后,Autopilot 会自动探索不同的解决方案以找到最佳模型。Autopilot 提供了对整个过程的完全可见性,以便数据科学家可以了解模型是如何创建的,并做出任何必要的调整。
一旦模型经过训练和优化,就必须部署。SageMaker 通过提供一键部署过程简化了这一过程。用户可以快速将模型部署到生产环境中,并具有自动扩展功能,无需担心底层基础设施。这种部署自动扩展功能允许用户设置基于指标的策略,以增加或减少后端服务器。例如,如果一段时间内的调用次数超过特定阈值,则可以扩展部署。SageMaker 确保模型的高可用性,并允许进行 A/B 测试以比较不同版本并决定最佳版本。SageMaker 还支持多模型端点,允许用户在单个端点上部署多个模型。
Amazon SageMaker 还提供了监控模型性能和分析部署后模型的功能。SageMaker Model Monitor持续监控已部署模型的品质(对于实时端点)或批量监控(对于异步作业)。可以定义警报,当指标阈值超过时通知用户。Model Monitor 可以根据如准确度等指标监控数据漂移和模型漂移。
最后,SageMaker 既是 AWS 中的一个平台,也是一个软件 SDK。SDK 提供了 Python 和 R 两种语言版本。SageMaker SDK 提供了一系列内置算法和框架,包括对机器学习社区中最受欢迎的算法的支持,如 XGBoost、TensorFlow、PyTorch 和 MXNet。它还支持一个市场,用户可以从 AWS 和其他 SageMaker 用户共享的大量算法和模型包中选择。
SageMaker 中的一个值得关注的部分,简化了模型开发中最重要的一环(偏差和公平性),就是 SageMaker Clarify。
SageMaker Clarify
Amazon SageMaker Clarify 是一个提供更多机器学习模型透明度的工具。SageMaker Clarify 的目标是帮助理解机器学习模型如何进行预测,从而实现模型可解释性和公平性。
SageMaker Clarify 的一个主要特性是其提供模型可解释性的能力。它帮助开发者理解输入数据与模型预测之间的关系。该服务生成特征归因,显示数据集中每个特征如何影响预测,这在许多领域都至关重要,尤其是在需要理解模型预测背后的推理时。除了提供对单个预测的洞察外,SageMaker Clarify 还提供全局解释能力。它衡量输入特征对模型预测的整体重要性,在整个数据集上汇总。特征影响分析允许开发者和数据科学家理解模型的总体行为,帮助他们从全局层面解释不同特征如何驱动模型预测。
此外,Clarify 可以帮助识别训练模型中的潜在偏差。该服务包括预训练和后训练偏差指标,帮助我们了解模型是否不公平地偏向某些群体。检查所有新模型是否存在偏差是最佳实践,但在金融或医疗保健等受监管行业中,偏差预测可能具有严重后果,因此这一点至关重要。
Clarify 通过使用一种称为 **SHapley Additive exPlanations(SHAP)的先进技术来提供模型可解释性。
SHAP
SHAP 是一种基于博弈论的解释任何机器学习模型输出的方法 [1]。SHAP 的目标是提供对单个特征对模型整体预测影响的了解。
实质上,SHAP 值通过将该特定特征值与该特征的基线值进行对比来评估其影响,突出其对预测的贡献。SHAP 值是每个实例中每个特征对预测的公平贡献分配。SHAP 值植根于合作博弈论,代表了对以下问题的解决方案:
“考虑到一个特征在预测结果中的差异,这部分差异中有多少可以归因于 每个特征?”
这些值是使用博弈论中的 Shapley 值概念计算的。Shapley 值通过对比模型预测与该特征存在和不存在的情况来确定特征的重要性。然而,由于模型遇到特征序列可能会影响其预测,Shapley 值考虑了所有可能的顺序。然后,它为特征分配一个重要性值,使得该特征在所有可能的联盟中的平均边际贡献等于这个值。
使用 SHAP 进行模型解释有几个优点。首先,它提供了解释的一致性。如果一个特征的影响发生变化,该特征的归因重要性也会成比例地变化。
其次,SHAP 保证局部准确性,这意味着所有特征的 SHAP 值之和将等于预测与数据集平均预测之间的差异。
使用 SHAP 摘要图可视化 SHAP 值是一个很好的方法。这些图提供了一个俯瞰特征重要性和驱动因素的全景。它们在图上绘制了每个特征的所有 SHAP 值,以便于可视化。图上的每个点代表一个特征和一个实例的 SHAP 值。Y 轴上的位置由特征决定,X 轴上的位置由 SHAP 值决定:

图 9.1 – 人口普查收入数据集的本地解释示例。条形表示 SHAP 值或每个特征在预测此特定实例时的相对重要性
在 SageMaker Clarify 的上下文中,当你运行一个澄清作业时,该服务会为你的数据集中的每个实例生成一组 SHAP 值。SageMaker Clarify 还可以通过在整个数据集上聚合 SHAP 值来提供全局特征重要性度量。
SHAP 值可以帮助你理解复杂的模型行为,突出潜在问题,并随着时间的推移改进你的模型。例如,通过检查 SHAP 值,你可能会发现某个特定特征对你的模型预测的影响比预期的更大,这会促使你探索为什么会发生这种情况。
在本节中,我们探讨了 AWS 以及更具体地,AWS 机器学习服务家族中的 SageMaker 提供了什么。SageMaker 中可用的功能,如模型可解释性、偏差检测和监控,是我们尚未在我们的机器学习管道中实现的部分。在下一节中,我们将探讨使用 SageMaker 构建一个完整的端到端 LightGBM 机器学习管道,包括这些关键步骤。
使用 Amazon SageMaker 构建 LightGBM 机器学习管道
我们将用于构建 SageMaker 管道案例研究的案例数据集来自 第四章,比较 LightGBM、XGBoost 和深度学习 的人口普查收入数据集。此数据集也作为 SageMaker 示例数据集提供,因此如果您是初学者,在 SageMaker 上使用它很容易。
我们将要构建的管道将包括以下步骤:
-
数据预处理。
-
模型训练和调优。
-
模型评估。
-
使用 Clarify 进行偏差和可解释性检查。
-
在 SageMaker 中的模型注册。
-
使用 AWS Lambda 进行模型部署。
这里有一个显示完整管道的图表:

图 9.2 – 用于人口普查收入分类的 SageMaker ML 管道
我们的方法是使用在 SageMaker Studio 中运行的 Jupyter Notebook 创建整个管道。接下来的部分将解释并展示每个管道步骤的代码,从设置 SageMaker 会话开始。
设置 SageMaker 会话
以下步骤假设您已经创建了一个 AWS 账户并设置了一个 SageMaker 域以开始。如果没有,可以参考以下文档来完成这些操作:
-
先决条件:
docs.aws.amazon.com/sagemaker/latest/dg/gs-set-up.xhtml -
加入 SageMaker 域:
docs.aws.amazon.com/sagemaker/latest/dg/gs-studio-onboard.xhtml
我们必须通过 boto3 初始化 SageMaker 会话并创建 S3、SageMaker 和 SageMaker Runtime 客户端以开始:
sess = sagemaker.Session()
region = sess.boto_region_name
s3_client = boto3.client("s3", region_name=region)
sm_client = boto3.client("sagemaker", region_name=region)
sm_runtime_client = boto3.client("sagemaker-runtime")
我们将使用 Amazon S3 来存储我们的训练数据、源代码以及由管道创建的所有数据和工件,例如序列化的模型。我们的数据和工件被分为一个读取桶和一个单独的写入桶。这是一个标准的最佳实践,因为它将数据存储的关注点分开。
SageMaker 会话有一个默认的 S3 桶的概念。如果没有提供默认桶名称,则会生成一个,并为您创建一个桶。在这里,我们获取对桶的引用。这是我们输出或写入桶。读取桶是我们之前创建的一个桶,用于存储我们的训练数据:
write_bucket = sess.default_bucket()
write_prefix = "census-income-pipeline"
read_bucket = "sagemaker-data"
read_prefix = "census-income"
管道中每个步骤的源代码、配置和输出都被捕获在我们 S3 写入桶中的文件夹内。为每个 S3 URI 创建变量很有用,这样可以避免在重复引用数据时出错,如下所示:
input_data_key = f"s3://{read_bucket}/{read_prefix}"
census_income_data_uri = f"{input_data_key}/census-income.csv"
output_data_uri = f"s3://{write_bucket}/{write_prefix}/"
scripts_uri = f"s3://{write_bucket}/{write_prefix}/scripts"
SageMaker 需要我们指定在运行训练、处理、Clarify 和预测作业时想要使用的计算实例类型。在我们的示例中,我们使用 m5.large 实例。大多数 EC2 实例类型都可以与 SageMaker 一起使用。然而,还有一些支持 GPU 和深度学习框架的特殊实例类型也是可用的:
train_model_id, train_model_version, train_scope = "lightgbm-classification-model", "*", "training"
process_instance_type = "ml.m5.large"
train_instance_count = 1
train_instance_type = "ml.m5.large"
predictor_instance_count = 1
predictor_instance_type = "ml.m5.large"
clarify_instance_count = 1
clarify_instance_type = "ml.m5.large"
SageMaker 使用标准的 EC2 实例进行训练,但在实例上运行特定的 Docker 镜像以提供 ML 功能。Amazon SageMaker 为各种 ML 框架和堆栈提供了许多预构建的 Docker 镜像。
SageMaker SDK 还提供了一个函数,用于在使用的 AWS 区域内搜索与我们所需的实例类型兼容的镜像。我们可以使用 retrieve 搜索镜像:
train_image_uri = retrieve(
region="us-east-1",
framework=None,
model_id=train_model_id,
model_version=train_model_version,
image_scope=train_scope,
instance_type=train_instance_type
)
我们将框架参数指定为 None,因为我们自己管理 LightGBM 的安装。
为了参数化我们的管道,我们必须从 sagemaker.workflow.parameters 包中定义 SageMaker 工作流参数。各种参数类型都有包装器可用:
train_instance_type_param = ParameterString(
name="TrainingInstanceType",
default_value=train_instance_type)
train_instance_count_param = ParameterInteger(
name="TrainingInstanceCount",
default_value=train_instance_count)
deploy_instance_type_param = ParameterString(
name="DeployInstanceType",
default_value=predictor_instance_type)
deploy_instance_count_param = ParameterInteger(
name="DeployInstanceCount",
default_value=predictor_instance_count)
在设置了管道参数、S3 数据路径和其他配置变量后,我们可以继续创建管道的预处理步骤。
预处理步骤
设置我们的预处理步骤有两个部分:创建一个执行预处理的 Python 脚本,并创建一个添加到管道中的处理器。
我们将要使用的脚本是一个带有主函数的常规 Python 脚本。我们将使用 scikit-learn 进行预处理。预处理脚本在此处并未完全重现,但在我们的源代码仓库中可用。值得注意的是,当管道执行步骤时,数据将从 S3 检索并添加到预处理实例的本地暂存目录中。从这里,我们可以使用标准的 pandas 工具读取数据:
local_dir = "/opt/ml/processing"
input_data_path = os.path.join("/opt/ml/processing/census-income", "census-income.csv")
logger.info("Reading claims data from {}".format(input_data_path))
df = pd.read_csv(input_data_path)
类似地,在处理完成后,我们可以将结果写入本地目录,SageMaker 会从该目录检索数据并将其上传到 S3:
train_output_path = os.path.join(f"{local_dir}/train", "train.csv")
X_train.to_csv(train_output_path, index=False)
预处理脚本定义后,我们需要将其上传到 S3,以便管道能够使用它:
s3_client.upload_file(
Filename="src/preprocessing.py", Bucket=write_bucket, Key=f"{write_prefix}/scripts/preprocessing.py"
)
我们可以如下定义预处理步骤。首先,我们必须创建一个 SKLearnProcessor 实例:
sklearn_processor = SKLearnProcessor(
framework_version="0.23-1",
role=sagemaker_role,
instance_count=1,
instance_type=process_instance_type,
base_job_name=f"{base_job_name_prefix}-processing",
)
SKLearnProcessor 处理需要 scikit-learn 的作业的处理任务。我们指定了 scikit-learn 框架版本以及我们之前定义的实例类型和数量。
然后将处理器添加到 ProcessingStep 以在管道中使用:
process_step = ProcessingStep(
name="DataProcessing",
processor=sklearn_processor,
inputs=[...],
outputs=[...],
job_arguments=[
"--train-ratio", "0.8",
"--validation-ratio", "0.1",
"--test-ratio", "0.1"
],
code=f"s3://{write_bucket}/{write_prefix}/scripts/preprocessing.py"
)
inputs 和 outputs 使用 ProcessingInput 和 ProcessingOutput 包装器定义,如下所示:
inputs = [ ProcessingInput(source=bank_marketing_data_uri, destination="/opt/ml/processing/bank-marketing") ]
outputs = [ ProcessingOutput(destination=f"{processing_output_uri}/train_data", output_name="train_data",
source="/opt/ml/processing/train"), ... ]
ProcessingStep 接收我们的 scikit-learn 处理器以及数据的输入和输出。ProcessingInput 实例定义了 S3 源和本地目录目标,以方便复制数据(这些是我们在预处理脚本中使用的相同本地目录)。同样,ProcessingOutput 实例接收本地目录源和 S3 目标。我们还设置了作业参数,这些参数作为 CLI 参数传递给预处理脚本。
预处理步骤设置完成后,我们可以继续进行训练。
模型训练和调整
我们以与预处理脚本相同的方式定义训练脚本:一个带有主函数的 Python 脚本,使用我们的标准 Python 工具(如 scikit-learn)训练一个 LightGBM 模型。然而,我们还需要安装 LightGBM 库本身。
安装库的另一种方法是将其构建到 Docker 镜像中,并将其用作 SageMaker 中的训练镜像。这是在 SageMaker 中管理环境的标准方式。然而,它需要大量的工作,并且需要在长期内维护镜像。或者,如果我们只需要安装少量依赖项,我们可以直接从我们的训练脚本中安装,如下所示。
我们必须定义一个辅助函数来安装包,然后使用它来安装 LightGBM:
def install(package):
subprocess.check_call([sys.executable, "-q", "-m", "pip", "install", package])
install("lightgbm")
import lightgbm as lgb
这也有一个优点,即每次我们运行训练时都会安装最新版本(或指定版本)。
在安装了包之后,训练脚本的其余部分将在预处理步骤准备的数据上训练标准的 LGBMClassifier。我们可以从脚本的参数中设置或训练数据和参数:
train_df = pd.read_csv(f"{args.train_data_dir}/train.csv")
val_df = pd.read_csv(f"{args.validation_data_dir}/validation.csv")
params = {
"n_estimators": args.n_estimators,
"learning_rate": args.learning_rate,
"num_leaves": args.num_leaves,
"max_bin": args.max_bin,
}
然后,我们必须进行标准的 scikit-learn 交叉验证评分,将模型拟合到数据,并输出训练和验证分数:
X, y = prepare_data(train_df)
model = lgb.LGBMClassifier(**params)
scores = cross_val_score(model, X, y, scoring="f1_macro")
train_f1 = scores.mean()
model = model.fit(X, y)
X_test, y_test = prepare_data(val_df)
test_f1 = f1_score(y_test, model.predict(X_test))
print(f"[0]#011train-f1:{train_f1:.2f}")
print(f"[0]#011validation-f1:{test_f1:.2f}")
如此所示,脚本接受 CLI 参数来设置超参数。这在优化阶段设置参数时被超参数调整步骤使用。我们可以使用 Python 的 ArgumentParser 来实现这个目的:
parser = argparse.ArgumentParser()
parser.add_argument("--boosting_type", type=str, default="gbdt")
parser.add_argument("--objective", type=str, default="binary")
parser.add_argument("--n_estimators", type=int, default=200)
parser.add_argument("--learning_rate", type=float, default=0.001)
parser.add_argument("--num_leaves", type=int, default=30)
parser.add_argument("--max_bin", type=int, default=300)
我们还可以看到,我们记录了训练和验证的 F1 分数,这使得 SageMaker 和 CloudWatch 能够从日志中提取数据用于报告和评估目的。
最后,我们需要将训练结果写入一个 JSON 文档中。这些结果随后可以在后续的管道处理中使用,并在 SageMaker 界面中作为作业的输出显示。该 JSON 文档存储在磁盘上,与序列化的模型文件一起:
metrics_data = {"hyperparameters": params,
"binary_classification_metrics":
{"validation:f1": {"value": test_f1},"train:f1": {"value":
train_f1}}
}
metrics_location = args.output_data_dir + "/metrics.json"
model_location = args.model_dir + "/lightgbm-model"
with open(metrics_location, "w") as f:
json.dump(metrics_data, f)
with open(model_location, "wb") as f:
joblib.dump(model, f)
与预处理步骤一样,结果被写入到本地目录中,SageMaker 会从中提取并将它们复制到 S3。
脚本定义后,我们可以在管道中创建调整步骤,该步骤训练模型并调整超参数。
我们必须定义一个类似 SKLearnProcessor 的 SageMaker Estimator,它封装了训练的配置,包括对脚本(在 S3 上)的引用:
static_hyperparams = {
"boosting_type": "gbdt",
"objective": "binary",
}
lgb_estimator = Estimator(
source_dir="src",
entry_point="lightgbm_train.py",
output_path=estimator_output_uri,
code_location=estimator_output_uri,
hyperparameters=static_hyperparams,
role=sagemaker_role,
image_uri=train_image_uri,
instance_count=train_instance_count,
instance_type=train_instance_type,
framework_version="1.3-1",
)
然后,我们可以定义我们的 SageMaker HyperparameterTuner,它执行实际的超参数调整。类似于 Optuna 或 FLAML,我们必须使用 SageMaker 包装器指定超参数的有效范围:
hyperparameter_ranges = {
"n_estimators": IntegerParameter(10, 400),
"learning_rate": ContinuousParameter(0.0001, 0.5, scaling_type="Logarithmic"),
"num_leaves": IntegerParameter(2, 200),
"max_bin": IntegerParameter(50, 500)
}
HyperparameterTuner 可以设置如下:
tuner_config_dict = {
"estimator": lgb_estimator,
"max_jobs": 20,
"max_parallel_jobs": 2,
"objective_metric_name": "validation-f1",
"metric_definitions": [{"Name": "validation-f1", "Regex": "validation-f1:([0-9\\.]+)"}],
"hyperparameter_ranges": hyperparameter_ranges,
"base_tuning_job_name": tuning_job_name_prefix,
"strategy": "Random"
}
tuner = HyperparameterTuner(**tuner_config_dict)
SageMaker 支持许多超参数调优策略,包括 Hyperband 调优。更多信息可以在超参数调优的文档中找到:docs.aws.amazon.com/sagemaker/latest/dg/automatic-model-tuning-how-it-works.xhtml。在这里,我们使用了随机搜索,最大作业大小为 20。在这里,AWS 的弹性基础设施可以发挥显著作用。如果我们增加训练实例数量,SageMaker 会自动将训练作业分配到所有机器上。配置额外的机器有一些开销并增加成本,但如果运行数千次试验,它也可以显著减少调优时间。
调优器的指标定义定义了正则表达式,用于从日志中提取结果指标,正如我们在之前的训练脚本中所示。参数优化框架相对于这里定义的指标进行优化,最小化或最大化指标。
定义了超参数调优器后,我们可以创建一个 TuningStep 以包含到管道中:
tuning_step = TuningStep(
name="LGBModelTuning",
tuner=tuner,
inputs={
"train": TrainingInput(...),
"validation": TrainingInput(...),
}
)
我们迄今为止定义的管道步骤准备数据并生成一个序列化到 S3 的训练模型。管道的下一步是创建一个 SageMaker Model,它封装了模型并用于评估、偏差和推理步骤。可以按照以下方式完成:
model = sagemaker.model.Model(
image_uri=train_image_uri,
model_data=tuning_step.get_top_model_s3_uri(
top_k=0, s3_bucket=write_bucket, prefix=model_prefix
),
sagemaker_session=sess,
role=sagemaker_role
)
inputs = sagemaker.inputs.CreateModelInput(instance_type=deploy_instance_type_param)
create_model_step = CreateModelStep(name="CensusIncomeModel", model=model, inputs=inputs)
Model 实例封装了部署和运行模型所需的所有必要配置。我们可以看到 model_data 是从调优步骤中产生的表现最好的模型中获取的。
我们迄今为止定义的管道步骤将生成处理后的数据和训练一个调优模型。处理数据在 S3 中的布局如图 9.3 所示:

图 9.3 – 处理作业结果的 S3 目录布局
如果我们需要进行部署步骤,我们可以继续进行。然而,我们将遵循最佳实践,并在管道中添加质量门,以检查模型的表现和偏差,并对其功能产生见解。
评估、偏差和可解释性
到目前为止,我们已经看到了向 SageMaker 管道添加步骤的一般模式:使用 SageMaker 的配置类设置配置,然后创建相关的管道步骤。
偏差配置
要将偏差检查添加到我们的管道中,我们必须创建以下配置:
bias_config = clarify.BiasConfig(
label_values_or_threshold=[1], facet_name="Sex", facet_values_or_threshold=[0], group_name="Age"
)
model_predictions_config = sagemaker.clarify.ModelPredictedLabelConfig(probability_threshold=0.5)
model_bias_check_config = ModelBiasCheckConfig(
data_config=model_bias_data_config,
data_bias_config=bias_config,
model_config=model_config,
model_predicted_label_config=model_predictions_config,
methods=["DPPL"]
)
BiasConfig 描述了我们要检查哪些方面(特征)。我们选择了 Sex 和 Age,这两个方面在处理人口统计数据时始终是必须检查的。
ModeLBiasCheckConfig 包装了数据配置、模型配置和偏差确认步骤。它还设置了用于偏差检查的方法。在这里,我们使用预测标签中正比例的差异(DPPL)。
DPPL 是一个衡量模型是否对数据的不同方面预测结果不同的指标。DPPL 是通过计算“a”方面和“d”方面的正面预测比例之间的差异来计算的。它通过将训练后的模型预测与数据集中最初存在的偏差进行比较,帮助评估模型预测是否存在偏差。例如,如果一个预测家庭贷款资格的模型对 70% 的男性申请人(方面“a”)预测了积极的结果,而对 60% 的女性申请人(方面“d”)预测了积极的结果,那么 10% 的差异可能表明对方面“d”存在偏见。
DPPL 公式表示如下:
DPPL = q a ′ − q d ′
在这里,q a ′ 是预测的方面“a”获得积极结果的概率,q d ′ 是类似的比例,对于二进制和多类别方面标签,标准化后的 DPPL 值介于 [-1, 1] 之间,而连续标签在 (-∞, +∞) 区间内变化。正 DPPL 值表明方面“a”相对于“d”的正面预测比例更高,表明存在正偏差。相反,负 DPPL 指示方面“d”的正面预测比例更高,表明存在负偏差。接近零的 DPPL 指示两个方面的正面预测比例相对相等,零值表示人口统计学上的完美平等。
您可以使用 ClarifyCheckStep 将偏差检查添加到管道中:
model_bias_check_step = ClarifyCheckStep(
name="ModelBiasCheck",
clarify_check_config=model_bias_check_config,
check_job_config=check_job_config,
skip_check=skip_check_model_bias_param,
register_new_baseline=register_new_baseline_model_bias_param,
supplied_baseline_constraints=supplied_baseline_constraints_model_bias_param
)
可解释性配置
可解释性的配置非常相似。我们不是创建 BiasConfig,而是必须创建 SHAPConfig:
shap_config = sagemaker.clarify.SHAPConfig(
seed=829,
num_samples=100,
agg_method="mean_abs",
save_local_shap_values=True
)
与 SHAPConfig 一起,我们必须创建 ModelExplainabilityCheckConfig 来计算 SHAP 值并创建可解释性报告:
model_explainability_config = ModelExplainabilityCheckConfig(
data_config=model_explainability_data_config,
model_config=model_config,
explainability_config=shap_config
)
然后使用 ClarifyCheckStep 将所有内容组合在一起:
model_explainability_step = ClarifyCheckStep(
name="ModelExplainabilityCheck",
clarify_check_config=model_explainability_config,
check_job_config=check_job_config,
skip_check=skip_check_model_explainability_param,
register_new_baseline=register_new_baseline_model_explainability_param,
supplied_baseline_constraints=supplied_baseline_constraints_model_explainability_param
)
评估
最后,我们还需要使用测试数据评估我们的模型。评估脚本与训练脚本非常相似,只是它从 S3 中提取调整后的模型进行评分。该脚本由一个主函数和两个步骤组成。首先,我们必须引导训练模型并进行评分(在我们的情况下,计算 F1 分数):
...
test_f1 = f1_score(y_test, model.predict(X_test))
# Calculate model evaluation score
logger.debug("Calculating F1 score.")
metric_dict = {
"classification_metrics": {"f1": {"value": test_f1}}
}
然后,我们必须将结果输出到 JSON 文件中:
# Save model evaluation metrics
output_dir = "/opt/ml/processing/evaluation"
pathlib.Path(output_dir).mkdir(parents=True, exist_ok=True)
logger.info("Writing evaluation report with F1: %f", test_f1)
evaluation_path = f"{output_dir}/evaluation.json"
with open(evaluation_path, "w") as f:
f.write(json.dumps(metric_dict))
评估 JSON 用于报告以及依赖于评估指标的后续步骤。
部署和监控 LightGBM 模型
我们现在可以添加我们管道的最终步骤以支持部署。管道的部署部分由三个步骤组成:
-
在 SageMaker 中注册模型。
-
条件检查以验证模型评估是否超过最小阈值。
-
使用 AWS Lambda 函数部署模型端点。
模型注册
要部署我们的模型,我们首先需要在 SageMaker 的 模型注册表 中注册我们的模型。
SageMaker 的模型注册表是一个中央存储库,您可以在其中管理和部署您的模型。
模型注册表提供了以下核心功能:
-
模型版本控制:每次训练并注册模型时,模型注册表中都会为其分配一个版本。这有助于您跟踪模型的各个迭代版本,这在需要比较模型性能、回滚到先前版本或在机器学习项目中保持可重复性时非常有用。
-
审批工作流程:模型注册表支持审批工作流程,模型可以被标记为“待手动审批”、“已批准”或“已拒绝”。这允许团队有效地管理其模型的生命周期,并确保只有经过批准的模型被部署。
-
模型目录:模型注册表充当一个目录,其中集中存储和访问所有模型。注册表中的每个模型都与元数据相关联,例如使用的训练数据、超参数和性能指标。
在注册我们的模型时,我们附加了从评估步骤计算出的指标。这些指标也用于模型漂移检测。
可能存在两种类型的漂移:数据漂移和模型漂移。
数据漂移指的是与我们的模型训练数据相比,输入数据的统计分布发生变化。例如,如果训练数据中男性和女性的比例为 60%到 40%,但用于预测的数据偏向于 80%男性与 20%女性,那么可能发生了漂移。
模型漂移是一种现象,即模型试图预测的目标变量的统计属性随着时间的推移以不可预见的方式发生变化,导致模型性能下降。
数据和模型漂移可能由于环境变化、社会行为、产品使用或其他在模型训练期间未考虑到的因素而发生。
SageMaker 支持漂移的持续监控。SageMaker 计算输入数据和我们所做预测的统计分布。两者都与训练数据中的分布进行比较。如果检测到漂移,SageMaker 可以向 AWS CloudWatch 生成警报。
我们可以按如下方式配置我们的指标:
model_metrics = ModelMetrics(
bias_post_training=MetricsSource(
s3_uri=model_bias_check_step.properties.CalculatedBaselineConstraints,
content_type="application/json"
),
explainability=MetricsSource(
s3_uri=model_explainability_step.properties.CalculatedBaselineConstraints,
content_type="application/json"
),
)
然后,对于漂移指标,我们必须设置DriftCheckBaselines:
drift_check_baselines = DriftCheckBaselines(
bias_post_training_constraints=MetricsSource( s3_uri=model_bias_check_step.properties.BaselineUsedForDriftCheckConstraints, content_type="application/json",
),
explainability_constraints=MetricsSource( s3_uri=model_explainability_step.properties.BaselineUsedForDriftCheckConstraints, content_type="application/json",
),
explainability_config_file=FileSource( s3_uri=model_explainability_config.monitoring_analysis_config_uri, content_type="application/json",
))
然后,我们必须创建一个包含以下代码的模型注册步骤:
register_step = RegisterModel(
name="LGBRegisterModel",
estimator=lgb_estimator,
model_data=tuning_step.get_top_model_s3_uri(
top_k=0, s3_bucket=write_bucket, prefix=model_prefix
),
content_types=["text/csv"],
response_types=["text/csv"],
inference_instances=[predictor_instance_type],
transform_instances=[predictor_instance_type],
model_package_group_name=model_package_group_name,
approval_status=model_approval_status_param,
model_metrics=model_metrics,
drift_check_baselines=drift_check_baselines
)
模型验证
条件检查使用评估步骤中的评估数据来确定模型是否适合部署:
cond_gte = ConditionGreaterThanOrEqualTo(
left=JsonGet(
step_name=evaluation_step.name,
property_file=evaluation_report,
json_path="classification_metrics.f1.value",
),
right=0.9,
)
condition_step = ConditionStep(
name="CheckCensusIncomeLGBEvaluation",
conditions=[cond_gte],
if_steps=[create_model_step, register_step, lambda_deploy_step],
else_steps=[]
)
在这里,我们创建了ConditionStep并比较了 F1 分数与0.9的阈值。如果模型的 F1 分数高于阈值,则可以继续部署。
使用 AWS Lambda 进行部署
部署脚本是一个标准的 AWS Lambda Python 脚本,它定义了一个lambda_handler函数,该函数获取 SageMaker 的客户端连接并继续创建模型端点:
def lambda_handler(event, context):
sm_client = boto3.client("sagemaker")
...
create_endpoint_config_response = sm_client.create_endpoint_config(
EndpointConfigName=endpoint_config_name,
ProductionVariants=[{
"VariantName": "Alltraffic",
"ModelName": model_name,
"InitialInstanceCount": instance_count,
"InstanceType": instance_type,
"InitialVariantWeight": 1}])
create_endpoint_response = sm_client.create_endpoint(
EndpointName=endpoint_name,
EndpointConfigName=endpoint_config_name)
值得注意的是,Lambda 函数不处理模型的请求。它仅在 SageMaker 中创建模型端点。
在 SageMaker 中,端点 是一个获取模型预测的 Web 服务。一旦模型训练完成,训练作业完成后,你需要部署模型以进行实时或批量预测。在 SageMaker 术语中,部署意味着设置端点 – 一个托管、生产就绪的模型。
SageMaker 中的端点是一个可扩展且安全的 RESTful API,你可以用它向模型发送实时推理请求。你的应用程序可以通过 REST API 或 AWS SDK 直接访问端点进行预测。它可以按需扩展和缩减实例,提供灵活性和成本效益。
SageMaker 还支持多模型端点,可以在单个端点上部署多个模型。如果许多模型使用频率不高或不是资源密集型,这个功能可以显著节省成本。
定义了 Lambda 脚本后,可以使用 LambdaStep 将其纳入管道:
lambda_deploy_step = LambdaStep(
name="LambdaStepRealTimeDeploy",
lambda_func=func,
inputs={
"model_name": pipeline_model_name,
"endpoint_config_name": endpoint_config_name,
"endpoint_name": endpoint_name,
"model_package_arn": register_step.steps[0].properties.ModelPackageArn,
"role": sagemaker_role,
"instance_type": deploy_instance_type_param,
"instance_count": deploy_instance_count_param
}
)
注意
模型端点一旦部署就会产生费用,直到其部署期间。一旦你运行了管道,就会创建一个端点。如果你只是实验或测试你的管道,你应该在完成后删除端点。
创建和运行管道
我们的所有管道步骤现在都已就绪,这意味着我们可以创建管道本身。Pipeline 构造函数接受我们已定义的名称和参数:
pipeline = Pipeline(
name=pipeline_name,
parameters=[process_instance_type_param,
train_instance_type_param,
train_instance_count_param,
deploy_instance_type_param,
deploy_instance_count_param,
clarify_instance_type_param,
skip_check_model_bias_param,
register_new_baseline_model_bias_param, supplied_baseline_constraints_model_bias_param,
skip_check_model_explainability_param, register_new_baseline_model_explainability_param, supplied_baseline_constraints_model_explainability_param,
model_approval_status_param],
我们还必须将我们定义的所有步骤作为列表参数传递,并最终更新管道:
steps=[
process_step,
train_step,
evaluation_step,
condition_step
],
sagemaker_session=sess)
pipeline.upsert(role_arn=sagemaker_role)
执行管道是通过调用 start 方法完成的:
start_response = pipeline.start(parameters=dict(
SkipModelBiasCheck=True,
RegisterNewModelBiasBaseline=True,
SkipModelExplainabilityCheck=True,
RegisterNewModelExplainabilityBaseline=True))
注意我们在这里定义的条件。在第一次运行管道时,我们必须在注册新的偏差和可解释性基线的同时跳过模型偏差和可解释性检查。
这两个检查都需要一个现有的基线来运行(否则,没有数据可以检查)。一旦建立了基线,我们就可以在后续运行中禁用跳过检查。
更多关于模型生命周期和创建基线的信息可以在 docs.aws.amazon.com/sagemaker/latest/dg/pipelines-quality-clarify-baseline-lifecycle.xhtml 找到。
结果
当管道执行时,你可以查看执行图以查看每个步骤的状态:

图 9.4 – LightGBM 人口普查收入管道成功执行
一旦管道完成,我们还可以在模型注册表中看到已注册的模型本身:

图 9.5 – SageMaker 模型注册表显示已批准的人口普查收入模型和相关端点
当选择模型时,可以查看偏差和可解释性报告。图 9.6 显示了由管道创建的模型的偏差报告。我们可以看到在 DPPL 中存在轻微的不平衡,但小于训练数据中的类别不平衡。报告表明没有强有力的证据表明存在偏差:

图 9.6 – 展示 DPPL 中轻微不平衡但小于训练数据类别不平衡的 Census Income 模型偏差报告
如 图 9.7 所示的可解释性报告显示了每个特征在 SHAP 值方面的重要性。在这里,我们可以看到 资本收益 和 国家 特征在预测的重要性方面占主导地位:

图 9.7 – 展示资本收益和 Country 特征主导重要性的 Census Income 模型的可解释性报告
偏差和可解释性报告也可以下载为 PDF 格式,可以轻松地与商业或非技术利益相关者共享。
使用端点进行预测
当然,如果我们不能使用部署的模型进行任何预测,那么我们的部署模型就没什么用了。我们可以通过 REST 调用或 Python SDK 使用部署的模型进行预测。以下是一个使用 Python SDK 的示例:
predictor = sagemaker.predictor.Predictor(endpoint_name, sagemaker_session=sess, serializer=CSVSerializer(), deserializer=CSVDeserializer())
payload = test_df.drop(["Target"], axis=1).iloc[:5]
result = predictor.predict(payload.values)
我们使用端点名称和会话获取 SageMaker Predictor,然后我们可以调用 predict 方法,传递一个 NumPy 数组(在本例中是从测试 DataFrame 获取的)。
由此,我们使用 SageMaker 创建了一个完整、端到端、生产就绪的管道。我们的管道包括数据预处理、自动模型调优、偏差验证、漂移检测以及完全可扩展的部署。
摘要
本章介绍了 AWS 和 Amazon SageMaker 作为构建和部署机器学习解决方案的平台。给出了 SageMaker 服务的概述,包括 Clarify 服务,该服务提供高级功能,如模型偏差检查和可解释性。
我们随后使用 SageMaker 服务构建了一个完整的机器学习(ML)管道。该管道包括机器学习生命周期的所有步骤,包括数据准备、模型训练、调优、模型评估、偏差检查、可解释性报告、针对测试数据的验证以及部署到云原生、可扩展的基础设施。
提供了具体的示例来构建管道中的每个步骤,强调全面自动化,旨在实现简单的重新训练和持续监控数据及模型过程。
下一章将探讨另一个名为 PostgresML 的 MLOps 平台。PostgresML 在服务器景观的基石——Postgres 数据库之上提供机器学习功能。
参考文献
| [**1] | S. M. Lundberg 和 S.-I. Lee, 在《神经网络信息处理系统 30 卷进展》中,关于解释模型预测的统一方法, I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan 和 R. Garnett 编著,Curran Associates, Inc., 2017, 第 4765–4774 页. |
|---|---|
| [**2] | R. P. Moro 和 P. Cortez, 银行 营销, 2012. |
第十章:使用 PostgresML 的 LightGBM 模型
在本章中,我们将探讨一个独特的 MLOps 平台,称为PostgresML。PostgresML 是一个 Postgres 数据库扩展,允许您使用 SQL 训练和部署机器学习模型。
PostgresML 和 SQL 与我们在本书中使用的 scikit-learn 编程风格有显著的不同。然而,正如我们在本章中将要看到的,在数据库级别进行机器学习模型开发和部署,在数据移动需求和推理延迟方面具有显著优势。
本章的主要内容包括以下几方面:
-
PostgresML 概述
-
开始使用 PostgresML
-
使用 PostgresML 和 LightGBM 的客户流失案例研究
技术要求
本章包括使用 PostgresML 的实际示例。我们将使用 Docker 来设置 PostgresML 环境,并建议运行示例。本章的代码可在github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-10找到。
介绍 PostgresML
PostgresML(postgresml.org/)是 Postgres 的一个扩展,允许从业者在一个 Postgres 数据库上实现整个机器学习生命周期,用于文本和表格数据。
PostgresML 利用 SQL 作为训练模型、创建部署和进行预测的接口。使用 SQL 意味着模型和数据操作可以无缝结合,并自然地融入 Postgres 数据库数据工程环境中。
拥有一个共享的数据和机器学习平台有许多优势。正如我们在上一章中看到的,使用 SageMaker,在数据移动方面花费了大量的努力。这在机器学习环境中是一个常见问题,其中数据,尤其是事务数据,存在于生产数据库中,需要创建复杂的数据工程工作流程来从生产源提取数据,将数据转换为机器学习使用,并将数据加载到机器学习平台可访问的存储中(例如 SageMaker 的 S3)。
通过将数据存储与机器学习平台相结合,PostgresML 消除了在平台之间移动数据的需求,节省了大量的时间、努力、存储,并可能节省出口成本。
此外,从实时交易数据建模意味着训练数据始终是最新的(直接从记录系统中读取),而不是在刷新后受到限制。这消除了由于处理过时数据或 ETL 作业错误转换或加载数据而产生的错误。
延迟和往返次数
模型部署的典型模式,我们在前面的章节中已经展示过,是将模型部署在 Web API 后面。在微服务术语中,模型部署只是另一个服务,可以由其他服务组成以实现整体系统目标。
作为网络服务部署具有几个优点。首先,当使用如 REST 这样的网络标准进行网络调用时,与其他系统的互操作性简单直接。其次,它允许您独立部署模型代码,与系统其他部分隔离,提供弹性和独立扩展。
然而,将模型作为独立服务部署也有显著的缺点:延迟和网络往返次数。
让我们考虑一个电子商务的例子。在电子商务环境中,一个常见的机器学习问题是欺诈检测。以下是简单电子商务系统的一个系统架构图:

图 10.1 – 简化的电子商务系统架构,说明了功能服务(交易)与机器学习驱动服务(欺诈检测)之间的交互
考虑到图 10.1中的架构,新交易的流程如下:
-
交易被发送到交易服务。
-
交易服务使用新交易的详细信息调用欺诈检测服务。
-
欺诈检测服务接收新交易,从模型存储中加载相关模型(如果需要),从交易存储中加载历史数据,并将预测结果发送给交易服务。
-
交易服务接收欺诈预测,并存储带有相关分类的交易。
在此工作流程上可能存在一些变体。然而,由于交易和欺诈检测服务的分离,处理一笔新交易需要多次网络往返。进行欺诈预测还需要从交易存储中获取历史数据以供模型使用。
网络调用延迟和往返次数给交易增加了显著的开销。如果目标是实现低延迟或实时系统,则需要更复杂的架构组件——例如,为模型和交易数据提供缓存以及更高吞吐量的网络服务。
使用 PostgresML,架构可以简化如下:

图 10.2 – 使用 PostgresML 将机器学习服务与数据存储相结合,允许更简单的系统设计
虽然这个例子过于简化,但重点是,在服务导向架构中使用独立模型服务利用机器学习模型的整体过程中,会添加显著的开销。
通过 PostgresML,我们可以消除单独模型存储的需求,以及加载模型的额外开销,并且更重要的是,将数据存储调用和预测合并为数据存储层上的单个调用,中间没有网络开销。PostgresML 的基准测试发现,在云环境中,简化的架构将性能提高了 40 倍[1]。
然而,这种架构也有缺点。首先,数据库现在是一个单点故障。如果数据库不可用,所有模型和预测能力也将不可用。其次,该架构将数据存储和机器学习建模与推理的关注点混合在一起。根据用例,训练和部署机器学习模型与提供 SQL 查询和存储数据相比,有不同的服务器基础设施需求。关注点的混合可能迫使你在一项或另一项责任上做出妥协,或者显著增加数据库基础设施成本以支持所有用例。
在本节中,我们介绍了 PostgresML,并在概念层面上解释了将我们的数据存储和机器学习服务结合起来的优势。现在,我们将探讨实际设置和开始使用 PostgresML,以及一些基本功能。
开始使用 PostgresML
当然,PostgresML 依赖于已安装的 PostgreSQL。PostgresML 需要 PostgreSQL 11,同时也支持更新的版本。PostgresML 还需要在您的系统上安装 Python 3.7+。支持 ARM 和 Intel/AMD 架构。
注意
本节概述了开始使用 PostgresML 及其在撰写时的功能所需的步骤和依赖项。对于最新信息,请查看官方网站:https://postgresml.org/。运行 PostgresML 最简单的方法是使用 Docker。更多信息,请参阅使用 Docker 快速入门文档:postgresml.org/docs/guides/setup/quick_start_with_docker。
该扩展可以使用官方包工具(如 APT)安装,或者从源代码编译。一旦所有依赖项和扩展都已安装,必须更新postgresql.conf以加载 PostgresML 库,并且必须重新启动数据库服务器:
shared_preload_libraries = 'pgml,pg_stat_statements'
sudo service postgresql restart
安装 PostgresML 后,必须在您计划使用的数据库中创建扩展。这可以通过常规的 PostgreSQL 方式在 SQL 控制台中完成:
CREATE EXTENSION pgml;
检查安装,如下所示:
SELECT pgml.version();
训练模型
现在,让我们看看 PostgresML 提供的功能。如介绍中所述,PostgresML 有一个 SQL API。以下代码示例应在 SQL 控制台中运行。
训练模型的扩展函数如下:
pgml.train(
project_name TEXT,
task TEXT DEFAULT NULL,
relation_name TEXT DEFAULT NULL,
y_column_name TEXT DEFAULT NULL,
algorithm TEXT DEFAULT 'linear',
hyperparams JSONB DEFAULT '{}'::JSONB,
search TEXT DEFAULT NULL,
search_params JSONB DEFAULT '{}'::JSONB,
search_args JSONB DEFAULT '{}'::JSONB,
preprocess JSONB DEFAULT '{}'::JSONB,
test_size REAL DEFAULT 0.25,
test_sampling TEXT DEFAULT 'random'
)
我们需要提供project_name作为第一个参数。PostgresML 将模型和部署组织到项目中,项目通过其名称唯一标识。
接下来,我们指定模型的任务:分类或回归。relation_name和y_column_name设置训练运行所需的数据。关系是定义数据的数据表或视图,Y 列的名称指定关系中的目标列。
这些是训练所需的唯一参数。训练线性模型(默认)可以如下进行:
SELECT * FROM pgml.train(
project_name => 'Regression Project',
task => 'regression',
relation_name => pgml.diabetes',
y_column_name => 'target'
);
当调用pgml.train时,PostgresML 将数据复制到pgml模式中:这确保所有训练运行都是可重复的,并允许使用不同的算法或参数但相同的数据重新运行训练。relation_name和task也仅在项目第一次进行训练时需要。要为项目训练第二个模型,我们可以简化训练调用如下:
SELECT * FROM pgml.train(
'Regression Project ',
algorithm => 'lightgbm'
);
当调用此代码时,将在相同的数据上训练一个 LightGBM 回归模型。
算法参数设置要使用的学习算法。PostgresML 支持各种算法,包括 LightGBM、XGBoost、scikit-learn 的随机森林和额外树,支持向量机(SVMs)、线性模型以及如 K-means 聚类的无监督算法。
默认情况下,25%的数据用作测试集,测试数据是随机选择的。这可以通过test_size和test_sampling参数进行控制。替代测试采样方法从第一行或最后一行选择数据。
超参数优化
PostgresML 支持执行搜索:网格搜索和随机搜索。要设置 HPO 的超参数范围,使用带有search_params参数的 JSON 对象。使用search_args指定 HPO 参数。以下是一个示例:
SELECT * FROM pgml.train('Regression Project',
algorithm => 'lightgbm',
search => 'random',
search_args => '{"n_iter": 100 }',
search_params => '{
"learning_rate": [0.001, 0.1, 0.5],
"n_estimators": [20, 100, 200]
}'
);
预处理
PostgresML 还支持在训练模型时执行某些类型的预处理。与训练数据和配置一样,预处理也存储在项目中,因此可以在使用模型进行预测时应用相同的预处理。
关于预处理,PostgresML 支持对分类变量进行编码、填充缺失值和对数值进行缩放。预处理规则使用 JSON 对象通过preprocess参数设置,如下所示:
SELECT pgml.train(
…
preprocess => '{
"model": {"encode": {"ordinal": ["Ford", "Kia",
"Volkswagen"]}}
"price": {"impute": "mean", scale: "standard"}
"fuel_economy": {"scale": "standard"}
}'
);
在这里,我们对模型特征应用了序数编码。或者,PostgresML 还支持独热编码和目标编码。我们还使用价格的平均值导入缺失值(如列中的NULL所示),并对价格和燃油经济性特征应用了标准(正态)缩放。
部署和预测
训练后,PostgresML 会自动在测试集上计算适当的指标,包括 R²、F1 分数、精确度、召回率、ROC_AUC、准确率和对数损失。如果模型的键指标(回归的 R²和分类的 F1)比当前部署的模型有所提高,PostgresML 将在训练后自动部署模型。
然而,也可以使用pgml.deploy函数手动管理项目的部署:
pgml.deploy(
project_name TEXT,
strategy TEXT DEFAULT 'best_score',
algorithm TEXT DEFAULT NULL
)
PostgresML 支持的部署策略有best_score,它立即部署具有最佳关键指标的模型;most_recent,它部署最近训练的模型;以及rollback,它将当前部署回滚到之前部署的模型。
部署模型后,可以使用pgml.predict函数进行预测:
pgml.predict (
project_name TEXT,
features REAL[]
)
pgml.predict函数接受项目名称和预测特征。特征可以是数组或复合类型。
PostgresML 仪表板
PostgresML 提供了一个基于 Web 的仪表板,以便更方便地访问 PostgresML 的功能。仪表板是独立于 PostgreSQL 部署的,不是管理或充分利用 PostgresML 功能所必需的,因为所有功能也可以通过 SQL 查询访问。
仪表板提供了访问项目、模型、部署和数据快照的列表。有关训练模型的更多详细信息也可以在仪表板中找到,包括超参数设置和训练指标:

图 10.3 – 显示已训练模型列表的 PostgresML 仪表板
除了提供项目、模型和部署的视图外,仪表板还允许创建 SQL 笔记本,类似于 Jupyter 笔记本。这些 SQL 笔记本提供了一个简单的界面,用于与 PostgresML 交互,如果另一个 SQL 控制台不可用的话。
这就结束了我们关于开始使用 PostgresML 的部分。接下来,我们将查看一个从头到尾的案例研究,即训练和部署 PostgresML 模型。
案例研究 – 使用 PostgresML 进行客户流失分析
让我们回顾一下电信提供商的客户流失问题。提醒一下,数据集包括客户及其与电信提供商相关的账户和成本信息。
数据加载和预处理
在现实世界的设置中,我们的数据通常已经存在于 PostgreSQL 数据库中。然而,在我们的示例中,我们将从加载数据开始。首先,我们必须创建数据加载到的表:
CREATE TABLE pgml.telco_churn
(
customerid VARCHAR(100),
gender VARCHAR(100),
seniorcitizen BOOLEAN,
partner VARCHAR(10),
dependents VARCHAR(10),
tenure REAL,
...
monthlycharges VARCHAR(50),
totalcharges VARCHAR(50),
churn VARCHAR(10)
);
注意,在我们的表结构中,对于一些列,类型可能不符合我们的预期:例如,月度和总费用应该是实数值。我们将在预处理阶段解决这个问题。
接下来,我们可以将我们的 CSV 数据加载到表中。PostgreSQL 提供了一个COPY语句来完成此目的:
COPY pgml.telco_churn (customerid,
gender,
seniorcitizen,
partner,
...
streamingtv,
streamingmovies,
contract,
paperlessbilling,
paymentmethod,
monthlycharges,
totalcharges,
churn
) FROM '/tmp/telco-churn.csv'
DELIMITER ','
CSV HEADER;
执行此语句将读取 CSV 文件并将数据添加到我们的表中。
注意
如果你在一个 Docker 容器中运行 PostgresML(推荐以开始使用),必须首先将 CSV 文件复制到容器运行时。可以使用以下命令完成此操作(替换为你自己的容器名称):
docker cp telco/telco-churn.csv postgresml-postgres-1:/tmp/telco-churn.csv。
数据加载后,我们可以进行预处理。我们分三步进行:在表中直接清理数据,创建一个将数据强制转换为适当类型的表视图,并使用 PostgresML 的预处理功能:
UPDATE pgml.telco_churn
SET totalcharges = NULL
WHERE totalcharges = ' ';
我们必须将总费用中的空文本值替换为NULL,以便 PostgresML 稍后填充这些值:
CREATE VIEW pgml.telco_churn_data AS
SELECT gender,
seniorcitizen,
CAST(CASE partner
WHEN 'Yes' THEN true
WHEN 'No' THEN false
END AS BOOLEAN) AS partner,
...
CAST(monthlycharges AS REAL),
CAST(totalcharges AS REAL),
CAST(CASE churn
WHEN 'Yes' THEN true
WHEN 'No' THEN false
END AS BOOLEAN) AS churn
FROM pgml.telco_churn;
我们首先创建一个视图来准备训练数据。值得注意的是,执行了两种类型转换:具有Yes/No值的特征被映射到布尔类型,我们将月度和总费用转换为REAL类型(在将文本值映射到NULL之后)。我们还从视图中排除了CustomerId,因为这不能用于训练。
训练和超参数优化
我们可以按照以下方式训练我们的 LightGBM 模型:
SELECT *
FROM pgml.train('Telco Churn',
task => 'classification',
relation_name => 'pgml.telco_churn_data',
y_column_name => 'churn',
algorithm => 'lightgbm',
preprocess => '{"totalcharges": {"impute": "mean"} }',
search => 'random',
search_args => '{"n_iter": 500 }',
search_params => '{
"num_leaves": [2, 4, 8, 16, 32, 64]
}'
);
我们将视图设置为我们的关系,其中 churn 列是我们的目标特征。对于预处理,我们使用平均值来请求 PostgresML 为totalcharges特征填充缺失值。
我们还使用随机搜索的 500 次迭代和指定的搜索参数范围来设置超参数优化。
完成训练后,我们将在仪表板中看到我们的训练和部署好的模型:

图 10.4 – 在 PostgresML 仪表板中看到的训练好的 LightGBM 模型
仪表板显示了我们的模型指标和最佳性能的超参数。我们的模型实现了 F1 分数为 0.6367 和准确度为 0.8239。
如果仪表板不可用,可以使用以下 SQL 查询检索相同的信息:
SELECT metrics, hyperparams
FROM pgml.models m
LEFT OUTER JOIN pgml.projects p on p.id = m.project_id
WHERE p.name = 'Telco Churn';
训练完成后,我们的模型将自动部署并准备好进行预测。
预测
我们可以使用复合类型作为特征数据手动进行预测。这可以按照以下方式完成:
SELECT pgml.predict(
'Telco Churn',
ROW (
CAST('Male' AS VARCHAR(30)),
1,
...
CAST('Electronic check' AS VARCHAR(30)),
CAST(20.25 AS REAL),
CAST(4107.25 AS REAL)
)
) AS prediction;
我们使用 PostgreSQL 的ROW表达式来设置数据,将字面量转换为模型正确的类型。
然而,利用 PostgresML 模型更常见的方式是将预测纳入常规业务查询中。例如,在这里,我们已从原始客户数据表中选择了所有数据,并使用pgml.predict函数为每一行添加了预测:
SELECT *,
pgml.predict(
'Telco Churn',
ROW (
gender,
seniorcitizen,
…
paymentmethod,
CAST(monthlycharges AS REAL),
CAST(totalcharges AS REAL)
)
) AS prediction
FROM pgml.telco_churn;
与手动调用预测函数类似,我们使用ROW表达式将数据传递给pgml.predict函数,但选择数据来自表。
这也清楚地说明了使用 PostgresML 的优势:ML 模型的消费者可以在单个网络调用中,以最小的开销,在同一个业务事务中查询新的客户数据并获取预测。
摘要
本章提供了 PostgresML 的概述,这是一个独特的 MLOps 平台,允许在现有的 PostgreSQL 数据库上通过 SQL 查询训练和调用模型。
我们讨论了该平台在简化支持机器学习的景观以及降低面向服务架构中的开销和网络延迟方面的优势。提供了核心功能和 API 的概述。
本章以一个利用 PostgresML 解决分类问题的实际例子结束,展示了如何训练 LightGBM 模型,执行超参数优化,部署它,并在几个 SQL 查询中利用它进行预测。
在下一章中,我们将探讨使用 LightGBM 的分布式和基于 GPU 的学习。
参考文献
| [**1] | PostgresML 比 Python HTTP 微服务快 8-40 倍,[在线]. 可在 https://postgresml.org/blog/postgresml-is-8x-faster-than-python-http-microservices找到。 |
|---|
第十一章:基于 LightGBM 的分布式和 GPU 学习
本章探讨了在分布式计算集群和 GPU 上训练 LightGBM 模型。分布式计算可以显著加快训练工作负载,并允许训练比单台机器上可用的内存大得多的数据集。我们将探讨利用 Dask 进行分布式计算以及 LightGBM 对基于 GPU 的训练的支持。
本章涵盖的主题如下:
-
使用 LightGBM 和 Dask 进行分布式学习
-
LightGBM 的 GPU 训练
技术要求
本章包括在分布式计算集群和 GPU 上训练和运行 LightGBM 模型的示例。运行这些示例需要 Dask 环境和 GPU。完整的代码示例可在 github.com/PacktPublishing/Practical-Machine-Learning-with-LightGBM-and-Python/tree/main/chapter-11 找到。
使用 LightGBM 和 Dask 进行分布式学习
Dask 是一个开源的 Python 分布式计算库。它旨在无缝集成到现有的 Python 库和工具中,包括 scikit-learn 和 LightGBM。本节探讨了使用 Dask 运行 LightGBM 的分布式训练工作负载。
Dask (www.dask.org/) 允许您在单台机器和多台机器上设置集群。在单台机器上运行 Dask 是默认设置,无需设置。然而,在单机集群(或调度器)上运行的工作负载可以轻松地使用分布式调度器运行。
Dask 提供了许多运行分布式集群的方法,包括与 Kubernetes、MPI 集成,或自动配置到超大规模平台(如 AWS 或 Google Cloud Platform)中。
当在单台机器上运行时,Dask 仍然将工作负载分布在多个线程上,这可以显著加快工作负载。
Dask 提供了集群管理实用类,可以轻松设置集群。可以按以下方式运行本地集群:
cluster = LocalCluster(n_workers=4, threads_per_worker=2)
client = Client(cluster)
上述代码创建了一个包含四个工作者的本地集群,每个工作者配置为运行两个线程。集群在 localhost 上运行,默认情况下调度器在端口 8786 上运行。可以通过参数配置主机 IP 和端口。除了运行调度器外,Dask 还启动了一个使用 Bokeh (docs.bokeh.org/en/latest/) 实现的诊断仪表板。默认情况下,仪表板在端口 8787 上运行。我们可以检查 工作者 页面以查看运行集群的状态,如图 图 11.1* 所示。

图 11.1 – 显示四个运行工作者的 Dask 诊断仪表板,每个工作者都有一些技术统计数据
集群启动并运行后,我们可以准备数据以在分布式集群上使用。
Dask 提供了自己实现的数据帧,称为 Dask DataFrame。Dask DataFrame 由许多较小的 pandas DataFrame 组成,这些 DataFrame 根据索引分割。每个部分可以存储在磁盘上或通过网络分布式,这使得可以处理比单个机器内存能容纳的更大的数据集。在 Dask DataFrame 上执行的操作会自动分布到 pandas DataFrame 上。
注意
当你的数据集适合 RAM 时,建议使用标准的 pandas DataFrame 而不是 Dask DataFrame。
我们可以通过加载 CSV 文件来创建一个 Dask DataFrame。请注意,CSV 文件可能位于 S3 或 HDFS 上,并且可能非常大。以下代码从一个 CSV 文件创建一个 Dask DataFrame:
import dask.dataframe as dd
df = dd.read_csv("covtype/covtype.csv", blocksize="64MB")
在这里,我们也指定了加载 CSV 文件时的块大小。块大小设置了数据集被分割成的块的大小,并使我们能够对单个 DataFrame 部分的内存进行细粒度控制。当调用 df.shape 时,我们得到一个有趣的结果:
df.shape
# (Delayed('int-a0031d1f-945d-42b4-af29-ea5e40148f3f'), 55)
列数以数字的形式返回。然而,当我们查看行数时,我们得到了一个名为 Delayed 的包装类。这表明尽管我们已经创建了 DataFrame,但数据并未加载到内存中。相反,Dask 在使用数据的工人上按需加载数据。我们可以强制 Dask 计算行数如下:
df.shape[0].compute()
在 DataFrame 中有了可用数据后,我们可以为训练准备它。我们使用 dask_ml 中的 train_test_split 函数将我们的数据分割成训练集和测试集:
X = df.iloc[:, :-1]
y = df.iloc[:, -1]
X_train, X_test, y_train, y_test = dask_ml.model_selection.train_test_split(X, y)
尽管来自 dask_ml 的函数与 scikit-learn 的 train_test_split 功能相似,但 Dask 版本保持了底层 Dask DataFrame 的分布式特性。
我们现在已设置好集群,并准备好了训练数据。我们现在可以开始训练我们的 LightGBM 模型了。
LightGBM 库团队提供了并维护了每个可用的学习算法的 Dask 版本:DaskLGBMRegressor、DaskLGBMClassifier 和 DaskLGBMRanker。这些是围绕标准 LightGBM scikit-learn 接口包装的,并增加了指定要使用的 Dask 集群客户端的功能。
当 LightGBM 在 Dask 集群上运行时,每个 Dask 工作器对应一个 LightGBM 工作器。LightGBM 将单个工作器上的所有数据分区连接成一个单一的数据集,并且每个 LightGBM 工作器独立使用本地数据集。
然后,每个 LightGBM 工作器协同工作以训练单个 LightGBM 模型,使用 Dask 集群进行通信。当执行数据并行训练(如 Dask 的情况)时,LightGBM 使用 Reduce-Scatter 策略:
-
在直方图构建阶段,每个工作器为不同的非重叠特征构建直方图。然后,执行一个 Reduce-Scatter 操作:每个工作器将其直方图的一部分与每个其他工作器共享。
-
在 Reduce-Scatter 之后,每个工作器都有一个特征子集的完整直方图,然后为这些特征找到最佳分割。
-
最后,执行一个收集操作:每个工作器将其最佳分割与所有其他工作器共享,因此所有工作器都拥有所有最佳分割。
选择最佳特征分割,并根据此对数据进行分区。
幸运的是,分布式算法的复杂性对我们来说是隐藏的,训练代码与我们所熟悉的 scikit-learn 训练代码相同:
dask_model = lgb.DaskLGBMClassifier(n_estimators=200, client=client)
dask_model.fit(X_train, y_train)
运行前面的代码训练 LightGBM 模型,我们可以通过检查 Dask 仪表板的 状态 页面来查看进度,如图 图 11**.2 所示。

图 11.2 – 当 LightGBM 模型正在训练时的 Dask 仪表板状态页面
Dask LightGBM 模型可以使用 Pickle 或 joblib 完全序列化,并且我们可以按以下方式将模型保存到磁盘:
with open("dask-model.pkl", "wb") as f:
pickle.dump(dask_model, f)
可以通过调用模型的 predict 方法来进行预测。请注意,Dask 模型期望一个 Dask DataFrame 或数组:
predictions = dask_model.predict(X_test)
与获取 Dask DataFrame 的形状类似,预测操作也是延迟的,仅在需要时计算。我们可以使用 compute 来获取预测值:
predictions.compute()
这就结束了我们对利用 Dask 进行 LightGBM 分布式训练的探讨。有了 Dask,LightGBM 可以在单个服务器的计算能力之外的巨大数据集上训练模型。Dask 随着您的需求扩展,因此您可以从您的本地笔记本电脑开始,随着数据量的增长,转移到高性能计算环境或云基础设施。此外,如前所述,Dask 设计得可以与现有的 Python 库(如 pandas、NumPy 和 scikit-learn)和谐工作,为数据科学家提供了一个熟悉的环境,同时扩展了这些工具的功能。
接下来,我们将探讨在需要使用 GPU 训练大型模型时如何加快 LightGBM 的训练速度。
LightGBM 的 GPU 训练
LightGBM 库原生支持在 GPU 上训练模型 [1]。支持两种 GPU 平台:通过 OpenCL 和 CUDA 的 GPU。利用 OpenCL 通过 GPU 提供了对最广泛的 GPU 范围的支持(包括 AMD GPU),并且比在 CPU 上运行模型要快得多。然而,如果您有可用的 NVIDIA GPU,CUDA 平台提供了最快的运行时间。
设置 LightGBM 以支持 GPU
设置您的环境以使用 GPU 可能会有些棘手,但我们将在此回顾核心步骤。
注意
这里讨论的 GPU 设置步骤提供的是设置您环境的指南和概述。这里列出的库和驱动程序的精确版本号可能已过时,建议您查阅官方文档以获取最新版本:lightgbm.readthedocs.io/en/latest/GPU-Tutorial.xhtml。
为了使用 GPU,我们必须从源代码编译和构建 LightGBM 库。以下说明假设是在 Ubuntu Linux 构建环境中;其他平台的步骤类似。在我们能够构建库之前,我们必须安装一些依赖项。
重要的是,首先,为你的环境安装 GPU 驱动程序。如果你有 NVIDIA GPU,也要安装 CUDA。有关安装说明,可从相应供应商网站获取:
接下来,我们需要安装 OpenCL 头文件:
sudo apt install --no-install-recommends
sudo apt install --no-install-recommends nvidia-opencl-dev opencl-headers
最后,安装库的构建依赖项:
sudo apt install --no-install-recommends git cmake build-essential libboost-dev libboost-system-dev libboost-filesystem-dev
现在,我们已经准备好编译具有 GPU 支持的 LightGBM 库。克隆存储库并构建库,设置USE_GPU标志:
git clone --recursive https://github.com/microsoft/LightGBM
cd LightGBM
mkdir build
cd build
cmake -DUSE_GPU=1 ..
make -j$(nproc)
cd ..
如第二章中所述,集成学习 – Bagging 和 Boosting, LightGBM 是一个具有 Python 接口的 C++库。根据前面的说明,我们已经构建了具有 GPU 支持的库,但我们必须构建和安装 Python 包,以便从 Python(包括 scikit-learn API)使用该库:
cd python-package/
python setup.py install --user --precompile
在 GPU 上运行 LightGBM
在 GPU 上运行训练代码很简单。我们将设备参数设置为gpu或cuda:
model = lgb.LGBMClassifier(
n_estimators=150,
device="cuda",
is_enable_sparse=False
)
model = model.fit(X_train, y_train)
如前述代码所示,我们通过将is_enable_sparse设置为False来关闭 LightGBM 的稀疏矩阵优化。LightGBM 的稀疏特性在 GPU 设备上不受支持。此外,根据你的数据集,你可能会收到以下警告,指出multi_logloss未实现:
Metric multi_logloss is not implemented in cuda version. Fall back to evaluation on CPU.
值得注意的是,这里执行的回退操作仅用于评估,而不是训练;训练仍然在 GPU 上执行。我们可以通过检查nvidia-smi(针对 NVIDIA GPU)来验证是否使用了 GPU:

图 11.3 – 当 LightGBM 正在训练时 nvidia-smi 的输出(如图所示,GPU 利用率达到 40%)
实现的速度提升取决于你的 GPU。在 Forest Cover 数据集上,训练时间从 171 秒减少到 11 秒(速度提升了 15 倍),进行了 150 次迭代。
使用 GPU 带来的巨大性能提升在执行参数调整时特别有用。我们可以使用 Optuna 等基于 GPU 的训练来显著加速对最优参数的搜索。所需做的只是将模型训练移动到objective函数中的 GPU 设备。在定义目标函数时,我们按照常规指定我们的 Optuna 参数范围:
def objective(trial):
lambda_l1 = trial.suggest_float(
'lambda_l1', 1e-8, 10.0, log=True),
lambda_l2 = trial.suggest_float(
'lambda_l2', 1e-8, 10.0, log=True),
...
然后,我们使用 Optuna 参数创建模型,并确保指定设备为cuda(或gpu):
model = lgb.LGBMClassifier(
force_row_wise=True,
boosting_type=boosting_type,
n_estimators=n_estimators,
lambda_l1=lambda_l1,
lambda_l2=lambda_l2,
...
learning_rate=learning_rate,
max_bin=max_bin,
device="cuda")
目标函数的最后部分是返回交叉验证的分数:
scores = cross_val_score(model, X_train, y_train, scoring="f1_macro")
return scores.mean()
然后,我们可以像平常一样运行参数研究:
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=10, gc_after_trial=True, n_jobs=1)
重要的是,我们还在这里将n_jobs参数设置为1,因为利用 GPU 运行并行作业可能会引起不必要的竞争和开销。
在使用 GPU 进行训练以获得最佳性能时,有一些值得注意的最佳实践:
-
总是验证 GPU 是否正在使用。即使设置了
device=gpu,如果 GPU 不可用,LightGBM 会回退到 CPU 训练。一个检查的好方法是使用如nvidia-smi之类的工具,如之前所示,或者将训练时间与参考基准进行比较。 -
使用更小的
max_bin大小。大型数据集减少了较小max_bin大小的影响,而更少的 bins 有助于 GPU 上的训练。同样,如果你的 GPU 支持,可以使用单精度浮点数以增加性能。 -
GPU 训练最适合大型密集数据集。数据需要移动到 GPU 的 VRAM 中进行训练,如果数据集太小,移动数据涉及的开销就太显著了。
-
避免对特征列进行 one-hot 编码,因为这会导致稀疏特征矩阵,这在 GPU 上表现不佳。
这部分内容总结了如何使用 LightGBM 与 GPU 结合。尽管设置可能更复杂,但由于 GPU 能够同时处理数千个线程,因此 GPU 提供了显著的训练速度提升,尤其是在处理大型数据集时,能够实现高效的并行处理。GPU 的巨大并行性对于 LightGBM 中的基于直方图的算法尤其有益,使得如构建直方图等操作更加高效和有效。
摘要
在本章中,我们讨论了两种使用 LightGBM 加速计算的方法。第一种是使用 Python 库 Dask 在多台机器上进行大规模分布式训练。我们展示了如何设置 Dask 集群,如何使用 Dask DataFrame 将数据分发到集群,以及如何在集群上运行 LightGBM。
第二种,我们还探讨了如何利用 GPU 与 LightGBM 结合。值得注意的是,GPU 设置很复杂,但一旦可用,就能实现显著的加速。我们还讨论了在 GPU 上训练 LightGBM 模型的一些最佳实践。
参考文献
| *[1] | 张华,史思,和蔡家驹,大型树提升的 GPU 加速,2017. |
|---|



浙公网安备 33010602011771号