统计机器学习教程-全-

统计机器学习教程(全)

原文:Statistics for Machine Learning

协议:CC BY-NC-SA 4.0

零、前言

机器学习中的复杂统计让很多开发人员感到担忧。了解统计数据有助于您构建强大的机器学习模型,这些模型针对给定的问题陈述进行了优化。我相信任何机器学习的实践者都应该精通统计学和数学,这样他们才能以有效的方式推测和解决任何机器学习问题。在这本书里,我们将涵盖统计学和机器学习的基础知识,让你全面了解机器学习技术在相关问题上的应用。我们将使用 Python 和 R 编程讨论常用算法在各种领域问题上的应用。我们会用到scikit-learne1071randomForestc50xgboost等库。我们还将借助 Keras 软件复习深度学习的基础知识。此外,我们将对纯 Python 编程语言的强化学习有一个概述。

本书由以下目标驱动:

  • 帮助新手掌握各种基础知识,同时让有经验的专业人士更新他们对各种概念的知识,并在他们选择的数据上应用算法时更加清晰。
  • 为了给出 Python 和 R 的整体视图,本书将带您浏览使用这两种语言的各种示例。
  • 为了介绍机器学习的新趋势,深度学习和强化学习的基础知识都有合适的例子来教你最先进的技术。

这本书涵盖了什么

第 1 章从统计到机器学习的旅程,向您介绍了统计和机器学习的所有必要基础知识和基本构件。本章中所有的基础知识都在 Python 和 R 代码示例的支持下进行了解释。

第二章统计与机器学习的并行性,用线性回归和套索/岭回归的例子比较统计建模与机器学习的区别并得出相似之处。

第 3 章逻辑回归对比随机森林,用分类实例描述逻辑回归和随机森林的比较,说明两个建模过程的详细步骤。到本章结束时,您将对统计流和机器学习有一个完整的了解。

第 4 章基于树的机器学习模型、重点介绍了行业从业者使用的各种基于树的机器学习模型,包括决策树、bagging、随机森林、AdaBoost、梯度 boosting 和 XGBoost,并附有两种语言的 HR 减员示例。

第 5 章K-最近邻和朴素贝叶斯说明了简单的机器学习方法。k 近邻是用乳腺癌数据解释的。通过一个使用各种自然语言处理预处理技术的消息分类例子来解释朴素贝叶斯模型。

第 6 章支持向量机和神经网络,描述了支持向量机涉及的各种功能和内核的使用。然后介绍神经网络。深度学习的基础知识在本章中有详尽的介绍。

第七章推荐引擎,向我们展示了如何基于相似用户找到相似电影,这是基于用户-用户相似度矩阵。在第二部分中,基于电影-电影相似性矩阵提出建议,其中使用余弦相似性提取相似电影。最后,应用考虑用户和电影来确定推荐的协同过滤技术,该技术与最小二乘法交替使用。

第 8 章 【无监督学习】介绍了各种技术,如 k 均值聚类、主成分分析、奇异值分解和基于深度学习的深度自动编码器。最后解释了为什么深度自动编码器比传统的主成分分析技术更强大。

第 9 章强化学习提供了详尽的技术,学习在不同的状态下达到目标的最佳路径,例如马尔可夫决策过程、动态规划、蒙特卡罗方法和时间差异学习。最后,为使用机器学习和强化学习的优秀应用程序提供了一些用例。

这本书你需要什么

本书假设您了解 Python 和 R 的基础知识以及如何安装库。它并不假设你已经具备了高等统计和数学的知识,比如线性代数等等。

本书通篇使用了以下版本的软件,但它应该可以与任何更新的软件一起运行:

  • Anaconda 3–4 . 3 . 1(所有 Python 及其相关包都包含在 Anaconda、Python 3.6.1、NumPy 1.12.1、Pandas 0.19.2 和 scikit-learn 0.18.1 中)
  • R 3.4.0 和 RStudio 1.0.143
  • Theano 0.9.0 版
  • 硬 2.0.2

这本书是给谁的

这本书是为那些很少或没有统计学背景的开发人员准备的,他们希望在他们的系统中实现机器学习。R 或 Python 中的一些编程知识会很有用。

约定

在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“在numpy包中没有实现mode功能。”。任何命令行输入或输出都编写如下:

>>> import numpy as np 
>>> from scipy import stats 
>>> data = np.array([4,5,1,2,7,2,6,9,3]) 
# Calculate Mean 
>>> dt_mean = np.mean(data) ; 
print ("Mean :",round(dt_mean,2)) 

新名词重要词语以粗体显示。

Warnings or important notes appear like this. Tips and tricks appear like this.

读者反馈

我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。要给我们发送一般反馈,只需发送电子邮件feedback@packtpub.com,并在您的邮件主题中提及书名。如果您对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于www.packtpub.com/authors的作者指南。

客户支持

现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。

下载示例代码

你可以从你在http://www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”选项卡上。

  3. 点击代码下载和勘误表。

  4. 在搜索框中输入图书的名称。

  5. 选择要下载代码文件的书籍。

  6. 从您购买这本书的下拉菜单中选择。

  7. 点击代码下载。

下载文件后,请确保使用最新版本的解压缩文件夹:

  • 视窗系统的 WinRAR / 7-Zip
  • zipeg/izp/un ARX for MAC
  • 适用于 Linux 的 7-Zip / PeaZip

这本书的代码包也托管在 GitHub 上,网址为。我们还有来自丰富的图书和视频目录的其他代码包,可在https://github.com/PacktPublishing/获得。看看他们!

下载这本书的彩色图片

我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。彩色图像将帮助您更好地理解给定输出的变化。您可以从https://www . packtpub . com/sites/default/files/downloads/statistics formachineselearning _ color images . pdf下载此文件。

正误表

虽然我们已经注意确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现一个错误,也许是文本或代码中的错误,如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问http://www.packtpub.com/submit-errata,选择您的书籍,点击勘误表提交表格链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。要查看之前提交的勘误表,请前往https://www.packtpub.com/books/content/support并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。

海盗行为

互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称。请通过copyright@packtpub.com联系我们,获取疑似盗版资料的链接。我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以在questions@packtpub.com联系我们,我们将尽最大努力解决。

一、从统计到机器学习的旅程

最近一段时间,机器学习 ( ML )和数据科学获得了前所未有的普及。预计该领域在未来几年将呈指数级增长。首先,什么是机器学习?为什么有人需要不厌其烦地去理解这些原则?我们有你的答案。一个简单的例子可能是电子商务网站中的书籍推荐,当有人去搜索某本特定的书或任何其他产品推荐时,这些推荐被一起购买,以向用户提供他们可能喜欢的想法。听起来很神奇,对吧?事实上,利用机器学习,能够实现的远不止这些。

机器学习是一个研究分支,其中模型可以基于数据从经验中自动学习,而不像统计模型那样专门建模。随着时间的推移和更多的数据,模型预测将变得更好。

在第一章中,我们将介绍理解统计和机器学习术语所必需的基本概念,这些术语对于理解两个流之间的相似性是必要的,这些流要么是全职统计学家,要么是执行机器学习的软件工程师,但希望理解最大似然方法背后的统计工作。我们将快速介绍理解模型构建模块所必需的基础知识。

在本章中,我们将介绍以下内容:

  • 模型建立和验证的统计术语
  • 模型构建和验证的机器学习术语
  • 机器学习模型概述

模型建立和验证的统计术语

统计学是数学的一个分支,研究数字数据的收集、分析、解释、表示和组织。

统计主要分为两个子分支:

  • 描述性统计:这些用于汇总数据,如连续数据类型(如年龄)的平均值、标准差,而频率和百分比用于分类数据(如性别)。
  • 推断统计:很多时候,收集整个数据(统计方法论中也叫总体)是不可能的,因此收集数据点的子集,也叫样本,会得出关于整个总体的结论,这就是所谓的推断统计。推断是使用假设检验、数字特征的估计、数据内关系的相关性等得出的。

统计建模是对数据进行统计,通过分析变量的重要性来发现潜在的隐藏关系。

机器学习

机器学习是计算机科学的一个分支,它利用过去的经验来学习和使用它的知识来做出未来的决定。机器学习是计算机科学、工程学和统计学的交叉。机器学习的目标是从给定的例子中归纳出一个可检测的模式或创建一个未知的规则。机器学习领域概述如下:

机器学习大致分为三类,但根据情况,这些类别可以结合起来,以实现特定应用的预期结果:

  • 监督学习:这是教机器学习其他变量和一个目标变量之间的关系,类似于老师给学生提供成绩反馈的方式。监督学习的主要部分如下:
    • 分类问题
    • 回归问题
  • 无监督学习:在无监督学习中,算法在没有任何监督或者没有提供任何目标变量的情况下自行学习。这是一个在给定数据中寻找隐藏模式和关系的问题。无监督学习中的类别如下:
    • 降维
    • 使聚集
  • 强化学习:这允许机器或代理基于来自环境的反馈来学习其行为。在强化学习中,代理人在没有监督的情况下采取一系列果断的行动,最终会得到奖励,要么是+1,要么是-1。基于最终的回报,代理重新评估其路径。强化学习问题更接近人工智能方法论,而不是常用的机器学习算法。

在某些情况下,当变量数量非常多时,我们首先执行无监督学习来降低维数,然后执行有监督学习。同样,在一些人工智能应用中,监督学习与强化学习相结合可以用来解决问题;一个例子是自动驾驶汽车,最初,使用监督学习将图像转换为某种数字格式,并与驾驶动作(向左、向前、向右和向后)相结合。

统计建模和机器学习的主要区别

虽然统计建模和机器学习方法之间有内在的相似性,但有时对于许多实践者来说并不明显。在下表中,我们简洁地解释了差异,以显示两个流相似的方式以及它们之间的差异:

| 统计建模 | 机器学习 |
| 数学方程形式的变量之间关系的形式化。 | 不依赖于基于规则的编程就能从数据中学习的算法。 |
| 在对数据执行模型拟合之前,需要假设模型曲线的形状(例如,线性、多项式等)。 | 不需要假设基础形状,因为机器学习算法可以基于提供的数据自动学习复杂的模式。 |
| 统计模型预测产量的准确率为 85%,置信度为 90%。 | 机器学习只是以 85%的准确率预测输出。 |
| 在统计建模中,对参数进行各种诊断,如 p 值等。 | 机器学习模型不执行任何统计诊断显著性测试。 |
| 数据将被分成 70%-30%,以创建培训和测试数据。根据训练数据开发模型,并根据测试数据进行测试。 | 数据将被分成 50%-25%-25%,以创建培训、验证和测试数据。基于训练和超参数开发的模型根据验证数据进行调整,最后根据测试数据进行评估。 |
| 统计模型可以在称为训练数据的单个数据集上开发,因为诊断是在整体精度和单个变量水平上执行的。 | 由于缺乏对变量的诊断,机器学习算法需要在两个数据集上进行训练,称为训练和验证数据,以确保两点验证。 |
| 统计建模主要用于研究目的。 | 机器学习非常适合在生产环境中实现。 |
| 来自统计和数学学院。 | 来自计算机科学学院。 |

机器学习模型开发和部署的步骤

为了开发、验证和实现机器学习模型,机器学习模型的开发和部署包括一系列几乎类似于统计建模过程的步骤。步骤如下:

  1. 数据的收集:机器学习的数据直接从结构化的源数据、网页报废、API、聊天交互等收集,因为机器学习可以对结构化和非结构化的数据(语音、图像和文本)进行工作。

  2. 数据准备和缺失/异常处理:数据按照选择的机器学习算法进行格式化;此外,缺失值处理需要通过用平均值/中值替换缺失值和异常值来执行,以此类推。

  3. 数据分析与特征工程:需要对数据进行分析,以便发现任何隐藏的模式和变量之间的关系等等。具有适当业务知识的正确特征工程将解决 70%的问题。此外,在实践中,数据科学家 70%的时间都花在功能工程任务上。

  4. 对训练和验证数据进行训练算法:后特征工程,在统计建模中将数据分为三大块(训练、验证和测试数据),而不是两块(训练和测试)。机器学习应用于训练数据,模型的超参数根据验证数据进行调整,以避免过度拟合。

  5. 在测试数据上测试算法:一旦模型在列车和验证数据上表现出足够好的性能,将对照看不见的测试数据检查其性能。如果表现仍然足够好,我们可以进行下一步,也是最后一步。

  6. 部署算法:训练好的机器学习算法将部署在直播流数据上,对结果进行分类。一个例子是由电子商务网站实现的推荐系统。

模型建立和验证的统计基础和术语

统计学本身是一门庞大的学科,可以写一本完整的书;然而,这里的尝试是集中在机器学习角度非常必要的关键概念上。在这一节中,涵盖了一些基础知识,其余的概念将在后面的章节中涵盖,只要有必要理解机器学习的统计等价物。

预测分析依赖于一个主要假设:历史会重演!

通过在验证关键度量后对历史数据拟合预测模型,相同的模型将被用于基于对过去数据有意义的相同解释变量来预测未来事件。

统计模型实施者的先行者是银行业和制药业;在一段时间内,分析也扩展到了其他行业。

统计模型是一类数学模型,通常由将一个或多个变量与近似现实联系起来的数学方程来指定。统计模型所包含的假设描述了一组概率分布,这将它与非统计、数学或机器学习模型区分开来

统计模型总是从一些潜在的假设开始,所有的变量都应该适用于这些假设,那么模型提供的性能在统计上是显著的。因此,了解所有构建模块中涉及的各种点点滴滴为成为一名成功的统计学家提供了坚实的基础。

在下一节中,我们用相关代码描述了各种基本原理:

  • 人口:这是总体,观察的完整列表,或者关于研究对象的所有数据点。
  • 样本:样本是人群的子集,通常是正在分析的人群的一小部分。

Usually, it is expensive to perform an analysis on an entire population; hence, most statistical methods are about drawing conclusions about a population by analyzing a sample.

  • 参数与统计:任何基于总体计算的度量都是一个参数,而在样本上,它被称为统计
  • 平均值:这是一个简单的算术平均值,通过取值的总和除以这些值的计数来计算。平均值对数据中的异常值很敏感。离群值是指集合或列的值与同一数据中的许多其他值高度偏离;它通常有很高或很低的值。
  • 中位数:这是数据的中点,按照升序或者降序排列计算。如果有 N 的观察。
  • 模式:这是数据中重复次数最多的数据点:

使用numpy数组和stats包计算平均值、中值和模式的 Python 代码如下:

>>> import numpy as np 
>>> from scipy import stats 

>>> data = np.array([4,5,1,2,7,2,6,9,3]) 

# Calculate Mean 
>>> dt_mean = np.mean(data) ; print ("Mean :",round(dt_mean,2)) 

# Calculate Median 
>>> dt_median = np.median(data) ; print ("Median :",dt_median)          

# Calculate Mode 
>>> dt_mode =  stats.mode(data); print ("Mode :",dt_mode[0][0])                    

前面代码的输出如下:

We have used a NumPy array instead of a basic list as the data structure; the reason behind using this is the scikit-learn package built on top of NumPy array in which all statistical models and machine learning algorithms have been built on NumPy array itself. The mode function is not implemented in the numpy package, hence we have used SciPy's stats package. SciPy is also built on top of NumPy arrays.

描述性统计(平均值、中位数和模式)的 R 代码如下:

data <- c(4,5,1,2,7,2,6,9,3) 
dt_mean = mean(data) ; print(round(dt_mean,2)) 
dt_median = median (data); print (dt_median) 

func_mode <- function (input_dt) { 
  unq <- unique(input_dt)  unq[which.max(tabulate(match(input_dt,unq)))] 
} 

dt_mode = func_mode (data); print (dt_mode) 

We have used the default stats package for R; however, the mode function was not built-in, hence we have written custom code for calculating the mode.

  • 变异的度量:离差是数据中的变异,衡量数据中变量值的不一致性。分散实际上提供了一个关于传播的概念,而不是中心价值。
  • 范围:这是数值的最大值和最小值之差。
  • 方差:这是与均值的方差的均值( xi =数据点,T5】=数据的均值, N =数据点个数)。方差的维度是实际值的平方。在群体中使用分母 N-1 代替 N 作为样本的原因是由于自由度。 1 计算方差时,样本中自由度的损失是由于样本替代的提取:

  • 标准差:这是方差的平方根。通过对方差应用平方根,我们测量相对于原始变量的离差,而不是维度的平方:

  • 分位数:这些只是数据中完全相同的片段。分位数包括百分位数、十分位数、四分位数等等。这些度量是在按升序排列数据后计算的:
    • 百分位数:这不过是数据点低于原始整体数据值的百分比。中位数是第 50 个百分位,因为低于中位数的数据点数量约为数据的 50%。
    • 十分位数:这是第 10 个百分位数,表示十分位数以下的数据点数量是整个数据的 10%。
    • 四分位数:这是数据的四分之一,也是第 25 百分位。第一个四分位数是数据的 25%,第二个四分位数是数据的 50%,第三个四分位数是数据的 75%。第二个四分位数也被称为中位数或第 50 百分位数或第 5 十分位数。
    • 四分位数区间:这是第三个四分位数和第一个四分位数的差值。它能有效识别数据中的异常值。四分位数范围描述了中间 50%的数据点。

Python 代码如下:

>>> from statistics import variance, stdev 
>>> game_points = np.array([35,56,43,59,63,79,35,41,64,43,93,60,77,24,82]) 

# Calculate Variance 
>>> dt_var = variance(game_points) ; print ("Sample variance:", round(dt_var,2)) 

# Calculate Standard Deviation 
>>> dt_std = stdev(game_points) ; print ("Sample std.dev:", round(dt_std,2)) 

# Calculate Range 
>>> dt_rng = np.max(game_points,axis=0) - np.min(game_points,axis=0) ; print ("Range:",dt_rng) 

#Calculate percentiles 
>>> print ("Quantiles:") 
>>> for val in [20,80,100]: 
>>>      dt_qntls = np.percentile(game_points,val)  
>>>      print (str(val)+"%" ,dt_qntls) 

# Calculate IQR                             
>>> q75, q25 = np.percentile(game_points, [75 ,25]); print ("Inter quartile range:",q75-q25) 

前面代码的输出如下:

离差(方差、标准差、范围、分位数和 IQR)的 R 码如下:

game_points <- c(35,56,43,59,63,79,35,41,64,43,93,60,77,24,82) 
dt_var = var(game_points); print(round(dt_var,2)) 
dt_std = sd(game_points); print(round(dt_std,2)) 
range_val<-function(x) return(diff(range(x)))  
dt_range = range_val(game_points); print(dt_range) 
dt_quantile = quantile(game_points,probs = c(0.2,0.8,1.0)); print(dt_quantile) 
dt_iqr = IQR(game_points); print(dt_iqr) 

  • 假设检验:这是通过对一个样本进行一些统计检验,对整体人群进行推断的过程。零假设和替代假设是验证假设是否具有统计学意义的方法。
  • P 值:假设零假设为真(通常在建模中,针对每个自变量,小于 0.05 的 P 值被认为是显著的,大于 0.05 的 P 值被认为是不显著的),获得检验统计结果的概率至少和实际观察到的一样极端;尽管如此,这些值和定义可以根据上下文而改变)。

假设检验的步骤如下:

  • 假设检验的例子:一个巧克力制造商,也是你的朋友,声称他工厂生产的所有巧克力至少有 1000g 重,你有一种奇怪的感觉,这可能不是真的;你们都采集了 30 块巧克力的样品,发现平均巧克力重量为 990 g,样品标准偏差为 12.5 g,给定 0.05 的显著性水平,我们可以拒绝你朋友提出的索赔吗?

无效假设为 0 ≥ 1000 (所有巧克力重量均超过 1000)。

收集的样本:

计算测试统计:

t =(990-1000)/(12.5/sqrt(30))= 4,3818

t 表中的临界 t 值= t0.05,30 = 1.699 = > - t0.05,30 = -1.699

P 值= 7.03 e-05

测试统计为 -4.3818 ,小于 -1.699 的临界值。因此,我们可以拒绝零假设(你朋友的说法),即巧克力的平均重量超过 1000 克。

另外,决定索赔的另一种方法是使用 p 值。小于 0.05 的 p 值意味着索赔值和分布平均值显著不同,因此我们可以拒绝零假设:

Python 代码如下:

>>> from scipy import stats  
>>> xbar = 990; mu0 = 1000; s = 12.5; n = 30 

# Test Statistic 
>>> t_smple  = (xbar-mu0)/(s/np.sqrt(float(n))); print ("Test Statistic:",round(t_smple,2)) 

# Critical value from t-table 
>>> alpha = 0.05 
>>> t_alpha = stats.t.ppf(alpha,n-1); print ("Critical value from t-table:",round(t_alpha,3))           

#Lower tail p-value from t-table                          
>>> p_val = stats.t.sf(np.abs(t_smple), n-1); print ("Lower tail p-value from t-table", p_val)  

t 分布的 R 码如下:

xbar = 990; mu0 = 1000; s = 12.5 ; n = 30 
t_smple = (xbar - mu0)/(s/sqrt(n));print (round(t_smple,2)) 

alpha = 0.05 
t_alpha = qt(alpha,df= n-1);print (round(t_alpha,3)) 

p_val = pt(t_smple,df = n-1);print (p_val) 

  • ⅰ型和ⅱ型错误:假设检验通常是对样本而不是整个人群进行的,这是由于收集所有可用数据的可用资源的实际限制。然而,当样本量的增加导致第一类和第二类误差最小化时,从样本中进行总体推断有其自身的成本,例如拒绝好的结果或接受错误的结果,更不用说单独使用了:
    • 第一类错误:当无效假设为真时,拒绝该假设
    • 第二类错误:接受虚假的零假设
  • 正态分布:这在统计学中非常重要,因为中心极限定理指出,来自具有均值 μ 和方差 σ2 的总体的所有可能样本大小 n 的总体接近正态分布:

例:假设入学考试的考试成绩符合正态分布。此外,平均测试分数为 52 ,标准偏差为 16.3 。考试中得分 67 以上的学生比例是多少?

Python 代码如下:

>>> from scipy import stats 
>>> xbar = 67; mu0 = 52; s = 16.3 

# Calculating z-score 
>>> z = (67-52)/16.3  

# Calculating probability under the curve     
>>> p_val = 1- stats.norm.cdf(z) 
>>> print ("Prob. to score more than 67 is ",round(p_val*100,2),"%") 

正态分布的 R 码如下:

xbar = 67; mu0 = 52; s = 16.3 
pr = 1- pnorm(67, mean=52, sd=16.3) 
print(paste("Prob. to score more than 67 is ",round(pr*100,2),"%")) 

  • 卡方:这种独立性检验是分类数据统计分析中最基本、最常见的假设检验之一。给定两个分类随机变量 XY ,独立性的卡方检验决定了它们之间是否存在统计相关性。

测试通常通过从数据计算 χ2 和从表格计算( m-1n-1 )度的 χ2 来进行。根据实际值和表值(以较高者为准),决定两个变量是否独立:

示例:在下表中,计算吸烟习惯是否对锻炼行为有影响:

Python 代码如下:

>>> import pandas as pd 
>>> from scipy import stats 

>>> survey = pd.read_csv("survey.csv")   

# Tabulating 2 variables with row & column variables respectively 
>>> survey_tab = pd.crosstab(survey.Smoke, survey.Exer, margins = True) 

使用crosstab函数创建表格时,我们将额外获得行和列合计字段。然而,为了创建观察到的表,我们需要提取变量部分并忽略总数:

# Creating observed table for analysis 
>>> observed = survey_tab.ix[0:4,0:3]  

stats 包中的chi2_contingency函数使用观察到的表,并随后计算其预期的表,然后计算 p 值,以检查两个变量是否相关。如果 p 值< 0.05 ,两个变量之间有很强的依赖性,而如果 p 值> 0.05 ,变量之间没有依赖性:

>>> contg = stats.chi2_contingency(observed= observed) 
>>> p_value = round(contg[1],3) 
>>> print ("P-value is: ",p_value) 

p 值为0.483,表示吸烟习惯和运动行为之间没有依赖性。

卡方的 R 码如下:

survey = read.csv("survey.csv",header=TRUE) 
tbl = table(survey$Smoke,survey$Exer) 
p_val = chisq.test(tbl) 

  • 方差分析:方差分析检验两个或两个以上总体均值相等的假设。ANOVAs 通过比较不同因素水平的反应变量均值来评估一个或多个因素的重要性。零假设认为所有的总体平均值都是相等的,而另一种假设认为至少有一个是不同的。

一家肥料公司经过研究开发出三种新型通用肥料,可用于种植任何类型的作物。为了找出这三种作物的产量是否相似,他们在研究中随机选择了六种作物类型。根据随机区组设计,每种作物类型将分别用所有三种肥料进行测试。下表以克/米 2 为单位表示产量。在 0.05 的显著性水平上,测试三种新型肥料的平均产量是否相等:

| 肥料 1 | 肥料 2 | 肥料 3 |
| Sixty-two | Fifty-four | Forty-eight |
| Sixty-two | Fifty-six | Sixty-two |
| Ninety | Fifty-eight | Ninety-two |
| forty-two | Thirty-six | Ninety-six |
| Eighty-four | seventy-two | Ninety-two |
| Sixty-four | Thirty-four | Eighty |

Python 代码如下:

>>> import pandas as pd 
>>> from scipy import stats 
>>> fetilizers = pd.read_csv("fetilizers.csv") 

使用stats包计算单向方差分析:

>>> one_way_anova = stats.f_oneway(fetilizers["fertilizer1"], fetilizers["fertilizer2"], fetilizers["fertilizer3"]) 

>>> print ("Statistic :", round(one_way_anova[0],2),", p-value :",round(one_way_anova[1],3)) 

结果:p 值确实小于 0.05,因此我们可以拒绝肥料的平均作物产量相等的无效假设。肥料对农作物有很大的影响。

方差分析的 R 码如下:

fetilizers = read.csv("fetilizers.csv",header=TRUE) 
r = c(t(as.matrix(fetilizers))) 
f = c("fertilizer1","fertilizer2","fertilizer3") 
k = 3; n = 6 
tm = gl(k,1,n*k,factor(f)) 
blk = gl(n,k,k*n) 
av = aov(r ~ tm + blk) 
smry = summary(av) 

  • 混淆矩阵:这是实际与预测的矩阵。这个概念可以通过使用该模型进行癌症预测的例子得到更好的解释:

混淆矩阵中使用的一些术语包括:

(TP/TP+FP)

(TP/TP+fn)

(TN/TN+FP)

利用曲线下面积设定截止概率阈值,将预测概率分为不同类别;我们将在接下来的章节中介绍这种方法是如何工作的。

  • 观察和表现窗口:在统计建模中,模型试图提前而不是当下预测事件,这样就会存在一些缓冲时间来进行纠正措施。例如,信用卡公司的一个问题是,例如,特定客户在未来 12 个月内违约的概率有多大?这样我就可以给他打电话,提供任何折扣或相应地制定我的收藏策略。

为了回答这个问题,需要使用过去 24 个月的自变量和未来 12 个月的因变量来开发违约概率模型(或技术术语中的行为记分卡)。用X**Y变量准备数据后,随机拆分成 70%-30%作为训练和测试数据;这种方法被称为及时验证,因为列车样本和测试样本来自同一时间段:

  • 时间内和时间外验证:时间内验证意味着从同一时间段获得训练和测试数据集,而时间外验证意味着从不同时间段获得训练和测试数据集。通常,由于训练数据集和测试数据集的特征可能不同这一明显的原因,模型在非实时验证中的表现比在实时验证中的表现更差。
  • R 平方(决定系数):这是由模型解释的响应变量变化百分比的度量。这也是一种衡量模型与仅利用平均值作为估计值相比将误差降至最低程度的方法。在某些极端情况下,R 平方的值也可能小于零,这意味着模型的预测值比仅将简单平均值作为所有观测值的预测值表现更差。我们将在接下来的章节中详细研究这个参数:

  • 调整后的 R 平方:调整后的 R 平方统计量的解释几乎与 R 平方相同,但是如果模型中包含没有强相关性的额外变量,它会惩罚 R 平方值:

这里, R2 =样本 R 平方值, n =样本量, k =预测因子(或)变量数。

调整后的 R 平方值是评价线性回归质量的关键指标。任何具有值 R2 调整后的> = 0.7 的线性回归模型都被认为是足够好的实施模型。

例:样本的 R 平方值为 0.5,,样本量为 50 ,自变量数量为 10 。计算调整后的 R 平方:

  • 最大似然估计(MLE) :这是通过寻找最大化观察可能性的参数值来估计统计模型(准确地说是逻辑回归)的参数值。我们将在第 3 章逻辑回归与随机森林中更深入地介绍这种方法。
  • 阿卡克信息准则(AIC) :这在逻辑回归中使用,类似于线性回归的调整 R-square 原理。它衡量给定数据集的模型的相对质量:

这里, k =预测因子或变量的数量

AIC 的想法是,如果模型中包含没有很强预测能力的额外变量,就惩罚目标函数。这是逻辑回归中的一种正则化。

  • :这个来自信息论,是对数据中杂质的度量。如果样本完全齐次,熵为零,如果样本等分,则熵为 1 。在决策树中,具有最大异质性的预测器将被认为最接近根节点,以贪婪模式将给定数据分类。我们将在第 4 章基于树的机器学习模型中更深入地讨论这个主题:

这里, n =类数。熵在中间最大,值为 1 ,在极端值为 0 时最小。较低的熵值是可取的,因为它将更好地隔离类别:

例如:给定两种硬币,其中第一种是公平的( 1/2 头和 1/2 尾概率),另一种是有偏差的( 1/3 头和 2/3 尾概率),计算两者的熵,并证明哪一种在建模方面更好:

从这两个值中,决策树算法选择有偏硬币而不是公平硬币作为观察分割器,因为熵的值较少。

  • 信息增益:这是根据给定的属性对示例进行划分所导致的熵的预期降低。这个想法是从混合类开始,并保持分区,直到每个节点都观察到最纯的类。在每个阶段,以贪婪的方式选择具有最大信息增益的变量:

*信息增益=父项熵-和(加权% 子项熵)

加权% =特定子节点中的观察数/总和(所有子节点中的观察数)

  • 基尼:基尼杂质是一种错误分类的度量,适用于多类分类器上下文。基尼系数的工作原理与熵几乎相同,只是基尼系数的计算速度更快:

这里, i =类数。基尼系数和熵之间的相似性如下所示:

偏差与方差的权衡

除了白噪声,每个模型都有偏差和方差误差分量。偏差和方差彼此成反比;当试图减少一个组件时,模型的另一个组件将增加。真正的艺术在于通过平衡两者来创造良好的契合。理想的模型将具有低偏差和低方差。

偏差部分的误差来自基础学习算法中的错误假设。高偏差会导致算法错过特征和目标输出之间的相关关系;这种现象导致了装配不足的问题。

另一方面,方差分量的误差来自对模型拟合变化的敏感性,甚至是训练数据的微小变化;高方差会导致过拟合问题:

高偏差模型的一个例子是逻辑回归或线性回归,其中模型的拟合仅仅是一条直线,并且由于线性模型不能很好地逼近基础数据的事实,可能具有高误差分量。

高方差模型的一个例子是决策树,其中模型可能会创建太多的摆动曲线作为拟合,其中即使训练数据的微小变化也会导致曲线拟合的剧烈变化。

目前,最先进的模型正在利用高方差模型,如决策树,并在它们之上执行集成,以减少由高方差引起的误差,同时不损害由偏差分量引起的误差增加。这个类别最好的例子是随机森林,其中许多决策树将独立生长和集成,以便得出最佳匹配;我们将在接下来的章节中介绍这一点:

培训和测试数据

在实践中,在统计建模中,数据通常会被随机分成 70-30 或 80-20 个训练和测试数据集,其中用于建立模型的训练数据及其有效性将在测试数据上进行检查:

在下面的代码中,我们将原始数据分成 70%-30%的训练和测试数据。这里需要考虑的一个要点是,我们为随机数设置种子值,以便每次在训练和测试数据中创建相同的观察值时重复随机采样。重现结果非常需要重复性:

# Train & Test split 
>>> import pandas as pd       
>>> from sklearn.model_selection import train_test_split 

>>> original_data = pd.read_csv("mtcars.csv")      

在下面的代码中,train size0.7,这意味着 70%的数据应该被分割到训练数据集中,剩下的 30%应该在测试数据集中。随机状态是生成伪随机数过程中的种子,这使得每次运行时通过拆分完全相同的观察结果可以重复:

>>> train_data,test_data = train_test_split(original_data,train_size = 0.7,random_state=42) 

用于统计建模的列车和测试分离的 R 代码如下:

full_data = read.csv("mtcars.csv",header=TRUE) 
set.seed(123) 
numrow = nrow(full_data) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 
train_data = full_data[trnind,] 
test_data = full_data[-trnind,] 

模型构建和验证的机器学习术语

统计建模和机器学习之间似乎有相似之处,我们将在后续章节中深入讨论。然而,已经提供了如下快速视图:在统计建模中,具有两个独立变量的线性回归试图拟合具有最少误差的最佳平面,而在机器学习中,独立变量已经被转换成误差项的平方(平方确保函数将变得凸,这增强了更快的收敛并且还确保了全局最优),并且基于系数值而不是独立变量进行优化:

机器学习利用优化来调整各种算法的所有参数。因此,了解一些关于优化的基础知识是一个好主意。

在步入梯度下降之前,引入凸函数和非凸函数是非常有帮助的。凸函数是这样的函数,其中函数上任意两个随机点之间的线也位于函数内,而非凸函数则不是这样。重要的是要知道函数是凸的还是非凸的,因为在凸函数中,局部最优也是全局最优,而对于非凸函数,局部最优不能保证全局最优:

这看起来是个棘手的问题吗?一个转变可能是在不同的随机地点启动搜索过程;这样,它通常会收敛到全局最优值:

  • 梯度下降:这是一种最小化由模型参数θ**εRd参数化的目标函数J(θ)的方法,方法是在与目标函数相对于参数的梯度相反的方向上更新参数。学习率决定了达到最小值所采取的步骤的大小。
  • 整批梯度下降(每次迭代考虑的所有训练观测值):整批梯度下降中,每次迭代考虑所有观测值;这种方法会占用大量内存,并且速度也会很慢。此外,在实践中,我们不需要所有的观察来更新权重。尽管如此,这种方法还是以巨大的计算量为代价,以较少的噪声提供了更新参数的最佳方式。
  • 随机梯度下降(每次迭代一个观测值):该方法通过在每个迭代阶段取一个观测值来更新权重。该方法提供了遍历权重的最快方法;然而,收敛时会产生大量噪音。
  • 迷你批量梯度下降(每次迭代约 30 个训练观察或更多):这是巨大计算成本和快速更新权重方法之间的权衡。在这种方法中,在每次迭代中,将随机选择大约 30 个观测值,并计算梯度来更新模型权重。在这里,很多人可能会问,为什么最低 30,而不是任何其他数字?如果我们研究统计基础,需要考虑 30 个观察,以便将样本近似为总体。然而,即使是 40、50 等也会在批量选择上做得很好。尽管如此,从业者需要改变批次大小并验证结果,以确定模型以什么值产生最佳结果:

线性回归与梯度下降

在下面的代码中,对同一数据集上以统计方式应用线性回归和以机器学习方式应用梯度下降进行了比较:

>>> import numpy as np 
>>> import pandas as pd 

以下代码描述了使用熊猫数据框读取数据:

>>> train_data = pd.read_csv("mtcars.csv")      

将 DataFrame 变量转换为 NumPy 数组,以便在 scikit learn 包中处理它们,因为 scikit-learn 是基于 NumPy 数组本身构建的,接下来将显示:

>>> X = np.array(train_data["hp"])  ; y = np.array(train_data["mpg"])  
>>> X = X.reshape(32,1); y = y.reshape(32,1) 

从 scikit-learn 包导入线性回归;这适用于最小二乘法:

>>> from sklearn.linear_model import LinearRegression 
>>> model = LinearRegression(fit_intercept = True) 

对数据拟合线性回归模型,显示单变量(hp变量)的截距和系数:

>>> model.fit(X,y) 
>>> print ("Linear Regression Results" ) 
>>> print ("Intercept",model.intercept_[0] ,"Coefficient", model.coef_[0]) 

现在我们将从头开始应用梯度下降;在未来的章节中,我们可以使用 scikit-learn 内置模块,而不是从基本原则开始。然而,在这里,已经提供了关于优化方法的内部工作的说明,整个机器学习已经建立在该优化方法上。

定义梯度下降函数gradient_descent如下:

  • x:自变量。
  • y:因变量。
  • learn_rate:梯度更新的学习速率;过低会导致收敛变慢,过高会导致梯度过冲。
  • batch_size:每次迭代更新梯度时考虑的观测数;数字越大,迭代次数越少,数字越小,误差越小。理想情况下,由于统计显著性,批次大小应为最小值 30。但是,需要尝试各种设置来检查哪一个更好。
  • max_iter:最大迭代次数,超过该次数算法将自动终止:
>>> def gradient_descent(x, y,learn_rate, conv_threshold,batch_size, max_iter): 
...    converged = False 
...    iter = 0 
...    m = batch_size   
...    t0 = np.random.random(x.shape[1]) 
...    t1 = np.random.random(x.shape[1]) 

Mean square error calculation
Squaring of error has been performed to create the convex function, which has nice convergence properties:
...    MSE = (sum([(t0 + t1*x[i] - y[i])**2 for i in range(m)])/ m)

下面的代码声明,运行算法直到它不满足收敛标准:

...      while not converged:         
...          grad0 = 1.0/m * sum([(t0 + t1*x[i] - y[i]) for i in range(m)])   
...          grad1 = 1.0/m * sum([(t0 + t1*x[i] - y[i])*x[i] for i in range(m)])  
...          temp0 = t0 - learn_rate * grad0 
...          temp1 = t1 - learn_rate * grad1     
...          t0 = temp0 
...          t1 = temp1 

用更新的参数计算新的误差,以便检查新的误差变化是否超过预定的收敛阈值;否则,停止迭代并返回参数:

...          MSE_New = (sum( [ (t0 + t1*x[i] - y[i])**2 for i in range(m)] ) / m) 
...          if abs(MSE - MSE_New ) <= conv_threshold: 
...              print 'Converged, iterations: ', iter 
...              converged = True     
...          MSE = MSE_New    
...          iter += 1      
...          if iter == max_iter: 
...              print 'Max interactions reached' 
...              converged = True 
...          return t0,t1 

以下代码描述了使用定义的值运行梯度下降函数。学习率= 0.0003,收敛阈值= 1e-8,批量= 32,最大迭代次数= 1500000:

>>> if __name__ == '__main__': 
...      Inter, Coeff = gradient_descent(x = X,y = y,learn_rate=0.00003 , conv_threshold = 1e-8, batch_size=32,max_iter=1500000) 
...      print ('Gradient Descent Results')  
...      print (('Intercept = %s Coefficient = %s') %(Inter, Coeff))  

线性回归相对于梯度下降的 R 代码如下:

# Linear Regression 
train_data = read.csv("mtcars.csv",header=TRUE) 
model <- lm(mpg ~ hp, data = train_data) 
print (coef(model)) 

# Gradient descent 
gradDesc <- function(x, y, learn_rate, conv_threshold, batch_size, max_iter) { 
  m <- runif(1, 0, 1) 
  c <- runif(1, 0, 1) 
  ypred <- m * x + c 
  MSE <- sum((y - ypred) ^ 2) / batch_size 

  converged = F 
  iterations = 0 

  while(converged == F) { 
    m_new <- m - learn_rate * ((1 / batch_size) * (sum((ypred - y) * x))) 
    c_new <- c - learn_rate * ((1 / batch_size) * (sum(ypred - y))) 

    m <- m_new 
    c <- c_new 
    ypred <- m * x + c
 MSE_new <- sum((y - ypred) ^ 2) / batch_size

 if(MSE - MSE_new <= conv_threshold) {
 converged = T
 return(paste("Iterations:",iterations,"Optimal intercept:", c, "Optimal slope:", m))
 }
 iterations = iterations + 1

 if(iterations > max_iter) {
 converged = T
 return(paste("Iterations:",iterations,"Optimal intercept:", c, "Optimal slope:", m))
 }
 MSE = MSE_new
 }
}
gradDesc(x = train_data$hp,y =  train_data$mpg, learn_rate = 0.00003, conv_threshold = 1e-8, batch_size = 32, max_iter = 1500000)

机器学习损失

机器学习中的损失函数或成本函数是将变量的值映射到一个实数上的函数,该实数直观地表示与变量值相关的一些成本。优化方法通过改变参数值来最小化损失函数,这是机器学习的中心主题。

零一损耗为L0-1 = 1(m<= 0);在零一损失中, m >的损失值为0= 0,而 1 的损失值为 m < 0 。这种损失的难点在于它是不可微的、非凸的,并且还是 NP 难的。因此,为了使优化可行和可解,这些损失被不同问题的不同替代损失所代替。

用于机器学习代替零一损失的替代损失如下。零一损耗不可微,因此使用近似损耗来代替:

  • 平方损失(用于回归)
  • 铰链损耗(SVM)
  • 逻辑/对数损失(逻辑回归)

一些损失函数如下:

何时停止调整机器学习模型

何时停止调整机器学习模型中的超参数是一个价值百万美元的问题。这个问题大部分可以通过关注训练和测试错误来解决。在增加模型复杂性的同时,会出现以下几个阶段:

  • 阶段 1 :欠拟合阶段-高列车和高测试误差(或低列车和低测试精度)
  • 阶段 2 :良好拟合阶段(理想场景)-低列车和低测试误差(或高列车和高测试精度)
  • 阶段 3 :过拟合阶段-低列车和高测试误差(或高列车和低测试精度)

培训、验证和测试数据

由于多种原因,交叉验证在统计建模领域并不流行;统计模型本质上是线性的和稳健的,并且没有高方差/过拟合问题。因此,模型拟合将在训练或测试数据上保持不变,这在机器学习世界中并不成立。此外,在统计建模中,除了聚合度量之外,许多测试都是在单个参数级别执行的,而在机器学习中,我们在单个参数级别没有可见性:

在下面的代码中,提供了 R 和 Python 的实现。如果未提供任何百分比,则默认参数为列车数据 50%,验证数据 25%,其余测试数据 25%。

Python 实现只有一个训练和测试分割功能,因此我们使用了两次,并且还使用了要分割的观察数而不是百分比(如前面的训练和测试分割示例所示)。因此,需要一个定制的函数来分割成三个数据集:

>>> import pandas as pd       
>>> from sklearn.model_selection import train_test_split               

>>> original_data = pd.read_csv("mtcars.csv")                    

>>> def data_split(dat,trf = 0.5,vlf=0.25,tsf = 0.25): 
...      nrows = dat.shape[0]     
...      trnr = int(nrows*trf) 
...      vlnr = int(nrows*vlf)     

下面的 Python 代码将数据分为训练数据和剩余数据。剩余的数据将进一步分为验证和测试数据集:

...      tr_data,rmng = train_test_split(dat,train_size = trnr,random_state=42) 
...      vl_data, ts_data = train_test_split(rmng,train_size = vlnr,random_state=45)     
...      return (tr_data,vl_data,ts_data) 

对原始数据执行拆分功能以创建三个数据集(按 50%、25%和 25%拆分)如下:

>>> train_data, validation_data, test_data = data_split (original_data ,trf=0.5, vlf=0.25,tsf=0.25) 

列车、验证和测试分离的代码如下:

# Train Validation & Test samples 
trvaltest <- function(dat,prop = c(0.5,0.25,0.25)){ 
  nrw = nrow(dat) 
  trnr = as.integer(nrw *prop[1]) 
  vlnr = as.integer(nrw*prop[2]) 
  set.seed(123) 
  trni = sample(1:nrow(dat),trnr) 
  trndata = dat[trni,] 
  rmng = dat[-trni,] 
  vlni = sample(1:nrow(rmng),vlnr) 
  valdata = rmng[vlni,] 
  tstdata = rmng[-vlni,] 
  mylist = list("trn" = trndata,"val"= valdata,"tst" = tstdata) 
  return(mylist) 
} 
outdata = trvaltest(mtcars,prop = c(0.5,0.25,0.25)) 
train_data = outdata$trn; valid_data = outdata$val; test_data = outdata$tst 

交叉验证

交叉验证是以计算为代价确保模型稳健性的另一种方式。在普通的建模方法中,基于列车数据开发模型,并基于测试数据进行评估。在一些极端情况下,训练和测试可能没有被均匀地选择,一些看不见的极端情况可能出现在测试数据中,这将拖累模型的性能。

另一方面,在交叉验证方法中,数据被分成相等的部分,并对数据的所有其他部分进行训练,只有一部分除外,将对其性能进行评估。用户选择了多少零件,这个过程就重复多少次。

示例:在五重交叉验证中,将数据分为五部分,随后对四部分数据进行训练,并对一部分数据进行测试。这个过程将运行五次,以便覆盖数据中的所有点。最后,计算的误差将是所有误差的平均值:

网格搜索

机器学习中的网格搜索是一种流行的方法,用于调整模型的超参数,以便找到确定最佳拟合的最佳组合:

在下面的代码中,已经执行了实现来确定特定用户是否会点击广告。网格搜索已经使用决策树分类器来实现分类。调整参数是树的深度、终端节点中的最小观察数以及执行节点拆分所需的最小观察数:

# Grid search 
>>> import pandas as pd 
>>> from sklearn.tree import DecisionTreeClassifier 
>>> from sklearn.model_selection import train_test_split 
>>> from sklearn.metrics import classification_report,confusion_matrix,accuracy_score 
>>> from sklearn.pipeline import Pipeline 
>>> from sklearn.grid_search import GridSearchCV 

>>> input_data = pd.read_csv("ad.csv",header=None)                        

>>> X_columns = set(input_data.columns.values) 
>>> y = input_data[len(input_data.columns.values)-1] 
>>> X_columns.remove(len(input_data.columns.values)-1) 
>>> X = input_data[list(X_columns)] 

将数据分为训练和测试两部分:

>>> X_train, X_test,y_train,y_test = train_test_split(X,y,train_size = 0.7,random_state=33) 

创建一个管道,为网格搜索创建变量组合:

>>> pipeline = Pipeline([ 
...      ('clf', DecisionTreeClassifier(criterion='entropy')) ]) 

要探索的组合以 Python 字典格式作为参数给出:

>>> parameters = { 
...      'clf__max_depth': (50,100,150), 
...      'clf__min_samples_split': (2, 3), 
...      'clf__min_samples_leaf': (1, 2, 3)} 

n_jobs字段用于选择计算机中的核心数量;-1表示它使用计算机中的所有核心。评分方法是准确性,可以选择很多其他选项,如precisionrecallf1:

>>> grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, scoring='accuracy') 
>>> grid_search.fit(X_train, y_train)  

使用网格搜索的最佳参数进行预测:

>>> y_pred = grid_search.predict(X_test) 

输出如下:

>>> print ('\n Best score: \n', grid_search.best_score_) 
>>> print ('\n Best parameters set: \n')   
>>> best_parameters = grid_search.best_estimator_.get_params() 
>>> for param_name in sorted(parameters.keys()): 
>>>     print ('\t%s: %r' % (param_name, best_parameters[param_name])) 
>>> print ("\n Confusion Matrix on Test data \n",confusion_matrix(y_test,y_pred)) 
>>> print ("\n Test Accuracy \n",accuracy_score(y_test,y_pred)) 
>>> print ("\nPrecision Recall f1 table \n",classification_report(y_test, y_pred)) 

决策树上网格搜索的代码如下:

# Grid Search on Decision Trees 
library(rpart) 
input_data = read.csv("ad.csv",header=FALSE) 
input_data$V1559 = as.factor(input_data$V1559) 
set.seed(123) 
numrow = nrow(input_data) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 

train_data = input_data[trnind,];test_data = input_data[-trnind,] 
minspset = c(2,3);minobset = c(1,2,3) 
initacc = 0 

for (minsp in minspset){ 
  for (minob in minobset){ 
    tr_fit = rpart(V1559 ~.,data = train_data,method = "class",minsplit = minsp, minbucket = minob) 
    tr_predt = predict(tr_fit,newdata = train_data,type = "class") 
    tble = table(tr_predt,train_data$V1559) 
    acc = (tble[1,1]+tble[2,2])/sum(tble) 
    acc 
    if (acc > initacc){ 
      tr_predtst = predict(tr_fit,newdata = test_data,type = "class") 
      tblet = table(test_data$V1559,tr_predtst) 
      acct = (tblet[1,1]+tblet[2,2])/sum(tblet) 
      acct 
      print(paste("Best Score")) 
      print( paste("Train Accuracy ",round(acc,3),"Test Accuracy",round(acct,3))) 
      print( paste(" Min split ",minsp," Min obs per node ",minob)) 
      print(paste("Confusion matrix on test data")) 
      print(tblet) 
      precsn_0 = (tblet[1,1])/(tblet[1,1]+tblet[2,1]) 
      precsn_1 = (tblet[2,2])/(tblet[1,2]+tblet[2,2]) 
      print(paste("Precision_0: ",round(precsn_0,3),"Precision_1: ",round(precsn_1,3))) 
      rcall_0 = (tblet[1,1])/(tblet[1,1]+tblet[1,2]) 
      rcall_1 = (tblet[2,2])/(tblet[2,1]+tblet[2,2]) 
      print(paste("Recall_0: ",round(rcall_0,3),"Recall_1: ",round(rcall_1,3))) 
      initacc = acc 
    } 
  } 
} 

机器学习模型概述

机器学习模型主要分为有监督、无监督和强化学习方法。我们将在后面的章节中详细讨论每种技术;下面是对它们的一个非常基本的总结:

  • 监督式学习:这是教师向学生提供反馈,说明他们在考试中是否表现良好。其中目标变量确实存在,模型确实得到了调整以实现它。许多机器学习方法都属于这一类:
    • 分类问题
    • 逻辑回归
    • 套索和岭回归
    • 决策树(分类树)
    • 装袋分级机
    • 随机森林分类器
    • 增强分类器(adaboost、梯度增强和 xboost)
    • SVM 分类器
    • 推荐引擎
    • 回归问题
    • 线性回归(套索和岭回归)
    • 决策树(回归树)
    • 装袋回归器
    • 随机森林回归器
    • 升压回归器- (adaboost、梯度升压和 xboost)
    • SVM 回归器
  • 无监督学习:类似于师生类比,教师不呈现,反馈给学生,学生需要自己准备。无监督学习没有监督学习中的那么多:
    • 主成分分析 ( 主成分分析
    • k-均值聚类
  • 强化学习:这是一个场景,其中一个代理在到达目标之前需要做出多个决策,并且它提供了一个奖励,要么是+1,要么是-1,而不是通知代理在路径上的表现有多好或有多差:
    • 马尔可夫决策过程
    • 蒙特卡罗方法
    • 时间差异学习
  • 逻辑回归:这是结果是离散类而不是连续值的问题。比如客户会不会到,会不会购买产品,等等。在统计学方法中,它使用最大似然法来计算单个变量的参数。相反,在机器学习方法中,相对于 β 系数(也称为权重),对数损失将被最小化。逻辑回归具有高偏差和低方差误差。
  • 线性回归:用于客户收入等连续变量的预测。它利用误差最小化来拟合统计方法中的最佳可能线。然而,在机器学习方法中,平方损失将相对于 β 系数最小化。线性回归也具有高偏差和低方差误差。
  • 套索和脊线回归:这使用正则化通过对系数应用惩罚来控制过拟合问题。在岭回归中,对系数的平方和应用惩罚,而在套索中,对系数的绝对值应用惩罚。可以调整惩罚以改变模型拟合的动态。岭回归试图最小化系数的大小,而套索试图消除它们。
  • 决策树:递归二进制分裂应用于在每一级分裂类,以将观察分类到它们最纯的类。分类错误率只是该区域中不属于最常见类别的训练观察值的分数。决策树有一个过度拟合的问题,因为它们在拟合的方式上有很高的方差;修剪通过完全生长树来减少过拟合问题。决策树具有低偏差和高方差误差。
  • 装袋:这是一种应用在决策树上的集成技术,目的是最小化方差误差,同时不会因为偏差而增加误差分量。在打包过程中,用观察值和所有变量(列)的子样本选择各种样本,随后在每个样本上独立地拟合单个决策树,随后通过进行最大投票(在回归情况下,计算结果的平均值)来集成结果。
  • 随机森林:这个和套袋差不多,除了一点不同。在打包中,为每个样本选择所有的变量/列,而在随机森林中,选择几个子列。选择几个变量而不是全部的原因是,在每个独立的抽样树中,重要的变量总是在分裂的顶层首先出现,这使得所有的树看起来或多或少相似,违背了集成的唯一目的:它在多样化和独立的个体模型上比在相关的个体模型上工作得更好。随机森林既有低偏差又有方差误差。
  • Boosting :这是一个顺序算法,应用在弱分类器上,比如一个决策树桩(一级决策树或者有一个根节点和两个终端节点的树),通过集合结果来创建一个强分类器。该算法首先为所有观测值分配相等的权重,然后进行后续迭代,通过增加错误分类观测值的权重和减少正确分类观测值的权重,将更多的注意力放在错误分类观测值上。最后,所有的个体分类器被组合以创建一个强分类器。Boosting 可能存在过拟合问题,但通过仔细调整参数,我们可以获得最佳的自机器学习模型。
  • 支持向量机(SVMs) :这通过在类之间拟合尽可能宽的超平面来最大化类间的余量。在非线性可分离类的情况下,它使用核将观测值移动到高维空间,然后用那里的超平面线性分离它们。
  • 推荐引擎:这利用协同过滤算法,通过考虑将使用该特定项目的相似用户的口味,向其各自的用户识别过去没有使用过的高概率项目。它使用交替最小二乘 ( ALS )方法来解决这个问题。
  • 主成分分析(PCA) :这是一种降维技术,用主成分代替原始变量进行计算。在数据方差最大的地方确定主成分;随后,顶部的 n 分量将通过覆盖大约 80%的方差来获取,并将用于进一步的建模过程,或者探索性分析将作为无监督学习来执行。
  • K-means 聚类:这是一种无监督算法,主要用于分割练习。K-means 聚类将给定的数据分类到 k 聚类中,使得在聚类中,变化最小,而在整个聚类中,变化最大。
  • 马尔可夫决策过程(MDP) :在强化学习中,MDP 是一个数学框架,用于在结果部分随机、部分受控的情况或环境中对代理的决策进行建模。在该模型中,环境被建模为一组状态和动作,代理可以执行这些状态和动作来控制系统的状态。目标是控制系统,使代理人的总收益最大化。
  • 蒙特卡罗方法:蒙特卡罗方法不需要完全了解环境,与 MDP 形成对比。蒙特卡罗方法只需要经验,经验是通过从与环境的实际或模拟交互中获得的状态、动作和回报的样本序列获得的。蒙特卡罗方法探索空间,直到选定样本序列的最终结果,并相应地更新估计。
  • 时间差异学习:这是强化学习中的一个核心主题。时间差异是蒙特卡罗和动态规划思想的结合。与蒙特卡罗类似,时间差分方法可以直接从原始经验中学习,而无需环境动态模型。像动态规划一样,时间差分方法部分基于其他学习的估计来更新估计,而不等待最终结果。时间差异是两全其美的,在 AlphaGo 等游戏中最常用。

摘要

在本章中,我们获得了统计建模和机器学习中涉及的各种基本构建块和子组件的高级视图,例如与统计上下文相关的均值、方差、四分位数范围、p 值、偏差与方差权衡、AIC、基尼系数、曲线下面积等,以及与机器学习相关的交叉验证、梯度下降和网格搜索概念。在 Python 和 R 代码的支持下,我们已经用各种库解释了所有的概念,如numpyscipypandasscikit- learn,以及 Python 中的stats模型和 R 中的基本stats包。在下一章中,我们将学习使用 Python 和 R 代码在机器学习中用线性回归问题和岭/套索回归来绘制统计模型和机器学习模型之间的相似之处。

二、统计与机器学习的并行性

乍一看,机器学习似乎离统计学很远。然而,如果我们深入研究它们,我们可以在两者之间找到相似之处。在这一章中,我们将深入探讨细节。已经对线性回归和套索/岭回归进行了比较,以便提供统计建模和机器学习之间的简单比较。这是两个世界的基本模型,从一开始就很好。

在本章中,我们将介绍以下内容:

  • 了解统计参数和诊断
  • 将统计诊断等同于机器学习模型中的补偿因素
  • 脊线和套索回归
  • 调整后的 R 平方与精度的比较

回归模型和机器学习模型的比较

线性回归和机器学习模型都试图以不同的方式解决同一个问题。在以下拟合最佳可能平面的双变量方程的简单示例中,回归模型试图通过最小化超平面和实际观测值之间的误差来拟合最佳可能超平面。然而,在机器学习中,同样的问题已经被转化为优化问题,其中误差以平方形式建模,以通过改变权重来最小化误差。

在统计建模中,样本是从总体中抽取的,模型将根据采样数据进行拟合。然而,在机器学习中,即使是像 30 个观察值这样的小数字也足以在每次迭代结束时更新权重;在少数情况下,例如在线学习,模型将会更新,甚至只需观察一次:

机器学习模型可以有效地并行化,并在多台机器上工作,其中模型权重在机器之间传播,等等。在使用 Spark 的大数据的情况下,实现了这些技术。

统计模型本质上是参数化的,这意味着模型将具有参数,对这些参数执行诊断以检查模型的有效性。而机器学习模型是非参数的,没有任何参数或曲线假设;这些模型基于所提供的数据自行学习,并得出复杂且错综复杂的函数,而不是预定义的函数拟合。

在统计建模中需要进行多重共线性检查。然而,在机器学习空间中,权重被自动调整以补偿多重共线性问题。如果我们考虑基于树的集成方法,如 bagging、随机森林、boosting 等,多重共线性甚至不存在,因为底层模型是决策树,它首先不存在多重共线性问题。

随着大数据和分布式并行计算的发展,更复杂的模型正在产生用过去的技术不可能产生的最先进的结果。

机器学习模型中的补偿因素

用一个由两个支架支撑的梁的例子来解释机器学习模型中的补偿因素,以便将统计诊断等同起来。如果其中一个支架不存在,横梁最终会因为失去平衡而掉下来。类似的类比也适用于比较统计建模和机器学习方法。

使用整体模型精度和单个参数显著性检验对训练数据的统计建模方法进行两点验证。由于无论是线性回归还是逻辑回归,由于模型本身的形状差异较小,因此在看不见的数据上,其效果变差的可能性很小。因此,在部署期间,这些模型不会导致太多偏离的结果。

然而,在机器学习空间中,模型具有高度的灵活性,可以从简单变得高度复杂。最重要的是,机器学习中不会对单个变量进行统计诊断。因此,确保稳健性以避免模型的过度拟合是很重要的,这将确保它在实现阶段的可用性,以确保对看不见的数据的正确使用。

如前所述,在机器学习中,数据将被分成三部分(训练数据-50%,验证数据-25%,测试数据-25%),而不是统计方法中的两部分。机器学习模型应基于训练数据开发,其超参数应基于验证数据进行调整,以确保两点验证等价;这样,无需在单个变量级别执行诊断,就能确保模型的稳健性:

在深入研究这两种流之间的比较之前,我们将开始分别了解每种模型的基本原理。让我们从线性回归开始!这个模型听起来可能微不足道;然而,了解线性回归的工作原理将为更高级的统计和机器学习模型奠定基础。以下是线性回归的假设。

线性回归的假设

线性回归有以下假设,否则线性回归模型不成立:

  • 因变量应该是自变量的线性组合
  • 误差项没有自相关
  • 误差应为零均值,且呈正态分布
  • 没有多重共线性或多重共线性很少
  • 错误术语应该是同源的

具体解释如下:

  • 因变量应为自变量的线性组合: Y 应为 X 变量的线性组合。请注意,在下面的等式中, X2 已经升到了 2 的幂,等式仍然保持变量线性组合的假设:

如何诊断:查看残差与自变量的残差图。此外,尽量包含多项式项,并查看剩余值的任何减少,因为在简单的线性模型无法捕捉到信号的情况下,多项式项可能会从数据中捕捉到更多信号。

在前面的示例图中,最初,应用了线性回归,并且误差似乎有一个模式,而不是纯粹的白噪声;在这种情况下,它只是显示了非线性的存在。在增加多项式的幂之后,现在误差看起来就像白噪声。

  • 误差项无自相关:误差项存在相关性会影响模型精度。

如何诊断:寻找杜宾-沃森试验。杜宾-沃森的 d 检验了残差不是线性自相关的零假设。而 d 可以位于 04 之间,如果 d ≈ 2 表示无自相关, 0 < d < 2 表示正自相关, 2 < d < 4 表示负自相关。

  • 误差应具有零均值且正态分布:误差应具有零均值,以便模型创建无偏估计。绘制误差将显示误差的分布。然而,如果误差项不是正态分布的,这意味着置信区间将变得太宽或太窄,这导致基于最小二乘法估计系数的困难:

如何诊断:查看 Q-Q 图,还有像 Kolmogorov-Smirnov 测试这样的测试会有帮助。通过查看上面的 Q-Q 图,很明显,第一张图表显示误差是正态分布的,因为与对角线相比,残差似乎没有太大的偏差,而在右边的图表中,它清楚地显示误差不是正态分布的;在这些场景中,我们需要通过对数变换等重新评估变量,以使残差看起来像它们在左侧图表上一样。

  • 无多重共线性或很少多重共线性:多重共线性是指自变量之间相互关联的情况,这种情况通过膨胀系数/估计值的大小来创建不稳定的模型。也很难确定哪个变量有助于预测响应变量。 VIF 通过计算相对于所有其他自变量的 R 平方值来计算每个自变量,并试图逐个排除哪个变量具有最高的 VIF 值:

如何诊断:查看散点图,计算数据所有变量的相关系数。计算方差膨胀因子 ( VIF )。如果 VIF < = 4 暗示没有多重共线性,那么在银行场景中,人们也用 VIF < = 2

  • 误差应该是同态的:误差应该相对于自变量具有恒定的方差,这导致估计的置信区间不切实际地宽或窄,这降低了模型的性能。不保持同质性的一个原因是由于数据中存在异常值,这会将模型拟合拖向权重更高的异常值:

如何诊断:查看残差与因变量图;如果确实存在任何锥形或发散的模式,这表明误差没有恒定的方差,这影响了它的预测。

线性回归建模中应用的步骤

以下步骤应用于工业中的线性回归建模:

  1. 缺失值和异常值处理
  2. 自变量的相关性检验
  3. 训练和测试随机分类
  4. 根据列车数据拟合模型
  5. 根据测试数据评估模型

第一性原理的简单线性回归示例

整个章节都展示了广受欢迎的葡萄酒质量数据集,该数据集可从位于https://archive.ics.uci.edu/ml/datasets/Wine+Quality的 UCI 机器学习存储库中公开获得。

简单线性回归是预测因变量/响应变量 Y 的简单方法,给定独立变量/预测变量 X 。假设 XY 之间呈线性关系:

β0β1 是两个未知常数,分别是截距和斜率参数。一旦我们确定了常数,我们就可以利用它们来预测因变量:

残差是从模型预测的第 i 个观测响应值和第 i 个响应值之间的差值。显示了平方和的残差。最小二乘法通过最小化误差来选择估计值。

为了从统计学上证明线性回归是显著的,我们必须进行假设检验。假设我们从零假设开始,即 XY 之间没有显著关系:

因为,如果 β1 = 0 ,那么模型显示两个变量之间没有关联( Y = β0 + ε ,这些都是零假设假设;为了证明这个假设是对是错,我们需要确定 β1 距离 0 足够远(精确地说,与 0 的距离具有统计学意义),这样我们就可以确信 β1 是非零的,并且两个变量之间具有显著的关系。现在,问题是,离零还有多远?它取决于 β1 的分布,这是它的平均值和标准误差(类似于标准差)。在某些情况下,如果标准误差较小,即使相对较小的值也可能提供强有力的证据证明 β1 ≠ 0 ,因此 XY 之间存在关系。相比之下,如果 SE(β1) 大,那么 β1 的绝对值一定大,这样我们才能拒绝零假设。我们通常会进行以下测试,以检查 β1 偏离值 0 的标准偏差有多少:

利用这个 t 值,假设 β1 = 0 ,我们计算观察到任何等于 |t| 或更大值的概率;这个概率也被称为 p 值。如果 p 值< 0.05 ,则表示 β1 明显远离 0 ,因此我们可以拒绝零假设,同意存在强关系,而如果 p 值> 0.05 ,则我们接受零假设,得出两个变量之间没有显著关系的结论。

一旦我们有了系数值,我们将尝试预测相关值,并检查 R 平方值;如果该值为 > = 0.7 ,则表示该模型足够好,可以部署在看不见的数据上,反之如果不是这么好的值(<【0.6】),则可以断定该模型不够好,不能部署。

使用葡萄酒质量数据的简单线性回归示例

在葡萄酒质量数据中,因变量( Y )为葡萄酒质量,我们选择的自变量( X )为酒精含量。我们在这里测试两者之间是否有任何显著的关系,以检查酒精百分比的变化是否是葡萄酒质量的决定因素:

>>> import pandas as pd 
>>> from sklearn.model_selection import train_test_split     
>>> from sklearn.metrics import r2_score 

>>> wine_quality = pd.read_csv("winequality-red.csv",sep=';')   
>>> wine_quality.rename(columns=lambda x: x.replace(" ", "_"), inplace=True) 

在下一步中,使用 70%-30%的规则将数据分为训练数据和测试数据:

>>> x_train,x_test,y_train,y_test = train_test_split (wine_quality ['alcohol'], wine_quality["quality"],train_size = 0.7,random_state=42) 

从数据框中分离出一个变量后,它就变成了熊猫系列,因此我们需要再次将其转换回熊猫数据框:

>>> x_train = pd.DataFrame(x_train);x_test = pd.DataFrame(x_test) 
>>> y_train = pd.DataFrame(y_train);y_test = pd.DataFrame(y_test) 

以下函数用于计算数据帧各列的平均值。计算alcohol(独立)和quality(非独立)变量的平均值:

>>> def mean(values): 
...      return round(sum(values)/float(len(values)),2) 
>>> alcohol_mean = mean(x_train['alcohol']) 
>>> quality_mean = mean(y_train['quality']) 

计算回归模型的系数确实需要方差和协方差:

>>> alcohol_variance = round(sum((x_train['alcohol'] - alcohol_mean)**2),2) 
>>> quality_variance = round(sum((y_train['quality'] - quality_mean)**2),2) 

>>> covariance = round(sum((x_train['alcohol'] - alcohol_mean) * (y_train['quality'] - quality_mean )),2) 
>>> b1 = covariance/alcohol_variance 
>>> b0 = quality_mean - b1*alcohol_mean 
>>> print ("\n\nIntercept (B0):",round(b0,4),"Co-efficient (B1):",round(b1,4)) 

计算系数后,需要预测quality变量,这将使用 R 平方值测试拟合质量:

>>> y_test["y_pred"] = pd.DataFrame(b0+b1*x_test['alcohol']) 
>>> R_sqrd = 1- ( sum((y_test['quality']-y_test['y_pred'])**2) / sum((y_test['quality'] - mean(y_test['quality']))**2 )) 
>>> print ("Test R-squared value",round(R_sqrd,4)) 

从测试的 R 平方值,我们可以得出结论:qualityalcohol变量在葡萄酒数据中没有很强的关系,因为 R 平方小于 0.7

下面的代码描述了使用第一性原理的简单回归拟合:

wine_quality = read.csv("winequality-red.csv",header=TRUE,sep = ";",check.names = FALSE) 
names(wine_quality) <- gsub(" ", "_", names(wine_quality)) 

set.seed(123) 
numrow = nrow(wine_quality) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 
train_data = wine_quality[trnind,] 
test_data = wine_quality[-trnind,] 

x_train = train_data$alcohol;y_train = train_data$quality 
x_test = test_data$alcohol; y_test = test_data$quality 

x_mean = mean(x_train); y_mean = mean(y_train) 
x_var = sum((x_train - x_mean)**2) ; y_var = sum((y_train-y_mean)**2) 
covariance = sum((x_train-x_mean)*(y_train-y_mean)) 

b1 = covariance/x_var   
b0 = y_mean - b1*x_mean 

pred_y = b0+b1*x_test 

R2 <- 1 - (sum((y_test-pred_y )^2)/sum((y_test-mean(y_test))^2)) 
print(paste("Test Adjusted R-squared :",round(R2,4))) 

多线性回归示例-逐步建模方法

在本节中,我们实际展示了行业专家在使用样本葡萄酒数据进行线性回归建模时遵循的方法。statmodels.api包已用于多元线性回归演示目的,而不是 scikit-learn,因为前者提供变量诊断,而后者仅提供最终精度,等等:

>>> import numpy as np 
>>> import pandas as pd 
>>> import statsmodels.api as sm 
>>> import matplotlib.pyplot as plt 
>>> import seaborn as sns 
>>> from sklearn.model_selection import train_test_split     
>>> from sklearn.metrics import r2_score 

>>> wine_quality = pd.read_csv("winequality-red.csv",sep=';')   
# Step for converting white space in columns to _ value for better handling  
>>> wine_quality.rename(columns=lambda x: x.replace(" ", "_"), inplace=True) 
>>> eda_colnms = [ 'volatile_acidity',  'chlorides', 'sulphates', 'alcohol','quality'] 
# Plots - pair plots 
>>> sns.set(style='whitegrid',context = 'notebook') 

样本五变量的配对图如下所示;但是,我们鼓励您尝试各种组合来直观地检查各种其他变量之间的各种关系:

>>> sns.pairplot(wine_quality[eda_colnms],size = 2.5,x_vars= eda_colnms, y_vars= eda_colnms) 
>>> plt.show() 

除了直观的图表外,还计算相关系数,以显示数字术语中的相关程度;这些图表用于在初始阶段删除变量,如果有很多变量的话:

>>> # Correlation coefficients 
>>> corr_mat = np.corrcoef(wine_quality[eda_colnms].values.T) 
>>> sns.set(font_scale=1) 
>>> full_mat = sns.heatmap(corr_mat, cbar=True, annot=True, square=True, fmt='.2f',annot_kws={'size': 15}, yticklabels=eda_colnms, xticklabels=eda_colnms) 
>>> plt.show() 

向后和向前选择

有各种方法来添加或移除变量,以确定最佳模型。

在逆向方法中,迭代从考虑所有变量开始,我们将一个接一个地移除变量,直到满足所有规定的统计量(如无显著性和多重共线性等)。最后检查整体统计,如 R 平方值为 > 0.7 ,则认为是好模型,否则拒绝。在工业界,从业者主要倾向于研究落后的方法。

在向前的情况下,我们将从没有变量开始,并继续添加重要的变量,直到整个模型的拟合度提高。

在下面的方法中,我们使用了反向选择方法,从所有 11 个独立变量开始,并在每次迭代后从分析中逐个移除它们(无关紧要和多共线的变量):

>>> colnms = ['fixed_acidity', 'volatile_acidity', 'citric_acid', 'residual_sugar', 'chlorides', 'free_sulfur_dioxide',  'total_sulfur_dioxide', 'density', 'pH', 'sulphates', 'alcohol'] 

>>> pdx = wine_quality[colnms] 
>>> pdy = wine_quality["quality"] 

通过随机执行数据分割来创建列车和测试数据。random_state(随机种子)用于可再现的结果:

>>> x_train,x_test,y_train,y_test = train_test_split(pdx, pdy, train_size = 0.7, random_state = 42) 

在下面的代码中,添加constant意味着创建一个截取变量。如果我们不创建截距,系数将相应改变:

>>> x_train_new = sm.add_constant(x_train) 
>>> x_test_new = sm.add_constant(x_test) 
>>> full_mod = sm.OLS(y_train,x_train_new) 

下面的代码创建了一个模型概要,包括 R 平方、调整后的 R 平方和自变量的 p 值:

>>> full_res = full_mod.fit() 
>>> print ("\n \n",full_res.summary()) 

下面的代码根据第一性原理计算了所有单个变量的 VIF。这里我们计算每个变量的 R 平方值,并将其转换为 VIF 值:

>>> print ("\nVariance Inflation Factor") 
>>> cnames = x_train.columns 
>>> for i in np.arange(0,len(cnames)): 
...      xvars = list(cnames) 
...      yvar = xvars.pop(i) 
...      mod = sm.OLS(x_train[yvar],sm.add_constant( x_train_new[xvars])) 
...      res = mod.fit() 
...      vif = 1/(1-res.rsquared) 
...      print (yvar,round(vif,3))   

前面的代码生成了下面的输出,我们可以从这里开始考虑调整多线性回归模型。

迭代 1:

调整模型时要关注的关键指标是 AIC、调整后的 R 平方、单个变量的 P > |t| 和 VIF 值(如下所示)。任何模型都可以被认为符合以下经验法则标准:

  • AIC :无绝对值显著。这是一个相对的衡量标准,越低越好。
  • 调整后的 R 平方:是 ≥ 0.7
  • 个体变量的 P 值(P > |t|) :为 ≤ 0.05
  • 个体变量的 VIF :是 ≤ 5 (在银行业,有些地方也有人用 ≤ 2 )。

通过查看前面的结果,residual_sugar的 p 值最高0.668fixed_acidity的 VIF 值最高7.189。在这种情况下,总是首先移除最不重要的变量,因为不重要是比多重共线性更严重的问题,尽管在到达最终模型时两者都应该被移除。

从列列表中删除residual_sugar变量后,运行前面的代码;我们从迭代 2 中得到以下结果:

  • AIC :仅仅从 2231 降到了 2229。
  • 调整后的 R 平方:数值没有从 0.355 变化。
  • 单个变量的 P 值(P > |t|) :密度仍然是最不重要的,值为 0.713。
  • 个人变量的 VIF:fixed_acidityVIF ≥ 5 。但是density变量需要先删除,因为优先级被赋予了无意义。

迭代 2:

基于迭代 2,我们需要移除密度变量并重新运行练习,直到没有违反 p 值和 VIF 发生。为了节省空间,我们确实跳过了中间步骤;但是,我们鼓励您手动执行逐个移除变量的逐步过程。

在迭代 5 之后,模型不能进一步改进,因为它满足所有 p 值和 VIF 准则。结果显示在这里。

迭代 5:

在这个例子中,我们得到了迭代 5 后的最终结果:

  • AIC :从迭代 1 的2231减少到迭代 5 的2225
  • 调整后的 R 平方:数值改为0.356,略有提升,但不够值!
  • 个体变量的 P 值(P > |t|) :没有一个变量是无关紧要的;所有值都小于 0.05。
  • 个体变量的 VIF :所有变量小于 5。因此,我们不需要基于 VIF 值移除任何进一步的变量。

我们得到的答案是因变量和自变量之间不存在强关系。然而,我们仍然可以根据测试数据进行预测,并计算 R 平方来再次确认。

If a predictive model shows as strong relationship, the prediction step is a must-have utilize model in the deployment stage. Hence, we are predicting and evaluating the R-squared value here.

以下代码步骤利用该模型来预测测试数据:

>>> # Prediction of data   
>>> y_pred = full_res.predict(x_test_new) 
>>> y_pred_df = pd.DataFrame(y_pred) 
>>> y_pred_df.columns = ['y_pred'] 

>>> pred_data = pd.DataFrame(y_pred_df['y_pred']) 
>>> y_test_new = pd.DataFrame(y_test) 
>>> y_test_new.reset_index(inplace=True) 
>>> pred_data['y_test'] = pd.DataFrame(y_test_new['quality'])

对于 R 平方计算,利用 scikit-learn 包sklean.metrics模块:

>>> # R-square calculation 
>>> rsqd = r2_score(y_test_new['quality'].tolist(), y_pred_df['y_pred'].tolist()) 
>>> print ("\nTest R-squared value:",round(rsqd,4))

测试数据的调整后 R 平方值显示为 0.3519,而训练 R 平方值为 0.356。从最终的结果,我们可以得出结论,自变量和因变量(质量)之间的关系不能用线性回归方法建立。

葡萄酒数据线性回归的 R 码如下:

library(usdm) 
# Linear Regression 
wine_quality = read.csv("winequality-red.csv",header=TRUE,sep = ";",check.names = FALSE) 
names(wine_quality) <- gsub(" ", "_", names(wine_quality)) 

set.seed(123) 
numrow = nrow(wine_quality) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 
train_data = wine_quality[trnind,] 
test_data = wine_quality[-trnind,] 
xvars = c("volatile_acidity","chlorides","free_sulfur_dioxide",  
           "total_sulfur_dioxide","pH","sulphates","alcohol") 
yvar = "quality" 
frmla = paste(yvar,"~",paste(xvars,collapse = "+")) 
lr_fit = lm(as.formula(frmla),data = train_data) 
print(summary(lr_fit)) 
#VIF calculation 
wine_v2 = train_data[,xvars] 
print(vif(wine_v2)) 
#Test prediction 
pred_y = predict(lr_fit,newdata = test_data) 
R2 <- 1 - (sum((test_data[,yvar]-pred_y )^2)/sum((test_data[,yvar]-mean(test_data[,yvar]))^2)) 
print(paste("Test Adjusted R-squared :",R2))

机器学习模型-脊线和套索回归

在线性回归中,只有平方的残差和 ( RSS )被最小化,而在脊线和套索回归中,对系数值应用惩罚(也称为收缩惩罚,以使用调谐参数【λ】来正则化系数。

λ=0 时,惩罚没有影响,岭/套索产生与线性回归相同的结果,而λ->∩将系数归零:

在我们深入研究脊和套索之前,有必要了解一些关于拉格朗日乘子的概念。可以用下面的格式显示前面的目标函数,其中目标只是受预算成本约束( s )的 RSS。对于 λ 的每个值,都有一个 s 这样的值,它将提供等效的等式,如带有惩罚因子的总体目标函数所示:

岭回归在最小二乘估计具有高方差的情况下工作良好。岭回归相对于需要 2P 模型的最佳子集选择具有计算优势。相比之下,对于λ的任何固定值,岭回归仅拟合单个模型,并且模型拟合过程可以非常快速地执行。

岭回归的一个缺点是,它将包括所有的预测因子,并根据它们的重要性缩小权重,但它没有将值精确地设置为零,以便从模型中消除不必要的预测因子;拉索回归克服了这个问题。给定预测器的数量非常大的情况,使用 ridge 可以提供准确性,但是它包括所有变量,这在模型的紧凑表示中是不期望的;这个问题在 lasso 中不存在,因为它会将不必要变量的权重设置为零。

从套索生成的模型非常像子集选择,因此它们比由岭回归生成的模型更容易解释。

岭回归机器学习示例

岭回归是一种机器学习模型,在这种模型中,我们不对自变量进行任何统计诊断,而只是利用模型来拟合测试数据并检查拟合的准确性。这里,我们使用了scikit-learn包:

>>> from sklearn.linear_model import Ridge 

>>> wine_quality = pd.read_csv("winequality-red.csv",sep=';') 
>>> wine_quality.rename(columns=lambda x: x.replace(" ", "_"), inplace=True) 

>>> all_colnms = ['fixed_acidity', 'volatile_acidity', 'citric_acid', 'residual_sugar', 'chlorides', 'free_sulfur_dioxide', 'total_sulfur_dioxide', 'density', 'pH', 'sulphates', 'alcohol'] 

>>> pdx = wine_quality[all_colnms] 
>>> pdy = wine_quality["quality"] 

>>> x_train,x_test,y_train,y_test = train_test_split(pdx,pdy,train_size = 0.7,random_state=42)

从零开始的网格搜索的简单版本描述如下,其中alphas的各种值将在网格搜索中进行测试,以测试模型的适用性:

>>> alphas = [1e-4,1e-3,1e-2,0.1,0.5,1.0,5.0,10.0]

R 平方的初始值被设置为0,以便跟踪其更新值,并在新值大于现有值时打印:

>>> initrsq = 0 

>>> print ("\nRidge Regression: Best Parameters\n") 
>>> for alph in alphas: 
...      ridge_reg = Ridge(alpha=alph)  
...      ridge_reg.fit(x_train,y_train)   0 
...      tr_rsqrd = ridge_reg.score(x_train,y_train) 
...      ts_rsqrd = ridge_reg.score(x_test,y_test) 

以下代码始终跟踪测试 R 平方值,并在新值大于现有最佳值时打印:

>>>     if ts_rsqrd > initrsq: 
...          print ("Lambda: ",alph,"Train R-Squared value:",round(tr_rsqrd,5),"Test R-squared value:",round(ts_rsqrd,5)) 
...          initrsq = ts_rsqrd 

这显示在下面的截图中:

另外,请注意,岭回归生成的测试 R 平方值与多元线性回归(0.3519)得到的值相似,但对变量的诊断没有压力,等等。因此,机器学习模型相对紧凑,可以用于自动学习,而无需人工干预来重新训练模型;这是将 ML 模型用于部署目的的最大优势之一。

葡萄酒质量数据的岭回归 R 代码如下:

# Ridge regression 
library(glmnet) 

wine_quality = read.csv("winequality-red.csv",header=TRUE,sep = ";",check.names = FALSE) 
names(wine_quality) <- gsub(" ", "_", names(wine_quality)) 

set.seed(123) 
numrow = nrow(wine_quality) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 
train_data = wine_quality[trnind,]; test_data = wine_quality[-trnind,] 

xvars = c("fixed_acidity","volatile_acidity","citric_acid","residual_sugar","chlorides","free_sulfur_dioxide",            "total_sulfur_dioxide","density","pH","sulphates","alcohol") 
yvar = "quality" 

x_train = as.matrix(train_data[,xvars]);y_train = as.double (as.matrix (train_data[,yvar])) 
x_test = as.matrix(test_data[,xvars]) 

print(paste("Ridge Regression")) 
lambdas = c(1e-4,1e-3,1e-2,0.1,0.5,1.0,5.0,10.0) 
initrsq = 0 
for (lmbd in lambdas){ 
  ridge_fit = glmnet(x_train,y_train,alpha = 0,lambda = lmbd) 
  pred_y = predict(ridge_fit,x_test) 
  R2 <- 1 - (sum((test_data[,yvar]-pred_y )^2)/sum((test_data[,yvar]-mean(test_data[,yvar]))^2)) 

  if (R2 > initrsq){ 
    print(paste("Lambda:",lmbd,"Test Adjusted R-squared :",round(R2,4))) 
    initrsq = R2 
  } 
}

套索回归机器学习模型示例

套索回归是岭回归的近亲,在岭回归中,系数的绝对值被最小化,而不是值的平方。通过这样做,我们消除了一些无关紧要的变量,这些变量是非常紧凑的表示,类似于 OLS 方法。

除了对系数的模/绝对值进行惩罚外,以下实现类似于岭回归:

>>> from sklearn.linear_model import Lasso 

>>> alphas = [1e-4,1e-3,1e-2,0.1,0.5,1.0,5.0,10.0] 
>>> initrsq = 0 
>>> print ("\nLasso Regression: Best Parameters\n") 

>>> for alph in alphas: 
...      lasso_reg = Lasso(alpha=alph)  
...      lasso_reg.fit(x_train,y_train)     
...      tr_rsqrd = lasso_reg.score(x_train,y_train) 
...      ts_rsqrd = lasso_reg.score(x_test,y_test) 

...      if ts_rsqrd > initrsq: 
...          print ("Lambda: ",alph,"Train R-Squared value:",round(tr_rsqrd,5),"Test R-squared value:",round(ts_rsqrd,5)) 
...          initrsq = ts_rsqrd

这显示在下面的截图中:

>>> ridge_reg = Ridge(alpha=0.001)  
>>> ridge_reg.fit(x_train,y_train)   
>>> print ("\nRidge Regression coefficient values of Alpha = 0.001\n") 
>>> for i in range(11):  
...     print (all_colnms[i],": ",ridge_reg.coef_[i]) 

>>> lasso_reg = Lasso(alpha=0.001)  
>>> lasso_reg.fit(x_train,y_train) 
>>> print ("\nLasso Regression coefficient values of Alpha = 0.001\n") 
>>> for i in range(11): 
...      print (all_colnms[i],": ",lasso_reg.coef_[i])

以下结果显示了两种方法的系数值;套索回归中密度系数设置为0,而岭回归中密度值为-5.5672;此外,岭回归中的系数都不是零值:

葡萄酒质量数据上套索回归的 R 码如下:

# Above Data processing steps are same as Ridge Regression, only below section of the code do change 

# Lasso Regression 
print(paste("Lasso Regression")) 
lambdas = c(1e-4,1e-3,1e-2,0.1,0.5,1.0,5.0,10.0) 
initrsq = 0 
for (lmbd in lambdas){ 
  lasso_fit = glmnet(x_train,y_train,alpha = 1,lambda = lmbd) 
  pred_y = predict(lasso_fit,x_test) 
  R2 <- 1 - (sum((test_data[,yvar]-pred_y )^2)/sum((test_data[,yvar]-mean(test_data[,yvar]))^2)) 

  if (R2 > initrsq){ 
    print(paste("Lambda:",lmbd,"Test Adjusted R-squared :",round(R2,4))) 
    initrsq = R2 
  } 
}

线性回归和岭/套索回归中的正则化参数

线性回归中调整后的 R 平方总是不利的,增加额外的不太重要的变量是线性回归中调整数据的一种类型,但它会调整到模型的唯一拟合。然而,在机器学习中,许多参数被调整以调节过拟合问题。在调整为正则化的套索/脊线回归惩罚参数( λ )的示例中,有无限个值可用于以无限种方式正则化模型:

总的来说,预测模式的统计方法和机器学习方法有许多相似之处。

摘要

在本章中,您已经学习了统计模型与机器学习模型在回归问题上的比较。多元线性回归方法已经通过使用statsmodel包的逐步迭代过程进行了说明,去除了无关紧要的多共线变量。然而,在机器学习模型中,变量的移除不需要被移除并且权重被自动调整,但是具有可以被调整以微调模型拟合的参数,因为机器学习模型基于数据自己学习,而不是仅仅通过手动移除变量来建模。尽管我们在线性回归和套索/岭回归方法之间获得了几乎相同的精度结果,但是通过使用诸如随机森林的高度强大的机器学习模型,我们可以在模型精度上比传统的统计模型获得更好的提升。在下一章中,我们将详细介绍一个具有逻辑回归和高度强大的机器学习模型(如随机森林)的分类示例。

三、逻辑回归与随机森林

在本章中,我们将以德国信贷数据的分类为例,对逻辑回归和随机森林进行比较。逻辑回归是信贷和风险行业中非常普遍使用的技术,用于检查违约问题的概率。如今,信贷和风险部门与监管机构面临的主要挑战是机器学习模型的黑箱性质,这减缓了高级模型在这一领域的使用。然而,通过将逻辑回归与随机森林进行比较,一些转折是可能的;在这里,我们将讨论变量重要性图及其与逻辑回归 p 值的相似之处,我们也不应该忘记一个主要事实,即在公平的基础上,任何模型中的显著变量仍然是显著的,尽管任何两个模型之间总是存在一些变量显著性的变化。

最大似然估计

逻辑回归的原理是最大似然估计;在这里,我们将详细解释它的原理,这样我们就可以在接下来的章节中涵盖逻辑回归的更多基础知识。最大似然估计是在给定观测值的情况下估计模型参数的一种方法,通过找到最大化观测值可能性的参数值,这意味着找到最大化事件 1 和非事件 0 的概率 p 的参数,如您所知:

概率(事件+非事件)= 1

:样本 (0,1,0,0,1,0) 取自二项式分布。最大似然估计是多少?

:假设对于二项式分布 P(X=1) =P(X=0) = 1- 其中为参数:

这里 log 为了数学上的方便,应用于方程的两边;此外,最大化可能性与最大化可能性对数是一样的:

通过将导数等于零来确定的最大值:

然而,我们需要做双重微分来确定由导数等于零得到的鞍点是最大值还是最小值。如果值最大;对数(L( )) 的双微分应为负值:

即使在双微分中没有替换值,我们也可以确定它是负值,因为分母值是平方的,并且它对两个项都有负号。尽管如此,我们正在进行替代,其价值是:

因此,已经证明在值 = 1/3 时,可能性最大。如果我们替换对数似然函数中的值,我们将获得:

计算 -2ln(L)* 背后的原因是复制在适当的逻辑回归中计算的度量。事实上:

AIC = -2ln(L) + 2k

因此,逻辑回归试图通过最大化单个参数的似然性来寻找参数。但一个小的区别是,在逻辑回归中,将使用伯努利分布,而不是二项式分布。准确地说,伯努利只是二项式的一个特例,因为主要结果只有两个类别,所有的轨迹都是从这两个类别中产生的。

逻辑回归-介绍和优势

逻辑回归应用最大似然估计,将因变量转换为关于自变量的 logit 变量(因变量出现或不出现的概率的自然对数)。这样,逻辑回归估计了某个事件发生的概率。在下面的等式中,概率对数作为解释变量的函数线性变化:

人们可以简单地问,为什么赔率、对数(赔率)而不是概率?事实上,这是面试官在分析面试中最喜欢的问题。

原因如下:

通过将概率转换为 log(赔率),我们将范围从[0,1]扩展到[- ∞,+∞ ]。通过概率拟合模型,我们将遇到一个有限范围的问题,并且通过应用对数变换,我们掩盖了所涉及的非线性,并且我们可以仅用变量的线性组合进行拟合。

还有一个问题,如果有人用 0-1 问题而不是逻辑回归来拟合线性回归,会发生什么?

下图提供了简要说明:

  • 误差项往往会在 X (自变量)的中间值处变大,在极值处变小,这违反了线性回归假设,即误差应为零均值,且应呈正态分布
  • X 的结束值处,生成大于 1 且小于 0 的无意义预测
  • 普通最小二乘 ( OLS )估计效率低,标准误差有偏差
  • X 中间值误差方差高,末端方差低

所有这些问题都可以通过逻辑回归来解决。

逻辑回归中涉及的术语

逻辑回归是许多面试官最喜欢用来测试分析师统计敏锐度的依据。有人说过,即使有人理解了逻辑回归中的 1000 个概念,面试官总会提出 1001 个问题。因此,从逻辑回归的基本原理中积累知识以建立一个坚实的基础是非常值得的:

  • 信息值(IV) :这在将变量纳入模型之前对其进行初步筛选时非常有用。工业上主要使用 IV 来消除模型拟合前第一步中的主要变量,因为最终模型中存在的变量数量约为 10 个。因此,初始处理需要将变量从 400+左右减少。

  • :在下表中,连续变量(价格)已经根据价格范围和该区间中统计的事件和非事件的数量分解为十分位数(10 个区间),并且已经计算了所有区间的信息值并加在一起。我们得到的总值为 0.0356 ,这意味着它是对事件进行分类的弱预测因子。

  • 阿卡克信息标准(AIC) :这衡量给定数据集的统计模型的相对质量。这是偏见和差异之间的权衡。在比较两个模型时,AIC 较少的模型比价值较高的模型更受青睐。

如果我们仔细观察下面的等式, k 参数(模型中包含的变量数量)正在惩罚模型的过拟合现象。这意味着我们可以通过在模型中加入更多不太重要的变量来人工证明模型的训练精度;通过这样做,我们可以在训练数据上获得更好的准确性,但是在测试数据上,准确性会降低。这种现象可能是逻辑回归中的某种正则化:

AIC = -2ln(L) + 2k

L =最大似然值(为数学方便应用对数变换)

k =模型中的变量数量

  • 接收器工作特性(ROC)曲线:这是一个曲线图,说明了二进制分类器在判别阈值变化时的性能。该曲线是通过绘制不同阈值下的真阳性率 ( TPR )与假阳性率 ( FPR )来创建的。

了解 ROC 曲线效用的一个简单方法是,如果我们将阈值(threshold 是介于 01 之间的实值,用于将输出的预测概率转换为类,因为 logistic 回归预测的是概率)保持得很低,我们会将大部分预测观测值放在正类下,即使其中一些应该放在负类下。另一方面,将阈值保持在非常高的水平会惩罚积极类别,但消极类别会改善。理想情况下,阈值的设置应在两个类别之间权衡价值,并产生更高的整体准确性:

Optimum threshold = Threshold where maximum (sensitivity + specificity) is possible

在我们进入本质之前,我们将可视化混淆矩阵,以理解以下各种公式:

ROC 曲线如下所示:

  • 等级排序:按照预测概率对观测值进行降序排序后,创建十分位数(10 个相等的区间,每个区间占总观测值的 10%)。通过将每个十分位数中的事件数量相加,我们将得到每个十分位数的聚合事件,并且这个数量应该按降序排列,否则将严重违反逻辑回归方法。

思考等级排序为什么重要的一种方式?当我们将分界点设置在前三至四十分之一时,这将非常有用,以发送营销活动,在这些活动中,细分市场有更高的机会响应活动。如果模型的等级顺序不成立,即使在选择了前三至四个十分位数之后,在分界点以下也会有很大一部分被遗漏,这是危险的。

  • 一致性/c-统计:这是逻辑回归模型中二元结果拟合质量的度量。它是实际事件的预测事件概率高于非事件的对的比例。

  • 示例:在下表中,实际值和预测值均以七行样本显示。实际是真实的范畴,无论默认与否;而预测是从逻辑回归模型预测的概率。计算一致性值。

为了计算一致性,我们需要将表拆分为两个(每个表的实际值为 10 ),并应用两个表中每行的笛卡尔乘积来形成对:

在下表中,当 1 类别的预测概率高于 0 类别的预测概率时,已计算出完整的笛卡尔积并将该对分类为一致对。如果是反过来,那么这一对就被归类为不和谐的一对。在特殊情况下,如果两个概率相同,这些对将被归类为并列。




  • C-statistics:这是 0.83315%或 83.315%,任何大于 0.7%或 70%的值都被认为是实用的好模型。
  • 分歧:违约账户平均得分与非违约账户平均得分的距离。距离越大,评分系统在区分好的和坏的观察方面就越有效。
  • K-S 统计:这是两个种群分布之间的最大距离。它有助于区分默认帐户和非默认帐户。
  • 人口稳定指数(PSI) :这是用于检查当前人口中使用信用评分模型的漂移是否与相应发展时间的人口相同的指标:
    • PSI < = 0.1 :这表示当前人口的特征相对于发展人口没有变化
    • 0.1 < PSI < = 0.25 :表示发生了一些变化,提醒注意,但仍可使用
    • PSI > 0.25 :这表明与开发时间相比,当前人口的分数分布发生了很大的变化

逻辑回归建模中的应用步骤

以下步骤应用于工业中的线性回归建模:

  1. 排除标准和好与坏的定义确定
  2. 初始数据准备和单变量分析
  3. 派生/虚拟变量创建
  4. 细分类和粗分类
  5. 在训练数据上拟合逻辑模型
  6. 根据测试数据评估模型

使用德国信贷数据的逻辑回归示例

开源的德国信贷数据已被 UCI 机器学习库用于逻辑回归建模:https://archive . ics . UCI . edu/ml/datasets/Statlog+(德语+信贷+数据)

>>> import os 
>>> os.chdir("D:\\Book writing\\Codes\\Chapter 3") 

>>> import numpy as np 
>>> import pandas as pd 

>>> from sklearn.model_selection import train_test_split     
>>> from sklearn.metrics import accuracy_score,classification_report 

>>> credit_data = pd.read_csv("credit_data.csv") 

下面的代码描述了数据的前五行:

>>> print (credit_data.head()) 

我们总共有 20 个解释变量,其中一个因变量叫做classclass变量2的值表示默认,1表示非默认。为了建模为 0-1 问题,我们在以下代码中通过1移除了该值:

>>> credit_data['class'] = credit_data['class']-1 

为了知道每个变量对自变量的预测能力,这里我们做了一个信息值计算。在下面的代码中,分类变量和连续变量都被考虑在内。

如果数据类型是object,这意味着它是一个分类变量,任何其他变量,如int64等,将被视为连续的,并将相应地被分成 10 个相等的部分(也称为十分位数)。

>>> def IV_calc(data,var): 
...    if data[var].dtypes == "object": 
...     dataf = data.groupby([var])['class'].agg(['count','sum']) 
...        dataf.columns = ["Total","bad"]     
...        dataf["good"] = dataf["Total"] - dataf["bad"] 
...        dataf["bad_per"] = dataf["bad"]/dataf["bad"].sum() 
...        dataf["good_per"] = dataf["good"]/dataf["good"].sum() 
...        dataf["I_V"] = (dataf["good_per"] - dataf["bad_per"]) * np.log(dataf["good_per"]/dataf["bad_per"]) 
...        return dataf 
...    else: 
...        data['bin_var'] = pd.qcut(data[var].rank(method='first'),10) 
...        dataf = data.groupby(['bin_var'])['class'].agg(['count','sum']) 
...        dataf.columns = ["Total","bad"]     
...        dataf["good"] = dataf["Total"] - dataf["bad"] 
...        dataf["bad_per"] = dataf["bad"]/dataf["bad"].sum() 
...        dataf["good_per"] = dataf["good"]/dataf["good"].sum() 
...        dataf["I_V"] = (dataf["good_per"] - dataf["bad_per"]) * np.log(dataf["good_per"]/dataf["bad_per"]) 
...        return dataf 

为了便于说明,已经计算了Credit_history(分类)和Duration_in_month(连续)的信息值。Credit_history得到的总体 IV 为0.29,说明预测力中等,Duration_in_month0.34,为强预测力。

>>> print ("\n\nCredit History - Information Value\n")
>>> print (IV_calc(credit_data,'Credit_history'))

>>> print ("\n\nCredit History - Duration in month\n")
>>> print (IV_calc(credit_data,'Duration_in_month'))

然而,在实际场景中,初始数据有时有大约 500 个变量,甚至更多。在这种情况下,很难为每个变量单独运行代码。下面的代码是为在一次操作中自动计算所有离散和连续变量的总信息值而开发的!

>>> discrete_columns = ['Status_of_existing_checking_account', 'Credit_history', 'Purpose', 'Savings_Account','Present_Employment_since', 'Personal_status_and_sex', 'Other_debtors','Property','Other_installment_plans', 'Housing', 'Job', 'Telephone', 'Foreign_worker']

>>> continuous_columns = ['Duration_in_month', 'Credit_amount', 'Installment_rate_in_percentage_of_disposable_income', 'Present_residence_since', 'Age_in_years','Number_of_existing_credits_at_this_bank', 'Number_of_People_being_liable_to_provide_maintenance_for']

>>> total_columns = discrete_columns + continuous_columns
# List of IV values
>>> Iv_list = []
>>> for col in total_columns:
...    assigned_data = IV_calc(data = credit_data,var = col)
...    iv_val = round(assigned_data["I_V"].sum(),3)
...    dt_type = credit_data[col].dtypes
...    Iv_list.append((iv_val,col,dt_type))

>>> Iv_list = sorted(Iv_list,reverse = True)

>>> for i in range(len(Iv_list)):
...    print (Iv_list[i][0],",",Iv_list[i][1],",type =",Iv_list[i][2])

在下面的输出中,所有具有信息值的变量都以降序显示。在信息值、变量名和变量类型也被显示之后。如果类型是object,这意味着它是一个分类变量;同样,如果 type 是int64,这意味着它是一个 64 位整数值。我们将在下一阶段的分析中考虑前 15 个变量。

保留前 15 个变量后,我们在离散和连续类别中有以下变量。随后,我们将对离散变量进行虚拟变量编码,并使用连续变量。

>>> dummy_stseca = pd.get_dummies(credit_data['Status_of_existing_checking_account'], prefix='status_exs_accnt')
>>> dummy_ch = pd.get_dummies(credit_data['Credit_history'], prefix='cred_hist')
>>> dummy_purpose = pd.get_dummies(credit_data['Purpose'], prefix='purpose')
>>> dummy_savacc = pd.get_dummies(credit_data['Savings_Account'], prefix='sav_acc')
>>> dummy_presc = pd.get_dummies(credit_data['Present_Employment_since'], prefix='pre_emp_snc')
>>> dummy_perssx = pd.get_dummies(credit_data['Personal_status_and_sex'], prefix='per_stat_sx')
>>> dummy_property = pd.get_dummies(credit_data['Property'], prefix='property')
>>> dummy_othinstpln = pd.get_dummies(credit_data['Other_installment_plans'], prefix='oth_inst_pln')
>>> dummy_forgnwrkr = pd.get_dummies(credit_data['Foreign_worker'], prefix='forgn_wrkr')
>>> dummy_othdts = pd.get_dummies(credit_data['Other_debtors'], prefix='oth_debtors')

>>> continuous_columns = ['Duration_in_month', 'Credit_amount', 'Installment_rate_in_percentage_of_disposable_income', 'Age_in_years', 'Number_of_existing_credits_at_this_bank' ]

>>> credit_continuous = credit_data[continuous_columns]
>>> credit_data_new = pd.concat([dummy_stseca,dummy_ch, dummy_purpose,dummy_savacc, dummy_presc,dummy_perssx, dummy_property, dummy_othinstpln,dummy_othdts, dummy_forgnwrkr,credit_continuous,credit_data['class']],axis=1)

数据已经在训练和测试之间以 70-30 的比例平均分割,random_state42用作伪随机数生成的种子,以便在多次运行多个用户时产生可再现的结果。

>>> x_train,x_test,y_train,y_test = train_test_split( credit_data_new.drop(['class'] ,axis=1),credit_data_new['class'],train_size = 0.7,random_state=42)

>>> y_train = pd.DataFrame(y_train)
>>> y_test = pd.DataFrame(y_test)

使用pd.get_dummies()函数生成虚拟变量时,生成的虚拟数量等于其中的类数量。然而,与一个变量中的类的数量相比,在一个数量中创建的虚拟变量的数量足够少(如果所有其他剩余的变量都是0,这将代表一个额外的类)以在该设置中表示。例如,如果样本变量的类别决策响应可以取三个值中的任何一个(不能说),这可以用两个伪变量( d1d2 来表示所有的设置。

  • 如果D1 = 1**D2 = 0,我们可以分配类别
  • 如果D1 = 0**D2 = 1,我们可以分配类别
  • 如果D1 = 0**D2 = 0,我们可以分配类别 不能说

这样,使用两个虚拟变量,我们表示了所有三个类别。同样,我们可以用 N-1 虚拟变量来表示 N 类变量。

事实上,具有相同数量的伪变量会在输出中产生 NAN 值,因为它会产生冗余。因此,我们在这里从所有已创建模型的分类变量中删除一个额外的类别列:

>>> remove_cols_extra_dummy = ['status_exs_accnt_A11', 'cred_hist_A30', 'purpose_A40', 'sav_acc_A61','pre_emp_snc_A71','per_stat_sx_A91', 'oth_debtors_A101','property_A121', 'oth_inst_pln_A141','forgn_wrkr_A201']

在这里,我们已经创建了额外的列表,用于在使用向后消除方法时一步一步迭代地消除无关紧要的变量;每次迭代结束后,我们会不断将最无关紧要且多共线的变量添加到remove_cols_insig列表中,这样在训练模型时那些变量就会被移除。

>>> remove_cols_insig = []
>>> remove_cols = list(set(remove_cols_extra_dummy+remove_cols_insig))

现在进行模型最重要的一步,应用Logit函数、n变量,并创建总结:

>>> import statsmodels.api as sm
>>> logistic_model = sm.Logit(y_train, sm.add_constant(x_train.drop( remove_cols, axis=1))).fit()
>>> print (logistic_model.summary())

汇总代码生成如下输出,其中最不重要的变量是purpose_A46,p 值为0.937:

此外,计算 VIF 值以检查多重共线性,尽管在使用 VIF > 5 移除多重共线变量之前,需要移除无关紧要的变量。从以下结果来看,Per_stat_sx_A93是 VIF 为6.177的最多共线变量:

>>> print ("\nVariance Inflation Factor")
>>> cnames = x_train.drop(remove_cols,axis=1).columns
>>> for i in np.arange(0,len(cnames)):
...   xvars = list(cnames)
...   yvar = xvars.pop(i)
...   mod = sm.OLS(x_train.drop(remove_cols,axis=1)[yvar], sm.add_constant( x_train.drop (remove_cols,axis=1)[xvars]))
...   res = mod.fit()
...   vif = 1/(1-res.rsquared)
...   print (yvar,round(vif,3))

我们还检查分类器在尝试预测结果方面有多好,为此我们将运行 c 统计值,该值计算总对中一致对的比例。该值越高越好,但在生产环境中部署模型时,该值至少应为 0.7。下面的代码描述了根据第一性原理计算 c-统计量的各个步骤:

>>> y_pred = pd.DataFrame (logistic_model. predict(sm.add_constant (x_train.drop (remove_cols,axis=1))))
>>> y_pred.columns = ["probs"]
>>> both = pd.concat([y_train,y_pred],axis=1)

Zeros 是从实际和预测表中分离出来的数据,条件作为实际类应用于 Zeros。然而,一是一个实际类的条件应用于一的分裂。

>>> zeros = both[['class','probs']][both['class']==0]
>>> ones = both[['class','probs']][both['class']==1]

>>> def df_crossjoin(df1, df2, **kwargs):
...   df1['_tmpkey'] = 1
...   df2['_tmpkey'] = 1
...   res = pd.merge(df1, df2, on='_tmpkey', **kwargs).drop('_tmpkey', axis=1)
...   res.index = pd.MultiIndex.from_product((df1.index, df2.index))
...   df1.drop('_tmpkey', axis=1, inplace=True)
...   df2.drop('_tmpkey', axis=1, inplace=True)
...   return res

在接下来的步骤中,我们将为 1 和 0 数据生成笛卡尔乘积,以计算一致和不一致对:

>>> joined_data = df_crossjoin(ones,zeros)

如果对1类的概率高于0类,则一对是一致的;如果对1类的概率低于0类,则一对是不一致的。如果两个概率相同,我们就把它们归入成对范畴。和谐对的数量越高,模型越好!

>>> joined_data['concordant_pair'] = 0
>>> joined_data.loc[joined_data['probs_x'] > joined_data['probs_y'], 'concordant_pair'] =1
>>> joined_data['discordant_pair'] = 0
>>> joined_data.loc[joined_data['probs_x'] < joined_data['probs_y'], 'discordant_pair'] =1
>>> joined_data['tied_pair'] = 0
>>> joined_data.loc[joined_data['probs_x'] == joined_data['probs_y'],'tied_pair'] =1
>>> p_conc = (sum(joined_data['concordant_pair'])*1.0 )/ (joined_data.shape[0])
>>> p_disc = (sum(joined_data['discordant_pair'])*1.0 )/ (joined_data.shape[0])

>>> c_statistic = 0.5 + (p_conc - p_disc)/2.0
>>> print ("\nC-statistic:",round(c_statistic,4))

得到的c-statistic0.8388,大于 0.7 需要认为是好模型。

我们将始终关注 c-统计和对数似然(AIC)是如何变化的(这里是-309.29),同时逐一移除各种变量,以证明何时停止。

在移除无关紧要的变量purpose_A46之前,检查其 VIF 和Per_stat_sx_A93变量的 p 值非常重要。在某些情况下,我们可能需要同时考虑这两个指标,并进行权衡。

但是,下表是我们需要移除pupose_A46变量的明确结果:

移除purpose_A46变量后,我们需要重复该过程,直到不存在无关紧要的多共线变量。然而,我们需要关注 AIC 和 c 统计值是如何变化的。在下面的代码中,我们已经显示了一个接一个移除变量的顺序,但是我们鼓励用户亲自动手来独立验证结果:

>>> remove_cols_insig = ['purpose_A46', 'purpose_A45', 'purpose_A44', 'sav_acc_A63', ... 'oth_inst_pln_A143','property_A123', 'status_exs_accnt_A12', 'pre_emp_snc_A72', ... 'pre_emp_snc_A75','pre_emp_snc_A73', 'cred_hist_A32', 'cred_hist_A33', ... 'purpose_A410','pre_emp_snc_A74','purpose_A49', 'purpose_A48', 'property_A122', ... 'per_stat_sx_A92','forgn_wrkr_A202','per_stat_sx_A94', 'purpose_A42', ... 'oth_debtors_A102','Age_in_years','sav_acc_A64','sav_acc_A62', 'sav_acc_A65', ... 'oth_debtors_A103']

最后,在消除了无关紧要的和多共线的变量之后,获得了以下最终结果:

  • Log-Likelihood : -334.35
  • c-statistic : 0.8035

如果我们将这些与初始值进行比较,log-可能性从-309.29 降至-334.35,这是一个好迹象,c-statistics 也从 0.8388 略微降至 0.8035。但尽管如此,该模型仍然适用,变量数量要少得多。在不太影响模型性能的情况下移除额外的变量也会在模型的实现过程中提高效率!

在下面的图表中绘制了 ROC 曲线与 TPR 对 FPR 的关系,并且描述了曲线下的面积,其值为 0.80

>>> import matplotlib.pyplot as plt 
>>> from sklearn import metrics 
>>> from sklearn.metrics import auc 
>>> fpr, tpr, thresholds = metrics.roc_curve(both['class'],both['probs'], pos_label=1) 

>>> roc_auc = auc(fpr,tpr) 
>>> plt.figure() 
>>> lw = 2 
>>> plt.plot(fpr, tpr, color='darkorange', lw=lw, label='ROC curve (area = %0.2f)' % roc_auc) 
>>> 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 (1-Specificity)') 
>>> plt.ylabel('True Positive Rate') 
>>> plt.title('ROC Curve - German Credit Data') 
>>> plt.legend(loc="lower right") 
>>> plt.show() 

一旦我们从训练数据集中找到了最佳情况,下一个也是最后一个任务就是从概率到默认值预测类别。有许多方法可以设置阈值,将预测概率转换为实际类别。在下面的代码中,我们进行了简单的网格搜索,以确定最佳概率阈值截止值。尽管如此,甚至灵敏度和特异性曲线也可以用于这项任务。

>>> for i in list(np.arange(0,1,0.1)):
...   both["y_pred"] = 0
...   both.loc[both["probs"] > i, 'y_pred'] = 1
...   print ("Threshold",i,"Train Accuracy:", round(accuracy_score(both['class'], both['y_pred']),4))

从前面的结果中,我们发现,在阈值0.5值处,我们可以看到0.7713的最大精度。因此,我们也在0.5设置了测试数据分类的阈值:

>>> both["y_pred"] = 0
>>> both.loc[both["probs"] > 0.5, 'y_pred'] = 1
>>> print ("\nTrain Confusion Matrix\n\n", pd.crosstab(both['class'], both['y_pred'], ... rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nTrain Accuracy:",round(accuracy_score(both['class'],both['y_pred']),4))

接下来讨论结果。在将阈值设置为0.5并将预测概率分类为01类后,混淆矩阵已经通过取行中的实际值和列中的预测值来计算,这表明0.7713的准确性为 77.13%:

现在,将对测试数据应用0.5阈值,以验证模型在各种数据集之间是否一致,代码如下:

>>> y_pred_test = pd.DataFrame( logistic_model.predict( sm.add_constant( ... x_test.drop(remove_cols,axis=1))))
>>> y_pred_test.columns = ["probs"]
>>> both_test = pd.concat([y_test,y_pred_test],axis=1)
>>> both_test["y_pred"] = 0
>>> both_test.loc[both_test["probs"] > 0.5, 'y_pred'] = 1
>>> print ("\nTest Confusion Matrix\n\n", pd.crosstab( both_test['class'], ... both_test['y_pred'],rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nTest Accuracy:", round(accuracy_score( both_test['class'], ... both_test['y_pred']),4))

从测试数据的结果来看,获得的准确度为0.8053或 80.53%;我们的逻辑回归分类器对默认和非默认进行了非常有力的分类!

逻辑回归的 r 码如下:

# Variable Importance 
library(mctest) 
library(dummies) 
library(Information) 
library(pROC) 
credit_data = read.csv("credit_data.csv") 
credit_data$class = credit_data$class-1
 # I.V Calculation 
IV <- create_infotables(data=credit_data, y="class", parallel=FALSE)
for (i in 1:length(colnames(credit_data))-1){ 
 seca = IV[[1]][i][1] 
 sum(seca[[1]][5]) 
 print(paste(colnames(credit_data)[i],",IV_Value:",round(sum(seca[[1]][5]),4)))
} 
 # Dummy variables creation 
dummy_stseca =data.frame(dummy(credit_data$Status_of_existing_checking_account))
dummy_ch = data.frame(dummy(credit_data$Credit_history)) 
dummy_purpose = data.frame(dummy(credit_data$Purpose))
dummy_savacc = data.frame(dummy(credit_data$Savings_Account)) dummy_presc = data.frame(dummy(credit_data$Present_Employment_since)) dummy_perssx = data.frame(dummy(credit_data$Personal_status_and_sex)) dummy_othdts = data.frame(dummy(credit_data$Other_debtors)) dummy_property = data.frame(dummy(credit_data$Property)) dummy_othinstpln = data.frame(dummy(credit_data$Other_installment_plans)) 
dummy_forgnwrkr = data.frame(dummy(credit_data$Foreign_worker))
 # Cleaning the variables name from . to _ 
colClean <- function(x){ colnames(x) <- gsub("\\.", "_", colnames(x)); x } 
dummy_stseca = colClean(dummy_stseca) ;dummy_ch = colClean(dummy_ch) dummy_purpose = colClean(dummy_purpose); dummy_savacc= colClean(dummy_savacc) 
dummy_presc= colClean(dummy_presc);dummy_perssx= colClean(dummy_perssx); 
dummy_othdts= colClean(dummy_othdts);dummy_property= colClean(dummy_property); 
dummy_othinstpln= colClean(dummy_othinstpln);dummy_forgnwrkr= colClean(dummy_forgnwrkr); 

continuous_columns = c('Duration_in_month', 'Credit_amount','Installment_rate_in_percentage_of_disposable_income', 'Age_in_years','Number_of_existing_credits_at_this_bank')
credit_continuous = credit_data[,continuous_columns]
credit_data_new = cbind(dummy_stseca,dummy_ch,dummy_purpose,dummy_savacc,dummy_presc,dummy_perssx, dummy_othdts,dummy_property,dummy_othinstpln,dummy_forgnwrkr,credit_continuous,credit_data$class) 
colnames(credit_data_new)[51] <- "class" 
 # Setting seed for repeatability of results of train & test split
set.seed(123) 
numrow = nrow(credit_data_new) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 
train_data = credit_data_new[trnind,] 
test_data = credit_data_new[-trnind,] 

remove_cols_extra_dummy = c("Status_of_existing_checking_account_A11","Credit_history_A30", "Purpose_A40", "Savings_Account_A61", "Present_Employment_since_A71", "Personal_status_and_sex_A91" "Other_debtors_A101", "Property_A121", "Other_installment_plans_A141", "Foreign_worker_A201") 

# Removing insignificant variables one by one 
remove_cols_insig = c("Purpose_A46","Purpose_A45","Purpose_A44","Savings_Account_A63", "Other_installment_plans_A143", "Property_A123", "Status_of_existing_checking_account_A12", "Present_Employment_since_A72", "Present_Employment_since_A75", "Present_Employment_since_A73","Credit_history_A32","Credit_history_A33", "Purpose_A40","Present_Employment_since_A74","Purpose_A49","Purpose_A48", "Property_A122","Personal_status_and_sex_A92","Foreign_worker_A202", "Personal_status_and_sex_A94","Purpose_A42","Other_debtors_A102", "Age_in_years","Savings_Account_A64","Savings_Account_A62", "Savings_Account_A65", "Other_debtors_A103") 
remove_cols = c(remove_cols_extra_dummy,remove_cols_insig) 
glm_fit = glm(class ~.,family = "binomial",data = train_data[,!(names(train_data) %in% remove_cols)])
 # Significance check - p_value summary(glm_fit) 

# Multi collinearity check - VIF 
remove_cols_vif = c(remove_cols,"class") 
vif_table = imcdiag(train_data[,!(names(train_data) %in% remove_cols_vif)],train_data$class,detr=0.001, conf=0.99) 
vif_table
 # Predicting probabilities
 train_data$glm_probs = predict(glm_fit,newdata = train_data,type = "response") 
test_data$glm_probs = predict(glm_fit,newdata = test_data,type = "response") 
 # Area under 
ROC ROC1 <- roc(as.factor(train_data$class),train_data$glm_probs) plot(ROC1, col = "blue") 
print(paste("Area under the curve",round(auc(ROC1),4))) 
 # Actual prediction based on threshold tuning 
threshold_vals = c(0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9) 
for (thld in threshold_vals){ 
 train_data$glm_pred = 0
 train_data$glm_pred[train_data$glm_probs>thld]=1 
 tble = table(train_data$glm_pred,train_data$class) 
 acc = (tble[1,1]+tble[2,2])/sum(tble)
 print(paste("Threshold",thld,"Train accuracy",round(acc,4))) 
} 
 # Best threshold from above search is 0.5 with accuracy as 0.7841
best_threshold = 0.5 
 # Train confusion matrix & accuracy 
train_data$glm_pred = 0
train_data$glm_pred[train_data$glm_probs>best_threshold]=1 
tble = table(train_data$glm_pred,train_data$class) 
acc = (tble[1,1]+tble[2,2])/sum(tble) 
print(paste("Confusion Matrix - Train Data"))
 print(tble) print(paste("Train accuracy",round(acc,4))) 

# Test confusion matrix & accuracy 
test_data$glm_pred = 0 test_data$glm_pred[test_data$glm_probs>best_threshold]=1 
tble_test = table(test_data$glm_pred,test_data$class) 
acc_test = (tble_test[1,1]+tble_test[2,2])/sum(tble_test)
print(paste("Confusion Matrix - Test Data")) print(tble_test)
print(paste("Test accuracy",round(acc_test,4))) 

随机森林

随机森林 ( RF )是数据科学领域中经常使用的一种非常强大的技术,用于解决跨行业的各种问题,也是赢得像 Kaggle 这样的竞赛的银弹。我们将在下一章从基础到深入介绍各种概念;这里我们只限于最基本的必需品。随机森林是决策树的集合,我们知道,逻辑回归有很高的偏差和低方差技术;另一方面,决策树具有高方差和低偏差,使得决策树不稳定。通过平均决策树,我们将最小化模型的方差分量,这使得近似最接近理想模型。

RF 侧重于对训练数据的观测值和变量进行采样,以开发独立的决策树,并分别对分类和回归问题进行多数投票。相比之下,bagging 只随机对观察值进行采样,并为所有决策树选择所有缺少代表根显著变量的列。这种方式使得树木相互依赖,准确性将受到影响。

以下是使用随机森林从观测中选择子样本时的一些经验法则。尽管如此,任何参数都可以调整,以进一步提高结果!每棵树都是根据从训练数据中提取的采样数据开发的,并如图所示进行拟合

每棵单独的树的训练数据中大约 2/3 的观测值

选择列 sqrt(p) - >对于分类问题,如果 p 是训练数据中的总列数

如果 p 是列数,回归问题的 p/3 - >

在下图中,两个样本显示为蓝色和粉色,在打包场景中,选择了一些观察值和所有列。而在随机森林中,选择一些观察值和列来创建不相关的单个树。

下图中,一个示例想法展示了射频分类器的工作原理。每棵树都是单独生长的,每棵树的深度因所选样本而异,但最终会进行投票来决定最终的类别。

由于决策树的集成,射频具有可解释性,无法确定每个变量的显著性;相反,只能提供可变重要性。在下图中,提供了一个可变绩效的样本,包括基尼系数的平均下降:

使用德国信用数据的随机森林示例

同样的德国信用数据被用来说明随机森林模型,以便提供苹果对苹果的比较。与逻辑回归相比,任何人都可以观察到的一个非常显著的差异是,应用于数据预处理的工作量急剧减少。以下差异值得一提:

  • 在射频中,我们没有从基于显著性和 VIF 值的分析中逐个移除变量,因为显著性测试不适用于最大似然模型。然而,对训练数据进行了五重交叉验证,以确保模型的稳健性。
  • 我们在逻辑回归程序中移除了一个额外的虚拟变量,而在射频中,我们没有从分析中移除额外的虚拟变量,因为后者会自动处理多重共线性。事实上,建立集成的基础单一模型是决策树,多重共线性对它来说根本不是问题。我们将在下一章深入讨论决策树。
  • 随机森林比逻辑回归需要更少的人力和干预来训练模型。这种工作方式使得 ML 模型成为软件工程师的最爱,让他们可以轻松地部署它们。此外,ML 模型可以基于数据自动学习,没有太多麻烦。

应用于德国信用数据的随机森林:

>>> import pandas as pd
>>> from sklearn.ensemble import RandomForestClassifier
 >>> credit_data = pd.read_csv("credit_data.csv")
>>> credit_data['class'] = credit_data['class']-1

虚拟变量的创建步骤类似于逻辑回归预处理步骤:

>>> dummy_stseca = pd.get_dummies(credit_data['Status_of_existing_checking_account'], prefix='status_exs_accnt')
>>> dummy_ch = pd.get_dummies(credit_data['Credit_history'], prefix='cred_hist')
>>> dummy_purpose = pd.get_dummies(credit_data['Purpose'], prefix='purpose')
>>> dummy_savacc = pd.get_dummies(credit_data['Savings_Account'], prefix='sav_acc')
>>> dummy_presc = pd.get_dummies(credit_data['Present_Employment_since'], prefix='pre_emp_snc')
>>> dummy_perssx = pd.get_dummies(credit_data['Personal_status_and_sex'], prefix='per_stat_sx')
>>> dummy_othdts = pd.get_dummies(credit_data['Other_debtors'], prefix='oth_debtors')
>>> dummy_property = pd.get_dummies(credit_data['Property'], prefix='property')
>>> dummy_othinstpln = pd.get_dummies(credit_data['Other_installment_plans'], prefix='oth_inst_pln')
>>> dummy_housing = pd.get_dummies(credit_data['Housing'], prefix='housing')
>>> dummy_job = pd.get_dummies(credit_data['Job'], prefix='job')
>>> dummy_telephn = pd.get_dummies(credit_data['Telephone'], prefix='telephn')
>>> dummy_forgnwrkr = pd.get_dummies(credit_data['Foreign_worker'], prefix='forgn_wrkr')

>>> continuous_columns = ['Duration_in_month', 'Credit_amount', 'Installment_rate_in_percentage_of_disposable_income', 'Present_residence_since','Age_in_years','Number_of_existing_credits_at_this_bank',
'Number_of_People_being_liable_to_provide_maintenance_for']

>>> credit_continuous = credit_data[continuous_columns]

在接下来的变量组合步骤中,我们没有从所有分类变量中移除一个额外的虚拟变量。由于变量相对于所有其他变量的代表性,为status_of_existing_checking_account变量创建的所有虚拟变量都已在随机森林中使用,而不是在逻辑回归中删除的一列。

>>> credit_data_new = pd.concat([dummy_stseca, dummy_ch,dummy_purpose, dummy_savacc,dummy_presc,dummy_perssx,dummy_othdts, dummy_property, dummy_othinstpln,dummy_housing,dummy_job, dummy_telephn, dummy_forgnwrkr, credit_continuous,credit_data['class']],axis=1)

在下面的示例中,数据被分成 70-30 份。原因是,在训练期间,我们将在网格搜索中执行五重交叉验证,这将产生类似的效果,即分别将数据分成 50-25-25 的训练、验证和测试数据集。

>>> x_train,x_test,y_train,y_test = train_test_split( credit_data_new.drop( ['class'],axis=1),credit_data_new['class'],train_size = 0.7,random_state=42)

随机森林 ML 模型应用于假设的超参数值,如下所示:

  • 树木数量为1000
  • 纵切的标准是gini
  • 每棵决策树可以生长的最大深度为100
  • 每次不符合分裂条件所需的最小观测值为3
  • 树节点中的最小观察次数应为2

但是,需要使用网格搜索来调整最佳参数值:

>>> rf_fit = RandomForestClassifier( n_estimators=1000, criterion="gini", max_depth=100, min_samples_split=3,min_samples_leaf=2)
>>> rf_fit.fit(x_train,y_train)

>>> print ("\nRandom Forest -Train Confusion Matrix\n\n", pd.crosstab(y_train, rf_fit.predict( x_train),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\n Random Forest - Train accuracy",round(accuracy_score( y_train, rf_fit.predict(x_train)),3))

>>> print ("\nRandom Forest - Test Confusion Matrix\n\n",pd.crosstab(y_test, rf_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nRandom Forest - Test accuracy",round(accuracy_score(y_test, rf_fit.predict(x_test)),3))

从观察上述结果来看,随机森林产生的测试精度为0.855,远高于逻辑回归结果的测试精度 0.8053,即使在仔细调整和去除无关紧要的多共线变量后也是如此。这整个现象归结为偏见与差异权衡的核心主题。线性模型非常稳健,并且没有足够的方差来拟合数据中的非线性,然而,使用集成技术,我们可以最小化来自传统决策树的方差误差,这产生了具有来自偏差和方差分量的最小误差的结果。

通过使用网格搜索方法来获得最优超参数,可以进一步优化随机森林的精度,对于最优超参数,精度可以比随机选择的超参数高得多。在下一节中,我们将详细介绍网格搜索方法。

随机森林上的网格搜索

通过使用以下设置更改各种超参数来执行网格搜索。但是,我们鼓励读者尝试其他参数,以便在此领域进一步探索。

  • 树木数量为(1000,2000,3000)
  • 最大深度为(100,200,300)
  • 每次分裂的最小样本数为(2,3)
  • 叶节点中的最小样本为(1,2)

导入Pipeline如下:

>>> from sklearn.pipeline import Pipeline >>> from sklearn.model_selection import train_test_split,GridSearchCV

Pipeline功能创建组合,这些组合将依次应用,以确定最佳组合:

>>> pipeline = Pipeline([ ('clf',RandomForestClassifier(criterion='gini'))]) >>> parameters = {
 ...    'clf__n_estimators':(1000,2000,3000), ...    'clf__max_depth':(100,200,300), ...    'clf__min_samples_split':(2,3), ...    'clf__min_samples_leaf':(1,2) }

在下文中,网格搜索利用 5 的交叉验证来确保模型的健壮性,这是创建模型两点验证的 ML 方式:

>>> grid_search = GridSearchCV(pipeline,parameters,n_jobs=-1, cv=5, verbose=1, ... scoring='accuracy')
>>> grid_search.fit(x_train,y_train)

>>> print ('Best Training score: %0.3f' % grid_search.best_score_)
>>> print ('Best parameters set:')
>>> best_parameters = grid_search.best_estimator_.get_params()
>>> for param_name in sorted(parameters.keys()):
 ...    print ('\t%s: %r' % (param_name, best_parameters[param_name]))

>>> predictions = grid_search.predict(x_test)

>>> print ("Testing accuracy:",round(accuracy_score(y_test, predictions),4))
>>> print ("\nComplete report of Testing data\n",classification_report(y_test, ... predictions))

>>> print ("\n\nRandom Forest Grid Search- Test Confusion Matrix\n\n", pd.crosstab(y_test, predictions,rownames = ["Actuall"],colnames = ["Predicted"]))

从网格搜索的结果来看,很明显最佳测试精度为0.8911或 89.11%,这比逻辑回归模型提高了约 10%。通过提高 10%的预测准确性,因批准不良客户贷款而导致的损失将大大减少。

In a simple random forest model, train accuracy is 97.4 percent, but test accuracy is comparatively lower at 85.5 percent; whereas, in grid search methodology, train accuracy is 82 percent, but test accuracy is 89.11 percent. This highlights the issue of overfitting by building a model on single data compared with a five-fold cross validation methodology used in grid search. Hence, it is advisable to perform cross-validation to avoid over-fitting problems and ensure robustness in machine learning models.

最后,如果我们将逻辑回归的混淆矩阵与随机森林进行比较,误报将大大减少。

  • 逻辑回归——43 个实际违约客户被预测为非违约类别
  • 带有网格搜索的随机森林—32 个实际默认客户已被预测为非默认类别

这 11 个额外客户所产生的损失将通过信用行业的机器学习模型来消除,这是通过给不值得的客户提供信用来避免巨大损失的救命稻草!

对德国信用数据进行网格搜索的随机森林的 R 代码如下:

# Random Forest 
library(randomForest) 
library(e1071) 
credit_data = read.csv("credit_data.csv") 
credit_data$class = credit_data$class-1 
credit_data$class = as.factor(credit_data$class) 

set.seed(123) 
numrow = nrow(credit_data) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 
train_data = credit_data[trnind,] 
test_data = credit_data[-trnind,] 

rf_fit = randomForest(class~.,data = train_data, mtry=4, maxnodes= 2000,ntree=1000,nodesize = 2) 
rf_pred = predict(rf_fit,data = train_data,type = "response") 
rf_predt = predict(rf_fit,newdata = test_data,type ="response") 

tble = table(train_data$class,rf_pred) 
tblet = table(test_data$class,rf_predt) 

acc = (tble[1,1]+tble[2,2])/sum(tble) 
acct = (tblet[1,1]+tblet[2,2])/sum(tblet) 
print(paste("Train acc",round(acc,4),"Test acc",round(acct,4))) 

# Grid Search 
rf_grid = tune(randomForest,class~.,data = train_data,ranges = list( mtry = c(4,5), 
 maxnodes = c(700,1000),
 ntree = c(1000,2000,3000), 
 nodesize = c(1,2)
), 
tunecontrol = tune.control(cross = 5) 
) 
summary(rf_grid) 
best_model = rf_grid$best.model 
summary(best_model)

 y_pred_train = predict(best_model,data = train_data) 
train_conf_mat = table(train_data$class,y_pred_train)
print(paste("Train Confusion Matrix - Grid Search:")) print(train_conf_mat)
train_acc = (train_conf_mat[1,1]+ train_conf_mat[2,2])/sum(train_conf_mat)
print(paste("Train_accuracy-Grid Search:",round(train_acc,4)))

y_pred_test = predict(best_model,newdata = test_data) 
test_conf_mat = table(test_data$class,y_pred_test)
print(paste("Test Confusion Matrix - Grid Search:")) print(test_conf_mat) 

test_acc = (test_conf_mat[1,1]+ test_conf_mat[2,2]) /sum(test_conf_mat)
print(paste("Test_accuracy-Grid Search:",round(test_acc,4)))

可变重要性图

变量重要性图按照基尼系数的平均下降值,按降序列出了最重要的变量。顶部变量比底部变量对模型的贡献更大,并且在对违约和非违约客户进行分类时具有很高的预测能力。

令人惊讶的是,在 Python scikit-learn 中,网格搜索没有可变重要性功能,因此我们使用网格搜索的最佳参数,并用简单的随机森林scikit-learn功能绘制可变重要性图。然而,在 R 编程中,我们有这样的规定,因此 R 代码在这里是紧凑的:

>>> import matplotlib.pyplot as plt 
>>> rf_fit = RandomForestClassifier(n_estimators=1000, criterion="gini", max_depth=300, min_samples_split=3,min_samples_leaf=1) 
>>> rf_fit.fit(x_train,y_train)    
>>> importances = rf_fit.feature_importances_ 
>>> std = np.std([tree.feature_importances_ for tree in rf_fit.estimators_], axis=0) 
>>> indices = np.argsort(importances)[::-1] 

>>> colnames = list(x_train.columns) 
# Print the feature ranking 
>>> print("\nFeature ranking:\n") 
>>> for f in range(x_train.shape[1]): 
...    print ("Feature", indices[f], ",", colnames[indices[f]], round(importances [indices[f]],4)) 

>>> plt.figure() 
>>> plt.bar(range(x_train.shape[1]), importances[indices], color="r", yerr= std[indices],  align="center") 
>>> plt.xticks(range(x_train.shape[1]), indices) 
>>> plt.xlim([-1, x_train.shape[1]]) 
>>> plt.show()

可变重要性随机森林的 r 代码如下:

# Variable Importance
vari = varImpPlot(best_model)
print(paste("Variable Importance - Table")) 
print(vari)

由于存在许多变量,因此很难在图上表示变量和名称,因此相同的内容如下所示。Credit_amount首先代表预测基尼系数平均下降的变量0.1075,随后是其他变量:

logistic 回归与随机森林的比较

监管机构给信贷风险行业带来的一个主要问题是机器学习模型的黑箱特性。本节着重于绘制逻辑回归和随机森林模型之间的相似之处,以创建随机森林的透明度,这样在批准机器学习模型的实施时,监管者就不会那么害怕了。最后但同样重要的是,读者还将接受统计模型与机器学习模型的比较教育。

在下表中,两个模型的解释变量根据它们对模型贡献的重要性按降序排列。在逻辑回归模型中,它是 p 值(最小值是更好的预测值),对于随机森林,它是基尼系数的平均下降(最大值是更好的预测值)。许多变量在重要性上非常匹配,例如,status_exs_accnt_A14credit_hist_A34Installment_rate_in_percentage_of_disposable_incomeproperty_A_24Credit_amountDuration_in_month等等。

读者不应该忽视的一个主要的潜在事实是,重要的变量在任何模型中都是重要的,无论是统计模型还是机器学习模型。但是,通过仔细比较这种方式,信贷和风险部门可以向监管机构提供解释,并说服他们实施机器学习模型。

摘要

在本章中,您已经学习了逻辑回归的工作原理及其逐步求解方法,通过不断检查 AIC 值和一致性值以统计方式确定最佳模型,迭代移除无关紧要的和多共线的变量来找到最佳拟合。随后,我们研究了机器学习模型和随机森林在计算测试精度中的应用。我们发现,通过使用网格搜索仔细调整随机森林的超参数,我们能够将结果的测试精度提高 10%,从逻辑回归的 80%提高到随机森林的 90%。

在下一章中,我们将覆盖完整的基于树的模型,如决策树、随机森林、增强树、模型集成等,以进一步提高准确性!

四、基于树的机器学习模型

基于树的方法的目标是将特征空间分割成多个简单的矩形区域,随后基于其所属区域中的训练观测值的均值或模式(准确地说,回归的均值和分类的模式)对给定观测值进行预测。与大多数其他分类器不同,决策树生成的模型易于解释。在本章中,我们将介绍以下基于决策树的人力资源数据模型示例,用于预测给定员工是否会在不久的将来离开组织。在本章中,我们将学习以下主题:

  • 决策树-简单模型和带类权重调整的模型
  • 打包(引导聚合)
  • 随机森林-基本随机森林及网格搜索在层次参数调整中的应用
  • 升压(AdaBoost、梯度升压、极限梯度升压- XGBoost)
  • 系综集合(具有异质和同质模型)

引入决策树分类器

决策树分类器以简单的英语句子产生规则,无需任何编辑即可轻松解释并呈现给高级管理层。决策树可以应用于分类或回归问题。决策树模型基于数据中的特征,学习一系列问题来推断样本的类别标签。

下图中,一个程序员自己要求简单的递归决策规则做相关的动作。这些行动将基于为每个问题提供的答案,无论是肯定的还是否定的。

决策树中使用的术语

与逻辑回归相比,决策树没有太多的机器。这里我们有几个指标要研究。我们将主要关注杂质测量;决策树根据设定的杂质标准递归地分割变量,直到它们达到某个停止标准(每个终端节点的最小观测值,任何节点上分割的最小观测值,等等):

  • 熵:熵来源于信息论,是对数据中杂质的度量。如果样本完全齐次,熵为零,如果样本等分,熵为 1。在决策树中,具有最大异质性的预测器将被认为最接近根节点,以贪婪模式将给定数据分类。我们将在本章中更深入地讨论这个主题:

其中 n =类的数量。熵在中间最大,值为 1 ,在极端最小,值为 0 。熵值越低越好,因为它可以更好地隔离班级。

  • 信息增益:信息增益是根据给定属性对示例进行划分所导致的熵的预期减少。这个想法是从混合类开始,并继续分区,直到每个节点达到它对最纯类的观察。在每个阶段,以贪婪的方式选择具有最大信息增益的变量。

*信息增益=父代熵-和(加权% 子代熵)

加权% =特定子节点中的观察数/总和(所有子节点中的观察数)

  • 基尼:基尼杂质是一种错误分类的度量,适用于多类分类器上下文。基尼系数的工作原理类似于熵,只是基尼系数的计算速度更快:

其中 i =班级数量。基尼系数和熵值的相似性如下图所示:

基于第一性原理的决策树工作方法

在下面的例子中,响应变量只有两个类:是否打网球。但是下表是根据不同日子记录的各种情况编制的。现在,我们的任务是找出变量产生的最显著的输出:是或否

  1. 该示例位于分类树下:

| | 展望 | 温度 | 湿度 | | 打网球 |
| D1 | 快活的 | 热的 | 高的 | 无力的 | 不 |
| D2 | 快活的 | 热的 | 高的 | 强烈的 | 不 |
| D3 | 遮蔽 | 热的 | 高的 | 无力的 | 是 |
| D4 | 雨 | 温和的 | 高的 | 无力的 | 是 |
| D5 | 雨 | 凉爽的 | 常态 | 无力的 | 是 |
| D6 | 雨 | 凉爽的 | 常态 | 强烈的 | 不 |
| D7 | 遮蔽 | 凉爽的 | 常态 | 强烈的 | 是 |
| D8 | 快活的 | 温和的 | 高的 | 无力的 | 不 |
| D9 | 快活的 | 凉爽的 | 常态 | 无力的 | 是 |
| D10 | 雨 | 温和的 | 常态 | 无力的 | 是 |
| D11 | 快活的 | 温和的 | 常态 | 强烈的 | 是 |
| D12 | 遮蔽 | 温和的 | 高的 | 强烈的 | 是 |
| D13 | 遮蔽 | 热的 | 常态 | 无力的 | 是 |
| D14 | 雨 | 温和的 | 高的 | 强烈的 | 不 |

  1. 以湿度变量为例对网球场地进行分类:
    • CHAID: 湿度有两个类别,我们的期望值应该均匀分布,以便计算变量的区分度:

计算x2T3(卡方)值:

计算自由度= (r-1) * (c-1)

其中 r =行组件数/变量类别数,C =响应变量数。

这里有两个行类别(高和正常)和两个列类别(否和是)。

因此= (2-1) * (2-1) = 1

卡方 2.8 的 p 值,一维 f = 0.0942

p 值可用以下 Excel 公式求得: = CHIDIST (2.8,1) = 0.0942

以类似的方式,我们将计算所有变量的 p 值,并选择 p 值较低的最佳变量。

  • :

熵=-σp * log2p

以类似的方式,我们将计算所有变量的信息增益,并选择具有最高信息增益的最佳变量。

  • 基尼:

基尼= 1-σp2T3]

以类似的方式,我们将计算所有变量的预期基尼系数,并选择期望值最低的 作为最佳值。

为了更好地理解,我们还将对风变量进行类似的计算:

  • CHAID: 风有两个类别,我们的期望值应该是均匀分布的,以便计算变量的区分度:

  • :

  • 基尼:

现在,我们将比较所有三个指标的两个变量,以便更好地理解它们。

| 变量 | CHAID
(p 值) | 熵
信息增益 | 基尼
期望值 |
| 湿度 | 0.0942 | 0.1518 | 0.3669 |
| 风 | 0.2733 | 0.0482 | 0.4285 |
| 较好的 | 低值 | 高价值 | 低值 |

对于所有三个计算,湿度被证明是比风更好的分类器。因此,我们可以确认所有的方法都传达了一个相似的故事。

逻辑回归与决策树的比较

在我们深入决策树的编码细节之前,在这里,我们将快速比较逻辑回归和决策树之间的差异,这样我们就知道哪个模型更好,以什么方式更好。

| 逻辑回归 | 决策树 |
| 逻辑回归模型看起来像是自变量和因变量之间的方程。 | 树型分类器用简单的英语句子产生规则,可以很容易地向高级管理层解释。 |
| 逻辑回归是一个参数模型,其中模型是通过将参数乘以自变量来预测因变量来定义的。 | 决策树是一种非参数模型,其中不存在预先假定的参数。隐式执行变量筛选或特征选择。 |
| 对响应(或因变量)做出假设,采用二项式或伯努利分布。 | 没有对数据的潜在分布做出假设。 |
| 模型的形状是预先定义的(逻辑曲线)。 | 模型的形状不是预定义的;相反,模型适合基于数据的最佳分类。 |
| 当自变量在本质上是连续的时,提供非常好的结果,线性也成立。 | 当大多数变量本质上是分类的时,提供最佳结果。 |
| 难以发现变量之间复杂的相互作用(变量之间的非线性关系)。 | 参数之间的非线性关系不会影响树的性能。经常揭露复杂的互动。树可以处理具有高度偏斜或多模态的数字数据,以及具有序数或非序数结构的分类预测器。 |
| 异常值和缺失值会降低逻辑回归的性能。 | 异常值和缺失值在决策树中被优雅地处理。 |

不同类型模型中误差分量的比较

需要对误差进行评估,以衡量模型的有效性,从而通过调整各种旋钮来进一步提高模型的性能。误差分量由偏差分量、方差分量和纯白噪声组成:

在以下三个地区中:

  • 第一区域具有高偏差和低方差误差分量。在这个区域,模型本质上非常稳健,例如线性回归或逻辑回归。
  • 尽管第三个区域具有高方差和低偏差误差分量,但是在这个区域中,模型非常不稳定,并且在本质上变化很大,类似于决策树,但是由于它们形状的本质的大量可变性,这些模型倾向于过度依赖于训练数据,并且在测试数据上产生较低的精度。
  • 最后但同样重要的是,中间区域,也称为第二区域,是理想的最佳位置,其中偏差和方差分量都适中,导致它产生最低的总误差。

将模型推向理想区域的补救措施

具有高偏差或高方差误差分量的模型不能产生理想的拟合。因此,需要进行一些改造。在下图中,详细显示了应用的各种方法。在线性回归的情况下,会有高偏差成分,这意味着模型不够灵活,无法拟合数据中的一些非线性。一种方法是将单线分割成小的线性片段,并通过将它们约束在节点处(也称为线性样条)将它们拟合到区域中。尽管决策树具有很高的方差问题,这意味着即使 X 值稍有变化也会导致其对应的 Y 值发生很大变化,但这个问题可以通过执行决策树的集成来解决:

实际上,实现样条曲线是一种困难且不太受欢迎的方法,因为除了检查每个单独方程的线性假设和其他诊断关键绩效指标(p 值、AIC、多重共线性等)之外,从业者还必须关注许多方程。相反,在决策树上执行集成在数据科学社区中最受欢迎,类似于打包、随机森林和 boosting,我们将在本章的后面部分深入讨论。集成技术通过聚集高度可变的个体分类器(如决策树)的结果来解决方差问题。

人力资源流失数据示例

在本节中,我们将使用根据开源许可协议https://www . Kaggle . com/pavansubhasht/IBM-HR-analytics-launch-dataset在 Kaggle 数据集中共享的 IBM Watson 的 HR launch 数据(该数据在获得数据管理员的事先许可后已在本书中使用)来预测员工是否会根据独立解释变量进行流失:

>>> import pandas as pd 
>>> hrattr_data = pd.read_csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 

>>> print (hrattr_data.head()) 

该数据中大约有 1470 个观察值和 35 个变量,这里显示了前五行,以便快速浏览变量:

出于建模目的,以下代码用于将“是”或“否”类别转换为 1 和 0,因为 scikit-learn 不直接适合字符/类别变量的模型,因此需要执行虚拟编码来利用模型中的变量:

>>> hrattr_data['Attrition_ind'] = 0 
>>> hrattr_data.loc[hrattr_data['Attrition'] =='Yes', 'Attrition_ind'] = 1 

为所有七个分类变量(此处按字母顺序显示)创建虚拟变量,它们是Business TravelDepartmentEducation FieldGenderJob RoleMarital StatusOvertime。我们在分析中忽略了四个变量,因为它们在观察中没有变化,它们是Employee countEmployee numberOver18Standard Hours:

>>> dummy_busnstrvl = pd.get_dummies(hrattr_data['BusinessTravel'], prefix='busns_trvl') 
>>> dummy_dept = pd.get_dummies(hrattr_data['Department'], prefix='dept') 
>>> dummy_edufield = pd.get_dummies(hrattr_data['EducationField'], prefix='edufield') 
>>> dummy_gender = pd.get_dummies(hrattr_data['Gender'], prefix='gend') 
>>> dummy_jobrole = pd.get_dummies(hrattr_data['JobRole'], prefix='jobrole') 
>>> dummy_maritstat = pd.get_dummies(hrattr_data['MaritalStatus'], prefix='maritalstat')  
>>> dummy_overtime = pd.get_dummies(hrattr_data['OverTime'], prefix='overtime')  

连续变量是分开的,稍后将与创建的虚拟变量合并:

>>> continuous_columns = ['Age','DailyRate','DistanceFromHome', 'Education', 'EnvironmentSatisfaction','HourlyRate','JobInvolvement','JobLevel','JobSatisfaction', 'MonthlyIncome', 'MonthlyRate', 'NumCompaniesWorked','PercentSalaryHike',  'PerformanceRating', 'RelationshipSatisfaction','StockOptionLevel', 'TotalWorkingYears', 'TrainingTimesLastYear','WorkLifeBalance', 'YearsAtCompany', 'YearsInCurrentRole', 'YearsSinceLastPromotion','YearsWithCurrManager'] 

>>> hrattr_continuous = hrattr_data[continuous_columns] 

在接下来的步骤中,从分类变量和直接连续变量导出的虚拟变量被合并:

>>> hrattr_data_new = pd.concat([dummy_busnstrvl, dummy_dept, dummy_edufield, dummy_gender, dummy_jobrole, dummy_maritstat, dummy_overtime, hrattr_continuous, hrattr_data['Attrition_ind']],axis=1) 

Here, we have not removed one extra derived dummy variable for each categorical variable due to the reason that multi-collinearity does not create a problem in decision trees as it does in either logistic or linear regression, hence we can simply utilize all the derived variables in the rest of the chapter, as all the models utilize decision trees as an underlying model, even after performing ensembles of it.

准备好基本数据后,出于培训和测试目的,需要将数据分成 70-30 份:

# Train and Test split 
>>> from sklearn.model_selection import train_test_split 
>>> x_train,x_test,y_train,y_test = train_test_split( hrattr_data_new.drop (['Attrition_ind'], axis=1),hrattr_data_new['Attrition_ind'],   train_size = 0.7, random_state=42) 

人力资源流失数据预处理代码:

hrattr_data = read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv")   
str(hrattr_data);summary(hrattr_data)   
hrattr_data$Attrition_ind = 0;   
hrattr_data$Attrition_ind[   hrattr_data$Attrition=="Yes"]=1   
hrattr_data$Attrition_ind=   as.factor(hrattr_data$Attrition_ind)   

remove_cols = c("EmployeeCount","EmployeeNumber","Over18",   "StandardHours","Attrition")   
hrattr_data_new =   hrattr_data[,!(names(hrattr_data) %in% remove_cols)]   

set.seed(123)   
numrow = nrow(hrattr_data_new)   
trnind = sample(1:numrow,size =   as.integer(0.7*numrow))   
train_data =   hrattr_data_new[trnind,]   
test_data = hrattr_data_new[-trnind,]   
 # Code for calculating   precision, recall for 0 and 1 categories and # at overall level which   will be used in all the classifiers in # later sections   
frac_trzero =   (table(train_data$Attrition_ind)[[1]])/nrow(train_data)   
frac_trone =   (table(train_data$Attrition_ind)[[2]])/nrow(train_data)   

frac_tszero =   (table(test_data$Attrition_ind)[[1]])/nrow(test_data)   
frac_tsone = (table(test_data$Attrition_ind)[[2]])/nrow(test_data)   

prec_zero <-   function(act,pred){  tble = table(act,pred)   
return( round(   tble[1,1]/(tble[1,1]+tble[2,1]),4))}   

prec_one <-   function(act,pred){ tble = table(act,pred)   
return( round(   tble[2,2]/(tble[2,2]+tble[1,2]),4))}   

recl_zero <-   function(act,pred){tble = table(act,pred)   
return( round(   tble[1,1]/(tble[1,1]+tble[1,2]),4))}   

recl_one <-   function(act,pred){ tble = table(act,pred)   
return( round(   tble[2,2]/(tble[2,2]+tble[2,1]),4))}   

accrcy <-   function(act,pred){ tble = table(act,pred)   
return(   round((tble[1,1]+tble[2,2])/sum(tble),4))} 

决策树分类器

scikit-learn 中的DecisionTtreeClassifier已用于建模目的,可在tree子模块中获得:

# Decision Tree Classifier 
>>> from sklearn.tree import DecisionTreeClassifier 

为 DT 分类器选择的参数在下面的代码中,分割标准为基尼,最大深度为 5,合格分割所需的最小观察数为 2,终端节点中应存在的最小样本数为 1:

 >>> dt_fit = DecisionTreeClassifier(criterion="gini", max_depth=5,min_samples_split=2,  min_samples_leaf=1,random_state=42) 
>>> dt_fit.fit(x_train,y_train) 

>>> print ("\nDecision Tree - Train Confusion  Matrix\n\n", pd.crosstab(y_train, dt_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))    
>>> from sklearn.metrics import accuracy_score, classification_report    
>>> print ("\nDecision Tree - Train accuracy\n\n",round(accuracy_score (y_train, dt_fit.predict(x_train)),3)) 
>>> print ("\nDecision Tree - Train Classification Report\n", classification_report(y_train, dt_fit.predict(x_train))) 

>>> print ("\n\nDecision Tree - Test Confusion Matrix\n\n",pd.crosstab(y_test, dt_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"])) 
>>> print ("\nDecision Tree - Test accuracy",round(accuracy_score(y_test, dt_fit.predict(x_test)),3)) 
>>> print ("\nDecision Tree - Test Classification Report\n", classification_report( y_test, dt_fit.predict(x_test))) 

通过仔细观察结果,我们可以推断出,即使测试准确率高(84.6%),一个类别的准确率和召回率(消耗率=是)也较低(准确率= 0.39召回率= 0.20 )。当管理层试图使用这种模式在实际流失之前主动向流失可能性高的员工提供一些额外福利时,这可能是一个严重的问题,因为这种模式无法识别将要离职的真正员工。因此,我们需要寻找其他的修改;一种方法是通过使用类权重来控制模型。通过利用类权重,我们可以以增加其他错误为代价来增加特定类的重要性。

例如,通过将类别权重增加到类别 1 ,我们可以识别更多具有实际流失特征的员工,但这样做,我们会将一些非潜在流失员工标记为潜在流失者(这应该是可以接受的)。

类别权重重要用途的另一个经典例子是,在银行场景中。在发放贷款时,拒绝一些好的申请比接受不良贷款要好。因此,即使在这种情况下,对违约者使用更高的权重比非违约者更好:

应用于人力资源流失数据的决策树分类器代码:

# Decision Trees using C5.0   package   
library(C50)   
dtree_fit = C5.0(train_data[-31],train_data$Attrition_ind,costs   = NULL,control = C5.0Control(minCases = 1))   
summary(dtree_fit)   
tr_y_pred = predict(dtree_fit,   train_data,type = "class")   
ts_y_pred =   predict(dtree_fit,test_data,type = "class")   
tr_y_act =   train_data$Attrition_ind;ts_y_act = test_data$Attrition_ind   

tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Train   Confusion Matrix"))   
print(tr_tble)   
tr_acc =   accrcy(tr_y_act,tr_y_pred)   
trprec_zero =   prec_zero(tr_y_act,tr_y_pred);    
trrecl_zero =   recl_zero(tr_y_act,tr_y_pred)   
trprec_one =   prec_one(tr_y_act,tr_y_pred);    
trrecl_one =   recl_one(tr_y_act,tr_y_pred)   
trprec_ovll = trprec_zero *frac_trzero   + trprec_one*frac_trone   
trrecl_ovll = trrecl_zero   *frac_trzero + trrecl_one*frac_trone   

print(paste("Decision Tree   Train accuracy:",tr_acc))   
print(paste("Decision Tree   - Train Classification Report"))   
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))   
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))   
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",   
round(trrecl_ovll,4)))   
 ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Test   Confusion Matrix"))   
print(ts_tble)   

ts_acc =   accrcy(ts_y_act,ts_y_pred)   
tsprec_zero =   prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred)   
tsprec_one =   prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred)   

tsprec_ovll = tsprec_zero *frac_tszero   + tsprec_one*frac_tsone   
tsrecl_ovll = tsrecl_zero   *frac_tszero + tsrecl_one*frac_tsone   

print(paste("Decision Tree   Test accuracy:",ts_acc))   
print(paste("Decision Tree   - Test Classification Report"))   
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))   
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))   
print(paste("Overall_Precision",round(tsprec_ovll,4),   
"Overall_Recall",round(tsrecl_ovll,4))) 

决策树分类器中类权重的调整

在下面的代码中,对类权重进行了调整,以查看具有相同参数的决策树的性能变化。创建一个虚拟数据框来保存组合的各种精度调用细节的所有结果:

>>> dummyarray = np.empty((6,10))
>>> dt_wttune = pd.DataFrame(dummyarray)

要考虑捕获的指标是零和一个类别的权重(例如,如果给定的零类别的权重是 0.2,那么自动地,一的权重应该是 0.8,因为总权重应该等于 1)、训练和测试精度、零类别的精度、一个类别和总体。同样,还会计算零类别、一类别和整体的召回率:

>>> dt_wttune.columns = ["zero_wght","one_wght","tr_accuracy", "tst_accuracy", "prec_zero","prec_one", "prec_ovll", "recl_zero","recl_one","recl_ovll"] 

零类别的权重从 0.01 到 0.5 进行验证,因为我们知道我们不想探索零类别将被赋予比一个类别更高的权重的情况:

>>> zero_clwghts = [0.01,0.1,0.2,0.3,0.4,0.5] 

>>> for i in range(len(zero_clwghts)): 
...    clwght = {0:zero_clwghts[i],1:1.0-zero_clwghts[i]} 
...    dt_fit = DecisionTreeClassifier(criterion="gini",  max_depth=5,               ... min_samples_split=2, min_samples_leaf=1,random_state=42,class_weight = clwght) 
...    dt_fit.fit(x_train,y_train) 
...    dt_wttune.loc[i, 'zero_wght'] = clwght[0]        
...    dt_wttune.loc[i, 'one_wght'] = clwght[1]      
...    dt_wttune.loc[i, 'tr_accuracy'] = round(accuracy_score(y_train, dt_fit.predict( x_train)),3)     
...    dt_wttune.loc[i, 'tst_accuracy'] = round(accuracy_score(y_test,dt_fit.predict( x_test)),3)     

...    clf_sp = classification_report(y_test,dt_fit.predict(x_test)).split() 
...    dt_wttune.loc[i, 'prec_zero'] = float(clf_sp[5])    
...    dt_wttune.loc[i, 'prec_one'] = float(clf_sp[10])    
...    dt_wttune.loc[i, 'prec_ovll'] = float(clf_sp[17])    

...    dt_wttune.loc[i, 'recl_zero'] = float(clf_sp[6])    
...    dt_wttune.loc[i, 'recl_one'] = float(clf_sp[11])    
...    dt_wttune.loc[i, 'recl_ovll'] = float(clf_sp[18]) 
...    print ("\nClass Weights",clwght,"Train accuracy:",round(accuracy_score( y_train,dt_fit.predict(x_train)),3),"Test accuracy:",round(accuracy_score(y_test, dt_fit.predict(x_test)),3)) 
...    print ("Test Confusion Matrix\n\n",pd.crosstab(y_test,dt_fit.predict( x_test),rownames = ["Actuall"],colnames = ["Predicted"])) 

从前面的截图中,我们可以看到,在类别权重值为 0.3(零)和 0.7(一)时,它使用决策树方法识别了更多的属性(61 个中的 25 个),而不会影响 83.9%的测试准确性:

将类别权重应用于人力资源流失数据的决策树分类器的代码:

#Decision Trees using C5.0   package - Error Costs   
library(C50)   
class_zero_wgt =   c(0.01,0.1,0.2,0.3,0.4,0.5)   

for (cwt in class_zero_wgt){   
  cwtz = cwt   
  cwto = 1-cwtz   
  cstvr = cwto/cwtz     
  error_cost <- matrix(c(0,   1, cstvr, 0), nrow = 2)     
  dtree_fit = C5.0(train_data[-31],train_data$Attrition_ind, 
 costs = error_cost,control = C5.0Control(  minCases =   1))   
  summary(dtree_fit)     
  tr_y_pred =   predict(dtree_fit, train_data,type = "class")   
  ts_y_pred =   predict(dtree_fit,test_data,type = "class")   

  tr_y_act =   train_data$Attrition_ind;   
  ts_y_act =   test_data$Attrition_ind   
  tr_acc =   accrcy(tr_y_act,tr_y_pred)   
  ts_acc =   accrcy(ts_y_act,ts_y_pred)     

  print(paste("Class   weights","{0:",cwtz,"1:",cwto,"}",   
              "Decision   Tree Train accuracy:",tr_acc,   
              "Decision   Tree Test accuracy:",ts_acc))   
  ts_tble =   table(ts_y_act,ts_y_pred)   
  print(paste("Test   Confusion Matrix"))   
  print(ts_tble)    
} 

装袋分级机

正如我们已经讨论过的,决策树具有很高的方差,这意味着如果我们将训练数据分别分成两个随机部分,并为每个样本拟合两个决策树,所获得的规则将会非常不同。而低方差和高偏差模型,如线性或逻辑回归,将在两个样本中产生相似的结果。Bagging 指的是 bootstrap 聚合(重复采样并替换,精确地执行结果的聚合),这是一种通用的方法来减少模型的方差。在这种情况下,它们是决策树。

聚合减少了方差,例如,当我们有 n 个独立的观测值 x 1 ,x 2 时,...,x n 各有方差 σ 2 ,观测值的均值 的方差由 σ 2 /n 给出,通过对一组观测值求平均值说明其减少了方差。在这里,我们通过从训练数据中获取许多样本(也称为自举),分别在每个样本上构建单独的决策树,对回归的预测进行平均,以及计算分类问题的模式来减少方差,以便获得同时具有低偏差和低方差的单个低方差模型:

在打包过程中,对行进行采样,同时选择所有的列/变量(而在随机森林中,将对行和列进行采样,这将在下一节中介绍)并为每个样本拟合单独的树。在下图中,两种颜色(粉色和蓝色)代表两个样本,对于每个样本,采样几行,但每次都选择所有的列(变量)。由于所有列的选择而存在的一个问题是,大多数树将描述相同的故事,其中最重要的变量最初将出现在分割中,并且这在所有树中重复,这不会产生去相关的树,因此我们在应用方差减少时可能不会获得更好的性能。这个问题将在 random forest 中避免(我们将在本章的下一节讨论这个问题),在本章中,我们还将对行和列进行采样:

在下面的代码中,使用相同的人力资源数据来拟合 bagging 分类器,以便在决策树方面比较苹果与苹果的结果:

# Bagging Classifier 
>>> from sklearn.tree import DecisionTreeClassifier
>>> from sklearn.ensemble import BaggingClassifier

这里使用的基本分类器是决策树,其参数设置与我们在决策树示例中使用的相同:

>>> dt_fit = DecisionTreeClassifier(criterion="gini", max_depth=5,min_samples_split=2, min_samples_leaf=1,random_state=42,class_weight = {0:0.3,1:0.7}) 

装袋中使用的参数为,n_estimators表示使用的单个决策树的数量为 5000,选择的最大样本和特征分别为 0.67 和 1.0,这意味着它将为每棵树和所有特征选择 2/3 rd 的观测值。更多详细信息,请参考 scikit-learn 手册http://sci kit-learn . org/stable/modules/generated/sklearn . ensemble . bagging classifier . html:

>>> bag_fit = BaggingClassifier(base_estimator= dt_fit,n_estimators=5000, max_samples=0.67, 
...              max_features=1.0,bootstrap=True, 
...              bootstrap_features=False, n_jobs=-1,random_state=42) 

>>> bag_fit.fit(x_train, y_train) 

>>> print ("\nBagging - Train Confusion Matrix\n\n",pd.crosstab(y_train, bag_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nBagging- Train accuracy",round(accuracy_score(y_train, bag_fit.predict(x_train)),3))  
>>> print ("\nBagging  - Train Classification Report\n",classification_report(y_train, bag_fit.predict(x_train))) 

>>> print ("\n\nBagging - Test Confusion Matrix\n\n",pd.crosstab(y_test, bag_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nBagging - Test accuracy",round(accuracy_score(y_test, bag_fit.predict(x_test)),3)) 
>>> print ("\nBagging - Test Classification Report\n",classification_report(y_test, bag_fit.predict(x_test))) 

对打包结果进行分析后,得到的测试准确率为 87.3%,而决策树的测试准确率为 84.6%。比较已识别的实际流失员工数量,装袋中有 13 名,而决策树中有 12 名,但分类为 1 的 0 的数量明显减少到 8 名,而 DT 中为 19 名。总体而言,打包提高了单棵树的性能:

人力资源损耗数据打包分类器的代码:

# Bagging Classifier - using   Random forest package but all variables selected   
library(randomForest)   
set.seed(43)   
rf_fit = randomForest(Attrition_ind~.,data   = train_data,mtry=30,maxnodes= 64,classwt = c(0.3,0.7), ntree=5000,nodesize =   1)   
tr_y_pred = predict(rf_fit,data   = train_data,type = "response")   
ts_y_pred =   predict(rf_fit,newdata = test_data,type = "response")   
tr_y_act = train_data$Attrition_ind;ts_y_act   = test_data$Attrition_ind   

tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Train   Confusion Matrix"))   
print(tr_tble)   
tr_acc =   accrcy(tr_y_act,tr_y_pred)   
trprec_zero =   prec_zero(tr_y_act,tr_y_pred); trrecl_zero = recl_zero(tr_y_act,tr_y_pred)   
trprec_one =   prec_one(tr_y_act,tr_y_pred);    
trrecl_one =   recl_one(tr_y_act,tr_y_pred)   
trprec_ovll = trprec_zero   *frac_trzero + trprec_one*frac_trone   
trrecl_ovll = trrecl_zero   *frac_trzero + trrecl_one*frac_trone   
print(paste("Random Forest   Train accuracy:",tr_acc))   
print(paste("Random Forest   - Train Classification Report"))   
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))   
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))   
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",   
round(trrecl_ovll,4)))   

ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Test   Confusion Matrix"))   
print(ts_tble)   
ts_acc =   accrcy(ts_y_act,ts_y_pred)   
tsprec_zero =   prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred)   
tsprec_one =   prec_one(ts_y_act,ts_y_pred);    
tsrecl_one =   recl_one(ts_y_act,ts_y_pred)   
tsprec_ovll = tsprec_zero   *frac_tszero + tsprec_one*frac_tsone   
tsrecl_ovll = tsrecl_zero   *frac_tszero + tsrecl_one*frac_tsone   
print(paste("Random Forest   Test accuracy:",ts_acc))   
print(paste("Random Forest   - Test Classification Report"))   
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))   
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))   
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",   
round(tsrecl_ovll,4))) 

随机森林分类器

随机森林通过利用去相关树做一个小的调整,提供了一个比套袋更好的方法。在 bagging 中,我们根据训练数据在自举样本上构建了许多决策树,但是 bagging 技术的一大缺点是它选择了所有变量。通过这样做,在每个决策树中,被选择来分割的候选/变量的顺序对于所有看起来彼此相关的单个树来说或多或少地保持相同。在聚集相关的个体实体时,减少它们的差异并不有效。

在随机森林中,在自举(重复采样和替换)过程中,从训练数据中抽取样本;不仅仅是简单地随机选择第二个和第三个观察值,类似于 bagging,它还从所有预测值中选择少数预测值/列(总 p 个预测值中的 m 个预测值)。

从总变量 p 中选择 m 个变量的经验法则是 m = sqrt(p) 用于分类,而 m = p/3 用于随机回归问题,以避免单个树之间的相关性。通过这样做,可以显著提高精度。射频的这种能力使其成为数据科学社区最喜欢使用的算法之一,成为跨越各种竞争甚至解决各种行业实际问题的制胜秘诀。

在下图中,不同的颜色代表不同的引导样本。在第一个样本中,选择第 1 st 、第 3 rd 、第 4 th、和第 7 th 列,而在第二个自举样本中,分别选择第 2 nd 、第 3 rd 、第 4 和第 5 th 列。这样,可以随机选择任何列,无论它们是否彼此相邻。虽然给出了 sqrt (p)p/3 的经验法则,但鼓励读者调整要选择的预测因子的数量:

示例图显示了在更改所选参数时测试误差变化的影响,显然 m = sqrt(p) 场景与 m =p 场景(我们可以称之为场景打包)相比,在测试数据上的表现更好:

出于说明的目的,这里使用了scikit-learn包中的随机森林分类器:

# Random Forest Classifier 
>>> from sklearn.ensemble import RandomForestClassifier 

随机森林中使用的参数是:n_estimators代表使用的个体决策树数量为 5000,选择的最大特征是 auto ,这意味着它将选择 sqrt(p) 进行分类,选择 p/3 进行自动回归。这里有一个简单的分类问题。每个叶的最小样本数提供了终端节点所需的最小观测数:

>>> rf_fit = RandomForestClassifier(n_estimators=5000,criterion="gini", max_depth=5, min_samples_split=2,bootstrap=True,max_features='auto',random_state=42, min_samples_leaf=1,class_weight = {0:0.3,1:0.7}) 
>>> rf_fit.fit(x_train,y_train)        

>>> print ("\nRandom Forest - Train Confusion Matrix\n\n",pd.crosstab(y_train, rf_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nRandom Forest - Train accuracy",round(accuracy_score(y_train, rf_fit.predict(x_train)),3)) 
>>> print ("\nRandom Forest  - Train Classification Report\n",classification_report( y_train, rf_fit.predict(x_train))) 

>>> print ("\n\nRandom Forest - Test Confusion Matrix\n\n",pd.crosstab(y_test, rf_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nRandom Forest - Test accuracy",round(accuracy_score(y_test, rf_fit.predict(x_test)),3)) 
>>> print ("\nRandom Forest - Test Classification Report\n",classification_report( y_test, rf_fit.predict(x_test))) 

与 bagging 87.3%相比,随机森林分类器产生了 87.8%的测试准确性,并且与 bagging 相比,还识别了 14 个实际磨损的员工,其中识别了 13 个磨损的员工:

# Plot of Variable importance by mean decrease in gini 
>>> model_ranks = pd.Series(rf_fit.feature_importances_,index=x_train.columns, name='Importance').sort_values(ascending=False, inplace=False) 
>>> model_ranks.index.name = 'Variables' 
>>> top_features = model_ranks.iloc[:31].sort_values(ascending=True,inplace=False) 
>>> import matplotlib.pyplot as plt 
>>> plt.figure(figsize=(20,10)) 
>>> ax = top_features.plot(kind='barh') 
>>> _ = ax.set_title("Variable Importance Plot") 
>>> _ = ax.set_xlabel('Mean decrease in Variance') 
>>> _ = ax.set_yticklabels(top_features.index, fontsize=13) 

从变量重要性图来看,似乎月收入变量最显著,其次是加班、总工作年限、股票期权级别、在公司的年限等等。这让我们对决定员工是留在公司还是离开组织的主要因素有了一些了解:

应用于人力资源流失数据的随机森林分类器的代码:

# Random Forest   
library(randomForest)   
set.seed(43)   
rf_fit =   randomForest(Attrition_ind~.,data = train_data,mtry=6, maxnodes= 64,classwt =   c(0.3,0.7),ntree=5000,nodesize = 1)   
tr_y_pred = predict(rf_fit,data   = train_data,type = "response")   
ts_y_pred =   predict(rf_fit,newdata = test_data,type = "response")   
tr_y_act =   train_data$Attrition_ind;ts_y_act = test_data$Attrition_ind   
tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Train   Confusion Matrix"))   
print(tr_tble)   
tr_acc =   accrcy(tr_y_act,tr_y_pred)   
trprec_zero = prec_zero(tr_y_act,tr_y_pred);   trrecl_zero = recl_zero(tr_y_act,tr_y_pred)   
trprec_one =   prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred)   
trprec_ovll = trprec_zero   *frac_trzero + trprec_one*frac_trone   
trrecl_ovll = trrecl_zero   *frac_trzero + trrecl_one*frac_trone   

print(paste("Random Forest   Train accuracy:",tr_acc))   
print(paste("Random Forest   - Train Classification Report"))   
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))   
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))   
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4)))   
ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Test   Confusion Matrix"))   
print(ts_tble)   
ts_acc =   accrcy(ts_y_act,ts_y_pred)   
tsprec_zero = prec_zero(ts_y_act,ts_y_pred);   tsrecl_zero = recl_zero(ts_y_act,ts_y_pred)   
tsprec_one =   prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred)   
tsprec_ovll = tsprec_zero   *frac_tszero + tsprec_one*frac_tsone   
tsrecl_ovll = tsrecl_zero   *frac_tszero + tsrecl_one*frac_tsone   

print(paste("Random Forest   Test accuracy:",ts_acc))   
print(paste("Random Forest   - Test Classification Report"))   
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))   
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))   
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4)))   

随机森林分类器-网格搜索

机器学习模型中的参数调整起着至关重要的作用。这里,我们展示了一个关于如何调整随机森林模型的网格搜索示例:

# Random Forest Classifier - Grid Search 
>>> from sklearn.pipeline import Pipeline 
>>> from sklearn.model_selection import train_test_split,GridSearchCV 

>>> pipeline = Pipeline([ ('clf',RandomForestClassifier(criterion='gini',class_weight = {0:0.3,1:0.7}))]) 

除了使用管道函数验证所有组合之外,调整参数类似于随机森林参数。需要评估的组合数量为(3×3×2×2) 5 = 36×5 = 180 个*组合。这里 5 是用在最后,由于交叉验证的五重:

>>> parameters = { 
...         'clf__n_estimators':(2000,3000,5000), 
...         'clf__max_depth':(5,15,30), 
...         'clf__min_samples_split':(2,3), 
...         'clf__min_samples_leaf':(1,2)  } 

>>> grid_search = GridSearchCV(pipeline,parameters,n_jobs=-1,cv=5,verbose=1, scoring='accuracy') 
>>> grid_search.fit(x_train,y_train) 

>>> print ('Best Training score: %0.3f' % grid_search.best_score_) 
>>> print ('Best parameters set:') 
>>> best_parameters = grid_search.best_estimator_.get_params()  
>>> for param_name in sorted(parameters.keys()): 
...     print ('\t%s: %r' % (param_name, best_parameters[param_name])) 

>>> predictions = grid_search.predict(x_test) 

>>> print ("Testing accuracy:",round(accuracy_score(y_test, predictions),4)) 
>>> print ("\nComplete report of Testing data\n",classification_report(y_test, predictions)) 

>>> print ("\n\nRandom Forest Grid Search- Test Confusion Matrix\n\n",pd.crosstab( y_test, predictions,rownames = ["Actuall"],colnames = ["Predicted"]))      

在前面的结果中,与已经探索的随机森林结果相比,网格搜索似乎没有提供太多优势。但是,实际上,大多数时候,与简单的模型探索相比,它会提供更好、更稳健的结果。然而,通过仔细评估许多不同的组合,它最终会发现最佳的参数组合:

随机森林分类器的代码,网格搜索应用于人力资源损耗数据;

# Grid Search - Random Forest   
library(e1071)   
library(randomForest)   
rf_grid =   tune(randomForest,Attrition_ind~.,data = train_data,classwt =   c(0.3,0.7),ranges = list( mtry = c(5,6),   
  maxnodes = c(32,64), ntree =   c(3000,5000), nodesize = c(1,2)   
),   
tunecontrol =   tune.control(cross = 5) )   
print(paste("Best   parameter from Grid Search"))   
print(summary(rf_grid))   
best_model = rf_grid$best.model   
tr_y_pred=predict(best_model,data   = train_data,type ="response")   
ts_y_pred=predict(best_model,newdata   = test_data,type= "response")   

tr_y_act =   train_data$Attrition_ind;   
ts_y_act= test_data$Attrition_ind   

tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Random Forest   Grid search Train Confusion Matrix"))   
print(tr_tble)   
tr_acc =   accrcy(tr_y_act,tr_y_pred)   
trprec_zero =   prec_zero(tr_y_act,tr_y_pred); trrecl_zero = recl_zero(tr_y_act,tr_y_pred)   
trprec_one =   prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred)   
trprec_ovll = trprec_zero   *frac_trzero + trprec_one*frac_trone   
trrecl_ovll = trrecl_zero   *frac_trzero + trrecl_one*frac_trone   

print(paste("Random Forest   Grid Search Train accuracy:",tr_acc))   
print(paste("Random Forest   Grid Search - Train Classification Report"))   
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))   
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))   
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4)))   

ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Random Forest   Grid search Test Confusion Matrix"))   
print(ts_tble)   
ts_acc =   accrcy(ts_y_act,ts_y_pred)   
tsprec_zero =   prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred)   
tsprec_one =   prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred)   
tsprec_ovll = tsprec_zero   *frac_tszero + tsprec_one*frac_tsone   
tsrecl_ovll = tsrecl_zero   *frac_tszero + tsrecl_one*frac_tsone   

print(paste("Random Forest   Grid Search Test accuracy:",ts_acc))   
print(paste("Random Forest   Grid Search - Test Classification Report"))   
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))   
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))   
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4)))

AdaBoost 分类器

Boosting 是另一个最先进的模型,被许多数据科学家用来赢得如此多的比赛。在这一部分,我们将介绍 AdaBoost 算法,接下来是梯度增强极限梯度增强 ( XGBoost )。Boosting 是一种通用方法,可以应用于许多统计模型。然而,在这本书里,我们将讨论 boosting 在决策树中的应用。在 bagging 中,我们从训练数据中提取了多个样本,然后组合各个树的结果来创建单个预测模型;该方法并行运行,因为每个引导样本不依赖于其他样本。Boosting 以顺序方式工作,不涉及自举采样;取而代之的是,每一棵树都适合于原始数据集的修改版本,并最终相加以创建一个强分类器:

上图是 AdaBoost 工作原理的示例方法。我们将在下面的算法描述中详细介绍一步一步的过程。最初,对数据拟合了一个简单的分类器(也称为决策树桩,它将数据分成两个区域),无论正确分类的类在下一次迭代(迭代 2)中将被赋予更低的权重,而对错误分类的类(观察者+蓝色图标)将被赋予更高的权重,另外,另一个决策树桩/弱分类器将被拟合到数据上,并将为下一次迭代再次改变权重(迭代 3,这里检查权重已经增加的符号)。一旦它完成迭代,这些与权重(每次迭代时根据错误率为每个分类器自动计算的权重)相结合,得到一个强分类器,它以惊人的精度预测类。

【AdaBoost 算法由以下步骤组成:

  1. 初始化观察权重 w i = 1/N,i=1,2,…,N 。其中 N =观察次数。
  2. 对于 m = 1 至 M:
    • 使用权重wIT5】将分类器 Gm(x) 拟合到训练数据
    • 计算:

  1. 输出:

所有的观察结果都同等重要。

In bagging and random forest algorithms, we deal with the columns of the data; whereas, in boosting, we adjust the weights of each observation and don't elect a few columns.

我们对数据进行分类,并评估总体误差。在最终的可加模型( α )评估中,应该给出用于计算权重的误差。直观的感觉是,对于误差较小的模型,将给予更高的权重。最后,将更新每个观察的权重。在这里,为了给下一次迭代更多的关注,对于不正确分类的观察,权重将增加,而对于正确分类的观察,权重将减少。

所有弱分类器与它们各自的权重相结合以形成强分类器。在下图中,分享了一个关于与初始迭代相比,权重在最后一次迭代中如何变化的快速想法:

# Adaboost Classifier 
>>> from sklearn.tree import DecisionTreeClassifier 
>>> from sklearn.ensemble import AdaBoostClassifier 

决策树桩被用作 AdaBoost 的基本分类器。如果我们观察下面的代码,树的深度保持为 1,它只有一次决策能力(也被认为是弱分类器):

>>> dtree = DecisionTreeClassifier(criterion='gini',max_depth=1) 

在 AdaBoost 中,决策树桩被用作基础估计器,以适合整个数据集,然后在同一数据集上适合分类器的额外副本达 5000 次。学习率将每个类的贡献缩小了 0.05。学习率和估计量之间有一个权衡。通过仔细选择低学习率和大量的估计量,我们可以很好地收敛到最优值,但是要牺牲计算能力:

>>>adabst_fit = AdaBoostClassifier(base_estimator= dtree,n_estimators=5000,learning_rate=0.05,random_state=42)

>>>adabst_fit.fit(x_train, y_train)
>>>print ("\nAdaBoost - Train Confusion Matrix\n\n", pd.crosstab(y_train, adabst_fit.predict(x_train), rownames = ["Actuall"],colnames = ["Predicted"]))
>>>print ("\nAdaBoost - Train accuracy",round(accuracy_score(y_train,adabst_fit.predict(x_train)), 3))
>>>print ("\nAdaBoost  - Train Classification Report\n",classification_report(y_train,adabst_fit.predict(x_train)))

就 1 值的召回率而言,AdaBoost 的结果似乎比已知的最佳随机森林分类器好得多。尽管与 87.8%的最佳精度相比,精度略有下降,降至 86.8%,但射频预测的 1 的数量为 23 个,这是 14 个,但增加了一些 0 的费用,但它在识别实际损耗方面确实取得了良好的进展:

应用于人力资源流失数据的 AdaBoost 分类器的代码:

# Adaboost classifier using   C5.0 with trails included for boosting   
library(C50)   
class_zero_wgt = 0.3   
class_one_wgt = 1-class_zero_wgt   
cstvr =   class_one_wgt/class_zero_wgt   
error_cost <- matrix(c(0, 1,   cstvr, 0), nrow = 2)   
# Fitting Adaboost model     
ada_fit = C5.0(train_data[-31],train_data$Attrition_ind,costs   = error_cost, trails = 5000,control = C5.0Control(minCases = 1))   
summary(ada_fit)   

tr_y_pred = predict(ada_fit,   train_data,type = "class")   
ts_y_pred =   predict(ada_fit,test_data,type = "class")   

tr_y_act =   train_data$Attrition_ind;ts_y_act = test_data$Attrition_ind   

tr_tble = table(tr_y_act,tr_y_pred)   
print(paste("AdaBoost -   Train Confusion Matrix"))   
print(tr_tble)   
tr_acc =   accrcy(tr_y_act,tr_y_pred)   
trprec_zero =   prec_zero(tr_y_act,tr_y_pred); trrecl_zero = recl_zero(tr_y_act,tr_y_pred)   
trprec_one =   prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred)   
trprec_ovll = trprec_zero   *frac_trzero + trprec_one*frac_trone   
trrecl_ovll = trrecl_zero   *frac_trzero + trrecl_one*frac_trone   
print(paste("AdaBoost   Train accuracy:",tr_acc))   
print(paste("AdaBoost -   Train Classification Report"))   
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))   
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))   
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4)))   

ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("AdaBoost -   Test Confusion Matrix"))   
print(ts_tble)   

ts_acc =   accrcy(ts_y_act,ts_y_pred)   
tsprec_zero =   prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred)   
tsprec_one =   prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred)   

tsprec_ovll = tsprec_zero   *frac_tszero + tsprec_one*frac_tsone   
tsrecl_ovll = tsrecl_zero   *frac_tszero + tsrecl_one*frac_tsone   

print(paste("AdaBoost Test   accuracy:",ts_acc))   
print(paste("AdaBoost -   Test Classification Report"))   
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))   
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))   
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4)))   

梯度提升分类器

梯度提升是一种赢得竞争的算法,其原理是通过将焦点转移到在以前的迭代中难以预测的有问题的观察,并对弱学习者(通常是决策树)进行集成,来迭代提升弱学习者。它以分阶段的方式建立模型,就像其他升压方法一样,但它通过允许优化任意可微损失函数来推广它们。

让我们从一个简单的例子开始理解 Gradient Boosting,因为 GB 在理解工作原理方面向许多数据科学家提出了挑战:

  1. 最初,我们在产生 75%准确度的观测值上拟合模型,剩余的无法解释的方差被捕获在 误差 项中:

  1. 然后,我们将在误差项上拟合另一个模型,以提取额外的解释成分,并将其添加到原始模型中,这将提高整体精度:

  1. 现在,模型提供了 80%的精度,等式如下:

  1. 我们再一次继续这个方法,在错误 2 组件上拟合模型,以提取进一步的解释组件:

  1. 现在,模型精度进一步提高到 85%,最终模型方程如下:

  1. 在这里,如果我们使用加权平均(对预测结果比其他模型更准确的更好的模型给予更高的重要性)而不是简单的加法,它将进一步改善结果。其实这就是梯度提升算法的作用!

After incorporating weights, the name of the error changed from error3 to error4, as both errors may not be exactly the same. If we find better weights, we will probably get accuracy of 90% instead of simple addition, where we have only got 85%.

梯度助推涉及三个要素:

  • 需要优化的损失函数:损失函数取决于正在解决的问题类型。在回归问题中,使用均方误差,在分类问题中,将使用对数损失。在提升过程中,在每个阶段,来自先前迭代的无法解释的损失将被优化,而不是从头开始。

  • 弱学习器进行预测:决策树在梯度提升中作为弱学习器使用。

  • 添加弱学习者使损失最小的加法模型函数:一次添加一棵树,模型中已有树不变。梯度下降程序用于在添加树时最小化损失。

梯度增强算法由以下步骤组成:

  1. 初始化:

  1. 对于 m = 1 到 M:
    • a)对于 i = 1,2,…,N 计算:

  1. 输出:

初始化常数最优常数模型,它只是一个终端节点,将被用作在后续步骤中进一步调整它的起点。 (2a) ,通过将实际结果与预测结果进行比较来计算残差/误差,随后是( 2b2c ),其中下一个决策树将根据误差项进行拟合,以给模型带来更多的解释力,并且在( 2d 中,在最后一次迭代中将额外的分量添加到模型中。最后,集成所有弱学习者来创建一个强学习者。

AdaBoosting 与梯度 boosting 的比较

在了解了 AdaBoost 和 gradient boost 两者后,读者可能会好奇,想详细了解两者的区别。在这里,我们正是为了解渴而呈现的!

scikit-learn 包中的梯度增强分类器已用于计算:

# Gradientboost Classifier
>>> from sklearn.ensemble import GradientBoostingClassifier

梯度增强算法中使用的参数如下。偏离已经被用于损失,因为我们试图解决的问题是 0/1 二元分类。学习率被选择为 0.05,要构建的树的数量是 5000 棵树,每个叶/末端节点的最小样本是 1,并且用于分裂资格的桶中所需的最小样本是 2:

>>> gbc_fit = GradientBoostingClassifier (loss='deviance', learning_rate=0.05, n_estimators=5000, min_samples_split=2, min_samples_leaf=1, max_depth=1, random_state=42 ) 

>>> gbc_fit.fit(x_train,y_train) 
>>> print ("\nGradient Boost - Train Confusion Matrix\n\n",pd.crosstab(y_train, gbc_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nGradient Boost - Train accuracy",round(accuracy_score(y_train, gbc_fit.predict(x_train)),3))
>>> print ("\nGradient Boost - Train Classification Report\n",classification_report( y_train, gbc_fit.predict(x_train)))

>>> print ("\n\nGradient Boost - Test Confusion Matrix\n\n",pd.crosstab(y_test, gbc_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nGradient Boost - Test accuracy",round(accuracy_score(y_test, gbc_fit.predict(x_test)),3)) >>> print ("\nGradient Boost - Test Classification Report\n",classification_report( y_test, gbc_fit.predict(x_test)))

如果我们分析结果,梯度增强给出了比 AdaBoost 更好的结果,最高可能的测试精度为 87.5%,大多数 1 被捕获为 24,而 AdaBoost 获得的测试精度为 86.8%。因此,事实证明,难怪每个数据科学家都试图使用这种算法来赢得竞争!

应用于人力资源流失数据的梯度提升分类器的 R 代码:

# Gradient boosting
library(gbm)

library(caret)
set.seed(43)
# Giving weights to all the observations in a way that total #weights will 
be euqal 1
model_weights <- ifelse(train_data$Attrition_ind == "0",
 (1/table(train_data$Attrition_ind)[1]) * 0.3,
 (1/table(train_data$Attrition_ind)[2]) * 0.7)
# Setting parameters for GBM
grid <- expand.grid(n.trees = 5000, interaction.depth = 1, shrinkage = .04, n.minobsinnode = 1)
# Fitting the GBM model
gbm_fit <- train(Attrition_ind ~ ., data = train_data, method = "gbm", weights = model_weights,
 tuneGrid=grid,verbose = FALSE)
# To print variable importance plot
summary(gbm_fit)

tr_y_pred = predict(gbm_fit, train_data,type = "raw")
ts_y_pred = predict(gbm_fit,test_data,type = "raw")
tr_y_act = train_data$Attrition_ind;ts_y_act = test_data$Attrition_ind

tr_tble = table(tr_y_act,tr_y_pred)
print(paste("Gradient Boosting - Train Confusion Matrix"))
print(tr_tble)

tr_acc = accrcy(tr_y_act,tr_y_pred)
trprec_zero = prec_zero(tr_y_act,tr_y_pred); trrecl_zero = 
recl_zero(tr_y_act,tr_y_pred)
trprec_one = prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred)

trprec_ovll = trprec_zero *frac_trzero + trprec_one*frac_trone
trrecl_ovll = trrecl_zero *frac_trzero + trrecl_one*frac_trone

print(paste("Gradient Boosting Train accuracy:",tr_acc))
print(paste("Gradient Boosting - Train Classification Report"))
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4)))

ts_tble = table(ts_y_act,ts_y_pred)
print(paste("Gradient Boosting - Test Confusion Matrix"))
print(ts_tble)
ts_acc = accrcy(ts_y_act,ts_y_pred)
tsprec_zero = prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = 
recl_zero(ts_y_act,ts_y_pred)
tsprec_one = prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred)
tsprec_ovll = tsprec_zero *frac_tszero + tsprec_one*frac_tsone
tsrecl_ovll = tsrecl_zero *frac_tszero + tsrecl_one*frac_tsone
print(paste("Gradient Boosting Test accuracy:",ts_acc))
print(paste("Gradient Boosting - Test Classification Report"))
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4)))

# Use the following code for performing cross validation on data - At the moment commented though
#fitControl <- trainControl(method = "repeatedcv", number = 4, repeats = 4)
# gbmFit1 <- train(Attrition_ind ~ ., data = train_data,
method = # "gbm", trControl = fitControl,tuneGrid=grid,verbose = FALSE)

极端梯度增强- XGBoost 分类器

XGBoost 是陈天琪在 2014 年基于梯度提升原理开发的新算法。自成立以来,它在数据科学界掀起了一场风暴。XGBoost 的开发在系统优化和机器学习原理方面都有很深的考虑。该库的目标是将机器的计算极限推向极致,以提供可扩展、可移植和准确的结果:

# Xgboost Classifier
>>> import xgboost as xgb
>>> xgb_fit = xgb.XGBClassifier(max_depth=2, n_estimators=5000, 
learning_rate=0.05)
>>> xgb_fit.fit(x_train, y_train)

>>> print ("\nXGBoost - Train Confusion Matrix\n\n",pd.crosstab(y_train, xgb_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"])) 
>>> print ("\nXGBoost - Train accuracy",round(accuracy_score(y_train, xgb_fit.predict(x_train)),3))
>>> print ("\nXGBoost  - Train Classification Report\n",classification_report(y_train, xgb_fit.predict(x_train)))
>>> print ("\n\nXGBoost - Test Confusion Matrix\n\n",pd.crosstab(y_test, xgb_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"])) 
>>> print ("\nXGBoost - Test accuracy",round(accuracy_score(y_test, xgb_fit.predict(x_test)),3))
>>> print ("\nXGBoost - Test Classification Report\n",classification_report(y_test, xgb_fit.predict(x_test)))

XGBoost 得到的结果几乎与梯度升压相似。获得的测试精度为 87.1%,而提升得到 87.5%,并且识别的 1 的数量为 23,而梯度提升中为 24。XGBoost 相对于 Gradient boost 的最大优势在于性能和可用于控制模型调整的选项。通过改变其中的一些,使得 XGBoost 甚至比梯度增强更好!

应用于人力资源流失数据的极限梯度提升分类器的 R 代码:

# Xgboost Classifier
library(xgboost); library(caret)

hrattr_data = read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv")
str(hrattr_data); summary(hrattr_data)
# Target variable creation
hrattr_data$Attrition_ind = 0;
hrattr_data$Attrition_ind[hrattr_data$Attrition=="Yes"]=1

# Columns to be removed due to no change in its value across observations
remove_cols = c("EmployeeCount","EmployeeNumber","Over18","StandardHours","Attrition")
hrattr_data_new = hrattr_data[,!(names(hrattr_data) %in% remove_cols)] 
# List of  variables with continuous values 
continuous_columns = c('Age','DailyRate', 'DistanceFromHome', 'Education', 'EnvironmentSatisfaction', 'HourlyRate', 'JobInvolvement', 'JobLevel', 'JobSatisfaction','MonthlyIncome', 'MonthlyRate', 'NumCompaniesWorked', 'PercentSalaryHike', 'PerformanceRating', 'RelationshipSatisfaction', 'StockOptionLevel', 'TotalWorkingYears',  'TrainingTimesLastYear', 'WorkLifeBalance', 'YearsAtCompany', 'YearsInCurrentRole', 'YearsSinceLastPromotion', 'YearsWithCurrManager')

# list of categorical variables
ohe_feats = c('BusinessTravel', 'Department', 'EducationField','Gender','JobRole', 'MaritalStatus', 'OverTime')

# one-hot-encoding categorical features
dummies <- dummyVars(~ BusinessTravel+Department+ EducationField+Gender+JobRole+MaritalStatus+OverTime, data = hrattr_data_new)
df_all_ohe <- as.data.frame(predict(dummies, newdata = hrattr_data_new))

# Cleaning column names and replace . with _

colClean <- function(x){ colnames(x) <- gsub("\\.", "_", colnames(x)); x }
df_all_ohe = colClean(df_all_ohe)

hrattr_data_new$Attrition_ind = as.integer(hrattr_data_new$Attrition_ind)

# Combining both continuous and dummy variables from categories
hrattr_data_v3 = cbind(df_all_ohe,hrattr_data_new [,(names(hrattr_data_new) %in% continuous_columns)], hrattr_data_new$Attrition_ind)

names(hrattr_data_v3)[52] = "Attrition_ind"

# Train and Test split based on 70% and 30%
set.seed(123)
numrow = nrow(hrattr_data_v3)
trnind = sample(1:numrow,size = as.integer(0.7*numrow))
train_data = hrattr_data_v3[trnind,]
test_data = hrattr_data_v3[-trnind,]

# Custom functions for calculation of Precision and Recall
frac_trzero = (table(train_data$Attrition_ind)[[1]])/nrow(train_data)
frac_trone = (table(train_data$Attrition_ind)[[2]])/nrow(train_data)

frac_tszero = (table(test_data$Attrition_ind)[[1]])/nrow(test_data)
frac_tsone = (table(test_data$Attrition_ind)[[2]])/nrow(test_data)
prec_zero <- function(act,pred){  tble = table(act,pred)
return( round( tble[1,1]/(tble[1,1]+tble[2,1]),4)  ) }

prec_one <- function(act,pred){ tble = table(act,pred)
return( round( tble[2,2]/(tble[2,2]+tble[1,2]),4)   ) }

recl_zero <- function(act,pred){tble = table(act,pred)
return( round( tble[1,1]/(tble[1,1]+tble[1,2]),4)   ) }

recl_one <- function(act,pred){ tble = table(act,pred)
return( round( tble[2,2]/(tble[2,2]+tble[2,1]),4)  ) }

accrcy <- function(act,pred){ tble = table(act,pred)
return( round((tble[1,1]+tble[2,2])/sum(tble),4)) }

y = train_data$Attrition_ind

# XGBoost Classifier Training
xgb <- xgboost(data = data.matrix(train_data[,-52]),label = y,eta = 0.04,max_depth = 2, nround=5000, subsample = 0.5, colsample_bytree = 0.5, seed = 1, eval_metric = "logloss", objective = "binary:logistic",nthread = 3)

# XGBoost value prediction on train and test data
tr_y_pred_prob <- predict(xgb, data.matrix(train_data[,-52]))
tr_y_pred <- as.numeric(tr_y_pred_prob > 0.5)
ts_y_pred_prob <- predict(xgb, data.matrix(test_data[,-52]))
ts_y_pred <- as.numeric(ts_y_pred_prob > 0.5)
tr_y_act = train_data$Attrition_ind;ts_y_act = test_data$Attrition_ind
tr_tble = table(tr_y_act,tr_y_pred)

# XGBoost Metric predictions on Train Data
print(paste("Xgboost - Train Confusion Matrix"))
print(tr_tble)
tr_acc = accrcy(tr_y_act,tr_y_pred)
trprec_zero = prec_zero(tr_y_act,tr_y_pred); trrecl_zero = recl_zero(tr_y_act,tr_y_pred)
trprec_one = prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred)
trprec_ovll = trprec_zero *frac_trzero + trprec_one*frac_trone
trrecl_ovll = trrecl_zero *frac_trzero + trrecl_one*frac_trone

print(paste("Xgboost Train accuracy:",tr_acc))
print(paste("Xgboost - Train Classification Report"))
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4)))

# XGBoost Metric predictions on Test Data
ts_tble = table(ts_y_act,ts_y_pred)
print(paste("Xgboost - Test Confusion Matrix"))
print(ts_tble)
ts_acc = accrcy(ts_y_act,ts_y_pred)
tsprec_zero = prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred)
tsprec_one = prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred)
tsprec_ovll = tsprec_zero *frac_tszero + tsprec_one*frac_tsone
tsrecl_ovll = tsrecl_zero *frac_tszero + tsrecl_one*frac_tsone

print(paste("Xgboost Test accuracy:",ts_acc))
print(paste("Xgboost - Test Classification Report"))
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4)))

系综集合-模型堆叠

集成或模型堆叠是一种将不同分类器组合成元分类器的方法,该元分类器比单独的每个单独的分类器具有更好的泛化性能。当你有疑问的时候,在处理你个人生活中的问题时,听取许多人的意见总是明智的!在模型上执行合奏有两种方式:

  • 具有不同类型分类器的集成:在该方法中,不同类型的分类器(例如,逻辑回归、决策树、随机森林等)被拟合在相同的训练数据上,并且基于是分类还是回归问题,基于多数投票还是平均来组合结果。
  • 具有单一类型分类器的集成,但在各种自举样本上单独构建:在该方法中,自举样本是从训练数据中提取的,并且每次将在提取的样本上拟合单独的模型(单个模型可以是决策树、随机森林等),并且所有这些结果在最后被组合以创建集成。这种方法适合处理高度灵活的模型,其中方差减少仍然提高了性能。

具有不同类型分类器的集成

如前一节简要提到的,不同的分类器将应用于相同的训练数据,并且对结果进行集合,或者采取多数投票,或者应用另一个分类器(也称为元分类器)来拟合从单个分类器获得的结果。这意味着,对于元分类器 X ,变量将是模型输出,而 Y 变量将是实际的 0/1 结果。通过这样做,我们将获得应该为每个分类器给出的权重,并且这些权重将相应地应用于对看不见的观察进行分类。这里显示了应用系综的所有三种方法:

  • 多数投票或平均:在该方法中,应用简单的模式函数(分类问题)从单个分类器中选择出现次数最多的类别。而对于回归问题,将计算平均值与实际值进行比较。
  • 元分类器在结果上的应用方法:从单个分类器预测实际结果 0 或 1,并在 0 和 1 之上应用元分类器。这种方法的一个小问题是元分类器会有点脆弱和僵化。我的意思是 0 和 1 只是给出结果,而不是提供精确的敏感性(比如概率)。
  • 元分类器对概率的应用方法:在该方法中,概率是从单个分类器中获得的,而不是 0 和 1。对概率应用元分类器使该方法比第一种方法更灵活。尽管用户可以试验这两种方法,看看哪一种表现更好。毕竟,机器学习就是探索和试错的方法。

在下图中,模型堆叠的完整流程已经用不同的阶段进行了描述:

使用多个分类器进行以下集成的步骤示例:

  • 四个分类器分别用于训练数据(逻辑回归、决策树、随机森林和 AdaBoost)
  • 已经为所有四个分类器确定了概率,然而,由于类别 0 的概率+类别 1 的概率= 1 的原因,在元分类器中仅使用了类别 1 的概率,因此只有一个概率足以表示,否则出现多重共线性问题
  • 逻辑回归已经被用作元分类器来模拟四个概率(从每个单独的分类器获得)之间关于最终 0/1 结果的关系
  • 已经为元分类器中使用的所有四个变量计算了系数,并将其应用于新数据,以计算将观测值分类到相应类别的最终聚集概率:
#Ensemble of Ensembles - by fitting various classifiers
>>> clwght = {0:0.3,1:0.7}

# Classifier 1 – Logistic Regression
>>> from sklearn.linear_model import LogisticRegression
>>> clf1_logreg_fit = LogisticRegression(fit_intercept=True,class_weight=clwght)
>>> clf1_logreg_fit.fit(x_train,y_train)

>>> print ("\nLogistic Regression for Ensemble - Train Confusion Matrix\n\n",pd.crosstab( y_train, clf1_logreg_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nLogistic Regression for Ensemble - Train accuracy",round( accuracy_score(y_train,clf1_logreg_fit.predict(x_train)),3))
>>> print ("\nLogistic Regression for Ensemble - Train Classification Report\n", classification_report(y_train,clf1_logreg_fit.predict(x_train)))
>>> print ("\n\nLogistic Regression for Ensemble - Test Confusion Matrix\n\n",pd.crosstab( y_test,clf1_logreg_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))     >
>> print ("\nLogistic Regression for Ensemble - Test accuracy",round( accuracy_score(y_test,clf1_logreg_fit.predict(x_test)),3))
>>> print ("\nLogistic Regression for Ensemble - Test Classification Report\n", classification_report( y_test,clf1_logreg_fit.predict(x_test)))

# Classifier 2 – Decision Tree
>>> from sklearn.tree import DecisionTreeClassifier
>>> clf2_dt_fit = DecisionTreeClassifier(criterion="gini", max_depth=5, min_samples_split=2, min_samples_leaf=1, random_state=42, class_weight=clwght)
>>> clf2_dt_fit.fit(x_train,y_train)

>>> print ("\nDecision Tree for Ensemble - Train Confusion Matrix\n\n",pd.crosstab( y_train, clf2_dt_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nDecision Tree for Ensemble - Train accuracy", round(accuracy_score( y_train,clf2_dt_fit.predict(x_train)),3))
>>> print ("\nDecision Tree for Ensemble - Train Classification Report\n", classification_report(y_train,clf2_dt_fit.predict(x_train)))
>>> print ("\n\nDecision Tree for Ensemble - Test Confusion Matrix\n\n", pd.crosstab(y_test, clf2_dt_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nDecision Tree for Ensemble - Test accuracy",round(accuracy_score(y_test, clf2_dt_fit.predict(x_test)),3))

>>> print ("\nDecision Tree for Ensemble - Test Classification Report\n", classification_report(y_test, clf2_dt_fit.predict(x_test)))

# Classifier 3 – Random Forest
>>> from sklearn.ensemble import RandomForestClassifier
>>> clf3_rf_fit = RandomForestClassifier(n_estimators=10000, criterion="gini", max_depth=6, min_samples_split=2,min_samples_leaf=1,class_weight = clwght)
>>> clf3_rf_fit.fit(x_train,y_train)

>>> print ("\nRandom Forest for Ensemble - Train Confusion Matrix\n\n", pd.crosstab(y_train, clf3_rf_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nRandom Forest for Ensemble - Train accuracy",round(accuracy_score( y_train,clf3_rf_fit.predict(x_train)),3))
>>> print ("\nRandom Forest for Ensemble - Train Classification Report\n", classification_report(y_train,clf3_rf_fit.predict(x_train))) 
>>> print ("\n\nRandom Forest for Ensemble - Test Confusion Matrix\n\n",pd.crosstab( y_test, clf3_rf_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))     
>>> print ("\nRandom Forest for Ensemble - Test accuracy",round(accuracy_score( y_test,clf3_rf_fit.predict(x_test)),3))
>>> print ("\nRandom Forest for Ensemble - Test Classification Report\n", classification_report(y_test,clf3_rf_fit.predict(x_test))) 
# Classifier 4 – Adaboost classifier
>>> from sklearn.ensemble import AdaBoostClassifier
>>> clf4_dtree = DecisionTreeClassifier(criterion='gini',max_depth=1,class_weight = clwght)
>>> clf4_adabst_fit = AdaBoostClassifier(base_estimator= clf4_dtree,
                n_estimators=5000,learning_rate=0.05,random_state=42)
>>> clf4_adabst_fit.fit(x_train, y_train)
>>> print ("\nAdaBoost for Ensemble  - Train Confusion Matrix\n\n",pd.crosstab(y_train, clf4_adabst_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))     
>>> print ("\nAdaBoost for Ensemble   - Train accuracy",round(accuracy_score(y_train, clf4_adabst_fit.predict(x_train)),3))
>>> print ("\nAdaBoost for Ensemble   - Train Classification Report\n", classification_report(y_train,clf4_adabst_fit.predict(x_train)))
>>> print ("\n\nAdaBoost for Ensemble   - Test Confusion Matrix\n\n", pd.crosstab(y_test, clf4_adabst_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))     
>>> print ("\nAdaBoost for Ensemble   - Test accuracy",round(accuracy_score(y_test, clf4_adabst_fit.predict(x_test)),3))
>>> print ("\nAdaBoost for Ensemble  - Test Classification Report\n", classification_report(y_test, clf4_adabst_fit.predict(x_test)))

在接下来的步骤中,我们执行分类器的集成:

>> ensemble = pd.DataFrame() 

在接下来的步骤中,我们只对类别 1 采用概率,因为它给出了高概率的直观感觉,并指示了更高类别 1 的值。但这不应该阻止某人,如果他们真的想把概率改为 0。在这种情况下,低概率值更适合类别 1,这让我们有点头疼!

>>> ensemble["log_output_one"] = pd.DataFrame(clf1_logreg_fit.predict_proba( x_train))[1]
>>> ensemble["dtr_output_one"] = pd.DataFrame(clf2_dt_fit.predict_proba(x_train))[1]
>>> ensemble["rf_output_one"] = pd.DataFrame(clf3_rf_fit.predict_proba(x_train))[1]
>>> ensemble["adb_output_one"] = pd.DataFrame(clf4_adabst_fit.predict_proba( x_train))[1]
>>> ensemble = pd.concat([ensemble,pd.DataFrame(y_train).reset_index(drop = True )],axis=1)

# Fitting meta-classifier
>>> meta_logit_fit =  LogisticRegression(fit_intercept=False)
>>> meta_logit_fit.fit(ensemble[['log_output_one', 'dtr_output_one', 'rf_output_one', 'adb_output_one']],ensemble['Attrition_ind'])
>>> coefs =  meta_logit_fit.coef_
>>> ensemble_test = pd.DataFrame()
>>> ensemble_test["log_output_one"] = pd.DataFrame(clf1_logreg_fit.predict_proba( x_test))[1]
>>> ensemble_test["dtr_output_one"] = pd.DataFrame(clf2_dt_fit.predict_proba( x_test))[1] 
>>> ensemble_test["rf_output_one"] = pd.DataFrame(clf3_rf_fit.predict_proba( x_test))[1]
>>> ensemble_test["adb_output_one"] = pd.DataFrame(clf4_adabst_fit.predict_proba( x_test))[1]
>>> coefs =  meta_logit_fit.coef_
>>> ensemble_test = pd.DataFrame()
>>> ensemble_test["log_output_one"] = pd.DataFrame(clf1_logreg_fit.predict_proba( x_test))[1]
>>> ensemble_test["dtr_output_one"] = pd.DataFrame(clf2_dt_fit.predict_proba( x_test))[1]
>>> ensemble_test["rf_output_one"] = pd.DataFrame(clf3_rf_fit.predict_proba( x_test))[1]
>>> ensemble_test["adb_output_one"] = pd.DataFrame(clf4_adabst_fit.predict_proba( x_test))[1]
>>> print ("\n\nEnsemble of Models - Test Confusion Matrix\n\n",pd.crosstab( ensemble_test['Attrition_ind'],ensemble_test['all_one'],rownames = ["Actuall"], colnames = ["Predicted"])) 
>>> print ("\nEnsemble of Models - Test accuracy",round(accuracy_score (ensemble_test['Attrition_ind'],ensemble_test['all_one']),3))
>>> print ("\nEnsemble of Models - Test Classification Report\n", classification_report( ensemble_test['Attrition_ind'], ensemble_test['all_one']))

虽然代码打印了列车测试精度混淆矩阵分类报告,但由于空间限制,我们未在此显示。建议用户在电脑上运行并检查结果。测试准确率为 87.5% ,为最高值(与梯度提升结果相同)。然而,通过仔细调整,基于添加更好的模型和移除低权重的模型,系综确实给出了更好的结果:

>>> coefs = meta_logit_fit.coef_
>>> print ("Co-efficients for LR, DT, RF and AB are:",coefs)

令人惊讶的是,AdaBoost 似乎拖累了整个团队的表现。一个提示是,要么更改 AdaBoost 中使用的参数并重新运行整个练习,要么从集成中移除 AdaBoost 分类器并重新运行集成步骤,以查看集成测试的准确性、精度和召回值是否有任何改进:

用于人力资源损耗数据的不同分类器集成的 r 代码:

# Ensemble of Ensembles with different type of Classifiers setwd 
("D:\\Book writing\\Codes\\Chapter 4") 

hrattr_data = read.csv("WA_Fn-UseC_-HR-Employee-Attrition.csv") 
str(hrattr_data) 
summary(hrattr_data) 

hrattr_data$Attrition_ind = 0; hrattr_data$Attrition_ind[hrattr_data$Attrition=="Yes"]=1 
hrattr_data$Attrition_ind = as.factor(hrattr_data$Attrition_ind) 

remove_cols = c ("EmployeeCount","EmployeeNumber","Over18",  "StandardHours","Attrition")
hrattr_data_new = hrattr_data[,!(names(hrattr_data) %in% remove_cols)]

set.seed(123)
numrow = nrow(hrattr_data_new)
trnind = sample(1:numrow,size = as.integer(0.7*numrow))
train_data = hrattr_data_new[trnind,]
test_data = hrattr_data_new[-trnind,]

# Ensemble of Ensembles with different type of Classifiers train_data$Attrition_ind = as.factor(train_data$Attrition_ind)

# Classifier 1 - Logistic Regression
glm_fit = glm(Attrition_ind ~.,family = "binomial",data = train_data) glm_probs = predict(glm_fit,newdata = train_data,type = "response")

# Classifier 2 - Decision Tree classifier
library(C50) 
dtree_fit = C5.0(train_data[-31],train_data$Attrition_ind,
 control = C5.0Control(minCases = 1))
dtree_probs = predict(dtree_fit,newdata = train_data,type = "prob")[,2]

# Classifier 3 - Random Forest
library(randomForest)
rf_fit = randomForest(Attrition_ind~., data = train_data,mtry=6,maxnodes= 64,ntree=5000,nodesize = 1)
rf_probs = predict(rf_fit,newdata = train_data,type = "prob")[,2]

# Classifier 4 - Adaboost
ada_fit = C5.0(train_data[-31],train_data$Attrition_ind,trails = 5000,control = C5.0Control(minCases = 1))
ada_probs = predict(ada_fit,newdata = train_data,type = "prob")[,2]

# Ensemble of Models
ensemble = data.frame(glm_probs,dtree_probs,rf_probs,ada_probs)
ensemble = cbind(ensemble,train_data$Attrition_ind)
names(ensemble)[5] = "Attrition_ind"
rownames(ensemble) <- 1:nrow(ensemble)

# Meta-classifier on top of individual classifiers
meta_clf = glm(Attrition_ind~.,data = ensemble,family = "binomial")
meta_probs = predict(meta_clf, ensemble,type = "response")

ensemble$pred_class = 0
ensemble$pred_class[meta_probs>0.5]=1

# Train confusion and accuracy metrics
tr_y_pred = ensemble$pred_class
tr_y_act = train_data$Attrition_ind;ts_y_act = test_data$Attrition_ind
tr_tble = table(tr_y_act,tr_y_pred)
print(paste("Ensemble - Train Confusion Matrix"))
print(tr_tble)

tr_acc = accrcy(tr_y_act,tr_y_pred)
print(paste("Ensemble Train accuracy:",tr_acc))

# Now verifing on test data
glm_probs = predict(glm_fit,newdata = test_data,type = "response")
dtree_probs = predict(dtree_fit,newdata = test_data,type = "prob")[,2]
rf_probs = predict(rf_fit,newdata = test_data,type = "prob")[,2]
ada_probs = predict(ada_fit,newdata = test_data,type = "prob")[,2]

ensemble_test = data.frame(glm_probs,dtree_probs,rf_probs,ada_probs)
ensemble_test = cbind(ensemble_test,test_data$Attrition_ind)
names(ensemble_test)[5] = "Attrition_ind"

rownames(ensemble_test) <- 1:nrow(ensemble_test)
meta_test_probs = predict(meta_clf,newdata = ensemble_test,type = "response")
ensemble_test$pred_class = 0
ensemble_test$pred_class[meta_test_probs>0.5]=1

# Test confusion and accuracy metrics
ts_y_pred = ensemble_test$pred_class
ts_tble = table(ts_y_act,ts_y_pred)
print(paste("Ensemble - Test Confusion Matrix"))
print(ts_tble)

ts_acc = accrcy(ts_y_act,ts_y_pred)
print(paste("Ensemble Test accuracy:",ts_acc))

使用单一类型分类器的带有自举样本的集成

在这种方法中,自举样本是从训练数据中提取的,并且每次都将在提取的样本上拟合单独的模型(单个模型可以是决策树、随机森林等),并且所有这些结果在最后被组合以创建一个集成。这种方法适合于处理高度灵活的模型,在这些模型中,方差的减少仍然会提高性能:

在下面的例子中,AdaBoost 被用作基础分类器,并且使用 bagging 分类器组合各个 AdaBoost 模型的结果以生成最终结果。尽管如此,每个 AdaBoost 都是由深度为 1(决策树桩)的决策树组成的。在这里,我们想展示分类器内部分类器内部分类器是可能的(虽然听起来像《盗梦空间》电影!):

# Ensemble of Ensembles - by applying bagging on simple classifier 
>>> from sklearn.tree import DecisionTreeClassifier 
>>> from sklearn.ensemble import BaggingClassifier 
>>> from sklearn.ensemble import AdaBoostClassifier 
>>> clwght = {0:0.3,1:0.7}

以下是 AdaBoost 分类器中使用的基本分类器(决策树桩):

>>> eoe_dtree = DecisionTreeClassifier(criterion='gini',max_depth=1,class_weight = clwght)

每个 AdaBoost 分类器由 500 棵决策树组成,学习率为 0.05:

>>> eoe_adabst_fit = AdaBoostClassifier(base_estimator= eoe_dtree, n_estimators=500,learning_rate=0.05,random_state=42) 
>>> eoe_adabst_fit.fit(x_train, y_train) 

>>> print ("\nAdaBoost - Train Confusion Matrix\n\n",pd.crosstab(y_train, eoe_adabst_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"])) 
>>> print ("\nAdaBoost - Train accuracy",round(accuracy_score(y_train, eoe_adabst_fit.predict(x_train)),3)) 
>>> print ("\nAdaBoost - Train Classification Report\n",classification_report(y_train, eoe_adabst_fit.predict(x_train))) 

>>> print ("\n\nAdaBoost - Test Confusion Matrix\n\n",pd.crosstab(y_test, eoe_adabst_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"])) 
>>> print ("\nAdaBoost - Test accuracy",round(accuracy_score(y_test, eoe_adabst_fit.predict(x_test)),3)) 
>>> print ("\nAdaBoost - Test Classification Report\n",classification_report(y_test, eoe_adabst_fit.predict(x_test)))

bagging 分类器由 50 个 AdaBoost 分类器组成,用于集成集成:

>>> bag_fit = BaggingClassifier(base_estimator= eoe_adabst_fit,n_estimators=50,
max_samples=1.0,max_features=1.0, bootstrap=True,
bootstrap_features=False,n_jobs=-1,random_state=42) 
>>> bag_fit.fit(x_train, y_train) 
>>> print ("\nEnsemble of AdaBoost - Train Confusion Matrix\n\n",pd.crosstab( y_train,bag_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"])) 
>>> print ("\nEnsemble of AdaBoost - Train accuracy",round(accuracy_score(y_train, bag_fit.predict(x_train)),3))
>>> print ("\nEnsemble of AdaBoost - Train Classification Report\n", classification_report( y_train,bag_fit.predict(x_train))) 

>>> print ("\n\nEnsemble of AdaBoost - Test Confusion Matrix\n\n",pd.crosstab(y_test, bag_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"])) 
>>> print ("\nEnsemble of AdaBoost - Test accuracy",round(accuracy_score(y_test,bag_fit.predict(x_test)),3)) 
>>> print ("\nEnsemble of AdaBoost - Test Classification Report\n", classification_report(y_test,bag_fit.predict(x_test)))

在 AdaBoost 上的集成结果显示了一些改进,其中获得的测试精度为 87.1%,几乎与梯度增强的 87.5%相当,这是我们迄今为止看到的最佳值。但是,这里标识的 1 的数量是 25,比梯度增强大。因此,已经证明了合奏确实有效!不幸的是,这些类型的函数在 R 软件中是不可用的,因此我们在这里不写等价的 R 代码。

摘要

在本章中,您已经了解了基于树的模型的完整细节,这些模型是目前行业中使用最多的,包括带有网格搜索的单个决策树和树的集成,如 bagging、random forest、boosting(包括 AdaBoost、gradient boost 和 XGBoost),最后是集成集成,也称为模型堆叠,通过进一步聚合结果来减少方差误差,从而进一步提高准确性。在模型堆叠中,您已经学习了如何确定每个模型的权重,这样就可以决定将哪个模型保留在最终结果中,以获得最佳的准确性。

在下一章中,您将学习 k 近邻和朴素贝叶斯,它们比基于树的模型计算量小。朴素贝叶斯模型将用一个自然语言处理用例来解释。事实上,朴素贝叶斯和 SVM 常用于变量(维数)非常多的情况下进行分类。

五、k 近邻和朴素贝叶斯

在前一章中,我们已经学习了计算密集型方法。相比之下,本章讨论了平衡它的简单方法!我们将在这里介绍两种技术,称为 k 近邻 ( KNN )和朴素贝叶斯。在谈到 KNN 之前,我们用一个模拟的例子解释了维数灾难的问题。随后,乳腺癌医学实例被用于使用 KNN 预测癌症是恶性还是良性。在这一章的最后一节中,已经用垃圾邮件/火腿分类解释了朴素贝叶斯,这还涉及到应用由以下基本预处理和建模步骤组成的自然语言处理 ( NLP )技术:

  • 标点符号删除
  • 单词标记化和小写转换
  • 停止词删除
  • 堵塞物
  • 词性标注的词汇化
  • 将单词转换为 TF-IDF 以创建单词的数字表示
  • 朴素贝叶斯模型在 TF-IDF 向量上的应用,用于预测邮件是垃圾邮件还是垃圾邮件

k 近邻

k 近邻是一种非参数机器学习模型,该模型存储训练观察,用于对看不见的测试数据进行分类。也可以称之为基于实例的学习。这种模型通常被称为懒惰学习,因为它在训练阶段不会学习任何东西,比如回归、随机森林等等。相反,它仅在测试/评估阶段开始工作,将给定的测试观察值与最近的训练观察值进行比较,这将花费大量时间来比较每个测试数据点。因此,这种技术在大数据上效率不高;此外,由于维度的诅咒,当变量数量较高时,性能确实会下降。

KNN 选民的例子

用下面这个小例子可以更好地解释 KNN。目标是根据他们的邻居,准确地说是地理位置(纬度和经度),预测选民将投票给哪个政党。在这里,我们假设我们可以根据大多数选民是否投票给附近的特定政党来确定他们将投票给哪个政党的潜在选民,这样他们就有很大的概率投票给多数党。然而,调整 k 值(要考虑的数字,其中大多数应该被计算在内)是一个百万美元的问题(与任何机器学习算法相同):

在上图中,我们可以看到研究的投票人将投票给党 2 。就在附近,一个邻居投了党 1 的票,另一个选民投了党 3 的票。但是三名选民投票给了第二党。事实上,通过这种方式,KNN 解决了任何给定的分类问题。回归问题是通过取给定圆或邻域或 k 值内的邻居的平均值来解决的。

维度的诅咒

KNN 完全取决于距离。因此,当 KNN 随着预测所需变量数量的增加而降低其预测能力时,关于维数灾难的研究是值得的。这是一个显而易见的事实,高维空间是巨大的。与低维空间中的点相比,高维空间中的点更倾向于彼此分散。虽然有许多方法可以检查维度曲线,但这里我们使用为 1D、2D 和 3D 空间生成的零到一之间的均匀随机值来验证这一假设。

在下面的代码行中,1000 个观测值之间的平均距离是随着维度的变化而计算的。很明显,随着维数的增加,点与点之间的距离以对数方式增加,这给了我们一个提示,即为了使机器学习算法正确工作,我们需要数据点随着维数的增加呈指数增长:

>>> import numpy as np 
>>> import pandas as pd 

# KNN Curse of Dimensionality 
>>> import random,math 

下面的代码从给定维数的均匀分布中生成介于 0 和 1 之间的随机数,这相当于数组或列表的长度:

>>> def random_point_gen(dimension): 
...     return [random.random() for _ in range(dimension)] 

以下函数通过取点与点之间的差和平方和来计算点与点之间欧氏距离(2-范数)平方和的均方根,最后取总距离的平方根:

>>> def distance(v,w): 
...     vec_sub = [v_i-w_i for v_i,w_i in zip(v,w)] 
...     sum_of_sqrs = sum(v_i*v_i for v_i in vec_sub) 
...     return math.sqrt(sum_of_sqrs) 

尺寸和对数都用于计算距离,代码如下:

>>> def random_distances_comparison(dimension,number_pairs): 
...     return [distance(random_point_gen(dimension),random_point_gen(dimension)) 
            for _ in range(number_pairs)] 

>>> def mean(x): 
...     return sum(x) / len(x) 

实验通过将尺寸从 1 改变为 201,增加 5 个尺寸来检查距离的增加:

>>> dimensions = range(1, 201, 5)

已经计算了最小和平均距离来检验,但是,两者都说明了类似的情况:

>>> avg_distances = [] 
>>> min_distances = [] 

>>> dummyarray = np.empty((20,4)) 
>>> dist_vals = pd.DataFrame(dummyarray) 
>>> dist_vals.columns = ["Dimension","Min_Distance","Avg_Distance","Min/Avg_Distance"] 

>>> random.seed(34) 
>>> i = 0 
>>> for dims in dimensions: 
...     distances = random_distances_comparison(dims, 1000)   
...     avg_distances.append(mean(distances))     
...     min_distances.append(min(distances))      

...     dist_vals.loc[i,"Dimension"] = dims 
...     dist_vals.loc[i,"Min_Distance"] = min(distances) 
...     dist_vals.loc[i,"Avg_Distance"] = mean(distances) 
...     dist_vals.loc[i,"Min/Avg_Distance"] = min(distances)/mean(distances) 

...     print(dims, min(distances), mean(distances), min(distances)*1.0 / mean( distances)) 
...     i = i+1 

# Plotting Average distances for Various Dimensions 
>>> import matplotlib.pyplot as plt 
>>> plt.figure() 
>>> plt.xlabel('Dimensions') 
>>> plt.ylabel('Avg. Distance') 
>>> plt.plot(dist_vals["Dimension"],dist_vals["Avg_Distance"]) 
>>> plt.legend(loc='best') 

>>> plt.show() 

From the preceding graph, it is proved that with the increase in dimensions, mean distance increases logarithmically. Hence the higher the dimensions, the more data is needed to overcome the curse of dimensionality!

1D、2D 和三维例子的维度诅咒

已经进行了快速分析,以了解距离 60 随机点是如何随着维度的增加而扩展的。最初为一维绘制随机点:

# 1-Dimension Plot 
>>> import numpy as np 
>>> import pandas as pd 
>>> import matplotlib.pyplot as plt 

>>> one_d_data = np.random.rand(60,1) 
>>> one_d_data_df = pd.DataFrame(one_d_data) 
>>> one_d_data_df.columns = ["1D_Data"] 
>>> one_d_data_df["height"] = 1 

>>> plt.figure() 
>>> plt.scatter(one_d_data_df['1D_Data'],one_d_data_df["height"]) 
>>> plt.yticks([]) 
>>> plt.xlabel("1-D points") 
>>> plt.show()

如果我们观察下图,所有 60 个数据点在一维上都非常接近:

在这里,我们在 2D 空间中重复同样的实验,取 60 个随机数,在坐标空间中取 xy 并直观地绘制出来:

# 2- Dimensions Plot 
>>> two_d_data = np.random.rand(60,2) 
>>> two_d_data_df = pd.DataFrame(two_d_data) 
>>> two_d_data_df.columns = ["x_axis","y_axis"] 

>>> plt.figure() 
>>> plt.scatter(two_d_data_df['x_axis'],two_d_data_df["y_axis"]) 
>>> plt.xlabel("x_axis");plt.ylabel("y_axis") 
>>> plt.show()  

通过观察 2D 图,我们可以看到相同的 60 个数据点出现了更多的差距:

最后,为三维空间绘制 60 个数据点。我们可以看到空间的进一步增加,这是非常明显的。这已经从视觉上向我们证明了,随着维数的增加,它会产生大量的空间,这使得分类器很难检测到信号:

# 3- Dimensions Plot 
>>> three_d_data = np.random.rand(60,3) 
>>> three_d_data_df = pd.DataFrame(three_d_data) 
>>> three_d_data_df.columns = ["x_axis","y_axis","z_axis"] 

>>> from mpl_toolkits.mplot3d import Axes3D 
>>> fig = plt.figure() 
>>> ax = fig.add_subplot(111, projection='3d') 
>>> ax.scatter(three_d_data_df['x_axis'],three_d_data_df["y_axis"],three_d_data_df ["z_axis"]) 
>>> plt.show() 

KNN 分类器与乳腺癌威斯康星数据示例

乳腺癌数据已从 UCI 机器学习知识库中获得,用于说明目的。这里的任务是使用 KNN 分类器基于各种收集的特征,例如团块厚度等,来发现癌症是恶性的还是良性的:

# KNN Classifier - Breast Cancer 
>>> import numpy as np 
>>> import pandas as pd 
>>> from sklearn.metrics import accuracy_score,classification_report 
>>> breast_cancer = pd.read_csv("Breast_Cancer_Wisconsin.csv") 

以下是显示数据外观的前几行。Class值有类24。值24分别代表良性和恶性等级。而所有其他变量在数值110之间变化,本质上非常明确:

只有Bare_Nuclei变量有一些缺失的值,这里我们用下面代码中最频繁的值(类别值1)来替换它们:

>>> breast_cancer['Bare_Nuclei'] = breast_cancer['Bare_Nuclei'].replace('?', np.NAN) 
>>> breast_cancer['Bare_Nuclei'] = breast_cancer['Bare_Nuclei'].fillna(breast_cancer[ 'Bare_Nuclei'].value_counts().index[0]) 

使用以下代码将类别转换为01指示器,用于分类器:

>>> breast_cancer['Cancer_Ind'] = 0 
>>> breast_cancer.loc[breast_cancer['Class']==4,'Cancer_Ind'] = 1

在下面的代码中,我们从分析中删除了非增值变量:

>>> x_vars = breast_cancer.drop(['ID_Number','Class','Cancer_Ind'],axis=1) 
>>> y_var = breast_cancer['Cancer_Ind'] 
>>> from sklearn.preprocessing import StandardScaler 
>>> x_vars_stdscle = StandardScaler().fit_transform(x_vars.values) 
>>> from sklearn.model_selection import train_test_split 

由于 KNN 对距离非常敏感,因此在应用算法之前,我们将对所有列进行标准化:

>>> x_vars_stdscle_df = pd.DataFrame(x_vars_stdscle, index=x_vars.index, columns=x_vars.columns) 
>>> x_train,x_test,y_train,y_test = train_test_split(x_vars_stdscle_df,y_var, train_size = 0.7,random_state=42)

KNN 分类器正在应用邻居值3p值表示它是 2-范数,也称为欧几里德距离,用于计算类:

>>> from sklearn.neighbors import KNeighborsClassifier 
>>> knn_fit = KNeighborsClassifier(n_neighbors=3,p=2,metric='minkowski') 
>>> knn_fit.fit(x_train,y_train) 

>>> print ("\nK-Nearest Neighbors - Train Confusion Matrix\n\n",pd.crosstab(y_train, knn_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]) )      
>>> print ("\nK-Nearest Neighbors - Train accuracy:",round(accuracy_score(y_train, knn_fit.predict(x_train)),3)) 
>>> print ("\nK-Nearest Neighbors - Train Classification Report\n", classification_report( y_train,knn_fit.predict(x_train))) 

>>> print ("\n\nK-Nearest Neighbors - Test Confusion Matrix\n\n",pd.crosstab(y_test, knn_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nK-Nearest Neighbors - Test accuracy:",round(accuracy_score( y_test,knn_fit.predict(x_test)),3)) 
>>> print ("\nK-Nearest Neighbors - Test Classification Report\n", classification_report(y_test,knn_fit.predict(x_test))) 

从结果来看,似乎 KNN 在恶性和良性分类方面做得很好,获得了 97.6%的测试准确率和 96%的恶性分类召回率。KNN 分类器的唯一不足是,它在测试阶段是计算密集型的,因为每个测试观测值将与训练数据中的所有可用观测值进行比较,而实际上 KNN 并没有从训练数据中学到什么。因此,我们也称它为懒惰分类器!

KNN 分类器的 R 码如下:

# KNN Classifier 
setwd("D:\\Book writing\\Codes\\Chapter 5") 
breast_cancer = read.csv("Breast_Cancer_Wisconsin.csv") 

# Column Bare_Nuclei have some missing values with "?" in place, we are replacing with median values 
# As Bare_Nuclei is discrete variable 
breast_cancer$Bare_Nuclei = as.character(breast_cancer$Bare_Nuclei)
breast_cancer$Bare_Nuclei[breast_cancer$Bare_Nuclei=="?"] = median(breast_cancer$Bare_Nuclei,na.rm = TRUE)
breast_cancer$Bare_Nuclei = as.integer(breast_cancer$Bare_Nuclei)
# Classes are 2 & 4 for benign & malignant respectively, we # have converted # 
to zero-one problem, as it is easy to convert to work # around with models 
breast_cancer$Cancer_Ind = 0
breast_cancer$Cancer_Ind[breast_cancer$Class==4]=1
breast_cancer$Cancer_Ind = as.factor( breast_cancer$Cancer_Ind) 

# We have removed unique id number from modeling as unique # numbers does not provide value in modeling 
# In addition, original class variable also will be removed # as the same has been replaced with derived variable 

remove_cols = c("ID_Number","Class") 
breast_cancer_new = breast_cancer[,!(names(breast_cancer) %in% remove_cols)] 

# Setting seed value for producing repetitive results 
# 70-30 split has been made on the data 

set.seed(123) 
numrow = nrow(breast_cancer_new) 
trnind = sample(1:numrow,size = as.integer(0.7*numrow)) 
train_data = breast_cancer_new[trnind,] 
test_data = breast_cancer_new[-trnind,] 

# Following is classical code for computing accuracy, # precision & recall 

frac_trzero = (table(train_data$Cancer_Ind)[[1]])/nrow(train_data)
frac_trone = (table(train_data$Cancer_Ind)[[2]])/nrow(train_data)

frac_tszero = (table(test_data$Cancer_Ind)[[1]])/nrow(test_data)
frac_tsone = (table(test_data$Cancer_Ind)[[2]])/nrow(test_data)

prec_zero <- function(act,pred){ tble = table(act,pred)
return( round( tble[1,1]/(tble[1,1]+tble[2,1]),4) ) } 

prec_one <- function(act,pred){ tble = table(act,pred)
return( round( tble[2,2]/(tble[2,2]+tble[1,2]),4) ) } 

recl_zero <- function(act,pred){tble = table(act,pred) 
return( round( tble[1,1]/(tble[1,1]+tble[1,2]),4) ) } 

recl_one <- function(act,pred){ tble = table(act,pred) 
return( round( tble[2,2]/(tble[2,2]+tble[2,1]),4) ) } 

accrcy <- function(act,pred){ tble = table(act,pred) 
return( round((tble[1,1]+tble[2,2])/sum(tble),4)) } 

# Importing Class package in which KNN function do present library(class) 

# Choosing sample k-value as 3 & apply on train & test data # respectively 

k_value = 3 
tr_y_pred = knn(train_data,train_data,train_data$Cancer_Ind,k=k_value)
ts_y_pred = knn(train_data,test_data,train_data$Cancer_Ind,k=k_value) 

# Calculating confusion matrix, accuracy, precision & # recall on train data 

tr_y_act = train_data$Cancer_Ind;ts_y_act = test_data$Cancer_Ind
tr_tble = table(tr_y_act,tr_y_pred) 
print(paste("Train Confusion Matrix")) 
print(tr_tble) 

tr_acc = accrcy(tr_y_act,tr_y_pred) 
trprec_zero = prec_zero(tr_y_act,tr_y_pred); trrecl_zero = recl_zero(tr_y_act,tr_y_pred) 
trprec_one = prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred) 
trprec_ovll = trprec_zero *frac_trzero + trprec_one*frac_trone
trrecl_ovll = trrecl_zero *frac_trzero + trrecl_one*frac_trone

print(paste("KNN Train accuracy:",tr_acc)) 
print(paste("KNN - Train Classification Report"))
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4))) 

# Calculating confusion matrix, accuracy, precision & # recall on test data 

ts_tble = table(ts_y_act, ts_y_pred) 
print(paste("Test Confusion Matrix")) 
print(ts_tble) 

ts_acc = accrcy(ts_y_act,ts_y_pred) 
tsprec_zero = prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred) 
tsprec_one = prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred) 

tsprec_ovll = tsprec_zero *frac_tszero + tsprec_one*frac_tsone
tsrecl_ovll = tsrecl_zero *frac_tszero + tsrecl_one*frac_tsone

print(paste("KNN Test accuracy:",ts_acc)) 
print(paste("KNN - Test Classification Report"))
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4))) 

KNN 分类器中 k 值的调整

在前一节中,我们只检查了 k 值为 3 的情况。实际上,在任何机器学习算法中,我们都需要调整旋钮来检查哪里可以获得更好的性能。在 KNN 的情况下,唯一的调谐参数是 k 值。因此,在下面的代码中,我们使用网格搜索来确定最佳 k 值:

# Tuning of K- value for Train & Test data 
>>> dummyarray = np.empty((5,3)) 
>>> k_valchart = pd.DataFrame(dummyarray) 
>>> k_valchart.columns = ["K_value","Train_acc","Test_acc"] 

>>> k_vals = [1,2,3,4,5] 

>>> for i in range(len(k_vals)): 
...     knn_fit = KNeighborsClassifier(n_neighbors=k_vals[i],p=2,metric='minkowski') 
...     knn_fit.fit(x_train,y_train) 

...     print ("\nK-value",k_vals[i]) 

...     tr_accscore = round(accuracy_score(y_train,knn_fit.predict(x_train)),3) 
...     print ("\nK-Nearest Neighbors - Train Confusion Matrix\n\n",pd.crosstab( y_train, knn_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]) )      
...     print ("\nK-Nearest Neighbors - Train accuracy:",tr_accscore) 
...     print ("\nK-Nearest Neighbors - Train Classification Report\n", classification_report(y_train,knn_fit.predict(x_train))) 

...     ts_accscore = round(accuracy_score(y_test,knn_fit.predict(x_test)),3)     
...     print ("\n\nK-Nearest Neighbors - Test Confusion Matrix\n\n",pd.crosstab( y_test,knn_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))       
...     print ("\nK-Nearest Neighbors - Test accuracy:",ts_accscore) 
...     print ("\nK-Nearest Neighbors - Test Classification Report\n",classification_report(y_test,knn_fit.predict(x_test))) 

...     k_valchart.loc[i, 'K_value'] = k_vals[i]       
...     k_valchart.loc[i, 'Train_acc'] = tr_accscore      
...     k_valchart.loc[i, 'Test_acc'] = ts_accscore                

# Ploting accuracies over varied K-values 
>>> import matplotlib.pyplot as plt 
>>> plt.figure() 
>>> plt.xlabel('K-value') 
>>> plt.ylabel('Accuracy') 
>>> plt.plot(k_valchart["K_value"],k_valchart["Train_acc"]) 
>>> plt.plot(k_valchart["K_value"],k_valchart["Test_acc"]) 

>>> plt.axis([0.9,5, 0.92, 1.005]) 
>>> plt.xticks([1,2,3,4,5]) 

>>> for a,b in zip(k_valchart["K_value"],k_valchart["Train_acc"]): 
...     plt.text(a, b, str(b),fontsize=10) 

>>> for a,b in zip(k_valchart["K_value"],k_valchart["Test_acc"]): 
...     plt.text(a, b, str(b),fontsize=10) 

>>> plt.legend(loc='upper right')     
>>> plt.show() 

k 值越小,由于列车数据的精度值非常高,而测试数据的精度值越小,因此出现越多的过拟合问题,随着 k 值的增加,列车和测试精度越来越收敛,变得越来越稳健。这个现象说明了典型的机器学习现象。至于进一步的分析,鼓励读者尝试高于 5 的 k 值,看看训练和测试精度是如何变化的。KNN 分类器中用于调整 k 值的 R 码如下:

# Tuning of K-value on Train & Test Data 
k_valchart = data.frame(matrix( nrow=5, ncol=3)) 
colnames(k_valchart) = c("K_value","Train_acc","Test_acc") 
k_vals = c(1,2,3,4,5) 

i = 1
for (kv in k_vals) { 
 tr_y_pred = knn(train_data,train_data,train_data$Cancer_Ind,k=kv)
 ts_y_pred = knn(train_data,test_data,train_data$Cancer_Ind,k=kv)
 tr_y_act = train_data$Cancer_Ind;ts_y_act = test_data$Cancer_Ind
 tr_tble = table(tr_y_act,tr_y_pred) 
 print(paste("Train Confusion Matrix")) 
 print(tr_tble) 
 tr_acc = accrcy(tr_y_act,tr_y_pred) 
 trprec_zero = prec_zero(tr_y_act,tr_y_pred); trrecl_zero = recl_zero(tr_y_act, tr_y_pred) 
 trprec_one = prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred) 
 trprec_ovll = trprec_zero *frac_trzero + trprec_one*frac_trone
 trrecl_ovll = trrecl_zero *frac_trzero + trrecl_one*frac_trone
 print(paste("KNN Train accuracy:",tr_acc)) 
 print(paste("KNN - Train Classification Report"))

print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4))) 
 ts_tble = table(ts_y_act,ts_y_pred) 
 print(paste("Test Confusion Matrix")) 
 print(ts_tble)
 ts_acc = accrcy(ts_y_act,ts_y_pred) 
 tsprec_zero = prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred) 
 tsprec_one = prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred) 
 tsprec_ovll = tsprec_zero *frac_tszero + tsprec_one*frac_tsone
 tsrecl_ovll = tsrecl_zero *frac_tszero + tsrecl_one*frac_tsone

 print(paste("KNN Test accuracy:",ts_acc)) 
 print(paste("KNN - Test Classification Report"))

print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4)))

 k_valchart[i,1] =kv 
 k_valchart[i,2] =tr_acc 
 k_valchart[i,3] =ts_acc i = i+1 } 
# Plotting the graph 
library(ggplot2) 
library(grid) 
ggplot(k_valchart, aes(K_value)) 
+ geom_line(aes(y = Train_acc, colour = "Train_Acc")) + 
geom_line(aes(y = Test_acc, colour = "Test_Acc"))+
labs(x="K_value",y="Accuracy") + 
geom_text(aes(label = Train_acc, y = Train_acc), size = 3)+
geom_text(aes(label = Test_acc, y = Test_acc), size = 3) 

朴素贝叶斯

贝叶斯算法的概念相当古老,从 18 世纪托马斯·贝叶斯开始就存在了。托马斯发展了从已知事件中确定未知事件概率的基本数学原理。例如,如果所有的苹果都是红色的,平均直径约为 4 英寸,那么,如果从篮子中随机选择一个红色、直径为 3.7 英寸的水果,那么该水果是苹果的概率是多少?幼稚术语确实假定了一个类中特定特征相对于其他特征的独立性。在这种情况下,颜色和直径之间没有相关性。这种独立性假设使得朴素贝叶斯分类器在计算便利性方面对特定任务最有效,例如基于单词的电子邮件分类,其中确实存在高维 vocab,即使在假设特征之间的独立性之后。朴素贝叶斯分类器在实际应用中表现得出奇的好。

贝叶斯分类器最适用于需要同时考虑大量属性信息来估计最终结果概率的问题。贝叶斯方法利用所有可用的证据来考虑预测,即使特征对预测的最终结果影响很小。然而,我们不应忽视这样一个事实,即大量影响相对较小的特征,加上其综合影响,将形成强大的分类器。

概率基础

在深入探讨朴素贝叶斯之前,最好重申一下基本原理。通过将事件发生的轨迹数除以轨迹总数,可以从观察到的数据中估计事件发生的概率。例如,如果一个包里有红蓝球,随机抽取 10 个球,逐一替换并带出 10 个3 个红球出现在步道中,我们可以说红色的概率为 0.3p 红色 = 3/10 = 0.3 。所有可能结果的总概率必须是 100%。

如果跟踪有两种结果,如电子邮件分类,要么是垃圾邮件,要么是垃圾邮件,并且两者不能同时发生,则这些事件被认为是相互排斥的。此外,如果这些结果涵盖了所有可能的事件,它将被称为穷尽事件。比如在邮件分类中如果 P(垃圾邮件)= 0.1 ,我们就能够计算出 P(火腿)= 1- 0.1 = 0.9 ,这两个事件是互斥的。在下面的文氏图中,所有可能的电子邮件类别都用结果类型表示(整个宇宙):

联合概率

尽管互斥案例很容易处理,但大多数实际问题确实属于非互斥事件的范畴。通过使用联合外观,我们可以预测事件结果。例如,如果电子邮件信息中出现像彩票这样的词,这很可能是垃圾邮件,而不是火腿。下图显示了垃圾邮件与彩票的联合概率。但是,如果您详细注意到,彩票圈并不完全包含在垃圾邮件圈内。这意味着并非所有垃圾邮件都包含“T4”彩票“T5”字样,也并非每封带有“T6”彩票“T7”字样的电子邮件都是垃圾邮件。

在下图中,除了 Venn 图表示中的彩票单词之外,我们还扩展了垃圾邮件和火腿类别:

我们已经看到,10%的电子邮件是垃圾邮件,4%的电子邮件有单词彩票,我们的任务是量化这两个比例之间的重叠程度。换句话说,我们需要识别 p(垃圾邮件)p(彩票)两者出现的联合概率,可以写成 p(垃圾邮件∪彩票)。如果这两个事件完全不相关,则称为独立事件,它们各自的值为 p(垃圾邮件∪彩票)= p(垃圾邮件) p(彩票)= 0.1 * 0.04 = 0.004* ,这是所有包含彩票一词的垃圾邮件的 0.4%。一般来说,对于独立事件P(A∪B)= P(A) P(B)*。

用条件概率理解贝叶斯定理

条件概率提供了一种使用贝叶斯定理计算相关事件之间关系的方法。例如 AB 是两个事件,我们想计算 P(A\B) 可以理解为事件发生的概率 A 给定事件 B 已经发生的事实,实际上这被称为条件概率,等式可以写成:

为了更好地理解,我们现在将讨论电子邮件分类示例。我们的目标是预测电子邮件是否是垃圾邮件,给出单词彩票和一些其他线索。在这种情况下,我们已经知道了垃圾邮件的总体概率,即 10%也称为先验概率。现在假设你已经获得了一条额外的信息,即所有信息中单词彩票的概率为 4%,也被称为边际可能性。现在,我们知道了彩票在之前的垃圾邮件中被使用的概率,被称为可能性

通过将贝叶斯定理应用于证据,我们可以计算后验概率,该概率计算消息是垃圾邮件的概率;鉴于彩票出现在消息中。平均而言,如果概率大于 50%,则表明该邮件是垃圾邮件,而不是火腿。

在上表中,已经显示了记录抽奖在垃圾邮件和 ham 消息中出现的次数及其各自可能性的样本频率表。可能性表显示 P(彩票\垃圾邮件)= 3/22 = 0.13 ,表明垃圾邮件包含术语彩票的概率为 13%。随后我们可以计算出 P(垃圾邮件∪彩票)= P(彩票\垃圾邮件) P(垃圾邮件)= (3/22) * (22/100) = 0.03* 。为了计算后验概率,我们把P(Spam∪彩票)P(彩票)分开,意思是(3/22)(22/100)/(4/100)= 0.75。因此,考虑到一条消息包含单词彩票*,该消息是垃圾邮件的概率为 75%。因此,不要相信急功近利的家伙!

朴素贝叶斯分类

在过去的例子中,我们已经看到了一个名为彩票的单词,然而,在这种情况下,我们将讨论一些额外的单词,如百万取消订阅来展示实际的分类器是如何工作的。让我们为这三个单词( W1W2W3 )的出现构建一个似然表,如下表所示为 100 封邮件:

当收到新邮件时,将计算后验概率以确定该电子邮件是垃圾邮件还是火腿。假设我们有一封邮件,里面有条款抽奖**退订,但是里面没有文字百万,有了这个细节,垃圾邮件的概率是多少?

利用贝叶斯定理,我们可以将问题定义为彩票=是百万=否退订=是:

由于单词之间的依赖性,求解前面的方程将具有很高的计算复杂度。随着单词数量的增加,这甚至会爆炸,并且需要巨大的内存来处理所有可能的交叉事件。这最终导致了单词独立性的直观转变(交叉条件独立性),为此它获得了贝叶斯分类器的朴素前缀名称。当两个事件独立时,我们可以写出P(A∪B)= P(A) P(B)*。事实上,这种等价性更容易计算,内存需求更少:

同样,我们也将计算 ham 消息的概率,如下所示:

通过在等式中替换前面的似然表,由于垃圾邮件/火腿的比率,我们可以简单地忽略两个等式中的分母项。垃圾邮件的总体可能性是:

计算比值后, 0.008864/0.004349 = 2.03 ,这意味着这条消息是垃圾邮件的可能性是 ham 的两倍。但是我们可以如下计算概率:

P(垃圾邮件)= 0.008864/(0.008864+0.004349)= 0.67

P(He) = 0.004349/(0.008864+0.004349) = 0.33

通过将似然值转换为概率,我们可以用一种形象的方式来展示两者中的任何一个来设定一些阈值,等等。

拉普拉斯估计量

在前面的计算中,所有的值都是非零的,这使得计算很好。然而在实践中,有些词从来不会出现在过去的特定类别中,而是突然出现在后面的阶段,这使得整个计算为零。

例如,在前面的等式中 W 3 确实有一个 0 值,而不是 13 ,它会将整个等式一起转换为 0 :

为了避免这种情况,拉普拉斯估计器本质上在频率表中的每个计数上增加了一个小数字,这确保了每个特征在每个类中都有非零的出现概率。通常拉普拉斯估计器设置为 1 ,保证每个类-特征组合在数据中至少被发现一次:

If you observe the equation carefully, value 1 is added to all three words in numerator and at the same time three has been added to all denominators to provide equivalence.

朴素贝叶斯垃圾短信分类示例

朴素贝叶斯分类器是使用在http://www.dt.fee.unicamp.br/~tiago/smsspamcollection/获得的垃圾短信收集数据开发的。在本章中,已经讨论了在构建朴素贝叶斯模型之前,在自然语言处理技术中可用的各种预处理技术:

>>> import csv 

>>> smsdata = open('SMSSpamCollection.txt','r') 
>>> csv_reader = csv.reader(smsdata,delimiter='\t') 

如果在使用旧版 Python 时遇到任何utf-8错误,可以使用以下sys包行代码,否则最新版本的 Python 3.6 不需要使用这些代码:

>>> import sys 
>>> reload (sys) 
>>> sys.setdefaultendocing('utf-8') 

正常编码通常从这里开始:

>>> smsdata_data = [] 
>>> smsdata_labels = [] 

>>> for line in csv_reader: 
...     smsdata_labels.append(line[0]) 
...     smsdata_data.append(line[1]) 

>>> smsdata.close() 

以下代码打印前 5 行:

>>> for i in range(5): 
...     print (smsdata_data[i],smsdata_labels[i])

获得前面的输出后,运行以下代码:

>>> from collections import Counter 
>>> c = Counter( smsdata_labels ) 
>>> print(c) 

在 5572 条观察结果中,4825 条是垃圾邮件,约占 86.5%,747 条垃圾邮件约占 13.4%。

使用自然语言处理技术,我们对数据进行了预处理,以获得最终的单词向量,并将其与最终的垃圾邮件或火腿结果进行映射。主要的预处理阶段包括:

  • 去除标点符号:在进行任何进一步处理之前,需要去除标点符号。string库的标点符号是!" #$% & '()+,-。/:;< = >?@[\]^_`{|}~* ,从所有消息中删除。
  • 单词标记化:单词是根据空格从句子中分块出来的,以便进一步处理。
  • 将单词转换为小写:转换为所有小写提供了去除重复项的功能,比如 Runrun ,其中第一个出现在句子的开头,后一个出现在句子的中间,以此类推,这些都需要统一起来去除重复项,就像我们在做包词技术一样。
  • 停词去除:停词是在文献中重复出现这么多次的词,但在句子的解释力上并没有太大的区别。比如:这个那个等等,需要去掉后再进一步处理。
  • 长度至少为三的:这里我们去掉了长度小于三的单词。
    *** 保持单词长度至少为三个:这里我们删除了长度小于三个的单词。词干:词干处理将单词词干到各自的词根。堵塞的例子是停止运行或停止运行。通过词干,我们减少了重复,提高了模型的准确性。* 词性标注:将词性标注应用到单词上,如名词、动词、形容词等。例如,跑步的词性标注是动词,而跑步的词性标注是名词。在某些情况下 running 是名词,引理化并不会把这个词归结为词根 run ,相反它只是让保持原样运行。因此,词性标注是一个非常关键的步骤,需要在应用词条化操作之前执行,以将单词归结为其词根。* 词的引理化:引理化是另一个不同的降维过程。在词汇化过程中,它将单词分解为词根,而不仅仅是截断单词。例如,当我们将 ate 单词传入以词性标记为动词的引理词时,将 ate 带到其词根单词中作为 eat 。**

**nltk包已用于所有预处理步骤,因为它在一个单一的屋顶中包含所有必要的自然语言处理功能:

>>> import nltk 
>>> from nltk.corpus import stopwords 
>>> from nltk.stem import WordNetLemmatizer 
>>> import string 
>>> import pandas as pd 
>>> from nltk import pos_tag 
>>> from nltk.stem import PorterStemmer  

函数已经写好(预处理)由所有步骤组成,以方便使用。但是,我们将解释每个部分中的所有步骤:

>>> def preprocessing(text): 

下面一行代码将单词拆分,并检查每个字符是否在标准标点符号中,如果是,它将被替换为空格,否则它不会被空格替换:

...     text2 = " ".join("".join([" " if ch in string.punctuation else ch for ch in text]).split()) 

以下代码将句子标记为基于空格的单词,并将它们放在一起作为应用进一步步骤的列表:

...     tokens = [word for sent in nltk.sent_tokenize(text2) for word in 
              nltk.word_tokenize(sent)] 

将所有大小写(大写、小写和正确)转换为小写可以减少语料库中的重复:

...     tokens = [word.lower() for word in tokens] 

如前所述,停止词是在理解句子时没有太大分量的词;它们用于连接单词,等等。我们用下面一行代码删除了它们:

...     stopwds = stopwords.words('english') 
...     tokens = [token for token in tokens if token not in stopwds]  

在下面的代码中,只保留长度大于3的单词来删除小单词,这几乎不包含太多要表达的意思:

...     tokens = [word for word in tokens if len(word)>=3] 

使用PorterStemmer函数对单词应用词干,该函数从单词中提取额外的后缀:

...     stemmer = PorterStemmer() 
...     tokens = [stemmer.stem(word) for word in tokens]  

词性标注是词条化的先决条件,基于这个词是名词还是动词等等,它会将其简化为词根:

...     tagged_corpus = pos_tag(tokens)     

pos_tag函数返回速度的一部分,四种格式表示名词,六种格式表示动词。NN(名词,普通,单数)NNP(名词,专有,单数)NNPS(名词,专有,复数)NNS(名词,普通,复数)VB(动词,基本形式)VBD(动词,过去时)VBG(动词,现在分词)VBN(动词,过去分词)VBP(动词,现在时,非第三人称单数)VBZ(动词,现在时,第三人称单数):

...    Noun_tags = ['NN','NNP','NNPS','NNS'] 
...    Verb_tags = ['VB','VBD','VBG','VBN','VBP','VBZ'] 
...    lemmatizer = WordNetLemmatizer()

prat_lemmatize函数的创建仅仅是因为pos_tag函数和引理函数的输入值不匹配。如果任何单词的标签属于相应的名词或动词标签类别,nv将相应地应用于引理功能:

...     def prat_lemmatize(token,tag): 
...         if tag in Noun_tags: 
...             return lemmatizer.lemmatize(token,'n') 
...         elif tag in Verb_tags: 
...             return lemmatizer.lemmatize(token,'v') 
...         else: 
...             return lemmatizer.lemmatize(token,'n') 

在执行标记化并应用了所有各种操作之后,我们需要将它重新连接起来以形成标记,下面的函数执行相同的操作:

...     pre_proc_text =  " ".join([prat_lemmatize(token,tag) for token,tag in tagged_corpus])              
...     return pre_proc_text 

以下步骤将预处理功能应用于数据并生成新的语料库:

>>> smsdata_data_2 = [] 
>>> for i in smsdata_data: 
...     smsdata_data_2.append(preprocessing(i))  

基于 70-30 的分割,数据将被分割成训练和测试数据,并转换成用于应用机器学习算法的 NumPy 数组:

>>> import numpy as np 
>>> trainset_size = int(round(len(smsdata_data_2)*0.70)) 
>>> print ('The training set size for this classifier is ' + str(trainset_size) + '\n') 
>>> x_train = np.array([''.join(rec) for rec in smsdata_data_2[0:trainset_size]]) 
>>> y_train = np.array([rec for rec in smsdata_labels[0:trainset_size]]) 
>>> x_test = np.array([''.join(rec) for rec in smsdata_data_2[trainset_size+1:len( smsdata_data_2)]]) 
>>> y_test = np.array([rec for rec in smsdata_labels[trainset_size+1:len( smsdata_labels)]]) 

下面的代码将单词转换为矢量格式,并应用术语频率-逆文档频率 ( TF-IDF )权重,这是一种增加高频单词权重的方法,同时惩罚通用术语,如at 等等。在下面的代码中,我们限制了词汇中最常见的 4,000 个单词,但是我们也可以调整该参数,以检查哪里可以获得更好的准确性:

# building TFIDF vectorizer  
>>> from sklearn.feature_extraction.text import TfidfVectorizer 
>>> vectorizer = TfidfVectorizer(min_df=2, ngram_range=(1, 2),  stop_words='english',  
    max_features= 4000,strip_accents='unicode',  norm='l2') 

在列车和测试数据中,TF-IDF 转换如下所示。todense功能用于创建数据以可视化内容:

>>> x_train_2 = vectorizer.fit_transform(x_train).todense() 
>>> x_test_2 = vectorizer.transform(x_test).todense() 

多项式朴素贝叶斯分类器适用于具有离散特征(例如字数)的分类,这通常需要大量的特征数。然而,在实践中,分数计数(如 TF-IDF)也能很好地工作。如果我们没有提到任何拉普拉斯估计量,它会取 1.0 的平均值,并将 1.0 与分子中的每个项和分母的总和相加:

>>> from sklearn.naive_bayes import MultinomialNB 
>>> clf = MultinomialNB().fit(x_train_2, y_train) 

>>> ytrain_nb_predicted = clf.predict(x_train_2) 
>>> ytest_nb_predicted = clf.predict(x_test_2) 

>>> from sklearn.metrics import classification_report,accuracy_score 

>>> print ("\nNaive Bayes - Train Confusion Matrix\n\n",pd.crosstab(y_train, ytrain_nb_predicted,rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nNaive Bayes- Train accuracy",round(accuracy_score(y_train, ytrain_nb_predicted),3)) 
>>> print ("\nNaive Bayes  - Train Classification Report\n",classification_report(y_train, ytrain_nb_predicted)) 

>>> print ("\nNaive Bayes - Test Confusion Matrix\n\n",pd.crosstab(y_test, ytest_nb_predicted,rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nNaive Bayes- Test accuracy",round(accuracy_score(y_test, ytest_nb_predicted),3)) 
>>> print ("\nNaive Bayes  - Test Classification Report\n",classification_report( y_test, ytest_nb_predicted)) 

从之前的结果来看,朴素贝叶斯已经产生了 96.6%的测试准确率的优秀结果,对于垃圾邮件的显著召回值为 76%,对于火腿的显著召回值几乎为 100%。

但是,如果我们想根据朴素贝叶斯的系数来检查前 10 个特征是什么,下面的代码会很方便:

# printing top features  
>>> feature_names = vectorizer.get_feature_names() 
>>> coefs = clf.coef_ 
>>> intercept = clf.intercept_ 
>>> coefs_with_fns = sorted(zip(clf.coef_[0], feature_names)) 

>>> print ("\n\nTop 10 features - both first & last\n") 
>>> n=10 
>>> top_n_coefs = zip(coefs_with_fns[:n], coefs_with_fns[:-(n + 1):-1]) 
>>> for (coef_1, fn_1), (coef_2, fn_2) in top_n_coefs: 
...     print('\t%.4f\t%-15s\t\t%.4f\t%-15s' % (coef_1, fn_1, coef_2, fn_2)) 

虽然 R 语言并不是 NLP 处理的流行选择,但这里我们已经展示了代码。我们鼓励读者修改代码,看看准确性如何变化,以便更好地理解概念。短信垃圾邮件/可疑邮件数据的朴素贝叶斯分类器的代码如下:

# Naive Bayes 
smsdata = read.csv("SMSSpamCollection.csv",stringsAsFactors = FALSE) 
# Try the following code for reading in case if you have 
#issues while reading regularly with above code 
#smsdata = read.csv("SMSSpamCollection.csv", 
#stringsAsFactors = FALSE,fileEncoding="latin1") 
str(smsdata) 
smsdata$Type = as.factor(smsdata$Type) 
table(smsdata$Type) 

library(tm) 
library(SnowballC) 
# NLP Processing 
sms_corpus <- Corpus(VectorSource(smsdata$SMS_Details)) 
corpus_clean_v1 <- tm_map(sms_corpus, removePunctuation)
corpus_clean_v2 <- tm_map(corpus_clean_v1, tolower) 
corpus_clean_v3 <- tm_map(corpus_clean_v2, stripWhitespace)
corpus_clean_v4 <- tm_map(corpus_clean_v3, removeWords, stopwords())
corpus_clean_v5 <- tm_map(corpus_clean_v4, removeNumbers)
corpus_clean_v6 <- tm_map(corpus_clean_v5, stemDocument) 

# Check the change in corpus 
inspect(sms_corpus[1:3]) 
inspect(corpus_clean_v6[1:3]) 

sms_dtm <- DocumentTermMatrix(corpus_clean_v6) 

smsdata_train <- smsdata[1:4169, ] 
smsdata_test <- smsdata[4170:5572, ] 

sms_dtm_train <- sms_dtm[1:4169, ] 
sms_dtm_test <- sms_dtm[4170:5572, ] 

sms_corpus_train <- corpus_clean_v6[1:4169] 
sms_corpus_test <- corpus_clean_v6[4170:5572]

prop.table(table(smsdata_train$Type))
prop.table(table(smsdata_test$Type)) 
frac_trzero = (table(smsdata_train$Type)[[1]])/nrow(smsdata_train)
frac_trone = (table(smsdata_train$Type)[[2]])/nrow(smsdata_train)
frac_tszero = (table(smsdata_test$Type)[[1]])/nrow(smsdata_test)
frac_tsone = (table(smsdata_test$Type)[[2]])/nrow(smsdata_test)

Dictionary <- function(x) { 
 if( is.character(x) ) { 
 return (x) 
 } 
 stop('x is not a character vector') 
} 
# Create the dictionary with at least word appears 1 time 
sms_dict <- Dictionary(findFreqTerms(sms_dtm_train, 1)) 
sms_train <- DocumentTermMatrix(sms_corpus_train,list(dictionary = sms_dict)) 
sms_test <- DocumentTermMatrix(sms_corpus_test,list(dictionary = sms_dict)) 
convert_tofactrs <- function(x) { 
 x <- ifelse(x > 0, 1, 0) 
 x <- factor(x, levels = c(0, 1), labels = c("No", "Yes")) 
 return(x) 
} 
sms_train <- apply(sms_train, MARGIN = 2, convert_tofactrs) 
sms_test <- apply(sms_test, MARGIN = 2, convert_tofactrs) 

# Application of Naïve Bayes Classifier with laplace Estimator
library(e1071) 
nb_fit <- naiveBayes(sms_train, smsdata_train$Type,laplace = 1.0)

tr_y_pred = predict(nb_fit, sms_train) 
ts_y_pred = predict(nb_fit,sms_test) 
tr_y_act = smsdata_train$Type;ts_y_act = smsdata_test$Type 

tr_tble = table(tr_y_act,tr_y_pred) 
print(paste("Train Confusion Matrix")) 
print(tr_tble) 

tr_acc = accrcy(tr_y_act,tr_y_pred) 
trprec_zero = prec_zero(tr_y_act,tr_y_pred);  trrecl_zero = recl_zero(tr_y_act,tr_y_pred) 
trprec_one = prec_one(tr_y_act,tr_y_pred); trrecl_one = recl_one(tr_y_act,tr_y_pred) 
trprec_ovll = trprec_zero *frac_trzero + trprec_one*frac_trone
trrecl_ovll = trrecl_zero *frac_trzero + trrecl_one*frac_trone

print(paste("Naive Bayes Train accuracy:",tr_acc)) 
print(paste("Naive Bayes - Train Classification Report"))
print(paste("Zero_Precision",trprec_zero,"Zero_Recall",trrecl_zero))
print(paste("One_Precision",trprec_one,"One_Recall",trrecl_one))
print(paste("Overall_Precision",round(trprec_ovll,4),"Overall_Recall",round(trrecl_ovll,4))) 

ts_tble = table(ts_y_act,ts_y_pred) 
print(paste("Test Confusion Matrix")) 
print(ts_tble) 

ts_acc = accrcy(ts_y_act,ts_y_pred) 
tsprec_zero = prec_zero(ts_y_act,ts_y_pred); tsrecl_zero = recl_zero(ts_y_act,ts_y_pred) 
tsprec_one = prec_one(ts_y_act,ts_y_pred); tsrecl_one = recl_one(ts_y_act,ts_y_pred) 
tsprec_ovll = tsprec_zero *frac_tszero + tsprec_one*frac_tsone
tsrecl_ovll = tsrecl_zero *frac_tszero + tsrecl_one*frac_tsone

print(paste("Naive Bayes Test accuracy:",ts_acc)) 
print(paste("Naive Bayes - Test Classification Report"))
print(paste("Zero_Precision",tsprec_zero,"Zero_Recall",tsrecl_zero))
print(paste("One_Precision",tsprec_one,"One_Recall",tsrecl_one))
print(paste("Overall_Precision",round(tsprec_ovll,4),"Overall_Recall",round(tsrecl_ovll,4))) 

摘要

在这一章中,您已经学习了 KNN 和朴素贝叶斯技术,它们需要稍微少一点的计算能力。事实上,KNN 被称为懒惰的学习者,因为除了与训练数据点进行比较以将其分类之外,它什么也学不到。另外,您已经看到了如何使用网格搜索技术来调整 k 值。虽然已经为朴素贝叶斯分类器提供了解释,但是已经为自然语言处理示例提供了所有著名的自然语言处理技术,以非常简洁的方式向您展示了该领域的特色。虽然在文本处理中,可以使用朴素贝叶斯或 SVM 技术,因为这两种技术可以处理高维数据,这在自然语言处理中非常相关,因为单词向量的数量在维度上相对较高,同时又很稀疏。

在下一章中,我们将讨论 SVM 和神经网络,并介绍深度学习模型,因为深度学习正在成为实现人工智能的下一代技术,最近这也受到了数据科学界的极大关注!***

六、支持向量机和神经网络

在本章中,我们将同时介绍支持向量机和神经网络,它们的计算复杂度较高,需要相对大量的计算资源,但在大多数情况下,与其他机器学习方法相比,它们确实提供了明显更好的结果。

一个支持向量机 ( SVM )可以想象成一个曲面,它最大化在多维空间中表示的各种类型的数据点之间的边界,也称为超平面,它在每个子区域中创建最均匀的点。

支持向量机可以用于任何类型的数据,但是对于相对于观测值具有非常高维度的数据类型具有特殊的额外优势,例如:

  • 文本分类,其中语言具有单词向量的维度
  • 为了通过正确标记色谱图来控制 DNA 测序的质量

支持向量机工作原理

支持向量机根据其工作原理主要分为三类:

  • 最大边距分类器
  • 支持向量分类器
  • 支持向量机

最大余量分类器

人们通常用最大裕度分类器来推广支持向量机。然而,与最大裕度分类器相比,支持向量机有更多的优点,我们将在本章中介绍。画出无限的超平面来对同一组数据进行分类是可行的,但问题是,哪一个才是理想的超平面?最大边缘分类器提供了一个答案:具有最大分离宽度边缘的超平面。

超平面:在继续之前,让我们快速回顾一下什么是超平面。在 n 维空间中,超平面是 n-1 维的平坦仿射子空间。这意味着,在二维空间中,超平面是一条将二维空间分成两半的直线。超平面由以下等式定义:

位于超平面上的点必须遵循上面的等式。然而,上面和下面也有区域。这意味着观测值可以落在两个区域中的任何一个,也称为类区域:

最大裕度分类器的数学表示如下,这是一个优化问题:

约束 2 通过取系数与 x 个变量的乘积,最终与类变量指标的乘积,确保观测值位于超平面的正确侧。

In non-separable cases, the maximum margin classifier will not have a separating hyperplane, which is also known as no feasible solution. This issue will be solved with support vector classifiers, which we will be covering in the next section.

在下图中,我们可以画出无限个独立的超平面来分隔两个类(蓝色和红色)。然而,最大裕度分类器试图在两个类别之间拟合最宽的平板(最大化正和负超平面之间的裕度),并且接触正和负超平面的观测值被称为支持向量:

Classifier performance purely depends on the support vectors and any changes to observation values which are not support vectors (or observations that do not touch hyperplanes) do not impact any change in the performance of the Maximum Margin Classifier, as only extreme points are considered in the algorithm.

支持向量分类器

支持向量分类器是最大裕度分类器的扩展版本,其中对于不可分离的情况允许一些违规,以便创建最佳拟合,即使在阈值限制内有微小误差。事实上,在现实生活场景中,我们几乎找不到任何具有纯可分类的数据;大多数类在重叠类中有一些或更多的观察。

支持向量分类器的数学表示如下,对约束稍加修正以适应误差项:

在约束 4 中, C 值是非负调谐参数,以适应模型中更多或更少的总体误差。具有较高的 C 值将导致更健壮的模型,而较低的值由于较少违反误差项而创建灵活的模型。在实践中, C 值将是所有机器学习模型中常见的一个调整参数。

更改 C 值对边距的影响如下图所示;使用高值的 C ,模型将更加宽容,并且在左图中也有违规(错误)的空间,而使用较低值的 C ,没有接受违规的范围导致边距宽度减小。 C 是支持向量分类器中的一个调整参数:

支持向量机

当决策边界是非线性的并且无论代价函数是什么都不能与支持向量分类器分离时,使用支持向量机!下图解释了一维和二维的非线性可分情况:

很明显,无论成本值是多少,我们都无法使用支持向量分类器进行分类。因此,我们需要使用另一种处理数据的方式,称为内核技巧,使用内核函数来处理非线性可分离的数据。

下图中,在将数据从一维数据转换为二维数据时,应用了一个 2 次多项式核。通过这样做,数据在更高维度上变得可线性分离。在左图中,不同的类(红色和蓝色)仅绘制在 X 1 上,而在应用了 2 度之后,我们现在有了 2 个维度, X 1X 2 1 (原始维度和新维度)。多项式核的次数是一个调整参数;从业者需要用不同的值来调整它们,以检查模型在哪些地方可能有更高的精度:

然而,在 2 维的情况下,核技巧如下所述应用于 2 次多项式核。在将数据投影到更高维度之后,似乎已经使用线性平面成功地对观测进行了分类:

内核函数

核函数是在给定原始特征向量的情况下,返回与其对应的映射特征向量的点积相同的值的函数。核函数不显式地将特征向量映射到更高维空间,也不计算映射向量的点积。内核通过一系列不同的运算产生相同的值,这些运算通常可以更有效地计算。

使用核函数的主要原因是消除了从给定的基本向量空间导出高维向量空间的计算要求,从而可以在更高维度上线性分离观测值。为什么有人需要像这样,导出的向量空间会随着维数的增加呈指数级增长,并且它会变得几乎难以继续计算,即使你有 30 左右的可变大小。以下示例显示了变量的大小是如何增长的。

:当我们有 xy 两个变量时,用一个多项式次数核,另外需要计算 x 2 ,y 2 ,以及 xy 维度。反之,如果我们有三个变量 x、y 和 z,那么我们需要计算 x 2y 2z 2xyyzxzxyz 向量空间。到这个时候,你会意识到多一个维度的增加会创造出如此多的组合。因此,需要注意降低其计算复杂性;这就是核仁创造奇迹的地方。内核在下面的等式中有更正式的定义:

多项式核:多项式核使用普遍,尤其是 2 次。事实上,支持向量机的发明者弗拉迪米尔·N·瓦普尼克利用 2 度核对手写数字进行分类。多项式核由以下等式给出:

径向基函数(RBF) /高斯核:对于需要非线性模型的问题,RBF 核是很好的首选。作为映射特征空间中的超平面的决策边界类似于作为原始空间中的超球面的决策边界。高斯核产生的特征空间可以有无限多个维度,否则这是不可能的。径向基函数核由以下等式表示:

这通常简化为以下等式:

当使用支持向量机时,缩放特征是可取的,但是当使用径向基函数核时,这是非常重要的。当γ值很小时,它会在更高的维度上给你一个尖的凸起;较大的值会产生更柔和、更宽的凸起。一个小的伽玛会给你低偏差和高方差的解;另一方面,高伽马会给你高偏差和低方差的解决方案,这就是你如何使用径向基函数核控制模型的拟合:

带有字母识别数据示例的 SVM 多标签分类器

字母识别数据已从 UCI 机器学习存储库中使用,用于使用 SVM 分类器进行说明。下载数据的链接在这里:https://archive.ics.uci.edu/ml/datasets/letter+recognition。任务是将大量黑白矩形像素显示器中的每一个识别为英文字母表中的 26 个大写字母之一(从 A 到 Z;总共 26 个类),基于整数中的一些特征,例如 x-box(框的水平位置)、y-box(框的垂直位置)、框的宽度、框的高度等等:

>>> import os 
""" First change the following directory link to where all input files do exist """ 
>>> os.chdir("D:\\Book writing\\Codes\\Chapter 6") 

>>> import pandas as pd 
>>> letterdata = pd.read_csv("letterdata.csv") 
>>> print (letterdata.head()) 

以下代码用于从x变量中移除目标变量,同时为了方便起见创建新的y变量:

>>> x_vars = letterdata.drop(['letter'],axis=1) 
>>> y_var = letterdata["letter"] 

由于 scikit-learn 不直接支持字符,我们需要将它们转换为数字映射。在这里,我们用字典做到了这一点:

>>> y_var = y_var.replace({'A':1,'B':2,'C':3,'D':4,'E':5,'F':6,'G':7, 'H':8,'I':9, 'J':10,'K':11,'L':12,'M':13,'N':14,'O':15,'P':16,'Q':17,'R':18,'S':19,'T':20,'U':21, 'V':22, 'W':23,'X':24,'Y':25,'Z':26}) 

>>> from sklearn.metrics import accuracy_score,classification_report 
>>> from sklearn.model_selection import train_test_split 
>>> x_train,x_test,y_train,y_test = train_test_split(x_vars,y_var,train_size = 0.7,random_state=42) 

# Linear Classifier 
>>> from sklearn.svm import SVC 

最大裕度分类器-线性核

以下代码显示了成本值为 1.0 的线性分类器(也称为最大利润分类器):

>>> svm_fit = SVC(kernel='linear',C=1.0,random_state=43) 
>>> svm_fit.fit(x_train,y_train) 

>>> print ("\nSVM Linear Classifier - Train Confusion Matrix\n\n",pd.crosstab(y_train, svm_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]) )      
>>> print ("\nSVM Linear Classifier - Train accuracy:",round(accuracy_score(y_train, svm_fit.predict(x_train)),3)) 
>>> print ("\nSVM Linear Classifier - Train Classification Report\n", classification_report(y_train,svm_fit.predict(x_train))) 

以下代码用于打印测试精度值:

>>> print ("\n\nSVM Linear Classifier - Test Confusion Matrix\n\n",pd.crosstab(y_test, svm_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nSVM Linear Classifier - Test accuracy:",round(accuracy_score( y_test,svm_fit.predict(x_test)),3)) 
>>> print ("\nSVM Linear Classifier - Test Classification Report\n", classification_report(y_test,svm_fit.predict(x_test)))

从上面的结果中,我们可以看到线性分类器的测试准确率为 85%,就准确率而言,这是一个不错的值。让我们也来探索一下多项式核。

多项式核

在下面的代码中使用了一个 2 次多项式核来检查是否有可能提高精度。成本值相对于线性分类器保持恒定,以便确定非线性核的影响:

#Polynomial Kernel
>>> svm_poly_fit = SVC(kernel='poly',C=1.0,degree=2)
>>> svm_poly_fit.fit(x_train,y_train)
>>> print ("\nSVM Polynomial Kernel Classifier - Train Confusion Matrix\n\n",pd.crosstab(y_train,svm_poly_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]) )
>>> print ("\nSVM Polynomial Kernel Classifier - Train accuracy:",round(accuracy_score( y_train,svm_poly_fit.predict(x_train)),3))
>>> print ("\nSVM Polynomial Kernel Classifier - Train Classification Report\n", classification_report(y_train,svm_poly_fit.predict(x_train)))

>>> print ("\n\nSVM Polynomial Kernel Classifier - Test Confusion Matrix\n\n", pd.crosstab(y_test,svm_poly_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))
>>> print ("\nSVM Polynomial Kernel Classifier - Test accuracy:",round(accuracy_score( y_test,svm_poly_fit.predict(x_test)),3))
>>> print ("\nSVM Polynomial Kernel Classifier - Test Classification Report\n", classification_report(y_test,svm_poly_fit.predict(x_test)))

多项式核产生了 95.4%的测试精度,与线性分类器的 85%的测试精度相比,这是一个巨大的改进。通过提高一度,我们实现了 10%的精度提升。

径向基函数核

在最后的实验中,使用径向基函数核来确定测试精度。这里,对于相应的其他核,成本值保持恒定,但是伽马值被选择为 0.1 以适合模型:

#RBF Kernel 
>>> svm_rbf_fit = SVC(kernel='rbf',C=1.0, gamma=0.1) 
>>> svm_rbf_fit.fit(x_train,y_train) 
>>> print ("\nSVM RBF Kernel Classifier - Train Confusion Matrix\n\n",pd.crosstab( y_train,svm_rbf_fit.predict(x_train),rownames = ["Actuall"],colnames = ["Predicted"]))      
>>> print ("\nSVM RBF Kernel Classifier - Train accuracy:",round(accuracy_score( y_train, svm_rbf_fit.predict(x_train)),3)) 
>>> print ("\nSVM RBF Kernel Classifier - Train Classification Report\n", classification_report(y_train,svm_rbf_fit.predict(x_train))) 

>>> print ("\n\nSVM RBF Kernel Classifier - Test Confusion Matrix\n\n", pd.crosstab(y_test,svm_rbf_fit.predict(x_test),rownames = ["Actuall"],colnames = ["Predicted"]))       
>>> print ("\nSVM RBF Kernel Classifier - Test accuracy:",round( accuracy_score( y_test,svm_rbf_fit.predict(x_test)),3)) 
>>> print ("\nSVM RBF Kernel Classifier - Test Classification Report\n", classification_report(y_test,svm_rbf_fit.predict(x_test))) 

从径向基函数核得到的测试精度为 96.9%,略好于多项式核的 95.4%。然而,通过使用网格搜索仔细调整参数,可以进一步提高测试精度。

网格搜索已经通过使用径向基函数核改变成本和伽马值来执行。以下代码描述了详细信息:

# Grid Search - RBF Kernel 
>>> from sklearn.pipeline import Pipeline 
>>> from sklearn.model_selection import train_test_split,GridSearchCV 

>>> pipeline = Pipeline([('clf',SVC(kernel='rbf',C=1,gamma=0.1 ))]) 

>>> parameters = {'clf__C':(0.1,0.3,1,3,10,30), 
              'clf__gamma':(0.001,0.01,0.1,0.3,1)} 

>>> grid_search_rbf = GridSearchCV(pipeline,parameters,n_jobs=-1,cv=5, verbose=1, scoring='accuracy') 
>>> grid_search_rbf.fit(x_train,y_train) 

>>> print ('RBF Kernel Grid Search Best Training score: %0.3f' % grid_search_rbf.best_score_) 
>>> print ('RBF Kernel Grid Search Best parameters set:') 
>>> best_parameters = grid_search_rbf.best_estimator_.get_params() 

>>> for param_name in sorted(parameters.keys()): 
...     print ('\t%s: %r' % (param_name, best_parameters[param_name])) 

>>> predictions = grid_search_rbf.predict(x_test) 
>>> print ("RBF Kernel Grid Search - Testing accuracy:",round(accuracy_score(y_test, predictions),4)) 
>>> print ("\nRBF Kernel Grid Search - Test Classification Report",classification_report( y_test, predictions)) 
>>> print ("\n\nRBF Kernel Grid Search- Test Confusion Matrix\n\n",pd.crosstab(y_test, predictions,rownames = ["Actuall"],colnames = ["Predicted"]))       

通过观察上述结果,我们可以得出结论,获得的最佳测试精度为 97.15%,这是比任何其他分类器获得的值都高的值。因此,我们可以得出结论,径向基函数核产生最好的结果可能!

SVM 分类器的下列代码:

# SVM Classifier   
# First change the following   directory link to where all the input files do exist   
setwd("D:\\Book   writing\\Codes\\Chapter 6")   

letter_data = read.csv("letterdata.csv")   
set.seed(123)   
numrow = nrow(letter_data)   
trnind = sample(1:numrow,size =   as.integer(0.7*numrow))   
train_data =   letter_data[trnind,]   
test_data = letter_data[-trnind,]   

library(e1071)   

accrcy <- function(matrx){    
  return(   sum(diag(matrx)/sum(matrx)))}   
precsn <- function(matrx){   
  return(diag(matrx) /   rowSums(matrx)) }   
recll <- function(matrx){   
  return(diag(matrx) /   colSums(matrx)) }   

# SVM - Linear Kernel   
svm_fit = svm(letter~.,data = train_data,kernel="linear",cost=1.0,   scale = TRUE)   
tr_y_pred = predict(svm_fit,   train_data)   
ts_y_pred =   predict(svm_fit,test_data)   
tr_y_act =   train_data$letter;ts_y_act = test_data$letter   

tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Train   Confusion Matrix"))   
print(tr_tble)   
tr_acc = accrcy(tr_tble)   
print(paste("SVM Linear   Kernel Train accuracy:",round(tr_acc,4)))   

tr_prec = precsn(tr_tble)   
print(paste("SVM Linear   Kernel Train Precision:"))   
print(tr_prec)   

tr_rcl = recll(tr_tble)   
print(paste("SVM Linear Kernel   Train Recall:"))   
print(tr_rcl)   

ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Test   Confusion Matrix"))   
print(ts_tble)   

ts_acc = accrcy(ts_tble)   
print(paste("SVM Linear   Kernel Test accuracy:",round(ts_acc,4)))   

ts_prec = precsn(ts_tble)   
print(paste("SVM Linear   Kernel Test Precision:"))   
print(ts_prec)   

ts_rcl = recll(ts_tble)   
print(paste("SVM Linear   Kernel Test Recall:"))   
print(ts_rcl)   

# SVM - Polynomial Kernel   
svm_poly_fit =   svm(letter~.,data = train_data,kernel="poly",cost=1.0,degree = 2    ,scale = TRUE)   

tr_y_pred =   predict(svm_poly_fit, train_data)   
ts_y_pred =   predict(svm_poly_fit,test_data)   

tr_y_act =   train_data$letter;ts_y_act = test_data$letter   

tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Train   Confusion Matrix"))   
print(tr_tble)   
tr_acc = accrcy(tr_tble)   
print(paste("SVM   Polynomial Kernel Train accuracy:",round(tr_acc,4)))   

tr_prec = precsn(tr_tble)   
print(paste("SVM   Polynomial Kernel Train Precision:"))   
print(tr_prec)   

tr_rcl = recll(tr_tble)   
print(paste("SVM   Polynomial Kernel Train Recall:"))   
print(tr_rcl)   

ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Test   Confusion Matrix"))   
print(ts_tble)   

ts_acc = accrcy(ts_tble)   
print(paste("SVM   Polynomial Kernel Test accuracy:",round(ts_acc,4)))   

ts_prec = precsn(ts_tble)   
print(paste("SVM   Polynomial Kernel Test Precision:"))   
print(ts_prec)   

ts_rcl = recll(ts_tble)   
print(paste("SVM   Polynomial Kernel Test Recall:"))   
print(ts_rcl)   

# SVM - RBF Kernel   
svm_rbf_fit = svm(letter~.,data   = train_data,kernel="radial",cost=1.0,gamma = 0.2  ,scale = TRUE)   

tr_y_pred =   predict(svm_rbf_fit, train_data)   
ts_y_pred =   predict(svm_rbf_fit,test_data)   

tr_y_act =   train_data$letter;ts_y_act = test_data$letter   

tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Train   Confusion Matrix"))   
print(tr_tble)   
tr_acc = accrcy(tr_tble)   
print(paste("SVM RBF   Kernel Train accuracy:",round(tr_acc,4)))   

tr_prec = precsn(tr_tble)   
print(paste("SVM RBF   Kernel Train Precision:"))   
print(tr_prec)   
 tr_rcl = recll(tr_tble)   
print(paste("SVM RBF   Kernel Train Recall:"))   
print(tr_rcl)   

ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Test   Confusion Matrix"))   
print(ts_tble)   

ts_acc = accrcy(ts_tble)   
print(paste("SVM RBF   Kernel Test accuracy:",round(ts_acc,4)))   

ts_prec = precsn(ts_tble)   
print(paste("SVM RBF   Kernel Test Precision:"))   
print(ts_prec)   

ts_rcl = recll(ts_tble)   
print(paste("SVM RBF   Kernel Test Recall:"))   
print(ts_rcl)   

# Grid search - RBF Kernel   
library(e1071)   
svm_rbf_grid =   tune(svm,letter~.,data = train_data,kernel="radial",scale=TRUE,ranges   = list(   
  cost = c(0.1,0.3,1,3,10,30),   
  gamma =   c(0.001,0.01,0.1,0.3,1) ),   
  tunecontrol =   tune.control(cross = 5))   

print(paste("Best   parameter from Grid Search"))   
print(summary(svm_rbf_grid))   

best_model =   svm_rbf_grid$best.model   
tr_y_pred = predict(best_model,data   = train_data,type = "response")   
ts_y_pred =   predict(best_model,newdata = test_data,type = "response")   
tr_y_act =   train_data$letter;ts_y_act = test_data$letter   

tr_tble =   table(tr_y_act,tr_y_pred)   
print(paste("Train   Confusion Matrix"))   
print(tr_tble)   
tr_acc = accrcy(tr_tble)   
print(paste("SVM RBF   Kernel Train accuracy:",round(tr_acc,4)))   

tr_prec = precsn(tr_tble)   
print(paste("SVM RBF   Kernel Train Precision:"))   
print(tr_prec)   

tr_rcl = recll(tr_tble)   
print(paste("SVM RBF   Kernel Train Recall:"))   
print(tr_rcl)   

ts_tble =   table(ts_y_act,ts_y_pred)   
print(paste("Test   Confusion Matrix"))   
print(ts_tble) 
ts_acc = accrcy(ts_tble)   
print(paste("SVM RBF   Kernel Test accuracy:",round(ts_acc,4)))   

ts_prec = precsn(ts_tble)   
print(paste("SVM RBF   Kernel Test Precision:"))   
print(ts_prec)   

ts_rcl = recll(ts_tble)   
print(paste("SVM RBF   Kernel Test Recall:"))   
print(ts_rcl)   

人工神经网络

人工神经网络 ( 神经网络 s)使用从生物大脑的复制品中得到的模型来模拟一组输入信号和输出信号之间的关系,生物大脑对来自其感觉输入的刺激做出反应。人脑由大约 900 亿个神经元组成,它们之间有大约 1 万亿个连接;人工神经网络方法试图使用互连的人工神经元(或节点)来建模问题,以解决机器学习问题。

众所周知,人工神经网络从生物神经元中获得灵感。我们将花一些时间了解生物神经元是如何工作的。细胞的树突通过生化过程接收输入信号,从而根据脉冲的相对重要性对其进行加权。当细胞体开始积累传入的信号时,就达到了一个阈值,在这个阈值下,细胞会激发,然后输出信号通过电化学过程沿着轴突传递。在轴突末端,电信号再次被处理为化学信号,传递给邻近的神经元,这些神经元将成为树突,传递给其他神经元。

类似的工作原理松散地用于构建人工神经网络,其中每个神经元都有一组输入,每个输入都被赋予特定的权重。神经元根据这些加权输入计算函数。线性神经元采用加权输入的线性组合,并对聚合和应用激活函数(sigmoid、tanh、relu 等)。详情如下图所示。

网络将输入的加权和馈入逻辑函数(在sigmoid函数的情况下)。逻辑函数根据设定的阈值返回一个介于 0 和 1 之间的值;例如,这里我们将阈值设置为 0.7。任何大于 0.7 的累积信号给出 1 的信号,反之亦然;任何小于 0.7 的累积信号都会返回 0:

Neural network models are being considered as universal approximators, which means by using a neural network methodology, we can solve any type of problems with the fine-tuned architecture. Hence, studying neural networks is a branch of study and special care is needed. In fact, deep learning is a branch of machine learning, where every problem is being modeled with artificial neural networks.

具有 n 个输入树突的典型人工神经元可以由以下公式表示。 w 权重允许 x 的每个 n 输入对输入信号的总和贡献或多或少的量。累加值被传递到激活函数 f(x) ,结果信号 y(x) 是输出轴:

选择构建神经网络所需的参数如下:

  • 激活函数:选择激活函数在将信号聚集成输出信号以传播到网络的其他神经元中起着主要作用。
  • 网络架构或拓扑:这代表所需的层数和每层的神经元数量。更多的层和神经元将创建高度非线性的决策边界,而如果我们减少架构,模型将更不灵活,更健壮。
  • 训练优化算法:优化算法的选择也起着至关重要的作用,以便快速准确地收敛到最佳的最优解,我们将在本章后面的章节中详细介绍。
  • 神经网络的应用:近年来,神经网络(深度学习的一个分支)在人工智能的应用方面获得了巨大的关注,在语音、文本、视觉等许多领域都是如此。我们将在本章后面的部分介绍深度学习。一些著名的应用如下:
    • 图像和视频:识别图像中的物体或分类它是狗还是猫
    • 文本处理(NLP): 基于深度学习的聊天机器人等等
    • 语音:识别语音
    • 结构化数据处理:构建功能强大的模型,以获得非线性决策边界

激活功能

激活功能是人工神经元处理信息并通过网络传递信息的机制。激活函数取一个数字,并对其执行某种固定的数学函数映射。有许多不同类型的激活函数。最受欢迎的有:

  • 乙状结肠的
  • 双曲正切
  • Relu
  • 线性的

Sigmoid 函数: Sigmoid 的数学形式为σ(x)= 1/(1+e-x)。它接受一个实数值,并将其压缩到 0 到 1 之间的范围内。Sigmoid 是一个流行的选择,这使得计算导数变得容易,并且易于解释。

Tanh 函数: Tanh 将实数挤压到 [-1,1] 的范围内。输出以零为中心。实际上,tanh 非线性总是优于 sigmoid 非线性。此外,还可以证明 tanh 是缩放的 sigmoid 神经元tanh(x)= 2σ(2x)1

整流线性单元(ReLU)功能:最近几年 ReLU 变得非常流行。它计算函数 f(x) = max (0,x) 。激活只是零阈值。

线性函数:线性激活函数用于线性回归问题,由于使用的函数为 f(x) = x ,因此它总是提供导数为 1。

Relu 由于其更好的收敛特性,现在被广泛用于代替 SigmoidTanh

下图描述了所有激活功能。线性激活函数用于线性回归情况,而所有其他激活函数用于分类问题:

正向传播和反向传播

在下面的例子中,前向传播和反向传播用两个隐藏层深度神经网络来说明,其中除了输入和输出层之外,两个层每个都有三个神经元。输入层中神经元的数量基于 x(独立)变量的数量,而输出层中神经元的数量由模型需要预测的类的数量决定。

为了方便起见,我们只显示了每层中的一个神经元;然而,读者可以尝试在同一层内创建其他神经元。权重和偏差是从一些随机数开始的,因此在向前和向后传递中,这些都可以更新,以便将误差最小化。

在前向传播过程中,要素被输入到网络,并通过后续层进行馈送,以产生输出激活。如果我们在隐藏层 1 中看到,获得的激活是偏置权重 1 和输入值加权组合的组合;如果总值超过阈值,它将触发到下一层,否则信号对于下一层值将是 0。偏差值是控制触发点所必需的。在某些情况下,加权组合信号较低;在这些情况下,偏差将补偿调整聚集值的额外金额,这可以触发下一个级别。完整的等式可以在下图中看到:

一旦在隐藏层 1 ( 隐藏层 1隐藏层 2隐藏层 3 神经元)中计算了所有神经元,则需要以类似的方式从第一层的隐藏神经元的输出中计算下一层神经元,并加上偏差(偏差权重 4)。下图描述了层 2 中显示的隐藏神经元 4:

在最后一层(也称为输出层),通过采用权重和从隐藏层 2 获得的输出的加权组合,以相同的方式从从隐藏层 2 获得的输出计算输出。一旦我们从模型中获得输出,就需要与实际值进行比较,并且我们需要在整个网络中反向传播误差,以便校正整个神经网络的权重:

在下图中,我们取了输出值的导数,然后乘以该值,得到误差分量,该误差分量是通过将实际值与模型输出求差得到的:

以类似的方式,我们也将从第二个隐藏层反向传播错误。在下图中,从第二个隐藏层中的隐藏 4 神经元计算误差:

在下图中,基于从第 2 层中的所有神经元获得的误差,为第 1 层中的隐藏 1 神经元计算误差:

一旦隐藏层 1 中的所有神经元都被更新,输入和隐藏层之间的权重也需要更新,因为我们不能更新输入变量的任何内容。在下图中,我们将更新输入和隐藏层 1 中的神经元的权重,因为层 1 中的神经元仅利用输入的权重:

最后,在下图中,第 2 层神经元正在正向传播过程中更新:

我们还没有展示下一次迭代,在这次迭代中,输出层的神经元被错误更新,反向传播再次开始。以类似的方式,更新所有的权重,直到解决方案收敛或达到迭代次数。

神经网络优化

各种技术已被用于优化神经网络的权重:

  • 随机梯度下降 ( SGD )
  • 动力
  • 内斯特罗夫加速梯度 ( NAG )
  • 自适应梯度(阿达格勒)
  • Adadelta
  • RMSprop
  • 自适应矩估计 ( 亚当)
  • 有限记忆布赖登-弗莱彻-戈德法布-尚诺 ( L-BFGS )

在实践中, Adam 是一个不错的默认选择;我们将在这一部分介绍它的工作方法。如果您负担不起完整的批量更新,那么试试 L-BFGS:

随机梯度下降

梯度下降是一种最小化目标函数 J( θ ) 的方法,该目标函数由模型参数θ ε R d 参数化,方法是相对于参数在目标函数梯度的相反方向上更新参数。学习率决定了达到最小值所采取的步骤的大小:

  • 批量梯度下降(每次迭代中使用的所有训练观察)
  • 每次迭代一次观察
  • 小批量梯度下降(每次迭代约 50 个训练观测值):

在下面的图像中,仔细观察了 2D 投影,其中比较了全批次和随机梯度下降随批次大小 1 的收敛特性。如果我们看到,由于考虑了所有的观察,整批更新更加平滑。然而,由于每次更新使用 1 观察的原因,SGD 具有摆动收敛特性:

动力

SGD 很难在一个维度比另一个维度更陡峭的曲面曲线上导航;在这些场景中,SGD 在峡谷的斜坡上振荡,同时只能沿着底部犹豫不决地向局部最优方向前进。

当利用动量时,我们把球推下山。球滚下坡时积累动量,途中变得越来越快,直至停止(由于空气阻力等);类似地,动量项对于梯度指向相同方向的维度增加,而对于梯度改变方向的维度减少更新。因此,我们获得了更快的收敛和更少的振荡:

nestereov 加速梯度- nag

如果一个球滚下山坡,盲目地沿着一个斜坡,这是非常不令人满意的,它应该知道它要去哪里,这样它就知道在山坡再次向上倾斜之前要减速。NAG 是一种赋予动量项这种先见之明的方法。

动量首先计算当前梯度(蓝色小向量),然后在更新的累积梯度方向(蓝色大向量)上进行大跳跃,而 NAG 首先在前一个累积梯度方向(棕色向量)上进行大跳跃,测量梯度,然后进行校正(绿色向量)。这种提前更新可以防止球跑得太快,从而提高响应速度和性能:

阿达格勒

Adagrad 是一种基于梯度的优化算法,它使差分学习速率适应参数,对不频繁的参数执行较大的更新,对频繁的参数执行较小的更新。

Adagrad 大大提高了 SGD 的鲁棒性,并将其用于大规模神经网络的训练。Adagrad 的主要优势之一是它消除了手动调整学习速率的需要。大多数实现使用默认值 0.01,并保持该值。

Adagrad 的主要弱点是它在分母中的平方梯度的累积:由于每个相加的项都是正的,所以累积的总和在训练过程中不断增长。这反过来会导致学习速率缩小,最终变得非常小,此时算法不再能够获取额外的知识。以下算法旨在解决这一缺陷。

Adadelta

Adadelta 是 Adagrad 的扩展,旨在降低其激进的单调递减的学习速率。不是累积所有过去的平方梯度,Adadelta 将累积的过去梯度的窗口限制为固定大小 w (不是低效存储 w 先前的平方梯度,梯度的总和被递归地定义为所有过去的平方梯度的衰减平均值)。

RMSprop

RMSpropAdadelta 都是在同一时间独立开发的,用于解决 Adagrad 的学习速率急剧下降的问题(RMSprop 还将学习速率除以平方梯度的指数衰减平均值)。

自适应矩估计-亚当

Adam 是另一种为每个参数计算自适应学习速率的方法。除了像 Adadelta 和 RMSprop 那样存储过去平方梯度的指数衰减平均值之外,Adam 还保持过去梯度的指数衰减平均值,类似于动量。

当你有疑问的时候,就用亚当!

有限记忆 broyden-Fletcher-goldfarb-shanno-L-BFGS 优化算法

L-BFGS 是 BFGS 的有限内存,它属于近似 BFGS 算法的拟牛顿方法家族,该算法利用有限的计算机内存。BFGS 目前被认为是最有效的,也是迄今为止最流行的,准牛顿更新公式。

L-BFGS 方法最好用下图来解释,其中迭代从随机点( xt )开始,在该点计算二阶导数或 hessian 矩阵,这是原始函数的二次近似;在计算二次函数后,它一步计算最小值,在计算函数值最小的新点( xt+1 )后,较早的点将成为下一次迭代的起点。

在第二次迭代中,将在新的点( xt+1 )和一步计算的另一个最小值( xt+2 )处进行另一次二次近似。这样,L-BFGS 以更快的方式收敛到解,并且即使在非凸函数上也是有效的(在 R 代码中,我们使用了 nnet 包,其中 L-BFGS 被用于优化目的):

神经网络中的缺失

丢弃是神经网络中的正则化,以避免数据的过拟合。典型地,初始层中的脱落率为 0.2(80%的神经元始终随机存在),中间层中的脱落率为 0.5。理解辍学概念的一个直观方法是办公室团队,其中一些团队成员擅长与客户沟通,尽管他们不擅长技术细节,而一些成员擅长技术知识,但没有足够好的沟通技巧。假设有些成员离开办公室,然后其他成员试图代替其他人完成工作。这样,善于沟通的团队成员也会同样学到技术细节;其他一些擅长技术知识的团队成员也学习与客户沟通。这样,所有团队成员将变得足够独立和健壮,可以执行所有类型的工作,这对办公室有好处(假设团队经理会给办公室所有团队成员足够的假期!):

基于 scikit-learn 的人工神经网络分类器在手写数字识别中的应用

以 scikit-learn 数据集的手写数字为例说明了一个人工神经网络分类器示例,其中手写数字是从 0 到 9 创建的,它们各自的 64 个特征(8×8 矩阵)的像素强度在 0 到 255 之间,因为可以表示任何黑白(或灰度)图像。对于彩色图像,RGB(红色、绿色和蓝色)通道将用于表示所有颜色:

# Neural Networks - Classifying hand-written digits  
>>> import pandas as pd 
>>> from sklearn.datasets import load_digits 
>>> from sklearn.cross_validation import train_test_split 
>>> from sklearn.pipeline import Pipeline 
>>> from sklearn.preprocessing import StandardScaler 

>>> from sklearn.neural_network import MLPClassifier 
>>> digits = load_digits() 
>>> X = digits.data 
>>> y = digits.target 

# Checking dimensions 
>>> print (X.shape) 
>>> print (y.shape) 

# Plotting first digit 
>>> import matplotlib.pyplot as plt  
>>> plt.matshow(digits.images[0])  
>>> plt.show() 

>>> from sklearn.model_selection import train_test_split 
>>> x_vars_stdscle = StandardScaler().fit_transform(X) 
>>> x_train,x_test,y_train,y_test = train_test_split(x_vars_stdscle,y,train_size = 0.7,random_state=42) 

# Grid Search - Neural Network  
>>> from sklearn.pipeline import Pipeline 
>>> from sklearn.model_selection import train_test_split,GridSearchCV 
>>> from sklearn.metrics import accuracy_score,classification_report 

MLP 分类器已经被用于第一和第二隐藏层中连续的 100 和 50 的隐藏层。亚当优化器已经被用于减少错误。所有学习率为 0.0001 的神经元都使用了 Relu 激活函数。最后,初始时迭代总数为 300:

>>> pipeline = Pipeline([('mlp',MLPClassifier(hidden_layer_sizes= (100,50,), activation='relu',solver='adam',alpha=0.0001,max_iter=300 ))]) 

上述参数仅用于启动分类器,而下面的代码描述了使用管道函数的网格搜索。学习率和最大迭代次数用于寻找最佳组合。但是,我们鼓励读者添加其他功能,并在可以获得更好结果的地方进行测试:

>>> parameters = {'mlp__alpha':(0.001,0.01,0.1,0.3,0.5,1.0), 'mlp__max_iter':(100,200,300)} 

使用了五重交叉验证的网格搜索,使用了默认的核心数量和评分作为准确性。尽管如此,您可以将其更改为 10 倍交叉验证等等,以了解准确性如何随着交叉验证指标的变化而变化:

>>> grid_search_nn = GridSearchCV(pipeline,parameters,n_jobs=-1,cv=5,verbose=1, scoring='accuracy') 
>>> grid_search_nn.fit(x_train,y_train) 

>>> print ('\n\nNeural Network Best Training score: %0.3f' % grid_search_nn.best_score_) 
>>> print ('\nNeural Network Best parameters set:') 
best_parameters = grid_search_nn.best_estimator_.get_params() 
>>> for param_name in sorted(parameters.keys()): 
...     print ('\t%s: %r' % (param_name, best_parameters[param_name])) 

获得最大精度值的最佳参数为 96.3%,α为 0.001,最大迭代次数为 200:

>>> predictions_train = grid_search_nn.predict(x_train) 
>>> predictions_test = grid_search_nn.predict(x_test) 
>>> print ("\nNeural Network Training accuracy:",round(accuracy_score(y_train, predictions_train),4)) 
>>> print ("\nNeural Network Complete report of Training data\n",classification_report(y_train, predictions_train)) 
>>> print ("\n\nNeural Network Grid Search- Train Confusion Matrix\n\n",pd.crosstab(y_train, predictions_train,rownames = ["Actuall"],colnames = ["Predicted"]))       

>>> print ("\n\nNeural Network Testing accuracy:",round(accuracy_score(y_test, predictions_test),4)) 
>>> print ("\nNeural Network Complete report of Testing data\n",classification_report( y_test, predictions_test)) 
>>> print ("\n\nNeural Network Grid Search- Test Confusion Matrix\n\n",pd.crosstab(y_test, predictions_test,rownames = ["Actuall"],colnames = ["Predicted"])) 

上图显示,获得的最佳测试精度为 97.59%,这是以特别好的精度预测数字。

在下面的 R 代码中,使用了nnet包,其中 L-BFGS 被用作优化器。nnet包中的神经元数量有一个最大值为 13 的限制。因此,我们无法检查神经元数量超过 13 的结果:

人工神经网络分类器的 R 码示例:

# Artificial Neural Networks   
setwd("D:\\Book   writing\\Codes\\Chapter 6")   
digits_data = read.csv("digitsdata.csv")   
remove_cols = c("target")   
x_data =   digits_data[,!(names(digits_data) %in% remove_cols)]   
y_data = digits_data[,c("target")]   
normalize <- function(x)   {return((x - min(x)) / (max(x) - min(x)))}   

data_norm <-   as.data.frame(lapply(x_data, normalize))   
data_norm <-   replace(data_norm, is.na(data_norm), 0.0)   
data_norm_v2 =   data.frame(as.factor(y_data),data_norm)   
names(data_norm_v2)[1] = "target"   

set.seed(123)   
numrow = nrow(data_norm_v2)   
trnind = sample(1:numrow,size =   as.integer(0.7*numrow))   
train_data =   data_norm_v2[trnind,]   
test_data = data_norm_v2[-trnind,]   
f <- as.formula(paste("target   ~",    
paste(names(train_data)[!names(train_data)   %in% "target"], collapse = " + ")))   

library(nnet)   
accuracy <-   function(mat){return(sum(diag(mat)) / sum(mat))}   

nnet_fit =   nnet(f,train_data,size=c(9),maxit=200)   
y_pred =   predict(nnet_fit,newdata = test_data,type = "class")   
tble =   table(test_data$target,y_pred)   
print(accuracy(tble))   

#Plotting nnet from the github   packages   
require(RCurl)   
root.url<-'https://gist.githubusercontent.com/fawda123'   
raw.fun<-paste(root.url,  '5086859/raw/cc1544804d5027d82b70e74b83b3941cd2184354/nnet_plot_fun.r',   
  sep='/')   
script<-getURL(raw.fun,   ssl.verifypeer = FALSE)   
eval(parse(text = script))   
rm('script','raw.fun')   
# Ploting the neural net   
plot(nnet_fit)   

# Grid Search - ANN   
neurons =   c(1,2,3,4,5,6,7,8,9,10,11,12,13)   
iters =   c(200,300,400,500,600,700,800,900)   

initacc = 0   
for(itr in iters){   
  for(nd in neurons){   
    nnet_fit =   nnet(f,train_data,size=c(nd),maxit=itr,trace=FALSE)   
    y_pred =   predict(nnet_fit,newdata = test_data,type = "class")   
    tble =   table(test_data$target,y_pred)   
    acc = accuracy(tble)   

    if (acc>initacc){   
      print(paste("Neurons",nd,"Iterations",itr,"Test   accuracy",acc))   
      initacc = acc   
    }   

  }   
} 

深度学习入门

深度学习是一类机器学习算法,它利用神经网络来建立模型,以解决结构化和非结构化数据集(如图像、视频、自然语言处理、语音处理等)上的监督和非监督问题:

深度神经网络/深度体系结构由输入和输出层之间的多个隐藏单元层组成。每层都与后续层完全连接。一层中每个人工神经元的输出是下一层中每个人工神经元的输出输入:

随着越来越多的隐藏层被添加到神经网络中,越来越复杂的决策边界被创建来分类不同的类别。在下图中可以看到复杂决策边界的示例:

解决方法

反向传播用于通过计算输出单元处的网络误差来求解深层,并通过层反向传播以更新权重来减少误差项。

设计深度神经网络的经验法则:虽然设计神经网络没有硬性的规则,但以下规则将提供一些指导:

  • 所有隐藏层每层都应该有相同数量的神经元
  • 通常,两个隐藏层足以解决大多数问题
  • 在每一层之后对所有输入变量使用缩放/批量归一化(均值 0,方差 1)提高了收敛效率
  • 除了使用动量和损失之外,每次迭代后步长的减小改善了收敛性

深度学习软件

深度学习软件在最近几年已经发展了许多倍。在这一章中,我们使用 Keras 来开发一个模型,因为 Keras 模型对于新手来说很容易理解和原型化新概念。然而,许多其他软件也存在,并被世界各地的许多从业者使用:

  • antao:蒙特利尔大学开发的基于 Python 的深度学习库
  • TensorFlow: 谷歌的深度学习库运行在 Python/C++之上
  • Keras / Lasagne: 轻量级包装,位于 antao/TensorFlow 之上,支持更快的模型原型制作
  • Torch: 基于 Lua 的深度学习库,广泛支持机器学习算法
  • Caffe: 深度学习库主要用于图片处理

TensorFlow 最近在深度学习社区中获得了发展势头,因为它得到了谷歌的支持,并且使用 TensorBoard 具有良好的可视化功能:

基于 Keras 的深度神经网络分类器在手写数字中的应用

我们使用与之前使用 scikit-learn 训练模型时相同的数据,以便在 scikit-learn 和深度学习软件 Keras 之间进行苹果对苹果的比较。因此,数据加载步骤保持不变:

>>> import numpy as np
>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> from sklearn.datasets import load_digits
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.metrics import accuracy_score,classification_report

从这里开始,我们将使用 Keras 库模块。尽管选择了各种优化器;我们将在我们的模型中使用Adam。我们也鼓励读者尝试其他优化方法:

>>> from keras.models import Sequential
>>> from keras.layers.core import Dense, Dropout, Activation
>>> from keras.optimizers import Adadelta,Adam,RMSprop
>>> from keras.utils import np_utils

通过运行前面的代码,如果我们在一个中央处理器上运行 Keras,我们将获得以下消息:

但是,如果我们在 GPU 上运行它,则会出现以下代码。事实上,我的个人电脑上安装了一个 GPU(型号: NVIDIA GTX 1060 ),内存容量为 6 GB RAM。对于大多数应用程序来说,6 GB 对于初学者和爱好者来说已经足够了,而对于深度学习的研究人员来说,可能需要 12gb;当然取决于工作的性质:

In order to change the mode from CPU to GPU and vice versa from GPU to CPU, one needs to update the Theano.rc text file saved in the user folder. The following figure provides the various values to be configured if we are using GPU in the Theano.rc file. For CPU, only the [global] option is needed. Replace the device with CPU in it and delete the rest ([nvcc] and [lib]), as the latter is used for GPU settings only!

以下代码从 scikit-learn 数据集加载数字数据。快速检查数据形状的一段代码,因为数据本身嵌入在 numpy 数组中,因此我们不需要将其更改为任何其他格式,因为深度学习模型在 numpy 数组上接受训练:

>>> digits = load_digits() 
>>> X = digits.data 
>>> y = digits.target 
>>> print (X.shape) 
>>> print (y.shape) 
>>> print ("\nPrinting first digit") 
>>> plt.matshow(digits.images[0])  
>>> plt.show()

前面的代码以矩阵形式打印第一位数字。下面的数字看起来像一个 0 :

我们正在使用以下代码执行数据标准化,以贬低系列,然后使用标准差将所有 64 个维度放在一个相似的尺度中。虽然在这种情况下,它不是很严格,因为所有的值都在 0 到 255 之间,但是通过这样做,我们稍微降低了计算要求:

>>> x_vars_stdscle = StandardScaler().fit_transform(X) 

代码的以下部分基于 70-30 的分割将数据分割为训练和测试:

>>> x_train,x_test,y_train,y_test = train_test_split(x_vars_stdscle,y,train_size = 0.7,random_state=42) 

调整超参数在调整深度学习模型中起着至关重要的作用(当然,这也适用于任何机器学习模型!).我们用nb_classes作为 10,是因为位数的范围是 0-9;batch_size为 128,这意味着对于每个批次,我们利用 128 个观察值来更新权重;最后,我们使用nb_epochs作为 200,这意味着模型需要训练的纪元数量是 200(同样,我们可以想象模型从头到尾将被更新 200 次):

# Defining hyper parameters 
>>> np.random.seed(1337)  
>>> nb_classes = 10 
>>> batch_size = 128 
>>> nb_epochs = 200 

下面的代码实际上基于nb_classes值为多类值创建了 n 维向量。在这里,我们将使用 softmax 分类器对所有训练观察进行训练,得到的维数为 10:

>>> Y_train = np_utils.to_categorical(y_train, nb_classes) 

核心模型构建代码看起来像乐高积木,如下所示。在这里,我们将模型初始化为顺序的,而不是并行的,等等:

#Deep Layer Model building in Keras 
>>> model = Sequential() 

在第一层,我们使用 100 个输入形状为 64 列的神经元(因为 X 中的列数为 64),随后是 relu 激活函数,drop 值为 0.5(我们随机选择了 drop;鼓励读者尝试不同的价值观,看看结果如何变化):

>>> model.add(Dense(100,input_shape= (64,))) 
>>> model.add(Activation('relu')) 
>>> model.add(Dropout(0.5))

在第二层,我们使用了 50 个神经元(为了比较使用 scikit-learn 方法获得的结果,我们使用了类似的体系结构):

>>> model.add(Dense(50)) 
>>> model.add(Activation('relu')) 
>>> model.add(Dropout(0.5)) 

在输出层,类的数量需要与 softmax 分类器一起使用:

>>> model.add(Dense(nb_classes)) 
>>> model.add(Activation('softmax')) 

这里我们用categorical_crossentropy编译,因为输出是多类;然而,如果我们想使用二进制类,我们需要使用binary_crossentropy来代替:

>>> model.compile(loss='categorical_crossentropy', optimizer='adam')

该模型正在以下步骤中使用所有给定的批次大小和时期数量进行训练:

#Model training
>>> model.fit(x_train, Y_train, batch_size=batch_size, nb_epoch=nb_epochs,verbose=1)

在这里,我们只是用损失值来表示时代的开始和结束阶段。据我们观察,在 200 次迭代中,损失值从 2.6925 降到了 0.0611:

#Model Prediction
>>> y_train_predclass = model.predict_classes(x_train,batch_size=batch_size)
>>> y_test_predclass = model.predict_classes(x_test,batch_size=batch_size)
>>> print ("\n\nDeep Neural Network - Train accuracy:"),
(round(accuracy_score(y_train,y_train_predclass),3))
>>> print ("\nDeep Neural Network - Train Classification Report")
>>> print classification_report(y_train,y_train_predclass)
>>> print ("\nDeep Neural Network - Train Confusion Matrix\n")
>>> print (pd.crosstab(y_train,y_train_predclass,rownames = ["Actuall"],colnames = ["Predicted"]) )

根据之前的训练结果,我们对训练数据有 100%的准确率:

>>> print ("\nDeep Neural Network - Test accuracy:"),(round(accuracy_score(y_test, y_test_predclass),3))
>>> print ("\nDeep Neural Network - Test Classification Report")
>>> print (classification_report(y_test,y_test_predclass))
>>> print ("\nDeep Neural Network - Test Confusion Matrix\n")
>>> print (pd.crosstab(y_test,y_test_predclass,rownames = ["Actuall"],colnames = ["Predicted"]) )

然而,真正的评估是对测试数据进行的。在这里,我们获得了 97.6%的准确率,这与 sci kit-learn 97.78%的准确率相似。因此,已经证明我们已经成功地在深度学习软件中复制了结果;然而,在 Keras 中,我们可以做比 scikit-learn 中好得多的事情(例如卷积神经网络、递归神经网络、自动编码器等等,这些在本质上都非常先进)。

摘要

在这一章中,你已经学习了计算量最大的方法,支持向量机和神经网络。支持向量机在维数非常高的数据上表现非常好,而其他方法在这种情况下无法工作。利用核函数,支持向量机可以达到很高的测试精度;在本章中,我们已经详细介绍了内核实际上是如何工作的。近年来,神经网络已经非常流行,用于解决各种问题;在这里,我们涵盖了使用 scikit-learn 和 Keras 构建神经网络模型所需的所有深度学习基础知识。此外,对 scikit-learn 和 Keras 模型的结果进行了比较,以显示苹果对苹果的比较。利用深度学习,可以解决许多新一代人工智能问题,无论是文本、语音、图像、视频等等。事实上,深度学习本身已经完全成为一个独立的领域。

在下一章中,我们将研究使用基于内容和协作过滤方法的推荐引擎,这是第一个向任何希望理解机器学习的新手解释的经典机器学习示例。

七、推荐引擎

推荐引擎 ( REs )最著名的用途是向任何想了解机器学习领域的未知人士或新手解释机器学习是什么。一个经典的例子可能是亚马逊如何推荐类似于你已经购买的书籍,你可能也非常喜欢!此外,从经验来看,推荐引擎似乎是大规模机器学习的一个例子,每个人都理解,或者可能已经理解。但是,尽管如此,推荐引擎正在各地使用。例如,你可能认识的人会在脸书或领英上出现,它会通过显示你最有可能喜欢交朋友的人或你可能感兴趣联系的专业人士来推荐你。当然,这些功能推动了他们的业务发展,并且是公司的核心驱动力。

RE 背后的想法是预测人们可能喜欢什么,并揭示项目/产品之间的关系,以帮助发现过程;这样,它就类似于一个搜索引擎。但一个主要的区别是,搜索引擎的工作方式是被动的;它们只在用户请求某样东西时才显示结果——但推荐引擎是主动的——它试图向人们呈现他们不一定搜索过或他们过去可能没有听说过的相关内容。

基于内容的过滤

基于内容的方法试图使用项目的内容或属性,以及两个内容片段之间相似性的一些概念,来生成关于给定项目的相似项目。在这种情况下,余弦相似度用于确定提供推荐的最近用户或项目。

例如:如果你买了一本书,那么你很有可能会买那些经常和其他顾客一起去的相关书籍,以此类推。

余弦相似性

因为我们将致力于这个概念,所以重申一下基础知识是很好的。余弦相似性是内积空间的两个非零向量之间的相似性度量,用于度量它们之间角度的余弦。00T3 的余弦为 1 ,对于任何其他角度都小于 1 :

这里AIT3BIT7】分别是向量 AB 的分量:**

例如:我们假设 A = [2,1,0,2,0,1,1]B = [2,1,1,1,1,0,1,1] 是两个向量,我们想要计算余弦相似度:


0.823 的值表示两个向量之间非常高的相似度,因为最高可能是 1 。在计算相似项目或用户时,我们将对他们的评分向量应用余弦相似度,并根据余弦相似度按降序排列,这将根据相似度得分对所有其他项目进行排序,接近我们正在比较的向量。我们将在本章稍后部分讨论的示例中详细了解这一点。

协同过滤

协同过滤是一种群策群力的方法,其中许多用户对项目的偏好集合被用来生成用户对他们尚未评价/审阅的项目的估计偏好。它基于相似性的概念。协同过滤是一种方法,在这种方法中,相似的用户及其评分不是由相似的年龄等决定的,而是由用户表现出的相似偏好决定的,例如观看的电影相似、评分相似等。

协同过滤相对于基于内容的过滤的优势

与基于内容的过滤相比,协作过滤具有许多优势。其中一些如下:

  • 不要求理解项目内容:项目的内容不一定能讲出事情的全貌,比如电影类型/流派等等。
  • 无物品冷启动问题:即使没有某个物品的信息,我们仍然可以预测物品等级,而无需等待用户购买。
  • 捕捉用户兴趣随时间的变化:只关注内容并不能为用户的视角和偏好提供任何灵活性。
  • 捕捉固有的细微特征:对于潜在因素模型来说,这是非常真实的。如果大多数用户购买了两个不相关的项目,那么另一个与其他用户有相似兴趣的用户很可能会购买那个不相关的项目。

基于交替最小二乘算法的协同过滤矩阵分解

交替最小二乘 ( ALS )是一种求解矩阵分解问题的优化技术。这种技术实现了良好的性能,并且被证明相对容易实现。这些算法是一大类潜在因素模型的成员,它们试图通过相对较少的未观察到的潜在原因/因素来解释大量用户和项目/电影之间观察到的交互。矩阵分解算法将用户项目数据(矩阵维数 m x n )视为稀疏矩阵,并尝试用两个低维密集矩阵( XY 进行重构,其中 X 具有维数 m x kY 具有维数 k x n 【T23

潜在因素可以解释为一些解释变量,试图解释用户行为背后的原因,因为协同过滤的目的是试图基于用户的行为进行预测,而不是电影或用户的属性,等等:

通过将 XY 矩阵相乘,我们试图重建原始矩阵 A ,通过减少原始可用等级之间的均方根误差从稀疏矩阵 A ( m x n 维度)和通过将X(mXk维度)和Y(mXk维度)但是 XY 相乘得到的矩阵填充了 m x n 尺寸中的所有槽,但是我们将减少 A 中仅有的可用额定值之间的误差。通过这样做,空白处的所有其他值将产生合理合理的评级。

但是,在这种情况下,未知值太多。未知值只不过是需要填入 XY 矩阵中的值,这样可以尽量接近矩阵 A 中的原始评分。为了解决这个问题,最初,从均匀分布在 01 之间生成随机值,并乘以 5 ,为两个 XY 矩阵生成 05 之间的值。下一步,实际应用 ALS 方法;迭代应用以下两个步骤,直到达到阈值迭代次数:

  1. X 值利用 Y 值、学习率( λ )和原始稀疏矩阵( A )进行更新
  2. Y 值利用 X 值、学习率( λ )和原始稀疏矩阵( A )进行更新

学习率( λ )用于控制收敛速度;较高的值会导致值的快速变化(类似于优化问题),有时会导致超出最佳值。类似地,低值需要多次迭代来收敛解。

这里提供了 ALS 的数学表示:

目标是最小化两者之间的误差或平方差。因此,它被称为最小二乘法。在简单的机器学习术语中,我们可以称之为回归问题,因为实际和预测之间的误差正在被最小化。实际上,这个方程从来没有通过计算逆来求解。然而,通过从 X 计算 Y 并从 Y 再次计算 X 已经实现了等价,并且这样,它将继续,直到所有迭代都达到,并且这是交替部分实际上出现的地方。开始时 Y 是人工生成的, X 将基于 Y 进行优化,后期通过基于 X 优化Y;通过这样做,最终解决方案开始在迭代次数上收敛到最优。接下来提供了基本的 Python 语法;我们将在下面的例子中使用相同的例子,以说明电影镜头的例子:

推荐引擎模型的评估

需要计算对任何模型的评估,以确定该模型相对于实际数据有多好,从而可以通过调整超参数等来提高其性能。事实上,整个机器学习算法的准确性是根据其问题类型来衡量的。在分类问题中,需要计算混淆矩阵,而在回归问题中,需要计算均方误差或调整后的 R 平方值。

均方误差是原始稀疏用户项矩阵(也称为 A )与两个低维稠密矩阵( XY )重构误差的直接度量。这也是在迭代中最小化的目标函数:

均方根误差提供了与变量测度的原始维数相等的维数,因此我们可以分析误差分量与原始值的大小。在我们的示例中,我们计算了电影镜头数据的均方根误差 ( RMSE )。

基于网格搜索的推荐引擎超参数选择

在任何机器学习算法中,超参数的选择在模型如何概括底层数据方面起着至关重要的作用。同样,在推荐引擎中,我们可以使用以下超参数:

  • 迭代次数:迭代次数越高,算法收敛越好。实践证明,ALS 在 10 次迭代内收敛,但建议读者尝试各种数值,看看算法是如何工作的。
  • 潜在因素数量:这些是试图提供人群行为模式背后原因的解释变量。潜在因素越高,模型越好,但是过高的值可能无法提供显著的提升。
  • 学习率:学习率是一个可调旋钮,用来改变算法的收敛速度。过高的值可能会因高振荡而突然出现而不是收敛,过低的值可能会让算法采取过多的步骤来收敛。

鼓励读者尝试各种组合,看看准确度值和推荐结果如何变化。在后面的部分中,我们尝试了各种值来提供说明。

电影镜头数据的推荐引擎应用

著名的电影镜头数据已从教育发展推荐部分下的链接https://grouplens.org/datasets/movielens/使用,文件名显示为 ml-latest-small.zip ,其中所有需要的文件均以.csv格式保存(ratings.csvmovies.csvlinks.csvtags.csv)。为了简单起见,我们在以下示例中使用的文件只是分级和电影。尽管如此,我们鼓励读者合并其他文件,以进一步提高准确性!

>>> import os 
""" First change the following directory link to where all input files do exist """ 
>>> os.chdir("D:\\Book writing\\Codes\\Chapter 7\\ml-latest-small\\ml-latest-small") 

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

在下面的代码中,收视率数据提供了用户 ID、电影 ID 和收视率值的详细信息,这意味着每个唯一的用户,他/她已经给了多少部电影,收视率是多少!

>>> ratings = pd.read_csv("ratings.csv") 
>>> print (ratings.head()) 

在电影数据中,为每部电影存储了具有唯一电影标识、电影标题及其流派的详细信息。我们在这一章没有使用体裁;但是,您可以尝试通过将文本拆分并转换为一个热编码向量(将类别映射到数字空间)来将流派添加到数据中,以提高模型的准确性:

>>> movies = pd.read_csv("movies.csv") 
>>> print (movies.head()) 

在下面的代码中,我们将分级和电影数据结合在一起,以便可以轻松检索标题进行显示:

#Combining movie ratings & movie names 
>>> ratings = pd.merge(ratings[['userId','movieId','rating']], movies[['movieId', 'title']],how='left',left_on ='movieId' ,right_on = 'movieId')

下面的代码将数据转换为矩阵形式,其中行是唯一的用户标识,列是唯一的电影标识,矩阵中的值是用户提供的评分。这个矩阵本质上主要是稀疏的,因此我们将 NAN 值替换为 0 来进行计算。代码后面部分的全部计算都基于这个矩阵:

>>> rp = ratings.pivot_table(columns = ['movieId'],index = ['userId'],values = 'rating') 
>>> rp = rp.fillna(0) 

熊猫数据帧建立在 NumPy 数组之上,因此建议使用 NumPy 数组代替熊猫数据帧;在计算用户-用户相似性矩阵或项目-项目相似性矩阵时,像这样的小转换节省了巨大的计算开销。

# Converting pandas DataFrame to NumPy for faster execution in loops etc. 
>>> rp_mat = rp.as_matrix() 

The main reason behind the improved computational performance of NumPy array compared with pandas is due to the homogeneity of elements in NumPy array. At the same time, this feature does not allow NumPy arrays to carry heterogeneous elements (for example, character, numeric, float, and so on.). Also, if someone is writing for loops on NumPy arrays means, they might be doing something wrong, as NumPy is built for manipulating all the elements in a shot, rather than hovering around each element.

样本余弦相似性由以下伪值代码说明。但是,基于内容的过滤方法保持不变:

>>> from scipy.spatial.distance import cosine 
#The cosine of the angle between them is about 0.822\. 
>>> a= np.asarray( [2, 1, 0, 2, 0, 1, 1, 1]) 
>>> b = np.asarray( [2, 1, 1, 1, 1, 0, 1, 1]) 
>>> print (1-cosine(a,b)) 

在接下来的部分中,我们已经介绍了以下小节:

  • 用户-用户相似性矩阵
  • 电影-电影(项目-项目)相似性矩阵
  • 使用 ALS 的协同过滤
  • 协同过滤中的网格搜索

用户-用户相似性矩阵

下面的代码说明了基于完全蛮力计算的用户-用户相似性矩阵计算(在时间复杂度为的另一个 for 循环中使用一个 for 循环在 2 上)。有许多其他有效的方法来计算相同的内容,但是为了便于读者理解,这里我们提供了一个尽可能简单的方法:

>>> m, n = rp.shape 
# User similarity matrix 
>>> mat_users = np.zeros((m, m)) 

>>> for i in range(m): 
...     for j in range(m): 
...         if i != j: 
...             mat_users[i][j] = (1- cosine(rp_mat[i,:], rp_mat[j,:])) 
...         else: 
...             mat_users[i][j] = 0\. 

>>> pd_users = pd.DataFrame(mat_users,index =rp.index ,columns= rp.index ) 

以下自定义函数将任意用户 ID 和要显示的相似用户数作为输入,并根据相似用户的相关余弦相似度分数返回相似用户:

# Finding similar users 
>>> def topn_simusers(uid = 16,n=5): 
...     users = pd_users.loc[uid,:].sort_values(ascending = False) 
...     topn_users = users.iloc[:n,] 
...     topn_users = topn_users.rename('score')     
...     print ("Similar users as user:",uid) 
...     return pd.DataFrame(topn_users) 

>>> print (topn_simusers(uid=17,n=10)) 

我们的任务不是仅仅通过观察相似的用户本身来完成的;相反,我们也想看看任何特定用户评价最高的电影是什么。以下功能为任何给定用户及其最喜欢的电影提供该信息:

# Finding most rated movies of a user 
>>> def topn_movieratings(uid = 355,n_ratings=10):     
...     uid_ratings = ratings.loc[ratings['userId']==uid] 
...     uid_ratings = uid_ratings.sort_values(by='rating',ascending = [False]) 
...     print ("Top",n_ratings ,"movie ratings of user:",uid) 
...     return uid_ratings.iloc[:n_ratings,]     

>>> print (topn_movieratings(uid=596,n_ratings=10)) 

下面的截图显示了用户596评分最高的电影及其标题,这样我们就可以知道该用户对哪些电影的评分最高:

电影-电影相似矩阵

代码的前几节讨论了基于内容的用户-用户相似性,而在下一节中,我们将讨论一个纯粹的电影-电影相似性关系矩阵,这样我们将更深入地挖掘每部电影与其他电影的接近程度。

在下面的代码中,时间函数被用于计算电影-电影相似性矩阵。在我的 i7 电脑上花了整整 30 分钟。它可能需要更多的时间在中等计算机上,因此我已经存储了输出结果,并为方便起见进行了回读;我们鼓励读者自己运行并检查:

# Movie similarity matrix 
>>> import time 
>>> start_time = time.time() 
>>> mat_movies = np.zeros((n, n)) 

>>> for i in range(n): 
...     for j in range(n): 
...         if i!=j: 
...             mat_movies[i,j] = (1- cosine(rp_mat[:,i], rp_mat[:,j])) 
...         else: 
...             mat_movies[i,j] = 0\. 
>>> print("--- %s seconds ---" % (time.time() - start_time)) 

>>> pd_movies = pd.DataFrame(mat_movies,index =rp.columns ,columns= rp.columns )

以下两行代码是可选的;我更喜欢从磁盘上读回来,而不是重新运行代码并等待 30 分钟:

>>> pd_movies.to_csv('pd_movies.csv',sep=',') 
>>> pd_movies = pd.read_csv("pd_movies.csv",index_col='movieId') 

Readers are encouraged to apply scipy.spatial.distance.cdist function with cosine, as the parameter can speed up the runtime.

以下代码用于根据用户的偏好等级检索最相似的顶级电影n数量。这种分析对于了解其他哪些电影与你实际喜欢的电影相似非常重要:

# Finding similar movies 
>>> def topn_simovies(mid = 588,n=15): 
...     mid_ratings = pd_movies.loc[mid,:].sort_values(ascending = False) 
...     topn_movies = pd.DataFrame(mid_ratings.iloc[:n,]) 
...     topn_movies['index1'] = topn_movies.index 
...     topn_movies['index1'] = topn_movies['index1'].astype('int64') 
...     topn_movies = pd.merge(topn_movies,movies[['movieId','title']],how = 'left', left_on ='index1' ,right_on = 'movieId') 
...     print ("Movies similar to movie id:",mid,",",movies['title'][movies['movieId'] == mid].to_string(index=False),",are") 
...     del topn_movies['index1'] 
...     return topn_movies 

>>> print (topn_simovies(mid=589,n=15)) 

仔细查看以下结果,与Terminator 2相似的电影有Jurassic ParkTerminator, TheBraveheartForrest GumpSpeed等;所有这些电影实际上都属于动作类。结果似乎听起来足够让我从这个分析中选择我的下一部电影来看!基于内容的过滤似乎奏效了!

使用 ALS 的协同过滤

我们已经完成了基于内容的过滤,从下一节开始,我们将讨论使用 ALS 方法的协作过滤:

# Collaborative filtering 
>>> import os 
""" First change the following directory link to where all input files do exist """ 
>>> os.chdir("D:\\Book writing\\Codes\\Chapter 7\\ml-latest-small\\ml-latest-small") 

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

>>> ratings = pd.read_csv("ratings.csv") 
>>> print (ratings.head()) 

>>> movies = pd.read_csv("movies.csv") 
>>> print (movies.head()) 

>>> rp = ratings.pivot_table(columns = ['movieId'],index = ['userId'],values = 'rating') 
>>> rp = rp.fillna(0) 

>>> A = rp.values 
>>> print ("\nShape of Original Sparse Matrix",A.shape) 

与基于内容的过滤相比,初始数据处理步骤保持不变。这里,我们将使用的主要文件是稀疏分级矩阵。

以下W矩阵实际上与原始评分矩阵(矩阵A)具有相同的维度,但是每当用户对任何电影提供评分时,其值仅为01(任何电影的最低有效评分为 0.5,最高评分为 5);我们需要这种类型的矩阵来计算误差等等(我们将在后面的代码部分中看到它的应用),因为这种方式更便于最小化误差:

>>> W = A>0.5 
>>> W[W==True]=1 
>>> W[W==False]=0 
>>> W = W.astype(np.float64,copy=False) 

同样,还需要另一个矩阵W_pred来提供建议。W_pred矩阵的值为01,与W矩阵完全相反。这样做的原因是,如果我们将预测的评分矩阵乘以这个W_pred矩阵,这将使已经提供的评分的所有值0,以便其他未审核/未评分的值可以很容易地按降序排列,并向从未评分/看过这些电影的用户建议前 5 或前 10 部电影。如果您仔细观察,这里我们也为所有对角线元素分配了零,因为我们不应该向用户推荐与最有可能的电影相同的电影,这是明智的:

>>> W_pred = A<0.5 
>>> W_pred[W_pred==True]=1 
>>> W_pred[W_pred==False]=0 
>>> W_pred = W_pred.astype(np.float64,copy=False) 
>>> np.fill_diagonal(W_pred,val=0) 

超参数在以下代码中用样本值初始化,迭代次数设置为200,潜在因子数量设置为100,学习率为0.1:

# Parameters 
>>> m,n = A.shape 
>>> n_iterations = 200 
>>> n_factors = 100 
>>> lmbda = 0.1  

XY值从均匀分布【0-1】的随机数开始,乘以5,在 0 和 5 之间转换。XY的维数分别为( m x k )和( k x n ),因为我们将从一个随机值开始,并针对每次迭代逐步优化:

>>> X = 5 * np.random.rand(m,n_factors) 
>>> Y = 5* np.random.rand(n_factors,n) 

RMSE 值通过以下公式计算。这里,我们与W矩阵相乘,以便在误差计算中仅考虑评级指标;虽然矩阵np.dot(X, Y)在整个矩阵中都有值,但是我们不应该考虑它们,因为误差度量只需要为可用的等级计算:

>>> def get_error(A, X, Y, W):
... return np.sqrt(np.sum((W * (A - np.dot(X, Y)))**2)/np.sum(W))

以下步骤是整个 ALS 方法中最关键的部分。最初,这里我们是基于给定的Y优化X,然后是基于给定的X优化Y;我们将重复这个过程,直到完成所有迭代次数。每 10 次迭代后,我们打印以查看 RMSE 值如何随着迭代次数的变化而变化:

>>> errors = [] 
>>> for itr in range(n_iterations): 
...     X = np.linalg.solve(np.dot(Y,Y.T)+ lmbda * np.eye(n_factors),np.dot(Y,A.T)).T 
...     Y = np.linalg.solve(np.dot(X.T,X)+ lmbda * np.eye(n_factors),np.dot(X.T,A)) 
...     if itr%10 == 0: 
...         print(itr," iterations completed","RMSError value is:",get_error(A,X,Y,W)) 
...     errors.append(get_error(A,X,Y,W)) 

从前面的结果来看,很明显,误差值实际上随着迭代次数的增加而减少,这实际上是算法按预期执行。以下代码用于在图表上绘制相同的错误:

>>> print ("RMSError of rated movies: ",get_error(A,X,Y,W)) 
>>> plt.plot(errors); 
>>> plt.ylim([0, 3.5]); 
>>> plt.xlabel("Number of Iterations");plt.ylabel("RMSE") 
>>> plt.title("No.of Iterations vs. RMSE") 
>>> plt.show()

一旦迭代次数结束,我们将获得更新的 XY 矩阵,这些矩阵将用于创建整个预测评级矩阵,该矩阵可以从简单的点积获得,如下所示:

>>> A_hat = np.dot(X,Y) 

在计算出预测矩阵(A_hat)后,下一个也是最后一个任务就是利用它向用户推荐最相关的电影。在下面的代码中,我们根据任何特定用户提供的电影评论模式或评分向他们推荐电影:

>>> def print_recommovies(uid=315,n_movies=15,pred_mat = A_hat,wpred_mat = W_pred ): 
...     pred_recos = pred_mat*wpred_mat 
...     pd_predrecos = pd.DataFrame(pred_recos,index =rp.index ,columns= rp.columns ) 
...     pred_ratings = pd_predrecos.loc[uid,:].sort_values(ascending = False) 
...     pred_topratings = pred_ratings[:n_movies,] 
...     pred_topratings = pred_topratings.rename('pred_ratings')   
...     pred_topratings = pd.DataFrame(pred_topratings) 
...     pred_topratings['index1'] = pred_topratings.index 
...     pred_topratings['index1'] = pred_topratings['index1'].astype('int64') 
...     pred_topratings = pd.merge(pred_topratings,movies[['movieId','title']],how = 'left',left_on ='index1' ,right_on = 'movieId') 
...     del pred_topratings['index1']     
...     print ("\nTop",n_movies,"movies predicted for the user:",uid," based on collaborative filtering\n") 
...     return pred_topratings 

>>> predmtrx = print_recommovies(uid=355,n_movies=10,pred_mat=A_hat,wpred_mat=W_pred) 
>>> print (predmtrx)

从前面的推荐可以看出,电影用户 355 最可能喜欢的是Goodfellas,其次是Princess BrideThere's Something About Mary等等。嗯,这些推荐需要用户自己判断!

协同过滤中的网格搜索

正如我们前面提到的,我们需要调整参数,以便了解我们将在哪里获得最佳的机器学习模型。在任何机器学习模型中,调整参数都是一种事实上的标准。在下面的代码中,我们尝试了迭代次数、潜在因素和学习率的各种组合。整个代码或多或少会保持不变,但我们总是保持一个标签,记录我们见过的最少的错误;如果出现的任何新错误少于现有错误,我们会相应地打印组合:

# Grid Search on Collaborative Filtering 
>>> niters = [20,50,100,200] 
>>> factors = [30,50,70,100] 
>>> lambdas = [0.001,0.01,0.05,0.1] 

>>> init_error = float("inf") 

>>> print("\n\nGrid Search results of ALS Matrix Factorization:\n") 
>>> for niter in niters: 
...     for facts in factors: 
...         for lmbd in lambdas: 

...             X = 5 * np.random.rand(m,facts) 
...             Y = 5* np.random.rand(facts,n) 

...             for itr in range(niter): 
...                 X = np.linalg.solve(np.dot(Y,Y.T)+ lmbd * np.eye(facts), np.dot(Y,A.T)).T 
...                 Y = np.linalg.solve(np.dot(X.T,X)+ lmbd * np.eye(facts), np.dot(X.T,A)) 

...             error = get_error(A,X,Y,W) 

...             if error<init_error: 
...                 print ("No.of iters",niter,"No.of Factors",facts,"Lambda",lmbd, "RMSE",error) 
...                 init_error = error 

从网格搜索中获得的最佳可能 RMSE 值是1.695345,它小于从基本方法中获得的 RMSE 值,即1.6961。因此,在实现任何算法之前执行网格搜索总是明智的。

在 R 代码中,recommenderlab包已经被用来解决协同过滤问题,因为这个包有很多特性和功能可以玩。但是基本的基于内容的过滤算法是建立在第一原则之上的:

The following R code may take about 30 minutes to run (of course runtime depends on the system configuration though!).

推荐引擎(基于内容和协作过滤)的代码如下:

setwd("D:\\Book writing\\Codes\\Chapter 7\\ml-latest-small\\ml-latest-small")
ratings = read.csv("ratings.csv") 
movies = read.csv("movies.csv") 

ratings = ratings[,!names(ratings) %in% c("timestamp")]

library(reshape2) 

# Creating Pivot table 
ratings_mat = acast(ratings,userId~movieId)
ratings_mat[is.na(ratings_mat)] =0 

# Content-based filtering 
library(lsa)
a = c(2, 1, 0, 2, 0, 1, 1, 1) 
b = c(2, 1, 1, 1, 1, 0, 1, 1) 
print (paste("Cosine similarity between A and B is",round(cosine(a,b), 4))) 

m = nrow(ratings_mat);n = ncol(ratings_mat) 

# User similarity 
matrix mat_users = matrix(nrow = m, ncol = m) 
for (i in 1:m){ 
 for (j in 1:m){ 
 if (i != j){ 
 mat_users[i,j] = cosine(ratings_mat[i,],ratings_mat[j,])
 } else { 
 mat_users[i,j] = 0.0 
 } 
 }
} 
colnames(mat_users) = rownames(ratings_mat); 
rownames(mat_users) = rownames(ratings_mat) 
df_users = as.data.frame(mat_users) 

# Finding similar users 
topn_simusers <- function(uid=16,n=5){ 
 sorted_df = sort(df_users[uid,],decreasing = TRUE)[1:n]
 print(paste("Similar users as user:",uid)) 
 return(sorted_df) 
} 
print(topn_simusers(uid = 17,n=10)) 

# Finding most rated movies of a user 
library(sqldf) 

ratings_withmovie = sqldf(" select a.*,b.title from ratings as a left join movies as b on a.movieId = b.movieId") 

# Finding most rated movies of a user 
topn_movieratings <- function(uid=355,n_ratings=10){ 
 uid_ratings = ratings_withmovie[ratings_withmovie$userId==uid,]
 sorted_uidrtng = uid_ratings[order(-uid_ratings$rating),]
 return(head(sorted_uidrtng,n_ratings)) 
} 
print( topn_movieratings(uid = 596,n=10)) 

# Movies similarity matrix 
mat_movies = matrix(nrow = n, ncol = n) 
for (i in 1:n){ 
 for (j in 1:n){ 
 if (i != j){ 
 mat_movies[i,j] = cosine(ratings_mat[,i],ratings_mat[,j]) 
 } else { 
 mat_movies[i,j] = 0.0 
 } 
 } 
} 
colnames(mat_movies) = colnames(ratings_mat); 
rownames(mat_movies) = colnames(ratings_mat) 
df_movies = as.data.frame(mat_movies)

write.csv(df_movies,"df_movies.csv") 

df_movies = read.csv("df_movies.csv") 
rownames(df_movies) = df_movies$X 
colnames(df_movies) = c("aaa",df_movies$X) 
df_movies = subset(df_movies, select=-c(aaa)) 

# Finding similar movies 
topn_simovies <- function(mid=588,n_movies=5){ 
 sorted_df = sort(df_movies[mid,],decreasing = TRUE)[1:n_movies]
 sorted_df_t = as.data.frame(t(sorted_df)) 
 colnames(sorted_df_t) = c("score") 
 sorted_df_t$movieId = rownames(sorted_df_t)

 print(paste("Similar",n_movies, "movies as compared to the movie",mid,"are :")) 
 sorted_df_t_wmovie = sqldf(" select a.*,b.title from sorted_df_t as a left join movies as b on a.movieId = b.movieId")
 return(sorted_df_t_wmovie) 
} 
print(topn_simovies(mid = 589,n_movies=15)) 

# Collaborative filtering 
ratings = read.csv("ratings.csv") 
movies = read.csv("movies.csv") 

library(sqldf) 
library(reshape2) 
library(recommenderlab) 

ratings_v2 = ratings[,-c(4)]
ratings_mat = acast(ratings_v2,userId~movieId) 
ratings_mat2 = as(ratings_mat, "realRatingMatrix")

getRatingMatrix(ratings_mat2)

#Plotting user-item complete matrix 
image(ratings_mat2, main = "Raw Ratings")

# Fitting ALS method on Data
rec=Recommender(ratings_mat2[1:nrow(ratings_mat2)],method="UBCF", param=list(normalize = "Z-score",method="Cosine",nn=5, minRating=1))
rec_2=Recommender(ratings_mat2[1:nrow(ratings_mat2)],method="POPULAR")

print(rec) 
print(rec_2) 

names(getModel(rec)) 
getModel(rec)$nn 

# Create predictions for all the users 
recom_pred = predict(rec,ratings_mat2[1:nrow(ratings_mat2)], type="ratings") 

# Putting predictions into list 
rec_list<-as(recom_pred,"list") 
head(summary(rec_list)) 

print_recommendations <- function(uid=586,top_nmovies=10){ 
 recoms_list = rec_list[[uid]] 
 sorted_df = as.data.frame(sort(recoms_list,decreasing = TRUE)[1:top_nmovies]) 
 colnames(sorted_df) = c("score") 
 sorted_df$movieId = rownames(sorted_df) 
 print(paste("Movies recommended for the user",uid,"are follows:"))
 sorted_df_t_wmovie = sqldf(" select a.*,b.title from sorted_df as a left join movies as b on a.movieId = b.movieId")
 return(sorted_df_t_wmovie) 
} 
print(print_recommendations(uid = 580,top_nmovies = 15))

摘要

在本章中,您已经了解了向用户推荐电影的基于内容和协作过滤技术,或者通过使用余弦相似度考虑其他用户,或者通过考虑电影评分进行矩阵分解计算。在计算上,基于内容的过滤计算速度更快,但只考虑一个维度,要么是其他用户,要么是其他类似的电影。而在协同过滤中,推荐是通过同时考虑用户和电影维度来提供的。所有的 Python 实现都是从第一原则开始的,因为我们没有一个足够好的包来实现它,而且了解基础知识也很好。在 R 编程中,我们使用recommenderlab包应用协同过滤。最后,展示了一个关于如何在推荐引擎中调整超参数的网格搜索示例。

在下一章中,我们将介绍无监督学习的细节,更准确地说,是聚类和主成分分析模型。

八、无监督学习

无监督学习的目标是发现数据中不存在目标变量的隐藏模式或结构,以执行分类或回归方法。无监督学习方法通常更具挑战性,因为结果是主观的,并且没有简单的分析目标,例如预测类别或连续变量。这些方法是探索性数据分析的一部分。除此之外,很难评估从无监督学习方法中获得的结果,因为没有普遍接受的机制来验证结果。

尽管如此,无监督学习方法作为当今的一个热门话题,在各个领域都变得越来越重要,许多研究人员目前正在积极研究它们,以探索这一新的领域。一些好的应用是:

  • 基因组学:无监督学习应用于理解来自 DNA 的基因组范围的生物学见解,以更好地理解疾病和人类。这些类型的任务本质上更具探索性。
  • 搜索引擎:搜索引擎可能会根据其他相似用户的点击历史来选择向特定个人显示哪些搜索结果。
  • 知识提取:从原始文本中提取概念的分类,生成知识图,创建自然语言处理领域的语义结构。
  • 客户细分:在银行业中,像聚类这样的无监督学习被应用于对相似客户进行分组,营销部门基于这些细分设计他们的联系策略。比如,年龄较大的低风险客户会以定期存款产品为目标,风险较高的年轻客户会以信用卡或共同基金为目标,等等。
  • 社交网络分析:识别社交网络中联系更紧密、共同特征相似的人群的内聚群体。

在本章中,我们将介绍使用公开可用的数据执行无监督学习的以下技术:

  • k-均值聚类
  • 主成分分析
  • 奇异值分解
  • 深度自动编码器

k-均值聚类

聚类是对观测值进行分组的任务,其方式是同一聚类的成员彼此更相似,而不同聚类的成员彼此差异很大。

聚类通常用于探索数据集,以识别其中的底层模式或创建一组特征。在社交网络的情况下,它们可以聚集在一起,以识别社区并暗示人与人之间缺失的联系。这里有几个例子:

  • 在反洗钱措施中,可以使用异常检测来识别可疑活动和个人
  • 在生物学中,聚类被用来寻找具有相似表达模式的基因组
  • 在营销分析中,聚类被用来寻找相似客户的细分,以便不同的营销策略可以相应地应用于不同的客户细分

k-means 聚类算法是一个迭代过程,将聚类或质心的中心移动到其组成点的平均位置,并迭代地将实例重新分配到它们最接近的聚类,直到可能的聚类中心数量或达到的迭代数量没有显著变化。

k 均值的成本函数由属于该聚类的观测值与其各自质心值之间的欧几里德距离(平方范数)决定。理解这个方程的一个直观方法是,如果只有一个聚类( k=1 ,那么所有观测值之间的距离与其单个平均值进行比较。然而,如果集群的数量增加到 2 ( k= 2 ),则计算两个平均值,并将一些观测值分配给集群 1 ,并且基于接近度将其他观测值分配给集群二 - 。随后,在成本函数中通过应用相同的距离度量来计算距离,但是对它们的聚类中心分别进行计算:

k-表示来自第一性原理的工作方法

k-means 工作方法在以下示例中进行了说明,其中考虑了 12 个实例的 XY 值。任务是从数据中确定最佳聚类。

| 实例 | X | Y |
| one | seven | eight |
| Two | Two | four |
| three | six | four |
| four | three | Two |
| five | six | five |
| six | five | seven |
| seven | three | three |
| eight | one | four |
| nine | five | four |
| Ten | seven | seven |
| Eleven | seven | six |
| Twelve | Two | one |

在 2D 图上绘制数据点后,我们可以看到大致有两个聚类是可能的,其中左下是第一个聚类,右上是另一个聚类,但是在许多实际情况下,变量(或维度)太多,我们无法简单地将它们可视化。因此,我们需要一种数学和算法的方法来解决这些类型的问题。

迭代 1:让我们假设所有 12 实例中的两个实例有两个中心。在这里,我们选择了实例 1 ( X = 7,Y = 8 )和实例 8 ( X = 1,Y = 4 ),因为它们似乎处于两个极端。对于每个实例,我们将计算它相对于两个质心的欧几里德距离,并将其分配给最近的聚类中心。

| 实例 | X | Y | 质心 1 距离 | 质心 2 距离 | 分配的集群 |
| one | seven | eight | Seven point two one | Zero | C2 |
| Two | Two | four | One | Six point four | C1 |
| three | six | four | Five | Four point one two | C2 |
| four | three | Two | Two point eight three | Seven point two one | C1 |
| five | six | five | Five point one | Three point one six | C2 |
| six | five | seven | Five | Two point two four | C2 |
| seven | three | three | Two point two four | Six point four | C1 |
| eight | one | four | Zero | Seven point two one | C1 |
| nine | five | four | Four | Four point four seven | C1 |
| Ten | seven | seven | Six point seven one | One | C2 |
| Eleven | seven | six | Six point three two | Two | C2 |
| Twelve | Two | one | Three point one six | Eight point six | C1 |
| 质心 1 | one | four | | | |
| 质心 2 | seven | eight | | | |

两点 A (X1,Y1)B (X2,Y2) 之间的欧氏距离如下所示:

质心距离计算通过采用欧几里德距离来执行。示例计算如下所示。例如,六个相对于两个质心(质心 1 和质心 2)。

下表描述了实例到两个质心的分配,如前表格式所示:

如果我们仔细观察前面的图表,我们会发现除了实例 9 (X =5,Y = 4) 之外,所有的实例似乎都被适当地分配了。但是,在后期,应该适当分配。让我们在下面的步骤中看看作业是如何发展的。

迭代 2:在这个迭代中,新的质心是从该簇或质心的指定实例中计算出来的。基于指定点的简单平均值计算新质心。

| 实例 | X | Y | 分配的集群 |
| one | seven | eight | C2 |
| Two | Two | four | C1 |
| three | six | four | C2 |
| four | three | Two | C1 |
| five | six | five | C2 |
| six | five | seven | C2 |
| seven | three | three | C1 |
| eight | one | four | C1 |
| nine | five | four | C1 |
| Ten | seven | seven | C2 |
| Eleven | seven | six | C2 |
| Twelve | Two | one | C1 |
| 质心 1 | Two point six seven | three | |
| 质心 2 | Six point three three | Six point one seven | |

质心 1 和 2 的示例计算如下所示。类似的方法也将应用于所有后续迭代:

更新质心后,我们需要将实例重新分配到最近的质心,这将在迭代 3 中执行。

迭代 3:在此迭代中,基于实例和新质心之间的欧氏距离计算新的赋值。在任何变化的情况下,新的质心将被迭代计算,直到分配中没有变化是可能的或者达到迭代次数。下表描述了新质心和所有实例之间的距离度量:

| 实例 | X | Y | 质心 1 距离 | 质心 2 距离 | 之前分配的集群 | 新分配的集群 | 变了? |
| one | seven | eight | Six point six one | One point nine five | C2 | C2 | 不 |
| Two | Two | four | One point two | Four point eight four | C1 | C1 | 不 |
| three | six | four | Three point four eight | Two point one nine | C2 | C2 | 不 |
| four | three | Two | One point zero five | Five point three four | C1 | C1 | 不 |
| five | six | five | Three point eight eight | One point two two | C2 | C2 | 不 |
| six | five | seven | Four point six three | One point five seven | C2 | C2 | 不 |
| seven | three | three | Zero point three three | Four point six | C1 | C1 | 不 |
| eight | one | four | One point nine five | Five point seven five | C1 | C1 | 不 |
| nine | five | four | Two point five four | Two point five five | C1 | C1 | 不 |
| Ten | seven | seven | Five point eight nine | One point zero seven | C2 | C2 | 不 |
| Eleven | seven | six | Five point two seven | Zero point six nine | C2 | C2 | 不 |
| Twelve | Two | one | Two point one one | Six point seven four | C1 | C1 | 不 |
| 质心 1 | Two point six seven | three | | | | | |
| 质心 2 | Six point three three | Six point one seven | | | | | |

似乎没有登记变更。因此,我们可以说解是收敛的。这里需要注意的一点是,除了实例 9 (X = 5,Y = 4)之外,所有的实例都分类得非常清楚。基于本能,似乎应该将其分配给质心 2,但经过仔细计算,该实例更接近聚类 1,而不是聚类 2。然而,距离的差异很小(质心为 1 时为 2.54,质心为 2 时为 2.55)。

最佳聚类数和聚类评价

虽然选择簇的数量更多的是一门艺术,而不是科学,但是选择最优的簇的数量是可能的,通过增加簇的数量,解释能力不会有太多的边际增加。在实际应用中,企业通常应该能够提供他们正在寻找的大约数量的集群。

肘形法

在 k-均值聚类中,使用肘形方法来确定最佳聚类数。肘形法绘制由不同的 k 值产生的成本函数值。如你所知,如果 k 增加,平均畸变将减少,每个簇将有更少的组成实例,并且实例将更接近它们各自的质心。然而,平均失真的改善将随着 k 的增加而下降。失真改善下降最多的 k 的值被称为肘,在该值下,我们应该停止将数据分成更多的簇。

用轮廓系数评价聚类:轮廓系数是对聚类紧密度和分离度的度量。更高的值代表更好的聚类质量。轮廓系数对于分离良好的紧凑簇较高,对于重叠簇较低。轮廓系数值确实从-1 变化到+1,数值越高越好。

轮廓系数是按实例计算的。对于一组实例,它被计算为单个样本得分的平均值。

a 是集群中实例之间的平均距离, b 是实例和下一个最近集群中实例之间的平均距离。

虹膜数据实例的 k 均值聚类

著名的 iris 数据已经从 UCI 机器学习存储库中使用 k-means 聚类进行说明。下载数据的链接在这里:http://archive.ics.uci.edu/ml/datasets/Iris。鸢尾的数据有三种类型的花:濑户花、云芝花和弗吉尼亚花,以及它们各自的萼片长度、萼片宽度、花瓣长度和花瓣宽度的测量值。我们的任务是根据花朵的尺寸对它们进行分组。代码如下:

>>> import os 
""" First change the following directory link to where all input files do exist """ 
>>> os.chdir("D:\\Book writing\\Codes\\Chapter 8") 

K-means algorithm from scikit-learn has been utilized in the following example 

# K-means clustering 
>>> import numpy as np 
>>> import pandas as pd 
>>> import matplotlib.pyplot as plt 
>>> from scipy.spatial.distance import cdist, pdist 

>>> from sklearn.cluster import KMeans 
>>> from sklearn.metrics import silhouette_score 

>>> iris = pd.read_csv("iris.csv") 
>>> print (iris.head()) 

以下代码用于将class变量分离为因变量,以便在绘图中创建颜色,并且应用于给定x变量的无监督学习算法不存在任何目标变量:

>>> x_iris = iris.drop(['class'],axis=1) 
>>> y_iris = iris["class"] 

作为样本度量,已经使用了三个聚类,但是在现实生活中,我们不知道有多少聚类的数据会被提前使用,因此我们需要通过反复试验来测试结果。下面选择的最大迭代次数是 300,但是,也可以更改该值,并相应地检查结果:

>>> k_means_fit = KMeans(n_clusters=3,max_iter=300) 
>>> k_means_fit.fit(x_iris) 

>>> print ("\nK-Means Clustering - Confusion Matrix\n\n",pd.crosstab(y_iris, k_means_fit.labels_,rownames = ["Actuall"],colnames = ["Predicted"]) )      
>>> print ("\nSilhouette-score: %0.3f" % silhouette_score(x_iris, k_means_fit.labels_, metric='euclidean')) 

从前面的混淆矩阵中,我们可以看到所有的刚毛花都被正确地聚类,而 50 朵云芝花中有 2 朵,50 朵海滨花中有 14 朵被错误地分类。

Again, to reiterate, in real-life examples we do not have the category names in advance, so we cannot measure accuracy, and so on.

以下代码用于执行敏感度分析,以检查实际上有多少个群集可以更好地解释数据段:

>>> for k in range(2,10): 
...     k_means_fitk = KMeans(n_clusters=k,max_iter=300) 
...     k_means_fitk.fit(x_iris) 
...     print ("For K value",k,",Silhouette-score: %0.3f" % silhouette_score(x_iris, k_means_fitk.labels_, metric='euclidean')) 

前面结果中的侧影系数值显示K value 2K value 3比其他所有数值得分都高。作为一个经验法则,我们需要取下一个轮廓系数最高的K value。在这里,我们可以说K value 3更好。此外,在得出最优K value之前,我们还需要查看聚类内的平均值变化值和肘形图。

# Avg. within-cluster sum of squares 
>>> K = range(1,10) 

>>> KM = [KMeans(n_clusters=k).fit(x_iris) for k in K] 
>>> centroids = [k.cluster_centers_ for k in KM] 

>>> D_k = [cdist(x_iris, centrds, 'euclidean') for centrds in centroids] 

>>> cIdx = [np.argmin(D,axis=1) for D in D_k] 
>>> dist = [np.min(D,axis=1) for D in D_k] 
>>> avgWithinSS = [sum(d)/x_iris.shape[0] for d in dist] 

# Total with-in sum of square 
>>> wcss = [sum(d**2) for d in dist] 
>>> tss = sum(pdist(x_iris)**2)/x_iris.shape[0] 
>>> bss = tss-wcss 

# elbow curve - Avg. within-cluster sum of squares 
>>> fig = plt.figure() 
>>> ax = fig.add_subplot(111) 
>>> ax.plot(K, avgWithinSS, 'b*-') 
>>> plt.grid(True) 
>>> plt.xlabel('Number of clusters') 
>>> plt.ylabel('Average within-cluster sum of squares') 

从肘部图来看,似乎在值为 3 时,斜率发生了剧烈变化。在这里,我们可以选择最佳 k 值为三。

# elbow curve - percentage of variance explained 
>>> fig = plt.figure() 
>>> ax = fig.add_subplot(111) 
>>> ax.plot(K, bss/tss*100, 'b*-') 
>>> plt.grid(True) 
>>> plt.xlabel('Number of clusters') 
>>> plt.ylabel('Percentage of variance explained')
>>> plt.show()

最后但同样重要的是,方差解释值的总百分比应该大于 80%,以决定最佳的聚类数。即使在这里,k 值为 3 似乎给出了总方差的合理值。因此,我们可以从前面的所有指标(轮廓、聚类内平均方差和解释的总方差)中得出结论,三个聚类是理想的。

使用虹膜数据进行 k 均值聚类的 R 码如下:

setwd("D:\\Book   writing\\Codes\\Chapter 8")   

iris_data = read.csv("iris.csv")   
x_iris =   iris_data[,!names(iris_data) %in% c("class")]   
y_iris = iris_data$class   

km_fit = kmeans(x_iris,centers   = 3,iter.max = 300 )   

print(paste("K-Means   Clustering- Confusion matrix"))   
table(y_iris,km_fit$cluster)   

mat_avgss = matrix(nrow = 10,   ncol = 2)   

# Average within the cluster   sum of square   
print(paste("Avg. Within   sum of squares"))   
for (i in (1:10)){   
  km_fit =   kmeans(x_iris,centers = i,iter.max = 300 )   
  mean_km =   mean(km_fit$withinss)   
  print(paste("K-Value",i,",Avg.within   sum of squares",round(mean_km, 2)))   
  mat_avgss[i,1] = i   
  mat_avgss[i,2] = mean_km   
}   
 plot(mat_avgss[,1],mat_avgss[,2],type   = 'o',xlab = "K_Value",ylab = "Avg. within sum of square")   
title("Avg. within sum of   squares vs. K-value")   

mat_varexp = matrix(nrow = 10,   ncol = 2)   
# Percentage of Variance   explained   
print(paste("Percent.   variance explained"))   
for (i in (1:10)){   
  km_fit =   kmeans(x_iris,centers = i,iter.max = 300 )   
  var_exp =   km_fit$betweenss/km_fit$totss   
  print(paste("K-Value",i,",Percent   var explained",round(var_exp,4)))   
  mat_varexp[i,1]=i   
  mat_varexp[i,2]=var_exp   
}   

plot(mat_varexp[,1],mat_varexp[,2],type   = 'o',xlab = "K_Value",ylab = "Percent Var explained")   
title("Avg. within sum of   squares vs. K-value") 

主成分分析

主成分分析 ( 主成分分析)是一种非常有用的降维技术。主成分分析通过将数据投影到低维子空间来降低数据集的维数。例如,可以通过将点投影到一条线上来减少 2D 数据集。然后,数据集中的每个实例将由单个值表示,而不是一对值。类似地,通过将变量投影到一个平面上,可以将三维数据集简化为二维。主成分分析有以下实用程序:

  • 减轻维度的进程
  • 压缩数据,同时最大限度地减少信息丢失
  • 在监督学习的下一阶段,主成分将被进一步用于随机森林、boosting 等等
  • 理解具有数百个维度的数据结构可能是困难的,因此,通过将维度减少到 2D 或三维,观察可以很容易地可视化

主成分分析可以很容易地用机械工程课程的机器绘图模块中绘制的机械支架的下图来解释。图的左侧描绘了组件的俯视图、正视图和侧视图。但是,在右侧,绘制了一个等轴测视图,其中使用了一个图像来可视化组件的外观。因此,可以想象左边的图像是实际变量,右边的图像是第一主成分,其中大部分方差已经被捕获。

最后,通过旋转方向轴,三幅图像被一幅图像所取代。事实上,我们在主成分分析中复制了相同的技术。

在以下示例中解释了主成分工作方法,其中实际数据显示在 2D 空间中,其中 XY 轴用于绘制数据。主成分是获取数据最大变化的成分。

下图说明了拟合主成分后的外观。第一个主成分覆盖数据中的最大方差,第二个主成分与第一个主成分正交,因为我们知道所有的主成分都是相互正交的。我们可以用第一主成分本身来表示整个数据。事实上,这就是用更少的维度来表示数据、节省空间以及获取数据中的最大方差是多么有利,这可以用于下一阶段的监督学习。这是计算主成分的核心优势。

特征向量和特征值在线性代数、物理、力学等领域具有重要意义。刷新,特征向量和特征值的基础知识是必要的,当研究主成分分析。特征向量是线性变换简单地通过拉伸/压缩和/或翻转作用的轴(方向);而特征值给出了压缩发生的因素。换句话说,线性变换的特征向量是非零向量,当对其应用线性变换时,其方向不变。

更正式地说, A 是向量空间的线性变换,是非零向量,那么 A 的本征向量如果的标量倍数。该条件可以写成下面的等式:

在上式中,为特征向量, A 为方阵,λ为标量,称为特征值。特征向量经过 A 变换后,方向保持不变;只有它的大小发生了变化,如特征值所示,也就是说,将一个矩阵乘以它的一个特征向量等于缩放特征向量,这是原始矩阵的紧凑表示。下图描述了 2D 空间中图形表示的特征向量和特征值:

下面的例子描述了如何从方阵计算特征向量和特征值及其理解。请注意,特征向量和特征值只能针对方阵(行和列维度相同的方阵)进行计算。

回想一下 AA 的任意特征向量的乘积必须等于特征向量乘以特征值的大小的等式:

一个特征方程表示矩阵的行列式,即数据矩阵和单位矩阵的乘积与一个特征值之差为 0

前面矩阵的两个特征值都等于 -2 。我们可以用特征值来代替方程中的特征向量:

将上式中的特征值代入,我们将得到以下公式:

前面的方程可以改写为方程组,如下所示:

这个方程表明它可以有多个特征向量的解,我们可以用任何保持前面方程的值来代替它来验证这个方程。在这里,我们用了向量【1 1】进行验证,似乎得到了证明。

主成分分析需要在计算中使用单位特征向量,因此我们需要用范数来划分单位特征向量,或者我们需要归一化特征向量。2 范数方程如下所示:

输出向量的范数计算如下:

单位特征向量如下所示:

来自基本原则的常设仲裁院工作方法

主成分分析工作方法在以下示例数据中进行了描述,每个实例或数据点都有两个维度。这里的目标是将 2D 数据简化为一维(也称为主成分):

| 实例 | X | Y |
| one | Zero point seven two | Zero point one three |
| Two | Zero point one eight | Zero point two three |
| three | Two point five | Two point three |
| four | Zero point four five | Zero point one six |
| five | Zero point zero four | Zero point four four |
| six | Zero point one three | Zero point two four |
| seven | Zero point three | Zero point zero three |
| eight | Two point six five | Two point one |
| nine | Zero point nine one | Zero point nine one |
| Ten | Zero point four six | Zero point three two |
| 列中值 | Zero point eight three | Zero point six nine |

在进行任何分析之前,第一步是从所有观察值中减去平均值,这将去除变量的比例因子,并使它们在各个维度上更加一致。

| X | Y |
| 0.72 - 0.83 = -0.12 | 0.13 - 0.69 = - 0.55 |
| 0.18 - 0.83 = -0.65 | 0.23 - 0.69 = - 0.46 |
| 2.50 - 0.83 = 1.67 | 2.30 - 0.69 = 1.61 |
| 0.45 - 0.83 = -0.38 | 0.16 - 0.69 = - 0.52 |
| 0.04 - 0.83 = -0.80 | 0.44 - 0.69 = - 0.25 |
| 0.13 - 0.83 = -0.71 | 0.24 - 0.69 = - 0.45 |
| 0.30 - 0.83 = -0.53 | 0.03 - 0.69 = - 0.66 |
| 2.65 - 0.83 = 1.82 | 2.10 - 0.69 = 1.41 |
| 0.91 - 0.83 = 0.07 | 0.91 - 0.69 = 0.23 |
| 0.46 - 0.83 = -0.37 | 0.32 - 0.69 = -0.36 |

使用两种不同的技术计算主成分:

  • 数据的协方差矩阵
  • 奇异值分解

我们将在下一节讨论奇异值分解技术。在本节中,我们将使用协方差矩阵方法来求解特征向量和特征值。

协方差是对两个变量一起变化的程度的度量,也是对两组变量之间相关性强度的度量。如果两个变量的协方差为零,我们可以断定两组变量之间不会有任何相关性。协方差公式如下:

以下公式中显示了 XY 变量的样本协方差计算。然而,它是整个协方差矩阵的 2×2 矩阵(也是一个方阵)。

由于协方差矩阵是平方的,我们可以从中计算特征向量和特征值。您可以参考前面章节中解释的方法。

通过求解前面的方程,我们可以得到特征向量和特征值,如下所示:

使用以下 Python 语法可以获得前面提到的结果:

>>> import numpy as np
>>> w, v = np.linalg.eig(np.array([[ 0.91335 ,0.75969 ],[ 0.75969,0.69702]]))
\>>> print ("\nEigen Values\n", w)
>>> print ("\nEigen Vectors\n", v)

一旦我们获得特征向量和特征值,我们就可以将数据投影到主成分中。第一特征向量具有最大特征值,并且是第一主成分,因为我们希望将原始 2D 数据简化为 1D 数据。

从前面的结果中,我们可以看到来自原始 2D 数据的第一主成分的 1D 投影。此外,1.5725 的特征值解释了这样一个事实,即主成分解释的方差比原始变量多 57%。在多维数据的情况下,经验法则是选择特征值或主成分的值大于投影应该考虑的值。

基于 scikit-learn 的主成分分析在手写数字中的应用

以 scikit-learn 数据集的手写数字为例说明了主成分分析的例子,其中手写数字是从 0-9 及其相应的 64 个像素强度特征(8×8 矩阵)创建的。这里的想法是将 64 维的原始特征表示成尽可能少的:

# PCA - Principal Component Analysis 
>>> import matplotlib.pyplot as plt 
>>> from sklearn.decomposition import PCA 
>>> from sklearn.datasets import load_digits 

>>> digits = load_digits() 
>>> X = digits.data 
>>> y = digits.target 

>>> print (digits.data[0].reshape(8,8)) 

使用plt.show功能绘制图表:

>>> plt.matshow(digits.images[0])  
>>> plt.show()  

在执行主成分分析之前,建议对输入数据进行缩放,以消除由于数据维度不同而导致的任何问题。例如,在对客户数据应用主成分分析时,他们的工资比客户的年龄具有更大的维度。因此,如果我们不把所有的变量放在一个相似的维度,一个变量将解释整个变化,而不是它的实际影响。在下面的代码中,我们对所有列分别应用了缩放:

>>> from sklearn.preprocessing import scale 
>>> X_scale = scale(X,axis=0)

在下文中,我们使用了两个主成分,因此我们可以在 2D 图上表示性能。在后面的部分中,我们也应用了 3D。

>>> pca = PCA(n_components=2) 
>>> reduced_X = pca.fit_transform(X_scale) 

>>> zero_x, zero_y = [],[] ; one_x, one_y = [],[] 
>>> two_x,two_y = [],[]; three_x, three_y = [],[] 
>>> four_x,four_y = [],[]; five_x,five_y = [],[] 
>>> six_x,six_y = [],[]; seven_x,seven_y = [],[] 
>>> eight_x,eight_y = [],[]; nine_x,nine_y = [],[] 

在下面的代码部分,我们将相关的主成分分别附加到每个数字上,这样我们就可以创建所有 10 个数字的散点图:

>>> for i in range(len(reduced_X)): 
...     if y[i] == 0: 
...         zero_x.append(reduced_X[i][0]) 
...         zero_y.append(reduced_X[i][1]) 

...     elif y[i] == 1: 
...         one_x.append(reduced_X[i][0]) 
...         one_y.append(reduced_X[i][1]) 

...     elif y[i] == 2: 
...         two_x.append(reduced_X[i][0]) 
...         two_y.append(reduced_X[i][1]) 

...     elif y[i] == 3: 
...         three_x.append(reduced_X[i][0]) 
...         three_y.append(reduced_X[i][1]) 

...     elif y[i] == 4: 
...         four_x.append(reduced_X[i][0]) 
...         four_y.append(reduced_X[i][1]) 

...     elif y[i] == 5: 
...         five_x.append(reduced_X[i][0]) 
...         five_y.append(reduced_X[i][1]) 

...     elif y[i] == 6: 
...         six_x.append(reduced_X[i][0]) 
...         six_y.append(reduced_X[i][1]) 

...     elif y[i] == 7: 
...         seven_x.append(reduced_X[i][0]) 
...         seven_y.append(reduced_X[i][1]) 

...     elif y[i] == 8: 
...         eight_x.append(reduced_X[i][0]) 
...         eight_y.append(reduced_X[i][1]) 

...     elif y[i] == 9: 
...         nine_x.append(reduced_X[i][0]) 
...         nine_y.append(reduced_X[i][1]) 
>>> zero = plt.scatter(zero_x, zero_y, c='r', marker='x',label='zero') 
>>> one = plt.scatter(one_x, one_y, c='g', marker='+') 
>>> two = plt.scatter(two_x, two_y, c='b', marker='s') 

>>> three = plt.scatter(three_x, three_y, c='m', marker='*') 
>>> four = plt.scatter(four_x, four_y, c='c', marker='h') 
>>> five = plt.scatter(five_x, five_y, c='r', marker='D') 

>>> six = plt.scatter(six_x, six_y, c='y', marker='8') 
>>> seven = plt.scatter(seven_x, seven_y, c='k', marker='*') 
>>> eight = plt.scatter(eight_x, eight_y, c='r', marker='x') 

>>> nine = plt.scatter(nine_x, nine_y, c='b', marker='D') 

>>> plt.legend((zero,one,two,three,four,five,six,seven,eight,nine), 
...            ('zero','one','two','three','four','five','six', 'seven','eight','nine'), 
...            scatterpoints=1, 
...            loc='lower left', 
...            ncol=3, 
...            fontsize=10) 

>>> plt.xlabel('PC 1') 
>>> plt.ylabel('PC 2') 

>>> plt.show() 

虽然前面的情节看起来有点混乱,但它确实提供了一些关于数字之间远近的概念。我们得到的想法是数字 68 非常相似,数字 47 离中心组非常远,以此类推。然而,我们也应该尝试使用更多数量的主成分分析,因为有时,我们可能无法代表两个维度本身的每个变化。

在下面的代码中,我们应用了三个主成分分析,这样我们就可以在三维空间中更好地查看数据。除了为每个数字创建一个额外维度( XYZ )之外,该过程与两个主成分分析非常相似。

# 3-Dimensional data 
>>> pca_3d = PCA(n_components=3) 
>>> reduced_X3D = pca_3d.fit_transform(X_scale) 

>>> zero_x, zero_y,zero_z = [],[],[] ; one_x, one_y,one_z = [],[],[] 
>>> two_x,two_y,two_z = [],[],[]; three_x, three_y,three_z = [],[],[] 
>>> four_x,four_y,four_z = [],[],[]; five_x,five_y,five_z = [],[],[] 
>>> six_x,six_y,six_z = [],[],[]; seven_x,seven_y,seven_z = [],[],[] 
>>> eight_x,eight_y,eight_z = [],[],[]; nine_x,nine_y,nine_z = [],[],[] 

>>> for i in range(len(reduced_X3D)): 

...     if y[i]==10: 
...         continue  

...     elif y[i] == 0: 
...         zero_x.append(reduced_X3D[i][0]) 
...         zero_y.append(reduced_X3D[i][1]) 
...         zero_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 1: 
...         one_x.append(reduced_X3D[i][0]) 
...         one_y.append(reduced_X3D[i][1]) 
...         one_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 2: 
...         two_x.append(reduced_X3D[i][0]) 
...         two_y.append(reduced_X3D[i][1]) 
...         two_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 3: 
...         three_x.append(reduced_X3D[i][0]) 
...         three_y.append(reduced_X3D[i][1]) 
...         three_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 4: 
...         four_x.append(reduced_X3D[i][0]) 
...         four_y.append(reduced_X3D[i][1]) 
...         four_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 5: 
...         five_x.append(reduced_X3D[i][0]) 
...         five_y.append(reduced_X3D[i][1]) 
...         five_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 6: 
...         six_x.append(reduced_X3D[i][0]) 
...         six_y.append(reduced_X3D[i][1]) 
...         six_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 7: 
...         seven_x.append(reduced_X3D[i][0]) 
...         seven_y.append(reduced_X3D[i][1]) 
...         seven_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 8: 
...         eight_x.append(reduced_X3D[i][0]) 
...         eight_y.append(reduced_X3D[i][1]) 
...         eight_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 9: 
...         nine_x.append(reduced_X3D[i][0]) 
...         nine_y.append(reduced_X3D[i][1]) 
...         nine_z.append(reduced_X3D[i][2]) 

 # 3- Dimensional plot 
>>> from mpl_toolkits.mplot3d import Axes3D 
>>> fig = plt.figure() 
>>> ax = fig.add_subplot(111, projection='3d') 

>>> ax.scatter(zero_x, zero_y,zero_z, c='r', marker='x',label='zero') 
>>> ax.scatter(one_x, one_y,one_z, c='g', marker='+',label='one') 
>>> ax.scatter(two_x, two_y,two_z, c='b', marker='s',label='two') 

>>> ax.scatter(three_x, three_y,three_z, c='m', marker='*',label='three') 
>>> ax.scatter(four_x, four_y,four_z, c='c', marker='h',label='four') 
>>> ax.scatter(five_x, five_y,five_z, c='r', marker='D',label='five') 

>>> ax.scatter(six_x, six_y,six_z, c='y', marker='8',label='six') 
>>> ax.scatter(seven_x, seven_y,seven_z, c='k', marker='*',label='seven') 
>>> ax.scatter(eight_x, eight_y,eight_z, c='r', marker='x',label='eight') 

>>> ax.scatter(nine_x, nine_y,nine_z, c='b', marker='D',label='nine') 
>>> ax.set_xlabel('PC 1') 
>>> ax.set_ylabel('PC 2') 
>>> ax.set_zlabel('PC 3') 

>>> plt.legend(loc='upper left', numpoints=1, ncol=3, fontsize=10, bbox_to_anchor=(0, 0)) 

>>> plt.show()

matplotlib 图与其他软件图如 R 图等相比有一个很大的优势。它们是交互式的,这意味着我们可以旋转它们,并从各个方向看到它们的外观。我们鼓励读者通过旋转和探索来观察情节。在一个 3D 剧情中,我们可以看到一个类似的故事,有更多的解释。数字 2 在图的最左边,数字 0 在图的下部。而数字 4 在右上角,数字 6 似乎更偏向 PC 1 轴。这样,我们就可以形象地看到数字是如何分布的。在 4 个 PCA 的情况下,我们需要去寻找支线剧情并分别可视化它们。

在无监督学习中,选择要提取的主成分分析的数量是一个开放式的问题,但是为了获得近似的视图,有一些转折。有两种方法可以确定集群的数量:

  • 检查解释的总方差在哪里略微减少
  • 解释的总差异超过 80%

下面的代码确实提供了用主成分数量的变化解释的总方差。随着电脑数量的增加,将会解释更多的差异。但是,挑战是限制尽可能少的个人电脑,这将通过限制解释开始的边际方差增加减少的地方来实现。

# Choosing number of Principal Components 
>>> max_pc = 30 

>>> pcs = [] 
>>> totexp_var = [] 

>>> for i in range(max_pc): 
...     pca = PCA(n_components=i+1) 
...     reduced_X = pca.fit_transform(X_scale) 
...     tot_var = pca.explained_variance_ratio_.sum() 
...     pcs.append(i+1) 
...     totexp_var.append(tot_var) 

>>> plt.plot(pcs,totexp_var,'r') 
>>> plt.plot(pcs,totexp_var,'bs') 
>>> plt.xlabel('No. of PCs',fontsize = 13) 
>>> plt.ylabel('Total variance explained',fontsize = 13) 
 >>> plt.xticks(pcs,fontsize=13) 
>>> plt.yticks(fontsize=13) 
>>> plt.show() 

从前面的图中,我们可以看到解释的总方差在 10 个主成分分析时略微减小;而在 21 个主成分分析中,解释的总方差大于 80%。选择哪种价值取决于企业和用户。

应用于手写数字数据的主成分分析的 R 代码如下:

# PCA   
digits_data = read.csv("digitsdata.csv")   

remove_cols = c("target")   
x_data =   digits_data[,!(names(digits_data) %in% remove_cols)]   
y_data = digits_data[,c("target")]   

# Normalizing the data   
normalize <- function(x)   {return((x - min(x)) / (max(x) - min(x)))}   
data_norm <-   as.data.frame(lapply(x_data, normalize))   
data_norm <-   replace(data_norm, is.na(data_norm), 0.0)   

# Extracting Principal   Components   
pr_out =prcomp(data_norm)   
pr_components_all = pr_out$x   

# 2- Dimensional PCA   
K_prcomps = 2   
pr_components =   pr_components_all[,1:K_prcomps]   

pr_components_df =   data.frame(pr_components)   
pr_components_df =   cbind(pr_components_df,digits_data$target)   
names(pr_components_df)[K_prcomps+1]   = "target"   

out <- split(   pr_components_df , f = pr_components_df$target )   
zero_df = out$`0`;one_df =   out$`1`;two_df = out$`2`; three_df = out$`3`; four_df = out$`4`   
five_df = out$`5`;six_df =   out$`6`;seven_df = out$`7`;eight_df = out$`8`;nine_df = out$`9`   

library(ggplot2)   
# Plotting 2-dimensional PCA   
ggplot(pr_components_df, aes(x   = PC1, y = PC2, color = factor(target,labels = c("zero","one","two",   "three","four", "five","six","seven","eight","nine"))))   +    
geom_point()+ggtitle("2-D   PCA on Digits Data") +   
labs(color = "Digtis")   

# 3- Dimensional PCA   
# Plotting 3-dimensional PCA   
K_prcomps = 3   

pr_components =   pr_components_all[,1:K_prcomps]   
pr_components_df =   data.frame(pr_components)   
pr_components_df =   cbind(pr_components_df,digits_data$target)   
names(pr_components_df)[K_prcomps+1]   = "target"   

pr_components_df$target =   as.factor(pr_components_df$target)   

out <- split(   pr_components_df , f = pr_components_df$target )   
zero_df = out$`0`;one_df =   out$`1`;two_df = out$`2`; three_df = out$`3`; four_df = out$`4`   
five_df = out$`5`;six_df =   out$`6`;seven_df = out$`7`;eight_df = out$`8`;nine_df = out$`9`   

library(scatterplot3d)   
colors <- c("darkred",   "darkseagreen4", "deeppink4", "greenyellow", "orange",   "navyblue", "red", "tan3", "steelblue1",   "slateblue")   
colors <- colors[as.numeric(pr_components_df$target)]   
s3d =   scatterplot3d(pr_components_df[,1:3], pch = 16, color=colors,   
xlab = "PC1",ylab = "PC2",zlab   = "PC3",col.grid="lightblue",main = "3-D PCA on   Digits Data")   
legend(s3d$xyz.convert(3.1,   0.1, -3.5), pch = 16, yjust=0,   
       legend =   levels(pr_components_df$target),col =colors,cex = 1.1,xjust = 0)   

# Choosing number of Principal   Components   
pr_var =pr_out$sdev ^2   
pr_totvar = pr_var/sum(pr_var)   
plot(cumsum(pr_totvar), xlab="Principal   Component", ylab ="Cumilative Prop. of Var.",   ylim=c(0,1),type="b",main = "PCAs vs. Cum prop of Var   Explained") 

奇异值分解

主成分分析的许多实现使用奇异值分解来计算特征向量和特征值。奇异值分解由下式给出:

U 的列称为数据矩阵的左奇异向量, V 的列为其右奇异向量,的对角条目为其奇异值。左奇异向量是协方差矩阵的特征向量,的对角元素是协方差矩阵特征值的平方根。

在继续使用支持向量机之前,最好了解支持向量机的一些优点和要点:

  • 奇异值分解甚至可以应用于矩形矩阵;而特征值只为方阵定义。通过奇异值分解方法获得的特征值的等价物被称为奇异值,获得的与特征向量等价的向量被称为奇异向量。然而,因为它们本质上是矩形的,所以我们需要为它们的维度分别有左奇异向量和右奇异向量。
  • 如果一个矩阵 A 有一个不可逆的特征向量矩阵 P ,那么 A 就没有特征分解。但是如果 Am x n 实矩阵与mT16n的话,那么 A 可以用奇异值分解来写。
  • UV 都是正交矩阵,表示 U T U = I ( Im x m 维)或 V T V = I (此处 In x n 维),其中
  • 为非负对角矩阵,尺寸为 m x n

然后,奇异值和奇异向量的计算通过以下方程组完成:

在第一阶段,奇异值/特征值用以下方程计算。一旦我们得到奇异/特征值,我们将代入确定 V 或右奇异/特征向量:

一旦我们获得了右奇异向量和对角值,我们将使用下面提到的等式代入以获得左奇异向量 U :

这样,我们将计算原始方程组矩阵的奇异值分解。

基于 scikit-learn 的支持向量机在手写数字中的应用

奇异值分解可以应用于相同的手写数字数据,进行苹果对苹果的技术比较。

# SVD 
>>> import matplotlib.pyplot as plt 
>>> from sklearn.datasets import load_digits 

>>> digits = load_digits() 
>>> X = digits.data 
>>> y = digits.target 

在下面的代码中,使用了 15 个 300 次迭代的奇异向量,但是我们鼓励读者更改值并检查 SVD 的性能。我们使用了两种类型的奇异值分解函数,一种函数randomized_svd提供原始矩阵的分解,一种函数TruncatedSVD提供总方差解释比。实际上,用户可能不需要查看所有的分解,他们可以使用TruncatedSVD函数来实现他们的实际目的。

>>> from sklearn.utils.extmath import randomized_svd 
>>> U,Sigma,VT = randomized_svd(X,n_components=15,n_iter=300,random_state=42) 

>>> import pandas as pd 
>>> VT_df = pd.DataFrame(VT) 

>>> print ("\nShape of Original Matrix:",X.shape) 
>>> print ("\nShape of Left Singular vector:",U.shape) 
>>> print ("Shape of Singular value:",Sigma.shape) 
>>> print ("Shape of Right Singular vector",VT.shape) 

通过看前面的截图,我们可以看到维(1797 x 64)的原始矩阵已经分解为左奇异向量(1797 x 15)、奇异值(15 的对角矩阵)和右奇异向量(15 x 64)。我们可以通过将所有三个矩阵按顺序相乘来获得原始矩阵。

>>> n_comps = 15 
>>> from sklearn.decomposition import TruncatedSVD 
>>> svd = TruncatedSVD(n_components=n_comps, n_iter=300, random_state=42) 
>>> reduced_X = svd.fit_transform(X) 

>>> print("\nTotal Variance explained for %d singular features are %0.3f"%(n_comps, svd.explained_variance_ratio_.sum())) 

15 个奇异值特征的总方差解释为 83.4%。但是读者需要改变不同的值来决定最佳值。

以下代码说明了总方差的变化,并分别解释了奇异值数量的变化:

# Choosing number of Singular Values 
>>> max_singfeat = 30 
>>> singfeats = [] 
>>> totexp_var = [] 

>>> for i in range(max_singfeat): 
...     svd = TruncatedSVD(n_components=i+1, n_iter=300, random_state=42) 
...     reduced_X = svd.fit_transform(X) 
...     tot_var = svd.explained_variance_ratio_.sum() 
...     singfeats.append(i+1) 
...     totexp_var.append(tot_var) 

>>> plt.plot(singfeats,totexp_var,'r') 
>>> plt.plot(singfeats,totexp_var,'bs') 
>>> plt.xlabel('No. of Features',fontsize = 13) 
>>> plt.ylabel('Total variance explained',fontsize = 13) 

>>> plt.xticks(pcs,fontsize=13) 
>>> plt.yticks(fontsize=13) 
>>> plt.show()

从前面的图中,我们可以根据需要选择 8 个或 15 个奇异向量。

应用于手写数字数据的奇异值分解的 R 码如下:

#SVD    
library(svd)   

digits_data = read.csv("digitsdata.csv")   

remove_cols = c("target")   
x_data =   digits_data[,!(names(digits_data) %in% remove_cols)]   
y_data = digits_data[,c("target")]   

sv2 <- svd(x_data,nu=15)   

# Computing the square of the   singular values, which can be thought of as the vector of matrix energy   
# in order to pick top singular   values which preserve at least 80% of variance explained   
energy <- sv2$d ^ 2   
tot_varexp = data.frame(cumsum(energy)   / sum(energy))   

names(tot_varexp) = "cum_var_explained"   
tot_varexp$K_value =   1:nrow(tot_varexp)   

plot(tot_varexp[,2],tot_varexp[,1],type   = 'o',xlab = "K_Value",ylab = "Prop. of Var Explained")   
title("SVD - Prop. of Var   explained with K-value")    

深度自动编码器

自动编码器神经网络是一种无监督学习算法,它应用反向传播将目标值设置为等于输入y(I)= x(I)。自动编码器试图学习一个函数 h w,b (x) ≈ x ,意味着它试图学习一个恒等式函数的近似值,从而输出类似于 x

虽然试图复制身份函数看起来是微不足道的学习功能,但通过对网络设置约束,例如限制隐藏单元的数量,我们可以发现关于数据的有趣结构。假设大小为 10 x 10 像素的输入图片具有总共 100 个输入值的强度值,第二层(隐藏层)中的神经元数量为 50 个单位,最后输出层具有 100 个单位的神经元,因为我们需要传递图像以将其映射到自身,并且当在该过程中实现该表示时,我们将强制网络学习输入的压缩表示, 也就是隐藏单元激活a(2)εR100T5】,用它我们必须尝试重建 100 像素输入 x 。 如果输入数据是完全随机的,没有任何相关性,等等。压缩将非常困难,而如果底层数据具有一些相关性或可检测的结构,则该算法将能够发现相关性并紧凑地表示它们。事实上,自动编码器最终往往会学习到与主成分分析非常相似的低维表示。

使用编码器-解码器结构的建模技术

训练自动编码器模型有点棘手,因此提供了详细的说明,以便读者更好地理解。在训练阶段,整个编码器-解码器部分针对与解码器输出相同的输入进行训练。为了获得期望的输出,当我们通过会聚层和发散层时,特征将在中间层被压缩。一旦通过减少迭代次数的误差值进行了足够的训练,我们将使用训练好的编码器部分来为下一阶段的建模或可视化等创建潜在特征。

下图显示了一个示例。输入和输出层有五个神经元,而中间部分的神经元数量逐渐减少。压缩层只有两个神经元,这是我们希望从数据中提取的潜在维数。

下图描述了使用训练好的编码器部分从新的输入数据创建潜在特征,这些特征将用于可视化或模型的下一阶段:

深度自动编码器应用于手写数字使用 Keras

用相同的手写数字数据解释深度自动编码器,以显示这种非线性方法与线性方法(如主成分分析和奇异值分解)的区别。非线性方法通常表现得更好,但这些方法是一种黑箱模型,我们无法确定背后的解释。Keras 软件已被用于构建此处的深度自动编码器,因为它们像乐高积木一样工作,这使得用户可以轻松地玩模型的不同架构和参数,以便更好地理解:

# Deep Auto Encoders 
>>> import matplotlib.pyplot as plt 
>>> from sklearn.preprocessing import StandardScaler 
>>> from sklearn.datasets import load_digits 

>>> digits = load_digits() 
>>> X = digits.data 
>>> y = digits.target 

>>> print (X.shape) 
>>> print (y.shape) 
>>> x_vars_stdscle = StandardScaler().fit_transform(X) 
>>> print (x_vars_stdscle.shape) 

用于构建编码器-解码器架构的 Keras 密集神经元模块:

>>> from keras.layers import Input,Dense 
>>> from keras.models import Model

这里使用了 NVIDIA GTX 1060 的 GPU,还安装了cuDNNCNMeM库,在常规 GPU 性能的基础上,速度进一步提升 4-5 倍。这些库利用了 20%的图形处理器内存,剩下 80%的内存用于处理数据。用户需要小心,如果他们有像 3 GB 到 4 GB 这样的低内存 GPU,他们可能无法利用这些库。

The reader needs to consider one important point that, syntax of Keras code, will remain same in both CPU and GPU mode.

以下几行代码是模型的核心。输入数据有 64 列。我们需要将这些列作为层的输入,因此我们给出了 64 的形状。此外,神经网络的每一层都有名称,我们将在接下来的代码部分解释原因。在第一隐藏层中,使用了 32 个密集神经元,这意味着来自输入层的所有 64 个输入都连接到第一隐藏层中的 32 个神经元。整个维度流就像 64,32,16,2,16,32,64 。我们已经将输入压缩到两个神经元,以便在 2D 图上绘制组件,然而,如果我们需要绘制 3D 数据(我们将在下一节中讨论),我们需要将隐藏的三层编号更改为三,而不是二。训练完成后,我们需要使用编码器部分并预测输出。

# 2-Dimensional Architecture 

>>> input_layer = Input(shape=(64,),name="input") 

>>> encoded = Dense(32, activation='relu',name="h1encode")(input_layer) 
>>> encoded = Dense(16, activation='relu',name="h2encode")(encoded) 
>>> encoded = Dense(2, activation='relu',name="h3latent_layer")(encoded) 

>>> decoded = Dense(16, activation='relu',name="h4decode")(encoded) 
>>> decoded = Dense(32, activation='relu',name="h5decode")(decoded) 
>>> decoded = Dense(64, activation='sigmoid',name="h6decode")(decoded) 

为了训练模型,我们需要通过架构的起点和终点。在下面的代码中,我们提供的输入为input_layer,输出为decoded,这是最后一层(名称为h6decode):

>>> autoencoder = Model(input_layer, decoded) 

Adam 优化已用于优化均方误差,因为我们希望在网络输出层的末端再现原始输入:

>>> autoencoder.compile(optimizer="adam", loss="mse") 

该网络用 100 个时期和每批 256 个观测值的批量来训练。20%的验证分割用于检查随机选择的验证数据的准确性,以确保稳健性,就好像我们只对训练数据进行训练可能会产生过拟合问题,这在高度非线性的模型中非常常见:

# Fitting Encoder-Decoder model 
>>> autoencoder.fit(x_vars_stdscle, x_vars_stdscle, epochs=100,batch_size=256, shuffle=True,validation_split= 0.2 ) 

从前面的结果可以看出,该模型已经在 1437 个训练样本上进行了训练,并在 360 个样本上进行了验证。通过查看损失值,训练和验证损失分别从 1.2314 降至 0.9361 和 1.0451 降至 0.7326。因此,我们正朝着正确的方向前进。然而,我们鼓励读者尝试各种体系结构和迭代次数、批次大小等,看看精度可以进一步提高多少。

一旦编码器-解码器部分已经被训练,我们只需要采取编码器部分来压缩输入特征,以获得压缩的潜在特征,这是降维的核心思想!在下面的代码中,我们构建了另一个模型,它有一个训练好的输入层和一个中间隐藏层(h3latent_layer)。这就是为网络的每一层分配名称的原因。

# Extracting Encoder section of the Model for prediction of latent variables 
>>> encoder = Model(autoencoder.input,autoencoder.get_layer("h3latent_layer").output) 

Extracted encoder section of the whole model used for prediction of input variables to generate sparse 2-dimensional representation, which is being performed with the following code 

# Predicting latent variables with extracted Encoder model 
>>> reduced_X = encoder.predict(x_vars_stdscle)  

只是为了检查减少的输入变量的维度,我们可以看到,对于所有观察,我们可以看到两个维度或两个列向量:

 >>> print (reduced_X.shape) 

守则的以下部分与 2D 常设仲裁院非常相似:

>>> zero_x, zero_y = [],[] ; one_x, one_y = [],[] 
>>> two_x,two_y = [],[]; three_x, three_y = [],[] 
>>> four_x,four_y = [],[]; five_x,five_y = [],[] 
>>> six_x,six_y = [],[]; seven_x,seven_y = [],[] 
>>> eight_x,eight_y = [],[]; nine_x,nine_y = [],[] 

# For 2-Dimensional data 
>>> for i in range(len(reduced_X)): 
...     if y[i] == 0: 
...         zero_x.append(reduced_X[i][0]) 
...         zero_y.append(reduced_X[i][1]) 

...     elif y[i] == 1: 
...         one_x.append(reduced_X[i][0]) 
...         one_y.append(reduced_X[i][1]) 

...     elif y[i] == 2: 
...         two_x.append(reduced_X[i][0]) 
...         two_y.append(reduced_X[i][1]) 
 ...     elif y[i] == 3: 
...         three_x.append(reduced_X[i][0]) 
...         three_y.append(reduced_X[i][1]) 

...     elif y[i] == 4: 
...         four_x.append(reduced_X[i][0]) 
...         four_y.append(reduced_X[i][1]) 

...     elif y[i] == 5: 
...         five_x.append(reduced_X[i][0]) 
...         five_y.append(reduced_X[i][1]) 

...     elif y[i] == 6: 
...         six_x.append(reduced_X[i][0]) 
...         six_y.append(reduced_X[i][1]) 

...     elif y[i] == 7: 
...         seven_x.append(reduced_X[i][0]) 
...         seven_y.append(reduced_X[i][1]) 

...     elif y[i] == 8: 
...         eight_x.append(reduced_X[i][0]) 
 ...        eight_y.append(reduced_X[i][1]) 

 ...    elif y[i] == 9: 
 ...        nine_x.append(reduced_X[i][0]) 
 ...        nine_y.append(reduced_X[i][1]) 

>>> zero = plt.scatter(zero_x, zero_y, c='r', marker='x',label='zero') 
>>> one = plt.scatter(one_x, one_y, c='g', marker='+') 
>>> two = plt.scatter(two_x, two_y, c='b', marker='s') 

>>> three = plt.scatter(three_x, three_y, c='m', marker='*') 
>>> four = plt.scatter(four_x, four_y, c='c', marker='h') 
>>> five = plt.scatter(five_x, five_y, c='r', marker='D') 

>>> six = plt.scatter(six_x, six_y, c='y', marker='8') 
>>> seven = plt.scatter(seven_x, seven_y, c='k', marker='*') 
>>> eight = plt.scatter(eight_x, eight_y, c='r', marker='x') 

>>> nine = plt.scatter(nine_x, nine_y, c='b', marker='D') 

>>> plt.legend((zero,one,two,three,four,five,six,seven,eight,nine), 
...  ('zero','one','two','three','four','five','six','seven','eight','nine'), 
...            scatterpoints=1,loc='lower right',ncol=3,fontsize=10) 

>>> plt.xlabel('Latent Feature 1',fontsize = 13) 
>>> plt.ylabel('Latent Feature 2',fontsize = 13) 

>>> plt.show() 

从前面的图中,我们可以看到数据点被很好地分开,但问题是观察的方向,因为这些特征不会像相互正交的主成分那样,按照相互垂直的维度变化。在深度自动编码器的情况下,我们需要从 (0,0) 改变方向的视图来可视化这种非线性分类,我们将在下面的 3D 情况中详细看到。

以下是 3D 潜在特征的代码。除了h3latent_layer之外,所有代码保持不变,其中我们必须将值从2替换为3,因为这是编码器部分的结尾,我们将利用它创建潜在特征,最终,它将用于绘图目的。

# 3-Dimensional architecture 
>>> input_layer = Input(shape=(64,),name="input") 

>>> encoded = Dense(32, activation='relu',name="h1encode")(input_layer) 
>>> encoded = Dense(16, activation='relu',name="h2encode")(encoded) 
>>> encoded = Dense(3, activation='relu',name="h3latent_layer")(encoded) 

>>> decoded = Dense(16, activation='relu',name="h4decode")(encoded) 
>>> decoded = Dense(32, activation='relu',name="h5decode")(decoded) 
>>> decoded = Dense(64, activation='sigmoid',name="h6decode")(decoded) 

>>> autoencoder = Model(input_layer, decoded) 
autoencoder.compile(optimizer="adam", loss="mse") 

# Fitting Encoder-Decoder model 
>>> autoencoder.fit(x_vars_stdscle, x_vars_stdscle, epochs=100,batch_size=256, shuffle=True,validation_split= 0.2) 

从前面的结果中,我们可以看到,在包含三维而不是二维的情况下,获得的损失值小于 2D 用例中的损失值。100 个时期后两个潜在因素的训练和验证损失为 0.9061 和 0.7326,100 个时期后三个潜在因素的训练和验证损失为 0.8032 和 0.6464。这意味着,通过多包含一个维度,我们可以显著减少误差。这样,读者可以更改各种参数来确定降维的理想架构:

# Extracting Encoder section of the Model for prediction of latent variables 
>>> encoder = Model(autoencoder.input,autoencoder.get_layer("h3latent_layer").output) 

# Predicting latent variables with extracted Encoder model 
>>> reduced_X3D = encoder.predict(x_vars_stdscle) 

>>> zero_x, zero_y,zero_z = [],[],[] ; one_x, one_y,one_z = [],[],[] 
>>> two_x,two_y,two_z = [],[],[]; three_x, three_y,three_z = [],[],[] 
>>> four_x,four_y,four_z = [],[],[]; five_x,five_y,five_z = [],[],[] 
>>> six_x,six_y,six_z = [],[],[]; seven_x,seven_y,seven_z = [],[],[] 
>>> eight_x,eight_y,eight_z = [],[],[]; nine_x,nine_y,nine_z = [],[],[] 

>>> for i in range(len(reduced_X3D)): 

...     if y[i]==10: 
...         continue 

...     elif y[i] == 0: 
...         zero_x.append(reduced_X3D[i][0]) 
...         zero_y.append(reduced_X3D[i][1]) 
...         zero_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 1: 
...         one_x.append(reduced_X3D[i][0]) 
...         one_y.append(reduced_X3D[i][1]) 
...         one_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 2: 
...         two_x.append(reduced_X3D[i][0]) 
...         two_y.append(reduced_X3D[i][1]) 
...         two_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 3: 
...         three_x.append(reduced_X3D[i][0]) 
...         three_y.append(reduced_X3D[i][1]) 
...         three_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 4: 
...         four_x.append(reduced_X3D[i][0]) 
...         four_y.append(reduced_X3D[i][1]) 
...         four_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 5: 
...         five_x.append(reduced_X3D[i][0]) 
...         five_y.append(reduced_X3D[i][1]) 
...         five_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 6: 
...         six_x.append(reduced_X3D[i][0]) 
...         six_y.append(reduced_X3D[i][1]) 
...         six_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 7: 
...         seven_x.append(reduced_X3D[i][0]) 
...         seven_y.append(reduced_X3D[i][1]) 
...         seven_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 8: 
...         eight_x.append(reduced_X3D[i][0]) 
...         eight_y.append(reduced_X3D[i][1]) 
...         eight_z.append(reduced_X3D[i][2]) 

...     elif y[i] == 9: 
...         nine_x.append(reduced_X3D[i][0]) 
...         nine_y.append(reduced_X3D[i][1]) 
...         nine_z.append(reduced_X3D[i][2]) 

 # 3- Dimensional plot 
>>> from mpl_toolkits.mplot3d import Axes3D 
>>> fig = plt.figure() 
>>> ax = fig.add_subplot(111, projection='3d') 

>>> ax.scatter(zero_x, zero_y,zero_z, c='r', marker='x',label='zero') 
>>> ax.scatter(one_x, one_y,one_z, c='g', marker='+',label='one') 
>>> ax.scatter(two_x, two_y,two_z, c='b', marker='s',label='two') 

>>> ax.scatter(three_x, three_y,three_z, c='m', marker='*',label='three') 
>>> ax.scatter(four_x, four_y,four_z, c='c', marker='h',label='four') 
>>> ax.scatter(five_x, five_y,five_z, c='r', marker='D',label='five') 

>>> ax.scatter(six_x, six_y,six_z, c='y', marker='8',label='six') 
>>> ax.scatter(seven_x, seven_y,seven_z, c='k', marker='*',label='seven') 
>>> ax.scatter(eight_x, eight_y,eight_z, c='r', marker='x',label='eight') 
 >>> ax.scatter(nine_x, nine_y,nine_z, c='b', marker='D',label='nine') 

>>> ax.set_xlabel('Latent Feature 1',fontsize = 13) 
>>> ax.set_ylabel('Latent Feature 2',fontsize = 13) 
>>> ax.set_zlabel('Latent Feature 3',fontsize = 13) 

>>> ax.set_xlim3d(0,60) 

>>> plt.legend(loc='upper left', numpoints=1, ncol=3, fontsize=10, bbox_to_anchor=(0, 0)) 

>>> plt.show() 

与三个主成分分析相比,深度自动编码器的三维图确实提供了很好的分离分类。这里我们得到了更好的数字分离。这里读者应该考虑的一个重要点是,上面的图是从 (0,0,0) 旋转的视图,因为数据分离不会发生在正交平面上(像 PCAs),因此我们需要从原点看到视图,以便看到这种非线性分类。

摘要

在本章中,您已经学习了各种无监督学习方法,使用 k 均值聚类、PCA、SVD 和深度自动编码器来识别数据中的结构和模式。此外,用虹膜数据解释了 k-均值聚类算法。给出了基于各种性能指标选择最佳 k 值的方法。scikit-learn 的手写数据被用来比较线性方法(如 PCA 和 SVD)与非线性技术和深度自动编码器之间的差异。详细给出了主成分分析和奇异值分解的区别,以便读者理解奇异值分解,它甚至可以应用于用户数和产品数不一定相等的矩形矩阵。最后,通过可视化,证明深度自动编码器比主成分分析和奇异值分解等线性无监督学习方法更擅长数字分离。

在下一章中,我们将讨论各种强化学习方法及其在人工智能等领域的应用。

九、强化学习

强化学习 ( RL )是机器学习继有监督和无监督学习之后的第三大板块。这些技术近年来在人工智能的应用中获得了很大的吸引力。在强化学习中,要进行顺序决策,而不是一次性决策,这使得很难在少数情况下训练模型。在这一章中,我们将介绍强化学习中使用的各种技术,并提供实例来支持。虽然涵盖所有主题超出了本书的范围,但我们确实涵盖了最重要的基础知识,让读者对这一主题产生足够的热情。本章讨论的主题有:

  • 马尔可夫决策过程
  • 贝尔曼方程
  • 动态规划
  • 蒙特卡罗方法
  • 时间差异学习
  • 强化学习和机器学习综合应用的人工智能最新趋势

强化学习导论

强化学习模仿人类的学习方式:通过与环境互动,重复获得较高回报的行动,避免因行动而获得较低或负面回报的冒险行动。

详细比较有监督、无监督和强化学习

由于机器学习有三个主要部分,让我们从较高的层次来看看主要的区别和相似之处:

  • 监督学习:在监督学习中,我们有一个训练集,对于每个训练算法,我们都给出了正确的答案。训练示例包含所有正确的答案,训练算法的工作就是复制正确的答案。
  • 无监督学习:在无监督学习中,我们有一组未标记的数据和一个学习算法。学习算法的工作是用 k-means、PCA 等算法来寻找数据中的结构。
  • 强化学习:在强化学习中,我们没有目标变量。相反,我们有奖励信号,代理需要自己规划路径,以达到奖励存在的目标。

强化学习的特点

  • 奖励信号的反馈不是即时的。它被延迟了许多时间步长
  • 实现目标需要顺序决策,因此时间在强化问题中起着重要作用(这里数据的 IID 假设不成立)
  • 代理的操作会影响其接收的后续数据

在强化学习中,需要一点点监督,但与监督学习相比,监督要少得多。

以下是强化学习问题的几个实际例子:

  • 自主直升机:自主直升机的目标是通过控制操纵杆、踏板等,改变其滚转、俯仰和偏航来控制其位置。传感器每秒发送 10 次输入,提供直升机位置和方向的精确估计。直升机的工作是接收这种输入,并控制操纵杆相应地移动。当直升机处于这个位置和方位时,很难提供直升机下一步需要做什么的信息,也没有训练集来控制动作。相反,RL 算法给出了不同类型的反馈:当直升机表现良好时,它会给出奖励信号,当直升机做错事情时,它会给出负面奖励。基于这些信号,直升机控制旅程。学习算法的工作是提供奖励函数并自行训练路径。
  • 电脑象棋:电脑下棋是另一个例子,在游戏的任何阶段,我们都不会事先知道最佳的棋步是什么;所以用监督学习算法下棋是非常困难的。很难说 X 是棋盘位置而 Y 是这个特定棋盘位置的最佳移动。取而代之的是,每当它赢了一局,我们提供奖励(+1),每当它输了一局,我们给出负奖励(-1),我们让算法算出在一段时间内赢得游戏的必要步骤。
  • 训练猫咪:我们在猫咪做好事的时候给它奖励,每次它做坏事的时候,我们都明确表示这是不好的行为。一段时间后,猫学会做更多的好事,少做坏事。

强化学习比监督学习难的原因包括:

  • 这不是一次性的决策问题。因此,在监督学习中,一种算法根据给定的属性预测某人是否患有癌症;而在 RL 中,你必须在一段时间内持续采取行动。我们称之为顺序决策。
  • 在一盘棋中,我们在赢/输之前走了 60 步。我们不确定哪些是正确的动作,哪些是错误的动作。在第 25 步,我们走错了一步,最终导致我们在第 60 步输掉了比赛。
  • 信用分配问题:(正面或负面奖励)多做一件好事,少做一件坏事。
  • 在汽车碰撞的例子中,在碰撞前的某个时刻,驾驶员可能会刹车。然而,不是刹车导致了撞车,而是刹车前发生了一些事情,最终导致了撞车。RL 在一段时间内学习模式,这可能包括之前开得太快,没有观察其他道路交通,忽略警告标志,等等。
  • RL 适用于不同的应用,当有长期后果时,用于顺序决策。

强化学习基础

在我们深入探讨强化学习的细节之前,我想介绍一些理解 RL 方法的各种细节所必需的基础知识。这些基础知识出现在本章的各个部分,我们将在需要时详细解释:

  • 环境:这是任何有状态的系统,以及状态间转换的机制。例如,机器人的环境是它操作的景观或设施。
  • Agent: 这是一个与环境交互的自动化系统。
  • 状态:环境或系统的状态是完全描述环境的一组变量或特征。
  • 目标或吸收状态或终结状态:这是提供比其他任何状态更高折扣累积奖励的状态。高累积奖励可防止最佳策略依赖于训练期间的初始状态。每当一个特工达到目的,我们就要完成一集。
  • 动作:这定义了状态之间的转换。代理负责执行或至少推荐一个动作。在执行该动作时,代理从环境中收集奖励(或惩罚)。
  • 策略:这定义了要为环境的任何状态选择和执行的操作。换句话说,政策就是代理人的行为;这是一张从国家到行动的地图。政策可以是确定性的,也可以是随机的。
  • 最佳策略:这是通过训练生成的策略。它定义了 Q-learning 中的模型,并随着任何新的情节不断更新。
  • 奖励:这量化了代理人与环境的正面或负面互动。奖励通常是代理人到达每个州后立即获得的收入。
  • 回报或价值函数:价值函数(也叫回报)是对每个状态未来回报的预测。这些用于评估状态的好/坏,在此基础上,代理将选择/采取行动来选择下一个最佳状态:

  • 插曲:这定义了从初始状态达到目标状态所需的步骤数。剧集也被称为试验。
  • 地平线:这是在奖励最大化中使用的未来步骤或动作的数量。地平线可以是无限的,在这种情况下,为了让政策的价值趋同,未来的回报会打折扣。
  • 探索对开发: RL 是一种试错学习。目标是找到最佳政策;同时,保持警惕,探索一些未知的政策。一个经典的例子是寻宝:如果我们只是贪婪地去那些地方(探索),我们就找不到其他可能存在隐藏宝藏的地方(探索)。通过探索未知的状态,通过冒险,即使当眼前的回报很低并且没有失去最大回报时,我们也可能实现更大的目标。换句话说,我们正在逃离局部最优,以便实现全局最优(这就是探索),而不仅仅是单纯关注眼前的回报(这就是剥削)。这里有几个例子来解释这种差异:
    • 餐厅选择:偶尔去逛逛不知名的餐厅,可能会发现比我们经常喜欢的餐厅好很多的餐厅:
      • 剥削:去自己喜欢的餐厅
      • 探索:尝试新餐厅
    • 石油钻探示例:通过探索新的未开发位置,我们可能会获得比仅仅探索同一个地方更有益的新见解:
      • 开采:在已知最佳位置钻探石油
      • 探索:在新的位置钻孔
  • 状态-值对状态-动作函数:在动作-值中,Q 表示代理在状态 S 下采取动作 A 时,以及之后按照某个策略π(a|s)行事(这是在给定状态下采取动作的概率)时,将获得的预期回报(累计折扣奖励)。

在状态值中,该值是座席处于状态 s 时根据策略 π(a|s) 进行操作的预期回报。更具体地说,状态值是对策略下的操作值的预期:

  • 策略内与策略外 TD 控制:策略外学习者独立于代理的动作学习最优策略的值。Q-learning 是一个偏离政策的学习者。策略学习者学习由代理执行的策略的价值,包括探索步骤。
  • 预测与控制问题:预测讲的是我做得有多好,基于给定的政策:意思是,如果有人给了我一个政策,我执行了,我会因此得到多少奖励。然而,在控制中,问题是找到最好的策略,这样我就可以获得最大的回报。
  • 预测:评估给定策略的状态值。

对于统一随机策略,所有状态的价值函数是什么?

  • 控制:通过寻找最佳策略来优化未来。

什么是所有可能策略的最优价值函数,什么是最优策略?

通常在强化学习中,我们需要先解决预测问题,然后才能解决控制问题,就像我们需要找出所有的策略来找出最佳或最优的策略一样。

  • RL 代理分类:RL 代理包括以下一个或多个组件:
    • 策略:代理的行为功能(从状态到动作的映射);策略可以是确定性的,也可以是随机的
    • 价值函数:每个状态对每个状态的预期未来奖励的预测有多好
    • 模型:代理对环境的表示。一个模型预测环境下一步会做什么:
      • 跃迁: p 预测下一个状态(即动力学):

让我们解释 RL 代理分类法中基于策略和价值组合的各种可能的类别,并用下面的迷宫示例对单个组件进行建模。在下面的迷宫中,你既有起点,也有目标;代理需要尽快达到目标,走一条获得总最大奖励和最小总负奖励的路径。这个问题主要有五种分类方法可以解决:

  • 基于价值
  • 基于策略
  • 演员评论家
  • 无模型
  • 基于模型

类别 1 -基于价值

价值函数看起来确实像图像的右边(未来折扣奖励的总和),其中每个州都有一些价值。假设,距离目标一步之遥的状态的值为-1;距离目标两步的值为-2。同样,起点的值为-16。如果代理卡在错误的地方,该值可能高达-24。事实上,代理确实会根据达到目标的最佳可能值在网格中移动。例如,代理处于值为-15 的状态。在这里,它可以选择向北或向南移动,所以它选择向北移动是因为奖励很高,为-14,而不是向南移动,值为-16。通过这种方式,代理选择它在网格中的路径,直到它达到目标。

  • 值函数:在所有状态下只定义值
  • 无策略(隐式):不存在独占策略;策略是根据每个州的值来选择的

第 2 类-基于政策

下图中的箭头表示座席在任何这些状态下选择的下一个移动方向。例如,代理首先向东移动,然后向北移动,跟随所有箭头,直到达到目标。这也称为从状态到动作的映射。一旦我们有了这个映射,代理只需要读取它并做出相应的行为。

  • 政策:政策或箭头得到调整,以达到未来可能的最大回报。顾名思义,只有策略被存储和优化,才能实现回报最大化。
  • 无值函数:状态不存在值。

第三类-演员兼评论家

在 Actor-criteria 中,我们同时具有策略和价值功能(或者是基于价值和基于策略的组合)。这种方法是两全其美的:

  • 政策
  • 价值函数

第 4 类-无型号

在 RL 中,一个基本的区别是它是基于模型的还是无模型的。在无模型环境中,我们没有明确地对环境建模,或者我们不知道完整环境的全部动态。相反,我们只需直接转到策略或价值函数来获得经验,并弄清楚策略如何影响奖励:

  • 策略和/或价值功能
    • 没有模型

第 5 类-基于模型

在基于模型的 RL 中,我们首先构建环境的整个动态:

  • 策略和/或价值功能
  • 模型

在浏览了所有上述类别之后,下面的文氏图显示了一个位置上 RL 代理的整个分类法。如果你拿起任何与强化学习相关的论文,这些方法可以适用于这一领域的任何部分。

顺序决策中的基本范畴

顺序决策中有两种基本类型的问题:

  • 强化学习(例如自主直升机等):
    • 环境最初是未知的
    • 代理与环境交互,并从环境中获得策略、奖励和价值
    • 代理改进了其策略
  • 策划(如国际象棋、雅达利游戏等):
    • 环境模型或环境的完整动力学是已知的
    • 代理使用其模型执行计算(没有任何外部交互)
    • 代理改进了其策略
    • 这些类型的问题也被称为推理、搜索、自省等等

虽然前面两个类别可以根据给定的问题联系在一起,但这基本上是两种类型的设置的广泛观点。

马尔可夫决策过程和贝尔曼方程

马尔可夫决策过程 ( MDP )正式描述了一个强化学习的环境。其中:

  • 环境是完全可观察的
  • 当前状态完全表征了过程(这意味着未来状态完全依赖于当前状态,而不是历史状态或值)
  • 几乎所有的 RL 问题都可以形式化为多目标规划(例如,最优控制主要处理连续的多目标规划)

MDP 的中心思想: MDP 研究一个国家的简单马尔可夫性质;例如,St+1T5】完全依赖最新状态StT9】而非任何历史依赖。在下面的等式中,当前状态捕获历史中的所有相关信息,这意味着当前状态是对未来的充分统计:**

这个属性的直观意义可以用自动直升机的例子来解释:下一步是让直升机要么向右、向左、俯仰,要么滚转等等,完全取决于直升机的当前位置,而不是五分钟前的位置。

MDP 建模: RL 问题使用 MDP 公式作为五元组来建模世界( S,A,{P sa },y,R )

  • S -状态集(直升机可能方位集)
  • A -一组动作(设置所有可能拉动操纵杆的位置)
  • P sa -状态转移分布(或状态转移概率分布)提供从一个状态到另一个状态的转移以及马尔可夫过程所需的相应概率:

  • γ -折扣系数:

  • R -奖赏函数(将一组状态映射到实数,正数或负数):

回报的计算方法是对未来的回报进行贴现,直到达到最终状态。

MDP 的贝尔曼方程:MDP 的数学公式采用贝尔曼方程,求解得到环境的最优策略。贝尔曼方程也被称为动态规划方程,并且是与被称为动态规划的数学优化方法相关联的最优性的必要条件。贝尔曼方程是可以在整个环境中求解的线性方程。然而,求解这些方程的时间复杂度是 O (n 3 ) ,当环境中的状态数量较大时,这在计算上变得非常昂贵;有时,探索所有的州是不可行的,因为环境本身就很大。在这些情况下,我们需要考虑其他解决问题的方法。

在贝尔曼方程中,价值函数可以分解为两部分:

  • 立竿见影的奖励 R t+1 ,来自你最终将与之同归于尽的继承国
  • 继承州的折扣值 yv(S t+1 ) 从该时间步长开始,您将获得:

MDP 的网格世界示例:机器人导航任务生活在以下类型的网格世界中。一个障碍物显示为单元(2,2),机器人无法通过它导航。我们希望机器人移动到右上角的单元格(4,3),当它到达该位置时,机器人将获得+1 的奖励。机器人应该避开牢房(4,2),因为如果它进入那个牢房,它将获得-1 奖励。

机器人可以处于以下任何位置:

  • 11 个状态 -(除了细胞(2,2),其中我们有一个机器人的障碍)
  • A =

在现实世界中,机器人的动作是嘈杂的,机器人可能无法准确地移动到它被要求移动的地方。例子可能包括它的一些轮子打滑,它的零件连接松散,它有不正确的驱动器,等等。当被要求移动 1 米时,它可能不能准确移动 1 米;相反,它可以移动 90-105 厘米,以此类推。

在简化的网格世界中,机器人的随机动力学可以建模如下。如果我们命令机器人向北走,机器人有 10 %的几率向左拖动,有 10%的几率向右拖动。实际上只有 80%的时间可能是向北的。当机器人从墙上反弹(包括障碍物)并停留在同一位置时,不会发生任何事情:

这个网格世界示例中的每个状态都由(x,y)坐标表示。假设它处于状态(3,1),我们要求机器人向北移动,那么状态转移概率矩阵如下:

机器人停留在同一位置的概率为 0。

正如我们所知,所有状态转移概率总和总计为 1:

奖励功能:

对于所有其他状态,都有小的负奖励值,这意味着它会在绕着电网运行时为机器人的电池或燃料消耗充电,这将创建在达到奖励+1 的目标时不会浪费移动或时间的解决方案,这将鼓励机器人以尽可能少的燃料使用尽快达到目标。

当机器人达到+1 或-1 状态时,世界结束。达到任何一种状态后都不可能有更多的奖励;这些可以称为吸收状态。这些都是零成本吸收状态,机器人永远呆在那里。

MDP 工作模式:

  • 在状态 S 0
  • 选择a0T3】
  • 到达S1~ Ps0,a0
  • 选择a1T3】
  • 到达S2~ PT4S1a1T9】
  • 等等....

过了一段时间,需要所有的奖励和总结才能获得:

贴现因子模拟了一个经济应用,其中今天赚的一美元比明天赚的一美元更有价值。

机器人需要随时间选择动作(一个 0 ,一个 1 ,一个 2,....)最大化预期收益:

在此期间,强化学习算法学习一个策略,该策略是每个状态的动作映射,这意味着它是一个推荐的动作,机器人需要根据它存在的状态采取该动作:。

网格世界的最优策略:策略从状态映射到动作,也就是说,如果你处于一个特定的状态,你需要采取这个特定的动作。以下策略是使总回报或折扣奖励总和的期望值最大化的最优策略。策略总是查看当前状态,而不是以前的状态,这是马尔可夫属性:

一个棘手的问题是在位置(3,1):最优政策显示向左(西)而不是向北(北),这可能会有更少的州;然而,我们可能会进入一个更危险的状态。所以,左转可能需要更长的时间,但它会安全到达目的地,而不会陷入负面陷阱。这些类型的东西可以从计算中获得,这对于人类来说并不明显,但是计算机非常擅长提出这些策略:

定义: V π ,V,π**

V π =对于任何给定的保单π,价值函数为 V π : S - > R 使得 V π (S) 是从状态 S 开始的预期总收益,并执行π

网格世界的随机策略:以下是随机策略及其值函数的示例。这项政策是一项相当糟糕的负面政策。对于任何策略,我们都可以记下该特定策略的价值函数:

用简单的英语来说,贝尔曼方程说明当前状态的值等于应用于新状态的预期总回报的即时奖励和折扣因子(S’)乘以它们对这些状态采取行动(策略)的概率。

贝尔曼方程用于求解封闭形式策略的价值函数,给定固定策略,如何求解价值函数方程。

贝尔曼方程对价值函数施加了一组线性约束。原来,我们通过求解一组线性方程组来求解任意状态 S 下的价值函数。

有网格世界问题的贝尔曼方程示例:

为单元格 (3,1) 选择的策略是向北移动。然而,我们在系统中有随机性,大约 80%的时间它在所述方向上移动,并且 20% 的时间它侧向漂移,或者向左(10%)或者向右(10%)。

网格内 MDP 的所有 11 种状态都可以写出类似的方程。我们可以获得以下指标,我们将使用线性方程方法系统从这些指标中求解所有未知值:

  • 11 个方程
  • 11 个未知的价值函数变量
  • 11 项限制

这是用n方程求解一个n变量问题,对于这个问题,我们可以使用一个方程组很容易地找到精确的解的形式,从而得到由所有状态组成的网格的整个封闭形式的 V (π)的精确解。

动态规划

动态规划是一种通过将复杂问题分解为子问题并解决每个子问题来解决它们的顺序方法。一旦它解决了子问题,它就把这些子问题的解决方案放在一起,以解决原来的复杂问题。在强化学习领域,动态规划是一种求解方法,在给定环境的完美模型如马尔可夫决策过程(MDP)的情况下,计算最优策略。

动态编程适用于具有以下两个属性的问题。MDP 实际上满足这两个属性,这使得 DP 非常适合通过求解贝尔曼方程来解决它们:

  • 最优子结构
    • 优选原则适用
    • 最优解可以分解为子问题
  • 重叠子问题
    • 子问题重复出现多次
    • 解决方案可以缓存和重用
  • 幸运的是,MDP 同时满足这两个属性!
    • 贝尔曼方程具有状态值的递归分解
    • 价值函数存储和重用解决方案

然而,经典的动态规划算法在强化学习中的效用有限,这是因为它们假设了一个完美的模型和高计算开销。然而,这仍然很重要,因为它们为理解 RL 领域中的所有方法提供了必要的基础。

用动态规划计算最优策略的算法

利用动态规划计算 MDP 最优策略的标准算法如下,我们将在本章后面的部分详细介绍这两种算法:

  • 值迭代算法:迭代算法,对状态值进行迭代,直到达到最优值;并且随后,最佳值被用来确定最佳策略
  • 策略迭代算法:一种迭代算法,其中策略评估和策略改进交替使用以达到最优策略

值迭代算法:值迭代算法很容易计算,因为它只对状态值进行迭代应用。首先,我们将计算最优值函数 V* ,然后将这些值插入最优策略方程以确定最优策略。仅给出问题的大小,对于 11 个可能的州,每个州可以有四个政策(北-北、南-南、东-东、西),这给出了一个总体的 11 个 4 个可能的政策。值迭代算法由以下步骤组成:

  1. 初始化所有状态的 V(S) = 0
  2. 对于每个,更新:

  1. 通过重复计算步骤 2,我们将最终收敛到所有状态的最优值:

在算法的步骤 2 中,有两种更新值的方法

  • 同步更新 -通过执行同步更新(或贝尔曼备份操作符),我们将执行 RHS 计算,并用以下公式代替 LHS:

  • 异步更新 -一次更新一个状态的值,而不是同时更新所有状态,其中状态将以固定顺序更新(更新状态号 1,然后是 2,以此类推。).在收敛期间,异步更新比同步更新快一点。

网格世界上值迭代的说明示例:下图说明了值迭代在网格世界上的应用,本节末尾提供了解决一个实际问题的完整代码。在使用贝尔曼方程将先前的值迭代算法应用于 MDP 之后,我们获得了所有状态的以下最优值 V*(伽马值选择为 0.99 ):

当我们将这些值插入到我们的策略等式中时,我们获得了以下策略网格:

这里,在位置(3,1)处,我们想从数学上证明为什么最优政策建议向左(西)而不是向上(北):

Due to the wall, whenever the robot tries to move towards South (downwards side), it will remain in the same place, hence we assigned the value of the current position 0.71 for probability of 0.1.

同样,对于 north,我们计算总收益如下:

因此,最好是向西而不是向北移动,因此选择了最佳政策来这样做。

策略迭代算法:策略迭代是获得 MDP 最优策略的另一种方式,其中策略评估和策略改进算法被迭代地应用,直到解收敛到最优策略。策略迭代算法由以下步骤组成:

  1. 初始化随机策略π
  2. 重复执行以下操作,直到收敛
    • 使用线性方程组求解当前策略的贝尔曼方程以获得 V π :

  1. 通过重复这些步骤,价值和政策都将收敛到最佳值:

策略迭代倾向于处理较小的问题。如果一个 MDP 有大量的州,政策的反复计算将是昂贵的。因此,大型 MDP 倾向于使用价值迭代,而不是策略迭代。

如果我们在现实生活中不知道确切的状态转移概率呢例如 P s,aT5

我们需要使用以下简单公式从数据中估计概率:

如果对于某些状态没有可用的数据,这导致 0/0 问题,我们可以从均匀分布中获得默认概率。

使用基本 Python 的值和策略迭代算法的网格世界示例

经典的网格世界的例子已经被用来说明用动态规划求解 MDP 贝尔曼方程的价值和政策迭代。在下面的网格中,代理将从网格西南角的(1,1)位置开始,目标是向东北角移动,到达位置(4,3)。一旦达到目标,代理将获得+1 的奖励。在旅程中,它应该避开危险区域(4,2),因为这将给出奖励-1 的负面惩罚。代理无法从任何方向进入障碍物(2,2)所在的位置。目标区和危险区是终端状态,这意味着代理继续移动,直到它达到这两种状态之一。所有其他州的奖励是-0.02。在这里,任务是为每个状态(总共 11 个状态)的代理确定最优策略(移动方向),使代理的总回报最大,或者使代理能够尽快达到目标。代理可以朝 4 个方向移动:北、南、东、西。

完整的代码是用带有类实现的 Python 编程语言编写的。进一步阅读,请参考 Python 中的面向对象编程,了解类、对象、构造函数等。

导入random包,以生成 N、E、S、W 方向的移动:

>>> import random,operator

下面的argmax函数根据每个状态的值计算给定状态中的最大状态:

>>> def argmax(seq, fn):
...     best = seq[0]; best_score = fn(best)
...     for x in seq:
...         x_score = fn(x)
...     if x_score > best_score:
...         best, best_score = x, x_score
...     return best

为了在组件级别添加两个向量,使用了以下代码:

>>> def vector_add(a, b):
...     return tuple(map(operator.add, a, b))

方向提供了增量值,需要添加到代理的现有位置;方位可以应用于 x 轴或 y 轴:

>>> orientations = [(1,0), (0, 1), (-1, 0), (0, -1)]

下面的函数用于将代理转向正确的方向,因为我们知道,在每个命令中,代理在大约 80%的时间内都在那个方向上移动,而 10%的时间它会向右移动,10%的时间它会向左移动。:

>>> def turn_right(orientation):
...     return orientations[orientations.index(orientation)-1]
>>> def turn_left(orientation):
...     return orientations[(orientations.index(orientation)+1) % len(orientations)]
>>> def isnumber(x):
...     return hasattr(x, '__int__')

马尔可夫决策过程在这里被定义为一个类。每个 MDP 由初始位置、状态、转换模型、奖励函数和伽马值定义。

>>> class MDP:
... def __init__(self, init_pos, actlist, terminals, transitions={}, states=None, gamma=0.99):
...     if not (0 < gamma <= 1):
...         raise ValueError("MDP should have 0 < gamma <= 1 values")
...     if states:
...         self.states = states
...     else:
...         self.states = set()
...         self.init_pos = init_pos
...         self.actlist = actlist
...         self.terminals = terminals
...         self.transitions = transitions
...         self.gamma = gamma
...         self.reward = {}

返回该州的数字奖励:

... def R(self, state):
...     return self.reward[state]

带有从一个状态和一个动作的转换模型,返回每个状态的(概率、结果-状态)对列表:

... def T(self, state, action):
...     if(self.transitions == {}):
...         raise ValueError("Transition model is missing")
...     else:
...         return self.transitions[state][action]

可以在特定状态下执行的一组操作:

... def actions(self, state):
...     if state in self.terminals:
...         return [None]
...     else:
...         return self.actlist

GridMDP是为用每个状态、终端位置、初始位置和伽马值(折扣)的网格值来建模 2D 网格世界而创建的:

>>> class GridMDP(MDP):
... def __init__(self, grid, terminals, init_pos=(0, 0), gamma=0.99):

以下代码用于反转网格,因为我们希望在底部而不是顶部看到行 0 :

... grid.reverse()

以下__init__命令是网格类中用于初始化参数的构造函数:

... MDP.__init__(self, init_pos, actlist=orientations,
terminals=terminals, gamma=gamma)
... self.grid = grid
... self.rows = len(grid)
... self.cols = len(grid[0])
... for x in range(self.cols):
...     for y in range(self.rows):
...         self.reward[x, y] = grid[y][x]
...         if grid[y][x] is not None:
...             self.states.add((x, y))

状态转换向所需方向随机提供 80%,向左右方向随机提供 10%。这是为了模拟机器人在地板上滑动的随机性,等等:

... def T(self, state, action):
...     if action is None:
...         return [(0.0, state)]
...     else:
...         return [(0.8, self.go(state, action)),
...                (0.1, self.go(state, turn_right(action))),
...                (0.1, self.go(state, turn_left(action)))]

根据状态在有效状态列表中的位置,返回方向上的状态。如果下一个状态不在列表中,就像撞墙一样,那么代理应该保持相同的状态:

... def go(self, state, direction):
...     state1 = vector_add(state, direction)
...     return state1 if state1 in self.states else state

将(x,y)到 v 的映射转换为[[...,v,...]]网格:

... def to_grid(self, mapping):
...     return list(reversed([[mapping.get((x, y), None)
...                         for x in range(self.cols)]
...                         for y in range(self.rows)]))

将方向转换为箭头,以获得更好的图形表示:

... def to_arrows(self, policy):
...     chars = {(1, 0): '>', (0, 1): '^', (-1, 0): '<', (0, -1):
 'v', None: '.'}
...     return self.to_grid({s: chars[a] for (s, a) in policy.items()})

以下代码用于使用值迭代求解 MDP,并返回最佳状态值:

>>> def value_iteration(mdp, epsilon=0.001):
...     STSN = {s: 0 for s in mdp.states}
...     R, T, gamma = mdp.R, mdp.T, mdp.gamma
...     while True:
...         STS = STSN.copy()
...         delta = 0
...         for s in mdp.states:
...             STSN[s] = R(s) + gamma * max([sum([p * STS[s1] for 
...             (p, s1) in T(s,a)]) for a in mdp.actions(s)])
...             delta = max(delta, abs(STSN[s] - STS[s]))
...         if delta < epsilon * (1 - gamma) / gamma:
...             return STS

给定一个 MDP 和一个效用函数STS,确定最佳策略,作为从状态到动作的映射:

>>> def best_policy(mdp, STS):
...     pi = {}
...     for s in mdp.states:
...         pi[s] = argmax(mdp.actions(s), lambda a: expected_utility(a, s, STS, mdp))
...     return pi

根据 MDP 和 STS,在状态s下进行a的预期效用:

>>> def expected_utility(a, s, STS, mdp):
...     return sum([p * STS[s1] for (p, s1) in mdp.T(s, a)])

以下代码用于通过交替执行策略评估和策略改进步骤,使用策略迭代来解决 MDP 问题:

>>> def policy_iteration(mdp):
...     STS = {s: 0 for s in mdp.states}
...     pi = {s: random.choice(mdp.actions(s)) for s in mdp.states}
...     while True:
...         STS = policy_evaluation(pi, STS, mdp)
...         unchanged = True
...         for s in mdp.states:
...             a = argmax(mdp.actions(s),lambda a: expected_utility(a, s, STS, mdp))
...             if a != pi[s]:
...                 pi[s] = a
...                 unchanged = False
...         if unchanged:
...             return pi

以下代码用于使用近似值(修改后的策略迭代)将更新后的效用映射U从 MDP 的每个州返回到其效用:

>>> def policy_evaluation(pi, STS, mdp, k=20):
...     R, T, gamma = mdp.R, mdp.T, mdp.gamma
 ..     for i in range(k):
...     for s in mdp.states:
...         STS[s] = R(s) + gamma * sum([p * STS[s1] for (p, s1) in T(s, pi[s])])
...     return STS

>>> def print_table(table, header=None, sep=' ', numfmt='{}'):
...     justs = ['rjust' if isnumber(x) else 'ljust' for x in table[0]]
...     if header:
...         table.insert(0, header)
...     table = [[numfmt.format(x) if isnumber(x) else x for x in row]
...             for row in table]
...     sizes = list(map(lambda seq: max(map(len, seq)),
...                      list(zip(*[map(str, row) for row in table]))))
...     for row in table:
...         print(sep.join(getattr(str(x), j)(size) for (j, size, x)
...             in zip(justs, sizes, row)))

下面是一个 4 x 3 网格环境的输入网格,它向代理提出了一个顺序决策问题:

>>> sequential_decision_environment = GridMDP([[-0.02, -0.02, -0.02, +1],
...                                           [-0.02, None, -0.02, -1],
...                                           [-0.02, -0.02, -0.02, -0.02]],
...                                           terminals=[(3, 2), (3, 1)])

以下代码用于在给定的顺序决策环境中执行值迭代:

>>> value_iter = best_policy(sequential_decision_environment,value_iteration (sequential_decision_environment, .01))
>>> print("\n Optimal Policy based on Value Iteration\n")
>>> print_table(sequential_decision_environment.to_arrows(value_iter))

策略迭代的代码是:

>>> policy_iter = policy_iteration(sequential_decision_environment)
>>> print("\n Optimal Policy based on Policy Iteration & Evaluation\n")
>>> print_table(sequential_decision_environment.to_arrows(policy_iter))

从前面有两个结果的输出中,我们可以得出结论,值和策略迭代都为代理提供了相同的最优策略,使其能够以最快的方式跨越网格到达目标状态。当问题规模足够大时,在计算上最好选择价值迭代而不是策略迭代,因为在策略迭代中,我们需要在策略评估和策略改进的每次迭代中执行两个步骤。

蒙特卡罗方法

使用蒙特卡罗 ( MC )方法,我们将首先计算价值函数并确定最优策略。在这种方法中,我们不假设对环境有完全的了解。MC 只需要经验,经验由状态、动作和来自与环境的实际或模拟交互的奖励的样本序列组成。从实际经验中学习是惊人的,因为它不需要事先了解环境的动态,但仍能达到最佳行为。这与人类或动物如何从实际经验而不是任何数学模型中学习非常相似。令人惊讶的是,在许多情况下,很容易根据期望的概率分布生成经验样本,但以显式形式获得分布是不可行的。

蒙特卡罗方法解决了强化学习问题的基础上平均样本回报的每一集。这意味着我们假设经验被分成几集,无论选择什么动作,所有的集最终都会终止。只有在每集结束后,才会对值进行估计并更改策略。MC 方法是逐集递增的,但不是逐步递增的(这是一种在线学习,我们将在时间差异学习部分介绍)。

蒙特卡罗方法对整个事件中每个状态-动作对的样本和平均回报进行计算。然而,在同一个事件中,在一个阶段采取行动后的回报取决于后来各州采取的行动。因为所有的动作选择都在进行学习,所以从早期状态的角度来看,问题变得不稳定。为了处理这种非平稳性,我们采用了动态规划的策略迭代思想,首先,我们计算一个固定的任意策略的价值函数;后来,我们改进了政策。

动态规划与蒙特卡罗方法的比较

动态编程需要对环境或所有可能的转换有完整的了解,而蒙特卡罗方法只在一个事件中对采样的状态-动作轨迹进行工作。DP 只包括一步过渡,而 MC 则一直到剧集结尾到达终端节点。关于 MC 方法的一个重要事实是,每个状态的估计是独立的,这意味着一个状态的估计不建立在任何其他状态的估计之上,就像 DP 的情况一样。

MC 相对于 DP 方法的主要优势

以下是 MC 相对于 DP 方法的主要优势:

  • 就计算费用而言,MC 方法更有吸引力,因为它的优点是估计单个状态的值与状态的数量无关
  • 可以生成许多示例剧集,从感兴趣的状态开始,只对这些状态的返回进行平均,而忽略所有其他状态
  • MC 方法具有从实际经验或模拟经验中学习的能力

蒙特卡罗预测

我们知道,蒙特卡罗方法预测给定策略的状态值函数。任何状态的价值都是从该状态开始的预期回报或预期累积未来折扣奖励。这些值是用 MC 方法估算的,只是为了对访问该州后观察到的回报进行平均。随着观察到越来越多的值,平均值应该会根据大数定律收敛到期望值。事实上,这是适用于所有蒙特卡罗方法的原理。蒙特卡罗策略评估算法包括以下步骤:

  1. 初始化:

  1. 永远重复:
    • 使用π生成一集
    • 对于剧集中出现的每个状态 s :
      • 第一次出现 s 后返回
      • G 添加到退货中
      • 平均回报率

蒙特卡罗预测对网格世界问题的适用性

下图是为了说明而绘制的。然而,实际上,蒙特卡罗方法不能很容易地用于解决网格世界类型的问题,因为并不是所有的策略都保证终止。如果发现某个策略导致代理保持相同的状态,那么下一集将永远不会结束。像(State-Action-悬赏-State-Action ( SARSA ,我们将在 TD Learning Control 中的本章后半部分介绍)这样的分步学习方法没有这个问题,因为它们会在剧集中快速了解到这样的策略很差,然后切换到其他东西。

用 Python 模拟 21 点蒙特卡罗方法示例

流行的赌场纸牌游戏 21 点的目标是获得数值之和尽可能大而不超过 21 的牌。所有的牌面(国王、王后和杰克)都算 10,一张王牌可以算 1,也可以算 11,这取决于玩家想要的使用方式。只有 ace 有这个灵活性选项。所有其他卡片都是按面值计价的。游戏从发给庄家和玩家的两张牌开始。庄家的一张牌面朝上,另一张面朝下。如果玩家在前两张牌中有一张“自然 21”(一张王牌和一张 10 张牌),除非庄家也有一张“自然”,否则玩家获胜,在这种情况下,游戏是平局。如果玩家没有自然牌,那么他可以要求额外的牌,一张接一张(命中),直到他停止(卡住)或超过 21 张(破产)。如果玩家破产,他就输了;如果玩家坚持,那么就轮到庄家了。庄家根据固定策略打或粘,没有选择:庄家通常粘在 17 或更大的任何和上,否则打。如果庄家破产,那么玩家自动获胜。如果他坚持,结果要么是赢,要么是输,要么是平,这取决于庄家或玩家的总和是否接近 21。

二十一点问题可以公式化为一个插曲式的有限 MDP,其中二十一点的每一局都是一个插曲。在终端状态下,每一集的赢、输和抽分别给予+1、-1 和 0 的奖励,游戏状态下的剩余奖励给予 0 的值,不打折扣(gamma = 1)。所以终端奖励也是这个游戏的回报。我们从无限副牌中抽牌,这样就不存在可追踪的图案。整个游戏在下面的代码中用 Python 建模。

以下代码片段的灵感来自于张的 RL Python 代码,并在的学生理查德·萨顿的许可下发表在本书中,他是著名的强化:学习:简介 T5 的作者。

导入以下包用于阵列操作和可视化:

>>> from __future__ import print_function 
>>> import numpy as np 
>>> import matplotlib.pyplot as plt 
>>> from mpl_toolkits.mplot3d import Axes3D 

在每个回合,玩家或庄家可以采取一种可能的行动:要么打,要么站。这是仅有的两种可能的状态:

>>> ACTION_HIT = 0 
>>> ACTION_STAND = 1   
>>> actions = [ACTION_HIT, ACTION_STAND] 

玩家的策略是用 21 组数值模拟的,因为玩家在超过 21:

>>> policyPlayer = np.zeros(22) 

>>> for i in range(12, 20): 
...     policyPlayer[i] = ACTION_HIT 

如果玩家得到 20 或 21 的值,他就采取棍上策略,否则他将继续打一副牌来抽一张新卡:

>>> policyPlayer[20] = ACTION_STAND 
>>> policyPlayer[21] = ACTION_STAND 

玩家目标策略的函数形式:

>>> def targetPolicyPlayer(usableAcePlayer, playerSum, dealerCard): 
...     return policyPlayer[playerSum] 

玩家行为策略的函数形式:

>>> def behaviorPolicyPlayer(usableAcePlayer, playerSum, dealerCard): 
...     if np.random.binomial(1, 0.5) == 1: 
...         return ACTION_STAND 
...     return ACTION_HIT 

经销商的固定策略是继续下注,直到值为 17,然后在 17 到 21 之间下注:

>>> policyDealer = np.zeros(22) 
>>> for i in range(12, 17): 
...     policyDealer[i] = ACTION_HIT 
>>> for i in range(17, 22): 
...     policyDealer[i] = ACTION_STAND 

以下功能用于从一副牌中抽出一张新的替换牌:

>>> def getCard(): 
...     card = np.random.randint(1, 14) 
...     card = min(card, 10) 
...     return card 

让我们玩游戏吧!

>>> def play(policyPlayerFn, initialState=None, initialAction=None): 

  1. 玩家、玩家轨迹和玩家是否使用 ace 的总和为 11:
...     playerSum = 0 
...     playerTrajectory = [] 
...     usableAcePlayer = False 

  1. 抽卡的经销商状态:
...     dealerCard1 = 0 
...     dealerCard2 = 0 
...     usableAceDealer = False 

...     if initialState is None: 

  1. 生成随机初始状态:
...         numOfAce = 0 

  1. 初始化玩家的卡片:
...         while playerSum < 12: 

  1. 如果玩家的牌总数少于 12 张,则总是打一副抽牌:
...             card = getCard() 
...             if card == 1: 
...                 numOfAce += 1 
...                 card = 11 
...                 usableAcePlayer = True 
...             playerSum += card 

  1. 如果玩家的和大于 21,他必须至少持有一张王牌,但两张王牌也是可以的。在这种情况下,他将使用 ace 作为 1,而不是 11。如果玩家只有一张王牌,那么他就没有可用的王牌了:
...         if playerSum > 21: 
...             playerSum -= 10 
...             if numOfAce == 1: 
...                 usableAcePlayer = False 

  1. 初始化经销商卡:
...         dealerCard1 = getCard() 
...         dealerCard2 = getCard() 

...     else: 
...         usableAcePlayer = initialState[0] 
...         playerSum = initialState[1] 
...         dealerCard1 = initialState[2] 
...         dealerCard2 = getCard() 

  1. 初始化游戏状态:
...     state = [usableAcePlayer, playerSum, dealerCard1] 

  1. 初始化经销商的总和:
...     dealerSum = 0 
...     if dealerCard1 == 1 and dealerCard2 != 1: 
...         dealerSum += 11 + dealerCard2 
...         usableAceDealer = True 
...     elif dealerCard1 != 1 and dealerCard2 == 1: 
...         dealerSum += dealerCard1 + 11 
...         usableAceDealer = True 
...     elif dealerCard1 == 1 and dealerCard2 == 1: 
...         dealerSum += 1 + 11 
...         usableAceDealer = True 
...     else: 
...         dealerSum += dealerCard1 + dealerCard2 

  1. 游戏从这里开始,因为玩家需要从这里开始抽额外的牌:
...     while True: 
...         if initialAction is not None: 
...             action = initialAction 
...             initialAction = None 
...         else: 

  1. 根据玩家的当前总和采取行动:
...             action = policyPlayerFn(usableAcePlayer, playerSum, dealerCard1) 

  1. 跟踪玩家的轨迹进行重要性采样:
...         playerTrajectory.append([action, (usableAcePlayer, playerSum, dealerCard1)]) 

...         if action == ACTION_STAND: 
...             break 

  1. 如果要打出一副牌,则获得一张新牌:
...         playerSum += getCard() 

  1. 如果总和大于 21,玩家在这里崩溃,游戏结束,他得到-1 的奖励。然而,如果他有一张王牌,他可以用它来挽救比赛,否则他会输。
...         if playerSum > 21: 
...             if usableAcePlayer == True: 
...                 playerSum -= 10 
...                 usableAcePlayer = False 
...             else: 
...                 return state, -1, playerTrajectory 

  1. 现在轮到庄家了。他会根据一个总和抽牌:如果他到了 17 岁,他会停下来,否则继续抽牌。如果庄家也有王牌,他可以用它来达到破产的情况,否则他就破产了:
...     while True: 
...         action = policyDealer[dealerSum] 
...         if action == ACTION_STAND: 
...             break 
...         dealerSum += getCard() 
...         if dealerSum > 21: 
...             if usableAceDealer == True: 
...                 dealerSum -= 10 
...                 usableAceDealer = False 
...             else: 
...                 return state, 1, playerTrajectory 

  1. 现在,我们将玩家的总和与庄家的总和进行比较,以决定谁在不破产的情况下获胜:
...     if playerSum > dealerSum: 
...         return state, 1, playerTrajectory 
...     elif playerSum == dealerSum: 
...         return state, 0, playerTrajectory 
...     else: 
...         return state, -1, playerTrajectory 

以下代码说明了带有在线策略的蒙特卡罗示例:

>>> def monteCarloOnPolicy(nEpisodes): 
...     statesUsableAce = np.zeros((10, 10)) 
...     statesUsableAceCount = np.ones((10, 10)) 
...     statesNoUsableAce = np.zeros((10, 10)) 
...     statesNoUsableAceCount = np.ones((10, 10)) 
...     for i in range(0, nEpisodes): 
...         state, reward, _ = play(targetPolicyPlayer) 
...         state[1] -= 12 
...         state[2] -= 1 
...         if state[0]: 
...             statesUsableAceCount[state[1], state[2]] += 1 
...             statesUsableAce[state[1], state[2]] += reward 
...         else: 
...             statesNoUsableAceCount[state[1], state[2]] += 1 
...             statesNoUsableAce[state[1], state[2]] += reward 
...     return statesUsableAce / statesUsableAceCount, statesNoUsableAce / statesNoUsableAceCount 

下面的代码讨论了带有探索开始的蒙特卡罗,其中每个状态-动作对的所有回报都被累加和平均,而不管观察到它们时实施的是什么策略:

>>> def monteCarloES(nEpisodes): 
...     stateActionValues = np.zeros((10, 10, 2, 2)) 
...     stateActionPairCount = np.ones((10, 10, 2, 2)) 

行为策略是贪婪的,得到平均收益(s,a)的argmax:

...     def behaviorPolicy(usableAce, playerSum, dealerCard): 
...         usableAce = int(usableAce) 
...         playerSum -= 12 
...         dealerCard -= 1 
...         return np.argmax(stateActionValues[playerSum, dealerCard, usableAce, :] 
                      / stateActionPairCount[playerSum, dealerCard, usableAce, :]) 

播放将持续几集,每集随机初始化状态、动作和状态-动作对的更新值:

...     for episode in range(nEpisodes): 
...         if episode % 1000 == 0: 
...             print('episode:', episode) 
...         initialState = [bool(np.random.choice([0, 1])), 
...                        np.random.choice(range(12, 22)), 
...                        np.random.choice(range(1, 11))] 
...         initialAction = np.random.choice(actions) 
...         _, reward, trajectory = play(behaviorPolicy, initialState, initialAction) 
...         for action, (usableAce, playerSum, dealerCard) in trajectory: 
...             usableAce = int(usableAce) 
...             playerSum -= 12 
...             dealerCard -= 1 

更新状态-动作对的值:

...             stateActionValues[playerSum, dealerCard, usableAce, action] += reward 
...             stateActionPairCount[playerSum, dealerCard, usableAce, action] += 1 
...     return stateActionValues / stateActionPairCount 

打印状态值:

>>> figureIndex = 0 
>>> def prettyPrint(data, tile, zlabel='reward'): 
...     global figureIndex 
...     fig = plt.figure(figureIndex) 
...     figureIndex += 1 
...     fig.suptitle(tile) 
...     ax = fig.add_subplot(111, projection='3d') 
...     x_axis = [] 
...     y_axis = [] 
...     z_axis = [] 
...     for i in range(12, 22): 
...         for j in range(1, 11): 
...             x_axis.append(i) 
...             y_axis.append(j) 
...             z_axis.append(data[i - 12, j - 1]) 
...     ax.scatter(x_axis, y_axis, z_axis,c='red') 
...     ax.set_xlabel('player sum') 
...     ax.set_ylabel('dealer showing') 
...     ax.set_zlabel(zlabel) 

10,000 和 500,000 次迭代有或没有可用 ace 的策略结果:

>>> def onPolicy(): 
...     statesUsableAce1, statesNoUsableAce1 = monteCarloOnPolicy(10000) 
...     statesUsableAce2, statesNoUsableAce2 = monteCarloOnPolicy(500000) 
...     prettyPrint(statesUsableAce1, 'Usable Ace & 10000 Episodes') 
...     prettyPrint(statesNoUsableAce1, 'No Usable Ace & 10000 Episodes') 
...     prettyPrint(statesUsableAce2, 'Usable Ace & 500000 Episodes') 
...     prettyPrint(statesNoUsableAce2, 'No Usable Ace & 500000 Episodes') 
...     plt.show() 

策略迭代的优化或蒙特卡罗控制:

>>> def MC_ES_optimalPolicy(): 
...     stateActionValues = monteCarloES(500000) 
...     stateValueUsableAce = np.zeros((10, 10)) 
...     stateValueNoUsableAce = np.zeros((10, 10)) 
    # get the optimal policy 
...     actionUsableAce = np.zeros((10, 10), dtype='int') 
...     actionNoUsableAce = np.zeros((10, 10), dtype='int') 
...     for i in range(10): 
...         for j in range(10): 
...             stateValueNoUsableAce[i, j] = np.max(stateActionValues[i, j, 0, :]) 
...             stateValueUsableAce[i, j] = np.max(stateActionValues[i, j, 1, :]) 
...             actionNoUsableAce[i, j] = np.argmax(stateActionValues[i, j, 0, :]) 
...             actionUsableAce[i, j] = np.argmax(stateActionValues[i, j, 1, :]) 
...     prettyPrint(stateValueUsableAce, 'Optimal state value with usable Ace') 
...     prettyPrint(stateValueNoUsableAce, 'Optimal state value with no usable Ace') 
...     prettyPrint(actionUsableAce, 'Optimal policy with usable Ace', 'Action (0 Hit, 1 Stick)') 
...     prettyPrint(actionNoUsableAce, 'Optimal policy with no usable Ace', 'Action (0 Hit, 1 Stick)') 
...     plt.show() 

# Run on-policy function 
>>> onPolicy()

从前面的图表中,我们可以得出结论,一手牌中的可用王牌即使在玩家总和较低的组合中也能给出高得多的奖励,而对于没有可用王牌的玩家来说,如果这些值小于 20,就赢得的奖励而言,这些值是非常不同的。

# Run Monte Carlo Control or Explored starts 
>>> MC_ES_optimalPolicy() 

从最优策略和状态值,我们可以得出这样的结论:如果有一张可用的王牌,我们可以打得比棒还多,而且与手里没有王牌时相比,状态值的奖励要高得多。虽然我们谈论的结果是显而易见的,但我们可以看到手握王牌的影响有多大。

时间差异学习

时间差异 ( TD )学习是强化学习的中心和新颖主题。TD 学习是蒙特卡洛 ( MC )和动态规划 ( DP )思想的结合。与蒙特卡罗方法一样,时域方法可以直接从经验中学习,而不需要环境模型。与动态规划类似,TD 方法部分基于其他学习到的估计值更新估计值,而不需要等待最终结果,这与 MC 方法不同,后者仅在达到最终结果后更新估计值。

蒙特卡罗方法与时间差分学习的比较

虽然蒙特卡罗方法和时间差分学习有相似之处,但是时间差分学习相对于蒙特卡罗方法有其固有的优势。

| 蒙特卡罗方法 | 时间差学习 |
| MC 必须等到剧集结束后才能知道回归。 | TD 每走一步都可以在线学习,不需要等到剧集结束。 |
| MC 具有高方差和低偏差。 | TD 具有低方差和一些体面的偏见。 |
| MC 没有利用马尔可夫特性。 | 道明利用了马尔可夫特性。 |

TD 预测

TD 和 MC 都是用经验解决 z 预测问题。给定一些策略π,两种方法都更新了它们对在该体验中出现的非终端状态 S t 的估计vvT6】π。蒙特卡罗方法等到访问后的返回已知,然后使用该返回作为 V(S t ) 的目标。

前面的方法可以称为常数- α MC ,其中 MC 必须等到剧集结束后才能确定增量为 V(S t ) (这时才知道 G t )。

TD 方法只需要等到下一个时间步。在时间 t+1 时,他们立即形成一个目标,并使用观察到的奖励Rt+1T5】和估计 V(S t+1 ) 进行有用的更新。最简单的 TD 方法,称为 TD(0) ,是:

MC 更新的目标是GtT3,而 TD 更新的目标是Rt+1+y V(St+1)

在下图中,对 TD 和 MC 方法进行了比较。正如我们在方程 TD(0)中写的,我们使用真实数据的一个步骤,然后使用下一个状态的价值函数的估计值。同样,我们也可以用两步真实数据来更好地了解现实,并估计第三阶段的价值函数。然而,随着我们增加步骤,最终需要越来越多的数据来执行参数更新,这将花费越多的时间。当我们在每一集中采取无限步直到它接触到更新参数的终点时,TD 就变成了蒙特卡罗方法。

估算 v 算法的 TD (0)由以下步骤组成:

  1. 初始化:

  1. 重复(每集):
    • 初始化 S
    • 重复(每集的每一步):
      • π给 S 的
      • 采取行动 A,观察 R,S'
  2. 直到 S 结束。

道明学习的驾驶办公室示例

在这个简单的例子中,你每天从家到办公室,你试图预测早上到达办公室需要多长时间。当你离开家时,你会注意到时间、一周中的哪一天、天气(是否下雨、刮风等等)以及你认为相关的任何其他参数。例如,在周一早上,你正好在早上 8 点离开,你估计需要 40 分钟才能到达办公室。早上 8 点 10 分,你注意到有一个贵宾经过,你需要等到完整的车队已经出发,所以你重新估计从那以后需要 45 分钟,或者总共需要 55 分钟。15 分钟后,你及时完成了旅程的高速公路部分。现在你进入一条绕行道路,你现在把你的总旅行时间减少到 50 分钟。不幸的是,此时,你被困在一堆牛车后面,道路太窄,无法通过。你最终不得不跟随那些牛车,直到你在 8:50 转到你办公室所在的小街上。七分钟后,你到达你的办公室停车场。状态、时间和预测的顺序如下:

本例中的奖励是旅程中每一段经过的时间,我们使用的是折扣系数(gamma, v = 1 ,因此每个状态的回报是从该状态到目的地(办公室)的实际时间。每个状态的值是预测的到达时间,这是上表中的第二列,也就是所遇到的每个状态的当前估计值。

在上图中,蒙特卡罗用于绘制事件序列的预测总时间。箭头始终显示常数-α MC 方法推荐的预测变化。这些是每个阶段的估计值和实际回报(57 分钟)之间的误差。在 MC 方法中,学习只发生在完成之后,为此需要等到 57 分钟过去。然而,在现实中,你可以在达到最终结果之前进行估计,并相应地修正你的估计。道明的工作原理是一样的,在每个阶段,它都试图相应地预测和修正估计值。所以,TD 方法是马上学的,不需要等到最后的结果。事实上,这就是人类在现实生活中的预测。由于这些积极的特性,TD 学习被认为是强化学习中的新方法。

南亚区域合作联盟政策贸易发展控制

状态-动作-奖励-状态-动作 ( SARSA )是一个策略上的 TD 控制问题,其中策略将使用策略迭代(GPI)进行优化,只有时间 TD 方法用于评估预测的策略。第一步,算法学习一个 SARSA 函数。特别地,对于策略上的方法,我们使用用于学习 v π的 TD 方法来估计当前行为策略π以及所有状态和动作(a)的 q π (s,a)现在,我们考虑从状态-动作对到状态-动作对的转换,并学习状态-动作对的值:

该更新在从非终端状态StT3】的每次转换之后完成。如果 S t+1 为端子,那么 Q (S t+1、 A t+1 ) 定义为零。这个规则使用五个事件的每一个元素(StT17】、AtT21】、 RtSt+1T27】、*At+1T31】,它们构成了从一个状态-动作对到下一个状态-动作对的过渡。这种五重组合产生了该算法的名称 SARSA。*****

与所有策略上的方法一样,我们不断地为行为策略π估计 q π ,同时相对于 q π向贪婪方向改变π。SARSA 的计算算法如下:

  1. 初始化:

  1. 重复(每集):
    • 初始值设定项
    • 使用从 Q 派生的策略从 S 中选择 A(例如,ε-贪婪)
    • 重复(每集的每一步):
      • 采取行动 A ,观察 R,S'
      • 从使用源自 Q 的策略中选择A’(例如ε - greedy)
        ** * *

** 直到 S 结束*

*# 问-学习-脱离策略 TD 控制

q 学习是许多强化学习问题在实际应用中最常用的方法。偏离策略的 TD 控制算法被称为 Q 学习。在这种情况下,学习到的动作值函数 Q 直接逼近最佳动作值函数,而与所遵循的策略无关。这种近似简化了算法的分析,并使早期收敛证明成为可能。该策略仍然有效,因为它决定了访问和更新哪些状态-动作对。然而,正确收敛所需要的只是继续更新所有对。正如我们所知,这是一个最低要求,因为任何保证在一般情况下找到最优行为的方法都必须要求它。收敛的算法如下所示:

  1. 初始化:

  1. 重复(每集):
    • 初始值设定项
    • 重复(每集的每一步):
      • 使用从 Q 派生的策略从 S 中选择 A(例如,ε -贪婪)
      • 采取行动 A,观察 R,S'
  2. 直到 S 结束

道明控制有政策和无政策的悬崖行走实例

一个悬崖行走网格世界的例子被用来比较 SARSA 和 Q 学习,以突出政策上(SARSA)和政策下(Q 学习)方法之间的差异。这是一个标准的未折扣的、不连续的任务,有开始和结束目标状态,允许在四个方向(北、西、东、南)移动。奖励-1 用于除标记为悬崖的区域之外的所有过渡,踩下该区域将对代理处以奖励-100 的惩罚,并立即将代理送回开始位置。

以下代码片段从张的 RL Python 代码中获得灵感,并经强化学习的著名作者 Richard S. Sutton 的学生许可,在本书中发布(详情请参见阅读部分):

# Cliff-Walking - TD learning - SARSA & Q-learning 
>>> from __future__ import print_function 
>>> import numpy as np 
>>> import matplotlib.pyplot as plt 

# Grid dimensions 
>>> GRID_HEIGHT = 4 
>>> GRID_WIDTH = 12 

# probability for exploration, step size,gamma  
>>> EPSILON = 0.1 
>>> ALPHA = 0.5 
>>> GAMMA = 1 

# all possible actions 
>>> ACTION_UP = 0; ACTION_DOWN = 1;ACTION_LEFT = 2;ACTION_RIGHT = 3 
>>> actions = [ACTION_UP, ACTION_DOWN, ACTION_LEFT, ACTION_RIGHT] 

# initial state action pair values 
>>> stateActionValues = np.zeros((GRID_HEIGHT, GRID_WIDTH, 4)) 
>>> startState = [3, 0] 
>>> goalState = [3, 11] 

# reward for each action in each state 
>>> actionRewards = np.zeros((GRID_HEIGHT, GRID_WIDTH, 4)) 
>>> actionRewards[:, :, :] = -1.0 
>>> actionRewards[2, 1:11, ACTION_DOWN] = -100.0 
>>> actionRewards[3, 0, ACTION_RIGHT] = -100.0 

# set up destinations for each action in each state 
>>> actionDestination = [] 
>>> for i in range(0, GRID_HEIGHT): 
...     actionDestination.append([]) 
...     for j in range(0, GRID_WIDTH): 
...         destinaion = dict() 
...         destinaion[ACTION_UP] = [max(i - 1, 0), j] 
...         destinaion[ACTION_LEFT] = [i, max(j - 1, 0)] 
...         destinaion[ACTION_RIGHT] = [i, min(j + 1, GRID_WIDTH - 1)] 
...         if i == 2 and 1 <= j <= 10: 
...             destinaion[ACTION_DOWN] = startState 
...         else: 
...             destinaion[ACTION_DOWN] = [min(i + 1, GRID_HEIGHT - 1), j] 
...         actionDestination[-1].append(destinaion) 
>>> actionDestination[3][0][ACTION_RIGHT] = startState 

# choose an action based on epsilon greedy algorithm 
>>> def chooseAction(state, stateActionValues): 
...     if np.random.binomial(1, EPSILON) == 1: 
...         return np.random.choice(actions) 
...     else: 
...         return np.argmax(stateActionValues[state[0], state[1], :]) 

# SARSA update 

>>> def sarsa(stateActionValues, expected=False, stepSize=ALPHA): 
...     currentState = startState 
...     currentAction = chooseAction(currentState, stateActionValues) 
...     rewards = 0.0 
...     while currentState != goalState: 

...         newState = actionDestination[currentState[0]][currentState[1]] [currentAction] 

...         newAction = chooseAction(newState, stateActionValues) 
...         reward = actionRewards[currentState[0], currentState[1], currentAction] 
...         rewards += reward 
...         if not expected: 
...             valueTarget = stateActionValues[newState[0], newState[1], newAction] 
...         else: 
...             valueTarget = 0.0 
...             actionValues = stateActionValues[newState[0], newState[1], :] 
...             bestActions = np.argwhere(actionValues == np.max(actionValues)) 
...             for action in actions: 
...                 if action in bestActions: 

...                     valueTarget += ((1.0 - EPSILON) / len(bestActions) + EPSILON / len(actions)) * stateActionValues[newState[0], newState[1], action] 

...                 else: 
...                     valueTarget += EPSILON / len(actions) * stateActionValues[newState[0], newState[1], action] 
...         valueTarget *= GAMMA 
...         stateActionValues[currentState[0], currentState[1], currentAction] += stepSize * (reward+ valueTarget - stateActionValues[currentState[0], currentState[1], currentAction]) 
...         currentState = newState 
...         currentAction = newAction 
...     return rewards 

# Q-learning update 
>>> def qlearning(stateActionValues, stepSize=ALPHA): 
...     currentState = startState 
...     rewards = 0.0 
...     while currentState != goalState: 
...         currentAction = chooseAction(currentState, stateActionValues) 
...         reward = actionRewards[currentState[0], currentState[1], currentAction] 
...         rewards += reward 
...         newState = actionDestination[currentState[0]][currentState[1]] [currentAction] 
...         stateActionValues[currentState[0], currentState[1], currentAction] += stepSize * (reward + GAMMA * np.max(stateActionValues[newState[0], newState[1], :]) - 
...             stateActionValues[currentState[0], currentState[1], currentAction]) 
...         currentState = newState 
...     return rewards 

# print optimal policy 
>>> def printOptimalPolicy(stateActionValues): 
...     optimalPolicy = [] 
...     for i in range(0, GRID_HEIGHT): 
...         optimalPolicy.append([]) 
...         for j in range(0, GRID_WIDTH): 
...             if [i, j] == goalState: 
...                 optimalPolicy[-1].append('G') 
...                 continue 
...             bestAction = np.argmax(stateActionValues[i, j, :]) 
...             if bestAction == ACTION_UP: 
...                 optimalPolicy[-1].append('U') 
...             elif bestAction == ACTION_DOWN: 
...                 optimalPolicy[-1].append('D') 
...             elif bestAction == ACTION_LEFT: 
...                 optimalPolicy[-1].append('L') 
...             elif bestAction == ACTION_RIGHT: 
...                 optimalPolicy[-1].append('R') 
...     for row in optimalPolicy: 
...         print(row) 

>>> def SARSAnQLPlot(): 
    # averaging the reward sums from 10 successive episodes 
...     averageRange = 10 

    # episodes of each run 
...     nEpisodes = 500 

    # perform 20 independent runs 
...     runs = 20 

...     rewardsSarsa = np.zeros(nEpisodes) 
...     rewardsQlearning = np.zeros(nEpisodes) 
...     for run in range(0, runs): 
...         stateActionValuesSarsa = np.copy(stateActionValues) 
...         stateActionValuesQlearning = np.copy(stateActionValues) 
...         for i in range(0, nEpisodes): 
            # cut off the value by -100 to draw the figure more elegantly 
...             rewardsSarsa[i] += max(sarsa(stateActionValuesSarsa), -100) 
...             rewardsQlearning[i] += max(qlearning(stateActionValuesQlearning), -100) 

    # averaging over independent runs 
...     rewardsSarsa /= runs 
...     rewardsQlearning /= runs 

    # averaging over successive episodes 
...     smoothedRewardsSarsa = np.copy(rewardsSarsa) 
...     smoothedRewardsQlearning = np.copy(rewardsQlearning) 
...     for i in range(averageRange, nEpisodes): 
...         smoothedRewardsSarsa[i] = np.mean(rewardsSarsa[i - averageRange: i + 1]) 
...         smoothedRewardsQlearning[i] = np.mean(rewardsQlearning[i - averageRange: i + 1]) 

    # display optimal policy 
...     print('Sarsa Optimal Policy:') 
...     printOptimalPolicy(stateActionValuesSarsa) 
...     print('Q-learning Optimal Policy:') 
...     printOptimalPolicy(stateActionValuesQlearning) 

    # draw reward curves 
...     plt.figure(1) 
...     plt.plot(smoothedRewardsSarsa, label='Sarsa') 
...     plt.plot(smoothedRewardsQlearning, label='Q-learning') 
...     plt.xlabel('Episodes') 
...     plt.ylabel('Sum of rewards during episode') 
...     plt.legend() 

# Sum of Rewards for SARSA versus Qlearning 
>>> SARSAnQLPlot() 

在最初的过渡之后,Q-learning 学习最优策略的值以沿着最优路径行走,在该路径中,代理沿着悬崖边缘行进。不幸的是,这会导致偶尔因为ε-贪婪的动作选择而掉下悬崖。而另一方面,SARSA 将动作选择考虑在内,并通过网格的上部学习更长、更安全的路径。虽然 Q-learning 学习到了最优策略的价值,但它的在线性能比 SARSA 差,后者学习的是迂回且最安全的策略。即使我们观察下图中显示的以下奖励总和,SARSA 在该集期间的奖励负总和也比 Q-learning 少。

融合机器学习和深度学习的强化学习应用

强化学习与机器学习或深度学习相结合,为近年来的各种前沿问题创造了最先进的人工智能解决方案。代码示例的完整解释超出了本书的范围,但是我们将为您提供这些技术内部的高级视图。以下是该领域最流行和已知的最新趋势,但应用并不仅限于此:

  • 汽车控制(自动驾驶汽车)
  • 谷歌深度思维阿尔法围棋游戏
  • 机器人技术(以足球为例)

汽车控制-自动驾驶汽车

自动驾驶汽车是这个行业的新趋势,许多科技巨头现在都在这个领域工作。深度学习技术,如卷积神经网络,用于学习控制动作的 Q 函数,如向前、向后、向左和向右转弯等,通过混合和匹配从可用的动作空间。整个算法被称为一个 DQN ( DeepQ Network )。这种方法可以用于玩雅达利、赛车等游戏。详见斯坦福大学四月余拉斐尔·佩莱斯斯基-史密斯里什·贝迪的论文模拟自主车辆控制深度强化学习

谷歌 DeepMind 的 AlphaGo

谷歌 DeepMind 的 AlphaGo 是人工智能领域的一个新轰动,因为许多行业专家曾预测,击败人类玩家需要大约 10 年的时间,但 AlphaGo 战胜人类的事实证明他们错了。围棋的主要复杂性在于其穷尽的搜索空间:假设 b 是游戏的广度, d 是游戏的深度,这意味着围棋要探索的组合是( b ~250, d ~150),而象棋则是( b ~35, d ~80)。这清楚地表明了围棋在复杂性上的不同。事实上,IBM 深蓝在 1997 年使用蛮力或穷举搜索技术击败了加里·卡斯帕罗夫,这在围棋游戏中是不可能的。

AlphaGo 使用价值网络评估董事会头寸,使用政策网络选择移动。神经网络在最先进的蒙特卡罗树搜索程序级别上运行,用于模拟和估计搜索树中每个状态的值。进一步阅读,请参考大卫·西尔弗等人的论文掌握深度神经网络和树搜索围棋,摘自谷歌深度思维

机器人足球

机器人学作为一个强化学习领域,与大多数研究得很好的强化学习标准问题有很大不同。机器人学中的问题通常最好用高维、连续的状态和动作来表示。机器人强化学习中常见的 10-30 维连续动作被认为很大。强化学习在机器人技术上的应用涉及许多挑战,包括考虑真实物理系统的无噪声环境,以及通过真实世界的经验进行学习可能成本高昂。因此,算法或流程需要足够健壮,以完成必要的工作。此外,为指导学习系统的环境生成奖励值和奖励函数将是困难的。

虽然机器人强化学习有多种建模方法,但一种实用的价值函数逼近方法使用多层感知器来学习各种子任务,如学习防御、拦截、位置控制、踢腿、运动速度控制、运球和罚球。更多详情,请参考论文机器人学中的强化学习:一项调查,作者为詹斯·科伯安德鲁·巴格内尔扬·彼得斯

有很多内容需要涵盖,这本书作为强化学习的介绍,而不是详尽的讨论。感兴趣的读者请浏览进一步阅读部分的资源。我们希望你会喜欢它!

进一步阅读

强化学习有许多经典资源,我们鼓励读者浏览这些资源:

摘要

在本章中,您学习了各种强化学习技术,如马尔可夫决策过程、贝尔曼方程、动态规划、蒙特卡罗方法、时间差分学习,包括策略内(SARSA)和策略外(Q-learning),并通过 Python 示例以实用的方式了解其实现。您还学习了 Q-learning 如今在许多实际应用中是如何使用的,因为这种方法是通过与环境交互从反复试验中学习的。

接下来,我们研究了机器学习强化学习的一些其他实际应用,以及用于解决最新问题的深度学习。

最后,如果你想全职从事强化学习,已经为你提供了进一步的阅读。我们祝你一切顺利!*

posted @ 2025-09-04 14:14  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报