Python-数据科学秘籍-全-

Python 数据科学秘籍(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

如今,我们生活在一个万物互联的世界里,海量数据不断生成,而人类无法对所有数据进行分析并作出决策。人类的决策越来越多地被计算机做出的决策所替代,这得益于数据科学领域。数据科学深刻渗透到我们互联的世界中,市场上对那些不仅能彻底理解数据科学算法,还能编程实现这些算法的人才需求日益增长。数据科学是一个交叉学科的领域,涉及数据挖掘、机器学习、统计学等多个方面。这给各级数据科学家带来了巨大压力——无论是那些渴望成为数据科学家的人,还是目前已经从事这一领域的实践者。将这些算法视为黑箱并在决策系统中使用,会导致事与愿违的结果。在无数算法和复杂问题面前,必须深刻理解底层算法,才能为任何问题选择最合适的算法。

Python 作为一门编程语言,多年来不断发展,今天已成为数据科学家的首选语言。它不仅能够作为脚本语言快速构建原型,还具备用于完整软件开发的复杂语言结构,结合其强大的数值计算库支持,使其在数据科学家和广泛的科学编程社区中获得了极大的流行。不仅如此,Python 在 Web 开发者中也非常受欢迎,得益于如 Django 和 Flask 这样的框架。

本书经过精心编写,旨在满足不同数据科学家的需求——从初学者到有经验的数据科学家——通过精心设计的案例,涵盖了数据科学的不同方面,包括数据探索、数据分析与挖掘、机器学习和大规模机器学习。每一章都精心设计了案例,探索这些方面。书中提供了足够的数学知识,帮助读者深入理解算法的运作原理。必要时,还为好奇的读者提供了充分的参考资料。这些案例的编写方式简洁易懂,便于读者跟随与理解。

本书将数据科学的艺术与强大的 Python 编程带给读者,帮助他们掌握数据科学的核心概念。读者无需具备 Python 知识即可阅读本书。非 Python 程序员可以参考第一章,介绍 Python 的数据结构和函数式编程概念。

早期章节讲解数据科学的基础知识,后续章节则专注于高级数据科学算法。书中详细介绍了当前在行业中被领先数据科学家广泛使用的最先进的算法,包括集成方法、随机森林、带正则化的回归等。书中还详细介绍了一些在学术界非常流行、但在主流应用中尚未广泛推广的算法,如旋转森林。

在当今市场上有很多关于数据科学的自学书籍,我们认为在涵盖数据科学算法背后的数学哲学与实现细节的正确平衡上存在空白。本书试图填补这一空白。在每个章节中,都会提供足够的数学介绍,让读者思考算法的原理;我相信读者能够在自己的应用中充分受益于这些方法。

需要注意的是,这些方法是为了向读者解释数据科学算法而编写的。它们并未在极端条件下经过严格的测试,因此未必适用于生产环境。生产级数据科学代码必须经过严格的工程化流程。

本书既可以作为学习数据科学方法的指南,也可以作为快速参考手册。它是一本自成体系的书,旨在向没有编程背景的新读者介绍数据科学,并帮助他们成为该领域的专家。

本书内容概述

第一章,数据科学中的 Python,介绍了 Python 内置的数据结构和函数,这些在数据科学编程中非常实用。

第二章,Python 环境,介绍了 Python 的科学编程和绘图库,包括 NumPy、matplotlib 和 scikit-learn。

第三章,数据分析 – 探索与处理,涵盖了数据预处理和转换的常用方法,用于执行探索性数据分析任务,从而高效地构建数据科学算法。

第四章,数据分析 – 深入剖析,介绍了降维概念,以应对数据科学中的维度灾难问题。首先介绍简单的方法,然后详细讨论了先进的最前沿降维技术。

第五章,数据挖掘 – 大海捞针,讨论了无监督数据挖掘技术,从对距离方法和核方法的详细讨论开始,接着是聚类和异常值检测技术。

第六章,机器学习 1,介绍了监督数据挖掘技术,包括最近邻、朴素贝叶斯和分类树。起初,我们将重点强调监督学习中的数据准备。

第七章,机器学习 2,介绍了回归问题,并接着讲解了包括 LASSO 和岭回归在内的正则化方法。最后,我们将讨论交叉验证技术,作为选择这些方法超参数的一种方式。

第八章,集成方法,介绍了包括 bagging、boosting 和梯度提升在内的各种集成技术。本章将展示如何通过构建一个集成模型或多个模型来实现一个强大的、先进的数据科学方法,而不是为给定问题构建单一模型。

第九章,构建树,介绍了一些基于树的算法的 bagging 方法。由于它们对噪声的鲁棒性以及对各种问题的普适性,它们在数据科学社区中非常流行。

第十章,大规模机器学习 - 在线学习,涵盖了大规模机器学习以及适用于处理此类大规模问题的算法。包括可以处理流数据和无法完全加载到内存中的数据的算法。

本书所需的工具

本书中的所有实验方法均在配备 Intel i7 处理器、运行 Windows 7 64 位操作系统的 8 GB 内存机器上进行开发和测试。

本书中的开发方法使用了 Python 2.7.5、NumPy 1.8.0、SciPy 0.13.2、Matplotlib 1.3.1、NLTK 3.0.2 和 scikit-learn 0.15.2 版本。

相同的代码也应该在 Linux 变种和 Mac 上运行,只要安装了这里提到的相应库。或者,可以创建一个包含这些库版本的 Python 虚拟环境,你可以在其中运行所有实验。

本书适合的人群

本书面向各类数据科学专业人士,无论是学生还是从业者,从初学者到专家。本书的不同配方满足不同读者的需求。初学者可以在前五章花些时间熟悉数据科学。专家可以参考后面的章节,了解如何使用 Python 实现高级技术。本书涵盖了适当的数学内容,并为希望理解数据科学的计算机程序员提供必要的参考资料。非 Python 背景的读者也能有效使用本书。书的第一章介绍了 Python 作为数据科学的编程语言。如果你有一些基础编程经验将更有帮助。本书大部分内容是自足的,旨在向新读者介绍数据科学,并帮助他们成为该领域的专家。

章节

在本书中,你会找到几个常见的标题(准备工作、如何操作、工作原理、还有更多、另见)。

为了清晰地说明如何完成一个配方,我们使用以下这些部分:

准备工作

本节告诉你在配方中应期待什么,并描述如何设置任何软件或配方所需的初步设置。

如何操作…

本节包含完成配方所需的步骤。

工作原理…

本节通常包括对上一节内容的详细解释。

还有更多…

本节包含配方的附加信息,旨在让读者更深入了解该配方。

另见

本节提供了配方的其他有用链接。

约定

在本书中,你会发现几种文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义说明。

文本中的代码词,如函数名,展示如下:

我们调用get_iris_data()函数来获取输入数据。我们使用 Scikit learn 的train_test_split函数来将输入数据集分成两个部分。

代码块设置如下:

        # Shuffle the dataset
        shuff_index = np.random.shuffle(range(len(y)))
        x_train = x[shuff_index,:].reshape(x.shape)
        y_train = np.ravel(y[shuff_index,:])

公式通常以图像的形式呈现,如下所示,

约定

通常,数学部分在每个配方的开始处引入。在一些章节中,本章大部分配方所需的常见数学知识会包含在第一个配方的引言部分。

外部网址如下所示:

scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html

针对第三方库中一些算法实现的具体调用细节如下所示。

“输入样本的预测类别被计算为具有最高平均预测概率的类别。如果基础估计器没有实现 predict_proba 方法,则使用投票法。”

在适用的地方,提供科学期刊和论文的参考文献,如下所示:

请参阅 Leo Breiman 的论文,获取有关 Bagging 的更多信息。

Leo Breiman. 1996. Bagging predictors.Mach. Learn.24, 2 (August 1996), 123-140. DOI=10.1023/A:1018054314350 http://dx.doi.org/10.1023/A:1018054314350

程序输出和图表通常作为图片提供。例如:

约定

任何命令行输入或输出如下所示:

Counter({'Peter': 4, 'of': 4, 'Piper': 4, 'pickled': 4, 'picked': 4, 'peppers': 4, 'peck': 4, 'a': 2, 'A': 1, 'the': 1, 'Wheres': 1, 'If': 1})

在我们希望读者检查 Python shell 中的某些变量时,通常会指定如下:

>>> print b_tuple[0]
1
>>> print b_tuple[-1]
c
>>> 

注意

警告或重要说明会以类似这样的框出现。

小贴士

提示和技巧通常以这种形式呈现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对本书的看法——您喜欢或不喜欢什么。读者反馈对我们非常重要,因为它帮助我们开发出您能够最大程度受益的书籍。

若要向我们发送一般反馈,请通过电子邮件发送到<feedback@packtpub.com>,并在邮件主题中提到本书的标题。

如果您在某个领域拥有专业知识,并且有兴趣撰写或参与书籍的编写,请参考我们的作者指南:www.packtpub.com/authors

客户支持

现在,作为 Packt 书籍的骄傲拥有者,我们提供一些帮助,帮助您最大程度地从您的购买中获益。

下载示例代码

您可以从您的账户下载所有您购买的 Packt Publishing 书籍的示例代码文件,下载地址为www.packtpub.com。如果您是在其他地方购买的本书,您可以访问www.packtpub.com/support并注册,文件会直接通过电子邮件发送给您。

下载本书的彩色图片

我们还为您提供了包含本书所用屏幕截图/图表的彩色图片的 PDF 文件。彩色图片将帮助您更好地理解输出变化。您可以从www.packtpub.com/sites/default/files/downloads/1234OT_ColorImages.pdf下载此文件。

勘误

虽然我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本错误或代码错误——我们将非常感激您向我们报告。这样,您可以帮助其他读者避免困扰,并帮助我们改进后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表格链接,输入勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误将上传到我们的网站或添加到该书名下的现有勘误列表中。

要查看之前提交的勘误信息,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需的信息将显示在勘误部分。

盗版

互联网上的版权侵权问题在各类媒体中都持续存在。在 Packt,我们非常重视保护我们的版权和许可证。如果你在互联网上发现我们作品的任何非法复制品,请立即提供相关的网址或网站名称,以便我们采取措施进行处理。

如果您发现任何涉嫌盗版的内容,请通过<copyright@packtpub.com>联系我们,并附上盗版材料的链接。

感谢您帮助我们保护作者的权益,并支持我们为您提供有价值的内容。

问题

如果您对本书的任何部分有问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章:Python 数据科学

在本章中,我们将涵盖以下内容:

  • 使用字典对象

  • 操作字典中的字典

  • 操作元组

  • 使用集合

  • 写入一个列表

  • 从另一个列表创建列表 - 列表推导式

  • 使用迭代器

  • 生成迭代器和生成器

  • 使用可迭代对象

  • 将函数作为变量传递

  • 在另一个函数中嵌入函数

  • 将函数作为参数传递

  • 返回函数

  • 使用装饰器更改函数行为

  • 使用 lambda 创建匿名函数

  • 使用 map 函数

  • 使用过滤器

  • 使用 zip 和 izip

  • 处理表格数据中的数组

  • 预处理列

  • 排序列表

  • 使用键排序

  • 使用 itertools

介绍

Python 编程语言提供了许多内建的数据结构和函数,这些对于数据科学编程非常有用。在本章中,我们将介绍一些最常用的结构和函数。在随后的章节中,你会看到这些将在不同主题的各个部分中使用。掌握这些结构和函数将有助于你快速启动程序,以便处理数据和开发算法。

虽然本章是对常用数据结构和方法的快速概览,但随着你成为熟练的 Python 用户,你将开始发现自己的方法,将这些数据结构结合使用,以满足你的需求。

每个数据结构都有其用途,虽然不同的情况可能需要同时使用两个或多个数据结构来满足需求。你将在本书的一些示例中看到这一点。

使用字典对象

在 Python 中,容器是可以容纳任意数量对象的对象。它们提供了一种访问子对象并对其进行迭代的方式。字典、元组、列表和集合是 Python 中的容器对象。collections 模块提供了更多的容器类型。让我们在本节中详细了解字典对象。

准备工作

让我们看一个 Python 脚本示例,理解字典是如何操作的。这个脚本试图获取文本中的单词计数,即每个单词在给定文本中出现的次数。

如何操作它……

让我们继续演示如何在 Python 中操作字典。我们将使用一个简单的句子来演示字典的使用。然后我们将进行实际的字典创建:

# 1.Load a variable with sentences
sentence = "Peter Piper picked a peck of pickled peppers A peck of pickled \
peppers Peter Piper picked If Peter Piper picked a peck of pickled \
peppers Wheres the peck of pickled peppers Peter Piper picked"

# 2.Initialize a dictionary object
word_dict = {}

# 3.Perform the word count
for word in sentence.split():
    if word not in word_dict:
        word_dict[word] =1
    else:  
        word_dict[word]+=1
# 4.print the outputprint (word_dict)

它是如何工作的……

上述代码构建了一个单词频率表;每个单词及其频率都被计算出来。最终的打印语句会输出以下内容:

{'a': 2, 'A': 1, 'Peter': 4, 'of': 4, 'Piper': 4, 'pickled': 4, 'picked': 4, 'peppers': 4, 'the': 1, 'peck': 4, 'Wheres': 1, 'If': 1}

上述输出是一个键值对。对于每个单词(键),我们有一个频率(值)。字典数据结构是一个哈希映射,其中值是按键存储的。在上面的示例中,我们使用了字符串作为键;然而,任何其他不可变的数据类型也可以用作键。

参阅以下 URL,了解 Python 中可变和不可变对象的详细讨论:

docs.python.org/2/reference/datamodel.html

类似地,值可以是任何数据类型,包括自定义类。

在第 2 步中,我们初始化了字典。它在初始化时是空的。当向字典中添加新键时,通过新键访问字典将抛出 KeyError。在前面的示例第 3 步中,我们在 for 循环中包含了一个 if 语句来处理这种情况。然而,我们也可以使用以下方法:

word_dict.setdefault(word,0)

每次访问字典的键时,如果我们在循环中向字典添加元素,这个语句必须重复,因为在循环中我们无法预知新的键。使用 setdefault 重写第 3 步如下所示:

for word in sentence.split():
word_dict.setdefault(word,0)
word_dict[word]+=1

还有更多内容…

Python 2.5 及以上版本有一个名为 defaultdict 的类,它位于 collections 模块中。这个类处理了 setdefault 操作。defaultdict 类的调用方法如下:

from collections import defaultdict

sentence = "Peter Piper picked a peck of pickled peppers  A peck of pickled \
            peppers Peter Piper picked If Peter Piper picked a peck of pickled \
            peppers Wheres the peck of pickled peppers Peter Piper picked"

word_dict = defaultdict(int)

for word in sentence.split():
    word_dict[word]+=1print word_dict   

如你所见,我们在代码中包含了 collections.defaultdict 并初始化了字典。请注意,defaultdict 需要一个函数作为参数。这里我们传递了 int() 函数,因此当字典遇到一个之前未出现的键时,它会使用 int() 函数返回的值来初始化该键,在本例中是零。稍后我们将在本书中使用 defaultdict

注意

普通字典不会记住键插入的顺序。而在 collections 模块中,Python 提供了一个名为 OrderedDict 的容器,它可以记住键插入的顺序。更多细节请查看以下 Python 文档:

docs.python.org/2/library/collections.html#collections.OrderedDict

遍历字典非常简单;使用字典中提供的 keys() 函数,我们可以遍历键,使用 values() 函数,我们可以遍历值,或者使用 items() 函数,我们可以同时遍历键和值。请看以下示例:

For key, value in word_dict.items():
print key,value

在这个示例中,使用 dict.items(),我们可以遍历字典中存在的键和值。

Python 的字典文档非常详尽,是处理字典时的得力助手:

docs.python.org/2/tutorial/datastructures.html#dictionaries

字典作为中间数据结构非常有用。如果你的程序使用 JSON 在模块之间传递信息,字典是适合的正确数据类型。将字典从 JSON 文件中加载进来,或者将字典转储为 JSON 字符串,都非常方便。

Python 为我们提供了非常高效的处理 JSON 的库:

docs.python.org/2/library/json.html

Counter 是字典的子类,用于计数可哈希对象。我们用词频计数的例子可以轻松通过 Counter 来实现。

看以下示例:

from collections import Counter

sentence = "Peter Piper picked a peck of pickled peppers  A peck of pickled \
            peppers Peter Piper picked If Peter Piper picked a peck of pickled \
            peppers Wheres the peck of pickled peppers Peter Piper picked"

words = sentence.split()

word_count = Counter(words)

print word_count['Peter']print word_dict   

输出如下,你可以将这个输出与你之前的输出进行对比:

Counter({'Peter': 4, 'of': 4, 'Piper': 4, 'pickled': 4, 'picked': 4, 'peppers': 4, 'peck': 4, 'a': 2, 'A': 1, 'the': 1, 'Wheres': 1, 'If': 1})

你可以通过以下链接了解更多关于 Counter 的信息:

docs.python.org/2/library/collections.html#collections.Counter

参见

  • 第一章中的 使用字典的字典 章节,使用 Python 进行数据科学

使用字典的字典

正如我们之前提到的,这些数据结构的真正力量在于你如何创造性地使用它们来完成任务。让我们通过一个例子来理解如何在字典中使用字典。

准备开始

看下面的表格:

准备开始

在第一列,我们有三个用户,其他列是电影。单元格中的值是用户对电影的评分。假设我们想要在内存中表示这些数据,以便更大代码库的其他部分可以轻松访问这些信息。我们将使用字典的字典来实现这个目标。

如何实现……

我们将使用匿名函数创建 user_movie_rating 字典,演示字典的字典的概念。

我们将填充数据以展示字典的字典的有效使用:

from collections import defaultdict

user_movie_rating = defaultdict(lambda :defaultdict(int))

# Initialize ratings for Alice
user_movie_rating["Alice"]["LOR1"] =  4
user_movie_rating["Alice"]["LOR2"] =  5
user_movie_rating["Alice"]["LOR3"] =  3
user_movie_rating["Alice"]["SW1"]  =  5
user_movie_rating["Alice"]["SW2"]  =  3
print user_movie_rating

它是如何工作的……

user_movie_rating 是一个字典的字典。如前一节所述,defaultdict 接受一个函数作为参数;在这个例子中,我们传入了一个内置的匿名函数 lambda,它返回一个字典。因此,每次传入新键到 user_movie_rating 时,都会为该键创建一个新的字典。我们将在后续章节中深入了解 lambda 函数。

通过这种方式,我们可以非常快速地访问任何用户-电影组合的评分。类似地,在很多场景中,字典的字典也非常有用。

作为对字典的总结,我想提到的是,熟练掌握字典数据结构将有助于简化你在数据科学编程中的许多任务。正如我们稍后将看到的,字典常用于在机器学习中存储特征和标签。Python 的 NLTK 库在文本挖掘中广泛使用字典来存储特征:

www.nltk.org/book/ch05.html

章节标题为 使用 Python 字典将单词映射到属性 是一本很好的阅读材料,有助于理解字典如何有效地使用。

参见

  • 第一章中的 创建匿名函数 章节,使用 Python 进行数据科学

使用元组

元组是 Python 中的一种容器类型,属于序列类型。元组是不可变的,可以包含由逗号分隔并用圆括号括起来的异构元素序列。它们支持以下操作:

  • innot in

  • 比较、连接、切片和索引

  • min()max()

准备工作

与我们使用字典时构建完整程序的方式不同,我们将在这里将元组视为碎片化代码,集中于创建和操作的过程。

如何做到……

让我们来看一些示例,演示如何创建和操作元组:

# 1.Ways of creating a tuple
a_tuple = (1,2,'a')
b_tuple =1,2,'c'

# 2.Accessing elements of a tuple through index
print b_tuple[0]
print b_tuple[-1]

# 3.It is not possible to change the value of an item in a tuple,
# for example the next statement will result in an error.
try:
    b_tuple[0] = 20
except:
    print "Cannot change value of tuple by index"    

# 4.Though tuples are immutable
# But elements of a tuple can be mutable objects,
# for instance a list, as in the following line of code
c_tuple =(1,2,[10,20,30])
c_tuple[2][0] = 100

# 5.Tuples once created cannot be extended like list, 
# however two tuples can be concatenated.

print a_tuple + b_tuple

# 6 Slicing of uples
a =(1,2,3,4,5,6,7,8,9,10)
print a[1:]
print a[1:3]
print a[1:6:2]
print a[:-1]

# 7.Tuple min max
print min(a),max(a)

# 8.in and not in
if 1 in a:
    print "Element 1 is available in tuple a"
else:
print "Element 1 is available in tuple a"

它是如何工作的……

在第 1 步中,我们创建了一个元组。严格来说,圆括号并非必需,但为了更好的可读性,使用圆括号是一个选项。如你所见,我们创建了一个包含数字和字符串值的异构元组。第 2 步详细说明了如何通过索引访问元组中的元素。索引从零开始。可以使用负数来反向访问元组。打印语句的输出如下:

>>> print b_tuple[0]
1
>>> print b_tuple[-1]
c
>>> 

Python 元组的索引从 0 开始。元组是不可变的。

在第 3 步中,我们将查看元组的最重要特性——不可变性。无法更改元组中某个元素的值;第 3 步会导致解释器抛出错误:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

这看起来可能有些局限;然而,从数据科学的角度来看,不可变特性具有巨大的价值。

提示

在构建机器学习程序时,特别是在从原始数据生成特征时,创建特征元组可以确保值不会被下游程序更改。

由于这些特征存在于元组中,因此没有下游程序可以意外地更改特征值。

然而,我们要指出,元组可以包含可变对象作为其成员,例如列表。如果我们有一个像第 4 步所示的元组,元组的第三个元素是一个列表。现在,让我们尝试更改列表中的一个元素:

c_tuple[2][0] = 100

我们将如下打印元组:

print  c_tuple

我们将得到如下输出:

(1, 2, [100, 20, 30])

如你所见,列表中第一个元素的值已被更改为100

在第 5 步中,我们连接了两个元组。另一种有趣的使用元组的方式是,当不同的模块为机器学习程序创建不同的特征时。

提示

举个例子,假设你有一个模块正在创建类似词袋的特征,另一个模块正在为典型的文本分类程序创建数值特征。这些模型可以输出元组,最终模块可以将这些元组连接起来,得到完整的特征向量。

由于其不可变特性,与列表不同,元组在创建后无法扩展。它不支持 append 函数。这个不可变特性的另一个好处是,元组可以作为字典中的键。

提示

通常在创建键时,我们可能需要使用自定义分隔符将不同的字符串值连接起来,形成一个唯一的键。而是可以创建一个包含这些字符串值的元组,作为键来使用。

这提高了程序输出的可读性,并且避免了在手动组合键时引入错误。

在第 6 步中,我们将详细介绍元组中的切片操作。通常,切片操作有三个数字,并用冒号分隔。第一个数字决定切片的起始索引,第二个数字决定结束索引,最后一个数字是步长。第 6 步的例子将会说明这一点:

print a[1:]

它打印出的结果如下:

(2, 3, 4, 5, 6, 7, 8, 9, 10)

在这种情况下,我们只指定了起始索引 1。(记住,索引从零开始。)我们得到了一个从索引 1 开始的元组切片。让我们看另一个例子:

print a[1:3]

它打印出的结果如下:

(2, 3)

在这里,我们指定了起始索引为 1,结束索引为 3

注意

切片操作是右闭合的。

尽管我们指定了结束索引为 3,输出会返回到索引 2,即结束前一个索引。所以,23 都是我们输出切片的一部分。最后,让我们提供三个参数,包括起始和结束索引,以及步长:

print a[1:6:2]

它显示出的结果如下:

(2, 4, 6)

在这里,我们的步长为 2。除了起始和结束索引外,我们还指定了步长。因此,每次跳过两个索引,产生之前所示的输出。

让我们看一下负索引:

print a[:-1]

在这里,我们使用了负索引。输出结果如下:

(1, 2, 3, 4, 5, 6, 7, 8, 9)

除了最后一个元素外,切片返回的是所有元素:

print a[::-1]

值得思考的是,前述语句的输出结果如下——一个好奇的读者应该能搞清楚我们是如何得到以下输出的:

(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

在第 7 步中,我们将展示如何使用 min()max() 函数来获取元组中的最小值和最大值:

>>> print min(a), max(a)
1 10
>>>

在第 8 步中,我们将展示条件运算符 innot in;这可以有效地用于判断一个元素是否属于元组:

if 1 in a:
    print "Element 1 is available in tuple a"
else:
    print "Element 1 is available in tuple a"

还有更多…

正如我们在前面一节中看到的,我们通过索引访问元组中的元素。为了提高程序的可读性,假设我们希望为元组中的每个元素分配一个名称,并通过名称访问这些元素。这就是 namedtuple 发挥作用的地方。以下链接提供了 namedtuple 的详细文档:

docs.python.org/2/library/collections.html#collections.namedtuple

让我们通过一个简单的例子来说明如何使用 namedtuple

from collections import namedtuple

vector = namedtuple("Dimension",'x y z')
vec_1 = vector(1,1,1)
vec_2 = vector(1,0,1)

manhattan_distance = abs(vec_1.x - vec_2.x) + abs(vec_1.y - vec_2.y) \
                            + abs(vec_1.z - vec_2.z)

print "Manhattan distance between vectors = %d"%(manhattan_distance)

你可以看到,我们使用对象表示法访问了 vec_1vec_2 的元素,vec_1.xvec_1.y 等等。现在,我们不再使用索引,而是编写了更易读的程序。Vec_1.x 相当于 vec_1[0]

另见

  • 数据分析 – 探索与清洗配方在第三章,分析数据 - 探索与清洗中将文本表示为词袋模型(Bag-of-words)。

使用集合

集合与列表数据结构非常相似,除了它不允许重复元素外。集合是一个无序的同类元素集合。通常,集合用于从列表中删除重复的元素。然而,集合支持诸如交集、并集、差集和对称差集等操作。这些操作在很多应用场景中非常实用。

准备中

在这一节中,我们将编写一个小程序来了解集合数据结构的各种实用功能。在我们的示例中,我们将使用杰卡德系数(Jaccard's coefficient)计算两句话之间的相似度分数。我们将在后续章节中详细讲解杰卡德系数以及其他类似的度量方法。这里是对该度量的简要介绍。杰卡德系数是一个介于零和一之间的数字,其中一表示高度相似。它是基于两个集合之间有多少元素是相同的来计算的。

如何实现…

让我们看一些用于集合创建和操作的 Python 脚本:

# 1.Initialize two sentences.
st_1 = "dogs chase cats"
st_2 = "dogs hate cats"

# 2.Create set of words from strings
st_1_wrds = set(st_1.split())
st_2_wrds = set(st_2.split())

# 3.Find out the number of unique words in each set, vocabulary size.
no_wrds_st_1 = len(st_1_wrds)
no_wrds_st_2 = len(st_2_wrds)

# 4.Find out the list of common words between the two sets.
# Also find out the count of common words.
cmn_wrds = st_1_wrds.intersection(st_2_wrds)
no_cmn_wrds = len(st_1_wrds.intersection(st_2_wrds))

# 5.Get a list of unique words between the two sets.
# Also find out the count of unique words.
unq_wrds = st_1_wrds.union(st_2_wrds)
no_unq_wrds = len(st_1_wrds.union(st_2_wrds))

# 6.Calculate Jaccard similarity 
similarity = no_cmn_wrds / (1.0 * no_unq_wrds)

# 7.Let us now print to grasp our output.
print "No words in sent_1 = %d"%(no_wrds_st_1)
print "Sentence 1 words =", st_1_wrds
print "No words in sent_2 = %d"%(no_wrds_st_2)
print "Sentence 2 words =", st_2_wrds
print "No words in common = %d"%(no_cmn_wrds)
print "Common words =", cmn_wrds
print "Total unique words = %d"%(no_unq_wrds)
print "Unique words=",unq_wrds
print "Similarity = No words in common/No unique words, %d/%d = %.2f"%(no_cmn_wrds,no_unq_wrds,similarity)

它是如何工作的…

在步骤 1 和步骤 2 中,我们取了两句话,将它们分割成单词,并使用set()函数创建了两个集合。set函数可以用于将列表或元组转换为集合。请看下面的代码片段:

>>> a =(1,2,1)
>>> set(a)
set([1, 2])
>>> b =[1,2,1]
>>> set(b)
set([1, 2]

在这个例子中,a是一个元组,b是一个列表。通过使用set()函数,重复的元素被移除,返回一个集合对象。st_1.split()st_2.split()方法返回一个列表,我们将它传递给set()函数,以获得集合对象。

现在,让我们使用杰卡德系数计算两句话之间的相似度分数。我们将在后续章节的相似度度量部分详细讨论杰卡德系数以及其他类似的度量方法。我们将利用集合的union()intersection()函数来计算相似度分数。

在步骤 4 中,我们将执行两个操作。首先,通过使用intersection()函数,我们将尝试找出两个集合之间共有的单词。两句话之间的共同单词是'cats''dogs'。接着,我们将找出共同单词的数量,这个数量是 2。在下一步中,我们将使用union()函数找出两个集合之间唯一单词的列表。这两句话之间唯一的单词是'cats''hate''dogs''chase'。这在自然语言处理领域有时被称为词汇表。最后,我们将在步骤 6 中计算杰卡德系数,它是两个集合之间共同单词的数量与两个集合之间唯一单词的数量之比。

这个程序的输出如下所示:

No words in sent_1 = 3
Sentence 1 words = set(['cats', 'dogs', 'chase'])
No words in sent_2 = 3
Sentence 2 words = set(['cats', 'hate', 'dogs'])
No words in common = 2
Common words = set(['cats', 'dogs'])
Total unique words = 4
Unique words= set(['cats', 'hate', 'dogs', 'chase'])
Similarity = No words in common/No unique words, 2/4 = 0.50

还有更多…

我们给出前面的示例是为了展示集合函数的使用。然而,你也可以使用像 scikit-learn 这样的库中的内置函数。接下来,我们将尽可能多地利用这些库中的函数,而不是手动编写这些工具函数:

# Load libraries
from sklearn.metrics import jaccard_similarity_score

# 1.Initialize two sentences.
st_1 = "dogs chase cats"
st_2 = "dogs hate cats"

# 2.Create set of words from strings
st_1_wrds = set(st_1.split())
st_2_wrds = set(st_2.split())

unq_wrds = st_1_wrds.union(st_2_wrds)

a  =[ 1 if w in st_1_wrds else 0 for w in unq_wrds ]
b  =[ 1 if w in st_2_wrds else 0 for w in unq_wrds]

print a
print b
print jaccard_similarity_score(a,b)

输出如下:

[1, 0, 1, 1]
[1, 1, 1, 0]
0.5

编写一个列表

列表是一个容器对象和序列类型。它们与元组相似,但不同之处在于它们是同质的并且是可变的。列表允许执行添加操作。它们也可以用作堆栈或队列。与元组不同,列表是可扩展的;你可以在创建列表后使用 append 函数向列表中添加元素。

准备开始

类似于我们看到元组的方式,我们将看到列表作为碎片化的代码,在这些代码中我们将集中于创建和操作活动,而不是像处理字典那样编写完整的程序。

如何实现…

让我们看一些演示列表创建和操作活动的 Python 脚本:

# 1.Let us look at a quick example of list creation. 
a = range(1,10)
print a
b = ["a","b","c"]
print b

# 2.List can be accessed through indexing. Indexing starts at 0.
print a[0]

# 3.With negative indexing the elements of a list are accessed from backwards.
a[-1]

# 4.Slicing is accessing a subset of list by providing two indices.
print a[1:3]  # prints [2, 3]
print a[1:]   # prints [2, 3, 4, 5, 6, 7, 8, 9]
print a[-1:]  # prints [9]
print a[:-1]  # prints [1, 2, 3, 4, 5, 6, 7, 8]

#5.List concatenation
a = [1,2]
b = [3,4]
print a + b # prints [1, 2, 3, 4]

# 6.	List  min max
print min(a),max(a)

# 7.	in and not in
if 1 in a:
    print "Element 1 is available in list a"
else:
    print "Element 1 is available in tuple a"

# 8\. Appending and extending list
a = range(1,10)
print a
a.append(10)
print a

# 9.List as a stack
a_stack = []

a_stack.append(1)
a_stack.append(2)
a_stack.append(3)

print a_stack.pop()
print a_stack.pop()
print a_stack.pop()

# 10.List as queue
a_queue = []

a_queue.append(1)
a_queue.append(2)
a_queue.append(3)

print a_queue.pop(0)
print a_queue.pop(0)
print a_queue.pop(0)

# 11.	List sort and reverse
from random import shuffle
a = range(1,20)
shuffle(a)
print a
a.sort()
print a

a.reverse()
print a

它是如何工作的…

在第 1 步中,我们看到了创建列表的不同方式。请注意,我们只有同质元素。列表中可以有重复元素,而不像集合那样没有重复。第 2、3、4、5、6 和 7 步与元组的操作相似。我们不再详细讨论这些步骤。它们包括索引、切片、拼接、最小值最大值以及 innot in 操作,这些都与元组相似。

第 8 步展示了 appendextend 操作。这是列表与元组开始有所不同的地方。(当然,我们知道这些列表是同质的。)让我们来看一下代码第一部分的输出:

>>> a = range(1,10)
>>> print a

[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a.append(10)
>>> print a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>>

我们可以看到 10 已经被添加到 a 列表中。

以下是第二部分的输出,其中展示了 extend

>>> b=range(11,15)
>>> a.extend(b)
>>> print a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>>

我们通过另一个列表 b 扩展了原来的 a 列表。

在第 9 步中,我们将展示如何将列表用作堆栈。pop() 函数帮助检索列表中最后一个添加的元素。输出如下:

3
2
1

最后一个被添加的元素是第一个被检索的元素,遵循后进先出LIFO)的堆栈方式。

在第 10 步中,我们将使用列表实现一个队列。将 0 作为参数传递给 pop() 函数表示要获取的元素的索引。输出如下:

1
2
3

输出遵循队列的 LIFO 样式。然而,这并不是一种高效的方法。弹出第一个元素并不最优,因为列表的实现方式。执行此操作的高效方法是使用下一个小节中解释的 deque 数据结构。

最后一步详细介绍了列表中的排序和反转操作。列表有一个内置函数,sort(),用于对列表的元素进行排序。默认情况下,它会按升序排序。排序的详细内容将在本章后面的小节中解释。reverse() 函数将反转列表中的元素。

我们将首先创建一个包含从 1 到 19 的元素的列表:

a = range(1,20)

我们将使用random模块中的shuffle()函数对元素进行洗牌。这样我们就能展示排序操作。洗牌后的输出如下:

[19, 14, 11, 12, 4, 13, 17, 5, 2, 3, 1, 16, 8, 15, 18, 6, 7, 9, 10]

现在,a.sort()进行就地排序,当我们打印a时,将得到以下输出:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

a.reverse()也是一个就地操作,产生以下输出:

[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

还有更多…

deque代表双端队列。与只能在一个方向进行附加和弹出的栈和队列不同,deque可以在两端进行appendpop操作:

docs.python.org/2/library/collections.html#collections.deque

从另一个列表创建列表——列表推导式

推导式是一种从另一个序列创建序列的方式。例如,我们可以从另一个列表或元组创建一个列表。让我们来看一个列表推导式。通常,列表推导式包括以下几个特点:

  • 一个序列,比如我们感兴趣的元素列表

  • 一个表示序列元素的变量

  • 一个输出表达式,负责使用输入序列的元素生成输出序列

  • 一个可选的谓词表达式

准备好

让我们定义一个简单的问题,以便理解推导式中涉及的所有不同元素。给定一个包含正负数的输入列表,我们需要一个输出列表,该列表包含所有负数元素的平方。

如何做…

在以下脚本中,我们将展示一个简单的列表推导式示例:

# 1.	Let us define a simple list with some positive and negative numbers.
a = [1,2,-1,-2,3,4,-3,-4]

# 2.	Now let us write our list comprehension.
# pow() a power function takes two input and
# its output is the first variable raised to the power of the second.
b = [pow(x,2) for x in a if x < 0]

# 3.	Finally let us see the output, i.e. the newly created list b.
print b

它是如何工作的…

这个示例是用来解释推导式的各个组成部分的。让我们来看第 2 步:

b = [pow(x,2) for x in a if x < 0]

这段代码的解释如下:

  • 我们的输入列表是a,输出列表是b

  • 我们将使用变量x表示列表中的每个元素

  • pow(x, 2)是输出表达式,它使用输入中的元素生成输出列表

  • 最后,if x < 0是谓词表达式,控制哪些输入列表中的元素将用于生成输出列表

还有更多…

推导式的语法与字典完全相同。一个简单的示例将说明如下:

a = {'a':1,'b':2,'c':3}
b = {x:pow(y,2) for x,y in a.items()}
print b

在前面的示例中,我们从输入字典a创建了一个新的字典b。输出如下:

{'a': 1, 'c': 9, 'b': 4}

你可以看到我们保留了a字典的键,但是现在新值是a中原始值的平方。需要注意的一点是在推导式中使用了花括号而不是方括号。

我们可以通过一个小技巧对元组进行推导。请参见以下示例:

def process(x):
    if isinstance(x,str):
        return x.lower()
    elif isinstance(x,int):
        return x*x
    else:
        return -9

a = (1,2,-1,-2,'D',3,4,-3,'A')
b = tuple(process(x) for x in a )

print b

我们没有使用pow()函数,而是用了一个新的过程函数。我会留给你作为练习来解读这个过程函数的作用。请注意,我们遵循了与列表推导式相同的语法;不过,我们使用了花括号而非方括号。该程序的输出如下:

<generator object <genexpr> at 0x05E87D00>

哎呀!我们想要一个元组,但最终得到了一个生成器(更多关于生成器的内容将在后续章节中讨论)。正确的方法如下:

b = tuple(process(x) for x in a )

现在,打印 b 语句将输出以下内容:

(1, 4, 1, 4, 'd', 9, 16, 9, 'a')

Python 推导式基于集合构造符号:

en.wikipedia.org/wiki/Set-builder_notation

Itertools.dropwhile:

docs.python.org/2/library/itertools.html#itertools.dropwhile

使用谓词和序列,dropwhile 将只返回序列中满足谓词条件的项。

使用迭代器

对于数据科学程序来说,数据是一个显而易见的关键输入。数据的大小可能不同——有些数据可能适合存入内存,而有些则不行。记录访问机制可能因数据格式而异。有趣的是,不同的算法可能要求以不同长度的块进行处理。例如,假设你正在编写一个随机梯度下降算法,并希望在每个 epoch 中传递 5000 条记录,那么能够处理数据访问、理解数据格式、遍历数据并向调用者提供所需数据的抽象会非常有用。这样可以使代码更加简洁。大多数时候,重点在于我们如何使用数据,而不是如何访问数据。Python 为我们提供了一种优雅的方式——迭代器,来处理所有这些需求。

准备好

Python 中的迭代器实现了迭代器模式。它允许我们逐一遍历序列,而无需将整个序列实例化!

如何做……

让我们创建一个简单的迭代器,称为 simple counter,并提供一些代码来有效使用该迭代器:

# 1.	Let us write a simple iterator.
class SimpleCounter(object):
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        'Returns itself as an iterator object'
        return self

    def next(self):
        'Returns the next value till current is lower than end'
        if self.current > self.end:

            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

# 2.	Now let us try to access the iterator
c = SimpleCounter(1,3)
print c.next()
print c.next()
print c.next()
print c.next()

# 3.	Another way to access
for entry in iter(c):
    print entry     

它是如何工作的……

在步骤 1 中,我们定义了一个名为 SimpleCounter 的类。__init__ 构造函数接受两个参数,startend,定义了序列的开始和结束。注意这两个方法,__iter__next。任何旨在作为迭代器对象的 Python 对象都应该支持这两个函数。__iter__ 返回整个类对象作为迭代器对象。next 方法返回迭代器中的下一个值。

如步骤 2 所示,我们可以使用 next() 函数访问迭代器中的连续元素。Python 还为我们提供了一个方便的函数 iter(),它可以在循环中依次访问元素,如步骤 3 所示。iter() 内部使用 next() 函数。

需要注意的一点是,迭代器对象只能使用一次。运行上述代码后,我们将尝试如下访问迭代器:

print next(c)

它会抛出 StopIteration 异常。在序列耗尽后,调用 c.next() 会导致 StopIteration 异常:

    raise StopIteration
StopIteration
>>>

iter() 函数处理此异常,并在数据耗尽后退出循环。

还有更多……

让我们看另一个迭代器的例子。假设我们需要在程序中访问一个非常大的文件;然而,在程序中,我们每次只处理一行:

f = open(some_file_of_interest)
for l in iter(f):
print l
f.close()

在 Python 中,文件对象是一个迭代器;它支持iter()next()函数。因此,我们可以避免将整个文件加载到内存中,而是每次处理一行。

迭代器让你能够编写自定义代码,以一种应用程序所需的方式访问数据源。

以下链接提供了有关在 Python 中以多种方式使用迭代器的更多信息:

无限迭代器,count()cycle()repeat()在 itertools 中的使用:

docs.python.org/2/library/itertools.html#itertools.cycle

生成迭代器和生成器

我们在上一节中看到迭代器是什么;现在在这一节中,让我们看看如何生成一个迭代器。

准备好

生成器提供了一种简洁的语法来遍历一系列值,省去了需要同时使用 iter 和 next()两个函数的麻烦。我们不需要编写类。需要注意的一点是,生成器和可迭代对象都能生成一个迭代器。

它是如何做到的……

让我们看一下以下示例;如果你理解了上一节的推导式内容,应该很容易跟上。在这个例子中,我们使用了生成器推导式。如果你还记得,我们曾尝试用这种方式做一个元组推导式,结果得到了一个生成器对象:

SimpleCounter  = (x**2 for x in range(1,10))

tot = 0
for val in SimpleCounter:
    tot+=val

print tot    

它是如何工作的……

SimpleCounter and we use it in a for loop in order to access the underlying data sequentially. Note that we have not used the iter() function here. Notice how clean the code is. We successfully recreated our old SimpleCounter class in a very elegant manner.

还有更多……

让我们看看如何使用 yield 语句来创建生成器:

def my_gen(low,high):
    for x in range(low,high):
        yield x**2

tot = 0     

for val in my_gen(1,10):
    tot+=val
print tot    

在上面的例子中,my_gen()函数是一个生成器;我们使用了 yield 语句以序列的形式返回输出。

在上一节中,我们提到过,生成器和可迭代对象都生成一个迭代器。让我们通过尝试使用iter函数来调用生成器,来验证这一点:

gen = (x**2 for x in range(1,10))

for val in iter(gen):
    print val

在我们进入下一节关于可迭代对象的内容之前,需要注意的一点是,生成器一旦遍历完序列,就结束了——没有更多数据。

注意

使用生成器对象,我们只能遍历序列一次。

使用可迭代对象

可迭代对象与生成器类似,除了一个关键区别:我们可以一直使用一个可迭代对象,也就是说,一旦我们遍历完序列中的所有元素,我们可以从头再开始访问它,而不像生成器那样只能遍历一次。

它们是基于对象的生成器,不保存任何状态。任何具有iter方法并且能够生成数据的类,都可以作为一个无状态的对象生成器使用。

准备好

让我们通过一个简单的例子来理解可迭代对象。如果你已经理解了前面的生成器和迭代器的示例,那么这个步骤应该很容易跟上。

它是如何做到的……

让我们创建一个简单的可迭代对象,叫做 SimpleIterable,并展示一些操作它的脚本:

# 1.	Let us define a simple class with __iter__ method.
class SimpleIterable(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __iter__(self):
        for x in range(self.start,self.end):
            yield x**2

#  Now let us invoke this class and iterate over its values two times.
c = SimpleIterable(1,10)

# First iteration
tot = 0
for val in iter(c):
    tot+=val

print tot

# Second iteration
tot =0
for val in iter(c):
    tot+=val

print tot

它是如何工作的…

在第 1 步中,我们创建了一个简单的类,它是我们的可迭代对象。初始化构造函数接受两个参数,start 和 end,类似于我们之前的例子。我们定义了一个名为 iter 的函数,它将返回我们所需的序列。在这个给定的数字范围内,将返回这些数字的平方。

接下来,我们有两个循环。在第一个循环中,我们遍历了从 1 到 10 的数字范围。当我们运行第二个循环时,你会注意到它再次遍历序列,并且没有抛出任何异常。

另见

  • 第一章中的使用迭代器食谱,使用 Python 进行数据科学

  • 第一章中的生成迭代器 - 生成器食谱,使用 Python 进行数据科学

将函数作为变量传递

Python 除了支持命令式编程范式外,还支持函数式编程。在前面的部分中,我们已经看到了几个函数式编程构造,但没有明确解释。让我们在这一节中回顾它们。函数在 Python 中是头等公民。它们具有属性,可以被引用并赋值给变量。

准备工作

让我们看看这一节中将函数作为变量传递的范式。

如何做…

让我们定义一个简单的函数,看看它如何作为变量使用:

# 1.Let us define a simple function.
def square_input(x):
    return x*x
# We will follow it by assigning that function to a variable
square_me = square_input

# And finally invoke the variable
print square_me(5)    

它是如何工作的…

我们在第 1 步中定义了一个简单的函数;它接受一个输入,返回该输入的平方。我们将这个函数赋值给 square_me 变量。最后,我们可以通过调用 square_me 并传入有效参数来调用该函数。这演示了如何在 Python 中将函数作为变量处理。这是一个非常重要的函数式编程构造。

将函数嵌套在另一个函数中

这个食谱将解释另一个函数式编程构造:在另一个函数中定义函数。

准备工作

让我们举一个简单的例子,编写一个函数,返回给定输入列表的平方和。

如何做…

让我们写一个简单的函数,演示在另一个函数中使用函数:

# 1.	Let us define a function of function to find the sum of squares of the given input
def sum_square(x):
    def square_input(x):
        return x*x
    return sum([square_input(x1) for x1 in x])

# Print the output to check for correctness
print sum_square([2,4,5])    

它是如何工作的…

在第 1 步中,你可以看到我们在 sum_square() 函数中定义了一个 square_input() 函数。父函数使用它来执行平方和操作。在下一步中,我们调用了该函数并打印了它的输出。

输出结果如下:

[4, 9, 16]

将函数作为参数传递

How to do it…

现在让我们演示如何将一个函数作为参数传递:

from math import log

def square_input(x):
    return x*x

# 1.	Define a generic function, which will take another function as input
# and will apply it on the given input sequence.
def apply_func(func_x,input_x):
    return map(func_x,input_x)

# Let us try to use the apply_func() and verify the results  
a = [2,3,4]

print apply_func(square_input,a)
print apply_func(log,a)    

它是如何工作的…

在第 1 步中,我们定义了一个 apply_func 函数,接受两个变量。第一个变量是一个函数,第二个变量是一个序列。如你所见,我们使用了 map 函数(后续的食谱中将详细介绍该函数)将给定的函数应用于序列中的所有元素。

接下来,我们对列表 a 调用了 apply_func;首先使用 square_input 函数,然后是 log 函数。输出结果如下:

[4, 9, 16]

如你所见,a 中的所有元素都是平方的。map 函数将square_input函数应用于序列中的所有元素:

[0.69314718055994529, 1.0986122886681098, 1.3862943611198906]

类似地,log 函数也应用于序列中的所有元素。

返回一个函数

在本节中,让我们看看那些会返回另一个函数的函数。

准备工作

让我们用一个高中例子来尝试解释函数返回函数的用法。

我们的问题是,给定一个半径为 r 的圆柱体,我们想知道它在不同高度下的体积:

www.mathopenref.com/cylindervolume.html

Volume = area * height = pi * r² * h

上述公式给出了填充圆柱体的确切立方单位。

如何实现……

让我们编写一个简单的函数来演示函数返回函数的概念。此外,我们将编写一小段代码来展示用法:

# 1.	Let us define a function which will explain our
#  concept of function returning a function.
def cylinder_vol(r):
    pi = 3.141
    def get_vol(h):
        return pi * r**2 * h
    return get_vol

# 2.	Let us define a radius and find get a volume function,
#  which can now find out the volume for the given radius and any height.
radius = 10
find_volume = cylinder_vol(radius)

# 3.	Let us try to find out the volume for different heights
height = 10
print "Volume of cylinder of radius %d and height %d = %.2f  cubic units" \
                %(radius,height,find_volume(height))        

height = 20
print "Volume of cylinder of radius %d and height %d = %.2f  cubic units" \
                %(radius,height,find_volume(height))        

它是如何工作的……

在第 1 步中,我们定义了一个名为cylinder_vol()的函数;它接受一个参数r,即半径。在这个函数中,我们定义了另一个函数get_vol()get_vol()函数可以访问 r 和 pi,并以高度作为参数。对于给定的半径 r,作为cylinder_vol()的参数,传递不同的高度作为参数给get_vol()

在第 2 步中,我们定义了一个半径;在这个例子中,定义为 10,并用它调用cylinder_vol()函数。它返回get_vol()函数,我们将其存储在名为find_volume的变量中。

在第 3 步,我们用不同的高度 10 和 20 调用了find_volume。请注意,我们没有提供半径。

输出结果如下:

Volume of cylinder of radius 10 and height 10 = 3141.00  cubic units
Volume of cylinder of radius 10 and height 20 = 6282.00  cubic units

还有更多……

Functools 是一个用于高阶函数的模块:

docs.python.org/2/library/functools.html

使用装饰器改变函数的行为

装饰器包装一个函数并改变其行为。通过一些实际示例更容易理解它们。让我们在这个示例中看看一些装饰器是如何工作的。

准备工作

你还记得我们解释函数作为参数传递给另一个函数,函数作为变量,以及函数返回函数的部分吗?最重要的是,你还记得圆柱体的例子吗?如果你跟上了,装饰器应该不难。在这个练习中,我们将对给定字符串进行一系列清理操作。对于一个含有混合大小写和标点符号的字符串,我们将使用装饰器编写一个清理程序,这个程序可以非常容易地扩展。

如何实现……

让我们为文本处理编写一个简单的装饰器:

from string import punctuation

def pipeline_wrapper(func):

    def to_lower(x):
        return x.lower()

    def remove_punc(x):
        for p in punctuation:
            x = x.replace(p,'')
        return x

    def wrapper(*args,**kwargs):
        x = to_lower(*args,**kwargs)
        x = remove_punc(x)
        return func(x)
    return wrapper

@pipeline_wrapper        
def tokenize_whitespace(inText):
    return inText.split()

s = "string. With. Punctuation?"    
print tokenize_whitespace(s)

它是如何工作的……

让我们从最后两行开始:

s = "string. With. Punctuation?" 
print tokenize_whitespace(s)

我们声明了一个字符串变量,想要清理这个字符串。在我们的例子中,我们希望具备以下功能:

  • 我们希望字符串转换为小写

  • 我们希望去除标点符号

  • 我们希望返回一个单词列表

你可以看到我们调用了tokenize_whitespace函数,并将字符串 s 作为参数传递。让我们来看一下tokenize_whitespace函数:

@pipeline_wrapper 
def tokenize_whitespace(inText):
return inText.split()

我们看到这是一个非常简单的函数,输入一个字符串,函数会通过空格将其拆分,并返回一个单词列表。我们将使用装饰器改变这个函数的行为。你可以看到,我们将为此函数使用的装饰器是 @pipeline_wrapper。这是一种更简便的调用方式:

tokenize_whitespace = pipeline_wrapper (clean_tokens)

现在,让我们来看一下装饰器函数:

def pipeline_wrapper(func):

def to_lower(x):
return x.lower()
def remove_punc(x):
for p in punctuation:
x = x.replace(p,'')
return x
def wrapper(*args,**kwargs):
x = to_lower(*args,**kwargs)
x = remove_punc(x)
return func(x)
return wrapper

你可以看到,pipeline_wrapper 返回了 wrapper 函数。在 wrapper 函数中,你可以看到最后的返回语句返回了 func;这是我们传递给 wrapper 的原始函数。wrapper 修改了我们原始的 tokenize_whitespace 函数的行为。tokenize_whitespace 的输入首先被 to_lower() 函数修改,将输入字符串转换为小写,然后是 remove_punc() 函数,去除标点符号。最终输出如下:

['string', 'with', 'punctuation']

完全是我们想要的——去除标点符号,将字符串转换为小写,最后得到一个单词列表。

使用 lambda 创建匿名函数

匿名函数是在 Python 中通过 lambda 语句创建的。未绑定到名称的函数被称为匿名函数。

准备好了吗

如果你跟随了关于将函数作为参数传递的部分,那么这一节的示例和之前的非常相似。我们在那一节传递了一个预定义函数;在这里,我们将传递一个 lambda 函数。

如何实现……

我们将看到一个简单的示例,使用玩具数据集来解释 Python 中的匿名函数:

# 1.	Create a simple list and a function similar to the
# one in functions as parameter section.
a =[10,20,30]

def do_list(a_list,func):
    total = 0
    for element in a_list:
        total+=func(element)
    return total

print do_list(a,lambda x:x**2)   
print do_list(a,lambda x:x**3)   

b =[lambda x: x%3 ==0  for x in a  ]

它是如何工作的……

在第 1 步中,我们有一个名为do_list的函数,它接受另一个函数作为参数。do_list使用一个列表和一个函数,将输入函数应用于给定列表的元素,累加转换后的值并返回结果。

接下来,我们将调用 do_list 函数,第一个参数是我们的输入列表 a,第二个参数是我们的 lambda 函数。让我们解码我们的 lambda 函数:

lambda x:x**2

匿名函数是通过关键字 lambda 声明的;紧跟其后的是为函数定义的参数。在这个例子中,x 是传递给这个匿名函数的参数的名称。冒号操作符后面的表达式是返回值。输入参数通过该表达式进行计算,并作为输出返回。在这个输入中,返回的是输入的平方。在接下来的 print 语句中,我们有一个 lambda 函数,它返回给定输入的立方值。

使用 map 函数

map 是一个内置的 Python 函数。它接受一个函数和一个可迭代对象作为参数:

map(aFunction, iterable)

该函数会作用于可迭代对象的所有元素,并将结果作为列表返回。由于将一个函数传递给 map,所以 lambda 通常与 map 一起使用。

准备好了吗

让我们看看一个非常简单的使用 map 函数的示例。

如何实现……

让我们看看如何使用 map 函数的示例:

#First let us declare a list.
a =[10,20,30]
# Let us now call the map function in our Print statement.
print map(lambda x:x**2,a)   

它是如何工作的……

这与前一个例子中的代码非常相似。map 函数接受两个参数,第一个是函数,第二个是序列。在我们的示例代码中,我们使用了一个匿名函数:

lambda x:x**2

该函数对给定输入进行平方操作。我们还将一个列表传递给了 map

map 函数应用于列表中的所有元素,计算它们的平方,并将结果作为列表返回。

输出结果如下:

[100,400,900]

还有更多内容…

同样,任何其他函数也可以应用于列表:

print map(lambda x:x**3,a)

使用 map,我们可以用一行代码替换前面例子中的代码:

print sum(map(lambda x:x**2,a))
print sum(map(lambda x:x**3,a))

如果我们有 N 个序列,map 函数需要一个 N 参数的函数。让我们看一个例子来理解这一点:

a =[10,20,30]
b = [1,2,3]

print map(pow,a,b) 

我们将两个序列 ab 传递给 map 函数。注意,传递的函数是幂函数,它接受两个参数。让我们看一下前面代码片段的结果:

[10, 400, 27000]
>>>

如你所见,列表 a 中的元素被提升为列表 b 中相同位置的值的幂。需要注意的是,两个列表应该具有相同的大小;如果不相同,Python 会用 None 填充较小的列表。虽然我们的示例操作的是列表,但任何可迭代对象都可以传递给 map 函数。

使用过滤器

凭借其名字,filter 函数根据给定的函数从序列中过滤元素。对于包含负数和正数的序列,我们可以使用 filter 函数,例如,筛选出所有负数。filter 是一个内置的 Python 函数,它接受一个函数和一个可迭代对象作为参数:

Filter(aFunction, iterable)

作为参数传递的函数根据测试返回布尔值。

该函数应用于可迭代对象的所有元素,所有经过函数处理后返回 True 的项都会以列表的形式返回。匿名函数 lambda 是与 filter 一起使用的最常见方式。

准备工作

让我们看一个简单的代码,看看 filter 函数是如何工作的。

如何实现…

让我们看一个关于如何使用 filter 函数的例子:

# Let us declare a list.
a = [10,20,30,40,50]
# Let us apply Filter function on all the elements of the list.
print filter(lambda x:x>10,a)

如何运作…

我们在这里使用的 lambda 函数非常简单;如果给定的值大于十,它返回 True,否则返回 False。我们的打印语句给出如下结果:

[20, 30, 40, 50]

如你所见,只有大于十的元素被返回。

使用 zipizip

zip 函数接受两个等长的集合,并将它们成对地合并在一起。zip 是一个内置的 Python 函数。

准备工作

让我们通过一个非常简单的示例演示 zip

如何实现…

让我们将两个序列传递给 zip 函数并打印输出:

print zip(range(1,5),range(1,5))

如何运作…

我们传递给 zip 函数的两个参数是两个列表,它们的值都在 15 之间。

range 函数接受三个参数:列表的起始值、结束值和步长值。默认的步长值为 1。在我们的例子中,我们传递了 1 和 5 作为列表的起始和结束值。记住,Python 的范围是右闭的,所以 range(1, 5) 会返回如下列表:

[1,2,3,4]

我们将两个序列传递给 zip 函数,结果如下:

[(1, 1), (2, 2), (3, 3), (4, 4)]

请记住,两个集合的大小应该相同;如果不同,则输出会被截断为最小集合的大小。

还有更多…

现在,看看以下代码:

x,y = zip(*out)
print x,y

你能猜到输出是什么吗?

让我们看看 * 运算符的作用。* 运算符将集合解包到位置参数中:

a =(2,3)
print pow(*a)
a_dict = {"x":10,"y":10,"z":10,"x1":10,"y1":10,"z1":10} 

** 运算符将字典解包为一组命名参数。在这种情况下,当我们将 ** 运算符应用于字典时,输出将是 6。看看以下接受六个参数的函数:

def dist(x,y,z,x1,y1,z1):
return abs((x-x1)+(y-y1)+(z-z1))

print dist(**a_dict) 

print 语句的输出为零。

有了这两个运算符,我们可以编写一个没有变量数量限制的函数:

def any_sum(*args):
tot = 0
for arg in args:
tot+=arg
return tot

print any_sum(1,2)
print any_sum(1,2,3)
any_sum function can now work on any number of variables. A curious reader may comment about why not use a list instead as an argument to the any_sum function, where we can now pass a list of values. Very well, yes in this case, but we will soon encounter cases where we really don't know what kind of arguments will be passed.

回到 zip 工具。zip 的一个缺点是它会一次性计算出整个列表。当我们有两个非常大的列表时,这可能会成为问题。这时,izip 会派上用场。它们只有在需要时才计算元素。izipitertools 的一部分。有关更多详细信息,请参考 itertools 的相关文档。

另请参见

  • 在 第一章中的 与 Itertools 一起工作 配方,使用 Python 进行数据科学

从表格数据处理中处理数组

任何数据科学应用的核心是为特定问题找到合适的数据处理方法。在机器学习的情况下,方法要么是监督学习,要么是无监督学习,用于预测或分类数据。在这一步骤之前,会花费大量时间进行数据转换,使数据适合这些方法。

通常,数据以多种方式提供给数据科学程序。数据科学程序员面临的挑战是如何访问数据,并将其通过 Python 数据结构传递给后续代码部分。掌握通过 Python 访问数据的方法,在编写数据科学程序时非常有用,因为它能让你快速进入问题的核心。

通常,数据以文本文件的形式提供,数据项之间用逗号或制表符分隔。此时可以使用 Python 内建的文件对象工具。如我们之前所见,文件对象实现了 __iter__()next() 方法。这使得我们能够处理非常大的文件,这些文件无法完全加载到内存中,而是每次读取文件的一小部分。

Python 机器学习库如 scikit-learn 依赖于 NumPy 库。在本节中,我们将介绍高效读取外部数据并将其转换为 NumPy 数组以便进行后续数据处理的方法。

准备开始

NumPy 提供了一个名为 genfromtext 的函数,用于从表格数据创建 NumPy 数组。一旦数据以 NumPy 数组的形式提供,后续的系统处理这些数据就变得更加容易。让我们看看如何利用 genfromtext。以下代码是使用 NumPy 版本 1.8.0 编写的。

如何实现…

让我们先导入必要的库。接下来,我们将定义一个示例输入。最后,我们将展示如何处理表格数据。

# 1.	Let us simulate a small tablular input using StringIO
import numpy as np
from StringIO import StringIO
in_data = StringIO("10,20,30\n56,89,90\n33,46,89")

# 2.Read the input using numpy’s genfromtext to create a nummpy array.
data = np.genfromtxt(in_data,dtype=int,delimiter=",")

# cases where we may not need to use some columns.
in_data = StringIO("10,20,30\n56,89,90\n33,46,89")
data = np.genfromtxt(in_data,dtype=int,delimiter=",",usecols=(0,1))

# providing column names
in_data = StringIO("10,20,30\n56,89,90\n33,46,89")
data = np.genfromtxt(in_data,dtype=int,delimiter=",",names="a,b,c")

# using column names from data
in_data = StringIO("a,b,c\n10,20,30\n56,89,90\n33,46,89")
data = np.genfromtxt(in_data,dtype=int,delimiter=",",names=True)

它是如何工作的……

在第 1 步中,我们使用 StringIO 工具模拟了一个表格数据。我们有三行三列。行是以换行符分隔的,列是以逗号分隔的。

在第 2 步中,我们使用了 NumPy 中的 genfromtxt 将数据加载为 NumPy 数组。

genfromtxt 的第一个参数是文件的来源和文件名;在我们的案例中,它是 StringIO 对象。输入是以逗号为分隔符的;delimiter 参数允许我们指定分隔符。在运行前面的代码后,数据值如下所示:

>>> data
array([[10, 20, 30],
       [56, 89, 90],
       [33, 46, 89]])

如你所见,我们成功地将数据从字符串加载到 NumPy 数组中。

还有更多……

以下是 genfromtxt 函数的各种参数和默认值:

genfromtxt(fname, dtype=<type 'float'>, comments='#', delimiter=None, skiprows=0, skip_header=0, skip_footer=0, converters=None, missing='', missing_values=None, filling_values=None, usecols=None, names=None, excludelist=None, deletechars=None, replace_space='_', autostrip=False, case_sensitive=True, defaultfmt='f%i', unpack=None, usemask=False, loose=True, invalid_raise=True)

唯一的必需参数是数据来源的名称。在我们的案例中,我们使用了一个 StringIO 对象。它可以是对应文件名的字符串,或者是一个具有读取方法的类似文件的对象。它也可以是远程文件的 URL。

第一步是将给定的行拆分成列。一旦文件打开以供读取,genfromtxt 会将非空行拆分成一系列字符串。空行会被忽略,注释行也会被忽略。comments 选项帮助 gentext 判断哪些是注释行。字符串根据由 delimiter 选项指定的分隔符被拆分成列。在我们的示例中,我们使用了 , 作为分隔符。/t 也是一个常用的分隔符。在默认情况下,gentext 中的分隔符是 None,这意味着它假设行是通过空格来分隔成列的。

通常,当行被转换为字符串序列并随后提取列时,单独的列不会去除前导或尾随的空格。在代码的后面部分,必须处理这个问题,特别是当某些变量被用作字典的键时。例如,如果前导或尾随空格没有一致地处理,可能会导致代码中的错误。设置 autostrip=True 可以帮助避免这个问题。

很多时候,我们希望在读取文件时跳过前 n 行或后 n 行。这可能是由于文件中存在头部或尾部信息。skip_header = n 会在读取时跳过前 n 行,同样,skip_footer = n 会跳过最后 n 行。

类似于不需要的行,我们可能会遇到很多情况,在这些情况下我们不需要使用某些列。usecols 参数用于指定我们感兴趣的列列表:

in_data = StringIO("10,20,30\n56,89,90\n33,46,89")

data = np.genfromtxt(in_data,dtype=int,delimiter=",",usecols=(0,1))

如前面的示例所示,我们仅选择了两列,第 0 列和第 1 列。数据对象如下所示:

>>> data
array([[10, 20],
       [56, 89],
       [33, 46]])

可以使用 names 参数提供自定义的列名。一个由逗号分隔的列名字符串如下所示:

in_data = StringIO("10,20,30\n56,89,90\n33,46,89")
data = np.genfromtxt(in_data,dtype=int,delimiter=",",names="a,b,c")

>>> data
array([(10, 20, 30), (56, 89, 90), (33, 46, 89)], 
      dtype=[('a', '<i4'), ('b', '<i4'), ('c', '<i4')])

通过将 names 设置为 True,输入数据中的第一行将作为列头:

in_data = StringIO("a,b,c\n10,20,30\n56,89,90\n33,46,89")
data = np.genfromtxt(in_data,dtype=int,delimiter=",",names=True)

>>> data
array([(10, 20, 30), (56, 89, 90), (33, 46, 89)], 
      dtype=[('a', '<i4'), ('b', '<i4'), ('c', '<i4')])

NumPy 还有另一种简单的方法可以从文本输入创建 NumPy 数组,即 loadtxt

docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt.html

这比 genfromtxt 要简单一些;如果你需要一个简单的读取器,而不需要任何复杂的数据处理机制(如处理缺失值),你可以选择 loadtxt

然而,如果我们不想将数据加载为 NumPy 数组,而是希望加载为列表,Python 为我们提供了一个默认的 csv 库:

docs.python.org/2/library/csv.html

上述 csv 库中的一个有趣方法是 csv.Sniffer.sniff()。如果我们有一个非常大的 csv 文件,并且想要了解其结构,我们可以使用 sniff()。这将返回一个方言子类,包含 csv 文件的大部分属性。

预处理列

我们获得的数据通常不是我们可以直接使用的格式。在机器学习术语中,需要应用很多数据处理步骤,这些步骤通常被称为数据预处理。一种处理方法是将所有输入作为字符串导入,并在后续阶段进行必要的数据转换。另一种方法是直接在源头进行这些转换。genfromtext 提供了一些功能,可以在从源读取数据时执行这些数据转换。

准备工作

请考虑以下文本行:

30kg,inr2000,31.11,56.33,1
52kg,inr8000.35,12,16.7,2

这是我们在现实生活中获取数据的典型例子。前两列包含字符串 kg 和 inr,分别附加在实际数值的前后。

让我们尝试将这些数据按如下方式导入到一个 NumPy 数组中:

in_data = StringIO("30kg,inr2000,31.11,56.33,1\n52kg,inr8000.35,12,16.7,2")
data = np.genfromtxt(in_data,delimiter=",")

这将产生以下结果:

>>> data
array([[   nan,    nan,  31.11,  56.33,   1\.  ],
       [   nan,    nan,  12\.  ,  16.7 ,   2\.  ]])

如你所见,前两列没有被读取。

如何操作…

让我们导入必要的库并开始。我们将继续定义一个示例输入,最后展示数据预处理过程。

import numpy as np
from StringIO import StringIO

# Define a data set
in_data = StringIO("30kg,inr2000,31.11,56.33,1\n52kg,inr8000.35,12,16.7,2")

# 1.Let us define two data pre-processing using lambda functions,
strip_func_1 = lambda x : float(x.rstrip("kg"))
strip_func_2 = lambda x : float(x.lstrip("inr"))

# 2.Let us now create a dictionary of these functions,
convert_funcs = {0:strip_func_1,1:strip_func_2}

# 3.Now provide this dictionary of functions to genfromtxt.
data = np.genfromtxt(in_data,delimiter=",", converters=convert_funcs)

# Using a lambda function to handle conversions
in_data = StringIO("10,20,30\n56,,90\n33,46,89")
mss_func = lambda x : float(x.strip() or -999)
data = np.genfromtxt(in_data,delimiter=",", converters={1:mss_func})

它是如何工作的…

在第 1 步中,我们定义了两个 lambda 函数,一个用于第 1 列,需要去掉右侧的字符串 'kg',另一个用于第 2 列,需要去掉左侧的字符串 'inr'。

在第 2 步中,我们将定义一个字典,其中键是需要应用函数的列名,值是该函数。这个字典作为参数传递给 genfromtext,参数名为 converters

现在,输出结果如下:

>>> data
array([[  3.00000000e+01,   2.00000000e+03,   3.11100000e+01,
          5.63300000e+01,   1.00000000e+00],
       [  5.20000000e+01,   8.00035000e+03,   1.20000000e+01,
          1.67000000e+01,   2.00000000e+00]])

请注意,Nan 已经消失,取而代之的是输入中的实际值。

还有更多…

转换器还可以通过 lambda 函数处理输入记录中的缺失值:

in_data = StringIO("10,20,30\n56,,90\n33,46,89")
mss_func = lambda x : float(x.strip() or -999)
data = np.genfromtxt(in_data,delimiter=",", converters={1:mss_func})

lambda 函数将为缺失值返回 -999。在我们的输入中,第二行的第二列为空,应将其替换为 -999。最终输出如下:

>>> data
array([[  10.,   20.,   30.],
       [  56., -999.,   90.],
       [  33.,   46.,   89.]])

请参考此处提供的 SciPy 文档以获取更多详细信息:

docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt.html

docs.scipy.org/doc/numpy/reference/generated/numpy.genfromtxt.html

排序列表

我们将从排序一个列表开始,然后继续排序其他可迭代对象。

准备工作

有两种方法进行排序。第一种是使用列表中的内建sort函数,另一种是使用sorted函数。让我们通过一个示例来操作一下。

如何操作…

让我们看看如何利用sortsorted函数:

# Let us look at a very small code snippet, which does sorting of a given list.
a = [8, 0, 3, 4, 5, 2, 9, 6, 7, 1]
b = [8, 0, 3, 4, 5, 2, 9, 6, 7, 1]

print a
a.sort()
print a

print b
b_s = sorted(b)
print b_s

它是如何工作的…

我们声明了两个列表,ab,它们具有相同的元素。为了方便验证输出,我们将打印列表a

[8, 0, 3, 4, 5, 2, 9, 6, 7, 1]

我们使用了列表数据类型提供的sort函数,a.sort(),来进行就地排序。以下的打印语句表明列表已经排序完成:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

现在,我们将使用sorted函数。该函数对列表进行排序并返回一个新的已排序列表。你可以看到,我们通过sorted(b)调用了它,并将输出存储在b_s中。对b_s进行打印时,输出是排序后的结果:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

还有更多内容…

sort函数仅适用于列表数据类型。默认情况下,排序是按升序进行的;可以通过reverse参数控制排序顺序。默认情况下,reverse设置为False

>>> a = [8, 0, 3, 4, 5, 2, 9, 6, 7, 1]
>>> print a
[8, 0, 3, 4, 5, 2, 9, 6, 7, 1]
>>> a.sort(reverse=True)
>>> print a
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>>
Now, we have a descending order sorting.
For other iterables, we have to fall back on the sorted function. Let's look at a tuple example:
>>> a = (8, 0, 3, 4, 5, 2, 9, 6, 7, 1)
>>> sorted(a)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

按键排序

到目前为止,我们看到所有示例都是通过元素来排序列表或序列。现在,我们继续看看是否可以使用键来排序。在前面的示例中,元素就是键。在现实世界中,存在更复杂的记录,其中记录包含多个列,我们可能希望按一列或多列进行排序。我们将通过一系列元组来演示这一点,其他序列对象也是如此。

准备工作

在我们的示例中,一个元组表示一个人的记录,包括他的名字、ID 和年龄。我们来编写一个排序程序,按不同字段排序。

如何操作…

让我们通过使用列表和元组来定义一个类似记录的结构。我们将使用这些数据来演示如何通过键进行数据排序:

#1.The first step is to create a list of tuples, which we will use to test our sorting.

employee_records = [ ('joe',1,53),('beck',2,26), \
                     ('ele',6,32),('neo',3,45),  \
                    ('christ',5,33),('trinity',4,29), \
                    ]

# 2.Let us now sort it by employee name
print sorted(employee_records,key=lambda emp : emp[0])
"""
It prints as follows
[('beck', 2, 26), ('christ', 5, 33), ('ele', 6, 32), ('joe', 1, 53), ('neo', 3, 45), ('trinity', 4, 29)]
"""
# 3.Let us now sort it by employee id
print sorted(employee_records,key=lambda emp : emp[1])
"""
It prints as follows
[('joe', 1, 53), ('beck', 2, 26), ('neo', 3, 45), ('trinity', 4, 29), ('christ', 5, 33), ('ele', 6, 32)]
"""
# 4.Finally we sort it with employee age
print sorted(employee_records,key=lambda emp : emp[2])
"""
Its prints as follows
[('beck', 2, 26), ('trinity', 4, 29), ('ele', 6, 32), ('christ', 5, 33), ('neo', 3, 45), ('joe', 1, 53)]
"""

它是如何工作的…

在我们的示例中,每个记录有三个字段:姓名、身份证和年龄。我们使用了lambda函数来传递一个排序键,用来排序给定的记录。在步骤 2 中,我们将姓名作为键进行排序。同样,在步骤 2 和 3 中,我们将 ID 和年龄作为键进行排序。我们可以看到在各个步骤中,输出按我们希望的特定键进行排序。

还有更多内容…

由于按键排序的重要性,Python 提供了一个方便的函数来访问键,而无需编写lambdaoperator模块提供了itemgetterattrgettermethodcaller函数。我们看到的排序示例可以使用itemgetter这样编写:

from operator import itemgetter
employee_records = [ ('joe',1,53),('beck',2,26), \
                     ('ele',6,32),('neo',3,45),  \
                     ('christ',5,33),('trinity',4,29), \
                     ]
print sorted(employee_records,key=itemgetter(0))
"""
[('beck', 2, 26), ('christ', 5, 33), ('ele', 6, 32), ('joe', 1, 53), ('neo', 3, 45), ('trinity', 4, 29)]
"""
print sorted(employee_records,key=itemgetter(1))
"""
[('joe', 1, 53), ('beck', 2, 26), ('neo', 3, 45), ('trinity', 4, 29), ('christ', 5, 33), ('ele', 6, 32)]
"""
print sorted(employee_records,key=itemgetter(2))
"""
[('beck', 2, 26), ('trinity', 4, 29), ('ele', 6, 32), ('christ', 5, 33), ('neo', 3, 45), ('joe', 1, 53)]
"""

注意,我们没有使用 lambda 函数,而是使用 itemgetter 来指定排序的键。若需要多级排序,可以向 itemgetter 提供多个字段;例如,假设我们需要按姓名排序,然后按年龄排序,我们的代码如下:

>>> sorted(employee_records,key=itemgetter(0,1))
[('beck', 2, 26), ('christ', 5, 33), ('ele', 6, 32), ('joe', 1, 53), ('neo', 3, 45), ('trinity', 4, 29)]

attrgettermethodcaller 在我们的可迭代元素是类对象时非常有用。看下面的例子:

# Let us now enclose the employee records as class objects,
class employee(object):
    def __init__(self,name,id,age):
        self.name = name
        self.id = id
        self.age = age
    def pretty_print(self):
       print self.name,self.id,self.age

# Now let us populate a list with these class objects.
employee_records = []
emp1 = employee('joe',1,53)
emp2 = employee('beck',2,26)
emp3 = employee('ele',6,32)

employee_records.append(emp1)
employee_records.append(emp2)
employee_records.append(emp3)

# Print the records
for emp in employee_records:
    emp.pretty_print()

from operator import attrgetter
employee_records_sorted = sorted(employee_records,key=attrgetter('age'))
# Now let us print the sorted list,
for emp in employee_records_sorted:
    emp.pretty_print()

构造函数使用三个变量初始化类:姓名、年龄和 ID。我们还拥有 pretty_print 方法来打印类对象的值。

接下来,让我们用这些类对象填充一个列表:

employee_records = []
emp1 = employee('joe',1,53)
emp2 = employee('beck',2,26)
emp3 = employee('ele',6,32)

employee_records.append(emp1)
employee_records.append(emp2)
employee_records.append(emp3)

现在,我们有一个员工对象列表。每个对象中有三个变量:姓名、ID 和年龄。让我们打印出列表,查看其顺序:

joe 1 53
beck 2 26
ele 6 32

如你所见,插入顺序已经被保留。现在,让我们使用 attrgetter 按年龄字段对列表进行排序:

employee_records_sorted = sorted(employee_records,key=attrgetter('age'))

让我们打印排序后的列表。

输出如下:

beck 2 26
ele 6 32
joe 1 53

你可以看到,现在记录已经按年龄排序。

methodcaller 可用于排序,当我们希望使用类中的方法来决定排序时。为了演示,我们添加一个随机方法,它将年龄除以 ID:

class employee(object):
    def __init__(self,name,id,age):
        self.name = name
        self.id = id
        self.age = age

    def pretty_print(self):
       print self.name,self.id,self.age

    def random_method(self):
       return self.age / self.id 

# Populate data
employee_records = []
emp1 = employee('joe',1,53)
emp2 = employee('beck',2,26)
emp3 = employee('ele',6,32)

employee_records.append(emp1)
employee_records.append(emp2)
employee_records.append(emp3)

from operator import methodcaller
employee_records_sorted = sorted(employee_records,key=methodcaller('random_method'))
for emp in employee_records_sorted:
    emp.pretty_print() 

现在我们可以通过调用此方法来对列表进行排序:

sorted(employee_records,key=methodcaller('random_method'))

现在,让我们按排序顺序打印列表并查看输出:

ele 6 32
beck 2 26
joe 1 53

使用 itertools 进行工作

Itertools 包括处理可迭代对象的函数;它的灵感来源于如 Haskell 这样的函数式编程语言。它们承诺内存高效且非常快速。

准备就绪

Itertools 中有很多函数可以使用;我们将在通过例子进行讲解时逐一介绍其中的一些。已提供完整函数列表的链接。

如何实现…

让我们继续看一组 Python 脚本,用于演示如何使用 itertools:

# Load libraries
from itertools import chain,compress,combinations,count,izip,islice

# 1.Chain example, where different iterables can be combined together.
a = [1,2,3]
b = ['a','b','c']
print list(chain(a,b)) # prints [1, 2, 3, 'a', 'b', 'c']

# 2.Compress example, a data selector, where the data in the first iterator
#  is selected based on the second iterator.
a = [1,2,3]
b = [1,0,1]
print list(compress(a,b)) # prints [1, 3]

# 3.From a given list, return n length sub sequences.
a = [1,2,3,4]
print list(combinations(a,2)) # prints [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]

# 4.A counter which produces infinite consequent integers, given a start integer,
a = range(5)
b = izip(count(1),a)
for element in b:
    print element

# 5.	Extract an iterator from another iterator, 
# let us say we want an iterator which only returns every 
# alternate elements from the input iterator
a = range(100)
b = islice(a,0,100,2)
print list(b)

它是如何工作的…

第 1 步是非常直接的,其中两个可迭代对象通过 chain() 被合并。需要注意的是,chain() 直到实际调用时才会被实现。请看以下命令行:

>>> chain(a,b)
<itertools.chain object at 0x060DD0D0>

调用 chain(a,b) 返回链对象。然而,当我们运行以下命令时,实际输出是:

>>> list(chain(a,b))
[1, 2, 3, 'a', 'b', 'c']

第 2 步介绍了 compress。在这个例子中,a 的元素根据 b 中的元素来选择。你可以看到,b 中的第二个值是零,因此 a 中的第二个值也没有被选中。

第 3 步进行简单的数学组合。我们有一个输入列表 a,并希望将 a 中的元素进行两两组合。

第 4 步解释了计数器对象,它可以作为一个无限序列号资源,给定一个起始号码。运行代码后,我们将得到如下输出:

(1, 0)
(2, 1)
(3, 2)
(4, 3)
(5, 4)

你可以看到,我们在这里使用了 izip。 (zipizip 在前面的章节中已经介绍过。) 我们的输出是一个元组,其中第一个元素由计数器提供,第二个元素由我们的输入列表 a 提供。

第 5 步详细介绍了 islice 操作;islice 与我们在前一节中讲解的 slice 相同,唯一的区别是 islice 更节省内存,且除非被调用,否则不会生成完整的输出。

参见docs.python.org/2/library/itertools.html获取完整的 itertools 列表。

第二章. Python 环境

在这一章中,我们将介绍以下几个配方:

  • 使用 NumPy 库

  • 使用 matplotlib 绘图

  • 使用 scikit-learn 进行机器学习

介绍

在这一章中,我们将介绍 Python 环境,这将在本书中被广泛使用。我们将从 NumPy 开始,它是一个用于高效处理数组和矩阵的 Python 库。它是本书中大多数其他库的基础。接着,我们将介绍一个名为 matplotlib 的 Python 绘图库。最后,我们将介绍一个名为 scikit-learn 的机器学习库。

使用 NumPy 库

NumPy 提供了一种高效的方式来处理 Python 中的大型数组。大多数 Python 科学计算库在内部都使用 NumPy 进行数组和矩阵操作。在本书中,我们将广泛使用 NumPy。在这一节中,我们将介绍 NumPy。

准备工作

我们将编写一系列 Python 语句来操作数组和矩阵,并在过程中学习如何使用 NumPy。我们的目的是让你习惯于使用 NumPy 数组,因为 NumPy 将作为本书中大多数配方的基础。

如何操作…

让我们从创建一些简单的矩阵和数组开始:

#Recipe_1a.py
# Importing numpy as np
import numpy as np
# Creating arrays
a_list = [1,2,3]
an_array = np.array(a_list)
# Specify the datatype
an_array = np.array(a_list,dtype=float)

# Creating matrices
a_listoflist = [[1,2,3],[5,6,7],[8,9,10]]
a_matrix = np.matrix(a_listoflist,dtype=float)

现在我们将编写一个小的便利函数来检查我们的 NumPy 对象:

#Recipe_1b.py
# A simple function to examine given numpy object 
def display_shape(a):
    print 
    print a
    print
    print "Nuber of elements in a = %d"%(a.size)
    print "Number of dimensions in a = %d"%(a.ndim)
    print "Rows and Columns in a ",a.shape
    print 

display_shape(a_matrix)

让我们看看创建数组的其他几种方法:

#Recipe_1c.py
# Alternate ways of creating arrays
# 1\. Leverage np.arange to create numpy array
created_array = np.arange(1,10,dtype=float)
display_shape(created_array)

# 2\. Using np.linspace to create numpy array
created_array = np.linspace(1,10)
display_shape(created_array)

# 3\. Create numpy arrays in using np.logspace
created_array = np.logspace(1,10,base=10.0)
display_shape(created_array)

# Specify step size in arange while creating
# an array. This is where it is different
# from np.linspace
created_array = np.arange(1,10,2,dtype=int)
display_shape(created_array)

我们现在来看看创建一些特殊矩阵的方法:

#Recipe_1d.py
# Create a matrix will all elements as 1
ones_matrix = np.ones((3,3))
display_shape(ones_matrix)
# Create a matrix with all elements as 0
zeros_matrix = np.zeros((3,3))
display_shape(zeros_matrix)

# Identity matrix
# k parameter  controls the index of 1
# if k =0, (0,0),(1,1,),(2,2) cell values
# are set to 1 in a 3 x 3 matrix
identity_matrix = np.eye(N=3,M=3,k=0)
display_shape(identity_matrix)
identity_matrix = np.eye(N=3,k=1)
display_shape(identity_matrix)

掌握了数组和矩阵创建的知识后,我们来看一些变形操作:

Recipe_1e.py
# Array shaping
a_matrix = np.arange(9).reshape(3,3)
display_shape(a_matrix)
.
.
.
display_shape(back_array)

现在,继续查看一些矩阵操作:

#Recipe_1f.py
# Matrix operations
a_matrix = np.arange(9).reshape(3,3)
b_matrix = np.arange(9).reshape(3,3)
.
.
.
print "f_matrix, row sum",f_matrix.sum(axis=1)

最后,让我们看看一些反向、复制和网格操作:

#Recipe_1g.py
# reversing elements
display_shape(f_matrix[::-1])
.
.
.
zz = zz.flatten()

让我们看一下 NumPy 库中的一些随机数生成方法:

#Recipe_1h.py
# Random numbers
general_random_numbers = np.random.randint(1,100, size=10)
print general_random_numbers
.
.
.
uniform_rnd_numbers = np.random.normal(loc=0.2,scale=0.2,size=(3,3))

它是如何工作的…

让我们从导入 NumPy 库开始:

# Importing numpy as np
import numpy as np

让我们继续看一下在 NumPy 中创建数组的各种方式:

# Arrays
a_list = [1,2,3]
an_array = np.array(a_list)
# Specify the datatype
an_array = np.array(a_list,dtype=float)

数组可以通过列表创建。在前面的示例中,我们声明了一个包含三个元素的列表。然后,我们可以使用np.array()将列表转换为一个 NumPy 一维数组。

数据类型也可以被指定,如前面代码中的最后一行所示:

我们现在将从数组转向矩阵:

# Matrices
a_listoflist = [[1,2,3],[5,6,7],[8,9,10]]
a_matrix = np.matrix(a_listoflist,dtype=float)

我们将从一个listoflist创建一个矩阵。同样,我们可以指定数据类型。

在我们继续之前,我们将定义一个display_shape函数。我们将在后面频繁使用这个函数:

def display_shape(a):
    print 
    print a
    print
    print "Nuber of elements in a = %d"%(a.size)
    print "Number of dimensions in a = %d"%(a.ndim)
    print "Rows and Columns in a ",a.shape
    print

每个 NumPy 对象都有以下三个属性:

size: 给定 NumPy 对象中的元素数量

ndim: 维度的数量

shape: shape 返回一个包含对象维度的元组

这个函数除了打印原始元素外,还会打印出所有三个属性。

让我们调用之前创建的矩阵来使用这个函数:

display_shape(a_matrix)

它是如何工作的…

正如你所看到的,我们的矩阵包含九个元素,并且有两个维度。最后,我们可以看到形状显示了每个维度的大小和元素的数量。在这种情况下,我们有一个三行三列的矩阵。

现在让我们看看创建数组的其他几种方式:

created_array = np.arange(1,10,dtype=float)
display_shape(created_array)

NumPy 的 arrange 函数返回给定区间内均匀间隔的值。在此情况下,我们想要一个从 1 到 10 之间均匀间隔的数字。有关 arange 的更多信息,请参考以下链接:

docs.scipy.org/doc/numpy/reference/generated/numpy.arange.html

# An alternate way to create array
created_array = np.linspace(1,10)
display_shape(created_array)

NumPy 的 linspace 类似于 arrange。不同之处在于我们会请求所需样本的数量。使用 linspace,我们可以指定在给定范围内需要多少个元素。默认情况下,它返回 50 个元素。然而,在 arange 中,我们需要指定步长:

created_array = np.logspace(1,10,base=10.0)
display_shape(created_array)

NumPy 提供了多种函数来创建特殊类型的数组:

ones_matrix = np.ones((3,3))
display_shape(ones_matrix)

# Create a matrix with all elements as 0
zeros_matrix = np.zeros((3,3))
display_shape(zeros_matrix)

ones()zeros() 函数分别用于创建一个包含 1 和 0 的矩阵:

它是如何工作的…

矩阵的单位矩阵创建如下:

identity_matrix = np.eye(N=3,M=3,k=0)
display_shape(identity_matrix)

k 参数控制值 1 开始的索引位置:

identity_matrix = np.eye(N=3,k=1)
display_shape(identity_matrix)

它是如何工作的…

数组的形状可以通过 reshape 函数来控制:

# Array shaping
a_matrix = np.arange(9).reshape(3,3)
display_shape(a_matrix)

通过传递 -1,我们可以将数组重塑为所需的任意维度:

# Paramter -1 refers to as many as dimension needed
back_to_array = a_matrix.reshape(-1)
display_shape(back_to_array)

它是如何工作的…

ravelflatten 函数可用于将矩阵转换为一维数组:

a_matrix = np.arange(9).reshape(3,3)
back_array = np.ravel(a_matrix)
display_shape(back_array)

a_matrix = np.arange(9).reshape(3,3)
back_array = a_matrix.flatten()
display_shape(back_array)

它是如何工作的…

让我们来看一些矩阵操作,比如加法:

c_matrix = a_matrix + b_matrix

我们还将看一下逐元素乘法:

d_matrix = a_matrix * b_matrix

以下代码展示了一个矩阵乘法操作:

e_matrix = np.dot(a_matrix,b_matrix)

最后,我们将对矩阵进行转置:

f_matrix = e_matrix.T

minmax 函数可用于找到矩阵中的最小值和最大值。sum 函数可用于找到矩阵中每行或每列的总和:

print
print "f_matrix,minimum = %d"%(f_matrix.min())
print "f_matrix,maximum = %d"%(f_matrix.max())
print "f_matrix, col sum",f_matrix.sum(axis=0)
print "f_matrix, row sum",f_matrix.sum(axis=1)

它是如何工作的…

矩阵的元素可以通过以下方式进行反转:

# reversing elements
display_shape(f_matrix[::-1])

copy 函数可用于复制一个矩阵,如下所示:

# Like python all elements are used by reference
# if copy is needed copy() command is used
f_copy = f_matrix.copy()

最后,让我们看看 mgrid 功能:

# Grid commands
xx,yy,zz = np.mgrid[0:3,0:3,0:3]
xx = xx.flatten()
yy = yy.flatten()
zz = zz.flatten()

mgrid 功能可用于获取 m 维度的坐标。在前面的示例中,我们有三个维度。在每个维度中,我们的值范围从 03。让我们打印 xxyyzz 来更好地理解:

它是如何工作的…

让我们看看每个数组的第一个元素。[0,0,0] 是我们三维空间中的第一个坐标。所有三个数组中的第二个元素,[0,0,1] 是我们空间中的另一个点。类似地,使用 mgrid,我们捕获了三维坐标系统中的所有点。

NumPy 为我们提供了一个名为 random 的模块,用于生成随机数。让我们看看一些随机数生成的例子:

# Random numbers
general_random_numbers = np.random.randint(1,100, size=10)
print general_random_numbers

使用随机模块中的randint函数,我们可以生成随机整数。我们可以传递startendsize参数。在我们的例子中,起始值是1,结束值是100,大小是10。我们希望获得 1 到 100 之间的 10 个随机整数。让我们看看返回的输出:

它是如何工作的…

也可以生成其他分布的随机数。让我们来看一个例子,获取 10 个来自normal分布的随机数:

uniform_rnd_numbers = np.random.normal(loc=0.2,scale=0.2,size=10)
print uniform_rnd_numbers

使用normal函数,我们将从normal分布中生成一个随机样本。normal分布的均值和标准差参数由locscale参数指定。最后,size决定了样本的数量。

通过传递一个包含行和列值的元组,我们也可以生成一个随机矩阵:

uniform_rnd_numbers = np.random.normal(loc=0.2,scale=0.2,size=(3,3))

在前面的示例中,我们生成了一个 3 x 3 的矩阵,代码如下所示:

它是如何工作的…

还有更多…

你可以参考以下链接,查看一些优秀的 NumPy 文档:

www.numpy.org/

另请参见

  • 在第三章中的使用 matplotlib 绘图示例,分析数据 - 探索与整理

  • 在第三章中的机器学习与 Scikit Learn示例,分析数据 - 探索与整理

使用 matplotlib 绘图

Matplotlib Python 是一个二维绘图库。Python 可以生成各种图形,包括直方图、散点图、折线图、点图、热图等。在本书中,我们将使用matplotlibpyplot接口来满足我们所有的可视化需求。

准备就绪

在这个示例中,我们将介绍使用pyplot的基本绘图机制。我们将在本书中的几乎所有示例中使用pyplot来进行可视化。

我们在本书中的所有示例中都使用了 matplotlib 版本 1.3.1。在你的命令行中,你可以调用__version__属性来检查版本:

准备就绪

如何操作…

让我们从查看如何使用 matplotlib 的pyplot模块绘制简单图形开始:

#Recipe_2a.py
import numpy as np
import matplotlib.pyplot as plt
def simple_line_plot(x,y,figure_no):
    plt.figure(figure_no)
    plt.plot(x,y)
    plt.xlabel('x values')
    plt.ylabel('y values')
    plt.title('Simple Line')

def simple_dots(x,y,figure_no):
    plt.figure(figure_no)
    plt.plot(x,y,'or')
    plt.xlabel('x values')
    plt.ylabel('y values')
    plt.title('Simple Dots')

def simple_scatter(x,y,figure_no):
    plt.figure(figure_no)
    plt.scatter(x,y)
    plt.xlabel('x values')
    plt.ylabel('y values')
    plt.title('Simple scatter')

def scatter_with_color(x,y,labels,figure_no):
    plt.figure(figure_no)
    plt.scatter(x,y,c=labels)
    plt.xlabel('x values')
    plt.ylabel('y values')
    plt.title('Scatter with color')

if __name__ == "__main__":

    plt.close('all')
    # Sample x y data for line and simple dot plots
    x = np.arange(1,100,dtype=float)
    y = np.array([np.power(xx,2) for xx in x])

    figure_no=1
    simple_line_plot(x,y,figure_no)
    figure_no+=1
    simple_dots(x,y,figure_no)

    # Sample x,y data for scatter plot
    x = np.random.uniform(size=100)
    y = np.random.uniform(size=100)

    figure_no+=1
    simple_scatter(x,y,figure_no)
    figure_no+=1
    label = np.random.randint(2,size=100)
    scatter_with_color(x,y,label,figure_no)
    plt.show()

现在我们将继续探讨一些高级主题,包括生成热图和标记 xy 轴:

#Recipe_2b.py
import numpy as np
import matplotlib.pyplot as plt
def x_y_axis_labeling(x,y,x_labels,y_labels,figure_no):
    plt.figure(figure_no)
    plt.plot(x,y,'+r')
    plt.margins(0.2)
    plt.xticks(x,x_labels,rotation='vertical')
    plt.yticks(y,y_labels,)

def plot_heat_map(x,figure_no):
    plt.figure(figure_no)
    plt.pcolor(x)
    plt.colorbar()

if __name__ == "__main__":

    plt.close('all')
    x = np.array(range(1,6))
    y = np.array(range(100,600,100))
    x_label = ['element 1','element 2','element 3','element 4','element 5']
    y_label = ['weight1','weight2','weight3','weight4','weight5']

    x_y_axis_labeling(x,y,x_label,y_label,1)

    x = np.random.normal(loc=0.5,scale=0.2,size=(10,10))
    plot_heat_map(x,2)

    plt.show()

它是如何工作的…

我们将从导入所需的模块开始。在使用pyplot时,建议你导入 NumPy:

import numpy as np
import matplotlib.pyplot as plt

我们从主函数的代码开始。可能会有之前程序运行生成的图形。关闭它们是个好习惯,因为我们将在程序中使用更多的图形:

    plt.close('all')

我们将继续使用 NumPy 生成一些数据,以演示如何使用pyplot进行绘图:

    # Sample x y data for line and simple dot plots
    x = np.arange(1,100,dtype=float)
    y = np.array([np.power(xx,2) for xx in x])

我们在 x 和 y 变量中各生成了 100 个元素。我们的 y 是 x 变量的平方。

让我们继续进行一个简单的折线图:

    figure_no=1
    simple_line_plot(x,y,figure_no)

当我们的程序中有多个图表时,给每个图表编号是一种好的做法。变量 figure_no 用于编号我们的图表。让我们看看 simple_line_plot 函数:

def simple_line_plot(x,y,figure_no):
    plt.figure(figure_no)
    plt.plot(x,y)
    plt.xlabel('x values')
    plt.ylabel('y values')
    plt.title('Simple Line')

如你所见,我们通过在 pyplot 中调用 figure 函数来开始编号图表。我们将 figure_no 变量从主程序中传入。之后,我们简单地调用 plot 函数,并传入我们的 x 和 y 值。我们可以通过使用 xlabelylabel 函数分别为 x 轴和 y 轴命名,使得图表更有意义。最后,我们还可以为图表添加标题。就这样,我们的第一个简单折线图就准备好了。直到调用 show() 函数,图表才会显示。在我们的代码中,我们将调用 show() 函数以查看所有图表。我们的图表如下所示:

它是如何工作的…

在这里,我们将 x 值绘制在 x 轴上,将 x 的平方 绘制在 y 轴上。

我们创建了一个简单的折线图。我们可以看到一条漂亮的曲线,因为我们的y 值x 值的平方。

接下来我们进行下一个图表:

    figure_no+=1
    simple_dots(x,y,figure_no)

我们将增加图表编号并调用 simple_dots 函数。我们想将我们的 xy 值绘制为点,而不是折线。让我们看一下 simple_dots 函数:

def simple_dots(x,y,figure_no):
    plt.figure(figure_no)
    plt.plot(x,y,'or')
    plt.xlabel('x values')
    plt.ylabel('y values')
    plt.title('Simple Dots')

每一行都与我们之前的函数类似,除了以下这一行:

    plt.plot(x,y,'or')

or 参数表示我们需要点 (o),并且点的颜色是红色 (r)。以下是前一个命令的输出:

它是如何工作的…

让我们继续下一个图表。

我们将要查看一个散点图。让我们使用 NumPy 生成一些数据:

    # Sample x,y data for scatter plot
    x = np.random.uniform(size=100)
    y = np.random.uniform(size=100)

我们从均匀分布中抽取了 100 个数据点。现在我们将调用 simple_scatter 函数来生成我们的散点图:

    figure_no+=1
    simple_scatter(x,y,figure_no)

simple_scatter 函数中,所有行都与之前的绘图程序类似,除了以下这一行:

    plt.scatter(x,y)

我们没有在 pyplot 中调用 plot 函数,而是调用了 scatter 函数。我们的图表如下所示:

它是如何工作的…

接下来,我们进行最终的图表绘制,这是一个散点图,但点的颜色根据它们所属的类别标签进行区分:

    figure_no+=1
    label = np.random.randint(2,size=100)
    scatter_with_color(x,y,label,figure_no)

我们将增加图表编号以跟踪我们的图表。在接下来的代码行中,我们将为数据点分配一些随机标签,标签为 10。最后,我们将调用 scatter_with_color 函数,传入我们的 xy 和标签变量。

在函数中,让我们看一下与之前的散点图代码不同的一行:

    plt.scatter(x,y,c=labels)

我们将标签传递给 c 参数,表示颜色。每个标签将被分配一个唯一的颜色。在我们的例子中,所有标签为 0 的点将与标签为 1 的点有不同的颜色,如下所示:

它是如何工作的…

让我们继续绘制一些热图,并添加坐标轴标签。

再次,我们将从主函数开始:

    plt.close('all')
    x = np.array(range(1,6))
    y = np.array(range(100,600,100))
    x_label = ['element 1','element 2','element 3','element 4','element 5']
    y_label = ['weight1','weight2','weight3','weight4','weight5']

    x_y_axis_labeling(x,y,x_label,y_label,1)

作为良好的实践,我们将在生成新图形之前,通过调用 close 函数关闭所有之前的图形。接下来,我们将生成一些数据。我们的 x 是一个包含五个元素的数组,起始值为 1,结束值为 5。我们的 y 是一个包含五个元素的数组,起始值为 100,结束值为 500。我们定义了两个 x_labely_label 列表,它们将作为我们绘图的标签。最后,我们调用了 x_y_axis_labeling 函数,目的是演示如何在 x 轴和 y 轴上标记刻度。

让我们来看一下以下函数:

def x_y_axis_labeling(x,y,x_labels,y_labels,figure_no):
    plt.figure(figure_no)
    plt.plot(x,y,'+r')
    plt.margins(0.2)
    plt.xticks(x,x_labels,rotation='vertical')
    plt.yticks(y,y_labels,)

我们将通过调用 pyplot 的 dot 函数来绘制简单的点图。然而,在这种情况下,我们希望我们的点显示为 + 而不是 o。因此,我们将指定 +r。我们选择的颜色是红色,因此 r

在接下来的两行中,我们将指定我们的 x 轴和 y 轴的刻度标记。通过调用 xticks 函数,我们将传递我们的 x 值及其标签。此外,我们会设置文本垂直旋转,以避免它们重叠。类似地,我们将为 y 轴指定刻度标记。让我们来看一下我们的图表,如下所示:

它是如何工作的…

让我们看看如何使用 pyplot 生成热图:

    x = np.random.normal(loc=0.5,scale=0.2,size=(10,10))
    plot_heat_map(x,2)

我们将生成一些数据用于热图。在这个例子中,我们生成了一个 10 x 10 的矩阵,矩阵中的值来自一个正态分布,其均值由 loc 变量指定为 0.5,标准差由 scale 变量指定为 0.2。我们将使用这个矩阵调用 plot_heat_map 函数。第二个参数是图形编号:

def plot_heat_map(x,figure_no):
    plt.figure(figure_no)
    plt.pcolor(x)
    plt.colorbar()

我们将调用 pcolor 函数来生成热图。下一行调用了 colorbar 函数,用于显示我们值范围的颜色渐变:

它是如何工作的…

还有更多…

有关 matplotlib 的更多信息,你可以参考 matplotlib 的常规文档,网址为 matplotlib.org/faq/usage_faq.html

以下链接是关于 pyplot 的一个极好的教程:

matplotlib.org/users/pyplot_tutorial.html

Matplotlib 提供了出色的三维绘图功能。有关更多信息,请参考以下链接:

matplotlib.org/mpl_toolkits/mplot3d/tutorial.html

matplotlib 中的 pylab 模块将 NumPy 的命名空间与 pyplot 结合在一起。Pylab 还可以用来生成本食谱中展示的各种类型的图表。

使用 scikit-learn 进行机器学习

Scikit-learn 是一个多功能的 Python 机器学习库。在本书中,我们将广泛使用这个库。我们在本书中的所有示例中使用了 scikit-learn 版本 0.15.2。在命令行中,你可以通过调用 __version__ 属性来检查版本:

使用 scikit-learn 进行机器学习

准备工作

在本例中,我们将展示 scikit-learn 的一些功能,并了解它们的 API 组织方式,以便在未来的例子中能顺利使用它。

如何操作…

Scikit-learn 为我们提供了一个内建的数据集。让我们看看如何访问这个数据集并使用它:

#Recipe_3a.py
from sklearn.datasets import load_iris,load_boston,make_classification                         make_circles, make_moons

# Iris dataset
data = load_iris()
x = data['data']
y = data['target']
y_labels = data['target_names']
x_labels = data['feature_names']

print
print x.shape
print y.shape
print x_labels
print y_labels

# Boston dataset
data = load_boston()
x = data['data']
y = data['target']
x_labels = data['feature_names']

print
print x.shape
print y.shape
print x_labels

# make some classification dataset
x,y = make_classification(n_samples=50,n_features=5, n_classes=2)

print
print x.shape
print y.shape

print x[1,:]
print y[1]

# Some non linear dataset
x,y = make_circles()
import numpy as np
import matplotlib.pyplot as plt
plt.close('all')
plt.figure(1)
plt.scatter(x[:,0],x[:,1],c=y)

x,y = make_moons()
import numpy as np
import matplotlib.pyplot as plt
plt.figure(2)
plt.scatter(x[:,0],x[:,1],c=y)

plt.show()

让我们继续查看如何在 scikit-learn 中调用一些机器学习功能:

#Recipe_3b.py
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
# Data Preprocessing routines
x = np.asmatrix([[1,2],[2,4]])
poly = PolynomialFeatures(degree = 2)
poly.fit(x)
x_poly = poly.transform(x)

print "Original x variable shape",x.shape
print x
print
print "Transformed x variables",x_poly.shape
print x_poly

#alternatively 
x_poly = poly.fit_transform(x)

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris

data = load_iris()
x = data['data']
y = data['target']

estimator = DecisionTreeClassifier()
estimator.fit(x,y)
predicted_y = estimator.predict(x)
predicted_y_prob = estimator.predict_proba(x)
predicted_y_lprob = estimator.predict_log_proba(x)

from sklearn.pipeline import Pipeline

poly = PolynomialFeatures(n=3)
tree_estimator = DecisionTreeClassifier()

steps = [('poly',poly),('tree',tree_estimator)]
estimator = Pipeline(steps=steps)
estimator.fit(x,y)
predicted_y = estimator.predict(x)

工作原理…

让我们加载 scikit-learn 库,并导入包含各种函数的模块,以便提取内建数据集:

from sklearn.datasets import load_iris,load_boston,make_classification

我们将查看的第一个数据集是 iris 数据集。欲了解更多信息,请参考en.wikipedia.org/wiki/Iris_flower_data_set

由 Sir Donald Fisher 引入,这是一个经典的分类问题数据集:

data = load_iris()
x = data['data']
y = data['target']
y_labels = data['target_names']
x_labels = data['feature_names']

load_iris函数在调用时,会返回一个字典对象。通过使用适当的键查询字典对象,可以提取出预测变量x、响应变量y、响应变量名称以及特征名称。

让我们继续打印它们并查看它们的值:

print
print x.shape
print y.shape
print x_labels
print y_labels

工作原理…

如你所见,我们的预测变量有 150 个实例和四个属性。我们的响应变量有 150 个实例,每个预测实例都有一个类标签。接下来,我们将打印出特征名称——花瓣和萼片的宽度和长度,最后是类标签。在我们未来的大部分例子中,我们将广泛使用这个数据集。

让我们继续检查另一个内建的数据集——波士顿房价数据集,该数据集用于回归问题:

# Boston dataset
data = load_boston()
x = data['data']
y = data['target']
x_labels = data['feature_names']

数据的加载与 iris 数据集非常相似,数据的各个组件,包括预测变量和响应变量,都是通过使用字典中的相应键进行查询的。让我们打印这些变量,以便检查它们:

工作原理…

如你所见,我们的预测变量集x有 506 个实例和 13 个特征。我们的响应变量有 506 个条目。最后,我们还将打印出特征名称。

Scikit-learn 还为我们提供了帮助我们生成具有某些期望属性的随机分类数据集的函数:

# make some classification dataset
x,y = make_classification(n_samples=50,n_features=5, n_classes=2)

make_classification函数是一个可以用来生成分类数据集的函数。在我们的例子中,我们生成了一个包含 50 个实例的数据集,其中实例数由n_samples参数决定,特征数由n_features参数决定,类别数由n_classes参数设置。让我们检查该函数的输出:

print x.shape
print y.shape

print x[1,:]
print y[1]

工作原理…

如你所见,我们的预测变量x有 150 个实例和五个特征。我们的响应变量有 150 个实例,每个预测实例都有一个类标签。

我们将打印出预测变量集 x 中的第二条记录。你可以看到,我们有一个维度为 5 的向量,表示我们请求的五个特征。最后,我们还将打印出响应变量 y。对于我们预测变量集中的第二行,类标签为 1

Scikit-learn 还为我们提供了能够生成具有非线性关系的数据的函数:

# Some non linear dataset
x,y = make_circles()
import numpy as npimport matplotlib.pyplot as plt
plt.close('all')
plt.figure(1)
plt.scatter(x[:,0],x[:,1],c=y)

你现在应该已经熟悉了前一个示例中的 pyplot。让我们先看看我们的图表,以便理解非线性关系:

它是如何工作的…

如你所见,我们的分类结果生成了两个同心圆。我们的 x 是一个包含两个变量的数据集。变量 y 是类标签。正如同心圆所示,我们的预测变量之间的关系是非线性的。

另一个有趣的函数来生成非线性关系是 scikit-learn 中的 make_moons

x,y = make_moons()
import numpy as np
import matplotlib.pyplot as plt
plt.figure(2)
plt.scatter(x[:,0],x[:,1],c=y)

让我们通过查看其图表来理解非线性关系:

它是如何工作的…

弯月形的图表显示了我们的预测变量集 x 中的属性是非线性相关的。

接下来,让我们换个角度来理解 scikit-learn 的 API 结构。使用 scikit-learn 的一个主要优势是它干净的 API 结构。所有从 BaseEstimator 类派生的数据建模类都必须严格实现 fittransform 函数。我们将通过一些例子进一步了解这一点。

让我们从 scikit-learn 的预处理模块开始:

import numpy as np
from sklearn.preprocessing import PolynomialFeatures

我们将使用 PolynomialFeatures 类来展示使用 scikit-learn SDK 的简便性。有关多项式的更多信息,请参见以下链接:

zh.wikipedia.org/wiki/多项式

对于一组预测变量,我们可能想要添加更多的变量到预测集,以查看我们的模型准确度是否能够提高。我们可以使用现有特征的多项式作为新特征。PolynomialFeatures 类可以帮助我们做到这一点:

# Data Preprocessing routines
x = np.asmatrix([[1,2],[2,4]])

我们将首先创建一个数据集。在此情况下,我们的数据集包含两个实例和两个属性:

poly = PolynomialFeatures(degree = 2)

我们将继续实例化 PolynomialFeatures 类,并设置所需的多项式的阶数。在此情况下,我们选择的是二阶多项式:

poly.fit(x)
x_poly = poly.transform(x)

然后,有两个函数,fittransformfit 函数用于执行转换所需的计算。在此情况下,fit 是多余的,但稍后我们将看到更多 fit 函数使用的例子。

transform 函数接收输入,并根据 fit 执行的计算来转换给定的输入:

#alternatively 
x_poly = poly.fit_transform(x)

或者,在这种情况下,fittransform 可以一次性调用。让我们看看原始 x 变量和转换后 x 变量的值和形状:

它是如何工作的…

在 scikit-learn 中,任何实现机器学习方法的类都必须继承自 BaseEstimator。请参阅以下链接了解 BaseEstimator:

scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html

BaseEstimator 期望实现类提供 fittransform 方法。这样可以保持 API 的简洁。

让我们看另一个例子。这里,我们从树模块中导入了一个名为 DecisionTreeClassifier 的类。DecisionTreeClassifier 实现了决策树算法:

from sklearn.tree import DecisionTreeClassifier

让我们将这个类付诸实践:

from sklearn.datasets import load_iris

data = load_iris()
x = data['data']
y = data['target']

estimator = DecisionTreeClassifier()
estimator.fit(x,y)
predicted_y = estimator.predict(x)
predicted_y_prob = estimator.predict_proba(x)
predicted_y_lprob = estimator.predict_log_proba(x)

让我们使用鸢尾花数据集来看看决策树算法如何应用。我们将在 xy 变量中加载鸢尾花数据集。接下来,我们将实例化 DecisionTreeClassifier。然后通过调用 fit 函数,并传入我们的 x 预测变量y 响应变量,来构建模型。这将构建出决策树模型。现在,我们已经准备好模型进行预测。我们将使用 predict 函数来预测给定输入的类别标签。如你所见,我们使用了与 PolynomialFeatures 中相同的 fitpredict 方法。还有两个其他方法,predict_proba,它给出预测的概率,以及 predict_log_proba,它提供预测概率的对数值。

现在,让我们来看一个有趣的工具叫做管道(pipe lining)。不同的机器学习方法可以通过管道链式连接在一起:

from sklearn.pipeline import Pipeline

poly = PolynomialFeatures(n=3)
tree_estimator = DecisionTreeClassifier()

让我们从实例化数据处理例程 PolynomialFeaturesDecisionTreeClassifier 开始:

steps = [('poly',poly),('tree',tree_estimator)]

我们将定义一个元组列表来表示我们的链式操作顺序。我们希望先运行多项式特征生成,然后是决策树:

estimator = Pipeline(steps=steps)
estimator.fit(x,y)
predicted_y = estimator.predict(x)

现在我们可以通过使用 steps 变量声明的列表来实例化我们的 Pipeline 对象。接下来,我们可以像往常一样通过调用 fitpredict 方法继续进行业务操作。

我们可以调用 named_steps 属性来检查管道各个阶段的模型:

它是如何工作的…

还有更多内容…

在 scikit-learn 中有许多其他数据集创建函数。请参考以下链接:

scikit-learn.org/stable/datasets/

在使用 make_circlemake_moons 创建非线性数据集时,我们提到可以为数据集添加很多期望的属性。通过引入不正确的类别标签,数据可能会稍微被污染。请参考以下链接,了解可以引入这些细微变化的数据选项:

scikit-learn.org/stable/modules/generated/sklearn.datasets.make_circles.html

scikit-learn.org/stable/modules/generated/sklearn.datasets.make_moons.html

另见

  • 第二章中的绘图教程,使用 Python 环境

第三章 数据分析——探索与清洗

本章将介绍以下几种方法:

  • 图形化分析单变量数据

  • 分组数据并使用点图

  • 使用散点图展示多变量数据

  • 使用热力图

  • 执行汇总统计和绘制图表

  • 使用箱线图

  • 填补缺失数据

  • 执行随机抽样

  • 缩放数据

  • 标准化数据

  • 执行分词

  • 去除停用词

  • 词干化

  • 执行词形还原

  • 将文本表示为词袋模型

  • 计算词频和逆文档频率

介绍

在你开始任何数据科学应用之前,长期来看,深入了解你即将处理的数据总是很有帮助的。对底层数据的理解将帮助你为当前问题选择正确的算法。以不同粒度级别探索数据的过程叫做探索性数据分析EDA)。在许多情况下,EDA能够揭示通常通过数据挖掘算法发现的模式。EDA帮助我们理解数据特征,并为你提供适当的指导,以便选择适合当前问题的算法。

本章将详细介绍EDA。我们将探讨用于有效执行EDA操作的实践技巧和工具。

数据预处理和转换是另外两个重要的过程,它们能够提高数据科学模型的质量,并增加数据科学项目的成功率。

数据预处理是使数据准备好供数据挖掘方法或机器学习算法使用的过程。它包括很多内容,如数据清洗、属性子集选择、数据转换等。我们将在本章中介绍数值数据预处理和文本数据预处理。

文本数据与数值数据有所不同。我们需要不同的转换方法,以使其适合机器学习算法的输入。在本章中,我们将看到如何转换文本数据。通常,文本转换是一个分阶段的过程,包含多个组件,形成一个管道。

一些组件如下所示:

  • 分词

  • 停用词移除

  • 基本形式转换

  • 特征推导

通常,这些组件会应用于给定的文本,以提取特征。在管道的最后,文本数据将被转换成适合机器学习算法输入的形式。在本章中,我们将为每个组件提供相应的解决方案。

许多时候,在数据收集阶段可能会引入很多错误。这些错误可能是由于人为错误、限制或数据测量/收集过程中的缺陷。数据不一致性是一个大挑战。我们将从数据填充开始我们的数据预处理工作,填充是处理输入数据中的错误的方法,然后继续采用其他方法。

通过图形分析单变量数据

仅包含一个变量/列的数据集称为单变量数据。单变量是数学中的一个通用术语,指的是只有一个变量的表达式、方程式、函数或多项式。在我们的例子中,我们将限制单变量函数于数据集。假设我们将测量一组人的身高(单位:米),数据将如下所示:

5, 5.2, 6, 4.7,…

我们的测量仅涉及人群的一个属性——身高。这是单变量数据的一个示例。

准备工作

我们通过可视化查看样本单变量数据来开始我们的EDA流程。通过正确的可视化技术,分析数据特征变得容易。我们将使用pyplot绘制图表以可视化数据。Pyplot 是 matplotlib 绘图库的状态机接口。图形和坐标轴会自动创建,以生成所需的图表。以下链接是pyplot的一个很好的参考:

matplotlib.org/users/pyplot_tutorial.html

本例中,我们将使用国情咨文中总统向国会提出的请求数量。以下链接包含数据:

www.presidency.ucsb.edu/data/sourequests.php

以下是数据的示例:

1946, 41
1947, 23
1948, 16
1949, 28
1950, 20
1951, 11
1952, 19
1953, 14
1954, 39
1955, 32
1956, 
1957, 14
1958, 
1959, 16
1960, 6

我们将直观地查看这些数据,并识别数据中的任何异常值。我们将采用递归方法处理异常值。一旦识别出异常值,我们将从数据集中移除它们,并重新绘制剩余数据,以便寻找新的异常值。

注意

在每次迭代中移除识别出的异常值后,递归检查数据是检测异常值的常见方法。

如何操作…

我们将使用 NumPy 的数据加载工具加载数据。然后,我们将解决数据质量问题;在此案例中,我们将解决如何处理空值。正如你所看到的数据,1956 年和 1958 年存在空值。让我们使用 lambda 函数将空值替换为0

接下来,让我们绘制数据,寻找任何趋势:

# Load libraries
import numpy as np
from matplotlib.pylab import frange
import matplotlib.pyplot as plt

fill_data = lambda x : int(x.strip() or 0)
data = np.genfromtxt('president.txt',dtype=(int,int),converters={1:fill_data},\
            delimiter=",")
x = data[:,0]
y = data[:,1]

# 2.Plot the data to look for any trends or values
plt.close('all')
plt.figure(1)
plt.title("All data")
plt.plot(x,y,'ro')
plt.xlabel('year')plt.ylabel('No Presedential Request')

让我们计算百分位数值,并将其作为参考线绘制到已经生成的图中:

#3.Calculate percentile values (25th, 50th,75th) for the data to understand data distribution
perc_25 = np.percentile(y,25)
perc_50 = np.percentile(y,50)
perc_75 = np.percentile(y,75)
print
print "25th Percentile    = %0.2f"%(perc_25)
print "50th Percentile    = %0.2f"%(perc_50)
print "75th Percentile    = %0.2f"%(perc_75)
print
#4.Plot these percentile values as reference in the plot we generated in the previous step.
# Draw horizontal lines at 25,50 and 75th percentile
plt.axhline(perc_25,label='25th perc',c='r')
plt.axhline(perc_50,label='50th perc',c='g')
plt.axhline(perc_75,label='75th perc',c='m')plt.legend(loc='best')

最后,我们通过可视化检查数据中的异常值,然后使用掩码函数将其移除。接下来,我们再次绘制去除异常值后的数据:

#5.Look for outliers if any in the data by visual inspection.
# Remove outliers using mask function 
# Remove outliers 0 and 54
y_masked = np.ma.masked_where(y==0,y)
#  Remove point 54
y_masked = np.ma.masked_where(y_masked==54,y_masked)

#6 Plot the data again.
plt.figure(2)
plt.title("Masked data")
plt.plot(x,y_masked,'ro')
plt.xlabel('year')
plt.ylabel('No Presedential Request')
plt.ylim(0,60)

# Draw horizontal lines at 25,50 and 75th percentile
plt.axhline(perc_25,label='25th perc',c='r')
plt.axhline(perc_50,label='50th perc',c='g')
plt.axhline(perc_75,label='75th perc',c='m')
plt.legend(loc='best')plt.show()

它是如何工作的…

在第一步中,我们将应用在前一章中学到的一些数据加载技巧。你会注意到年份19561958被留空。我们将使用匿名函数将它们替换为0

fill_data = lambda x : int(x.strip() or 0)

fill_data匿名函数将替换数据集中的任何空值;在这种情况下,第 11 行和第 13 行将替换为 0:

data = np.genfromtxt('president.txt',dtype=(int,int),converters={1:fill_data},delimiter=",")

我们将fill_data传递给genfromtxt函数的converters参数。请注意,converters接受一个字典作为输入。字典中的键指定我们的函数应该应用于哪一列。值表示函数。在此案例中,我们指定fill_data作为函数,并将键设置为 1,表示fill_data函数必须应用于第 1 列。现在让我们在控制台中查看数据:

>>> data[7:15]
array([[1953,   14],
       [1954,   39],
       [1955,   32],
       [1956,    0],
       [1957,   14],
       [1958,    0],
       [1959,   16],
       [1960,    6]])
>>>

如我们所见,年份19561958被添加了一个0值。为了便于绘图,我们将把年份数据加载到 x 轴,国情咨文中总统向国会提出的请求数量加载到 y 轴:

x = data[:,0]
y = data[:,1]

如你所见,在第一列,年份被加载到x中,下一列加载到y中。

在第 2 步中,我们将绘制数据,x 轴为年份,y 轴表示值:

plt.close('all')

我们将首先关闭之前程序中打开的任何图形:

plt.figure(1)

我们将为我们的图形指定一个编号。当程序中有很多图形时,这非常有用:

plt.title("All data")

我们将为我们的图形指定一个标题:

plt.plot(x,y,'ro')

最后,我们将绘制 x 和 y。'ro'参数告诉 pyplot 将 x 和 y 绘制为红色(r)的小圆点(0):

plt.xlabel('year')
plt.ylabel('No Presedential Request')

最后,提供了xy轴的标签。

输出如下所示:

它是如何工作的…

随便看一下这张图,你会发现数据分布得很广泛,第一眼看不出任何趋势或模式。然而,仔细观察,你会注意到三个点:一个位于右上方,另两个则位于1960年附近的x轴上。它们与样本中的所有其他点截然不同,因此,它们是异常值。

注意

异常值是指位于分布整体模式之外的观测值(Moore 和 McCabe 1999)。

为了进一步理解这些点,我们将借助百分位数来分析。

注意

如果我们有一个长度为 N 的向量 V,V 的第 q 个百分位数是 V 的排序副本中的第 q 个排名值。如果归一化排名与 q 不完全匹配,则两个最邻近点的值和距离,以及插值参数将决定百分位数。此函数与中位数相同,如果q=50,与最小值相同,如果q=0,与最大值相同,如果q=100

请参考docs.scipy.org/doc/numpy-dev/reference/generated/numpy.percentile.html了解更多信息。

为什么不使用平均值?我们将在总结统计部分探讨平均值;然而,查看百分位数有其独特的优势。平均值通常会被异常值所扭曲;例如,右上方的异常值会将平均值拉高,而接近 1960 的异常值则可能相反。百分位数能更清晰地反映数据集中值的范围。我们可以使用 NumPy 来计算百分位数。

在第 3 步中,我们将计算百分位数并打印出来。

计算并打印出的百分位数值如下:

它是如何工作的…

注意

百分位数的解读:

数据集中有 25%的点小于 13.00(25 百分位数值)。

数据集中有 50%的点小于 18.50(50 百分位数值)。

数据集中有 75%的点小于 25.25(75 百分位数值)。

需要注意的一点是,50 百分位数是中位数。百分位数给我们提供了数据值范围的良好视角。

在第 4 步中,我们将在图表中以水平线的形式绘制这些百分位数值,以增强我们的可视化效果:

# Draw horizontal lines at 25,50 and 75th percentile
plt.axhline(perc_25,label='25th perc',c='r')
plt.axhline(perc_50,label='50th perc',c='g')
plt.axhline(perc_75,label='75th perc',c='m')
plt.legend(loc='best')

我们使用了plt.axhline()函数来绘制这些水平线。这个函数会在给定的 y 值处,从 x 的最小值绘制到 x 的最大值。通过 label 参数,我们给它命名,并通过 c 参数设置线条的颜色。

提示

理解任何函数的好方法是将函数名传递给 Python 控制台中的help()。在这种情况下,在 Python 控制台中输入help(plt.axhline)将提供相关详情。

最后,我们将使用plt.legend()添加图例,并通过loc参数,要求 pyplot 自动选择最佳位置放置图例,以免影响图表的可读性。

我们的图表现在如下所示:

它是如何工作的…

在第 5 步中,我们将使用 NumPy 中的掩码函数来移除异常值:

# Remove zero values
y_masked = np.ma.masked_where(y==0,y)
#  Remove 54
y_masked = np.ma.masked_where(y_masked==54,y_masked)

掩码是隐藏数组中某些值的便捷方法,而不需要将其从数组中删除。我们使用了ma.masked_where函数,其中传入了一个条件和一个数组。该函数会根据条件掩盖数组中符合条件的值。我们的第一个条件是掩盖y数组中所有值为0的点。我们将新的掩盖后的数组存储为y_masked。然后,我们对y_masked应用另一个条件,移除第 54 个点。

最后,在第 6 步中,我们将重复绘图步骤。我们的最终图表如下所示:

它是如何工作的…

另见

  • 创建匿名函数 例子见第一章, Python 数据科学应用

  • 预处理列 例子见第一章, Python 数据科学应用

  • 使用 Python 获取数据 例子见第一章, Python 数据科学应用

  • 离群值 相关内容请参考 第四章,数据分析 - 深度剖析

对数据进行分组并使用点图

EDA 是通过从多个角度放大和缩小数据,以更好地理解数据。现在让我们用点图从不同的角度来看数据。点图是一种简单的图表,其中数据被分组并绘制在简单的尺度上。我们可以决定如何分组数据。

注意

点图最好用于小到中等大小的数据集。对于大型数据,通常使用直方图。

准备就绪

对于本练习,我们将使用与上一节相同的数据。

如何操作……

让我们加载必要的库。接着加载我们的数据,并在此过程中处理缺失值。最后,我们将使用频率计数器对数据进行分组:

# Load libraries
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
from collections import OrderedDict
from matplotlib.pylab import frange

# 1.Load the data and handle missing values.
fill_data = lambda x : int(x.strip() or 0)
data = np.genfromtxt('president.txt',dtype=(int,int),converters={1:fill_data},delimiter=",")
x = data[:,0]
y = data[:,1]

# 2.Group data using frequency (count of individual data points).
# Given a set of points, Counter() returns a dictionary, where key is a data point,
# and value is the frequency of data point in the dataset.
x_freq = Counter(y)
x_ = np.array(x_freq.keys())y_ = np.array(x_freq.values())

我们将继续按年份范围对数据进行分组并绘制:

# 3.Group data by range of years
x_group = OrderedDict()
group= 5
group_count=1
keys = []
values = []
for i,xx in enumerate(x):
    # Individual data point is appended to list keys
    keys.append(xx)
    values.append(y[i])
    # If we have processed five data points (i.e. five years)
    if group_count == group:
        # Convert the list of keys to a tuple
        # use the new tuple as the ke to x_group dictionary
        x_group[tuple(keys)] = values
        keys= []
        values =[]
        group_count = 1

    group_count+=1
# Accomodate the last batch of keys and values
x_group[tuple(keys)] = values 

print x_group
# 4.Plot the grouped data as dot plot.
plt.subplot(311)
plt.title("Dot Plot by Frequency")
# Plot the frequency
plt.plot(y_,x_,'ro')
plt.xlabel('Count')
plt.ylabel('# Presedential Request')
# Set the min and max limits for x axis
plt.xlim(min(y_)-1,max(y_)+1)

plt.subplot(312)
plt.title("Simple dot plot")
plt.xlabel('# Presendtial Request')plt.ylabel('Frequency')

最后,我们将准备数据用于简单的点图,并继续绘制:

# Prepare the data for simple dot plot
# For every (item, frequency) pair create a 
# new x and y
# where x is a list, created using using np.repeat
# function, where the item is repeated frequency times.
# y is a list between 0.1 and frequency/10, incremented
# by 0.1
for key,value in x_freq.items():
    x__ = np.repeat(key,value)
    y__ = frange(0.1,(value/10.0),0.1)
    try:
        plt.plot(x__,y__,'go')
    except ValueError:
        print x__.shape, y__.shape
    # Set the min and max limits of x and y axis
    plt.ylim(0.0,0.4)
    plt.xlim(xmin=-1) 

plt.xticks(x_freq.keys())

plt.subplot(313)
x_vals =[]
x_labels =[]
y_vals =[]
x_tick = 1
for k,v in x_group.items():
    for i in range(len(k)):
        x_vals.append(x_tick)
        x_label = '-'.join([str(kk) if not i else str(kk)[-2:] for i,kk in enumerate(k)])
        x_labels.append(x_label)
    y_vals.extend(list(v))
    x_tick+=1

plt.title("Dot Plot by Year Grouping")
plt.xlabel('Year Group')
plt.ylabel('No Presedential Request')
try:
    plt.plot(x_vals,y_vals,'ro')
except ValueError:
    print len(x_vals),len(y_vals)

plt.xticks(x_vals,x_labels,rotation=-35)plt.show()

它是如何工作的……

在第 1 步中,我们将加载数据。这与前面的食谱中讨论的数据加载是一样的。在开始绘制数据之前,我们想对它们进行分组,以便查看整体数据特征。

在第 2 步和第 3 步中,我们将使用不同的标准对数据进行分组。

让我们来看第 2 步。

在这里,我们将使用来自 collections 包的 Counter() 函数。

注意

给定一组点,Counter() 返回一个字典,其中键是数据点,值是数据点在数据集中的频率。

我们将把数据集传递给 Counter(),从实际数据点中提取键,并将值(即频率)提取到 numpy 数组 x_y_ 中,以便绘图。因此,我们现在已经通过频率对数据进行了分组。

在我们继续绘制之前,我们将在第 3 步对这些数据进行再次分组。

我们知道 x 轴是年份。我们的数据也按年份升序排列。在这一步中,我们将按年份范围对数据进行分组,这里每组 5 年;也就是说,假设我们将前五年作为一组,第二组是接下来的五年,以此类推:

group= 5
group_count=1
keys = []
values = []

group 变量定义了我们希望在一个组中包含多少年;在这个例子中,我们有 5 个组,keysvalues 是两个空列表。我们将继续从 xy 中填充它们,直到 group_count 达到 group,也就是 5

for i,xx in enumerate(x):
keys.append(xx)
values.append(y[i])
if group_count == group:
x_group[tuple(keys)] = values
keys= []
values =[]
group_count = 0
    group_count+=1
x_group[tuple(keys)] = values 

x_group 是现在存储值组的字典名称。我们需要保留插入记录的顺序,因此在这种情况下,我们将使用 OrderedDict

注意

OrderedDict 会保留插入键的顺序。

现在让我们继续绘制这些值。

我们希望将所有图形绘制在单个窗口中;因此,我们将使用subplot参数到子图中,该参数定义了行数(3,百位数中的数字),列数(1,十位数中的数字),最后是绘图编号(个位数中的 1)。我们的绘图输出如下:

工作原理...

在顶部图表中,数据按频率分组。在这里,我们的 x 轴是计数,y 轴是总统请求的数量。我们可以看到,30 次或更多总统请求仅发生了一次。如前所述,点图在分析不同分组下数据点的范围方面表现良好。

中间图可以看作是一个非常简单的直方图。正如图表标题(plt.title()中)所说,这是点图的最简单形式,其中x轴是实际值,y 轴是数据集中该 x 值出现的次数。在直方图中,必须仔细设置箱体大小;否则,可能会扭曲关于数据的完整图像。然而,在这个简单的点图中可以避免这种情况。

在底部图表中,我们按年份对数据进行了分组。

另请参阅

  • 在第一章中的创建匿名函数章节中,使用 Python 进行数据科学*

  • 在第一章中的数据预处理章节中,使用 Python 进行数据科学*

  • 在第一章中的使用 Python 获取数据章节中,使用 Python 进行数据科学*

  • 在第一章中的使用字典对象章节中,使用 Python 进行数据科学*

使用散点图进行多变量数据分析

从单列开始,我们现在将转向多列。在多变量数据分析中,我们有兴趣看到我们分析的列之间是否有任何关系。在两列/变量情况下,最好的起点是标准散点图。关系可以分为四种类型,如下所示:

  • 无关系

  • 简单

  • 多变量(不简单)关系

准备工作

我们将使用鸢尾花数据集。这是由罗纳德·菲舍尔爵士引入的多变量数据集。有关更多信息,请参阅archive.ics.uci.edu/ml/datasets/Iris

鸢尾花数据集有 150 个实例和四个属性/列。这 150 个实例由鸢尾花的三个物种(山鸢尾、维吉尼亚鸢尾和变色鸢尾)的每种 50 条记录组成。四个属性分别是以厘米为单位的萼片长度、萼片宽度、花瓣长度和花瓣宽度。因此,鸢尾花数据集还是一个很好的分类数据集。可以编写分类方法,通过适当的训练,给定一条记录,我们可以分类出该记录属于哪个物种。

如何做...

让我们加载必要的库并提取鸢尾花数据:

# Load Librarires
from sklearn.datasets import load_iris
import numpy as np
import matplotlib.pyplot as plt
import itertools

# 1\. Load Iris dataset
data = load_iris()
x = data['data']
y = data['target']col_names = data['feature_names']

我们将继续演示如何使用散点图:

# 2.Perform a simple scatter plot. 
# Plot 6 graphs, combinations of our columns, sepal length, sepal width,
# petal length and petal width.
plt.close('all')
plt.figure(1)
# We want a plot with
# 3 rows and 2 columns, 3 and 2 in
# below variable signifies that.
subplot_start = 321
col_numbers = range(0,4)
# Need it for labeling the graph
col_pairs = itertools.combinations(col_numbers,2)
plt.subplots_adjust(wspace = 0.5)

for col_pair in col_pairs:
    plt.subplot(subplot_start)
    plt.scatter(x[:,col_pair[0]],x[:,col_pair[1]],c=y)
    plt.xlabel(col_names[col_pair[0]])
    plt.ylabel(col_names[col_pair[1]])
    subplot_start+=1plt.show()

它是如何工作的……

scikit 库提供了一个方便的函数 load_iris() 来加载鸢尾花数据集。我们将在步骤 1 中使用此函数将鸢尾花数据加载到变量 data 中。data 是一个字典对象。通过 datatarget 键,我们可以检索记录和类标签。我们将查看 xy 值:

>>> x.shape
(150, 4)
>>> y.shape
(150,)
>>>

如你所见,x 是一个包含 150 行和四列的矩阵;y 是一个长度为 150 的向量。还可以使用 feature_names 关键字查询 data 字典以查看列名,如下所示:

>>> data['feature_names']

['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
>>>

然后我们将在步骤 2 中创建一个散点图,展示鸢尾花数据的各个变量。像之前一样,我们将使用 subplot 来将所有图形容纳在一个单一的图形中。我们将使用 itertools.Combination 来获取我们列的两个组合:

col_pairs = itertools.combinations(col_numbers,2)

我们可以迭代 col_pairs 来获取两个列的组合,并为每个组合绘制一个散点图,如下所示:

plt.scatter(x[:,col_pair[0]],x[:,col_pair[1]],c=y)

我们将传递一个 c 参数来指定点的颜色。在这种情况下,我们将传递我们的 y 变量(类标签),这样鸢尾花的不同物种将在散点图中以不同的颜色绘制。

生成的图形如下:

它是如何工作的…

如你所见,我们已经绘制了两个列的组合。我们还使用三种不同的颜色来表示类标签。让我们看一下左下角的图形,花瓣长度与花瓣宽度的关系。我们看到,不同的值范围属于不同的类标签。现在,这为分类问题提供了很好的线索;如果问题是分类,那么花瓣宽度和花瓣长度是很好的候选变量。

注意

对于鸢尾花数据集,花瓣宽度和花瓣长度就可以将记录按其所属的花卉种类分类。

在特征选择过程中,可以通过双变量散点图快速做出此类观察。

另见

  • 在第一章的 使用迭代器 这一节中,使用 Python 数据科学

  • 在第一章的 与 itertools 一起工作 这一节中,使用 Python 数据科学

使用热力图

热力图是另一种有趣的可视化技术。在热力图中,数据以矩阵的形式表示,其中属性的取值范围以颜色渐变的形式表示。查看以下维基百科参考资料,了解热力图的一般介绍:

en.wikipedia.org/wiki/Heat_map

准备工作

我们将再次使用鸢尾花数据集来演示如何构建热力图。我们还将看到热力图在这组数据上可以如何使用的不同方式。

在这个示例中,我们将看到如何将整个数据表示为热力图,并如何从热力图中对数据进行各种解读。让我们继续构建鸢尾花数据集的热力图。

如何操作……

让我们加载必要的库并导入 Iris 数据集。我们将通过数据的均值来缩放变量:

# Load libraries
from sklearn.datasets import load_iris
from sklearn.preprocessing import scale
import numpy as np
import matplotlib.pyplot as plt

# 1\. Load iris dataset
data = load_iris()
x = data['data']
y = data['target']
col_names = data['feature_names']

# 2\. Scale the variables, with mean value
x = scale(x,with_std=False)
x_ = x[1:26,]y_labels = range(1,26)

让我们绘制热图:

# 3\. Plot the Heat map
plt.close('all')

plt.figure(1)
fig,ax = plt.subplots()
ax.pcolor(x_,cmap=plt.cm.Greens,edgecolors='k')
ax.set_xticks(np.arange(0,x_.shape[1])+0.5)
ax.set_yticks(np.arange(0,x_.shape[0])+0.5)
ax.xaxis.tick_top()
ax.yaxis.tick_left()
ax.set_xticklabels(col_names,minor=False,fontsize=10)
ax.set_yticklabels(y_labels,minor=False,fontsize=10)plt.show()

它是如何工作的…

在第 1 步中,我们将加载 Iris 数据集。与其他方法类似,我们将数据字典对象存储为 x 和 y,以确保清晰。在第 2 步中,我们将通过均值缩放变量:

x = scale(x,with_std=False)

在参数标准设置为 false 的情况下,scale 函数将只使用列的均值来规范化数据。

缩放的原因是将每一列的值的范围调整到一个共同的尺度,通常是在 0 到 1 之间。将它们缩放到相同的尺度对于热图可视化非常重要,因为值决定了颜色梯度。

提示

别忘了缩放变量,使它们处于相同的范围内。如果没有适当的缩放,可能会导致某些变量的范围和尺度较大,从而主导其他变量。

在第 3 步中,我们将执行实际的绘图操作。在绘图之前,我们将对数据进行子集选择:

x = x[1:26,]
col_names = data['feature_names']
y_labels = range(1,26)

如你所见,我们只选择了数据集中的前 25 条记录。我们这么做是为了使 y 轴上的标签可读。我们将把 x 轴和 y 轴的标签分别存储在col_namesy_labels中。最后,我们将使用 pyplot 中的pcolor函数绘制 Iris 数据的热图,并稍微调整一下 pcolor,使其看起来更加美观:

ax.set_xticks(np.arange(0,x.shape[1])+0.5)
ax.set_yticks(np.arange(0,x.shape[0])+0.5)

x轴和y轴的刻度被均匀设置:

ax.xaxis.tick_top()

x 轴刻度显示在图表的顶部:

ax.yaxis.tick_left()

y 轴刻度显示在图表的左侧:

ax.set_xticklabels(col_names,minor=False,fontsize=10)
ax.set_yticklabels(y_labels,minor=False,fontsize=10)

最后,我们将传递标签值。

输出图如下所示:

它是如何工作的…

还有更多...

另一种有趣的使用热图的方法是查看按其各自类别分离的变量;例如,在 Iris 数据集中,我们将为三个类别绘制三个不同的热图。代码如下:

x1 = x[0:50]
x2 = x[50:99]
x3 = x[100:149]

x1 = scale(x1,with_std=False)
x2 = scale(x2,with_std=False)
x3 = scale(x3,with_std=False)

plt.close('all')
plt.figure(2)
fig,(ax1, ax2, ax3) = plt.subplots(3, sharex=True, sharey=True)
y_labels = range(1,51)

ax1.set_xticks(np.arange(0,x.shape[1])+0.5)
ax1.set_yticks(np.arange(0,50,10))

ax1.xaxis.tick_bottom()
ax1.set_xticklabels(col_names,minor=False,fontsize=2)

ax1.pcolor(x1,cmap=plt.cm.Greens,edgecolors='k')
ax1.set_title(data['target_names'][0])

ax2.pcolor(x2,cmap=plt.cm.Greens,edgecolors='k')
ax2.set_title(data['target_names'][1])

ax3.pcolor(x3,cmap=plt.cm.Greens,edgecolors='k')
ax3.set_title(data['target_names'][2])plt.show()   

让我们看看这个图:

还有更多...

前 50 条记录属于setosa类,接下来的 50 条属于versicolor类,最后 50 条属于virginica类。我们将为每个类绘制三个热图。

单元格中填充的是记录的实际值。你可以注意到,对于setosa,萼片宽度变化较大,但对于versicolorvirginica来说则没有表现出明显的差异。

另见

  • 数据缩放方法见第三章,数据分析 - 探索与整理

执行总结统计和绘图

使用总结统计的主要目的是很好地了解数据的位置和离散程度。总结统计包括均值、中位数和标准差,这些量是比较容易计算的。然而,在使用这些统计量时需要小心。如果底层数据不是单峰的,而是具有多个峰值,那么这些统计量可能并不十分有用。

注意

如果给定的数据是单峰的,即只有一个峰值,那么均值(给出位置)和标准差(给出方差)是非常有价值的指标。

准备就绪

让我们使用鸢尾花数据集来探索这些汇总统计量。在这一部分,我们没有一个完整的程序输出单一结果;然而,我们将通过不同的步骤展示不同的汇总度量。

如何操作……

让我们首先导入必要的库。接下来,我们将加载鸢尾花数据集:

# Load Libraries
from sklearn.datasets import load_iris
import numpy as np
from scipy.stats import trim_mean

# Load iris data
data = load_iris()
x = data['data']
y = data['target']col_names = data['feature_names']

现在我们演示如何计算均值、修剪均值和范围值:

# 1.	Calculate and print the mean value of each column in the Iris dataset
print "col name,mean value"
for i,col_name in enumerate(col_names):
    print "%s,%0.2f"%(col_name,np.mean(x[:,i]))
print    

# 2.	Trimmed mean calculation.
p = 0.1 # 10% trimmed mean
print
print "col name,trimmed mean value"
for i,col_name in enumerate(col_names):
    print "%s,%0.2f"%(col_name,trim_mean(x[:,i],p))
print

# 3.	Data dispersion, calculating and display the range values.
print "col_names,max,min,range"
for i,col_name in enumerate(col_names):
    print "%s,%0.2f,%0.2f,%0.2f"%(col_name,max(x[:,i]),min(x[:,i]),max(x[:,i])-min(x[:,i]))
print

最后,我们将展示方差、标准差、均值绝对偏差和中位数绝对偏差的计算:

# 4.	Data dispersion, variance and standard deviation
print "col_names,variance,std-dev"
for i,col_name in enumerate(col_names):
    print "%s,%0.2f,%0.2f"%(col_name,np.var(x[:,i]),np.std(x[:,i]))
print

# 5.	Mean absolute deviation calculation  
def mad(x,axis=None):
    mean = np.mean(x,axis=axis)
    return np.sum(np.abs(x-mean))/(1.0 * len(x))

print "col_names,mad"
for i,col_name in enumerate(col_names):
    print "%s,%0.2f"%(col_name,mad(x[:,i]))
print

# 6.	Median absolute deviation calculation
def mdad(x,axis=None):
    median = np.median(x,axis=axis)
    return np.median(np.abs(x-median))

print "col_names,median,median abs dev,inter quartile range"
for i,col_name in enumerate(col_names):
    iqr = np.percentile(x[:,i],75) - np.percentile(x[i,:],25)
    print "%s,%0.2f,%0.2f,%0.2f"%(col_name,np.median(x[:,i]), mdad(x[:,i]),iqr)
print

它是如何工作的……

鸢尾花数据集的加载在此配方中没有重复。假设读者可以参考前面的配方来完成相同的操作。此外,我们假设 x 变量已经加载了所有鸢尾花记录的实例,每条记录有四列。

步骤 1 打印了鸢尾花数据集中每一列的均值。我们使用了 NumPy 的 mean 函数来完成此操作。打印语句的输出如下:

它是如何工作的……

如您所见,我们得到了每列的均值。计算均值的代码如下:

np.mean(x[:,i])

我们在循环中遍历了所有的行和列。因此,我们通过列计算均值。

另一个有趣的度量是所谓的修剪均值。它有其自身的优点。给定样本的 10% 修剪均值是通过排除样本中最大和最小的 10% 值,计算剩余 80% 样本的算术均值来得出的。

注意

与常规均值相比,修剪均值对异常值的敏感度较低。

SciPy 提供了一个修剪均值函数。我们将在步骤 2 中演示修剪均值的计算。输出结果如下:

它是如何工作的……

使用鸢尾花数据集时,我们没有看到太大的差异,但在实际数据集中,修剪均值非常有用,因为它能更好地反映数据的位置。

到目前为止,我们所看到的是数据的位置,均值和修剪均值能很好地推断出数据的位置。另一个重要的方面是数据的离散程度。查看数据离散度的最简单方法是范围,它的定义如下:给定一组值 x,范围是 x 的最大值减去 x 的最小值。在步骤 3 中,我们将计算并打印相同的结果:

它是如何工作的……

注意

如果数据的范围非常窄,例如,大多数值集中在单一值附近,且我们有一些极端值,那么范围可能会产生误导。

当数据落在一个非常狭窄的范围内,并且围绕单个值聚集时,方差被用作数据分布/扩展的典型度量。方差是单个值与均值之间差的平方的和,然后除以实例数。在第 4 步中,我们将看到方差计算。

在前面的代码中,除了方差,我们还可以看到标准差,即标准差。由于方差是差的平方,因此它与原始数据的度量尺度不同。我们将使用标准差,它是方差的平方根,以便将数据恢复到原始尺度。让我们来看一下打印语句的输出,其中列出了方差和标准差:

它是如何工作的…

正如我们之前提到的,均值对异常值非常敏感;方差也使用均值,因此也容易受到与均值相同的问题影响。我们可以使用其他方差度量来避免这一陷阱。一个这样的度量是绝对平均偏差;它不是取单个值与均值之间差的平方并将其除以实例数,而是取均值与单个值之间差的绝对值,并将其除以实例数。在第 5 步中,我们将为此定义一个函数:

def mad(x,axis=None):
mean = np.mean(x,axis=axis)
return np.sum(np.abs(x-mean))/(1.0 * len(x))

如您所见,函数返回均值与单个值之间的绝对差异。本步骤的输出如下:

它是如何工作的…

当数据包含许多异常值时,另有一组度量非常有用。它们是中位数和百分位数。我们在前一部分绘制单变量数据时已经看到过百分位数。传统上,中位数定义为数据集中一个值,使得数据集中一半的点小于该值,另一半大于该值。

注意

百分位数是中位数概念的推广。第 50 百分位数就是传统的中位数值。

我们在前一部分看到了第 25 百分位数和第 75 百分位数。第 25 百分位数是这样一个值,数据集中有 25%的点小于这个值:

>>> 
>>> a = [8,9,10,11]
>>> np.median(a)
9.5
>>> np.percentile(a,50)
9.5

中位数是数据分布位置的度量。通过使用百分位数,我们可以得到数据分布的一个度量,即四分位差。四分位差是第 75 百分位数与第 25 百分位数之间的距离。类似于之前解释的均值绝对偏差,我们也有中位数绝对偏差。

在第 6 步中,我们将计算并显示四分位差和中位数绝对偏差。我们将定义以下函数以计算中位数绝对偏差:

def mdad(x,axis=None):
median = np.median(x,axis=axis)
return np.median(np.abs(x-median))

输出结果如下:

它是如何工作的…

另见

  • 第三章中的 分组数据与使用图表 章节,数据分析 - 探索与整理

使用箱形图和须状图

箱形图和须状图是与汇总统计信息非常配合的工具,可以查看数据的统计摘要。箱形图可以有效地表示数据中的四分位数,也可以显示离群值(如果有的话),突出显示数据的整体结构。箱形图由以下几个特征组成:

  • 一条水平线表示中位数,标示数据的位置

  • 跨越四分位距的箱体,表示测量分散度

  • 一组从中央箱体水平和垂直延伸的须状线,它们表示数据分布的尾部。

准备开始

让我们使用箱形图来观察 Iris 数据集。

如何做…

让我们首先加载必要的库,然后加载 Iris 数据集:

# Load Libraries
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

# Load Iris dataset
data = load_iris()
x = data['data']
plt.close('all')

让我们演示如何创建一个箱形图:

# Plot the box and whisker
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.boxplot(x)
ax.set_xticklabels(data['feature_names'])
plt.show()    

工作原理…

这段代码非常简单。我们将加载 Iris 数据集到 x,并将 x 的值传递给 pyplot 中的箱形图函数。如你所知,我们的 x 有四列。箱形图如下所示:

工作原理…

箱形图在一个图中捕捉了四个列的位置和变化。

水平的红线表示中位数,它标示了数据的位置。你可以看到,花萼长度的中位数高于其余列。

可以看到跨越四个变量的箱体,表示测量分散度的四分位距。

你可以看到一组从中央箱体水平和垂直延伸的须状线,它们表示数据分布的尾部。须状线帮助你观察数据集中的极端值。

还有更多内容…

看到数据如何在不同类标签中分布也是很有趣的。类似于我们在散点图中的做法,接下来我们将通过箱形图实现相同的操作。以下代码和图表展示了如何在不同类标签之间绘制箱形图:

y=data['target']
class_labels = data['target_names']

fig = plt.figure(2,figsize=(18,10))
sub_plt_count = 321
for t in range(0,3):
    ax = fig.add_subplot(sub_plt_count)
    y_index = np.where(y==t)[0]
    x_ = x[y_index,:]
    ax.boxplot(x_)
    ax.set_title(class_labels[t])   
    ax.set_xticklabels(data['feature_names'])
    sub_plt_count+=1
plt.show()

如下图所示,我们现在为每个类标签绘制了一个箱形图:

更多内容…

数据填充

在许多现实世界的场景中,我们会遇到不完整或缺失数据的问题。我们需要一个策略来处理这些不完整的数据。这个策略可以单独使用数据,或者在有类标签的情况下结合类标签来制定。

准备开始

让我们首先看看不使用类标签进行数据填充的方法。

一个简单的技术是忽略缺失值,因此避免数据填充的开销。然而,这只适用于数据充足的情况,而这并非总是如此。如果数据集中有非常少的缺失值,并且缺失值的百分比很小,我们可以忽略它们。通常情况下,问题不在于忽略单个变量的单个值,而在于忽略包含此变量的元组。在忽略整个元组时,我们必须更加小心,因为该元组中的其他属性可能对我们的任务非常关键。

处理缺失数据的更好方法是进行估算。现在,估算过程可以仅考虑数据或与类标签一起进行。对于连续变量,可以使用均值、中位数或最频繁的值来替换缺失值。Scikit-learn 在 preprocessing 模块中提供了一个Imputer()函数来处理缺失数据。让我们看一个例子,我们将在鸢尾花数据集中执行数据填充以更好地理解填充技术。

如何操作…

让我们首先加载必要的库来开始。我们将像往常一样加载鸢尾花数据集,并引入一些任意的缺失值:

# Load Libraries
from sklearn.datasets import load_iris
from sklearn.preprocessing import Imputer
import numpy as np
import numpy.ma as ma

# 1\. Load Iris Data Set
data = load_iris()
x = data['data']
y = data['target']

# Make a copy of hte original x value
x_t = x.copy()

# 2.	Introduce missing values into second row
x_t[2,:] = np.repeat(0,x.shape[1])

让我们看看数据填充的实际效果:

# 3.	Now create an imputer object with strategy as mean, 
# i.e. fill the missing values with the mean value of the missing column.
imputer = Imputer(missing_values=0,strategy="mean")
x_imputed = imputer.fit_transform(x_t)

mask = np.zeros_like(x_t)
mask[2,:] = 1
x_t_m = ma.masked_array(x_t,mask)

print np.mean(x_t_m,axis=0)print x_imputed[2,:]

工作原理…

第一步是将鸢尾花数据加载到内存中。在第二步中,我们将引入一些缺失值;在本例中,我们将所有第三行的列都设置为0

在第 3 步中,我们将使用 Imputer 对象处理缺失数据:

imputer = Imputer(missing_values=0,strategy="mean")

如您所见,我们需要两个参数,missing_values来指定缺失值,以及策略,这是一种处理这些缺失值的方法。Imputer 对象提供以下三种策略:

  • 均值

  • 中位数

  • most_frequent

使用均值,任何值为0的单元格都将被其所属列的均值替换。对于中位数,将使用中位数值来替换0,而对于most_frequent,正如其名称所示,将使用最频繁出现的值来替换0。基于我们应用程序的上下文,可以应用其中一种策略。

x[2,:]的初始值如下:

>>> x[2,:]
array([ 4.7,  3.2,  1.3,  0.2])

我们将所有列中的值设置为0,并使用均值策略的填充器。

在查看填充器输出之前,让我们计算所有列的均值:

import numpy.ma as ma
mask = np.zeros_like(x_t)
mask[2,:] = 1
x_t_m = ma.masked_array(x_t,mask)

print np.mean(x_t_m,axis=0)

输出如下:

[5.851006711409397 3.053020134228189 3.7751677852349017 1.2053691275167793]

现在,让我们查看第 2 行的填充输出:

print x_imputed[2,:]

以下是输出:

[ 5.85100671  3.05302013  3.77516779  1.20536913]

正如您所见,填充器已经用各自列的均值填充了缺失的值。

还有更多内容…

正如我们讨论的,我们还可以利用类标签并使用均值或中位数来填补缺失值:

# Impute based on class label
missing_y = y[2]
x_missing = np.where(y==missing_y)[0]
y = data['target']
# Mean stragegy 
print np.mean(x[x_missing,:],axis=0)
# Median stragegy
print np.median(x[x_missing,:],axis=0)

与其使用整个数据集的均值或中位数,我们将数据子集化到缺失元组的类变量中:

missing_y = y[2]

我们在第三条记录中引入了缺失值。我们将把与此记录相关的类别标签赋值给 missing_y 变量:

x_missing = np.where(y==missing_y)[0]

现在,我们将提取所有具有相同类别标签的元组:

# Mean stragegy 
print np.mean(x[x_missing,:],axis=0)
# Median stragegy
print np.median(x[x_missing,:],axis=0)

我们现在可以通过用所有属于该类别标签的元组的均值或中位数来替换缺失的元组,应用均值或中位数策略。

我们取了该子集的均值/中位数作为数据插补过程。

参见

  • 在第三章中的执行汇总统计食谱,分析数据 - 探索与整理

执行随机采样

在这里,我们将学习如何执行数据的随机采样。

准备工作

通常,在访问整个数据集非常昂贵的情况下,采样可以有效地用于提取数据集的一部分进行分析。采样在探索性数据分析(EDA)中也可以有效使用。样本应该是底层数据集的良好代表。它应具有与底层数据集相同的特征。例如,就均值而言,样本均值应尽可能接近原始数据的均值。

在简单随机采样中,每个元组被选中的机会是均等的。对于我们的示例,我们想从鸢尾花数据集中随机选择十条记录。

如何执行...

我们将从加载必要的库并导入鸢尾花数据集开始:

# Load libraries
from sklearn.datasets import load_iris
import numpy as np

# 1.	Load the Iris data set
data = load_iris()
x = data['data']

让我们演示如何执行采样:

# 2.	Randomly sample 10 records from the loaded dataset
no_records = 10
x_sample_indx = np.random.choice(range(x.shape[0]),no_records)
print x[x_sample_indx,:]

它是如何工作的…

在步骤 1 中,我们将加载鸢尾花数据集。在步骤 2 中,我们将使用 numpy.random 中的 choice 函数进行随机选择。

我们将传递给 choice 函数的两个参数是原始数据集中所有行的范围变量以及我们需要的样本大小。从零到原始数据集中的总行数,choice 函数会随机选择 n 个整数,其中 n 是样本的大小,由我们的 no_records 决定。

另一个重要方面是 choice 函数的一个参数是 replace,默认设置为 True;它指定是否需要进行有放回或无放回采样。无放回采样会从原始列表中移除已采样的项,因此它不会成为未来采样的候选项。有放回采样则相反;每个元素在未来的采样中都有相等的机会被再次采样,即使它之前已经被采样过。

还有更多…

分层采样

如果底层数据集由不同的组组成,简单的随机抽样可能无法捕获足够的样本以代表数据。例如,在一个二分类问题中,10%的数据属于正类,90%属于负类。这种问题在机器学习中被称为类别不平衡问题。当我们在这种不平衡数据集上进行抽样时,样本也应该反映前述的百分比。这种抽样方法叫做分层抽样。我们将在未来的机器学习章节中详细讨论分层抽样。

渐进式抽样

我们如何确定给定问题所需的正确样本大小?我们之前讨论了几种抽样技术,但我们没有选择正确样本大小的策略。这个问题没有简单的答案。一种方法是使用渐进式抽样。选择一个样本大小,通过任何抽样技术获取样本,对数据执行所需的操作,并记录结果。然后,增加样本大小并重复这些步骤。这种迭代过程叫做渐进式抽样。

数据缩放

在这一部分,我们将学习如何进行数据缩放。

准备就绪

缩放是一种重要的数据转换类型。通常,通过对数据集进行缩放,我们可以控制数据类型可以采用的值的范围。在一个包含多列的数据集中,范围和尺度更大的列往往会主导其他列。我们将对数据集进行缩放,以避免这些干扰。

假设我们正在根据功能数量和代码行数来比较两个软件产品。与功能数量的差异相比,代码行数的差异会非常大。在这种情况下,我们的比较将被代码行数所主导。如果我们使用任何相似度度量,结果的相似度或差异将主要受代码行数的影响。为了避免这种情况,我们将采用缩放。最简单的缩放方法是最小-最大缩放。让我们来看看在一个随机生成的数据集上如何进行最小-最大缩放。

如何做……

让我们生成一些随机数据来测试我们的缩放功能:

# Load Libraries
import numpy as np

# 1.	Generate some random data for scaling
np.random.seed(10)
x = [np.random.randint(10,25)*1.0 for i in range(10)]

现在,我们将演示缩放:

# 2.Define a function, which can perform min max scaling given a list of numbers
def min_max(x):
    return [round((xx-min(x))/(1.0*(max(x)-min(x))),2) for xx in x]

# 3.Perform scaling on the given input list.    
print x 
print min_max(x)    

它是如何工作的……

在步骤 1 中,我们将生成一个介于 10 到 25 之间的随机数字列表。在步骤 2 中,我们将定义一个函数来对给定的输入执行最小-最大缩放。最小-最大缩放的定义如下:

x_scaled = x – min(x) / max(x) –min (x)

在步骤 2 中,我们定义一个函数来执行上述任务。

这会将给定值的范围进行转换。转换后,值将落在[0, 1]范围内。

在步骤 3 中,我们将首先打印原始输入列表。输出结果如下:

[19, 23, 14, 10, 11, 21, 22, 19, 23, 10]

我们将把这个列表传递给我们的min_max函数,以获得缩放后的输出,结果如下:

[0.69, 1.0, 0.31, 0.0, 0.08, 0.85, 0.92, 0.69, 1.0, 0.0]

你可以看到缩放的效果;最小的数字 10 被赋值为 0.0,而最大的数字 23 被赋值为 1.0。因此,我们将数据缩放到 [0,1] 范围内。

还有更多…

Scikit-learn 提供了一个 MinMaxScaler 函数来实现这个功能:

from sklearn.preprocessing import MinMaxScaler
import numpy as np

np.random.seed(10)
x = np.matrix([np.random.randint(10,25)*1.0 for i in range(10)])
x = x.T
minmax = MinMaxScaler(feature_range=(0.0,1.0))
print x
x_t = minmax.fit_transform(x)
print x_t

输出如下:

[19.0, 23.0, 14.0, 10.0, 11.0, 21.0, 22.0, 19.0, 23.0, 10.0]
[0.69, 1.0, 0.31, 0.0, 0.08, 0.85, 0.92, 0.69, 1.0, 0.0]

我们看到过将数据缩放到范围(0,1)的例子;这可以扩展到任何范围。假设我们的新范围是 nr_min, nr_max,那么最小-最大公式会修改如下:

x_scaled =  ( x – min(x) / max(x) –min (x) ) * (nr_max- nr_min) + nr_min

以下是 Python 代码:

import numpy as np

np.random.seed(10)
x = [np.random.randint(10,25)*1.0 for i in range(10)]

def min_max_range(x,range_values):
    return [round( ((xx-min(x))/(1.0*(max(x)-min(x))))*(range_values[1]-range_values[0]) \
    + range_values[0],2) for xx in x]

print min_max_range(x,(100,200))

其中,range_values 是一个包含两个元素的元组,0 位置是新范围的下限,1 位置是上限。让我们在输入数据上调用这个函数,看看输出结果如下:

print min_max_range(x,(100,200))

[169.23, 200.0, 130.77, 100.0, 107.69, 184.62, 192.31, 169.23, 200.0, 100.0]

最低值 10 现在被缩放到 100,最高值 23 被缩放到 200

数据标准化

标准化是将输入数据转换为均值为 0,标准差为 1 的过程。

准备就绪

如果你给定一个向量 X,X 的均值为 0 且标准差为 1 可以通过以下公式实现:

注意

标准化 X = x – 均值(值) / 标准差(X)

让我们看看如何在 Python 中实现这个过程。

如何实现…

首先,让我们导入必要的库。接着,我们将生成输入数据:

# Load Libraries
import numpy as np
from sklearn.preprocessing import scale

# Input data generation
np.random.seed(10)
x = [np.random.randint(10,25)*1.0 for i in range(10)]

我们现在准备演示标准化:

x_centered = scale(x,with_mean=True,with_std=False)
x_standard = scale(x,with_mean=True,with_std=True)

print x
print x_centered
print x_standard
print "Orginal x mean = %0.2f, Centered x mean = %0.2f, Std dev of \
        standard x =%0.2f"%(np.mean(x),np.mean(x_centered),np.std(x_standard))

它是如何工作的…

我们将使用 np.random 生成一些随机数据:

x = [np.random.randint(10,25)*1.0 for i in range(10)]

我们将使用 scikit-learn 的 scale 函数进行标准化:

x_centered = scale(x,with_mean=True,with_std=False)
x_standard = scale(x,with_mean=True,with_std=True)

x_centered 只使用均值进行缩放;你可以看到 with_mean 参数设置为 Truewith_std 设置为 False

x_standard 使用均值和标准差进行了标准化。

现在让我们来看一下输出。

原始数据如下:

[19.0, 23.0, 14.0, 10.0, 11.0, 21.0, 22.0, 19.0, 23.0, 10.0]

Next, we will print x_centered, where we centered it with the mean value:

[ 1.8  5.8 -3.2 -7.2 -6.2  3.8  4.8  1.8  5.8 -7.2]

Finally we will print x_standardized, where we used both the mean and standard deviation:

[ 0.35059022  1.12967961 -0.62327151 -1.4023609  -1.20758855  0.74013492
  0.93490726  0.35059022  1.12967961 -1.4023609 ]

Orginal x mean = 17.20, Centered x mean = 0.00, Std dev of standard x =1.00

还有更多…

注意

标准化可以推广到任何级别和范围,具体如下:

标准化值 = 值 – 水平 / 范围

我们将前面的公式分成两部分:仅分子部分,称为居中,和整个公式,称为标准化。使用均值,居中在回归中起着关键作用。考虑一个包含两个属性的数据集,体重和身高。我们将对数据进行居中,使得预测变量体重的均值为 0。这使得解释截距变得更加容易。截距将被解释为在预测变量值设为其均值时,预期的身高。

执行分词

当你得到任何文本时,第一步是将文本分词成一个基于给定问题需求的格式。分词是一个非常广泛的术语;我们可以在以下不同的粒度级别进行分词:

  • 段落级

  • 句子级

  • 词级

在本节中,我们将看到句子级别和词语级别的分词。方法相似,可以轻松应用于段落级别或根据问题需要的其他粒度级别。

准备就绪

我们将看到如何在一个示例中执行句子级别和词语级别的分词。

如何执行……

让我们开始演示句子分词:

# Load Libraries
from nltk.tokenize import sent_tokenize
from nltk.tokenize import word_tokenize
from collections import defaultdict

# 1.Let us use a very simple text to demonstrate tokenization
# at sentence level and word level. You have seen this example in the
# dictionary recipe, except for some punctuation which are added.

sentence = "Peter Piper picked a peck of pickled peppers. A peck of pickled \
peppers, Peter Piper picked !!! If Peter Piper picked a peck of pickled \
peppers, Wheres the peck of pickled peppers Peter Piper picked ?"

# 2.Using nltk sentence tokenizer, we tokenize the given text into sentences
# and verify the output using some print statements.

sent_list = sent_tokenize(sentence)

print "No sentences = %d"%(len(sent_list))
print "Sentences"
for sent in sent_list: print sent

# 3.With the sentences extracted let us proceed to extract
# words from these sentences.
word_dict = defaultdict(list)
for i,sent in enumerate(sent_list):
    word_dict[i].extend(word_tokenize(sent))

print word_dict

下面简要了解一下 NLTK 如何执行句子分词:

def sent_tokenize(text, language='english'):
    """
    Return a sentence-tokenized copy of *text*,
    using NLTK's recommended sentence tokenizer
    (currently :class:`.PunktSentenceTokenizer`
    for the specified language).

    :param text: text to split into sentences
    :param language: the model name in the Punkt corpus
    """
    tokenizer = load('tokenizers/punkt/{0}.pickle'.format(language))
    return tokenizer.tokenize(text)

它是如何工作的……

在第 1 步,我们将初始化一个名为 sentence 的变量,并为其赋值一个段落。这与我们在字典示例中使用的例子相同。在第 2 步,我们将使用 nltk 的sent_tokenize函数从给定的文本中提取句子。你可以查看nltk 的sent_tokenize函数的源代码。

如你所见,sent_tokenize加载了一个预构建的分词模型,使用这个模型,它将给定文本进行分词并返回输出。这个分词模型是nltk.tokenize.punkt模块中的 PunktSentenceTokenizer 实例。这个分词器在不同语言中有多个预训练的实例。在我们的案例中,你可以看到语言参数设置为英文。

让我们看一下这一步的输出:

No sentences = 3
Sentences
Peter Piper picked a peck of pickled peppers.
A peck of pickled             peppers, Peter Piper picked !!!
If Peter Piper picked a peck of pickled             peppers, Wheres the peck of pickled peppers Peter Piper picked ?

如你所见,句子分词器已将输入文本拆分为三句。让我们继续进行第 3 步,在该步骤中我们将把这些句子分词成单词。这里,我们将使用word_tokenize函数从每个句子中提取单词,并将它们存储在一个字典中,其中键是句子编号,值是该句子的单词列表。让我们查看打印语句的输出:

defaultdict(<type 'list'>, {0: ['Peter', 'Piper', 'picked', 'a', 'peck', 'of', 'pickled', 'peppers', '.'], 1: ['A', 'peck', 'of', 'pickled', 'peppers', ',', 'Peter', 'Piper', 'picked', '!', '!', '!'], 2: ['If', 'Peter', 'Piper', 'picked', 'a', 'peck', 'of', 'pickled', 'peppers', ',', 'Wheres', 'the', 'peck', 'of', 'pickled', 'peppers', 'Peter', 'Piper', 'picked', '?']})

word_tokenize使用正则表达式将句子分割成单词。查看word_tokenize的源代码会很有帮助,源代码可以在www.nltk.org/_modules/nltk/tokenize/punkt.html#PunktLanguageVars.word_tokenize找到。

还有更多内容…

对于句子分词,我们已经在 NLTK 中看到了实现方法。还有其他可用的方法。nltk.tokenize.simple模块有一个line_tokenize方法。让我们使用之前相同的输入句子,使用line_tokenize进行处理:

# Load Libraries
from nltk.tokenize import line_tokenize

sentence = "Peter Piper picked a peck of pickled peppers. A peck of pickled \
peppers, Peter Piper picked !!! If Peter Piper picked a peck of pickled \
peppers, Wheres the peck of pickled peppers Peter Piper picked ?"

sent_list = line_tokenize(sentence)
print "No sentences = %d"%(len(sent_list))
print "Sentences"
for sent in sent_list: print sent

# Include new line characters
sentence = "Peter Piper picked a peck of pickled peppers. A peck of pickled\n \
peppers, Peter Piper picked !!! If Peter Piper picked a peck of pickled\n \
peppers, Wheres the peck of pickled peppers Peter Piper picked ?"

sent_list = line_tokenize(sentence)
print "No sentences = %d"%(len(sent_list))
print "Sentences"
for sent in sent_list: print sent

输出如下:

No sentences = 1
Sentences
Peter Piper picked a peck of pickled peppers. A peck of pickled             peppers, Peter Piper picked !!! If Peter Piper picked a peck of pickled             peppers, Wheres the peck of pickled peppers Peter Piper picked ?

你可以看到我们只提取了输入中的句子。

现在让我们修改输入,包含换行符:

sentence = "Peter Piper picked a peck of pickled peppers. A peck of pickled\n \
peppers, Peter Piper picked !!! If Peter Piper picked a peck of pickled\n \
peppers, Wheres the peck of pickled peppers Peter Piper picked ?"

注意,我们添加了一个换行符。我们将再次应用line_tokenize来获得以下输出:

No sentences = 3
Sentences
Peter Piper picked a peck of pickled peppers. A peck of pickled
             peppers, Peter Piper picked !!! If Peter Piper picked a peck of pickled
             peppers, Wheres the peck of pickled peppers Peter Piper picked ?

你可以看到它已经根据换行符对我们的句子进行了分词,现在我们有了三句。

请参阅第三章NLTK书籍,它有更多关于句子和词语分词的参考。可以在www.nltk.org/book/ch03.html找到。

另见

  • 在第一章中使用字典对象使用 Python 进行数据科学

  • 在第一章中使用写入列表使用 Python 进行数据科学

移除停用词

在文本处理过程中,我们关注的是能够帮助我们将给定文本与语料库中其他文本区分开来的词语或短语。我们将这些词语或短语称为关键词短语。每个文本挖掘应用程序都需要一种方法来找出关键词短语。信息检索应用程序需要关键词短语来轻松检索和排序搜索结果。文本分类系统则需要关键词短语作为特征,以供分类器使用。

这就是停用词的作用所在。

“有时,一些极为常见的词语,它们在帮助选择与用户需求匹配的文档方面似乎没有什么价值,会被完全从词汇表中排除。这些词语被称为停用词。”

由 Christopher D. Manning, Prabhakar Raghavan 和 Hinrich Schütze 编写的《信息检索导论》

Python NLTK 库为我们提供了一个默认的停用词语料库,我们可以加以利用,如下所示:

>>> from nltk.corpus import stopwords
>>> stopwords.words('english')
[u'i', u'me', u'my', u'myself', u'we', u'our', u'ours', u'ourselves', u'you', u'your', u'yours', u'yourself', u'yourselves', u'he', u'him', u'his', u'himself', u'she', u'her', u'hers', u'herself', u'it', u'its', u'itself', u'they', u'them', u'their', u'theirs', u'themselves', u'what', u'which', u'who', u'whom', u'this', u'that', u'these', u'those', u'am', u'is', u'are', u'was', u'were', u'be', u'been', u'being', u'have', u'has', u'had', u'having', u'do', u'does', u'did', u'doing', u'a', u'an', u'the', u'and', u'but', u'if', u'or', u'because', u'as', u'until', u'while', u'of', u'at', u'by', u'for', u'with', u'about', u'against', u'between', u'into', u'through', u'during', u'before', u'after', u'above', u'below', u'to', u'from', u'up', u'down', u'in', u'out', u'on', u'off', u'over', u'under', u'again', u'further', u'then', u'once', u'here', u'there', u'when', u'where', u'why', u'how', u'all', u'any', u'both', u'each', u'few', u'more', u'most', u'other', u'some', u'such', u'no', u'nor', u'not', u'only', u'own', u'same', u'so', u'than', u'too', u'very', u's', u't', u'can', u'will', u'just', u'don', u'should', u'now']
>>>

你可以看到,我们已经打印出了英语的停用词列表。

如何实现……

让我们加载必要的库并引入输入文本:

# Load libraries
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import string

text = "Text mining, also referred to as text data mining, roughly equivalent to text analytics,\
refers to the process of deriving high-quality information from text. High-quality information is \
typically derived through the devising of patterns and trends through means such as statistical \
pattern learning. Text mining usually involves the process of structuring the input text \
(usually parsing, along with the addition of some derived linguistic features and the removal \
of others, and subsequent insertion into a database), deriving patterns within the structured data, \
and finally evaluation and interpretation of the output. 'High quality' in text mining usually \
refers to some combination of relevance, novelty, and interestingness. Typical text mining tasks \
include text categorization, text clustering, concept/entity extraction, production of granular \
taxonomies, sentiment analysis, document summarization, and entity relation modeling \
(i.e., learning relations between named entities).Text analysis involves information retrieval, \
lexical analysis to study word frequency distributions, pattern recognition, tagging/annotation, \
information extraction, data mining techniques including link and association analysis, \
visualization, and predictive analytics. The overarching goal is, essentially, to turn text \
into data for analysis, via application of natural language processing (NLP) and analytical \
methods.A typical application is to scan a set of documents written in a natural language and \
either model the document set for predictive classification purposes or populate a database \
or search index with the information extracted."

现在,让我们演示停用词移除的过程:

words = word_tokenize(text)
# 2.Let us get the list of stopwords from nltk stopwords english corpus.
stop_words = stopwords.words('english')

print "Number of words = %d"%(len(words)) 
# 3.	Filter out the stop words.
words = [w for w in words if w not in stop_words]
print "Number of words,without stop words = %d"%(len(words)) 

words = [w for w in words if w not in string.punctuation]
print "Number of words,without stop words and punctuations = %d"%(len(words))

它是如何工作的……

在第 1 步中,我们将从 nltk 导入必要的库。我们需要英语停用词列表,因此我们将导入停用词语料库。我们还需要将输入文本分解为单词。为此,我们将从nltk.tokenize模块中导入word_tokenize函数。

对于我们的输入文本,我们从维基百科的文本挖掘介绍部分获取了段落,详细内容可以在en.wikipedia.org/wiki/Text_mining中找到。

最后,我们将使用word_tokenize函数将输入文本分词。现在,words是一个包含所有从输入中分词的单词的列表。让我们看看打印函数的输出,打印出words列表的长度:

Number of words = 259

我们的列表中共有 259 个词。

在第 2 步中,我们将编译一个包含英语停用词的列表,称为stop_words

在第 2 步中,我们将使用列表推导式来获取最终的词汇表;只有那些不在我们在第 2 步中创建的停用词列表中的词汇才会被保留。这样,我们就能从输入文本中移除停用词。现在让我们看看我们打印语句的输出,打印出移除停用词后的最终列表:

Number of words,without stop words = 195

你可以看到,我们从输入文本中去除了将近 64 个停用词。

还有更多……

停用词不仅限于常规的英语单词。它是基于上下文的,取决于手头的应用程序以及您希望如何编程您的系统。理想情况下,如果我们不关心特殊字符,我们可以将它们包含在停用词列表中。让我们看一下以下代码:

import string
words = [w for w in words if w not in string.punctuation]
print "Number of words,without stop words and punctuations = %d"%(len(words)) 

在这里,我们将执行另一个列表推导式,以便从单词中移除标点符号。现在,输出结果如下所示:

Number of words, without stop words and punctuations = 156

提示

请记住,停用词移除是基于上下文的,并且取决于应用程序。如果您正在处理移动端或聊天室文本的情感分析应用,表情符号是非常有用的。您不会将它们移除,因为它们构成了下游机器学习应用的一个非常好的特征集。

通常情况下,在文档中,停用词的频率是非常高的。然而,您的语料库中可能还有其他词语,它们的频率也可能非常高。根据您的上下文,您可以将它们添加到停用词列表中。

另见

  • 执行分词 方案在 第三章,分析数据 - 探索与清洗

  • 列表生成 方案在 第一章,使用 Python 进行数据科学

词干提取

在这里,我们将看到如何进行词干提取。

准备工作

文本的标准化是一个不同的课题,我们需要不同的工具来应对它。在本节中,我们将探讨如何将单词转换为其基本形式,以便为我们的处理带来一致性。我们将从传统的方法开始,包括词干提取和词形还原。英语语法决定了某些单词在句子中的用法。例如,perform、performing 和 performs 表示相同的动作;它们根据语法规则出现在不同的句子中。

词干提取和词形还原的目标是将单词的屈折形式,有时还包括衍生形式,归约为一个共同的基本形式。

《信息检索简介》 作者:Christopher D. Manning, Prabhakar Raghavan & Hinrich Schütze

让我们来看看如何使用 Python NLTK 执行词干提取。NLTK 为我们提供了一套丰富的功能,可以帮助我们轻松完成词干提取:

>>> import nltk.stem
>>> dir(nltk.stem)
['ISRIStemmer', 'LancasterStemmer', 'PorterStemmer', 'RSLPStemmer', 'RegexpStemmer', 'SnowballStemmer', 'StemmerI', 'WordNetLemmatizer', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'api', 'isri', 'lancaster', 'porter', 'regexp', 'rslp', 'snowball', 'wordnet']
>>>  

我们可以看到模块中的函数列表,针对我们的兴趣,以下是一些词干提取器:

  • Porter – Porter 词干提取器

  • Lancaster – Lancaster 词干提取器

  • Snowball – Snowball 词干提取器

Porter 是最常用的词干提取器。该算法在将单词转化为根形式时并不是特别激进。

Snowball 是对 Porter 的改进。它在计算时间上比 Porter 更快。

Lancaster 是最激进的词干提取器。与 Porter 和 Snowball 不同,最终的单词令牌在经过 Lancaster 处理后将无法被人类读取,但它是这三者中最快的。

在本方案中,我们将使用其中一些函数来查看如何进行单词的词干提取。

如何做…

首先,让我们加载必要的库并声明我们希望在其上演示词干提取的数据集:

# Load Libraries
from nltk import stem

#1\. small input to figure out how the three stemmers perform.
input_words = ['movies','dogs','planes','flowers','flies','fries','fry','weeks','planted','running','throttle']

让我们深入了解以下不同的词干提取算法:

#2.Porter Stemming
porter = stem.porter.PorterStemmer()
p_words = [porter.stem(w) for w in input_words]
print p_words

#3.Lancaster Stemming
lancaster = stem.lancaster.LancasterStemmer()
l_words = [lancaster.stem(w) for w in input_words]
print l_words

#4.Snowball stemming
snowball = stem.snowball.EnglishStemmer()
s_words = [snowball.stem(w) for w in input_words]
print s_words

wordnet_lemm = stem.WordNetLemmatizer()
wn_words = [wordnet_lemm.lemmatize(w) for w in input_words]
print wn_words

它是如何工作的…

在第 1 步中,我们将从 nltk 导入词干模块。我们还将创建一个我们希望进行词干提取的单词列表。如果你仔细观察,这些单词被选择时已经具有不同的词缀,包括 s、ies、ed、ing 等。此外,还有一些单词已经处于根形式,如“throttle”和“fry”。这个示例的目的是查看词干提取算法如何处理它们。

第 2、3 和 4 步非常相似;我们将分别对输入文本应用 porter、lancaster 和 snowball 词干提取器,并打印输出。我们将使用列表推导式将这些单词应用到输入上,最后打印输出。让我们查看打印输出,了解词干提取的效果:

[u'movi', u'dog', u'plane', u'flower', u'fli', u'fri', u'fri', u'week', u'plant', u'run', u'throttl']

这是第 2 步的输出。我们对输入单词应用了 Porter 词干提取。我们可以看到,带有词缀 ies、s、ed 和 ing 的单词已被简化为它们的根形式:

Movies – movi
Dogs   - dog
Planes – plane
Running – run and so on.

有趣的是,“throttle”没有发生变化。

在第 3 步中,我们将打印 Lancaster 词干提取器的输出,结果如下:

[u'movy', 'dog', 'plan', 'flow', 'fli', 'fri', 'fry', 'week', 'plant', 'run', 'throttle']

单词“throttle”保持不变。请注意“movies”发生了什么变化。

类似地,让我们看看在第 4 步中 Snowball 词干提取器的输出:

[u'movi', u'dog', u'plane', u'flower', u'fli', u'fri', u'fri', u'week', u'plant', u'run', u'throttl']

输出与 porter 词干提取器的结果非常相似。

还有更多…

这三种算法都相当复杂;深入了解这些算法的细节超出了本书的范围。我建议你可以通过网络进一步了解这些算法的更多细节。

有关 porter 和 snowball 词干提取器的详细信息,请参考以下链接:

snowball.tartarus.org/algorithms/porter/stemmer.html

另见

  • 第一章中的列表推导式示例,使用 Python 进行数据科学

执行单词词形还原

在这部分中,我们将学习如何执行单词词形还原。

准备工作

词干提取是一个启发式过程,通过去除单词的词缀来获得单词的根形式。在前面的示例中,我们看到它可能会错误地去除正确的词汇,也就是去除派生词缀。

请参见以下维基百科链接,了解派生模式:

en.wikipedia.org/wiki/Morphological_derivation#Derivational_patterns

另一方面,词形还原使用形态学分析和词汇来获取单词的词根。它尝试仅改变屈折结尾,并从字典中给出基础单词。

请参阅维基百科有关屈折变化的更多信息:en.wikipedia.org/wiki/Inflection

在这个示例中,我们将使用 NLTK 的WordNetLemmatizer

如何操作…

首先,我们将加载必要的库。和之前的示例一样,我们将准备一个文本输入来演示词形还原。然后我们将以如下方式实现词形还原:

# Load Libraries
from nltk import stem

#1\. small input to figure out how the three stemmers perform.
input_words = ['movies','dogs','planes','flowers','flies','fries','fry','weeks', 'planted','running','throttle']

#2.Perform lemmatization.
wordnet_lemm = stem.WordNetLemmatizer()
wn_words = [wordnet_lemm.lemmatize(w) for w in input_words]
print wn_words

它是如何工作的…

第 1 步与我们的词干提取食谱非常相似。我们将提供输入。在第 2 步中,我们将进行词形还原。这个词形还原器使用 Wordnet 的内建 morphy 函数。

wordnet.princeton.edu/man/morphy.7WN.html

让我们看看打印语句的输出:

[u'movie', u'dog', u'plane', u'flower', u'fly', u'fry', 'fry', u'week', 'planted', 'running', 'throttle']

第一个注意到的是单词 movie。你可以看到它得到了正确的处理。Porter 和其他算法将最后一个字母 e 去掉了。

还有更多…

让我们看看使用词形还原器的一个小例子:

>>> wordnet_lemm.lemmatize('running')
'running'
>>> porter.stem('running')
u'run'
>>> lancaster.stem('running')
'run'
>>> snowball.stem('running')
u'run'

单词 running 理应还原为 run,我们的词形还原器应该处理得当。我们可以看到它没有对 running 做出任何更改。然而,我们的启发式词干提取器处理得很好!那么,我们的词形还原器怎么了?

提示

默认情况下,词形还原器假定输入是一个名词;我们可以通过将词汇的词性标签(POS)传递给词形还原器来进行修正,如下所示:

>>> wordnet_lemm.lemmatize('running','v') u'run'

另见

  • 在第三章中执行分词食谱,分析数据 - 探索与整理

将文本表示为词袋模型

在这里,我们将学习如何将文本表示为词袋模型。

准备就绪

为了对文本进行机器学习,我们需要将文本转换为数值特征向量。在本节中,我们将探讨词袋模型表示,其中文本被转换为数值向量,列名是底层的单词,值可以是以下几种:

  • 二进制,表示词汇是否出现在给定的文档中。

  • 频率,表示单词在给定文档中的出现次数。

  • TFIDF,这是一个分数,我们将在后面讲解。

词袋模型是最常见的文本表示方式。顾名思义,词语的顺序被忽略,只有词语的存在/缺失对这种表示方法至关重要。这是一个两步过程,如下所示:

  1. 对于文档中每个出现在训练集中的单词,我们将分配一个整数并将其存储为字典。

  2. 对于每个文档,我们将创建一个向量。向量的列是实际的单词本身。它们构成了特征。单元格的值可以是二进制、频率或 TFIDF。

如何操作…

让我们加载必要的库并准备数据集来演示词袋模型:

# Load Libraries
from nltk.tokenize import sent_tokenize
from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import stopwords

# 1.	Our input text, we use the same input which we had used in stop word removal recipe.
text = "Text mining, also referred to as text data mining, roughly equivalent to text analytics,\
refers to the process of deriving high-quality information from text. High-quality information is \
typically derived through the devising of patterns and trends through means such as statistical \
pattern learning. Text mining usually involves the process of structuring the input text \
(usually parsing, along with the addition of some derived linguistic features and the removal \
of others, and subsequent insertion into a database), deriving patterns within the structured data, \
and finally evaluation and interpretation of the output. 'High quality' in text mining usually \
refers to some combination of relevance, novelty, and interestingness. Typical text mining tasks \
include text categorization, text clustering, concept/entity extraction, production of granular \
taxonomies, sentiment analysis, document summarization, and entity relation modeling \
(i.e., learning relations between named entities).Text analysis involves information retrieval, \
lexical analysis to study word frequency distributions, pattern recognition, tagging/annotation, \
information extraction, data mining techniques including link and association analysis, \
visualization, and predictive analytics. The overarching goal is, essentially, to turn text \
into data for analysis, via application of natural language processing (NLP) and analytical \
methods.A typical application is to scan a set of documents written in a natural language and \
either model the document set for predictive classification purposes or populate a database \
or search index with the information extracted."

让我们深入了解如何将文本转换为词袋模型表示:

#2.Let us divide the given text into sentences
sentences = sent_tokenize(text)

#3.Let us write the code to generate feature vectors.
count_v = CountVectorizer()
tdm = count_v.fit_transform(sentences)

# While creating a mapping from words to feature indices, we can ignore
# some words by providing a stop word list.
stop_words = stopwords.words('english')
count_v_sw = CountVectorizer(stop_words=stop_words)
sw_tdm = count_v.fit_transform(sentences)

# Use ngrams
count_v_ngram = CountVectorizer(stop_words=stop_words,ngram_range=(1,2))
ngram_tdm = count_v.fit_transform(sentences)

它是如何工作的…

在第 1 步中,我们将定义输入。这与我们用于停用词去除的输入相同。在第 2 步中,我们将导入句子分词器,并将给定的输入分割成句子。我们将把这里的每个句子当作一个文档:

提示

根据你的应用,文档的概念可以有所变化。在这个例子中,我们的句子被视为一个文档。在某些情况下,我们也可以将一个段落视为文档。在网页挖掘中,单个网页可以视为一个文档,或者被 <p> 标签分隔的网页部分也可以视为一个文档。

>>> len(sentences)
6
>>>

如果我们打印句子列表的长度,我们会得到六,因此在我们的例子中,我们有六个文档。

在第 3 步中,我们将从 scikit-learn.feature_extraction 文本包中导入 CountVectorizer。它将文档集合——在此案例中是句子列表——转换为矩阵,其中行是句子,列是这些句子中的单词。这些单词的计数将作为单元格的值插入。

我们将使用 CountVectorizer 将句子列表转换为一个词项文档矩阵。让我们逐一解析输出。首先,我们来看一下 count_v,它是一个 CountVectorizer 对象。我们在引言中提到过,我们需要构建一个包含给定文本中所有单词的字典。count_vvocabulary_ 属性提供了单词及其相关 ID 或特征索引的列表:

它是如何工作的…

可以通过 vocabulary_ 属性检索这个字典。它是一个将词项映射到特征索引的映射。我们还可以使用以下函数获取单词(特征)的列表:

>>> count_v.get_feature_names()

现在让我们来看一下 tdm,这是我们使用 CountVectorizer 转换给定输入后得到的对象:

>>> type(tdm)
<class 'scipy.sparse.csr.csr_matrix'>
>>>

如你所见,tdm 是一个稀疏矩阵对象。请参考以下链接,了解更多关于稀疏矩阵表示的信息:

docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.sparse.csr_matrix.html

我们可以查看这个对象的形状,并检查其中的一些元素,如下所示:

它是如何工作的…

我们可以看到矩阵的形状是 6 X 122。我们有六个文档,也就是在我们的语境下的句子,122 个单词构成了词汇表。请注意,这是稀疏矩阵表示形式;因为并非所有句子都会包含所有单词,所以很多单元格的值会是零,因此我们只会打印那些非零条目的索引。

tdm.indptr 中,我们知道文档 1 的条目从 0 开始,在 tdm.datatdm.indices 数组中结束于 18,如下所示:

>>> tdm.data[0:17]
array([4, 2, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int64)
>>> tdm.indices[0:17]
array([107,  60,   2,  83, 110,   9,  17,  90,  28,   5,  84, 108,  77,
        67,  20,  40,  81])
>>>

我们可以通过以下方式验证这一点:

>>> count_v.get_feature_names()[107]
u'text'
>>> count_v.get_feature_names()[60]
u'mining'

我们可以看到,107 对应单词 text,在第一句话中出现了四次,类似地,mining 出现了一次。因此,在这个示例中,我们将给定的文本转换成了一个特征向量,其中的特征是单词。

还有更多内容…

CountVectorizer 类提供了许多其他功能,用于将文本转换为特征向量。我们来看看其中的一些功能:

>>> count_v.get_params()
{'binary': False, 'lowercase': True, 'stop_words': None, 'vocabulary': None, 'tokenizer': None, 'decode_error': u'strict', 'dtype': <type 'numpy.int64'>, 'charset_error': None, 'charset': None, 'analyzer': u'word', 'encoding': u'utf-8', 'ngram_range': (1, 1), 'max_df': 1.0, 'min_df': 1, 'max_features': None, 'input': u'content', 'strip_accents': None, 'token_pattern': u'(?u)\\b\\w\\w+\\b', 'preprocessor': None}
>>>	

第一个参数是二进制的,设置为False;我们也可以将其设置为True。这样,最终的矩阵将不再显示计数,而是根据单词是否出现在文档中,显示 1 或 0。

默认情况下,lowercase设置为True;输入文本会在单词映射到特征索引之前转为小写。

在创建单词与特征索引的映射时,我们可以通过提供一个停用词列表来忽略一些单词。请查看以下示例:

from nltk.corpus import stopwords
stop_words = stopwords.words('english')

count_v = CountVectorizer(stop_words=stop_words)
sw_tdm = count_v.fit_transform(sentences)

如果我们打印出已构建的词汇表的大小,我们可以看到以下内容:

>>> len(count_v_sw.vocabulary_)
106
>>>

我们可以看到现在有 106 个词,而之前是 122 个。

我们还可以为CountVectorizer提供一个固定的词汇集。最终的稀疏矩阵列将仅来自这些固定集,任何不在该集合中的内容将被忽略。

下一个有趣的参数是 n-gram 范围。你可以看到传递了一个元组(1,1)。这确保了在创建特征集时只使用单个词语或一元词。例如,可以将其更改为(1,2),这会告诉CountVectorizer创建一元词和二元词。让我们看一下下面的代码和输出:

count_v_ngram = CountVectorizer(stop_words=stop_words,ngram_range=(1,2))
ngram_tdm = count_v.fit_transform(sentences)

现在一元词和二元词都成为了我们的特征集的一部分。

我将留给你去探索其他参数。这些参数的文档可以通过以下链接访问:

scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html

另见

  • 使用字典食谱在第一章中,Python 数据科学入门

  • 去除停用词、词干提取、词形还原食谱在第三章中,数据分析——探索与整理

计算词频和逆文档频率

在这一部分,我们将学习如何计算词频和逆文档频率。

准备工作

出现次数和计数作为特征值是不错的选择,但它们也存在一些问题。假设我们有四篇长度不同的文档,这会使得较长文档中的词语比较短文档中的词语权重更高。因此,我们将不使用原始的词频,而是对其进行归一化处理;我们将一个词在文档中出现的次数除以文档中的总词数。这个度量叫做词频。词频也并非没有问题。有些词语会出现在很多文档中。这些词会主导特征向量,但它们不足以区分语料库中的文档。在我们探讨一个可以避免这个问题的新度量之前,先来定义一下文档频率。类似于词频,它是相对于文档的局部度量,我们可以计算一个称为文档频率的得分,它是词语在语料库中出现的文档数除以语料库中的总文档数。

我们将使用的最终度量是词频和文档频率倒数的乘积,这就是所谓的 TFIDF 得分。

如何操作…

加载必要的库并声明将用于展示词频和逆文档频率的输入数据:

# Load Libraries
from nltk.tokenize import sent_tokenize
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer

# 1.	We create an input document as in the previous recipe.

text = "Text mining, also referred to as text data mining, roughly equivalent to text analytics,\
refers to the process of deriving high-quality information from text. High-quality information is \
typically derived through the devising of patterns and trends through means such as statistical \
pattern learning. Text mining usually involves the process of structuring the input text \
(usually parsing, along with the addition of some derived linguistic features and the removal \
of others, and subsequent insertion into a database), deriving patterns within the structured data, \
and finally evaluation and interpretation of the output. 'High quality' in text mining usually \
refers to some combination of relevance, novelty, and interestingness. Typical text mining tasks \
include text categorization, text clustering, concept/entity extraction, production of granular \
taxonomies, sentiment analysis, document summarization, and entity relation modeling \
(i.e., learning relations between named entities).Text analysis involves information retrieval, \
lexical analysis to study word frequency distributions, pattern recognition, tagging/annotation, \
information extraction, data mining techniques including link and association analysis, \
visualization, and predictive analytics. The overarching goal is, essentially, to turn text \
into data for analysis, via application of natural language processing (NLP) and analytical \
methods.A typical application is to scan a set of documents written in a natural language and \
either model the document set for predictive classification purposes or populate a database \
or search index with the information extracted."

让我们看看如何计算词频和逆文档频率:

# 2.	Let us extract the sentences.
sentences = sent_tokenize(text)

# 3.	Create a matrix of term document frequency.
stop_words = stopwords.words('english')

count_v = CountVectorizer(stop_words=stop_words)
tdm = count_v.fit_transform(sentences)

#4.	Calcuate the TFIDF score.
tfidf = TfidfTransformer()
tdm_tfidf = tfidf.fit_transform(tdm)

它是如何工作的…

步骤 1、2 和 3 与之前的教程相同。让我们来看一下步骤 4,在这一步骤中,我们将传递步骤 3 的输出,以计算 TFIDF 得分:

>>> type(tdm)
<class 'scipy.sparse.csr.csr_matrix'>
>>>

Tdm 是一个稀疏矩阵。现在,让我们使用索引、数据和索引指针来查看这些矩阵的值:

它是如何工作的…

数据显示的是值,而不是出现次数,而是单词的归一化 TFIDF 得分。

还有更多内容…

再一次,我们可以通过查看可以传递的参数,深入了解 TFIDF 转换器:

>>> tfidf.get_params()
{'use_idf': True, 'smooth_idf': True, 'sublinear_tf': False, 'norm': u'l2'}
>>>

相关文档可以在scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfTransformer.html找到。

第四章 数据分析——深度分析

本章将涵盖以下主题:

  • 提取主成分

  • 使用内核 PCA

  • 使用奇异值分解(SVD)提取特征

  • 使用随机投影减少数据维度

  • 使用NMF非负矩阵分解)分解特征矩阵

简介

本章我们将讨论降维的相关技术。在上一章中,我们探讨了如何浏览数据以理解其特征,从而为实际应用提供有意义的使用方式。我们将讨论仅限于二元数据。想象一下,如果有一个拥有数百列的数据集,我们如何进行分析以了解如此大维度数据集的特征?我们需要高效的工具来解决这个难题,以便处理数据。

如今,高维数据无处不在。考虑为一个中等规模的电子商务网站构建产品推荐引擎。即便只有几千种产品,考虑的变量数量依然非常庞大。生物信息学也是另一个拥有极高维度数据的领域。基因表达的微阵列数据集可能包含成千上万的维度。

如果你的任务是探索数据或为算法准备数据,那么高维度,通常称为维度灾难,是一个很大的障碍。我们需要高效的方法来处理这个问题。此外,许多现有的数据挖掘算法的复杂度会随着维度数量的增加而呈指数级增长。随着维度的增加,算法的计算难度也变得无法处理,因此在许多应用中不可行。

降维技术尽可能保留数据的结构,同时减少维度的数量。因此,在降维后的特征空间中,算法的执行时间得以缩短,因为维度变得较低。由于数据的结构得以保留,获得的结果可以是原始数据空间的可靠近似。保留结构意味着两点;第一是保持原始数据集中的变异性,第二是保留新投影空间中数据向量之间的距离。

矩阵分解:

矩阵分解提供了几种降维技术。我们的数据通常是一个矩阵,实例位于行中,特征位于列中。在之前的例子中,我们一直将数据存储为 NumPy 矩阵。例如,在鸢尾花数据集(Iris dataset)中,我们的数据元组或实例表示为矩阵的行,而特征(如花萼和花瓣的宽度和长度)则为矩阵的列。

矩阵分解是一种表达矩阵的方法。假设 A 是两个其他矩阵 B 和 C 的乘积。矩阵 B 应该包含可以解释数据变化方向的向量。矩阵 C 应该包含这种变化的幅度。因此,我们的原始矩阵 A 现在表示为矩阵 B 和 C 的线性组合。

我们将在接下来的章节中看到的技术利用矩阵分解来解决降维问题。有些方法要求基本向量必须彼此正交,如主成分分析(PCA);还有一些方法则不强求这一要求,如字典学习。

让我们系好安全带,在本章中看看这些技术如何实际应用。

提取主成分

我们将要看的第一个技术是 主成分分析PCA)。PCA 是一种无监督方法。在多变量问题中,PCA 用于通过最小的信息损失来减少数据的维度,换句话说,就是保留数据中的最大变化。这里所说的变化,指的是数据扩展的最大方向。让我们来看一下下面的图:

提取主成分

我们有一个包含两个变量 x1x2 的散点图。对角线表示最大变化方向。通过使用 PCA,我们的目标是捕捉这种变化的方向。因此,我们的任务不是使用两个变量 x1x2 的方向来表示数据,而是要找到一个由蓝线表示的向量,并仅用这个向量来表示数据。实质上,我们希望将数据的维度从二维降到一维。

我们将利用数学工具特征值和特征向量来找到这个蓝色线向量。

我们在上一章中看到,方差衡量数据的离散程度或扩展程度。我们看到的是一维的示例。若维度超过一维,就很容易将变量之间的相关性表示为矩阵,这个矩阵称为协方差矩阵。当协方差矩阵的值被标准差归一化时,我们得到相关矩阵。在我们的例子中,协方差矩阵是一个二维的 2 X 2 矩阵,表示两个变量 x1x2,它衡量这两个变量是否朝同一方向变化或一起变化的程度。

当我们进行特征值分解,即得到协方差矩阵的特征向量和特征值时,主特征向量是具有最大特征值的向量,它指向原始数据中最大方差的方向。

在我们的示例中,这应该是图中由蓝线表示的向量。然后,我们将继续把输入数据投影到这个蓝线向量上,以获得降维后的结果。

对于一个包含 n 个实例和 m 个维度的数据集(n x m),PCA 将其投影到一个较小的子空间(n x d),其中 d << m。

需要注意的一点是,PCA 在计算上非常昂贵。

PCA 可以在协方差矩阵和相关矩阵上执行。记住,当使用具有不均匀尺度的数据集的协方差矩阵进行 PCA 时,结果可能不太有用。感兴趣的读者可以参考 Bernard Flury 的《A First Course in Multivariate Statistics》一书,了解使用相关矩阵或协方差矩阵进行 PCA 的话题。

www.springer.com/us/book/9780387982069

准备就绪

我们将使用 Iris 数据集来理解如何有效地使用 PCA 减少数据集的维度。Iris 数据集包含来自三种不同物种的 150 朵鸢尾花的测量数据。

Iris 数据集中的三种类别如下:

  • Iris Setosa

  • Iris Versicolor

  • Iris Virginica

以下是 Iris 数据集中的四个特征:

  • 花萼长度(单位:厘米)

  • 花萼宽度(单位:厘米)

  • 花瓣长度(单位:厘米)

  • 花瓣宽度(单位:厘米)

我们能否仅使用两列而不是四列来表示数据中大多数变化?我们的目标是减少数据的维度。在这种情况下,我们的数据有四列。假设我们正在构建一个分类器来预测一种新实例的花卉类型;我们能否在减少维度后的空间中完成这项任务?我们是否可以将列数从四列减少到两列,并且仍然为分类器实现较好的准确性?

PCA 的步骤如下:

  1. 将数据集标准化,使其均值为零。

  2. 找出数据集的相关矩阵和单位标准差值。

  3. 将相关矩阵降维为其特征向量和特征值。

  4. 根据按降序排列的特征值,选择前 n 个特征向量。

  5. 将输入的特征向量矩阵投影到新的子空间。

如何操作……

让我们加载必要的库,并调用来自 scikit-learn 的实用函数load_iris来获取 Iris 数据集:

import numpy as np
from sklearn.datasets import load_iris
from sklearn.preprocessing import scale
import scipy
import matplotlib.pyplot as plt

# Load Iris data
data = load_iris()
x = data['data']
y = data['target']

# Since PCA is an unsupervised method, we will not be using the target variable y
# scale the data such that mean = 0 and standard deviation = 1
x_s = scale(x,with_mean=True,with_std=True,axis=0)

# Calculate correlation matrix
x_c = np.corrcoef(x_s.T)

# Find eigen value and eigen vector from correlation matrix
eig_val,r_eig_vec = scipy.linalg.eig(x_c)
print 'Eigen values \n%s'%(eig_val)
print '\n Eigen vectors \n%s'%(r_eig_vec)

# Select the first two eigen vectors.
w = r_eig_vec[:,0:2]

# # Project the dataset in to the dimension
# from 4 dimension to 2 using the right eignen vector
x_rd = x_s.dot(w)

# Scatter plot the new two dimensions
plt.figure(1)
plt.scatter(x_rd[:,0],x_rd[:,1],c=y)
plt.xlabel("Component 1")
plt.ylabel("Component 2")

现在,我们将对这些数据进行标准化,使其均值为零,标准差为一,我们将利用numpyscorr_coef函数来找出相关矩阵:

x_s = scale(x,with_mean=True,with_std=True,axis=0)
x_c = np.corrcoef(x_s.T)

然后,我们将进行特征值分解,并将我们的 Iris 数据投影到前两个主特征向量上。最后,我们将在降维空间中绘制数据集:

eig_val,r_eig_vec = scipy.linalg.eig(x_c)
print 'Eigen values \n%s'%(eig_val)
print '\n Eigen vectors \n%s'%(r_eig_vec)
# Select the first two eigen vectors.
w = r_eig_vec[:,0:2]

# # Project the dataset in to the dimension
# from 4 dimension to 2 using the right eignen vector
x_rd = x_s.dot(w)

# Scatter plot the new two dimensions
plt.figure(1)
plt.scatter(x_rd[:,0],x_rd[:,1],c=y)
plt.xlabel("Component 1")
plt.ylabel("Component 2")

使用函数 scale。scale 函数可以执行中心化、缩放和标准化。中心化是将每个值减去均值,缩放是将每个值除以变量的标准差,最后标准化是先进行中心化,再进行缩放。使用变量 with_mean 和 with_std,scale 函数可以执行这三种归一化技术。

它是如何工作的……

Iris 数据集有四列。尽管列不多,但足以满足我们的需求。我们的目标是将 Iris 数据集的维度从四降到二,并且仍然保留数据的所有信息。

我们将使用 scikit-learn 中方便的load_iris函数将 Iris 数据加载到xy变量中。x变量是我们的数据矩阵,我们可以查看它的形状:

>>>x.shape
(150, 4)
>>>

我们将对数据矩阵x进行标准化,使其均值为零,标准差为一。基本规则是,如果你的数据中的所有列都在相同的尺度上测量并且具有相同的单位,你就不需要对数据进行标准化。这将允许 PCA 捕捉这些基本单位的最大变异性:

x_s = scale(x,with_mean=True,with_std=True,axis=0)

我们将继续构建输入数据的相关性矩阵:

n 个随机变量 X1, ..., Xn 的相关性矩阵是一个 n × n 的矩阵,其中第 i, j 个元素是 corr(Xi, Xj),参考维基百科。

然后我们将使用 SciPy 库计算矩阵的特征值和特征向量。让我们看看我们的特征值和特征向量:

print Eigen values \n%s%(eig_val)
print \n Eigen vectors \n%s%(r_eig_vec)

输出如下:

如何工作…

在我们的案例中,特征值是按降序打印的。一个关键问题是,我们应该选择多少个组件?在接下来的章节中,我们将解释几种选择组件数量的方法。

你可以看到我们仅选择了右侧特征向量的前两列。所保留组件对y变量的区分能力,是测试数据中保留了多少信息或变异性的一个很好的方法。

我们将把数据投影到新的降维空间。

最后,我们将绘制xy轴上的各个组件,并根据目标变量进行着色:

如何工作…

你可以看到,组件12能够区分三种鸢尾花的类别。因此,我们有效地使用 PCA 将维度从四降到二,并且仍然能够区分属于不同鸢尾花类别的样本。

更多内容…

在前一节中,我们提到过会概述几种方法,帮助我们选择应该包含多少个组件。在我们的方案中,我们只包含了两个。以下是一些更加经验性的方法,用于选择组件:

  1. 特征值标准:

    一个特征值为一意味着该组件将解释大约一个变量的变异性。因此,根据这个标准,一个组件至少应当解释一个变量的变异性。我们可以说,我们将只包含特征值大于或等于一的组件。根据你的数据集,你可以设定这个阈值。在一个非常高维的数据集中,仅包含能够解释一个变量的组件可能并不是非常有用。

  2. 方差解释比例标准:

    让我们运行以下代码:

    print "Component, Eigen Value, % of Variance, Cummulative %"
    cum_per = 0
    per_var = 0
    for i,e_val in enumerate(eig_val):
        per_var = round((e_val / len(eig_val)),3)
        cum_per+=per_var
    print ('%d, %0.2f, %0.2f, %0.2f')%(i+1, e_val, per_var*100,cum_per*100)
    
  3. 输出结果如下:更多内容…

对每个主成分,我们打印了其特征值、该主成分所解释的方差百分比以及解释的方差的累计百分比。例如,主成分1的特征值为2.912.91/4给出了该主成分所解释的方差百分比,即 72.80%。现在,如果我们包含前两个主成分,我们就可以解释数据中 95.80%的方差。

将相关矩阵分解为其特征向量和特征值是一种通用技术,可以应用于任何矩阵。在本例中,我们将应用它来分析相关矩阵,以便理解数据分布的主轴,即观察数据最大变化的轴。

主成分分析(PCA)既可以作为探索性技术,也可以作为下游算法的数据准备技术。文档分类数据集问题通常具有非常大的维度特征向量。PCA 可以用来减少数据集的维度,从而在将数据输入到分类算法之前,只保留最相关的特征。

PCA 的一个缺点是它是一个计算开销较大的操作。最后,关于 numpy 的 corrcoeff 函数,需要指出的是,corrcoeff 函数会在内部对数据进行标准化作为其计算的一部分。但由于我们希望明确地说明缩放的原因,因此我们在本节中显式地进行了说明。

提示

PCA 何时有效?

输入数据集应该具有相关的列,才能使 PCA 有效工作。如果输入变量之间没有相关性,PCA 将无法帮助我们。

另见

  • 在第四章中,执行奇异值分解的技巧,数据分析 - 深度探讨

使用核 PCA

PCA 假设数据中的所有主变化方向都是直线。然而,许多实际数据集并不符合这一假设。

注意

PCA 仅限于数据变化呈直线的那些变量。换句话说,它仅适用于线性可分的数据。

在本节中,我们将介绍核 PCA,它将帮助我们减少数据集的维度,尤其是当数据集中的变化不是直线时。我们将显式创建这样的数据集,并应用核 PCA 进行分析。

在核 PCA 中,核函数应用于所有数据点。这将输入数据转换为核空间。然后,在核空间中执行普通 PCA。

准备工作

我们在这里不会使用鸢尾花数据集,而是生成一个变化不是直线的数据集。这样,我们就不能在这个数据集上应用简单的 PCA。让我们继续查看我们的配方。

如何实现…

让我们加载必要的库。我们将使用 scikit-learn 库中的make_circles函数生成一个数据集。我们将绘制这个数据并在该数据集上执行普通 PCA:

from sklearn.datasets import make_circles
import matplotlib.pyplot as plt
import numpy as np
from sklearn.decomposition import PCA
from sklearn.decomposition import KernelPCA

# Generate a dataset where the variations cannot be captured by a straight line.
np.random.seed(0)
x,y = make_circles(n_samples=400, factor=.2,noise=0.02)

# Plot the generated dataset
plt.close('all')
plt.figure(1)
plt.title("Original Space")
plt.scatter(x[:,0],x[:,1],c=y)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$")

# Try to fit the data using normal PCA
pca = PCA(n_components=2)
pca.fit(x)
x_pca = pca.transform(x)

接下来,我们将绘制数据集的前两个主成分。我们只使用第一个主成分来绘制数据集:

plt.figure(2)
plt.title("PCA")
plt.scatter(x_pca[:,0],x_pca[:,1],c=y)
plt.xlabel("$Component_1$")
plt.ylabel("$Component_2$")

# Plot using the first component from normal pca
class_1_indx = np.where(y==0)[0]
class_2_indx = np.where(y==1)[0]

plt.figure(3)
plt.title("PCA- One component")
plt.scatter(x_pca[class_1_indx,0],np.zeros(len(class_1_indx)),color='red')
plt.scatter(x_pca[class_2_indx,0],np.zeros(len(class_2_indx)),color='blue')

让我们通过执行核 PCA 并绘制主成分来完成它:

# Create  KernelPCA object in Scikit learn, specifying a type of kernel as a parameter.
kpca = KernelPCA(kernel="rbf",gamma=10)
# Perform KernelPCA
kpca.fit(x)
x_kpca = kpca.transform(x)

# Plot the first two components.
plt.figure(4)
plt.title("Kernel PCA")
plt.scatter(x_kpca[:,0],x_kpca[:,1],c=y)
plt.xlabel("$Component_1$")
plt.ylabel("$Component_2$")
plt.show()

它是如何工作的…

在步骤 1 中,我们使用 scikit 的数据生成函数生成了一个数据集。在此案例中,我们使用了make_circles函数。我们可以用这个函数创建两个同心圆,一个大圆包含一个小圆。每个同心圆属于某一类。因此,我们创建了一个由两个同心圆组成的两类问题。

首先,让我们看看我们生成的数据。make_circles函数生成了一个包含 400 个数据点的二维数据集。原始数据的图如下:

它是如何工作的…

这张图描述了我们的数据是如何分布的。外圈属于第一类,内圈属于第二类。我们能不能将这些数据用线性分类器来处理?我们无法做到。数据中的变化不是直线。我们无法使用普通的 PCA。因此,我们将求助于核 PCA 来变换数据。

在我们深入探讨核 PCA 之前,让我们先看看如果对这个数据集应用普通 PCA 会发生什么。

让我们看看前两个组件的输出图:

它是如何工作的…

如你所见,PCA 的组件无法以线性方式区分这两个类别。

让我们绘制第一个主成分并看看它的类别区分能力。下面的图表是我们只绘制第一个主成分时的情况,解释了 PCA 无法区分数据:

它是如何工作的…

普通的 PCA 方法是一种线性投影技术,当数据是线性可分时效果良好。在数据不可线性分割的情况下,需要使用非线性技术来进行数据集的降维。

注意

核 PCA 是一种用于数据降维的非线性技术。

让我们继续创建一个核 PCA 对象,使用 scikit-learn 库。以下是我们的对象创建代码:

KernelPCA(kernel=rbf,gamma=10) 

我们选择了径向基函数RBF)核,γ值为 10。γ是核的参数(用于处理非线性)——核系数。

在进一步讨论之前,让我们看一下有关核函数的一些理论。简单来说,核函数是一个计算两个向量点积的函数,也就是说,计算它们的相似度,这两个向量作为输入传递给它。

对于两个点 xx',在某个输入空间中,RBFGaussian 核的定义如下:

它是如何工作的…

其中,

它是如何工作的…

RBF 随着距离的增加而减小,取值范围在 0 和 1 之间。因此,它可以被解释为一种相似性度量。RBF 核的特征空间具有无限维度——维基百科

可以在以下位置找到:

en.wikipedia.org/wiki/Radial_basis_function_kernel

现在,让我们将输入从特征空间转换到核空间。我们将在核空间中执行 PCA。

最后,我们将绘制前两个主成分的散点图。数据点的颜色根据它们的类别值来区分:

如何工作…

你可以在这个图中看到,数据点在核空间中是线性分开的。

还有更多…

Scikit-learn 的核 PCA 对象还允许使用其他类型的核,如下所示:

  • 线性

  • 多项式

  • Sigmoid

  • 余弦

  • 预计算

Scikit-learn 还提供了其他类型的非线性生成数据。以下是另一个示例:

from sklearn.datasets import make_moons
x,y = make_moons(100)
plt.figure(5)
plt.title("Non Linear Data")
plt.scatter(x[:,0],x[:,1],c=y)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$")
plt.savefig('fig-7.png')
plt.show()

数据图像如下所示:

还有更多…

使用奇异值分解提取特征

在我们讨论完 PCA 和核 PCA 之后,我们可以通过以下方式来解释降维:

  • 你可以将相关变量转换为一组不相关的变量。通过这种方式,我们将得到一个较低维度的解释,揭示数据之间的关系而不损失任何信息。

  • 你可以找出主轴,这些轴记录了最多的数据变异。

奇异值分解 (SVD)是另一种矩阵分解技术,可以用来解决维度灾难问题。它可以用较少的维度找到原始数据的最佳近似。与 PCA 不同,SVD 作用于原始数据矩阵。

注意

SVD 不需要协方差矩阵或相关矩阵。它直接作用于原始数据矩阵。

SVD 将一个m x n的矩阵A分解成三个矩阵的乘积:

使用奇异值分解提取特征

这里,U 是一个m x k的矩阵,V 是一个n x k的矩阵,S 是一个k x k的矩阵。U 的列称为左奇异向量,V 的列称为右奇异向量。

S 矩阵对角线上的值称为奇异值。

准备就绪

我们将使用鸢尾花数据集进行此次练习。我们的任务是将数据集的维度从四维降至二维。

如何做…

让我们加载必要的库并获取鸢尾花数据集:

from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
import numpy as np
from sklearn.preprocessing import scale
from scipy.linalg import svd

# Load Iris dataset
data = load_iris()
x = data['data']
y = data['target']

# Proceed by scaling the x variable w.r.t its mean,
x_s = scale(x,with_mean=True,with_std=False,axis=0)

# Decompose the matrix using SVD technique.We will use SVD implementation in scipy.
U,S,V = svd(x_s,full_matrices=False)

# Approximate the original matrix by selecting only the first two singular values.
x_t = U[:,:2]

# Finally we plot the datasets with the reduced components.
plt.figure(1)
plt.scatter(x_t[:,0],x_t[:,1],c=y)
plt.xlabel("Component 1")
plt.ylabel("Component 2")
plt.show()

现在,我们将展示如何对鸢尾花数据集执行 SVD 操作:

# Proceed by scaling the x variable w.r.t its mean,
x_s = scale(x,with_mean=True,with_std=False,axis=0)
# Decompose the matrix using SVD technique.We will use SVD implementation in scipy.
U,S,V = svd(x_s,full_matrices=False)

# Approximate the original matrix by selecting only the first two singular values.
x_t = U[:,:2]

# Finally we plot the datasets with the reduced components.
plt.figure(1)
plt.scatter(x_t[:,0],x_t[:,1],c=y)
plt.xlabel("Component 1")
plt.ylabel("Component 2")
plt.show()

如何工作…

鸢尾花数据集有四列。虽然列数不多,但它足以满足我们的目的。我们计划将鸢尾花数据集的维度从四降到二,并仍然保留所有数据的信息。

我们将使用 Scikit-learn 中的load_iris函数将鸢尾花数据加载到xy变量中。x变量是我们的数据矩阵;我们可以通过以下方式检查其形状:

>>>x.shape
(150, 4)
>>>

我们使用数据矩阵x的均值对其进行中心化。经验法则是,如果所有列都在相同的尺度上测量并具有相同的度量单位,则无需缩放数据。这将使 PCA 捕捉到这些基础单位的最大变化。请注意,我们在调用 scale 函数时只使用了均值:

x_s = scale(x,with_mean=True,with_std=False,axis=0)
  1. 在我们缩放过的输入数据集上运行 SVD 方法。

  2. 选择前两个奇异分量。该矩阵是原始输入数据的一个简化近似。

  3. 最后,绘制矩阵的列,并根据类别值进行着色:它是如何工作的…

还有更多内容…

SVD 是一种双模因子分析方法,我们从一个任意的矩形矩阵开始,该矩阵包含两种类型的实体。这与我们之前的 PCA 方法不同,PCA 以相关矩阵作为输入,属于单模因子分析,因为输入的方阵的行和列表示的是相同的实体。

在文本挖掘应用中,输入通常以术语-文档矩阵TDM)的形式呈现。在 TDM 中,行对应单词,列对应文档。单元格条目填充的是术语频率或词频逆文档频率TFIDF)得分。它是一个包含两种实体的矩形矩阵:单词和文档,这些实体分别出现在矩阵的行和列中。

SVD 广泛应用于文本挖掘中,用于揭示单词和文档、文档和文档、单词和单词之间的隐藏关系(语义关系)。

通过在术语-文档矩阵上应用 SVD,我们将其转换为一个新的语义空间,其中那些在同一文档中不一起出现的单词,在新的语义空间中仍然可以靠得很近。SVD 的目标是找到一种有效的方式来建模单词和文档之间的关系。应用 SVD 后,每个文档和单词都可以表示为一个因子值的向量。我们可以选择忽略那些值非常低的分量,从而避免数据集中的噪声。这将导致对文本语料库的近似表示。这就是潜在语义分析LSA)。

这一思路的应用在文档索引、搜索和信息检索中具有非常高的实用性。我们可以不再像传统倒排索引那样对原始单词进行索引,而是对 LSA(潜在语义分析)的输出进行索引。这有助于避免同义词和歧义词等问题。在同义词问题中,用户可能倾向于使用不同的词来表示同一个实体。常规索引在这种情况下容易出问题。由于文档是通过常规词汇进行索引的,搜索时可能无法获得相关结果。例如,如果我们为一些与金融工具相关的文档建立索引,通常涉及的词汇可能是货币、金钱等相似的词。货币和金钱是同义词。当用户搜索金钱时,他也应该看到与货币相关的文档。然而,使用常规索引时,搜索引擎只能返回包含“金钱”一词的文档。而使用潜在语义索引,包含“货币”一词的文档也会被检索到。在潜在语义空间中,货币和金钱彼此接近,因为它们在文档中的邻近词汇是相似的。

歧义词是指具有多重含义的词。例如,“银行”可以指金融机构或河岸。与同义词类似,歧义词也可以在潜在语义空间中处理。

有关 LSA 和潜在语义索引的更多信息,请参考 Deerwester 等人的论文:

citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.108.8490。有关特征值与奇异值的比较研究,请参阅 Cleve Moler 的《用 MATLAB 进行数值计算》一书。尽管示例使用 MATLAB,你仍然可以通过我们的教程将其在 Python 中实现:

in.mathworks.com/moler/eigs.pdf

使用随机投影减少数据维度

我们之前看到的降维方法计算开销较大,并且速度较慢。随机投影是另一种比这些方法更快的降维方式。

随机投影与 Johnson-Lindenstrauss 引理相关。根据该引理,存在一个将高维空间映射到低维欧几里得空间的映射,使得点之间的距离保持在ε范围内。目标是保持数据中任意两点之间的成对距离,同时减少数据的维度。

假设我们给定了一个n维的欧几里得空间数据,根据引理,我们可以将其映射到一个维度为 k 的欧几里得空间,在这个空间中,点之间的距离保持不变,误差范围在(1-ε)和(1+ε)之间。

准备开始

本次练习我们将使用 20 个新闻组数据集(qwone.com/~jason/20Newsgroups/)。

这是一个包含大约 20,000 个新闻组文档的集合,几乎均匀地划分为 20 个不同的新闻类别。Scikit-learn 提供了一个方便的函数来加载这个数据集:

from sklearn.datasets import fetch_20newsgroups
data = fetch_20newsgroups(categories=cat)

您可以通过提供一个类别字符串列表来加载所有库或感兴趣的类别列表。在我们的例子中,我们将使用sci.crypt类别。

我们将把输入文本加载为一个词项-文档矩阵,其中特征是单独的单词。在此基础上,我们将应用随机投影以减少维度数量。我们将尝试看看文档之间的距离是否在降维空间中得以保持,且每个实例是一个文档。

如何做到……

首先加载必要的库。使用 scikit 的工具函数fetch20newsgroups,我们将加载数据。我们将从所有数据中仅选择sci.crypt类别。然后,我们将把文本数据转化为向量表示:

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import euclidean_distances
from sklearn.random_projection import GaussianRandomProjection
import matplotlib.pyplot as plt
import numpy as np

# Load 20 newsgroup dataset
# We select only sci.crypt category
# Other categories include
# 'sci.med', 'sci.space' ,'soc.religion.christian'
cat =['sci.crypt']
data = fetch_20newsgroups(categories=cat)

# Create a term document matrix, with term frequencies as the values
# from the above dataset.
vectorizer = TfidfVectorizer(use_idf=False)
vector = vectorizer.fit_transform(data.data)

# Perform the projection. In this case we reduce the dimension to 1000
gauss_proj = GaussianRandomProjection(n_components=1000)
gauss_proj.fit(vector)
# Transform the original data to the new space
vector_t = gauss_proj.transform(vector)

# print transformed vector shape
print vector.shape
print vector_t.shape

# To validate if the transformation has preserved the distance, we calculate the old and the new distance between the points
org_dist = euclidean_distances(vector)
red_dist = euclidean_distances(vector_t)

diff_dist = abs(org_dist - red_dist)

# We take the difference between these points and plot them 
# as a heatmap (only the first 100 documents).
plt.figure()
plt.pcolor(diff_dist[0:100,0:100])
plt.colorbar()
plt.show()

现在让我们继续演示随机投影的概念。

它是如何工作的……

加载新闻组数据集后,我们将通过TfidfVectorizer(use_idf=False)将其转换为矩阵。

请注意,我们已经将use_idf设置为False。这创建了我们的输入矩阵,其中行是文档,列是单独的单词,单元格的值是单词的计数。

如果我们使用print vector.shape命令打印我们的向量,我们将得到以下输出:

(595, 16115)

我们可以看到,我们的输入矩阵有 595 个文档和 16115 个单词;每个单词是一个特征,因此也是一个维度。

我们将使用一个密集的高斯矩阵进行数据投影。高斯随机矩阵是通过从正态分布N(0, 1/组件数量)中抽样元素生成的。在我们的例子中,组件数量是 1000。我们的目标是将维度从 16115 降到 1000。然后,我们将打印出原始维度和降维后的维度,以验证维度的减少。

最后,我们想验证在投影之后数据特性是否得以保持。我们将计算向量之间的欧几里得距离。我们将记录原始空间和投影空间中的距离。我们将像第 7 步一样计算它们之间的差异,并将差异绘制成热图:

它是如何工作的……

如您所见,梯度范围在 0.000 到 0.105 之间,表示原始空间和降维空间中向量距离的差异。原始空间和投影空间中距离的差异几乎都在一个非常小的范围内。

还有更多内容……

关于随机投影有很多参考文献。这是一个非常活跃的研究领域。有兴趣的读者可以参考以下论文:

随机投影实验:

dl.acm.org/citation.cfm?id=719759

机器学习中的随机投影实验:

citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.13.9205

在我们的方案中,我们使用了高斯随机投影,其中通过从正态分布 N(0,1/1000)中抽样生成了一个高斯随机矩阵,其中 1000 是降维空间的所需维度。

然而,使用稠密矩阵在处理时可能会产生严重的内存问题。为了避免这种情况,Achlioptas 提出了稀疏随机投影。与从标准正态分布中选择不同,矩阵条目是从{-1.0,1}中选择的,选择概率分别为{1/6,2/3,1/6}。如你所见,0 出现的概率为三分之二,因此最终的矩阵将是稀疏的。用户可以参考 Achlioptas 的开创性论文 Dimitris Achlioptas数据库友好的随机投影:使用二进制硬币的 Johnson-Lindenstrauss 方法。《计算机与系统科学杂志》,66(4):671–687,2003 年。

scikit 实现允许用户选择结果矩阵的密度。假设我们将密度指定为 d,s 为 1/d,则矩阵的元素可以从以下方程中选择:

更多内容…

概率如下:

更多内容…

另见

  • 使用核 PCA 配方见第四章,数据分析 - 深入解析

使用非负矩阵分解分解特征矩阵

我们之前从数据降维的角度讨论了所有的矩阵分解方法。现在,让我们从协同过滤的角度讨论这个方法,使它更有趣。虽然数据降维是我们的目标,非负矩阵分解 (NMF) 在使用协同过滤算法的推荐系统中得到了广泛应用。

假设我们的输入矩阵 A 的维度为m x n。NMF 将输入矩阵分解为两个矩阵,A_dashH

使用非负矩阵分解分解特征矩阵

假设我们想将 A 矩阵的维度减少到 d,即我们希望将原始的 m x n 矩阵分解成 m x d,其中 d << n。

A_dash矩阵的大小为 m x d,H矩阵的大小为 d x m。NMF 将其作为一个优化问题来解决,即最小化以下函数:

使用非负矩阵分解分解特征矩阵

著名的 Netflix 挑战赛就是使用 NMF 解决的。请参阅以下链接:

Gábor Takács 等人,(2008)。矩阵分解与基于邻域的算法解决 Netflix 奖问题。载于:2008 年 ACM 推荐系统大会论文集,瑞士洛桑,10 月 23 日至 25 日,267-274:

dl.acm.org/citation.cfm?id=1454049

准备就绪

为了说明 NMF,让我们创建一个推荐问题。像 MovieLens 或 Netflix 这样的典型推荐系统中,存在一组用户和一组物品(电影)。如果每个用户对一些电影进行了评分,我们希望预测他们对尚未评分电影的评分。我们假设用户没有看过他们没有评分的电影。我们的预测算法输出的是这些电影的评分。然后,我们可以推荐预测引擎给出非常高评分的电影给这些用户。

我们的玩具问题如下:我们有以下电影:

电影 ID 电影名称
1 《星际大战》
2 《黑客帝国》
3 《盗梦空间》
4 《哈利·波特》
5 《霍比特人》
6 《纳瓦隆的枪》
7 《拯救大兵瑞恩》
8 《敌人门前》
9 《勇敢的心》
10 《大逃亡》

我们有十部电影,每部电影都有一个电影 ID。我们也有 10 个用户对这些电影进行了评分,具体如下:

电影 ID
用户 ID 1
1 5.0
2 4.2
3 2.5
4 3.8
5 2.1
6 4.7
7 2.8
8 4.5
9 0.0
10 4.1

为了提高可读性,我们将数据保持为矩阵形式,其中行对应用户,列对应电影。单元格中的数值表示评分,范围从 1 到 5,其中 5 表示用户对电影的高度喜好,1 表示用户不喜欢。单元格中的 0 表示用户未对该电影进行评分。在本例中,我们将使用 NMF 对user_id x movie_id矩阵进行分解。

如何进行…

我们将首先加载必要的库,然后创建我们的数据集。我们将把数据集存储为矩阵:

import numpy as np
from collections import defaultdict
from sklearn.decomposition import NMF
import matplotlib.pyplot as plt

# load our ratings matrix in python.
ratings = [
[5.0, 5.0, 4.5, 4.5, 5.0, 3.0, 2.0, 2.0, 0.0, 0.0],
[4.2, 4.7, 5.0, 3.7, 3.5, 0.0, 2.7, 2.0, 1.9, 0.0],
[2.5, 0.0, 3.3, 3.4, 2.2, 4.6, 4.0, 4.7, 4.2, 3.6],
[3.8, 4.1, 4.6, 4.5, 4.7, 2.2, 3.5, 3.0, 2.2, 0.0],
[2.1, 2.6, 0.0, 2.1, 0.0, 3.8, 4.8, 4.1, 4.3, 4.7],
[4.7, 4.5, 0.0, 4.4, 4.1, 3.5, 3.1, 3.4, 3.1, 2.5],
[2.8, 2.4, 2.1, 3.3, 3.4, 3.8, 4.4, 4.9, 4.0, 4.3],
[4.5, 4.7, 4.7, 4.5, 4.9, 0.0, 2.9, 2.9, 2.5, 2.1],
[0.0, 3.3, 2.9, 3.6, 3.1, 4.0, 4.2, 0.0, 4.5, 4.6],
[4.1, 3.6, 3.7, 4.6, 4.0, 2.6, 1.9, 3.0, 3.6, 0.0]
]

movie_dict = {
1:"Star Wars",
2:"Matrix",
3:"Inception",
4:"Harry Potter",
5:"The hobbit",
6:"Guns of Navarone",
7:"Saving Private Ryan",
8:"Enemy at the gates",
9:"Where eagles dare",
10:"Great Escape"
}

A = np.asmatrix(ratings,dtype=float)

# perform non negative matrix transformation on the data.
max_components = 2
reconstruction_error = []
nmf = None
nmf = NMF(n_components = max_components,random_state=1)
A_dash = nmf.fit_transform(A)

# Examine the reduced matrixfor i in range(A_dash.shape[0]):
for i in range(A_dash.shape[0]):
    print "User id = %d, comp1 score = %0.2f, comp 2 score = %0.2f"%(i+1,A_dash[i][0],A_dash[i][1])

plt.figure(1)
plt.title("User Concept Mapping")
x = A_dash[:,0]
y = A_dash[:,1]
plt.scatter(x,y)
plt.xlabel("Component 1 Score")
plt.ylabel("Component 2 Score")

# Let us examine our component matrix F.
F = nmf.components_
plt.figure(2)
plt.title("Movie Concept Mapping")
x = F[0,:]
y = F[1,:]
plt.scatter(x,y)
plt.xlabel("Component 1 score")
plt.ylabel("Component 2  score")
for i in range(F[0,:].shape[0]):
    plt.annotate(movie_dict[i+1],(F[0,:][i],F[1,:][i]))
plt.show()

现在我们将演示非负矩阵分解(NMF):

# perform non negative matrix transformation on the data.
max_components = 2
reconstruction_error = []
nmf = None
nmf = NMF(n_components = max_components,random_state=1)
A_dash = nmf.fit_transform(A)

# Examine the reduced matrixfor i in range(A_dash.shape[0]):
for i in range(A_dash.shape[0]):
    print "User id = %d, comp1 score = %0.2f, comp 2 score = %0.2f"%(i+1,A_dash[i][0],A_dash[i][1])
plt.figure(1)
plt.title("User Concept Mapping")
x = A_dash[:,0]
y = A_dash[:,1]
plt.scatter(x,y)
plt.xlabel("Component 1 Score")
plt.ylabel("Component 2 Score")

# Let us examine our component matrix F.
F = nmf.components_
plt.figure(2)
plt.title("Movie Concept Mapping")
x = F[0,:]
y = F[1,:]
plt.scatter(x,y)
plt.xlabel("Component 1 score")
plt.ylabel("Component 2  score")
for i in range(F[0,:].shape[0]):
    plt.annotate(movie_dict[i+1],(F[0,:][i],F[1,:][i]))
plt.show()

如何运作…

我们将从列表中将数据加载到 NumPy 矩阵 A 中。我们将根据max_components变量选择将维度降至 2。然后,我们将用组件数初始化 NMF 对象。最后,我们将应用该算法以获得降维后的矩阵A_dash

这就是我们需要做的。scikit 库为我们隐藏了许多细节。现在让我们来看一下后台发生了什么。从正式角度来看,NMF 将原始矩阵分解成两个矩阵,这两个矩阵相乘后,可以得到我们原始矩阵的近似值。看一下我们代码中的以下一行:

A_dash = nmf.fit_transform(A)

输入矩阵 A 被转换为简化后的矩阵 A_dash。让我们来看一下新矩阵的形状:

>>>A_dash.shape
(10, 2)

原始矩阵被简化为两列,而不是原来的十列。这就是简化空间。从这个数据的角度来看,我们可以说我们的算法已经将原来的十部电影分成了两个概念。单元格的数值表示用户对每个概念的亲和力。

我们将打印并查看亲和力的表现:

for i in range(A_dash.shape[0]):
print User id = %d, comp1 score = %0.2f, comp 2 score =%0.2f%(i+1,A_dash[i][0],A_dash[i][1])

输出如下所示:

How it works…

看看用户 1;前面图像中的第一行显示,用户 1 在概念 1 上得分为 2.14,而在概念 2 上得分为 0,表明用户 1 对概念 1 有更强的亲和力。

看看用户 ID 为 3 的用户;这个用户对概念 1 有更多的亲和力。现在我们已经将输入数据集减少为二维,展示在图表中会更加清晰。

在我们的 x 轴上,我们有组件 1,y 轴上是组件 2。我们将以散点图的形式绘制各种用户。我们的图形如下所示:

How it works…

你可以看到,我们有两组用户;一组是组件 1 得分大于 1.5 的用户,另一组是得分小于 1.5 的用户。我们能够将用户分为两类,基于简化的特征空间。

让我们看看另一个矩阵 F

F = nmf.components_

F 矩阵有两行;每一行对应我们的组件,十列,每一列对应一个电影 ID。换句话说,就是电影对这些概念的亲和力。我们来绘制这个矩阵。

你可以看到我们的 x 轴是第一行,y 轴是第二行。在步骤 1 中,我们声明了一个字典。我们希望这个字典为每个点标注电影名称:

for i in range(F[0,:].shape[0]):
plt.annotate(movie_dict[i+1],(F[0,:][i],F[1,:][i]))

annotate 方法将字符串(用于标注)作为第一个参数,并且将 xy 坐标作为一个元组。

你可以看到输出图表如下:

How it works…

你可以看到,我们有两组明显不同的群体。所有战争电影的组件 1 得分非常低,而组件 2 得分非常高。所有奇幻电影的得分则正好相反。我们可以大胆地说,组件 1 包含了战争电影,而得分高的用户对战争电影有很强的亲和力。同样的情况也适用于奇幻电影。

因此,通过使用 NMF,我们能够发掘出输入矩阵中关于电影的隐藏特征。

还有更多内容……

我们看到特征空间从十维被简化到二维,接下来,让我们看看这如何应用于推荐引擎。我们从这两个矩阵中重建原始矩阵:

reconstructed_A = np.dot(W,H)
np.set_printoptions(precision=1)
print reconstructed_A

重建后的矩阵如下所示:

还有更多…

它与原始矩阵有多大不同?原始矩阵在这里给出;请查看高亮的行:

电影 ID
用户 ID 1
1 5.0
2 4.2
3 2.5
4 3.8
5 2.1
6 4.7
7 2.8
8 4.5
9 0.0
10 4.1

对于用户 6 和电影 3,我们现在有了评分。这将帮助我们决定是否推荐这部电影给用户,因为他还没有看过。记住,这是一个玩具数据集;现实世界中的场景有许多电影和用户。

另见

  • 使用奇异值分解提取特征 配方在 第四章中,数据分析 - 深入探讨

第五章 数据挖掘——大海捞针

在本章中,我们将覆盖以下主题:

  • 使用距离度量

  • 学习和使用核方法

  • 使用 k-means 方法进行数据聚类

  • 学习向量量化

  • 在单变量数据中寻找异常值

  • 使用局部异常因子方法发现异常值

介绍

本章我们将主要关注无监督数据挖掘算法。我们将从涵盖各种距离度量的食谱开始。理解距离度量和不同的空间在构建数据科学应用程序时至关重要。任何数据集通常都是一组属于特定空间的对象。我们可以将空间定义为从中抽取数据集中的点的点集。最常见的空间是欧几里得空间。在欧几里得空间中,点是实数向量。向量的长度表示维度的数量。

接下来,我们将介绍一个食谱,介绍核方法。核方法是机器学习中一个非常重要的主题。它们帮助我们通过线性方法解决非线性数据问题。我们将介绍核技巧的概念。

接下来,我们将介绍一些聚类算法的食谱。聚类是将一组点划分为逻辑组的过程。例如,在超市场景中,商品按类别进行定性分组。然而,我们将关注定量方法。具体来说,我们将把注意力集中在 k-means 算法上,并讨论它的局限性和优点。

我们的下一个食谱是一种无监督技术,称为学习向量量化。它既可以用于聚类任务,也可以用于分类任务。

最后,我们将讨论异常值检测方法。异常值是数据集中与其他观测值有显著差异的观测值。研究这些异常值非常重要,因为它们可能是反映异常现象或数据生成过程中存在错误的信号。在机器学习模型应用于数据之前,了解如何处理异常值对于算法非常关键。本章将重点介绍几种经验性的异常值检测技术。

在本章中,我们将重点依赖 Python 库,如 NumPy、SciPy、matplotlib 和 scikit-learn 来编写大部分食谱。我们还将从脚本编写转变为在本章中编写过程和类的风格。

使用距离度量

距离和相似度度量是各种数据挖掘任务的关键。在本食谱中,我们将看到一些距离度量的应用。我们的下一个食谱将涉及相似度度量。在查看各种距离度量之前,让我们先定义一个距离度量。

作为数据科学家,我们总是会遇到不同维度的点或向量。从数学角度来看,一组点被定义为一个空间。在这个空间中,距离度量被定义为一个函数 d(x,y),它以空间中的两个点 x 和 y 作为参数,并输出一个实数。这个距离函数,即实数输出,应该满足以下公理:

  1. 距离函数的输出应该是非负的,d(x,y) >= 0

  2. 距离函数的输出只有在 x = y 时才为零

  3. 距离应该是对称的,也就是说,d(x,y) = d(y,x)

  4. 距离应该遵循三角不等式,也就是说,d(x,y) <= d(x,z) + d(z,y)

仔细查看第四个公理可以发现,距离是两点之间最短路径的长度。

你可以参考以下链接获取有关公理的更多信息:

en.wikipedia.org/wiki/Metric_%28mathematics%29

准备开始

我们将研究欧几里得空间和非欧几里得空间中的距离度量。我们将从欧几里得距离开始,然后定义 Lr-norm 距离。Lr-norm 是一个距离度量家族,欧几里得距离是其中的一个成员。接着,我们会讨论余弦距离。在非欧几里得空间中,我们将研究 Jaccard 距离和汉明距离。

如何实现…

让我们从定义函数开始,以计算不同的距离度量:

import numpy as np

def euclidean_distance(x,y):
    if len(x) == len(y):
        return np.sqrt(np.sum(np.power((x-y),2)))
    else:
        print "Input should be of equal length"
    return None

def lrNorm_distance(x,y,power):
    if len(x) == len(y):
        return np.power(np.sum (np.power((x-y),power)),(1/(1.0*power)))
    else:
        print "Input should be of equal length"
    return None

def cosine_distance(x,y):
    if len(x) == len(y):
        return np.dot(x,y) / np.sqrt(np.dot(x,x) * np.dot(y,y))
    else:
        print "Input should be of equal length"
    return None

def jaccard_distance(x,y):
    set_x = set(x)
    set_y = set(y)
    return 1 - len(set_x.intersection(set_y)) / len(set_x.union(set_y))

def hamming_distance(x,y):
    diff = 0
    if len(x) == len(y):
        for char1,char2 in zip(x,y):
            if char1 != char2:
                diff+=1
        return diff
    else:
        print "Input should be of equal length"
    return None

现在,让我们编写一个主程序来调用这些不同的距离度量函数:

if __name__ == "__main__":

    # Sample data, 2 vectors of dimension 3
    x = np.asarray([1,2,3])
    y = np.asarray([1,2,3])
    # print euclidean distance    
    print euclidean_distance(x,y)
    # Print euclidean by invoking lr norm with
    # r value of 2    
    print lrNorm_distance(x,y,2)
    # Manhattan or citi block Distance
    print lrNorm_distance(x,y,1)

    # Sample data for cosine distance
    x =[1,1]
    y =[1,0]
    print 'cosine distance'
    print cosine_distance(x,y)

    # Sample data for jaccard distance    
    x = [1,2,3]
    y = [1,2,3]
    print jaccard_distance(x,y)

    # Sample data for hamming distance    
    x =[11001]
    y =[11011]
    print hamming_distance(x,y)

它是如何工作的…

让我们来看看主函数。我们创建了一个示例数据集和两个三维向量,并调用了euclidean_distance函数。

这是一种最常用的距离度量,即欧几里得距离。它属于 Lr-Norm 距离的家族。如果空间中的点是由实数构成的向量,那么该空间被称为欧几里得空间。它也被称为 L2 范数距离。欧几里得距离的公式如下:

它是如何工作的...

如你所见,欧几里得距离是通过在每个维度上计算距离(减去对应的维度),将距离平方,最后取平方根来推导出来的。

在我们的代码中,我们利用了 NumPy 的平方根和幂函数来实现前述公式:

np.sqrt(np.sum(np.power((x-y),2)))

欧几里得距离是严格正的。当 x 等于 y 时,距离为零。通过我们如何调用欧几里得距离,可以清楚地看到这一点:

x = np.asarray([1,2,3])
y = np.asarray([1,2,3])

print euclidean_distance(x,y)

如你所见,我们定义了两个 NumPy 数组,xy。我们保持它们相同。现在,当我们用这些参数调用euclidean_distance函数时,输出是零。

现在,让我们调用 L2 范数函数,lrNorm_distance

Lr-Norm 距离度量是距离度量家族中的一个成员,欧几里得距离属于该家族。我们可以通过它的公式来更清楚地理解这一点:

它是如何工作的...

你可以看到,我们现在有了一个参数r。让我们将r替换为 2,这样会将前面的方程转化为欧几里得方程。因此,欧几里得距离也称为 L2 范数距离:

lrNorm_distance(x,y,power):

除了两个向量外,我们还将传入一个名为power的第三个参数。这就是公式中定义的r。将其调用并设置power值为 2 时,将得到欧几里得距离。你可以通过运行以下代码来验证:

print lrNorm_distance(x,y,2)

这将得到零作为结果,这类似于欧几里得距离函数。

让我们定义两个示例向量,xy,并调用cosine_distance函数。

在将点视为方向的空间中,余弦距离表示给定输入向量之间角度的余弦值作为距离值。欧几里得空间以及点为整数或布尔值的空间,是余弦距离函数可以应用的候选空间。输入向量之间的角度余弦值是输入向量的点积与各自 L2 范数的乘积之比:

np.dot(x,y) / np.sqrt(np.dot(x,x) * np.dot(y,y))

让我们看一下分子,其中计算了输入向量之间的点积:

np.dot(x,y)

我们将使用 NumPy 的点积函数来获取点积值。xy这两个向量的点积定义如下:

它是如何工作的…

现在,让我们看一下分母:

np.sqrt(np.dot(x,x) * np.dot(y,y))

我们再次使用点积函数来计算输入向量的 L2 范数:

np.dot(x,x) is equivalent to 

tot = 0
for i in range(len(x)):
tot+=x[i] * x[i]

因此,我们可以计算两个输入向量之间角度的余弦值。

接下来,我们将讨论 Jaccard 距离。与之前的调用类似,我们将定义示例向量并调用jaccard_distance函数。

从实数值的向量开始,让我们进入集合。通常称为 Jaccard 系数,它是给定输入向量交集与并集大小的比率。减去这个值等于 Jaccard 距离。如你所见,在实现中,我们首先将输入列表转换为集合。这样我们就可以利用 Python 集合数据类型提供的并集和交集操作:

set_x = set(x)
set_y = set(y)

最后,距离计算如下:

1 - len(set_x.intersection(set_y)) / (1.0 * len(set_x.union(set_y)))

我们必须使用set数据类型中可用的交集和并集功能来计算距离。

我们的最后一个距离度量是汉明距离。对于两个比特向量,汉明距离计算这两个向量中有多少个比特不同:

for char1,char2 in zip(x,y):
    if char1 != char2:
        diff+=1
return diff

如你所见,我们使用了zip功能来检查每个比特,并维护一个计数器,统计有多少个比特不同。汉明距离用于分类变量。

还有更多……

记住,通过从我们的距离值中减去 1,我们可以得到一个相似度值。

还有一种我们没有详细探讨的距离,但它被广泛使用,那就是曼哈顿距离或城市块距离。这是一种 L1 范数距离。通过将 r 值设置为 1 传递给 Lr 范数距离函数,我们将得到曼哈顿距离。

根据数据所处的底层空间,需要选择合适的距离度量。在算法中使用这些距离时,我们需要注意底层空间的情况。例如,在 k 均值算法中,每一步都会计算所有相互接近点的平均值作为簇中心。欧几里得空间的一个优点是点的平均值存在且也是该空间中的一个点。请注意,我们的 Jaccard 距离输入是集合,集合的平均值是没有意义的。

在使用余弦距离时,我们需要检查底层空间是否为欧几里得空间。如果向量的元素是实数,则空间是欧几里得的;如果它们是整数,则空间是非欧几里得的。余弦距离在文本挖掘中最为常见。在文本挖掘中,单词被视为坐标轴,文档是这个空间中的一个向量。两个文档向量之间夹角的余弦值表示这两个文档的相似度。

SciPy 实现了所有这些列出的距离度量及更多内容:

docs.scipy.org/doc/scipy/reference/spatial.distance.html

上面的 URL 列出了 SciPy 支持的所有距离度量。

此外,scikit-learn 的pairwise子模块提供了一个叫做pairwise_distance的方法,可以用来计算输入记录之间的距离矩阵。你可以在以下位置找到:

scikitlearn.org/stable/modules/generated/sklearn.metrics.pairwise.pairwise_distances.html

我们曾提到过汉明距离是用于分类变量的。这里需要提到的是,通常用于分类变量的一种编码方式是独热编码。在进行独热编码后,汉明距离可以作为输入向量之间的相似度/距离度量。

另见

  • 使用随机投影减少数据维度,见第四章,数据分析 - 深入探索

学习和使用核方法

在本食谱中,我们将学习如何使用核方法进行数据处理。掌握核方法可以帮助你解决非线性问题。本食谱是核方法的入门介绍。

通常,线性模型——可以使用直线或超平面分离数据的模型——易于解释和理解。数据中的非线性使得我们无法有效使用线性模型。如果数据能够被转换到一个关系变为线性的空间中,我们就可以使用线性模型。然而,在变换后的空间中进行数学计算可能变成一个昂贵的操作。这就是核函数发挥作用的地方。

核函数是相似度函数。它接收两个输入参数,这两个输入之间的相似度就是核函数的输出。在这个示例中,我们将探讨核函数如何实现这种相似性。我们还将讨论所谓的“核技巧”。

正式定义一个核函数 K 是一个相似度函数:K(x1,x2) > 0 表示 x1 和 x2 之间的相似度。

准备就绪

在查看各种核函数之前,我们先来数学上定义一下:

准备就绪

这里,xixj 是输入向量:

准备就绪

上述映射函数用于将输入向量转化为一个新的空间。例如,如果输入向量位于一个 n 维空间中,变换函数将其转换为一个维度为 m 的新空间,其中 m >> n:

准备就绪准备就绪

上图表示的是点积:

准备就绪

上图是点积,xixj 现在通过映射函数被转换到一个新的空间中。

在这个示例中,我们将看到一个简单的核函数在实际中的应用。

我们的映射函数如下所示:

准备就绪

当原始数据被输入到这个映射函数时,它将输入转化为新的空间。

如何做到这一点…

让我们创建两个输入向量,并按照前面章节中描述的方式定义映射函数:

import numpy as np
# Simple example to illustrate Kernel Function concept.
# 3 Dimensional input space
x = np.array([10,20,30])
y = np.array([8,9,10])

# Let us find a mapping function to transform this space
# phi(x1,x2,x3) = (x1x2,x1x3,x2x3,x1x1,x2x2,x3x3)
# this will transorm the input space into 6 dimesions

def mapping_function(x):
    output_list  =[]
    for i in range(len(x)):
        output_list.append(x[i]*x[i])

    output_list.append(x[0]*x[1])
    output_list.append(x[0]*x[2])
    output_list.append(x[1]*x[0])
    output_list.append(x[1]*x[2])
    output_list.append(x[2]*x[1])
    output_list.append(x[2]*x[0])
    return np.array(output_list)

现在,让我们看看主程序如何调用核变换。在主函数中,我们将定义一个核函数并将输入变量传递给它,然后打印输出:

如何做

if __name_ == "__main__"
    # Apply the mapping function
    tranf_x = mapping_function(x)
    tranf_y = mapping_function(y)
    # Print the output
    print tranf_x
    print np.dot(tranf_x,tranf_y)

    # Print the equivalent kernel functions
    # transformation output.
    output = np.power((np.dot(x,y)),2)
    print output

它是如何工作的…

让我们从主函数开始跟踪这个程序。我们创建了两个输入向量,xy。这两个向量都是三维的。

然后我们定义了一个映射函数。这个映射函数利用输入向量的值,将输入向量转化为一个维度增加的新空间。在这个例子中,维度的数量从三维增加到九维。

现在,让我们对这些向量应用一个映射函数,将它们的维度增加到九维。

如果我们打印 tranf_x,我们将得到如下结果:

[100 400 900 200 300 200 600 600 300]

如你所见,我们将输入向量 x 从三维空间转化为一个九维向量。

现在,让我们在变换后的空间中计算点积并打印输出。

输出为 313600,一个标量值。

现在让我们回顾一下:我们首先将两个输入向量转换到更高维的空间,然后计算点积以得到标量输出。

我们所做的是一个非常昂贵的操作,将原始的三维向量转换为九维向量,然后在其上执行点积操作。

相反,我们可以选择一个内核函数,它能够在不显式地将原始空间转换为新空间的情况下得到相同的标量输出。

我们的新内核定义如下:

它是如何工作的...

对于两个输入,xy,该内核计算向量的点积,并对它们进行平方。

在打印内核的输出后,我们得到了 313600。

我们从未进行过转换,但仍然能够得到与转换空间中点积输出相同的结果。这就是所谓的内核技巧。

选择这个内核并没有什么魔力。通过扩展内核,我们可以得到我们的映射函数。有关扩展的详细信息,请参考以下文献:

en.wikipedia.org/wiki/Polynomial_kernel

还有更多...

有多种类型的内核。根据我们的数据特性和算法需求,我们需要选择合适的内核。以下是其中的一些:

线性内核:这是最简单的内核函数。对于两个给定的输入,它返回输入的点积:

还有更多...

多项式内核:它的定义如下:

还有更多...

在这里,xy 是输入向量,d 是多项式的次数,c 是常数。在我们的配方中,我们使用了次数为 2 的多项式内核。

以下是线性和多项式内核的 scikit 实现:

scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.linear_kernel.html#sklearn.metrics.pairwise.linear_kernel

scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.polynomial_kernel.html#sklearn.metrics.pairwise.polynomial_kernel

另见

  • 使用内核 PCA 配方见第四章,数据分析 – 深入分析

  • 通过随机投影减少数据维度 配方见第四章,数据分析 – 深入分析

使用 k-means 方法进行数据聚类

在本节中,我们将讨论 k-means 算法。K-means 是一种寻求中心的无监督算法。它是一种迭代的非确定性方法。所谓迭代是指算法步骤会重复,直到达到指定步数的收敛。非确定性意味着不同的起始值可能导致不同的最终聚类结果。该算法需要输入聚类的数量k。选择k值没有固定的方法,它必须通过多次运行算法来确定。

对于任何聚类算法,其输出的质量由聚类间的凝聚度和聚类内的分离度来决定。同一聚类中的点应彼此靠近;不同聚类中的点应相互远离。

准备就绪

在我们深入了解如何用 Python 编写 k-means 算法之前,有两个关键概念需要讨论,它们将帮助我们更好地理解算法输出的质量。第一个是与聚类质量相关的定义,第二个是用于衡量聚类质量的度量标准。

每个由 k-means 检测到的聚类都可以通过以下度量进行评估:

  1. 聚类位置:这是聚类中心的坐标。K-means 算法从一些随机点作为聚类中心开始,并通过迭代不断找到新的中心,使得相似的点被聚集到一起。

  2. 聚类半径:这是所有点与聚类中心的平均偏差。

  3. 聚类质量:这是聚类中点的数量。

  4. 聚类密度:这是聚类质量与其半径的比值。

现在,我们将衡量输出聚类的质量。如前所述,这是一个无监督问题,我们没有标签来验证输出,因此无法计算诸如精确度、召回率、准确度、F1 分数或其他类似的度量指标。我们将为 k-means 算法使用的度量标准称为轮廓系数。它的取值范围是-1 到 1。负值表示聚类半径大于聚类之间的距离,从而导致聚类重叠,这表明聚类效果较差。较大的值,即接近 1 的值,表示聚类效果良好。

轮廓系数是为聚类中每个点定义的。对于一个聚类 C 和聚类中的一个点i,令xi为该点与聚类中所有其他点的平均距离。

现在,计算点i与另一个聚类 D 中所有点的平均距离,记为 D。选择这些值中的最小值,称为yi

准备就绪

对于每个簇,所有点的轮廓系数的平均值可以作为簇质量的良好度量。所有数据点的轮廓系数的平均值可以作为整体簇质量的度量。

让我们继续生成一些随机数据:

import numpy as np
import matplotlib.pyplot as plt

def get_random_data():
    x_1 = np.random.normal(loc=0.2,scale=0.2,size=(100,100))
    x_2 = np.random.normal(loc=0.9,scale=0.1,size=(100,100))
    x = np.r_[x_1,x_2]
    return x

我们从正态分布中采样了两组数据。第一组数据的均值为0.2,标准差为0.2。第二组数据的均值为0.9,标准差为0.1。每个数据集的大小为 100 * 100——我们有100个实例和100个维度。最后,我们使用 NumPy 的行堆叠函数将它们合并。我们的最终数据集大小为 200 * 100。

让我们绘制数据的散点图:

x = get_random_data()

plt.cla()
plt.figure(1)
plt.title("Generated Data")
plt.scatter(x[:,0],x[:,1])
plt.show()

绘图如下:

准备就绪

虽然我们只绘制了第一维和第二维,但你仍然可以清楚地看到我们有两个簇。现在,让我们开始编写我们的 k-means 聚类算法。

如何实现...

让我们定义一个函数,可以对给定的数据和参数k执行 k-means 聚类。该函数对给定数据进行聚类,并返回整体的轮廓系数。

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

def form_clusters(x,k):
    """
    Build clusters
    """
    # k = required number of clusters
    no_clusters = k
    model = KMeans(n_clusters=no_clusters,init='random')
    model.fit(x)
    labels = model.labels_
    print labels
    # Cacluate the silhouette score
    sh_score = silhouette_score(x,labels)
    return sh_score

让我们调用上述函数,针对不同的k值,并存储返回的轮廓系数:

sh_scores = []
for i in range(1,5):
    sh_score = form_clusters(x,i+1)
    sh_scores.append(sh_score)

no_clusters = [i+1 for i in range(1,5)]

最后,让我们绘制不同k值的轮廓系数。

no_clusters = [i+1 for i in range(1,5)]

plt.figure(2)
plt.plot(no_clusters,sh_scores)
plt.title("Cluster Quality")
plt.xlabel("No of clusters k")
plt.ylabel("Silhouette Coefficient")
plt.show()

如何运作...

如前所述,k-means 是一个迭代算法。大致来说,k-means 的步骤如下:

  1. 从数据集中初始化k个随机点作为初始中心点。

  2. 按照以下步骤进行,直到指定次数的迭代收敛:

    • 将点分配给距离最近的簇中心。通常使用欧几里得距离来计算点与簇中心之间的距离。

    • 根据本次迭代中的分配重新计算新的簇中心。

    • 如果点的簇分配与上一次迭代相同,则退出循环。算法已收敛到最优解。

  3. 我们将使用来自 scikit-learn 库的 k-means 实现。我们的聚类函数接受 k 值和数据集作为参数,并运行 k-means 算法:

    model = KMeans(n_clusters=no_clusters,init='random')
    model.fit(x)
    

no_clusters是我们将传递给函数的参数。使用 init 参数时,我们将初始中心点设置为随机值。当 init 设置为 random 时,scikit-learn 会根据数据估算均值和方差,然后从高斯分布中采样 k 个中心。

最后,我们必须调用 fit 方法,在我们的数据集上运行 k-means 算法:

labels = model.labels_
sh_score = silhouette_score(x,labels)
return sh_score

我们获取标签,即每个点的簇分配,并计算出簇中所有点的轮廓系数。

在实际场景中,当我们在数据集上启动 k-means 算法时,我们并不知道数据中存在的集群数量;换句话说,我们不知道 k 的理想值。然而,在我们的示例中,我们知道 k=2,因为我们生成的数据已经适配了两个集群。因此,我们需要针对不同的 k 值运行 k-means:

sh_scores = []
for i in range(1,5):
sh_score = form_clusters(x,i+1)
sh_scores.append(sh_score)

对于每次运行,也就是每个 k 值,我们都会存储轮廓系数。k 与轮廓系数的图表可以揭示数据集的理想 k 值:

no_clusters = [i+1 for i in range(1,5)]

plt.figure(2)
plt.plot(no_clusters,sh_scores)
plt.title("Cluster Quality")
plt.xlabel("No of clusters k")
plt.ylabel("Silhouette Coefficient")
plt.show()

它是如何工作的…

正如预期的那样,k=2 时,我们的轮廓系数非常高。

还有更多…

关于 k-means 需要注意的几点:k-means 算法不能用于类别数据,对于类别数据需要使用 k-medoids。k-medoids 不是通过平均所有点来寻找集群中心,而是选择一个点,使其与该集群中所有其他点的平均距离最小。

在分配初始集群时需要小心。如果数据非常密集且集群之间距离非常远,且初始随机中心选择在同一个集群中,那么 k-means 可能表现不佳。

通常情况下,k-means 算法适用于具有星形凸集群的数据。有关星形凸数据点的更多信息,请参考以下链接:

mathworld.wolfram.com/StarConvex.html

如果数据中存在嵌套集群或其他复杂集群,k-means 的结果可能会是无意义的输出。

数据中异常值的存在可能导致较差的结果。一种好的做法是在运行 k-means 之前,进行彻底的数据探索,以便了解数据特征。

一种在算法开始时初始化中心的替代方法是 k-means++方法。因此,除了将 init 参数设置为随机值外,我们还可以使用 k-means++来设置它。有关 k-means++的更多信息,请参考以下论文:

k-means++:小心初始化的优势ACM-SIAM 研讨会关于离散算法。2007 年

另见

  • 在第五章中的距离度量工作法配方,数据挖掘 - 在大海捞针

使用局部离群因子方法发现异常值

局部离群因子(LOF)是一种异常值检测算法,它通过比较数据实例与其邻居的局部密度来检测异常值。其目的是决定数据实例是否属于相似密度的区域。它可以在数据集群数量未知且集群具有不同密度和大小的情况下,检测出数据集中的异常值。它受 KNN(K-最近邻)算法的启发,并被广泛使用。

准备工作

在前一个配方中,我们研究了单变量数据。在这个配方中,我们将使用多变量数据并尝试找出异常值。让我们使用一个非常小的数据集来理解 LOF 算法进行异常值检测。

我们将创建一个 5 X 2 的矩阵,查看数据后我们知道最后一个元组是异常值。我们也可以将其绘制为散点图:

from collections import defaultdict
import numpy as np

instances = np.matrix([[0,0],[0,1],[1,1],[1,0],[5,0]])

import numpy as np
import matplotlib.pyplot as plt

x = np.squeeze(np.asarray(instances[:,0]))
y = np.squeeze(np.asarray(instances[:,1]))
plt.cla()
plt.figure(1)
plt.scatter(x,y)
plt.show()

绘图结果如下:

准备中

LOF 通过计算每个点的局部密度来工作。根据点的 k 近邻的距离,估算该点的局部密度。通过将点的局部密度与其邻居的密度进行比较,检测出异常值。与邻居相比,异常值的密度较低。

为了理解 LOF,我们需要了解一些术语定义:

  • 对象 P 的 k 距离是对象 P 与其第 k 近邻之间的距离。K 是算法的一个参数。

  • P 的 k 距离邻域是所有距离 P 的距离小于或等于 P 与其第 k 近邻之间距离的对象 Q 的列表。

  • 从 P 到 Q 的可达距离定义为 P 与其第 k 近邻之间的距离与 P 到 Q 之间的距离中的最大值。以下符号可能有助于澄清这一点:

    Reachability distance (P ß Q) = > maximum(K-Distance(P), Distance(P,Q))
    
  • P 的局部可达密度(LRD(P))是 P 的 k 距离邻域与 P 及其邻居的可达距离之和的比值。

  • P 的局部异常因子(LOF(P))是 P 的局部可达密度与 P 的 k 近邻局部可达密度的比值的平均值。

如何实现……

  1. 让我们计算点之间的pairwise距离:

    k = 2
    distance = 'manhattan'
    
    from sklearn.metrics import pairwise_distances
    dist = pairwise_distances(instances,metric=distance)
    
  2. 让我们计算 k 距离。我们将使用heapq来获取 k 近邻:

    # Calculate K distance
    import heapq
    k_distance = defaultdict(tuple)
    # For each data point
    for i in range(instances.shape[0]):
        # Get its distance to all the other points.
        # Convert array into list for convienience
        distances = dist[i].tolist()
        # Get the K nearest neighbours
        ksmallest = heapq.nsmallest(k+1,distances)[1:][k-1]
        # Get their indices
        ksmallest_idx = distances.index(ksmallest)
        # For each data point store the K th nearest neighbour and its distance
        k_distance[i]=(ksmallest,ksmallest_idx)
    
  3. 计算 k 距离邻域:

    def all_indices(value, inlist):
        out_indices = []
        idx = -1
        while True:
            try:
                idx = inlist.index(value, idx+1)
                out_indices.append(idx)
            except ValueError:
                break
        return out_indices
    # Calculate K distance neighbourhood
    import heapq
    k_distance_neig = defaultdict(list)
    # For each data point
    for i in range(instances.shape[0]):
        # Get the points distances to its neighbours
        distances = dist[i].tolist()
        print "k distance neighbourhood",i
        print distances
        # Get the 1 to K nearest neighbours
        ksmallest = heapq.nsmallest(k+1,distances)[1:]
        print ksmallest
        ksmallest_set = set(ksmallest)
        print ksmallest_set
        ksmallest_idx = []
        # Get the indices of the K smallest elements
        for x in ksmallest_set:
                ksmallest_idx.append(all_indices(x,distances))
        # Change a list of list to list
        ksmallest_idx = [item for sublist in ksmallest_idx for item in sublist]
        # For each data pont store the K distance neighbourhood
        k_distance_neig[i].extend(zip(ksmallest,ksmallest_idx))
    
  4. 然后,计算可达距离和 LRD:

    #Local reachable density
    local_reach_density = defaultdict(float)
    for i in range(instances.shape[0]):
        # LRDs numerator, number of K distance neighbourhood
        no_neighbours = len(k_distance_neig[i])
        denom_sum = 0
        # Reachability distance sum
        for neigh in k_distance_neig[i]:
            # maximum(K-Distance(P), Distance(P,Q))
            denom_sum+=max(k_distance[neigh[1]][0],neigh[0])
        local_reach_density[i] = no_neighbours/(1.0*denom_sum)
    
  5. 计算 LOF:

    lof_list =[]
    #Local Outlier Factor
    for i in range(instances.shape[0]):
        lrd_sum = 0
        rdist_sum = 0
        for neigh in k_distance_neig[i]:
            lrd_sum+=local_reach_density[neigh[1]]
            rdist_sum+=max(k_distance[neigh[1]][0],neigh[0])
        lof_list.append((i,lrd_sum*rdist_sum))
    

它是如何工作的……

在步骤 1 中,我们选择曼哈顿距离作为距离度量,k 值为 2。我们正在查看数据点的第二近邻。

然后我们必须计算我们的元组之间的成对距离。成对相似性存储在 dist 矩阵中。正如你所看到的,dist 的形状如下:

>>> dist.shape
(5, 5)
>>>

它是一个 5 X 5 的矩阵,其中行和列是各个元组,单元格的值表示它们之间的距离。

在步骤 2 中,我们导入heapq

import heapq

heapq是一种数据结构,也称为优先队列。它类似于常规队列,区别在于每个元素都有一个优先级,优先级高的元素会先于优先级低的元素被处理。

请参考维基百科链接获取有关优先队列的更多信息:

en.wikipedia.org/wiki/Priority_queue

Python 的 heapq 文档可以在docs.python.org/2/library/heapq.html找到。

k_distance = defaultdict(tuple)

接下来,我们定义一个字典,其中键是元组 ID,值是元组与其第 k 近邻的距离。在我们的情况下,应该是第二近邻。

然后,我们进入一个 for 循环,以便为每个数据点找到第 k 近邻的距离:

distances = dist[i].tolist()

从我们的距离矩阵中,我们提取第 i 行。正如你所看到的,第 i 行捕获了对象i与所有其他对象之间的距离。记住,单元格值(i,i)表示与自身的距离。我们需要在下一步中忽略这一点。我们必须将数组转换为列表以方便操作。让我们通过一个例子来理解这一点。距离矩阵如下所示:

>>> dist
array([[ 0.,  1.,  2.,  1.,  5.],
       [ 1.,  0.,  1.,  2.,  6.],
       [ 2.,  1.,  0.,  1.,  5.],
       [ 1.,  2.,  1.,  0.,  4.],
       [ 5.,  6.,  5.,  4.,  0.]]) 

假设我们处于 for 循环的第一次迭代中,因此我们的i=0。(记住,Python 的索引是从0开始的)。

所以,现在我们的距离列表将如下所示:

[ 0.,  1.,  2.,  1.,  5.]

从中,我们需要第 k 近邻,即第二近邻,因为我们在程序开始时将 K 设置为2

从中我们可以看到,索引 1 和索引 3 都可以作为我们的第 k 近邻,因为它们的值都是1

现在,我们使用heapq.nsmallest函数。记住,我们之前提到过,heapq是一个普通队列,但每个元素都有一个优先级。在这种情况下,元素的值即为优先级。当我们说给我 n 个最小值时,heapq会返回最小的元素:

# Get the Kth nearest neighbours
ksmallest = heapq.nsmallest(k+1,distances)[1:][k-1]

让我们看一下heapq.nsmallest函数的作用:

>>> help(heapq.nsmallest)
Help on function nsmallest in module heapq:

nsmallest(n, iterable, key=None)
    Find the n smallest elements in a dataset.

    Equivalent to:  sorted(iterable, key=key)[:n]

它返回给定数据集中的 n 个最小元素。在我们的例子中,我们需要第二近邻。此外,我们需要避免之前提到的(i,i)。因此,我们必须传递 n=3 给heapq.nsmallest。这确保它返回三个最小元素。然后,我们对列表进行子集操作,排除第一个元素(参见 nsmallest 函数调用后的[1:]),最终获取第二近邻(参见[1:]后的[k-1])。

我们还必须获取i的第二近邻的索引,并将其存储在字典中:

# Get their indices
ksmallest_idx = distances.index(ksmallest)
# For each data point store the K th nearest neighbour and its distance
k_distance[i]=(ksmallest,ksmallest_idx)

让我们打印出我们的字典:

print k_distance
defaultdict(<type 'tuple'>, {0: (1.0, 1), 1: (1.0, 0), 2: (1.0, 1), 3: (1.0, 0), 4: (5.0, 0)})

我们的元组有两个元素:距离和在距离数组中元素的索引。例如,对于0,第二近邻是索引为1的元素。

计算完所有数据点的 k 距离后,我们接下来找出 k 距离邻域。

在步骤 3 中,我们为每个数据点找到 k 距离邻域:

# Calculate K distance neighbourhood
import heapq
k_distance_neig = defaultdict(list)

与前一步类似,我们导入了 heapq 模块并声明了一个字典,用来保存我们的 k 距离邻域的详细信息。让我们回顾一下什么是 k 距离邻域:

P 的 k 距离邻域是所有与 P 的距离小于或等于 P 与其第 k 近邻之间距离的对象 Q 的列表:

distances = dist[i].tolist()
# Get the 1 to K nearest neighbours
ksmallest = heapq.nsmallest(k+1,distances)[1:]
ksmallest_set = set(ksmallest)

前两行应该对你很熟悉。我们在上一步已经做过了。看看第二行。在这里,我们再次调用了 n smallest,n=3(即 K+1),但我们选择了输出列表中的所有元素,除了第一个。(猜猜为什么?答案在前一步中)。

让我们通过打印值来查看其实际操作。像往常一样,在循环中,我们假设我们看到的是第一个数据点或元组,其中 i=0。

我们的距离列表如下所示:

[0.0, 1.0, 2.0, 1.0, 5.0]

我们的heapq.nsmallest函数返回如下:

[1.0, 1.0]

这些是 1 到 k 个最近邻的距离。我们需要找到它们的索引,简单的 list.index 函数只会返回第一个匹配项,因此我们将编写all_indices函数来检索所有索引:

def all_indices(value, inlist):
    out_indices = []
    idx = -1
    while True:
        try:
            idx = inlist.index(value, idx+1)
            out_indices.append(idx)
        except ValueError:
            break
    return out_indices

通过值和列表,all_indices将返回值在列表中出现的所有索引。我们必须将 k 个最小值转换为集合:

ksmallest_set = set(ksmallest)

所以,[1.0,1.0]变成了一个集合([1.0])。现在,使用 for 循环,我们可以找到所有元素的索引:

# Get the indices of the K smallest elements
for x in ksmallest_set:
ksmallest_idx.append(all_indices(x,distances))

我们为 1.0 得到两个索引;它们是 1 和 2:

ksmallest_idx = [item for sublist in ksmallest_idx for item in sublist]

下一个 for 循环用于将列表中的列表转换为列表。all_indices函数返回一个列表,我们然后将此列表添加到ksmallest_idx列表中。因此,我们通过下一个 for 循环将其展开。

最后,我们将 k 个最小的邻域添加到我们的字典中:

k_distance_neig[i].extend(zip(ksmallest,ksmallest_idx))

然后我们添加元组,其中元组中的第一个项是距离,第二个项是最近邻的索引。让我们打印 k 距离邻域字典:

defaultdict(<type 'list'>, {0: [(1.0, 1), (1.0, 3)], 1: [(1.0, 0), (1.0, 2)], 2: [(1.0, 1), (1.0, 3)], 3: [(1.0, 0), (1.0, 2)], 4: [(4.0, 3), (5.0, 0)]})

在步骤 4 中,我们计算 LRD。LRD 是通过可达距离来计算的。让我们回顾一下这两个定义:

  • 从 P 到 Q 的可达距离被定义为 P 与其第 k 个最近邻之间的距离和 P 与 Q 之间的距离的最大值。以下符号可能有助于澄清这一点:

    Reachability distance (P ß Q) = > maximum(K-Distance(P), Distance(P,Q))
    
  • P 的局部可达密度(LRD(P))是 P 的 k 距离邻域与 k 及其邻域的可达距离之和的比率:

    #Local reachable density
    local_reach_density = defaultdict(float)
    

我们首先声明一个字典来存储 LRD:

for i in range(instances.shape[0]):
# LRDs numerator, number of K distance neighbourhood
no_neighbours = len(k_distance_neig[i])
denom_sum = 0
# Reachability distance sum
for neigh in k_distance_neig[i]:
# maximum(K-Distance(P), Distance(P,Q))
denom_sum+=max(k_distance[neigh[1]][0],neigh[0])
   local_reach_density[i] = no_neighbours/(1.0*denom_sum)

对于每个点,我们将首先找到该点的 k 距离邻域。例如,对于 i = 0,分子将是 len(k_distance_neig[0]),即 2。

现在,在内部的 for 循环中,我们计算分母。然后我们计算每个 k 距离邻域点的可达距离。该比率存储在local_reach_density字典中。

最后,在步骤 5 中,我们计算每个点的 LOF:

for i in range(instances.shape[0]):
lrd_sum = 0
rdist_sum = 0
for neigh in k_distance_neig[i]:
lrd_sum+=local_reach_density[neigh[1]]
rdist_sum+=max(k_distance[neigh[1]][0],neigh[0])
lof_list.append((i,lrd_sum*rdist_sum))

对于每个数据点,我们计算其邻居的 LRD 之和和与其邻居的可达距离之和,并将它们相乘以得到 LOF。

LOF 值非常高的点被视为异常点。让我们打印lof_list

[(0, 4.0), (1, 4.0), (2, 4.0), (3, 4.0), (4, 18.0)]

正如你所见,最后一个点与其他点相比,LOF 非常高,因此,它是一个异常点。

还有更多……

为了更好地理解 LOF,你可以参考以下论文:

LOF:识别基于密度的局部异常点

Markus M. Breunig, Hans-Peter Kriegel, Raymond T. Ng, Jörg Sander

Proc. ACM SIGMOD 2000 国际数据管理会议,德克萨斯州达拉斯,2000 年

学习向量量化

在这个示例中,我们将看到一种无模型的方法来聚类数据点,称为学习向量量化,简称 LVQ。LVQ 可以用于分类任务。使用这种技术很难在目标变量和预测变量之间做出推理。与其他方法不同,很难弄清楚响应变量 Y 与预测变量 X 之间存在哪些关系。它们在许多现实世界的场景中作为黑箱方法非常有效。

准备工作

LVQ 是一种在线学习算法,其中数据点一次处理一个。它的直观理解非常简单。假设我们已经为数据集中不同的类别识别了原型向量。训练点会朝向相似类别的原型靠近,并会排斥其他类别的原型。

LVQ 的主要步骤如下:

为数据集中的每个类别选择 k 个初始原型向量。如果是二分类问题,我们决定为每个类别选择两个原型向量,那么我们将得到四个初始原型向量。初始原型向量是从输入数据集中随机选择的。

我们将开始迭代。我们的迭代将在 epsilon 值达到零或预定义阈值时结束。我们将决定一个 epsilon 值,并在每次迭代中递减 epsilon 值。

在每次迭代中,我们将对一个输入点进行抽样(有放回抽样),并找到离该点最近的原型向量。我们将使用欧几里得距离来找到最近的点。我们将如下更新最近点的原型向量:

如果原型向量的类别标签与输入数据点相同,我们将通过原型向量与数据点之间的差值来增大原型向量。

如果类别标签不同,我们将通过原型向量与数据点之间的差值来减少原型向量。

我们将使用 Iris 数据集来演示 LVQ 的工作原理。如同我们之前的一些示例,我们将使用 scikit-learn 的方便数据加载函数来加载 Iris 数据集。Iris 是一个广为人知的分类数据集。然而我们在这里使用它的目的是仅仅为了演示 LVQ 的能力。没有类别标签的数据集也可以被 LVQ 使用或处理。由于我们将使用欧几里得距离,因此我们将使用 minmax 缩放来对数据进行缩放。

from sklearn.datasets import load_iris
import numpy as np
from sklearn.metrics import euclidean_distances

data = load_iris()
x = data['data']
y = data['target']

# Scale the variables
from sklearn.preprocessing import MinMaxScaler
minmax = MinMaxScaler()
x = minmax.fit_transform(x)

如何实现…

  1. 让我们首先声明 LVQ 的参数:

    R = 2
    n_classes = 3
    epsilon = 0.9
    epsilon_dec_factor = 0.001
    
  2. 定义一个类来保存原型向量:

    class prototype(object):
        """
        Class to hold prototype vectors
        """
    
        def __init__(self,class_id,p_vector,eplsilon):
            self.class_id = class_id
            self.p_vector = p_vector
            self.epsilon = epsilon
    
        def update(self,u_vector,increment=True):
            if increment:
                # Move the prototype vector closer to input vector
                self.p_vector = self.p_vector + self.epsilon*(u_vector - self.p_vector)
            else:
                # Move the prototype vector away from input vector
                self.p_vector = self.p_vector - self.epsilon*(u_vector - self.p_vector)
    
  3. 这是一个用于找到给定向量最接近的原型向量的函数:

    def find_closest(in_vector,proto_vectors):
        closest = None
        closest_distance = 99999
        for p_v in proto_vectors:
            distance = euclidean_distances(in_vector,p_v.p_vector)
            if distance < closest_distance:
                closest_distance = distance
                closest = p_v
        return closest
    
  4. 一个方便的函数,用来找到最接近的原型向量的类别 ID 如下:

    def find_class_id(test_vector,p_vectors):
        return find_closest(test_vector,p_vectors).class_id
    
  5. 选择初始的 K * 类别数的原型向量:

    # Choose R initial prototypes for each class        
    p_vectors = []
    for i in range(n_classes):
        # Select a class
        y_subset = np.where(y == i)
        # Select tuples for choosen class
        x_subset  = x[y_subset]
        # Get R random indices between 0 and 50
        samples = np.random.randint(0,len(x_subset),R)
        # Select p_vectors
        for sample in samples:
            s = x_subset[sample]
            p = prototype(i,s,epsilon)
            p_vectors.append(p)
    
    print "class id \t Initial protype vector\n"
    for p_v in p_vectors:
        print p_v.class_id,'\t',p_v.p_vector
           print
    
  6. 进行迭代调整原型向量,以便使用现有的数据点对任何新的输入点进行分类/聚类:

    while epsilon >= 0.01:
        # Sample a training instance randonly
        rnd_i = np.random.randint(0,149)
        rnd_s = x[rnd_i]
        target_y = y[rnd_i]
    
        # Decrement epsilon value for next iteration
        epsilon = epsilon - epsilon_dec_factor    
        # Find closes prototype vector to given point
        closest_pvector = find_closest(rnd_s,p_vectors)
    
        # Update closes prototype vector
        if target_y == closest_pvector.class_id:
            closest_pvector.update(rnd_s)
        else:
            closest_pvector.update(rnd_s,False)
        closest_pvector.epsilon = epsilon
    
    print "class id \t Final Prototype Vector\n"
    for p_vector in p_vectors:
        print p_vector.class_id,'\t',p_vector.p_vector
    
  7. 以下是一个小的测试,用来验证我们方法的正确性:

    predicted_y = [find_class_id(instance,p_vectors) for instance in x ]
    
    from sklearn.metrics import classification_report
    
    print
    print classification_report(y,predicted_y,target_names=['Iris-Setosa','Iris-Versicolour', 'Iris-Virginica'])
    

它是如何工作的…

在第 1 步中,我们初始化了算法的参数。我们选择了 R 值为 2,也就是说,每个类别标签有两个原型向量。Iris 数据集是一个三类问题,所以我们一共有六个原型向量。我们必须选择我们的 epsilon 值和 epsilon 减小因子。

然后我们在第 2 步中定义了一个数据结构,用来存储每个原型向量的详细信息。我们的类为数据集中每个点存储以下内容:

self.class_id = class_id
self.p_vector = p_vector
self.epsilon = epsilon

原型向量所属的类别 ID 就是向量本身和 epsilon 值。它还有一个 update 函数,用于更改原型值:

def update(self,u_vector,increment=True):
if increment:
# Move the prototype vector closer to input vector
self.p_vector = self.p_vector + self.epsilon*(u_vector - self.p_vector)
else:
# Move the prototype vector away from input vector
self.p_vector = self.p_vector - self.epsilon*(u_vector - self.p_vector)

在第 3 步中,我们定义了以下函数,该函数以任意给定的向量作为输入,并接受所有原型向量的列表。在所有原型向量中,函数返回与给定向量最接近的原型向量:

for p_v in proto_vectors:
distance = euclidean_distances(in_vector,p_v.p_vector)
if distance < closest_distance:
closest_distance = distance
closest = p_v

正如你所见,它会遍历所有的原型向量,找出最接近的一个。它使用欧几里得距离来衡量相似度。

第 4 步是一个小函数,可以返回与给定向量最接近的原型向量的类别 ID。

现在我们已经完成了 LVQ 算法所需的所有预处理,可以进入第 5 步的实际算法。对于每个类别,我们必须选择初始的原型向量。然后,我们从每个类别中选择 R 个随机点。外部循环遍历每个类别,对于每个类别,我们选择 R 个随机样本并创建我们的原型对象,具体如下:

samples = np.random.randint(0,len(x_subset),R)
# Select p_vectors
for sample in samples:
s = x_subset[sample]
p = prototype(i,s,epsilon)
p_vectors.append(p)

在第 6 步中,我们迭代地增加或减少原型向量。我们会持续循环,直到 epsilon 值降到 0.01 以下。

然后我们从数据集中随机采样一个点,具体如下:

# Sample a training instance randonly
rnd_i = np.random.randint(0,149)
rnd_s = x[rnd_i]
target_y = y[rnd_i]

点和它对应的类别 ID 已被提取。

然后我们可以找到最接近这个点的原型向量,具体如下:

closest_pvector = find_closest(rnd_s,p_vectors)

如果当前点的类别 ID 与原型的类别 ID 匹配,我们会调用 update 方法,增量设置为 True,否则我们将调用 update 方法,增量设置为 False

# Update closes prototype vector
if target_y == closest_pvector.class_id:
closest_pvector.update(rnd_s)
else:
closest_pvector.update(rnd_s,False)

最后,我们更新最接近的原型向量的 epsilon 值:

closest_pvector.epsilon = epsilon

我们可以打印出原型向量,以便手动查看:

print "class id \t Final Prototype Vector\n"
for p_vector in p_vectors:
print p_vector.class_id,'\t',p_vector.p_vector

在第 7 步中,我们将原型向量投入实际应用,进行预测:

predicted_y = [find_class_id(instance,p_vectors) for instance in x ]

我们可以使用 find_class_id 函数获得预测的类别 ID。我们将一个点和所有已学习的原型向量传递给它,以获取类别 ID。

最后,我们给出我们的预测输出,以生成分类报告:

print classification_report(y,predicted_y,target_names=['Iris-Setosa','Iris-Versicolour', 'Iris-Virginica'])

分类报告函数是由 scikit-learn 库提供的一个便捷函数,用于查看分类准确度评分:

它是如何工作的…

你可以看到我们在分类方面做得相当不错。请记住,我们并没有保留单独的测试集。绝不要根据训练数据来衡量模型的准确性。始终使用训练过程中未见过的测试集。我们这么做仅仅是为了说明。

还有更多...

请记住,这种技术不像其他分类方法那样涉及任何优化准则。因此,很难判断原型向量生成的效果如何。

在我们的配方中,我们将原型向量初始化为随机值。你也可以使用 K-Means 算法来初始化原型向量。

另见

  • 使用 K-Means 聚类数据 配方见 第五章, 数据挖掘——大海捞针

查找单变量数据中的异常值

异常值是与数据集中其他数据点相距较远的数据点。在数据科学应用中,它们必须小心处理。无意中将它们包含在某些算法中可能导致错误的结果或结论。正确处理异常值并使用合适的算法来应对它们是非常重要的。

“异常值检测是一个极其重要的问题,直接应用于许多领域,包括欺诈检测(Bolton, 2002)、识别计算机网络入侵和瓶颈(Lane, 1999)、电子商务中的犯罪活动和检测可疑活动(Chiu, 2003)。”
--- Jayakumar 和 Thomas, 基于多变量异常值检测的聚类新程序(《数据科学期刊》11(2013), 69-84)

在本配方中,我们将查看如何检测单变量数据中的异常值,然后转向查看多变量数据和文本数据中的异常值。

准备就绪

在本配方中,我们将查看以下三种单变量数据异常值检测方法:

  • 中位数绝对偏差

  • 均值加减三个标准差

让我们看看如何利用这些方法来发现单变量数据中的异常值。在进入下一节之前,我们先创建一个包含异常值的数据集,以便我们可以通过经验评估我们的方法:

import numpy as np
import matplotlib.pyplot as plt

n_samples = 100
fraction_of_outliers = 0.1
number_inliers = int ( (1-fraction_of_outliers) * n_samples )
number_outliers = n_samples - number_inliers

我们将创建 100 个数据点,其中 10% 会是异常值:

# Get some samples from a normal distribution
normal_data = np.random.randn(number_inliers,1)

我们将在 NumPy 的 random 模块中使用 randn 函数生成我们的正常值数据。这将是一个均值为零,标准差为一的分布样本。让我们验证一下我们样本的均值和标准差:

# Print the mean and standard deviation
# to confirm the normality of our input data.
mean = np.mean(normal_data,axis=0)
std = np.std(normal_data,axis=0)
print "Mean =(%0.2f) and Standard Deviation (%0.2f)"%(mean[0],std[0])

我们将使用 NumPy 中的函数来计算均值和标准差,并打印输出。让我们查看输出结果:

Mean =(0.24) and Standard Deviation (0.90)

正如你所看到的,均值接近零,标准差接近一。

现在,让我们创建异常值数据。这将是整个数据集的 10%,即 10 个点,假设我们的样本大小是 100。正如你所看到的,我们从 -9 到 9 之间的均匀分布中采样了异常值。这个范围内的任何点都有相等的被选择的机会。我们将合并正常值和异常值数据。在运行异常值检测程序之前,最好通过散点图查看数据:

# Create outlier data
outlier_data = np.random.uniform(low=-9,high=9,size=(number_outliers,1))
total_data = np.r_[normal_data,outlier_data]
print "Size of input data = (%d,%d)"%(total_data.shape)
# Eyeball the data
plt.cla()
plt.figure(1)
plt.title("Input points")
plt.scatter(range(len(total_data)),total_data,c='b')

让我们来看一下生成的图形:

准备就绪

我们的y轴是我们生成的实际值,x轴是一个累积计数。您可以做一个练习,标记您认为是异常值的点。稍后我们可以将程序输出与您的手动选择进行比较。

如何操作…

  1. 让我们从中位数绝对偏差开始。然后我们将绘制我们的值,并将异常值标记为红色:

    # Median Absolute Deviation
    median = np.median(total_data)
    b = 1.4826
    mad = b * np.median(np.abs(total_data - median))
    outliers = []
    # Useful while plotting
    outlier_index = []
    print "Median absolute Deviation = %.2f"%(mad)
    lower_limit = median - (3*mad)
    upper_limit = median + (3*mad)
    print "Lower limit = %0.2f, Upper limit = %0.2f"%(lower_limit,upper_limit)
    for i in range(len(total_data)):
        if total_data[i] > upper_limit or total_data[i] < lower_limit:
            print "Outlier %0.2f"%(total_data[i])
            outliers.append(total_data[i])
            outlier_index.append(i)
    
    plt.figure(2)
    plt.title("Outliers using mad")
    plt.scatter(range(len(total_data)),total_data,c='b')
    plt.scatter(outlier_index,outliers,c='r')
    plt.show()
    
  2. 继续进行均值加减三倍标准差的操作,我们将绘制我们的值,并将异常值标记为红色:

    # Standard deviation
    std = np.std(total_data)
    mean = np.mean(total_data)
    b = 3
    outliers = []
    outlier_index = []
    lower_limt = mean-b*std
    upper_limt = mean+b*std
    print "Lower limit = %0.2f, Upper limit = %0.2f"%(lower_limit,upper_limit)
    for i in range(len(total_data)):
        x = total_data[i]
        if x > upper_limit or x < lower_limt:
            print "Outlier %0.2f"%(total_data[i])
            outliers.append(total_data[i])
            outlier_index.append(i)
    
    plt.figure(3)
    plt.title("Outliers using std")
    plt.scatter(range(len(total_data)),total_data,c='b')
    plt.scatter(outlier_index,outliers,c='r')
    plt.savefig("B04041 04 10.png")
    plt.show()
    

它是如何工作的…

在第一步中,我们使用中位数绝对偏差来检测数据中的异常值:

median = np.median(total_data)
b = 1.4826
mad = b * np.median(np.abs(total_data - median))

我们首先使用 NumPy 中的中位数函数计算数据集的中位数值。接着,我们声明一个值为 1.4826 的变量。这个常数将与偏离中位数的绝对偏差相乘。最后,我们计算每个数据点相对于中位数值的绝对偏差的中位数,并将其乘以常数 b。

任何偏离中位数绝对偏差三倍以上的点都被视为我们方法中的异常值:

lower_limit = median - (3*mad)
upper_limit = median + (3*mad)

print "Lower limit = %0.2f, Upper limit = %0.2f"%(lower_limit,upper_limit)

然后,我们计算了中位数绝对偏差的上下限,如前所示,并将每个点分类为异常值或正常值,具体如下:

for i in range(len(total_data)):
if total_data[i] > upper_limit or total_data[i] < lower_limit:
print "Outlier %0.2f"%(total_data[i])
outliers.append(total_data[i])
outlier_index.append(i)

最后,所有异常值都存储在名为 outliers 的列表中。我们还必须将异常值的索引存储在一个名为 outlier_index 的单独列表中。这是为了方便绘图,您将在下一步中看到这一点。

然后我们绘制原始点和异常值。绘图结果如下所示:

它是如何工作的…

被红色标记的点被算法判定为异常值。

在第三步中,我们编写第二个算法,即均值加减三倍标准差:

std = np.std(total_data)
mean = np.mean(total_data)
b = 3

我们接着计算数据集的标准差和均值。在这里,您可以看到我们设置了b = 3。正如我们算法的名字所示,我们需要一个标准差为三,而这个 b 也正是用来实现这个目标的:

lower_limt = mean-b*std
upper_limt = mean+b*std

print "Lower limit = %0.2f, Upper limit = %0.2f"%(lower_limit,upper_limit)

for i in range(len(total_data)):
x = total_data[i]
if x > upper_limit or x < lower_limt:
print "Outlier %0.2f"%(total_data[i])
outliers.append(total_data[i])
outlier_index.append(i)

我们可以计算出上下限,方法是将均值减去三倍标准差。使用这些值,我们可以在 for 循环中将每个点分类为异常值或正常值。然后我们将所有异常值及其索引添加到两个列表中,outliers 和 outlier_index,以便绘图。

最后,我们绘制了异常值:

它是如何工作的…

还有更多内容…

根据异常值的定义,给定数据集中的异常值是那些与其他数据点距离较远的点。数据集中心的估算值和数据集分布的估算值可以用来检测异常值。在我们在本食谱中概述的方法中,我们使用均值和中位数作为数据中心的估算值,使用标准差和中位数绝对偏差作为分布的估算值。分布也被称为尺度。

让我们稍微理清一下我们的检测异常值方法为何有效。首先从使用标准差的方法开始。对于高斯数据,我们知道 68.27% 的数据位于一个标准差内,95.45% 位于两个标准差内,99.73% 位于三个标准差内。因此,根据我们的规则,任何与均值的距离超过三个标准差的点都被归类为异常值。然而,这种方法并不稳健。我们来看一个小例子。

让我们从正态分布中抽取八个数据点,均值为零,标准差为一。

让我们使用 NumPy.random 中的便捷函数来生成我们的数据:

np.random.randn(8)

这给出了以下数值:

-1.76334861, -0.75817064,  0.44468944, -0.07724717,  0.12951944,0.43096092, -0.05436724, -0.23719402

现在我们手动添加两个异常值,例如 45 和 69,加入到这个列表中。

我们的数据集现在看起来如下:

-1.763348607322289, -0.7581706357821458, 0.4446894368956213, -0.07724717210195432, 0.1295194428816003, 0.4309609200681169, -0.05436724238743103, -0.23719402072058543, 45, 69

前面数据集的均值是 11.211,标准差是 23.523

我们来看一下上限规则,均值 + 3 * 标准差。这是 11.211 + 3 * 23.523 = 81.78。

现在,根据这个上限规则,45 和 69 两个点都不是异常值!均值和标准差是数据集中心和尺度的非稳健估计量,因为它们对异常值非常敏感。如果我们用一个极端值替换数据集中任意一个点(样本量为 n),这将完全改变均值和标准差的估计值。估计量的这一特性被称为有限样本断点。

注意

有限样本断点被定义为,在样本中,能被替换的观察值比例,替换后估计器仍然能准确描述数据。

因此,对于均值和标准差,有限样本断点是 0%,因为在大样本中,替换哪怕一个点也会显著改变估计值。

相比之下,中位数是一个更稳健的估计值。中位数是有限观察集合中按升序排序后的中间值。为了使中位数发生显著变化,我们必须替换掉数据中远离中位数的一半观察值。这就给出了中位数的 50% 有限样本断点。

中位绝对偏差法归因于以下论文:

Leys, C., 等,检测异常值:不要使用均值周围的标准差,使用中位数周围的绝对偏差,《实验社会心理学杂志》(2013), dx.doi.org/10.1016/j.jesp.2013.03.013

另见

  • 第一章中的 执行汇总统计和绘图 配方,使用 Python 进行数据科学

第六章 机器学习 1

本章我们将涵盖以下主题:

  • 为模型构建准备数据

  • 查找最近邻

  • 使用朴素贝叶斯对文档进行分类

  • 构建决策树解决多类别问题

介绍

本章我们将探讨监督学习技术。在上一章中,我们介绍了包括聚类和学习向量量化在内的无监督技术。我们将从一个分类问题开始,然后转向回归问题。分类问题的输入是在下一章中的一组记录或实例。

每条记录或实例可以写作一个集合(X,y),其中 X 是属性集,y 是对应的类别标签。

学习一个目标函数 F,该函数将每条记录的属性集映射到预定义的类别标签 y,是分类算法的任务。

分类算法的一般步骤如下:

  1. 寻找合适的算法

  2. 使用训练集学习模型,并使用测试集验证模型

  3. 应用模型预测任何未见过的实例或记录

第一步是确定正确的分类算法。没有固定的方法来选择合适的算法,这需要通过反复的试错过程来完成。选择算法后,创建训练集和测试集,并将其提供给算法,以学习一个模型,也就是前面定义的目标函数 F。通过训练集创建模型后,使用测试集来验证模型。通常,我们使用混淆矩阵来验证模型。我们将在我们的食谱中进一步讨论混淆矩阵:查找最近邻。

我们将从一个食谱开始,展示如何将输入数据集划分为训练集和测试集。接下来我们将介绍一种懒惰学习者算法用于分类,称为 K-最近邻算法。然后我们将看一下朴素贝叶斯分类器。接下来我们将探讨一个使用决策树处理多类别问题的食谱。本章我们选择的算法并非随意选择。我们将在本章中介绍的三种算法都能够处理多类别问题,除了二分类问题。在多类别问题中,我们有多个类别标签,实例可以属于这些类别中的任意一个。

为模型构建准备数据

在这个示例中,我们将展示如何从给定的数据集中创建训练集和测试集用于分类问题。测试数据集永远不会被展示给模型。在实际应用中,我们通常会建立另一个名为 dev 的数据集。Dev 代表开发数据集:我们可以用它在模型的连续运行过程中持续调优模型。模型使用训练集进行训练,模型的性能度量,如准确率,则在 dev 数据集上进行评估。根据这些结果,如果需要改进,模型将进一步调优。在后续章节中,我们将介绍一些比简单的训练测试集拆分更复杂的数据拆分方法。

准备工作

我们将使用鸢尾花数据集来进行这个示例。由于我们已经在许多之前的示例中使用过它,所以使用这个数据集展示概念非常简单。

如何实现…

# Load the necesssary Library
from sklearn.cross_validation import train_test_split
from sklearn.datasets import load_iris
import numpy as np

def get_iris_data():
    """
    Returns Iris dataset
    """
    # Load iris dataset
    data = load_iris()

    # Extract the dependend and independent variables
    # y is our class label
    # x is our instances/records
    x    = data['data']
    y    = data['target']

    # For ease we merge them
    # column merge
    input_dataset = np.column_stack([x,y])

    # Let us shuffle the dataset
    # We want records distributed randomly
    # between our test and train set

    np.random.shuffle(input_dataset)

    return input_dataset

# We need  80/20 split.
# 80% of our records for Training
# 20% Remaining for our Test set
train_size = 0.8
test_size  = 1-train_size

# get the data
input_dataset = get_iris_data()
# Split the data
train,test = train_test_split(input_dataset,test_size=test_size)

# Print the size of original dataset
print "Dataset size ",input_dataset.shape
# Print the train/test split
print "Train size ",train.shape
print "Test  size",test.shape

这很简单。让我们检查一下类别标签是否在训练集和测试集之间按比例分配。这是一个典型的类别不平衡问题:

def get_class_distribution(y):

"""
Given an array of class labels
Return the class distribution
"""
    distribution = {}
    set_y = set(y)
    for y_label in set_y:
        no_elements = len(np.where(y == y_label)[0])
        distribution[y_label] = no_elements
    dist_percentage = {class_label: count/(1.0*sum(distribution.values())) for class_label,count in distribution.items()}
    return dist_percentage

def print_class_label_split(train,test):
  """
  Print the class distribution
  in test and train dataset
  """  
    y_train = train[:,-1]

    train_distribution = get_class_distribution(y_train)
    print "\nTrain data set class label distribution"
    print "=========================================\n"
    for k,v in train_distribution.items():
        print "Class label =%d, percentage records =%.2f"%(k,v)

    y_test = test[:,-1]    

    test_distribution = get_class_distribution(y_test)

    print "\nTest data set class label distribution"
    print "=========================================\n"

    for k,v in test_distribution.items():
        print "Class label =%d, percentage records =%.2f"%(k,v)

print_class_label_split(train,test)

让我们看看如何将类别标签均匀分配到训练集和测试集中:

# Perform Split the data
stratified_split = StratifiedShuffleSplit(input_dataset[:,-1],test_size=test_size,n_iter=1)

for train_indx,test_indx in stratified_split:
    train = input_dataset[train_indx]
    test =  input_dataset[test_indx]
    print_class_label_split(train,test)

如何工作…

在我们导入必要的库模块后,必须编写一个方便的函数get_iris_data(),该函数将返回鸢尾花数据集。然后,我们将xy数组按列连接成一个名为input_dataset的单一数组。接着,我们对数据集进行打乱,以便记录能够随机分配到测试集和训练集。该函数返回一个包含实例和类别标签的单一数组。

我们希望将 80%的记录包含在训练数据集中,剩余的用作测试数据集。train_sizetest_size变量分别表示应该分配到训练集和测试集中的百分比值。

我们必须调用get_iris_data()函数以获取输入数据。然后,我们利用 scikit-learn 的cross_validation模块中的train_test_split函数将输入数据集拆分成两部分。

最后,我们可以打印原始数据集的大小,接着是测试集和训练集的大小:

如何工作…

我们的原始数据集有 150 行和五列。记住,只有四个属性;第五列是类别标签。我们已经将xy按列连接。

正如你所看到的,150 行数据中的 80%(即 120 条记录)已被分配到我们的训练集中。我们已经展示了如何轻松地将输入数据拆分为训练集和测试集。

记住,这是一个分类问题。算法应该被训练来预测给定未知实例或记录的正确类别标签。为此,我们需要在训练期间为算法提供所有类别的均匀分布。鸢尾花数据集是一个三类问题,我们应该确保所有三类都有相等的代表性。让我们看看我们的方法是否已经解决了这个问题。

我们必须定义一个名为get_class_distribution的函数,该函数接受一个单一的y参数(类别标签数组)。此函数返回一个字典,其中键是类别标签,值是该分布记录的百分比。因此,字典提供了类别标签的分布情况。我们必须在以下函数中调用此函数,以了解训练集和测试集中的类别分布。

print_class_label_split函数不言自明。我们必须将训练集和测试集作为参数传递。由于我们已经将xy连接在一起,最后一列就是我们的类别标签。然后,我们提取y_trainy_test中的训练集和测试集类别标签。我们将它们传递给get_class_distribution函数,以获取类别标签及其分布的字典,最后打印出来。

最后,我们可以调用print_class_label_split,我们的输出应该如下所示:

它是如何工作的…

现在让我们查看输出结果。如您所见,我们的训练集和测试集中的类别标签分布不同。测试集中正好 40%的实例属于class label 1。这不是正确的分割方式。我们应该在训练集和测试集之间拥有相等的分布。

在最后一段代码中,我们使用了来自 scikit-learn 的StratifiedShuffleSplit,以实现训练集和测试集中类别的均匀分布。让我们查看StratifiedShuffleSplit的参数:

stratified_split = StratifiedShuffleSplit(input_dataset[:,-1],test_size=test_size,n_iter=1)

第一个参数是输入数据集。我们传递所有行和最后一列。我们的测试集大小由我们最初声明的test_size变量定义。我们可以假设仅使用n_iter变量进行一次分割。然后,我们继续调用print_class_label_split函数打印类别标签的分布情况。让我们查看输出结果:

它是如何工作的…

现在,我们已经在测试集和训练集之间均匀分布了类别标签。

还有更多...

在使用机器学习算法之前,我们需要仔细准备数据。为训练集和测试集提供均匀的类别分布是构建成功分类模型的关键。

在实际的机器学习场景中,除了训练集和测试集外,我们还会创建另一个称为开发集(dev set)的数据集。我们可能在第一次迭代中无法正确构建模型。我们不希望将测试数据集展示给我们的模型,因为这可能会影响我们下一次模型构建的结果。因此,我们创建了这个开发集,可以在迭代模型构建过程中使用。

我们在本节中指定的 80/20 法则是一个理想化的场景。然而,在许多实际应用中,我们可能没有足够的数据来留下如此多的实例作为测试集。在这种情况下,一些实用技术,如交叉验证,便派上用场。在下一章中,我们将探讨各种交叉验证技术。

查找最近邻

在我们进入具体步骤之前,让我们先花些时间了解如何检查我们的分类模型是否达到预期的表现。在介绍部分,我们提到过一个术语叫做混淆矩阵。

混淆矩阵是实际标签与预测标签的矩阵排列。假设我们有一个二分类问题,即y可以取值为TF。假设我们训练了一个分类器来预测y。在我们的测试数据中,我们知道y的实际值,同时我们也有模型预测的y值。我们可以按照以下方式填充混淆矩阵:

查找最近邻

这里是一个表格,我们方便地列出了来自测试集的结果。记住,我们知道测试集中的类别标签,因此我们可以将分类模型的输出与实际类别标签进行比较。

  • TP(真正例)下,我们统计的是测试集中所有标签为T的记录,且模型也预测为T的记录数。

  • FN(假阴性)的情况下,我们统计的是所有实际标签为T,但算法预测为 N 的记录数。

  • FP表示假阳性,即实际标签为F,但算法预测为T

  • TN表示真阴性,即算法预测的标签和实际类别标签都为F

通过了解这个混淆矩阵,我们现在可以推导出一些性能指标,用于衡量我们分类模型的质量。在未来的章节中,我们将探索更多的指标,但现在我们将介绍准确率和错误率。

准确率定义为正确预测与总预测次数的比率。从混淆矩阵中,我们可以知道 TP 和 TN 的总和是正确预测的总数:

查找最近邻

从训练集得出的准确率通常过于乐观。应该关注测试集上的准确率值,以确定模型的真实表现。

掌握了这些知识后,让我们进入我们的示例。我们首先要看的分类算法是 K-最近邻,简称 KNN。在详细讲解 KNN 之前,让我们先看看一个非常简单的分类算法,叫做死记硬背分类算法。死记硬背分类器会记住整个训练数据,也就是说,它将所有数据加载到内存中。当我们需要对一个新的训练实例进行分类时,它会尝试将新的训练实例与内存中的任何训练实例进行匹配。它会将测试实例的每个属性与训练实例中的每个属性进行匹配。如果找到匹配项,它会将测试实例的类别标签预测为匹配的训练实例的类别标签。

你现在应该知道,如果测试实例与加载到内存中的任何训练实例不相似,这个分类器会失败。

KNN 类似于死记硬背分类器,不同之处在于它不是寻找完全匹配,而是使用相似度度量。与死记硬背分类器类似,KNN 将所有训练集加载到内存中。当它需要对一个测试实例进行分类时,它会计算测试实例与所有训练实例之间的距离。利用这个距离,它会选择训练集中 K 个最接近的实例。现在,测试集的预测是基于 K 个最近邻的多数类别。

例如,如果我们有一个二分类问题,并且选择我们的 K 值为 3,那么如果给定的测试记录的三个最近邻类别分别为 1、1 和 0,它将把测试实例分类为 1,也就是多数类别。

KNN 属于一种叫做基于实例学习的算法家族。此外,由于对测试实例的分类决定是在最后做出的,因此它也被称为懒学习者。

准备中

对于这个示例,我们将使用 scikit 的 make_classification 方法生成一些数据。我们将生成一个包含四列/属性/特征和 100 个实例的矩阵:

from sklearn.datasets import make_classification

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

from sklearn.ensemble import BaggingClassifier
from sklearn.neighbors import KNeighborsClassifier

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    x,y = make_classification(n_features=4)
    return x,y

def plot_data(x,y):
    """
    Plot a scatter plot fo all variable combinations
    """
    subplot_start = 321
    col_numbers = range(0,4)
    col_pairs = itertools.combinations(col_numbers,2)

    for col_pair in col_pairs:
        plt.subplot(subplot_start)
        plt.scatter(x[:,col_pair[0]],x[:,col_pair[1]],c=y)
        title_string = str(col_pair[0]) + "-" + str(col_pair[1])
        plt.title(title_string)
        x_label = str(col_pair[0])
        y_label = str(col_pair[1])
        plt.xlabel(x_label)
        plt.xlabel(y_label)
        subplot_start+=1

    plt.show()

x,y = get_data()    
plot_data(x,y)

get_data 函数内部调用 make_classification 来生成任何分类任务的测试数据。

在将数据输入任何算法之前,最好先对数据进行可视化。我们的 plot_data 函数会生成所有变量之间的散点图:

准备中

我们已经绘制了所有变量的组合。顶部的两个图表显示了第 0 列与第 1 列之间的组合,接着是第 0 列与第 2 列的组合。数据点也按其类别标签上色。这展示了这些变量组合在进行分类任务时提供了多少信息。

如何做…

我们将数据集准备和模型训练分成两个不同的方法:get_train_test 用来获取训练和测试数据,build_model 用来构建模型。最后,我们将使用 test_model 来验证我们模型的有效性:

from sklearn.cross_validation import StratifiedShuffleSplit
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report

def get_train_test(x,y):
    """
    Perpare a stratified train and test split
    """
    train_size = 0.8
    test_size = 1-train_size
    input_dataset = np.column_stack([x,y])
    stratified_split = StratifiedShuffleSplit(input_dataset[:,-1],test_size=test_size,n_iter=1)

    for train_indx,test_indx in stratified_split:
        train_x = input_dataset[train_indx,:-1]
        train_y = input_dataset[train_indx,-1]
        test_x =  input_dataset[test_indx,:-1]
        test_y = input_dataset[test_indx,-1]
    return train_x,train_y,test_x,test_y

def build_model(x,y,k=2):
    """
    Fit a nearest neighbour model
    """
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(x,y)
    return knn

def test_model(x,y,knn_model):
    y_predicted = knn_model.predict(x)
    print classification_report(y,y_predicted)

if __name__ == "__main__":

    # Load the data    
    x,y = get_data()

    # Scatter plot the data
    plot_data(x,y)

    # Split the data into train and test    
    train_x,train_y,test_x,test_y = get_train_test(x,y)

    # Build the model
    knn_model = build_model(train_x,train_y)

    # Test the model
    print "\nModel evaluation on training set"
    print "================================\n"
    test_model(train_x,train_y,knn_model)

    print "\nModel evaluation on test set"
    print "================================\n"
    test_model(test_x,test_y,knn_model)

如何运作…

让我们尝试遵循主方法中的代码。我们必须从调用get_data并使用plot_data进行绘图开始,如前一部分所述。

如前所述,我们需要将一部分训练数据分离出来用于测试,以评估我们的模型。然后,我们调用get_train_test方法来完成这一操作。

get_train_test中,我们决定了训练测试集的分割比例,标准为 80/20。然后,我们使用 80%的数据来训练我们的模型。现在,我们在拆分之前,使用 NumPy 的column_stack方法将 x 和 y 合并为一个矩阵。

然后,我们利用在前面配方中讨论的StratifiedShuffleSplit,以便在训练集和测试集之间获得均匀的类标签分布。

凭借我们的训练集和测试集,我们现在准备好构建分类器了。我们必须使用我们的训练集、属性x和类标签y来调用构建模型函数。该函数还接收K作为参数,表示邻居的数量,默认值为二。

我们使用了 scikit-learn 的 KNN 实现,KNeighborsClassifier。然后,我们创建分类器对象并调用fit方法来构建我们的模型。

我们准备好使用训练数据来测试模型的表现了。我们可以将训练数据(x 和 y)以及模型传递给test_model函数。

我们知道我们的实际类标签(y)。然后,我们调用预测函数,使用我们的 x 来获得预测标签。接着,我们打印出一些模型评估指标。我们可以从打印模型的准确率开始,接着是混淆矩阵,最后展示一个名为classification_report的函数的输出。scikit-learn 的 metrics 模块提供了一个名为classification_report的函数,可以打印出多个模型评估指标。

让我们来看看我们的模型指标:

How it works…

如你所见,我们的准确率得分是 91.25%。我们不会重复准确率的定义,你可以参考介绍部分。

现在让我们来看一下混淆矩阵。左上角的单元格是正确正类单元格。我们可以看到没有假负类,但有七个假正类(第二行的第一个单元格)。

最后,我们在分类报告中得到了精确度、召回率、F1 分数和支持度。让我们来看看它们的定义:

精确度是正确正类与正确正类和假正类之和的比率

准确率是正确正类与正确正类和假负类之和的比率

F1 分数是精确度和敏感度的调和平均值

我们将在未来章节的单独配方中更详细地讨论这个指标。现在,暂时可以说我们会有较高的精确度和召回率值。

知道我们模型的准确率约为 91%是好的,但真正的考验将是在测试数据上运行时。让我们看看测试数据的指标:

How it works…

很高兴地知道,我们的模型在测试数据上的准确率为 95%,这表明我们在拟合模型方面做得很好。

还有更多内容…

让我们更深入地了解我们已经构建的模型:

更多内容…

我们调用了一个名为get_params的函数。该函数返回所有传递给模型的参数。让我们来检查每个参数。

第一个参数指的是 KNN 实现所使用的底层数据结构。由于训练集中的每个记录都必须与其他所有记录进行比较,暴力实现可能会占用大量计算资源。因此,我们可以选择 kd_treeball_tree 作为数据结构。暴力方法会对每个记录使用暴力法,通过循环遍历所有记录。

叶子大小是传递给 kd_treeball_tree 方法的参数。

度量是用于寻找邻居的距离度量方法。两者的 p 值将 Minkowski 距离简化为欧几里得距离。

最后,我们有了权重参数。KNN 根据其 K 个最近邻的类别标签来决定测试实例的类别标签。多数投票决定测试实例的类别标签。但是,如果我们将权重设置为距离,那么每个邻居会被赋予一个与其距离成反比的权重。因此,在决定测试集的类别标签时,进行的是加权投票,而不是简单投票。

另请参见

  • 为模型构建准备数据 这一方法在第六章,机器学习 I中有提到。

  • 距离度量法工作 这一方法在第五章,数据挖掘——大海捞针中有提到。

使用朴素贝叶斯分类文档

在这个方案中,我们将查看一个文档分类问题。我们将使用的算法是朴素贝叶斯分类器。贝叶斯定理是驱动朴素贝叶斯算法的引擎,如下所示:

使用朴素贝叶斯分类文档

它展示了事件 X 发生的可能性,前提是我们已经知道事件 Y 已经发生。在我们的方案中,我们将对文本进行分类或分组。我们的分类问题是二分类问题:给定一条电影评论,我们想要分类评论是正面还是负面。

在贝叶斯术语中,我们需要找到条件概率:给定评论,评论为正的概率,和评论为负的概率。我们可以将其表示为一个方程:

使用朴素贝叶斯分类文档

对于任何评论,如果我们有前面的两个概率值,我们可以通过比较这些值将评论分类为正面或负面。如果负面的条件概率大于正面的条件概率,我们将评论分类为负面,反之亦然。

现在,让我们通过贝叶斯定理来讨论这些概率:

使用朴素贝叶斯分类文档

由于我们将比较这两个方程来最终确定预测结果,我们可以忽略分母,因为它只是一个简单的缩放因子。

上述方程的左侧(LHS)称为后验概率。

让我们看一下右侧(RHS)的分子部分:

使用朴素贝叶斯分类文档

P(positive) 是正类的概率,称为先验概率。这是我们基于训练集对正类标签分布的信念。

我们将从我们的训练测试中进行估计。计算公式如下:

使用朴素贝叶斯分类文档

P(review|positive) 是似然度。它回答了这样一个问题:在给定类别为正类的情况下,获得该评论的可能性是多少。同样,我们将从我们的训练集中进行估计。

在我们进一步展开似然度方程之前,让我们引入独立性假设的概念。由于这个假设,该算法被称为朴素的。与实际情况相反,我们假设文档中的词是相互独立的。我们将利用这个假设来计算似然度。

一条评论是一个单词列表。我们可以将其用数学符号表示如下:

使用朴素贝叶斯分类文档

基于独立性假设,我们可以说,这些词在评论中共同出现的概率是评论中各个单词的单独概率的乘积。

现在我们可以将似然度方程写成如下形式:

使用朴素贝叶斯分类文档

所以,给定一个新的评论,我们可以利用这两个方程——先验概率和似然度,来计算该评论是正面的还是负面的。

希望你到现在为止都跟上了。现在还有最后一块拼图:我们如何计算每个单词的概率?

使用朴素贝叶斯分类文档

这一步是指训练模型。

从我们的训练集出发,我们将取出每个评论,并且我们也知道它的标签。对于评论中的每一个词,我们将计算条件概率并将其存储在表格中。这样,我们就可以利用这些值来预测未来的测试实例。

理论够了!让我们深入了解我们的步骤。

准备工作

对于这个步骤,我们将使用 NLTK 库来处理数据和算法。在安装 NLTK 时,我们也可以下载数据集。一个这样的数据集是电影评论数据集。电影评论数据集被分为两类,正面和负面。对于每个类别,我们都有一个词列表;评论已经预先分割成单词:

from nltk.corpus import movie_reviews

如此处所示,我们将通过从 NLTK 导入语料库模块来包括数据集。

我们将利用 NaiveBayesClassifier 类,这个类定义在 NLTK 中,用来构建模型。我们将把我们的训练数据传递给一个名为 train() 的函数,以建立我们的模型。

如何操作……

让我们从导入必要的函数开始。接下来我们将导入两个工具函数。第一个函数用来获取电影评论数据,第二个函数帮助我们将数据划分为训练集和测试集:

from nltk.corpus import movie_reviews
from sklearn.cross_validation import StratifiedShuffleSplit
import nltk
from nltk.corpus import stopwords
from nltk.collocations import BigramCollocationFinder
from nltk.metrics import BigramAssocMeasures

def get_data():
    """
    Get movie review data
    """
    dataset = []
    y_labels = []
    # Extract categories
    for cat in movie_reviews.categories():
        # for files in each cateogry    
        for fileid in movie_reviews.fileids(cat):
            # Get the words in that category
            words = list(movie_reviews.words(fileid))
            dataset.append((words,cat))
            y_labels.append(cat)
    return dataset,y_labels

def get_train_test(input_dataset,ylabels):
    """
    Perpare a stratified train and test split
    """
    train_size = 0.7
    test_size = 1-train_size
    stratified_split = StratifiedShuffleSplit(ylabels,test_size=test_size,n_iter=1,random_state=77)

    for train_indx,test_indx in stratified_split:
        train   = [input_dataset[i] for i in train_indx]
        train_y = [ylabels[i] for i in train_indx]

        test    = [input_dataset[i] for i in test_indx]
        test_y  = [ylabels[i] for i in test_indx]
    return train,test,train_y,test_y

现在我们将介绍三个函数,这些函数主要用于特征生成。我们需要为分类器提供特征或属性。这些函数根据评论生成一组特征:

def build_word_features(instance):
    """
    Build feature dictionary
    Features are binary, name of the feature is word iteslf
    and value is 1\. Features are stored in a dictionary
    called feature_set
    """
    # Dictionary to store the features
    feature_set = {}
    # The first item in instance tuple the word list
    words = instance[0]
    # Populate feature dicitonary
    for word in words:
        feature_set[word] = 1
    # Second item in instance tuple is class label
    return (feature_set,instance[1])

def build_negate_features(instance):
    """
    If a word is preceeded by either 'not' or 'no'
    this function adds a prefix 'Not_' to that word
    It will also not insert the previous negation word
    'not' or 'no' in feature dictionary
    """
    # Retreive words, first item in instance tuple
    words = instance[0]
    final_words = []
    # A boolean variable to track if the 
    # previous word is a negation word
    negate = False
    # List of negation words
    negate_words = ['no','not']
    # On looping throught the words, on encountering
    # a negation word, variable negate is set to True
    # negation word is not added to feature dictionary
    # if negate variable is set to true
    # 'Not_' prefix is added to the word
    for word in words:
        if negate:
            word = 'Not_' + word
            negate = False
        if word not in negate_words:
            final_words.append(word)
        else:
            negate = True
    # Feature dictionary
    feature_set = {}
    for word in final_words:
        feature_set[word] = 1
    return (feature_set,instance[1])

def remove_stop_words(in_data):
    """
    Utility function to remove stop words
    from the given list of words
    """
    stopword_list = stopwords.words('english')
    negate_words = ['no','not']
    # We dont want to remove the negate words
    # Hence we create a new stop word list excluding
    # the negate words
    new_stopwords = [word for word in stopword_list if word not in negate_words]
    label = in_data[1]
    # Remove stopw words
    words = [word for word in in_data[0] if word not in new_stopwords]
    return (words,label)

def build_keyphrase_features(instance):
    """
    A function to extract key phrases
    from the given text.
    Key Phrases are words of importance according to a measure
    In this key our phrase of is our length 2, i.e two words or bigrams
    """
    feature_set = {}
    instance = remove_stop_words(instance)
    words = instance[0]

    bigram_finder  = BigramCollocationFinder.from_words(words)
    # We use the raw frequency count of bigrams, i.e. bigrams are
    # ordered by their frequency of occurence in descending order
    # and top 400 bigrams are selected.
    bigrams        = bigram_finder.nbest(BigramAssocMeasures.raw_freq,400)
    for bigram in bigrams:
        feature_set[bigram] = 1
    return (feature_set,instance[1])

现在让我们编写一个函数来构建我们的模型,并稍后检查我们的模型,以评估其有效性:

def build_model(features):
    """
    Build a naive bayes model
    with the gvien feature set.
    """
    model = nltk.NaiveBayesClassifier.train(features)
    return model    

def probe_model(model,features,dataset_type = 'Train'):
    """
    A utility function to check the goodness
    of our model.
    """
    accuracy = nltk.classify.accuracy(model,features)
    print "\n" + dataset_type + " Accuracy = %0.2f"%(accuracy*100) + "%" 

def show_features(model,no_features=5):
    """
    A utility function to see how important
    various features are for our model.
    """
    print "\nFeature Importance"
    print "===================\n"
    print model.show_most_informative_features(no_features)        

在第一次尝试时,很难将模型调整到最佳状态。我们需要尝试不同的特征和参数调整。这基本上是一个反复试验的过程。在下一节代码中,我们将通过改进模型来展示我们的不同尝试:

def build_model_cycle_1(train_data,dev_data):
    """
    First pass at trying out our model
    """
    # Build features for training set
    train_features =map(build_word_features,train_data)
    # Build features for test set
    dev_features = map(build_word_features,dev_data)
    # Build model
    model = build_model(train_features)    
    # Look at the model
    probe_model(model,train_features)
    probe_model(model,dev_features,'Dev')

    return model

def build_model_cycle_2(train_data,dev_data):
    """
    Second pass at trying out our model
    """

    # Build features for training set
    train_features =map(build_negate_features,train_data)
    # Build features for test set
    dev_features = map(build_negate_features,dev_data)
    # Build model
    model = build_model(train_features)    
    # Look at the model
    probe_model(model,train_features)
    probe_model(model,dev_features,'Dev')

    return model

def build_model_cycle_3(train_data,dev_data):
    """
    Third pass at trying out our model
    """

    # Build features for training set
    train_features =map(build_keyphrase_features,train_data)
    # Build features for test set
    dev_features = map(build_keyphrase_features,dev_data)
    # Build model
    model = build_model(train_features)    
    # Look at the model
    probe_model(model,train_features)
    probe_model(model,dev_features,'Dev')
    test_features = map(build_keyphrase_features,test_data)
    probe_model(model,test_features,'Test')

    return model

最后,我们将编写一段代码,以便调用我们之前定义的所有函数:

if __name__ == "__main__":

    # Load data
    input_dataset, y_labels = get_data()
    # Train data    
    train_data,all_test_data,train_y,all_test_y = get_train_test(input_dataset,y_labels)
    # Dev data
    dev_data,test_data,dev_y,test_y = get_train_test(all_test_data,all_test_y)

    # Let us look at the data size in our different 
    # datasets
    print "\nOriginal  Data Size   =", len(input_dataset)
    print "\nTraining  Data Size   =", len(train_data)
    print "\nDev       Data Size   =", len(dev_data)
    print "\nTesting   Data Size   =", len(test_data)    

    # Different passes of our model building exercise    
    model_cycle_1 =  build_model_cycle_1(train_data,dev_data)
    # Print informative features
    show_features(model_cycle_1)    
    model_cycle_2 = build_model_cycle_2(train_data,dev_data)
    show_features(model_cycle_2)
    model_cycle_3 = build_model_cycle_3(train_data,dev_data)
    show_features(model_cycle_3)

它是如何工作的……

让我们尝试按照主函数中的步骤来执行这个过程。我们从调用get_data函数开始。如前所述,电影评论数据被存储为两类:正面和负面。我们的第一个循环遍历这些类别。在第二个循环中,我们使用这些类别来检索文件 ID。使用这些文件 ID,我们进一步获取单词,如下所示:

            words = list(movie_reviews.words(fileid))

我们将这些单词附加到一个名为dataset的列表中。类别标签将附加到另一个名为y_labels的列表中。

最后,我们返回单词及其相应的类别标签:

    return dataset,y_labels

凭借数据集,我们需要将其划分为测试集和训练集:

 # Train data    
    train_data,all_test_data,train_y,all_test_y = get_train_test(input_dataset,y_labels)

我们调用了get_train_test函数,传入一个输入数据集和类别标签。此函数为我们提供了一个分层样本。我们使用 70%的数据作为训练集,剩余的作为测试集。

我们再次调用get_train_test,并传入上一步返回的测试数据集:

    # Dev data
    dev_data,test_data,dev_y,test_y = get_train_test(all_test_data,all_test_y)

我们创建了一个单独的数据集,并称其为开发集。我们需要这个数据集来调整我们的模型。我们希望我们的测试集真正充当测试集的角色。在构建模型的不同过程中,我们不希望暴露测试集。

让我们打印出我们的训练集、开发集和测试集的大小:

它是如何工作的……

如你所见,70%的原始数据被分配到我们的训练集中。其余的 30%再次被划分为DevTesting的 70/30 分配。

让我们开始我们的模型构建活动。我们将调用build_model_cycle_1,并使用我们的训练集和开发集数据。在这个函数中,我们将首先通过调用build_word_feature并对数据集中的所有实例进行 map 操作来创建特征。build_word_feature是一个简单的特征生成函数。每个单词都是一个特征。这个函数的输出是一个特征字典,其中键是单词本身,值为 1。这种类型的特征通常被称为词袋模型(BOW)。build_word_features函数同时在训练集和开发集数据上调用:

    # Build features for training set
    train_features =map(build_negate_features,train_data)
    # Build features for test set
    dev_features = map(build_negate_features,dev_data)

我们现在将继续使用生成的特征来训练我们的模型:

    # Build model
    model = build_model(train_features)    

我们需要测试我们的模型有多好。我们使用probe_model函数来实现这一点。Probe_model接受三个参数。第一个参数是我们感兴趣的模型,第二个参数是我们希望评估模型性能的特征,最后一个参数是用于显示目的的字符串。probe_model函数通过在nltk.classify模块中使用准确度函数来计算准确率指标。

我们调用probe_model两次:第一次使用训练数据来查看模型在训练集上的表现,第二次使用开发集数据:

    # Look at the model
    probe_model(model,train_features)
    probe_model(model,dev_features,'Dev')

现在让我们来看一下准确率数据:

它是如何工作的…

我们的模型在使用训练数据时表现得非常好。这并不令人惊讶,因为模型在训练阶段已经见过这些数据。它能够正确地分类训练记录。然而,我们的开发集准确率很差。我们的模型只能正确分类 60%的开发集实例。显然,我们的特征对于帮助模型高效分类未见过的实例并不够有信息量。查看哪些特征对将评论分类为正面或负面有更大贡献是很有帮助的:

show_features(model_cycle_1) 

我们将调用show_features函数来查看各特征对模型的贡献。Show_features函数利用 NLTK 分类器对象中的show_most_informative_feature函数。我们第一个模型中最重要的特征如下:

它是如何工作的…

读取方式是:特征stupidity = 1在将评论分类为负面时比其他特征更有效 15 倍。

接下来,我们将使用一组新的特征来进行第二轮模型构建。我们将通过调用build_model_cycle_2来实现。build_model_cycle_2build_model_cycle_1非常相似,唯一不同的是在 map 函数中调用了不同的特征生成函数。

特征生成函数称为 build_negate_features。通常,像 “not” 和 “no” 这样的词被称为否定词。假设我们的评论者说这部电影不好。如果我们使用之前的特征生成器,单词 “good” 在正面和负面评论中会被平等对待。但我们知道,单词 “good” 应该用于区分正面评论。为了避免这个问题,我们将查找单词列表中的否定词“no”和“not”。我们希望将我们的示例句子修改如下:

"movie is not good" to "movie is not_good"

通过这种方式,no_good 可以作为一个很好的特征,帮助区分负面评论与正面评论。build_negate_features 函数完成了这个工作。

现在,让我们看看使用此否定特征构建的模型的探测输出:

工作原理…

我们在开发数据上的模型准确率提高了近 2%。现在让我们看看这个模型中最具信息量的特征:

工作原理…

看看最后一个特征。添加否定词后,“Not_funny” 特征对于区分负面评论比其他特征信息量多 11.7 倍。

我们能提高模型的准确性吗?目前,我们的准确率是 70%。让我们进行第三次实验,使用一组新的特征。我们将通过调用 build_model_cycle_3 来实现。build_model_cycle_3build_model_cycle_2 非常相似,除了在 map 函数内部调用的特征生成函数不同。

build_keyphrase_features 函数用作特征生成器。让我们详细看看这个函数。我们将不使用单词作为特征,而是从评论中生成关键词组,并将它们用作特征。关键词组是我们通过某种度量认为重要的短语。关键词组可以由两个、三个或多个单词组成。在我们的例子中,我们将使用两个单词(二元组)来构建关键词组。我们将使用的度量是这些短语的原始频率计数。我们将选择频率计数较高的短语。在生成关键词组之前,我们将进行一些简单的预处理。我们将从单词列表中移除所有停用词和标点符号。remove_stop_words 函数会被调用来移除停用词和标点符号。NLTK 的语料库模块中有一个英文停用词列表。我们可以按如下方式检索它:

stopword_list = stopwords.words('english')

同样,Python 的字符串模块维护着一个标点符号列表。我们将按如下方式移除停用词和标点符号:

words = [word for word in in_data[0] if word not in new_stopwords \
and word not in punctuation]

然而,我们不会移除“not”和no。我们将通过“not”创建一组新的停用词,其中包含前一步中的否定词:

new_stopwords = [word for word in stopword_list if word not in negate_words]

我们将利用 NLTK 的 BigramCollocationFinder 类来生成我们的关键词组:

    bigram_finder  = BigramCollocationFinder.from_words(words)
    # We use the raw frequency count of bigrams, i.e. bigrams are
    # ordered by their frequency of occurence in descending order
    # and top 400 bigrams are selected.
    bigrams        = bigram_finder.nbest(BigramAssocMeasures.raw_freq,400) 

我们的度量是频率计数。你可以看到,在最后一行中我们将其指定为 raw_freq。我们将要求搭配查找器返回最多 400 个短语。

装载了新特征后,我们将继续构建模型并测试模型的正确性。让我们看看模型的输出:

工作原理…

是的!我们在开发集上取得了很大进展。从第一次使用词语特征时的 68%准确率,我们通过关键短语特征将准确率从 12%提升至 80%。现在让我们将测试集暴露给这个模型,检查准确度:

    test_features = map(build_keyphrase_features,test_data)
    probe_model(model,test_features,'Test')

工作原理…

我们的测试集准确度高于开发集准确度。我们在训练一个在未见数据集上表现良好的模型方面做得不错。在结束本教程之前,我们来看一下最有信息量的关键短语:

工作原理…

关键短语“奥斯卡提名”在区分评论为正面时比其他任何特征都要有帮助,效果是其他 10 倍。你无法否认这一点。我们可以看到我们的关键短语信息量非常大,因此我们的模型比前两次运行表现得更好。

还有更多…

我们是如何知道 400 个关键短语和度量频次计数是双词生成的最佳参数的呢?通过反复试验。虽然我们没有列出我们的试验过程,但基本上我们使用了不同的组合,比如 200 个短语与逐点互信息(pointwise mutual information),以及其他类似的方法。

这就是现实世界中需要做的事情。然而,我们不是每次都盲目地搜索参数空间,而是关注了最有信息量的特征。这给了我们有关特征区分能力的线索。

另见

  • 在第六章中的为模型构建准备数据教程,机器学习 I

构建决策树来解决多类问题

在本教程中,我们将讨论构建决策树来解决多类分类问题。从直觉上看,决策树可以定义为通过一系列问题来得出答案的过程:一系列的“如果-那么”语句按层次结构排列构成了决策树。正因为这种特性,它们易于理解和解释。

请参考以下链接,详细了解决策树的介绍:

en.wikipedia.org/wiki/Decision_tree

理论上,可以为给定的数据集构建许多决策树。某些树比其他树更准确。有一些高效的算法可以在有限的时间内开发出一个合理准确的树。一个这样的算法是亨特算法。像 ID3、C4.5 和 CART(分类和回归树)这样的算法都基于亨特算法。亨特算法可以概述如下:

给定一个数据集 D,其中有 n 条记录,每条记录包含 m 个属性/特征/列,并且每条记录被标记为 y1、y2 或 y3,算法的处理流程如下:

  • 如果 D 中的所有记录都属于同一类别标签,例如 y1,那么 y1 就是树的叶子节点,并被标记为 y1。

  • 如果 D 中的记录属于多个类标签,则会采用特征测试条件将记录划分为更小的子集。假设在初始运行时,我们对所有属性进行特征测试条件,并找到一个能够将数据集划分为三个子集的属性。这个属性就成为根节点。我们对这三个子集应用测试条件,以找出下一层的节点。这个过程是迭代进行的。

请注意,当我们定义分类时,我们定义了三个类别标签 y1、y2 和 y3。与前两个食谱中我们只处理了两个标签的问题不同,这是一个多类问题。我们在大多数食谱中使用的鸢尾花数据集就是一个三类问题。我们的记录分布在三个类别标签之间。我们可以将其推广到 n 类问题。数字识别是另一个例子,我们需要将给定图像分类为零到九之间的某个数字。许多现实世界的问题本质上就是多类问题。一些算法也天生可以处理多类问题。对于这些算法,无需进行任何修改。我们将在各章节中讨论的算法都能处理多类问题。决策树、朴素贝叶斯和 KNN 算法擅长处理多类问题。

让我们看看如何利用决策树来处理这个食谱中的多类问题。理解决策树的原理也有助于此。随机森林是下一章我们将探讨的一种更为复杂的方法,它在行业中被广泛应用,且基于决策树。

现在让我们深入了解决策树的使用方法。

准备就绪

我们将使用鸢尾花数据集来演示如何构建决策树。决策树是一种非参数的监督学习方法,可以用来解决分类和回归问题。如前所述,使用决策树的优点有很多,具体如下:

  • 它们易于解释

  • 它们几乎不需要数据准备和特征转换:请记住我们在前一个食谱中的特征生成方法

  • 它们天然支持多类问题

决策树并非没有问题。它们存在的一些问题如下:

  • 它们很容易发生过拟合:在训练集上表现出高准确率,而在测试数据上表现很差。

  • 可以为给定数据集拟合出数百万棵树。

  • 类别不平衡问题可能会对决策树产生较大影响。类别不平衡问题出现在我们的训练集在二分类问题中未包含相等数量的实例标签时。这个问题在多类问题中也同样适用。

决策树的一个重要部分是特征测试条件。让我们花些时间理解特征测试条件。通常,我们的实例中的每个属性可以被理解为以下之一。

二元属性:这是指一个属性可以取两个可能的值,比如 true 或 false。在这种情况下,特征测试条件应该返回两个值。

名义属性:这是指一个属性可以取多个值,比如 n 个值。特征测试条件应该返回 n 个输出,或者将它们分组为二元拆分。

顺序属性:这是指属性的值之间存在隐含的顺序关系。例如,假设我们有一个虚拟属性叫做 size,它可以取小、中、大这三个值。这个属性有三个值,并且它们之间有顺序:小、中、大。它们由特征属性测试条件处理,这类似于名义属性的处理方式。

连续属性:这些是可以取连续值的属性。它们会被离散化为顺序属性,然后进行处理。

特征测试条件是一种根据叫做“杂质”的标准或度量,将输入记录拆分为子集的方法。这个杂质是相对于每个属性在实例中的类别标签计算的。贡献最大杂质的属性被选为数据拆分属性,或者换句话说,就是树中该层的节点。

让我们通过一个例子来解释它。我们将使用一种叫做熵的度量来计算杂质。

熵的定义如下:

准备工作

其中:

准备工作

让我们考虑一个例子:

X = {2,2}

现在我们可以根据这个集合计算熵,如下所示:

准备工作

这个集合的熵为 0。熵为 0 表示同质性。在 Python 中编写熵计算非常简单。看一下以下的代码示例:

import math

def prob(data,element):
    """Calculates the percentage count of a given element

    Given a list and an element, returns the elements percentage count

    """
    element_count =0
   	# Test conditoin to check if we have proper input
    if len(data) == 0 or element == None \
                or not isinstance(element,(int,long,float)):
        return None      
    element_count = data.count(element)
    return element_count / (1.0 * len(data))

def entropy(data):
    """"Calcuate entropy
    """
    entropy =0.0

    if len(data) == 0:
        return None

    if len(data) == 1:
        return 0
    try:
        for element in data:
            p = prob(data,element)
            entropy+=-1*p*math.log(p,2)
    except ValueError as e:
        print e.message

    return entropy

为了找到最佳的分割变量,我们将利用熵。首先,我们将根据类别标签计算熵,如下所示:

准备工作

让我们定义另一个术语,称为信息增益。信息增益是一种衡量在给定实例中哪个属性对区分类别标签最有用的度量方法。

信息增益是父节点的熵与子节点的平均熵之间的差值。在树的每一层,我们将使用信息增益来构建树:

en.wikipedia.org/wiki/Information_gain_in_decision_trees

我们将从训练集中的所有属性开始,计算整体的熵。让我们来看以下的例子:

准备工作

前面的数据集是为用户收集的虚拟数据,用来帮助他了解自己喜欢什么类型的电影。数据集包含四个属性:第一个属性是用户是否根据主演决定观看电影,第二个属性是用户是否根据电影是否获得奥斯卡奖来决定是否观看,第三个属性是用户是否根据电影是否票房成功来决定是否观看。

为了构建前面示例的决策树,我们将从整个数据集的熵计算开始。这是一个二类问题,因此 c = 2。并且,数据集共有四条记录,因此整个数据集的熵如下:

准备中

数据集的总体熵值为 0.811。

现在,让我们看一下第一个属性,即主演属性。对于主演为 Y 的实例,有一个实例类别标签为 Y,另一个实例类别标签为 N。对于主演为 N 的实例,两个实例类别标签均为 N。我们将按如下方式计算平均熵:

准备中

这是一个平均熵值。主演为 Y 的记录有两条,主演为 N 的记录也有两条;因此,我们有 2/4.0 乘以熵值。

当对这个数据子集计算熵时,我们可以看到在两个记录中,主演 Y 的一个记录的类别标签为 Y,另一个记录的类别标签为 N。类似地,对于主演为 N,两个记录的类别标签均为 N。因此,我们得到了该属性的平均熵。

主演属性的平均熵值为 0.5。

信息增益现在是 0.811 – 0.5 = 0.311。

同样地,我们将找到所有属性的信息增益。具有最高信息增益的属性胜出,并成为决策树的根节点。

相同的过程将重复进行,以便找到节点的第二级,依此类推。

如何操作…

让我们加载所需的库。接下来,我们将编写两个函数,一个用于加载数据,另一个用于将数据拆分为训练集和测试集:

from sklearn.datasets import load_iris
from sklearn.cross_validation import StratifiedShuffleSplit
import numpy as np
from sklearn import tree
from sklearn.metrics import accuracy_score,classification_report,confusion_matrix
import pprint

def get_data():
    """
    Get Iris data
    """
    data = load_iris()
    x = data['data']
    y = data['target']
    label_names = data['target_names']

    return x,y,label_names.tolist()

def get_train_test(x,y):
    """
    Perpare a stratified train and test split
    """
    train_size = 0.8
    test_size = 1-train_size
    input_dataset = np.column_stack([x,y])
    stratified_split = StratifiedShuffleSplit(input_dataset[:,-1], \
            test_size=test_size,n_iter=1,random_state = 77)

    for train_indx,test_indx in stratified_split:
        train_x = input_dataset[train_indx,:-1]
        train_y = input_dataset[train_indx,-1]
        test_x =  input_dataset[test_indx,:-1]
        test_y = input_dataset[test_indx,-1]
    return train_x,train_y,test_x,test_y

让我们编写函数来帮助我们构建和测试决策树模型:

def build_model(x,y):
    """
    Fit the model for the given attribute 
    class label pairs
    """
    model = tree.DecisionTreeClassifier(criterion="entropy")
    model = model.fit(x,y)
    return model

def test_model(x,y,model,label_names):
    """
    Inspect the model for accuracy
    """
    y_predicted = model.predict(x)
    print "Model accuracy = %0.2f"%(accuracy_score(y,y_predicted) * 100) + "%\n"
    print "\nConfusion Matrix"
    print "================="
    print pprint.pprint(confusion_matrix(y,y_predicted))

    print "\nClassification Report"
    print "================="

    print classification_report(y,y_predicted,target_names=label_names)

最后,调用我们定义的所有其他函数的主函数如下:

if __name__ == "__main__":
    # Load the data
    x,y,label_names = get_data()
    # Split the data into train and test    
    train_x,train_y,test_x,test_y = get_train_test(x,y)
    # Build model    
    model = build_model(train_x,train_y)
    # Evaluate the model on train dataset    
    test_model(train_x,train_y,model,label_names)    
    # Evaluate the model on test dataset
    test_model(test_x,test_y,model,label_names)

它是如何工作的…

我们从主函数开始。我们在 xylabel_names 变量中调用 get_data 来检索鸢尾花数据集。我们获取标签名称,以便在看到模型准确性时,可以根据单个标签进行衡量。如前所述,鸢尾花数据集是一个三类问题。我们需要构建一个分类器,可以将任何新实例分类为三种类型之一:setosa、versicolor 或 virginica。

一如既往,get_train_test返回分层的训练和测试数据集。然后,我们利用 scikit-learn 中的StratifiedShuffleSplit获取具有相等类标签分布的训练和测试数据集。

我们必须调用build_model方法,在我们的训练集上构建决策树。scikit-learn 中的DecisionTreeClassifier类实现了一个决策树:

model = tree.DecisionTreeClassifier(criterion="entropy")

如你所见,我们指定了我们的特征测试条件是使用criterion变量来设置的熵。然后,我们通过调用fit函数来构建模型,并将模型返回给调用程序。

现在,让我们继续使用test_model函数来评估我们的模型。该模型接受实例x、类标签y、决策树模型model以及类标签名称label_names

scikit-learn 中的模块度量提供了三种评估标准:

from sklearn.metrics import accuracy_score,classification_report,confusion_matrix

我们在前面的步骤和引言部分定义了准确率。

混淆矩阵打印了引言部分定义的混淆矩阵。混淆矩阵是一种评估模型表现的好方法。我们关注的是具有真正例和假正例值的单元格值。

最后,我们还有classification_report来打印精确度、召回率和 F1 得分。

我们必须首先在训练数据上评估模型:

它是如何工作的…

我们在训练数据集上做得非常好,准确率达到了 100%。真正的考验是使用测试数据集,那里才是决定成败的地方。

让我们通过使用测试数据集来查看模型评估:

它是如何工作的…

我们的分类器在测试集上的表现也非常出色。

还有更多…

让我们探查一下模型,看看各个特征如何有助于区分不同类别。

def get_feature_names():
    data = load_iris()
    return data['feature_names']

def probe_model(x,y,model,label_names):

    feature_names = get_feature_names()
    feature_importance = model.feature_importances_
    print "\nFeature Importance\n"
    print "=====================\n"
    for i,feature_name in enumerate(feature_names):
        print "%s = %0.3f"%(feature_name,feature_importance[i])

    # Export the decison tree as a graph
    tree.export_graphviz(model,out_file='tree.dot')

决策树分类器对象提供了一个属性feature_importances_,可以调用它来获取各个特征对构建我们模型的重要性。

我们写了一个简单的函数get_feature_names,用于获取特征名称。不过,这也可以作为get_data的一部分来添加。

让我们来看一下打印语句的输出:

还有更多…

这看起来好像花瓣宽度和花瓣长度在我们的分类中贡献更大。

有趣的是,我们还可以将分类器构建的树导出为 dot 文件,并使用 GraphViz 包进行可视化。在最后一行,我们将模型导出为 dot 文件:

# Export the decison tree as a graph
tree.export_graphviz(model,out_file='tree.dot')

你可以下载并安装 Graphviz 包来可视化它:

www.graphviz.org/

另请参见

  • 为模型构建准备数据的步骤见第六章,机器学习 I

  • 查找最近邻的步骤见第六章,机器学习 I

  • 在第六章中,使用朴素贝叶斯分类文档的技巧,机器学习 I

第七章 机器学习 2

在本章中,我们将介绍以下几种方法:

  • 使用回归预测实值

  • 学习带 L2 收缩的回归——岭回归

  • 学习带 L1 收缩的回归——LASSO

  • 使用交叉验证迭代器与 L1 和 L2 收缩

介绍

在本章中,我们将介绍回归技术及其在 Python 中的实现方法。接着,我们将讨论回归方法固有的一些缺点,并探讨如何通过收缩方法来解决这些问题。收缩方法中需要设置一些参数。我们将讨论交叉验证技术,以找到收缩方法的最佳参数值。

在上一章中,我们讨论了分类问题。在本章中,让我们将注意力转向回归问题。在分类问题中,响应变量Y要么是二元的,要么是一组离散值(在多类和多标签问题中)。而在回归中,响应变量是一个实值数字。

回归可以看作是一种函数逼近。回归的任务是找到一个函数,使得当X(一组随机变量)作为输入传递给该函数时,它应该返回Y,即响应变量。X也被称为自变量,而Y则被称为因变量。

我们将利用上一章中学到的技术,将数据集分为训练集、开发集和测试集,在训练集上迭代地构建模型,并在开发集上进行验证。最后,我们将使用测试集来全面评估模型的优劣。

本章将从使用最小二乘估计的简单线性回归方法开始。在第一种方法的开头,我们将简要介绍回归的框架,这是理解本章其他方法所必需的基础背景信息。尽管非常强大,简单回归框架也存在一个缺点。由于无法控制线性回归系数的上下限,它们往往会过拟合给定的数据。(线性回归的代价函数是无约束的,我们将在第一种方法中进一步讨论)。输出的回归模型可能在未见过的数据集上表现不佳。为了应对这个问题,使用了收缩方法。收缩方法也被称为正则化方法。在接下来的两种方法中,我们将介绍两种不同的收缩方法——LASSO 和岭回归。在最后一种方法中,我们将介绍交叉验证的概念,并展示如何利用它来估计传递给岭回归(作为一种收缩方法)的参数 alpha。

使用回归预测实值

在我们深入探讨此方案之前,先快速了解回归一般是如何运作的。这个介绍对于理解当前方案以及后续方案非常重要。

回归是一种特殊形式的函数逼近。以下是预测变量集:

使用回归预测实值数

每个实例xi具有m个属性:

使用回归预测实值数

回归的任务是找到一个函数,使得当X作为输入提供给该函数时,应该返回一个Y响应变量。Y是一个实值向量:

使用回归预测实值数

我们将使用波士顿房价数据集来解释回归框架。

以下链接提供了波士顿房价数据集的良好介绍:

archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.names

在本例中,响应变量Y是波士顿地区自有住房的中位数价值。有 13 个预测变量。前面的网页链接提供了所有预测变量的良好描述。回归问题的定义是找到一个函数F,使得如果我们给这个函数一个以前未见过的预测值,它应该能够给出中位数房价。

函数F(X)是我们线性回归模型的输出,它是输入X的线性组合,因此得名线性回归:

使用回归预测实值数

wi变量是前述方程中的未知值。建模过程的关键在于发现wi变量。通过使用我们的训练数据,我们将找到wi的值;wi被称为回归模型的系数。

线性回归建模问题可以表述为:使用训练数据来找到系数:

使用回归预测实值数

使得:

使用回归预测实值数

上述公式值尽可能小。

该方程的值越低(在优化术语中称为代价函数),线性回归模型越好。因此,优化问题就是最小化前述方程,即找到wi系数的值,使其最小化该方程。我们不会深入探讨优化算法的细节,但了解这个目标函数是有必要的,因为接下来的两个方案需要你理解它。

现在,问题是我们如何知道使用训练数据构建的模型,即我们新找到的系数w1, w2,..wm,是否足够准确地预测未见过的记录?我们将再次利用之前定义的成本函数。当我们将模型应用于开发集或测试集时,我们找到实际值和预测值之间差异平方的平均值,如下所示:

使用回归预测实数

上述方程被称为均方误差(mean squared error)——这是我们用来判断回归模型是否值得使用的标准。我们希望得到一个输出模型,使得实际值和预测值之间的差异平方的平均值尽可能低。寻找系数的这个方法被称为最小二乘估计。

我们将使用 scikit-learn 的LinearRegression类。然而,它在内部使用scipy.linalg.lstsq方法。最小二乘法为我们提供了回归问题的闭式解。有关最小二乘法及其推导的更多信息,请参考以下链接:

en.wikipedia.org/wiki/Least_squares

en.wikipedia.org/wiki/Linear_least_squares_(mathematics)

我们对回归做了一个非常简单的介绍。感兴趣的读者可以参考以下书籍:www.amazon.com/exec/obidos/ASIN/0387952845/trevorhastie-20

www.amazon.com/Neural-Networks-Learning-Machines-Edition/dp/0131471392

准备就绪

波士顿数据集有 13 个属性和 506 个实例。目标变量是一个实数,房屋的中位数值在千美元左右。

参考以下 UCI 链接了解有关波士顿数据集的更多信息:archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.names

我们将提供这些预测变量和响应变量的名称,如下所示:

准备就绪

如何实现…

我们将从加载所有必要的库开始。接下来,我们将定义第一个函数get_data()。在这个函数中,我们将读取波士顿数据集,并将其作为预测变量x和响应变量y返回:

# Load libraries
from sklearn.datasets import load_boston
from sklearn.cross_validation import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    return x,y    

在我们的build_model函数中,我们将使用给定的数据构建线性回归模型。以下两个函数,view_modelmodel_worth,用于检查我们所构建的模型:

def build_model(x,y):
    """
    Build a linear regression model
    """
    model = LinearRegression(normalize=True,fit_intercept=True)
    model.fit(x,y)
    return model    

def view_model(model):
    """
    Look at model coeffiecients
    """
    print "\n Model coeffiecients"
    print "======================\n"
    for i,coef in enumerate(model.coef_):
        print "\tCoefficient %d  %0.3f"%(i+1,coef)

    print "\n\tIntercept %0.3f"%(model.intercept_)

def model_worth(true_y,predicted_y):
    """
    Evaluate the model
    """
    print "\tMean squared error = %0.2f"%(mean_squared_error(true_y,predicted_y))

plot_residual函数用于绘制我们回归模型中的误差:

def plot_residual(y,predicted_y):
    """
    Plot residuals
    """
    plt.cla()
    plt.xlabel("Predicted Y")
    plt.ylabel("Residual")
    plt.title("Residual Plot")
    plt.figure(1)
    diff = y - predicted_y
    plt.plot(predicted_y,diff,'go')
    plt.show()

最后,我们将编写我们的main函数,用于调用之前的所有函数:

if __name__ == "__main__":

    x,y = get_data()

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    # Build the model
    model = build_model(x_train,y_train)
    predicted_y = model.predict(x_train)

    # Plot the residual
    plot_residual(y_train,predicted_y)
    # View model coeffiecients    
    view_model(model)

    print "\n Model Performance in Training set\n"
    model_worth(y_train,predicted_y)  

    # Apply the model on dev set
    predicted_y = model.predict(x_dev)
    print "\n Model Performance in Dev set\n"
    model_worth(y_dev,predicted_y)  

    #Prepare some polynomial features
    poly_features = PolynomialFeatures(2)
    poly_features.fit(x_train)
    x_train_poly = poly_features.transform(x_train)
    x_dev_poly   = poly_features.transform(x_dev)

    # Build model with polynomial features
    model_poly = build_model(x_train_poly,y_train)
    predicted_y = model_poly.predict(x_train_poly)
    print "\n Model Performance in Training set (Polynomial features)\n"
    model_worth(y_train,predicted_y)  

    # Apply the model on dev set
    predicted_y = model_poly.predict(x_dev_poly)
    print "\n Model Performance in Dev set  (Polynomial features)\n"
    model_worth(y_dev,predicted_y)  

    # Apply the model on Test set
    x_test_poly = poly_features.transform(x_test)
    predicted_y = model_poly.predict(x_test_poly)

    print "\n Model Performance in Test set  (Polynomial features)\n"
    model_worth(y_test,predicted_y)  

    predicted_y = model.predict(x_test)
    print "\n Model Performance in Test set  (Regular features)\n"
    model_worth(y_test,predicted_y)  

它是如何工作的…

让我们从主模块开始,跟着代码走。我们将使用 get_data 函数加载预测变量 x 和响应变量 y

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    return x,y    

该函数调用了 scikit-learn 提供的便捷 load_boston() 函数,以便将波士顿房价数据集作为 NumPy 数组进行检索。

我们将使用 Scikit 库中的 train_test_split 函数,将数据划分为训练集和测试集。我们将保留数据集的 30% 用于测试:

x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

我们将在下一行中提取开发集:

x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

在下一行,我们将通过调用 build_model 方法使用训练数据集来构建我们的模型。该模型创建一个 LinearRegression 类型的对象。LinearRegression 类封装了 SciPy 的最小二乘法:

    model = LinearRegression(normalize=True,fit_intercept=True)

让我们看看初始化该类时传递的参数。

fit_intercept 参数设置为 True。这告诉线性回归类进行数据中心化。通过数据中心化,我们将每个预测变量的均值设置为零。线性回归方法要求数据根据其均值进行中心化,以便更好地解释截距。除了按均值对每个特征进行中心化外,我们还将通过其标准差对每个特征进行归一化。我们将使用 normalize 参数并将其设置为 True 来实现这一点。有关如何按列进行归一化的更多信息,请参考第三章,缩放与数据标准化 的相关内容。通过 fit_intercept 参数,我们将指示算法包含一个截距,以适应响应变量中的任何常数偏移。最后,我们将通过调用 fit 函数并使用响应变量 y 和预测变量 x 来拟合模型。

注意

有关线性回归方法的更多信息,请参考 Trevor Hastie 等人所著的《统计学习元素》一书。

检查我们构建的模型是一种良好的做法,这样我们可以更好地理解模型,以便进一步改进或提升可解释性。

现在,让我们绘制残差图(预测的 y 与实际的 y 之间的差异),并将预测的 y 值作为散点图展示。我们将调用 plot_residual 方法来实现:

    # Plot the residual
    plot_residual(y_train,predicted_y)

让我们看看下面的图表:

它是如何工作的...

我们可以使用这个散点图来验证数据集中的回归假设。我们没有看到任何模式,点均匀地分布在零残差值附近。

注意

有关使用残差图来验证线性回归假设的更多信息,请参考 Daniel. T. Larose 所著的《数据挖掘方法与模型》一书。

然后,我们将使用view_model方法检查我们的模型。在这个方法中,我们将打印截距和系数值。线性回归对象有两个属性,一个叫做coef_,它提供了系数的数组,另一个叫做intercept_,它提供截距值:

它是如何工作的…

让我们来看一下coefficient 6,即房屋中可居住的房间数量。系数值的解释是:每增加一个房间,价格上升三倍。

最后,我们将通过调用model_worth函数来评估我们的模型好坏,该函数使用我们预测的响应值和实际响应值,二者均来自训练集和开发集。

这个函数打印出均方误差值,即实际值与预测值之间差值的平方的平均值:

它是如何工作的…

我们在开发集中的值较低,这表明我们的模型表现良好。让我们看看是否可以改善均方误差。如果我们为模型提供更多特征会怎么样?让我们从现有的属性中创建一些特征。我们将使用 scikit-learn 中的PolynomialFeatures类来创建二阶多项式:

    #Prepare some polynomial features
    poly_features = PolynomialFeatures(2)
    poly_features.fit(x_train)
    x_train_poly = poly_features.transform(x_train)
    x_dev_poly   = poly_features.transform(x_dev)

我们将2作为参数传递给PolynomialFeatures,表示我们需要二阶多项式。如果类初始化为空,2也是默认值:

它是如何工作的…

快速查看新x的形状,我们现在有 105 个属性,而原本只有 13 个。让我们使用新的多项式特征来构建模型,并检查模型的准确性:

    # Build model with polynomial features
    model_poly = build_model(x_train_poly,y_train)
    predicted_y = model_poly.predict(x_train_poly)
    print "\n Model Performance in Training set (Polynomial           features)\n"
    model_worth(y_train,predicted_y)  

    # Apply the model on dev set
    predicted_y = model_poly.predict(x_dev_poly)
    print "\n Model Performance in Dev set  (Polynomial features)\n"
    model_worth(y_dev,predicted_y)  

它是如何工作的…

我们的模型已经很好地拟合了训练数据集。在开发集和训练集中,我们的多项式特征表现优于原始特征。

最后,让我们看看使用多项式特征的模型和使用常规特征的模型在测试集上的表现:

    # Apply the model on Test set
    x_test_poly = poly_features.transform(x_test)
    predicted_y = model_poly.predict(x_test_poly)

    print "\n Model Performance in Test set  (Polynomial features)\n"
    model_worth(y_test,predicted_y)  

predicted_y = model.predict(x_test)
    print "\n Model Performance in Test set  (Regular features)\n"
    model_worth(y_test,predicted_y)  

它是如何工作的…

我们可以看到,在测试数据集上,使用多项式特征的表现优于我们原始特征的表现。

这就是你需要知道的如何在 Python 中进行线性回归。我们了解了线性回归的工作原理,以及如何构建模型来预测实数值。

还有更多…

在继续之前,我们将查看PolynomialFeatures类中的另一个参数设置,叫做interaction_only

poly_features = PolynomialFeatures(interaction_only=True)

通过将interaction_only设置为true-with x1x2 attributes-only,只会创建x1*x2属性。x1x2的平方不会创建,假设阶数为二。

我们的测试集结果与开发集的结果不如预期,尤其是在普通特征和多项式特征上。这是线性回归的一个已知问题。线性回归对于处理方差的能力较弱。我们面临的问题是高方差和低偏差。随着模型复杂度的增加,也就是呈现给模型的特征数量增多,模型往往能够很好地拟合训练数据——因此偏差较低——但开始在测试数据上出现下降的结果。对此问题,有几种技术可以应对。

我们来看看一个叫做递归特征选择的方法。所需特征的数量作为参数传递给该方法。它递归地筛选特征。在第 i 次运行中,会对数据拟合一个线性模型,并根据系数的值来筛选特征;那些权重较低的特征会被剔除。如此一来,迭代就继续进行,直到我们得到所需数量的特征时,迭代才会停止。接下来,让我们看看一个代码示例:

# Load libraries
from sklearn.datasets import load_boston
from sklearn.cross_validation import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from itertools import combinations
from sklearn.feature_selection import RFE

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    return x,y    

def build_model(x,y,no_features):
    """
    Build a linear regression model
    """
    model = LinearRegression(normalize=True,fit_intercept=True)
    rfe_model = RFE(estimator=model,n_features_to_select=no_features)
    rfe_model.fit(x,y)
    return rfe_model    

def view_model(model):
    """
    Look at model coeffiecients
    """
    print "\n Model coeffiecients"
    print "======================\n"
    for i,coef in enumerate(model.coef_):
        print "\tCoefficient %d  %0.3f"%(i+1,coef)

    print "\n\tIntercept %0.3f"%(model.intercept_)

def model_worth(true_y,predicted_y):
    """
    Evaluate the model
    """
    print "\tMean squared error = %0.2f"%(mean_squared_error(true_y,predicted_y))
    return mean_squared_error(true_y,predicted_y)

def plot_residual(y,predicted_y):
    """
    Plot residuals
    """
    plt.cla()
    plt.xlabel("Predicted Y")
    plt.ylabel("Residual")
    plt.title("Residual Plot")
    plt.figure(1)
    diff = y - predicted_y
    plt.plot(predicted_y,diff,'go')
    plt.show()

def subset_selection(x,y):
    """
    subset selection method
    """
    # declare variables to track
    # the model and attributes which produces
    # lowest mean square error
    choosen_subset = None
    low_mse = 1e100
    choosen_model = None
    # k value ranges from 1 to the number of 
    # attributes in x
    for k in range(1,x.shape[1]+1):
        print "k= %d "%(k)
        # evaluate all attribute combinations
        # of size k+1
        subsets = combinations(range(0,x.shape[1]),k+1)
        for subset in subsets:
            x_subset = x[:,subset]
            model = build_model(x_subset,y)
            predicted_y = model.predict(x_subset)
            current_mse = mean_squared_error(y,predicted_y)
            if current_mse < low_mse:
                low_mse = current_mse
                choosen_subset = subset
                choosen_model = model

    return choosen_model, choosen_subset,low_mse    

if __name__ == "__main__":

    x,y = get_data()

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    #Prepare some polynomial features
    poly_features = PolynomialFeatures(interaction_only=True)
    poly_features.fit(x_train)
    x_train_poly = poly_features.transform(x_train)
    x_dev_poly   = poly_features.transform(x_dev)

    #choosen_model,choosen_subset,low_mse = subset_selection(x_train_poly,y_train)    
    choosen_model = build_model(x_train_poly,y_train,20)
    #print choosen_subse
    predicted_y = choosen_model.predict(x_train_poly)
    print "\n Model Performance in Training set (Polynomial features)\n"
    mse  = model_worth(y_train,predicted_y)  

    # Apply the model on dev set
    predicted_y = choosen_model.predict(x_dev_poly)
    print "\n Model Performance in Dev set  (Polynomial features)\n"
    model_worth(y_dev,predicted_y)  

    # Apply the model on Test set
    x_test_poly = poly_features.transform(x_test)
    predicted_y = choosen_model.predict(x_test_poly)

    print "\n Model Performance in Test set  (Polynomial features)\n"
    model_worth(y_test,predicted_y)  

这段代码与之前的线性回归代码非常相似,唯一不同的是build_model方法:

def build_model(x,y,no_features):
    """
    Build a linear regression model
    """
    model = LinearRegression(normalize=True,fit_intercept=True)
    rfe_model = RFE(estimator=model,n_features_to_select=no_features)
    rfe_model.fit(x,y)
    return rfe_model    

除了预测变量x和响应变量ybuild_model方法还接受一个参数,即要保留的特征数量no_features。在这个例子中,我们传递了一个值 20,要求递归特征消除方法只保留 20 个重要特征。如你所见,我们首先创建了一个线性回归对象。这个对象被传递给RFE类。RFE 代表递归特征消除,这是 scikit-learn 提供的一个类,用于实现递归特征消除。现在,我们来评估一下我们的模型在训练集、开发集和测试集上的表现:

更多内容...

测试数据集的均方误差为 13.20,几乎是之前的一半。因此,我们能够有效地使用递归特征消除方法进行特征选择,从而提升我们的模型。

另请参见

  • 第三章中的数据标准化方法,数据分析 – 探索与整理

  • 第三章中的数据标准化方法,数据分析 – 探索与整理

  • 第六章中的为模型构建准备数据方法,机器学习 I

使用 L2 收缩学习回归 – 岭回归

让我们将之前讨论的回归技术扩展,以包括正则化。在训练线性回归模型时,一些系数可能会取非常高的值,导致模型的不稳定。正则化或收缩是一种控制系数权重的方法,目的是使它们不会取非常大的值。让我们再次看看线性回归的成本函数,以理解回归固有的问题,以及我们通过控制系数权重所指的内容:

学习回归与 L2 收缩 – 岭回归学习回归与 L2 收缩 – 岭回归

线性回归试图找到系数w0…wm,使得它最小化上述方程。线性回归存在一些问题。

如果数据集包含许多相关的预测变量,数据的非常小的变化可能导致模型不稳定。此外,我们还会面临解释模型结果的问题。例如,如果我们有两个负相关的变量,这些变量将对响应变量产生相反的影响。我们可以手动查看这些相关性,移除其中一个有影响的变量,然后继续构建模型。然而,如果我们能够自动处理这些场景,那将更有帮助。

我们在之前的例子中介绍了一种方法,叫做递归特征消除,用来保留最具信息性的属性并丢弃其余的属性。然而,在这种方法中,我们要么保留一个变量,要么不保留它;我们的决策是二元的。在这一部分,我们将看到一种方法,能够以控制变量权重的方式,强烈惩罚不必要的变量,使它们的权重降到极低。

我们将修改线性回归的成本函数,以包含系数。如你所知,成本函数的值应该在最佳模型时达到最小。通过将系数包含在成本函数中,我们可以对取非常高值的系数进行严重惩罚。一般来说,这些技术被称为收缩方法,因为它们试图收缩系数的值。在这个例子中,我们将看到 L2 收缩,通常称为岭回归。让我们看看岭回归的成本函数:

学习回归与 L2 收缩 – 岭回归

如你所见,系数的平方和被加入到成本函数中。这样,当优化程序试图最小化上述函数时,它必须大幅减少系数的值才能达到目标。α参数决定了收缩的程度。α值越大,收缩越大。系数值会被减小至零。

在了解了这些数学背景后,让我们进入实际操作,看看岭回归的应用。

准备工作

再次使用波士顿数据集来演示岭回归。波士顿数据集有 13 个属性和 506 个实例。目标变量是一个实数,房屋的中位数价格在几千美元之间。有关波士顿数据集的更多信息,请参考以下 UCI 链接:

archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.names

我们打算生成二次的多项式特征,并仅考虑交互效应。在本教程的最后,我们将看到系数如何受到惩罚。

如何操作……

我们将首先加载所有必要的库。接下来,我们将定义第一个函数get_data()。在这个函数中,我们将读取波士顿数据集,并将其返回为预测变量x和响应变量y

# Load libraries
from sklearn.datasets import load_boston
from sklearn.cross_validation import train_test_split
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import PolynomialFeatures

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    x    = x - np.mean(x,axis=0)

    return x,y    

在我们的下一个build_model函数中,我们将使用给定的数据构建岭回归模型。以下两个函数,view_modelmodel_worth,用于检查我们构建的模型:

def build_model(x,y):
    """
    Build a Ridge regression model
    """
    model = Ridge(normalize=True,alpha=0.015)
    model.fit(x,y)
    # Track the scores- Mean squared residual for plot
    return model    

def view_model(model):
    """
    Look at model coeffiecients
    """
    print "\n Model coeffiecients"
    print "======================\n"
    for i,coef in enumerate(model.coef_):
        print "\tCoefficient %d  %0.3f"%(i+1,coef)

    print "\n\tIntercept %0.3f"%(model.intercept_)

def model_worth(true_y,predicted_y):
    """
    Evaluate the model
    """
    print "\tMean squared error = %0.2f"%(mean_squared_error(true_y,predicted_y))
    return mean_squared_error(true_y,predicted_y)

最后,我们将编写我们的main函数,该函数用于调用所有前面的函数:

if __name__ == "__main__":

    x,y = get_data()

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    #Prepare some polynomial features
    poly_features = PolynomialFeatures(interaction_only=True)
    poly_features.fit(x_train)
    x_train_poly = poly_features.transform(x_train)
    x_dev_poly   = poly_features.transform(x_dev)
    x_test_poly = poly_features.transform(x_test)

    #choosen_model,choosen_subset,low_mse = subset_selection(x_train_poly,y_train)    
    choosen_model = build_model(x_train_poly,y_train)

    predicted_y = choosen_model.predict(x_train_poly)
    print "\n Model Performance in Training set (Polynomial features)\n"
    mse  = model_worth(y_train,predicted_y)  
    view_model(choosen_model)

    # Apply the model on dev set
    predicted_y = choosen_model.predict(x_dev_poly)
    print "\n Model Performance in Dev set  (Polynomial features)\n"
    model_worth(y_dev,predicted_y)  

    # Apply the model on Test set
    predicted_y = choosen_model.predict(x_test_poly)

    print "\n Model Performance in Test set  (Polynomial features)\n"
    model_worth(y_test,predicted_y)  

它是如何工作的……

让我们从主模块开始,并跟随代码。我们使用get_data函数加载了预测变量x和响应变量y。该函数调用了 scikit-learn 的便捷函数load_boston(),将波士顿房价数据集作为 NumPy 数组获取。

接下来,我们将使用 scikit-learn 库中的train_test_split函数将数据集划分为训练集和测试集。我们将保留 30%的数据集用于测试。在下一行,我们将从中提取开发集。

然后,我们将构建多项式特征:

poly_features = PolynomialFeatures(interaction_only=True)
poly_features.fit(x_train)

如你所见,我们将interaction_only设置为true。通过将interaction_only设置为true,对于x1x2属性,仅创建x1*x2属性,而不创建x1x2的平方,假设度数为 2。默认的度数为 2:

x_train_poly = poly_features.transform(x_train)
x_dev_poly = poly_features.transform(x_dev)
x_test_poly = poly_features.transform(x_test)

使用transform函数,我们将转换我们的训练集、开发集和测试集,以包含多项式特征。

在下一行,我们将通过调用build_model方法使用训练数据集构建我们的岭回归模型:

model = Ridge(normalize=True,alpha=0.015)
model.fit(x,y)

数据集中的属性通过其均值进行中心化,并使用标准差进行标准化,方法是使用normalize参数并将其设置为trueAlpha控制收缩的程度,其值设置为0.015。我们不是凭空得出这个数字的,而是通过多次运行模型得出的。稍后在本章中,我们将看到如何通过经验得出此参数的正确值。我们还将使用fit_intercept参数来拟合该模型的截距。然而,默认情况下,fit_intercept参数设置为true,因此我们不会显式指定它。

现在让我们看看模型在训练集中的表现。我们将调用 model_worth 方法来获取均方误差。此方法需要预测的响应变量和实际的响应变量,以返回均方误差:

predicted_y = choosen_model.predict(x_train_poly)
print "\n Model Performance in Training set (Polynomial features)\n"
mse = model_worth(y_train,predicted_y) 

我们的输出如下所示:

它是如何工作的…

在将模型应用于测试集之前,让我们先看看系数的权重。我们将调用一个名为 view_model 的函数来查看系数的权重:

view_model(choosen_model)

它是如何工作的…

我们没有展示所有的系数,共有 92 个。然而,从一些系数来看,收缩效应应该是显而易见的。例如,系数 1 几乎为 0(请记住,它是一个非常小的值,这里仅显示了前三位小数)。

让我们继续看看模型在开发集中的表现:

predicted_y = choosen_model.predict(x_dev_poly)
print "\n Model Performance in Dev set (Polynomial features)\n"
model_worth(y_dev,predicted_y) 

它是如何工作的…

不错,我们已经达到了比训练误差更低的均方误差。最后,让我们来看一下我们模型在测试集上的表现:

它是如何工作的…

与前面配方中的线性回归模型相比,我们在测试集上的表现更好。

还有更多…

我们之前提到过,线性回归模型对数据集中的任何小变化都非常敏感。让我们看一个小例子,来演示这一点:

# Load libraries
from sklearn.datasets import load_boston
from sklearn.cross_validation import train_test_split
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import PolynomialFeatures

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    x    = x - np.mean(x,axis=0)

    return x,y    

在这段代码中,我们将使用 build_model 函数对原始数据进行线性回归和岭回归模型的拟合:

lin_model,ridg_model = build_model(x,y)

我们将在原始数据中引入少量噪声,具体如下:

# Add some noise to the dataset
noise = np.random.normal(0,1,(x.shape))
x = x + noise

我们将再次在噪声数据集上拟合模型。最后,我们将比较系数的权重:

还有更多…

在添加了少量噪声后,当我们尝试使用线性回归拟合模型时,分配的权重与前一个模型分配的权重非常不同。现在,让我们看看岭回归的表现:

还有更多…

权重在第一个和第二个模型之间没有发生剧烈变化。希望这可以展示岭回归在噪声数据条件下的稳定性。

选择合适的 alpha 值总是很棘手。一个粗暴的方法是通过多个值来运行,并跟踪系数的路径。通过路径,选择一个系数变化不剧烈的 alpha 值。我们将使用 coeff_path 函数绘制系数的权重。

我们来看看 coeff_path 函数。它首先生成一组 alpha 值:

alpha_range = np.linspace(10,100.2,300)

在这种情况下,我们生成了从 10 到 100 之间均匀分布的 300 个数字。对于这些 alpha 值中的每一个,我们将构建一个模型并保存其系数:

for alpha in alpha_range:
    model = Ridge(normalize=True,alpha=alpha)
    model.fit(x,y)
    coeffs.append(model.coef_)

最后,我们将绘制这些系数权重与 alpha 值的关系图:

还有更多…

如您所见,值在 alpha 值为 100 附近趋于稳定。您可以进一步缩放到接近 100 的范围,并寻找一个理想的值。

另见

  • 在第七章中的使用回归预测实数值方法,机器学习 II

  • 在第三章中的数据缩放方法,数据分析 – 探索与整理

  • 在第三章中的标准化数据方法,数据分析 – 探索与整理

  • 在第六章中的准备数据以构建模型方法,机器学习 I

使用 L1 收缩学习回归 – LASSO

最小绝对收缩与选择算子LASSO)是另一种常用的回归问题收缩方法。与岭回归相比,LASSO 能得到稀疏解。所谓稀疏解,指的是大部分系数被缩减为零。在 LASSO 中,很多系数被设为零。对于相关变量,LASSO 只选择其中一个,而岭回归则为两个变量的系数分配相等的权重。因此,LASSO 的这一特性可以用来进行变量选择。在这个方法中,让我们看看如何利用 LASSO 进行变量选择。

让我们看一下 LASSO 回归的代价函数。如果你已经跟随前两个方法,你应该能很快识别出区别:

学习使用 L1 收缩的回归 – LASSO

系数会受到系数绝对值之和的惩罚。再次强调,alpha 控制惩罚的程度。我们来尝试理解为什么 L1 收缩会导致稀疏解的直观原因。

我们可以将前面的方程重写为一个无约束的代价函数和一个约束,如下所示:

最小化:

学习使用 L1 收缩的回归 – LASSO

受约束条件的影响:

学习使用 L1 收缩的回归 – LASSO

记住这个方程,我们在系数空间中绘制w0w1的代价函数值:

学习使用 L1 收缩的回归 – LASSO

蓝色线条表示不同w0w1值下代价函数(无约束)的等高线。绿色区域表示由 eta 值决定的约束形状。两个区域交汇的优化值是w0设为 0 时的情况。我们展示了一个二维空间,在这个空间中,通过将w0设为 0,我们的解变得稀疏。在多维空间中,我们将有一个菱形区域,LASSO 通过将许多系数缩减为零,给出一个稀疏解。

准备开始

我们将再次使用波士顿数据集来演示 LASSO 回归。波士顿数据集有 13 个属性和 506 个实例。目标变量是一个实数,房屋的中位数价值在千元范围内。

有关更多波士顿数据集的信息,请参考以下 UCI 链接:

archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.names

我们将看到如何使用 LASSO 进行变量选择。

如何操作……

我们将首先加载所有必要的库。接下来我们定义我们的第一个函数get_data()。在此函数中,我们将读取波士顿数据集,并将其作为预测变量x和响应变量y返回:

# Load libraries
from sklearn.datasets import load_boston
from sklearn.cross_validation import train_test_split
from sklearn.linear_model import Lasso, LinearRegression
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
import numpy as np

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    return x,y    

在我们接下来的build_model函数中,我们将使用给定的数据构建 LASSO 回归模型。接下来的两个函数,view_modelmodel_worth,用于检查我们构建的模型:

def build_models(x,y):
    """
    Build a Lasso regression model
    """
    # Alpha values uniformly
    # spaced between 0.01 and 0.02
    alpha_range = np.linspace(0,0.5,200)
    model = Lasso(normalize=True)
    coeffiecients = []
    # Fit a model for each alpha value
    for alpha in alpha_range:
        model.set_params(alpha=alpha)
        model.fit(x,y)
        # Track the coeffiecients for plot
        coeffiecients.append(model.coef_)
    # Plot coeffients weight decay vs alpha value
    # Plot model RMSE vs alpha value
    coeff_path(alpha_range,coeffiecients)
    # View coeffiecient value
    #view_model(model)

def view_model(model):
    """
    Look at model coeffiecients
    """
    print "\n Model coeffiecients"
    print "======================\n"
    for i,coef in enumerate(model.coef_):
        print "\tCoefficient %d  %0.3f"%(i+1,coef)

    print "\n\tIntercept %0.3f"%(model.intercept_)

def model_worth(true_y,predicted_y):
    """
    Evaluate the model
    """
    print "\t Mean squared error = %0.2f\n"%(mean_squared_error(true_y,predicted_y))

我们将定义两个函数,coeff_pathget_coeff,来检查我们的模型系数。coeff_path函数由build_model函数调用,用于绘制不同 alpha 值下的系数权重。get_coeff函数则由主函数调用:

def coeff_path(alpha_range,coeffiecients):
    """
    Plot residuals
    """
    plt.close('all')
    plt.cla()

    plt.figure(1)
    plt.xlabel("Alpha Values")
    plt.ylabel("Coeffiecient Weight")
    plt.title("Coeffiecient weights for different alpha values")
    plt.plot(alpha_range,coeffiecients)
    plt.axis('tight')

    plt.show()

def get_coeff(x,y,alpha):
    model = Lasso(normalize=True,alpha=alpha)
    model.fit(x,y)
    coefs = model.coef_
    indices = [i for i,coef in enumerate(coefs) if abs(coef) > 0.0]
    return indices

最后,我们将编写我们的main函数,用于调用之前所有的函数:

if __name__ == "__main__":

    x,y = get_data()
    # Build multiple models for different alpha values
    # and plot them    
    build_models(x,y)
    print "\nPredicting using all the variables"
    full_model = LinearRegression(normalize=True)
    full_model.fit(x,y)
    predicted_y = full_model.predict(x)
    model_worth(y,predicted_y)    

    print "\nModels at different alpha values\n"
    alpa_values = [0.22,0.08,0.01]
    for alpha in alpa_values:

        indices = get_coeff(x,y,alpha)   
        print "\t alpah =%0.2f Number of variables selected = %d "%(alpha,len(indices))
        print "\t attributes include ", indices
        x_new = x[:,indices]
        model = LinearRegression(normalize=True)
        model.fit(x_new,y)
        predicted_y = model.predict(x_new)
        model_worth(y,predicted_y)

它是如何工作的……

让我们从主模块开始,跟随代码进行。我们将使用get_data函数加载预测变量x和响应变量y。该函数调用了 scikit-learn 提供的便捷load_boston()函数,从而将波士顿房价数据集作为 NumPy 数组载入。

我们将继续调用build_models。在build_models中,我们将为不同的alpha值构建多个模型:

alpha_range = np.linspace(0,0.5,200)
model = Lasso(normalize=True)
coeffiecients = []
# Fit a model for each alpha value
for alpha in alpha_range:
model.set_params(alpha=alpha)
model.fit(x,y)
# Track the coeffiecients for plot
coeffiecients.append(model.coef_)

正如你所看到的,在 for 循环中,我们还将不同 alpha 值的系数值存储在一个列表中。

让我们通过调用coeff_path函数来绘制不同 alpha 值下的系数值:

plt.close('all')
plt.cla()

plt.figure(1)
plt.xlabel("Alpha Values")
plt.ylabel("Coeffiecient Weight")
plt.title("Coeffiecient weights for different alpha values")
plt.plot(alpha_range,coeffiecients)
plt.axis('tight')
plt.show()

x轴上,你可以看到我们有 alpha 值,而在y轴上,我们将为给定的 alpha 值绘制所有系数。让我们看看输出的图表:

它是如何工作的……

不同颜色的线条代表不同的系数值。正如你所看到的,随着 alpha 值的增大,系数权重趋向于零。从这个图表中,我们可以选择 alpha 的值。

作为参考,我们先拟合一个简单的线性回归模型:

print "\nPredicting using all the variables"
full_model = LinearRegression(normalize=True)
full_model.fit(x,y)
predicted_y = full_model.predict(x)
model_worth(y,predicted_y) 

让我们看看当我们尝试使用新构建的模型进行预测时的均方误差:

它是如何工作的……

让我们继续根据 LASSO 来选择系数:

print "\nModels at different alpha values\n"
alpa_values = [0.22,0.08,0.01]
for alpha in alpa_values:
indices = get_coeff(x,y,alpha) 

根据之前的图表,我们选择了0.220.080.01作为 alpha 值。在循环中,我们将调用get_coeff方法。该方法使用给定的 alpha 值拟合 LASSO 模型,并仅返回非零系数的索引:

model = Lasso(normalize=True,alpha=alpha)
model.fit(x,y)
coefs = model.coef_

indices = [i for i,coef in enumerate(coefs) if abs(coef) > 0.0]

本质上,我们只选择那些系数值非零的属性——特征选择。让我们回到我们的for循环,在那里我们将使用减少后的系数拟合线性回归模型:

print "\t alpah =%0.2f Number of variables selected = %d "%(alpha,len(indices))
print "\t attributes include ", indices
x_new = x[:,indices]
model = LinearRegression(normalize=True)
model.fit(x_new,y)
predicted_y = model.predict(x_new)
model_worth(y,predicted_y)

我们想要了解的是,如果我们使用减少后的属性集来预测模型,与使用整个数据集最初构建的模型相比,模型的效果如何:

它是如何工作的……

看看我们的第一次尝试,alpha 值为0.22时。只有两个系数的非零值,分别为512。均方误差为30.51,仅比使用所有变量拟合的模型多了9

类似地,对于0.08的 alpha 值,存在三个非零系数。我们可以看到均方误差有所改善。最后,对于0.01的 alpha 值,13 个属性中有 9 个被选择,且均方误差非常接近使用所有属性构建的模型。

如你所见,我们并没有使用所有属性来拟合模型。我们能够使用 LASSO 自动选择一部分属性子集。因此,我们已经看到了 LASSO 如何用于变量选择。

还有更多内容……

通过保留最重要的变量,LASSO 避免了过拟合。然而,如你所见,均方误差值并不理想。我们可以看到,LASSO 导致了预测能力的损失。

如前所述,对于相关变量,LASSO 只选择其中一个,而岭回归则对两个变量的系数赋予相等的权重。因此,与 LASSO 相比,岭回归具有更高的预测能力。然而,LASSO 能够进行变量选择,而岭回归则不能。

注意

请参考Trevor Hastie 等人的《稀疏统计学习:Lasso 与泛化》一书,了解更多关于 LASSO 和岭回归方法的信息。

另见:

  • 在第三章中,数据标准化的内容,数据分析 – 探索与清洗

  • 在第三章中,数据标准化的内容,数据分析 – 探索与清洗

  • 在第六章中,模型构建数据准备的内容,机器学习 I

  • 在第七章中,L2 收缩回归 – 岭回归的内容,机器学习 II

使用交叉验证迭代器与 L1 和 L2 收缩

在上一章中,我们介绍了将数据划分为训练集和测试集的方法。在接下来的步骤中,我们再次对测试数据集进行划分,以得到开发数据集。其目的是将测试集从模型构建周期中分离出来。然而,由于我们需要不断改进模型,因此我们使用开发集来测试每次迭代中的模型准确性。虽然这是一个好的方法,但如果数据集不是很大,实施起来会比较困难。我们希望尽可能多地提供数据来训练模型,但仍然需要保留部分数据用于评估和最终测试。在许多实际场景中,获取一个非常大的数据集是非常罕见的。

在这个示例中,我们将看到一种叫做交叉验证的方法,帮助我们解决这个问题。这个方法通常被称为 k 折交叉验证。训练集被分成 k 个折叠。模型在 K-1(K 减 1)个折叠上进行训练,剩下的折叠用来测试。这样,我们就不需要单独的开发数据集了。

让我们看看 scikit-learn 库提供的一些迭代器,以便有效地执行 k 折交叉验证。掌握了交叉验证的知识后,我们将进一步了解如何利用交叉验证选择收缩方法中的 alpha 值。

准备工作

我们将使用鸢尾花数据集来演示各种交叉验证迭代器的概念。接着,我们会回到波士顿住房数据集,演示如何利用交叉验证成功找到收缩方法中的理想 alpha 值。

如何实现…

让我们看看如何使用交叉验证迭代器:

from sklearn.datasets import load_iris
from sklearn.cross_validation import KFold,StratifiedKFold

def get_data():
    data = load_iris()
    x = data['data']
    y = data['target']
    return x,y

def class_distribution(y):
        class_dist = {}
        total = 0
        for entry in y:
            try:
                class_dist[entry]+=1
            except KeyError:
                class_dist[entry]=1
            total+=1

        for k,v in class_dist.items():
            print "\tclass %d percentage =%0.2f"%(k,v/(1.0*total))

if __name__ == "__main__":
    x,y = get_data()
    # K Fold
    # 3 folds
    kfolds = KFold(n=y.shape[0],n_folds=3)
    fold_count  =1
    print
    for train,test in kfolds:
        print "Fold %d x train shape"%(fold_count),x[train].shape,\
        " x test shape",x[test].shape
        fold_count==1
    print
    #Stratified KFold
    skfolds = StratifiedKFold(y,n_folds=3)
    fold_count  =1
    for train,test in skfolds:
        print "\nFold %d x train shape"%(fold_count),x[train].shape,\
        " x test shape",x[test].shape
        y_train = y[train]
        y_test  = y[test]
        print "Train Class Distribution"
        class_distribution(y_train)
        print "Test Class Distribution"
        class_distribution(y_test)

        fold_count+=1

    print

在我们的主函数中,我们将调用get_data函数来加载鸢尾花数据集。然后我们将演示一个简单的 k 折和分层 k 折交叉验证。

通过掌握 k 折交叉验证的知识,让我们写一个食谱,利用这些新获得的知识来提升岭回归:

# Load libraries
from sklearn.datasets import load_boston
from sklearn.cross_validation import KFold,train_test_split
from sklearn.linear_model import Ridge
from sklearn.grid_search import GridSearchCV
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import PolynomialFeatures
import numpy as np

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    return x,y    

我们将首先加载所有必要的库。接下来,我们将定义我们的第一个函数get_data()。在这个函数中,我们将读取波士顿数据集,并将其返回为预测变量x和响应变量y

在我们的下一个build_model函数中,我们将使用给定的数据构建岭回归模型,并利用 k 折交叉验证。

以下两个函数,view_modelmodel_worth,用于 introspect(内省)我们构建的模型。

最后,我们将编写display_param_results函数,以查看每个折叠中的模型误差:

def build_model(x,y):
    """
    Build a Ridge regression model
    """
    kfold = KFold(y.shape[0],5)
    model = Ridge(normalize=True)

    alpha_range = np.linspace(0.0015,0.0017,30)
    grid_param = {"alpha":alpha_range}
    grid = GridSearchCV(estimator=model,param_grid=grid_param,cv=kfold,scoring='mean_squared_error')
    grid.fit(x,y)
    display_param_results(grid.grid_scores_)
    print grid.best_params_
    # Track the scores- Mean squared residual for plot
    return grid.best_estimator_

def view_model(model):
    """
    Look at model coeffiecients
    """
    #print "\n Estimated Alpha = %0.3f"%model.alpha_
    print "\n Model coeffiecients"
    print "======================\n"
    for i,coef in enumerate(model.coef_):
        print "\tCoefficient %d  %0.3f"%(i+1,coef)

    print "\n\tIntercept %0.3f"%(model.intercept_)

def model_worth(true_y,predicted_y):
    """
    Evaluate the model
    """
    print "\tMean squared error = %0.2f"%(mean_squared_error(true_y,predicted_y))
    return mean_squared_error(true_y,predicted_y)

def display_param_results(param_results):
    fold = 1
    for param_result in param_results:
        print "Fold %d Mean squared error %0.2f"%(fold,abs(param_result[1])),param_result[0]
        fold+=1

最后,我们将编写我们的main函数,用来调用所有前面的函数:

if __name__ == "__main__":

    x,y = get_data()

    # Divide the data into Train and test    
    x_train,x_test,y_train,y_test = train_test_split(x,y,test_size = 0.3,random_state=9)

    #Prepare some polynomial features
    poly_features = PolynomialFeatures(interaction_only=True)
    poly_features.fit(x_train)
    x_train_poly = poly_features.transform(x_train)
    x_test_poly  = poly_features.transform(x_test)

    choosen_model = build_model(x_train_poly,y_train)
    predicted_y = choosen_model.predict(x_train_poly)
    model_worth(y_train,predicted_y)

    view_model(choosen_model)

    predicted_y = choosen_model.predict(x_test_poly)
    model_worth(y_test,predicted_y)

工作原理…

让我们从主方法开始。我们将从KFold类开始。这个迭代器类是通过实例化数据集中实例的数量和我们所需的折数来创建的:

kfolds = KFold(n=y.shape[0],n_folds=3)

现在,我们可以按以下方式遍历这些折叠:

fold_count =1
print
for train,test in kfolds:
print "Fold %d x train shape"%(fold_count),x[train].shape,\
" x test shape",x[test].shape
fold_count==1

让我们看看打印语句的输出:

工作原理…

我们可以看到数据被分成三部分,每部分有 100 个实例用于训练,50 个实例用于测试。

接下来,我们将转到StratifiedKFold。回想一下我们在上一章中讨论的关于训练集和测试集中的类分布均匀的问题。StratifiedKFold在三个折叠中实现了均匀的类分布。

它是这样调用的:

skfolds = StratifiedKFold(y,n_folds=3)

由于它需要知道数据集中类标签的分布,因此这个迭代器对象将响应变量y作为其参数之一。另一个参数是请求的折叠数。

让我们打印出这三折数据集中训练集和测试集的形状,并查看它们的类分布。我们将使用class_distribution函数打印每一折中的类分布:

fold_count =1
for train,test in skfolds:
print "\nFold %d x train shape"%(fold_count),x[train].shape,\
" x test shape",x[test].shape
y_train = y[train]
y_test = y[test]
print "Train Class Distribution"
class_distribution(y_train)
print "Test Class Distribution"
class_distribution(y_test)

fold_count+=1

How it works…

你可以看到类的分布是均匀的。

假设你构建了一个五折交叉验证的数据集,拟合了五个不同的模型,并得到了五个不同的准确度分数。现在,你可以取这些分数的平均值来评估模型的好坏。如果你不满意,你可以继续调整参数,重新构建模型,再在五折数据上运行并查看平均准确度分数。这样,你可以通过仅使用训练数据集找到合适的参数值,不断改进模型。

掌握了这些知识后,我们来重新审视我们之前的岭回归问题。

让我们从main模块开始,跟着代码走。我们将使用get_data函数加载预测变量x和响应变量y。该函数调用 scikit-learn 便捷的load_boston()函数,将波士顿房价数据集作为 NumPy 数组提取出来。

接下来,我们将使用 scikit-learn 库中的train_test_split函数将数据分割为训练集和测试集。我们将保留 30%的数据集用于测试。

然后我们将构建多项式特征:

poly_features = PolynomialFeatures(interaction_only=True)
poly_features.fit(x_train)

如你所见,我们将interaction_only设置为true。通过将interaction_only设置为true——并配合x1x2属性——只会创建x1*x2属性。x1x2的平方不会被创建,假设度数为二。默认度数为二:

x_train_poly = poly_features.transform(x_train)
x_test_poly = poly_features.transform(x_test)

使用transform函数,我们将转换训练集和测试集,以包含多项式特征。我们来调用build_model函数。在build_model函数中,我们首先注意到的是 k 折交叉验证的声明。我们将在此应用交叉验证的知识,并创建一个五折数据集:

kfold = KFold(y.shape[0],5)

然后我们将创建我们的岭回归对象:

model = Ridge(normalize=True)

现在让我们看看如何利用 k 折交叉验证来找出岭回归的理想 alpha 值。在接下来的行中,我们将使用GridSearchCV创建一个对象:

grid = GridSearchCV(estimator=model,param_grid=grid_param,cv=kfold,scoring='mean_squared_error')

GridSearchCV是 scikit-learn 中的一个便捷函数,帮助我们使用一系列参数训练模型。在这种情况下,我们希望找到理想的 alpha 值,因此我们想用不同的 alpha 值训练我们的模型。让我们看看传递给GridSearchCV的参数:

估算器:这是应该使用给定参数和数据运行的模型类型。在我们的例子中,我们希望运行岭回归。因此,我们将创建一个岭回归对象,并将其传递给GridSearchCV

参数网格:这是我们希望用来评估模型的参数字典。让我们详细处理这个问题。我们将首先声明要用于构建模型的 alpha 值范围:

alpha_range = np.linspace(0.0015,0.0017,30)

这给我们一个 NumPy 数组,包含 30 个均匀间隔的元素,起始于 0.0015,结束于 0.0017。我们希望为每个这些值构建一个模型。我们将创建一个名为grid_param的字典对象,并在 alpha 键下添加生成的 NumPy 数组作为 alpha 值:

grid_param = {"alpha":alpha_range}

我们将把这个字典作为参数传递给GridSearchCV。看一下条目param_grid=grid_param

Cv:这定义了我们感兴趣的交叉验证类型。我们将传递之前创建的 k 折(五折)迭代器作为 cv 参数。

最后,我们需要定义一个评分函数。在我们的案例中,我们关注的是求得平方误差。这是我们用来评估模型的指标。

因此,内部的GridSearchCV将为我们的每个参数值构建五个模型,并在排除的折中测试时返回平均分数。在我们的案例中,我们有五个测试折,因此会返回这五个测试折中分数的平均值。

解释完这些后,我们将开始拟合我们的模型,也就是说,启动网格搜索活动。

最后,我们想查看不同参数设置下的输出。我们将使用display_param_results函数来显示不同折中的平均均方误差:

它是如何工作的…

输出的每一行显示了参数 alpha 值和来自测试折的平均均方误差。我们可以看到,当我们深入到 0.0016 的范围时,均方误差在增加。因此,我们决定停在 0.0015。我们可以查询网格对象以获取最佳参数和估计器:

print grid.best_params_
return grid.best_estimator_

这不是我们测试的第一组 alpha 值。我们最初的 alpha 值如下:

alpha_range = np.linspace(0.01,1.0,30)

以下是我们的输出:

它是如何工作的…

当我们的 alpha 值超过 0.01 时,均方误差急剧上升。因此,我们又给出了一个新的范围:

alpha_range = np.linspace(0.001,0.1,30)

我们的输出如下:

它是如何工作的…

这样,我们通过迭代的方式得到了一个范围,从 0.0015 开始,到 0.0017 结束。

然后,我们将从我们的网格搜索中获取最佳估计器,并将其应用于我们的训练和测试数据:

choosen_model = build_model(x_train_poly,y_train)
predicted_y = choosen_model.predict(x_train_poly)
model_worth(y_train,predicted_y)

我们的model_worth函数打印出我们训练数据集中的均方误差值:

它是如何工作的…

让我们查看我们的系数权重:

它是如何工作的…

我们没有显示所有的系数,但当你运行代码时,可以查看所有的值。

最后,让我们将模型应用到我们的测试数据集:

它是如何工作的…

因此,我们通过交叉验证和网格搜索成功地得出了岭回归的 alpha 值。与岭回归配方中的值相比,我们的模型实现了较低的均方误差。

还有更多内容……

scikit-learn 提供了其他交叉验证迭代器。在这种情况下,特别值得关注的是留一法迭代器。你可以在scikit-learn.org/stable/modules/cross_validation.html#leave-one-out-loo上阅读更多关于它的信息。

在这种方法中,给定折数,它会留出一条记录进行测试,并将其余的记录用于训练。例如,如果你的输入数据有 100 个实例,并且我们要求五折交叉验证,那么每一折中我们将有 99 个实例用于训练,一个实例用于测试。

在我们之前使用的网格搜索方法中,如果没有为交叉验证cv)参数提供自定义迭代器,它将默认使用留一法交叉验证方法:

grid = GridSearchCV(estimator=model,param_grid=grid_param,cv=None,scoring='mean_squared_error')

另见

  • 第三章中的数据缩放方法,数据分析 – 探索与整理

  • 第三章中的标准化数据方法,数据分析 – 探索与整理

  • 第六章中的为模型构建准备数据方法,机器学习 I

  • 第七章中的L2 缩减回归 – 岭回归方法,机器学习 II

  • 第七章中的L2 缩减回归 – Lasso方法,机器学习 II

第八章 集成方法

在本章中,我们将讨论以下内容:

  • 理解集成方法——袋装法(Bagging)

  • 理解集成方法——提升法(Boosting),AdaBoost

  • 理解集成方法——梯度提升(Gradient Boosting)

介绍

在本章中,我们将讨论涉及集成方法的内容。当我们在现实生活中面临不确定性决策时,通常会向多个朋友寻求意见。我们根据从这些朋友那里获得的集体知识做出决策。集成方法在机器学习中的概念类似。前几章中,我们为我们的数据集构建了单一模型,并使用该模型对未见过的测试数据进行预测。如果我们在这些数据上构建多个模型,并根据所有这些模型的预测结果做出最终预测,会怎么样呢?这就是集成方法的核心思想。使用集成方法解决某个问题时,我们会构建多个模型,并利用它们所有的预测结果来对未见过的数据集做出最终预测。对于回归问题,最终输出可能是所有模型预测值的平均值。在分类问题中,则通过多数投票来决定输出类别。

基本思想是拥有多个模型,每个模型在训练数据集上产生略有不同的结果。有些模型比其他模型更好地学习数据的某些方面。其信念是,所有这些模型的最终输出应该优于任何单一模型的输出。

如前所述,集成方法的核心是将多个模型结合在一起。这些模型可以是相同类型的,也可以是不同类型的。例如,我们可以将神经网络模型的输出与贝叶斯模型的输出结合在一起。本章中我们将限制讨论使用相同类型的模型进行集成。通过像袋装法(Bagging)和提升法(Boosting)这样的技术,数据科学社区广泛使用相同类型模型的集成方法。

自助聚合(Bootstrap aggregation),通常称为袋装法(Bagging),是一种优雅的技术,用于生成大量模型并将它们的输出结合起来以做出最终预测。袋装法中的每个模型只使用部分训练数据。袋装法的核心思想是减少数据的过拟合。如前所述,我们希望每个模型与其他模型略有不同。因此,我们对每个模型的训练数据进行有放回的抽样,从而引入了变异性。引入模型变异的另一种方式是对属性进行抽样。我们不会将所有属性提供给模型,而是让不同的模型获得不同的属性集。袋装法可以轻松并行化。基于现有的并行处理框架,可以在不同的训练数据子集上并行构建模型。袋装法不适用于线性预测器,如线性回归。

Boosting 是一种集成技术,能够产生一系列逐渐复杂的模型。它是按顺序工作的,通过基于前一个模型的错误来训练新的模型。每一个训练出来的模型都会有一个与之关联的权重,这个权重是根据该模型在给定数据上表现的好坏来计算的。当最终做出预测时,这些权重决定了某个特定模型对最终结果的影响程度。与 Bagging 不同,Boosting 不太容易进行并行化处理。由于模型是按序列构建的,它无法并行化处理。序列中较早的分类器所犯的错误被视为难以分类的实例。该框架的设计是让序列中后续的模型拾取前一个预测器所犯的错误或误分类,并尝试改进它们。Boosting 通常使用非常弱的分类器,例如决策树桩,即一个只有一个分裂节点和两个叶子的决策树,作为集成中的一部分。关于 Boosting 的一个非常著名的成功案例是 Viola-Jones 人脸检测算法,其中多个弱分类器(决策树桩)被用来找出有效特征。你可以在以下网站上查看更多关于该成功案例的内容:

en.wikipedia.org/wiki/Viola%E2%80%93Jones_object_detection_framework

在本章中,我们将详细研究 Bagging 和 Boosting 方法。我们将在最后的配方中扩展讨论一种特殊类型的 Boosting,叫做梯度 Boosting。我们还将探讨回归和分类问题,并看看如何通过集成学习来解决这些问题。

理解集成学习—Bagging 方法

集成方法属于一种叫做基于委员会学习的方法家族。与其将分类或回归的决策交给单一模型,不如通过一组模型来共同决策。Bagging 是一种著名且广泛使用的集成方法。

Bagging 也叫做自助聚合。只有当我们能够在基础模型中引入变异性时,Bagging 才能发挥作用。也就是说,如果我们能够成功地在基础数据集中引入变异性,就会导致具有轻微变化的模型。

我们利用自助采样为这些模型引入数据集的变异性。自助采样是通过随机抽取给定数据集中的实例,指定次数地进行采样,且可以选择是否有放回。 在 Bagging 中,我们通过自助采样生成 m 个不同的数据集,并为每个数据集构建一个模型。最后,我们对所有模型的输出进行平均,来产生回归问题中的最终预测结果。

假设我们对数据进行 m 次自助采样,我们将得到 m 个模型,也就是 y m 个值,最终的预测结果如下:

理解集成学习 – Bagging 方法

对于分类问题,最终的输出是通过投票决定的。假设我们的集成模型中有一百个模型,并且我们有一个二类分类问题,类别标签为{+1,-1}。如果超过 50 个模型预测输出为+1,我们就将预测结果判定为+1。

随机化是另一种可以在模型构建过程中引入变化的技术。例如,可以随机选择每个集成模型的一个属性子集。这样,不同的模型将拥有不同的属性集。这种技术叫做随机子空间方法。

对于非常稳定的模型,Bagging 可能无法取得很好的效果。如果基础分类器对数据的微小变化非常敏感,Bagging 则会非常有效。例如,决策树非常不稳定,未剪枝的决策树是 Bagging 的良好候选模型。但是,如果是一个非常稳定的模型,比如最近邻分类器 K,我们可以利用随机子空间方法,为最近邻方法引入一些不稳定性。

在接下来的方案中,你将学习如何在 K 最近邻算法中应用 Bagging 和随机子空间方法。我们将解决一个分类问题,最终的预测将基于多数投票。

准备工作…

我们将使用 Scikit learn 中的KNeighborsClassifier进行分类,并使用BaggingClassifier来应用 Bagging 原理。我们将通过make_classification便捷函数生成本方案的数据。

如何实现

让我们导入必要的库,并编写一个get_data()函数,为我们提供数据集,以便我们可以处理这个方案:

from sklearn.datasets import make_classification
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.metrics import classification_report
from sklearn.cross_validation import train_test_split

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    print no_features,redundant_features,informative_features,repeated_features
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

让我们开始编写三个函数:

函数build_single_model用给定数据构建一个简单的 K 最近邻模型。

函数build_bagging_model实现了 Bagging 过程。

函数view_model用于检查我们已经构建的模型:

def build_single_model(x,y):
    model = KNeighborsClassifier()
    model.fit(x,y)
    return model

def build_bagging_model(x,y):
	bagging = BaggingClassifier(KNeighborsClassifier(),n_estimators=100,random_state=9 \
             ,max_samples=1.0,max_features=0.7,bootstrap=True,bootstrap_features=True)
	bagging.fit(x,y)
	return bagging

def view_model(model):
    print "\n Sampled attributes in top 10 estimators\n"
    for i,feature_set in  enumerate(model.estimators_features_[0:10]):
        print "estimator %d"%(i+1),feature_set

最后,我们将编写主函数,调用其他函数:

if __name__ == "__main__":
    x,y = get_data()    

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    # Build a single model    
    model = build_single_model(x_train,y_train)
    predicted_y = model.predict(x_train)
    print "\n Single Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
    # Build a bag of models
    bagging = build_bagging_model(x_train,y_train)
    predicted_y = bagging.predict(x_train)
    print "\n Bagging Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
	view_model(bagging)

    # Look at the dev set
    predicted_y = model.predict(x_dev)
    print "\n Single Model Accuracy on Dev data\n"
    print classification_report(y_dev,predicted_y)

    print "\n Bagging Model Accuracy on Dev data\n"
    predicted_y = bagging.predict(x_dev)
    print classification_report(y_dev,predicted_y)

它是如何工作的…

我们从主方法开始。首先调用get_data函数,返回一个包含预测变量的矩阵 x 和响应变量的向量 y。让我们来看一下get_data函数:

    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
 x,y =make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)

看一下传递给make_classification方法的参数。第一个参数是所需的实例数;在这种情况下,我们说需要 500 个实例。第二个参数是每个实例所需的属性数。我们说我们需要30个属性,这由变量no_features定义。第三个参数,flip_y,会随机交换 3%的实例。这是为了在我们的数据中引入一些噪声。下一个参数指定了这30个特征中应该有多少个是足够信息量大的,可以用于我们的分类。我们指定 60%的特征,也就是 30 个中的 18 个应该是有信息量的。下一个参数是关于冗余特征的。这些特征是由信息性特征的线性组合生成的,用于在特征之间引入相关性。最后,重复特征是从信息性特征和冗余特征中随机抽取的重复特征。

让我们使用train_test_split将数据划分为训练集和测试集。我们保留 30%的数据用于测试:

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

我们再次利用train_test_split将我们的测试数据分为开发集和测试集。

    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

在划分好数据以构建、评估和测试模型后,我们继续构建模型。我们将首先通过调用以下代码构建一个单一模型,使用KNeighborsClassifier

model = build_single_model(x_train,y_train)

在这个函数内部,我们创建了一个KNeighborsClassifier类型的对象,并将我们的数据进行拟合,如下所示:

def build_single_model(x,y):
    model = KNeighborsClassifier()
    model.fit(x,y)
    return model

如前一节所述,KNearestNeighbor是一个非常稳定的算法。让我们看看这个模型的表现。我们在训练数据上执行预测,并查看我们的模型指标:

    predicted_y = model.predict(x_train)
    print "\n Single Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)

classification_report是 Scikit learn 模块中的一个方便函数。它给出一个包含precisionrecallf1-score的表格:

如何工作…

350个实例中,我们的精确度为 87%。有了这个结果,让我们继续构建我们的集成模型:

    bagging = build_bagging_model(x_train,y_train)

我们使用训练数据调用build_bagging_model函数,构建一个分类器集合,如下所示:

def build_bagging_model(x,y):
bagging =             BaggingClassifier(KNeighborsClassifier(),n_estimators=100,random_state=9 \
           ,max_samples=1.0,max_features=0.7,bootstrap=True,bootstrap_features=True)
bagging.fit(x,y)
return bagging

在这个方法中,我们调用了BaggingClassifier类。让我们看一下我们传递给这个类的参数,以便初始化它。

第一个参数是底层估计器或模型。通过传递KNeighborClassifier,我们告诉集成分类器我们想要构建一个由KNearestNeighbor分类器组成的集合。下一个参数指定了我们将构建的估计器的数量。在这种情况下,我们说需要100个估计器。random_state参数是随机数生成器使用的种子。为了在不同的运行中保持一致性,我们将其设置为一个整数值。

我们的下一个参数是 max_samples,指定在从输入数据集进行自助采样时,每个估计器选择的实例数。在这种情况下,我们要求集成程序选择所有实例。

接下来,参数 max_features 指定在为估计器进行自助采样时要包含的属性数量。我们假设只选择 70%的属性。因此,在集成中的每个估计器/模型将使用不同的属性子集来构建模型。这就是我们在上一节中介绍的随机空间方法。该函数继续拟合模型并将模型返回给调用函数。

    bagging = build_bagging_model(x_train,y_train)
    predicted_y = bagging.predict(x_train)
    print "\n Bagging Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)

让我们看看模型的准确性:

How it works…

你可以看到模型指标有了大幅提升。

在测试模型之前,让我们先查看通过调用 view_model 函数分配给不同模型的属性:

    view_model(bagging)

我们打印出前十个模型选择的属性,如下所示:

def view_model(model):
    print "\n Sampled attributes in top 10 estimators\n"
    for i,feature_set in  enumerate(model.estimators_features_[0:10]):
        print "estimator %d"%(i+1),feature_set

How it works…

从结果中可以看出,我们已经几乎随机地为每个估计器分配了属性。通过这种方式,我们为每个估计器引入了变异性。

让我们继续检查我们单一分类器和估计器集合在开发集中的表现:

    # Look at the dev set
    predicted_y = model.predict(x_dev)
    print "\n Single Model Accuracy on Dev data\n"
    print classification_report(y_dev,predicted_y)

    print "\n Bagging Model Accuracy on Dev data\n"
    predicted_y = bagging.predict(x_dev)
    print classification_report(y_dev,predicted_y)

How it works…

正如预期的那样,与单一分类器相比,我们的估计器集合在开发集中的表现更好。

还有更多……

正如我们之前所说,对于分类问题,获得最多票数的标签将被视为最终预测。除了投票方案,我们还可以要求组成模型输出标签的预测概率。最终,可以取这些概率的平均值来决定最终的输出标签。在 Scikit 的情况下,API 文档提供了关于如何执行最终预测的详细信息:

'输入样本的预测类别是通过选择具有最高平均预测概率的类别来计算的。如果基础估计器没有实现predict phobia方法,则会使用投票。'

scikit-learn.org/stable/modules/generated/sklearn.ensemble.BaggingClassifier.html

在上一章中,我们讨论了交叉验证。虽然交叉验证看起来与 Bagging 非常相似,但它们在实际使用中是不同的。在交叉验证中,我们创建 K 折,并根据这些折的模型输出来选择模型的参数,就像我们为岭回归选择 alpha 值一样。这样做主要是为了避免在模型构建过程中暴露我们的测试数据。交叉验证可以用于 Bagging,以确定我们需要向 Bagging 模块添加的估计器数量。

然而,Bagging 的一个缺点是我们失去了模型的可解释性。考虑一个经过剪枝的简单决策树,它很容易解释。但一旦我们有了 100 个这样的模型集合,它就变成了一个黑箱。为了提高准确性,我们牺牲了可解释性。

有关 Bagging 的更多信息,请参阅 Leo Breiman 的以下论文:

Leo Breiman. 1996. Bagging predictors.Mach. Learn.24, 2 (August 1996), 123-140. DOI=10.1023/A:1018054314350 http://dx.doi.org/10.1023/A:1018054314350

另请参见

  • 使用交叉验证迭代器,请参阅第七章,《机器学习 2》

  • 构建决策树解决多类问题,请参阅第六章,《机器学习 1》

理解集成 - 提升方法

Boosting 是一种强大的集成技术。它几乎被用在大多数数据科学应用中。事实上,它是数据科学家工具包中最重要的工具之一。Boosting 技术利用了类似于 Bagging 的一组估计器。但在这里相似性就结束了。在我们深入研究我们的方法之前,让我们快速看一下 Boosting 如何作为一种非常有效的集成技术。

让我们来看一个熟悉的二类分类问题,其中输入是一组预测变量(X),输出是一个响应变量(Y),它可以取01作为值。分类问题的输入表示如下:

理解集成 - 提升方法

分类器的任务是找到一个可以近似的函数:

理解集成 - 提升方法

分类器的误分类率定义为:

理解集成 - 提升方法

假设我们构建了一个非常弱的分类器,其错误率略好于随机猜测。在 Boosting 中,我们在略微修改的数据集上构建了一系列弱分类器。我们为每个分类器轻微修改数据,并最终得到 M 个分类器:

理解集成 - 提升方法

最后,它们所有的预测通过加权多数投票进行组合:

理解集成 - 提升方法

这种方法称为 AdaBoost。

权重 alpha 和模型构建的顺序方式是 Boosting 与 Bagging 不同的地方。正如前面提到的,Boosting 在每个分类器上构建了一系列略微修改的数据集上的弱分类器。让我们看看这个微小的数据修改指的是什么。正是从这个修改中我们得出了我们的权重 alpha。

最初对于第一个分类器,m=1,我们将每个实例的权重设置为 1/N,也就是说,如果有一百条记录,每条记录的权重为 0.001。让我们用 w 来表示权重-现在我们有一百个这样的权重:

理解集成 - 提升方法

所有记录现在都有相等的机会被分类器选择。我们构建分类器,并在训练数据上测试它,以获得误分类率。请参考本节前面给出的误分类率公式。我们将稍微修改它,加入权重,如下所示:

理解集成方法 – 提升方法

其中 abs 表示结果的绝对值。根据这个误差率,我们计算我们的 alpha(模型权重)如下:

理解集成方法 – 提升方法

其中 epsilon 是一个非常小的值。

假设我们的模型 1 的误差率是 0.3,也就是说,模型能够正确分类 70%的记录。因此,该模型的权重大约是 0.8,这是一个不错的权重。基于此,我们将返回并设置个别记录的权重,如下所示:

理解集成方法 – 提升方法

正如你所看到的,所有被错误分类的属性的权重都会增加。这增加了被下一个分类器选择的错误分类记录的概率。因此,下一个分类器在序列中选择权重更大的实例并尝试拟合它。通过这种方式,所有未来的分类器将开始集中处理之前分类器错误分类的记录。

这就是提升方法的威力。它能够将多个弱分类器转化为一个强大的集成模型。

让我们看看提升方法如何应用。在我们编写代码的过程中,我们还将看到 AdaBoost 的一个小变种,称为 SAMME。

开始使用…

我们将利用 scikit-learn 的DecisionTreeClassifier类进行分类,并使用AdaBoostClassifier应用提升原理。我们将使用make_classification便捷函数生成数据。

如何实现

让我们导入必要的库,并编写一个函数get_data(),为我们提供一个数据集来实现这个方案。

from sklearn.datasets import make_classification
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import classification_report,zero_one_loss
from sklearn.cross_validation import train_test_split
from sklearn.tree import DecisionTreeClassifier
import numpy as np
import matplotlib.pyplot as plt
import itertools

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    print no_features,redundant_features,informative_features,repeated_features
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

def build_single_model(x,y):
    model = DecisionTreeClassifier()
    model.fit(x,y)
    return model

def build_boosting_model(x,y,no_estimators=20):
    boosting = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1,min_samples_leaf=1),random_state=9 \
    ,n_estimators=no_estimators,algorithm="SAMME")
    boosting.fit(x,y)
    return boosting

def view_model(model):
    print "\n Estimator Weights and Error\n"
    for i,weight in  enumerate(model.estimator_weights_):
        print "estimator %d weight = %0.4f error = %0.4f"%(i+1,weight,model.estimator_errors_[i])

    plt.figure(1)
    plt.title("Model weight vs error")
    plt.xlabel("Weight")
    plt.ylabel("Error")
    plt.plot(model.estimator_weights_,model.estimator_errors_)

def number_estimators_vs_err_rate(x,y,x_dev,y_dev):
    no_estimators = range(20,120,10)
    misclassy_rate = []
    misclassy_rate_dev = []

    for no_estimator in no_estimators:
        boosting = build_boosting_model(x,y,no_estimators=no_estimator)
        predicted_y = boosting.predict(x)
        predicted_y_dev = boosting.predict(x_dev)        
        misclassy_rate.append(zero_one_loss(y,predicted_y))
        misclassy_rate_dev.append(zero_one_loss(y_dev,predicted_y_dev))

    plt.figure(2)
    plt.title("No estimators vs Mis-classification rate")
    plt.xlabel("No of estimators")
    plt.ylabel("Mis-classification rate")
    plt.plot(no_estimators,misclassy_rate,label='Train')
    plt.plot(no_estimators,misclassy_rate_dev,label='Dev')

    plt.show() 

if __name__ == "__main__":
    x,y = get_data()    
    plot_data(x,y)

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    # Build a single model    
    model = build_single_model(x_train,y_train)
    predicted_y = model.predict(x_train)
    print "\n Single Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_train,predicted_y)*100),"%"

    # Build a bag of models
    boosting = build_boosting_model(x_train,y_train, no_estimators=85)
    predicted_y = boosting.predict(x_train)
    print "\n Boosting Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_train,predicted_y)*100),"%"

    view_model(boosting)

    # Look at the dev set
    predicted_y = model.predict(x_dev)
    print "\n Single Model Accuracy on Dev data\n"
    print classification_report(y_dev,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_dev,predicted_y)*100),"%"

    print "\n Boosting Model Accuracy on Dev data\n"
    predicted_y = boosting.predict(x_dev)
    print classification_report(y_dev,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_dev,predicted_y)*100),"%"

    number_estimators_vs_err_rate(x_train,y_train,x_dev,y_dev)

让我们继续并编写以下三个函数:

函数 build_single_model 用于使用给定数据构建一个简单的决策树模型。

函数 build_boosting_model,它实现了提升算法。

函数 view_model,用于检查我们构建的模型。

def build_single_model(x,y):
    model = DecisionTreeClassifier()
    model.fit(x,y)
    return model

def build_boosting_model(x,y,no_estimators=20):
    boosting = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1,min_samples_leaf=1),random_state=9 \
    ,n_estimators=no_estimators,algorithm="SAMME")
    boosting.fit(x,y)
    return boosting

def view_model(model):
    print "\n Estimator Weights and Error\n"
    for i,weight in  enumerate(model.estimator_weights_):
        print "estimator %d weight = %0.4f error = %0.4f"%(i+1,weight,model.estimator_errors_[i])

    plt.figure(1)
    plt.title("Model weight vs error")
    plt.xlabel("Weight")
    plt.ylabel("Error")
    plt.plot(model.estimator_weights_,model.estimator_errors_)

然后我们编写一个名为 number_estimators_vs_err_rate 的函数。我们使用此函数来查看随着集成模型中模型数量的变化,我们的误差率是如何变化的。

def number_estimators_vs_err_rate(x,y,x_dev,y_dev):
    no_estimators = range(20,120,10)
    misclassy_rate = []
    misclassy_rate_dev = []

    for no_estimator in no_estimators:
        boosting = build_boosting_model(x,y,no_estimators=no_estimator)
        predicted_y = boosting.predict(x)
        predicted_y_dev = boosting.predict(x_dev)        
        misclassy_rate.append(zero_one_loss(y,predicted_y))
        misclassy_rate_dev.append(zero_one_loss(y_dev,predicted_y_dev))

    plt.figure(2)
    plt.title("No estimators vs Mis-classification rate")
    plt.xlabel("No of estimators")
    plt.ylabel("Mis-classification rate")
    plt.plot(no_estimators,misclassy_rate,label='Train')
    plt.plot(no_estimators,misclassy_rate_dev,label='Dev')

    plt.show()

最后,我们将编写主函数,它将调用其他函数。

if __name__ == "__main__":
    x,y = get_data()    
    plot_data(x,y)

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    # Build a single model    
    model = build_single_model(x_train,y_train)
    predicted_y = model.predict(x_train)
    print "\n Single Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_train,predicted_y)*100),"%"

    # Build a bag of models
    boosting = build_boosting_model(x_train,y_train, no_estimators=85)
    predicted_y = boosting.predict(x_train)
    print "\n Boosting Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_train,predicted_y)*100),"%"

    view_model(boosting)

    # Look at the dev set
    predicted_y = model.predict(x_dev)
    print "\n Single Model Accuracy on Dev data\n"
    print classification_report(y_dev,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_dev,predicted_y)*100),"%"

    print "\n Boosting Model Accuracy on Dev data\n"
    predicted_y = boosting.predict(x_dev)
    print classification_report(y_dev,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_dev,predicted_y)*100),"%"

    number_estimators_vs_err_rate(x_train,y_train,x_dev,y_dev)

它是如何工作的…

让我们从主方法开始。我们首先调用get_data函数返回数据集,其中 x 为预测变量矩阵,y 为响应变量向量。让我们深入了解get_data函数:

    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
 x,y =make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)

查看传递给make_classification方法的参数。第一个参数是所需的实例数量;在这种情况下,我们说我们需要 500 个实例。第二个参数给出了每个实例所需的属性数量。我们说我们需要 30 个属性,正如no_features变量所定义的那样。第三个参数flip_y,随机交换 3%的实例。这是为了在数据中引入一些噪音。接下来的参数指定了这些 30 个特征中应该有多少个是足够有用的,可以用于分类。我们指定 60%的特征,即 30 个特征中的 18 个应该是有信息量的。接下来的参数是冗余特征。这些特征是通过有信息特征的线性组合生成的,用来引入特征之间的相关性。最后,重复特征是从有信息特征和冗余特征中随机选取的重复特征。

让我们使用train_test_split将数据分为训练集和测试集。我们将 30%的数据保留用于测试。

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

再次,我们利用train_test_split将我们的测试数据分为开发集和测试集。

    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

在将数据分为用于构建、评估和测试模型的部分后,我们开始构建我们的模型。

让我们先拟合一棵单一的决策树,并查看该树在训练集上的表现:

    # Build a single model    
    model = build_single_model(x_train,y_train)

我们通过调用build_single_model函数,传入预测变量和响应变量来构建模型。在这个过程中,我们拟合一棵单一的决策树,并将树返回给调用函数。

def build_single_model(x,y):
    model = DecisionTreeClassifier()
    model.fit(x,y)
    return model

让我们使用classification_report评估模型的好坏,这是来自 Scikit learn 的一个实用函数,它显示一组指标,包括precision(精确度)、recall(召回率)和f1-score;我们还会显示误分类率。

    predicted_y = model.predict(x_train)
    print "\n Single Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
    print "Fraction of misclassfication =     
           %0.2f"%(zero_one_loss(y_train,predicted_y)*100),"%"

它是如何工作的…

如你所见,我们的决策树模型完美地拟合了数据——我们的误分类率为 0。在我们在开发集上测试该模型之前,让我们构建我们的集成模型:

    # Build a bag of models
    boosting = build_boosting_model(x_train,y_train, no_estimators=85)

使用build_boosting_model方法,我们按如下方式构建我们的集成模型:

    boosting = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1,min_samples_leaf=1),random_state=9 \
    ,n_estimators=no_estimators,algorithm="SAMME")
    boosting.fit(x,y)

我们利用 Scikit learn 中的AdaBoostClassifier构建我们的提升集成。我们使用以下参数实例化该类:

估计器——在我们的案例中,我们说我们想要构建一个决策树的集成。因此,我们传入DecisionTreeClassifier对象。

max_depth——我们不希望在集成中使用完全生长的树木。我们只需要树桩——只有两个叶子节点和一个分割节点的树。因此,我们将max_depth参数设置为 1。

使用n_estimators参数,我们指定要生成的树木数量;在此案例中,我们将生成 86 棵树。

最后,我们有一个参数叫做 algorithm,它被设置为SAMMESAMME代表逐阶段加法建模,使用多类指数损失函数。SAMME是对 AdaBoost 算法的改进。它试图将更多的权重分配给误分类的记录。模型权重α是SAMME与 AdaBoost 的区别所在。

它是如何工作的…

我们在前面的公式中忽略了常数 0.5。让我们来看一下新的添加项:log(K-1)。如果 K=2,那么前面的公式就简化为 AdaBoost。在这里,K 是响应变量中的类别数。对于二分类问题,SAMME 就会简化为 AdaBoost,正如前面所述。

让我们拟合模型,并将其返回给调用函数。我们在训练数据集上运行该模型,再次查看模型的表现:

    predicted_y = boosting.predict(x_train)
    print "\n Boosting Model Accuracy on training data\n"
    print classification_report(y_train,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_train,predicted_y)*100),"%"

它是如何工作的…

结果与我们原始模型的表现没有太大不同。我们已经正确分类了几乎 98%的记录。

在对开发集进行测试之前,让我们首先查看我们构建的 Boosting 集成模型:

    view_model(boosting)

在 view_model 内部,我们首先打印出分配给每个分类器的权重:

    print "\n Estimator Weights and Error\n"
    for i,weight in  enumerate(model.estimator_weights_):
        print "estimator %d weight = %0.4f error = %0.4f"%(i+1,weight,model.estimator_errors_[i])

它是如何工作的…

这里我们展示了前 20 个集成的权重。根据它们的误分类率,我们为这些估计器分配了不同的权重。

让我们继续绘制一个图表,显示估计器权重与每个估计器所产生错误之间的关系:

    plt.figure(1)
    plt.title("Model weight vs error")
    plt.xlabel("Weight")
    plt.ylabel("Error")
    plt.plot(model.estimator_weights_,model.estimator_errors_)

它是如何工作的…

正如你所看到的,正确分类的模型被分配了比错误率较高的模型更多的权重。

现在让我们看看单一决策树和集成决策树在开发数据上的表现:

    # Look at the dev set
    predicted_y = model.predict(x_dev)
    print "\n Single Model Accuracy on Dev data\n"
    print classification_report(y_dev,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_dev,predicted_y)*100),"%"

    print "\n Boosting Model Accuracy on Dev data\n"
    predicted_y = boosting.predict(x_dev)
    print classification_report(y_dev,predicted_y)
    print "Fraction of misclassfication = %0.2f"%(zero_one_loss(y_dev,predicted_y)*100),"%"

就像我们对训练数据做的那样,我们打印出分类报告和误分类率:

它是如何工作的…

正如你所看到的,单一决策树表现不佳。尽管它在训练数据上显示了 100%的准确率,但在开发数据上却误分类了近 40%的记录——这是过拟合的迹象。相比之下,Boosting 模型能够更好地拟合开发数据。

我们如何改进 Boosting 模型?其中一种方法是测试训练集中的错误率与我们想要在集成中包含的分类器数量之间的关系。

    number_estimators_vs_err_rate(x_train,y_train,x_dev,y_dev)

以下函数会根据集成的数量递增并绘制错误率:

def number_estimators_vs_err_rate(x,y,x_dev,y_dev):
    no_estimators = range(20,120,10)
    misclassy_rate = []
    misclassy_rate_dev = []

    for no_estimator in no_estimators:
        boosting = build_boosting_model(x,y,no_estimators=no_estimator)
        predicted_y = boosting.predict(x)
        predicted_y_dev = boosting.predict(x_dev)        
        misclassy_rate.append(zero_one_loss(y,predicted_y))
        misclassy_rate_dev.append(zero_one_loss(y_dev,predicted_y_dev))

    plt.figure(2)
    plt.title("No estimators vs Mis-classification rate")
    plt.xlabel("No of estimators")
    plt.ylabel("Mis-classification rate")
    plt.plot(no_estimators,misclassy_rate,label='Train')
    plt.plot(no_estimators,misclassy_rate_dev,label='Dev')

    plt.show()

如你所见,我们声明了一个列表,起始值为 20,结束值为 120,步长为 10。在for循环中,我们将列表中的每个元素作为估计器参数传递给build_boosting_model,然后继续访问模型的错误率。接着我们检查开发集中的错误率。现在我们有两个列表—一个包含训练数据的所有错误率,另一个包含开发数据的错误率。我们将它们一起绘制,x轴是估计器的数量,y轴是开发集和训练集中的错误分类率。

它是如何工作的…

上述图表给出了一个线索,在大约 30 到 40 个估计器时,开发集中的错误率非常低。我们可以进一步实验树模型参数,以获得一个良好的模型。

还有更多内容…

Boosting 方法首次在以下开创性论文中提出:

Freund, Y. & Schapire, R. (1997), 'A decision theoretic generalization of on-line learning and an application to boosting', Journal of Computer and System Sciences 55(1), 119–139.

最初,大多数 Boosting 方法将多类问题简化为二类问题和多个二类问题。以下论文将 AdaBoost 扩展到多类问题:

Multi-class AdaBoost Statistics and Its Interface, Vol. 2, No. 3 (2009), pp. 349-360, doi:10.4310/sii.2009.v2.n3.a8 by Trevor Hastie, Saharon Rosset, Ji Zhu, Hui Zou

本文还介绍了 SAMME,这是我们在配方中使用的方法。

另请参见

  • 在第六章中,构建决策树解决多类问题的配方,机器学习 I

  • 在第七章中,使用交叉验证迭代器的配方,机器学习 II

  • 在第八章中,理解集成方法 – 自助法的配方,模型选择与评估

理解集成方法 – 梯度 Boosting

让我们回顾一下前面配方中解释的 Boosting 算法。在 Boosting 中,我们以逐步的方式拟合加法模型。我们顺序地构建分类器。构建每个分类器后,我们估计分类器的权重/重要性。根据权重/重要性,我们调整训练集中实例的权重。错误分类的实例比正确分类的实例权重更高。我们希望下一个模型能够选择那些错误分类的实例并在其上进行训练。训练集中的那些没有正确拟合的实例会通过这些权重被识别出来。换句话说,这些记录是前一个模型的不足之处。下一个模型试图克服这些不足。

梯度 Boosting 使用梯度而非权重来识别这些不足之处。让我们快速了解如何使用梯度来改进模型。

让我们以一个简单的回归问题为例,假设我们已知所需的预测变量X和响应变量Y,其中Y是一个实数。

理解集成 – 梯度提升

梯度提升过程如下:

它从一个非常简单的模型开始,比如均值模型。

理解集成 – 梯度提升

预测值仅仅是响应变量的均值。

然后它会继续拟合残差。残差是实际值 y 与预测值 y_hat 之间的差异。

理解集成 – 梯度提升

接下来的分类器是在如下数据集上进行训练的:

理解集成 – 梯度提升

随后模型会在前一个模型的残差上进行训练,因此算法会继续构建所需数量的模型,最终形成集成模型。

让我们尝试理解为什么我们要在残差上进行训练。现在应该清楚,Boosting 方法构建的是加性模型。假设我们建立两个模型F1(X)F2(X)来预测Y1。根据加性原理,我们可以将这两个模型结合起来,如下所示:

理解集成 – 梯度提升

也就是说,我们结合两个模型的预测结果来预测 Y_1。

等价地,我们可以这样说:

理解集成 – 梯度提升

残差是模型未能很好拟合的部分,或者简单来说,残差就是前一个模型的不足之处。因此,我们利用残差来改进模型,即改进前一个模型的不足。基于这个讨论,你可能会好奇为什么这种方法叫做梯度提升(Gradient Boosting)而不是残差提升(Residual Boosting)。

给定一个可微的函数,梯度表示该函数在某些值处的一阶导数。在回归问题中,目标函数为:

理解集成 – 梯度提升

其中,F(xi)是我们的回归模型。

线性回归问题是通过最小化前述函数来解决的。我们在F(xi)的值处求该函数的一阶导数,如果用该导数值的负值来更新权重系数,我们将朝着搜索空间中的最小解前进。前述成本函数关于F(xi)的一阶导数是F(xi ) – yi。请参阅以下链接了解推导过程:

zh.wikipedia.org/wiki/%E6%A0%B9%E5%87%BB%E4%BD%8D%E9%9A%8F

F(xi ) – yi,即梯度,是我们残差yi – F(xi)的负值,因此得名梯度提升(Gradient Boosting)。

有了这个理论,我们可以进入梯度提升的具体步骤。

开始使用……

我们将使用波士顿数据集来演示梯度提升。波士顿数据集有 13 个特征和 506 个实例。目标变量是一个实数,即房屋的中位数价格(单位为千美元)。波士顿数据集可以从 UCI 链接下载:

archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.names

我们打算生成 2 次方的多项式特征,并仅考虑交互效应。

如何操作

让我们导入必要的库,并编写一个get_data()函数来提供我们需要使用的数据集,以便完成这个任务:

# Load libraries
from sklearn.datasets import load_boston
from sklearn.cross_validation import train_test_split
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import PolynomialFeatures
import numpy as np
import matplotlib.pyplot as plt

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    return x,y    

def build_model(x,y,n_estimators=500):
    """
    Build a Gradient Boost regression model
    """
    model = GradientBoostingRegressor(n_estimators=n_estimators,verbose=10,\
            subsample=0.7, learning_rate= 0.15,max_depth=3,random_state=77)
    model.fit(x,y)
    return model    

def view_model(model):
    """
    """
    print "\n Training scores"
    print "======================\n"
    for i,score in enumerate(model.train_score_):
        print "\tEstimator %d score %0.3f"%(i+1,score)

    plt.cla()
    plt.figure(1)
    plt.plot(range(1,model.estimators_.shape[0]+1),model.train_score_)
    plt.xlabel("Model Sequence")
    plt.ylabel("Model Score")
    plt.show()

    print "\n Feature Importance"
    print "======================\n"
    for i,score in enumerate(model.feature_importances_):
        print "\tFeature %d Importance %0.3f"%(i+1,score)

def model_worth(true_y,predicted_y):
    """
    Evaluate the model
    """
    print "\tMean squared error = %0.2f"%(mean_squared_error(true_y,predicted_y))

if __name__ == "__main__":

    x,y = get_data()

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    #Prepare some polynomial features
    poly_features = PolynomialFeatures(2,interaction_only=True)
    poly_features.fit(x_train)
    x_train_poly = poly_features.transform(x_train)
    x_dev_poly   = poly_features.transform(x_dev)

    # Build model with polynomial features
    model_poly = build_model(x_train_poly,y_train)
    predicted_y = model_poly.predict(x_train_poly)
    print "\n Model Performance in Training set (Polynomial features)\n"
    model_worth(y_train,predicted_y)  

    # View model details
    view_model(model_poly)

    # Apply the model on dev set
    predicted_y = model_poly.predict(x_dev_poly)
    print "\n Model Performance in Dev set  (Polynomial features)\n"
    model_worth(y_dev,predicted_y)  

    # Apply the model on Test set
    x_test_poly = poly_features.transform(x_test)
    predicted_y = model_poly.predict(x_test_poly)

    print "\n Model Performance in Test set  (Polynomial features)\n"
    model_worth(y_test,predicted_y)  

让我们编写以下三个函数。

build_model 函数实现了梯度提升算法。

view_model 和 model_worth 函数,用于检查我们构建的模型:

def build_model(x,y,n_estimators=500):
    """
    Build a Gradient Boost regression model
    """
    model = GradientBoostingRegressor(n_estimators=n_estimators,verbose=10,\
            subsample=0.7, learning_rate= 0.15,max_depth=3,random_state=77)
    model.fit(x,y)
    return model    

def view_model(model):
    """
    """
    print "\n Training scores"
    print "======================\n"
    for i,score in enumerate(model.train_score_):
        print "\tEstimator %d score %0.3f"%(i+1,score)

    plt.cla()
    plt.figure(1)
    plt.plot(range(1,model.estimators_.shape[0]+1),model.train_score_)
    plt.xlabel("Model Sequence")
    plt.ylabel("Model Score")
    plt.show()

    print "\n Feature Importance"
    print "======================\n"
    for i,score in enumerate(model.feature_importances_):
        print "\tFeature %d Importance %0.3f"%(i+1,score)

def model_worth(true_y,predicted_y):
    """
    Evaluate the model
    """
    print "\tMean squared error = %0.2f"%(mean_squared_error(true_y,predicted_y))

最后,我们编写主函数,该函数将调用其他函数:

if __name__ == "__main__":

    x,y = get_data()

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    #Prepare some polynomial features
    poly_features = PolynomialFeatures(2,interaction_only=True)
    poly_features.fit(x_train)
    x_train_poly = poly_features.transform(x_train)
    x_dev_poly   = poly_features.transform(x_dev)

    # Build model with polynomial features
    model_poly = build_model(x_train_poly,y_train)
    predicted_y = model_poly.predict(x_train_poly)
    print "\n Model Performance in Training set (Polynomial features)\n"
    model_worth(y_train,predicted_y)  

    # View model details
    view_model(model_poly)

    # Apply the model on dev set
    predicted_y = model_poly.predict(x_dev_poly)
    print "\n Model Performance in Dev set  (Polynomial features)\n"
    model_worth(y_dev,predicted_y)  

    # Apply the model on Test set
    x_test_poly = poly_features.transform(x_test)
    predicted_y = model_poly.predict(x_test_poly)

    print "\n Model Performance in Test set  (Polynomial features)\n"
    model_worth(y_test,predicted_y)  

如何运作…

让我们从主模块开始,跟随代码进行操作。我们使用 get_data 函数加载预测变量 x 和响应变量 y:

def get_data():
    """
    Return boston dataset
    as x - predictor and
    y - response variable
    """
    data = load_boston()
    x    = data['data']
    y    = data['target']
    return x,y    

该函数调用 Scikit learn 的便捷函数load_boston()来获取波士顿房价数据集,并将其作为 numpy 数组加载。

我们继续使用 Scikit 库中的 train_test_split 函数将数据划分为训练集和测试集。我们保留 30%的数据集用于测试。

x_train,x_test_all,y_train,y_test_all = 
train_test_split(x,y,test_size = 0.3,random_state=9)

从这 30%的数据中,我们在下一行再次提取开发集:

x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

我们接下来构建多项式特征如下:

poly_features = PolynomialFeatures(interaction_only=True)
poly_features.fit(x_train)

如你所见,我们已将 interaction_only 设置为 True。将 interaction_only 设置为 True 时,给定 x1 和 x2 属性时,只会创建 x1*x2 的交互项,而不会创建 x1 的平方和 x2 的平方,假设多项式的度数为 2。默认的度数是 2。

x_train_poly = poly_features.transform(x_train)
x_dev_poly = poly_features.transform(x_dev)
x_test_poly = poly_features.transform(x_test)

使用 transform 函数,我们将训练集、开发集和测试集转换为包含多项式特征的数据集:

让我们继续构建我们的模型:

    # Build model with polynomial features
    model_poly = build_model(x_train_poly,y_train)

在 build_model 函数内部,我们按如下方式实例化 GradientBoostingRegressor 类:

    model = GradientBoostingRegressor(n_estimators=n_estimators,verbose=10,\
            subsample=0.7, learning_rate= 0.15,max_depth=3,random_state=77)

让我们看看这些参数。第一个参数是集成模型的数量。第二个参数是 verbose——当设置为大于 1 的数字时,它会在每个模型(在此为树)的构建过程中打印进度。下一个参数是 subsample,它决定了模型将使用的训练数据百分比。在本例中,0.7 表示我们将使用 70%的训练数据集。下一个参数是学习率。它是一个收缩参数,用于控制每棵树的贡献。接下来的参数 max_depth 决定了构建树的大小。random_state 参数是随机数生成器使用的种子。为了在不同的运行中保持一致性,我们将其设置为一个整数值。

由于我们将 verbose 参数设置为大于 1,在拟合模型时,我们会在每次模型迭代过程中看到以下结果:

如何运作...

如你所见,训练损失随着每次迭代而减少。第四列是袋外改进得分。在子采样中,我们仅选择了数据集的 70%;袋外得分是用剩下的 30%计算的。与前一个模型相比,损失有所改善。例如,在第二次迭代中,相较于第一次迭代构建的模型,我们有 10.32 的改进。

让我们继续检查集成模型在训练数据上的表现:

    predicted_y = model_poly.predict(x_train_poly)
    print "\n Model Performance in Training set (Polynomial features)\n"
    model_worth(y_train,predicted_y)  

它是如何工作的…

如你所见,我们的提升集成模型已经完美地拟合了训练数据。

model_worth 函数打印出模型的更多细节,具体如下:

它是如何工作的…

在详细输出中看到的每个不同模型的得分,都作为属性存储在模型对象中,可以通过以下方式检索:

print "\n Training scores"
print "======================\n"
for i,score in enumerate(model.train_score_):
print "\tEstimator %d score %0.3f"%(i+1,score)

让我们在图表中展示这个结果:

plt.cla()
plt.figure(1)
plt.plot(range(1,model.estimators_.shape[0]+1),model.train_score_)
plt.xlabel("Model Sequence")
plt.ylabel("Model Score")
plt.show()

x 轴表示模型编号,y 轴显示训练得分。记住,提升是一个顺序过程,每个模型都是对前一个模型的改进。

它是如何工作的…

如图所示,均方误差(即模型得分)随着每一个后续模型的增加而减小。

最后,我们还可以看到与每个特征相关的重要性:

    print "\n Feature Importance"
    print "======================\n"
    for i,score in enumerate(model.feature_importances_):
        print "\tFeature %d Importance %0.3f"%(i+1,score)

让我们看看各个特征之间的堆叠情况。

它是如何工作的…

梯度提升将特征选择和模型构建统一为一个操作。它可以自然地发现特征之间的非线性关系。请参考以下论文,了解如何将梯度提升用于特征选择:

Zhixiang Xu, Gao Huang, Kilian Q. Weinberger, 和 Alice X. Zheng. 2014. 梯度提升特征选择。在 第 20 届 ACM SIGKDD 国际知识发现与数据挖掘大会论文集(KDD '14)。ACM, 纽约, NY, USA, 522-531。

让我们将开发数据应用到模型中并查看其表现:

    # Apply the model on dev set
    predicted_y = model_poly.predict(x_dev_poly)
    print "\n Model Performance in Dev set  (Polynomial features)\n"
    model_worth(y_dev,predicted_y)  

它是如何工作的…

最后,我们来看一下测试集上的表现。

它是如何工作的…

如你所见,与开发集相比,我们的集成模型在测试集上的表现极为出色。

还有更多内容…

欲了解更多关于梯度提升的信息,请参考以下论文:

Friedman, J. H. (2001). 贪婪函数逼近:一种梯度提升机。统计年鉴,第 1189–1232 页。

在这份报告中,我们用平方损失函数解释了梯度提升。然而,梯度提升应该被视为一个框架,而不是一个方法。任何可微分的损失函数都可以在这个框架中使用。用户可以选择任何学习方法和可微损失函数,并将其应用于梯度提升框架。

Scikit Learn 还提供了一种用于分类的梯度提升方法,称为 GradientBoostingClassifier。

scikit-learn.org/stable/modules/generated/sklearn.ensemble.GradientBoostingClassifier.html

另见

  • 理解集成方法, Bagging 方法 配方见 第八章,模型选择与评估

  • 理解集成方法AdaBoost 增强方法 配方见 第八章,模型选择与评估

  • 使用回归预测实值数 配方见 第七章,机器学习 II

  • 使用 LASSO 回归进行变量选择 配方见 第七章,机器学习 II

  • 使用交叉验证迭代器 配方见 第七章,机器学习 II

第九章:生长树

本章我们将涵盖以下食谱:

  • 从树到森林——随机森林

  • 生长极度随机化的树

  • 生长旋转森林

介绍

在本章中,我们将看到更多基于树的算法的袋装方法。由于它们对噪声的鲁棒性以及对各种问题的普适性,它们在数据科学社区中非常受欢迎。

大多数这些方法的名声在于它们相比其他方法能够在没有任何数据准备的情况下获得非常好的结果,而且它们可以作为黑盒工具交给软件工程师使用。

除了前文提到的过高的要求外,还有一些其他优点。

从设计上看,袋装法非常适合并行化。因此,这些方法可以轻松应用于集群环境中的大规模数据集。

决策树算法在树的每一层将输入数据划分为不同的区域。因此,它们执行了隐式的特征选择。特征选择是构建良好模型中的一个重要任务。通过提供隐式特征选择,决策树相较于其他技术处于有利位置。因此,带有决策树的袋装法具备这一优势。

决策树几乎不需要数据准备。例如,考虑属性的缩放。属性的缩放对决策树的结构没有影响。此外,缺失值不会影响决策树。异常值对决策树的影响也很小。

在我们之前的一些食谱中,我们使用了多项式特征,仅保留了交互项。通过集成树方法,这些交互关系得到了处理。我们无需进行显式的特征转换来适应特征交互。

基于线性回归的模型在输入数据中存在非线性关系时会失败。当我们解释核主成分分析(Kernel PCA)食谱时,我们看到过这种效果。基于树的算法不受数据中非线性关系的影响。

对于树基方法的主要投诉之一是树的剪枝困难,容易导致过拟合。大树往往也会拟合底层数据中的噪声,从而导致低偏差和高方差。然而,当我们生长大量树木,并且最终预测是所有树的输出的平均值时,就能避免方差问题。

本章我们将介绍三种基于树的集成方法。

我们的第一个食谱是实现随机森林用于分类问题。Leo Breiman 是这一算法的发明者。随机森林是一种集成技术,通过内部使用大量的树来构建模型,用于解决回归或分类问题。

我们的第二个方法是极端随机化树(Extremely Randomized trees),这是一种与随机森林非常相似的算法。通过与随机森林相比,增加更多的随机化,它声称可以更有效地解决方差问题。此外,它还稍微减少了计算复杂度。

我们的最后一个方法是旋转森林(Rotation Forest)。前两个方法需要大量的树作为其集成的一部分,以获得良好的性能。旋转森林声称可以用较少的树实现类似或更好的性能。此外,该算法的作者声称,其基础估计器可以是任何其他的模型,而不仅仅是树。通过这种方式,它被视为构建类似于梯度提升(Gradient Boosting)集成的新框架。

从树到森林——随机森林

随机森林方法构建了许多相互之间不相关的树(森林)。给定一个分类或回归问题,该方法构建许多树,最终的预测结果要么是森林中所有树的预测平均值(对于回归),要么是多数投票分类的结果。

这应该让你想起 Bagging。随机森林是另一种 Bagging 方法。Bagging 背后的基本思想是使用大量的噪声估计器,通过平均来处理噪声,从而减少最终输出中的方差。树对训练数据集中的噪声非常敏感。由于树是噪声估计器,它们非常适合用于 Bagging。

让我们写下构建随机森林的步骤。森林中所需的树的数量是用户指定的一个参数。假设 T 是需要构建的树的数量:

我们从 1 到 T 进行迭代,也就是说,我们构建 T 棵树:

  • 对于每棵树,从我们的输入数据集中抽取大小为 D 的自助抽样。

  • 我们继续将一棵树 t 拟合到输入数据:

    • 随机选择 m 个属性。

    • 选择最好的属性作为分裂变量,使用预定义的标准。

    • 将数据集分成两部分。记住,树是二叉的。在树的每一层,输入数据集都会被分成两部分。

    • 我们继续在已分割的数据集上递归地执行前面三步。

  • 最后,我们返回 T 棵树。

为了对一个新实例做出预测,我们在 T 中所有的树上进行多数投票来做分类;对于回归问题,我们取每棵树 t 在 T 中返回的平均值。

我们之前提到过,随机森林构建的是非相关的树。让我们看看集成中的各个树是如何彼此不相关的。通过为每棵树从数据集中抽取自助样本,我们确保不同的树会接触到数据的不同部分。这样,每棵树都会尝试建模数据集的不同特征。因此,我们遵循集成方法引入底层估计器的变化。但这并不保证底层树之间完全没有相关性。当我们进行节点分裂时,并不是选择所有特征,而是随机选择特征的一个子集。通过这种方式,我们尝试确保我们的树之间没有相关性。

与 Boosting 相比,我们的估计器集成在 Boosting 中是弱分类器,而在随机森林中,我们构建具有最大深度的树,以使其完美拟合自助样本,从而降低偏差。其结果是引入了高方差。然而,通过构建大量的树并使用平均化原则进行最终预测,我们希望解决这个方差问题。

让我们继续深入了解我们的随机森林配方。

准备开始

我们将生成一些分类数据集,以演示随机森林算法。我们将利用 scikit-learn 中的随机森林实现,该实现来自集成模块。

如何操作…

我们将从加载所有必要的库开始。让我们利用 sklearn.dataset 模块中的 make_classification 方法来生成训练数据,以演示随机森林:

from sklearn.datasets import make_classification
from sklearn.metrics import classification_report, accuracy_score
from sklearn.cross_validation import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.grid_search import RandomizedSearchCV
from operator import itemgetter

import numpy as np

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

现在我们将编写 build_forest 函数来构建完全生长的树,并继续评估森林的性能。接着我们将编写可用于搜索森林最优参数的方法:

def build_forest(x,y,x_dev,y_dev):
    """
    Build a random forest of fully grown trees
    and evaluate peformance
    """
    no_trees = 100
    estimator = RandomForestClassifier(n_estimators=no_trees)
    estimator.fit(x,y)

    train_predcited = estimator.predict(x)
    train_score = accuracy_score(y,train_predcited)
    dev_predicted = estimator.predict(x_dev)
    dev_score = accuracy_score(y_dev,dev_predicted)

    print "Training Accuracy = %0.2f Dev Accuracy = %0.2f"%(train_score,dev_score)

def search_parameters(x,y,x_dev,y_dev):
    """
    Search the parameters of random forest algorithm
    """
    estimator = RandomForestClassifier()
    no_features = x.shape[1]
    no_iterations = 20
    sqr_no_features = int(np.sqrt(no_features))

    parameters = {"n_estimators"      : np.random.randint(75,200,no_iterations),
                 "criterion"         : ["gini", "entropy"],
                 "max_features"      : [sqr_no_features,sqr_no_features*2,sqr_no_features*3,sqr_no_features+10]
                 }

    grid = RandomizedSearchCV(estimator=estimator,param_distributions=parameters,\
    verbose=1, n_iter=no_iterations,random_state=77,n_jobs=-1,cv=5)
    grid.fit(x,y)
    print_model_worth(grid,x_dev,y_dev)

    return grid.best_estimator_

def print_model_worth(grid,x_dev,y_dev):    
    # Print the goodness of the models
    # We take the top 5 models
    scores = sorted(grid.grid_scores_, key=itemgetter(1), reverse=True) [0:5]

    for model_no,score in enumerate(scores):
        print "Model %d, Score = %0.3f"%(model_no+1,score.mean_validation_score)
        print "Parameters = {0}".format(score.parameters)
    print
    dev_predicted = grid.predict(x_dev)

    print classification_report(y_dev,dev_predicted)

最后,我们编写一个主函数,用于调用我们之前定义的函数:

if __name__ == "__main__":
    x,y = get_data()    

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    build_forest(x_train,y_train,x_dev,y_dev)
    model = search_parameters(x,y,x_dev,y_dev)
    get_feature_importance(model)

它是如何工作的…

让我们从我们的主函数开始。我们调用 get_data 来获取预测器特征 x 和响应特征 y。在 get_data 中,我们利用 make_classification 数据集来生成我们的随机森林训练数据:

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

让我们看看传递给make_classification方法的参数。第一个参数是所需的实例数量;在此例中,我们需要 500 个实例。第二个参数是每个实例所需的属性数量。我们设定需要 30 个属性。第三个参数flip_y会随机交换 3%的实例。这是为了向数据中引入一些噪声。接下来的参数指定从这 30 个属性中有多少个是足够有用的信息属性,用于我们的分类任务。我们设定 60%的特征,即 30 个特征中的 18 个应该具有信息量。下一个参数与冗余特征有关。这些冗余特征是通过信息特征的线性组合生成的,以引入特征之间的相关性。最后,重复特征是重复的特征,它们是从信息特征和冗余特征中随机抽取的。

让我们使用train_test_split将数据划分为训练集和测试集。我们将 30%的数据保留用于测试:

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

我们再次使用train_test_split将我们的测试数据分成开发集和测试集:

    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

在数据被分配用于构建、评估和测试模型后,我们继续构建我们的模型:

build_forest(x_train,y_train,x_dev,y_dev)

我们使用训练集和开发集数据调用build_forest函数来构建随机森林模型。让我们来看一下这个函数的内部实现:

    no_trees = 100
    estimator = RandomForestClassifier(n_estimators=no_trees)
    estimator.fit(x,y)

    train_predcited = estimator.predict(x)
    train_score = accuracy_score(y,train_predcited)
    dev_predicted = estimator.predict(x_dev)
    dev_score = accuracy_score(y_dev,dev_predicted)

    print "Training Accuracy = %0.2f Dev Accuracy = %0.2f"%(train_score,dev_score)

我们的集成中需要 100 棵树,所以我们使用变量no_trees来定义树的数量。我们利用 scikit-learn 中的RandomForestClassifier类进行检查并应用。正如你所见,我们将所需的树的数量作为参数传递。然后,我们继续拟合我们的模型。

现在让我们找到我们训练集和开发集的模型准确度分数:

工作原理…

不错!我们在开发集上达到了 83%的准确率。让我们看看是否可以提高我们的得分。随机森林中还有其他可调的参数,可以调节以获得更好的模型。有关可以调节的参数列表,请参考以下链接:

scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html

我们调用search_parameters函数,并使用训练数据和开发数据来调整我们随机森林模型的各项参数。

在一些前面的实例中,我们使用了 GridSearchCV 来遍历参数空间,以寻找最佳的参数组合。GridSearchCV 进行的是非常彻底的搜索。然而,在本实例中,我们将使用 RandomizedSearchCV。我们为每个参数提供一个参数值的分布,并指定所需的迭代次数。在每次迭代中,RandomizedSearchCV 将从参数分布中随机选择一个值并拟合模型:

parameters = {"n_estimators" : np.random.randint(75,200,no_iterations),
"criterion" : ["gini", "entropy"],
"max_features" : [sqr_no_features,sqr_no_features*2,sqr_no_features*3,sqr_no_features+10]
}

我们提供一个参数字典,就像我们在 GridSearchCV 中做的那样。在我们的情况下,我们想要测试三个参数。

第一个参数是模型中的树木数量,通过n_estimators参数表示。通过调用 randint 函数,我们获得一个 75 到 200 之间的整数列表。树木的大小由no_iterations参数定义:

no_iterations = 20

这是我们将传递给 RandomizedSearchCV 的参数,表示我们希望执行的迭代次数。在这20个元素的数组中,RandomizedSearchCV 将为每次迭代随机抽取一个值。

下一个参数是准则,我们在基尼指数和熵之间随机选择,并将其作为每次迭代中拆分节点的准则。

最重要的参数max_features定义了算法在拆分每个节点时应该选择的特征数量。在我们描述随机森林的伪代码中,我们指定了每次拆分节点时需要随机选择 m 个特征。max_features参数定义了 m。在这里,我们提供了一个包含四个值的列表。变量sqr_no_features是输入数据集中特征数量的平方根:

sqr_no_features = int(np.sqrt(no_features))

列表中的其他值是平方根的一些变化。

让我们用这个参数分布来实例化 RandomizedSearchCV:

grid = RandomizedSearchCV(estimator=estimator,param_distributions=parameters,\
verbose=1, n_iter=no_iterations,random_state=77,n_jobs=-1,cv=5)

第一个参数是底层估算器,即我们试图优化其参数的模型。它是我们的RandomForestClassifier

estimator = RandomForestClassifier()

第二个参数param_distributions是通过字典参数定义的分布。我们定义了迭代次数,即我们希望运行 RandomForestClassifier 的次数,使用参数n_iter。通过cv参数,我们指定所需的交叉验证次数,在我们的例子中是5次交叉验证。

让我们继续拟合模型,看看模型的效果如何:

grid.fit(x,y)
print_model_worth(grid,x_dev,y_dev)

它是如何工作的…

如你所见,我们有五个折叠,也就是说,我们希望在每次迭代中进行五折交叉验证。我们总共执行20次迭代,因此我们将构建 100 个模型。

让我们看看print_model_worth函数内部。我们将网格对象和开发数据集传递给这个函数。网格对象在一个名为grid_scores_的属性中存储了它构建的每个模型的评估指标,这个属性是一个列表。让我们将这个列表按降序排序,以构建最佳模型:

scores = sorted(grid.grid_scores_, key=itemgetter(1), reverse=True) [0:5]

我们选择排名前五的模型,如索引所示。接下来我们打印这些模型的详细信息:

for model_no,score in enumerate(scores):
print "Model %d, Score = %0.3f"%(model_no+1,score.mean_validation_score)
print "Parameters = {0}".format(score.parameters)
print

我们首先打印评估分数,并接着展示模型的参数:

它是如何工作的…

我们根据模型的得分将模式按降序排列,从而将最佳的模型参数放在最前面。我们将选择这些参数作为我们的模型参数。属性best_estimator_将返回具有这些参数的模型。

让我们使用这些参数并测试我们的开发数据:

dev_predicted = grid.predict(x_dev)
print classification_report(y_dev,dev_predicted)

predict 函数将内部使用best_estimtor

它是如何工作的…

太棒了!我们有一个完美的模型,分类准确率为 100%。

还有更多…

在内部,RandomForestClassifier使用DecisionTreeClassifier。有关构建决策树时传递的所有参数,请参考以下链接:

scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

一个我们感兴趣的参数是 splitter。splitter 的默认值设置为 best。根据max_features属性,内部实现将选择划分机制。可用的划分机制包括以下几种:

  • best: 从由max_features参数定义的给定属性集中选择最佳可能的划分

  • random: 随机选择一个划分属性

你可能已经注意到,在实例化RandomForestClassifier时,这个参数不可用。唯一的控制方式是为max_features参数赋值,该值应小于数据集中的属性数量。

在工业界,随机森林被广泛用于变量选择。在 Scikit learn 中,变量重要性是通过基尼不纯度(gini impurity)来计算的。用于节点划分的基尼和熵准则根据它们将数据集划分为高不纯度的子集的能力来识别最佳划分属性,以便后续的划分能产生良好的分类。一个变量的重要性由它能在划分后的数据集中引入的不纯度量决定。有关更多详细信息,请参考以下书籍:

Breiman, Friedman, "分类与回归树",1984 年。

我们可以编写一个小函数来打印重要特征:

def get_feature_importance(model):
    feature_importance = model.feature_importances_
    fm_with_id = [(i,importance) for i,importance in enumerate(feature_importance)]
    fm_with_id = sorted(fm_with_id, key=itemgetter(1),reverse=True)[0:10]
    print "Top 10 Features"
    for importance in fm_with_id:
        print "Feature %d importance = %0.3f"%(importance[0],importance[1])
    print

一个 Random Forest 对象有一个名为feature_importances_的变量。我们使用这个变量并创建一个包含特征编号和重要性的元组列表:

    feature_importance = model.feature_importances_
    fm_with_id = [(i,importance) for i,importance in enumerate(feature_importance)]

我们接着按重要性降序排列,并选择前 10 个特征:

    fm_with_id = sorted(fm_with_id, key=itemgetter(1),reverse=True)[0:10]

然后我们打印出前 10 个特征:

还有更多…

随机森林的另一个有趣方面是袋外估计(OOB)。记住,我们最初从数据集中对每棵树进行自助采样(bootstrap)。由于自助采样,某些记录在某些树中不会被使用。假设记录 1 在 100 棵树中被使用,在 150 棵树中未被使用。然后,我们可以使用那 150 棵树来预测该记录的类别标签,从而计算该记录的分类误差。袋外估计可以有效地评估我们森林的质量。以下网址给出了 OOB 如何有效使用的示例:

scikit-learn.org/dev/auto_examples/ensemble/plot_ensemble_oob.html

Scikit learn 中的 RandomForestClassifier 类来源于ForestClassifier。其源代码可以在以下链接找到:

github.com/scikit-learn/scikit-learn/blob/a95203b/sklearn/ensemble/forest.py#L318

当我们在 RandomForestClassifier 中调用predict方法时,它内部调用了在 ForestClassifier 中定义的predict_proba方法。在这里,最终的预测不是通过投票完成,而是通过对森林中不同树的每个类别的概率进行平均,并基于最高概率决定最终类别。

Leo Breiman 关于随机森林的原始论文可以在以下链接下载:

link.springer.com/article/10.1023%2FA%3A1010933404324

你还可以参考 Leo Breiman 和 Adele Cutler 维护的网站:

www.stat.berkeley.edu/~breiman/RandomForests/cc_home.htm

另请参见

  • 第六章中的构建决策树以解决多类问题配方,机器学习 I

  • 第八章中的理解集成,梯度提升配方,模型选择与评估

  • 第八章中的理解集成,袋装方法配方,模型选择与评估

生长极端随机树

极端随机树,也称为 Extra Trees 算法,与前面配方中描述的随机森林在两方面有所不同:

  1. 它不使用自助法(bootstrapping)为集成中的每棵树选择实例;相反,它使用完整的训练数据集。

  2. 给定 K 作为在某个节点上要随机选择的属性数量,它选择一个随机切分点,而不考虑目标变量。

如前面的配方所示,随机森林在两个地方使用了随机化。首先,选择用于训练森林中树的实例时使用了自助法来选择训练实例。其次,在每个节点,随机选择了一组属性,从中选出一个属性,依据的是基尼不纯度或熵准则。极端随机树更进一步,随机选择分裂属性。

极端随机树在以下论文中被提出:

P. Geurts, D. Ernst., 和 L. Wehenkel,“极端随机树”,《机器学习》,63(1),3-42,2006

根据本文,除了之前列出的技术方面,还有两个方面使得极端随机树更为适用:

Extra-Trees 方法背后的原理是,通过显式地随机化切分点和属性,结合集成平均法,应该能够比其他方法使用的较弱随机化方案更强烈地减少方差。

与随机森林相比,切分点的随机化(即在每个节点选择用于切分数据集的属性)结合切分点的随机化,即忽略任何标准,最后平均每棵树的结果,将在未知数据集上表现出更优的性能。

第二个优点是计算复杂度:

从计算角度来看,假设树是平衡的,树的生长过程的复杂度与学习样本大小呈 N log N 的数量级,就像大多数树生长过程一样。然而,考虑到节点切分过程的简洁性,我们预计常数因子将比其他集成方法中局部优化切分点的情况要小得多。

由于没有计算时间用于识别最佳的切分属性,这种方法比随机森林在计算上更高效。

让我们写下构建极度随机化树的步骤。森林中所需的树的数量通常由用户指定。设 T 为需要构建的树的数量。

我们从 1 迭代到 T,也就是我们构建 T 棵树:

  • 对于每棵树,我们选择完整的输入数据集。

  • 然后我们继续拟合一棵树 t 到输入数据:

    • 随机选择 m 个属性。

    • 随机选择一个属性作为切分变量。

    • 将数据集分成两部分。请记住,树是二叉的。在树的每一层,输入数据集都会被分成两部分。

    • 对我们分割的数据集递归执行前述三个步骤。

  • 最后,我们返回 T 棵树。

  • 让我们看看极度随机化树的配方。

准备好了...

我们将生成一些分类数据集来演示极度随机化树。为此,我们将利用 Scikit Learn 中极度随机化树集成模块的实现。

如何做到这一点...

我们从加载所有必要的库开始。让我们利用sklearn.dataset模块中的make_classification方法来生成训练数据:

from sklearn.datasets import make_classification
from sklearn.metrics import classification_report, accuracy_score
from sklearn.cross_validation import train_test_split, cross_val_score
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.grid_search import RandomizedSearchCV
from operator import itemgetter

def get_data():
 """
 Make a sample classification dataset
 Returns : Independent variable y, dependent variable x
 """
 no_features = 30
 redundant_features = int(0.1*no_features)
 informative_features = int(0.6*no_features)
 repeated_features = int(0.1*no_features)
 x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
 n_informative = informative_features, n_redundant = redundant_features \
 ,n_repeated = repeated_features,random_state=7)
    return x,y

我们编写build_forest函数,在其中构建完全生长的树,并继续评估森林的性能:

def build_forest(x,y,x_dev,y_dev):
    """
    Build a Extremely random tress
    and evaluate peformance
    """
    no_trees = 100
    estimator = ExtraTreesClassifier(n_estimators=no_trees,random_state=51)
    estimator.fit(x,y)

    train_predcited = estimator.predict(x)
    train_score = accuracy_score(y,train_predcited)
    dev_predicted = estimator.predict(x_dev)
    dev_score = accuracy_score(y_dev,dev_predicted)

    print "Training Accuracy = %0.2f Dev Accuracy = %0.2f"%(train_score,dev_score)
    print "cross validated score"
    print cross_val_score(estimator,x_dev,y_dev,cv=5)

def search_parameters(x,y,x_dev,y_dev):
    """
    Search the parameters 
    """
    estimator = ExtraTreesClassifier()
    no_features = x.shape[1]
    no_iterations = 20
    sqr_no_features = int(np.sqrt(no_features))

    parameters = {"n_estimators"      : np.random.randint(75,200,no_iterations),
                 "criterion"         : ["gini", "entropy"],
                 "max_features"      : [sqr_no_features,sqr_no_features*2,sqr_no_features*3,sqr_no_features+10]
                 }

    grid = RandomizedSearchCV(estimator=estimator,param_distributions=parameters,\
    verbose=1, n_iter=no_iterations,random_state=77,n_jobs=-1,cv=5)
    grid.fit(x,y)
    print_model_worth(grid,x_dev,y_dev)

    return grid.best_estimator_

最后,我们编写一个主函数来调用我们定义的函数:

if __name__ == "__main__":
    x,y = get_data()    

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    build_forest(x_train,y_train,x_dev,y_dev)
    model = search_parameters(x,y,x_dev,y_dev)

它是如何工作的…

让我们从主函数开始。我们调用get_data来获取预测属性和响应属性。在get_data函数内部,我们利用 make_classification 数据集来生成我们配方的训练数据,具体如下:

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

让我们看看传递给make_classification方法的参数。第一个参数是所需的实例数量;在这个例子中,我们需要 500 个实例。第二个参数是每个实例所需的属性数量。我们需要 30 个属性。第三个参数,flip_y,会随机交换 3%的实例。这样做是为了给我们的数据引入一些噪声。下一个参数指定了这些 30 个特征中,有多少个特征应足够信息量,以便用于分类。我们指定 60%的特征,也就是 30 个中的 18 个,应该是有信息量的。下一个参数是冗余特征。这些特征是通过有信息量的特征的线性组合生成的,以便在特征之间引入相关性。最后,重复特征是从有信息量特征和冗余特征中随机选取的重复特征。

让我们使用train_test_split将数据分为训练集和测试集。我们将 30%的数据用于测试:

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

再次,我们使用train_test_split将我们的测试数据分为开发集和测试集:

    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

数据已经分割用于构建、评估和测试模型,我们继续构建我们的模型:

build_forest(x_train,y_train,x_dev,y_dev)

我们调用build_forest函数,使用我们的训练集和开发集数据来构建极端随机化树模型。让我们来看一下这个函数:

no_trees = 100
    estimator = ExtraTreesClassifier(n_estimators=no_trees,random_state=51)
    estimator.fit(x,y)

    train_predcited = estimator.predict(x)
    train_score = accuracy_score(y,train_predcited)
    dev_predicted = estimator.predict(x_dev)
    dev_score = accuracy_score(y_dev,dev_predicted)

    print "Training Accuracy = %0.2f Dev Accuracy = %0.2f"%(train_score,dev_score)
    print "cross validated score"
    print cross_val_score(estimator,x_dev,y_dev,cv=5)

我们需要 100 棵树来构建我们的集成模型,因此我们使用变量 no_trees 来定义树的数量。我们利用 Scikit Learn 中的ExtraTreesClassifier类。如你所见,我们将所需的树的数量作为参数传递。这里有一个需要注意的点是参数 bootstrap。有关 ExtraTreesClassifier 的参数,请参考以下网址:

scikit-learn.org/stable/modules/generated/sklearn.ensemble.ExtraTreesClassifier.html

参数 bootstrap 默认设置为False。与以下网址给出的RandomForestClassifier的 bootstrap 参数进行对比:

scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html

如前所述,森林中的每棵树都是用所有记录进行训练的。

我们继续如下拟合我们的模型:

    train_predcited = estimator.predict(x)

然后我们继续找到训练集和开发集数据的模型准确度得分:

    train_score = accuracy_score(y,train_predcited)
    dev_predicted = estimator.predict(x_dev)
    dev_score = accuracy_score(y_dev,dev_predicted)

让我们打印出训练集和开发集的数据得分:

    print "Training Accuracy = %0.2f Dev Accuracy = %0.2f"%(train_score,dev_score)

它是如何工作的…

现在让我们进行五折交叉验证,查看模型预测的结果:

它是如何工作的…

相当不错的结果。我们几乎在其中一个折叠中达到了 90%的准确率。我们可以像对随机森林一样在参数空间内进行随机搜索。让我们调用 search_parameters 函数,传入我们的训练和测试数据集。请参阅前面的内容了解 RandomizedSearchCV 的解释。然后,我们将打印 search_parameters 函数的输出:

如何运作……

如同前面的步骤,我们已按得分从高到低对模型进行排名,因此最好的模型参数会排在最前面。我们将选择这些参数作为我们的模型参数。属性 best_estimator_ 将返回具有这些参数的模型。

接下来,您将看到为最佳估计器生成的分类报告。预测函数将内部使用 best_estimator_。报告是通过以下代码生成的:

dev_predicted = grid.predict(x_dev)
print classification_report(y_dev,dev_predicted)

很棒!我们有一个完美的模型,分类准确率为 100%。

还有更多内容……

极端随机树在时间序列分类问题中非常受欢迎。有关更多信息,请参阅以下论文:

Geurts, P., Blanco Cuesta A., 和 Wehenkel, L. (2005a). 生物序列分类的分段与组合方法。收录于:IEEE 生物信息学与计算生物学计算智能研讨会论文集,194–201。

参见

  • 构建决策树解决多类别问题 章节内容参见 第六章,机器学习 I

  • 理解集成方法,袋装法 章节内容参见 第八章,模型选择与评估

  • 从树到森林的生长,随机森林 章节内容参见 第九章,机器学习 III

旋转森林的生长

随机森林和袋装法在非常大的集成中能够给出令人印象深刻的结果;拥有大量估计器会提高这些方法的准确性。相反,旋转森林是设计用来处理较小数量的集成。

让我们写下构建旋转森林的步骤。森林中所需的树木数量通常由用户指定。让 T 表示需要构建的树的数量。

我们从 1 开始迭代直到 T,也就是说,我们构建 T 棵树。

对于每棵树 t,执行以下步骤:

  • 将训练集中的特征分成 K 个大小相等的不重叠子集。

  • 我们有 K 个数据集,每个数据集包含 K 个特征。对于每个 K 数据集,我们进行如下操作:从每个 K 数据集中自助抽取 75%的数据,并使用抽取的样本进行后续步骤:

    • 对 K 中的第 i 个子集进行主成分分析。保留所有主成分。对于 Kth 子集中的每个特征 j,我们有一个主成分 a。我们将其表示为 aij,这是第 i 个子集中的第 j 个属性的主成分。

    • 存储子集的主成分。

  • 创建一个大小为 n X n 的旋转矩阵,其中 n 是属性的总数。将主成分排列在矩阵中,使得这些成分匹配原始训练数据集中特征的位置。

  • 使用矩阵乘法将训练数据集投影到旋转矩阵上。

  • 使用投影数据集构建决策树。

  • 存储树和旋转矩阵。

有了这些知识,让我们跳入我们的配方。

准备工作…

我们将生成一些分类数据集来演示旋转森林。根据我们的了解,目前没有现成的 Python 实现来支持旋转森林。因此,我们将编写自己的代码。我们将利用 Scikit Learn 实现的决策树分类器,并使用train_test_split方法进行自助抽样。

如何实现…

我们将从加载所有必要的库开始。让我们利用来自sklearn.dataset模块的make_classification方法生成训练数据。接着我们使用一个方法来选择一个随机属性子集,称为gen_random_subset

from sklearn.datasets import make_classification
from sklearn.metrics import classification_report
from sklearn.cross_validation import train_test_split
from sklearn.decomposition import PCA
from sklearn.tree import DecisionTreeClassifier
import numpy as np

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 50
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

def get_random_subset(iterable,k):
    subsets = []
    iteration = 0
    np.random.shuffle(iterable)
    subset = 0
    limit = len(iterable)/k
    while iteration < limit:
        if k <= len(iterable):
            subset = k
        else:
            subset = len(iterable)
        subsets.append(iterable[-subset:])
        del iterable[-subset:]
        iteration+=1
    return subsets

现在我们编写一个函数build_rotationtree_model,在其中我们将构建完全生长的树,并使用model_worth函数评估森林的表现:

def build_rotationtree_model(x_train,y_train,d,k):
    models = []
    r_matrices = []
    feature_subsets = []
    for i in range(d):
        x,_,_,_ = train_test_split(x_train,y_train,test_size=0.3,random_state=7)
        # Features ids
        feature_index = range(x.shape[1])
        # Get subsets of features
        random_k_subset = get_random_subset(feature_index,k)
        feature_subsets.append(random_k_subset)
        # Rotation matrix
        R_matrix = np.zeros((x.shape[1],x.shape[1]),dtype=float)
        for each_subset in random_k_subset:
            pca = PCA()
            x_subset = x[:,each_subset]
            pca.fit(x_subset)
            for ii in range(0,len(pca.components_)):
                for jj in range(0,len(pca.components_)):
                    R_matrix[each_subset[ii],each_subset[jj]] = pca.components_[ii,jj]

        x_transformed = x_train.dot(R_matrix)

        model = DecisionTreeClassifier()
        model.fit(x_transformed,y_train)
        models.append(model)
        r_matrices.append(R_matrix)
    return models,r_matrices,feature_subsets

def model_worth(models,r_matrices,x,y):

    predicted_ys = []
    for i,model in enumerate(models):
        x_mod =  x.dot(r_matrices[i])
        predicted_y = model.predict(x_mod)
        predicted_ys.append(predicted_y)

    predicted_matrix = np.asmatrix(predicted_ys)
    final_prediction = []
    for i in range(len(y)):
        pred_from_all_models = np.ravel(predicted_matrix[:,i])
        non_zero_pred = np.nonzero(pred_from_all_models)[0]  
        is_one = len(non_zero_pred) > len(models)/2
        final_prediction.append(is_one)

    print classification_report(y, final_prediction)

最后,我们编写一个主函数,用于调用我们之前定义的函数:

if __name__ == "__main__":
    x,y = get_data()    
#    plot_data(x,y)

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    # Build a bag of models
    models,r_matrices,features = build_rotationtree_model(x_train,y_train,25,5)
    model_worth(models,r_matrices,x_train,y_train)
    model_worth(models,r_matrices,x_dev,y_dev)

它是如何工作的…

让我们从主函数开始。我们调用get_data来获取响应属性中的预测器属性。在get_data内部,我们利用make_classification数据集生成我们的训练数据,具体如下:

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

让我们看一下传递给make_classification方法的参数。第一个参数是所需实例的数量;在这种情况下,我们需要 500 个实例。第二个参数是每个实例所需的属性数量。我们需要 30 个属性。第三个参数flip_y随机交换 3%的实例。这样做是为了在数据中引入一些噪声。下一个参数是关于 30 个特征中应该具有足够信息量用于分类的特征数量。我们规定,60%的特征,也就是 30 个中的 18 个应该具有信息量。下一个参数是冗余特征。冗余特征是通过信息性特征的线性组合生成的,用于在特征之间引入相关性。最后,重复特征是从信息性特征和冗余特征中随机选择的重复特征。

让我们使用train_test_split将数据分割成训练集和测试集。我们将 30%的数据用于测试:

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

我们再次利用train_test_split将测试数据分成开发集和测试集,具体如下:

    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

在数据被划分为用于构建、评估和测试模型的部分后,我们开始构建我们的模型:

    models,r_matrices,features = build_rotationtree_model(x_train,y_train,25,5)

我们调用build_rotationtree_model函数来构建我们的旋转森林。我们传入我们的训练数据、预测变量x_train和响应变量y_train,要构建的树的总数(在本例中为25),以及要使用的特征子集(在本例中为5)。

让我们跳到该函数:

    models = []
    r_matrices = []
    feature_subsets = []

我们首先声明三个列表,用于存储每棵决策树、该树的旋转矩阵,以及在该迭代中使用的特征子集。然后我们继续构建我们的集成中的每棵树。

作为第一项工作,我们进行自助法以保留数据的 75%:

        x,_,_,_ = train_test_split(x_train,y_train,test_size=0.3,random_state=7)

我们利用 Scikit learn 中的train_test_split函数进行自助法(bootstrapping)。然后我们按以下方式决定特征子集:

        # Features ids
        feature_index = range(x.shape[1])
        # Get subsets of features
        random_k_subset = get_random_subset(feature_index,k)
        feature_subsets.append(random_k_subset)

get_random_subset函数接受特征索引和所需 k 个子集的数量作为参数,并返回 K 个子集。

在该函数内部,我们会打乱特征索引。特征索引是一个数字数组,从 0 开始,直到训练集中的特征数量:

    np.random.shuffle(iterable)

假设我们有 10 个特征,我们的 k 值是 5,表示我们需要具有 5 个不重叠特征索引的子集;我们需要进行两次迭代。我们将所需的迭代次数存储在 limit 变量中:

    limit = len(iterable)/k
    while iteration < limit:
        if k <= len(iterable):
            subset = k
        else:
            subset = len(iterable)
        iteration+=1

如果我们的所需子集少于总属性数,我们可以继续使用我们可迭代对象中的前 k 个条目。由于我们已经打乱了可迭代对象,我们将在不同的时间返回不同的数量:

        subsets.append(iterable[-subset:])

在选择一个子集后,我们将其从可迭代对象中移除,因为我们需要不重叠的集合:

        del iterable[-subset:]

当所有子集准备好后,我们按以下方式声明我们的旋转矩阵:

        # Rotation matrix
        R_matrix = np.zeros((x.shape[1],x.shape[1]),dtype=float)

如您所见,我们的旋转矩阵的大小是 n x n,其中 n 是我们数据集中的属性数量。您可以看到,我们使用了 shape 属性来声明这个填充了零的矩阵:

        for each_subset in random_k_subset:
            pca = PCA()
            x_subset = x[:,each_subset]
            pca.fit(x_subset)

对于每个只有 K 个特征的数据子集,我们继续进行主成分分析。

我们按如下方式填充我们的旋转矩阵:

            for ii in range(0,len(pca.components_)):
                for jj in range(0,len(pca.components_)):
                    R_matrix[each_subset[ii],each_subset[jj]] = pca.components_[ii,jj]

例如,假设我们的子集中有三个属性,总共有六个属性。为了说明,假设我们的子集是:

2,4,6 and 1,3,5

我们的旋转矩阵 R 是 6 x 6 的大小。假设我们要填充第一个特征子集的旋转矩阵。我们将有三个主成分,分别对应于 2、4 和 6,大小为 1 x 3。

来自 Scikit learn 的 PCA 输出是一个大小为主成分 X 特征的矩阵。我们通过 for 循环遍历每个主成分值。在第一次运行时,我们感兴趣的特征是 2,来自 PCA 的主成分矩阵(0,0)的单元格给出了特征 2 对主成分 1 的贡献值。我们需要找到旋转矩阵中这个值的位置。我们使用主成分矩阵中的索引 ii 和 jj 与子集列表结合来找到旋转矩阵中的正确位置:

                    R_matrix[each_subset[ii],each_subset[jj]] = pca.components_[ii,jj]

each_subset[0]each_subset[0]将使我们处于旋转矩阵中的(2,2)单元格。随着循环的进行,组件矩阵中(0,1)单元格中的下一个组件值将被放置到旋转矩阵中的(2,4)单元格,最后一个将放在旋转矩阵中的(2,6)单元格。这对于第一个子集中的所有属性都是如此。让我们进入第二个子集;这里第一个属性是 1。组件矩阵中的(0,0)单元格对应于旋转矩阵中的(1,1)单元格。

以这种方式进行,你会发现属性组件的值与属性本身的顺序一致。

在旋转矩阵准备好后,让我们将输入数据投影到旋转矩阵上:

        x_transformed = x_train.dot(R_matrix)

现在是时候拟合我们的决策树了:

model = DecisionTreeClassifier()
model.fit(x_transformed,y_train)

最后,我们存储我们的模型和相应的旋转矩阵:

models.append(model)
r_matrices.append(R_matrix)

构建好模型后,让我们使用model_worth函数来检验模型在训练数据和开发数据上的表现:

model_worth(models,r_matrices,x_train,y_train)
model_worth(models,r_matrices,x_dev,y_dev)

让我们看看我们的model_worth函数:

    for i, model in enumerate(models):
        x_mod =  x.dot(r_matrices[i])
        predicted_y = model.predict(x_mod)
        predicted_ys.append(predicted_y)

在这个函数内部,我们使用每棵树进行预测。然而,在预测之前,我们先使用旋转矩阵对输入数据进行投影。我们将所有预测的输出存储在一个名为predicted_ys的列表中。假设我们有 100 个实例需要预测,并且我们有 10 个模型。在每个实例中,我们会有 10 个预测结果。为了方便起见,我们将这些结果存储为一个矩阵:

predicted_matrix = np.asmatrix(predicted_ys)

现在我们继续为每个输入记录给出最终分类:

    final_prediction = []
    for i in range(len(y)):
        pred_from_all_models = np.ravel(predicted_matrix[:,i])
        non_zero_pred = np.nonzero(pred_from_all_models)[0]  
        is_one = len(non_zero_pred) > len(models)/2
        final_prediction.append(is_one)

我们会将最终的预测结果存储在一个名为final_prediction的列表中。我们遍历每个实例的预测结果。假设我们在第一个实例(在 for 循环中 i=0);pred_from_all_models存储来自我们模型中所有树的输出。它是一个由 0 和 1 组成的数组,表示在该实例中模型所分类的类别。

我们将其转换为另一个数组non_zero_pred,该数组只包含父数组中非零的条目。

最后,如果这个非零数组的长度大于我们模型数量的一半,我们就说我们最终的预测结果是该实例的类别为 1。我们在这里实现的是经典的投票机制。

让我们通过调用classification_report来看看我们的模型现在有多好:

    print classification_report(y, final_prediction)

以下是我们模型在训练集上的表现:

它是如何工作的…

让我们看看我们的模型在开发数据集上的表现:

它是如何工作的…

还有更多内容…

有关旋转森林的更多信息可以参考以下论文:

旋转森林:一种新的分类器集成方法,Juan J. Rodriguez,IEEE 计算机学会会员,Ludmila I. Kuncheva,IEEE 会员,Carlos J. Alonso

该论文还声称,在 33 个数据集上将极度随机化树与 Bagging、AdBoost 和随机森林进行比较时,极度随机化树的表现优于其他三种算法。

与梯度提升法类似,论文的作者声称极度随机化方法是一个总体框架,且基础集成方法不一定非得是决策树。目前正在进行其他算法的测试工作,如朴素贝叶斯、神经网络等。

另见

  • 提取主成分 配方见于第四章,数据分析 - 深度解析

  • 通过随机投影减少数据维度 配方见于第四章,数据分析 - 深度解析

  • 建立决策树以解决多分类问题 配方见于第六章,机器学习 I

  • 理解集成方法,梯度提升法 配方见于第八章,模型选择与评估

  • 从树到森林,随机森林 配方见于第九章,机器学习 III

  • 生长极度随机化树 配方见于第九章,机器学习 III

第十章 大规模机器学习 – 在线学习

在本章中,我们将看到以下内容:

  • 使用感知机作为在线线性算法

  • 使用随机梯度下降进行回归

  • 使用随机梯度下降进行分类

引言

本章中,我们将专注于大规模机器学习以及适合解决此类大规模问题的算法。直到现在,当我们训练所有模型时,我们假设训练集可以完全存入计算机的内存中。在本章中,我们将看到如何在这一假设不再成立时构建模型。我们的训练数据集非常庞大,因此无法完全加载到内存中。我们可能需要按块加载数据,并且仍能生成一个具有良好准确度的模型。训练集无法完全存入计算机内存的问题可以扩展到流数据上。对于流数据,我们不会一次性看到所有的数据。我们应该能够根据我们所接触到的数据做出决策,并且还应有一个机制,在新数据到达时不断改进我们的模型。

我们将介绍基于随机梯度下降的算法框架。这是一个多功能的框架,用于处理非常大规模的数据集,这些数据集无法完全存入我们的内存中。包括逻辑回归、线性回归和线性支持向量机等多种类型的线性算法,都可以通过该框架进行处理。我们在前一章中介绍的核技巧,也可以纳入该框架中,以应对具有非线性关系的数据集。

我们将从感知机算法开始介绍,感知机是最古老的机器学习算法。感知机易于理解和实现。然而,感知机仅限于解决线性问题。基于核的感知机可以用于解决非线性数据集。

在我们的第二个案例中,我们将正式介绍基于梯度下降的方法框架,以及如何使用该方法执行回归任务。我们将查看不同的损失函数,看看如何使用这些函数构建不同类型的线性模型。我们还将看到感知机如何属于随机梯度下降家族。

在我们的最后一个案例中,我们将看到如何使用随机梯度下降框架构建分类算法。

即使我们没有直接的流数据示例,通过现有的数据集,我们将看到如何解决流数据的使用场景。在线学习算法不限于流数据,它们也可以应用于批量数据,只是它们一次只处理一个实例。

使用感知机作为在线学习算法

如前所述,感知机是最古老的机器学习算法之一。它最早在 1943 年的一篇论文中提到:

《神经活动中固有思想的逻辑演算》。沃伦·S·麦卡洛克和沃尔特·皮茨,伊利诺伊大学医学学院,伊利诺伊神经精神病学研究所精神病学系,芝加哥大学,美国芝加哥。

让我们重新审视分类问题的定义。每个记录或实例可以表示为一个集合(X, y),其中 X 是一组属性,y 是相应的类标签。

学习一个目标函数 F,该函数将每个记录的属性集映射到预定义的类标签 y,是分类算法的任务。

我们的差异在于我们面对的是一个大规模的学习问题。我们的所有数据无法完全加载到主内存中。因此,我们需要将数据保存在磁盘上,并且每次只使用其中的一部分来构建我们的感知器模型。

让我们继续概述感知器算法:

  1. 将模型的权重初始化为一个小的随机数。

  2. 将输入数据x以其均值进行中心化。

  3. 在每个时间步 t(也称为 epoch):

    • 洗牌数据集

    • 选择单个记录实例并进行预测。

    • 观察预测与真实标签输出的偏差。

    • 如果预测与真实标签不同,则更新权重。

让我们考虑以下场景。我们将完整的数据集存储在磁盘上。在单个 epoch 中,即在步骤 3 中,所有提到的步骤都在磁盘上的所有数据上执行。在在线学习场景中,一组基于窗口函数的实例将随时提供给我们。我们可以在单个 epoch 中根据窗口中的实例数量更新权重。

让我们来看一下如何更新我们的权重。

假设我们的输入 X 如下:

使用感知器作为在线学习算法

我们的Y如下:

使用感知器作为在线学习算法

我们将定义我们的权重如下方程:

使用感知器作为在线学习算法

我们在看到每个记录后的预测定义如下:

使用感知器作为在线学习算法

符号函数在权重和属性的乘积为正时返回+1,如果乘积为负,则返回-1。

感知器接着将预测的 y 与实际的 y 进行比较。如果预测的 y 是正确的,它将继续处理下一个记录。如果预测错误,则有两种情况。如果预测的 y 是+1,而实际的 y 是-1,则减小该权重值与 x 的乘积,反之亦然。如果实际的 y 是+1,而预测的 y 是-1,则增加权重。我们可以用方程式来更清楚地表示这一点:

使用感知器作为在线学习算法

通常,学习率 alpha 会被提供,以便以可控的方式更新权重。由于数据中存在噪声,完全的增量和减量将导致权重无法收敛:

将感知机作为在线学习算法使用

Alpha 是一个非常小的值,范围在 0.1 到 0.4 之间。

现在让我们跳入我们的配方。

准备工作

我们将使用 make_classification 生成数据,采用生成器函数批量生成,以模拟大规模数据和数据流,并继续编写感知机算法。

如何实现…

让我们加载必要的库。然后我们将编写一个名为 get_data 的生成器函数:

from sklearn.datasets import make_classification
from sklearn.metrics import  classification_report
from sklearn.preprocessing import scale
import numpy as np

def get_data(batch_size):
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    b_size = 0
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.8*no_features)
    repeated_features = int(0.1*no_features)

    while b_size < batch_size:
        x,y = make_classification(n_samples=1000,n_features=no_features,flip_y=0.03,\
                n_informative = informative_features, n_redundant = redundant_features \
                ,n_repeated = repeated_features, random_state=51)
        y_indx = y < 1
        y[y_indx] = -1
        x = scale(x,with_mean=True,with_std=True)

        yield x,y
        b_size+=1

我们将编写两个函数,一个用来构建感知机模型,另一个用来测试我们的模型的有效性:

def build_model(x,y,weights,epochs,alpha=0.5):
    """
    Simple Perceptron
    """

    for i in range(epochs):

        # Shuffle the dataset
        shuff_index = np.random.shuffle(range(len(y)))
        x_train = x[shuff_index,:].reshape(x.shape)
        y_train = np.ravel(y[shuff_index,:])

        # Build weights one instance at a time
        for index in range(len(y)):
            prediction = np.sign( np.sum(x_train[index,:] * weights) ) 
            if prediction != y_train[index]:
                weights = weights + alpha * (y_train[index] * x_train[index,:])

    return weights

def model_worth(x,y,weights):
    prediction = np.sign(np.sum(x * weights,axis=1))
print classification_report(y,prediction)

最后,我们将编写主函数来调用所有前面的函数,以展示感知机算法:

if __name__ == "__main__":
    data = get_data(10)    
    x,y = data.next()
    weights = np.zeros(x.shape[1])    
    for i in range(10):
        epochs = 100
        weights = build_model(x,y,weights,epochs)
        print
        print "Model worth after receiving dataset batch %d"%(i+1)    
        model_worth(x,y,weights)
        print
        if i < 9:
            x,y = data.next()

工作原理…

让我们从主函数开始。我们将要求生成器给我们发送 10 组数据:

    data = get_data(10)    

在这里,我们希望模拟大规模数据和数据流。在构建模型时,我们无法访问所有数据,只能访问部分数据:

    x,y = data.next()

我们将使用生成器中的 next() 函数来获取下一组数据。在 get_data 函数中,我们将使用 scikit-learn 的 make_classification 函数:

        x,y = make_classification(n_samples=1000,n_features=no_features,flip_y=0.03,\
                n_informative = informative_features, n_redundant = redundant_features \
                ,n_repeated = repeated_features, random_state=51)

让我们看看传递给 make_classification 方法的参数。第一个参数是所需的实例数量,在本例中,我们需要 1,000 个实例。第二个参数是每个实例所需的属性数量。我们假设需要 30 个属性。第三个参数 flip_y 随机交换 3% 的实例。这是为了在数据中引入一些噪声。下一个参数涉及这 30 个特征中有多少个应该足够有信息量,以便用于我们的分类。我们指定 60% 的特征,即 30 个特征中的 18 个,应该是有信息量的。下一个参数是冗余特征。这些特征是通过有信息量的特征的线性组合生成的,以便在特征之间引入相关性。最后,重复特征是从有信息量的特征和冗余特征中随机抽取的重复特征。

当我们调用 next() 时,我们将获得 1,000 个数据实例。该函数返回一个 y 标签,值为 {0,1};我们希望得到 {-1,+1},因此我们将 y 中的所有零改为 -1

        y_indx = y < 1
        y[y_indx] = -1

最后,我们将使用 scikit-learn 的 scale 函数来对数据进行中心化处理。

让我们使用第一批数据来构建模型。我们将用零初始化我们的权重矩阵:

    weights = np.zeros(x.shape[1])    

由于我们需要 10 批数据来模拟大规模学习和数据流,因此我们将在 for 循环中执行 10 次模型构建:

    for i in range(10):
        epochs = 100
        weights = build_model(x,y,weights,epochs)

我们的感知机算法在build_model中构建。一个预测变量 x、响应变量 y、权重矩阵和时间步数或周期数作为参数传递。在我们的情况下,我们已将周期数设置为100。此函数还有一个附加参数:alpha 值:

def build_model(x,y,weights,epochs,alpha=0.5)

默认情况下,我们将 alpha 值设置为0.5

让我们在build_model中查看。我们将从数据洗牌开始:

        # Shuffle the dataset
        shuff_index = np.random.shuffle(range(len(y)))
        x_train = x[shuff_index,:].reshape(x.shape)
        y_train = np.ravel(y[shuff_index,:])

我们将遍历数据集中的每条记录,并开始更新我们的权重:


 # Build weights one instance at a time
        for index in range(len(y)):
            prediction = np.sign( np.sum(x_train[index,:] * weights) ) 
            if prediction != y_train[index]:
                weights = weights + alpha * (y_train[index] * x_train[index,:])

在 for 循环中,你可以看到我们在做预测:

            prediction = np.sign( np.sum(x_train[index,:] * weights) ) 

我们将把训练数据与权重相乘,并将它们加在一起。最后,我们将使用 np.sign 函数来获取我们的预测结果。根据预测结果,我们将更新我们的权重:

                weights = weights + alpha * (y_train[index] * x_train[index,:])

就是这样。我们将把权重返回给调用函数。

在我们的主函数中,我们将调用model_worth函数来打印模型的优度。这里,我们将使用classification_report便捷函数来打印模型的准确度评分:

        print
        print "Model worth after receiving dataset batch %d"%(i+1)    
        model_worth(x,y,weights)

然后我们将继续更新模型以处理下一批输入数据。请注意,我们没有更改weights参数。它会随着每批新数据的到来而更新。

让我们看看model_worth打印了什么:

它是如何工作的…

还有更多…

Scikit-learn 为我们提供了感知机的实现。更多细节请参考以下网址:

scikit-learn.org/stable/modules/generated/sklearn.linear_model.Perceptron.html

感知机算法中可以改进的另一个方面是使用更多特征。

记住预测方程,我们可以将其重写如下:

还有更多…

我们用一个函数替换了 x 值。在这里,我们可以发送一个特征生成器。例如,一个多项式特征生成器可以添加到我们的get_data函数中,如下所示:

def get_data(batch_size):
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    b_size = 0
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.8*no_features)
    repeated_features = int(0.1*no_features)
    poly = PolynomialFeatures(degree=2)

    while b_size < batch_size:
        x,y = make_classification(n_samples=1000,n_features=no_features,flip_y=0.03,\
                n_informative = informative_features, n_redundant = redundant_features \
                ,n_repeated = repeated_features, random_state=51)
        y_indx = y < 1
        y[y_indx] = -1
        x = poly.fit_transform(x)
        yield x,y
        b_size+=1

最后,基于核的感知机算法可以处理非线性数据集。有关基于核的感知机的更多信息,请参阅维基百科文章:

en.wikipedia.org/wiki/Kernel_perceptron

另见

  • 在第五章中,学习和使用核的方法,数据挖掘 - 在大海捞针中找针

使用随机梯度下降进行回归

在典型的回归设置中,我们有一组预测变量(实例),如下所示:

使用随机梯度下降进行回归

每个实例有 m 个属性,如下所示:

使用随机梯度下降进行回归

响应变量 Y 是一个实值条目的向量。回归的任务是找到一个函数,使得当 x 作为输入提供给该函数时,它应返回 y:

使用随机梯度下降进行回归

前面的函数通过一个权重向量进行参数化,也就是说,使用权重向量和输入向量的组合来预测Y,因此通过权重向量重新编写函数会得到如下形式:

使用随机梯度下降进行回归

所以,现在的问题是我们如何知道我们拥有正确的权重向量?我们将使用损失函数 L 来得到正确的权重向量。损失函数衡量了预测错误的成本。它通过经验来衡量预测 y 时的成本,当实际值为 y 时。回归问题现在变成了寻找正确的权重向量的问题,该向量能够最小化损失函数。对于我们包含n个元素的整个数据集,总的损失函数如下:

使用随机梯度下降进行回归

我们的权重向量应该是那些能够最小化前面值的权重向量。

梯度下降是一种优化技术,用于最小化前面的方程。对于这个方程,我们将计算梯度,即相对于 W 的一阶导数。

不同于批量梯度下降等其他优化技术,随机梯度下降每次只操作一个实例。随机梯度下降的步骤如下:

  1. 对于每一个周期,打乱数据集。

  2. 选择一个实例及其响应变量 y。

  3. 计算损失函数及其相对于权重的导数。

  4. 更新权重。

假设:

使用随机梯度下降进行回归

这表示相对于 w 的导数。权重更新如下:

使用随机梯度下降进行回归

如你所见,权重朝着与梯度相反的方向移动,从而实现下降,最终得到的权重向量值能够减少目标成本函数。

平方损失是回归中常用的损失函数。一个实例的平方损失定义如下:

使用随机梯度下降进行回归

前面方程的导数被代入到权重更新方程中。通过这些背景知识,让我们继续介绍随机梯度下降回归的步骤。

如感知机中所解释的,学习率 eta 被添加到权重更新方程中,以避免噪声的影响:

使用随机梯度下降进行回归

准备工作

我们将利用 scikit-learn 实现的 SGD 回归。与之前的一些示例一样,我们将使用 scikit-learn 中的make_regression函数生成数据,以展示随机梯度下降回归。

如何操作…

让我们从一个非常简单的示例开始,演示如何构建一个随机梯度下降回归器。

我们首先将加载所需的库。然后我们将编写一个函数来生成预测变量和响应变量,以演示回归:

from sklearn.datasets import make_regression
from sklearn.linear_model import SGDRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.cross_validation import train_test_split

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30

    x,y = make_regression(n_samples=1000,n_features=no_features,\
             random_state=51)
    return x,y

我们将继续编写有助于我们构建、验证和检查模型的函数:

def build_model(x,y):
    estimator = SGDRegressor(n_iter = 10, shuffle=True,loss = "squared_loss", \
            learning_rate='constant',eta0=0.01,fit_intercept=True, \
            penalty='none')
    estimator.fit(x,y)

    return estimator

def model_worth(model,x,y):
    predicted_y = model.predict(x)
    print "\nMean absolute error = %0.2f"%mean_absolute_error(y,predicted_y)
    print "Mean squared error = %0.2f"%mean_squared_error(y,predicted_y)

def inspect_model(model):
    print "\nModel Itercept {0}".format(model.intercept_)
    print
    for i,coef in enumerate(model.coef_):
        print "Coefficient {0} = {1:.3f}".format(i+1,coef)

最后,我们将编写我们的主函数来调用所有前面的函数:

if __name__ == "__main__":
    x,y = get_data()

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    model = build_model(x_train,y_train)

    inspect_model(model)

    print "Model worth on train data"
    model_worth(model,x_train,y_train)
    print "Model worth on dev data"
    model_worth(model,x_dev,y_dev)

    # Building model with l2 regularization
    model = build_model_regularized(x_train,y_train)
    inspect_model(model)

工作原理…

让我们从主函数开始。我们将调用get_data函数来生成我们的预测变量 x 和响应变量 y:

    x,y = get_data()

get_data函数中,我们将利用 scikit-learn 中便捷的make_regression函数来生成回归问题的数据集:

    no_features = 30
    x,y = make_regression(n_samples=1000,n_features=no_features,\
             random_state=51)	

如您所见,我们将生成一个包含 1,000 个实例的数据集,其中实例数量由n_samples参数指定,特征数量由n_features参数定义,共有 30 个特征。

让我们使用train_test_split将数据划分为训练集和测试集。我们将保留 30%的数据用于测试:

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

我们将再次利用train_test_split来将测试数据划分为开发集和测试集:

    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

在数据划分为用于构建、评估和测试模型之后,我们将继续构建我们的模型。

我们将使用训练数据集调用build_model函数:

    model = build_model(x_train,y_train)

build_model中,我们将利用 scikit-learn 的 SGD 回归器类来构建我们的随机梯度下降方法:

    estimator = SGDRegressor(n_iter = 10, shuffle=True,loss = "squared_loss", \
            learning_rate='constant',eta0=0.01,fit_intercept=True, \
            penalty='none')
    estimator.fit(x,y)

SGD 回归器是一种广泛的方法,可以用于拟合具有大量参数的多种线性模型。我们将首先解释随机梯度下降的基本方法,然后再继续解释其他细节。

让我们看看我们使用的参数。第一个参数是我们希望通过数据集的次数,用于更新权重。这里,我们将设置为 10 次迭代。与感知机类似,在遍历所有记录一次之后,我们需要在开始下一次迭代时打乱输入记录。我们使用shuffle参数来实现这一点。shuffle的默认值为true,我们这里包含它是为了说明。我们的损失函数是平方损失,并且我们要进行线性回归,因此我们将通过loss参数指定这一点。

我们的学习率eta是一个常数,我们将通过learning_rate参数指定。我们将通过eta=0来为学习率提供一个值。然后我们会说我们需要拟合截距,因为我们没有按数据的均值来对数据进行中心化。最后,惩罚参数控制所需的收缩类型。在我们的案例中,我们不需要任何收缩,因此使用none字符串。

我们将通过调用拟合函数并传入我们的预测变量和响应变量来构建模型。最后,我们将把构建好的模型返回给调用函数。

现在让我们检查一下我们的模型,并查看截距和系数的值:

    inspect_model(model)

在检查模型时,我们将打印模型截距和系数的值:

工作原理…

现在让我们来看一下我们的模型在训练数据中的表现:

    print "Model worth on train data"
    model_worth(model,x_train,y_train)

我们将调用 model_worth 函数来查看模型的表现。model_worth 函数打印出平均绝对误差和均方误差的值。

均方误差定义如下:

它是如何工作的…

平均绝对误差定义如下:

它是如何工作的…

均方误差对异常值非常敏感。因此,平均绝对误差是一个更稳健的衡量标准。让我们通过训练数据来查看模型的表现:

它是如何工作的…

现在让我们来看一下使用开发数据的模型表现:

它是如何工作的…

还有更多…

我们可以在随机梯度下降框架中加入正则化。回顾前一章节中岭回归的成本函数:

还有更多…

在这里,我们加入了平方损失函数的扩展版本,并添加了正则化项——权重平方和。我们可以将其包含在我们的梯度下降过程中。假设我们将正则化项表示为 R(W)。我们的权重更新规则现在如下:

还有更多…

如你所见,现在我们有了损失函数关于权重向量 w 的导数,正则化项对权重的导数被添加到我们的权重更新规则中。

让我们写一个新的函数来构建包含正则化的模型:

def build_model_regularized(x,y):
    estimator = SGDRegressor(n_iter = 10,shuffle=True,loss = "squared_loss", \
            learning_rate='constant',eta0=0.01,fit_intercept=True, \
            penalty='l2',alpha=0.01)
    estimator.fit(x,y)

    return estimator

我们可以通过如下方式从主函数中调用这个函数:

model = build_model_regularized(x_train,y_train)
inspect_model(model)

让我们来看看与之前构建模型方法相比,我们传递的新参数:

    estimator = SGDRegressor(n_iter = 10,shuffle=True,loss = "squared_loss", \
            learning_rate='constant',eta0=0.01,fit_intercept=True, \
            penalty='l2',alpha=0.01)

之前,我们提到过惩罚项为 none。现在,你可以看到我们提到需要在模型中加入 L2 惩罚项。我们将给 alpha 参数一个值为 0.01。让我们来看看我们的系数:

还有更多…

你可以看到 L2 正则化的效果:很多系数已被压缩为零。类似地,L1 正则化和结合了 L1 和 L2 正则化的弹性网方法,也可以通过惩罚参数来实现。

记得在介绍中提到过,随机梯度下降更像是一个框架,而不是单一的方法。通过更改损失函数,可以使用这个框架生成其他线性模型。

可以使用不敏感于 epsilon 的损失函数构建支持向量机回归模型。这个损失函数定义如下:

还有更多…

请参考以下网址,了解可以传递给 scikit-learn 中 SGD 回归器的各种参数:

scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDRegressor.html

另见

  • 在第七章中,使用回归预测实数值 的方法,机器学习 II

  • 第七章中的岭回归的收缩示例,机器学习 II

使用随机梯度下降进行分类

分类问题的设置与回归问题非常相似,唯一的区别在于响应变量。在分类问题中,响应是一个类别变量。由于其性质,我们有不同的损失函数来衡量错误预测的代价。假设我们的讨论和配方是针对二分类器的,我们的目标变量 Y 可以取值 {0,1}。

我们将使用该损失函数的导数作为权重更新规则,以得到我们的权重向量。

scikit-learn 的 SGD 分类器类为我们提供了多种损失函数。然而,在本示例中,我们将看到对数损失,它将给出逻辑回归。

逻辑回归为以下形式的数据拟合一个线性模型:

使用随机梯度下降进行分类

我们已经给出了一个广义的符号表示。假设截距是我们权重向量的第一维。对于二分类问题,应用对数几率函数来得到预测,如下所示:

使用随机梯度下降进行分类

上述函数也被称为 sigmoid 函数。对于非常大的正值 x_i,该函数将返回接近 1 的值,反之,对于非常大的负值,将返回接近 0 的值。由此,我们可以将对数损失函数定义如下:

使用随机梯度下降进行分类

将上述损失函数应用于梯度下降的权重更新规则,我们可以得到适当的权重向量。

对于在 scikit-learn 中定义的对数损失函数,请参考以下网址:

scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html

了解这些知识后,让我们进入基于随机梯度下降的分类配方。

准备工作

我们将利用 scikit-learn 实现的随机梯度下降分类器。就像在之前的一些示例中一样,我们将使用 scikit-learn 的 make_classification 函数来生成数据,以演示随机梯度下降分类。

如何实现……

我们从一个非常简单的例子开始,演示如何构建一个随机梯度下降回归器。

我们将首先加载所需的库。然后,我们将编写一个函数来生成预测变量和响应变量:

from sklearn.datasets import make_classification
from sklearn.metrics import  accuracy_score
from sklearn.cross_validation import train_test_split
from sklearn.linear_model import SGDClassifier

import numpy as np

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    x,y = make_classification(n_samples=1000,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

我们将继续编写一些函数,帮助我们构建和验证我们的模型:

def build_model(x,y,x_dev,y_dev):
    estimator = SGDClassifier(n_iter=50,shuffle=True,loss="log", \
                learning_rate = "constant",eta0=0.0001,fit_intercept=True, penalty="none")
    estimator.fit(x,y)
    train_predcited = estimator.predict(x)
    train_score = accuracy_score(y,train_predcited)
    dev_predicted = estimator.predict(x_dev)
    dev_score = accuracy_score(y_dev,dev_predicted)

    print 
    print "Training Accuracy = %0.2f Dev Accuracy = %0.2f"%(train_score,dev_score)

最后,我们将编写主函数,调用所有先前的函数:

if __name__ == "__main__":
    x,y = get_data()    

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)
    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

    build_model(x_train,y_train,x_dev,y_dev)

它是如何工作的……

让我们从主函数开始。我们将调用get_data来获取我们的x预测属性和y响应属性。在get_data中,我们将利用make_classification数据集来生成用于随机森林方法的训练数据:

def get_data():
    """
    Make a sample classification dataset
    Returns : Independent variable y, dependent variable x
    """
    no_features = 30
    redundant_features = int(0.1*no_features)
    informative_features = int(0.6*no_features)
    repeated_features = int(0.1*no_features)
    x,y = make_classification(n_samples=500,n_features=no_features,flip_y=0.03,\
            n_informative = informative_features, n_redundant = redundant_features \
            ,n_repeated = repeated_features,random_state=7)
    return x,y

让我们来看一下传递给make_classification方法的参数。第一个参数是所需的实例数量。在这种情况下,我们需要 500 个实例。第二个参数是每个实例所需的属性数量。我们设置为 30。第三个参数flip_y,随机交换 3%的实例。这是为了在数据中引入噪声。接下来的参数是关于这 30 个特征中有多少个应该足够信息量,可以用于分类。我们指定 60%的特征,也就是 30 个中的 18 个,应该是有信息量的。接下来的参数是冗余特征。这些特征是通过信息特征的线性组合生成的,用来在特征之间引入相关性。最后,重复特征是从信息特征和冗余特征中随机抽取的重复特征。

让我们使用train_test_split将数据分为训练集和测试集。我们将预留 30%的数据用于测试:

    # Divide the data into Train, dev and test    
    x_train,x_test_all,y_train,y_test_all = train_test_split(x,y,test_size = 0.3,random_state=9)

再一次,我们将利用train_test_split将测试数据分成开发集和测试集:

    x_dev,x_test,y_dev,y_test = train_test_split(x_test_all,y_test_all,test_size=0.3,random_state=9)

我们将数据分为用于构建、评估和测试模型的三部分,然后开始构建我们的模型:

build_model(x_train,y_train,x_dev,y_dev)

build_model中,我们将利用 scikit-learn 的SGDClassifier类来构建我们的随机梯度下降方法:

    estimator = SGDClassifier(n_iter=50,shuffle=True,loss="log", \
                learning_rate = "constant",eta0=0.0001,fit_intercept=True, penalty="none")

让我们来看一下我们使用的参数。第一个参数是我们希望遍历数据集的次数,以更新权重。在这里,我们设定为 50 次迭代。与感知机一样,在遍历完所有记录一次后,我们需要对输入记录进行洗牌,开始下一轮迭代。shuffle参数用于控制这个操作。shuffle的默认值是 true,这里我们加上它是为了说明。我们的损失函数是对数损失:我们希望进行逻辑回归,并且通过loss参数来指定这一点。我们的学习率 eta 是一个常数,我们将通过learning_rate参数来指定。我们将通过eta0参数来提供学习率的值。接着,我们将设置需要拟合截距,因为我们没有通过均值对数据进行中心化。最后,penalty参数控制所需的收缩类型。在我们的案例中,我们将设置不需要任何收缩,使用none字符串。

我们将通过调用fit函数来构建我们的模型,并用训练集和开发集来评估我们的模型:

 estimator.fit(x,y)
    train_predcited = estimator.predict(x)
    train_score = accuracy_score(y,train_predcited)
    dev_predicted = estimator.predict(x_dev)
    dev_score = accuracy_score(y_dev,dev_predicted)

    print 
    print "Training Accuracy = %0.2f Dev Accuracy = %0.2f"%(train_score,dev_score)

让我们看看我们的准确度得分:

它是如何工作的…

还有更多内容……

对于 SGD 分类,可以应用正则化(L1、L2 或弹性网)。这个过程与回归相同,因此我们在这里不再重复。请参考前面的步骤。

在我们的示例中,学习率(eta)是常数,但这不一定是必要的。在每次迭代时,eta 值可以被减小。学习率参数learning_rate可以设置为一个最优的字符串或 invscaling。请参考以下 scikit 文档:

scikit-learn.org/stable/modules/sgd.html

该参数的设置如下:

estimator = SGDClassifier(n_iter=50,shuffle=True,loss="log", \
learning_rate = "invscaling",eta0=0.001,fit_intercept=True, penalty="none")

我们使用fit方法来构建我们的模型。如前所述,在大规模机器学习中,我们知道所有数据不会一次性提供给我们。当我们按批次接收数据时,需要使用partial_fit方法,而不是fit。使用fit方法会重新初始化权重,并且我们将失去上一批数据的所有训练信息。有关partial_fit的更多信息,请参考以下链接:

scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html#sklearn.linear_model.SGDClassifier.partial_fit

另请参见

  • 在第七章中,使用岭回归进行收缩的食谱,机器学习 II

  • 在第九章中,使用随机梯度下降进行回归的食谱,机器学习 III

posted @ 2025-07-20 11:31  绝不原创的飞龙  阅读(13)  评论(0)    收藏  举报