Python-机器学习示例第四版-一-

68 阅读1小时+

Python 机器学习示例第四版(一)

原文:annas-archive.org/md5/143aadf706a620d20916160319321b2e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《Python 机器学习实战》第四版是一本全面的指南,适合初学者和希望学习更高级技术(如多模态建模)的经验丰富的机器学习 (ML) 从业者。本版强调最佳实践,为 ML 工程师、数据科学家和分析师提供宝贵的见解。

探索高级技术,包括关于 NLP 变换器(BERT 和 GPT)以及使用 PyTorch 和 Hugging Face 的多模态计算机视觉模型的两章新内容。你将通过实际案例学习关键建模技术,例如预测股价和创建图像搜索引擎。

本书通过复杂的挑战,弥合理论理解与实践应用之间的差距。提升你的机器学习专业技能,解决复杂问题,并在这本权威指南中解锁机器学习高级技术的潜力。

本书的目标读者

如果你是机器学习爱好者、数据分析师或数据工程师,且对机器学习充满热情,并且希望开始进行机器学习任务,那么这本书适合你。本书假设你已经具备 Python 编程基础,基本了解统计学概念会有帮助,尽管这不是必要的。

本书内容

第一章机器学习与 Python 入门,将开启你的 Python 机器学习之旅。它从机器学习是什么、我们为何需要它以及它在过去几十年的发展开始,接着讨论典型的机器学习任务,并以一种实用且有趣的方式探索几种处理数据和模型的基本技术。你还将设置为后续章节中的示例和项目所需的软件和工具。

第二章使用朴素贝叶斯构建电影推荐引擎,聚焦于分类,特别是二分类和朴素贝叶斯。章节的目标是构建一个电影推荐系统。你将学习分类的基本概念,并了解朴素贝叶斯,这是一种简单而强大的算法。它还展示了如何微调模型,这是每个数据科学或机器学习从业者都需要掌握的重要技能。

第三章使用树形算法预测在线广告点击率,介绍并深入讲解了树形算法(包括决策树、随机森林和提升树),通过解决广告点击率问题来展开。你将从根到叶探索决策树,并从零开始实现树模型,使用 scikit-learn 和 XGBoost。同时,还将涵盖特征重要性、特征选择和集成方法。

第四章使用逻辑回归预测在线广告点击率,是广告点击率预测项目的延续,重点介绍了一种非常具有可扩展性的分类模型——逻辑回归。你将探索逻辑回归的工作原理,以及如何处理大规模数据集。本章还涵盖了类别变量编码、L1 和 L2 正则化、特征选择、在线学习和随机梯度下降。

第五章使用回归算法预测股票价格,聚焦于几种流行的回归算法,包括线性回归、回归树和回归森林。它将鼓励你利用这些算法解决一个价值数十亿(甚至数万亿)美元的问题——股票价格预测。你将使用 scikit-learn 和 TensorFlow 来实践回归问题的解决。

第六章使用人工神经网络预测股票价格,深入介绍和解释了神经网络模型。它涵盖了神经网络的构建模块,以及诸如激活函数、前馈和反向传播等重要概念。你将从构建最简单的神经网络开始,并通过增加更多的层来深入研究。我们将从零开始实现神经网络,使用 TensorFlow 和 PyTorch,并训练神经网络来预测股票价格。

第七章使用文本分析技术挖掘 20 个新闻组数据集,将开启你学习旅程的第二步——无监督学习。它探索了一个自然语言处理问题——分析新闻组数据。你将获得处理文本数据的实际经验,特别是如何将单词和短语转换为机器可读的数值,以及如何清理那些意义不大的单词。你还将使用一种叫做 t-SNE 的降维技术来可视化文本数据。最后,你将学习如何使用嵌入向量表示单词。

第八章通过聚类和主题建模发现新闻组数据集中的潜在主题,讨论了如何以无监督的方式从数据中识别不同的观察组。你将使用 K-means 算法对新闻组数据进行聚类,并通过非负矩阵分解和潜在狄利克雷分配来检测主题。你将惊讶于你能够从 20 个新闻组数据集中挖掘出多少有趣的主题!

第九章使用支持向量机识别面孔,继续了监督学习和分类的旅程。具体来说,它专注于多类别分类和支持向量机分类器。它讨论了支持向量机算法如何搜索决策边界,以便将不同类别的数据分开。你将使用 scikit-learn 实现该算法,并应用于解决包括人脸识别在内的各种实际问题。

第十章机器学习最佳实践,旨在全面证明您的学习成果,并为实际项目做好准备。它包括 21 个最佳实践,贯穿整个机器学习工作流程。

第十一章使用卷积神经网络对服装图像进行分类,介绍了使用卷积神经网络CNNs),一种非常强大的现代机器学习模型,来对服装图像进行分类。它涵盖了 CNN 的构建模块和架构,以及如何使用 PyTorch 进行实现。在探索了服装图像数据后,您将开发 CNN 模型,将图像分类为十个类别,并利用数据增强和迁移学习技术来提升分类器的效果。

第十二章使用循环神经网络进行序列预测,从定义顺序学习开始,探索循环神经网络RNNs)如何非常适合此类任务。您将学习各种类型的 RNN 及其常见应用。您将使用 PyTorch 实现 RNN,并将其应用于解决三个有趣的顺序学习问题:IMDb 电影评论的情感分析、股票价格预测和文本自动生成。

第十三章使用 Transformer 模型推进语言理解与生成,深入探讨了为顺序学习设计的 Transformer 神经网络。它重点关注输入序列中的关键部分,并比 RNNs 更好地捕捉长期依赖关系。您将探索两种前沿的 Transformer 模型 BERT 和 GPT,并将它们用于情感分析和文本生成,超越前一章的性能。

第十四章使用 CLIP 构建图像搜索引擎:一种多模态方法,探索了一个多模态模型 CLIP,它将视觉和文本数据融合在一起。这个强大的模型能够理解图像和文本之间的关系。您将深入了解其架构及其学习方式,然后构建一个图像搜索引擎。最后,您将通过一个零样本图像分类项目来总结全部内容,推动该模型的能力极限。

第十五章在复杂环境中做出决策:强化学习,介绍了通过经验学习并与环境互动的过程。在探讨了强化学习的基本原理后,您将通过一个简单的动态规划算法探索 FrozenLake 环境。您将学习蒙特卡洛学习,并将其用于值逼近和控制。您还将开发时序差分算法,并使用 Q 学习解决出租车问题。

为了最大程度地从本书中受益

为了为您的项目创建智能认知动作,本书假设您具备基础的 Python 知识、基本的机器学习算法,以及一些基础的 Python 库,如 NumPy 和 pandas。

下载示例代码文件

本书的代码包托管在 GitHub 上,地址为github.com/packtjaniceg/Python-Machine-Learning-by-Example-Fourth-Edition/。我们还提供了其他代码包,来自我们丰富的书籍和视频目录,您可以在github.com/PacktPublishing/找到。快来看看吧!

下载彩色图像

我们还提供了包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以在此处下载:packt.link/gbp/9781835085622

使用的约定

本书中使用了多种文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter(X)用户名。例如:“除了评分矩阵data,我们还记录了movie ID到列索引的映射。”

REPL 代码块设置如下:

>>> smoothing = 1
>>> likelihood = get_likelihood(X_train, label_indices, smoothing)
>>> print('Likelihood:\n', likelihood) 

代码输出将如下所示:

Likelihood:
 {'Y': array([0.4, 0.6, 0.4]), 'N': array([0.33333333, 0.33333333, 0.66666667])} 

粗体:表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会以这种方式显示在文本中。以下是一个例子:“根据类输出的可能性,有三种类型的分类——二分类多分类多标签分类。”

警告或重要提示将以这种方式显示。

提示和技巧以这种方式显示。

联系我们

我们欢迎读者的反馈。

一般反馈:通过电子邮件发送至feedback@packtpub.com并在邮件主题中注明书名。如果您对本书的任何方面有疑问,请通过questions@packtpub.com与我们联系。

勘误:虽然我们已经尽最大努力确保内容的准确性,但错误仍然有可能发生。如果您在本书中发现错误,请您报告给我们。请访问www.packtpub.com/submit-errata,点击提交勘误,并填写表单。

盗版:如果您在互联网上遇到任何非法的我们作品的副本,请提供该副本的地址或网站名称。请通过copyright@packtpub.com与我们联系,并提供该材料的链接。

如果您有兴趣成为作者:如果您在某个主题方面具有专业知识,并且有兴趣撰写或参与编写书籍,请访问authors.packtpub.com

分享您的想法

在阅读完Python Machine Learning By Example - 第四版后,我们希望听到您的想法!请点击此处直接前往 Amazon 评价页面并分享您的反馈。

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

免费下载本书的 PDF 版本

感谢您购买本书!

您喜欢随时随地阅读,但又无法随身携带纸质书籍吗?

您购买的电子书是否与您选择的设备不兼容?

别担心,现在每本 Packt 书籍都附赠一个无 DRM 的 PDF 版本,完全免费。

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

福利不仅如此,您还可以独享折扣、时事通讯,并每天在邮箱中获得精彩的免费内容。

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

  1. 扫描二维码或访问以下链接:

packt.link/free-ebook/9781835085622

  1. 提交您的购买凭证。

  2. 就这样!我们会直接将免费的 PDF 和其他福利发送到您的邮箱。

第一章:开始使用机器学习和 Python

人工智能AI)超越人类知识的概念通常被称为“技术奇点”。一些来自 AI 研究界及其他领域的预测表明,这一奇点可能会在未来 30 年内到来。无论它的时间表如何,有一点是明确的:AI 的崛起突显了分析能力和机器学习技能日益重要。掌握这些学科不仅能帮助我们理解和与越来越复杂的 AI 系统互动,还能让我们积极参与塑造其发展与应用,确保它们造福人类。

在本章中,我们将从机器学习的一些基本而重要的概念开始我们的机器学习之旅。我们将从机器学习的定义开始,探讨我们为何需要它,以及它在几十年来的发展历程。接下来,我们将讨论典型的机器学习任务,并探索几种处理数据和模型的基本技巧。

在本章的最后,我们将设置 Python 软件,这是机器学习和数据科学领域最流行的编程语言,并安装本书所需的库和工具。

我们将详细讨论以下主题:

  • 机器学习简介

  • 了解前提条件

  • 开始了解三种类型的机器学习

  • 深入探讨机器学习的核心

  • 数据预处理和特征工程

  • 模型结合

  • 安装软件并进行设置

机器学习简介

在这一部分,我们将通过简要介绍机器学习、为何需要机器学习、机器学习与自动化的区别以及机器学习如何改善我们的生活,来开启我们的机器学习之旅。

机器学习是一个大约在 1960 年左右被创造的术语,由两个词组成——机器,对应计算机、机器人或其他设备,和学习,指的是旨在获取或发现事件模式的活动,而这正是人类擅长的事情。有趣的例子包括人脸识别、语言翻译、回复电子邮件、做出数据驱动的商业决策,以及创建各种类型的内容。你将在本书中看到更多这样的例子。

了解我们为何需要机器学习

为什么我们需要机器学习,为什么我们希望机器像人类一样学习呢?我们可以从三个主要角度来理解:维护、风险减轻和性能提升。

首先,当然,计算机和机器人可以全天候工作,不会感到疲倦。从长远来看,机器的成本远低于人工。而且,对于涉及大量庞大数据集或复杂计算的复杂问题,让计算机完成所有工作不仅更合理,而且更具智能。由人类设计的算法驱动的机器能够学习潜在的规则和内在的模式,从而有效地执行任务。

学习机器比人类更适合处理那些日常的、重复的或繁琐的任务。除此之外,机器学习的自动化可以减轻由于疲劳或注意力不集中而导致的风险。自动驾驶汽车,如图 1.1所示,是一个很好的例子:一辆车能够通过感知环境并做出决策,而无需人类输入。另一个例子是在生产线上使用机器人臂,这能显著减少伤害和成本。

图 1.1:自动驾驶汽车的示例

假设人类不会感到疲劳,或者我们有足够的资源雇佣足够的轮班工人;那么机器学习是否还会有一席之地?当然会!有许多已报告和未报告的案例表明,机器在某些方面表现得与领域专家相当,甚至更好。由于算法是通过从事实真相和人类专家所做的最经过深思熟虑的决策中学习,机器可以与专家一样表现出色。

事实上,即使是最优秀的专家也会犯错误。机器通过利用来自个体专家的集体智慧,可以最大程度地减少做出错误决策的机会。一项主要研究表明,机器在诊断某些类型的癌症时优于医生,这正是这种理念的证明(www.nature.com/articles/d41586-020-00847-2)。AlphaGodeepmind.com/research/case-studies/alphago-the-story-so-far)可能是机器战胜人类最著名的例子——DeepMind 创建的 AI 程序在一场五局围棋比赛中击败了世界围棋冠军李世石。

此外,从经济和社会障碍的角度来看,部署学习机器比训练个人成为专家更加具备可扩展性。当前的诊断设备能够达到与合格医生相似的性能水平。我们可以在一周内将数千台诊断设备分布到全球各地,但几乎不可能在同一时间内招募并分配相同数量的合格医生。

你可能会反驳:如果我们有足够的资源和能力雇佣最优秀的领域专家,并随后汇聚他们的意见——机器学习是否还会有一席之地?可能不会(至少目前如此)——学习机器可能无法超越最聪明的人的联合努力。然而,配备学习机器的个人却能够超越最优秀的专家团队。这是一种新兴的概念,称为基于 AI 的辅助AI 与人类智能结合,它倡导机器与人类的共同努力。它为用户提供支持、指导或解决方案。更重要的是,它能够适应并从用户互动中学习,随着时间的推移不断提升性能。

我们可以将之前的陈述总结为以下不等式:

人类 + 机器学习 → 最智能且不知疲倦的人类 ≥ 机器学习 > 人类

人工智能生成内容 (AIGC)是近年来的一个突破。它利用 AI 技术创造或协助创造各种类型的内容,如文章、产品描述、音乐、图像和视频。

机器人参与的医学手术是人类与机器学习协同作用的一个极好例子。图 1.2展示了手术室中机器人臂与外科医生的合作:

A picture containing person, clothing, medical equipment, technician  Description automatically generated

图 1.2:AI 辅助手术

区分机器学习和自动化

那么,机器学习是否仅仅等同于涉及编程和执行人工编写或人工策划规则集的自动化呢?一种流行的误解认为,机器学习与自动化相同,因为它执行指令性和重复性的任务,并且不再深入思考。如果答案是是的,那么我们为什么不能仅仅雇佣许多软件程序员,继续编写新规则或扩展旧规则呢?

一个原因是,定义、维护和更新规则随着时间推移变得越来越昂贵。活动或事件的可能模式数量可能是巨大的,因此,穷举所有模式在实际操作中是不可行的。尤其当事件是动态的、不断变化的或实时演变时,问题变得更加复杂。开发学习算法,让计算机从大量数据中学习、提取模式并自行解决问题,变得更加容易和高效。

机器学习与传统编程的区别可以从图 1.3中看出:

A diagram of a computer model  Description automatically generated with low confidence

图 1.3:机器学习与传统编程的对比

在传统编程中,计算机遵循一组预定义的规则来处理输入数据并生成结果。在机器学习中,计算机试图模仿人类思维。它与输入数据、预期输出和环境进行互动,并推导出由一个或多个数学模型表示的模式。然后,这些模型用于与未来的输入数据互动并生成结果。与自动化不同,在机器学习的设置中,计算机并不会收到明确的指令性编码。

数据量正在呈指数增长。如今,文本、音频、图像和视频数据的洪流难以估量。物联网IoT)是新型互联网的最新发展,它将日常设备互联起来。物联网将把家电和自动驾驶汽车的数据带到前台。这一趋势可能会继续,我们将拥有更多生成和处理的数据。除了数据量的增加,近年来可用数据的质量也在不断提高,部分原因是存储成本降低。这推动了机器学习算法和数据驱动解决方案的发展。

机器学习应用

阿里巴巴的联合创始人马云在 2018 年的一次演讲中解释道,过去 20 年是 IT 的时代,但未来 30 年将是数据技术DT)的时代(www.alizila.com/jack-ma-dont-fear-smarter-computers/)。在 IT 时代,企业因计算机软件和基础设施而不断壮大。如今,大多数行业的企业已经收集了大量数据,现在正是利用 DT 来解锁洞察、推导模式并推动新业务增长的最佳时机。广义上讲,机器学习技术使企业能够更好地理解客户行为、与客户互动并优化运营管理。

对于我们个人来说,机器学习技术已经在每天不断改善我们的生活。我们都熟悉的一个机器学习应用就是垃圾邮件过滤。另一个是在线广告,根据广告商收集到的关于我们的信息,自动投放广告。请继续关注接下来的章节,您将学习如何开发算法来解决这两个问题以及更多问题。

搜索引擎是我们无法想象没有的机器学习应用。它涉及信息检索,解析我们所寻找的内容,查询相关的最佳记录,并应用上下文排序和个性化排序,根据主题相关性和用户偏好对页面进行排序。电子商务和媒体公司在推荐系统的应用上处于前沿,这些系统帮助客户更快找到产品、服务和文章。

机器学习的应用是无限的,我们每天都能听到新的例子:信用卡欺诈检测、总统选举预测、即时语音翻译、机器人顾问、AI 生成艺术、客服聊天机器人以及由生成性 AI 技术提供的医学或法律咨询——应有尽有!

在 1983 年的战争游戏电影中,一台计算机做出了生死攸关的决策,这些决策本可能导致第三次世界大战。就我们所知,当时的技术并未能完成如此壮举。然而,在 1997 年,深蓝超计算机成功击败了一位世界象棋冠军(en.wikipedia.org/wiki/Deep_Blue_(chess_computer))。在 2005 年,一辆斯坦福大学的自动驾驶汽车在沙漠中自主行驶了超过 130 英里(en.wikipedia.org/wiki/DARPA_Grand_Challenge_(2005))。2007 年,另一支队伍的汽车在城市道路上行驶了超过 60 英里(en.wikipedia.org/wiki/DARPA_Grand_Challenge_(2007))。2011 年,沃森计算机在一场问答比赛中击败了人类对手(en.wikipedia.org/wiki/Watson_(computer))。正如前面提到的,AlphaGo 程序在 2016 年击败了世界顶级围棋选手。截至 2023 年,ChatGPT 已经在多个行业广泛应用,如客户支持、内容生成、市场研究以及培训与教育(www.forbes.com/sites/bernardmarr/2023/05/30/10-amazing-real-world-examples-of-how-companies-are-using-chatgpt-in-2023)。

如果我们假设计算机硬件是限制因素,那么我们可以尝试预测未来。美国著名发明家和未来学家雷·库兹韦尔就是这样做的,他在 2017 年预测,人工智能将在大约 2029 年达到人类水平的智能(aibusiness.com/responsible-ai/ray-kurzweil-predicts-that-the-singularity-will-take-place-by-2045)。接下来会发生什么?

迫不及待想开始自己的机器学习之旅吗?让我们从先决条件和机器学习的基本类型开始。

了解先决条件

模拟人类智能的机器学习是人工智能的一个子领域——这是计算机科学中的一个领域,专注于创建系统。软件工程是计算机科学中的另一个领域。通常,我们可以将 Python 编程视为一种软件工程。机器学习与线性代数、概率论、统计学和数学优化也有着紧密的关系。我们通常基于统计学、概率论和线性代数构建机器学习模型,然后通过数学优化来优化这些模型。

阅读本书的大多数人应该已经具备了良好的,或至少足够的 Python 编程能力。那些对数学知识不太自信的人,可能会想知道应该花多少时间学习或复习前面提到的科目。别担心;我们将在本书中不深入探讨数学细节的情况下,让机器学习为我们所用。这只需要一些概率论和线性代数的基础 101 知识,这有助于我们理解机器学习技术和算法的机制。而且它会变得越来越容易,因为我们将在 Python 这个我们喜欢且熟悉的语言中,从零开始和使用流行的包来构建模型。

对于那些想学习或复习概率论和线性代数的人,可以随时查找基础的概率论和基础线性代数资源。网上有许多资源,例如,people.ucsc.edu/~abrsvn/intro_prob_1.pdf,哈佛大学的在线课程Introduction to Probabilitypll.harvard.edu/course/introduction-probability-edx),讲授概率 101,以及关于基础线性代数的论文:www.maths.gla.ac.uk/~ajb/dvi-ps/2w-notes.pdf

想要系统学习机器学习的人可以报读计算机科学、人工智能,近年来也有数据科学与人工智能的硕士课程。同时,也有各种数据科学训练营。不过,训练营的选择通常较为严格,因为它们更侧重于就业导向,而且课程时长通常较短,范围从 4 到 10 周不等。另一个选择是免费的大规模开放在线课程MOOCs),例如 Andrew Ng 的著名机器学习课程。最后但同样重要的是,行业博客和网站是我们了解最新进展的好资源。

机器学习不仅仅是一项技能,它也有点像运动。我们可以参加几种机器学习竞赛,例如 Kaggle(www.kaggle.com)——有时是为了不错的现金奖励,有时是为了乐趣,但大多数时候是为了发挥我们的特长。不过,要赢得这些竞赛,我们可能需要利用某些技巧,这些技巧仅在竞赛环境中有用,而不适用于解决商业问题的环境。没错——没有免费的午餐定理(en.wikipedia.org/wiki/No_free_lunch_theorem)在这里适用。在机器学习的背景下,这一定理表明,没有任何单一算法能够在所有可能的数据集和问题领域中普遍优越。

接下来,我们将看看三种机器学习类型。

入门三种机器学习类型

一个机器学习系统接收输入数据——这些数据可以是数值的、文本的、视觉的或视听的。系统通常有一个输出——这可以是一个浮点数,比如自驾车的加速度,或者是一个整数,表示一个类别(也称为),例如通过图像识别分辨出猫或老虎。

机器学习的主要任务是探索和构建能够从历史数据中学习并对新输入数据做出预测的算法。对于数据驱动的解决方案,我们需要定义一个评估函数,称为损失代价函数,它用于衡量模型学习的效果。在这种设置下,我们创建一个优化问题,目标是以最有效和最优化的方式进行学习。

根据学习数据的性质,机器学习任务大致可以分为以下三类:

  • 无监督学习:当学习数据仅包含指示信号而没有附加任何描述(我们称之为无标签数据)时,必须由我们来找出数据背后的结构,发现隐藏的信息,或者确定如何描述数据。无监督学习可用于检测异常情况,如欺诈或设备故障,或将具有相似在线行为的客户分组进行营销活动。数据可视化使数据更易理解,降维则从噪声数据中提取相关信息,这些也属于无监督学习的范畴。

  • 监督学习:当学习数据除了指示信号外,还附带描述、目标或期望输出(我们称之为有标签数据)时,学习目标是找到一个将输入映射到输出的一般规则。学习到的规则随后用于为新数据标注未知的输出。标签通常由事件日志系统提供,或者由人类专家评估。如果可行,标签也可以通过人类评审员、众包等方式生成。

监督学习通常用于日常应用中,比如人脸和语音识别、产品或电影推荐、销售预测和垃圾邮件检测。

  • 强化学习:学习数据提供反馈,系统根据反馈适应动态条件,最终实现特定目标。系统根据反馈评估其性能并作出相应反应。最著名的实例包括用于工业自动化的机器人、自驾车和国际象棋大师 AlphaGo。强化学习与监督学习的关键区别在于与环境的互动。

下图展示了机器学习任务的类型:

图 1.4:机器学习任务类型

如图所示,我们可以将监督学习进一步细分为回归和分类。回归训练并预测连续值的响应,例如预测房价,而分类则尝试找到适当的类别标签,比如分析正/负情感和预测贷款违约。

如果并非所有学习样本都被标记,但有一些被标记了,那么我们就有半监督 学习。这利用未标记的数据(通常是大量的)进行训练,除了少量的标记数据。半监督学习适用于获取完全标记数据集代价昂贵,并且标记小部分数据更加实际的情况。例如,标记高光谱遥感图像通常需要熟练的专家,而获取未标记数据相对容易。

对这些抽象概念感到有些困惑吗?别担心,我们将在本书后面遇到许多这些类型的机器学习任务的具体例子。例如,在第二章使用朴素贝叶斯构建电影推荐引擎中,我们将深入探讨监督学习分类及其流行的算法和应用。同样,在第五章使用回归算法预测股票价格中,我们将探索监督学习回归。

我们将在第八章使用聚类和主题建模在新闻组数据集中发现潜在主题中重点介绍无监督学习技术和算法。最后但同样重要的是,第三种机器学习任务——强化学习,将在第十五章在复杂环境中使用强化学习做出决策中介绍。

除了根据学习任务对机器学习进行分类外,我们还可以按时间顺序进行分类。

机器学习算法发展的简要历史

事实上,我们拥有一个完整的机器学习算法“动物园”,这些算法经历了不同程度的流行。我们可以大致将它们分为五种主要的方式:基于逻辑的学习、统计学习、人工神经网络、遗传算法和深度学习。

基于逻辑的系统是最早占主导地位的。它们使用由人工专家指定的基本规则,系统利用这些规则尝试通过形式逻辑、背景知识和假设进行推理。

统计学习理论试图找到一个函数来形式化变量之间的关系。在 1980 年代中期,人工神经网络ANNs)开始崭露头角。人工神经网络模仿动物的大脑,由相互连接的神经元组成,这些神经元也是生物神经元的模仿。它们试图建模输入与输出值之间的复杂关系,并捕捉数据中的模式。人工神经网络在 1990 年代被统计学习系统所取代。

遗传算法GA)在 1990 年代很受欢迎。它们模仿生物进化过程,尝试通过变异和交叉等方法找到最优解。

在 2000 年代,集成学习方法引起了人们的关注,这些方法结合了多个模型以提高性能。

自 2010 年代末以来,我们看到深度学习成为主导力量。深度学习这个术语大约在 2006 年被提出,指的是具有多层的深度神经网络。深度学习的突破源于图形处理单元GPU)的整合和应用,它们大大加速了计算。大数据集的可用性也推动了深度学习的革命。

GPU 最初是为了渲染视频游戏而开发的,擅长并行矩阵和向量代数。人们认为深度学习与人类学习的方式相似。因此,它可能实现“有感知的机器”的承诺。当然,在本书中,我们将在第十一章《使用卷积神经网络对服装图像进行分类》和第十二章《利用循环神经网络进行序列预测》中深入探讨深度学习,在第六章《使用人工神经网络预测股价》中也有简要讨论。

机器学习算法持续快速发展,研究领域涵盖迁移学习生成模型和强化学习,这些是 AIGC 的核心支柱。我们将在第十三章《通过 Transformer 模型推动语言理解与生成》和第十四章《使用 CLIP 构建图像搜索引擎:一种多模态方法》中探讨这些最新进展。

我们中的一些人可能听说过摩尔定律——这是一种经验法则,声称计算机硬件随时间呈指数级增长。该法则由英特尔的联合创始人戈登·摩尔(Gordon Moore)于 1965 年首次提出。根据摩尔定律,芯片上的晶体管数量每两年应该翻一倍。在下图中,你可以看到这一规律得到了很好的验证(气泡的大小对应 GPU 中晶体管的平均数量):

一张包含文字、截图、图表、线条的图片  描述自动生成

图 1.5:过去几十年的晶体管数量

共识似乎是,摩尔定律将在接下来的几十年继续有效。这为雷·库兹韦尔(Ray Kurzweil)预测在 2029 年实现真正的机器智能提供了一些可信度。

深入挖掘机器学习的核心

在讨论了机器学习算法的分类后,我们现在将深入探讨机器学习的核心——用数据进行概括、不同层次的概括以及如何达到合适的概括层次的方法。

用数据进行概括

数据的好处是它在世界上有大量存在。坏处是处理这些数据非常困难。挑战来自于数据的多样性和噪声。我们人类通常处理进入我们耳朵和眼睛的数据。这些输入会转化为电信号或化学信号。在一个非常基础的层面,计算机和机器人也处理电信号。

这些电信号随后会被转化为 0 和 1。然而,在本书中我们使用 Python 进行编程,在这个层面上,通常我们将数据表示为数字或文本。然而,文本并不是特别方便,因此我们需要将其转化为数值。

特别是在监督学习的背景下,我们有一个类似于考试复习的场景。我们有一组练习题和实际考试。我们应该能够在没有提前接触相同问题的情况下回答考试问题。这被称为泛化——我们从练习题中学到一些东西,并希望能够将这些知识应用于其他类似的问题。在机器学习中,这些练习题被称为训练集训练样本。机器学习模型从这些样本中推导出模式。而实际考试则是测试集测试样本。模型最终将在这些测试集中应用。学习效果通过学习模型与测试的兼容性来衡量。

有时,在练习题和实际考试之间,我们会进行模拟考试来评估自己在实际考试中的表现,并帮助复习。这些模拟考试在机器学习中被称为验证集验证样本。它们帮助我们验证模型在模拟环境中的表现,然后我们根据结果对模型进行微调,以实现更高的准确度。

传统的程序员会与业务分析师或其他专家进行沟通,然后实现一个税收规则,例如将某个值与另一个对应的值相乘。而在机器学习环境中,我们可以给计算机提供一堆输入和输出示例;或者,如果我们想更有雄心,可以将实际的税法文本输入程序。我们可以让机器消耗这些数据并自动推导出税收规则,就像自动驾驶汽车不需要太多明确的人类输入一样。

在物理学中,我们也遇到几乎相同的情况。我们想要了解宇宙如何运作,并用数学语言来制定规律。由于我们不知道宇宙如何运作,唯一能做的就是在尝试制定规律时测量所产生的误差,并尽量减少它。在监督学习任务中,我们将结果与期望值进行比较。在无监督学习中,我们通过相关指标来衡量我们的成功。例如,我们希望数据点根据相似性进行分组,形成簇;这些指标可以是簇内数据点的相似度,或者两个簇之间数据点的差异度。在强化学习中,程序通过评估自己的操作来进行学习,例如,在国际象棋游戏中使用预定义的函数来评估其走法。

除了数据的正确泛化外,还有两种泛化层次,过拟合和欠拟合,我们将在下一节中探讨这两个层次。

过拟合、欠拟合与偏差-方差权衡

在本节中,我们将详细探讨两种泛化层次,并深入分析偏差-方差权衡。

过拟合

达到合适的拟合模型是机器学习任务的目标。那么,如果模型出现过拟合怎么办?过拟合意味着模型对现有观察数据拟合得过于完美,但无法预测未来新的观察数据。让我们看一下以下的类比。

如果我们做很多考试的练习题,可能会开始找到一些与学科内容无关的答题方法。例如,给定只有五道练习题,我们可能会发现,如果选择题中有两次出现土豆,一次出现西红柿,三次出现香蕉,答案总是A;如果有一次出现土豆,三次出现西红柿,两次出现香蕉,答案总是B。然后我们可能会得出这样的结论:这总是成立的,并在之后应用这样的理论,尽管学科或答案与土豆、西红柿或香蕉无关。更糟糕的是,我们可能会逐字记住每一道题的答案。这样,我们在练习题上会得分很高,并希望实际考试中的问题与练习题相同。然而,实际上,我们在考试中的得分会很低,因为考试中很少会出现完全相同的问题。

记忆现象可能导致过拟合。这种情况发生在我们从训练集中过度提取信息,使得我们的模型只在这些训练数据上表现良好。然而,过拟合并不会帮助我们将模型推广到新数据,并从中推导出真正的规律。因此,模型在处理之前未见过的数据集时表现很差。我们称这种情况为机器学习中的高方差。让我们快速回顾一下方差:方差衡量的是预测结果的分散程度,即预测结果的变化性。它可以通过以下方式计算:

这里,ŷ 是预测值,E[] 是期望或期望值,表示基于概率分布的随机变量的平均值。

以下示例展示了典型的过拟合情况,其中回归曲线试图完美地适应所有观察到的样本:

A picture containing map, screenshot  Description automatically generated

图 1.6:过拟合示例

过拟合发生在我们尝试基于相对于少量观察样本过多的参数来描述学习规则,而不是描述潜在关系,例如之前的土豆、西红柿和香蕉的例子,其中我们仅从五个学习样本中推导出三个参数。过拟合还发生在我们使模型过于复杂,以至于它完美拟合所有训练样本,就像之前提到的,记住所有问题的答案。

欠拟合

相反的情况是欠拟合。当模型欠拟合时,它在训练集上的表现不好,在测试集上也不会表现好,这意味着它未能捕捉到数据的潜在趋势。如果我们用的数据不足来训练模型,就像我们不复习足够的材料而无法通过考试;如果我们尝试为数据拟合错误的模型,也会发生这种情况,就像我们如果采取错误的方法和错误的学习方式,做任何练习或考试都会得低分。我们将这些情况描述为机器学习中的 偏差,尽管它的方差较低,因为训练集和测试集中的表现一致,都是不好的。如果你需要快速回顾一下偏差,下面是它的定义:偏差是预测值和真实值之间的差异。它的计算方法如下:

这里,ŷ 是预测值,y 是真实值。

以下示例展示了典型的欠拟合情况,其中回归曲线没有很好地拟合数据,或者没有捕捉到数据的潜在模式:

A picture containing screenshot, line, diagram, plot  Description automatically generated

图 1.7:欠拟合示例

现在,让我们看看一个良好拟合的示例应该是什么样子:

A picture containing screenshot, line, plot  Description automatically generated

图 1.8:期望拟合的示例

偏差-方差权衡

显然,我们希望避免过拟合和欠拟合。回想一下,偏差是学习算法中由不正确假设引起的误差;高偏差会导致欠拟合。方差衡量模型预测对数据集变化的敏感度。因此,我们需要避免偏差或方差过高的情况。那么,是否意味着我们应该始终尽量将偏差和方差都降到最低?答案是,如果可能的话,应该是的。但在实践中,偏差和方差之间存在明确的权衡关系,减少一个会增加另一个。这就是所谓的偏差-方差权衡。听起来有点抽象?让我们看下一个例子。

假设我们被要求构建一个模型,基于电话民调数据预测候选人当选为美国下任总统的概率。该民调使用邮政编码进行调查。我们从某个邮政编码随机选择样本,估计该候选人获胜的概率为 61%。然而,事实证明他们输了选举。我们的模型出错的地方在哪里?我们首先可能想到的是样本数量过少,仅来自一个邮政编码。这个问题也来源于高偏差,因为某个地区的人们通常有相似的人口特征,尽管这样做会导致估算结果的方差较小。那么,能否通过使用来自更多邮政编码的样本来解决这个问题呢?是的,但不要太高兴。这可能会导致估算结果的方差同时增加。我们需要找到最佳的样本大小——即选择最佳的邮政编码数量,以实现最低的整体偏差和方差。

最小化模型的总误差需要谨慎平衡偏差和方差。给定一组训练样本,x[1]、x[2]、……、x[n],以及它们的目标值,y[1]、y[2]、……、y[n],我们希望找到一个回归函数ŷ(x),使得它尽可能准确地估计出真实关系 y(x)。我们通过均方误差MSE)来衡量估算误差,即回归模型的好坏:

E表示期望。这个误差可以通过以下公式分解为偏差和方差两个部分(尽管理解这一点需要一点基本的概率论知识):

其中,Bias项衡量估算误差,Variance项描述了估算值ŷ相对于其均值E[ŷ]的波动幅度。学习模型ŷ(x)越复杂,训练样本数量越大,偏差就越小。然而,这也会导致模型进行更多的调整,以更好地适应增多的数据点。结果,方差会增大。

我们通常采用交叉验证技术,以及正则化和特征降维,来找到平衡偏差和方差、减少过拟合的最优模型。接下来我们将讨论这些内容。

你可能会问为什么我们只关注过拟合问题:欠拟合呢?这是因为欠拟合很容易识别:如果模型在训练集上表现不佳,则会发生。发生这种情况时,我们需要找到一个更好的模型或调整一些参数以更好地拟合数据,这在任何情况下都是必须的。另一方面,过拟合很难察觉。通常情况下,当我们得到一个在训练集上表现良好的模型时,会过于高兴并认为它已经可以立即投入生产。这可能非常危险。我们应该采取额外措施,确保出色的性能不是由于过拟合造成的,并且这种出色的性能适用于排除训练数据的数据。

使用交叉验证避免过拟合

你将在本书的后续多次看到交叉验证的实际运用。因此,如果你发现这部分内容难以理解,请不要恐慌,因为你很快就会成为交叉验证的专家。

请记住,在练习问题和实际考试之间,有模拟考试,我们可以评估我们在实际考试中的表现如何,并利用这些信息进行必要的复习。在机器学习中,验证过程有助于评估模型如何推广到独立或未见数据集的能力。在传统的验证设置中,原始数据通常被分成三个子集,通常为 60%的训练集,20%的验证集,以及其余的 20%作为测试集。如果在划分后有足够的训练样本,并且我们只需要一个模拟性能的粗略估计,那么这种设置就足够了。否则,交叉验证更可取。交叉验证有助于减少变异性,从而限制过拟合。

在一轮交叉验证中,原始数据被分为两个子集,分别用于训练测试(或验证)。记录测试性能。类似地,通过不同的划分执行多轮交叉验证。所有轮次的测试结果最终平均以生成模型预测性能的更可靠估计。

当训练样本非常大时,通常将其分为训练、验证和测试(三个子集),并在后两者上进行性能检查。在这种情况下,交叉验证不太理想,因为为每一轮训练模型的计算成本很高。但如果你能负担得起,没有理由不使用交叉验证。当数据量不那么大时,交叉验证绝对是个不错的选择。

目前主要有两种交叉验证方案:穷举式和非穷举式。在穷举式方案中,我们在每轮中留下固定数量的观测值作为测试(或验证)样本,使用其余的观测值作为训练样本。这个过程会重复进行,直到所有可能的不同样本子集都被用于测试一次。例如,我们可以应用留一交叉验证LOOCV),让每个样本都成为一次测试集。对于一个大小为 n 的数据集,LOOCV 需要进行 n 轮交叉验证。当 n 较大时,这可能会非常慢。下图展示了 LOOCV 的工作流:

测试截图 自动生成的描述,信心较低

图 1.9:留一交叉验证的工作流

另一方面,非穷举式方案,顾名思义,并不会尝试所有可能的分区。这个方案中最常用的一种是k 折交叉验证。首先,我们将原始数据随机分为 k 个等大小 的折叠。在每次试验中,这些折叠中的一个会成为测试集,其余的数据将成为训练集。

我们将这个过程重复 k 次,每个折叠都作为指定的测试集。最后,我们将 k 组测试结果进行平均,以便评估。常见的 k 值为 3、5 和 10。下表展示了五折交叉验证的设置:

轮次折叠 1折叠 2折叠 3折叠 4折叠 5
1测试训练训练训练训练
2训练测试训练训练训练
3训练训练测试训练训练
4训练训练训练测试训练
5训练训练训练训练测试

表 1.1:五折交叉验证的设置

与 LOOCV 相比,K 折交叉验证通常具有较低的方差,因为我们使用的是一组样本,而不是单个样本进行验证。

我们还可以多次随机将数据分为训练集和测试集。这种方法正式称为留出法。这个算法的问题在于,有些样本可能永远不会进入测试集,而有些样本可能会被多次选入测试集。

最后但同样重要的是,嵌套交叉验证是交叉验证的组合。它包括以下两个阶段:

  • 内部交叉验证:这个阶段用于找到最佳拟合,可以实现为 k 折交叉验证。

  • 外部交叉验证:这个阶段用于性能评估和统计分析。

我们将在整本书中非常密集地使用交叉验证。在此之前,让我们通过类比来了解交叉验证,这将帮助我们更好地理解它。

一位数据科学家计划开车上班,他的目标是每天都在早上 9 点之前到达。他需要决定出发时间和路线。他尝试了周一、周二和周三这三天的不同参数组合,并记录了每次试验的到达时间。然后,他找出了最佳的调度并每天应用它。然而,效果并没有预期的好。

事实证明,调度模型对前面三天收集到的数据点发生了过拟合,可能在周四和周五效果不佳。更好的解决方案是,在周四和周五上测试从周一到周三得到的最佳参数组合,并根据不同的学习日和测试日组合,重复这一过程。这种类比交叉验证确保了所选调度在整个星期内都能有效。

总之,交叉验证通过结合对不同数据子集的预测性能评估,得出了更准确的模型表现评估。这项技术不仅减少了方差,避免了过拟合,还能让我们了解一个模型在实际中的总体表现。

通过正则化避免过拟合

另一种防止过拟合的方法是正则化。回想一下,模型的不必要复杂性是过拟合的来源。正则化通过在我们试图最小化的误差函数中添加额外的参数,从而惩罚复杂模型。

根据奥卡姆剃刀原理,应该偏好简单的方法。威廉·奥卡姆是一位僧侣和哲学家,大约在 1320 年,他提出了一个观点:应该选择最简单的、符合数据的假设。这样做的一个理由是,我们可以创造出比复杂模型更少的简单模型。例如,直观上,我们知道高次多项式模型比线性模型更多。原因在于,一条直线(y = ax + b)只由两个参数控制——截距 b 和斜率 a。直线的系数可以在二维空间内变化。而二次多项式则为二次项增加了一个额外的系数,我们可以通过系数在三维空间内进行表示。因此,使用高次多项式函数找到一个完全拟合所有训练数据点的模型要容易得多,因为它的搜索空间远大于线性函数。然而,这些容易得到的模型在泛化能力上远不如线性模型,更容易发生过拟合。当然,简单模型所需的计算时间也较少。下图展示了我们如何分别尝试将线性函数和高次多项式函数拟合到数据上:

一张包含线条、截图、文本、图表的图片 说明自动生成

图 1.10:用线性函数和多项式函数拟合数据

线性模型更为优选,因为它可能更好地推广到从底层分布中抽取的更多数据点。我们可以使用正则化通过对高阶多项式施加惩罚来减少其影响。即使从训练数据中学习到一个不那么准确且不那么严格的规则,这也能抑制模型的复杂性。

在本书中,我们将频繁使用正则化,从第四章使用逻辑回归预测在线广告点击率》开始。现在,让我们看一个类比,帮助你更好地理解正则化。

一位数据科学家希望给他的机器人看门狗配备识别陌生人和朋友的能力。他为其提供了以下学习样本:

男性年轻戴眼镜穿灰色朋友
女性中年一般无眼镜穿黑色陌生人
男性年轻矮小戴眼镜穿白色朋友
男性年长矮小无眼镜穿黑色陌生人
女性年轻一般戴眼镜穿白色朋友
男性年轻矮小无眼镜穿红色朋友

表 1.2:机器人看门狗的训练样本

机器人可能会迅速学会以下规则:

  • 任何中年女性,身高一般,未戴眼镜且穿着黑色衣服的,都是陌生人

  • 任何身材矮小的中年男性,戴眼镜且穿着黑色衣服的,都是陌生人

  • 其他任何人都是他的朋友

尽管这些规则完全符合训练数据,但它们似乎过于复杂,不太可能很好地推广到新访客。相比之下,数据科学家限制了学习的方面。一个对数百个其他访客有效的松散规则可能如下:任何没有眼镜、穿黑色衣服的人都是陌生人。

除了惩罚复杂性,我们还可以通过早期停止训练过程来防止过拟合。如果我们限制模型的学习时间或设置某些内部停止标准,更有可能得到一个更简单的模型。通过这种方式控制模型复杂度,因此,过拟合变得不太可能。这种方法在机器学习中称为早期停止

最后但同样重要的是,值得注意的是正则化应保持在适度水平,或者更准确地说,需要对其进行微调以达到最佳水平。正则化过小不会产生任何影响;正则化过大则会导致欠拟合,因为它会使模型远离真实值。我们将在第四章使用逻辑回归预测在线广告点击率》、第五章使用回归算法预测股票价格》和第六章使用人工神经网络预测股票价格》中探讨如何实现最佳正则化。

通过特征选择和降维来避免过拟合

我们通常将数据表示为一个数字网格(矩阵)。每一列代表一个变量,在机器学习中我们称之为特征。在监督学习中,其中一个变量实际上不是特征,而是我们试图预测的标签。在监督学习中,每一行是一个样本,我们可以用它来进行训练或测试。

特征的数量对应于数据的维度。我们的机器学习方法依赖于维度的数量与样本数量之间的关系。例如,文本和图像数据是高维的,而传感器数据(如温度、压力或 GPS)则维度相对较少。

拟合高维数据在计算上是昂贵的,并且容易出现过拟合,因为其复杂度较高。更高的维度也无法进行可视化,因此我们不能使用简单的诊断方法。

并不是所有特征都是有用的,它们可能只是增加了结果的随机性。因此,通常需要进行良好的特征选择。特征选择是挑选出一个重要特征子集,以便更好地构建模型的过程。在实际应用中,并不是数据集中的每个特征都包含有助于区分样本的信息;一些特征要么是冗余的,要么是无关的,因此可以在损失较小的情况下丢弃。

原则上,特征选择归结为多次二进制决策,即是否包含某个特征。对于 n 个特征,我们可以得到 2^n 个特征集,对于特征数量较多时,这个数字可能非常大。例如,10 个特征时,我们有 1,024 种可能的特征集(例如,如果我们在决定穿什么衣服,特征可以是温度、降雨、天气预报和我们要去的地方)。基本上,我们有两个选择:要么从所有特征开始,并逐步去除特征,要么从最小的特征集开始,并逐步添加特征。然后,我们将每次迭代中的最佳特征集进行比较。在某一时刻,暴力评估变得不可行。因此,发明了更先进的特征选择算法,用于提取最有用的特征/信号。我们将在第四章使用逻辑回归预测在线广告点击率中详细讨论如何进行特征选择。

另一种常见的降维方法是将高维数据转化为低维空间。这被称为降维特征投影。我们将在第七章使用文本分析技术挖掘 20 个新闻组数据集中详细讨论这一点,届时我们将把文本数据编码为二维空间;以及在第九章使用支持向量机识别面孔中,我们将讨论如何将高维图像数据投影到低维空间。

在本节中,我们讨论了机器学习的目标是找到数据的最佳泛化,并避免不良的泛化。在接下来的两节中,我们将探讨如何通过机器学习的各个阶段的技巧来接近这一目标,包括下一节中的数据预处理和特征工程,以及随后一节中的建模。

数据预处理和特征工程

数据预处理和特征工程在机器学习中起着至关重要的基础作用。这就像为一栋建筑奠定基础——基础越强大、准备得越充分,最终的结构(机器学习模型)就会越好。下面是它们关系的细分:

  • 预处理为高效学习准备数据:来自各种来源的原始数据通常包含不一致、错误和无关信息。预处理通过清理、组织和转换数据,将其转化为适合所选机器学习算法的格式。这使得算法能够更轻松高效地理解数据,从而提高模型性能。

  • 预处理有助于提高模型的准确性和泛化能力:通过处理缺失值、异常值和不一致性,预处理减少了数据中的噪音。这使得模型能够专注于数据中的真实模式和关系,从而提高预测的准确性,并在未见过的数据上实现更好的泛化能力。

  • 特征工程提供有意义的输入变量:原始数据经过转换和处理,生成新的特征或选择相关特征。新特征可能会改善模型性能并产生有价值的洞见。

总体而言,数据预处理和特征工程是机器学习工作流中至关重要的一步。通过投入时间和精力进行适当的预处理和特征工程,你为构建可靠、准确和具有广泛泛化能力的机器学习模型奠定了基础。在本节中,我们将首先讨论预处理阶段。

预处理与探索

在学习过程中,我们需要高质量的学习材料。我们无法从胡言乱语中学习,因此会自动忽略那些不合理的内容。机器学习系统无法识别胡言乱语,因此我们需要通过清理输入数据来帮助它。人们常说,清理数据占机器学习的很大一部分。有时,数据清理工作已经为我们完成,但你不应依赖这种情况。

要决定如何清理数据,我们需要熟悉数据。有些项目尝试自动探索数据并做出一些智能的操作,比如生成报告。遗憾的是,目前我们还没有普遍的解决方案,因此你需要做一些工作。

我们可以做两件事,它们并不是互相排斥的:首先,扫描数据,其次,可视化数据。这还取决于我们处理的数据类型——无论是数字网格、图像、音频、文本,还是其他什么类型。

最终,数字网格是最方便的形式,我们将始终致力于拥有数值特征。假设在本节的其余部分我们有一张数字表格。

我们想知道哪些特征有缺失值,缺失值如何分布,以及我们拥有哪些类型的特征。值大致可以遵循正态分布、二项分布、泊松分布,或其他分布。特征可以是二元的:要么是,是或否,正或负,等等。它们也可以是分类的:属于某个类别,例如大陆(非洲、亚洲、欧洲、南美洲、北美洲等)。分类变量也可以是有序的,例如高、中、低。特征也可以是定量的,例如温度(以度数表示)或价格(以美元表示)。现在,让我们深入探讨如何应对每种情况。

处理缺失值

很多时候,我们会缺失某些特征的值。这可能由于各种原因发生。始终拥有一个值可能不方便、昂贵,甚至是不可能的。也许我们过去无法测量某个数量,因为我们没有合适的设备,或者根本没有意识到这个特征是相关的。然而,我们只能接受过去的缺失值。

有时,我们很容易就能发现缺失值,只需通过扫描数据或统计某个特征的值的数量,并将这个数量与根据行数预计的值的数量进行比较,就能发现缺失值。某些系统会用例如 999,999 或-1 来编码缺失值。如果有效值远小于 999,999,那么这种做法是合理的。如果幸运的话,你可能会有数据字典或元数据提供的信息,帮助你了解特征的详细情况。

一旦我们知道缺失值的存在,就会出现如何处理它们的问题。最简单的答案是忽略它们。然而,某些算法无法处理缺失值,程序会直接拒绝继续执行。在其他情况下,忽略缺失值会导致结果不准确。第二种解决方案是用固定值替代缺失值——这叫做插补。我们可以用某一特征有效值的算术平均值中位数众数来进行插补。理想情况下,我们会有一些相对可靠的变量的先验知识。例如,我们可能知道某个地点的季节性温度平均值,可以根据日期对缺失的温度值进行插补。我们将在第十章机器学习最佳实践中详细讨论如何处理缺失数据。类似的,接下来几节的技术将在后续章节中讨论和应用,以防你对它们的使用方式感到不确定。

标签编码

人类能够处理各种类型的值。机器学习算法(有些例外)要求数值型的值。如果我们提供一个字符串,例如Ivan,除非我们使用专门的软件,否则程序不会知道如何处理。在这个例子中,我们处理的是一个类别特征——可能是名字。我们可以把每个独特值看作一个标签。(在这个特定例子中,我们还需要决定如何处理大小写——Ivanivan是否相同?)。然后我们可以用一个整数替换每个标签——标签编码

以下示例展示了标签编码的工作原理:

标签编码标签
非洲1
亚洲2
欧洲3
南美洲4
北美洲5
其他6

表 1.3:标签编码示例

这种方法在某些情况下可能会有问题,因为学习者可能会得出有顺序的结论(除非这是预期的,例如,bad=0ok=1good=2,和excellent=3)。在前面的映射表中,AsiaNorth America在编码后相差4,这有点不直观,因为很难量化它们。下一节的独热编码采用了不同的方法。

独热编码

一对 K,或者说独热编码,方案使用虚拟变量来编码类别特征。最初,这一方法应用于数字电路。虚拟变量具有二进制值,如比特,因此它们取值为零或一(相当于真或假)。例如,如果我们想要编码大洲,我们将有虚拟变量,如is_asia,如果该大洲是Asia,则为真,否则为假。一般来说,我们需要的虚拟变量数量等于独特值的数量减去一(或者有时是独特值的确切数量)。我们可以从虚拟变量中自动确定一个标签,因为它们是互斥的。

如果虚拟变量的值都是假值,那么正确的标签就是没有虚拟变量的标签。下表展示了大陆的编码方式:

大陆是否为非洲是否为亚洲是否为欧洲是否为南美洲是否为北美洲
非洲10000
亚洲01000
欧洲00100
南美洲00010
北美00001
其他00000

表 1.4: 独热编码示例

编码会生成一个矩阵(数字网格),其中包含许多零(假值)和偶尔的 1(真值)。这种类型的矩阵称为稀疏矩阵。稀疏矩阵表示由scipy包很好地处理,稍后我们将在本章中讨论它。

密集嵌入

虽然独热编码是一种简单且稀疏的分类特征表示,密集嵌入提供了一个紧凑的、连续的表示,能够基于数据中的共现模式捕捉语义关系。例如,使用密集嵌入,可能将大陆类别表示为类似于以下的三维连续向量:

  • 非洲: [0.9, -0.2, 0.5]

  • 亚洲: [-0.1, 0.8, 0.6]

  • 欧洲: [0.6, 0.3, -0.7]

  • 南美洲: [0.5, 0.2, 0.1]

  • 北美洲: [0.4, 0.3, 0.2]

  • 其他: [-0.8, -0.5, 0.4]

在这个例子中,你可能会注意到南美和北美的向量比非洲和亚洲的向量更接近。密集嵌入能够捕捉类别之间的相似性。在另一个例子中,你可能会看到欧洲和北美的向量更接近,这基于文化上的相似性。

我们将在第七章使用文本分析技术挖掘 20 个新闻组数据集中进一步探讨密集嵌入。

缩放

不同特征的值可能相差几个数量级。有时,这意味着较大的值会主导较小的值。这取决于我们使用的算法。为了某些算法能够正常工作,我们需要对数据进行缩放。

以下是我们可以应用的几种常见策略:

  • 标准化会移除特征的均值,并将其除以标准差。如果特征值是正态分布的,我们将得到一个高斯分布,它围绕零对称,方差为一。

  • 如果特征值不是正态分布的,我们可以去除中位数并除以四分位数范围。四分位数范围是指第一四分位数和第三四分位数之间的范围(或第 25^(th)和第 75^(th)百分位数)。

  • 零到一之间的范围是特征缩放中常见的范围选择。

在本书中的许多项目中,我们将使用缩放。

数据预处理的高级版本通常称为特征工程。我们将在接下来的部分讨论这一点。

特征工程

特征工程是创建或改进特征的过程。特征通常是基于常识、领域知识或先前的经验来创建的。特征创建有一些常见的技术;然而,并不能保证创建新特征会改善你的结果。有时,我们可以使用无监督学习找到的簇作为额外的特征。深度神经网络通常能够自动推导出特征。

我们将简要介绍一些特征工程技术:多项式转换和分箱。

多项式转换

如果我们有两个特征,ab,我们可能会怀疑它们之间存在多项式关系,例如 a² + ab + b²。我们可以将一个新特征视为 ab交互,例如它们的乘积 ab。交互不一定是乘积——虽然这是最常见的选择——它也可以是和、差或比率。如果我们使用比率来避免除以零,我们应该在除数和被除数上都加上一个小常数。

多项式关系中的特征数量和多项式的阶数没有限制。然而,如果我们遵循奥卡姆剃刀原理,我们应该避免使用高阶多项式和多个特征的交互。在实际应用中,复杂的多项式关系往往更难计算,并且容易过拟合,但如果你确实需要更好的结果,它们可能值得考虑。我们将在第十章《机器学习最佳实践》中的最佳实践 12——在没有领域知识的情况下进行特征工程部分中看到多项式转换的应用。

分箱

有时候,将特征值分到几个箱子里是有用的。例如,我们可能只关心某一天是否下雨。根据降水值,我们可以将值二值化,如果降水值不为零,则为真值,否则为假值。我们也可以使用统计方法将值分为高、中、低三个箱子。在营销中,我们通常更关心年龄段,比如 18 至 24 岁,而不是具体的年龄,比如 23 岁。

分箱过程不可避免地会导致信息的丢失。然而,根据你的目标,这可能不是问题,实际上还可能减少过拟合的机会。当然,这样做会提高速度,并减少内存或存储需求及冗余。

任何现实世界中的机器学习系统都应该有两个模块:一个数据预处理模块,我们在本节中已经覆盖,另一个是建模模块,将在下一节中介绍。

模型组合

一个模型接受数据(通常是预处理过的数据)并产生预测结果。如果我们使用多个模型呢?通过结合各个模型的预测结果,我们能做出更好的决策吗?我们将在本节讨论这一点。

让我们从一个类比开始。在高中时,我们会和其他同学一起坐在一起学习,但考试时我们不应该一起合作。原因当然是,老师想知道我们学到了什么,如果我们只是从朋友那里抄答案,我们可能什么也没学到。后来,在生活中我们发现团队合作很重要。例如,这本书是整个团队的成果,或者可能是多个团队的成果。

显然,一个团队可以比单个个体产生更好的结果。然而,这与奥卡姆剃刀原理相悖,因为单个个体相比团队可以提出更简单的理论。在机器学习中,我们仍然倾向于让模型通过以下模型组合方案进行合作:

  • 投票和平均

  • Bagging

  • 提升

  • 堆叠

现在我们来深入探讨它们。

投票和平均

这可能是最容易理解的模型集成类型。它只是意味着最终的输出将是多个模型预测输出值的多数平均值。也可以为集成中的各个模型分配不同的权重;例如,某些更可靠的模型可能会被赋予两个投票权。

然而,结合彼此高度相关的模型的结果并不能保证显著的改进。最好通过使用不同的特征或不同的算法来某种程度上多样化模型。如果你发现两个模型高度相关,例如,你可以决定从集成中移除一个模型,并按比例增加另一个模型的权重。

Bagging

自助聚合Bootstrap aggregating,简称bagging)是由著名统计学家、加州大学伯克利分校的 Leo Breiman 于 1994 年提出的算法,它将自助法应用于机器学习问题。自助法是一种统计程序,通过有放回地抽样数据,从现有数据集中创建多个数据集。自助法可以用来衡量模型的属性,例如偏差和方差。

一般来说,bagging 算法遵循以下步骤:

  1. 我们通过有放回地抽样生成新的训练集。

  2. 对于每个生成的训练集,我们拟合一个新的模型。

  3. 我们通过平均或多数投票来结合模型的结果。

下图说明了使用分类作为示例的 bagging 步骤(圆圈和叉号代表来自两个类别的样本):

图 1.11:用于分类的 bagging 工作流

正如你所想的,bagging 可以减少过拟合的可能性。

我们将在第三章中深入研究 bagging,使用基于树的算法预测在线广告点击率

提升

在监督学习的背景下,我们将弱学习器定义为比基线稍好一点的学习器,例如随机分配类别或平均值。就像蚂蚁一样,弱学习器个体很弱,但它们组合在一起,能做出令人惊讶的事情。

考虑到每个单独学习者的强度并使用权重是有意义的。这个总体思想叫做提升。在提升过程中,所有模型是按顺序训练的,而不是像集成方法(bagging)那样并行训练。每个模型都在相同的数据集上训练,但每个数据样本的权重不同,会考虑到前一个模型的成功。模型训练完成后,权重会被重新分配,用于下一轮训练。通常,对于预测错误的样本,会增加其权重,以加强对这些样本的预测难度。

下图展示了提升过程的步骤,仍以分类为例(圆圈和叉号代表来自两个类别的样本,圆圈或叉号的大小表示分配给它的权重):

设备截图 描述自动生成,信心较低

图 1.12:分类任务中的提升工作流程

有许多种提升算法;这些提升算法的主要区别在于它们的加权方案。如果你曾为考试学习过,可能已经应用了类似的技巧,通过识别自己在练习题中遇到困难的类型,并集中精力攻克难题。

Viola-Jones 是一个流行的人脸检测框架,它利用提升算法高效地识别图像中的人脸。在图像或视频中检测人脸属于监督学习。我们给学习者提供包含人脸区域的示例。这里存在不平衡问题,因为通常没有人脸的区域要远多于有脸的区域(大约多 1 万倍)。

一系列分类器逐步筛选出这些负面图像区域。每个阶段,分类器使用越来越多的特征,并在较少的图像窗口上进行处理。其思想是将大部分时间花费在包含人脸的图像区域上。在这种情况下,使用提升(boosting)来选择特征并结合结果。

堆叠

**堆叠(Stacking)**方法是将机器学习模型的输出值作为另一个算法的输入值。你当然可以将更高层次算法的输出再作为另一个预测器的输入。你可以使用任何任意拓扑结构,但出于实际原因,你应该先尝试简单的设置,这也符合奥卡姆剃刀原则。

一个有趣的事实是,堆叠常被用于 Kaggle 比赛中的获胜模型。例如,奥托集团产品分类挑战赛的第一名(www.kaggle.com/c/otto-grou…)就是由一个包含 30 多种不同模型的堆叠模型获得的。

到目前为止,我们已经讨论了在数据预处理和建模阶段更容易达到机器学习模型正确泛化所需的一些技巧。我知道你迫不及待想开始一个机器学习项目。让我们通过设置工作环境来做好准备。

安装软件和设置

正如书名所示,Python 是我们在整本书中实现所有机器学习算法和技术的编程语言。我们还将使用许多流行的 Python 包和工具,如 NumPy、SciPy、scikit-learn、TensorFlow 和 PyTorch。在本章结束时,确保你已经正确设置了工具和工作环境,即使你已经是 Python 专家或对上述一些工具非常熟悉。

设置 Python 和环境

本书中我们将使用 Python 3。Anaconda 的 Python 3 发行版是数据科学和机器学习从业者的最佳选择之一。

Anaconda 是一个免费的 Python 发行版,专为数据分析和科学计算设计。它有自己的包管理器 conda。该发行版(docs.anaconda.com/free/anaconda/,根据你的操作系统或 Python 版本 3.7 到 3.11)包含大约 700 个 Python 包(截至 2023 年),使其非常方便。对于普通用户来说,Minicondaconda.io/miniconda.html)发行版可能是更好的选择。Miniconda 包含了 conda 包管理器和 Python。显然,Miniconda 占用的磁盘空间比 Anaconda 要小得多。

安装 Anaconda 和 Miniconda 的过程是相似的。你可以按照docs.conda.io/projects/conda/en/latest/user-guide/install/上的说明进行操作。首先,你需要根据你的操作系统和 Python 版本下载合适的安装程序,如下所示:

A picture containing text, screenshot, font  Description automatically generated

图 1.13:根据你的操作系统选择的安装入口

按照你的操作系统列出的步骤操作。你可以选择图形用户界面(GUI)或命令行界面(CLI)。我个人认为后者更容易。

Anaconda 附带了自己的 Python 安装。在我的机器上,Anaconda 安装程序在我的主目录中创建了一个 anaconda 目录,并大约需要 900 MB 的空间。类似地,Miniconda 安装程序会在你的主目录中安装一个 miniconda 目录。

安装完毕后,随时可以进行尝试。验证是否正确设置 Anaconda 的一种方法是,在 Linux/Mac 的终端或 Windows 的命令提示符中输入以下命令(从现在开始,我们只提到终端):

python 

上述命令行将显示你的 Python 运行环境,如下图所示:

图 1.14:在终端中运行“python”后的截图

如果您没有看到此信息,请检查系统路径或 Python 的运行路径。

总结这一部分,我想强调为什么 Python 是机器学习和数据科学中最受欢迎的语言。首先,Python 因其高可读性和简洁性而闻名,使得构建机器学习模型变得更加容易。我们可以花更少的时间去担心正确的语法和编译,从而有更多的时间去寻找合适的机器学习解决方案。其次,我们拥有大量的 Python 库和框架来支持机器学习:

任务Python 库
数据分析NumPy、SciPy 和 pandas
数据可视化Matplotlib 和 Seaborn
建模scikit-learn、TensorFlow、Keras 和 PyTorch

表 1.5:机器学习中常用的 Python 库

下一步是设置一些本书中将要使用的包。

安装主要的 Python 包

在本书的大多数项目中,我们将使用 NumPy (www.numpy.org/)、SciPy (scipy.org/)、pandas 库 (pandas.pydata.org/)、scikit-learn (scikit-learn.org/stable/)、TensorFlow (www.tensorflow.org/) 和 PyTorch (pytorch.org/)。

在接下来的章节中,我们将介绍几种本书中主要使用的 Python 包的安装方法。

Conda 环境提供了一种为不同项目隔离依赖项和包的方法。因此,建议为新项目创建并使用一个环境。我们可以使用以下命令创建一个名为“pyml”的环境:

conda create --name pyml python=3.10 

在这里,我们还指定了 Python 版本 3.10,虽然这是可选的,但强烈推荐使用。这样做是为了避免默认使用最新版本的 Python,因为它可能与许多 Python 包不兼容。例如,在编写本文时(2023 年底),PyTorch 不支持 Python 3.11

为了激活新创建的环境,我们使用以下命令:

conda activate pyml 

激活的环境会显示在提示符前,如下所示:

(pyml) hayden@haydens-Air ~ % 

NumPy

NumPy 是使用 Python 进行机器学习的基础包。它提供了强大的工具,包括以下内容:

  • N维数组(ndarray)类及其多个子类,代表矩阵和数组

  • 各种复杂的数组函数

  • 有用的线性代数功能

NumPy 的安装说明可以在 numpy.org/install/ 找到。或者,您也可以通过 condapip 在命令行中安装,具体如下:

conda install numpy 

pip install numpy 

验证安装的快捷方式是按照如下方式在 Python 中导入:

>>> import numpy 

如果没有出现错误信息,则表示安装成功。

SciPy

在机器学习中,我们主要使用 NumPy 数组来存储由特征向量组成的数据向量或矩阵。SciPy (scipy.org/) 使用 NumPy 数组,并提供各种科学和数学函数。在终端中安装 SciPy 与以下方式类似:

conda install scipy 

pip install scipy 

pandas

我们还使用 pandas 库 (pandas.pydata.org/) 在本书后面进行数据整理。获取 pandas 的最佳方法是通过 pipconda,例如:

conda install pandas 

scikit-learn

scikit-learn 库是一个优化性能的 Python 机器学习包,其大部分代码运行速度几乎与等效的 C 代码一样快。NumPy 和 SciPy 也是如此。scikit-learn 需要安装 NumPy 和 SciPy。如 scikit-learn.org/stable/install.html 中的安装指南所述,安装 scikit-learn 的最简单方法是使用 pipconda,如下所示:

pip install -U scikit-learn 

conda install -c conda-forge scikit-learn 

这里,我们使用“-c conda-forge”选项告诉 condaconda-forge 渠道中搜索软件包,这是一个由社区驱动的渠道,提供广泛的开源软件包。

TensorFlow

TensorFlow 是由 Google Brain 团队发明的 Python 友好的开源库,用于高性能数值计算。它使机器学习更快速,深度学习更容易,具有基于 Python 的便捷前端 API 和基于高性能 C++ 的后端执行。TensorFlow 2 在其首个成熟版本 1.0 的基础上进行了大规模重设计,并于 2019 年底发布。

TensorFlow 因其深度学习模块而广为人知。然而,其最强大之处在于计算图,其算法基于此构建。基本上,计算图用于通过张量传达输入与输出之间的关系。

例如,如果我们想评估线性关系 y = 3 * a + 2 * b,我们可以在以下计算图中表示它:

包含屏幕截图、圆圈、图表、草图的图片 自动生成描述

图 1.15:y = 3 * a + 2 * b 机器的计算图

这里,ab 是输入张量,cd 是中间张量,y 是输出。

您可以将计算图视为由边缘连接的节点网络。每个节点是一个张量,每条边缘是一个操作或函数,它接受其输入节点并将值返回给其输出节点。为了训练机器学习模型,TensorFlow 构建计算图并相应地计算梯度(梯度是向量,提供达到最优解的最陡峭方向)。在接下来的章节中,您将看到使用 TensorFlow 训练机器学习模型的示例。

如果你有兴趣深入了解 TensorFlow 和计算图,我们强烈建议你访问 www.tensorflow.org/guide/data

TensorFlow 允许在 CPU 和 GPU 之间轻松部署计算,这使得大规模的高成本机器学习成为可能。在本书中,我们将重点使用 CPU 作为计算平台。因此,按照 www.tensorflow.org/install/ 的说明,安装 TensorFlow 2 的命令行如下:

conda install -c conda-forge tensorflow 

或者

pip install tensorflow 

你可以通过在 Python 中导入 PyTorch 来验证安装是否成功。

PyTorch

PyTorch 是一个开源机器学习库,主要用于开发深度学习模型。它提供了一个灵活高效的框架来构建神经网络并在 GPU 上执行计算。PyTorch 由 Facebook 的 AI 研究实验室开发,并在研究和工业界广泛使用。

与 TensorFlow 类似,PyTorch 的计算基于 有向无环图DAG)。不同之处在于,PyTorch 使用 动态计算图,允许在运行时即时构建计算图,而 TensorFlow 使用 静态 计算图,计算图结构在执行前已定义并执行。这种动态特性使得模型设计更加灵活,调试更加简便,也便于动态控制流,因此适用于广泛的应用。

由于其灵活性、易用性和高效的计算能力,PyTorch 已成为深度学习领域研究人员和从业人员的热门选择。其直观的界面和强大的社区支持使其成为多种应用的有力工具,包括计算机视觉、自然语言处理、强化学习等。

要安装 PyTorch,建议根据系统和方法查阅 pytorch.org/get-started/locally/ 上的最新安装指令。

例如,我们通过 conda 在 Mac 上安装最新稳定版本(截至 2023 年底为 2.2.0),使用以下命令:

conda install pytorch::pytorch torchvision  -c pytorch 

最佳实践

如果你在安装过程中遇到问题,请阅读说明页面上提供的平台和软件包特定的建议。本书中的所有 PyTorch 代码都可以在 CPU 上运行,除非特别指出仅适用于 GPU。不过,如果你希望加快神经网络模型的训练并充分享受 PyTorch 的优势,建议使用 GPU。如果你有显卡,请参考安装说明并设置适当的计算平台。例如,我在 Windows 上使用 GPU 安装时使用以下命令:

conda install pytorch torchvision pytorch-cuda=11.8 -c pytorch -c nvidia 

要检查是否正确安装了带有 GPU 支持的 PyTorch,可以运行以下 Python 代码:

>>> import torch
>>> torch.cuda.is_available()
True 

另外,你可以使用 Google Colab (colab.research.google.com/),免费使用 GPU 训练一些神经网络模型。

我们将会大量使用其他一些包,例如Matplotlib用于绘图和可视化,Seaborn用于可视化,NLTK用于自然语言处理任务,transformers用于基于大型数据集预训练的先进模型,OpenAI Gym用于强化学习。每当我们首次遇到某个包时,我们会提供安装详情。

总结

我们刚刚完成了 Python 和机器学习之旅的第一步!在本章中,我们熟悉了机器学习的基础知识。我们从机器学习的定义、重要性和简短历史开始,还了解了最近的发展动态。我们还学习了典型的机器学习任务,并探索了几种处理数据和模型的基本技术。现在,我们已经掌握了基本的机器学习知识,并且设置好了相关的软件和工具,让我们为接下来的实际机器学习示例做好准备吧。

在下一章中,我们将构建一个电影推荐引擎,作为我们的第一个机器学习项目!

练习

  1. 你能说出机器学习和传统编程(基于规则的自动化)之间的区别吗?

  2. 什么是过拟合,我们如何避免它?

  3. 列举两种特征工程方法。

  4. 列举两种组合多个模型的方法。

  5. 如果你感兴趣,可以安装 Matplotlib (matplotlib.org/)。我们将在本书中使用它进行数据可视化。

加入我们书籍的 Discord 社区

加入我们社区的 Discord 空间,和作者及其他读者一起讨论:

packt.link/yuxi

第二章:使用朴素贝叶斯构建电影推荐引擎

如承诺的那样,在本章中,我们将以机器学习分类,特别是二元分类,开始我们的监督学习之旅。本章的目标是构建一个电影推荐系统,这是从现实生活中的一个例子学习分类的一个很好的起点——电影流媒体服务提供商已经在做这件事,我们也可以做到。

在本章中,您将学习分类的基本概念,包括分类的作用、不同类型及应用,重点解决一个二元分类问题,使用一种简单而强大的算法——朴素贝叶斯。最后,本章将演示如何微调模型,这是一项每个数据科学或机器学习从业者都应该掌握的重要技能。

我们将详细讨论以下主题:

  • 开始分类

  • 探索朴素贝叶斯

  • 实现朴素贝叶斯

  • 使用朴素贝叶斯构建电影推荐系统

  • 评估分类性能

  • 使用交叉验证调整模型

开始分类

电影推荐可以被看作一个机器学习分类问题。例如,如果预测您会喜欢某部电影,因为您曾喜欢或观看过类似的电影,那么它会出现在您的推荐列表中;否则,它不会。让我们从学习机器学习分类的基本概念开始。

分类是监督学习的主要实例之一。给定一个包含观测值及其相关类别输出的训练数据集,分类的目标是学习一个通用规则,将观测值(也称为特征预测变量)正确映射到目标类别(也称为标签)。换句话说,一个训练好的分类模型会在模型从训练样本的特征和目标中学习后生成,如图 2.1的前半部分所示。当新的或未见过的数据输入时,训练好的模型将能够确定它们的目标类别。类信息将根据已知的输入特征,使用训练好的分类模型进行预测,如图 2.1的后半部分所示:

图 2.1:分类中的训练和预测阶段

一般来说,分类根据类别输出的可能性有三种类型——二元多类多标签分类。我们将在本节中逐一介绍它们。

二元分类

二分类将观测值分类为两种可能类别之一。我们每天遇到的垃圾邮件过滤就是二分类的典型应用,它识别电子邮件(输入观测值)是垃圾邮件还是非垃圾邮件(输出类别)。客户流失预测是另一个常见的例子,预测系统从客户关系管理CRM)系统中获取客户细分数据和活动数据,并识别哪些客户可能会流失。

营销和广告行业的另一个应用是在线广告的点击预测——即根据用户的兴趣信息和浏览历史,预测广告是否会被点击。最后但同样重要的是,二分类也在生物医学科学中得到了应用,例如,在癌症早期诊断中,根据 MRI 图像将患者分类为高风险或低风险组。

图 2.2所示,二分类尝试找到一种方法,将数据分为两个类别(分别用点和叉表示):

图 2.2:二分类示例

别忘了,预测一个人是否喜欢某部电影也是一个二分类问题。

多类分类

这种分类方法也称为多项式分类。与二分类只有两种可能类别不同,它允许多于两个的类别。手写数字识别是一个常见的分类实例,自 20 世纪初以来,已进行大量研究和开发。例如,一个分类系统可以学习读取并理解手写的邮政编码(大多数国家的数字从 0 到 9),从而自动对信封进行分类。

手写数字识别已成为学习机器学习过程中的*“Hello, World!”*,由国家标准与技术研究院NIST)构建的扫描文档数据集,称为修改版国家标准与技术研究院MNIST),是一个常用的基准数据集,用于测试和评估多类分类模型。图 2.3展示了从 MNIST 数据集中提取的四个样本,分别代表数字“9”、“2”、“1”和“3”:

图 2.3:来自 MNIST 数据集的样本

另一个例子,在图 2.4中,多类分类模型尝试找到分隔边界,将数据分为以下三种不同的类别(分别用点、叉和三角形表示):

一张包含屏幕截图、图表的图片  描述自动生成

图 2.4:多类分类示例

多标签分类

在前两种类型的分类中,目标类别是互斥的,每个样本只能分配一个,且仅有一个标签。而在多标签分类中则正好相反。由于现代应用中类别组合的特性,越来越多的研究关注多标签分类。例如,一张同时捕捉到大海和日落的图片可以同时属于这两种概念场景,而在二分类情况下,它只能是猫或狗的图像,或者在多类别情况下只能是橙子、苹果或香蕉中的一种水果。同样,冒险电影常常与其他类型的电影结合,如奇幻、科幻、恐怖和剧情片。

另一个典型的应用是蛋白质功能分类,因为一个蛋白质可能具有多种功能——存储、抗体、支撑、运输等等。

解决n标签分类问题的典型方法是将其转化为一组n二分类问题,每个二分类问题由一个独立的二分类器处理。

请参考图 2.5,查看如何将多标签分类问题重构为多个二分类问题:

A diagram of a multi-label classifier  Description automatically generated with medium confidence

图 2.5:将三标签分类转化为三个独立的二分类问题

再次使用蛋白质功能分类的例子,我们可以将其转化为几个二分类问题,例如:它是用来存储的吗?它是用来抗体的吗?它是用来支撑的吗?

为了解决这些问题,研究人员开发了许多强大的分类算法,其中朴素贝叶斯、支持向量机SVMs)、决策树、逻辑回归和神经网络常常被使用。

在接下来的章节中,我们将介绍朴素贝叶斯的机制及其深入实现,以及其他重要概念,包括分类器调优和分类性能评估。请关注接下来的章节,了解其他分类算法。

探索朴素贝叶斯

朴素贝叶斯分类器属于概率分类器的范畴。它计算每个预测特征(也称为属性信号)属于每个类别的概率,从而对所有类别的概率分布做出预测。当然,从得到的概率分布中,我们可以得出数据样本最有可能关联的类别。朴素贝叶斯具体做的事情,如其名所示,包含以下内容:

  • 贝叶斯:也就是说,它根据贝叶斯定理,将观察到的输入特征在给定可能类别下的概率映射到在观察到的证据基础上给定类别的概率。

  • 朴素:即假设预测特征是相互独立的,从而简化概率计算。

我将在下一节通过实例来解释贝叶斯定理。

贝叶斯定理通过实例

在深入了解分类器之前,理解贝叶斯定理非常重要。让AB表示任何两个事件。事件可能是“明天会下雨”,“从一副扑克牌中抽出两张国王”或者“一个人患有癌症”。在贝叶斯定理中,P(A | B)表示在B为真的情况下,A发生的概率。它可以通过以下公式计算:

这里,P(B | A)是给定B发生时B的概率,而P(A)和P(B)分别是BA发生的概率。是不是太抽象了?让我们通过以下具体的例子来考虑:

  • 例子 1:给定两个硬币,其中一个是不公平的,90%的翻转结果为正面,10%的结果为反面,另一个是公平的。随机选择一个硬币并进行翻转。如果我们得到正面,那个硬币是那个不公平的硬币的概率是多少?

我们可以通过首先将U表示为选择不公平硬币的事件,F表示公平硬币,H表示出现正面的事件来解决这个问题。因此,在出现正面时,*P(U | H)*是选择不公平硬币的概率,可以通过以下公式计算:

如我们所知,P(H | U)为0.9P(U)为0.5,因为我们从两个硬币中随机选择一个。然而,推导出出现正面的概率P(H)并不那么简单,因为两种事件可能导致以下情况,其中U是选择不公平硬币,F是选择公平硬币:

现在,P(U | H)变为以下形式:

所以,在贝叶斯定理下,得到正面时选择不公平硬币的概率是0.64

  • 例子 2:假设一位医生报告了以下癌症筛查测试结果,涉及 10,000 人:
癌症无癌症总计
筛查阳性80900980
筛查阴性2090009020
总计100990010000

表 2.1:癌症筛查结果示例

这表明 100 名癌症患者中有 80 人被正确诊断,另外 20 人未被诊断出;癌症在 9,900 名健康人中有 900 人被误诊。

如果某个人的筛查结果为阳性,那么他实际上患有癌症的概率是多少?我们将患癌症和筛查结果为阳性分别表示为CPos。因此,P(Pos |C) = 80/100 = 0.8P(C) = 100/10000 = 0.01P(Pos) = 980/10000 = 0.098

我们可以应用贝叶斯定理来计算P(C | Pos):

给定一个阳性筛查结果,受试者患癌症的几率是 8.16%,这比在没有进行筛查的情况下(假设 100/10000=1%)高出很多。

  • 例子 3:在一个工厂中,三台机器 ABC 分别占据了 35%、20% 和 45% 的灯泡生产份额。每台机器生产的次品灯泡比例分别为 1.5%、1% 和 2%。一只由该工厂生产的灯泡被鉴定为次品,表示为事件 D。分别求出这只灯泡是由机器 ABC 制造的概率。

再次,我们可以简单地遵循贝叶斯定理:

所以,根据贝叶斯定理,这个灯泡是由 ABC 机器制造的概率分别是 0.3230.1230.554

此外,无论哪种方式,我们甚至不需要计算 P(D),因为我们知道以下情况:

我们还知道以下概念:

所以,我们有以下公式:

这种简便的方法得出了与原始方法相同的结果,但速度更快。现在你理解了贝叶斯定理是朴素贝叶斯的核心,我们可以轻松地继续进行分类器本身的内容。

朴素贝叶斯的原理

让我们从讨论算法背后的魔力开始——朴素贝叶斯是如何工作的。给定一个数据样本,x,它有n个特征,x[1]、x[2]、...、x[n](x 代表一个特征向量,并且 x = (x[1]、x[2]、...、x[n])),朴素贝叶斯的目标是确定这个样本属于 K 个可能类别 y[1]、y[2]、...、y[K] 中每一个的概率,即 P(y[K] |x)P(x[1]、x[2]、...、x[n]),其中 k = 1, 2, …, K

这看起来和我们刚刚处理的没什么不同:xx[1]、x[2]、...、x[n]。这是一个联合事件,表示一个样本观察到特征值 x[1]、x[2]、...、x[n]。y[K] 是样本属于类别 k 的事件。我们可以直接应用贝叶斯定理:

让我们详细看一下每个组成部分:

  • P(y[k]) 描述了类别的分布情况,并没有进一步观察特征的知识。因此,它也被称为贝叶斯概率术语中的先验概率。先验概率可以是预定的(通常是均匀的,即每个类别的发生机会相同),也可以通过一组训练样本进行学习。

  • P(y[k]|x),与先前的 P(y[k]) 相比,是后验概率,它包含了额外的观察信息。

  • P(x |y[K]),或 P(x[1], x[2],..., x[n]|y[k]),是给定样本属于类别y[k]时,n个特征的联合分布。这表示具有这些值的特征共同发生的可能性。在贝叶斯术语中,这被称为似然。显然,随着特征数量的增加,计算似然将变得困难。在朴素贝叶斯中,这是通过特征独立性假设来解决的。n个特征的联合条件分布可以表示为单个特征条件分布的联合乘积:

每个条件分布可以通过一组训练样本高效地学习得到。

  • P(x),也称为证据,仅依赖于特征的整体分布,而与特定类别无关,因此它是一个归一化常数。因此,后验概率与先验概率和似然成正比:

图 2.6 总结了如何训练朴素贝叶斯分类模型并将其应用于新数据:

图 2.6:朴素贝叶斯分类的训练和预测阶段

朴素贝叶斯分类模型是通过使用标记数据进行训练的,每个实例都与一个类别标签相关联。在训练过程中,模型学习给定每个类别时特征的概率分布。这涉及到计算给定每个类别时观察到每个特征值的似然。一旦训练完成,模型就可以应用于新的、未标记的数据。为了对一个新实例进行分类,模型使用贝叶斯定理计算给定观察到的特征下每个类别的概率。

在我们深入探讨朴素贝叶斯的实现之前,让我们通过一个简化的电影推荐示例来看一下朴素贝叶斯分类器的应用。给定四个(伪)用户,他们是否喜欢三部电影,m[1],m[2]m[3](用 1 或 0 表示),以及他们是否喜欢目标电影(表示为事件Y)或不喜欢(表示为事件N),如以下表格所示,我们需要预测另一个用户喜欢该电影的可能性:

IDm1m2m3用户是否喜欢目标电影
训练数据1011Y
2001N
3000Y
4110Y
测试案例5110?

表 2.2:电影推荐的玩具数据示例

用户是否喜欢三部电影,m[1],m[2]m[3],是我们可以用来预测目标类别的特征(信号)。我们拥有的训练数据是包含评分和目标信息的四个样本。

现在,让我们首先计算先验,P(Y) 和 P(N)。通过训练集,我们可以轻松获得以下数据:

另外,我们也可以假设一个均匀先验,例如P(Y) = 50%。

为了简化起见,我们将用户喜欢三部电影与否的事件分别表示为f[1], f[2],f[3]。为了计算后验概率 P(Y| x),其中 x = (1, 1, 0),第一步是计算可能性,P(f[1] = 1| Y), P(f[2] = 1| Y), 和 P(f[3] = 0| Y),同样地,P(f[1] = 1| N), P(f[2] = 1| N), 和 P(f[3] = 0| N),基于训练集计算。然而,你可能注意到,由于在N*类中没有看到 f[1] = 1,我们会得到 P(f[1] = 1| N) = 0。因此,我们将得到如下结果:

这意味着我们将不加思索地通过任何方式预测类别 = Y

为了消除零乘法因子(未知的可能性),我们通常会为每个特征分配一个初始值为 1 的值,即我们从 1 开始计算每个特征的可能值。这种技术也被称为拉普拉斯平滑。通过这种修改,我们现在得到如下结果:

这里,给定类别N,0 + 1 表示有零个m[1]的点赞,加上+1 平滑;1 + 2 表示有一个数据点(ID = 2),加上 2(2 个可能的值)+ 1 平滑。给定类别Y,1 + 1 表示有一个m[1]的点赞(ID = 4),加上+1 平滑;3 + 2 表示有 3 个数据点(ID = 1, 3, 4),加上 2(2 个可能的值)+ 1 平滑。

同样地,我们可以计算以下内容:

现在,我们可以按如下方式计算两个后验概率之间的比率:

另外,请记住这一点:

所以,最后我们得到了以下结果:

新用户喜欢目标电影的概率为92.1%

我希望在经历了理论和一个玩具示例后,你现在对朴素贝叶斯有了扎实的理解。让我们准备在下一部分实现它。

实现朴素贝叶斯

在手动计算完电影偏好示例之后,正如我们承诺的那样,我们将从头开始实现朴素贝叶斯。然后,我们将使用scikit-learn包来实现它。

从头实现朴素贝叶斯

在我们开发模型之前,先定义一下我们刚才使用的玩具数据集:

>>> import numpy as np
>>> X_train = np.array([
...     [0, 1, 1],
...     [0, 0, 1],
...     [0, 0, 0],
...     [1, 1, 0]])
>>> Y_train = ['Y', 'N', 'Y', 'Y']
>>> X_test = np.array([[1, 1, 0]]) 

对于模型,从先验开始,我们首先按标签对数据进行分组,并按类别记录它们的索引:

>>> def get_label_indices(labels):
...     """
...     Group samples based on their labels and return indices
...     @param labels: list of labels
...     @return: dict, {class1: [indices], class2: [indices]}
...     """
...     from collections import defaultdict
...     label_indices = defaultdict(list)
...     for index, label in enumerate(labels):
...         label_indices[label].append(index)
...     return label_indices 

看看我们得到的结果:

>>> label_indices = get_label_indices(Y_train)
>>> print('label_indices:\n', label_indices)
    label_indices:
    defaultdict(<class 'list'>, {'Y': [0, 2, 3], 'N': [1]}) 

使用label_indices,我们计算先验概率:

>>> def get_prior(label_indices):
...     """
...     Compute prior based on training samples
...     @param label_indices: grouped sample indices by class
...     @return: dictionary, with class label as key, corresponding
...              prior as the value
...     """
...     prior = {label: len(indices) for label, indices in
...                                      label_indices.items()}
...     total_count = sum(prior.values())
...     for label in prior:
...         prior[label] /= total_count
...     return prior 

看看计算得到的先验:

>>> prior = get_prior(label_indices)
>>> print('Prior:', prior)
 Prior: {'Y': 0.75, 'N': 0.25} 

计算完prior后,我们继续计算likelihood,即条件概率P(feature|class)

>>> def get_likelihood(features, label_indices, smoothing=0):
...     """
...     Compute likelihood based on training samples
...     @param features: matrix of features
...     @param label_indices: grouped sample indices by class
...     @param smoothing: integer, additive smoothing parameter
...     @return: dictionary, with class as key, corresponding
...              conditional probability P(feature|class) vector 
...              as value
...     """
...     likelihood = {}
...     for label, indices in label_indices.items():
...         likelihood[label] = features[indices, :].sum(axis=0)
...                                + smoothing
...         total_count = len(indices)
...         likelihood[label] = likelihood[label] /
...                                 (total_count + 2 * smoothing)
...     return likelihood 

在这里,我们将smoothing值设置为 1,也可以设置为 0 表示没有平滑,或者设置为其他任何正值,只要能够获得更好的分类性能:

>>> smoothing = 1
>>> likelihood = get_likelihood(X_train, label_indices, smoothing)
>>> print('Likelihood:\n', likelihood)
Likelihood:
 {'Y': array([0.4, 0.6, 0.4]), 'N': array([0.33333333, 0.33333333, 0.66666667])} 

如果你觉得这些内容有点困惑,可以随时查看图 2.7来刷新一下记忆:

A screenshot of a computer  Description automatically generated with low confidence

图 2.7:计算先验和似然的简单示例

在准备好先验和似然后,我们可以计算测试/新样本的后验概率:

>>> def get_posterior(X, prior, likelihood):
...     """
...     Compute posterior of testing samples, based on prior and
...     likelihood
...     @param X: testing samples
...     @param prior: dictionary, with class label as key,
...                   corresponding prior as the value
...     @param likelihood: dictionary, with class label as key,
...                        corresponding conditional probability
...                            vector as value
...     @return: dictionary, with class label as key, corresponding
...              posterior as value
...     """
...     posteriors = []
...     for x in X:
...         # posterior is proportional to prior * likelihood
...         posterior = prior.copy()
...         for label, likelihood_label in likelihood.items():
...             for index, bool_value in enumerate(x):
...                 posterior[label] *= likelihood_label[index] if
...                   bool_value else (1 - likelihood_label[index])
...         # normalize so that all sums up to 1
...         sum_posterior = sum(posterior.values())
...         for label in posterior:
...             if posterior[label] == float('inf'):
...                 posterior[label] = 1.0
...             else:
...                 posterior[label] /= sum_posterior
...         posteriors.append(posterior.copy())
...     return posteriors 

现在,让我们使用这个预测函数预测我们一个样本测试集的类别:

>>> posterior = get_posterior(X_test, prior, likelihood)
>>> print('Posterior:\n', posterior)
Posterior:
 [{'Y': 0.9210360075805433, 'N': 0.07896399241945673}] 

这正是我们之前得到的结果。我们已经成功从零开始开发了朴素贝叶斯,现在可以开始使用scikit-learn实现了。

使用 scikit-learn 实现朴素贝叶斯

从头编写代码并实现你自己的解决方案是学习机器学习模型的最佳方式。当然,你也可以通过直接使用 scikit-learn API 中的BernoulliNB模块(scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.BernoulliNB.html)来走捷径:

>>> from sklearn.naive_bayes import BernoulliNB 

让我们初始化一个具有平滑因子(在scikit-learn中指定为alpha)为1.0,并且从训练集学习的prior(在scikit-learn中指定为fit_prior=True)的模型:

>>> clf = BernoulliNB(alpha=1.0, fit_prior=True) 

要使用fit方法训练朴素贝叶斯分类器,我们使用以下代码行:

>>> clf.fit(X_train, Y_train) 

要使用predict_proba方法获取预测概率结果,我们使用以下代码行:

>>> pred_prob = clf.predict_proba(X_test)
>>> print('[scikit-learn] Predicted probabilities:\n', pred_prob)
[scikit-learn] Predicted probabilities:
 [[0.07896399 0.92103601]] 

最后,我们做如下操作,使用predict方法直接获取预测的类别(0.5 是默认的阈值,如果类别Y的预测概率大于 0.5,则分配类别Y;否则,使用类别N):

>>> pred = clf.predict(X_test)
>>> print('[scikit-learn] Prediction:', pred)
[scikit-learn] Prediction: ['Y'] 

使用 scikit-learn 得到的预测结果与我们使用自己解决方案得到的结果一致。既然我们已经从零开始和使用scikit-learn实现了算法,为什么不直接用它来解决电影推荐问题呢?

使用朴素贝叶斯构建电影推荐系统

在完成了玩具示例之后,现在是时候使用一个真实数据集构建一个电影推荐系统(或更具体地说,构建一个电影偏好分类器)了。我们这里使用一个电影评分数据集(grouplens.org/datasets/movielens/)。该数据集由 GroupLens 研究小组从 MovieLens 网站(movielens.org)收集。

为了演示,我们将使用稳定的小型数据集——MovieLens 1M 数据集(可以从files.grouplens.org/datasets/movielens/ml-1m.zipgrouplens.org/datasets/movielens/1m/下载),它包含约 100 万条评分,评分范围从 1 到 5,分半星递增,由 6,040 个用户对 3,706 部电影进行评分(最后更新于 2018 年 9 月)。

解压ml-1m.zip文件,你将看到以下四个文件:

  • movies.dat:它以MovieID::Title::Genres格式包含电影信息。

  • ratings.dat:它包含用户电影评分,格式为UserID::MovieID::Rating::Timestamp。在本章中,我们将只使用此文件中的数据。

  • users.dat:它包含用户信息,格式为UserID::Gender::Age::Occupation::Zip-code

  • README

让我们尝试根据用户对其他电影的评分(再次,评分范围从 1 到 5)预测用户是否喜欢某部电影。

准备数据

首先,我们导入所有必要的模块,并将ratings.dat读取到pandas DataFrame 对象中:

>>> import numpy as np
>>> import pandas as pd
>>> data_path = 'ml-1m/ratings.dat'
>>> df = pd.read_csv(data_path, header=None, sep='::', engine='python')
>>> df.columns = ['user_id', 'movie_id', 'rating', 'timestamp']
>>> print(df)
         user_id  movie_id  rating  timestamp
0              1      1193       5  978300760
1              1       661       3  978302109
2              1       914       3  978301968
3              1      3408       4  978300275
4              1      2355       5  978824291
...          ...       ...     ...        ...
1000204     6040      1091       1  956716541
1000205     6040      1094       5  956704887
1000206     6040       562       5  956704746
1000207     6040      1096       4  956715648
1000208     6040      1097       4  956715569
[1000209 rows x 4 columns] 

现在,让我们看看这个百万行数据集中有多少独特的用户和电影:

>>> n_users = df['user_id'].nunique()
>>> n_movies = df['movie_id'].nunique()
>>> print(f"Number of users: {n_users}")
Number of users: 6040
>>> print(f"Number of movies: {n_movies}")
Number of movies: 3706 

接下来,我们将构建一个 6,040(用户数量)行和 3,706(电影数量)列的矩阵,其中每一行包含用户的电影评分,每一列代表一部电影,使用以下函数:

>>> def load_user_rating_data(df, n_users, n_movies):
...    data = np.zeros([n_users, n_movies], dtype=np.intc)
              movie_id_mapping = {}
              for user_id, movie_id, rating in zip(df['user_id'], df['movie_id'], df['rating']):
                    user_id = int(user_id) - 1
                    if movie_id not in movie_id_mapping:
                         movie_id_mapping[movie_id] = len(movie_id_mapping)
                   data[user_id, movie_id_mapping[movie_id]] = rating
              return data, movie_id_mapping
>>> data, movie_id_mapping = load_user_rating_data(df, n_users, n_movies) 

除了评分矩阵data,我们还记录了电影 ID与列索引的映射。列索引从 0 到 3,705,因为我们有 3,706 部电影。

始终建议分析数据分布,以识别数据集中是否存在类别不平衡问题。我们进行如下操作:

>>> values, counts = np.unique(data, return_counts=True)
... for value, count in zip(values, counts):
...     print(f'Number of rating {value}: {count}')
Number of rating 0: 21384031
Number of rating 1: 56174
Number of rating 2: 107557
Number of rating 3: 261197
Number of rating 4: 348971
Number of rating 5: 226310 

如你所见,大多数评分是未知的;对于已知评分,35%的评分为 4 分,其次是 26%的评分为 3 分,23%的评分为 5 分,然后是 11%和 6%的评分为 2 分和 1 分。

由于大多数评分是未知的,我们选择评分已知最多的电影作为目标电影,以便更容易进行预测验证。我们按以下方式查找每部电影的评分数量:

>>> print(df['movie_id'].value_counts())
2858    3428
260     2991
1196    2990
1210    2883
480     2672
        ...
3458       1
2226       1
1815       1
398        1
2909       1
Name: movie_id, Length: 3706, dtype: int64 

因此,目标电影是 ID,我们将把其他电影的评分作为特征。我们只使用目标电影有评分的行,以便验证预测的准确性。我们将数据集按如下方式构建:

>>> target_movie_id = 2858
>>> X_raw = np.delete(data, movie_id_mapping[target_movie_id], axis=1)
>>> Y_raw = data[:, movie_id_mapping[target_movie_id]]
>>> X = X_raw[Y_raw > 0]
>>> Y = Y_raw[Y_raw > 0]
>>> print('Shape of X:', X.shape)
Shape of X: (3428, 3705)
>>> print('Shape of Y:', Y.shape)
Shape of Y: (3428,) 

我们可以将评分大于 3 的电影视为喜欢的(被推荐的)电影:

>>> recommend = 3
>>> Y[Y <= recommend] = 0
>>> Y[Y > recommend] = 1
>>> n_pos = (Y == 1).sum()
>>> n_neg = (Y == 0).sum()
>>> print(f'{n_pos} positive samples and {n_neg} negative samples.')
2853 positive samples and 575 negative samples. 

作为解决分类问题的一条经验法则,我们始终需要分析标签分布,并查看数据集是否平衡(或不平衡)。

最佳实践

处理分类问题中的不平衡数据集需要仔细考虑并采用适当的技术,以确保模型能够有效地从数据中学习并生成可靠的预测。以下是几种应对类别不平衡的策略:

  • 过采样:我们可以通过生成合成样本或复制现有样本来增加少数类实例的数量。

  • 欠采样:我们可以通过随机删除样本来减少多数类实例的数量。请注意,我们甚至可以结合过采样和欠采样来获得更平衡的数据集。

  • 类别加权:我们还可以在模型训练期间为少数类样本分配更高的权重。通过这种方式,我们可以对少数类的误分类进行更严重的惩罚。

接下来,为了全面评估分类器的表现,我们可以将数据集随机划分为两个集合:训练集和测试集,分别模拟学习数据和预测数据。通常,原始数据集中包含在测试集中的比例可以是 20%、25%、30%或 33.3%。

最佳实践

以下是选择测试集划分的一些指南:

  • 小规模数据集:如果你的数据集较小(例如,少于几千个样本),较大的测试集划分(例如,25%到 30%)可能更为合适,以确保有足够的数据进行训练和测试。

  • 中到大规模数据集:对于中到大规模的数据集(例如,数万到数百万的样本),较小的测试集划分(例如,20%)可能仍然为评估提供足够的数据,同时允许更多数据用于训练。20%的测试集划分在这种情况下是一个常见的选择。

  • 简单模型:较少复杂的模型通常不容易发生过拟合,因此使用较小的测试集划分可能是可行的。

  • 复杂模型:像深度学习模型这样的复杂模型更容易发生过拟合。因此,建议使用更大的测试集划分(例如,30%)。

我们使用scikit-learn中的train_test_split函数进行随机划分,并保留每个类别的样本比例:

>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, Y_train, Y_test = train_test_split(X, Y,
...     test_size=0.2, random_state=42) 

在实验和探索过程中,指定一个固定的random_state(例如,42)是一个好的做法,以确保每次程序运行时生成相同的训练集和测试集。这可以确保我们在引入随机性并进一步进行时,分类器能够在固定的数据集上正常运行并表现良好。

我们检查训练集和测试集的大小如下:

>>> print(len(Y_train), len(Y_test))
2742 686 

train_test_split函数的另一个优点是,结果中的训练集和测试集将具有相同的类别比例。

训练朴素贝叶斯模型

接下来,我们在训练集上训练一个朴素贝叶斯模型。你可能会注意到输入特征的值从 0 到 5,而不是我们玩具示例中的 0 或 1。因此,我们使用来自 scikit-learn 的MultinomialNB模块(scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.MultinomialNB.html),而不是BernoulliNB模块,因为MultinomialNB可以处理整数特征以及分数计数。我们导入该模块,初始化一个平滑因子为1.0prior从训练集学习得到的模型,并根据训练集训练该模型,如下所示:

>>> from sklearn.naive_bayes import MultinomialNB
>>> clf = MultinomialNB(alpha=1.0, fit_prior=True)
>>> clf.fit(X_train, Y_train) 

然后,我们使用训练好的模型对测试集进行预测。我们得到预测概率如下:

>>> prediction_prob = clf.predict_proba(X_test)
>>> print(prediction_prob[0:10])
[[7.50487439e-23 1.00000000e+00]
 [1.01806208e-01 8.98193792e-01]
 [3.57740570e-10 1.00000000e+00]
 [1.00000000e+00 2.94095407e-16]
 [1.00000000e+00 2.49760836e-25]
 [7.62630220e-01 2.37369780e-01]
 [3.47479627e-05 9.99965252e-01]
 [2.66075292e-11 1.00000000e+00]
 [5.88493563e-10 9.99999999e-01]
 [9.71326867e-09 9.99999990e-01]] 

对于每个测试样本,我们输出类别 0 的概率,接着是类别 1 的概率。

我们得到测试集的预测类别如下:

>>> prediction = clf.predict(X_test)
>>> print(prediction[:10])
[[1\. 1\. 1\. 0\. 0\. 0\. 1\. 1\. 1\. 1.] 

最后,我们通过分类准确度来评估模型的性能,准确度是正确预测的比例:

>>> accuracy = clf.score(X_test, Y_test)
>>> print(f'The accuracy is: {accuracy*100:.1f}%')
The accuracy is: 71.6% 

分类准确率约为 72%,这意味着我们构建的朴素贝叶斯分类器能大约三分之二的时间正确地为用户推荐电影。理想情况下,我们还可以利用movies.dat文件中的电影类型信息,以及users.dat文件中的用户人口统计信息(性别、年龄、职业和邮政编码)。显然,相似类型的电影通常会吸引相似的用户,而相似人口统计特征的用户可能有相似的电影偏好。我们将把这部分留给你作为练习,进一步探索。

到目前为止,我们已经深入介绍了第一个机器学习分类器,并通过预测准确度评估了其性能。还有其他分类指标吗?让我们在下一节看看。

评估分类性能

除了准确率外,还有几个评估指标可以帮助我们深入了解分类性能,避免类别不平衡的影响。具体如下:

  • 混淆矩阵

  • 精确度

  • 召回率

  • F1 得分

  • 曲线下面积

混淆矩阵通过预测值和真实值总结测试实例,并以列联表的形式呈现:

A picture containing text, screenshot, font, number  Description automatically generated

图 2.8:混淆矩阵的列联表

为了说明这一点,我们可以计算我们朴素贝叶斯分类器的混淆矩阵。我们使用scikit-learn中的confusion_matrix函数来计算,但也可以很容易自己编写代码:

>>> from sklearn.metrics import confusion_matrix
>>> print(confusion_matrix(Y_test, prediction, labels=[0, 1]))
[[ 60  47]
 [148 431]] 

从结果的混淆矩阵可以看出,有 47 个假阳性案例(模型错误地将不喜欢的电影判定为喜欢),和 148 个假阴性案例(模型未能检测到喜欢的电影)。因此,分类准确度仅是所有真实案例的比例:

精确度衡量的是正确的正类预测所占的比例,在我们的案例中如下:

召回率,另一方面,衡量的是被正确识别的真实正例的比例,在我们的案例中如下:

召回率也称为真正率

F1 得分全面包含了精确度和召回率,并等同于它们的调和平均数

我们通常更看重F1 得分,而非单独的精确度或召回率。

让我们使用scikit-learn中的相应函数计算这三个指标,具体如下:

>>> from sklearn.metrics import precision_score, recall_score, f1_score
>>> precision_score(Y_test, prediction, pos_label=1)
0.9016736401673641
>>> recall_score(Y_test, prediction, pos_label=1)
0.7443868739205527
>>> f1_score(Y_test, prediction, pos_label=1)
0.815515610217597 

另一方面,负类(不喜欢)也可以根据上下文视为正类。例如,将0类分配为pos_label,我们将得到以下结果:

>>> f1_score(Y_test, prediction, pos_label=0)
0.38095238095238093 

为了获取每个类别的精确率、召回率和 f1 得分,我们可以不需要像之前那样耗费精力调用三个函数处理所有类别标签,快速的方法是调用classification_report函数:

>>> from sklearn.metrics import classification_report
>>> report = classification_report(Y_test, prediction)
>>> print(report)
              precision    recall  f1-score   support
         0.0       0.29      0.56      0.38       107
         1.0       0.90      0.74      0.82       579
   micro avg       0.72      0.72      0.72       686
   macro avg       0.60      0.65      0.60       686
weighted avg       0.81      0.72      0.75       686 

这里,weighted avg是根据各类别比例计算的加权平均值。

分类报告提供了分类器在每个类别上的表现的全面视图。因此,它在类别不平衡的分类任务中非常有用,因为在这种情况下,我们可以通过将每个样本分类为占主导地位的类别来轻松获得高准确率,但少数类别的精确率、召回率和 f1 得分将显著较低。

精确率、召回率和 f1 得分同样适用于多类分类,在这种情况下,我们可以简单地将我们感兴趣的类别视为正类,其他类别视为负类。

在调整二分类器的过程中(即尝试不同的超参数组合,例如我们朴素贝叶斯分类器中的平滑因子),如果有一组参数能够在同时达到最高的平均 f1 得分和各类别的个别 f1 得分,那将是最理想的情况。然而,通常情况并非如此。有时,某个模型的平均 f1 得分高于另一个模型,但某个特定类别的 f1 得分却显著较低;有时,两个模型的平均 f1 得分相同,但一个模型在某个类别上的 f1 得分较高,而在另一个类别上的得分较低。在这种情况下,我们如何判断哪个模型效果更好呢?曲线下面积AUC)是接收器操作特性ROC)的一个综合指标,经常用于二分类任务。

ROC 曲线是将不同概率阈值下的真实正例率与假正例率进行比较的图形,阈值范围从 0 到 1。对于一个测试样本,如果其正类的概率大于阈值,则分配为正类;否则,使用负类。总结一下,真实正例率等同于召回率,而假正例率是错误地将负类标识为正类的比例。让我们编写代码并展示我们模型在0.00.10.2、……、1.0等阈值下的 ROC 曲线:

>>> pos_prob = prediction_prob[:, 1]
>>> thresholds = np.arange(0.0, 1.1, 0.05)
>>> true_pos, false_pos = [0]*len(thresholds), [0]*len(thresholds)
>>> for pred, y in zip(pos_prob, Y_test):
...     for i, threshold in enumerate(thresholds):
...         if pred >= threshold:
...            # if truth and prediction are both 1
...             if y == 1:
...                 true_pos[i] += 1
...            # if truth is 0 while prediction is 1
...             else:
...                 false_pos[i] += 1
...         else:
...             break 

接下来,让我们计算所有阈值设置下的真实正例率和假正例率(记住,正测试样本有516.0个,负测试样本有1191个):

>>> n_pos_test = (Y_test == 1).sum()
>>> n_neg_test = (Y_test == 0).sum()
>>> true_pos_rate = [tp / n_pos_test for tp in true_pos]
>>> false_pos_rate = [fp / n_neg_test for fp in false_pos] 

现在,我们可以使用matplotlib绘制 ROC 曲线:

>>> import matplotlib.pyplot as plt
>>> plt.figure()
>>> lw = 2
>>> plt.plot(false_pos_rate, true_pos_rate,
...          color='darkorange', lw=lw)
>>> plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
>>> plt.xlim([0.0, 1.0])
>>> plt.ylim([0.0, 1.05])
>>> plt.xlabel('False Positive Rate')
>>> plt.ylabel('True Positive Rate')
>>> plt.title('Receiver Operating Characteristic')
>>> plt.legend(loc="lower right")
>>> plt.show() 

参见图 2.9中的 ROC 曲线结果:

A picture containing text, line, plot, screenshot  Description automatically generated

图 2.9:ROC 曲线

在图表中,虚线是基准线,表示随机猜测,其中真正例率与假正例率线性增加;其 AUC 为 0.5。实线是我们模型的 ROC 曲线,其 AUC 略低于 1。在理想情况下,真正例样本的概率为 1,因此 ROC 曲线从 100% 的真正例和 0% 的假正例点开始。这样的完美曲线的 AUC 为 1。为了计算我们模型的准确 AUC,可以借助 scikit-learnroc_auc_score 函数:

>>> from sklearn.metrics import roc_auc_score
>>> roc_auc_score(Y_test, pos_prob)
0.6857375752586637 

什么 AUC 值表明分类器表现良好?不幸的是,并没有一个“魔法”数字。我们使用以下经验法则作为一般指导:AUC 在 0.70.8 之间的分类模型被认为是可接受的,0.80.9 之间的是优秀的,任何高于 0.9 的则是卓越的。再次强调,在我们的案例中,我们只使用了非常稀疏的电影评分数据。因此,0.69 的 AUC 实际上是可以接受的。

你已经学习了几种分类指标,我们将在下一节探讨如何正确衡量它们以及如何微调我们的模型。

使用交叉验证来调整模型

将评估限制在一个固定的数据集上可能会产生误导,因为它很大程度上依赖于为该数据集选择的具体数据点。我们可以简单地避免采用来自一个固定测试集的分类结果,这也是我们之前实验中所做的。相反,我们通常采用 k 折交叉验证技术来评估模型在实际中的整体表现。

k 折交叉验证设置中,原始数据首先随机划分为 k 个大小相等的子集,并且通常保持类别比例。然后,这些 k 个子集依次作为测试集,用于评估模型。在每次试验中,其余的 k-1 个子集(不包括保留的一折)构成训练集,用于训练模型。最后,计算所有 k 次试验的平均性能,以生成整体结果:

图 2.10:三折交叉验证示意图

从统计学角度看,k 折交叉验证的平均性能是评估模型整体表现的更好估计。给定与机器学习模型和/或数据预处理算法相关的不同参数集,甚至是两个或多个不同的模型,模型调优和/或模型选择的目标是选择一个分类器的参数集,以便获得最佳的平均性能。牢记这些概念后,我们可以开始调整我们的朴素贝叶斯分类器,结合交叉验证和 ROC 曲线的 AUC 测量。

k折交叉验证中,通常将k设置为 3、5 或 10。如果训练集较小,推荐使用较大的k(5 或 10),以确保每个折叠中有足够的训练样本。如果训练集较大,较小的值(如 3 或 4)就足够了,因为较高的k会导致在大数据集上训练时计算成本过高。

我们将使用scikit-learnStratifiedKFold类中的split()方法将数据分割成保持类分布的块:

>>> from sklearn.model_selection import StratifiedKFold
>>> k = 5
>>> k_fold = StratifiedKFold(n_splits=k, random_state=42) 

在初始化 5 折生成器后,我们选择探索以下参数的不同值:

  • alpha:表示平滑因子,每个特征的初始值。

  • fit_prior:表示是否使用专门为训练数据调整的先验。

我们从以下选项开始:

>>> smoothing_factor_option = [1, 2, 3, 4, 5, 6]
>>> fit_prior_option = [True, False]
>>> auc_record = {} 

然后,对于k_fold对象的split()方法生成的每个折叠,我们重复分类器初始化、训练和预测的过程,并使用上述参数组合之一,记录结果 AUC:

>>> for train_indices, test_indices in k_fold.split(X, Y):
...     X_train_k, X_test _k= X[train_indices], X[test_indices]
...     Y_train_k, Y_test_k = Y[train_indices], Y[test_indices]
...     for alpha in smoothing_factor_option:
...         if alpha not in auc_record:
...             auc_record[alpha] = {}
...         for fit_prior in fit_prior_option:
...             clf = MultinomialNB(alpha=alpha,
...                                 fit_prior=fit_prior)
...             clf.fit(X_train_k, Y_train_k)
...             prediction_prob = clf.predict_proba(X_test_k)
...             pos_prob = prediction_prob[:, 1]
...             auc = roc_auc_score(Y_test_k, pos_prob)
...             auc_record[alpha][fit_prior] = auc +
...                        auc_record[alpha].get(fit_prior, 0.0) 

最后,我们展示如下结果:

>>> for smoothing, smoothing_record in auc_record.items():
...     for fit_prior, auc in smoothing_record.items():
...         print(f'    {smoothing}        {fit_prior}  
...               {auc/k:.5f}')
smoothing  fit prior  auc
    1        True    0.65647
    1        False    0.65708
    2        True    0.65795
    2        False    0.65823
    3        True    0.65740
    3        False    0.65801
    4        True    0.65808
    4        False    0.65795
    5        True    0.65814
    5        False    0.65694
    6        True    0.65663
    6        False    0.65719 

(2, False)的设置能够提供最佳的平均 AUC,值为0.65823

最后,我们使用最佳超参数组合(2, False)重新训练模型并计算 AUC:

>>> clf = MultinomialNB(alpha=2.0, fit_prior=False)
>>> clf.fit(X_train, Y_train)
>>> pos_prob = clf.predict_proba(X_test)[:, 1]
>>> print('AUC with the best model:', roc_auc_score(Y_test,
...       pos_prob))
AUC with the best model:  0.6862056720417091 

经过微调的模型达到了0.686的 AUC 值。通常,使用交叉验证调整模型超参数是提高学习性能和减少过拟合的最有效方法之一。

总结

在本章中,你了解了机器学习分类的基本概念,包括分类类型、分类性能评估、交叉验证和模型调优。你还了解了简单却强大的分类器——朴素贝叶斯。我们深入探讨了朴素贝叶斯的机制和实现,并通过几个示例进行说明,其中最重要的一个是电影推荐项目。

使用朴素贝叶斯进行二分类是本章的主要讨论内容。在下一章中,我们将使用另一种二分类算法——决策树来解决广告点击率预测问题。

练习

  1. 如前所述,我们仅从电影评分数据中提取了用户与电影的关系,其中大多数评分是未知的。你能否还利用movies.datusers.dat文件中的数据?

  2. 熟能生巧——另一个可以加深理解的好项目是心脏病分类。数据集可以直接从archive.ics.uci.edu/ml/datasets/Heart+Disease下载。

  3. 别忘了使用你在本章中学到的技术,微调从练习 2 中得到的模型。它能达到的最佳 AUC 是多少?

参考文献

为了确认在本章中使用了 MovieLens 数据集,我想引用以下论文:

F. Maxwell Harper 和 Joseph A. Konstan. 2015. 电影 Lens 数据集:历史与背景. ACM 互动智能系统学报 (TiiS) 5, 4, 文章 19 (2015 年 12 月),19 页。DOI:dx.doi.org/10.1145/2827872

加入我们书籍的 Discord 讨论区

加入我们社区的 Discord 讨论区,与作者及其他读者进行交流:

packt.link/yuxi

第三章:使用基于树的算法预测在线广告点击率

在上一章中,我们构建了一个电影推荐系统。在本章和下一章中,我们将解决数字广告中最具数据驱动问题之一:广告点击率预测——给定用户及其访问的页面,预测他们点击某个广告的可能性。我们将重点学习基于树的算法(包括决策树、随机森林模型和提升树),并利用它们来解决这个价值数十亿美元的问题。

我们将探索从根到叶子的决策树,以及集成版本,即一片树的森林。这不仅仅是一个理论章节,还包含了许多手工计算和从零开始实现树模型的部分。我们将使用 scikit-learn 和 XGBoost,这是一个流行的 Python 包,用于基于树的算法。

本章将涉及以下主题:

  • 广告点击率预测的简要概述

  • 从根到叶子探索决策树

  • 从头开始实现决策树

  • 使用 scikit-learn 实现决策树

  • 使用决策树预测广告点击率

  • 集成决策树——随机森林

  • 集成决策树——梯度提升树

广告点击率预测的简要概述

在线展示广告是一个数十亿美元的行业。在线展示广告有不同的格式,包括由文本、图像和 Flash 组成的横幅广告,以及富媒体广告,如音频和视频。广告商或其代理商将广告投放到各种网站,甚至移动应用程序上,以接触潜在客户并传递广告信息。

在线展示广告已成为机器学习应用的最佳示例之一。显然,广告商和消费者都对精准定位的广告充满兴趣。在过去 20 年里,行业在很大程度上依赖机器学习模型预测广告定位的有效性:例如,某个年龄段的观众对某产品感兴趣的可能性,某些家庭收入群体的顾客看到广告后会购买某产品的可能性,频繁访问体育网站的用户会花更多时间阅读某个广告的可能性,等等。最常见的效果衡量标准是点击率CTR),即某个广告被点击的次数与其总展示次数的比率。通常,在没有点击诱饵或垃圾内容的情况下,较高的 CTR 表示广告定位准确,在线广告活动成功。

点击率预测包含了机器学习的潜力和挑战。它主要涉及二分类问题,即预测给定页面(或应用)上的某个广告是否会被用户点击,预测特征来自以下三个方面:

  • 广告内容和信息(类别、位置、文本、格式等)

  • 页面内容和发布者信息(类别、上下文、领域等)

  • 用户信息(年龄、性别、位置、收入、兴趣、搜索历史、浏览历史、设备等)

假设我们作为一个广告代理机构,代表多个广告主进行广告投放,我们的工作是为正确的受众投放合适的广告。假设我们手头有一个现有数据集(以下小段示例;实际上,预测特征的数量可能会达到数千个),该数据集来自一个月前运行的数百万条广告记录,我们需要开发一个分类模型来学习并预测未来的广告投放结果:

A picture containing text, number, screenshot, font  Description automatically generated

图 3.1:用于训练和预测的广告样本

如你在图 3.1中看到的,特征大多是类别型的。事实上,数据可以是数值型或类别型的。让我们在下一节中更详细地探讨这一点。

从两种数据类型开始——数值型和类别型

乍一看,前述数据集中的特征是类别型的——例如,男性或女性、四个年龄组之一、预定义的站点类别之一,以及用户是否对体育感兴趣。这类数据与我们之前处理的数值型特征数据有所不同。

类别型特征,也称为定性特征,表示具有可计数选项的不同特征或类别。类别型特征可能有,也可能没有逻辑顺序。例如,家庭收入从低到中到高是一个有序特征,而广告的类别则不是有序的。

数值型(也叫定量)特征则具有数学意义,作为一种测量方式,当然也是有序的。例如,物品的计数(例如,家庭中孩子的数量、房屋中的卧室数量以及距离某个事件的天数)是离散的数值型特征;个体的身高、温度和物体的重量是连续的数值型特征。心电图数据集(archive.ics.uci.edu/ml/datasets/Cardiotocography)包含了离散型(例如每秒加速度的次数或每秒胎动的次数)和连续型(例如长期变异性的均值)数值特征。

类别型特征也可以取数字值。例如,1 到 12 可以表示一年中的月份,1 和 0 可以表示成年人和未成年人。但这些值并不具有数学含义。

你之前学习的朴素贝叶斯分类器适用于数值型和类别型特征,因为似然值,P(x |y) 或 P(feature |class),的计算方式是相同的。

假设我们正在考虑使用朴素贝叶斯预测点击率,并且尝试向我们的广告客户解释模型。然而,我们的客户可能会发现很难理解每个属性的先验和可能性及其乘积。那么,有没有一种分类器,既易于解释又能直接处理分类数据呢?决策树就是答案!

从根节点到叶节点探索决策树

决策树是一种树形图,即一个顺序图,展示了所有可能的决策选择及其相应的结果。从决策树的根节点开始,每个内部节点表示做出决策的依据。每个节点的分支表示一个选择如何导致下一个节点。最后,每个终端节点,即叶子,代表产生的结果。

例如,我们刚刚做出了几个决策,导致我们使用决策树来解决广告问题:

包含文本、截图、图表、字体的图片,描述自动生成

图 3.2:使用决策树找到合适的算法

第一个条件,或称根节点,是特征类型是数值型还是类别型。假设我们的广告点击流数据主要包含类别特征,那么它会走右侧分支。在下一个节点,我们的工作需要能够被非技术客户理解,因此它会走右侧分支并到达选择决策树分类器的叶节点。

你还可以查看路径,看看它们适合哪些问题。决策树分类器以决策树的形式工作。它通过一系列基于特征值和相应条件的测试(表现为内部节点)将观察结果映射到类分配(表示为叶节点)。在每个节点,会提出一个关于特征值和特征特性的问询;根据问题的答案,观察结果会被分割成子集。进行顺序测试,直到得出关于观察结果目标标签的结论。从根节点到叶节点的路径代表了决策过程和分类规则。

在一个更简化的场景中,如图 3.3所示,我们希望预测自驾车广告上的点击不点击,我们可以手动构建一个适用于现有数据集的决策树分类器。例如,如果一个用户对科技感兴趣并且拥有汽车,他们更有可能点击广告;而在这个子集之外的人假设不太可能点击广告。然后,我们使用训练过的树来预测两个新的输入,其结果分别是点击不点击

图 3.3:使用训练过的决策树预测点击/不点击

在决策树构建完成后,分类新样本是非常简单的,如你刚刚看到的那样:从根节点开始,应用测试条件并按相应分支进行,直到达到叶节点,并将与该叶节点关联的类标签分配给新样本。

那么,如何构建一个合适的决策树呢?

构建决策树

决策树是通过将训练样本划分为连续的子集来构建的。这个划分过程会在每个子集上递归地重复进行。在每个节点的划分中,会根据子集中特征的值进行条件测试。当子集内的所有样本属于同一类,或再进行分裂不能改善该子集的类纯度时,该节点的递归划分结束。

重要说明

类纯度是指在数据子集中目标变量(类标签)的同质性。如果大多数实例属于同一类别,则认为子集具有较高的类纯度。换句话说,具有高类纯度的子集大多数包含相同类标签的实例,而具有低类纯度的子集则包含来自多个类的实例。

理论上,要对具有n种不同值的特征(无论是数值型还是类别型)进行划分,有n种不同的二叉分裂方法(如图3.4所示,通过来进行条件测试),更不用说其他的划分方式(例如图3.4中的三路和四路分裂):

A picture containing text, screenshot, diagram, font  Description automatically generated

图 3.4:二叉分裂和多路分裂的示例

如果不考虑划分特征的顺序,对于一个m维数据集,已经有n^m 种可能的树形结构。

已经开发了许多算法来高效地构建准确的决策树。常见的包括以下几种:

  • 迭代二分法 3ID3):该算法通过贪心搜索的方式,以自上而下的顺序选择最佳属性进行数据集的划分,每次迭代都不进行回溯。

  • C4.5:这是 ID3 的改进版本,引入了回溯机制。它会遍历已构建的树,并在纯度得到改善时,用叶节点替换分支节点。

  • 分类与回归树CART):该算法使用二叉分裂来构建决策树,我们稍后会更详细地讨论。CART 的灵活性、效率、可解释性和鲁棒性使其成为各种分类和回归任务的热门选择。

  • 卡方自动交互检测法CHAID):该算法常用于直接营销。它涉及复杂的统计概念,但基本原理是确定合并预测变量的最佳方式,从而最有效地解释结果。

这些算法的基本思想是通过在选择最显著特征进行数据划分时进行一系列局部优化来贪婪地生长树。数据集根据该特征的最优值进行划分。我们将在下一节讨论显著特征的度量和特征的最优分割值。

首先,我们将更详细地研究 CART 算法,然后我们将实现它作为最著名的决策树算法。它使用二元分割构建树,并将每个节点生长为左右子节点。在每次分割中,它贪婪地搜索最显著的特征及其值的组合;所有可能的组合都会被尝试并通过度量函数进行测试。选定的特征和值作为分割点,算法随后将数据集划分如下:

  • 拥有此值特征(对于分类特征)或更大值(对于数值特征)的样本成为右子节点

  • 剩余的样本成为左子节点

这个分割过程会重复进行,并递归地将输入样本划分为两个子组。当以下任一标准满足时,分割过程停止:

  • 新节点的最小样本数:当样本数不大于进一步分割所需的最小样本数时,分割停止,以防止树过度调整到训练集,从而导致过拟合。

  • 树的最大深度:当节点的深度达到最大树深度时,节点停止生长。深度定义为从根节点到终端节点之间发生的分割次数。更深的树对训练集更加具体,可能导致过拟合。

没有分支的节点成为叶子节点,该节点的样本的主导类别就是预测值。一旦所有分割过程完成,树就构建好了,并且在终端节点处标注了分配的标签,所有内部节点上方的分割点(特征和值)也已显示。

如承诺的那样,在研究了选择最优分割特征和数值的度量标准后,我们将从头开始实现 CART 决策树算法。

衡量分割的度量标准

在选择最佳的特征和值组合作为分割点时,可以使用两个标准,例如基尼不纯度信息增益,来衡量分割的质量。

基尼不纯度

基尼不纯度,顾名思义,衡量数据点的类别分布的不纯度率,或者类别混合率。对于一个具有K类的数据集,假设来自类别k(1 ≤ k ≤ K)的数据占整个数据集的比例f[k](0 ≤ f[k]* ≤ 1),则该数据集的基尼不纯度可以表示为:

较低的基尼不纯度意味着数据集更加纯净。例如,当数据集只包含一个类别时,假设该类别的比例为1,其他类别的比例为0,其基尼不纯度为 1 – (1² + 0²) = 0。再举个例子,假设数据集记录了大量掷硬币的结果,正面和反面各占一半样本。此时基尼不纯度为 1 – (0.5² + 0.5²) = 0.5。

在二元分类情况下,基尼不纯度在不同的正类比例下可以通过以下代码块进行可视化:

>>> import matplotlib.pyplot as plt
>>> import numpy as np 

正类的比例从0变化到1

>>> pos_fraction = np.linspace(0.00, 1.00, 1000) 

基尼不纯度相应地被计算出来,接着是基尼不纯度正类比例的图示:

>>> gini = 1 – pos_fraction**2 – (1-pos_fraction)**2 

这里,1-pos_fraction是负类比例:

>>> plt.plot(pos_fraction, gini)
>>> plt.ylim(0, 1)
>>> plt.xlabel('Positive fraction')
>>> plt.ylabel('Gini impurity')
>>> plt.show() 

请参见图 3.5以获取最终结果:

A picture containing line, plot, diagram, screenshot  Description automatically generated

图 3.5:基尼不纯度与正类比例的关系

如你所见,在二元分类情况下,如果正类比例为 50%,则不纯度将达到最高值0.5;如果正类比例为 100%或 0%,则不纯度为0

给定数据集的标签,我们可以实现基尼不纯度计算函数,如下所示:

>>> def gini_impurity(labels):
...     # When the set is empty, it is also pure
...     if len(labels) == 0:
...         return 0
...     # Count the occurrences of each label
...     counts = np.unique(labels, return_counts=True)[1]
...     fractions = counts / float(len(labels))
...     return 1 - np.sum(fractions ** 2) 

尝试一些示例:

>>> print(f'{gini_impurity([1, 1, 0, 1, 0]):.4f}')
0.4800
>>> print(f'{gini_impurity([1, 1, 0, 1, 0, 0]):.4f}')
0.5000
>>> print(f'{gini_impurity([1, 1, 1, 1]):.4f}')
0.0000 

为了评估划分的质量,我们只需将所有结果子组的基尼不纯度加总,并结合每个子组的比例作为相应的权重因子。同样,基尼不纯度的加权和越小,表示划分越好。

看看下面的自动驾驶汽车广告示例。在这里,我们分别根据用户的性别和对技术的兴趣来划分数据:

A picture containing text, screenshot, number, font  Description automatically generated

图 3.6:根据性别或对技术的兴趣划分数据

第一轮划分的加权基尼不纯度可以按以下方式计算:

第二次划分如下:

因此,根据用户对技术的兴趣来划分数据比根据性别划分更为有效。

信息增益

另一个指标,信息增益,衡量的是划分后纯度的改善,或者说是通过划分减少的不确定性。信息增益越高,表示划分效果越好。我们通过比较划分前后的来获得信息增益。

熵是一个表示不确定性的概率度量。给定一个K类数据集,且f[k] (0 ≤ f[k] ≤ 1) 表示来自类别 k (1 ≤ k ≤ K) 的数据比例,则该数据集的定义如下:

较低的熵意味着数据集更加纯净,模糊性更小。在完美的情况下,若数据集仅包含一个类别,则熵为:

在掷硬币的例子中,熵变为:

类似地,我们可以通过以下代码,直观地展示在二分类情况下,正类比例变化时熵的变化:

>>> pos_fraction = np.linspace(0.001, 0.999, 1000)
>>> ent = - (pos_fraction * np.log2(pos_fraction) +
...         (1 - pos_fraction) * np.log2(1 - pos_fraction))
>>> plt.plot(pos_fraction, ent)
>>> plt.xlabel('Positive fraction')
>>> plt.ylabel('Entropy')
>>> plt.ylim(0, 1)
>>> plt.show() 

这将给出以下输出:

A picture containing screenshot, plot, line, diagram  Description automatically generated

图 3.7:熵与正类比例的关系

如你所见,在二分类情况下,如果正类比例为 50%,熵会在1时达到最大;如果正类比例为 100%或 0%,熵将降到0

给定数据集的标签,可以按如下方式实现entropy计算函数:

>>> def entropy(labels):
...     if len(labels) == 0:
...         return 0
...     counts = np.unique(labels, return_counts=True)[1]
...     fractions = counts / float(len(labels))
...     return - np.sum(fractions * np.log2(fractions)) 

用一些例子来测试一下:

>>> print(f'{entropy([1, 1, 0, 1, 0]):.4f}')
0.9710
>>> print(f'{entropy([1, 1, 0, 1, 0, 0]):.4f}')
1.0000
>>> print(f'{entropy([1, 1, 1, 1]):.4f}')
-0.0000 

现在你已经完全理解了熵的概念,我们可以探讨一下信息增益如何衡量分割后不确定性的减少,定义为分割前(父节点)与分割后(子节点)熵的差异:

信息增益 = ) - ) = 父节点) - 子节点

分割后的熵是各子节点熵的加权和,这与加权基尼不纯度类似。

在构建树节点的过程中,我们的目标是寻找能够获得最大信息增益的分割点。由于父节点的熵不变,我们只需要衡量分割后子节点的熵。最佳分割是子节点熵最低的那个分割。

为了更好地理解这一点,让我们再次查看自动驾驶汽车广告的例子。

对于第一个选项,分割后的熵可以按如下方式计算:

第二种分割方式如下:

为了探索,我们还可以计算信息增益,方法如下:

#1 信息增益 = 0.971 - 0.951 = 0.020

#2 信息增益 = 0.971 - 0.551 = 0.420

根据信息增益 = 基于熵的评估,第二次分割是更优的,这也是基尼不纯度准则得出的结论。

通常,选择这两个指标中的一个——基尼不纯度和信息增益——对训练出的决策树性能影响较小。它们都衡量了分割后子节点的加权不纯度。我们可以将它们合并为一个函数来计算加权不纯度:

>>> criterion_function = {'gini': gini_impurity,
...                       'entropy': entropy}
>>> def weighted_impurity(groups, criterion='gini'):
...     """
...     Calculate weighted impurity of children after a split
...     @param groups: list of children, and a child consists a
...                    list of class labels
...     @param criterion: metric to measure the quality of a split,
...                       'gini' for Gini impurity or 'entropy' for
...                           information gain
...     @return: float, weighted impurity
...     """
...     total = sum(len(group) for group in groups)
...     weighted_sum = 0.0
...     for group in groups:
...         weighted_sum += len(group) / float(total) *
...                           criterion_functioncriterion
...     return weighted_sum 

用我们刚刚手动计算的例子来测试,如下所示:

>>> children_1 = [[1, 0, 1], [0, 1]]
>>> children_2 = [[1, 1], [0, 0, 1]]
>>> print(f"Entropy of #1 split: {weighted_impurity(children_1,
...       'entropy'):.4f}")
Entropy of #1 split: 0.9510
>>> print(f"Entropy of #2 split: {weighted_impurity(children_2,
...       'entropy'):.4f}")
Entropy of #2 split: 0.5510 

现在你已经牢牢掌握了分割评估指标,我们将在下一部分中从零开始实现 CART 树算法。

从零开始实现决策树

我们手动在一个玩具数据集上开发 CART 树算法,步骤如下:

图 3.8:一个广告数据的示例

首先,我们通过尝试每个特征的所有可能值来决定第一个划分点,即根节点。我们利用刚刚定义的weighted_impurity函数来计算每个可能组合的加权基尼不纯度,如下所示:

如果我们根据用户是否对科技感兴趣来划分数据,我们就有第 1、第 5 和第 6 个样本作为一组,剩余的样本作为另一组。第一组的类别为[1, 1, 0],第二组的类别为[0, 0, 0, 1]

Gini(interest, tech) = weighted_impurity([[1, 1, 0], [0, 0, 0, 1]])
                     = 0.405 

如果我们根据用户是否对时尚感兴趣来划分数据,我们就有第 2 和第 3 个样本作为一组,剩余的样本作为另一组。第一组的类别为[0, 0],第二组的类别为[1, 0, 1, 0, 1]

Gini(interest, Fashion) = weighted_impurity([[0, 0], [1, 0, 1, 0, 1]])
                        = 0.343 

同样地,我们有以下情况:

Gini(interest, Sports) = weighted_impurity([[0, 1], [1, 0, 0, 1, 0]]) 
                       = 0.486
Gini(occupation, professional) = weighted_impurity([[0, 0, 1, 0],
                                                    [1, 0, 1]]) = 0.405
Gini(occupation, student) = weighted_impurity([[0, 0, 1, 0],
                                               [1, 0, 1]]) = 0.405
Gini(occupation, retired) = weighted_impurity([[1, 0, 0, 0, 1, 1], [1]])
                          = 0.429 

根节点选择用户兴趣特征中的“时尚”值,因为这个组合实现了最低的加权不纯度或最高的信息增益。我们现在可以构建树的第一层,如下所示:

A picture containing text, screenshot, font, number  Description automatically generated

图 3.9:根据“是否对时尚感兴趣?”划分数据

如果我们对一层深的树感到满意,我们可以在这里停止,将右分支标签0和左分支标签1作为多数类标签分配。

或者,我们可以继续深入,从左分支构建第二层(右分支无法再分割):

Gini(interest, tech) = weighted_impurity([[0, 1],
    [1, 1, 0]]) = 0.467
Gini(interest, Sports) = weighted_impurity([[1, 1, 0],
    [0, 1]]) = 0.467
Gini(occupation, professional) = weighted_impurity([[0, 1, 0],
    [1, 1]]) = 0.267
Gini(occupation, student) = weighted_impurity([[1, 0, 1],
    [0, 1]]) = 0.467
Gini(occupation, retired) = weighted_impurity([[1, 0, 1, 1],
    [0]]) = 0.300 

使用由(occupation, professional)指定的第二个划分点,具有最低的基尼不纯度,我们的树变成了这样:

A picture containing text, screenshot, diagram, number  Description automatically generated

图 3.10:根据“职业是否为专业?”进一步划分数据

只要树的深度不超过最大深度且节点包含足够的样本,我们就可以继续划分过程。

现在树构建的过程已经清楚了,是时候开始编码了。

我们首先定义一个工具函数,根据特征和值将节点划分为左右子节点:

>>> def split_node(X, y, index, value):
...     x_index = X[:, index]
...     # if this feature is numerical
...     if X[0, index].dtype.kind in ['i', 'f']:
...         mask = x_index >= value
...     # if this feature is categorical
...     else:
...         mask = x_index == value
...     # split into left and right child
...     left = [X[~mask, :], y[~mask]]
...     right = [X[mask, :], y[mask]]
...     return left, right 

我们检查特征是数值型还是类别型,并相应地划分数据。

有了划分测量和生成函数,我们现在定义贪心搜索函数,它尝试所有可能的划分,并根据选择标准返回最佳划分及其结果的子节点:

>>> def get_best_split(X, y, criterion):
...     best_index, best_value, best_score, children =
...                                        None, None, 1, None
...     for index in range(len(X[0])):
...         for value in np.sort(np.unique(X[:, index])):
...             groups = split_node(X, y, index, value)
...             impurity = weighted_impurity(
...                         [groups[0][1], groups[1][1]], criterion)
...             if impurity < best_score:
...                 best_index, best_value, best_score, children =
...                                index, value, impurity, groups
...     return {'index': best_index, 'value': best_value,
...             'children': children} 

选择和划分过程会在每个后续子节点上以递归方式进行。当满足停止标准时,过程会在某个节点停止,并将主要标签分配给该叶节点:

>>> def get_leaf(labels):
...     # Obtain the leaf as the majority of the labels
...     return np.bincount(labels).argmax() 

最后,递归函数将它们全部连接起来:

  • 如果两个子节点之一为空,它就会分配一个叶节点

  • 如果当前分支的深度超过允许的最大深度,它就会分配一个叶节点

  • 如果节点不包含进行进一步分裂所需的足够样本,它将分配一个叶节点

  • 否则,程序将继续按照最优分裂点进行进一步分裂

这可以通过以下函数实现:

>>> def split(node, max_depth, min_size, depth, criterion):
...     left, right = node['children']
...     del (node['children'])
...     if left[1].size == 0:
...         node['right'] = get_leaf(right[1])
...         return
...     if right[1].size == 0:
...         node['left'] = get_leaf(left[1])
...         return
...     # Check if the current depth exceeds the maximal depth
...     if depth >= max_depth:
...         node['left'], node['right'] =
...                         get_leaf(left[1]), get_leaf(right[1])
...         return
...     # Check if the left child has enough samples
...     if left[1].size <= min_size:
...         node['left'] = get_leaf(left[1])
...     else:
...         # It has enough samples, we further split it
...         result = get_best_split(left[0], left[1], criterion)
...         result_left, result_right = result['children']
...         if result_left[1].size == 0:
...             node['left'] = get_leaf(result_right[1])
...         elif result_right[1].size == 0:
...             node['left'] = get_leaf(result_left[1])
...         else:
...             node['left'] = result
...             split(node['left'], max_depth, min_size,
...                                       depth + 1, criterion)
...     # Check if the right child has enough samples
...     if right[1].size <= min_size:
...         node['right'] = get_leaf(right[1])
...     else:
...         # It has enough samples, we further split it
...         result = get_best_split(right[0], right[1], criterion)
...         result_left, result_right = result['children']
...         if result_left[1].size == 0:
...             node['right'] = get_leaf(result_right[1])
...         elif result_right[1].size == 0:
...             node['right'] = get_leaf(result_left[1])
...         else:
...             node['right'] = result
...             split(node['right'], max_depth, min_size,
...                                         depth + 1, criterion) 

该函数首先从节点字典中提取左右子节点。然后检查左子节点或右子节点是否为空。如果为空,它会给相应的子节点分配一个叶节点。接着,它检查当前深度是否超过树允许的最大深度。如果超过,则为左右子节点都分配叶节点。如果左子节点有足够的样本进行分裂(大于 min_size),它将使用 get_best_split 函数计算最佳分裂。如果结果分裂产生空子节点,它将为相应子节点分配叶节点;否则,它会递归地对左子节点调用 split 函数。右子节点执行类似的步骤。

最后,树构建的入口点如下:

>>> def train_tree(X_train, y_train, max_depth, min_size,
...                criterion='gini'):
...     X = np.array(X_train)
...     y = np.array(y_train)
...     root = get_best_split(X, y, criterion)
...     split(root, max_depth, min_size, 1, criterion)
...     return root 

现在,让我们使用前面的手工计算例子来测试它:

>>> X_train = [['tech', 'professional'],
...            ['fashion', 'student'],
...            ['fashion', 'professional'],
...            ['sports', 'student'],
...            ['tech', 'student'],
...            ['tech', 'retired'],
...            ['sports', 'professional']]
>>> y_train = [1, 0, 0, 0, 1, 0, 1]
>>> tree = train_tree(X_train, y_train, 2, 2) 

为了验证模型生成的树与我们手工构建的树是相同的,我们编写一个函数来显示这棵树:

>>> CONDITION = {'numerical': {'yes': '>=', 'no': '<'},
...              'categorical': {'yes': 'is', 'no': 'is not'}}
>>> def visualize_tree(node, depth=0):
...     if isinstance(node, dict):
...         if node['value'].dtype.kind in ['i', 'f']:
...             condition = CONDITION['numerical']
...         else:
...             condition = CONDITION['categorical']
...         print('{}|- X{} {} {}'.format(depth * '  ',
...             node['index'] + 1, condition['no'], node['value']))
...         if 'left' in node:
...             visualize_tree(node['left'], depth + 1)
...         print('{}|- X{} {} {}'.format(depth * '  ',
...             node['index'] + 1, condition['yes'], node['value']))
...         if 'right' in node:
...             visualize_tree(node['right'], depth + 1)
...     else:
...         print(f"{depth * '  '}[{node}]")
>>> visualize_tree(tree)
|- X1 is not fashion
 |- X2 is not professional
   [0]
 |- X2 is professional
   [1]
|- X1 is fashion
 [0] 

我们可以通过以下数值例子进行测试:

>>> X_train_n = [[6, 7],
...             [2, 4],
...             [7, 2],
...             [3, 6],
...             [4, 7],
...             [5, 2],
...             [1, 6],
...             [2, 0],
...             [6, 3],
...             [4, 1]]
>>> y_train_n = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]
>>> tree = train_tree(X_train_n, y_train_n, 2, 2)
>>> visualize_tree(tree)
|- X2 < 4
 |- X1 < 7
   [1]
 |- X1 >= 7
   [0]
|- X2 >= 4
 |- X1 < 2
   [1]
 |- X1 >= 2
   [0] 

我们的决策树模型生成的树与我们手工制作的树相同。

现在,在通过从头实现一个决策树后,你对决策树有了更深入的理解,我们可以继续使用 scikit-learn 实现决策树。

使用 scikit-learn 实现决策树

在这里,我们将使用 scikit-learn 的决策树模块(scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html),该模块已经得到充分开发和优化:

>>> from sklearn.tree import DecisionTreeClassifier
>>> tree_sk = DecisionTreeClassifier(criterion='gini',
...                                max_depth=2, min_samples_split=2)
>>> tree_sk.fit(X_train_n, y_train_n) 

为了可视化我们刚刚构建的树,我们利用内置的 export_graphviz 函数,如下所示:

>>> from sklearn.tree import export_graphviz
>>> export_graphviz(tree_sk, out_file='tree.dot',
...                 feature_names=['X1', 'X2'], impurity=False,
...                 filled=True, class_names=['0', '1']) 

运行此命令将生成一个名为 tree.dot 的文件,可以通过 Graphviz 转换为 PNG 图像文件(介绍和安装说明可在 www.graphviz.org 找到),方法是在终端中运行以下命令:

dot -Tpng tree.dot -o tree.png 

请参考图 3.11查看结果:

A picture containing text, handwriting, font, screenshot  Description automatically generated

图 3.11:树形结构可视化

生成的树与我们之前构建的树基本相同。

我知道你迫不及待想使用决策树来预测广告点击率。让我们继续下一部分。

使用决策树预测广告点击率

经过几个示例后,现在是时候使用你刚刚学过并练习过的决策树算法来预测广告点击率了。我们将使用 Kaggle 机器学习竞赛的数据集,点击率预测www.kaggle.com/c/avazu-ctr-prediction)。该数据集可以从www.kaggle.com/c/avazu-ctr-prediction/data下载。

只有train.gz文件包含标注样本,因此我们只需要下载此文件并解压(这可能需要一些时间)。在本章中,我们将仅关注从train.gz解压出的train.csv文件中的前 300,000 个样本。

原始文件中的字段如下:

计算机截图  描述自动生成,信心较低

图 3.12:数据集的描述和示例值

我们通过运行以下命令来快速查看文件的开头:

head train | sed 's/,,/, ,/g;s/,,/, ,/g' | column -s, -t 

与简单的head train不同,输出更加整洁,因为所有列都对齐了:

一张包含文本的图片,截图,黑白,菜单  描述自动生成

图 3.13:数据的前几行

不要被匿名化和哈希化的值吓到。它们是分类特征,每个可能的值都对应着一个真实且有意义的值,但由于隐私政策,它们以这种方式呈现。可能C1代表用户性别,10051002分别表示男性和女性。

现在,让我们通过使用pandas来读取数据集。没错,pandas在处理表格数据时非常高效:

>>> import pandas as pd
>>> n_rows = 300000
>>> df = pd.read_csv("train.csv", nrows=n_rows) 

文件的前 300,000 行已加载并存储在一个 DataFrame 中。快速查看 DataFrame 的前五行:

>>> print(df.head(5))
id  click      hour C1 banner_pos   site_id ... C16 C17 C18 C19     C20 C21
0  1.000009e+18      0 14102100 1005          0 1fbe01fe ... 50 1722 0  35 -1 79
1  1.000017e+19      0 14102100 1005          0 1fbe01fe ... 50 1722 0  35 100084 79
2  1.000037e+19      0 14102100 1005          0 1fbe01fe ... 50 1722 0  35 100084 79
3  1.000064e+19      0 14102100 1005          0 1fbe01fe ... 50 1722 0  35 100084 79
4  1.000068e+19      0 14102100 1005          1 fe8cc448 ... 50 2161 0  35 -1 157 

目标变量是click列:

>>> Y = df['click'].values 

对于其余的列,有几个列应从特征中移除(idhourdevice_iddevice_ip),因为它们不包含太多有用的信息:

>>> X = df.drop(['click', 'id', 'hour', 'device_id', 'device_ip'],
                axis=1).values
>>> print(X.shape)
(300000, 19) 

每个样本有19个预测属性。

接下来,我们需要将数据划分为训练集和测试集。通常,我们通过随机选择样本来进行划分。然而,在我们的案例中,样本是按时间顺序排列的,正如hour字段所示。显然,我们不能使用未来的样本来预测过去的样本。因此,我们将前 90%作为训练样本,其余的作为测试样本:

>>> n_train = int(n_rows * 0.9)
>>> X_train = X[:n_train]
>>> Y_train = Y[:n_train]
>>> X_test = X[n_train:]
>>> Y_test = Y[n_train:] 

如前所述,决策树模型可以接受分类特征。然而,由于 scikit-learn 中的树基算法(截至 2024 年初的当前版本为 1.4.1)仅允许数值输入,我们需要将分类特征转换为数值特征。但请注意,通常我们不需要这么做;例如,我们之前从零开始开发的决策树分类器就可以直接接受分类特征。

我们现在将使用scikit-learn中的OneHotEncoder模块将基于字符串的分类特征转换为 one-hot 编码向量。one-hot 编码在第一章机器学习与 Python 入门中简要提到过。简而言之,它将具有k个可能值的分类特征转换为k个二进制特征。例如,具有三个可能值newseducationsports的网站类别特征,将被编码为三个二进制特征,如is_newsis_educationis_sports,其值为10

我们初始化一个OneHotEncoder对象如下:

>>> from sklearn.preprocessing import OneHotEncoder
>>> enc = OneHotEncoder(handle_unknown='ignore') 

我们在训练集上拟合模型如下:

>>> X_train_enc = enc.fit_transform(X_train)
>>> X_train_enc[0]
<1x8385 sparse matrix of type '<class 'numpy.float64'>'
with 19 stored elements in Compressed Sparse Row format>
>>> print(X_train_enc[0])
  (0, 2)		1.0
  (0, 6)		1.0
  (0, 188)		1.0
  (0, 2608)		1.0
  (0, 2679)		1.0
  (0, 3771)		1.0
  (0, 3885)		1.0
  (0, 3929)		1.0
  (0, 4879)		1.0
  (0, 7315)		1.0
  (0, 7319)		1.0
  (0, 7475)		1.0
  (0, 7824)		1.0
  (0, 7828)		1.0
  (0, 7869)		1.0
  (0, 7977)		1.0
  (0, 7982)		1.0
  (0, 8021)		1.0
  (0, 8189)		1.0 

每个转换后的样本都是一个稀疏向量。

我们使用训练好的 one-hot 编码器对测试集进行转换,如下所示:

>>> X_test_enc = enc.transform(X_test) 

记住,我们在之前的 one-hot 编码器中指定了handle_unknown='ignore'参数。这是为了防止因遇到未见过的分类值而导致错误。以之前的网站类别例子为例,如果有一个样本值为movie,那么所有三个转换后的二进制特征(is_newsis_educationis_sports)的值都会变成0。如果我们没有指定ignore,将会抛出错误。

到目前为止,我们进行交叉验证的方式是显式地将数据划分为折叠,并重复写一个for循环来逐个检查每个超参数。为了减少这种冗余,我们将引入一种更优雅的方法,使用来自 scikit-learn 的GridSearchCV模块。GridSearchCV隐式处理整个过程,包括数据拆分、折叠生成、交叉训练与验证,最后进行最佳参数的全面搜索。剩下的工作就是指定要调整的超参数以及每个超参数要探索的值。为了演示目的,我们将只调整max_depth超参数(其他超参数,如min_samples_splitclass_weight,也强烈推荐调整):

>>> from sklearn.tree import DecisionTreeClassifier
>>> parameters = {'max_depth': [3, 10, None]} 

我们为最大深度选择了三个选项——310和无限制。我们初始化一个使用基尼不纯度作为度量,并且将30作为进一步分裂所需的最小样本数的决策树模型:

>>> decision_tree = DecisionTreeClassifier(criterion='gini',
...                                        min_samples_split=30) 

分类度量应该是 ROC 曲线的 AUC,因为这是一个不平衡的二分类问题(在 300,000 个训练样本中,只有 51,211 个是点击,正样本 CTR 为 17%;我鼓励你自己去计算类分布)。至于网格搜索,我们使用三折交叉验证(因为训练集相对较小),并选择通过 AUC 衡量的最佳超参数:

>>> grid_search = GridSearchCV(decision_tree, parameters,
...                            n_jobs=-1, cv=3, scoring='roc_auc') 

注意,n_jobs=-1意味着我们使用所有可用的 CPU 处理器:

>>> grid_search.fit(X_train, y_train)
>>> print(grid_search.best_params_)
{'max_depth': 10} 

我们使用具有最佳参数的模型对任何未来的测试案例进行预测,如下所示:

>>> decision_tree_best = grid_search.best_estimator_
>>> pos_prob = decision_tree_best.predict_proba(X_test)[:, 1]
>>> from sklearn.metrics import roc_auc_score
>>> print(f'The ROC AUC on testing set is: {roc_auc_score(Y_test,
...           pos_prob):.3f}')
The ROC AUC on testing set is: 0.719 

我们在最优决策树模型上能达到的 AUC 值是 0.72。这看起来并不是很高,但点击率涉及许多复杂的人为因素,这就是为什么预测点击率并不是一项容易的任务。尽管我们可以进一步优化超参数,0.72 的 AUC 实际上已经相当不错了。作为对比,随机选择 17%的样本进行点击会生成 AUC 值0.499

>>> pos_prob = np.zeros(len(Y_test))
>>> click_index = np.random.choice(len(Y_test),
...                   int(len(Y_test) * 51211.0/300000),
...                   replace=False)
>>> pos_prob[click_index] = 1
>>> print(f'The ROC AUC on testing set using random selection is: {roc_auc_score(Y_test, pos_prob):.3f}')
The ROC AUC on testing set using random selection is: 0.499 

我们的决策树模型显著优于随机预测器。回头看,我们可以看到,决策树是一个基于训练数据集在每一步进行贪心搜索,寻找最佳分裂点的过程。然而,这往往会导致过拟合,因为最优分裂点很可能仅对训练样本有效。幸运的是,集成方法可以纠正这一点,随机森林就是一种通常优于简单决策树的集成树模型。

最佳实践

以下是为树算法准备数据的两个最佳实践:

  • 编码类别特征:如前所述,我们需要在将类别特征输入模型之前进行编码。独热编码和标签编码是常用的选择。

  • 缩放数值特征:我们需要注意数值特征的尺度,以防止尺度较大的特征在树的分裂决策中占主导地位。归一化或标准化通常用于此目的。

集成决策树——随机森林

集成技术中的袋装法(即自助聚合),我在第一章机器学习与 Python 入门》中简要提到过,能够有效克服过拟合问题。回顾一下,不同的训练样本集从原始训练数据中随机抽取并有放回地选取;每个得到的样本集用于拟合一个单独的分类模型。然后,这些分别训练的模型的结果通过多数投票结合在一起,做出最终决策。

如前文所述,树袋装法可以减少决策树模型的高方差,因此通常比单一的决策树表现更好。然而,在某些情况下,如果一个或多个特征是强指示器,个体树会在很大程度上根据这些特征构建,因此可能高度相关。聚合多个相关树不会有太大区别。为了让每棵树都不相关,随机森林在搜索每个节点的最佳分裂点时只考虑特征的随机子集。个体树现在基于不同的特征顺序集进行训练,这保证了更多的多样性和更好的表现。随机森林是树袋装模型的一种变体,具有额外的基于特征的袋装

要在我们的点击率预测项目中使用随机森林,我们可以使用来自 scikit-learn 的包。与我们在前一节中实现决策树的方式类似,我们只需调整 max_depth 参数:

>>> from sklearn.ensemble import RandomForestClassifier
>>> random_forest = RandomForestClassifier(n_estimators=100,
...                     criterion='gini', min_samples_split=30,
...                     n_jobs=-1) 

除了 max_depthmin_samples_splitclass_weight,这些是与单个决策树相关的重要超参数之外,还强烈推荐与随机森林(一组树)相关的超参数,例如 n_estimators。我们如下微调 max_depth

>>> grid_search = GridSearchCV(random_forest, parameters,
...                            n_jobs=-1, cv=3, scoring='roc_auc')
>>> grid_search.fit(X_train, y_train)
>>> print(grid_search.best_params_)
{'max_depth': None} 

我们使用具有最优参数 None 的模型来预测任何未来未见案例(节点扩展直至满足另一个停止标准):

>>> random_forest_best = grid_search.best_estimator_
>>> pos_prob = random_forest_best.predict_proba(X_test)[:, 1]
>>> print(f'The ROC AUC on testing set using random forest is: {roc_auc_
...       score(Y_test, pos_prob):.3f}')
The ROC AUC on testing set using random forest is: 0.759 

结果表明,随机森林模型大大提升了性能。

让我们总结几个关键的超参数调整:

  • max_depth:这是单个树的最大深度。如果太深可能会过拟合,如果太浅可能会欠拟合。

  • min_samples_split:此超参数表示在节点进一步分割所需的最小样本数。过小的值往往会导致过拟合,而过大的值可能会引入欠拟合。103050 可能是良好的起始选项。

上述两个超参数通常与单个决策树相关。以下两个参数更与随机森林或树集合相关:

  • max_features:此参数表示每次最佳分割点搜索考虑的特征数。通常情况下,对于一个 m 维数据集,推荐使用 (四舍五入)。在 scikit-learn 中可以指定为 max_features="sqrt"。其他选项包括 log2,20%,和原始特征的 50%。

  • n_estimators:此参数表示用于多数投票考虑的树的数量。一般来说,树越多,性能越好,但计算时间越长。通常设置为 100200500 等。

接下来,我们将讨论梯度提升树。

集成决策树 - 梯度提升树

提升,这是另一种集成技术,采用迭代方法而不是并行组合多个学习器。在提升树中,不再单独训练个体树。具体来说,在梯度提升树(GBT)(也称为梯度提升机)中,个体树按顺序训练,其中每棵树旨在纠正前一棵树的错误。以下两个图表说明了随机森林和 GBT 之间的差异。

随机森林模型独立构建每棵树,使用数据集的不同子集,然后通过多数投票或平均结果结合:

一棵树的图表 描述自动生成,置信度低

图 3.14:随机森林工作流程

GBT 模型一次构建一棵树,并在此过程中不断组合结果:

图 3.15:GBT 工作流程

GBT 通过不断改进集成模型的预测来工作,通过依次添加经过训练的决策树,每棵树都集中在前一棵树的残差上。它是这样工作的:

  • 初始化:该过程从一个初始的简单模型开始,通常是一个单一的决策树,它作为集成方法的起始点。

  • 顺序训练:后续的决策树按顺序训练,每棵树都试图纠正前一棵树的错误。每棵新树都是在集成模型前一棵树的预测残差(实际值与预测值之间的差异)上进行训练的。

  • 加法建模:每棵新决策树都以最小化总体误差的方式被添加到集成模型中。这些树通常是浅层的,具有有限的节点数,以避免过拟合并提高泛化能力。

  • 学习率:GBT 引入了一个学习率参数,它控制每棵树对集成模型的贡献。较低的学习率会导致学习进程较慢,但能够提升集成模型的整体表现和稳定性。

  • 集成预测:最终预测是通过将所有树的预测结果结合起来得到的。

我们将使用XGBoost包(xgboost.readthedocs.io/en/latest/)来实现 GBT。我们首先通过以下命令使用conda安装XGBoost Python API

conda install -c conda-forge xgboost 

我们也可以使用pip,如下所示:

pip install xgboost 

如果你遇到问题,请安装或升级CMake(一个跨平台的构建系统生成器),如下所示:

pip install CMake 

现在,让我们看看接下来的步骤。你将看到我们如何使用 GBT 预测点击:

  1. 我们导入 XGBoost 并初始化 GBT 模型:

    >>> import xgboost as xgb
    >>> model = xgb.XGBClassifier(learning_rate=0.1, max_depth=10,
    ...                           n_estimators=1000) 
    

我们将学习率设置为0.1,它决定了我们在每一步(在每棵树上,GBT 中的每一步)学习的速度。我们将在第四章使用逻辑回归预测在线广告点击率中详细讨论学习率。max_depth的值为 10,表示每棵树的最大深度。此外,我们将在 GBT 模型中按顺序训练 1,000 棵树。

  1. 接下来,我们将在之前准备的训练集上训练 GBT 模型:

    >>> model.fit(X_train_enc, Y_train) 
    
  2. 我们使用训练好的模型对测试集进行预测,并相应地计算 ROC AUC:

    >>> pos_prob = model.predict_proba(X_test_enc)[:, 1]
    >>> print(f'The ROC AUC on testing set using GBT is: {roc_auc_score(Y_test, pos_prob):.3f}')
    The ROC AUC on testing set using GBT is: 0.771 
    

我们使用 XGBoost GBT 模型达到了0.77的 AUC。

在本节中,你了解了另一种树集成方法——GBT,并将其应用于我们的广告点击预测。

最佳实践

所以,你在本章中学到了几种基于树的算法——太棒了!但是,选择合适的算法可能会有点棘手。这里有一个实用的指南:

  • 决策树(CART):这是最简单且最易解释的算法。我们通常将其用于较小的数据集。

  • 随机森林:它对过拟合更具鲁棒性,能够处理较大或复杂的数据集。

  • GBT:这是被认为是最强大的算法,适用于复杂问题,也是行业中最流行的基于树的算法。然而,它也容易发生过拟合。因此,建议使用超参数调优和正则化技术来避免过拟合。

总结

在本章中,我们首先介绍了一个典型的机器学习问题——在线广告点击率预测及其固有的挑战,包括分类特征。然后,我们研究了可以处理数值特征和分类特征的基于树的算法。

接下来,我们深入讨论了决策树算法:它的原理、不同类型、如何构建树,以及衡量节点分裂有效性的两个指标(基尼不纯度和熵)。在手动构建树之后,我们从零开始实现了该算法。

你还学习了如何使用 scikit-learn 中的决策树包,并将其应用于预测 CTR。我们通过采用基于特征的随机森林袋装算法,继续提高性能。最后,本章以几种调优随机森林模型的方法结束,并介绍了两种不同的集成决策树方法:随机森林和 GBT 建模。袋装(Bagging)和提升(Boosting)是两种可以提高学习性能的模型集成方法。

更多的练习总是有助于磨练你的技能。我建议在进入下一章之前,完成以下练习,我们将使用另一种算法:逻辑回归来解决广告点击率预测问题。

练习

  1. 在决策树点击率预测项目中,你是否可以调整其他超参数,如min_samples_splitclass_weight?你能够达到的最高 AUC 是多少?

  2. 在基于随机森林的点击率预测项目中,你是否可以调整其他超参数,如min_samples_splitmax_featuresn_estimators,在 scikit-learn 中?你能够达到的最高 AUC 是多少?

  3. 在基于 GBT 的点击率预测项目中,你可以调整哪些超参数?你能够达到的最高 AUC 是多少?你可以阅读xgboost.readthedocs.io/en/latest/python/python_api.html#module-xgboost.sklearn来找出答案。

加入我们书籍的 Discord 空间

加入我们社区的 Discord 空间,与作者和其他读者讨论:

packt.link/yuxi