PyTorch 与 Sklearn 机器学习指南(一)
原文:
zh.annas-archive.org/md5/2a872f7dd98f6fbe3043a236f689e451译者:飞龙
前言
通过接触新闻和社交媒体,您可能已经熟悉了机器学习已成为我们时代最激动人心的技术之一这一事实。微软、谷歌、Meta、苹果、亚马逊、IBM 等大公司都在机器学习研究和应用方面进行重大投资,这是有充分理由的。虽然看起来机器学习已成为我们时代的时尚词汇,但它绝非炒作。这一激动人心的领域开辟了新的可能性,并已成为我们日常生活中不可或缺的部分。与我们智能手机上的语音助手交谈,为客户推荐合适的产品,预防信用卡欺诈,从电子邮件收件箱中过滤垃圾邮件,检测和诊断医疗疾病,这些都是它的应用案例。
如果你想成为一名机器学习实践者,一个更好的问题解决者,甚至考虑从事机器学习研究的职业,那么这本书适合你!然而,对于初学者来说,机器学习背后的理论概念可能会非常压倒性。然而,近年来出版的许多实用书籍将帮助您通过实施强大的学习算法来开始机器学习的学习。
接触实际代码示例,并通过机器学习示例应用程序的实例工作,是深入了解这一领域的好方法。具体的例子帮助说明更广泛的概念,通过直接实施学习材料来展示。然而,请记住,强大的力量伴随着巨大的责任!除了使用 Python 和基于 Python 的机器学习库提供实际机器学习经验外,本书还介绍了机器学习算法背后的数学概念,这对于成功使用机器学习至关重要。因此,本书不同于纯粹的实用书籍;它讨论了关于机器学习概念的必要细节,提供直观而信息丰富的解释,说明机器学习算法的工作原理,如何使用它们,以及最重要的是如何避免最常见的陷阱。
在本书中,我们将踏上一段充满挑战的旅程,涵盖所有必要的主题和概念,为您在这一领域的起步提供帮助。如果您发现您对知识的渴望还没有得到满足,本书引用了许多有用的资源,供您追踪这一领域的重要突破。
本书适合谁
本书是学习如何将机器学习和深度学习应用于各种任务和数据集的理想伴侣。如果你是一个想跟上技术最新趋势的程序员,那这本书绝对适合你。此外,如果你是一名学生或考虑职业转型,这本书将是你了解机器学习世界的介绍和全面指南。
本书涵盖的内容
第一章,赋予计算机从数据中学习的能力,向您介绍了处理各种问题任务的主要机器学习子领域。此外,它讨论了创建典型的机器学习模型构建流水线的基本步骤,这将指导我们接下来的章节。
第二章,为分类训练简单的机器学习算法,回顾了机器学习的起源,并介绍了二元感知器分类器和自适应线性神经元。本章是对模式分类基础的温和介绍,并侧重于优化算法与机器学习的相互作用。
第三章,使用 Scikit-Learn 进行机器学习分类器之旅,描述了用于分类的基本机器学习算法,并使用最流行和全面的开源机器学习库之一 scikit-learn 提供了实际示例。
第四章,构建良好的训练数据集 – 数据预处理,讨论了如何处理未经处理的数据集中最常见的问题,例如缺失数据。它还讨论了几种识别数据集中最具信息价值特征的方法,并教授如何为机器学习算法准备不同类型的变量作为适当的输入。
第五章,通过降维压缩数据,描述了减少数据集特征数量的基本技术,同时保留大部分有用和区分信息。它讨论了通过主成分分析的标准降维方法,并将其与监督和非线性转换技术进行了比较。
第六章,学习模型评估和超参数调优的最佳实践,讨论了估计预测模型性能的可行与不可行之处。此外,它讨论了衡量我们模型性能的不同指标以及调优机器学习算法的技术。
第七章,结合不同模型进行集成学习,向您介绍了有效地组合多个学习算法的不同概念。它教授您如何构建专家集合来克服单个学习器的弱点,从而获得更准确和可靠的预测。
第八章,将机器学习应用于情感分析,讨论了将文本数据转换为机器学习算法可理解的有意义表示的基本步骤,以预测人们基于其写作的意见。
第九章,使用回归分析预测连续目标变量,讨论了建模目标和响应变量之间线性关系的基本技术,以便在连续尺度上进行预测。在介绍了不同线性模型之后,它还讨论了多项式回归和基于树的方法。
第十章,使用无标签数据进行工作 - 聚类分析,将注意力转向机器学习的另一个子领域,即无监督学习。我们将应用来自三个基本聚类算法家族的算法,以找到共享一定相似度的对象群体。
第十一章,从零开始实现多层人工神经网络,扩展了基于梯度优化的概念,我们首次在第二章中介绍了训练简单机器学习算法进行分类,以基于 Python 中流行的反向传播算法构建强大的多层神经网络。
第十二章,使用 PyTorch 并行训练神经网络,基于前一章节的知识提供了一个实用指南,用于更有效地训练神经网络。本章的重点是 PyTorch,这是一个开源的 Python 库,允许我们利用现代 GPU 的多个核心,并通过用户友好和灵活的 API 从常见的构建模块构建深度神经网络。
第十三章,深入探讨 - PyTorch 的机制,延续了上一章的内容,介绍了 PyTorch 更高级的概念和功能。PyTorch 是一个非常广泛和复杂的库,本章将向您介绍诸如动态计算图和自动微分等概念。您还将学习如何使用 PyTorch 的面向对象 API 来实现复杂的神经网络,以及 PyTorch Lightning 如何帮助您遵循最佳实践和减少样板代码。
第十四章,使用深度卷积神经网络对图像进行分类,介绍了卷积神经网络(CNNs)。CNN 代表一种特定类型的深度神经网络架构,特别适合处理图像数据集。由于其相对传统方法的卓越性能,CNNs 现在广泛用于计算机视觉中,用于各种图像识别任务的最新结果。在本章中,您将学习如何使用卷积层作为强大的图像特征提取器进行图像分类。
第十五章,使用递归神经网络对序列数据建模,介绍了另一种流行的用于深度学习的神经网络架构,特别适用于处理文本和其他类型的序列数据以及时间序列数据。作为热身练习,本章介绍了用于预测电影评论情感的递归神经网络。然后,我们将教会递归网络如何从书籍中摘取信息,以生成全新的文本。
第十六章,Transformer – 通过注意机制改进自然语言处理,专注于自然语言处理的最新趋势,并解释了注意机制如何帮助建模长序列中的复杂关系。特别是,本章介绍了具有影响力的 Transformer 架构以及诸如 BERT 和 GPT 等最新的 Transformer 模型。
第十七章,生成对抗网络用于合成新数据,介绍了一种流行的对抗性训练方法,适用于生成新的、看起来逼真的图像。本章从简要介绍自编码器开始,自编码器是一种特定类型的神经网络架构,可用于数据压缩。接着,本章展示了如何将自编码器的解码器部分与第二个神经网络结合起来,该神经网络能够区分真实和合成的图像。通过让两个神经网络在对抗训练中相互竞争,您将实现一个生成对抗网络,用于生成新的手写数字。
第十八章,图神经网络用于捕捉图结构化数据的依赖关系,超越了处理表格数据集、图像和文本的范畴。本章介绍了操作于图结构数据(如社交媒体网络和分子)上的图神经网络。在解释了图卷积的基础知识后,本章包括了一个教程,向您展示如何为分子数据实现预测模型。
第十九章,强化学习用于复杂环境中的决策制定,涵盖了机器学习的一个子类别,常用于训练机器人和其他自主系统。本章首先介绍了强化学习的基础知识(RL),以便熟悉代理/环境交互、RL 系统的奖励过程以及从经验中学习的概念。在了解了 RL 的主要类别之后,您将实现并训练一个代理,该代理可以使用 Q-learning 算法在网格世界环境中导航。最后,本章介绍了深度 Q-learning 算法,这是 Q-learning 的一种变体,使用深度神经网络。
要充分利用本书
理想情况下,您已经熟悉使用 Python 进行编程,以便跟随我们提供的代码示例来说明和应用各种算法和模型。要充分利用本书,理解数学符号将会很有帮助。
一台普通的笔记本电脑或台式计算机应该足以运行本书中的大部分代码,我们会在第一章中为您的 Python 环境提供指导。后续章节将在需要时介绍额外的库和安装建议。
最近的图形处理单元(GPU)可以加速后续深度学习章节中的代码运行时间。然而,并非必须使用 GPU,我们还提供使用免费云资源的说明。
下载示例代码文件
所有代码示例可通过 GitHub 下载,地址为github.com/rasbt/machine-learning-book。我们还提供来自丰富书目和视频目录的其他代码包,可在github.com/PacktPublishing/查看!
虽然我们建议使用 Jupyter Notebook 来执行交互式代码,但所有代码示例都以 Python 脚本(例如,ch02/ch02.py)和 Jupyter Notebook 格式(例如,ch02/ch02.ipynb)提供。此外,我们建议查看随每个章节附带的README.md文件,获取额外的信息和更新。
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在此处下载:static.packt-cdn.com/downloads/9781801819312_ColorImages.pdf。此外,本书的代码笔记本中也包含了分辨率较低的彩色图像,随示例代码文件捆绑提供。
约定
本书中使用了多种文本约定。
这里有一些这些风格的示例及其含义的解释。文本中的代码词汇显示如下:“已安装的包可以通过--upgrade标志进行更新。”
代码块设置如下:
def __init__(self, eta=0.01, n_iter=50, random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
Python 解释器中的任何输入都将按以下方式显示(请注意>>>符号)。预期的输出将显示无>>>符号:
>>> v1 = np.array([1, 2, 3])
>>> v2 = 0.5 * v1
>>> np.arccos(v1.dot(v2) / (np.linalg.norm(v1) *
... np.linalg.norm(v2)))
0.0
任何命令行输入或输出都将按以下方式显示:
pip install gym==0.20
新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,会在文本中以这种方式出现:“单击下一步按钮将您移动到下一个屏幕。”
警告或重要说明会以如下方式出现在框中。
小贴士和技巧会以这种方式出现。
联系我们
我们非常欢迎读者的反馈。
总体反馈:电子邮件至 feedback@packtpub.com,并在主题中提及书名。如有关于本书的任何问题,请发送电子邮件至 questions@packtpub.com。
勘误: 尽管我们已尽最大努力确保内容的准确性,但错误确实会发生。如果您在本书中发现错误,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择您的书籍,单击勘误提交表单链接,然后输入详细信息。
盗版问题:如果你在互联网上发现我们的作品以任何形式存在非法副本,我们将不胜感激您提供具体位置或网站名称。请通过电子邮件联系我们,邮箱为 copyright@packtpub.com,并提供相关链接。
如果你有意成为作者:如果你在某个领域有专业知识,并且有意撰写或贡献一本书,请访问authors.packtpub.com。
分享你的想法
一旦你阅读了《使用 PyTorch 和 Scikit-Learn 进行机器学习》,我们很想听听你的想法!请点击此处前往亚马逊评论页面为这本书分享你的反馈。
你的评论对我们和技术社区都非常重要,将帮助我们确保我们提供的内容质量卓越。
第一章:赋予计算机从数据中学习的能力
在我看来,机器学习,即利用算法理解数据的应用与科学,是所有计算机科学领域中最激动人心的领域!我们生活在一个数据丰富的时代;利用来自机器学习领域的自学习算法,我们可以将这些数据转化为知识。多亏了近年来开发的许多强大开源库,现在可能是进入机器学习领域并学习如何利用强大算法来识别数据模式并对未来事件进行预测的最佳时机。
在本章中,您将了解主要概念和不同类型的机器学习。与相关术语的基本介绍一起,我们将为成功利用机器学习技术解决实际问题奠定基础。
在本章中,我们将涵盖以下主题:
-
机器学习的一般概念
-
三种学习类型和基本术语
-
成功设计机器学习系统的构建模块
-
安装和设置 Python 进行数据分析和机器学习
构建智能机器以将数据转化为知识
在现代技术时代,我们拥有大量结构化和非结构化数据资源。在 20 世纪下半叶,人工智能(AI)的一个子领域——机器学习,作为自学习算法的一部分,通过从数据中获取知识来进行预测逐渐发展起来。
与要求人类手动推导规则并从分析大量数据中构建模型相比,机器学习提供了更高效的选择,即从数据中提取知识以逐步改进预测模型的性能并做出数据驱动的决策。
机器学习不仅在计算机科学研究中变得日益重要,而且在我们的日常生活中也扮演着越来越重要的角色。由于机器学习,我们享受到强大的电子邮件垃圾邮件过滤器,便捷的文本和语音识别软件,可靠的网络搜索引擎,推荐有趣电影观看,移动支票存款,估计送餐时间等服务。希望不久的将来,我们还将增加安全高效的自动驾驶汽车到这一列表中。在医疗应用方面也取得了显著进展;例如,研究人员证明深度学习模型可以以接近人类的准确度检测皮肤癌(www.nature.com/articles/nature21056)。最近,DeepMind 的研究人员使用深度学习预测了三维蛋白质结构,表现出色地超过了基于物理的方法(deepmind.com/blog/article/alphafold-a-solution-to-a-50-year-old-grand-challenge-in-biology)。准确的三维蛋白质结构预测在生物和制药研究中发挥着重要作用,最近在医疗保健领域中也有许多其他重要的机器学习应用。例如,研究人员设计了系统,可以预测 COVID-19 患者未来四天的氧需求,以帮助医院分配资源给需要的人群(ai.facebook.com/blog/new-ai-research-to-help-predict-covid-19-resource-needs-from-a-series-of-x-rays/)。当今社会的另一个重要话题是气候变化,这是我们面临的最大和最关键的挑战之一。目前,许多工作致力于开发智能系统来应对气候变化(www.forbes.com/sites/robtoews/2021/06/20/these-are-the-startups-applying-ai-to-tackle-climate-change)。解决气候变化的众多方法之一是精准农业的新兴领域。在这里,研究人员旨在设计基于计算机视觉的机器学习系统,优化资源配置,以最小化化肥的使用和浪费。
机器学习的三种不同类型
在本节中,我们将详细介绍三种机器学习类型:监督学习,无监督学习和强化学习。我们将了解这三种不同学习类型之间的基本差异,并使用概念示例来理解它们可以应用于的实际问题领域:
图 1.1: 三种不同类型的机器学习
使用监督学习对未来进行预测
监督学习的主要目标是从带标签的训练数据中学习模型,使我们能够对未见过或将来的数据进行预测。在这里,“监督”一词指的是一组训练示例(数据输入),其中已知所需的输出信号(标签)。监督学习就是对数据输入和标签之间关系建模的过程。因此,我们也可以将监督学习看作是“标签学习”。
图 1.2 概述了典型的监督学习工作流程,其中标记的训练数据传递给机器学习算法,以拟合一个可以对新的未标记数据输入进行预测的预测模型:
图 1.2: 监督学习过程
考虑电子邮件垃圾邮件过滤的例子,我们可以使用监督机器学习算法在一组带标记的电子邮件上进行模型训练,这些电子邮件已正确标记为垃圾邮件或非垃圾邮件,以预测新电子邮件是否属于这两个类别之一。像前面的电子邮件垃圾邮件过滤例子中这样的监督学习任务,也被称为分类任务。监督学习的另一个子类是回归,在回归中,输出信号是连续值。
预测类标签的分类
分类是监督学习的一个子类,其目标是根据过去的观察预测新实例或数据点的分类类标签。这些类标签是离散的、无序的值,可以理解为数据点的组成员资格。先前提到的电子邮件垃圾检测示例代表了二元分类任务的典型例子,其中机器学习算法学习一组规则来区分两个可能的类别:垃圾邮件和非垃圾邮件。
图 1.3 说明了一个二元分类任务的概念,给出了 30 个训练示例;其中 15 个训练示例标记为 A 类,15 个标记为 B 类。在这种情况下,我们的数据集是二维的,这意味着每个示例都与两个值相关联:x[1] 和 x[2]。现在,我们可以使用监督机器学习算法学习一个规则——以虚线表示的决策边界,它可以分开这两个类,并根据其 x[1] 和 x[2] 值将新数据分类到这两个类别中:
图 1.3: 分类新数据点
然而,类标签集合不必具有二进制性质。监督学习算法学习的预测模型可以将训练数据集中呈现的任何类标签分配给新的未标记数据点或实例。
多类分类任务的典型例子是手写字符识别。我们可以收集一个训练数据集,其中包含每个字母在字母表中的多个手写示例。这些字母(“A”,“B”,“C”等)代表我们想要预测的不同无序类别或类标签。现在,如果用户通过输入设备提供了一个新的手写字符,我们的预测模型将能够以一定的准确率预测字母表中的正确字母。然而,如果这些数字不是训练数据集的一部分,例如,我们的机器学习系统将无法正确识别任何数字 0 到 9 中的任何一个。
用于预测连续结果的回归
我们在前一节中学到,分类任务的目标是将无序标签分配给实例。监督学习的第二种类型是预测连续结果,也称为回归分析。在回归分析中,我们给定一些预测(解释)变量和一个连续的响应变量(结果),并试图找到这些变量之间的关系,以便预测结果。
请注意,在机器学习领域,预测变量通常被称为“特征”,响应变量通常被称为“目标变量”。我们将在本书中沿用这些约定。
例如,假设我们有兴趣预测学生的数学 SAT 成绩。(SAT 是美国常用的大学入学标准化测试。)如果学习时间与最终成绩之间存在关系,我们可以将其作为训练数据,学习一个模型,该模型使用学习时间来预测计划参加该测试的未来学生的测试成绩。
均值回归
“回归”一词由弗朗西斯·高尔顿在他的文章《遗传体质的中等回归》中于 1886 年创造。高尔顿描述了一个生物现象,即人群中身高的变异不会随时间增加。
他观察到父母的身高并不会传递给他们的孩子,而是他们孩子的身高会回归到人群的平均水平。
图 1.4 说明了线性回归的概念。给定一个特征变量 x 和一个目标变量 y,我们拟合一条直线到这些数据上,以最小化数据点与拟合线之间的距离——通常是平均平方距离。
现在,我们可以使用从这些数据中学到的截距和斜率来预测新数据的目标变量:
图 1.4:线性回归示例
使用强化学习解决交互式问题
另一种机器学习的类型是强化学习。在强化学习中,目标是开发一个系统(代理),通过与环境的互动来提高其性能。由于关于环境当前状态的信息通常还包括所谓的奖励信号,我们可以将强化学习看作与监督学习相关的领域。然而,在强化学习中,这种反馈不是正确的地面真实标签或值,而是一个衡量行动如何受奖励函数影响的度量。通过与环境的互动,代理可以利用强化学习来学习一系列通过探索性试错方法或审慎计划最大化此奖励的动作。
强化学习的一个流行例子是象棋程序。在这里,代理根据棋盘的状态(环境)决定一系列动作,奖励可以定义为在比赛结束时赢或输:
图 1.5:强化学习过程
强化学习有许多不同的子类型。然而,一个通用的方案是,在强化学习中,代理试图通过与环境的一系列互动来最大化奖励。每个状态可以与正面或负面奖励相关联,奖励可以定义为实现总体目标,例如赢得或输掉一场象棋比赛。例如,在象棋中,每一步的结果可以被看作是环境的不同状态。
进一步探讨象棋的例子,我们可以将访问棋盘上特定配置视为与更有可能导致获胜的状态相关联——例如,从棋盘上移除对手的棋子或威胁王后。然而,其他位置则与更有可能导致输掉比赛的状态相关联,例如在接下来的回合中失去对手的棋子。现在,在象棋游戏中,奖励(无论是赢得比赛的正面奖励还是输掉比赛的负面奖励)直到游戏结束后才会给出。此外,最终的奖励还取决于对手的棋局。例如,对手可能会牺牲王后,但最终赢得比赛。
总之,强化学习关注于学习选择一系列动作,以最大化总奖励,这可以通过即时采取行动后或通过延迟反馈来获得。
用无监督学习发现隐藏结构
在监督学习中,当我们训练模型时,我们事先知道正确答案(标签或目标变量),在强化学习中,我们为代理执行的特定操作定义奖励措施。然而,在无监督学习中,我们处理的是未标记数据或未知结构的数据。使用无监督学习技术,我们能够探索数据的结构,从中提取有意义的信息,而无需已知的结果变量或奖励函数的指导。
利用聚类找到子群体
聚类是一种探索性数据分析或模式发现技术,允许我们将一堆信息组织成有意义的子群体(簇),而不需要事先了解它们的群组成员资格。在分析过程中产生的每个簇定义了一组共享某种相似度但与其他簇中的对象更为不同的对象,这也是为什么有时将聚类称为无监督分类。聚类是一种从数据中提取有意义关系的重要技术。例如,它允许市场营销人员根据客户的兴趣发现客户群体,以制定不同的营销计划。
图 1.6说明了如何将聚类应用于将未标记数据组织成三个不同组或簇(A、B 和 C,顺序任意),基于它们特征的相似性,x[1]和x[2]:
图 1.6:聚类的工作原理
数据压缩的降维
无监督学习的另一个子领域是降维。通常,我们处理的是高维数据——每个观测都伴随着大量的测量,这可能会对有限的存储空间和机器学习算法的计算性能构成挑战。无监督降维是特征预处理中常用的方法,用于从数据中去除噪声,这些噪声可能会降低某些算法的预测性能。降维将数据压缩到更小的维度子空间,同时保留大部分相关信息。
有时,降维也可以用于数据可视化;例如,可以将高维特征集投影到一维、二维或三维特征空间中,以通过 2D 或 3D 散点图或直方图进行可视化。图 1.7展示了一个例子,其中非线性降维被应用于将一个 3D 瑞士卷压缩到一个新的 2D 特征子空间中:
图 1.7:从三维到二维的降维示例
基本术语和符号介绍
现在我们已经讨论了机器学习的三大类别——监督学习、无监督学习和强化学习——让我们来看看本书中将要使用的基本术语。下面的小节涵盖了我们在谈论数据集不同方面时会使用的常见术语,以及更精确和高效地进行数学表示的符号约定。
由于机器学习是一个广泛且跨学科的领域,您很可能会很快遇到许多指代相同概念的不同术语。第二小节汇总了机器学习文献中使用的许多常用术语,这对您作为参考资料可能会很有用。
本书中使用的符号和约定
图 1.8 描述了鸢尾花数据集的摘录,这是机器学习领域中的一个经典例子(更多信息可在 archive.ics.uci.edu/ml/datasets/iris 找到)。鸢尾花数据集包含了来自三种不同物种——山鸢尾、变色鸢尾和维吉尼亚鸢尾——的 150 朵鸢尾花的测量数据。
在这里,每个花样本代表数据集中的一行,而以厘米为单位的花测量数据则以列的形式存储,我们也称之为数据集的特征:
图 1.8:鸢尾花数据集
为了保持符号简单且高效,我们将使用线性代数的基础知识。在接下来的章节中,我们将使用矩阵符号来表示我们的数据。我们将遵循通常的约定,将每个样本表示为特征矩阵 X 中的单独行,其中每个特征存储为单独的列。
鸢尾花数据集包含 150 个样本和四个特征,可以写成一个 150×4 的矩阵,形式上表示为 :
符号约定
在本书的大部分内容中,除非另有说明,我们将使用上标 i 表示第 i 个训练样本,使用下标 j 表示训练数据集的第 j 个维度。
我们将使用小写的粗体字母来指代向量 (),使用大写的粗体字母来指代矩阵 (
)。为了指代向量或矩阵中的单个元素,我们将字母写成斜体 (x^(^n^) 或
)。
例如, 指的是鸢尾花样本 150 的第一维,即萼片长度。矩阵 X 中的每一行代表一个花实例,并且可以写成一个四维行向量,
:
每个特征维度是一个 150 维列向量,。例如:
类似地,我们可以将目标变量(这里是类标签)表示为一个 150 维列向量:
机器学习术语
机器学习是一个广泛的领域,也是一个非常跨学科的领域,它将许多其他研究领域的科学家聚集在一起。事实上,许多术语和概念已经被重新发现或重新定义,可能已经对您不陌生,但在不同的名称下出现。为了您的方便,在以下列表中,您可以找到一些常用术语及其同义词的选择,这在阅读本书和机器学习文献时可能会对您有所帮助:
-
训练示例:表中的一行,表示数据集中的一个观察值、记录、实例或样本(在大多数情况下,“样本”指的是训练示例的集合)。
-
训练:模型拟合,对于类似参数估计的参数模型。
-
特征,缩写 x:数据表或数据(设计)矩阵中的一列。同义词为预测变量、变量、输入、属性或协变量。
-
目标,缩写 y:与结果、输出、响应变量、因变量(类)标签和地面真实值同义。
-
损失函数:通常与成本函数同义使用。有时损失函数也被称为误差函数。在一些文献中,“损失”一词指的是单个数据点的损失,而成本是计算整个数据集上的损失(平均或总和)的度量。
构建机器学习系统的路线图
在前几节中,我们讨论了机器学习的基本概念和三种不同类型的学习。在本节中,我们将讨论伴随学习算法的机器学习系统的其他重要部分。
图 1.9展示了在预测建模中使用机器学习的典型工作流程,我们将在以下小节中讨论它:
图 1.9:预测建模工作流程
预处理 - 将数据整理成形
让我们首先讨论构建机器学习系统的路线图。原始数据很少以学习算法优化性能所需的形式和形状出现。因此,数据预处理是任何机器学习应用中最关键的步骤之一。
如果我们以前一节中的鸢尾花数据集为例,我们可以将原始数据看作一系列花卉图像,我们希望从中提取有意义的特征。有用的特征可能围绕花卉的颜色或花卉的高度、长度和宽度。
许多机器学习算法还要求所选特征在相同的尺度上以实现最佳性能,这通常通过将特征转换为范围为[0, 1]或具有零均值和单位方差的标准正态分布来实现,我们将在后面的章节中看到。
选定的一些特征可能高度相关,因此在一定程度上是多余的。在这些情况下,降维技术对于将特征压缩到较低维子空间是有用的。减少特征空间的维度具有存储空间需求较少和学习算法运行更快的优势。在某些情况下,如果数据集包含大量无关特征(或噪声),降维还可以改善模型的预测性能;也就是说,如果数据集具有低信噪比。
为了确定我们的机器学习算法不仅在训练数据集上表现良好,而且在新数据上也能很好地泛化,我们还希望将数据集随机分成单独的训练和测试数据集。我们使用训练数据集来训练和优化我们的机器学习模型,而将测试数据集保留到最后用于评估最终模型。
训练和选择预测模型
正如您将在后续章节中看到的那样,已经开发了许多不同的机器学习算法来解决不同的问题任务。从大卫·沃尔珀特(David Wolpert)著名的无免费午餐定理中可以总结出一个重要观点,即我们不能“免费”学习(没有先验区分的学习算法,D.H.沃尔珀特,1996 年;优化的无免费午餐定理,D.H.沃尔珀特和 W.G.麦克瑞迪,1997 年)。我们可以将这个概念与流行的说法联系起来,我想如果你唯一拥有的工具是一把锤子,那么处理一切就像处理钉子一样是诱人的(亚伯拉罕·马斯洛,1966 年)。例如,每种分类算法都有其固有的偏见,如果我们不对任务做任何假设,没有单一的分类模型能够享有优势。因此,在实践中,比较至少几种不同的学习算法以训练和选择表现最佳的模型至关重要。但在我们能够比较不同模型之前,我们首先必须决定一个用于衡量性能的度量标准。一个常用的度量标准是分类准确度,它定义为正确分类实例的比例。
一个合理的问题是:如果我们不在模型选择中使用这个测试数据集,而是将其保留到最后模型评估时使用,那么我们如何知道哪个模型在最终测试数据集和真实世界数据上表现良好?为了解决这个问题,可以使用总称为“交叉验证”的不同技术。在交叉验证中,我们进一步将数据集分成训练和验证子集,以估计模型的泛化性能。
最后,我们也不能期望软件库提供的不同学习算法的默认参数对我们的特定问题任务是最优的。因此,在后续章节中,我们将频繁使用超参数优化技术,这些技术帮助我们调整模型的性能。
我们可以将这些超参数视为不从数据中学习的参数,而是表示模型旋钮,我们可以调整它们以提高其性能。在后续章节中,当我们看到实际示例时,这将变得更加清晰。
评估模型和预测未见数据实例
在我们选择了在训练数据集上拟合的模型之后,我们可以使用测试数据集来估计它在这些未见数据上的表现,以估算所谓的泛化误差。如果我们对其性能满意,现在可以使用这个模型来预测新的未来数据。需要注意的是,先前提到的程序的参数(如特征缩放和降维)仅从训练数据集中获取,并且稍后相同的参数将重新应用于转换测试数据集以及任何新的数据实例——否则在测试数据上测得的性能可能会过于乐观。
使用 Python 进行机器学习
Python 是数据科学中最流行的编程语言之一,得益于其非常活跃的开发者和开源社区,已经开发出大量有用的科学计算和机器学习库。
尽管解释性语言(如 Python)在计算密集型任务中的性能不如低级别编程语言,但已开发出诸如 NumPy 和 SciPy 等扩展库,这些库建立在底层的 Fortran 和 C 实现之上,用于在多维数组上进行快速向量化操作。
对于机器学习编程任务,我们将主要参考 scikit-learn 库,这是目前最流行和易于访问的开源机器学习库之一。在后续章节中,当我们专注于机器学习的一个子领域深度学习时,我们将使用 PyTorch 库的最新版本,该库通过利用图形卡高效训练所谓的深度神经网络模型。
安装 Python 和从 Python 包索引中安装包
Python 可用于三大主要操作系统——Microsoft Windows、macOS 和 Linux——安装程序和文档均可从官方 Python 网站下载:www.python.org。
本书中提供的代码示例已针对 Python 3.9 编写和测试,我们通常建议您使用最新版本的 Python 3。一些代码也可能与 Python 2.7 兼容,但由于 Python 2.7 的官方支持已于 2019 年结束,并且大多数开源库已停止支持 Python 2.7(python3statement.org),我们强烈建议您使用 Python 3.9 或更新版本。
您可以通过执行以下命令检查您的 Python 版本
python --version
或
python3 --version
在您的终端(如果使用 Windows,则为 PowerShell)中执行。
本书中将使用的额外软件包可以通过pip安装程序安装,pip已成为 Python 标准库的一部分,自 Python 3.3 起。有关pip的更多信息,请访问docs.python.org/3/installing/index.html。
成功安装 Python 后,我们可以在终端中执行pip来安装额外的 Python 软件包:
pip install SomePackage
已安装的软件包可以通过--upgrade标志进行更新:
pip install SomePackage --upgrade
使用 Anaconda Python 发行版和包管理器
一个高度推荐的开源软件包管理系统,用于安装用于科学计算的 Python,是由 Continuum Analytics 提供的 conda。Conda 是免费的,并在宽松的开源许可下授权。其目标是帮助在不同操作系统上管理 Python 数据科学、数学和工程软件包的安装和版本。如果您希望使用 conda,它有不同的版本,包括 Anaconda、Miniconda 和 Miniforge:
-
Anaconda 预装了许多科学计算软件包。可以在
docs.anaconda.com/anaconda/install/下载 Anaconda 安装程序,并在docs.anaconda.com/anaconda/user-guide/getting-started/找到 Anaconda 快速入门指南。 -
Miniconda 是 Anaconda 的精简替代品(
docs.conda.io/en/latest/miniconda.html)。本质上,它与 Anaconda 类似,但没有预安装任何软件包,这是许多人(包括作者)喜欢的。 -
Miniforge 类似于 Miniconda,但由社区维护,并使用不同的软件包仓库(conda-forge),与 Miniconda 和 Anaconda 不同。我们发现 Miniforge 是 Miniconda 的一个很好的替代方案。下载和安装说明可在 GitHub 仓库中找到:
github.com/conda-forge/miniforge。
成功安装 conda(通过 Anaconda、Miniconda 或 Miniforge),我们可以使用以下命令安装新的 Python 软件包:
conda install SomePackage
可使用以下命令更新现有软件包:
conda update SomePackage
通过社区支持的 conda-forge 项目(conda-forge.org)可能会提供不在官方 conda 频道中的包,可以通过--channel conda-forge标志指定。例如:
conda install SomePackage --channel conda-forge
不能通过默认的 conda 频道或 conda-forge 获取的包可以通过pip安装,如前所述。例如:
pip install SomePackage
用于科学计算、数据科学和机器学习的包
在本书的前半部分,我们主要使用 NumPy 的多维数组来存储和操作数据。偶尔,我们将使用建立在 NumPy 之上的 pandas 库,它提供了额外的高级数据操作工具,使得处理表格数据变得更加方便。为了增强您的学习体验并可视化定量数据,Matplotlib 库是非常可定制化的,对于理解数据非常有帮助。
本书主要使用的机器学习库是 scikit-learn(第三章至第十一章)。第十二章,“使用 PyTorch 并行化神经网络训练”,将介绍深度学习库 PyTorch。
编写本书所用的主要 Python 包的版本号列在以下列表中。请确保您安装的包的版本号与这些版本号理想情况下相等,以确保代码示例正确运行:
-
NumPy 1.21.2
-
SciPy 1.7.0
-
Scikit-learn 1.0
-
Matplotlib 3.4.3
-
pandas 1.3.2
安装这些包后,您可以通过在 Python 中导入包并访问其__version__属性来再次检查安装的版本,例如:
>>> import numpy
>>> numpy.__version__
'1.21.2'
为了方便起见,我们在本书的免费代码存储库github.com/rasbt/machine-learning-book中包含了一个名为python-environment-check.py的脚本,这样您可以通过执行此脚本检查您的 Python 版本和包版本。
某些章节将需要额外的包,并将提供有关安装的信息。例如,目前不需要安装 PyTorch。第十二章将在需要时提供提示和说明。
如果尽管您的代码与章节中的代码完全相同仍然遇到错误,请先检查底层包的版本号,然后再花时间调试或与出版商或作者联系。有时,库的新版本引入了不向后兼容的更改,这可能解释这些错误。
如果你不想改变主要的 Python 安装包版本,我们建议在安装本书中使用的包时使用虚拟环境。如果你使用 Python 而没有 conda 管理器,你可以使用venv库创建一个新的虚拟环境。例如,你可以通过以下两个命令创建和激活虚拟环境:
python3 -m venv /Users/sebastian/Desktop/pyml-book
source /Users/sebastian/Desktop/pyml-book/bin/activate
请注意,每次打开新终端或 PowerShell 时都需要激活虚拟环境。你可以在docs.python.org/3/library/venv.html找到更多关于venv的信息。
如果你正在使用带有 conda 包管理器的 Anaconda,你可以按照以下方式创建和激活虚拟环境:
conda create -n pyml python=3.9
conda activate pyml
摘要
在本章中,我们以非常高的层次探讨了机器学习,并熟悉了我们将在后续章节中更详细探讨的大局和主要概念。我们了解到监督学习由两个重要的子领域组成:分类和回归。虽然分类模型允许我们将对象分类到已知类别中,但我们可以使用回归分析来预测目标变量的连续结果。无监督学习不仅提供了发现未标记数据结构的有用技术,还可用于特征预处理步骤中的数据压缩。
我们简要地介绍了将机器学习应用于问题任务的典型路线图,这将作为我们在后续章节中进行更深入讨论和实际示例的基础。最后,我们设置了我们的 Python 环境,并安装和更新了所需的包,以准备观看机器学习的实际操作。
在本书的后续部分中,除了机器学习本身,我们还将介绍不同的技术来预处理数据集,这将帮助你充分发挥不同机器学习算法的性能。虽然我们将在整本书中广泛涵盖分类算法,但我们也将探讨回归分析和聚类的不同技术。
我们前方有一段激动人心的旅程,涵盖机器学习广阔领域中的许多强大技术。然而,我们将一步步地接近机器学习,通过本书的各章节逐渐建立我们的知识基础。在接下来的章节中,我们将通过实现最早的分类机器学习算法之一来开始这段旅程,这将为我们准备好第三章,使用 scikit-learn 进行机器学习分类器的导览,在那里我们将涵盖更高级的机器学习算法,使用 scikit-learn 开源机器学习库。
加入我们书籍的 Discord 空间
加入书籍的 Discord 工作空间,与作者进行每月的问我任何会话:
第二章:为分类训练简单的机器学习算法
在本章中,我们将利用两种最早被算法描述的用于分类的机器学习算法:感知器和自适应线性神经元。我们将从头开始在 Python 中逐步实现感知器,并训练它来对鸢尾花数据集中的不同花种进行分类。这将帮助我们理解分类的机器学习算法的概念以及它们如何在 Python 中高效实现。
探讨使用自适应线性神经元进行优化基础,为通过 scikit-learn 机器学习库使用更复杂分类器奠定基础,《第三章》使用 scikit-learn 进行机器学习分类器之旅。
本章我们将讨论以下主题:
-
建立对机器学习算法的理解
-
使用 pandas、NumPy 和 Matplotlib 读取、处理和可视化数据
-
在 Python 中为 2 类问题实现线性分类器
人工神经元——对机器学习早期历史的简要介绍
在我们详细讨论感知器及相关算法之前,让我们简要回顾一下机器学习的起源。为了设计一种人工智能(AI),沃伦·麦卡洛克和沃尔特·皮茨在 1943 年发表了第一个简化脑细胞的概念,即所谓的麦卡洛克-皮茨(MCP)神经元(参见《神经活动中内在思想的逻辑演算》由W. S. 麦卡洛克和W. 皮茨,数学生物物理学公报,5(4):115-133,1943 年)。
生物神经元是大脑中相互连接的神经细胞,参与处理和传递化学和电信号,如图 2.1所示:
图 2.1:神经元处理化学和电信号
麦卡洛克和皮茨将这样的神经元描述为具有二进制输出的简单逻辑门;多个信号到达树突,然后被细胞体整合,如果累积信号超过一定阈值,则会生成一个输出信号,该信号将通过轴突传递。
仅几年后,弗兰克·罗森布拉特基于 MCP 神经元模型发布了感知器学习规则的第一个概念(参见《感知器:一个感知和识别自动机》由F. 罗森布拉特,康奈尔航空实验室,1957 年)。通过他的感知器规则,罗森布拉特提出了一种算法,可以自动学习最优权重系数,然后将其与输入特征相乘,以决定神经元是否发火(传递信号)。在监督学习和分类的背景下,这样的算法可以用于预测新数据点属于哪一类。
人工神经元的正式定义
更正式地说,我们可以将人工神经元的概念放入二分类任务(类别为 0 和 1)的背景中。我们可以定义一个决策函数,,它接受某些输入值x和相应的权重向量w的线性组合,其中z称为所谓的净输入z = w[1]x[1] + w[2]x[2] + ... + w[m]x[m]:
现在,如果特定示例x^(^i^)的净输入大于定义的阈值,,我们预测类别 1;否则,预测类别 0。在感知器算法中,决策函数,
,是单位阶跃函数的一个变体:
为了简化后续的代码实现,我们可以通过几个步骤修改此设置。首先,我们将阈值,,移动到方程的左侧:
其次,我们将所谓的偏置单元定义为 ,并将其作为净输入的一部分:
z = w[1]x[1] + ... + w[m]x[m] + b = w^Tx + b
第三,考虑到引入偏置单元和上述净输入z的重新定义,我们可以如下重新定义决策函数:
线性代数基础:点积和矩阵转置
在接下来的章节中,我们经常会使用线性代数的基本符号。例如,我们将使用向量点积来简写x和w中值的乘积之和,而上标T代表转置,这是一种将列向量转换为行向量及其相反操作。例如,假设我们有以下两个列向量:
然后,我们可以将向量a的转置表示为a^T = [a[1] a[2] a[3]],并将点积表示为
此外,转置操作也可以应用于矩阵,以反映其沿对角线的镜像,例如:
请注意,转置操作严格来说只适用于矩阵;然而,在机器学习的背景下,当我们使用术语“向量”时,我们指的是n × 1 或 1 × m矩阵。
在本书中,我们只会使用线性代数中非常基础的概念;然而,如果您需要快速复习,请查看 Zico Kolter 的出色的线性代数复习和参考,可在www.cs.cmu.edu/~zkolter/course/linalg/linalg_notes.pdf免费获取。
图 2.2说明了如何将净输入z = w^Tx + b通过感知器的决策函数(左子图)压缩为二进制输出(0 或 1),以及如何使用它来区分可以通过线性决策边界分离的两个类别(右子图):
图 2.2:阈值函数为二元分类问题生成线性决策边界
感知器学习规则
MCP 神经元和 Rosenblatt 的阈值感知器模型背后的整个理念是使用还原主义方法模拟大脑中的单个神经元的工作方式:它要么发射,要么不发射。因此,Rosenblatt 的经典感知器规则非常简单,感知器算法可以总结为以下步骤:
-
将权重和偏置单元初始化为 0 或小的随机数。
-
对于每个训练示例x^(^i^),
-
计算输出值,
-
更新权重和偏置单元。
-
在这里,输出值是由我们之前定义的单位阶跃函数预测的类标签,而权重向量w中的偏置单元和每个权重w[j]的同时更新,可以更正式地写成:
更新值(“增量”)计算如下:
注意,与偏置单元不同,权重w[j]对应于数据集中的特征x[j],它们参与确定上面定义的更新值。此外,
是学习率(通常是一个介于 0.0 和 1.0 之间的常数),y^(^i^)是第i个训练示例的真实类标签,
是预测类标签。重要的是要注意,偏置单元和权重向量中的所有权重是同时更新的,这意味着在更新之前我们不重新计算预测标签
,直到通过相应的更新值
和
更新偏置单元和所有权重。具体来说,对于二维数据集,我们可以将更新写成:
在我们在 Python 中实现感知器规则之前,让我们通过一个简单的思想实验来说明这个学习规则有多简单。在感知器正确预测类标签的两种情况下,由于更新值为 0,偏置单元和权重保持不变:
(1)
(2)
然而,在预测错误的情况下,权重被推向正类或负类的方向:
(3)
(4)
为了更好地理解特征值作为乘法因子,,让我们通过另一个简单的例子来说明:
假设,我们将这个例子误分类为类 0。在这种情况下,我们会总共增加相应的权重 2.5,以便下次我们遇到这个例子时,净输入
会更加正向,因此更有可能超过单位阶跃函数的阈值,将例子分类为类 1:
权重更新,,与
的值成比例。例如,如果我们有另一个例子,
,被错误地分类为类 0,那么我们将进一步推动决策边界,以便下次正确分类这个例子:
需要注意的是,感知器的收敛仅在两类线性可分的情况下才保证,这意味着两类不能通过线性决策边界完全分离。(感兴趣的读者可以在我的讲义中找到收敛证明:sebastianraschka.com/pdf/lecture-notes/stat453ss21/L03_perceptron_slides.pdf)。图 2.3展示了线性可分和线性不可分场景的视觉示例:
图 2.3:线性可分和非线性可分类示例
如果两类不能通过线性决策边界分开,我们可以设置对训练数据集的最大遍历次数(epochs)和/或允许的误分类数量阈值。否则,感知器将永远不会停止更新权重。本章后面,我们将介绍 Adaline 算法,该算法产生线性决策边界,即使类别不完全线性可分。在第三章,我们将学习可以产生非线性决策边界的算法。
下载示例代码
如果您直接从 Packt 购买了本书,可以从您在www.packtpub.com的帐户中下载示例代码文件。如果您在其他地方购买了本书,可以直接从github.com/rasbt/machine-learning-book下载所有代码示例和数据集。
现在,在我们进入下一节的实现之前,你刚刚学到的内容可以用一个简单的图表来总结,说明感知器的一般概念:
图 2.4:模型的权重和偏置根据误差函数进行更新
上图说明了感知器如何接收示例(x)的输入,并将其与偏置单元(b)和权重(w)结合起来计算净输入。然后,净输入传递给阈值函数,该函数生成 0 或 1 的二进制输出——示例的预测类标签。在学习阶段,此输出用于计算预测错误并更新权重和偏置单元。
在 Python 中实现感知器学习算法
在前面的部分,我们学习了 Rosenblatt 的感知器规则的工作原理;现在让我们在 Python 中实现它,并将其应用于我们在第一章,使计算机能够从数据中学习中介绍的鸢尾花数据集。
一个面向对象的感知器 API
我们将以面向对象的方式定义感知器接口作为 Python 类,这将允许我们通过fit方法初始化新的Perceptron对象,该对象可以从数据中学习,并通过单独的predict方法进行预测。作为惯例,我们在未初始化对象时通过调用对象的其他方法为属性添加下划线(_),例如self.w_。
Python 科学计算栈的其他资源
如果您对 Python 的科学库还不熟悉或需要复习,请参阅以下资源:
-
pandas:
pandas.pydata.org/pandas-docs/stable/user_guide/10min.html -
Matplotlib:
matplotlib.org/stable/tutorials/introductory/usage.html
以下是 Python 中感知器的实现:
import numpy as np
class Perceptron:
"""Perceptron classifier.
Parameters
------------
eta : float
Learning rate (between 0.0 and 1.0)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight
initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
b_ : Scalar
Bias unit after fitting.
errors_ : list
Number of misclassifications (updates) in each epoch.
"""
def __init__(self, eta=0.01, n_iter=50, random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self, X, y):
"""Fit training data.
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of
examples and n_features is the number of features.
y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0.0, scale=0.01,
size=X.shape[1])
self.b_ = np.float_(0.)
self.errors_ = []
for _ in range(self.n_iter):
errors = 0
for xi, target in zip(X, y):
update = self.eta * (target - self.predict(xi))
self.w_ += update * xi
self.b_ += update
errors += int(update != 0.0)
self.errors_.append(errors)
return self
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_) + self.b_
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.net_input(X) >= 0.0, 1, 0)
使用此感知器实现,我们现在可以通过给定的学习率eta()和迭代次数
n_iter(训练数据集的遍数)来初始化新的Perceptron对象。
通过fit方法,我们将偏置self.b_初始化为初始值 0,并将self.w_中的权重初始化为向量,,其中m表示数据集中的维度(特征)数量。
请注意,初始权重向量包含从标准偏差为 0.01 的正态分布中提取的小随机数,通过rgen.normal(loc=0.0, scale=0.01, size=1 + X.shape[1]),其中rgen是一个 NumPy 随机数生成器,我们使用用户指定的随机种子进行了初始化,以便在需要时可以重现以前的结果。
从技术上讲,我们可以将权重初始化为零(事实上,这是原始感知器算法中所做的)。然而,如果我们这样做,学习率 (
eta) 将不会对决策边界产生影响。如果所有权重都初始化为零,则学习率参数 eta 只影响权重向量的规模,而不影响方向。如果你熟悉三角学,考虑一个向量 v1 =[1 2 3],其中向量 v2 = 0.5 × v1 的角度将完全为零,如以下代码片段所示:
>>> v1 = np.array([1, 2, 3])
>>> v2 = 0.5 * v1
>>> np.arccos(v1.dot(v2) / (np.linalg.norm(v1) *
... np.linalg.norm(v2)))
0.0
这里,np.arccos 是反余弦三角函数,np.linalg.norm 是计算向量长度的函数。(我们决定从随机正态分布中抽取随机数,例如,而不是从均匀分布中抽取,并且使用标准偏差为 0.01,这是任意的;请记住,我们只是希望获得小的随机值,以避免全零向量的特性,正如前面讨论的。)
在阅读本章后,作为可选练习,你可以将self.w_ = rgen.normal(loc=0.0, scale=0.01, size=X.shape[1])改为self.w_ = np.zeros(X.shape[1]),然后使用不同的eta值运行下一节中呈现的感知器训练代码。你会观察到决策边界不会改变。
NumPy 数组索引
对于一维数组,NumPy 的索引工作方式类似于 Python 列表,使用方括号 ([]) 表示法。对于二维数组,第一个索引器指定行号,第二个索引器指定列号。例如,我们使用 X[2, 3] 来选择二维数组 X 中的第三行第四列。
在权重初始化完成后,fit 方法遍历训练数据集中的所有单个示例,并根据我们在前一节讨论的感知器学习规则更新权重。
类标签由 predict 方法预测,在训练期间在 fit 方法中调用以获取权重更新的类标签;但是 predict 也可以用于预测我们拟合模型后新数据的类标签。此外,我们还在 self.errors_ 列表中收集每个时期中的误分类数量,以便稍后分析我们的感知器在训练期间的表现。在 net_input 方法中使用的 np.dot 函数简单地计算向量点积,w^Tx + b。
向量化:用矢量化代码替代 for 循环
而不是使用 NumPy 计算两个数组a和b之间的向量点积,可以通过a.dot(b)或np.dot(a, b)执行计算,我们也可以通过纯 Python 在sum([i * j for i, j in zip(a, b)])中执行计算。然而,使用 NumPy 而不是经典的 Python for循环结构的优势在于,其算术操作是矢量化的。矢量化意味着将元素算术操作自动应用于数组中的所有元素。通过将我们的算术操作表述为对数组的一系列指令,而不是一次对每个元素执行一组操作,我们可以更好地利用具有单指令多数据(SIMD)支持的现代中央处理单元(CPU)架构。此外,NumPy 使用高度优化的线性代数库,如基本线性代数子程序(BLAS)和线性代数包(LAPACK),这些库是用 C 或 Fortran 编写的。最后,NumPy 还允许我们使用线性代数的基础以更紧凑和直观的方式编写代码,如向量和矩阵点积。
在鸢尾花数据集上训练感知器模型
为了测试我们的感知器实现,我们将在本章的其余部分限制以下分析和示例到两个特征变量(维度)。虽然感知器规则不限于两个维度,但仅考虑两个特征,萼片长度和花瓣长度,将允许我们在散点图中可视化训练模型的决策区域,以便学习目的。
请注意,出于实际原因,我们还将仅考虑鸢尾花数据集中的两个花类别,山鸢尾和变色鸢尾——记住,感知器是一个二元分类器。然而,感知器算法可以扩展到多类分类,例如一对全(OvA)技术。
多类分类的 OvA 方法
OvA,有时也称为一对多(OvR),是一种技术,允许我们将任何二元分类器扩展到多类问题。使用 OvA,我们可以为每个类别训练一个分类器,其中特定类别被视为正类,所有其他类别的示例被视为负类。如果我们要对新的未标记数据实例进行分类,我们将使用我们的n个分类器,其中n是类标签的数量,并将具有最高置信度的类标签分配给我们要分类的特定实例。在感知器的情况下,我们将使用 OvA 来选择与最大绝对净输入值相关联的类标签。
首先,我们将使用pandas库直接从UCI 机器学习库加载鸢尾花数据集到DataFrame对象,并通过tail方法打印最后五行来检查数据是否加载正确:
>>> import os
>>> import pandas as pd
>>> s = 'https://archive.ics.uci.edu/ml/'\
... 'machine-learning-databases/iris/iris.data'
>>> print('From URL:', s)
From URL: https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data
>>> df = pd.read_csv(s,
... header=None,
... encoding='utf-8')
>>> df.tail()
执行上述代码后,我们应该看到以下输出,显示了 Iris 数据集的最后五行:
图 2.5:Iris 数据集的最后五行
加载 Iris 数据集
如果你在离线工作或者 UCI 服务器在archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data暂时不可用时,你可以在本书的代码包中找到 Iris 数据集(以及本书中使用的所有其他数据集)。例如,要从本地目录加载 Iris 数据集,可以替换此行,
df = pd.read_csv(
'https://archive.ics.uci.edu/ml/'
'machine-learning-databases/iris/iris.data',
header=None, encoding='utf-8')
与以下一行:
df = pd.read_csv(
'your/local/path/to/iris.data',
header=None, encoding='utf-8')
接下来,我们提取与 50 朵山鸢尾花和 50 朵变色鸢尾花对应的前 100 个类标签,并将这些类标签转换为两个整数类标签1(变色鸢尾花)和0(山鸢尾花),然后将其分配给向量y,其中 pandas DataFrame的values方法产生了相应的 NumPy 表示。
同样,我们从这 100 个训练示例中提取第一个特征列(萼片长度)和第三个特征列(花瓣长度),并将它们分配给特征矩阵X,我们可以通过二维散点图来可视化:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> # select setosa and versicolor
>>> y = df.iloc[0:100, 4].values
>>> y = np.where(y == 'Iris-setosa', 0, 1)
>>> # extract sepal length and petal length
>>> X = df.iloc[0:100, [0, 2]].values
>>> # plot data
>>> plt.scatter(X[:50, 0], X[:50, 1],
... color='red', marker='o', label='Setosa')
>>> plt.scatter(X[50:100, 0], X[50:100, 1],
... color='blue', marker='s', label='Versicolor')
>>> plt.xlabel('Sepal length [cm]')
>>> plt.ylabel('Petal length [cm]')
>>> plt.legend(loc='upper left')
>>> plt.show()
执行上述代码示例后,我们应该看到以下散点图:
图 2.6:以萼片和花瓣长度分类的山鸢尾花和变色鸢尾花的散点图
图 2.6显示了 Iris 数据集中花例子在两个特征轴上的分布:花瓣长度和萼片长度(以厘米为单位)。在这个二维特征子空间中,我们可以看到线性决策边界应该足以将山鸢尾花和变色鸢尾花分开。因此,感知器这样的线性分类器应该能够完美地分类这个数据集中的花。
现在,是时候在我们刚刚提取的 Iris 数据子集上训练感知器算法了。此外,我们将绘制每个 epoch 的误分类错误,以检查算法是否收敛并找到了能够分离两种 Iris 花类的决策边界:
>>> ppn = Perceptron(eta=0.1, n_iter=10)
>>> ppn.fit(X, y)
>>> plt.plot(range(1, len(ppn.errors_) + 1),
... ppn.errors_, marker='o')
>>> plt.xlabel('Epochs')
>>> plt.ylabel('Number of updates')
>>> plt.show()
请注意,误分类错误的数量和更新次数是相同的,因为每当感知器错误分类一个示例时,感知器的权重和偏置就会更新。执行上述代码后,我们应该能看到误分类错误与迭代次数的图示,如图 2.7所示:
正如我们在图 2.7中所看到的,我们的感知器在第六次 epoch 后收敛,现在应该能够完美地对训练示例进行分类。让我们实现一个小便捷函数来可视化二维数据集的决策边界:
from matplotlib.colors import ListedColormap
def plot_decision_regions(X, y, classifier, resolution=0.02):
# setup marker generator and color map
markers = ('o', 's', '^', 'v', '<')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# plot the decision surface
x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
np.arange(x2_min, x2_max, resolution))
lab = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
lab = lab.reshape(xx1.shape)
plt.contourf(xx1, xx2, lab, alpha=0.3, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
# plot class examples
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0],
y=X[y == cl, 1],
alpha=0.8,
c=colors[idx],
marker=markers[idx],
label=f'Class {cl}',
edgecolor='black')
首先,我们定义了一些colors和markers并通过ListedColormap从颜色列表创建了一个色彩映射。然后,我们确定了两个特征的最小值和最大值,并使用这些特征向量通过 NumPy 的meshgrid函数创建了一对网格数组xx1和xx2。由于我们在两个特征维度上训练了感知器分类器,我们需要展平网格数组并创建一个与鸢尾花训练子集相同列数的矩阵,以便我们可以使用predict方法来预测相应网格点的类标签lab。
将预测的类标签lab重新整形为与xx1和xx2相同维度的网格后,我们现在可以通过 Matplotlib 的contourf函数绘制等高线图,该函数将不同决策区域映射到网格数组中每个预测类的不同颜色:
>>> plot_decision_regions(X, y, classifier=ppn)
>>> plt.xlabel('Sepal length [cm]')
>>> plt.ylabel('Petal length [cm]')
>>> plt.legend(loc='upper left')
>>> plt.show()
执行上述代码示例后,我们现在应该能看到一个决策区域的绘图,如图 2.8所示:
图 2.8:感知器决策区域的绘图
如图中所示,感知器学习了一个能够完美分类鸢尾花训练子集中所有样本的决策边界。
感知器收敛性
尽管感知器完美地分类了两类鸢尾花,但收敛是感知器的最大问题之一。罗森布拉特在数学上证明了,如果两个类可以通过线性超平面分开,感知器学习规则将收敛。然而,如果这些类不能通过线性决策边界完美分开,权重将永远不会停止更新,除非我们设置最大迭代次数。有兴趣的读者可以在我的讲义中找到这个证明的摘要,链接在sebastianraschka.com/pdf/lecture-notes/stat453ss21/L03_perceptron_slides.pdf。
自适应线性神经元和学习的收敛
在本节中,我们将介绍另一种单层神经网络(NN):自适应线性神经元(Adaline)。 Adaline 由伯纳德·维德罗和他的博士生泰德·霍夫在罗森布拉特感知器算法几年后发布,可以看作是后者的改进(An Adaptive “Adaline” Neuron Using Chemical “Memistors”, Technical Report Number 1553-2 by B. Widrow and colleagues, Stanford Electron Labs, Stanford, CA, October 1960)。
Adaline 算法特别有趣,因为它展示了定义和最小化连续损失函数的关键概念。这为理解其他用于分类的机器学习算法奠定了基础,如逻辑回归、支持向量机和多层神经网络,以及线性回归模型,我们将在后续章节中讨论。
Adaline 规则(也称为Widrow-Hoff 规则)与 Rosenblatt 的感知器之间的关键区别在于权重的更新是基于线性激活函数而不是感知器中的单位阶跃函数。在 Adaline 中,这个线性激活函数 简单地是净输入的恒等函数,因此
。
尽管线性激活函数用于学习权重,但我们仍然使用阈值函数来进行最终预测,这类似于我们之前讨论过的单位阶跃函数。
感知器和 Adaline 算法之间的主要区别在 图 2.9 中被突出显示:
Figure 2.9: 感知器和 Adaline 算法的比较
如 图 2.9 所示,Adaline 算法将真实类标签与线性激活函数的连续值输出进行比较以计算模型误差并更新权重。相比之下,感知器将真实类标签与预测类标签进行比较。
用梯度下降最小化损失函数
监督机器学习算法的一个关键组成部分是一个定义好的目标函数,在学习过程中要进行优化。这个目标函数通常是我们想要最小化的损失或成本函数。在 Adaline 的情况下,我们可以定义损失函数 L 为模型参数学习的均方误差(MSE),即计算结果与真实类标签之间的平均平方误差:
这个术语 只是为了方便起见,并且会使得推导损失函数对权重参数的梯度更加容易,正如我们将在下面的段落中看到的那样。这种连续的线性激活函数与单位阶跃函数相比的主要优势在于损失函数的可微性。这个损失函数的另一个好处是它是凸的;因此,我们可以使用一个非常简单但功能强大的优化算法,称为梯度下降,来找到最小化我们的损失函数以对 Iris 数据集中的示例进行分类的权重。
如 图 2.10 所示,我们可以将梯度下降的主要思想描述为在达到局部或全局损失最小值之前 向下爬山。在每次迭代中,我们沿着梯度的反方向迈出一步,步长由学习速率的值以及梯度的斜率决定(为简单起见,以下图仅为单个权重 w 可视化此过程):
Figure 2.10: 梯度下降的工作原理
使用梯度下降,我们现在可以通过沿着损失函数 L 的梯度 的反方向来更新模型参数 w 和 b:
参数变化, 和
,被定义为负梯度乘以学习率
:
要计算损失函数的梯度,我们需要计算损失函数对每个权重 w[j] 的偏导数:
类似地,我们计算损失对偏置的偏导数为:
请注意,分子中的 2 仅仅是一个常数缩放因子,我们可以省略它而不影响算法。去除缩放因子的效果与将学习率乘以 2 相同。以下信息框解释了这个缩放因子的来源。
因此,我们可以将权重更新写为:
由于我们同时更新所有参数,我们的 Adaline 学习规则变为:
均方误差导数
如果您熟悉微积分,可以得到 MSE 损失函数对第j个权重的偏导数如下:
同样的方法可以用来找到部分导数 ,除了
等于 -1,因此最后一步简化为
。
尽管 Adaline 学习规则看起来与感知器规则相同,我们应该注意 与
是一个实数,而不是整数类标签。此外,权重更新是基于训练数据集中的所有示例计算的(而不是在每个训练示例之后逐步更新参数),这也是为什么这种方法被称为 批量梯度下降。为了更加明确,并且在本章和本书后续讨论相关概念时避免混淆,我们将这个过程称为 全批量梯度下降。
在 Python 中实现 Adaline
由于感知器规则和 Adaline 非常相似,我们将采用先前定义的感知器实现,并更改fit方法,以便通过梯度下降来最小化损失函数更新权重和偏置参数:
class AdalineGD:
"""ADAptive LInear NEuron classifier.
Parameters
------------
eta : float
Learning rate (between 0.0 and 1.0)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
b_ : Scalar
Bias unit after fitting.
losses_ : list
Mean squared error loss function values in each epoch.
"""
def __init__(self, eta=0.01, n_iter=50, random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self, X, y):
""" Fit training data.
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples
is the number of examples and
n_features is the number of features.
y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0.0, scale=0.01,
size=X.shape[1])
self.b_ = np.float_(0.)
self.losses_ = []
for i in range(self.n_iter):
net_input = self.net_input(X)
output = self.activation(net_input)
errors = (y - output)
self.w_ += self.eta * 2.0 * X.T.dot(errors) / X.shape[0]
self.b_ += self.eta * 2.0 * errors.mean()
loss = (errors**2).mean()
self.losses_.append(loss)
return self
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_) + self.b_
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X))
>= 0.5, 1, 0)
与感知器不同,我们不是在评估每个单独的训练样例后更新权重,而是基于整个训练数据集计算梯度。对于偏置单元,这是通过self.eta * 2.0 * errors.mean()完成的,其中errors是包含偏导数值的数组。类似地,我们更新权重。然而,请注意,通过偏导数
更新权重涉及特征值x[j],我们可以通过将
errors与每个权重的每个特征值相乘来计算它们:
for w_j in range(self.w_.shape[0]):
self.w_[w_j] += self.eta *
(2.0 * (X[:, w_j]*errors)).mean()
为了更有效地实现权重更新而不使用for循环,我们可以在特征矩阵和误差向量之间进行矩阵-向量乘法:
self.w_ += self.eta * 2.0 * X.T.dot(errors) / X.shape[0]
请注意,activation方法对代码没有影响,因为它仅仅是一个恒等函数。在这里,我们添加了激活函数(通过activation方法计算得到)来说明信息如何通过单层神经网络传播的一般概念:来自输入数据的特征,净输入,激活和输出。
在下一章中,我们将学习关于逻辑回归分类器的内容,它使用了非恒等、非线性的激活函数。我们将看到逻辑回归模型与 Adaline 密切相关,唯一的区别在于它们的激活和损失函数。
现在,类似于之前的感知器实现,我们将损失值收集在self.losses_列表中,以检查算法在训练后是否收敛。
矩阵乘法
执行矩阵乘法类似于计算向量点积,其中矩阵中的每一行被视为单行向量。这种向量化方法代表了更紧凑的表示法,并且利用 NumPy 进行更有效的计算。例如:
请注意,在前述方程中,我们正在将一个矩阵与一个向量相乘,从数学上讲这是没有定义的。然而,请记住,我们使用的惯例是将此前述向量视为一个 3×1 矩阵。
在实践中,通常需要一些实验来找到一个良好的学习率,,以实现最佳收敛。因此,让我们选择两个不同的学习率,
和
,开始,并绘制损失函数与迭代次数的图表,以查看 Adaline 实现从训练数据中学习的效果。
超参数
学习率,(
eta),以及迭代次数(n_iter),是感知器和 Adaline 学习算法的所谓超参数(或调参参数)。在第六章,学习模型评估和超参数调优的最佳实践中,我们将看到不同的技术来自动找到不同超参数值,以获得分类模型的最佳性能。
现在让我们绘制两种不同学习率下的损失随着迭代次数的变化图表:
>>> fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))
>>> ada1 = AdalineGD(n_iter=15, eta=0.1).fit(X, y)
>>> ax[0].plot(range(1, len(ada1.losses_) + 1),
... np.log10(ada1.losses_), marker='o')
>>> ax[0].set_xlabel('Epochs')
>>> ax[0].set_ylabel('log(Mean squared error)')
>>> ax[0].set_title('Adaline - Learning rate 0.1')
>>> ada2 = AdalineGD(n_iter=15, eta=0.0001).fit(X, y)
>>> ax[1].plot(range(1, len(ada2.losses_) + 1),
... ada2.losses_, marker='o')
>>> ax[1].set_xlabel('Epochs')
>>> ax[1].set_ylabel('Mean squared error')
>>> ax[1].set_title('Adaline - Learning rate 0.0001')
>>> plt.show()
正如我们在得到的损失函数图中所看到的,我们遇到了两种不同类型的问题。左图显示了如果选择的学习率过大可能会发生的情况。在每个 epoch 中,而不是最小化损失函数,MSE 都会变大,因为我们超调了全局最小值。另一方面,我们可以看到右图中损失在减少,但是所选的学习率 太小,算法需要非常多的 epoch 才能收敛到全局损失最小值:
图 2.11: 子优化学习率下的误差图
图 2.12 描述了如果我们改变某个权重参数的值以最小化损失函数 L,可能会发生的情况。左侧的子图说明了选择得当的学习率的情况,损失逐渐减小,朝着全局最小值的方向移动。
右侧的子图说明了,如果我们选择一个过大的学习率,会发生什么情况——我们会超调全局最小值:
图 2.12: 选用适当学习率与学习率过大的比较
通过特征缩放改进梯度下降
在本书中我们会遇到许多需要进行某种形式特征缩放以获得最佳性能的机器学习算法,在第三章,使用 Scikit-Learn 的机器学习分类器概览,和第四章,构建良好的训练数据集——数据预处理中会详细讨论这一点。
梯度下降是许多算法之一,受益于特征缩放。在本节中,我们将使用一种称为标准化的特征缩放方法。这种标准化过程有助于梯度下降学习更快地收敛;然而,并不使原始数据集成为正态分布。标准化将每个特征的均值平移到零,并使每个特征具有标准偏差为 1(单位方差)。例如,要对第 j 个特征进行标准化,我们可以简单地从每个训练样本中减去样本均值 ,并将其除以其标准差
:
在这里,x[j] 是由所有训练样本 n 的第 j 个特征值组成的向量,这种标准化技术应用于数据集中的每个特征 j。
标准化有助于梯度下降学习的一个原因是,更容易找到适合所有权重(和偏差)的学习率。如果特征在非常不同的尺度上,那么对一个权重进行更新有效的学习率可能对另一个权重来说太大或太小。总体而言,使用标准化特征可以稳定训练过程,使优化器无需经过更多步骤就能找到良好或最优解(全局损失最小值)。图 2.13 展示了未经标准化特征(左侧)和经过标准化特征(右侧)可能的梯度更新情况,其中同心圆代表了在二维分类问题中两个模型权重的损失表面函数:
图 2.13:未经标准化和标准化特征上的梯度更新比较
可以通过使用内置的 NumPy 方法 mean 和 std 轻松实现标准化:
>>> X_std = np.copy(X)
>>> X_std[:,0] = (X[:,0] - X[:,0].mean()) / X[:,0].std()
>>> X_std[:,1] = (X[:,1] - X[:,1].mean()) / X[:,1].std()
经过标准化后,我们将再次训练 Adaline,并看到它现在在使用学习率为 的少量周期后收敛:
>>> ada_gd = AdalineGD(n_iter=20, eta=0.5)
>>> ada_gd.fit(X_std, y)
>>> plot_decision_regions(X_std, y, classifier=ada_gd)
>>> plt.title('Adaline - Gradient descent')
>>> plt.xlabel('Sepal length [standardized]')
>>> plt.ylabel('Petal length [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
>>> plt.plot(range(1, len(ada_gd.losses_) + 1),
... ada_gd.losses_, marker='o')
>>> plt.xlabel('Epochs')
>>> plt.ylabel('Mean squared error')
>>> plt.tight_layout()
>>> plt.show()
执行此代码后,我们应该看到决策区域的图以及损失下降的图,如 图 2.14 所示:
图 2.14:Adaline 的决策区域和随着周期数变化的 MSE 的绘图
正如图中所示,Adaline 现在在训练标准化特征后已经收敛。但请注意,即使所有花的例子都被正确分类,MSE 仍保持非零。
大规模机器学习和随机梯度下降
在前一节中,我们学习了通过沿着从整个训练数据集计算出的损失梯度的反方向迈出步伐来最小化损失函数;这也是为什么有时这种方法被称为全批量梯度下降。现在想象一下,我们有一个包含数百万数据点的非常大的数据集,在许多机器学习应用中这并不罕见。在这种情况下,运行全批量梯度下降可能在计算上非常昂贵,因为每次朝全局最小值迈出一步时,我们都需要重新评估整个训练数据集。
批量梯度下降算法的一种流行替代方案是随机梯度下降(SGD),有时也称为迭代或在线梯度下降。而不是基于所有训练样本上累积误差的和来更新权重,x^(^i^):
我们为每个训练样本逐步更新参数,例如:
虽然 SGD 可以被看作是梯度下降的一种近似,但它通常会因为更频繁的权重更新而更快地达到收敛。由于每个梯度是基于单个训练样本计算的,误差曲面比梯度下降中的要嘈杂,这也可以作为一个优势,即如果我们使用非线性损失函数,SGD 更容易逃离浅层局部最小值,正如我们稍后在 第十一章 从零开始实现多层人工神经网络 中将看到的。通过 SGD 获得令人满意的结果,重要的是要以随机顺序呈现训练数据;此外,我们希望在每个 epoch 中对训练数据集进行洗牌,以防止循环。
调整训练中的学习率
在 SGD 的实现中,固定的学习率,,通常会被随着时间推移逐渐减小的自适应学习率所取代,例如:
其中 c[1] 和 c[2] 是常数。注意,SGD 并不会达到全局损失最小值,而是非常接近它的某个区域。通过使用自适应学习率,我们可以进一步接近损失最小值。
SGD 的另一个优势是我们可以用它进行在线学习。在在线学习中,我们的模型会随着新的训练数据的到来而实时训练。如果我们积累了大量的数据,比如 Web 应用中的客户数据,这尤其有用。利用在线学习,系统可以立即适应变化,并且如果存储空间有限,更新模型后可以丢弃训练数据。
小批量梯度下降
在全批量梯度下降和 SGD 之间的一种折中方案是所谓的小批量梯度下降。小批量梯度下降可以理解为将全批量梯度下降应用于训练数据的较小子集,例如每次 32 个训练样本。与全批量梯度下降相比,通过小批量可以更快地达到收敛,因为权重更新更频繁。此外,小批量学习允许我们将在 SGD 中遍历训练样本的 for 循环替换为利用线性代数概念的向量化操作(例如通过点积实现加权和),这可以进一步提高学习算法的计算效率。
由于我们已经使用梯度下降实现了 Adaline 学习规则,我们只需进行一些调整即可修改学习算法以通过 SGD 更新权重。在 fit 方法内部,我们现在会在每个训练示例后更新权重。此外,我们将实现一个额外的 partial_fit 方法,该方法不会重新初始化权重,用于在线学习。为了在训练后检查算法是否收敛,我们将计算每个 epoch 中训练示例的平均损失。此外,我们将添加一个选项,在每个 epoch 前对训练数据进行洗牌,以避免优化损失函数时出现重复循环;通过 random_state 参数,我们允许指定一个随机种子以实现可重现性。
class AdalineSGD:
"""ADAptive LInear NEuron classifier.
Parameters
------------
eta : float
Learning rate (between 0.0 and 1.0)
n_iter : int
Passes over the training dataset.
shuffle : bool (default: True)
Shuffles training data every epoch if True to prevent
cycles.
random_state : int
Random number generator seed for random weight
initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
b_ : Scalar
Bias unit after fitting.
losses_ : list
Mean squared error loss function value averaged over all
training examples in each epoch.
"""
def __init__(self, eta=0.01, n_iter=10,
shuffle=True, random_state=None):
self.eta = eta
self.n_iter = n_iter
self.w_initialized = False
self.shuffle = shuffle
self.random_state = random_state
def fit(self, X, y):
""" Fit training data.
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of
examples and n_features is the number of features.
y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : object
"""
self._initialize_weights(X.shape[1])
self.losses_ = []
for i in range(self.n_iter):
if self.shuffle:
X, y = self._shuffle(X, y)
losses = []
for xi, target in zip(X, y):
losses.append(self._update_weights(xi, target))
avg_loss = np.mean(losses)
self.losses_.append(avg_loss)
return self
def partial_fit(self, X, y):
"""Fit training data without reinitializing the weights"""
if not self.w_initialized:
self._initialize_weights(X.shape[1])
if y.ravel().shape[0] > 1:
for xi, target in zip(X, y):
self._update_weights(xi, target)
else:
self._update_weights(X, y)
return self
def _shuffle(self, X, y):
"""Shuffle training data"""
r = self.rgen.permutation(len(y))
return X[r], y[r]
def _initialize_weights(self, m):
"""Initialize weights to small random numbers"""
self.rgen = np.random.RandomState(self.random_state)
self.w_ = self.rgen.normal(loc=0.0, scale=0.01,
size=m)
self.b_ = np.float_(0.)
self.w_initialized = True
def _update_weights(self, xi, target):
"""Apply Adaline learning rule to update the weights"""
output = self.activation(self.net_input(xi))
error = (target - output)
self.w_ += self.eta * 2.0 * xi * (error)
self.b_ += self.eta * 2.0 * error
loss = error**2
return loss
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_) + self.b_
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X))
>= 0.5, 1, 0)
我们现在在 AdalineSGD 分类器中使用的 _shuffle 方法工作原理如下:通过 np.random 中的 permutation 函数,我们生成一个在 0 到 100 范围内的唯一数字的随机序列。这些数字可以用作我们特征矩阵和类标签向量的索引,以对它们进行洗牌。
我们可以使用 fit 方法来训练 AdalineSGD 分类器,并使用我们的 plot_decision_regions 来绘制我们的训练结果:
>>> ada_sgd = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
>>> ada_sgd.fit(X_std, y)
>>> plot_decision_regions(X_std, y, classifier=ada_sgd)
>>> plt.title('Adaline - Stochastic gradient descent')
>>> plt.xlabel('Sepal length [standardized]')
>>> plt.ylabel('Petal length [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
>>> plt.plot(range(1, len(ada_sgd.losses_) + 1), ada_sgd.losses_,
... marker='o')
>>> plt.xlabel('Epochs')
>>> plt.ylabel('Average loss')
>>> plt.tight_layout()
>>> plt.show()
我们从执行前述代码示例中获得的两个图表如 图 2.15 所示:
图 2.15:使用 SGD 训练 Adaline 模型后的决策区域和平均损失图
正如您所见,平均损失迅速下降,经过 15 个 epoch 后的最终决策边界看起来类似于批量梯度下降的 Adaline。如果我们想要在流式数据的在线学习场景中更新我们的模型,我们可以简单地对单个训练示例调用 partial_fit 方法,例如 ada_sgd.partial_fit(X_std[0, :], y[0])。
总结
在本章中,我们对监督学习的线性分类器的基本概念有了很好的理解。在实现了感知器之后,我们看到了如何通过梯度下降的向量化实现和通过 SGD 的在线学习高效地训练自适应线性神经元。
现在我们已经学会了如何在 Python 中实现简单的分类器,我们准备进入下一章,在那里我们将使用 Python 的 scikit-learn 机器学习库来获取更先进和强大的机器学习分类器,这些分类器在学术界和工业界都广泛使用。
我们在实现感知器和 Adaline 算法时采用的面向对象方法将有助于理解基于相同核心概念实现的 scikit-learn API,这些概念也是本章节的基础:fit 和 predict 方法。基于这些核心概念,我们将学习用于建模类概率的逻辑回归,以及用于处理非线性决策边界的支持向量机。此外,我们还将介绍一类不同的监督学习算法,即基于树的算法,它们通常组合成强大的集成分类器。
加入我们书籍的 Discord 空间
加入书籍的 Discord 工作空间,与作者进行每月的 问答 会话:
第三章:使用 Scikit-Learn 进行机器学习分类器的介绍
在本章中,我们将介绍一些在学术界和工业界常用的流行和强大的机器学习算法。在学习了解几种用于分类的监督学习算法之间的差异时,我们还将理解它们各自的优缺点。此外,我们将首次使用 Scikit-Learn 库,该库提供了一个用户友好且一致的接口,有效和高效地使用这些算法。
本章节将涵盖以下主题:
-
介绍了用于分类的稳健和流行算法,如逻辑回归、支持向量机、决策树和k最近邻算法。
-
使用 Scikit-Learn 机器学习库的示例和解释,该库通过用户友好的 Python API 提供了各种机器学习算法。
-
探讨了具有线性和非线性决策边界分类器的优缺点。
选择分类算法
为特定问题任务选择适当的分类算法需要实践和经验;每种算法都有其独特之处,并基于某些假设。用 David H. Wolpert 的没有免费午餐定理的话来说,没有单一的分类器适用于所有可能的场景(The Lack of A Priori Distinctions Between Learning Algorithms, Wolpert, David H, Neural Computation 8.7 (1996): 1341-1390)。实际上,建议您始终比较至少几种不同学习算法的性能,以选择最适合特定问题的模型;这些算法可能在特征或示例数量、数据集中的噪声量以及类别是否线性可分方面有所不同。
最终,分类器的性能——包括计算性能和预测能力——在很大程度上取决于可用于学习的基础数据。训练监督机器学习算法所涉及的五个主要步骤可以总结如下:
-
选择特征和收集带标签的训练样本
-
选择性能度量标准
-
选择学习算法并训练模型
-
评估模型的性能
-
更改算法的设置并调整模型。
由于本书的方法是逐步建立机器学习知识,因此我们将主要关注本章节中不同算法的主要概念,并在本书的后期重新讨论诸如特征选择和预处理、性能指标以及超参数调整等主题,进行更详细的讨论。
使用 scikit-learn 的第一步——训练感知器
在 第二章,训练简单的机器学习算法进行分类 中,您学习了两种相关的分类学习算法,即感知器规则和Adaline,我们自己用 Python 和 NumPy 实现了这些算法。现在我们将看看 scikit-learn API,正如前文所述,它结合了用户友好和一致的界面与几种分类算法的高度优化实现。scikit-learn 库不仅提供多种学习算法,还提供许多方便的函数来预处理数据、微调和评估我们的模型。我们将在 第四章,构建良好的训练数据集 - 数据预处理 和 第五章,通过降维压缩数据 中更详细地讨论这些内容及其概念。
为了开始使用 scikit-learn 库,我们将训练一个与 第二章 中实现的感知器模型类似的模型。为简单起见,我们将在以下各节中一直使用已经熟悉的鸢尾花数据集。方便的是,鸢尾花数据集已经通过 scikit-learn 提供,因为它是一个简单而受欢迎的数据集,经常用于测试和实验算法。与前一章节类似,我们将仅使用鸢尾花数据集的两个特征进行可视化目的。
我们将把这 150 个花例子的花瓣长度和花瓣宽度分配给特征矩阵 X,并将相应的花种类的类标签分配给向量数组 y:
>>> from sklearn import datasets
>>> import numpy as np
>>> iris = datasets.load_iris()
>>> X = iris.data[:, [2, 3]]
>>> y = iris.target
>>> print('Class labels:', np.unique(y))
Class labels: [0 1 2]
函数 np.unique(y) 返回了存储在 iris.target 中的三个唯一类标签,正如我们所看到的,鸢尾花的类名 Iris-setosa、Iris-versicolor 和 Iris-virginica 已经存储为整数(这里是 0、1、2)。虽然许多 scikit-learn 函数和类方法也可以处理字符串格式的类标签,但使用整数标签是一种推荐的方法,可以避免技术故障并提高计算性能,因为整数标签具有较小的内存占用;此外,将类标签编码为整数是大多数机器学习库的常规约定。
为了评估训练模型在未见数据上的表现如何,我们将进一步将数据集分割为单独的训练和测试数据集。在 第六章,学习模型评估和超参数调优的最佳实践 中,我们将更详细地讨论模型评估的最佳实践。使用 scikit-learn 的 model_selection 模块中的 train_test_split 函数,我们随机将 X 和 y 数组分割为 30% 的测试数据(45 个例子)和 70% 的训练数据(105 个例子):
>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, test_size=0.3, random_state=1, stratify=y
... )
请注意,train_test_split函数在分割之前已经内部对训练数据集进行了洗牌;否则,所有来自类0和类1的示例都将最终进入训练数据集,而测试数据集将由类2的 45 个示例组成。通过random_state参数,我们为内部伪随机数生成器提供了一个固定的随机种子(random_state=1),用于洗牌数据集之前的操作。使用这样一个固定的random_state可以确保我们的结果是可重现的。
最后,我们利用内置的支持通过stratify=y进行分层。在这个上下文中,分层意味着train_test_split方法返回的训练和测试子集具有与输入数据集相同的类标签比例。我们可以使用 NumPy 的bincount函数来验证这一点,该函数统计数组中每个值的出现次数:
>>> print('Labels counts in y:', np.bincount(y))
Labels counts in y: [50 50 50]
>>> print('Labels counts in y_train:', np.bincount(y_train))
Labels counts in y_train: [35 35 35]
>>> print('Labels counts in y_test:', np.bincount(y_test))
Labels counts in y_test: [15 15 15]
许多机器学习和优化算法也需要特征缩放以获得最佳性能,正如我们在第二章中的梯度下降示例中看到的那样。在这里,我们将使用 scikit-learn 的preprocessing模块中的StandardScaler类对特征进行标准化:
>>> from sklearn.preprocessing import StandardScaler
>>> sc = StandardScaler()
>>> sc.fit(X_train)
>>> X_train_std = sc.transform(X_train)
>>> X_test_std = sc.transform(X_test)
使用前述代码,我们从preprocessing模块加载了StandardScaler类,并初始化了一个新的StandardScaler对象,将其赋给了变量sc。使用fit方法,StandardScaler估计了每个特征维度的训练数据的参数,即样本均值 和标准差
。通过调用
transform方法,我们使用这些估计参数标准化了训练数据, 和
。请注意,我们使用相同的缩放参数来标准化测试数据集,以便训练数据集和测试数据集的值可以相互比较。
在标准化训练数据之后,我们现在可以训练感知器模型。scikit-learn 中的大多数算法默认支持多类分类,通过一对多(OvR)方法,我们可以一次将三个花类别的数据输入到感知器中。代码如下所示:
>>> from sklearn.linear_model import Perceptron
>>> ppn = Perceptron(eta0=0.1, random_state=1)
>>> ppn.fit(X_train_std, y_train)
scikit-learn接口会让你想起我们在第二章中实现的感知器。在从linear_model模块加载Perceptron类之后,我们初始化了一个新的Perceptron对象,并通过fit方法训练了模型。在这里,模型参数eta0相当于我们自己感知器实现中使用的学习率eta。
正如您在第二章中记得的那样,找到合适的学习率需要一些实验。如果学习率太大,算法将会超出全局损失最小值。如果学习率太小,算法将需要更多的周期直到收敛,这可能会使学习变慢,尤其是对于大型数据集。此外,我们使用了random_state参数来确保每个周期后对训练数据集的初始洗牌具有可重复性。
在 scikit-learn 中训练了一个模型后,我们可以通过predict方法进行预测,就像在第二章中我们自己的感知器实现中一样。代码如下:
>>> y_pred = ppn.predict(X_test_std)
>>> print('Misclassified examples: %d' % (y_test != y_pred).sum())
Misclassified examples: 1
执行代码,我们可以看到感知器在 45 个花示例中误分类了 1 个。因此,测试数据集上的误分类率约为 0.022,或 2.2% ()。
分类错误与准确率
许多机器学习实践者报告模型的分类准确率,而不是错误率,计算方法如下:
1–error = 0.978,即 97.8%
使用分类错误或准确率仅仅是一种偏好。
注意,scikit-learn 还实现了许多不同的性能指标,这些指标可以通过metrics模块获得。例如,我们可以如下计算感知器在测试数据集上的分类准确率:
>>> from sklearn.metrics import accuracy_score
>>> print('Accuracy: %.3f' % accuracy_score(y_test, y_pred))
Accuracy: 0.978
在这里,y_test是真实的类标签,y_pred是我们先前预测的类标签。另外,scikit-learn 中的每个分类器都有一个score方法,通过将predict调用与accuracy_score结合来计算分类器的预测准确率,如下所示:
>>> print('Accuracy: %.3f' % ppn.score(X_test_std, y_test))
Accuracy: 0.978
过拟合
请注意,在本章中,我们将根据测试数据集评估模型的性能。在第六章中,您将学习到一些有用的技术,包括图形分析,如学习曲线,以检测和预防过拟合。过拟合是指模型在训练数据中捕捉到模式,但在未见数据中无法很好泛化。
最后,我们可以使用第二章中的plot_decision_regions函数来绘制新训练的感知器模型的决策区域,并可视化它如何有效地分离不同的花示例。不过,让我们做一个小修改,通过小圆圈突出显示来自测试数据集的数据实例:
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
def plot_decision_regions(X, y, classifier, test_idx=None,
resolution=0.02):
# setup marker generator and color map
markers = ('o', 's', '^', 'v', '<')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# plot the decision surface
x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
np.arange(x2_min, x2_max, resolution))
lab = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
lab = lab.reshape(xx1.shape)
plt.contourf(xx1, xx2, lab, alpha=0.3, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
# plot class examples
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0],
y=X[y == cl, 1],
alpha=0.8,
c=colors[idx],
marker=markers[idx],
label=f'Class {cl}',
edgecolor='black')
# highlight test examples
if test_idx:
# plot all examples
X_test, y_test = X[test_idx, :], y[test_idx]
plt.scatter(X_test[:, 0], X_test[:, 1],
c='none', edgecolor='black', alpha=1.0,
linewidth=1, marker='o',
s=100, label='Test set')
通过对plot_decision_regions函数进行轻微修改,我们现在可以指定要在结果图中标记的示例的索引。代码如下:
>>> X_combined_std = np.vstack((X_train_std, X_test_std))
>>> y_combined = np.hstack((y_train, y_test))
>>> plot_decision_regions(X=X_combined_std,
... y=y_combined,
... classifier=ppn,
... test_idx=range(105, 150))
>>> plt.xlabel('Petal length [standardized]')
>>> plt.ylabel('Petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
正如我们在结果图中所看到的,三种花的类别不能完全通过线性决策边界分开:
图 3.1:适用于鸢尾花数据集的多类感知器模型的决策边界
然而,请记住我们在第二章中的讨论,感知器算法在数据集上永远不会收敛,除非数据集完全线性可分,这就是为什么实际上不建议在实践中使用感知器算法的原因。在接下来的章节中,我们将看看更强大的线性分类器,即使类别不是完全线性可分的,它们也会收敛到损失最小值。
额外的感知器设置
Perceptron以及其他 scikit-learn 函数和类通常具有我们为了清晰起见而省略的其他参数。您可以使用 Python 中的help函数(例如help(Perceptron))或通过查阅优秀的 scikit-learn 在线文档scikit-learn.org/stable/了解更多有关这些参数的信息。
通过逻辑回归对类别概率建模
尽管感知器规则为分类的机器学习算法提供了一个不错和简单的介绍,但它的最大缺点是,如果类别不是完全线性可分的,它永远不会收敛。前一节中的分类任务就是这种情况的一个例子。原因在于权重不断更新,因为每个时期至少存在一个误分类的训练示例。当然,您可以改变学习率并增加时期的数量,但请注意,感知器将永远不会在这个数据集上收敛。
为了更好地利用我们的时间,现在我们将看一看另一种简单但更强大的线性和二元分类问题的算法:逻辑回归。请注意,尽管名为逻辑回归,但逻辑回归是一种分类模型,而不是回归模型。
逻辑回归和条件概率
逻辑回归是一种非常容易实现并且在线性可分类中表现非常好的分类模型。它是工业界中最广泛使用的分类算法之一。与感知器和 Adaline 类似,本章中的逻辑回归模型也是二元分类的线性模型。
逻辑回归用于多类
逻辑回归可以方便地推广到多类设置中,这被称为多项逻辑回归或Softmax 回归。关于多项逻辑回归的更详细覆盖超出了本书的范围,但有兴趣的读者可以在我的讲义笔记中找到更多信息,网址为sebastianraschka.com/pdf/lecture-notes/stat453ss21/L08_logistic__slides.pdf或youtu.be/L0FU8NFpx4E。
在多类设置中使用逻辑回归的另一种方法是通过 OvR 技术,这是我们之前讨论过的。
要解释作为二元分类的概率模型的逻辑回归的主要机制,让我们首先介绍几率:支持特定事件的几率。几率可以写作,其中p代表积极事件的概率。术语“积极事件”并不一定意味着“好”,而是指我们要预测的事件,例如,患者在某些症状下患某种疾病的概率;我们可以将积极事件看作类标签y = 1,症状看作特征x。因此,简要地说,我们可以定义概率p为p(y = 1|x),即给定其特征x的特定示例属于某个类 1 的条件概率。
接着我们可以进一步定义logit函数,它就是对数几率的对数(log-odds):
注意log表示自然对数,这是计算机科学中的常规约定。logit函数接受范围为 0 到 1 的输入值,并将它们转换为整个实数范围的值。
在逻辑模型下,我们假设加权输入(在第二章中称为净输入)与对数几率之间存在线性关系:
尽管前面描述了我们对于对数几率与净输入之间的线性关系的假设,我们实际上感兴趣的是概率p,即给定其特征的示例的类成员概率。虽然 logit 函数将概率映射到实数范围,我们可以考虑该函数的反函数将实数范围映射回[0, 1]范围的概率p。
这个 logit 函数的反函数通常称为逻辑 Sigmoid 函数,由于其典型的 S 形状,有时简称为Sigmoid 函数:
这里,z是净输入,即权重和输入(即与训练示例相关联的特征)的线性组合:
z = w^Tx + b
现在,让我们简单地绘制 Sigmoid 函数在范围–7 到 7 之间的一些值,以查看其外观:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> def sigmoid(z):
... return 1.0 / (1.0 + np.exp(-z))
>>> z = np.arange(-7, 7, 0.1)
>>> sigma_z = sigmoid(z)
>>> plt.plot(z, sigma_z)
>>> plt.axvline(0.0, color='k')
>>> plt.ylim(-0.1, 1.1)
>>> plt.xlabel('z')
>>> plt.ylabel('$\sigma (z)$')
>>> # y axis ticks and gridline
>>> plt.yticks([0.0, 0.5, 1.0])
>>> ax = plt.gca()
>>> ax.yaxis.grid(True)
>>> plt.tight_layout()
>>> plt.show()
执行前面的代码示例后,我们现在应该看到 S 形(sigmoid)曲线:
图 3.2:逻辑 Sigmoid 函数的绘图
我们可以看到,如果z趋向于无穷大(z→∞),接近 1,因为e^–^z 在z较大时变得非常小。类似地,如果z趋向于负无穷大(z→–∞),
趋向于 0,这是因为分母变得越来越大。因此,我们可以得出结论,这个 Sigmoid 函数接受实数值作为输入,并将它们转换为[0, 1]范围内的值,并在
处截距。
要对逻辑回归模型建立一些理解,我们可以将其与第二章相关联。在 Adaline 中,我们使用了恒等函数,,作为激活函数。在逻辑回归中,这个激活函数简单地变成了我们之前定义的 sigmoid 函数。
Adaline 与逻辑回归之间的区别在下图中有所说明,唯一的区别在于激活函数:
图 3.3:逻辑回归与 Adaline 比较
然后,通过 sigmoid 函数的输出解释为特定示例属于类 1 的概率,,给定其特征,x,并由权重和偏置参数化,w 和 b。例如,如果我们计算一个特定花示例的
,这意味着这个示例是
Iris-versicolor花的概率为 80%。因此,这种花是Iris-setosa花的概率可以计算为 p(y = 0|x; w, b) = 1 – p(y = 1|x; w, b) = 0.2,或者 20%。
预测的概率可以简单地通过阈值函数转换为二进制结果:
如果我们看看 sigmoid 函数的前面的图,这等同于以下内容:
实际上,有许多应用场景不仅关注预测的类标签,而是特别关注预测类成员概率的估计(应用阈值函数之前的 sigmoid 函数输出)。例如,逻辑回归在天气预报中使用,不仅可以预测某一天是否会下雨,还可以报告降雨的可能性。同样地,逻辑回归可以用于根据某些症状预测患者患某种疾病的概率,这就是为什么逻辑回归在医学领域中非常流行的原因。
通过逻辑损失函数学习模型权重
您已经了解了如何使用逻辑回归模型预测概率和类标签;现在,让我们简要讨论如何拟合模型的参数,例如权重和偏置单元,w 和 b。在前一章中,我们定义了均方误差损失函数如下:
我们最小化这个函数,以便学习我们的 Adaline 分类模型的参数。为了解释我们如何推导逻辑回归的损失函数,让我们首先定义可能性,,即当我们构建逻辑回归模型时要最大化的可能性,假设数据集中的个体示例是彼此独立的。该公式如下:
在实践中,最大化(自然)对数化简化了这个方程,这被称为对数似然函数:
首先,应用对数函数减少了数值下溢的可能性,这种情况可能发生在似然值非常小的情况下。其次,我们可以将因子的乘积转换为因子的总和,这样可以更容易地通过加法技巧获取此函数的导数,正如你可能从微积分中记得的那样。
推导似然函数
我们可以得到给定数据的模型似然性表达式,,如下所示。考虑到我们有一个二元分类问题,类标签为 0 和 1,我们可以将标签 1 视为伯努利变量——它可以取两个值,0 和 1,概率为 p:
。对于单个数据点,我们可以将这个概率写为
和
。
将这两个表达式放在一起,并使用简写 ,我们得到了伯努利变量的概率质量函数:
我们可以写出训练标签的似然性,假设所有训练示例是独立的,使用乘法规则计算所有事件发生的概率,如下所示:
现在,代入伯努利变量的概率质量函数,我们得到似然性的表达式,通过改变模型参数来最大化:
现在,我们可以使用梯度上升等优化算法来最大化这个对数似然函数。(梯度上升的工作方式与第二章中解释的梯度下降完全相同,只是梯度上升是最大化一个函数,而不是最小化它。)或者,让我们将对数似然重写为可以使用梯度下降最小化的损失函数 L,如第二章中所述:
为了更好地理解这个损失函数,让我们看看我们为单个训练样本计算的损失:
查看方程式,我们可以看到如果 y = 0,第一项变为零,如果 y = 1,第二项变为零:
:
>>> def loss_1(z):
... return - np.log(sigmoid(z))
>>> def loss_0(z):
... return - np.log(1 - sigmoid(z))
>>> z = np.arange(-10, 10, 0.1)
>>> sigma_z = sigmoid(z)
>>> c1 = [loss_1(x) for x in z]
>>> plt.plot(sigma_z, c1, label='L(w, b) if y=1')
>>> c0 = [loss_0(x) for x in z]
>>> plt.plot(sigma_z, c0, linestyle='--', label='L(w, b) if y=0')
>>> plt.ylim(0.0, 5.1)
>>> plt.xlim([0, 1])
>>> plt.xlabel('$\sigma(z)$')
>>> plt.ylabel('L(w, b)')
>>> plt.legend(loc='best')
>>> plt.tight_layout()
>>> plt.show()
结果绘制显示了在 x 轴上 0 到 1 范围内的 sigmoid 激活(sigmoid 函数的输入为范围在 –10 到 10 的 z 值),以及与之相关联的逻辑损失在 y 轴上:
图 3.4:逻辑回归中使用的损失函数图
我们可以看到,如果我们正确预测一个示例属于类别 1(连续线),损失接近于 0。类似地,我们可以看到在 y 轴上,如果我们正确预测 y = 0(虚线),损失也接近于 0。然而,如果预测错误,损失将趋向于无穷大。主要问题是我们通过越来越大的损失来惩罚错误的预测。
将 Adaline 实现转换为逻辑回归算法
如果我们要自己实现逻辑回归,我们可以简单地在 第二章 中的 Adaline 实现中用新的损失函数 L 替换原有的损失函数:
我们使用这个来计算每个时期分类所有训练示例的损失。此外,我们需要用 sigmoid 函数替换线性激活函数。如果我们对 Adaline 代码进行这些更改,我们将得到一个可工作的逻辑回归实现。以下是完全批量梯度下降的实现(但请注意,同样的更改也可以应用于随机梯度下降版本):
class LogisticRegressionGD:
"""Gradient descent-based logistic regression classifier.
Parameters
------------
eta : float
Learning rate (between 0.0 and 1.0)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight
initialization.
Attributes
-----------
w_ : 1d-array
Weights after training.
b_ : Scalar
Bias unit after fitting.
losses_ : list
Mean squared error loss function values in each epoch.
"""
def __init__(self, eta=0.01, n_iter=50, random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self, X, y):
""" Fit training data.
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the
number of examples and n_features is the
number of features.
y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : Instance of LogisticRegressionGD
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0.0, scale=0.01, size=X.shape[1])
self.b_ = np.float_(0.)
self.losses_ = []
for i in range(self.n_iter):
net_input = self.net_input(X)
output = self.activation(net_input)
errors = (y - output)
self.w_ += self.eta * 2.0 * X.T.dot(errors) / X.shape[0]
self.b_ += self.eta * 2.0 * errors.mean()
loss = (-y.dot(np.log(output))
- ((1 - y).dot(np.log(1 - output)))
/ X.shape[0])
self.losses_.append(loss)
return self
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_) + self.b_
def activation(self, z):
"""Compute logistic sigmoid activation"""
return 1\. / (1\. + np.exp(-np.clip(z, -250, 250)))
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X)) >= 0.5, 1, 0)
当我们拟合逻辑回归模型时,我们必须记住它仅适用于二元分类任务。
因此,让我们仅考虑山鸢尾和变色鸢尾花(类别 0 和 1),并检查我们的逻辑回归实现是否有效:
>>> X_train_01_subset = X_train_std[(y_train == 0) | (y_train == 1)]
>>> y_train_01_subset = y_train[(y_train == 0) | (y_train == 1)]
>>> lrgd = LogisticRegressionGD(eta=0.3,
... n_iter=1000,
... random_state=1)
>>> lrgd.fit(X_train_01_subset,
... y_train_01_subset)
>>> plot_decision_regions(X=X_train_01_subset,
... y=y_train_01_subset,
... classifier=lrgd)
>>> plt.xlabel('Petal length [standardized]')
>>> plt.ylabel('Petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
所得的决策区域图如下所示:
图 3.5:逻辑回归模型的决策区域图
逻辑回归的梯度下降学习算法
如果您将上一章中的 LogisticRegressionGD 与第二章中的 AdalineGD 代码进行比较,您可能会注意到权重和偏置更新规则保持不变(除了缩放因子 2)。使用微积分,我们可以证明梯度下降通过逻辑回归和 Adaline 更新参数的确是相似的。但请注意,下面导出的梯度下降学习规则的推导是为了对数回归梯度下降学习规则背后的数学概念感兴趣的读者。对于本章其余部分的跟进,这并非必需。
图 3.6 总结了我们如何计算对第 j 个权重的对数似然函数的偏导数:
图 3.6:计算对数似然函数的偏导数
注意,为简洁起见,我们省略了对训练示例的平均值。
请记住,从 第二章 我们知道,我们在梯度相反的方向上采取步骤。因此,我们反转 并更新第 j 个权重如下,包括学习率
:
虽然未显示损失函数对偏置单位的偏导数,但是偏置导数遵循相同的链式法则概念,导致以下更新规则:
权重和偏置单位的更新与 第二章 中 Adaline 的更新相同。
使用 scikit-learn 训练 logistic 回归模型
在前一小节中,我们刚刚完成了有用的编码和数学练习,这有助于说明 Adaline 和 logistic 回归之间的概念差异。 现在,让我们学习如何使用 scikit-learn 更优化的 logistic 回归实现,它还支持即插即用的多类设置。 请注意,在 scikit-learn 的最新版本中,用于多类分类的技术,即 multinomial 或 OvR,是自动选择的。 在以下代码示例中,我们将使用 sklearn.linear_model.LogisticRegression 类以及熟悉的 fit 方法在标准化的花卉训练数据集中训练模型的所有三个类。 另外,我们设置 multi_class='ovr' 以进行说明目的。 作为读者的练习,您可能希望将结果与 multi_class='multinomial' 进行比较。 请注意,multinomial 设置现在是 scikit-learn 的 LogisticRegression 类的默认选择,并且在实践中推荐用于互斥类,例如在 Iris 数据集中找到的类。 在这里,“互斥” 意味着每个训练示例只能属于一个单一类(与多标签分类相对,其中训练示例可以是多个类的成员)。
现在,让我们看看代码示例:
>>> from sklearn.linear_model import LogisticRegression
>>> lr = LogisticRegression(C=100.0, solver='lbfgs',
... multi_class='ovr')
>>> lr.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined,
... classifier=lr,
... test_idx=range(105, 150))
>>> plt.xlabel('Petal length [standardized]')
>>> plt.ylabel('Petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
在对训练数据拟合模型之后,我们绘制了决策区域、训练示例和测试示例,如 图 3.7 所示:
图 3.7:scikit-learn 的多类 logistic 回归模型的决策区域
凸优化算法
请注意,存在许多不同的算法用于解决优化问题。 对于最小化凸损失函数(例如 logistic 回归损失),建议使用比常规的 随机梯度下降 (SGD) 更高级的方法。 实际上,scikit-learn 实现了一整套这样的优化算法,可以通过 solver 参数指定,即 'newton-cg'、'lbfgs'、'liblinear'、'sag' 和 'saga'。
尽管逻辑回归损失是凸的,大多数优化算法应该可以轻松收敛到全局损失最小值。然而,使用一种算法而不是另一种算法有一定的优势。例如,在之前的版本(例如 v 0.21)中,scikit-learn 默认使用'liblinear',它不能处理多项式损失,并且仅限于多类分类的 OvR 方案。然而,在 scikit-learn v 0.22 中,默认解算器更改为'lbfgs',它代表了有限内存Broyden–Fletcher–Goldfarb–Shanno(BFGS)算法(en.wikipedia.org/wiki/Limited-memory_BFGS),在这方面更加灵活。
查看我们用于训练LogisticRegression模型的前述代码时,您可能会想,“这个神秘的参数 C 是什么?”在下一小节中,我们将讨论这个参数,介绍过拟合和正则化的概念。然而,在我们继续讨论这些主题之前,让我们完成我们关于类成员概率的讨论。
可以使用predict_proba方法计算训练样本属于某一类的概率。例如,我们可以预测测试数据集中前三个示例的概率如下:
>>> lr.predict_proba(X_test_std[:3, :])
此代码片段返回以下数组:
array([[3.81527885e-09, 1.44792866e-01, 8.55207131e-01],
[8.34020679e-01, 1.65979321e-01, 3.25737138e-13],
[8.48831425e-01, 1.51168575e-01, 2.62277619e-14]])
第一行对应于第一朵花的类成员概率,第二行对应于第二朵花的类成员概率,依此类推。请注意,每行中的列总和为 1,如预期那样。(您可以通过执行lr.predict_proba(X_test_std[:3, :]).sum(axis=1)来确认这一点。)
第一行中的最高值约为 0.85,这意味着第一个示例属于类别 3(Iris-virginica),预测概率为 85%。因此,正如您可能已经注意到的,我们可以通过识别每行中最大的列来获取预测的类标签,例如使用 NumPy 的argmax函数:
>>> lr.predict_proba(X_test_std[:3, :]).argmax(axis=1)
返回的类索引如下所示(它们对应于Iris-virginica,Iris-setosa和Iris-setosa):
array([2, 0, 0])
在上述代码示例中,我们计算了条件概率,并通过使用 NumPy 的argmax函数手动将其转换为类标签。在实践中,当使用 scikit-learn 时,更方便的获取类标签的方法是直接调用predict方法:
>>> lr.predict(X_test_std[:3, :])
array([2, 0, 0])
最后,如果您想预测单个花例的类标签,请注意:scikit-learn 期望数据输入为二维数组;因此,我们首先必须将单行切片转换为这样的格式。将单行条目转换为二维数据数组的一种方法是使用 NumPy 的reshape方法添加一个新维度,如下所示:
>>> lr.predict(X_test_std[0, :].reshape(1, -1))
array([2])
通过正则化解决过拟合
过拟合是机器学习中常见的问题,指的是模型在训练数据上表现良好,但在未见过的数据(测试数据)上表现不佳。如果一个模型存在过拟合问题,我们也会说这个模型具有高方差,这可能是由于参数过多导致模型过于复杂,无法很好地适应基础数据。同样,我们的模型也可能遭受欠拟合(高偏差)的问题,这意味着我们的模型不够复杂,无法很好地捕捉训练数据中的模式,因此在未见数据上也表现较差。
尽管到目前为止我们只遇到过用于分类的线性模型,但通过将线性决策边界与更复杂、非线性决策边界进行比较,可以最好地说明过拟合和欠拟合的问题,如在图 3.8中所示:
图 3.8:欠拟合、拟合良好和过拟合模型的示例
偏差-方差权衡
研究人员经常使用“偏差”和“方差”或“偏差-方差权衡”这些术语来描述模型的性能,也就是说,你可能会听到有人说某个模型具有“高方差”或“高偏差”。那么,这是什么意思呢?一般来说,我们可能会说“高方差”与过拟合成正比,“高偏差”与欠拟合成正比。
在机器学习模型的背景下,方差衡量了在多次重新训练模型时,例如在训练数据集的不同子集上,为对特定示例进行分类,模型预测的一致性(或变异性)。我们可以说模型对训练数据中随机性敏感。相反,偏差衡量了多次在不同训练数据集上重新构建模型时,预测值与正确值的偏差程度;偏差是衡量由于非随机性而产生的系统误差的指标。
如果你对“偏差”和“方差”这两个术语的技术规范和推导感兴趣,可以在我的讲义中找到相关内容:sebastianraschka.com/pdf/lecture-notes/stat451fs20/08-model-eval-1-intro__notes.pdf。
寻找一个良好的偏差-方差平衡的一种方式是通过正则化调整模型的复杂性。正则化是处理共线性(特征之间的高相关性)、从数据中过滤噪声以及最终预防过拟合的非常有用的方法。
正则化背后的概念是引入额外信息来惩罚极端参数(权重)值。最常见的正则化形式是所谓的L2 正则化(有时也称为 L2 缩减或权重衰减),可以写作如下形式:
在这里,被称为正则化参数。请注意,分母中的 2 只是一个缩放因子,因此在计算损失梯度时会被抵消。样本大小n被添加以将正则化项缩放类似于损失。
正则化和特征标准化
正则化是特征缩放如标准化之类的另一个重要原因。为了使正则化正常工作,我们需要确保所有特征都在可比较的尺度上。
逻辑回归的损失函数可以通过添加简单的正则化项进行正则化,这将在模型训练过程中缩小权重:
未正则化损失的偏导数定义如下:
将正则化项添加到损失函数中会改变偏导数的形式如下:
通过正则化参数,我们可以控制拟合训练数据的紧密程度,同时保持权重较小。通过增加
的值,我们增加了正则化强度。请注意,偏置单元,本质上是拦截项或负阈值,如我们在第二章中学到的那样,通常不被正则化。
在 scikit-learn 中实现的LogisticRegression类中的参数C,来自支持向量机的一个约定,这将是下一节的主题。术语C与正则化参数的倒数成反比,。因此,减少逆正则化参数
C的值意味着增加正则化强度,我们可以通过绘制两个权重系数的 L2 正则化路径来进行可视化:
>>> weights, params = [], []
>>> for c in np.arange(-5, 5):
... lr = LogisticRegression(C=10.**c,
... multi_class='ovr')
... lr.fit(X_train_std, y_train)
... weights.append(lr.coef_[1])
... params.append(10.**c)
>>> weights = np.array(weights)
>>> plt.plot(params, weights[:, 0],
... label='Petal length')
>>> plt.plot(params, weights[:, 1], linestyle='--',
... label='Petal width')
>>> plt.ylabel('Weight coefficient')
>>> plt.xlabel('C')
>>> plt.legend(loc='upper left')
>>> plt.xscale('log')
>>> plt.show()
通过执行前述代码,我们拟合了 10 个逻辑回归模型,使用不同的逆正则化参数C的值。为了说明目的,我们仅收集了类别1(在这里是数据集中的第二类:Iris-versicolor)的权重系数,对所有分类器进行了采集。请记住,我们正在使用一对多技术进行多类分类。
正如我们在生成的图表中所看到的,如果减少参数C,即增加正则化强度,权重系数会收缩:
图 3.9:逆正则化强度参数 C 对 L2 正则化模型结果的影响
增加正则化强度可以减少过拟合,因此我们可能会问为什么不默认强力正则化所有模型。原因是我们在调整正则化强度时必须小心。例如,如果正则化强度过高,权重系数接近零,模型可能因为欠拟合而表现非常糟糕,正如图 3.8所示。
关于逻辑回归的额外资源
由于个别分类算法的深入涵盖超出了本书的范围,推荐给希望了解更多关于逻辑回归的读者,《逻辑回归:从入门到高级概念与应用》,斯科特·梅纳德博士,Sage Publications,2009 年。
使用支持向量机进行最大间隔分类
另一个强大且广泛使用的学习算法是支持向量机(SVM),可以看作是感知器的扩展。使用感知器算法,我们最小化了分类错误。然而,在 SVM 中,我们的优化目标是最大化边界。边界被定义为分离超平面(决策边界)与离该超平面最近的训练样本之间的距离,这些样本被称为支持向量。
这在图 3.10中有所说明:
图 3.10:SVM 在决策边界和训练数据点之间最大化间隔
最大间隔直觉
决策边界具有大间隔的背后理念在于,它们往往具有较低的泛化误差,而具有小间隔的模型更容易过拟合。
不幸的是,虽然 SVM 背后的主要直觉相对简单,但其中的数学内容相当深奥,需要对约束优化有扎实的理解。
因此,SVM 中最大间隔优化背后的细节超出了本书的范围。但是,如果您有兴趣了解更多内容,我们建议以下资源:
-
克里斯·J·C·伯吉斯在《支持向量机模式识别的教程》(数据挖掘与知识发现,2(2): 121-167, 1998)中的出色解释
-
弗拉基米尔·瓦普尼克的著作《统计学习理论的本质》,Springer Science+Business Media, 2000
-
安德鲁·吴的非常详细的讲义笔记,可在
see.stanford.edu/materials/aimlcs229/cs229-notes3.pdf获取
使用松弛变量处理非线性可分情况
虽然我们不打算深入探讨最大间隔分类背后更复杂的数学概念,但让我们简要提到所谓的松弛变量,它由弗拉基米尔·瓦普尼克在 1995 年引入,导致了所谓的软间隔分类。引入松弛变量的动机是,在非线性可分数据中,SVM 优化目标中的线性约束需要放宽,以允许在适当的损失惩罚下优化的收敛,即使存在分类错误。
松弛变量的使用引入了常被称为S的变量,在支持向量机的上下文中,我们可以把S看作是控制误分类惩罚的超参数。较大的S值对应着较大的误差惩罚,而如果选择较小的S值,则对误分类错误的严格性较低。我们可以使用S参数来控制边界的宽度,从而调整偏差-方差的权衡,如图 3.11所示:
图 3.11: 逆正则化强度大值和小值对分类的影响
这个概念与正则化有关,我们在前一节中讨论了正则化回归的情况,在这种情况下,减小C的值会增加模型的偏差(欠拟合),降低方差(过拟合)。
现在我们已经了解了线性支持向量机的基本概念,让我们训练一个 SVM 模型来对我们鸢尾花数据集中的不同花进行分类:
>>> from sklearn.svm import SVC
>>> svm = SVC(kernel='linear', C=1.0, random_state=1)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined,
... classifier=svm,
... test_idx=range(105, 150))
>>> plt.xlabel('Petal length [standardized]')
>>> plt.ylabel('Petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
在使用前述代码示例训练鸢尾花数据集的分类器后,展示了支持向量机的三个决策区域,如图 3.12所示:
图 3.12: 支持向量机的决策区域
逻辑回归与支持向量机
在实际分类任务中,线性逻辑回归和线性支持向量机通常产生非常相似的结果。逻辑回归试图最大化训练数据的条件概率,这使得它比支持向量机更容易受到异常值的影响,后者主要关注最靠近决策边界的点(支持向量)。另一方面,逻辑回归有简单模型的优势,并且更容易实现,数学上更容易解释。此外,逻辑回归模型可以轻松更新,在处理流数据时非常有吸引力。
在 scikit-learn 中的替代实现方式
scikit-learn 库中的LogisticRegression类,我们在前面的章节中使用过,可以通过设置solver='liblinear'来使用 LIBLINEAR 库。LIBLINEAR 是台湾大学开发的高度优化的 C/C++库(www.csie.ntu.edu.tw/~cjlin/liblinear/)。
同样地,我们用来训练支持向量机的SVC类使用了 LIBSVM,这是一种专门用于支持向量机的等效 C/C++库(www.csie.ntu.edu.tw/~cjlin/libsvm/)。
使用 LIBLINEAR 和 LIBSVM 相比于例如原生 Python 实现的优势在于,它们允许快速训练大量线性分类器。然而,有时我们的数据集太大而无法放入计算机内存。因此,scikit-learn 还通过SGDClassifier类提供了替代实现,该类还通过partial_fit方法支持在线学习。SGDClassifier类的概念类似于我们在第二章中为 Adaline 实现的随机梯度算法。
我们可以初始化感知器的 SGD 版本(loss='perceptron')、逻辑回归(loss='log')和具有默认参数的 SVM(loss='hinge'),如下所示:
>>> from sklearn.linear_model import SGDClassifier
>>> ppn = SGDClassifier(loss='perceptron')
>>> lr = SGDClassifier(loss='log')
>>> svm = SGDClassifier(loss='hinge')
使用核 SVM 解决非线性问题
支持向量机(SVM)在机器学习从业者中享有极高的流行度的另一个原因是,它们可以轻松地应用核技巧来解决非线性分类问题。在我们讨论所谓的核 SVM的主要概念之前,让我们首先创建一个合成数据集,看看这样一个非线性分类问题可能会是什么样子。
线性不可分数据的核方法
使用以下代码,我们将使用 NumPy 的logical_or函数创建一个形如 XOR 门的简单数据集,其中 100 个示例将被分配类标签1,另外 100 个示例将被分配类标签-1:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> np.random.seed(1)
>>> X_xor = np.random.randn(200, 2)
>>> y_xor = np.logical_xor(X_xor[:, 0] > 0,
... X_xor[:, 1] > 0)
>>> y_xor = np.where(y_xor, 1, 0)
>>> plt.scatter(X_xor[y_xor == 1, 0],
... X_xor[y_xor == 1, 1],
... c='royalblue', marker='s',
... label='Class 1')
>>> plt.scatter(X_xor[y_xor == 0, 0],
... X_xor[y_xor == 0, 1],
... c='tomato', marker='o',
... label='Class 0')
>>> plt.xlim([-3, 3])
>>> plt.ylim([-3, 3])
>>> plt.xlabel('Feature 1')
>>> plt.ylabel('Feature 2')
>>> plt.legend(loc='best')
>>> plt.tight_layout()
>>> plt.show()
执行代码后,我们将得到一个带有随机噪声的 XOR 数据集,如图 3.13所示:
图 3.13:XOR 数据集的绘图
显然,如果我们使用线性超平面作为决策边界,通过前面章节讨论过的线性逻辑回归或线性支持向量机模型,我们将无法很好地将正类和负类的示例分开。
处理这种线性不可分数据的核方法的基本思想是通过映射函数创建原始特征的非线性组合,将其投影到高维空间中,使数据变得线性可分,如所示。正如图 3.14所示,我们可以将一个二维数据集转换为一个新的三维特征空间,其中通过以下投影使得类别变得可分:
这使我们能够通过一个线性超平面将绘图中显示的两类分开,如果我们将其投影回原始特征空间,则这将成为一个非线性决策边界,如下所示的同心圆数据集:
图 3.14:使用核方法对非线性数据进行分类的过程
在高维空间中使用核技巧找到分离超平面
要使用支持向量机解决非线性问题,我们会通过映射函数 将训练数据转换为更高维度的特征空间,并训练一个线性支持向量机模型来在这个新的特征空间中对数据进行分类。然后,我们可以使用同样的映射函数
将新的未见数据进行转换,并使用线性支持向量机模型进行分类。
然而,这种映射方法的一个问题是构建新特征在计算上非常昂贵,特别是在处理高维数据时。这就是所谓的核技巧发挥作用的地方。
虽然我们没有详细介绍如何解决二次规划任务来训练支持向量机,但在实践中,我们只需将点积 x^(^i^)^Tx^(^j^) 替换为 。为了避免显式计算两个点之间的点积步骤,我们定义了所谓的核函数:
最常用的核函数之一是径向基函数(RBF)核,也可以简称为高斯核:
这通常被简化为:
这里, 是要优化的自由参数。
大致而言,术语“核”可以被解释为一对示例之间的相似度函数。负号将距离度量反转为相似度评分,并且由于指数项的存在,结果的相似度评分将落入 1(完全相似示例)到 0(非常不相似示例)的范围内。
现在我们已经了解了核技巧背后的大局,让我们看看能否训练一个能够很好地分离异或数据的核支持向量机。在这里,我们简单地使用我们之前导入的 SVC 类,并将 kernel='linear' 参数替换为 kernel='rbf':
>>> svm = SVC(kernel='rbf', random_state=1, gamma=0.10, C=10.0)
>>> svm.fit(X_xor, y_xor)
>>> plot_decision_regions(X_xor, y_xor, classifier=svm)
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
如我们在结果图中所见,核支持向量机相对较好地分离了异或数据:
图 3.15:使用核方法在异或数据上的决策边界
我们将参数 ,设置为
gamma=0.1,可以理解为高斯球体的截断参数。如果增加 的值,我们将增加训练示例的影响或范围,这将导致更紧密和更崎岖的决策边界。为了更好地理解
,让我们将 RBF 核支持向量机应用于我们的鸢尾花数据集:
>>> svm = SVC(kernel='rbf', random_state=1, gamma=0.2, C=1.0)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined, classifier=svm,
... test_idx=range(105, 150))
>>> plt.xlabel('Petal length [standardized]')
>>> plt.ylabel('Petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
由于我们选择了相对较小的值来设置 ,所以 RBF 核支持向量机模型的决策边界将比较柔和,如图 3.16所示:
图 3.16:使用具有小图 3.17值的 RBF 核 SVM 模型在鸢尾花数据集上的决策边界
现在,让我们增加图 3.17中的值并观察决策边界的效果:
>>> svm = SVC(kernel='rbf', random_state=1, gamma=100.0, C=1.0)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined, classifier=svm,
... test_idx=range(105,150))
>>> plt.xlabel('Petal length [standardized]')
>>> plt.ylabel('Petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
在图 3.17中,我们现在可以看到围绕类别0和1的决策边界使用了一个相对较大的值的情况:
图 3.17:使用具有大图 3.17值的 RBF 核 SVM 模型在鸢尾花数据集上的决策边界
尽管模型非常适合训练数据集,这样的分类器可能在未见数据上具有很高的泛化误差。这说明了当算法对训练数据中的波动过于敏感时,参数也在控制过拟合或方差中扮演着重要角色。
决策树学习
决策树分类器是有吸引力的模型,如果我们关心解释性的话。正如“决策树”这个名称所暗示的,我们可以将这个模型看作通过提出一系列问题来分解我们的数据。
考虑以下例子,我们使用决策树来决定特定一天的活动:
图 3.18:决策树的一个示例
基于我们训练数据集中的特征,决策树模型学习一系列问题,以推断示例的类别标签。虽然图 3.18说明了基于分类变量的决策树的概念,但如果我们的特征是实数,比如在鸢尾花数据集中,相同的概念也适用。例如,我们可以简单地沿着萼片宽度特征轴定义一个截断值,并提出一个二元问题:“萼片宽度 ≥ 2.8 cm 吗?”
使用决策算法,我们从树根开始,并根据导致最大信息增益(IG)的特征来分割数据,这将在接下来的部分详细解释。在迭代过程中,我们可以在每个子节点重复这个分割过程,直到叶子节点是纯净的。这意味着每个节点上的训练示例都属于同一类别。实际上,这可能导致一个非常深的树,有许多节点,这很容易导致过拟合。因此,我们通常希望通过设置树的最大深度来修剪树。
最大化信息增益 - 在投入最少的前提下获得最大的收益
要在最具信息性的特征上分割节点,我们需要定义一个通过树学习算法优化的目标函数。在这里,我们的目标函数是在每次分割时最大化信息增益,定义如下:
这里,f 是要执行分割的特征;D[p] 和 D[j] 分别是父节点和第 j 个子节点的数据集;I 是我们的不纯度度量;N[p] 是父节点的总训练示例数;N[j] 是第 j 个子节点中示例的数目。正如我们所见,信息增益简单地是父节点的不纯度与子节点不纯度之和的差异——子节点的不纯度越低,信息增益越大。然而,为了简化和减少组合搜索空间,大多数库(包括 scikit-learn)实现二叉决策树。这意味着每个父节点分裂为两个子节点,D[left] 和 D[right]:
在二叉决策树中常用的三种不纯度度量或分裂准则是基尼不纯度(I[G])、熵(I[H])和分类错误(I[E])。让我们从所有非空类的熵定义开始():
在这里,p(i|t) 是属于特定节点 t 的类 i 示例的比例。因此,如果一个节点上的所有示例属于同一类,则熵为 0,如果我们具有均匀的类分布,则熵是最大的。例如,在二元类设置中,如果 p(i=1|t) = 1 或 p(i=0|t) = 0,则熵为 0。如果类均匀分布,其中 p(i=1|t) = 0.5 和 p(i=0|t) = 0.5,则熵为 1。因此,我们可以说熵准则试图在树中最大化互信息。
为了提供直观感受,让我们通过以下代码可视化不同类分布的熵值:
>>> def entropy(p):
... return - p * np.log2(p) - (1 - p) * np.log2((1 - p))
>>> x = np.arange(0.0, 1.0, 0.01)
>>> ent = [entropy(p) if p != 0 else None for p in x]
>>> plt.ylabel('Entropy')
>>> plt.xlabel('Class-membership probability p(i=1)')
>>> plt.plot(x, ent)
>>> plt.show()
图 3.19 如下所示是前述代码生成的图形:
图 3.19:不同类成员概率的熵值
基尼不纯度可以理解为最小化误分类的概率准则:
与熵类似,基尼不纯度在类别完全混合时达到最大值,例如,在二元类别设置中(c = 2):
然而,在实践中,基尼不纯度和熵通常产生非常相似的结果,因此通常不值得花费大量时间评估不同的不纯度准则,而是试验不同的修剪阈值。实际上,正如您将在图 3.21 中看到的那样,基尼不纯度和熵的形状是相似的。
另一个不纯度度量是分类错误:
这是一个有用的修剪准则,但不推荐用于生成决策树,因为它对节点类别概率的变化不敏感。我们可以通过查看图 3.20 中显示的两种可能的分割场景来说明这一点:
图 3.20:决策树数据分割
我们从父节点开始,有一个数据集 D[p],其中包含来自类别 1 的 40 个示例和来自类别 2 的 40 个示例,我们将其分成两个数据集 D[left] 和 D[right]。使用分类错误作为分裂准则的信息增益在情景 A 和 B 中是相同的 (IG[E] = 0.25):
然而,基尼不纯度会偏向于情景 B 的分裂(),而非情景 A(IG[G] = 0.125),这的确更加纯净:
类似地,熵准则也会偏向于情景 B (IG[H] = 0.31),而非情景 A (IG[H] = 0.19):
为了更直观地比较我们之前讨论过的三种不纯度准则,让我们绘制类别 1 的概率范围 [0, 1] 内的不纯度指数。请注意,我们还将添加熵的缩放版本(熵 / 2)以观察到基尼不纯度是熵和分类错误之间的中间度量。代码如下:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> def gini(p):
... return p*(1 - p) + (1 - p)*(1 - (1-p))
>>> def entropy(p):
... return - p*np.log2(p) - (1 - p)*np.log2((1 - p))
>>> def error(p):
... return 1 - np.max([p, 1 - p])
>>> x = np.arange(0.0, 1.0, 0.01)
>>> ent = [entropy(p) if p != 0 else None for p in x]
>>> sc_ent = [e*0.5 if e else None for e in ent]
>>> err = [error(i) for i in x]
>>> fig = plt.figure()
>>> ax = plt.subplot(111)
>>> for i, lab, ls, c, in zip([ent, sc_ent, gini(x), err],
... ['Entropy', 'Entropy (scaled)',
... 'Gini impurity',
... 'Misclassification error'],
... ['-', '-', '--', '-.'],
... ['black', 'lightgray',
... 'red', 'green', 'cyan']):
... line = ax.plot(x, i, label=lab,
... linestyle=ls, lw=2, color=c)
>>> ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.15),
... ncol=5, fancybox=True, shadow=False)
>>> ax.axhline(y=0.5, linewidth=1, color='k', linestyle='--')
>>> ax.axhline(y=1.0, linewidth=1, color='k', linestyle='--')
>>> plt.ylim([0, 1.1])
>>> plt.xlabel('p(i=1)')
>>> plt.ylabel('impurity index')
>>> plt.show()
上述代码示例生成的图形如下:
图 3.21:不同类别成员概率在 0 到 1 之间的不纯度指数
构建决策树
决策树可以通过将特征空间划分为矩形来构建复杂的决策边界。然而,我们必须小心,因为决策树越深,决策边界就会变得越复杂,这很容易导致过拟合。使用 scikit-learn,我们现在将训练一个最大深度为 4 的决策树,使用基尼不纯度作为不纯度的标准。
尽管出于可视化目的可能希望进行特征缩放,请注意,对于决策树算法来说,特征缩放并不是必需的。代码如下:
>>> from sklearn.tree import DecisionTreeClassifier
>>> tree_model = DecisionTreeClassifier(criterion='gini',
... max_depth=4,
... random_state=1)
>>> tree_model.fit(X_train, y_train)
>>> X_combined = np.vstack((X_train, X_test))
>>> y_combined = np.hstack((y_train, y_test))
>>> plot_decision_regions(X_combined,
... y_combined,
... classifier=tree_model,
... test_idx=range(105, 150))
>>> plt.xlabel('Petal length [cm]')
>>> plt.ylabel('Petal width [cm]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
执行上述代码示例后,我们得到了决策树的典型轴对齐决策边界:
图 3.22:使用决策树的鸢尾花数据的决策边界
在 scikit-learn 中的一个很好的特性是,它允许我们在训练后直接可视化决策树模型,代码如下:
>>> from sklearn import tree
>>> feature_names = ['Sepal length', 'Sepal width',
... 'Petal length', 'Petal width']
>>> tree.plot_tree(tree_model,
... feature_names=feature_names,
... filled=True)
>>> plt.show()
图 3.23:拟合到鸢尾花数据集的决策树模型
在我们调用的 plot_tree 函数中设置 filled=True 将会根据该节点处的主要类标签为节点着色。还有许多其他选项可供选择,您可以在文档中找到:scikit-learn.org/stable/modules/generated/sklearn.tree.plot_tree.html。
查看决策树图,现在我们可以清楚地追溯决策树从训练数据集中确定的分割。关于每个节点的特征分割标准,请注意,向左的分支对应“True”,向右的分支对应“False”。
查看根节点,顶部有 105 个示例。第一个分割使用萼片宽度≤ 0.75 cm 的截止值将根节点分割为两个子节点,左子节点有 35 个示例,右子节点有 70 个示例。第一个分割后,我们可以看到左子节点已经是纯净的,只包含来自Iris-setosa类的示例(基尼不纯度 = 0)。然后,右侧的进一步分割用于将示例从Iris-versicolor和Iris-virginica类中分离出来。
查看这棵树,以及树的决策区域图,我们可以看到决策树在分离花卉类别方面做得非常好。不幸的是,scikit-learn 目前没有实现手动后修剪决策树的功能。但是,我们可以回到之前的代码示例,将我们的决策树的max_depth更改为3,然后与当前模型进行比较,但我们将此留给有兴趣的读者作为练习。
另外,scikit-learn 提供了决策树的自动成本复杂度后修剪过程。有兴趣的读者可以在以下教程中找到关于这个更高级主题的更多信息:scikit-learn.org/stable/auto_examples/tree/plot_cost_complexity_pruning.html。
通过随机森林组合多个决策树
集成方法在机器学习应用中已经广受欢迎,因为它们具有良好的分类性能并且对过拟合具有鲁棒性。虽然我们将在第七章“组合不同模型进行集成学习”中涵盖不同的集成方法,包括装袋和提升,让我们先讨论基于决策树的随机森林算法,这个算法以其良好的可扩展性和易用性而闻名。随机森林可以被看作是决策树的集成。随机森林的理念是将多个(深度)决策树进行平均,这些树个体上具有高方差,以建立一个更健壮的模型,具有更好的泛化性能,并且不易过拟合。随机森林算法可以总结为四个简单步骤:
-
绘制大小为n的随机bootstrap样本(从训练数据集中用替换随机选择n个示例)。
-
从 bootstrap 样本中生长一棵决策树。在每个节点:
-
随机选择d个特征,不替换。
-
使用提供最佳分割的特征来分割节点,例如,最大化信息增益的目标函数。
-
-
重复步骤 1-2 k次。
-
通过每棵树的预测结果进行聚合,通过多数投票分配类标签。关于多数投票的详细讨论将在第七章中进行。
在训练单独的决策树时,我们需要注意步骤 2中的一个小修改:在每个节点评估最佳分割时,我们只考虑一部分随机选择的特征。
带替换和不带替换抽样
如果你对“带”和“不带”替换抽样这些术语不太熟悉,让我们通过一个简单的思维实验来理解一下。假设我们在玩一个抽奖游戏,从一个罐子里随机抽取数字。开始时,罐子里有五个唯一的数字:0、1、2、3 和 4,每次抽取一个数字。在第一轮中,从罐子中抽取特定数字的概率为 1/5。现在,在不替换抽样中,我们在每次抽取后不将数字放回罐子里。因此,在下一轮中,从剩余数字集合中抽取特定数字的概率取决于上一轮的情况。例如,如果我们剩下的数字集合是 0、1、2 和 4,那么下一轮抽取数字 0 的概率将变为 1/4。
然而,在有放回随机抽样中,我们总是将抽取的数字放回罐子中,因此每次抽取特定数字的概率不会改变;我们可以多次抽取相同的数字。换句话说,在带替换抽样中,样本(数字)是独立的,且具有零的协方差。例如,五轮随机抽取数字的结果可能如下:
-
无放回随机抽样:2, 1, 3, 4, 0
-
有放回随机抽样:1, 3, 3, 4, 1
虽然随机森林的解释能力不如决策树,但随机森林的一个很大优势是我们不必过多担心选择良好的超参数值。通常情况下,我们不需要修剪随机森林,因为集成模型对于来自个体决策树预测的噪声具有相当的鲁棒性。在实践中,我们只需要关心一个参数,即随机森林中选择的树的数量,k,(步骤 3)。通常来说,树的数量越多,随机森林分类器的性能越好,但计算成本也会增加。
虽然在实践中较少见,随机森林分类器的其他超参数也可以进行优化——我们将在第六章“学习模型评估和超参数调优最佳实践”中讨论使用的技术——分别是 bootstrap 样本的大小n(步骤 1)和每次分割时随机选择的特征数d(步骤 2a)。通过 bootstrap 样本的大小n,我们控制随机森林的偏差-方差权衡。
减小引导样本的大小会增加单个树之间的多样性,因为特定训练示例被包含在引导样本中的概率较低。因此,缩小引导样本的大小可能会增加随机森林的随机性,有助于减少过拟合的影响。然而,较小的引导样本通常导致随机森林的整体性能较低,并且训练和测试性能之间的差距较小,但总体测试性能较低。相反,增加引导样本的大小可能会增加过拟合的程度。因为引导样本,以及因此单个决策树,变得更加相似,它们学会更加紧密地拟合原始训练数据集。
在大多数实现中,包括 scikit-learn 中的RandomForestClassifier实现,引导样本的大小通常选择与原始训练数据集中的训练示例数量相等,这通常可以提供良好的偏差-方差权衡。对于每次分裂时特征数d,我们希望选择一个比训练数据集中总特征数更小的值。scikit-learn 和其他实现中使用的合理默认值为 ,其中m是训练数据集中的特征数。
幸运的是,我们不必自己构建随机森林分类器,因为在 scikit-learn 中已经有了一个可以使用的实现:
>>> from sklearn.ensemble import RandomForestClassifier
>>> forest = RandomForestClassifier(n_estimators=25,
... random_state=1,
... n_jobs=2)
>>> forest.fit(X_train, y_train)
>>> plot_decision_regions(X_combined, y_combined,
... classifier=forest, test_idx=range(105,150))
>>> plt.xlabel('Petal length [cm]')
>>> plt.ylabel('Petal width [cm]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
执行前述代码后,我们应该能够看到由随机森林中树的集合形成的决策区域,如图 3.24所示:
图 3.24: 使用随机森林在鸢尾花数据集上的决策边界
使用上述代码,我们通过n_estimators参数训练了一个包含 25 棵决策树的随机森林。默认情况下,它使用基尼不纯度作为节点分裂的标准。尽管我们从一个非常小的训练数据集中生成了一个非常小的随机森林,但出于演示目的,我们使用了n_jobs参数,它允许我们在计算机的多个核心(这里是两个核心)上并行化模型训练。如果您在此代码中遇到错误,则可能是您的计算机不支持多进程。您可以省略n_jobs参数或将其设置为n_jobs=None。
K 最近邻算法 — 一种惰性学习算法
我们想在本章中讨论的最后一个监督学习算法是k 最近邻(KNN)分类器,这个算法特别有趣,因为它与我们迄今为止讨论过的学习算法在根本上是不同的。
KNN 是“懒惰学习器”的典型例子。它之所以被称为“懒惰”,不是因为它的表面简单性,而是因为它不从训练数据中学习判别函数,而是记忆训练数据集。
参数化与非参数化模型
机器学习算法可以分为参数化和非参数化模型。使用参数化模型,我们从训练数据集中估计参数,以学习可以分类新数据点的函数,而无需再需要原始训练数据集。典型的参数化模型包括感知器、逻辑回归和线性支持向量机。相比之下,非参数化模型无法用固定的一组参数来描述,并且参数数量随着训练数据量的增加而变化。到目前为止,我们看到的两个非参数化模型的例子是决策树分类器/随机森林和核(非线性)支持向量机。
KNN 属于描述为基于实例学习的非参数模型的子类。基于实例学习的模型以记忆训练数据集为特征,惰性学习是与学习过程中没有(零)成本相关的基于实例学习的特殊情况。
KNN 算法本身非常简单,可以通过以下步骤进行总结:
-
选择k的数量和距离度量
-
找出我们要分类的数据记录的k个最近邻居
-
通过多数投票分配类标签
图 3.25说明了如何根据其五个最近邻中的多数投票将新数据点(?)分配到三角形类标签:
图 3.25:k 近邻如何工作
根据选择的距离度量,KNN 算法找出训练数据集中与我们要分类的点最接近(最相似)的k个示例。然后,数据点的类标签由其k个最近邻之间的多数投票确定。
基于内存的方法的优缺点
这种基于内存的方法的主要优势在于,随着我们收集新的训练数据,分类器立即适应。然而,缺点是在最坏情况下,对新示例进行分类的计算复杂度随着训练数据集中示例数量的线性增长而增加,除非数据集的维度(特征)非常少,并且算法使用了有效的数据结构来更有效地查询训练数据。这些数据结构包括 k-d 树(en.wikipedia.org/wiki/K-d_tree)和球树(en.wikipedia.org/wiki/Ball_tree),这两者都受到 scikit-learn 支持。此外,除了查询数据的计算成本之外,大型数据集在存储能力有限的情况下也可能存在问题。
然而,在许多情况下,当我们处理相对较小到中等大小的数据集时,基于内存的方法可以提供良好的预测和计算性能,因此是处理许多现实世界问题的良好选择。最近使用最近邻方法的示例包括预测制药药物靶标属性(Machine Learning to Identify Flexibility Signatures of Class A GPCR Inhibition,Biomolecules,2020,Joe Bemister-Buffington,Alex J. Wolf,Sebastian Raschka 和 Leslie A. Kuhn,www.mdpi.com/2218-273X/10/3/454)和最先进的语言模型(Efficient Nearest Neighbor Language Models,2021,Junxian He,Graham Neubig 和 Taylor Berg-Kirkpatrick,arxiv.org/abs/2109.04212)。
通过执行以下代码,我们将使用 scikit-learn 中的欧氏距离度量实现一个 KNN 模型:
>>> from sklearn.neighbors import KNeighborsClassifier
>>> knn = KNeighborsClassifier(n_neighbors=5, p=2,
... metric='minkowski')
>>> knn.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std, y_combined,
... classifier=knn, test_idx=range(105,150))
>>> plt.xlabel('Petal length [standardized]')
>>> plt.ylabel('Petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.tight_layout()
>>> plt.show()
通过在此数据集的 KNN 模型中指定五个邻居,我们得到一个相对平滑的决策边界,如图 3.26所示:
图 3.26:鸢尾花数据集上的 k 近邻决策边界
解决平局
在平局的情况下,scikit-learn 实现的 KNN 算法会优先选择与要分类的数据记录距离较近的邻居。如果邻居的距离相似,则算法会选择在训练数据集中出现较早的类标签。
正确选择k值对于找到过拟合和欠拟合之间的良好平衡至关重要。我们还必须确保选择一个适合数据集特征的距离度量。通常,对于实值示例,如我们的鸢尾花数据集中的花,简单的欧氏距离度量常被使用,其特征以厘米为单位测量。然而,如果我们使用欧氏距离度量,同样重要的是标准化数据,以确保每个特征对距离的贡献相等。我们在之前代码中使用的minkowski距离只是欧氏距离和曼哈顿距离的一般化,可以写成如下形式:
如果设置参数p=2,则变为欧氏距离,或在p=1时变为曼哈顿距离。scikit-learn 中提供了许多其他距离度量,并可提供给metric参数。它们列在scikit-learn.org/stable/modules/generated/sklearn.metrics.DistanceMetric.html上。
最后,需要提到的是 KNN 非常容易因为维度灾难而过拟合。维度灾难描述了在固定大小的训练数据集的维度增加时,特征空间变得越来越稀疏的现象。即使是最近的邻居在高维空间中也可能相距甚远,导致估计不准确。
我们在逻辑回归章节讨论了正则化的概念,作为避免过拟合的一种方法。然而,在无法应用正则化的模型中,例如决策树和 KNN,我们可以使用特征选择和降维技术来帮助我们避免维度灾难。这将在接下来的两章中详细讨论。
带有 GPU 支持的替代机器学习实现
在处理大型数据集时,运行 k 最近邻算法或拟合具有许多估计器的随机森林可能需要大量的计算资源和处理时间。如果您的计算机配备了与最新版本的 NVIDIA CUDA 库兼容的 NVIDIA GPU,我们建议考虑使用 RAPIDS 生态系统(docs.rapids.ai/api)。例如,RAPIDS 的 cuML(docs.rapids.ai/api/cuml/stable/)库实现了许多带有 GPU 支持的 scikit-learn 机器学习算法,以加速处理速度。您可以在docs.rapids.ai/api/cuml/stable/estimator_intro.html找到 cuML 的介绍。如果您有兴趣了解更多关于 RAPIDS 生态系统的内容,请参阅我们与 RAPIDS 团队合作撰写的免费获取的期刊文章:《Python 中的机器学习:数据科学、机器学习和人工智能的主要发展和技术趋势》(www.mdpi.com/2078-2489/11/4/193)。
总结
在本章中,您学习了许多不同的机器学习算法,用于解决线性和非线性问题。您已经看到,如果我们关心解释性,决策树尤其具有吸引力。逻辑回归不仅是通过 SGD 进行在线学习的有用模型,还允许我们预测特定事件的概率。
虽然支持向量机(SVM)是强大的线性模型,可以通过核技巧扩展到非线性问题,但它们有许多需要调整的参数才能做出良好的预测。相比之下,集成方法如随机森林不需要太多参数调整,并且不像决策树那样容易过拟合,这使它们成为许多实际问题领域的理想模型。K 最近邻分类器通过惰性学习提供了一种替代分类的方法,允许我们在没有模型训练的情况下进行预测,但预测步骤更加消耗计算资源。
然而,比选择合适的学习算法更重要的是我们训练数据集中的可用数据。没有任何算法能够在没有信息丰富和有歧视性的特征的情况下做出良好的预测。
在下一章中,我们将讨论有关数据预处理、特征选择和降维的重要主题,这意味着我们需要构建强大的机器学习模型。随后,在第六章,学习模型评估和超参数调优的最佳实践中,我们将看到如何评估和比较模型的性能,并学习优化不同算法的有用技巧。
加入我们书籍的 Discord 空间
加入书籍的 Discord 工作区,与作者进行每月的问我任何会话: