Python 机器学习秘籍第二版(全)
原文:
annas-archive.org/md5/343c5e6c97737f77853e89eacb95df75
译者:飞龙
前言
当本书的第一版于 2018 年出版时,填补了机器学习(ML)内容日益丰富的关键空白。通过提供经过充分测试的、实用的 Python 示例,使从业者能够轻松地复制和粘贴代码,然后根据自己的用例进行调整。在短短五年时间里,机器学习领域继续随着深度学习(DL)和相关的 DL Python 框架的进展而蓬勃发展。
现在,到了 2023 年,有必要提供与最新 Python 库相适应的机器学习(ML)和深度学习(DL)从业者需要的实用内容,这本书旨在基于第一版作者已完成的(而且出色的)工作:
-
更新现有示例以使用最新的 Python 版本和框架
-
结合数据源、数据分析、ML 和 DL 中的现代实践
-
扩展 DL 内容,包括 PyTorch 中的张量、神经网络以及文本和视觉的 DL
-
通过在 API 中为我们的模型提供服务,将我们的模型提升一步
与第一版相似,本书采用基于任务的方法来进行机器学习,提供超过 200 个独立的解决方案(复制、粘贴和运行),用于数据科学家或机器学习工程师在建模过程中遇到的最常见任务。
本书使用的约定
本书使用以下排版约定:
斜体
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
恒定宽度
用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
恒定宽度加粗
显示用户应该按字面意义键入的命令或其他文本。
*恒定宽度斜体*
显示应由用户提供值或根据上下文确定值的文本。
使用代码示例
本书配有一个GitHub 存储库,其中提供了在 Docker 容器中运行 Jupyter Notebook 的说明及本书中使用的所有依赖项。通过在笔记本中复制本书中的命令,可以确保本书中的示例完全可再现。
如果您有技术问题或使用代码示例时遇到问题,请发送电子邮件至[email protected]。
本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了大量代码,否则无需联系我们请求授权。例如,编写使用本书多个代码片段的程序不需要授权。销售或分发 O’Reilly 书籍的示例需要授权。通过引用本书回答问题并引用示例代码不需要授权。将本书的大量示例代码整合到您产品的文档中需要授权。
我们感谢您的使用,但通常不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Python 机器学习菜谱, 第 2 版,Kyle Gallatin 和 Chris Albon (O’Reilly)。版权所有 2023 Kyle Gallatin, 978-1-098-13572-0。”
如果您觉得您使用的代码示例超出了合理使用范围或以上授权,请随时通过[email protected]与我们联系。
O’Reilly Online Learning
注意
O’Reilly Media已经提供技术和商业培训、知识和见解超过 40 年,帮助公司取得成功。
我们独特的专家和创新者网络通过书籍、文章以及我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的现场培训课程、深入的学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多个出版商的广泛文本和视频资源。欲了解更多信息,请访问https://oreilly.com。
如何联系我们
请将关于本书的评论和问题发送给出版商:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
Sebastopol, CA 95472
-
800-889-8969(美国或加拿大)
-
707-829-7019(国际或本地)
-
707-829-0104(传真)
我们为本书设有网页,列出勘误、示例以及任何额外信息。您可以访问https://oreil.ly/ml_python_2e。
关于我们的书籍和课程的新闻和信息,请访问https://oreilly.com。
在 LinkedIn 找到我们:https://linkedin.com/company/oreilly-media
在 Twitter 关注我们:https://twitter.com/oreillymedia
在 YouTube 观看我们:https://youtube.com/oreillymedia
致谢
本书的第二版之所以能够顺利出版,完全得益于第一版的出色内容、结构和质量,这些都是原作者克里斯·阿尔本所铺陈的。作为第二版的第一作者,我无法言尽这使得我的工作变得轻松了许多的程度。
当然,机器学习领域也在迅速发展,本书第二版的更新内容离不开同行们深思熟虑的反馈。特别感谢我的 Etsy 同事安德烈亚·海曼、玛丽亚·戈麦斯、亚力克·迈尔斯特鲁姆和布莱恩·施密特,他们不厌其烦地响应对各章节的意见征集,并被逼迫进入突如其来的头脑风暴会议,共同塑造了本版新增的内容。还要感谢技术审阅者吉吉亚莎·格罗弗、马特乌斯·坦哈和甘尼什·哈尔克,以及 O'Reilly 的编辑们:杰夫·布莱尔、妮可·巴特菲尔德和克莱尔·莱洛克。话虽如此,帮助我和这本书取得现在成就的人数(或多或少)是庞大的。我想感谢所有在我机器学习之旅中的人,以及在某种方式上帮助这本书成就今天的每一个人。爱你们。
第一章:在 NumPy 中处理向量、矩阵和数组
1.0 介绍
NumPy 是 Python 机器学习堆栈的基础工具。NumPy 允许在机器学习中经常使用的数据结构(向量、矩阵和张量)上进行高效操作。虽然本书的重点不是 NumPy,但在接下来的章节中会经常出现。本章涵盖了我们在机器学习工作流中可能遇到的最常见的 NumPy 操作。
1.1 创建向量
问题
您需要创建一个向量。
解决方案
使用 NumPy 创建一维数组:
# Load library
import numpy as np
# Create a vector as a row
vector_row = np.array([1, 2, 3])
# Create a vector as a column
vector_column = np.array([[1],
[2],
[3]])
讨论
NumPy 的主要数据结构是多维数组。向量只是一个单维数组。要创建向量,我们只需创建一个一维数组。就像向量一样,这些数组可以水平表示(即行)或垂直表示(即列)。
参见
1.2 创建矩阵
问题
您需要创建一个矩阵。
解决方案
使用 NumPy 创建二维数组:
# Load library
import numpy as np
# Create a matrix
matrix = np.array([[1, 2],
[1, 2],
[1, 2]])
讨论
要创建矩阵,我们可以使用 NumPy 的二维数组。在我们的解决方案中,矩阵包含三行和两列(一列为 1,一列为 2)。
实际上,NumPy 有一个专用的矩阵数据结构:
matrix_object = np.mat([[1, 2],
[1, 2],
[1, 2]])
matrix([[1, 2],
[1, 2],
[1, 2]])
然而,矩阵数据结构由于两个原因而不推荐使用。首先,数组是 NumPy 的事实标准数据结构。其次,绝大多数 NumPy 操作返回数组,而不是矩阵对象。
参见
1.3 创建稀疏矩阵
问题
鉴于数据中只有很少的非零值,您希望以高效的方式表示它。
解决方案
创建稀疏矩阵:
# Load libraries
import numpy as np
from scipy import sparse
# Create a matrix
matrix = np.array([[0, 0],
[0, 1],
[3, 0]])
# Create compressed sparse row (CSR) matrix
matrix_sparse = sparse.csr_matrix(matrix)
讨论
在机器学习中经常遇到的情况是有大量数据;然而,数据中大多数元素都是零。例如,想象一下一个矩阵,其中列是 Netflix 上的每部电影,行是每个 Netflix 用户,值是用户观看该特定电影的次数。这个矩阵将有成千上万的列和数百万的行!然而,由于大多数用户不会观看大多数电影,大多数元素将为零。
稀疏矩阵 是一个大部分元素为 0 的矩阵。稀疏矩阵仅存储非零元素,并假设所有其他值都为零,从而显著节省计算资源。在我们的解决方案中,我们创建了一个具有两个非零值的 NumPy 数组,然后将其转换为稀疏矩阵。如果查看稀疏矩阵,可以看到只存储了非零值:
# View sparse matrix
print(matrix_sparse)
(1, 1) 1
(2, 0) 3
有许多类型的稀疏矩阵。然而,在压缩稀疏行(CSR)矩阵中,(1, 1)
和 (2, 0)
表示非零值 1
和 3
的(从零开始计数的)索引。例如,元素 1
在第二行第二列。如果我们创建一个具有更多零元素的更大矩阵,然后将其与我们的原始稀疏矩阵进行比较,我们可以看到稀疏矩阵的优势:
# Create larger matrix
matrix_large = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
# Create compressed sparse row (CSR) matrix
matrix_large_sparse = sparse.csr_matrix(matrix_large)
# View original sparse matrix
print(matrix_sparse)
(1, 1) 1
(2, 0) 3
# View larger sparse matrix
print(matrix_large_sparse)
(1, 1) 1
(2, 0) 3
正如我们所见,尽管在更大的矩阵中添加了更多的零元素,但其稀疏表示与我们原始的稀疏矩阵完全相同。也就是说,添加零元素并没有改变稀疏矩阵的大小。
正如前面提到的,稀疏矩阵有许多不同的类型,如压缩稀疏列、列表列表和键字典。虽然解释这些不同类型及其影响超出了本书的范围,但值得注意的是,虽然没有“最佳”稀疏矩阵类型,但它们之间存在显著差异,我们应该意识到为什么选择一种类型而不是另一种类型。
另请参阅
1.4 预分配 NumPy 数组
问题
您需要预先分配给定大小的数组,并使用某些值。
解决方案
NumPy 具有使用 0、1 或您选择的值生成任意大小向量和矩阵的函数:
# Load library
import numpy as np
# Generate a vector of shape (1,5) containing all zeros
vector = np.zeros(shape=5)
# View the matrix
print(vector)
array([0., 0., 0., 0., 0.])
# Generate a matrix of shape (3,3) containing all ones
matrix = np.full(shape=(3,3), fill_value=1)
# View the vector
print(matrix)
array([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
讨论
使用预填充数据生成数组对于许多目的非常有用,例如使代码更具性能或使用合成数据来测试算法。在许多编程语言中,预先分配一个带有默认值(例如 0)的数组被认为是常见做法。
1.5 选择元素
问题
您需要在向量或矩阵中选择一个或多个元素。
解决方案
NumPy 数组使得选择向量或矩阵中的元素变得很容易:
# Load library
import numpy as np
# Create row vector
vector = np.array([1, 2, 3, 4, 5, 6])
# Create matrix
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Select third element of vector
vector[2]
3
# Select second row, second column
matrix[1,1]
5
讨论
像大多数 Python 中的事物一样,NumPy 数组是从零开始索引的,这意味着第一个元素的索引是 0,而不是 1。除此之外,NumPy 提供了大量方法来选择(即索引和切片)数组中的元素或元素组:
# Select all elements of a vector
vector[:]
array([1, 2, 3, 4, 5, 6])
# Select everything up to and including the third element
vector[:3]
array([1, 2, 3])
# Select everything after the third element
vector[3:]
array([4, 5, 6])
# Select the last element
vector[-1]
6
# Reverse the vector
vector[::-1]
array([6, 5, 4, 3, 2, 1])
# Select the first two rows and all columns of a matrix
matrix[:2,:]
array([[1, 2, 3],
[4, 5, 6]])
# Select all rows and the second column
matrix[:,1:2]
array([[2],
[5],
[8]])
1.6 描述矩阵
问题
您想要描述矩阵的形状、大小和维度。
解决方案
使用 NumPy 对象的 shape
、size
和 ndim
属性:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# View number of rows and columns
matrix.shape
(3, 4)
# View number of elements (rows * columns)
matrix.size
12
# View number of dimensions
matrix.ndim
2
讨论
这可能看起来很基础(而且确实如此);然而,一次又一次地,检查数组的形状和大小都是非常有价值的,无论是为了进一步的计算还是仅仅作为操作后的直觉检查。
1.7 对每个元素应用函数
问题
您想将某些函数应用于数组中的所有元素。
解决方案
使用 NumPy 的 vectorize
方法:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Create function that adds 100 to something
add_100 = lambda i: i + 100
# Create vectorized function
vectorized_add_100 = np.vectorize(add_100)
# Apply function to all elements in matrix
vectorized_add_100(matrix)
array([[101, 102, 103],
[104, 105, 106],
[107, 108, 109]])
讨论
NumPy 的vectorize
方法将一个函数转换为可以应用于数组或数组切片的所有元素的函数。值得注意的是,vectorize
本质上是对元素的for
循环,不会提高性能。此外,NumPy 数组允许我们在数组之间执行操作,即使它们的维度不同(这称为广播)。例如,我们可以使用广播创建一个更简单的版本:
# Add 100 to all elements
matrix + 100
array([[101, 102, 103],
[104, 105, 106],
[107, 108, 109]])
广播不适用于所有形状和情况,但它是在 NumPy 数组的所有元素上应用简单操作的常见方法。
1.8 查找最大值和最小值
问题
您需要在数组中找到最大或最小值。
解决方案
使用 NumPy 的max
和min
方法:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Return maximum element
np.max(matrix)
9
# Return minimum element
np.min(matrix)
1
讨论
我们经常想知道数组或数组子集中的最大值和最小值。这可以通过max
和min
方法来实现。使用axis
参数,我们还可以沿着特定轴应用操作:
# Find maximum element in each column
np.max(matrix, axis=0)
array([7, 8, 9])
# Find maximum element in each row
np.max(matrix, axis=1)
array([3, 6, 9])
1.9 计算平均值、方差和标准差
问题
您希望计算数组的一些描述性统计信息。
解决方案
使用 NumPy 的mean
、var
和std
:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Return mean
np.mean(matrix)
5.0
# Return variance
np.var(matrix)
6.666666666666667
# Return standard deviation
np.std(matrix)
2.5819888974716112
讨论
就像使用max
和min
一样,我们可以轻松地获得关于整个矩阵的描述性统计信息,或者沿着单个轴进行计算:
# Find the mean value in each column
np.mean(matrix, axis=0)
array([ 4., 5., 6.])
1.10 重新塑形数组
问题
您希望更改数组的形状(行数和列数),而不更改元素值。
解决方案
使用 NumPy 的reshape
:
# Load library
import numpy as np
# Create 4x3 matrix
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12]])
# Reshape matrix into 2x6 matrix
matrix.reshape(2, 6)
array([[ 1, 2, 3, 4, 5, 6],
[ 7, 8, 9, 10, 11, 12]])
讨论
reshape
允许我们重构一个数组,以便我们保持相同的数据但将其组织为不同数量的行和列。唯一的要求是原始矩阵和新矩阵的形状包含相同数量的元素(即,大小相同)。我们可以使用size
来查看矩阵的大小:
matrix.size
12
reshape
中一个有用的参数是-1
,它实际上意味着“需要多少就多少”,因此reshape(1, -1)
意味着一行和所需的列数:
matrix.reshape(1, -1)
array([[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]])
最后,如果我们提供一个整数,reshape
将返回一个长度为该整数的一维数组:
matrix.reshape(12)
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
1.11 转置向量或矩阵
问题
您需要转置向量或矩阵。
解决方案
使用T
方法:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Transpose matrix
matrix.T
array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
讨论
转置是线性代数中的常见操作,其中每个元素的列和行索引被交换。在线性代数课程之外通常被忽略的一个微妙的点是,从技术上讲,向量不能被转置,因为它只是一组值:
# Transpose vector
np.array([1, 2, 3, 4, 5, 6]).T
array([1, 2, 3, 4, 5, 6])
然而,通常将向量的转置称为将行向量转换为列向量(请注意第二对括号)或反之亦然:
# Transpose row vector
np.array([[1, 2, 3, 4, 5, 6]]).T
array([[1],
[2],
[3],
[4],
[5],
[6]])
1.12 扁平化矩阵
问题
您需要将矩阵转换为一维数组。
解决方案
使用flatten
方法:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Flatten matrix
matrix.flatten()
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
讨论
flatten
是一个简单的方法,将矩阵转换为一维数组。或者,我们可以使用reshape
创建一个行向量:
matrix.reshape(1, -1)
array([[1, 2, 3, 4, 5, 6, 7, 8, 9]])
另一种常见的数组展平方法是ravel
方法。与返回原始数组副本的flatten
不同,ravel
直接操作原始对象,因此速度稍快。此外,ravel
还允许我们展平数组列表,而flatten
方法则无法做到。这种操作对于展平非常大的数组和加速代码非常有用:
# Create one matrix
matrix_a = np.array([[1, 2],
[3, 4]])
# Create a second matrix
matrix_b = np.array([[5, 6],
[7, 8]])
# Create a list of matrices
matrix_list = [matrix_a, matrix_b]
# Flatten the entire list of matrices
np.ravel(matrix_list)
array([1, 2, 3, 4, 5, 6, 7, 8])
1.13 矩阵的秩
问题
你需要知道一个矩阵的秩。
解决方案
使用 NumPy 的线性代数方法matrix_rank
:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 1, 1],
[1, 1, 10],
[1, 1, 15]])
# Return matrix rank
np.linalg.matrix_rank(matrix)
2
讨论
矩阵的秩是由其列或行张成的向量空间的维数。在 NumPy 中由于matrix_rank
函数,计算矩阵的秩非常容易。
参见
1.14 获取矩阵的对角线
问题
你需要获取一个矩阵的对角线元素。
解决方案
使用 NumPy 的diagonal
:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3],
[2, 4, 6],
[3, 8, 9]])
# Return diagonal elements
matrix.diagonal()
array([1, 4, 9])
讨论
使用 NumPy 轻松获取矩阵的对角线元素,使用diagonal
函数。还可以通过使用offset
参数获取主对角线之外的对角线:
# Return diagonal one above the main diagonal
matrix.diagonal(offset=1)
array([2, 6])
# Return diagonal one below the main diagonal
matrix.diagonal(offset=-1)
array([2, 8])
1.15 计算一个矩阵的迹
问题
你需要计算一个矩阵的迹。
解决方案
使用trace
:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 2, 3],
[2, 4, 6],
[3, 8, 9]])
# Return trace
matrix.trace()
14
讨论
一个矩阵的迹是其对角线元素的和,通常在机器学习方法中被广泛使用。对于给定的 NumPy 多维数组,我们可以使用trace
函数来计算迹。或者,我们可以返回一个矩阵的对角线并计算其总和:
# Return diagonal and sum elements
sum(matrix.diagonal())
14
参见
1.16 计算点积
问题
你需要计算两个向量的点积。
解决方案
使用 NumPy 的dot
函数:
# Load library
import numpy as np
# Create two vectors
vector_a = np.array([1,2,3])
vector_b = np.array([4,5,6])
# Calculate dot product
np.dot(vector_a, vector_b)
32
讨论
两个向量和的点积定义如下:
其中是向量的第个元素,是向量的第个元素。我们可以使用 NumPy 的dot
函数来计算点积。或者,在 Python 3.5+ 中,我们可以使用新的@
运算符:
# Calculate dot product
vector_a @ vector_b
32
参见
1.17 矩阵的加法和减法
问题
你想要对两个矩阵进行加法或减法。
解决方案
使用 NumPy 的add
和subtract
:
# Load library
import numpy as np
# Create matrix
matrix_a = np.array([[1, 1, 1],
[1, 1, 1],
[1, 1, 2]])
# Create matrix
matrix_b = np.array([[1, 3, 1],
[1, 3, 1],
[1, 3, 8]])
# Add two matrices
np.add(matrix_a, matrix_b)
array([[ 2, 4, 2],
[ 2, 4, 2],
[ 2, 4, 10]])
# Subtract two matrices
np.subtract(matrix_a, matrix_b)
array([[ 0, -2, 0],
[ 0, -2, 0],
[ 0, -2, -6]])
讨论
或者,我们可以简单地使用+
和–
运算符:
# Add two matrices
matrix_a + matrix_b
array([[ 2, 4, 2],
[ 2, 4, 2],
[ 2, 4, 10]])
1.18 矩阵的乘法
问题
你想要对两个矩阵进行乘法运算。
解决方案
使用 NumPy 的dot
:
# Load library
import numpy as np
# Create matrix
matrix_a = np.array([[1, 1],
[1, 2]])
# Create matrix
matrix_b = np.array([[1, 3],
[1, 2]])
# Multiply two matrices
np.dot(matrix_a, matrix_b)
array([[2, 5],
[3, 7]])
讨论
或者,在 Python 3.5+ 中我们可以使用@
运算符:
# Multiply two matrices
matrix_a @ matrix_b
array([[2, 5],
[3, 7]])
如果我们想要进行逐元素乘法,可以使用*
运算符:
# Multiply two matrices element-wise
matrix_a * matrix_b
array([[1, 3],
[1, 4]])
参见
1.19 矩阵求逆
问题
您想要计算一个方阵的逆。
解决方案
使用 NumPy 的线性代数 inv
方法:
# Load library
import numpy as np
# Create matrix
matrix = np.array([[1, 4],
[2, 5]])
# Calculate inverse of matrix
np.linalg.inv(matrix)
array([[-1.66666667, 1.33333333],
[ 0.66666667, -0.33333333]])
讨论
方阵 A 的逆,A^(–1),是第二个矩阵,满足以下条件:
其中 I 是单位矩阵。在 NumPy 中,如果存在的话,我们可以使用 linalg.inv
计算 A^(–1)。为了看到这一点,我们可以将一个矩阵乘以它的逆,结果是单位矩阵:
# Multiply matrix and its inverse
matrix @ np.linalg.inv(matrix)
array([[ 1., 0.],
[ 0., 1.]])
参见
1.20 生成随机值
问题
您想要生成伪随机值。
解决方案
使用 NumPy 的 random
:
# Load library
import numpy as np
# Set seed
np.random.seed(0)
# Generate three random floats between 0.0 and 1.0
np.random.random(3)
array([ 0.5488135 , 0.71518937, 0.60276338])
讨论
NumPy 提供了生成随机数的多种方法,远远超出了此处所能涵盖的范围。在我们的解决方案中,我们生成了浮点数;然而,生成整数也很常见:
# Generate three random integers between 0 and 10
np.random.randint(0, 11, 3)
array([3, 7, 9])
或者,我们可以通过从分布中抽取数字来生成数字(请注意,这在技术上不是随机的):
# Draw three numbers from a normal distribution with mean 0.0
# and standard deviation of 1.0
np.random.normal(0.0, 1.0, 3)
array([-1.42232584, 1.52006949, -0.29139398])
# Draw three numbers from a logistic distribution with mean 0.0 and scale of 1.0
np.random.logistic(0.0, 1.0, 3)
array([-0.98118713, -0.08939902, 1.46416405])
# Draw three numbers greater than or equal to 1.0 and less than 2.0
np.random.uniform(1.0, 2.0, 3)
array([ 1.47997717, 1.3927848 , 1.83607876])
最后,有时候返回相同的随机数多次以获取可预测的、可重复的结果是有用的。我们可以通过设置伪随机生成器的“种子”(一个整数)来实现这一点。具有相同种子的随机过程将始终产生相同的输出。我们将在本书中使用种子,以确保您在书中看到的代码和在您的计算机上运行的代码产生相同的结果。
第二章:载入数据
2.0 简介
任何机器学习工作的第一步是将原始数据导入到我们的系统中。原始数据可以是日志文件、数据集文件、数据库,或者像亚马逊 S3 这样的云存储。此外,通常我们会希望从多个来源检索数据。
本章中的示例将介绍从多种来源加载数据的方法,包括 CSV 文件和 SQL 数据库。我们还将介绍如何使用具有可配置属性的模拟数据生成方法进行实验。最后,虽然在 Python 生态系统中有许多加载数据的方式,但我们将重点介绍使用 pandas 库的广泛方法来加载外部数据,以及使用 scikit-learn——一个开源的 Python 机器学习库——来生成模拟数据。
2.1 载入一个示例数据集
问题
您希望加载 scikit-learn 库中预先存在的示例数据集。
解决方案
scikit-learn 自带许多流行的数据集供您使用:
# Load scikit-learn's datasets
from sklearn import datasets
# Load digits dataset
digits = datasets.load_digits()
# Create features matrix
features = digits.data
# Create target vector
target = digits.target
# View first observation
features[0]
array([ 0., 0., 5., 13., 9., 1., 0., 0., 0., 0., 13.,
15., 10., 15., 5., 0., 0., 3., 15., 2., 0., 11.,
8., 0., 0., 4., 12., 0., 0., 8., 8., 0., 0.,
5., 8., 0., 0., 9., 8., 0., 0., 4., 11., 0.,
1., 12., 7., 0., 0., 2., 14., 5., 10., 12., 0.,
0., 0., 0., 6., 13., 10., 0., 0., 0.])
讨论
我们通常不希望在能够探索一些机器学习算法或方法之前,就必须加载、转换和清理真实世界的数据集。幸运的是,scikit-learn 提供了一些常见的数据集,我们可以快速加载。这些数据集通常被称为“玩具”数据集,因为它们比真实世界中的数据集要小得多,也更干净。scikit-learn 中一些流行的示例数据集包括:
load_iris
包含 150 个鸢尾花测量数据的观察结果。这是一个很好的数据集,用于探索分类算法。
load_digits
包含 1,797 个手写数字图像的观察结果。这是一个很好的数据集,适合用于图像分类的教学。
要查看这些数据集的更多细节,请打印 DESCR
属性:
# Load scikit-learn's datasets
from sklearn import datasets
# Load digits dataset
digits = datasets.load_digits()
# Print the attribute
print(digits.DESCR)
.. _digits_dataset:
Optical recognition of handwritten digits dataset
--------------------------------------------------
**Data Set Characteristics:**
:Number of Instances: 1797
:Number of Attributes: 64
:Attribute Information: 8x8 image of integer pixels in the range 0..16.
:Missing Attribute Values: None
:Creator: E. Alpaydin (alpaydin '@' boun.edu.tr)
:Date: July; 1998
...
参见
2.2 创建一个模拟数据集
问题
您需要生成一个模拟数据集。
解决方案
scikit-learn 提供了许多用于创建模拟数据的方法。其中,三种方法特别有用:make_regression
、make_classification
和 make_blobs
。
当我们需要一个设计用于线性回归的数据集时,make_regression
是一个不错的选择:
# Load library
from sklearn.datasets import make_regression
# Generate features matrix, target vector, and the true coefficients
features, target, coefficients = make_regression(n_samples = 100,
n_features = 3,
n_informative = 3,
n_targets = 1,
noise = 0.0,
coef = True,
random_state = 1)
# View feature matrix and target vector
print('Feature Matrix\n', features[:3])
print('Target Vector\n', target[:3])
Feature Matrix
[[ 1.29322588 -0.61736206 -0.11044703]
[-2.793085 0.36633201 1.93752881]
[ 0.80186103 -0.18656977 0.0465673 ]]
Target Vector
[-10.37865986 25.5124503 19.67705609]
如果我们有兴趣创建一个用于分类的模拟数据集,我们可以使用 make_classification
:
# Load library
from sklearn.datasets import make_classification
# Generate features matrix and target vector
features, target = make_classification(n_samples = 100,
n_features = 3,
n_informative = 3,
n_redundant = 0,
n_classes = 2,
weights = [.25, .75],
random_state = 1)
# View feature matrix and target vector
print('Feature Matrix\n', features[:3])
print('Target Vector\n', target[:3])
Feature Matrix
[[ 1.06354768 -1.42632219 1.02163151]
[ 0.23156977 1.49535261 0.33251578]
[ 0.15972951 0.83533515 -0.40869554]]
Target Vector
[1 0 0]
最后,如果我们需要一个设计用于聚类技术的数据集,scikit-learn 提供了 make_blobs
:
# Load library
from sklearn.datasets import make_blobs
# Generate features matrix and target vector
features, target = make_blobs(n_samples = 100,
n_features = 2,
centers = 3,
cluster_std = 0.5,
shuffle = True,
random_state = 1)
# View feature matrix and target vector
print('Feature Matrix\n', features[:3])
print('Target Vector\n', target[:3])
Feature Matrix
[[ -1.22685609 3.25572052]
[ -9.57463218 -4.38310652]
[-10.71976941 -4.20558148]]
Target Vector
[0 1 1]
讨论
从解决方案中可以看出,make_regression
返回一个浮点值的特征矩阵和一个浮点值的目标向量,而 make_classification
和 make_blobs
返回一个浮点值的特征矩阵和一个整数的目标向量,代表类的成员身份。
scikit-learn 的模拟数据集提供了广泛的选项来控制生成数据的类型。scikit-learn 的文档包含了所有参数的详细描述,但有几个值得注意。
在 make_regression
和 make_classification
中,n_informative
确定用于生成目标向量的特征数量。如果 n_informative
小于总特征数 (n_features
),则生成的数据集将具有冗余特征,可通过特征选择技术识别。
此外,make_classification
包含 weights
参数,允许我们模拟不平衡类别的数据集。例如,weights = [.25, .75]
将返回一个数据集,其中 25%的观测属于一类,75%的观测属于第二类。
对于 make_blobs
,centers
参数确定生成的簇数量。使用 matplotlib
可视化库,我们可以可视化 make_blobs
生成的簇:
# Load library
import matplotlib.pyplot as plt
# View scatterplot
plt.scatter(features[:,0], features[:,1], c=target)
plt.show()
参见
2.3 加载 CSV 文件
问题
您需要导入逗号分隔值(CSV)文件。
解决方案
使用 pandas 库的 read_csv
将本地或托管的 CSV 文件加载到 pandas DataFrame 中:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/data.csv'
# Load dataset
dataframe = pd.read_csv(url)
# View first two rows
dataframe.head(2)
integer | datetime | category | |
---|---|---|---|
0 | 5 | 2015-01-01 00:00:00 | 0 |
1 | 5 | 2015-01-01 00:00:01 | 0 |
讨论
关于加载 CSV 文件有两件事情需要注意。首先,在加载之前快速查看文件内容通常很有用。事先了解数据集的结构以及我们需要设置的参数是非常有帮助的。其次,read_csv
有超过 30 个参数,因此文档可能令人望而却步。幸运的是,这些参数大多是为了处理各种 CSV 格式而设定的。
CSV 文件的名称源于数值之间确实以逗号分隔(例如,一行可能是 2,"2015-01-01 00:00:00",0
);然而,常见的 CSV 文件使用其他分隔符,如制表符(称为 TSV 文件)。pandas 的sep
参数允许我们定义文件中使用的分隔符。尽管并非总是如此,CSV 文件常见的格式问题是文件的第一行用于定义列标题(例如,在我们的解决方案中是 integer, datetime, category
)。header
参数允许我们指定是否存在标题行以及其位置。如果不存在标题行,我们设置header=None
。
read_csv
函数返回一个 pandas DataFrame:这是处理表格数据常见且有用的对象,在本书中我们将更深入地讨论它。
2.4 加载 Excel 文件
问题
您需要导入 Excel 电子表格。
解决方案
使用 pandas 库的 read_excel
加载 Excel 电子表格:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/data.xlsx'
# Load data
dataframe = pd.read_excel(url, sheet_name=0, header=0)
# View the first two rows
dataframe.head(2)
integer | datetime | category | |
---|---|---|---|
5 | 2015-01-01 00:00:00 | 0 | |
0 | 5 | 2015-01-01 00:00:01 | 0 |
1 | 9 | 2015-01-01 00:00:02 | 0 |
讨论
此解决方案类似于我们用于读取 CSV 文件的解决方案。主要区别在于附加参数 sheet_name
,它指定了我们希望加载的 Excel 文件中的哪个工作表。sheet_name
可以接受包含工作表名称的字符串和指向工作表位置(从零开始计数)的整数。如果我们需要加载多个工作表,我们将它们包含在列表中。例如,sheet_name=[0,1,2, "Monthly Sales"]
将返回一个包含第一个、第二个和第三个工作表以及名为 Monthly Sales
的工作表的 pandas DataFrame 字典。
2.5 加载 JSON 文件
问题
您需要加载一个 JSON 文件进行数据预处理。
解决方案
pandas 库提供了 read_json
来将 JSON 文件转换为 pandas 对象:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/data.json'
# Load data
dataframe = pd.read_json(url, orient='columns')
# View the first two rows
dataframe.head(2)
类别 | 时间 | 整数 | |
---|---|---|---|
0 | 0 | 2015-01-01 00:00:00 | 5 |
1 | 0 | 2015-01-01 00:00:01 | 5 |
讨论
将 JSON 文件导入 pandas 类似于我们之前看到的几个示例。主要区别在于 orient
参数,它指示 pandas JSON 文件的结构。但是,可能需要一些试验才能弄清楚哪个参数(split
、records
、index
、columns
或 values
)是正确的。另一个 pandas 提供的有用工具是 json_normalize
,它可以帮助将半结构化的 JSON 数据转换为 pandas DataFrame。
参见
2.6 加载 Parquet 文件
问题
您需要加载一个 Parquet 文件。
解决方案
pandas 的 read_parquet
函数允许我们读取 Parquet 文件:
# Load library
import pandas as pd
# Create URL
url = 'https://machine-learning-python-cookbook.s3.amazonaws.com/data.parquet'
# Load data
dataframe = pd.read_parquet(url)
# View the first two rows
dataframe.head(2)
类别 | 时间 | 整数 | |
---|---|---|---|
0 | 0 | 2015-01-01 00:00:00 | 5 |
1 | 0 | 2015-01-01 00:00:01 | 5 |
讨论
Parquet 是大数据领域中流行的数据存储格式。它通常与 Hadoop 和 Spark 等大数据工具一起使用。虽然 PySpark 超出了本书的重点,但大规模运营的公司很可能会使用高效的数据存储格式,比如 Parquet,了解如何将其读入数据框架并对其进行操作是很有价值的。
参见
2.7 加载 Avro 文件
问题
您需要将 Avro 文件加载到 pandas DataFrame 中。
解决方案
使用 pandavro
库的 read_avro
方法:
# Load library
import requests
import pandavro as pdx
# Create URL
url = 'https://machine-learning-python-cookbook.s3.amazonaws.com/data.avro'
# Download file
r = requests.get(url)
open('data.avro', 'wb').write(r.content)
# Load data
dataframe = pdx.read_avro('data.avro')
# View the first two rows
dataframe.head(2)
类别 | 时间 | 整数 | |
---|---|---|---|
0 | 0 | 2015-01-01 00:00:00 | 5 |
1 | 0 | 2015-01-01 00:00:01 | 5 |
讨论
Apache Avro 是一种开源的二进制数据格式,依赖于数据结构。在撰写本文时,它还不像 Parquet 那样普遍。但是,由于其高效的特性,大型二进制数据格式(如 Avro、thrift 和 Protocol Buffers)正变得越来越流行。如果您使用大型数据系统,很可能在不久的将来会遇到其中一种格式。
参见
2.8 查询 SQLite 数据库
问题
您需要使用结构化查询语言(SQL)从数据库加载数据。
解决方案
pandas 的read_sql_query
允许我们向数据库发出 SQL 查询并加载数据:
# Load libraries
import pandas as pd
from sqlalchemy import create_engine
# Create a connection to the database
database_connection = create_engine('sqlite:///sample.db')
# Load data
dataframe = pd.read_sql_query('SELECT * FROM data', database_connection)
# View first two rows
dataframe.head(2)
名字 | 姓氏 | 年龄 | 预测试分数 | 后测试分数 | |
---|---|---|---|---|---|
0 | Jason | Miller | 42 | 4 | 25 |
1 | Molly | Jacobson | 52 | 24 | 94 |
讨论
SQL 是从数据库提取数据的通用语言。在这个配方中,我们首先使用create_engine
定义了一个连接到名为 SQLite 的 SQL 数据库引擎。接下来,我们使用 pandas 的read_sql_query
使用 SQL 查询该数据库,并将结果放入 DataFrame 中。
SQL 是一门独立的语言,虽然超出本书的范围,但对于希望学习机器学习的任何人来说,了解它肯定是值得的。我们的 SQL 查询SELECT * FROM data
要求数据库给我们表名为data
的所有列(*
)。
请注意,这是本书中几个配方之一,如果没有额外的代码将无法运行。具体来说,create_engine('sqlite:///sample.db')
假定 SQLite 数据库已经存在。
参见
2.9 查询远程 SQL 数据库
问题
您需要连接并从远程 SQL 数据库中读取数据。
解决方案
使用pymysql
建立连接,并用 pandas 将其读入数据框:
# Import libraries
import pymysql
import pandas as pd
# Create a DB connection
# Use the following example to start a DB instance
# https://github.com/kylegallatin/mysql-db-example
conn = pymysql.connect(
host='localhost',
user='root',
password = "",
db='db',
)
# Read the SQL query into a dataframe
dataframe = pd.read_sql("select * from data", conn)
# View the first two rows
dataframe.head(2)
整数 | 日期时间 | 类别 | |
---|---|---|---|
0 | 5 | 2015-01-01 00:00:00 | 0 |
1 | 5 | 2015-01-01 00:00:01 | 0 |
讨论
在本章中呈现的所有配方中,这可能是我们在现实世界中最常使用的一个。虽然连接并从示例sqlite
数据库中读取数据很有用,但它可能不代表您将需要连接的企业环境中的表。您将连接的大多数 SQL 实例都将要求您连接到远程计算机的主机和端口,并指定用于身份验证的用户名和密码。此示例需要您在本地启动运行的 SQL 实例,以模仿远程服务器上的工作流程。
参见
2.10 从 Google 表格加载数据
问题
您需要直接从 Google 表格中读取数据。
解决方案
使用 pandas 的read_CSV
并传递一个将 Google 表格导出为 CSV 的 URL:
# Import libraries
import pandas as pd
# Google Sheet URL that downloads the sheet as a CSV
url = "https://docs.google.com/spreadsheets/d/"\
"1ehC-9otcAuitqnmWksqt1mOrTRCL38dv0K9UjhwzTOA/export?format=csv"
# Read the CSV into a dataframe
dataframe = pd.read_csv(url)
# View the first two rows
dataframe.head(2)
整数 | 日期时间 | 类别 | |
---|---|---|---|
0 | 5 | 2015-01-01 00:00:00 | 0 |
1 | 5 | 2015-01-01 00:00:01 | 0 |
讨论
虽然 Google 表格可以轻松下载,但直接在 Python 中读取它们而无需任何中间步骤有时会很有帮助。上述 URL 末尾的/export?format=csv
查询参数创建了一个端点,我们可以从中下载文件或将其读入 pandas。
参见
2.11 从 S3 存储桶加载数据
问题
您需要从您有访问权限的 S3 存储桶中读取 CSV 文件。
解决方案
向 pandas 添加存储选项,使其可以访问 S3 对象:
# Import libraries
import pandas as pd
# S3 path to CSV
s3_uri = "s3://machine-learning-python-cookbook/data.csv"
# Set AWS credentials (replace with your own)
ACCESS_KEY_ID = "*`xxxxxxxxxxxxx`*"
SECRET_ACCESS_KEY = "*`xxxxxxxxxxxxxxxx`*"
# Read the CSV into a dataframe
dataframe = pd.read_csv(s3_uri,storage_options={
"key": ACCESS_KEY_ID,
"secret": SECRET_ACCESS_KEY,
}
)
# View first two rows
dataframe.head(2)
整数 | 日期时间 | 类别 | |
---|---|---|---|
0 | 5 | 2015-01-01 00:00:00 | 0 |
1 | 5 | 2015-01-01 00:00:01 | 0 |
讨论
许多企业现在将数据保存在云提供商的 Blob 存储中,如 Amazon S3 或 Google Cloud Storage(GCS)。机器学习从业者通常连接到这些来源以检索数据。虽然 S3 URI(s3://machine-learning-python-cookbook/data.csv
)是公共的,但仍然需要您提供自己的 AWS 访问凭据才能访问它。值得注意的是,公共对象还有 HTTP URL,可以从中下载文件,比如这个 CSV 文件的链接。
参见
2.12 加载非结构化数据
问题
您需要加载文本或图像等非结构化数据。
解决方案
使用基本的 Python open
函数加载信息:
# Import libraries
import requests
# URL to download the txt file from
txt_url = "https://machine-learning-python-cookbook.s3.amazonaws.com/text.txt"
# Get the txt file
r = requests.get(txt_url)
# Write it to text.txt locally
with open('text.txt', 'wb') as f:
f.write(r.content)
# Read in the file
with open('text.txt', 'r') as f:
text = f.read()
# Print the content
print(text)
Hello there!
讨论
虽然结构化数据可以轻松从 CSV、JSON 或各种数据库中读取,但非结构化数据可能更具挑战性,可能需要稍后进行定制处理。有时使用 Python 的基本open
函数打开并读取文件会很有帮助。这样我们就可以打开文件然后读取文件的内容。
参见
第三章:数据整理
3.0 引言
数据整理是一个广义术语,通常非正式地用来描述将原始数据转换为干净、有组织的格式,以便于使用的过程。对于我们来说,数据整理只是数据预处理的一个步骤,但是这是一个重要的步骤。
“整理”数据最常用的数据结构是数据框,它既直观又非常灵活。数据框是表格型的,意味着它们基于行和列,就像您在电子表格中看到的那样。这是一个根据泰坦尼克号乘客数据创建的数据框示例:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data as a dataframe
dataframe = pd.read_csv(url)
# Show first five rows
dataframe.head(5)
名称 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | 1st | 29.00 | 女性 | 1 | 1 |
1 | 艾莉森,海伦·洛林小姐 | 1st | 2.00 | 女性 | 0 | 1 |
2 | 艾莉森,哈德森·约书亚·克莱顿先生 | 1st | 30.00 | 男性 | 0 | 0 |
3 | 艾莉森,哈德森 JC 夫人(贝西·沃尔多·丹尼尔斯) | 1st | 25.00 | 女性 | 0 | 1 |
4 | 艾莉森,哈德森·特雷弗小主人 | 1st | 0.92 | 男性 | 1 | 0 |
在这个数据框中,有三个重要的事情需要注意。
首先,在数据框中,每一行对应一个观察结果(例如一个乘客),每一列对应一个特征(性别、年龄等)。例如,通过查看第一个观察结果,我们可以看到伊丽莎白·沃尔顿·艾伦小姐住在头等舱,年龄为 29 岁,是女性,并且幸存于这场灾难。
其次,在数据框中,每一行对应一个观察结果(例如一个乘客),每一列对应一个特征(性别、年龄等)。例如,通过查看第一个观察结果,我们可以看到伊丽莎白·沃尔顿·艾伦小姐住在头等舱,年龄为 29 岁,是女性,并且幸存于这场灾难。
第三,两列Sex
和SexCode
以不同格式包含相同的信息。在Sex
中,女性用字符串female
表示,而在SexCode
中,女性用整数1
表示。我们希望所有的特征都是唯一的,因此我们需要删除其中一列。
在本章中,我们将涵盖使用 pandas 库操作数据框的各种技术,旨在创建一个干净、结构良好的观察结果集以便进行进一步的预处理。
3.1 创建数据框
问题
您想要创建一个新的数据框。
解决方案
pandas 有许多用于创建新数据框对象的方法。一个简单的方法是使用 Python 字典实例化一个DataFrame
。在字典中,每个键是列名,每个值是一个列表,其中每个项目对应一行:
# Load library
import pandas as pd
# Create a dictionary
dictionary = {
"Name": ['Jacky Jackson', 'Steven Stevenson'],
"Age": [38, 25],
"Driver": [True, False]
}
# Create DataFrame
dataframe = pd.DataFrame(dictionary)
# Show DataFrame
dataframe
名称 | 年龄 | 驾驶员 | |
---|---|---|---|
0 | 杰基·杰克逊 | 38 | True |
1 | 史蒂文·史蒂文森 | 25 | False |
使用值列表很容易向任何数据框添加新列:
# Add a column for eye color
dataframe["Eyes"] = ["Brown", "Blue"]
# Show DataFrame
dataframe
名称 | 年龄 | 驾驶员 | 眼睛 | |
---|---|---|---|---|
0 | 杰基·杰克逊 | 38 | True | Brown |
1 | 史蒂文·史蒂文森 | 25 | False | 蓝色 |
讨论
pandas 提供了几乎无限种方法来创建 DataFrame。在实际应用中,几乎不会先创建一个空的 DataFrame,然后再填充数据。相反,我们的 DataFrame 通常是从其他来源(如 CSV 文件或数据库)加载真实数据而创建的。
3.2 获取关于数据的信息
问题
您想查看 DataFrame 的一些特征。
解决方案
加载数据后,最简单的事情之一是使用 head
查看前几行数据:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Show two rows
dataframe.head(2)
姓名 | 舱位 | 年龄 | 性别 | 生还 | 性别编码 | |
---|---|---|---|---|---|---|
0 | Allen, Miss Elisabeth Walton | 1st | 29.0 | 女性 | 1 | 1 |
1 | Allison, Miss Helen Loraine | 1st | 2.0 | 女性 | 0 | 1 |
我们也可以查看行数和列数:
# Show dimensions
dataframe.shape
(1313, 6)
我们可以使用 describe
获取任何数值列的描述统计信息:
# Show statistics
dataframe.describe()
年龄 | 生还 | 性别编码 | |
---|---|---|---|
count | 756.000000 | 1313.000000 | 1313.000000 |
mean | 30.397989 | 0.342727 | 0.351866 |
std | 14.259049 | 0.474802 | 0.477734 |
min | 0.170000 | 0.000000 | 0.000000 |
25% | 21.000000 | 0.000000 | 0.000000 |
50% | 28.000000 | 0.000000 | 0.000000 |
75% | 39.000000 | 1.000000 | 1.000000 |
max | 71.000000 | 1.000000 | 1.000000 |
另外,info
方法可以显示一些有用的信息:
# Show info
dataframe.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1313 entries, 0 to 1312
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Name 1313 non-null object
1 PClass 1313 non-null object
2 Age 756 non-null float64
3 Sex 1313 non-null object
4 Survived 1313 non-null int64
5 SexCode 1313 non-null int64
dtypes: float64(1), int64(2), object(3)
memory usage: 61.7+ KB
讨论
加载数据后,了解其结构和包含的信息是个好主意。理想情况下,我们可以直接查看完整的数据。但在大多数实际情况下,数据可能有数千到数百万行和列。因此,我们必须依靠提取样本来查看小部分数据片段,并计算数据的汇总统计信息。
在我们的解决方案中,我们使用了 Titanic 乘客的玩具数据集。使用 head
可以查看数据的前几行(默认为五行)。或者,我们可以使用 tail
查看最后几行。使用 shape
可以查看 DataFrame 包含多少行和列。使用 describe
可以查看任何数值列的基本描述统计信息。最后,info
显示了关于 DataFrame 的一些有用数据点,包括索引和列的数据类型、非空值和内存使用情况。
值得注意的是,汇总统计数据并不总是能完全反映事物的全部情况。例如,pandas 将 Survived
和 SexCode
列视为数值列,因为它们包含 1 和 0。然而,在这种情况下,这些数值实际上表示的是类别。例如,如果 Survived
等于 1,则表示该乘客在事故中生还。因此,某些汇总统计数据可能不适用,比如 SexCode
列的标准差(乘客性别的指示器)。
3.3 切片 DataFrame
问题
您需要选择特定的数据子集或 DataFrame 的切片。
解决方案
使用 loc
或 iloc
来选择一个或多个行或值:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Select first row
dataframe.iloc[0]
Name Allen, Miss Elisabeth Walton
PClass 1st
Age 29
Sex female
Survived 1
SexCode 1
Name: 0, dtype: object
我们可以使用:
来定义我们想要的行切片,比如选择第二、第三和第四行:
# Select three rows
dataframe.iloc[1:4]
名称 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
1 | 艾莉森小姐海伦·洛林 | 1st | 2.0 | 女性 | 0 | 1 |
2 | 艾莉森先生哈德森·乔舒亚·克莱顿 | 1st | 30.0 | 男性 | 0 | 0 |
3 | 艾莉森夫人哈德森 JC(贝西·沃尔多·丹尼尔斯) | 1st | 25.0 | 女性 | 0 | 1 |
我们甚至可以使用它来获取某个点之前的所有行,比如包括第四行在内的所有行:
# Select four rows
dataframe.iloc[:4]
名称 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
0 | 艾伦小姐伊丽莎白·沃尔顿 | 1st | 29.0 | 女性 | 1 | 1 |
1 | 艾莉森小姐海伦·洛林 | 1st | 2.0 | 女性 | 0 | 1 |
2 | 艾莉森先生哈德森·乔舒亚·克莱顿 | 1st | 30.0 | 男性 | 0 | 0 |
3 | 艾莉森夫人哈德森 JC(贝西·沃尔多·丹尼尔斯) | 1st | 25.0 | 女性 | 0 | 1 |
数据框不需要数值索引。我们可以将数据框的索引设置为任何唯一的值,比如乘客的姓名,然后通过姓名选择行:
# Set index
dataframe = dataframe.set_index(dataframe['Name'])
# Show row
dataframe.loc['Allen, Miss Elisabeth Walton']
Name Allen, Miss Elisabeth Walton
PClass 1st
Age 29
Sex female
Survived 1
SexCode 1
Name: Allen, Miss Elisabeth Walton, dtype: object
讨论
pandas 数据框中的所有行都有唯一的索引值。默认情况下,这个索引是一个整数,表示数据框中的行位置;然而,并不一定要这样。数据框索引可以设置为唯一的字母数字字符串或客户号码。为了选择单独的行和行的切片,pandas 提供了两种方法:
-
loc
在数据框的索引是标签时非常有用(例如,字符串)。 -
iloc
的工作原理是查找数据框中的位置。例如,iloc[0]
将返回数据框中的第一行,无论索引是整数还是标签。
熟悉loc
和iloc
在数据清洗过程中非常有用。
3.4 根据条件选择行
问题
您希望根据某些条件选择数据框的行。
解决方案
这在 pandas 中很容易实现。例如,如果我们想要选择所有泰坦尼克号上的女性:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Show top two rows where column 'sex' is 'female'
dataframe[dataframe['Sex'] == 'female'].head(2)
名称 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
0 | 艾伦小姐伊丽莎白·沃尔顿 | 1st | 29.0 | 女性 | 1 | 1 |
1 | 艾莉森小姐海伦·洛林 | 1st | 2.0 | 女性 | 0 | 1 |
请花点时间看一下这个解决方案的格式。我们的条件语句是dataframe['Sex'] == 'female'
;通过将其包装在dataframe[]
中,我们告诉 pandas“选择数据框中dataframe['Sex']
值为'female'的所有行”。这些条件导致了一个布尔值的 pandas 系列。
多条件也很容易。例如,这里我们选择所有女性且年龄在 65 岁或以上的乘客:
# Filter rows
dataframe[(dataframe['Sex'] == 'female') & (dataframe['Age'] >= 65)]
名称 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
73 | 克罗斯比夫人爱德华·吉福德(凯瑟琳·伊丽莎白... | 1st | 69.0 | 女性 | 1 | 1 |
讨论
在数据整理中,有条件地选择和过滤数据是最常见的任务之一。您很少需要源数据的所有原始数据;相反,您只对某些子集感兴趣。例如,您可能只对特定州的商店或特定年龄段的患者记录感兴趣。
3.5 排序数值
问题
您需要按列中的值对数据框进行排序。
解决方案
使用 pandas 的 sort_values
函数:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Sort the dataframe by age, show two rows
dataframe.sort_values(by=["Age"]).head(2)
姓名 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
763 | 迪恩,伊丽莎白·格莱迪斯(米尔文娜)小姐 | 3rd | 0.17 | 女性 | 1 | 1 |
751 | 丹博姆,吉尔伯特·西格瓦德·埃马纽尔大师 | 3rd | 0.33 | 男性 | 0 | 0 |
讨论
在数据分析和探索过程中,按照特定列或一组列对 DataFrame 进行排序通常非常有用。sort_values
的 by
参数接受一个列名列表,按列表中列名的顺序对 DataFrame 进行排序。
默认情况下,ascending
参数设置为 True
,因此它会将值从最低到最高排序。如果我们想要最年长的乘客而不是最年轻的,我们可以将其设置为 False
。
3.6 替换值
问题
您需要在 DataFrame 中替换值。
解决方案
pandas 的 replace
方法是查找和替换值的简便方法。例如,我们可以将 Sex
列中的任何 "female"
实例替换为 "Woman"
:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Replace values, show two rows
dataframe['Sex'].replace("female", "Woman").head(2)
0 Woman
1 Woman
Name: Sex, dtype: object
我们还可以同时替换多个值:
# Replace "female" and "male" with "Woman" and "Man"
dataframe['Sex'].replace(["female", "male"], ["Woman", "Man"]).head(5)
0 Woman
1 Woman
2 Man
3 Woman
4 Man
Name: Sex, dtype: object
我们还可以通过指定整个 DataFrame 而不是单个列来查找并替换 DataFrame
对象中的所有值:
# Replace values, show two rows
dataframe.replace(1, "One").head(2)
姓名 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | 1st | 29 | 女性 | One | One |
1 | 艾莉森,洛林小姐海伦 | 1st | 2 | 女性 | 0 | One |
replace
也接受正则表达式:
# Replace values, show two rows
dataframe.replace(r"1st", "First", regex=True).head(2)
姓名 | PClass | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | First | 29.0 | 女性 | 1 | 1 |
1 | 艾莉森,洛林小姐海伦 | First | 2.0 | 女性 | 0 | 1 |
讨论
replace
是我们用来替换值的工具。它简单易用,同时能够接受正则表达式。
3.7 重命名列
问题
您想要在 pandas DataFrame 中重命名列。
解决方案
使用 rename
方法重命名列:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Rename column, show two rows
dataframe.rename(columns={'PClass': 'Passenger Class'}).head(2)
姓名 | 乘客等级 | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | 1st | 29.0 | 女性 | 1 | 1 |
1 | 艾莉森,洛林小姐海伦 | 1st | 2.0 | 女性 | 0 | 1 |
注意,rename
方法可以接受一个字典作为参数。我们可以使用字典一次性更改多个列名:
# Rename columns, show two rows
dataframe.rename(columns={'PClass': 'Passenger Class', 'Sex': 'Gender'}).head(2)
姓名 | 乘客等级 | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | 1st | 29.0 | 女性 | 1 | 1 |
1 | 艾莉森,洛林小姐海伦 | 1st | 2.0 | 女性 | 0 | 1 |
讨论
使用将字典作为参数传递给 columns
参数的 rename
是我首选的重命名列的方法,因为它适用于任意数量的列。 如果我们想一次重命名所有列,这段有用的代码片段会创建一个以旧列名作为键、空字符串作为值的字典:
# Load library
import collections
# Create dictionary
column_names = collections.defaultdict(str)
# Create keys
for name in dataframe.columns:
column_names[name]
# Show dictionary
column_names
defaultdict(str,
{'Age': '',
'Name': '',
'PClass': '',
'Sex': '',
'SexCode': '',
'Survived': ''})
3.8 寻找最小值、最大值、总和、平均值和计数
问题
您想要找到数值列的最小值、最大值、总和、平均值或计数。
解决方案
pandas 提供了一些内置方法,用于常用的描述统计,如 min
、max
、mean
、sum
和 count
:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Calculate statistics
print('Maximum:', dataframe['Age'].max())
print('Minimum:', dataframe['Age'].min())
print('Mean:', dataframe['Age'].mean())
print('Sum:', dataframe['Age'].sum())
print('Count:', dataframe['Age'].count())
Maximum: 71.0
Minimum: 0.17
Mean: 30.397989417989415
Sum: 22980.879999999997
Count: 756
讨论
除了解决方案中使用的统计数据外,pandas 还提供了方差(var
)、标准差(std
)、峰度(kurt
)、偏度(skew
)、均值标准误(sem
)、众数(mode
)、中位数(median
)、值计数以及其他几种统计数据。
此外,我们还可以将这些方法应用于整个 DataFrame:
# Show counts
dataframe.count()
Name 1313
PClass 1313
Age 756
Sex 1313
Survived 1313
SexCode 1313
dtype: int64
3.9 寻找唯一值
问题
您想选择某一列中的所有唯一值。
解决方案
使用 unique
查看列中所有唯一值的数组:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Select unique values
dataframe['Sex'].unique()
array(['female', 'male'], dtype=object)
或者,value_counts
将显示所有唯一值及其出现次数:
# Show counts
dataframe['Sex'].value_counts()
male 851
female 462
Name: Sex, dtype: int64
讨论
unique
和 value_counts
对于操作和探索分类列非常有用。 在分类列中,通常需要在数据整理阶段处理类别。 例如,在 Titanic 数据集中,PClass
是指乘客票的类别。 在 Titanic 上有三个等级; 但是,如果我们使用 value_counts
,我们会发现一个问题:
# Show counts
dataframe['PClass'].value_counts()
3rd 711
1st 322
2nd 279
* 1
Name: PClass, dtype: int64
尽管几乎所有乘客都属于预期的三个类别之一,但是有一个乘客的类别是 *
。 在处理这类问题时有多种策略,我们将在第五章中进行讨论,但现在只需意识到,在分类数据中,“额外”类别是常见的,不应忽略。
最后,如果我们只想计算唯一值的数量,我们可以使用 nunique
:
# Show number of unique values
dataframe['PClass'].nunique()
4
3.10 处理缺失值
问题
您想要选择 DataFrame 中的缺失值。
解决方案
isnull
和 notnull
返回布尔值,指示值是否缺失:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
## Select missing values, show two rows
dataframe[dataframe['Age'].isnull()].head(2)
名称 | 舱位 | 年龄 | 性别 | 幸存 | 性别代码 | |
---|---|---|---|---|---|---|
12 | Aubert, Mrs Leontine Pauline | 1st | NaN | female | 1 | 1 |
13 | Barkworth, Mr Algernon H | 1st | NaN | male | 1 | 0 |
讨论
缺失值是数据整理中普遍存在的问题,然而许多人低估了处理缺失数据的难度。 pandas 使用 NumPy 的 NaN
(非数值)值表示缺失值,但重要的是要注意,pandas 中没有完全本地实现 NaN
。 例如,如果我们想用包含 male
的所有字符串替换缺失值,我们会得到一个错误:
# Attempt to replace values with NaN
dataframe['Sex'] = dataframe['Sex'].replace('male', NaN)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-7-5682d714f87d> in <module>()
1 # Attempt to replace values with NaN
----> 2 dataframe['Sex'] = dataframe['Sex'].replace('male', NaN)
NameError: name 'NaN' is not defined
---------------------------------------------------------------------------
要完全使用 NaN
的功能,我们首先需要导入 NumPy 库:
# Load library
import numpy as np
# Replace values with NaN
dataframe['Sex'] = dataframe['Sex'].replace('male', np.nan)
很多时候,数据集使用特定的值来表示缺失的观察值,例如 NONE
、-999
或 ..
。pandas 的 read_csv
函数包括一个参数,允许我们指定用于指示缺失值的值:
# Load data, set missing values
dataframe = pd.read_csv(url, na_values=[np.nan, 'NONE', -999])
我们还可以使用 pandas 的 fillna
函数来填充列的缺失值。在这里,我们使用 isna
函数显示 Age
为空的位置,然后用乘客的平均年龄填充这些值。
# Get a single null row
null_entry = dataframe[dataframe["Age"].isna()].head(1)
print(null_entry)
姓名 | 舱位 | 年龄 | 性别 | 幸存 | 性别编码 | |
---|---|---|---|---|---|---|
12 | 奥贝特,利昂汀·波琳娜夫人 | 1st | NaN | 女性 | 1 | 1 |
# Fill all null values with the mean age of passengers
null_entry.fillna(dataframe["Age"].mean())
姓名 | 舱位 | 年龄 | 性别 | 幸存 | 性别编码 | |
---|---|---|---|---|---|---|
12 | 奥贝特,利昂汀·波琳娜夫人 | 1st | 30.397989 | 女性 | 1 | 1 |
3.11 删除列
问题
您想要从您的 DataFrame 中删除一列。
解决方案
删除列的最佳方法是使用带有参数 axis=1
(即列轴)的 drop
:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Delete column
dataframe.drop('Age', axis=1).head(2)
姓名 | 舱位 | 性别 | 幸存 | 性别编码 | |
---|---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | 1st | 女性 | 1 | 1 |
1 | 艾莉森,露萍小姐 | 1st | 女性 | 0 | 1 |
你也可以使用列名的列表作为删除多列的主要参数:
# Drop columns
dataframe.drop(['Age', 'Sex'], axis=1).head(2)
姓名 | 舱位 | 幸存 | 性别编码 | |
---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | 1st | 1 | 1 |
1 | 艾莉森,露萍小姐 | 1st | 0 | 1 |
如果列没有名称(有时可能会发生),您可以使用 dataframe.columns
按其列索引删除它:
# Drop column
dataframe.drop(dataframe.columns[1], axis=1).head(2)
姓名 | 年龄 | 性别 | 幸存 | 性别编码 | |
---|---|---|---|---|---|
0 | 艾伦,伊丽莎白·沃尔顿小姐 | 29.0 | 女性 | 1 | 1 |
1 | 艾莉森,露萍小姐 | 2.0 | 女性 | 0 | 1 |
讨论
drop
是删除列的成语方法。另一种方法是 del dataframe['Age']
,大多数情况下可以工作,但不建议使用,因为它在 pandas 中的调用方式(其细节超出本书的范围)。
我建议您避免使用 pandas 的 inplace=True
参数。许多 pandas 方法包括一个 inplace
参数,当设置为 True
时,直接编辑 DataFrame。这可能会导致在更复杂的数据处理管道中出现问题,因为我们将 DataFrame 视为可变对象(从技术上讲确实如此)。我建议将 DataFrame 视为不可变对象。例如:
# Create a new DataFrame
dataframe_name_dropped = dataframe.drop(dataframe.columns[0], axis=1)
在这个例子中,我们没有改变 DataFrame dataframe
,而是创建了一个新的 DataFrame,称为 dataframe_name_dropped
,它是 dataframe
的修改版本。如果您将 DataFrame 视为不可变对象,那么您将会在将来避免很多麻烦。
3.12 删除行
问题
您想要从 DataFrame 中删除一行或多行。
解决方案
使用布尔条件创建一个新的 DataFrame,排除你想要删除的行:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Delete rows, show first three rows of output
dataframe[dataframe['Sex'] != 'male'].head(3)
姓名 | 舱位 | 年龄 | 性别 | 幸存 | 性别编码 | |
---|---|---|---|---|---|---|
0 | Allen, Miss Elisabeth Walton | 1st | 29.0 | 女性 | 1 | 1 |
1 | Allison, Miss Helen Loraine | 1st | 2.0 | 女性 | 0 | 1 |
3 | Allison, Mrs Hudson JC (Bessie Waldo Daniels) | 1st | 25.00 | 女性 | 0 | 1 |
讨论
技术上你可以使用drop
方法(例如,dataframe.drop([0, 1], axis=0)
来删除前两行),但更实用的方法是简单地将布尔条件包装在dataframe[]
中。这使我们能够利用条件语句的威力来删除单行或(更有可能)多行。
我们可以使用布尔条件轻松删除单行,通过匹配唯一值:
# Delete row, show first two rows of output
dataframe[dataframe['Name'] != 'Allison, Miss Helen Loraine'].head(2)
姓名 | 票类 | 年龄 | 性别 | 生存 | 性别编码 | |
---|---|---|---|---|---|---|
0 | Allen, Miss Elisabeth Walton | 1st | 29.0 | 女性 | 1 | 1 |
2 | Allison, Mr Hudson Joshua Creighton | 1st | 30.0 | 男性 | 0 | 0 |
我们甚至可以通过指定行索引来使用它删除单行:
# Delete row, show first two rows of output
dataframe[dataframe.index != 0].head(2)
姓名 | 票类 | 年龄 | 性别 | 生存 | 性别编码 | |
---|---|---|---|---|---|---|
1 | Allison, Miss Helen Loraine | 1st | 2.0 | 女性 | 0 | 1 |
2 | Allison, Mr Hudson Joshua Creighton | 1st | 30.0 | 男性 | 0 | 0 |
3.13 删除重复行
问题
您想从 DataFrame 中删除重复的行。
解决方案
使用drop_duplicates
,但要注意参数:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Drop duplicates, show first two rows of output
dataframe.drop_duplicates().head(2)
姓名 | 票类 | 年龄 | 性别 | 生存 | 性别编码 | |
---|---|---|---|---|---|---|
0 | Allen, Miss Elisabeth Walton | 1st | 29.0 | 女性 | 1 | 1 |
1 | Allison, Miss Helen Loraine | 1st | 2.0 | 女性 | 0 | 1 |
讨论
一个敏锐的读者会注意到,解决方案实际上并没有删除任何行:
# Show number of rows
print("Number Of Rows In The Original DataFrame:", len(dataframe))
print("Number Of Rows After Deduping:", len(dataframe.drop_duplicates()))
Number Of Rows In The Original DataFrame: 1313
Number Of Rows After Deduping: 1313
这是因为drop_duplicates
默认仅删除所有列完全匹配的行。因为我们 DataFrame 中的每一行都是唯一的,所以不会被删除。然而,通常我们只想考虑部分列来检查重复行。我们可以使用subset
参数来实现这一点:
# Drop duplicates
dataframe.drop_duplicates(subset=['Sex'])
姓名 | 票类 | 年龄 | 性别 | 生存 | 性别编码 | |
---|---|---|---|---|---|---|
0 | Allen, Miss Elisabeth Walton | 1st | 29.0 | 女性 | 1 | 1 |
2 | Allison, Mr Hudson Joshua Creighton | 1st | 30.0 | 男性 | 0 | 0 |
仔细观察上述输出:我们告诉drop_duplicates
仅考虑具有相同Sex
值的任意两行为重复行,并将其删除。现在我们只剩下两行的 DataFrame:一个女性和一个男性。你可能会问为什么drop_duplicates
决定保留这两行而不是两行不同的行。答案是drop_duplicates
默认保留重复行的第一次出现并丢弃其余的。我们可以使用keep
参数来控制这种行为:
# Drop duplicates
dataframe.drop_duplicates(subset=['Sex'], keep='last')
姓名 | 票类 | 年龄 | 性别 | 生存 | 性别编码 | |
---|---|---|---|---|---|---|
1307 | Zabour, Miss Tamini | 3rd | NaN | 女性 | 0 | 1 |
1312 | Zimmerman, Leo | 3rd | 29.0 | 男性 | 0 | 0 |
一个相关的方法是duplicated
,它返回一个布尔系列,指示行是否是重复的。如果您不想简单地删除重复项,这是一个不错的选择:
dataframe.duplicated()
0 False
1 False
2 False
3 False
4 False
...
1308 False
1309 False
1310 False
1311 False
1312 False
Length: 1313, dtype: bool
3.14 按值分组行
问题
您希望根据某些共享值对单独的行进行分组。
解决方案
groupby
是 pandas 中最强大的特性之一:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Group rows by the values of the column 'Sex', calculate mean # of each group
dataframe.groupby('Sex').mean(numeric_only=True)
性别 | 年龄 | 幸存者 | 性别代码 |
---|---|---|---|
女性 | 29.396424 | 0.666667 | 1.0 |
男性 | 31.014338 | 0.166863 | 0.0 |
讨论
groupby
是数据处理真正开始成形的地方。DataFrame 中每行代表一个人或事件是非常普遍的,我们想根据某些标准对它们进行分组,然后计算统计量。例如,您可以想象一个 DataFrame,其中每行是全国餐厅连锁店的单笔销售,我们想要每个餐厅的总销售额。我们可以通过按独立餐厅分组行,然后计算每组的总和来实现这一点。
新用户对groupby
经常写出这样的一行,然后对返回的内容感到困惑:
# Group rows
dataframe.groupby('Sex')
<pandas.core.groupby.DataFrameGroupBy object at 0x10efacf28>
为什么它没有返回更有用的东西?原因是groupby
需要与我们想应用于每个组的某些操作配对,比如计算聚合统计(例如均值、中位数、总和)。在讨论分组时,我们经常使用简写说“按性别分组”,但这是不完整的。为了使分组有用,我们需要按某些标准分组,然后对每个组应用函数:
# Group rows, count rows
dataframe.groupby('Survived')['Name'].count()
Survived
0 863
1 450
Name: Name, dtype: int64
注意在groupby
后添加了Name
?这是因为特定的摘要统计只对某些类型的数据有意义。例如,按性别计算平均年龄是有意义的,但按性别计算总年龄不是。在这种情况下,我们将数据分组为幸存或未幸存,然后计算每个组中的名称数量(即乘客数)。
我们还可以按第一列分组,然后按第二列对该分组进行分组:
# Group rows, calculate mean
dataframe.groupby(['Sex','Survived'])['Age'].mean()
Sex Survived
female 0 24.901408
1 30.867143
male 0 32.320780
1 25.951875
Name: Age, dtype: float64
3.15 按时间分组行
问题
您需要按时间段对单独的行进行分组。
解决方案
使用resample
按时间段分组行:
# Load libraries
import pandas as pd
import numpy as np
# Create date range
time_index = pd.date_range('06/06/2017', periods=100000, freq='30S')
# Create DataFrame
dataframe = pd.DataFrame(index=time_index)
# Create column of random values
dataframe['Sale_Amount'] = np.random.randint(1, 10, 100000)
# Group rows by week, calculate sum per week
dataframe.resample('W').sum()
销售金额 | |
---|---|
2017-06-11 | 86423 |
2017-06-18 | 101045 |
2017-06-25 | 100867 |
2017-07-02 | 100894 |
2017-07-09 | 100438 |
2017-07-16 | 10297 |
讨论
我们的标准Titanic数据集不包含日期时间列,因此对于这个示例,我们生成了一个简单的 DataFrame,其中每行代表一次单独的销售。对于每个销售,我们知道其日期时间和金额(这些数据并不真实,因为销售间隔正好为 30 秒,金额为确切的美元数,但为了简单起见,我们假装是这样的)。
原始数据如下所示:
# Show three rows
dataframe.head(3)
销售金额 | |
---|---|
2017-06-06 00:00:00 | 7 |
2017-06-06 00:00:30 | 2 |
2017-06-06 00:01:00 | 7 |
注意每次销售的日期和时间是 DataFrame 的索引;这是因为resample
需要索引是类似日期时间的值。
使用resample
,我们可以按照各种时间段(偏移)对行进行分组,然后可以在每个时间组上计算统计信息:
# Group by two weeks, calculate mean
dataframe.resample('2W').mean()
Sale_Amount | |
---|---|
2017-06-11 | 5.001331 |
2017-06-25 | 5.007738 |
2017-07-09 | 4.993353 |
2017-07-23 | 4.950481 |
# Group by month, count rows
dataframe.resample('M').count()
| | Sale_Amount |
| --- | --- | --- |
| 2017-06-30 | 72000 |
| 2017-07-31 | 28000 |
您可能注意到,在这两个输出中,日期时间索引是日期,即使我们按周和月进行分组。原因是默认情况下,resample
返回时间组的右“边缘”标签(最后一个标签)。我们可以使用label
参数控制此行为:
# Group by month, count rows
dataframe.resample('M', label='left').count()
Sale_Amount | |
---|---|
2017-05-31 | 72000 |
2017-06-30 | 28000 |
另请参阅
3.16 聚合操作和统计
问题
您需要对数据框中的每列(或一组列)进行聚合操作。
解决方案
使用 pandas 的agg
方法。在这里,我们可以轻松地获得每列的最小值:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Get the minimum of every column
dataframe.agg("min")
Name Abbing, Mr Anthony
PClass *
Age 0.17
Sex female
Survived 0
SexCode 0
dtype: object
有时,我们希望将特定函数应用于特定列集:
# Mean Age, min and max SexCode
dataframe.agg({"Age":["mean"], "SexCode":["min", "max"]})
Age | SexCode | |
---|---|---|
mean | 30.397989 | NaN |
min | NaN | 0.0 |
max | NaN | 1.0 |
我们还可以将聚合函数应用于组,以获取更具体的描述性统计信息:
# Number of people who survived and didn't survive in each class
dataframe.groupby(
["PClass","Survived"]).agg({"Survived":["count"]}
).reset_index()
PClass | Survived | Count |
---|---|---|
0 | * | 0 |
1 | 1st | 0 |
2 | 1st | 1 |
3 | 2nd | 0 |
4 | 2nd | 1 |
5 | 3rd | 0 |
6 | 3rd | 1 |
讨论
在探索性数据分析中,聚合函数特别有用,可用于了解数据的不同子群体和变量之间的关系。通过对数据进行分组并应用聚合统计,您可以查看数据中的模式,这些模式在机器学习或特征工程过程中可能会很有用。虽然视觉图表也很有帮助,但有这样具体的描述性统计数据作为参考,有助于更好地理解数据。
另请参阅
3.17 遍历列
问题
您希望遍历列中的每个元素并应用某些操作。
解决方案
您可以像对待 Python 中的任何其他序列一样处理 pandas 列,并使用标准 Python 语法对其进行循环:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Print first two names uppercased
for name in dataframe['Name'][0:2]:
print(name.upper())
ALLEN, MISS ELISABETH WALTON
ALLISON, MISS HELEN LORAINE
讨论
除了循环(通常称为for
循环)之外,我们还可以使用列表推导:
# Show first two names uppercased
[name.upper() for name in dataframe['Name'][0:2]]
['ALLEN, MISS ELISABETH WALTON', 'ALLISON, MISS HELEN LORAINE']
尽管有诱惑使用for
循环,更符合 Python 风格的解决方案应该使用 pandas 的apply
方法,详见配方 3.18。
3.18 在列中的所有元素上应用函数
问题
您希望在一列的所有元素上应用某些函数。
解决方案
使用apply
在列的每个元素上应用内置或自定义函数:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Create function
def uppercase(x):
return x.upper()
# Apply function, show two rows
dataframe['Name'].apply(uppercase)[0:2]
0 ALLEN, MISS ELISABETH WALTON
1 ALLISON, MISS HELEN LORAINE
Name: Name, dtype: object
讨论
apply
是进行数据清理和整理的好方法。通常会编写一个函数来执行一些有用的操作(将名字分开,将字符串转换为浮点数等),然后将该函数映射到列中的每个元素。
3.19 对组应用函数
问题
您已经使用groupby
对行进行分组,并希望对每个组应用函数。
解决方案
结合groupby
和apply
:
# Load library
import pandas as pd
# Create URL
url = 'https://raw.githubusercontent.com/chrisalbon/sim_data/master/titanic.csv'
# Load data
dataframe = pd.read_csv(url)
# Group rows, apply function to groups
dataframe.groupby('Sex').apply(lambda x: x.count())
性别 | 姓名 | 舱位 | 年龄 | 性别 | 幸存 | 性别编码 |
---|---|---|---|---|---|---|
女性 | 462 | 462 | 288 | 462 | 462 | 462 |
男性 | 851 | 851 | 468 | 851 | 851 | 851 |
讨论
在 Recipe 3.18 中提到了apply
。当你想对分组应用函数时,apply
特别有用。通过结合groupby
和apply
,我们可以计算自定义统计信息或将任何函数分别应用于每个组。
3.20 合并数据框
问题
您想要将两个数据框连接在一起。
解决方案
使用concat
和axis=0
沿行轴进行连接:
# Load library
import pandas as pd
# Create DataFrame
data_a = {'id': ['1', '2', '3'],
'first': ['Alex', 'Amy', 'Allen'],
'last': ['Anderson', 'Ackerman', 'Ali']}
dataframe_a = pd.DataFrame(data_a, columns = ['id', 'first', 'last'])
# Create DataFrame
data_b = {'id': ['4', '5', '6'],
'first': ['Billy', 'Brian', 'Bran'],
'last': ['Bonder', 'Black', 'Balwner']}
dataframe_b = pd.DataFrame(data_b, columns = ['id', 'first', 'last'])
# Concatenate DataFrames by rows
pd.concat([dataframe_a, dataframe_b], axis=0)
id | first | last | |
---|---|---|---|
0 | 1 | Alex | Anderson |
1 | 2 | Amy | Ackerman |
2 | 3 | Allen | Ali |
0 | 4 | Billy | Bonder |
1 | 5 | Brian | Black |
2 | 6 | Bran | Balwner |
您可以使用axis=1
沿列轴进行连接:
# Concatenate DataFrames by columns
pd.concat([dataframe_a, dataframe_b], axis=1)
id | first | last | id | first | last | |
---|---|---|---|---|---|---|
0 | 1 | Alex | Anderson | 4 | Billy | Bonder |
1 | 2 | Amy | Ackerman | 5 | Brian | Black |
2 | 3 | Allen | Ali | 6 | Bran | Balwner |
讨论
合并通常是一个你在计算机科学和编程领域听得较多的词汇,所以如果你以前没听说过,别担心。concatenate的非正式定义是将两个对象粘合在一起。在解决方案中,我们使用axis
参数将两个小数据框粘合在一起,以指示我们是否想要将两个数据框叠加在一起还是并排放置它们。
3.21 合并数据框
问题
您想要合并两个数据框。
解决方案
要进行内连接,使用merge
并使用on
参数指定要合并的列:
# Load library
import pandas as pd
# Create DataFrame
employee_data = {'employee_id': ['1', '2', '3', '4'],
'name': ['Amy Jones', 'Allen Keys', 'Alice Bees',
'Tim Horton']}
dataframe_employees = pd.DataFrame(employee_data, columns = ['employee_id',
'name'])
# Create DataFrame
sales_data = {'employee_id': ['3', '4', '5', '6'],
'total_sales': [23456, 2512, 2345, 1455]}
dataframe_sales = pd.DataFrame(sales_data, columns = ['employee_id',
'total_sales'])
# Merge DataFrames
pd.merge(dataframe_employees, dataframe_sales, on='employee_id')
员工编号 | 姓名 | 总销售额 | |
---|---|---|---|
0 | 3 | Alice Bees | 23456 |
1 | 4 | Tim Horton | 2512 |
merge
默认为内连接。如果我们想进行外连接,可以使用how
参数来指定:
# Merge DataFrames
pd.merge(dataframe_employees, dataframe_sales, on='employee_id', how='outer')
员工编号 | 姓名 | 总销售额 | |
---|---|---|---|
0 | 1 | Amy Jones | NaN |
1 | 2 | Allen Keys | NaN |
2 | 3 | Alice Bees | 23456.0 |
3 | 4 | Tim Horton | 2512.0 |
4 | 5 | NaN | 2345.0 |
5 | 6 | NaN | 1455.0 |
同一个参数可以用来指定左连接和右连接:
# Merge DataFrames
pd.merge(dataframe_employees, dataframe_sales, on='employee_id', how='left')
员工编号 | 姓名 | 总销售额 | |
---|---|---|---|
0 | 1 | Amy Jones | NaN |
1 | 2 | Allen Keys | NaN |
2 | 3 | Alice Bees | 23456.0 |
3 | 4 | Tim Horton | 2512.0 |
我们还可以在每个数据框中指定要合并的列名:
# Merge DataFrames
pd.merge(dataframe_employees,
dataframe_sales,
left_on='employee_id',
right_on='employee_id')
员工编号 | 姓名 | 总销售额 | |
---|---|---|---|
0 | 3 | Alice Bees | 23456 |
1 | 4 | Tim Horton | 2512 |
如果我们希望不是在两个列上进行合并,而是在每个 DataFrame 的索引上进行合并,我们可以将left_on
和right_on
参数替换为left_index=True
和right_index=True
。
讨论
我们需要使用的数据通常很复杂;它不总是一次性出现。相反,在现实世界中,我们通常面对来自多个数据库查询或文件的不同数据集。为了将所有数据汇总到一个地方,我们可以将每个数据查询或数据文件作为单独的 DataFrame 加载到 pandas 中,然后将它们合并成一个单一的 DataFrame。
这个过程对于使用过 SQL 的人可能会很熟悉,SQL 是一种用于执行合并操作(称为连接)的流行语言。虽然 pandas 使用的确切参数会有所不同,但它们遵循其他软件语言和工具使用的相同一般模式。
任何merge
操作都有三个方面需要指定。首先,我们必须指定要合并的两个 DataFrame。在解决方案中,我们将它们命名为dataframe_employees
和dataframe_sales
。其次,我们必须指定要合并的列名(们)-即,两个 DataFrame 之间共享值的列名。例如,在我们的解决方案中,两个 DataFrame 都有一个名为employee_id
的列。为了合并这两个 DataFrame,我们将匹配每个 DataFrame 的employee_id
列中的值。如果这两个列使用相同的名称,我们可以使用on
参数。但是,如果它们有不同的名称,我们可以使用left_on
和right_on
。
什么是左 DataFrame 和右 DataFrame?左 DataFrame 是我们在merge
中指定的第一个 DataFrame,右 DataFrame 是第二个。这种语言在我们将需要的下一组参数中再次出现。
最后一个方面,也是一些人难以掌握的最难的方面,是我们想要执行的合并操作的类型。这由how
参数指定。merge
支持四种主要类型的连接操作:
内部
仅返回在两个 DataFrame 中都匹配的行(例如,返回任何在dataframe_employees
和dataframe_sales
中的employee_id
值都出现的行)。
外部
返回两个 DataFrame 中的所有行。如果一行存在于一个 DataFrame 中但不在另一个 DataFrame 中,则填充缺失值 NaN(例如,在dataframe_employee
和dataframe_sales
中返回所有行)。
左
返回左 DataFrame 中的所有行,但只返回与左 DataFrame 匹配的右 DataFrame 中的行。对于缺失值填充NaN
(例如,从dataframe_employees
返回所有行,但只返回dataframe_sales
中具有出现在dataframe_employees
中的employee_id
值的行)。
右
返回右侧数据框的所有行,但仅返回左侧数据框中与右侧数据框匹配的行。对于缺失的值填充NaN
(例如,返回dataframe_sales
的所有行,但仅返回dataframe_employees
中具有出现在dataframe_sales
中的employee_id
值的行)。
如果你还没有完全理解,我鼓励你在你的代码中尝试调整how
参数,看看它如何影响merge
返回的结果。
参见
第四章:处理数值数据
4.0 引言
定量数据是某物的测量——无论是班级规模、月销售额还是学生分数。表示这些数量的自然方式是数值化(例如,29 名学生、销售额为 529,392 美元)。在本章中,我们将介绍多种策略,将原始数值数据转换为专门用于机器学习算法的特征。
4.1 重新调整特征
问题
您需要将数值特征的值重新缩放到两个值之间。
解决方案
使用 scikit-learn 的MinMaxScaler
来重新调整特征数组:
# Load libraries
import numpy as np
from sklearn import preprocessing
# Create feature
feature = np.array([[-500.5],
[-100.1],
[0],
[100.1],
[900.9]])
# Create scaler
minmax_scale = preprocessing.MinMaxScaler(feature_range=(0, 1))
# Scale feature
scaled_feature = minmax_scale.fit_transform(feature)
# Show feature
scaled_feature
array([[ 0\. ],
[ 0.28571429],
[ 0.35714286],
[ 0.42857143],
[ 1\. ]])
讨论
重新缩放 是机器学习中常见的预处理任务。本书后面描述的许多算法将假定所有特征在同一尺度上,通常是 0 到 1 或-1 到 1。有许多重新缩放技术,但最简单的之一称为最小-最大缩放。最小-最大缩放使用特征的最小值和最大值将值重新缩放到一个范围内。具体来说,最小-最大缩放计算:
其中是特征向量,是特征的单个元素,是重新调整的元素。在我们的例子中,我们可以从输出的数组中看到,特征已成功重新调整为 0 到 1 之间:
array([[ 0\. ],
[ 0.28571429],
[ 0.35714286],
[ 0.42857143],
[ 1\. ]])
scikit-learn 的MinMaxScaler
提供了两种重新调整特征的选项。一种选项是使用fit
来计算特征的最小值和最大值,然后使用transform
来重新调整特征。第二个选项是使用fit_transform
来同时执行这两个操作。这两个选项在数学上没有区别,但有时将操作分开会有实际的好处,因为这样可以将相同的转换应用于不同的数据集。
参见
4.2 标准化特征
问题
您希望将一个特征转换为具有均值为 0 和标准差为 1。
解决方案
scikit-learn 的StandardScaler
执行这两个转换:
# Load libraries
import numpy as np
from sklearn import preprocessing
# Create feature
x = np.array([[-1000.1],
[-200.2],
[500.5],
[600.6],
[9000.9]])
# Create scaler
scaler = preprocessing.StandardScaler()
# Transform the feature
standardized = scaler.fit_transform(x)
# Show feature
standardized
array([[-0.76058269],
[-0.54177196],
[-0.35009716],
[-0.32271504],
[ 1.97516685]])
讨论
对于问题 4.1 中讨论的最小-最大缩放的常见替代方案是将特征重新缩放为近似标准正态分布。为了实现这一目标,我们使用标准化来转换数据,使其均值为 0,标准差为 1。具体来说,特征中的每个元素都被转换,以便:
其中 是 的标准化形式。转换后的特征表示原始值与特征均值之间的标准偏差数(在统计学中也称为 z-score)。
标准化是机器学习预处理中常见的缩放方法,在我的经验中,它比最小-最大缩放更常用。但这取决于学习算法。例如,主成分分析通常在使用标准化时效果更好,而对于神经网络,则通常建议使用最小-最大缩放(这两种算法稍后在本书中讨论)。作为一个一般规则,我建议除非有特定原因使用其他方法,否则默认使用标准化。
我们可以通过查看解决方案输出的平均值和标准偏差来看到标准化的效果:
# Print mean and standard deviation
print("Mean:", round(standardized.mean()))
print("Standard deviation:", standardized.std())
Mean: 0.0
Standard deviation: 1.0
如果我们的数据存在显著的异常值,它可能通过影响特征的均值和方差而对我们的标准化产生负面影响。在这种情况下,通常可以通过使用中位数和四分位距来重新调整特征,从而提供帮助。在 scikit-learn 中,我们使用 RobustScaler
方法来实现这一点:
# Create scaler
robust_scaler = preprocessing.RobustScaler()
# Transform feature
robust_scaler.fit_transform(x)
array([[ -1.87387612],
[ -0.875 ],
[ 0\. ],
[ 0.125 ],
[ 10.61488511]])
4.3 规范化观测值
问题
您希望将观测值的特征值重新调整为单位范数(总长度为 1)。
解决方案
使用带有 norm
参数的 Normalizer
:
# Load libraries
import numpy as np
from sklearn.preprocessing import Normalizer
# Create feature matrix
features = np.array([[0.5, 0.5],
[1.1, 3.4],
[1.5, 20.2],
[1.63, 34.4],
[10.9, 3.3]])
# Create normalizer
normalizer = Normalizer(norm="l2")
# Transform feature matrix
normalizer.transform(features)
array([[ 0.70710678, 0.70710678],
[ 0.30782029, 0.95144452],
[ 0.07405353, 0.99725427],
[ 0.04733062, 0.99887928],
[ 0.95709822, 0.28976368]])
讨论
许多重新调整方法(例如,最小-最大缩放和标准化)作用于特征,但我们也可以跨个体观测值进行重新调整。Normalizer
将单个观测值上的值重新调整为单位范数(它们长度的总和为 1)。当存在许多等效特征时(例如,在文本分类中,每个单词或 n-word 组合都是一个特征时),通常会使用这种重新调整。
Normalizer
提供三种范数选项,其中欧几里德范数(通常称为 L2)是默认参数:
其中 是一个单独的观测值, 是该观测值在第 个特征上的值。
# Transform feature matrix
features_l2_norm = Normalizer(norm="l2").transform(features)
# Show feature matrix
features_l2_norm
array([[ 0.70710678, 0.70710678],
[ 0.30782029, 0.95144452],
[ 0.07405353, 0.99725427],
[ 0.04733062, 0.99887928],
[ 0.95709822, 0.28976368]])
或者,我们可以指定曼哈顿范数(L1):
# Transform feature matrix
features_l1_norm = Normalizer(norm="l1").transform(features)
# Show feature matrix
features_l1_norm
array([[ 0.5 , 0.5 ],
[ 0.24444444, 0.75555556],
[ 0.06912442, 0.93087558],
[ 0.04524008, 0.95475992],
[ 0.76760563, 0.23239437]])
直观上,L2 范数可以被视为鸟在纽约两点之间的距离(即直线距离),而 L1 范数可以被视为在街道上行走的人的距离(向北走一块,向东走一块,向北走一块,向东走一块,等等),这就是为什么它被称为“曼哈顿范数”或“出租车范数”的原因。
在实际应用中,注意到 norm="l1"
将重新调整观测值的值,使其总和为 1,这在某些情况下是一种可取的质量:
# Print sum
print("Sum of the first observation\'s values:",
features_l1_norm[0, 0] + features_l1_norm[0, 1])
Sum of the first observation's values: 1.0
4.4 生成多项式和交互特征
问题
您希望创建多项式和交互特征。
解决方案
即使有些人选择手动创建多项式和交互特征,scikit-learn 提供了一个内置方法:
# Load libraries
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
# Create feature matrix
features = np.array([[2, 3],
[2, 3],
[2, 3]])
# Create PolynomialFeatures object
polynomial_interaction = PolynomialFeatures(degree=2, include_bias=False)
# Create polynomial features
polynomial_interaction.fit_transform(features)
array([[ 2., 3., 4., 6., 9.],
[ 2., 3., 4., 6., 9.],
[ 2., 3., 4., 6., 9.]])
参数degree
确定多项式的最大次数。例如,degree=2
将创建被提升到二次幂的新特征:
而degree=3
将创建被提升到二次和三次幂的新特征:
此外,默认情况下,PolynomialFeatures
包括交互特征:
我们可以通过将interaction_only
设置为True
来限制仅创建交互特征:
interaction = PolynomialFeatures(degree=2,
interaction_only=True, include_bias=False)
interaction.fit_transform(features)
array([[ 2., 3., 6.],
[ 2., 3., 6.],
[ 2., 3., 6.]])
讨论
当我们希望包括特征与目标之间存在非线性关系时,通常会创建多项式特征。例如,我们可能怀疑年龄对患重大医疗状况的概率的影响并非随时间恒定,而是随年龄增加而增加。我们可以通过生成该特征的高阶形式(,等)来编码这种非恒定效果。
此外,我们经常遇到一种情况,即一个特征的效果取决于另一个特征。一个简单的例子是,如果我们试图预测我们的咖啡是否甜,我们有两个特征:(1)咖啡是否被搅拌,以及(2)是否添加了糖。单独来看,每个特征都不能预测咖啡的甜度,但它们的效果组合起来却可以。也就是说,只有当咖啡既加了糖又被搅拌时,咖啡才会变甜。每个特征对目标(甜度)的影响取决于彼此之间的关系。我们可以通过包含一个交互特征,即两个个体特征的乘积来编码这种关系。
4.5 特征转换
问题
你希望对一个或多个特征进行自定义转换。
解决方案
在 scikit-learn 中,使用FunctionTransformer
将一个函数应用到一组特征上:
# Load libraries
import numpy as np
from sklearn.preprocessing import FunctionTransformer
# Create feature matrix
features = np.array([[2, 3],
[2, 3],
[2, 3]])
# Define a simple function
def add_ten(x: int) -> int:
return x + 10
# Create transformer
ten_transformer = FunctionTransformer(add_ten)
# Transform feature matrix
ten_transformer.transform(features)
array([[12, 13],
[12, 13],
[12, 13]])
我们可以使用apply
在 pandas 中创建相同的转换:
# Load library
import pandas as pd
# Create DataFrame
df = pd.DataFrame(features, columns=["feature_1", "feature_2"])
# Apply function
df.apply(add_ten)
feature_1 | feature_2 | |
---|---|---|
0 | 12 | 13 |
1 | 12 | 13 |
2 | 12 | 13 |
讨论
通常希望对一个或多个特征进行一些自定义转换。例如,我们可能想创建一个特征,其值是另一个特征的自然对数。我们可以通过创建一个函数,然后使用 scikit-learn 的FunctionTransformer
或 pandas 的apply
将其映射到特征来实现这一点。在解决方案中,我们创建了一个非常简单的函数add_ten
,它为每个输入加了 10,但我们完全可以定义一个复杂得多的函数。
4.6 检测异常值
问题
你希望识别极端观察结果。
解决方案
检测异常值很遗憾更像是一种艺术而不是一种科学。然而,一种常见的方法是假设数据呈正态分布,并基于该假设在数据周围“画”一个椭圆,将椭圆内的任何观察结果归类为内围值(标记为1
),将椭圆外的任何观察结果归类为异常值(标记为-1
):
# Load libraries
import numpy as np
from sklearn.covariance import EllipticEnvelope
from sklearn.datasets import make_blobs
# Create simulated data
features, _ = make_blobs(n_samples = 10,
n_features = 2,
centers = 1,
random_state = 1)
# Replace the first observation's values with extreme values
features[0,0] = 10000
features[0,1] = 10000
# Create detector
outlier_detector = EllipticEnvelope(contamination=.1)
# Fit detector
outlier_detector.fit(features)
# Predict outliers
outlier_detector.predict(features)
array([-1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
在这些数组中,值为-1 表示异常值,而值为 1 表示内围值。这种方法的一个主要局限性是需要指定一个contamination
参数,它是异常值观察值的比例,这是我们不知道的值。将contamination
视为我们对数据清洁程度的估计。如果我们预计数据中有很少的异常值,我们可以将contamination
设置为较小的值。但是,如果我们认为数据可能有异常值,我们可以将其设置为较高的值。
我们可以不将观察结果作为一个整体来看待,而是可以查看单个特征,并使用四分位距(IQR)来识别这些特征中的极端值:
# Create one feature
feature = features[:,0]
# Create a function to return index of outliers
def indicies_of_outliers(x: int) -> np.array(int):
q1, q3 = np.percentile(x, [25, 75])
iqr = q3 - q1
lower_bound = q1 - (iqr * 1.5)
upper_bound = q3 + (iqr * 1.5)
return np.where((x > upper_bound) | (x < lower_bound))
# Run function
indicies_of_outliers(feature)
(array([0]),)
IQR 是一组数据的第一和第三四分位数之间的差异。您可以将 IQR 视为数据的主要集中区域的扩展,而异常值是远离数据主要集中区域的观察结果。异常值通常定义为第一四分位数的 1.5 倍 IQR 小于或第三四分位数的 1.5 倍 IQR 大于的任何值。
讨论
没有单一的最佳技术来检测异常值。相反,我们有一系列技术,各有优缺点。我们最好的策略通常是尝试多种技术(例如,EllipticEnvelope
和基于 IQR 的检测)并综合查看结果。
如果可能的话,我们应该查看我们检测到的异常值,并尝试理解它们。例如,如果我们有一个房屋数据集,其中一个特征是房间数量,那么房间数量为 100 的异常值是否真的是一座房子,还是实际上是一个被错误分类的酒店?
参见
4.7 处理异常值
问题
您的数据中存在异常值,您希望识别并减少其对数据分布的影响。
解决方案
通常我们可以采用三种策略来处理异常值。首先,我们可以放弃它们:
# Load library
import pandas as pd
# Create DataFrame
houses = pd.DataFrame()
houses['Price'] = [534433, 392333, 293222, 4322032]
houses['Bathrooms'] = [2, 3.5, 2, 116]
houses['Square_Feet'] = [1500, 2500, 1500, 48000]
# Filter observations
houses[houses['Bathrooms'] < 20]
价格 | 浴室 | 平方英尺 | |
---|---|---|---|
0 | 534433 | 2.0 | 1500 |
1 | 392333 | 3.5 | 2500 |
2 | 293222 | 2.0 | 1500 |
其次,我们可以将它们标记为异常值,并将“异常值”作为特征包含在内:
# Load library
import numpy as np
# Create feature based on boolean condition
houses["Outlier"] = np.where(houses["Bathrooms"] < 20, 0, 1)
# Show data
houses
价格 | 浴室 | 平方英尺 | 异常值 | |
---|---|---|---|---|
0 | 534433 | 2.0 | 1500 | 0 |
1 | 392333 | 3.5 | 2500 | 0 |
2 | 293222 | 2.0 | 1500 | 0 |
3 | 4322032 | 116.0 | 48000 | 1 |
最后,我们可以转换特征以减轻异常值的影响:
# Log feature
houses["Log_Of_Square_Feet"] = [np.log(x) for x in houses["Square_Feet"]]
# Show data
houses
价格 | 浴室 | 平方英尺 | 异常值 | 平方英尺的对数 | |
---|---|---|---|---|---|
0 | 534433 | 2.0 | 1500 | 0 | 7.313220 |
1 | 392333 | 3.5 | 2500 | 0 | 7.824046 |
2 | 293222 | 2.0 | 1500 | 0 | 7.313220 |
3 | 4322032 | 116.0 | 48000 | 1 | 10.778956 |
讨论
类似于检测异常值,处理它们没有硬性规则。我们处理它们应该基于两个方面。首先,我们应该考虑它们为何成为异常值。如果我们认为它们是数据中的错误,比如来自损坏传感器或错误编码的值,那么我们可能会删除该观测值或将异常值替换为NaN
,因为我们不能信任这些值。然而,如果我们认为异常值是真实的极端值(例如,一个有 200 个浴室的豪宅),那么将它们标记为异常值或转换它们的值更为合适。
其次,我们处理异常值的方式应该基于我们在机器学习中的目标。例如,如果我们想根据房屋特征预测房价,我们可能合理地假设拥有超过 100 个浴室的豪宅的价格受到不同动态的驱动,而不是普通家庭住宅。此外,如果我们正在训练一个在线住房贷款网站应用程序的模型,我们可能会假设我们的潜在用户不包括寻求购买豪宅的亿万富翁。
那么如果我们有异常值应该怎么办?考虑它们为何成为异常值,设定数据的最终目标,最重要的是记住,不处理异常值本身也是一种带有影响的决策。
另外一点:如果存在异常值,标准化可能不合适,因为异常值可能会严重影响均值和方差。在这种情况下,应该使用对异常值更具鲁棒性的重新缩放方法,比如RobustScaler
。
参见
4.8 特征离散化
问题
您有一个数值特征,并希望将其分割成离散的箱子。
解决方案
根据数据分割方式的不同,我们可以使用两种技术。首先,我们可以根据某个阈值对特征进行二值化:
# Load libraries
import numpy as np
from sklearn.preprocessing import Binarizer
# Create feature
age = np.array([[6],
[12],
[20],
[36],
[65]])
# Create binarizer
binarizer = Binarizer(threshold=18)
# Transform feature
binarizer.fit_transform(age)
array([[0],
[0],
[1],
[1],
[1]])
其次,我们可以根据多个阈值分割数值特征:
# Bin feature
np.digitize(age, bins=[20,30,64])
array([[0],
[0],
[1],
[2],
[3]])
注意,bins
参数的参数表示每个箱的左边缘。例如,20
参数不包括值为 20 的元素,只包括比 20 小的两个值。我们可以通过将参数 right
设置为 True
来切换这种行为:
# Bin feature
np.digitize(age, bins=[20,30,64], right=True)
array([[0],
[0],
[0],
[2],
[3]])
讨论
当我们有理由认为数值特征应该表现得更像分类特征时,离散化可以是一种有效的策略。例如,我们可能认为 19 岁和 20 岁的人的消费习惯几乎没有什么差异,但 20 岁和 21 岁之间存在显著差异(美国的法定饮酒年龄)。在这种情况下,将数据中的个体分为可以饮酒和不能饮酒的人可能是有用的。同样,在其他情况下,将数据离散化为三个或更多的箱子可能是有用的。
在解决方案中,我们看到了两种离散化的方法——scikit-learn 的Binarizer
用于两个区间和 NumPy 的digitize
用于三个或更多的区间——然而,我们也可以像使用Binarizer
那样使用digitize
来对功能进行二值化,只需指定一个阈值:
# Bin feature
np.digitize(age, bins=[18])
array([[0],
[0],
[1],
[1],
[1]])
另请参阅
4.9 使用聚类对观测进行分组
问题
您希望将观测聚类,以便将相似的观测分组在一起。
解决方案
如果您知道您有k个组,您可以使用 k 均值聚类来将相似的观测分组,并输出一个新的特征,其中包含每个观测的组成员资格:
# Load libraries
import pandas as pd
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
# Make simulated feature matrix
features, _ = make_blobs(n_samples = 50,
n_features = 2,
centers = 3,
random_state = 1)
# Create DataFrame
dataframe = pd.DataFrame(features, columns=["feature_1", "feature_2"])
# Make k-means clusterer
clusterer = KMeans(3, random_state=0)
# Fit clusterer
clusterer.fit(features)
# Predict values
dataframe["group"] = clusterer.predict(features)
# View first few observations
dataframe.head(5)
功能 _1 | 功能 _2 | 组 | |
---|---|---|---|
0 | –9.877554 | –3.336145 | 0 |
1 | –7.287210 | –8.353986 | 2 |
2 | –6.943061 | –7.023744 | 2 |
3 | –7.440167 | –8.791959 | 2 |
4 | –6.641388 | –8.075888 | 2 |
讨论
我们稍微超前一点,并且将在本书的后面更深入地讨论聚类算法。但是,我想指出,我们可以将聚类用作预处理步骤。具体来说,我们使用无监督学习算法(如 k 均值)将观测分成组。结果是一个分类特征,具有相似观测的成员属于同一组。
如果您没有理解所有这些,不要担心:只需将聚类可用于预处理的想法存档。如果您真的等不及,现在就可以翻到第十九章。
4.10 删除具有缺失值的观测
问题
您需要删除包含缺失值的观测。
解决方案
使用 NumPy 的巧妙一行代码轻松删除具有缺失值的观测:
# Load library
import numpy as np
# Create feature matrix
features = np.array([[1.1, 11.1],
[2.2, 22.2],
[3.3, 33.3],
[4.4, 44.4],
[np.nan, 55]])
# Keep only observations that are not (denoted by ~) missing
features[~np.isnan(features).any(axis=1)]
array([[ 1.1, 11.1],
[ 2.2, 22.2],
[ 3.3, 33.3],
[ 4.4, 44.4]])
或者,我们可以使用 pandas 删除缺失的观测:
# Load library
import pandas as pd
# Load data
dataframe = pd.DataFrame(features, columns=["feature_1", "feature_2"])
# Remove observations with missing values
dataframe.dropna()
功能 _1 | 功能 _2 | |
---|---|---|
0 | 1.1 | 11.1 |
1 | 2.2 | 22.2 |
2 | 3.3 | 33.3 |
3 | 4.4 | 44.4 |
讨论
大多数机器学习算法无法处理目标和特征数组中的任何缺失值。因此,我们不能忽略数据中的缺失值,必须在预处理过程中解决这个问题。
最简单的解决方案是删除包含一个或多个缺失值的每个观测,可以使用 NumPy 或 pandas 快速轻松地完成此任务。
也就是说,我们应该非常不情愿地删除具有缺失值的观测。删除它们是核心选项,因为我们的算法失去了观测的非缺失值中包含的信息。
同样重要的是,根据缺失值的原因,删除观测可能会向我们的数据引入偏差。有三种类型的缺失数据:
完全随机缺失(MCAR)
缺失值出现的概率与一切无关。例如,调查对象在回答问题之前掷骰子:如果她掷出六点,她会跳过那个问题。
随机缺失(MAR)
值缺失的概率并非完全随机,而是依赖于其他特征捕获的信息。例如,一项调查询问性别身份和年薪,女性更有可能跳过薪水问题;然而,她们的未响应仅依赖于我们在性别身份特征中捕获的信息。
缺失非随机(MNAR)
值缺失的概率并非随机,而是依赖于我们特征未捕获的信息。例如,一项调查询问年薪,女性更有可能跳过薪水问题,而我们的数据中没有性别身份特征。
如果数据是 MCAR 或 MAR,有时可以接受删除观测值。但是,如果值是 MNAR,缺失本身就是信息。删除 MNAR 观测值可能会在数据中引入偏差,因为我们正在删除由某些未观察到的系统效应产生的观测值。
另请参阅
4.11 填补缺失值
问题
您的数据中存在缺失值,并希望通过通用方法或预测来填补它们。
解决方案
您可以使用 k 最近邻(KNN)或 scikit-learn 的SimpleImputer
类来填补缺失值。如果数据量较小,请使用 KNN 进行预测和填补缺失值:
# Load libraries
import numpy as np
from sklearn.impute import KNNImputer
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
# Make a simulated feature matrix
features, _ = make_blobs(n_samples = 1000,
n_features = 2,
random_state = 1)
# Standardize the features
scaler = StandardScaler()
standardized_features = scaler.fit_transform(features)
# Replace the first feature's first value with a missing value
true_value = standardized_features[0,0]
standardized_features[0,0] = np.nan
# Predict the missing values in the feature matrix
knn_imputer = KNNImputer(n_neighbors=5)
features_knn_imputed = knn_imputer.fit_transform(standardized_features)
# Compare true and imputed values
print("True Value:", true_value)
print("Imputed Value:", features_knn_imputed[0,0])
True Value: 0.8730186114
Imputed Value: 1.09553327131
或者,我们可以使用 scikit-learn 的imputer
模块中的SimpleImputer
类,将缺失值用特征的均值、中位数或最频繁的值填充。然而,通常情况下,与 KNN 相比,我们通常会获得更差的结果:
# Load libraries
import numpy as np
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
# Make a simulated feature matrix
features, _ = make_blobs(n_samples = 1000,
n_features = 2,
random_state = 1)
# Standardize the features
scaler = StandardScaler()
standardized_features = scaler.fit_transform(features)
# Replace the first feature's first value with a missing value
true_value = standardized_features[0,0]
standardized_features[0,0] = np.nan
# Create imputer using the "mean" strategy
mean_imputer = SimpleImputer(strategy="mean")
# Impute values
features_mean_imputed = mean_imputer.fit_transform(features)
# Compare true and imputed values
print("True Value:", true_value)
print("Imputed Value:", features_mean_imputed[0,0])
True Value: 0.8730186114
Imputed Value: -3.05837272461
讨论
替换缺失数据的两种主要策略都有各自的优势和劣势。首先,我们可以使用机器学习来预测缺失数据的值。为此,我们将带有缺失值的特征视为目标向量,并使用其余子集特征来预测缺失值。虽然我们可以使用各种机器学习算法来填补值,但一个流行的选择是 KNN。在第十五章深入讨论了 KNN,简而言之,该算法使用k个最近的观测值(根据某个距离度量)来预测缺失值。在我们的解决方案中,我们使用了五个最接近的观测值来预测缺失值。
KNN 的缺点在于为了知道哪些观测值最接近缺失值,需要计算缺失值与每个观测值之间的距离。在较小的数据集中这是合理的,但是如果数据集有数百万个观测值,则很快会变得问题重重。在这种情况下,近似最近邻(ANN)是一个更可行的方法。我们将在第 15.5 节讨论 ANN。
与 KNN 相比,一种可替代且更可扩展的策略是用平均值、中位数或众数填补数值数据的缺失值。例如,在我们的解决方案中,我们使用 scikit-learn 将缺失值填充为特征的均值。填充的值通常不如我们使用 KNN 时接近真实值,但我们可以更轻松地将均值填充应用到包含数百万观察值的数据中。
如果我们使用填充,创建一个二进制特征指示观察是否包含填充值是一个好主意。
另请参阅
第五章:处理分类数据
5.0 介绍
通常有用的是,我们不仅仅用数量来衡量物体,而是用某种质量来衡量。我们经常用类别如性别、颜色或汽车品牌来表示定性信息。然而,并非所有分类数据都相同。没有内在排序的类别集称为名义。名义类别的例子包括:
-
蓝色,红色,绿色
-
男,女
-
香蕉,草莓,苹果
相比之下,当一组类别具有一些自然顺序时,我们称之为序数。例如:
-
低,中,高
-
年轻,年老
-
同意,中立,不同意
此外,分类信息通常以向量或字符串列(例如"Maine"
、"Texas"
、"Delaware"
)的形式表示在数据中。问题在于,大多数机器学习算法要求输入为数值。
k 最近邻算法是需要数值数据的一个例子。算法中的一步是计算观测之间的距离,通常使用欧氏距离:
其中和是两个观测值,下标表示观测的第个特征的值。然而,如果的值是一个字符串(例如"Texas"
),显然是无法进行距离计算的。我们需要将字符串转换为某种数值格式,以便可以将其输入到欧氏距离方程中。我们的目标是以一种能够正确捕捉类别信息(序数性,类别之间的相对间隔等)的方式转换数据。在本章中,我们将涵盖使这种转换以及克服处理分类数据时经常遇到的其他挑战的技术。
5.1 编码名义分类特征
问题
您有一个没有内在排序的名义类别特征(例如苹果,梨,香蕉),并且希望将该特征编码为数值。
解决方案
使用 scikit-learn 的LabelBinarizer
对特征进行独热编码:
# Import libraries
import numpy as np
from sklearn.preprocessing import LabelBinarizer, MultiLabelBinarizer
# Create feature
feature = np.array([["Texas"],
["California"],
["Texas"],
["Delaware"],
["Texas"]])
# Create one-hot encoder
one_hot = LabelBinarizer()
# One-hot encode feature
one_hot.fit_transform(feature)
array([[0, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 0, 1]])
我们可以使用classes_
属性来输出类别:
# View feature classes
one_hot.classes_
array(['California', 'Delaware', 'Texas'],
dtype='<U10')
如果我们想要反向进行独热编码,我们可以使用inverse_transform
:
# Reverse one-hot encoding
one_hot.inverse_transform(one_hot.transform(feature))
array(['Texas', 'California', 'Texas', 'Delaware', 'Texas'],
dtype='<U10')
我们甚至可以使用 pandas 来进行独热编码:
# Import library
import pandas as pd
# Create dummy variables from feature
pd.get_dummies(feature[:,0])
加利福尼亚 | 特拉华州 | 德克萨斯州 | |
---|---|---|---|
0 | 0 | 0 | 1 |
1 | 1 | 0 | 0 |
2 | 0 | 0 | 1 |
3 | 0 | 1 | 0 |
4 | 0 | 0 | 1 |
scikit-learn 的一个有用特性是能够处理每个观测列表包含多个类别的情况:
# Create multiclass feature
multiclass_feature = [("Texas", "Florida"),
("California", "Alabama"),
("Texas", "Florida"),
("Delaware", "Florida"),
("Texas", "Alabama")]
# Create multiclass one-hot encoder
one_hot_multiclass = MultiLabelBinarizer()
# One-hot encode multiclass feature
one_hot_multiclass.fit_transform(multiclass_feature)
array([[0, 0, 0, 1, 1],
[1, 1, 0, 0, 0],
[0, 0, 0, 1, 1],
[0, 0, 1, 1, 0],
[1, 0, 0, 0, 1]])
再次,我们可以使用classes_
方法查看类别:
# View classes
one_hot_multiclass.classes_
array(['Alabama', 'California', 'Delaware', 'Florida', 'Texas'], dtype=object)
讨论
我们可能认为正确的策略是为每个类分配一个数值(例如,Texas = 1,California = 2)。然而,当我们的类没有内在的顺序(例如,Texas 不是比 California “更少”),我们的数值值误创建了一个不存在的排序。
适当的策略是为原始特征的每个类创建一个二进制特征。在机器学习文献中通常称为 独热编码,而在统计和研究文献中称为 虚拟化。我们解决方案的特征是一个包含三个类(即 Texas、California 和 Delaware)的向量。在独热编码中,每个类都成为其自己的特征,当类出现时为 1,否则为 0。因为我们的特征有三个类,独热编码返回了三个二进制特征(每个类一个)。通过使用独热编码,我们可以捕捉观察值在类中的成员身份,同时保持类缺乏任何层次结构的概念。
最后,经常建议在对一个特征进行独热编码后,删除结果矩阵中的一个独热编码特征,以避免线性相关性。
参见
5.2 编码序数分类特征
问题
您有一个序数分类特征(例如高、中、低),并且希望将其转换为数值。
解决方案
使用 pandas DataFrame 的 replace
方法将字符串标签转换为数值等价物:
# Load library
import pandas as pd
# Create features
dataframe = pd.DataFrame({"Score": ["Low", "Low", "Medium", "Medium", "High"]})
# Create mapper
scale_mapper = {"Low":1,
"Medium":2,
"High":3}
# Replace feature values with scale
dataframe["Score"].replace(scale_mapper)
0 1
1 1
2 2
3 2
4 3
Name: Score, dtype: int64
讨论
经常情况下,我们有一个具有某种自然顺序的类的特征。一个著名的例子是 Likert 量表:
-
强烈同意
-
同意
-
中立
-
不同意
-
强烈不同意
在将特征编码用于机器学习时,我们需要将序数类转换为保持排序概念的数值。最常见的方法是创建一个将类的字符串标签映射到数字的字典,然后将该映射应用于特征。
根据我们对序数类的先前信息,选择数值值是很重要的。在我们的解决方案中,high
比 low
大三倍。在许多情况下这是可以接受的,但如果假设的类之间间隔不均等,这种方法可能失效:
dataframe = pd.DataFrame({"Score": ["Low",
"Low",
"Medium",
"Medium",
"High",
"Barely More Than Medium"]})
scale_mapper = {"Low":1,
"Medium":2,
"Barely More Than Medium":3,
"High":4}
dataframe["Score"].replace(scale_mapper)
0 1
1 1
2 2
3 2
4 4
5 3
Name: Score, dtype: int64
在此示例中,Low
和 Medium
之间的距离与 Medium
和 Barely More Than Medium
之间的距离相同,这几乎肯定不准确。最佳方法是在映射到类的数值值时要注意:
scale_mapper = {"Low":1,
"Medium":2,
"Barely More Than Medium":2.1,
"High":3}
dataframe["Score"].replace(scale_mapper)
0 1.0
1 1.0
2 2.0
3 2.0
4 3.0
5 2.1
Name: Score, dtype: float64
5.3 编码特征字典
问题
您有一个字典,并希望将其转换为特征矩阵。
解决方案
使用 DictVectorizer
:
# Import library
from sklearn.feature_extraction import DictVectorizer
# Create dictionary
data_dict = [{"Red": 2, "Blue": 4},
{"Red": 4, "Blue": 3},
{"Red": 1, "Yellow": 2},
{"Red": 2, "Yellow": 2}]
# Create dictionary vectorizer
dictvectorizer = DictVectorizer(sparse=False)
# Convert dictionary to feature matrix
features = dictvectorizer.fit_transform(data_dict)
# View feature matrix
features
array([[ 4., 2., 0.],
[ 3., 4., 0.],
[ 0., 1., 2.],
[ 0., 2., 2.]])
默认情况下,DictVectorizer
输出一个仅存储值非 0 的稀疏矩阵。当我们遇到大规模矩阵(通常在自然语言处理中)并希望最小化内存需求时,这非常有帮助。我们可以使用sparse=False
来强制DictVectorizer
输出一个密集矩阵。
我们可以使用get_feature_names
方法获取每个生成特征的名称:
# Get feature names
feature_names = dictvectorizer.get_feature_names()
# View feature names
feature_names
['Blue', 'Red', 'Yellow']
虽然不必要,为了说明我们可以创建一个 pandas DataFrame 来更好地查看输出:
# Import library
import pandas as pd
# Create dataframe from features
pd.DataFrame(features, columns=feature_names)
蓝色 | 红色 | 黄色 | |
---|---|---|---|
0 | 4.0 | 2.0 | 0.0 |
1 | 3.0 | 4.0 | 0.0 |
2 | 0.0 | 1.0 | 2.0 |
3 | 0.0 | 2.0 | 2.0 |
讨论
字典是许多编程语言中常用的数据结构;然而,机器学习算法期望数据以矩阵的形式存在。我们可以使用 scikit-learn 的DictVectorizer
来实现这一点。
这是自然语言处理时常见的情况。例如,我们可能有一系列文档,每个文档都有一个字典,其中包含每个单词在文档中出现的次数。使用DictVectorizer
,我们可以轻松创建一个特征矩阵,其中每个特征是每个文档中单词出现的次数:
# Create word count dictionaries for four documents
doc_1_word_count = {"Red": 2, "Blue": 4}
doc_2_word_count = {"Red": 4, "Blue": 3}
doc_3_word_count = {"Red": 1, "Yellow": 2}
doc_4_word_count = {"Red": 2, "Yellow": 2}
# Create list
doc_word_counts = [doc_1_word_count,
doc_2_word_count,
doc_3_word_count,
doc_4_word_count]
# Convert list of word count dictionaries into feature matrix
dictvectorizer.fit_transform(doc_word_counts)
array([[ 4., 2., 0.],
[ 3., 4., 0.],
[ 0., 1., 2.],
[ 0., 2., 2.]])
在我们的示例中,只有三个唯一的单词(红色
,黄色
,蓝色
),所以我们的矩阵中只有三个特征;然而,如果每个文档实际上是大学图书馆中的一本书,我们的特征矩阵将非常庞大(然后我们将希望将sparse
设置为True
)。
参见
5.4 填充缺失的类值
问题
您有一个包含缺失值的分类特征,您希望用预测值替换它。
解决方案
理想的解决方案是训练一个机器学习分类器算法来预测缺失值,通常是 k 近邻(KNN)分类器:
# Load libraries
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
# Create feature matrix with categorical feature
X = np.array([[0, 2.10, 1.45],
[1, 1.18, 1.33],
[0, 1.22, 1.27],
[1, -0.21, -1.19]])
# Create feature matrix with missing values in the categorical feature
X_with_nan = np.array([[np.nan, 0.87, 1.31],
[np.nan, -0.67, -0.22]])
# Train KNN learner
clf = KNeighborsClassifier(3, weights='distance')
trained_model = clf.fit(X[:,1:], X[:,0])
# Predict class of missing values
imputed_values = trained_model.predict(X_with_nan[:,1:])
# Join column of predicted class with their other features
X_with_imputed = np.hstack((imputed_values.reshape(-1,1), X_with_nan[:,1:]))
# Join two feature matrices
np.vstack((X_with_imputed, X))
array([[ 0\. , 0.87, 1.31],
[ 1\. , -0.67, -0.22],
[ 0\. , 2.1 , 1.45],
[ 1\. , 1.18, 1.33],
[ 0\. , 1.22, 1.27],
[ 1\. , -0.21, -1.19]])
另一种解决方案是使用特征的最频繁值填充缺失值:
from sklearn.impute import SimpleImputer
# Join the two feature matrices
X_complete = np.vstack((X_with_nan, X))
imputer = SimpleImputer(strategy='most_frequent')
imputer.fit_transform(X_complete)
array([[ 0\. , 0.87, 1.31],
[ 0\. , -0.67, -0.22],
[ 0\. , 2.1 , 1.45],
[ 1\. , 1.18, 1.33],
[ 0\. , 1.22, 1.27],
[ 1\. , -0.21, -1.19]])
讨论
当分类特征中存在缺失值时,我们最好的解决方案是打开我们的机器学习算法工具箱,预测缺失观测值的值。我们可以通过将具有缺失值的特征视为目标向量,其他特征视为特征矩阵来实现此目标。常用的算法之一是 KNN(在第十五章中详细讨论),它将缺失值分配给k个最近观测中最频繁出现的类别。
或者,我们可以使用特征的最频繁类别填充缺失值,甚至丢弃具有缺失值的观测。虽然不如 KNN 复杂,但这些选项在处理大数据时更具可扩展性。无论哪种情况,都建议包含一个二元特征,指示哪些观测包含了填充值。
参见
5.5 处理不平衡类别
问题
如果您有一个具有高度不平衡类别的目标向量,并且希望进行调整以处理类别不平衡。
解决方案
收集更多数据。如果不可能,请更改用于评估模型的指标。如果这样做不起作用,请考虑使用模型的内置类权重参数(如果可用),下采样或上采样。我们将在后面的章节中介绍评估指标,因此现在让我们专注于类权重参数、下采样和上采样。
为了演示我们的解决方案,我们需要创建一些具有不平衡类别的数据。Fisher 的鸢尾花数据集包含三个平衡类别的 50 个观察,每个类别表示花的物种(Iris setosa、Iris virginica 和 Iris versicolor)。为了使数据集不平衡,我们移除了 50 个 Iris setosa 观察中的 40 个,并合并了 Iris virginica 和 Iris versicolor 类别。最终结果是一个二元目标向量,指示观察是否为 Iris setosa 花。结果是 10 个 Iris setosa(类别 0)的观察和 100 个非 Iris setosa(类别 1)的观察:
# Load libraries
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
# Load iris data
iris = load_iris()
# Create feature matrix
features = iris.data
# Create target vector
target = iris.target
# Remove first 40 observations
features = features[40:,:]
target = target[40:]
# Create binary target vector indicating if class 0
target = np.where((target == 0), 0, 1)
# Look at the imbalanced target vector
target
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
scikit-learn 中的许多算法在训练期间提供一个参数来加权类别,以抵消其不平衡的效果。虽然我们尚未涵盖它,RandomForestClassifier
是一种流行的分类算法,并包含一个 class_weight
参数;在 14.4 节 中了解更多关于 RandomForestClassifier
的信息。您可以传递一个参数显式指定所需的类权重:
# Create weights
weights = {0: 0.9, 1: 0.1}
# Create random forest classifier with weights
RandomForestClassifier(class_weight=weights)
RandomForestClassifier(class_weight={0: 0.9, 1: 0.1})
或者您可以传递 balanced
,它会自动创建与类别频率成反比的权重:
# Train a random forest with balanced class weights
RandomForestClassifier(class_weight="balanced")
RandomForestClassifier(class_weight='balanced')
或者,我们可以对多数类进行下采样或者对少数类进行上采样。在 下采样 中,我们从多数类中无放回随机抽样(即观察次数较多的类别)以创建一个新的观察子集,其大小等于少数类。例如,如果少数类有 10 个观察,我们将从多数类中随机选择 10 个观察,然后使用这 20 个观察作为我们的数据。在这里,我们正是利用我们不平衡的鸢尾花数据做到这一点:
# Indicies of each class's observations
i_class0 = np.where(target == 0)[0]
i_class1 = np.where(target == 1)[0]
# Number of observations in each class
n_class0 = len(i_class0)
n_class1 = len(i_class1)
# For every observation of class 0, randomly sample
# from class 1 without replacement
i_class1_downsampled = np.random.choice(i_class1, size=n_class0, replace=False)
# Join together class 0's target vector with the
# downsampled class 1's target vector
np.hstack((target[i_class0], target[i_class1_downsampled]))
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
# Join together class 0's feature matrix with the
# downsampled class 1's feature matrix
np.vstack((features[i_class0,:], features[i_class1_downsampled,:]))[0:5]
array([[ 5\. , 3.5, 1.3, 0.3],
[ 4.5, 2.3, 1.3, 0.3],
[ 4.4, 3.2, 1.3, 0.2],
[ 5\. , 3.5, 1.6, 0.6],
[ 5.1, 3.8, 1.9, 0.4]])
我们的另一种选择是对少数类进行上采样。在 上采样 中,对于多数类中的每个观察,我们从少数类中随机选择一个观察,可以重复选择。结果是来自少数和多数类的相同数量的观察。上采样的实现非常类似于下采样,只是反向操作:
# For every observation in class 1, randomly sample from class 0 with
# replacement
i_class0_upsampled = np.random.choice(i_class0, size=n_class1, replace=True)
# Join together class 0's upsampled target vector with class 1's target vector
np.concatenate((target[i_class0_upsampled], target[i_class1]))
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
# Join together class 0's upsampled feature matrix with class 1's feature matrix
np.vstack((features[i_class0_upsampled,:], features[i_class1,:]))[0:5]
array([[ 5\. , 3.5, 1.6, 0.6],
[ 5\. , 3.5, 1.6, 0.6],
[ 5\. , 3.3, 1.4, 0.2],
[ 4.5, 2.3, 1.3, 0.3],
[ 4.8, 3\. , 1.4, 0.3]])
讨论
在现实世界中,不平衡的类别随处可见—大多数访问者不会点击购买按钮,而许多类型的癌症又是相当罕见的。因此,在机器学习中处理不平衡的类别是一项常见的活动。
我们最好的策略就是简单地收集更多的观察数据—尤其是来自少数类的观察数据。然而,通常情况下这并不可能,所以我们必须求助于其他选择。
第二种策略是使用更适合于不平衡类别的模型评估指标。准确率通常被用作评估模型性能的指标,但在存在不平衡类别的情况下,准确率可能并不合适。例如,如果只有 0.5%的观察数据属于某种罕见的癌症,那么即使是一个简单的模型预测没有人有癌症,准确率也会达到 99.5%。显然,这并不理想。我们将在后面的章节中讨论一些更好的指标,如混淆矩阵、精确度、召回率、F[1]分数和 ROC 曲线。
第三种策略是使用一些模型实现中包含的类别加权参数。这使得算法能够调整不平衡的类别。幸运的是,许多 scikit-learn 分类器都有一个class_weight
参数,这使得它成为一个不错的选择。
第四和第五种策略是相关的:下采样和上采样。在下采样中,我们创建一个与少数类相同大小的多数类的随机子集。在上采样中,我们从少数类中重复有放回地抽样,使其大小与多数类相等。选择使用下采样还是上采样是与上下文相关的决定,通常我们应该尝试两种方法,看看哪一种效果更好。
第六章:处理文本
6.0 引言
非结构化的文本数据,如书籍内容或推文,既是最有趣的特征来源之一,也是最复杂的处理之一。在本章中,我们将介绍将文本转换为信息丰富特征的策略,并使用一些出色的特征(称为嵌入),这些特征在涉及自然语言处理(NLP)的任务中变得日益普遍。
这并不意味着这里涵盖的配方是全面的。整个学术学科都专注于处理文本等非结构化数据。在本章中,我们将涵盖一些常用的技术;掌握这些将为我们的预处理工具箱增添宝贵的工具。除了许多通用的文本处理配方外,我们还将演示如何导入和利用一些预训练的机器学习模型来生成更丰富的文本特征。
6.1 清理文本
问题
你有一些非结构化文本数据,想要完成一些基本的清理工作。
解决方案
在下面的例子中,我们查看三本书的文本,并通过 Python 的核心字符串操作,特别是strip
、replace
和split
,对其进行清理:
# Create text
text_data = [" Interrobang. By Aishwarya Henriette ",
"Parking And Going. By Karl Gautier",
" Today Is The night. By Jarek Prakash "]
# Strip whitespaces
strip_whitespace = [string.strip() for string in text_data]
# Show text
strip_whitespace
['Interrobang. By Aishwarya Henriette',
'Parking And Going. By Karl Gautier',
'Today Is The night. By Jarek Prakash']
# Remove periods
remove_periods = [string.replace(".", "") for string in strip_whitespace]
# Show text
remove_periods
['Interrobang By Aishwarya Henriette',
'Parking And Going By Karl Gautier',
'Today Is The night By Jarek Prakash']
我们还创建并应用了一个自定义转换函数:
# Create function
def capitalizer(string: str) -> str:
return string.upper()
# Apply function
[capitalizer(string) for string in remove_periods]
['INTERROBANG BY AISHWARYA HENRIETTE',
'PARKING AND GOING BY KARL GAUTIER',
'TODAY IS THE NIGHT BY JAREK PRAKASH']
最后,我们可以使用正则表达式进行强大的字符串操作:
# Import library
import re
# Create function
def replace_letters_with_X(string: str) -> str:
return re.sub(r"[a-zA-Z]", "X", string)
# Apply function
[replace_letters_with_X(string) for string in remove_periods]
['XXXXXXXXXXX XX XXXXXXXXX XXXXXXXXX',
'XXXXXXX XXX XXXXX XX XXXX XXXXXXX',
'XXXXX XX XXX XXXXX XX XXXXX XXXXXXX']
讨论
一些文本数据在用于构建特征或在输入算法之前需要进行基本的清理。大多数基本的文本清理可以使用 Python 的标准字符串操作完成。在实际应用中,我们很可能会定义一个自定义的清理函数(例如capitalizer
),结合一些清理任务,并将其应用于文本数据。虽然清理字符串可能会删除一些信息,但它使数据更易于处理。字符串具有许多有用的固有方法用于清理和处理;一些额外的例子可以在这里找到:
# Define a string
s = "machine learning in python cookbook"
# Find the first index of the letter "n"
find_n = s.find("n")
# Whether or not the string starts with "m"
starts_with_m = s.startswith("m")
# Whether or not the string ends with "python"
ends_with_python = s.endswith("python")
# Is the string alphanumeric
is_alnum = s.isalnum()
# Is it composed of only alphabetical characters (not including spaces)
is_alpha = s.isalpha()
# Encode as utf-8
encode_as_utf8 = s.encode("utf-8")
# Decode the same utf-8
decode = encode_as_utf8.decode("utf-8")
print(
find_n,
starts_with_m,
ends_with_python,
is_alnum,
is_alpha,
encode_as_utf8,
decode,
sep = "|"
)
5|True|False|False|False|b'machine learning in python cookbook'|machine learning
in python cookbook
参见
6.2 解析和清理 HTML
问题
你有包含 HTML 元素的文本数据,并希望仅提取文本部分。
解决方案
使用 Beautiful Soup 广泛的选项集来解析和从 HTML 中提取:
# Load library
from bs4 import BeautifulSoup
# Create some HTML code
html = "<div class='full_name'>"\
"<span style='font-weight:bold'>Masego"\
"</span> Azra</div>"
# Parse html
soup = BeautifulSoup(html, "lxml")
# Find the div with the class "full_name", show text
soup.find("div", { "class" : "full_name" }).text
'Masego Azra'
讨论
尽管名字奇怪,Beautiful Soup 是一个功能强大的 Python 库,专门用于解析 HTML。通常 Beautiful Soup 用于实时网页抓取过程中处理 HTML,但我们同样可以使用它来提取静态 HTML 中嵌入的文本数据。Beautiful Soup 的全部操作远超出本书的范围,但即使是我们在解决方案中使用的方法,也展示了使用find()
方法可以轻松地解析 HTML 并从特定标签中提取信息。
参见
6.3 删除标点符号
问题
你有一项文本数据的特征,并希望去除标点符号。
解决方案
定义一个使用translate
和标点字符字典的函数:
# Load libraries
import unicodedata
import sys
# Create text
text_data = ['Hi!!!! I. Love. This. Song....',
'10000% Agree!!!! #LoveIT',
'Right?!?!']
# Create a dictionary of punctuation characters
punctuation = dict.fromkeys(
(i for i in range(sys.maxunicode)
if unicodedata.category(chr(i)).startswith('P')
),
None
)
# For each string, remove any punctuation characters
[string.translate(punctuation) for string in text_data]
['Hi I Love This Song', '10000 Agree LoveIT', 'Right']
讨论
Python 的 translate
方法因其速度而流行。在我们的解决方案中,首先我们创建了一个包含所有标点符号字符(按照 Unicode 标准)作为键和 None
作为值的字典 punctuation
。接下来,我们将字符串中所有在 punctuation
中的字符翻译为 None
,从而有效地删除它们。还有更可读的方法来删除标点,但这种有些“hacky”的解决方案具有比替代方案更快的优势。
需要意识到标点包含信息这一事实是很重要的(例如,“对吧?”与“对吧!”)。在需要手动创建特征时,删除标点可能是必要的恶;然而,如果标点很重要,我们应该确保考虑到这一点。根据我们试图完成的下游任务的不同,标点可能包含我们希望保留的重要信息(例如,使用“?”来分类文本是否包含问题)。
6.4 文本分词
问题
你有一段文本,希望将其分解成单独的单词。
解决方案
Python 的自然语言工具包(NLTK)具有强大的文本操作集,包括词分词:
# Load library
from nltk.tokenize import word_tokenize
# Create text
string = "The science of today is the technology of tomorrow"
# Tokenize words
word_tokenize(string)
['The', 'science', 'of', 'today', 'is', 'the', 'technology', 'of', 'tomorrow']
我们还可以将其分词成句子:
# Load library
from nltk.tokenize import sent_tokenize
# Create text
string = "The science of today is the technology of tomorrow. Tomorrow is today."
# Tokenize sentences
sent_tokenize(string)
['The science of today is the technology of tomorrow.', 'Tomorrow is today.']
讨论
分词,尤其是词分词,在清洗文本数据后是一项常见任务,因为它是将文本转换为我们将用来构建有用特征的数据的第一步。一些预训练的自然语言处理模型(如 Google 的 BERT)使用特定于模型的分词技术;然而,在从单词级别获取特征之前,词级分词仍然是一种相当常见的分词方法。
6.5 移除停用词
问题
给定标记化的文本数据,你希望移除极其常见的单词(例如,a、is、of、on),它们的信息价值很小。
解决方案
使用 NLTK 的 stopwords
:
# Load library
from nltk.corpus import stopwords
# You will have to download the set of stop words the first time
# import nltk
# nltk.download('stopwords')
# Create word tokens
tokenized_words = ['i',
'am',
'going',
'to',
'go',
'to',
'the',
'store',
'and',
'park']
# Load stop words
stop_words = stopwords.words('english')
# Remove stop words
[word for word in tokenized_words if word not in stop_words]
['going', 'go', 'store', 'park']
讨论
虽然“停用词”可以指任何我们希望在处理前移除的单词集,但通常这个术语指的是那些本身包含很少信息价值的极其常见的单词。是否选择移除停用词将取决于你的具体用例。NLTK 有一个常见停用词列表,我们可以用来查找并移除我们标记化的单词中的停用词:
# Show stop words
stop_words[:5]
['i', 'me', 'my', 'myself', 'we']
注意,NLTK 的 stopwords
假定标记化的单词都是小写的。
6.6 词干提取
问题
你有一些标记化的单词,并希望将它们转换为它们的根形式。
解决方案
使用 NLTK 的 PorterStemmer
:
# Load library
from nltk.stem.porter import PorterStemmer
# Create word tokens
tokenized_words = ['i', 'am', 'humbled', 'by', 'this', 'traditional', 'meeting']
# Create stemmer
porter = PorterStemmer()
# Apply stemmer
[porter.stem(word) for word in tokenized_words]
['i', 'am', 'humbl', 'by', 'thi', 'tradit', 'meet']
讨论
词干提取 通过识别和移除词缀(例如动名词),将单词减少到其词干,同时保持单词的根本含义。例如,“tradition” 和 “traditional” 都有 “tradit” 作为它们的词干,表明虽然它们是不同的词,但它们代表同一个一般概念。通过词干提取我们的文本数据,我们将其转换为不太可读但更接近其基本含义的形式,因此更适合跨观察进行比较。NLTK 的 PorterStemmer
实现了广泛使用的 Porter 词干提取算法,以移除或替换常见的后缀,生成词干。
参见
6.7 标记词性
问题
您拥有文本数据,并希望标记每个单词或字符的词性。
解决方案
使用 NLTK 的预训练词性标注器:
# Load libraries
from nltk import pos_tag
from nltk import word_tokenize
# Create text
text_data = "Chris loved outdoor running"
# Use pretrained part of speech tagger
text_tagged = pos_tag(word_tokenize(text_data))
# Show parts of speech
text_tagged
[('Chris', 'NNP'), ('loved', 'VBD'), ('outdoor', 'RP'), ('running', 'VBG')]
输出是一个包含单词和词性标签的元组列表。NLTK 使用宾树库的词性标签。宾树库的一些示例标签包括:
Tag | 词性 |
---|---|
NNP | 专有名词,单数 |
NN | 名词,单数或集合名词 |
RB | 副词 |
VBD | 动词,过去式 |
VBG | 动词,动名词或现在分词 |
JJ | 形容词 |
PRP | 人称代词 |
一旦文本被标记,我们可以使用标签找到特定的词性。例如,这里是所有的名词:
# Filter words
[word for word, tag in text_tagged if tag in ['NN','NNS','NNP','NNPS'] ]
['Chris']
更现实的情况可能是有数据,每个观察都包含一条推文,并且我们希望将这些句子转换为各个词性的特征(例如,如果存在专有名词,则为 1
,否则为 0
):
# Import libraries
from sklearn.preprocessing import MultiLabelBinarizer
# Create text
tweets = ["I am eating a burrito for breakfast",
"Political science is an amazing field",
"San Francisco is an awesome city"]
# Create list
tagged_tweets = []
# Tag each word and each tweet
for tweet in tweets:
tweet_tag = nltk.pos_tag(word_tokenize(tweet))
tagged_tweets.append([tag for word, tag in tweet_tag])
# Use one-hot encoding to convert the tags into features
one_hot_multi = MultiLabelBinarizer()
one_hot_multi.fit_transform(tagged_tweets)
array([[1, 1, 0, 1, 0, 1, 1, 1, 0],
[1, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 1]])
使用 classes_
,我们可以看到每个特征都是一个词性标签:
# Show feature names
one_hot_multi.classes_
array(['DT', 'IN', 'JJ', 'NN', 'NNP', 'PRP', 'VBG', 'VBP', 'VBZ'], dtype=object)
讨论
如果我们的文本是英语且不涉及专业主题(例如医学),最简单的解决方案是使用 NLTK 的预训练词性标注器。但是,如果 pos_tag
不太准确,NLTK 还为我们提供了训练自己标注器的能力。训练标注器的主要缺点是我们需要一个大型文本语料库,其中每个词的标签是已知的。构建这种标记语料库显然是劳动密集型的,可能是最后的选择。
参见
6.8 执行命名实体识别
问题
您希望在自由文本中执行命名实体识别(如“人物”,“州”等)。
解决方案
使用 spaCy 的默认命名实体识别管道和模型从文本中提取实体:
# Import libraries
import spacy
# Load the spaCy package and use it to parse the text
# make sure you have run "python -m spacy download en"
nlp = spacy.load("en_core_web_sm")
doc = nlp("Elon Musk offered to buy Twitter using $21B of his own money.")
# Print each entity
print(doc.ents)
# For each entity print the text and the entity label
for entity in doc.ents:
print(entity.text, entity.label_, sep=",")
(Elon Musk, Twitter, 21B)
Elon Musk, PERSON
Twitter, ORG
21B, MONEY
讨论
命名实体识别是从文本中识别特定实体的过程。像 spaCy 这样的工具提供预配置的管道,甚至是预训练或微调的机器学习模型,可以轻松识别这些实体。在本例中,我们使用 spaCy 识别文本中的人物(“Elon Musk”)、组织(“Twitter”)和金额(“21B”)。利用这些信息,我们可以从非结构化文本数据中提取结构化信息。这些信息随后可以用于下游机器学习模型或数据分析。
训练自定义命名实体识别模型超出了本示例的范围;但是,通常使用深度学习和其他自然语言处理技术来完成此任务。
另请参阅
6.9 将文本编码为词袋模型
问题
您有文本数据,并希望创建一组特征,指示观察文本中包含特定单词的次数。
解决方案
使用 scikit-learn 的CountVectorizer
:
# Load library
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
# Create text
text_data = np.array(['I love Brazil. Brazil!',
'Sweden is best',
'Germany beats both'])
# Create the bag of words feature matrix
count = CountVectorizer()
bag_of_words = count.fit_transform(text_data)
# Show feature matrix
bag_of_words
<3x8 sparse matrix of type '<class 'numpy.int64'>'
with 8 stored elements in Compressed Sparse Row format>
此输出是一个稀疏数组,在我们有大量文本时通常是必要的。但是,在我们的玩具示例中,我们可以使用toarray
查看每个观察结果的单词计数矩阵:
bag_of_words.toarray()
array([[0, 0, 0, 2, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 0]], dtype=int64)
我们可以使用get_feature_names
方法查看与每个特征关联的单词:
# Show feature names
count.get_feature_names_out()
array(['beats', 'best', 'both', 'brazil', 'germany', 'is', 'love',
'sweden'], dtype=object)
注意,I
从I love Brazil
中不被视为一个标记,因为默认的token_pattern
只考虑包含两个或更多字母数字字符的标记。
然而,这可能会令人困惑,为了明确起见,这里是特征矩阵的外观,其中单词作为列名(每行是一个观察结果):
beats | best | both | brazil | germany | is | love | sweden |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 2 | 0 | 0 | 1 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 |
1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
讨论
将文本转换为特征的最常见方法之一是使用词袋模型。词袋模型为文本数据中的每个唯一单词输出一个特征,每个特征包含在观察中出现的次数计数。例如,在我们的解决方案中,句子“I love Brazil. Brazil!”中,“brazil”特征的值为2
,因为单词brazil出现了两次。
我们解决方案中的文本数据故意很小。在现实世界中,文本数据的单个观察结果可能是整本书的内容!由于我们的词袋模型为数据中的每个唯一单词创建一个特征,因此生成的矩阵可能包含数千个特征。这意味着矩阵的大小有时可能会在内存中变得非常大。幸运的是,我们可以利用词袋特征矩阵的常见特性来减少我们需要存储的数据量。
大多数单词可能不会出现在大多数观察中,因此单词袋特征矩阵将主要包含值为 0 的值。我们称这些类型的矩阵为 稀疏。我们可以只存储非零值,然后假定所有其他值为 0,以节省内存,特别是在具有大型特征矩阵时。CountVectorizer
的一个好处是默认输出是稀疏矩阵。
CountVectorizer
配备了许多有用的参数,使得创建单词袋特征矩阵变得容易。首先,默认情况下,每个特征是一个单词,但这并不一定是情况。我们可以将每个特征设置为两个单词的组合(称为 2-gram)甚至三个单词(3-gram)。ngram_range
设置了我们的n-gram 的最小和最大大小。例如,(2,3)
将返回所有的 2-gram 和 3-gram。其次,我们可以使用 stop_words
轻松地去除低信息的填充词,可以使用内置列表或自定义列表。最后,我们可以使用 vocabulary
限制我们希望考虑的单词或短语列表。例如,我们可以仅为国家名称的出现创建一个单词袋特征矩阵:
# Create feature matrix with arguments
count_2gram = CountVectorizer(ngram_range=(1,2),
stop_words="english",
vocabulary=['brazil'])
bag = count_2gram.fit_transform(text_data)
# View feature matrix
bag.toarray()
array([[2],
[0],
[0]])
# View the 1-grams and 2-grams
count_2gram.vocabulary_
{'brazil': 0}
另请参阅
6.10 加权词重要性
问题
您希望一个单词袋,其中单词按其对观察的重要性加权。
解决方案
通过使用词频-逆文档频率()比较单词在文档(推文、电影评论、演讲文稿等)中的频率与单词在所有其他文档中的频率。scikit-learn 通过 TfidfVectorizer
轻松实现这一点:
# Load libraries
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
# Create text
text_data = np.array(['I love Brazil. Brazil!',
'Sweden is best',
'Germany beats both'])
# Create the tf-idf feature matrix
tfidf = TfidfVectorizer()
feature_matrix = tfidf.fit_transform(text_data)
# Show tf-idf feature matrix
feature_matrix
<3x8 sparse matrix of type '<class 'numpy.float64'>'
with 8 stored elements in Compressed Sparse Row format>
就像在食谱 6.9 中一样,输出是一个稀疏矩阵。然而,如果我们想将输出视为密集矩阵,我们可以使用 toarray
:
# Show tf-idf feature matrix as dense matrix
feature_matrix.toarray()
array([[ 0\. , 0\. , 0\. , 0.89442719, 0\. ,
0\. , 0.4472136 , 0\. ],
[ 0\. , 0.57735027, 0\. , 0\. , 0\. ,
0.57735027, 0\. , 0.57735027],
[ 0.57735027, 0\. , 0.57735027, 0\. , 0.57735027,
0\. , 0\. , 0\. ]])
vocabulary_
展示了每个特征的词汇:
# Show feature names
tfidf.vocabulary_
{'love': 6,
'brazil': 3,
'sweden': 7,
'is': 5,
'best': 1,
'germany': 4,
'beats': 0,
'both': 2}
讨论
单词在文档中出现的次数越多,该单词对该文档的重要性就越高。例如,如果单词 economy 经常出现,这表明文档可能与经济有关。我们称之为 词频 ()。
相反,如果一个词在许多文档中出现,它可能对任何单个文档的重要性较低。例如,如果某个文本数据中的每个文档都包含单词 after,那么它可能是一个不重要的词。我们称之为 文档频率 ()。
通过结合这两个统计量,我们可以为每个单词分配一个分数,代表该单词在文档中的重要性。具体来说,我们将 乘以文档频率的倒数 :
其中 是一个单词(术语), 是一个文档。关于如何计算 和 有许多不同的变体。在 scikit-learn 中, 简单地是单词在文档中出现的次数, 计算如下:
其中 是文档数量, 是术语 的文档频率(即术语出现的文档数量)。
默认情况下,scikit-learn 使用欧几里得范数(L2 范数)对 向量进行归一化。结果值越高,单词对文档的重要性越大。
另请参阅
6.11 使用文本向量计算搜索查询中的文本相似度
问题
您想要使用 向量来实现 Python 中的文本搜索功能。
解决方案
使用 scikit-learn 计算 向量之间的余弦相似度:
# Load libraries
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
# Create searchable text data
text_data = np.array(['I love Brazil. Brazil!',
'Sweden is best',
'Germany beats both'])
# Create the tf-idf feature matrix
tfidf = TfidfVectorizer()
feature_matrix = tfidf.fit_transform(text_data)
# Create a search query and transform it into a tf-idf vector
text = "Brazil is the best"
vector = tfidf.transform([text])
# Calculate the cosine similarities between the input vector and all other
vectors
cosine_similarities = linear_kernel(vector, feature_matrix).flatten()
# Get the index of the most relevent items in order
related_doc_indicies = cosine_similarities.argsort()[:-10:-1]
# Print the most similar texts to the search query along with the cosine
similarity
print([(text_data[i], cosine_similarities[i]) for i in related_doc_indicies])
[
(
'Sweden is best', 0.6666666666666666),
('I love Brazil. Brazil!', 0.5163977794943222),
('Germany beats both', 0.0
)
]
讨论
文本向量对于诸如搜索引擎之类的 NLP 用例非常有用。计算了一组句子或文档的 向量后,我们可以使用相同的 tfidf
对象来向量化未来的文本集。然后,我们可以计算输入向量与其他向量矩阵之间的余弦相似度,并按最相关的文档进行排序。
余弦相似度的取值范围为[0, 1.0],其中 0 表示最不相似,1 表示最相似。由于我们使用向量来计算向量之间的相似度,单词出现的频率也被考虑在内。然而,在一个小的语料库(文档集合)中,即使是“频繁”出现的词语也可能不频繁出现。在这个例子中,“瑞典是最好的”是最相关的文本,与我们的搜索查询“巴西是最好的”最相似。由于查询提到了巴西,我们可能期望“我爱巴西。巴西!”是最相关的;然而,由于“是”和“最好”,“瑞典是最好的”是最相似的。随着我们向语料库中添加的文档数量的增加,不重要的词语将被加权较少,对余弦相似度计算的影响也将减小。
参见
6.12 使用情感分析分类器
问题
您希望对一些文本的情感进行分类,以便作为特征或在下游数据分析中使用。
解决方案
使用transformers
库的情感分类器。
# Import libraries
from transformers import pipeline
# Create an NLP pipeline that runs sentiment analysis
classifier = pipeline("sentiment-analysis")
# Classify some text
# (this may download some data and models the first time you run it)
sentiment_1 = classifier("I hate machine learning! It's the absolute worst.")
sentiment_2 = classifier(
"Machine learning is the absolute"
"bees knees I love it so much!"
)
# Print sentiment output
print(sentiment_1, sentiment_2)
[
{
'label': 'NEGATIVE',
'score': 0.9998020529747009
}
]
[
{
'label': 'POSITIVE',
'score': 0.9990628957748413
}
]
讨论
transformers
库是一个极为流行的自然语言处理任务库,包含许多易于使用的 API,用于训练模型或使用预训练模型。我们将在第二十二章更详细地讨论 NLP 和这个库,但这个例子作为使用预训练分类器在您的机器学习流水线中生成特征、分类文本或分析非结构化数据的强大工具的高级介绍。
参见
第七章:处理日期和时间
7.0 引言
处理日期和时间(datetime),例如特定销售的时间或公共卫生统计的日期,在机器学习预处理中经常遇到。纵向数据(或时间序列数据)是重复收集同一变量的数据,随时间点变化。在本章中,我们将构建处理时间序列数据的策略工具箱,包括处理时区和创建滞后时间特征。具体来说,我们将关注 pandas 库中的时间序列工具,该工具集集中了许多其他通用库(如 datetime
)的功能。
7.1 字符串转换为日期
问题
给定一个表示日期和时间的字符串向量,你想要将它们转换为时间序列数据。
解决方案
使用 pandas 的 to_datetime
,并指定日期和/或时间的格式在 format
参数中:
# Load libraries
import numpy as np
import pandas as pd
# Create strings
date_strings = np.array(['03-04-2005 11:35 PM',
'23-05-2010 12:01 AM',
'04-09-2009 09:09 PM'])
# Convert to datetimes
[pd.to_datetime(date, format='%d-%m-%Y %I:%M %p') for date in date_strings]
[Timestamp('2005-04-03 23:35:00'),
Timestamp('2010-05-23 00:01:00'),
Timestamp('2009-09-04 21:09:00')]
我们也可能想要向 errors
参数添加一个参数来处理问题:
# Convert to datetimes
[pd.to_datetime(date, format="%d-%m-%Y %I:%M %p", errors="coerce")
for date in date_strings]
[Timestamp('2005-04-03 23:35:00'),
Timestamp('2010-05-23 00:01:00'),
Timestamp('2009-09-04 21:09:00')]
如果 errors="coerce"
,则任何发生的问题不会引发错误(默认行为),而是将导致错误的值设置为 NaT
(缺失值)。这允许你通过将其填充为 null 值来处理异常值,而不是为数据中的每个记录进行故障排除。
讨论
当日期和时间以字符串形式提供时,我们需要将它们转换为 Python 能够理解的数据类型。虽然有许多用于将字符串转换为日期时间的 Python 工具,但在其他示例中使用 pandas 后,我们可以使用 to_datetime
进行转换。使用字符串表示日期和时间的一个障碍是,字符串的格式在数据源之间可能有很大的变化。例如,一个日期向量可能将 2015 年 3 月 23 日表示为“03-23-15”,而另一个可能使用“3|23|2015”。我们可以使用 format
参数来指定字符串的确切格式。以下是一些常见的日期和时间格式代码:
代码 | 描述 | 示例 |
---|---|---|
%Y |
完整年份 | 2001 |
%m |
带零填充的月份 | 04 |
%d |
带零填充的日期 | 09 |
%I |
带零填充的小时(12 小时制) | 02 |
%p |
上午或下午 | AM |
%M |
带零填充的分钟数 | 05 |
%S |
带零填充的秒数 | 09 |
参见
7.2 处理时区
问题
你有时间序列数据,想要添加或更改时区信息。
解决方案
除非指定,否则 pandas 对象没有时区。我们可以在创建时使用 tz
添加时区:
# Load library
import pandas as pd
# Create datetime
pd.Timestamp('2017-05-01 06:00:00', tz='Europe/London')
Timestamp('2017-05-01 06:00:00+0100', tz='Europe/London')
我们可以使用 tz_localize
为先前创建的日期时间添加时区:
# Create datetime
date = pd.Timestamp('2017-05-01 06:00:00')
# Set time zone
date_in_london = date.tz_localize('Europe/London')
# Show datetime
date_in_london
Timestamp('2017-05-01 06:00:00+0100', tz='Europe/London')
我们也可以转换为不同的时区:
# Change time zone
date_in_london.tz_convert('Africa/Abidjan')
Timestamp('2017-05-01 05:00:00+0000', tz='Africa/Abidjan')
最后,pandas 的 Series
对象可以对每个元素应用 tz_localize
和 tz_convert
:
# Create three dates
dates = pd.Series(pd.date_range('2/2/2002', periods=3, freq='M'))
# Set time zone
dates.dt.tz_localize('Africa/Abidjan')
0 2002-02-28 00:00:00+00:00
1 2002-03-31 00:00:00+00:00
2 2002-04-30 00:00:00+00:00
dtype: datetime64[ns, Africa/Abidjan]
讨论
pandas 支持两组表示时区的字符串;然而,建议使用pytz
库的字符串。我们可以通过导入all_timezones
查看表示时区的所有字符串:
# Load library
from pytz import all_timezones
# Show two time zones
all_timezones[0:2]
['Africa/Abidjan', 'Africa/Accra']
7.3 选择日期和时间
问题
您有一个日期向量,想要选择一个或多个。
解决方案
使用两个布尔条件作为开始和结束日期:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create datetimes
dataframe['date'] = pd.date_range('1/1/2001', periods=100000, freq='H')
# Select observations between two datetimes
dataframe[(dataframe['date'] > '2002-1-1 01:00:00') &
(dataframe['date'] <= '2002-1-1 04:00:00')]
日期 | |
---|---|
8762 | 2002-01-01 02:00:00 |
8763 | 2002-01-01 03:00:00 |
8764 | 2002-01-01 04:00:00 |
或者,我们可以将日期列设置为 DataFrame 的索引,然后使用loc
进行切片:
# Set index
dataframe = dataframe.set_index(dataframe['date'])
# Select observations between two datetimes
dataframe.loc['2002-1-1 01:00:00':'2002-1-1 04:00:00']
日期 | 日期 |
---|---|
2002-01-01 01:00:00 | 2002-01-01 01:00:00 |
2002-01-01 02:00:00 | 2002-01-01 02:00:00 |
2002-01-01 03:00:00 | 2002-01-01 03:00:00 |
2002-01-01 04:00:00 | 2002-01-01 04:00:00 |
讨论
是否使用布尔条件或索引切片取决于具体情况。如果我们想要进行一些复杂的时间序列操作,将日期列设置为 DataFrame 的索引可能值得开销,但如果我们只想进行简单的数据处理,布尔条件可能更容易。
7.4 将日期数据拆分为多个特征
问题
您有一个包含日期和时间的列,并且希望创建年、月、日、小时和分钟的特征。
解决方案
使用 pandas Series.dt
中的时间属性:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create five dates
dataframe['date'] = pd.date_range('1/1/2001', periods=150, freq='W')
# Create features for year, month, day, hour, and minute
dataframe['year'] = dataframe['date'].dt.year
dataframe['month'] = dataframe['date'].dt.month
dataframe['day'] = dataframe['date'].dt.day
dataframe['hour'] = dataframe['date'].dt.hour
dataframe['minute'] = dataframe['date'].dt.minute
# Show three rows
dataframe.head(3)
日期 | 年 | 月 | 日 | 小时 | 分钟 | |
---|---|---|---|---|---|---|
0 | 2001-01-07 | 2001 | 1 | 7 | 0 | 0 |
1 | 2001-01-14 | 2001 | 1 | 14 | 0 | 0 |
2 | 2001-01-21 | 2001 | 1 | 21 | 0 | 0 |
讨论
有时将日期列分解为各个组成部分会很有用。例如,我们可能希望有一个特征仅包括观察年份,或者我们可能只想考虑某些观测的月份,以便无论年份如何都可以比较它们。
7.5 计算日期之间的差异
问题
您有两个日期时间特征,想要计算每个观测值之间的时间间隔。
解决方案
使用 pandas 减去两个日期特征:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create two datetime features
dataframe['Arrived'] = [pd.Timestamp('01-01-2017'), pd.Timestamp('01-04-2017')]
dataframe['Left'] = [pd.Timestamp('01-01-2017'), pd.Timestamp('01-06-2017')]
# Calculate duration between features
dataframe['Left'] - dataframe['Arrived']
0 0 days
1 2 days
dtype: timedelta64[ns]
我们经常希望删除days
的输出,仅保留数值:
# Calculate duration between features
pd.Series(delta.days for delta in (dataframe['Left'] - dataframe['Arrived']))
0 0
1 2
dtype: int64
讨论
有时我们需要的特征是两个时间点之间的变化(delta)。例如,我们可能有客户入住和退房的日期,但我们想要的特征是客户住店的持续时间。pandas 使用TimeDelta
数据类型使得这种计算变得简单。
参见
7.6 编码星期几
问题
您有一个日期向量,想知道每个日期的星期几。
解决方案
使用 pandas Series.dt
方法的day_name()
:
# Load library
import pandas as pd
# Create dates
dates = pd.Series(pd.date_range("2/2/2002", periods=3, freq="M"))
# Show days of the week
dates.dt.day_name()
0 Thursday
1 Sunday
2 Tuesday
dtype: object
如果我们希望输出为数值形式,因此更适合作为机器学习特征使用,可以使用weekday
,其中星期几表示为整数(星期一为 0)。
# Show days of the week
dates.dt.weekday
0 3
1 6
2 1
dtype: int64
讨论
如果我们想要比较过去三年每个星期日的总销售额,知道星期几可能很有帮助。pandas 使得创建包含星期信息的特征向量变得很容易。
另请参阅
7.7 创建滞后特征
问题
你想要创建一个滞后n个时间段的特征。
解决方案
使用 pandas 的shift
方法:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create data
dataframe["dates"] = pd.date_range("1/1/2001", periods=5, freq="D")
dataframe["stock_price"] = [1.1,2.2,3.3,4.4,5.5]
# Lagged values by one row
dataframe["previous_days_stock_price"] = dataframe["stock_price"].shift(1)
# Show data frame
dataframe
日期 | 股票价格 | 前几天的股票价格 | |
---|---|---|---|
0 | 2001-01-01 | 1.1 | NaN |
1 | 2001-01-02 | 2.2 | 1.1 |
2 | 2001-01-03 | 3.3 | 2.2 |
3 | 2001-01-04 | 4.4 | 3.3 |
4 | 2001-01-05 | 5.5 | 4.4 |
讨论
数据往往基于定期间隔的时间段(例如每天、每小时、每三小时),我们有兴趣使用过去的值来进行预测(通常称为滞后一个特征)。例如,我们可能想要使用前一天的价格来预测股票的价格。使用 pandas,我们可以使用shift
将值按一行滞后,创建一个包含过去值的新特征。
在我们的解决方案中,previous_days_stock_price
的第一行是一个缺失值,因为没有先前的stock_price
值。
7.8 使用滚动时间窗口
问题
给定时间序列数据,你想要计算一个滚动时间的统计量。
解决方案
使用 pandas DataFrame rolling
方法:
# Load library
import pandas as pd
# Create datetimes
time_index = pd.date_range("01/01/2010", periods=5, freq="M")
# Create data frame, set index
dataframe = pd.DataFrame(index=time_index)
# Create feature
dataframe["Stock_Price"] = [1,2,3,4,5]
# Calculate rolling mean
dataframe.rolling(window=2).mean()
股票价格 | |
---|---|
2010-01-31 | NaN |
2010-02-28 | 1.5 |
2010-03-31 | 2.5 |
2010-04-30 | 3.5 |
2010-05-31 | 4.5 |
讨论
滚动(也称为移动)时间窗口在概念上很简单,但一开始可能难以理解。假设我们有一个股票价格的月度观察数据。经常有用的是设定一个特定月数的时间窗口,然后在观察数据上移动,计算时间窗口内所有观察数据的统计量。
例如,如果我们有一个三个月的时间窗口,并且想要一个滚动均值,我们可以计算:
-
mean(一月, 二月, 三月)
-
mean(二月, 三月, 四月)
-
mean(三月, 四月, 五月)
-
等等
另一种表达方式:我们的三个月时间窗口“漫步”过观测值,在每一步计算窗口的平均值。
pandas 的rolling
方法允许我们通过使用window
指定窗口大小,然后快速计算一些常见统计量,包括最大值(max()
)、均值(mean()
)、值的数量(count()
)和滚动相关性(corr()
)。
滚动均值通常用于平滑时间序列数据,因为使用整个时间窗口的均值可以抑制短期波动的影响。
另请参阅
7.9 处理时间序列中的缺失数据
问题
你在时间序列数据中有缺失值。
解决方案
除了前面讨论的缺失数据策略之外,当我们有时间序列数据时,我们可以使用插值来填补由缺失值引起的间隙:
# Load libraries
import pandas as pd
import numpy as np
# Create date
time_index = pd.date_range("01/01/2010", periods=5, freq="M")
# Create data frame, set index
dataframe = pd.DataFrame(index=time_index)
# Create feature with a gap of missing values
dataframe["Sales"] = [1.0,2.0,np.nan,np.nan,5.0]
# Interpolate missing values
dataframe.interpolate()
销售 | |
---|---|
2010-01-31 | 1.0 |
2010-02-28 | 2.0 |
2010-03-31 | 3.0 |
2010-04-30 | 4.0 |
2010-05-31 | 5.0 |
或者,我们可以用最后一个已知值替换缺失值(即向前填充):
# Forward fill
dataframe.ffill()
销售 | |
---|---|
2010-01-31 | 1.0 |
2010-02-28 | 2.0 |
2010-03-31 | 2.0 |
2010-04-30 | 2.0 |
2010-05-31 | 5.0 |
我们还可以用最新的已知值替换缺失值(即向后填充):
# Backfill
dataframe.bfill()
销售 | |
---|---|
2010-01-31 | 1.0 |
2010-02-28 | 2.0 |
2010-03-31 | 5.0 |
2010-04-30 | 5.0 |
2010-05-31 | 5.0 |
讨论
插值是一种填补由缺失值引起的间隙的技术,实质上是在已知值之间绘制一条直线或曲线,并使用该线或曲线来预测合理的值。当时间间隔恒定、数据不易受到嘈杂波动影响、缺失值引起的间隙较小时,插值尤为有用。例如,在我们的解决方案中,两个缺失值之间的间隙由 2.0
和 5.0
所界定。通过在 2.0
和 5.0
之间拟合一条直线,我们可以推测出 3.0
到 4.0
之间的两个缺失值的合理值。
如果我们认为两个已知点之间的线是非线性的,我们可以使用 interpolate
的 method
参数来指定插值方法:
# Interpolate missing values
dataframe.interpolate(method="quadratic")
销售 | |
---|---|
2010-01-31 | 1.000000 |
2010-02-28 | 2.000000 |
2010-03-31 | 3.059808 |
2010-04-30 | 4.038069 |
2010-05-31 | 5.000000 |
最后,我们可能有大量的缺失值间隙,但不希望在整个间隙内插值。在这些情况下,我们可以使用 limit
限制插值值的数量,并使用 limit_direction
来设置是从间隙前的最后已知值向前插值,还是反之:
# Interpolate missing values
dataframe.interpolate(limit=1, limit_direction="forward")
销售 | |
---|---|
2010-01-31 | 1.0 |
2010-02-28 | 2.0 |
2010-03-31 | 3.0 |
2010-04-30 | NaN |
2010-05-31 | 5.0 |
向后填充和向前填充是一种朴素插值的形式,其中我们从已知值开始绘制一条平直线,并用它来填充缺失值。与插值相比,向后填充和向前填充的一个(轻微)优势在于它们不需要在缺失值的两侧都有已知值。
第八章:处理图像
8.0 介绍
图像分类是机器学习中最激动人心的领域之一。计算机从图像中识别模式和物体的能力是我们工具箱中非常强大的工具。然而,在将机器学习应用于图像之前,我们通常需要将原始图像转换为我们的学习算法可用的特征。与文本数据一样,也有许多预训练的分类器可用于图像,我们可以使用这些分类器来提取我们自己模型的输入中感兴趣的特征或对象。
为了处理图像,我们将主要使用开源计算机视觉库(OpenCV)。虽然市面上有许多优秀的库,但 OpenCV 是处理图像最流行和文档最完善的库。安装时可能会遇到一些挑战,但如果遇到问题,网上有很多指南。本书特别使用的是 opencv-python-headless==4.7.0.68
。您也可以使用 Python Cookbook Runner 中的 ML 确保所有命令可复现。
在本章中,我们将使用一组图像作为示例,可以从 GitHub 下载。
8.1 加载图像
问题
您想要加载一幅图像进行预处理。
解决方案
使用 OpenCV 的 imread
:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane.jpg", cv2.IMREAD_GRAYSCALE)
如果我们想查看图像,我们可以使用 Python 绘图库 Matplotlib:
# Show image
plt.imshow(image, cmap="gray"), plt.axis("off")
plt.show()
讨论
从根本上讲,图像是数据,当我们使用imread
时,我们将该数据转换为我们非常熟悉的数据类型——一个 NumPy 数组:
# Show data type
type(image)
numpy.ndarray
我们已将图像转换为一个矩阵,其元素对应于各个像素。我们甚至可以查看矩阵的实际值:
# Show image data
image
array([[140, 136, 146, ..., 132, 139, 134],
[144, 136, 149, ..., 142, 124, 126],
[152, 139, 144, ..., 121, 127, 134],
...,
[156, 146, 144, ..., 157, 154, 151],
[146, 150, 147, ..., 156, 158, 157],
[143, 138, 147, ..., 156, 157, 157]], dtype=uint8)
我们图像的分辨率是 3600 × 2270,正好是我们矩阵的确切尺寸:
# Show dimensions
image.shape
(2270, 3600)
矩阵中的每个元素实际上表示什么?在灰度图像中,单个元素的值是像素强度。强度值从黑色(0)到白色(255)变化。例如,我们图像左上角像素的强度值为 140:
# Show first pixel
image[0,0]
140
在表示彩色图像的矩阵中,每个元素实际上包含三个值,分别对应蓝色、绿色和红色的值(BGR):
# Load image in color
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
# Show pixel
image_bgr[0,0]
array([195, 144, 111], dtype=uint8)
有一个小细节:默认情况下,OpenCV 使用 BGR,但许多图像应用程序——包括 Matplotlib——使用红色、绿色、蓝色(RGB),这意味着红色和蓝色值被交换了。为了在 Matplotlib 中正确显示 OpenCV 彩色图像,我们首先需要将颜色转换为 RGB(对于硬拷贝读者,没有彩色图像我们深感抱歉)。
# Convert to RGB
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Show image
plt.imshow(image_rgb), plt.axis("off")
plt.show()
参见
8.2 保存图像
问题
您想要保存一幅图像进行预处理。
解决方案
使用 OpenCV 的 imwrite
:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane.jpg", cv2.IMREAD_GRAYSCALE)
# Save image
cv2.imwrite("images/plane_new.jpg", image)
True
讨论
OpenCV 的imwrite
将图像保存到指定的文件路径。图像的格式由文件名的扩展名(.jpg,.png等)定义。要注意的一个行为是:imwrite
会覆盖现有文件而不输出错误或要求确认。
8.3 调整图像大小
问题
您想要调整图像的大小以进行进一步的预处理。
解决方案
使用resize
来改变图像的大小:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Resize image to 50 pixels by 50 pixels
image_50x50 = cv2.resize(image, (50, 50))
# View image
plt.imshow(image_50x50, cmap="gray"), plt.axis("off")
plt.show()
讨论
调整图像大小是图像预处理中的常见任务,有两个原因。首先,图像以各种形状和大小出现,为了作为特征可用,图像必须具有相同的尺寸。标准化(调整大小)图像的过程会丢失较大图像中存在的一些信息,就像飞机图片中所看到的那样。图像是信息的矩阵,当我们减小图像的尺寸时,我们减少了该矩阵及其所包含信息的大小。其次,机器学习可能需要成千上万张图像。当这些图像非常大时,它们会占用大量内存,通过调整它们的大小,我们可以显著减少内存使用量。机器学习中常见的一些图像尺寸包括 32 × 32、64 × 64、96 × 96 和 256 × 256。总的来说,我们选择的图像调整方法往往是模型统计性能与训练计算成本之间的权衡。出于这个原因,Pillow 库提供了许多调整图像大小的选项。
8.4 裁剪图像
问题
您想要删除图像的外部部分以更改其尺寸。
解决方案
图像被编码为二维 NumPy 数组,因此我们可以通过切片数组轻松地裁剪图像:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image in grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Select first half of the columns and all rows
image_cropped = image[:,:128]
# Show image
plt.imshow(image_cropped, cmap="gray"), plt.axis("off")
plt.show()
讨论
由于 OpenCV 将图像表示为元素的矩阵,通过选择我们想保留的行和列,我们可以轻松地裁剪图像。如果我们知道我们只想保留每个图像的特定部分,裁剪可以特别有用。例如,如果我们的图像来自固定的安全摄像机,我们可以裁剪所有图像,使它们仅包含感兴趣的区域。
参见
8.5 模糊图像
问题
您想要使图像变得平滑。
解决方案
为了模糊图像,每个像素被转换为其邻居的平均值。数学上将这个邻居和操作表示为一个核(如果你不知道核是什么也不用担心)。这个核的大小决定了模糊的程度,较大的核产生更平滑的图像。在这里,我们通过对每个像素周围的 5 × 5 核的值取平均来模糊图像:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Blur image
image_blurry = cv2.blur(image, (5,5))
# Show image
plt.imshow(image_blurry, cmap="gray"), plt.axis("off")
plt.show()
为了突出显示核大小的效果,这里是使用 100 × 100 核进行的相同模糊处理的图像:
# Blur image
image_very_blurry = cv2.blur(image, (100,100))
# Show image
plt.imshow(image_very_blurry, cmap="gray"), plt.xticks([]), plt.yticks([])
plt.show()
讨论
卷积核在图像处理中被广泛使用,从锐化到边缘检测等方面,本章节将反复讨论。我们使用的模糊核如下所示:
# Create kernel
kernel = np.ones((5,5)) / 25.0
# Show kernel
kernel
array([[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04]])
核心元素在内核中是被检查的像素,而其余元素是其邻居。由于所有元素具有相同的值(归一化为总和为 1),因此每个元素对感兴趣像素的结果值都有相同的影响力。我们可以使用filter2D
手动将内核应用于图像,以产生类似的模糊效果:
# Apply kernel
image_kernel = cv2.filter2D(image, -1, kernel)
# Show image
plt.imshow(image_kernel, cmap="gray"), plt.xticks([]), plt.yticks([])
plt.show()
参见
8.6 锐化图像
问题
您想要锐化图像。
解决方案
创建一个突出显示目标像素的内核。然后使用filter2D
将其应用于图像:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Create kernel
kernel = np.array([[0, -1, 0],
[-1, 5,-1],
[0, -1, 0]])
# Sharpen image
image_sharp = cv2.filter2D(image, -1, kernel)
# Show image
plt.imshow(image_sharp, cmap="gray"), plt.axis("off")
plt.show()
讨论
锐化的工作原理与模糊类似,但不同于使用内核来平均周围值,我们构建了一个内核来突出像素本身。其结果效果使得边缘处的对比更加明显。
8.7 增强对比度
问题
我们希望增加图像中像素之间的对比度。
解决方案
直方图均衡化 是一种图像处理工具,可以使物体和形状更加突出。当我们有一个灰度图像时,可以直接在图像上应用 OpenCV 的equalizeHist
:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Enhance image
image_enhanced = cv2.equalizeHist(image)
# Show image
plt.imshow(image_enhanced, cmap="gray"), plt.axis("off")
plt.show()
然而,当我们有一幅彩色图像时,我们首先需要将图像转换为 YUV 颜色格式。Y 代表亮度,U 和 V 表示颜色。转换后,我们可以将equalizeHist
应用于图像,然后再转换回 BGR 或 RGB(对于只有黑白图像的读者表示抱歉):
# Load image
image_bgr = cv2.imread("images/plane.jpg")
# Convert to YUV
image_yuv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2YUV)
# Apply histogram equalization
image_yuv[:, :, 0] = cv2.equalizeHist(image_yuv[:, :, 0])
# Convert to RGB
image_rgb = cv2.cvtColor(image_yuv, cv2.COLOR_YUV2RGB)
# Show image
plt.imshow(image_rgb), plt.axis("off")
plt.show()
讨论
虽然详细解释直方图均衡化的工作原理超出了本书的范围,简短的解释是它转换图像,使其使用更广泛的像素强度范围。
虽然生成的图像通常看起来不够“真实”,但我们需要记住,图像只是底层数据的视觉表示。如果直方图均衡化能够使感兴趣的对象与其他对象或背景更易于区分(这并非总是如此),那么它可以成为我们图像预处理流程中的有价值的补充。
8.8 分离颜色
问题
您想要在图像中隔离一种颜色。
解决方案
定义一个颜色范围,然后将掩码应用于图像(对于只有黑白图像的读者表示抱歉):
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image
image_bgr = cv2.imread('images/plane_256x256.jpg')
# Convert BGR to HSV
image_hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
# Define range of blue values in HSV
lower_blue = np.array([50,100,50])
upper_blue = np.array([130,255,255])
# Create mask
mask = cv2.inRange(image_hsv, lower_blue, upper_blue)
# Mask image
image_bgr_masked = cv2.bitwise_and(image_bgr, image_bgr, mask=mask)
# Convert BGR to RGB
image_rgb = cv2.cvtColor(image_bgr_masked, cv2.COLOR_BGR2RGB)
# Show image
plt.imshow(image_rgb), plt.axis("off")
plt.show()
讨论
在 OpenCV 中隔离颜色是直接的。首先我们将图像转换为 HSV(色调、饱和度和值)。其次,我们定义我们想要隔离的值范围,这可能是最困难和耗时的部分。第三,我们为图像创建一个掩码。图像掩码是一种常见的技术,旨在提取感兴趣的区域。在这种情况下,我们的掩码仅保留白色区域:
# Show image
plt.imshow(mask, cmap='gray'), plt.axis("off")
plt.show()
最后,我们使用 bitwise_and
将掩码应用于图像,并将其转换为我们期望的输出格式。
8.9 图像二值化
问题
给定一张图像,您想要输出一个简化版本。
解决方案
阈值化 是将像素强度大于某个值的像素设置为白色,小于该值的像素设置为黑色的过程。更高级的技术是 自适应阈值化,其中像素的阈值由其邻域的像素强度决定。当图像中不同区域的光照条件发生变化时,这可能会有所帮助:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image_grey = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Apply adaptive thresholding
max_output_value = 255
neighborhood_size = 99
subtract_from_mean = 10
image_binarized = cv2.adaptiveThreshold(image_grey,
max_output_value,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
neighborhood_size,
subtract_from_mean)
# Show image
plt.imshow(image_binarized, cmap="gray"), plt.axis("off")
plt.show()
讨论
对图像进行二值化的过程涉及将灰度图像转换为其黑白形式。我们的解决方案在 adaptiveThreshold
中有四个重要参数。max_output_value
简单地确定输出像素强度的最大值。cv2.ADAPTIVE_THRESH_GAUSSIAN_C
将像素的阈值设置为其相邻像素强度的加权和。权重由高斯窗口确定。或者,我们可以将阈值简单地设置为相邻像素的平均值,使用 cv2.ADAPTIVE_THRESH_MEAN_C
:
# Apply cv2.ADAPTIVE_THRESH_MEAN_C
image_mean_threshold = cv2.adaptiveThreshold(image_grey,
max_output_value,
cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY,
neighborhood_size,
subtract_from_mean)
# Show image
plt.imshow(image_mean_threshold, cmap="gray"), plt.axis("off")
plt.show()
最后两个参数是块大小(用于确定像素阈值的邻域大小)和从计算阈值中减去的常数(用于手动微调阈值)。
阈值化的一个主要好处是 去噪 图像 —— 仅保留最重要的元素。例如,经常将阈值应用于印刷文本的照片,以隔离页面上的字母。
8.10 移除背景
问题
您想要隔离图像的前景。
解决方案
在所需前景周围标记一个矩形,然后运行 GrabCut 算法:
# Load library
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image and convert to RGB
image_bgr = cv2.imread('images/plane_256x256.jpg')
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Rectangle values: start x, start y, width, height
rectangle = (0, 56, 256, 150)
# Create initial mask
mask = np.zeros(image_rgb.shape[:2], np.uint8)
# Create temporary arrays used by grabCut
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
# Run grabCut
cv2.grabCut(image_rgb, # Our image
mask, # The Mask
rectangle, # Our rectangle
bgdModel, # Temporary array for background
fgdModel, # Temporary array for background
5, # Number of iterations
cv2.GC_INIT_WITH_RECT) # Initiative using our rectangle
# Create mask where sure and likely backgrounds set to 0, otherwise 1
mask_2 = np.where((mask==2) | (mask==0), 0, 1).astype('uint8')
# Multiply image with new mask to subtract background
image_rgb_nobg = image_rgb * mask_2[:, :, np.newaxis]
# Show image
plt.imshow(image_rgb_nobg), plt.axis("off")
plt.show()
讨论
我们首先注意到,即使 GrabCut 做得相当不错,图像中仍然有一些背景区域。我们可以回去手动标记这些区域为背景,但在现实世界中,我们有成千上万张图片,逐个手动修复它们是不可行的。因此,我们最好接受图像数据仍然会包含一些背景噪声。
在我们的解决方案中,我们首先在包含前景的区域周围标记一个矩形。GrabCut 假定这个矩形外的所有内容都是背景,并利用这些信息来推断出正方形内部可能是背景的区域。(要了解算法如何做到这一点,请参阅Itay Blumenthal的解释。)然后创建一个标记不同明确/可能背景/前景区域的掩码:
# Show mask
plt.imshow(mask, cmap='gray'), plt.axis("off")
plt.show()
黑色区域是矩形外部被假定为明确背景的区域。灰色区域是 GrabCut 认为可能是背景的区域,而白色区域则是可能是前景的区域。
然后使用该掩码创建第二个掩码,将黑色和灰色区域合并:
# Show mask
plt.imshow(mask_2, cmap='gray'), plt.axis("off")
plt.show()
然后将第二个掩码应用于图像,以便仅保留前景。
8.11 检测边缘
问题
您希望在图像中找到边缘。
解决方案
使用像 Canny 边缘检测器这样的边缘检测技术:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image_gray = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Calculate median intensity
median_intensity = np.median(image_gray)
# Set thresholds to be one standard deviation above and below median intensity
lower_threshold = int(max(0, (1.0 - 0.33) * median_intensity))
upper_threshold = int(min(255, (1.0 + 0.33) * median_intensity))
# Apply Canny edge detector
image_canny = cv2.Canny(image_gray, lower_threshold, upper_threshold)
# Show image
plt.imshow(image_canny, cmap="gray"), plt.axis("off")
plt.show()
讨论
边缘检测是计算机视觉中的一个主要话题。边缘非常重要,因为它们是信息量最大的区域。例如,在我们的图像中,一片天空看起来非常相似,不太可能包含独特或有趣的信息。然而,背景天空与飞机相遇的区域包含大量信息(例如,物体的形状)。边缘检测允许我们去除低信息量的区域,并分离包含最多信息的图像区域。
边缘检测有许多技术(Sobel 滤波器、Laplacian 边缘检测器等)。然而,我们的解决方案使用常用的 Canny 边缘检测器。Canny 检测器的工作原理对本书来说过于详细,但有一点我们需要解决。Canny 检测器需要两个参数来指定低梯度阈值和高梯度阈值。低和高阈值之间的潜在边缘像素被认为是弱边缘像素,而高于高阈值的像素被认为是强边缘像素。OpenCV 的Canny
方法包括所需的低和高阈值参数。在我们的解决方案中,我们将低和高阈值设置为图像中位数下方和上方的一个标准偏差。然而,在运行Canny
处理整个图像集之前,我们经常通过手动在几幅图像上试错来确定一对好的低和高阈值,以获得更好的结果。
参见
8.12 检测角点
问题
您希望检测图像中的角点。
解决方案
使用 OpenCV 的 Harris 角检测器cornerHarris
的实现:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image
image_bgr = cv2.imread("images/plane_256x256.jpg")
image_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
image_gray = np.float32(image_gray)
# Set corner detector parameters
block_size = 2
aperture = 29
free_parameter = 0.04
# Detect corners
detector_responses = cv2.cornerHarris(image_gray,
block_size,
aperture,
free_parameter)
# Large corner markers
detector_responses = cv2.dilate(detector_responses, None)
# Only keep detector responses greater than threshold, mark as white
threshold = 0.02
image_bgr[detector_responses >
threshold *
detector_responses.max()] = [255,255,255]
# Convert to grayscale
image_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Show image
plt.imshow(image_gray, cmap="gray"), plt.axis("off")
plt.show()
讨论
Harris 角点检测器是一种常用的检测两条边交点的方法。我们对检测角点的兴趣与检测边缘的原因相同:角点是信息量很高的点。Harris 角点检测器的完整解释可以在本文末尾的外部资源中找到,但简化的解释是它寻找窗口(也称为邻域或补丁),在这些窗口中,窗口的微小移动(想象抖动窗口)导致窗口内像素内容的显著变化。cornerHarris
包含三个重要参数,我们可以用它来控制检测到的边缘。首先,block_size
是用于角点检测的每个像素周围的邻域的大小。其次,aperture
是使用的 Sobel 核的大小(如果你不知道是什么也没关系),最后有一个自由参数,较大的值对应于识别更软的角点。
输出是一个灰度图像,描述了潜在的角点:
# Show potential corners
plt.imshow(detector_responses, cmap='gray'), plt.axis("off")
plt.show()
然后我们应用阈值处理,仅保留最可能的角点。或者,我们可以使用类似的检测器,即 Shi-Tomasi 角点检测器,它的工作方式与 Harris 检测器类似(goodFeaturesToTrack
),用于识别固定数量的强角点。goodFeaturesToTrack
有三个主要参数——要检测的角点数目,角点的最小质量(0 到 1 之间),以及角点之间的最小欧氏距离:
# Load images
image_bgr = cv2.imread('images/plane_256x256.jpg')
image_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Number of corners to detect
corners_to_detect = 10
minimum_quality_score = 0.05
minimum_distance = 25
# Detect corners
corners = cv2.goodFeaturesToTrack(image_gray,
corners_to_detect,
minimum_quality_score,
minimum_distance)
corners = np.int16(corners)
# Draw white circle at each corner
for corner in corners:
x, y = corner[0]
cv2.circle(image_bgr, (x,y), 10, (255,255,255), -1)
# Convert to grayscale
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Show image
plt.imshow(image_rgb, cmap='gray'), plt.axis("off")
plt.show()
另请参阅
8.13 为机器学习创建特征
问题
您希望将图像转换为机器学习的观察结果。
解决方案
使用 NumPy 的flatten
将包含图像数据的多维数组转换为包含观察值的向量:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Resize image to 10 pixels by 10 pixels
image_10x10 = cv2.resize(image, (10, 10))
# Convert image data to one-dimensional vector
image_10x10.flatten()
array([133, 130, 130, 129, 130, 129, 129, 128, 128, 127, 135, 131, 131,
131, 130, 130, 129, 128, 128, 128, 134, 132, 131, 131, 130, 129,
129, 128, 130, 133, 132, 158, 130, 133, 130, 46, 97, 26, 132,
143, 141, 36, 54, 91, 9, 9, 49, 144, 179, 41, 142, 95,
32, 36, 29, 43, 113, 141, 179, 187, 141, 124, 26, 25, 132,
135, 151, 175, 174, 184, 143, 151, 38, 133, 134, 139, 174, 177,
169, 174, 155, 141, 135, 137, 137, 152, 169, 168, 168, 179, 152,
139, 136, 135, 137, 143, 159, 166, 171, 175], dtype=uint8)
讨论
图像呈像素网格的形式呈现。如果图像是灰度的,每个像素由一个值表示(即,如果是白色,则像素强度为1
,如果是黑色则为0
)。例如,想象我们有一个 10 × 10 像素的图像:
plt.imshow(image_10x10, cmap="gray"), plt.axis("off")
plt.show()
在这种情况下,图像数据的尺寸将是 10 × 10:
image_10x10.shape
(10, 10)
如果我们展平数组,我们得到长度为 100 的向量(10 乘以 10):
image_10x10.flatten().shape
(100,)
这是我们图像的特征数据,可以与其他图像的向量合并,以创建我们将提供给机器学习算法的数据。
如果图像是彩色的,每个像素不是由一个值表示,而是由多个值表示(通常是三个),表示混合以形成该像素的最终颜色的通道(红色、绿色、蓝色等)。因此,如果我们的 10 × 10 图像是彩色的,每个观察值将有 300 个特征值:
# Load image in color
image_color = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_COLOR)
# Resize image to 10 pixels by 10 pixels
image_color_10x10 = cv2.resize(image_color, (10, 10))
# Convert image data to one-dimensional vector, show dimensions
image_color_10x10.flatten().shape
(300,)
图像处理和计算机视觉的一个主要挑战是,由于图像集合中每个像素位置都是一个特征,随着图像变大,特征数量会急剧增加:
# Load image in grayscale
image_256x256_gray = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Convert image data to one-dimensional vector, show dimensions
image_256x256_gray.flatten().shape
(65536,)
当图像为彩色时,特征数量甚至变得更大:
# Load image in color
image_256x256_color = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_COLOR)
# Convert image data to one-dimensional vector, show dimensions
image_256x256_color.flatten().shape
(196608,)
如输出所示,即使是小型彩色图像也有接近 200,000 个特征,这可能在训练模型时会引发问题,因为特征数量可能远远超过观察值的数量。
这个问题将推动后面章节讨论的降维策略,试图在不失去数据中过多信息的情况下减少特征数量。
8.14 将颜色直方图编码为特征
问题
您想创建一组表示图像中出现的颜色的特征。
解决方案
计算每个颜色通道的直方图:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
np.random.seed(0)
# Load image
image_bgr = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_COLOR)
# Convert to RGB
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Create a list for feature values
features = []
# Calculate the histogram for each color channel
colors = ("r","g","b")
# For each channel: calculate histogram and add to feature value list
for i, channel in enumerate(colors):
histogram = cv2.calcHist([image_rgb], # Image
[i], # Index of channel
None, # No mask
[256], # Histogram size
[0,256]) # Range
features.extend(histogram)
# Create a vector for an observation's feature values
observation = np.array(features).flatten()
# Show the observation's value for the first five features
observation[0:5]
array([ 1008., 217., 184., 165., 116.], dtype=float32)
讨论
在 RGB 颜色模型中,每种颜色都是三个颜色通道(即红色、绿色、蓝色)的组合。每个通道可以取 0 到 255 之间的 256 个值(由整数表示)。例如,我们图像中左上角的像素具有以下通道值:
# Show RGB channel values
image_rgb[0,0]
array([107, 163, 212], dtype=uint8)
直方图是数据值分布的表示。这里有一个简单的例子:
# Import pandas
import pandas as pd
# Create some data
data = pd.Series([1, 1, 2, 2, 3, 3, 3, 4, 5])
# Show the histogram
data.hist(grid=False)
plt.show()
在这个例子中,我们有一些数据,其中有两个1
,两个2
,三个3
,一个4
和一个5
。在直方图中,每个条形表示数据中每个值(1
,2
等)出现的次数。
我们可以将这种技术应用到每个颜色通道上,但是不是五种可能的值,而是 256 种(通道值的可能数)。x 轴表示 256 个可能的通道值,y 轴表示图像中所有像素中特定通道值出现的次数(对于没有彩色图像的纸质读者表示歉意):
# Calculate the histogram for each color channel
colors = ("r","g","b")
# For each channel: calculate histogram, make plot
for i, channel in enumerate(colors):
histogram = cv2.calcHist([image_rgb], # Image
[i], # Index of channel
None, # No mask
[256], # Histogram size
[0,256]) # Range
plt.plot(histogram, color = channel)
plt.xlim([0,256])
# Show plot
plt.show()
正如我们在直方图中看到的,几乎没有像素包含蓝色通道值在 0 到约 180 之间,而许多像素包含蓝色通道值在约 190 到约 210 之间。该通道值分布显示了所有三个通道的情况。然而,直方图不仅仅是一种可视化工具;每个颜色通道有 256 个特征,总共为 768 个特征,代表图像中颜色分布。
参见
8.15 使用预训练的嵌入作为特征
问题
您想从现有的 PyTorch 模型中加载预训练的嵌入,并将其用作您自己模型的输入。
解决方案
使用torchvision.models
选择模型,然后从中检索给定图像的嵌入:
# Load libraries
import cv2
import numpy as np
import torch
from torchvision import transforms
import torchvision.models as models
# Load image
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
# Convert to pytorch data type
convert_tensor = transforms.ToTensor()
pytorch_image = convert_tensor(np.array(image_rgb))
# Load the pretrained model
model = models.resnet18(pretrained=True)
# Select the specific layer of the model we want output from
layer = model._modules.get('avgpool')
# Set model to evaluation mode
model.eval()
# Infer the embedding with the no_grad option
with torch.no_grad():
embedding = model(pytorch_image.unsqueeze(0))
print(embedding.shape)
torch.Size([1, 1000])
讨论
在机器学习领域,迁移学习通常被定义为从一个任务学到的信息,并将其作为另一个任务的输入。我们可以利用已经从大型预训练图像模型(如 ResNet)学到的表示来快速启动我们自己的机器学习模型,而不是从零开始。更直观地说,你可以理解为,我们可以使用一个训练用于识别猫的模型的权重作为我们想要训练用于识别狗的模型的一个良好的起点。通过从一个模型向另一个模型共享信息,我们可以利用从其他数据集和模型架构学到的信息,而无需从头开始训练模型。
在计算机视觉中应用迁移学习的整个过程超出了本书的范围;然而,我们可以在 PyTorch 之外的许多不同方式中提取基于嵌入的图像表示。在 TensorFlow 中,另一个常见的深度学习库,我们可以使用tensorflow_hub
:
# Load libraries
import cv2
import tensorflow as tf
import tensorflow_hub as hub
# Load image
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Convert to tensorflow data type
tf_image = tf.image.convert_image_dtype([image_rgb], tf.float32)
# Create the model and get embeddings using the inception V1 model
embedding_model = hub.KerasLayer(
"https://tfhub.dev/google/imagenet/inception_v1/feature_vector/5"
)
embeddings = embedding_model(tf_image)
# Print the shape of the embedding
print(embeddings.shape)
(1, 1024)
参见
8.16 使用 OpenCV 检测对象
问题
您希望使用 OpenCV 中预训练的级联分类器来检测图像中的对象。
解决方案
下载并运行一个 OpenCV 的Haar 级联分类器。在这种情况下,我们使用一个预训练的人脸检测模型来检测图像中的人脸并画一个矩形框:
# Import libraries
import cv2
from matplotlib import pyplot as plt
# first run:
# mkdir models && cd models
# wget https://tinyurl.com/mrc6jwhp
face_cascade = cv2.CascadeClassifier()
face_cascade.load(
cv2.samples.findFile(
"models/haarcascade_frontalface_default.xml"
)
)
# Load image
image_bgr = cv2.imread("images/kyle_pic.jpg", cv2.IMREAD_COLOR)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Detect faces and draw a rectangle
faces = face_cascade.detectMultiScale(image_rgb)
for (x,y,w,h) in faces:
cv2.rectangle(image_rgb, (x, y),
(x + h, y + w),
(0, 255, 0), 5)
# Show the image
plt.subplot(1, 1, 1)
plt.imshow(image_rgb)
plt.show()
讨论
Haar 级联分类器是用于学习一组图像特征(特别是 Haar 特征)的机器学习模型,这些特征可以用于检测图像中的对象。这些特征本身是简单的矩形特征,通过计算矩形区域之间的和的差异来确定。随后,应用梯度提升算法来学习最重要的特征,并最终使用级联分类器创建相对强大的模型。
虽然这个过程的详细信息超出了本书的范围,但值得注意的是,这些预训练模型可以轻松从诸如OpenCV GitHub这样的地方下载为 XML 文件,并应用于图像,而无需自己训练模型。在你想要将简单的二进制图像特征(如contains_face
或任何其他对象)添加到你的数据中的情况下,这非常有用。
参见
8.17 使用 Pytorch 对图像进行分类
问题
您希望使用 Pytorch 中预训练的深度学习模型对图像进行分类。
解决方案
使用torchvision.models
选择一个预训练的图像分类模型,并将图像输入其中:
# Load libraries
import cv2
import json
import numpy as np
import torch
from torchvision import transforms
from torchvision.models import resnet18
import urllib.request
# Get imagenet classes
with urllib.request.urlopen(
"https://raw.githubusercontent.com/raghakot/keras-vis/master/resources/"
):
imagenet_class_index = json.load(url)
# Instantiate pretrained model
model = resnet18(pretrained=True)
# Load image
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Convert to pytorch data type
convert_tensor = transforms.ToTensor()
pytorch_image = convert_tensor(np.array(image_rgb))
# Set model to evaluation mode
model.eval()
# Make a prediction
prediction = model(pytorch_image.unsqueeze(0))
# Get the index of the highest predicted probability
_, index = torch.max(prediction, 1)
# Convert that to a percentage value
percentage = torch.nn.functional.softmax(prediction, dim=1)[0] * 100
# Print the name of the item at the index along with the percent confidence
print(imagenet_class_index[str(index.tolist()[0])][1],
percentage[index.tolist()[0]].item())
airship 6.0569939613342285
讨论
许多预训练的深度学习模型用于图像分类,通过 PyTorch 和 TensorFlow 都很容易获取。在这个例子中,我们使用了 ResNet18,这是一个深度神经网络架构,它在 ImageNet 数据集上训练,深度为 18 层。在 PyTorch 中还有更深的 ResNet 模型,如 ResNet101 和 ResNet152,此外还有许多其他可供选择的图像模型。在 ImageNet 数据集上训练的模型能够为我们在之前代码片段中从 GitHub 下载的imagenet_class_index
变量中定义的所有类别输出预测概率。
就像在 OpenCV 中的面部识别示例(参见 Recipe 8.16)一样,我们可以将预测的图像类别作为未来 ML 模型的下游特征,或者作为有用的元数据标签,为我们的图像添加更多信息。
参见
第九章:使用特征提取进行降维
9.0 介绍
通常情况下,我们可以接触到成千上万的特征。例如,在 第八章 中,我们将一个 256 × 256 像素的彩色图像转换成了 196,608 个特征。此外,因为每个像素可以取 256 个可能的值,我们的观察可以有 256¹⁹⁶⁶⁰⁸ 种不同的配置。许多机器学习算法在学习这样的数据时会遇到困难,因为收集足够的观察数据使算法能够正确运行是不现实的。即使在更结构化的表格数据集中,经过特征工程处理后,我们很容易就能得到数千个特征。
幸运的是,并非所有的特征都是相同的,特征提取 的目标是为了降低维度,将我们的特征集合 p[original] 转换成一个新的集合 p[new],使得 p[original] > p[new],同时保留大部分底层信息。换句话说,我们通过仅有少量数据丢失来减少特征数目,而数据仍能生成高质量的预测。在本章中,我们将涵盖多种特征提取技术来实现这一目标。
我们讨论的特征提取技术的一个缺点是,我们生成的新特征对人类来说是不可解释的。它们将具有与训练模型几乎相同的能力,但在人眼中看起来像是一组随机数。如果我们希望保留解释模型的能力,通过特征选择进行降维是更好的选择(将在 第十章 中讨论)。在特征选择期间,我们会移除我们认为不重要的特征,但保留其他特征。虽然这可能不像特征提取那样保留所有特征的信息,但它保留了我们不删除的特征——因此在分析过程中完全可解释。
9.1 使用主成分减少特征
问题
给定一组特征,你希望减少特征数目同时保留数据中的方差(重要信息)。
解决方案
使用 scikit 的 PCA
进行主成分分析:
# Load libraries
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn import datasets
# Load the data
digits = datasets.load_digits()
# Standardize the feature matrix
features = StandardScaler().fit_transform(digits.data)
# Create a PCA that will retain 99% of variance
pca = PCA(n_components=0.99, whiten=True)
# Conduct PCA
features_pca = pca.fit_transform(features)
# Show results
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_pca.shape[1])
Original number of features: 64
Reduced number of features: 54
讨论
主成分分析 (PCA) 是一种流行的线性降维技术。PCA 将观察结果投影到特征矩阵的(希望较少的)主成分上,这些主成分保留了数据中最多的方差,从实际上来说,我们保留了信息。PCA 是一种无监督技术,意味着它不使用目标向量的信息,而只考虑特征矩阵。
要了解 PCA 的工作原理的数学描述,请参阅本食谱末尾列出的外部资源。然而,我们可以使用一个简单的例子来理解 PCA 的直觉。在图 9-1 中,我们的数据包含两个特征,和。通过观察可视化结果,应该清楚地看到观察结果像雪茄一样散开,长度很长,高度很低。更具体地说,我们可以说“长度”的方差显著大于“高度”的方差。我们将“方向”中的最大方差称为第一主成分,“方向”中的第二大方差称为第二主成分(依此类推)。
如果我们想要减少特征,一个策略是将我们二维空间中的所有观察投影到一维主成分上。我们会丢失第二主成分中捕获的信息,但在某些情况下,这是可以接受的权衡。这就是 PCA。
图 9-1. PCA 的第一和第二主成分
PCA 在 scikit-learn 中是通过PCA
类实现的。n_components
有两个操作,取决于提供的参数。如果参数大于 1,pca
将返回那么多特征。这引出了如何选择最优特征数量的问题。幸运的是,如果n_components
的参数在 0 到 1 之间,pca
将返回保留原始特征方差百分之多少的最小特征数。通常使用 0.95 和 0.99 的值,分别表示保留原始特征方差的 95%和 99%。whiten=True
将每个主成分的值转换为具有零均值和单位方差。另一个参数和参数是svd_solver="randomized"
,它实现了一种随机算法,通常能够在更短的时间内找到第一个主成分。
我们的解决方案的输出显示,PCA 使我们能够通过减少 10 个特征的维度,同时仍保留特征矩阵中 99%的信息(方差)。
参见
9.2 在数据线性不可分时减少特征
问题
您怀疑您的数据是线性不可分的,并希望降低维度。
解决方案
使用使用核函数的主成分分析的扩展,以实现非线性降维:
# Load libraries
from sklearn.decomposition import PCA, KernelPCA
from sklearn.datasets import make_circles
# Create linearly inseparable data
features, _ = make_circles(n_samples=1000, random_state=1, noise=0.1, factor=0.1)
# Apply kernel PCA with radius basis function (RBF) kernel
kpca = KernelPCA(kernel="rbf", gamma=15, n_components=1)
features_kpca = kpca.fit_transform(features)
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_kpca.shape[1])
Original number of features: 2
Reduced number of features: 1
讨论
PCA 能够减少我们特征矩阵的维度(即特征数量)。标准 PCA 使用线性投影来减少特征。如果数据是线性可分的(即你可以在不同类别之间画一条直线或超平面),那么 PCA 效果很好。然而,如果你的数据不是线性可分的(即你只能使用曲线决策边界来分离类别),线性变换效果就不那么好了。在我们的解决方案中,我们使用了 scikit-learn 的make_circles
来生成一个模拟数据集,其中包含两个类别和两个特征的目标向量。make_circles
生成线性不可分的数据;具体来说,一个类别被另一个类别包围在所有方向上,如图 9-2 所示。
图 9-2. 线性不可分数据上投影的第一个主成分
如果我们使用线性 PCA 来降低数据维度,那么这两个类别将线性投影到第一个主成分上,使它们变得交织在一起,如图 9-3 所示。
图 9-3. 线性不可分数据的第一个主成分,没有核 PCA
理想情况下,我们希望进行一种转换,能够减少维度并使数据线性可分。核 PCA 可以做到这两点,如图 9-4 所示。
图 9-4. 带有核 PCA 的线性不可分数据的第一个主成分
核函数允许我们将线性不可分的数据投影到一个更高维度,使其线性可分;这被称为“核技巧”。如果你不理解核技巧的细节也不要担心;只需把核函数看作是投影数据的不同方法。在 scikit-learn 的kernelPCA
类中,我们可以使用多种核,通过kernel
参数指定。一个常用的核是高斯径向基函数核rbf
,但其他选项包括多项式核(poly
)和 sigmoid 核(sigmoid
)。我们甚至可以指定一个线性投影(linear
),它将产生与标准 PCA 相同的结果。
核 PCA 的一个缺点是我们需要指定一些参数。例如,在第 9.1 节中,我们将n_components
设置为0.99
,以便使PCA
选择保留 99%方差的成分数量。在核 PCA 中我们没有这个选项。相反,我们必须定义成分的数量(例如n_components=1
)。此外,核函数自带它们自己的超参数,我们需要设置;例如,径向基函数需要一个gamma
值。
那么我们如何知道使用哪些值?通过试错。具体而言,我们可以多次训练我们的机器学习模型,每次使用不同的核函数或不同的参数值。一旦找到产生最高质量预测值组合的值,我们就完成了。这是机器学习中的一个常见主题,我们将在第十二章深入学习这一策略。
参见
9.3 通过最大化类别可分性来减少特征
问题
您希望通过最大化类别之间的分离来减少分类器使用的特征数量。
解决方案
尝试线性判别分析(LDA)将特征投影到最大化类别分离的组件轴上:
# Load libraries
from sklearn import datasets
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
# Load Iris flower dataset:
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create and run an LDA, then use it to transform the features
lda = LinearDiscriminantAnalysis(n_components=1)
features_lda = lda.fit(features, target).transform(features)
# Print the number of features
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_lda.shape[1])
Original number of features: 4
Reduced number of features: 1
我们可以使用explained_variance_ratio_
来查看每个组件解释的方差量。在我们的解决方案中,单个组件解释了超过 99%的方差:
lda.explained_variance_ratio_
array([0.9912126])
讨论
LDA 是一个分类技术,也是一种流行的降维技术。LDA 的工作方式类似于 PCA,即将我们的特征空间投影到较低维空间上。然而,在 PCA 中,我们只对最大化数据方差的组件轴感兴趣,而在 LDA 中,我们还有额外的目标是最大化类别之间的差异。在图 9-5 中,我们有包含两个目标类别和两个特征的数据。如果我们将数据投影到 y 轴上,两个类别不容易分开(即它们重叠),而如果我们将数据投影到 x 轴上,我们将得到一个特征向量(即我们通过减少一个维度),它仍然保留了类别可分性。在现实世界中,当然,类别之间的关系会更复杂,维度会更高,但概念保持不变。
图 9-5. LDA 试图最大化我们类别之间的差异
在 scikit-learn 中,LDA 使用LinearDiscriminantAnalysis
实现,其中包括一个参数n_components
,表示我们希望返回的特征数量。要确定n_components
参数值(例如,要保留多少个参数),我们可以利用explained_variance_ratio_
告诉我们每个输出特征解释的方差,并且是一个排序数组。例如:
lda.explained_variance_ratio_
array([0.9912126])
具体地,我们可以运行LinearDiscriminantAnalysis
,将n_components
设置为None
,返回每个组件特征解释的方差比率,然后计算需要多少个组件才能超过一定阈值的方差解释(通常是 0.95 或 0.99):
# Create and run LDA
lda = LinearDiscriminantAnalysis(n_components=None)
features_lda = lda.fit(features, target)
# Create array of explained variance ratios
lda_var_ratios = lda.explained_variance_ratio_
# Create function
def select_n_components(var_ratio, goal_var: float) -> int:
# Set initial variance explained so far
total_variance = 0.0
# Set initial number of features
n_components = 0
# For the explained variance of each feature:
for explained_variance in var_ratio:
# Add the explained variance to the total
total_variance += explained_variance
# Add one to the number of components
n_components += 1
# If we reach our goal level of explained variance
if total_variance >= goal_var:
# End the loop
break
# Return the number of components
return n_components
# Run function
select_n_components(lda_var_ratios, 0.95)
1
参见
9.4 使用矩阵分解减少特征
问题
您有一个非负值的特征矩阵,并希望降低其维度。
解决方案
使用非负矩阵分解(NMF)来降低特征矩阵的维度:
# Load libraries
from sklearn.decomposition import NMF
from sklearn import datasets
# Load the data
digits = datasets.load_digits()
# Load feature matrix
features = digits.data
# Create, fit, and apply NMF
nmf = NMF(n_components=10, random_state=4)
features_nmf = nmf.fit_transform(features)
# Show results
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_nmf.shape[1])
Original number of features: 64
Reduced number of features: 10
讨论
NMF 是一种用于线性降维的无监督技术,分解(即将原始矩阵分解为多个矩阵,其乘积近似原始矩阵)特征矩阵为表示观察和其特征之间潜在关系的矩阵。直观地说,NMF 可以降低维度,因为在矩阵乘法中,两个因子(相乘的矩阵)的维度可以显著少于乘积矩阵。正式地说,给定所需的返回特征数r,NMF 将分解我们的特征矩阵,使之:
这里,V是我们的n × d特征矩阵(即d个特征,n个观察值),W是一个n × r的矩阵,H是一个r × d的矩阵。通过调整r的值,我们可以设置所需的降维量。
NMF 的一个主要要求是,正如其名称所示,特征矩阵不能包含负值。此外,与我们已经研究过的 PCA 和其他技术不同,NMF 不提供输出特征的解释方差。因此,我们找到n_components
的最佳值的最佳方式是尝试一系列值,找到在我们最终模型中产生最佳结果的值(见第十二章)。
参见
9.5 在稀疏数据上减少特征
问题
您有一个稀疏的特征矩阵,并希望降低其维度。
解决方案
使用截断奇异值分解(TSVD):
# Load libraries
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import TruncatedSVD
from scipy.sparse import csr_matrix
from sklearn import datasets
import numpy as np
# Load the data
digits = datasets.load_digits()
# Standardize feature matrix
features = StandardScaler().fit_transform(digits.data)
# Make sparse matrix
features_sparse = csr_matrix(features)
# Create a TSVD
tsvd = TruncatedSVD(n_components=10)
# Conduct TSVD on sparse matrix
features_sparse_tsvd = tsvd.fit(features_sparse).transform(features_sparse)
# Show results
print("Original number of features:", features_sparse.shape[1])
print("Reduced number of features:", features_sparse_tsvd.shape[1])
Original number of features: 64
Reduced number of features: 10
讨论
TSVD 与 PCA 类似,并且事实上,PCA 在其步骤中经常使用非截断的奇异值分解(SVD)。给定d个特征,SVD 将创建d × d的因子矩阵,而 TSVD 将返回n × n的因子,其中n是由参数预先指定的。TSVD 的实际优势在于,与 PCA 不同,它适用于稀疏特征矩阵。
TSVD 的一个问题是:由于它如何使用随机数生成器,输出的符号在拟合过程中可能会翻转。一个简单的解决方法是仅在预处理流水线中使用fit
一次,然后多次使用transform
。
与线性判别分析类似,我们必须指定要输出的特征(组件)的数量。这通过参数n_components
来完成。一个自然的问题是:什么是最佳的组件数量?一种策略是将n_components
作为超参数包含在模型选择中进行优化(即选择使模型训练效果最佳的n_components
值)。另一种方法是因为 TSVD 提供了原始特征矩阵每个组件解释的方差比例,我们可以选择解释所需方差的组件数量(常见值为 95%和 99%)。例如,在我们的解决方案中,前三个输出的组件大约解释了原始数据约 30%的方差:
# Sum of first three components' explained variance ratios
tsvd.explained_variance_ratio_[0:3].sum()
0.3003938537287226
我们可以通过创建一个函数来自动化这个过程,该函数将n_components
设置为原始特征数减一,然后计算解释原始数据所需方差量的组件数量:
# Create and run a TSVD with one less than number of features
tsvd = TruncatedSVD(n_components=features_sparse.shape[1]-1)
features_tsvd = tsvd.fit(features)
# List of explained variances
tsvd_var_ratios = tsvd.explained_variance_ratio_
# Create a function
def select_n_components(var_ratio, goal_var):
# Set initial variance explained so far
total_variance = 0.0
# Set initial number of features
n_components = 0
# For the explained variance of each feature:
for explained_variance in var_ratio:
# Add the explained variance to the total
total_variance += explained_variance
# Add one to the number of components
n_components += 1
# If we reach our goal level of explained variance
if total_variance >= goal_var:
# End the loop
break
# Return the number of components
return n_components
# Run function
select_n_components(tsvd_var_ratios, 0.95)
40
参见
第十章:使用特征选择进行降维
10.0 Introduction
在第九章中,我们讨论了如何通过创建具有(理想情况下)类似能力的新特征来降低特征矩阵的维度。这称为特征提取。在本章中,我们将介绍一种替代方法:选择高质量、信息丰富的特征并丢弃不太有用的特征。这称为特征选择。
有三种特征选择方法:过滤、包装和嵌入。过滤方法通过检查特征的统计属性选择最佳特征。我们明确设置统计量的阈值或手动选择要保留的特征数的方法是通过过滤进行特征选择的示例。包装方法使用试错法找到产生质量预测模型的特征子集。包装方法通常是最有效的,因为它们通过实际试验而非简单的假设来找到最佳结果。最后,嵌入方法在学习算法的培训过程中选择最佳特征子集作为其延伸部分。
理想情况下,我们会在本章节中描述所有三种方法。然而,由于嵌入方法与特定的学习算法紧密相连,要在深入探讨算法本身之前解释它们是困难的。因此,在本章中,我们仅涵盖过滤和包装特征选择方法,将嵌入方法的讨论留到那些深入讨论这些学习算法的章节中。
10.1 数值特征方差阈值法
Problem
您有一组数值特征,并希望过滤掉那些方差低(即可能包含较少信息)的特征。
Solution
选择方差高于给定阈值的特征子集:
# Load libraries
from sklearn import datasets
from sklearn.feature_selection import VarianceThreshold
# Import some data to play with
iris = datasets.load_iris()
# Create features and target
features = iris.data
target = iris.target
# Create thresholder
thresholder = VarianceThreshold(threshold=.5)
# Create high variance feature matrix
features_high_variance = thresholder.fit_transform(features)
# View high variance feature matrix
features_high_variance[0:3]
array([[ 5.1, 1.4, 0.2],
[ 4.9, 1.4, 0.2],
[ 4.7, 1.3, 0.2]])
Discussion
方差阈值法(VT)是一种通过过滤进行特征选择的示例,也是特征选择的最基本方法之一。其动机是低方差特征可能不太有趣(并且不太有用),而高方差特征可能更有趣。VT 首先计算每个特征的方差:
其中 是特征向量, 是单个特征值, 是该特征的平均值。接下来,它删除所有方差未达到该阈值的特征。
在使用 VT 时要牢记两点。首先,方差未居中;即,它位于特征本身的平方单位中。因此,当特征集包含不同单位时(例如,一个特征以年为单位,而另一个特征以美元为单位),VT 将无法正常工作。其次,方差阈值是手动选择的,因此我们必须凭借自己的判断来选择一个合适的值(或者使用第十二章中描述的模型选择技术)。我们可以使用 variances_
查看每个特征的方差:
# View variances
thresholder.fit(features).variances_
array([0.68112222, 0.18871289, 3.09550267, 0.57713289])
最后,如果特征已经标准化(均值为零,方差为单位),那么很显然 VT 将无法正确工作:
# Load library
from sklearn.preprocessing import StandardScaler
# Standardize feature matrix
scaler = StandardScaler()
features_std = scaler.fit_transform(features)
# Caculate variance of each feature
selector = VarianceThreshold()
selector.fit(features_std).variances_
array([1., 1., 1., 1.])
10.2 二进制特征方差的阈值处理
问题
您拥有一组二进制分类特征,并希望过滤掉方差低的特征(即可能包含少量信息)。
解决方案
选择一个伯努利随机变量方差高于给定阈值的特征子集:
# Load library
from sklearn.feature_selection import VarianceThreshold
# Create feature matrix with:
# Feature 0: 80% class 0
# Feature 1: 80% class 1
# Feature 2: 60% class 0, 40% class 1
features = [[0, 1, 0],
[0, 1, 1],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0]]
# Run threshold by variance
thresholder = VarianceThreshold(threshold=(.75 * (1 - .75)))
thresholder.fit_transform(features)
array([[0],
[1],
[0],
[1],
[0]])
讨论
与数值特征类似,选择高信息二分类特征并过滤掉信息较少的策略之一是检查它们的方差。在二进制特征(即伯努利随机变量)中,方差计算如下:
其中 是类 1
观察值的比例。因此,通过设置 ,我们可以移除大多数观察值为一类的特征。
10.3 处理高度相关的特征
问题
您有一个特征矩阵,并怀疑某些特征之间高度相关。
解决方案
使用相关性矩阵检查高度相关特征。如果存在高度相关的特征,请考虑删除其中一个:
# Load libraries
import pandas as pd
import numpy as np
# Create feature matrix with two highly correlated features
features = np.array([[1, 1, 1],
[2, 2, 0],
[3, 3, 1],
[4, 4, 0],
[5, 5, 1],
[6, 6, 0],
[7, 7, 1],
[8, 7, 0],
[9, 7, 1]])
# Convert feature matrix into DataFrame
dataframe = pd.DataFrame(features)
# Create correlation matrix
corr_matrix = dataframe.corr().abs()
# Select upper triangle of correlation matrix
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape),
k=1).astype(bool))
# Find index of feature columns with correlation greater than 0.95
to_drop = [column for column in upper.columns if any(upper[column] > 0.95)]
# Drop features
dataframe.drop(dataframe.columns[to_drop], axis=1).head(3)
0 | 2 | |
---|---|---|
0 | 1 | 1 |
1 | 2 | 0 |
2 | 3 | 1 |
讨论
在机器学习中,我们经常遇到的一个问题是高度相关的特征。如果两个特征高度相关,那么它们所包含的信息非常相似,同时包含这两个特征很可能是多余的。对于像线性回归这样简单的模型,如果不移除这些特征,则违反了线性回归的假设,并可能导致人为膨胀的 R-squared 值。解决高度相关特征的方法很简单:从特征集中删除其中一个特征。通过设置相关性阈值来移除高度相关特征是另一种筛选的例子。
在我们的解决方案中,首先我们创建了所有特征的相关性矩阵:
# Correlation matrix
dataframe.corr()
0 | 1 | 2 | |
---|---|---|---|
0 | 1.000000 | 0.976103 | 0.000000 |
1 | 0.976103 | 1.000000 | -0.034503 |
2 | 0.000000 | -0.034503 | 1.000000 |
接着,我们查看相关性矩阵的上三角来识别高度相关特征的成对:
# Upper triangle of correlation matrix
upper
0 | 1 | 2 | |
---|---|---|---|
0 | NaN | 0.976103 | 0.000000 |
1 | NaN | NaN | 0.034503 |
2 | NaN | NaN | NaN |
其次,我们从这些成对特征中移除一个特征。
10.4 删除分类中无关紧要的特征
问题
您有一个分类目标向量,并希望删除无信息的特征。
解决方案
如果特征是分类的,请计算每个特征与目标向量之间的卡方统计量():
# Load libraries
from sklearn.datasets import load_iris
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2, f_classif
# Load data
iris = load_iris()
features = iris.data
target = iris.target
# Convert to categorical data by converting data to integers
features = features.astype(int)
# Select two features with highest chi-squared statistics
chi2_selector = SelectKBest(chi2, k=2)
features_kbest = chi2_selector.fit_transform(features, target)
# Show results
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_kbest.shape[1])
Original number of features: 4
Reduced number of features: 2
如果特征是数量的,请计算每个特征与目标向量之间的 ANOVA F 值:
# Select two features with highest F-values
fvalue_selector = SelectKBest(f_classif, k=2)
features_kbest = fvalue_selector.fit_transform(features, target)
# Show results
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_kbest.shape[1])
Original number of features: 4
Reduced number of features: 2
而不是选择特定数量的特征,我们可以使用SelectPercentile
来选择顶部n百分比的特征:
# Load library
from sklearn.feature_selection import SelectPercentile
# Select top 75% of features with highest F-values
fvalue_selector = SelectPercentile(f_classif, percentile=75)
features_kbest = fvalue_selector.fit_transform(features, target)
# Show results
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_kbest.shape[1])
Original number of features: 4
Reduced number of features: 3
讨论
卡方统计检验两个分类向量的独立性。也就是说,统计量是类别特征中每个类别的观察次数与如果该特征与目标向量独立(即没有关系)时预期的观察次数之间的差异:
其中是类别中观察到的观测次数,是类别中预期的观测次数。
卡方统计量是一个单一的数字,它告诉您观察计数和在整体人群中如果没有任何关系时预期计数之间的差异有多大。通过计算特征和目标向量之间的卡方统计量,我们可以得到两者之间独立性的度量。如果目标与特征变量无关,那么对我们来说它是无关紧要的,因为它不包含我们可以用于分类的信息。另一方面,如果两个特征高度依赖,它们可能对训练我们的模型非常有信息性。
要在特征选择中使用卡方,我们计算每个特征与目标向量之间的卡方统计量,然后选择具有最佳卡方统计量的特征。在 scikit-learn 中,我们可以使用SelectKBest
来选择它们。参数k
确定我们想要保留的特征数,并过滤掉信息最少的特征。
需要注意的是,卡方统计只能在两个分类向量之间计算。因此,特征选择的卡方要求目标向量和特征都是分类的。然而,如果我们有一个数值特征,我们可以通过首先将定量特征转换为分类特征来使用卡方技术。最后,为了使用我们的卡方方法,所有值都需要是非负的。
或者,如果我们有一个数值特征,我们可以使用f_classif
来计算 ANOVA F 值统计量和每个特征以及目标向量的相关性。F 值分数检查如果我们按照目标向量对数值特征进行分组,每个组的平均值是否显著不同。例如,如果我们有一个二进制目标向量,性别和一个定量特征,测试分数,F 值将告诉我们男性的平均测试分数是否与女性的平均测试分数不同。如果不是,则测试分数对我们预测性别没有帮助,因此该特征是无关的。
10.5 递归消除特征
问题
你想要自动选择保留的最佳特征。
解决方案
使用 scikit-learn 的RFECV
进行递归特征消除(RFE),使用交叉验证(CV)。也就是说,使用包装器特征选择方法,重复训练模型,每次删除一个特征,直到模型性能(例如准确性)变差。剩下的特征就是最好的:
# Load libraries
import warnings
from sklearn.datasets import make_regression
from sklearn.feature_selection import RFECV
from sklearn import datasets, linear_model
# Suppress an annoying but harmless warning
warnings.filterwarnings(action="ignore", module="scipy",
message="^internal gelsd")
# Generate features matrix, target vector, and the true coefficients
features, target = make_regression(n_samples = 10000,
n_features = 100,
n_informative = 2,
random_state = 1)
# Create a linear regression
ols = linear_model.LinearRegression()
# Recursively eliminate features
rfecv = RFECV(estimator=ols, step=1, scoring="neg_mean_squared_error")
rfecv.fit(features, target)
rfecv.transform(features)
array([[ 0.00850799, 0.7031277 , 1.52821875],
[-1.07500204, 2.56148527, -0.44567768],
[ 1.37940721, -1.77039484, -0.74675125],
...,
[-0.80331656, -1.60648007, 0.52231601],
[ 0.39508844, -1.34564911, 0.4228057 ],
[-0.55383035, 0.82880112, 1.73232647]])
一旦我们进行了 RFE,我们就可以看到我们应该保留的特征数量:
# Number of best features
rfecv.n_features_
3
我们还可以看到哪些特征应该保留:
# Which categories are best
rfecv.support_
array([False, False, False, False, False, True, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, True, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False])
我们甚至可以查看特征的排名:
# Rank features best (1) to worst
rfecv.ranking_
array([11, 92, 96, 87, 46, 1, 48, 23, 16, 2, 66, 83, 33, 27, 70, 75, 29,
84, 54, 88, 37, 42, 85, 62, 74, 50, 80, 10, 38, 59, 79, 57, 44, 8,
82, 45, 89, 69, 94, 1, 35, 47, 39, 1, 34, 72, 19, 4, 17, 91, 90,
24, 32, 13, 49, 26, 12, 71, 68, 40, 1, 43, 63, 28, 73, 58, 21, 67,
1, 95, 77, 93, 22, 52, 30, 60, 81, 14, 86, 18, 15, 41, 7, 53, 65,
51, 64, 6, 9, 20, 5, 55, 56, 25, 36, 61, 78, 31, 3, 76])
讨论
这可能是本书到目前为止最复杂的配方,结合了一些我们尚未详细讨论的主题。然而,直觉足够简单,我们可以在这里解释它,而不是推迟到以后的章节。RFE 背后的想法是重复训练模型,每次更新该模型的权重或系数。第一次训练模型时,我们包括所有特征。然后,我们找到具有最小参数的特征(请注意,这假设特征已经重新缩放或标准化),意味着它不太重要,并从特征集中删除该特征。
那么显而易见的问题是:我们应该保留多少特征?我们可以(假设性地)重复此循环,直到我们只剩下一个特征。更好的方法要求我们包括一个新概念叫交叉验证。我们将在下一章详细讨论 CV,但这里是一般的想法。
给定包含(1)我们想要预测的目标和(2)特征矩阵的数据,首先我们将数据分为两组:一个训练集和一个测试集。其次,我们使用训练集训练我们的模型。第三,我们假装不知道测试集的目标,并将我们的模型应用于其特征以预测测试集的值。最后,我们将我们预测的目标值与真实的目标值进行比较,以评估我们的模型。
我们可以使用 CV 找到在 RFE 期间保留的最佳特征数。具体而言,在带有 CV 的 RFE 中,每次迭代后我们使用交叉验证评估我们的模型。如果 CV 显示在我们消除一个特征后模型改善了,那么我们继续下一个循环。然而,如果 CV 显示在我们消除一个特征后模型变差了,我们将该特征重新放回特征集,并选择这些特征作为最佳特征。
在 scikit-learn 中,使用RFECV
实现了带有多个重要参数的 RFE 与 CV。estimator
参数确定我们想要训练的模型类型(例如线性回归),step
参数在每个循环中设置要删除的特征数量或比例,scoring
参数设置我们在交叉验证期间用于评估模型质量的度量标准。
参见
第十一章:模型评估
11.0 介绍
在本章中,我们将探讨评估通过我们的学习算法创建的模型质量的策略。在讨论如何创建它们之前讨论模型评估可能看起来很奇怪,但我们的疯狂之中有一种方法。模型的实用性取决于其预测的质量,因此,从根本上说,我们的目标不是创建模型(这很容易),而是创建高质量的模型(这很难)。因此,在探索多种学习算法之前,让我们首先了解如何评估它们产生的模型。
11.1 交叉验证模型
问题
您希望评估您的分类模型在未预料到的数据上的泛化能力。
解决方案
创建一个管道,对数据进行预处理,训练模型,然后使用交叉验证进行评估:
# Load libraries
from sklearn import datasets
from sklearn import metrics
from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# Load digits dataset
digits = datasets.load_digits()
# Create features matrix
features = digits.data
# Create target vector
target = digits.target
# Create standardizer
standardizer = StandardScaler()
# Create logistic regression object
logit = LogisticRegression()
# Create a pipeline that standardizes, then runs logistic regression
pipeline = make_pipeline(standardizer, logit)
# Create k-fold cross-validation
kf = KFold(n_splits=5, shuffle=True, random_state=0)
# Conduct k-fold cross-validation
cv_results = cross_val_score(pipeline, # Pipeline
features, # Feature matrix
target, # Target vector
cv=kf, # Performance metric
scoring="accuracy", # Loss function
n_jobs=-1) # Use all CPU cores
# Calculate mean
cv_results.mean()
0.969958217270195
讨论
乍一看,评估监督学习模型似乎很简单:训练一个模型,然后使用某种性能指标(准确率、平方误差等)计算其表现。然而,这种方法基本上是有缺陷的。如果我们使用我们的数据训练一个模型,然后评估它在该数据上的表现,我们并没有达到我们的预期目标。我们的目标不是评估模型在训练数据上的表现,而是评估它在从未见过的数据上的表现(例如新客户、新犯罪案件、新图像)。因此,我们的评估方法应该帮助我们理解模型在从未见过的数据上进行预测的能力有多好。
一种策略可能是留出一部分数据用于测试。这被称为验证(或留出法)。在验证中,我们的观察(特征和目标)被分成两个集合,传统上称为训练集和测试集。我们拿出测试集并将其放在一边,假装我们以前从未见过它。接下来,我们使用训练集训练我们的模型,使用特征和目标向量来教模型如何做出最佳预测。最后,我们通过评估模型在测试集上的表现来模拟从未见过的外部数据。然而,验证方法有两个主要弱点。首先,模型的性能可能高度依赖于被选择为测试集的少数观察结果。其次,模型不是在所有可用数据上进行训练,也没有在所有可用数据上进行评估。
克服这些弱点的更好策略被称为k 折交叉验证(KFCV)。在 KFCV 中,我们将数据分成k个部分,称为折叠。然后,模型使用k-1个折叠组成的一个训练集进行训练,然后最后一个折叠被用作测试集。我们重复这个过程k次,每次使用不同的折叠作为测试集。然后对每个k次迭代中模型的表现进行平均,以产生一个总体测量。
在我们的解决方案中,我们使用五折交叉验证并将评估分数输出到 cv_results
中:
# View score for all 5 folds
cv_results
array([0.96111111, 0.96388889, 0.98050139, 0.97214485, 0.97214485])
在使用 KFCV 时有三个重要的注意事项。首先,KFCV 假设每个观察结果都是独立生成的(即数据是独立同分布的[IID])。如果数据是 IID 的,将观察结果随机分配到 fold 时进行洗牌是一个好主意。在 scikit-learn 中,我们可以设置 shuffle=True
来执行洗牌。
其次,在使用 KFCV 评估分类器时,通常有利于每个 fold 中大致包含来自不同目标类的观察结果的相同百分比(称为分层 k 折)。例如,如果我们的目标向量包含性别信息,并且观察结果中有 80% 是男性,那么每个 fold 将包含 80% 的男性和 20% 的女性观察结果。在 scikit-learn 中,我们可以通过将 KFold
类替换为 StratifiedKFold
来执行分层 k 折交叉验证。
最后,在使用验证集或交叉验证时,重要的是基于训练集预处理数据,然后将这些转换应用到训练集和测试集。例如,当我们对标准化对象 standardizer
进行 fit
操作时,我们仅计算训练集的均值和方差。然后,我们使用 transform
将该转换应用于训练集和测试集:
# Import library
from sklearn.model_selection import train_test_split
# Create training and test sets
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Fit standardizer to training set
standardizer.fit(features_train)
# Apply to both training and test sets which can then be used to train models
features_train_std = standardizer.transform(features_train)
features_test_std = standardizer.transform(features_test)
这样做的原因是因为我们假设测试集是未知数据。如果我们使用观察结果来同时拟合训练集和测试集的预处理器,测试集中的部分信息就会泄漏到训练集中。对于任何预处理步骤,如特征选择,都适用这一规则。
scikit-learn 的 pipeline
包使得在使用交叉验证技术时变得更加简单。我们首先创建一个管道来预处理数据(例如 standardizer
),然后训练一个模型(逻辑回归,logit
):
# Create a pipeline
pipeline = make_pipeline(standardizer, logit)
然后,我们使用该管道运行 KFCV,scikit 会为我们完成所有工作:
# Do k-fold cross-validation
cv_results = cross_val_score(pipeline, # Pipeline
features, # Feature matrix
target, # Target vector
cv=kf, # Performance metric
scoring="accuracy", # Loss function
n_jobs=-1) # Use all CPU cores
cross_val_score
带有三个参数,我们还没有讨论过,但值得注意:
cv
cv
确定了我们的交叉验证技术。K 折是目前最常用的,但还有其他方法,例如留一法交叉验证,其中 fold 的数量 k 等于数据集中的数据点数量。
scoring
scoring
定义了成功的度量标准,本章的其他示例中讨论了其中一些。
n_jobs=-1
n_jobs=-1
告诉 scikit-learn 使用所有可用的核心。例如,如果您的计算机有四个核心(笔记本电脑上常见的数量),那么 scikit-learn 将同时使用所有四个核心来加速操作。
一个小提示:当运行其中一些示例时,您可能会看到一个警告,提示“ConvergenceWarning: lbfgs failed to converge.” 这些示例中使用的配置旨在防止这种情况发生,但如果仍然发生,您可以暂时忽略它。我们将在本书后面深入研究具体类型的模型时解决此类问题。
参见
11.2 创建一个基线回归模型
问题
您想要一个简单的基线回归模型,以便与您训练的其他模型进行比较。
解决方案
使用 scikit-learn 的DummyRegressor
创建一个简单的基线模型:
# Load libraries
from sklearn.datasets import load_wine
from sklearn.dummy import DummyRegressor
from sklearn.model_selection import train_test_split
# Load data
wine = load_wine()
# Create features
features, target = wine.data, wine.target
# Make test and training split
features_train, features_test, target_train, target_test = train_test_split(
features, target, random_state=0)
# Create a dummy regressor
dummy = DummyRegressor(strategy='mean')
# "Train" dummy regressor
dummy.fit(features_train, target_train)
# Get R-squared score
dummy.score(features_test, target_test)
-0.0480213580840978
为了比较,我们训练我们的模型并评估性能分数:
# Load library
from sklearn.linear_model import LinearRegression
# Train simple linear regression model
ols = LinearRegression()
ols.fit(features_train, target_train)
# Get R-squared score
ols.score(features_test, target_test)
0.804353263176954
讨论
DummyRegressor
允许我们创建一个非常简单的模型,我们可以用作基线,以与我们训练的任何其他模型进行比较。这通常可以用来模拟产品或系统中的“天真”现有预测过程。例如,产品可能最初被硬编码为假设所有新用户在第一个月内都会花费 100 美元,而不考虑其特征。如果我们将这种假设编码到基线模型中,我们就能够通过比较虚拟模型的score
与训练模型的分数来明确说明使用机器学习方法的好处。
DummyRegressor
使用strategy
参数来设置预测方法,包括在训练集中使用平均值或中位数。此外,如果我们将strategy
设置为constant
并使用constant
参数,我们可以设置虚拟回归器来预测每个观测的某个常数值:
# Create dummy regressor that predicts 1s for everything
clf = DummyRegressor(strategy='constant', constant=1)
clf.fit(features_train, target_train)
# Evaluate score
clf.score(features_test, target_test)
-0.06299212598425186
关于score
的一个小注意事项。默认情况下,score
返回确定系数(R-squared,)得分:
其中是目标观测的真实值,是预测值,而是目标向量的平均值。
越接近 1,目标向量中方差被特征解释的程度就越高。
11.3 创建一个基准分类模型
问题
您想要一个简单的基线分类器来与您的模型进行比较。
解决方案
使用 scikit-learn 的DummyClassifier
:
# Load libraries
from sklearn.datasets import load_iris
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import train_test_split
# Load data
iris = load_iris()
# Create target vector and feature matrix
features, target = iris.data, iris.target
# Split into training and test set
features_train, features_test, target_train, target_test = train_test_split(
features, target, random_state=0)
# Create dummy classifier
dummy = DummyClassifier(strategy='uniform', random_state=1)
# "Train" model
dummy.fit(features_train, target_train)
# Get accuracy score
dummy.score(features_test, target_test)
0.42105263157894735
通过比较基线分类器和我们训练的分类器,我们可以看到改进:
# Load library
from sklearn.ensemble import RandomForestClassifier
# Create classifier
classifier = RandomForestClassifier()
# Train model
classifier.fit(features_train, target_train)
# Get accuracy score
classifier.score(features_test, target_test)
0.9736842105263158
讨论
一个分类器性能的常见测量是它比随机猜测好多少。scikit-learn 的DummyClassifier
使得这种比较变得容易。strategy
参数提供了多种生成值的选项。有两种特别有用的策略。首先,stratified
按训练集目标向量的类比例生成预测(例如,如果训练数据中有 20%的观察结果是女性,则DummyClassifier
将 20%的时间预测为女性)。其次,uniform
将在不同类别之间以均匀随机方式生成预测。例如,如果观察结果中有 20%是女性,80%是男性,则uniform
会生成 50%女性和 50%男性的预测。
参见
11.4 评估二元分类器预测
问题
给定一个训练好的分类模型,你想评估其质量。
解决方案
使用 scikit-learn 的cross_val_score
进行交叉验证,同时使用scoring
参数来定义一系列性能指标,包括准确度、精确度、召回率和F[1]。准确度是一种常见的性能指标。它简单地表示预测正确的观察比例:
其中:
真阳性数量。这些是属于阳性类别(患病、购买产品等)并且我们预测正确的观察结果。
真阴性数量。这些是属于阴性类别(未患病、未购买产品等)并且我们预测正确的观察结果。
假阳性数量,也称为I 型错误。这些是被预测为阳性类别但实际上属于阴性类别的观察结果。
假阴性数量,也称为II 型错误。这些是被预测为阴性类别但实际上属于阳性类别的观察结果。
我们可以通过设置scoring="accuracy"
来在三折(默认折数)交叉验证中测量准确度:
# Load libraries
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
# Generate features matrix and target vector
X, y = make_classification(n_samples = 10000,
n_features = 3,
n_informative = 3,
n_redundant = 0,
n_classes = 2,
random_state = 1)
# Create logistic regression
logit = LogisticRegression()
# Cross-validate model using accuracy
cross_val_score(logit, X, y, scoring="accuracy")
array([0.9555, 0.95 , 0.9585, 0.9555, 0.956 ])
准确率的吸引力在于它有一个直观和简单的英文解释:被正确预测的观测的比例。然而,在现实世界中,通常我们的数据有不平衡的类别(例如,99.9%的观测属于类别 1,只有 0.1%属于类别 2)。当面对不平衡的类别时,准确率会遇到一个悖论,即模型准确率很高,但缺乏预测能力。例如,想象我们试图预测一个在人群中发生率为 0.1%的非常罕见的癌症的存在。在训练完我们的模型后,我们发现准确率为 95%。然而,99.9%的人没有这种癌症:如果我们简单地创建一个“预测”没有人有这种癌症的模型,我们的天真模型将更准确,但显然它不能预测任何事情。因此,我们常常有动机使用其他指标,如精确度、召回率和F[1]分数。
精确度是每个被预测为正类的观测中实际为正类的比例。我们可以将其看作是我们预测中的噪声测量——也就是说,我们在预测某事是正的时候有多大可能是对的。精确度高的模型是悲观的,因为他们仅在非常肯定的情况下预测某个观测属于正类。形式上,精确度是:
# Cross-validate model using precision
cross_val_score(logit, X, y, scoring="precision")
array([0.95963673, 0.94820717, 0.9635996 , 0.96149949, 0.96060606])
召回率是每个真正正例中被正确预测的比例。召回率衡量了模型识别正类观测的能力。召回率高的模型是乐观的,因为他们在预测某个观测属于正类时的门槛很低:
# Cross-validate model using recall
cross_val_score(logit, X, y, scoring="recall")
array([0.951, 0.952, 0.953, 0.949, 0.951])
如果这是你第一次遇到精确度和召回率,如果需要一些时间才能完全理解它们,那是可以理解的。这是准确率的一个缺点;精确度和召回率不太直观。几乎总是我们希望在精确度和召回率之间达到某种平衡,而这种角色由F[1]分数扮演。F[1]分数是调和平均数(一种用于比率的平均数):
这个分数是正预测中实现的正确性的一种度量——也就是说,标记为正的观测中有多少实际上是正的:
# Cross-validate model using F1
cross_val_score(logit, X, y, scoring="f1")
array([0.95529884, 0.9500998 , 0.95827049, 0.95520886, 0.95577889])
讨论
作为评估指标,准确率具有一些有价值的特性,特别是它的直观性。然而,更好的指标通常涉及使用一定平衡的精确度和召回率——也就是说,我们模型的乐观和悲观之间存在一种权衡。F[1]代表着召回率和精确度之间的平衡,其中两者的相对贡献是相等的。
作为使用cross_val_score
的替代方案,如果我们已经有了真实的 y 值和预测的 y 值,我们可以直接计算准确率和召回率等指标:
# Load libraries
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# Create training and test split
X_train, X_test, y_train, y_test = train_test_split(X,
y,
test_size=0.1,
random_state=1)
# Predict values for training target vector
y_hat = logit.fit(X_train, y_train).predict(X_test)
# Calculate accuracy
accuracy_score(y_test, y_hat)
0.947
参见
11.5 评估二元分类器阈值
问题
你想评估一个二元分类器和各种概率阈值。
解决方案
使用接收者操作特征曲线(ROC 曲线)来评估二元分类器的质量。在 scikit-learn 中,我们可以使用roc_curve
来计算每个阈值下的真正例和假正例,然后绘制它们:
# Load libraries
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_curve, roc_auc_score
from sklearn.model_selection import train_test_split
# Create feature matrix and target vector
features, target = make_classification(n_samples=10000,
n_features=10,
n_classes=2,
n_informative=3,
random_state=3)
# Split into training and test sets
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Create classifier
logit = LogisticRegression()
# Train model
logit.fit(features_train, target_train)
# Get predicted probabilities
target_probabilities = logit.predict_proba(features_test)[:,1]
# Create true and false positive rates
false_positive_rate, true_positive_rate, threshold = roc_curve(
target_test,
target_probabilities
)
# Plot ROC curve
plt.title("Receiver Operating Characteristic")
plt.plot(false_positive_rate, true_positive_rate)
plt.plot([0, 1], ls="--")
plt.plot([0, 0], [1, 0] , c=".7"), plt.plot([1, 1] , c=".7")
plt.ylabel("True Positive Rate")
plt.xlabel("False Positive Rate")
plt.show()
讨论
接收者操作特征曲线是评估二元分类器质量的常见方法。ROC 在每个概率阈值(即观测被预测为一类的概率)下比较真正例和假正例的存在。通过绘制 ROC 曲线,我们可以看到模型的表现。一个完全正确预测每个观测的分类器将看起来像前一图中 ROC 输出的实线浅灰色线,立即向顶部直线上升。预测随机的分类器将出现为对角线。模型越好,它距实线越接近。
到目前为止,我们只根据它们预测的值来检查模型。然而,在许多学习算法中,这些预测的值是基于概率估计的。也就是说,每个观测都被赋予属于每个类别的显式概率。在我们的解决方案中,我们可以使用predict_proba
来查看第一个观测的预测概率:
# Get predicted probabilities
logit.predict_proba(features_test)[0:1]
array([[0.86891533, 0.13108467]])
我们可以使用classes_
来查看类别:
logit.classes_
array([0, 1])
在这个例子中,第一个观测有约 87%的概率属于负类(0
),13%的概率属于正类(1
)。默认情况下,scikit-learn 预测如果概率大于 0.5,则观测属于正类(称为阈值)。然而,我们经常希望明确地偏置我们的模型以使用不同的阈值出于实质性原因,而不是中间地带。例如,如果一个假阳性对我们的公司造成很高的成本,我们可能更喜欢一个概率阈值较高的模型。我们未能预测一些正例,但当观测被预测为正例时,我们可以非常确信预测是正确的。这种权衡体现在真正例率(TPR)和假正例率(FPR)中。TPR 是正确预测为真的观测数除以所有真正的正例观测数:
FPR 是错误预测的正例数除以所有真负例观测数:
ROC 曲线代表每个概率阈值下的相应 TPR 和 FPR。例如,在我们的解决方案中,大约 0.50 的阈值具有约 0.83 的 TPR 和约 0.16 的 FPR:
print("Threshold:", threshold[124])
print("True Positive Rate:", true_positive_rate[124])
print("False Positive Rate:", false_positive_rate[124])
Threshold: 0.5008252732632008
True Positive Rate: 0.8346938775510204
False Positive Rate: 0.1607843137254902
然而,如果我们将阈值提高到约 80%(即,在模型预测观测为正类之前,增加其必须确定的程度),TPR 显著下降,但 FPR 也是如此:
print("Threshold:", threshold[49])
print("True Positive Rate:", true_positive_rate[49])
print("False Positive Rate:", false_positive_rate[49])
Threshold: 0.8058575028551827
True Positive Rate: 0.5653061224489796
False Positive Rate: 0.052941176470588235
这是因为我们对预测为正类有更高要求,导致模型未能识别出一些正例(较低的 TPR),但也减少了负面观测被预测为正面的噪声(较低的 FPR)。
除了能够可视化 TPR 和 FPR 之间的权衡之外,ROC 曲线还可以用作模型的一般度量。模型越好,曲线越高,因此曲线下面积也越大。因此,通常计算 ROC 曲线下面积(AUC ROC)来判断模型在所有可能阈值下的总体质量。AUC ROC 越接近 1,模型越好。在 scikit-learn 中,我们可以使用roc_auc_score
来计算 AUC ROC:
# Calculate area under curve
roc_auc_score(target_test, target_probabilities)
0.9073389355742297
参见
11.6 评估多类分类器预测
问题
您有一个预测三个或更多类别的模型,并希望评估模型的性能。
解决方案
使用能够处理两个以上类别的评估指标进行交叉验证:
# Load libraries
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
# Generate features matrix and target vector
features, target = make_classification(n_samples = 10000,
n_features = 3,
n_informative = 3,
n_redundant = 0,
n_classes = 3,
random_state = 1)
# Create logistic regression
logit = LogisticRegression()
# Cross-validate model using accuracy
cross_val_score(logit, features, target, scoring='accuracy')
array([0.841 , 0.829 , 0.8265, 0.8155, 0.82 ])
讨论
当我们有均衡的类别(即目标向量中每个类别的观测值数量大致相等)时,准确率就像在二分类设置中一样,是一种简单且可解释的评估指标选择。准确率是正确预测数量除以观测数量,无论是在多分类还是二分类设置中都同样有效。然而,当我们有不平衡的类别(这是一个常见情况)时,我们应该倾向于使用其他评估指标。
许多 scikit-learn 内置的度量是用于评估二元分类器的。然而,许多这些度量可以扩展用于当我们有两个以上类别时的情况。精确率、召回率和F[1]分数是我们在之前的配方中已经详细介绍过的有用度量。虽然它们都是最初设计用于二元分类器的,但我们可以将它们应用于多类别设置,通过将我们的数据视为一组二元类别来处理。这样做使我们能够将度量应用于每个类别,就好像它是数据中唯一的类别一样,然后通过对所有类别的评估分数进行平均来聚合它们:
# Cross-validate model using macro averaged F1 score
cross_val_score(logit, features, target, scoring='f1_macro')
array([0.84061272, 0.82895312, 0.82625661, 0.81515121, 0.81992692])
这段代码中,macro
指的是用于计算类别评估分数平均值的方法。选项包括macro
、weighted
和micro
:
macro
计算每个类别的度量分数的均值,每个类别的权重相等。
weighted
计算每个类别的度量分数的均值,权重为数据中每个类别的大小。
micro
计算每个观测-类别组合的度量分数的均值。
11.7 可视化分类器的性能
问题
给定测试数据的预测类别和真实类别,您希望直观比较模型的质量。
解决方案
使用混淆矩阵,比较预测类别和真实类别:
# Load libraries
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
import pandas as pd
# Load data
iris = datasets.load_iris()
# Create features matrix
features = iris.data
# Create target vector
target = iris.target
# Create list of target class names
class_names = iris.target_names
# Create training and test set
features_train, features_test, target_train, target_test = train_test_split(
features, target, random_state=2)
# Create logistic regression
classifier = LogisticRegression()
# Train model and make predictions
target_predicted = classifier.fit(features_train,
target_train).predict(features_test)
# Create confusion matrix
matrix = confusion_matrix(target_test, target_predicted)
# Create pandas dataframe
dataframe = pd.DataFrame(matrix, index=class_names, columns=class_names)
# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None, cmap="Blues")
plt.title("Confusion Matrix"), plt.tight_layout()
plt.ylabel("True Class"), plt.xlabel("Predicted Class")
plt.show()
讨论
混淆矩阵是分类器性能的一种简单有效的可视化方式。混淆矩阵的主要优势之一是它们的可解释性。矩阵的每一列(通常可视化为热图)代表预测类别,而每一行显示真实类别。结果是每个单元格都是预测和真实类别的一种可能组合。这可能最好通过一个例子来解释。在解决方案中,左上角的单元格是预测为Iris setosa(由列表示)的观察数量,它们实际上是Iris setosa(由行表示)。这意味着模型准确地预测了所有Iris setosa的花。然而,该模型在预测Iris virginica时并不那么成功。右下角的单元格表示模型成功预测了十一个观察结果为Iris virginica,但(向上查看一个单元格)预测了一个实际上是Iris versicolor的花为virginica。
关于混淆矩阵有三点值得注意。首先,一个完美的模型会在对角线上有数值,其他地方都是零。一个糟糕的模型会使观察计数均匀地分布在单元格周围。其次,混淆矩阵让我们不仅能看到模型错在哪里,还能看到它错在哪里。也就是说,我们可以看到误分类的模式。例如,我们的模型很容易区分Iris virginica和Iris setosa,但在分类Iris virginica和Iris versicolor时稍微困难一些。最后,混淆矩阵适用于任意数量的类别(尽管如果目标向量中有一百万个类别,混淆矩阵的可视化可能会难以阅读)。
参见
11.8 评估回归模型
问题
您想要评估回归模型的性能。
解决方案
使用 均方误差(MSE):
# Load libraries
from sklearn.datasets import make_regression
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LinearRegression
# Generate features matrix, target vector
features, target = make_regression(n_samples = 100,
n_features = 3,
n_informative = 3,
n_targets = 1,
noise = 50,
coef = False,
random_state = 1)
# Create a linear regression object
ols = LinearRegression()
# Cross-validate the linear regression using (negative) MSE
cross_val_score(ols, features, target, scoring='neg_mean_squared_error')
array([-1974.65337976, -2004.54137625, -3935.19355723, -1060.04361386,
-1598.74104702])
另一个常见的回归指标是确定系数,R²:
# Cross-validate the linear regression using R-squared
cross_val_score(ols, features, target, scoring='r2')
array([0.8622399 , 0.85838075, 0.74723548, 0.91354743, 0.84469331])
讨论
MSE 是回归模型中最常见的评估指标之一。形式上,MSE 是:
其中 是观察次数, 是我们试图预测的目标的真实值,对于观察 , 是模型对 的预测值。均方误差(MSE)是所有预测值与真实值之间距离的平方和的度量。MSE 值越高,总体平方误差越大,因此模型越糟糕。平方误差项的数学优势包括强制所有误差值为正,但一个常常未被意识到的影响是,平方会比许多小误差更严厉地惩罚少量大误差,即使这些误差的绝对值相同。例如,想象两个模型,A 和 B,每个模型有两个观察:
-
模型 A 的误差为 0 和 10,因此其 MSE 为 0² + 10² = 100。
-
模型 B 每个误差为 5,因此其 MSE 为 5² + 5² = 50。
两个模型的总误差都是 10;然而,MSE 认为模型 A(MSE = 100)比模型 B(MSE = 50)更差。在实践中,这种影响很少成问题(实际上理论上有益),并且 MSE 作为评估指标运行得非常好。
一个重要的注释:在 scikit-learn 中,默认情况下,scoring
参数假定更高的值优于较低的值。然而,对于 MSE,情况并非如此,较高的值意味着模型较差。因此,scikit-learn 使用 neg_mean_squared_error
参数来观察 负 MSE。
一种常见的替代回归评估指标是我们在 Recipe 11.2 中使用的默认指标 ,它衡量模型解释的目标向量方差量。
其中 是第 i 个观察的真实目标值, 是第 i 个观察的预测值, 是目标向量的均值。当 接近 1.0 时,模型越好。
参见
11.9 评估聚类模型
问题
您已经使用了无监督学习算法来对数据进行聚类。现在您想知道它的表现如何。
解决方案
使用 轮廓系数 来衡量聚类的质量(请注意,这不是衡量预测性能的指标):
# Load libraries
import numpy as np
from sklearn.metrics import silhouette_score
from sklearn import datasets
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
# Generate features matrix
features, _ = make_blobs(n_samples = 1000,
n_features = 10,
centers = 2,
cluster_std = 0.5,
shuffle = True,
random_state = 1)
# Cluster data using k-means to predict classes
model = KMeans(n_clusters=2, random_state=1).fit(features)
# Get predicted classes
target_predicted = model.labels_
# Evaluate model
silhouette_score(features, target_predicted)
0.8916265564072141
讨论
监督模型评估比较预测(例如类别或定量值)与目标向量中对应的真实值。然而,使用聚类方法的最常见动机是你的数据没有目标向量。许多聚类评估指标需要一个目标向量,但是当你有一个可用的目标向量时,再次使用聚类这样的无监督学习方法可能会不必要地束手无策。
如果我们没有目标向量,我们无法评估预测与真实值之间的情况,但是我们可以评估簇本身的特性。直观地,我们可以想象“好”的簇在同一簇内的观察之间有非常小的距离(即密集的簇),而在不同簇之间有很大的距离(即分离良好的簇)。轮廓系数提供了一个单一值,同时衡量了这两个特性。形式上,第i个观察的轮廓系数为:
其中是观察的轮廓系数,是与同一类别所有观察之间的平均距离,是与不同类别最接近的簇中所有观察之间的平均距离。silhouette_score
返回的值是所有观察的平均轮廓系数。轮廓系数的范围在-1 到 1 之间,1 表示密集且分离良好的簇。
参见
11.10 创建自定义评估度量
问题
您希望使用您创建的度量来评估一个模型。
解决方案
创建度量作为一个函数,并使用 scikit-learn 的make_scorer
将其转换为评分器函数:
# Load libraries
from sklearn.metrics import make_scorer, r2_score
from sklearn.model_selection import train_test_split
from sklearn.linear_model import Ridge
from sklearn.datasets import make_regression
# Generate features matrix and target vector
features, target = make_regression(n_samples = 100,
n_features = 3,
random_state = 1)
# Create training set and test set
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.10, random_state=1)
# Create custom metric
def custom_metric(target_test, target_predicted):
# Calculate R-squared score
r2 = r2_score(target_test, target_predicted)
# Return R-squared score
return r2
# Make scorer and define that higher scores are better
score = make_scorer(custom_metric, greater_is_better=True)
# Create ridge regression object
classifier = Ridge()
# Train ridge regression model
model = classifier.fit(features_train, target_train)
# Apply custom scorer
score(model, features_test, target_test)
0.9997906102882058
讨论
虽然 scikit-learn 有许多内置的度量指标来评估模型性能,但通常定义我们自己的度量也很有用。scikit-learn 通过使用make_scorer
使这变得简单。首先,我们定义一个接受两个参数(真实目标向量和我们的预测值)并输出某个分数的函数。其次,我们使用make_scorer
创建一个评分器对象,确保指定高或低分数是可取的(使用greater_is_better
参数)。
在解决方案中,自定义度量(custom_metric
)只是一个玩具示例,因为它简单地包装了一个用于计算R²分数的内置度量。在实际情况中,我们将用我们想要的任何自定义度量替换custom_metric
函数。然而,我们可以通过将结果与 scikit-learn 的r2_score
内置方法进行比较,看到计算R²的自定义度量确实有效:
# Predict values
target_predicted = model.predict(features_test)
# Calculate R-squared score
r2_score(target_test, target_predicted)
0.9997906102882058
参见
11.11 可视化训练集大小的效果
问题
你想要评估训练集中观测数量对某些指标(准确率、F[1] 等)的影响。
解决方案
绘制准确性与训练集大小的图表:
# Load libraries
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_digits
from sklearn.model_selection import learning_curve
# Load data
digits = load_digits()
# Create feature matrix and target vector
features, target = digits.data, digits.target
# Create CV training and test scores for various training set sizes
train_sizes, train_scores, test_scores = learning_curve(# Classifier
RandomForestClassifier(),
# Feature matrix
features,
# Target vector
target,
# Number of folds
cv=10,
# Performance metric
scoring='accuracy',
# Use all computer cores
n_jobs=-1,
# Sizes of 50
# Training set
train_sizes=np.linspace(
0.01,
1.0,
50))
# Create means and standard deviations of training set scores
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
# Create means and standard deviations of test set scores
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)
# Draw lines
plt.plot(train_sizes, train_mean, '--', color="#111111", label="Training score")
plt.plot(train_sizes, test_mean, color="#111111", label="Cross-validation score")
# Draw bands
plt.fill_between(train_sizes, train_mean - train_std,
train_mean + train_std, color="#DDDDDD")
plt.fill_between(train_sizes, test_mean - test_std,
test_mean + test_std, color="#DDDDDD")
# Create plot
plt.title("Learning Curve")
plt.xlabel("Training Set Size"), plt.ylabel("Accuracy Score"),
plt.legend(loc="best")
plt.tight_layout()
plt.show()
讨论
学习曲线 可视化模型在训练集和交叉验证中随着训练集观测数量增加而表现的性能(例如准确率、召回率)。它们通常用于确定我们的学习算法是否会从收集额外的训练数据中受益。
在我们的解决方案中,我们绘制了随机森林分类器在 50 个不同训练集大小上的准确性,范围从观测数据的 1%到 100%。交叉验证模型的逐渐增加的准确性得分告诉我们,我们可能会从额外的观测中受益(尽管在实践中这可能并不可行)。
参见
11.12 创建评估指标的文本报告
问题
你想要一个分类器性能的快速描述。
解决方案
使用 scikit-learn 的 classification_report
:
# Load libraries
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
# Load data
iris = datasets.load_iris()
# Create features matrix
features = iris.data
# Create target vector
target = iris.target
# Create list of target class names
class_names = iris.target_names
# Create training and test set
features_train, features_test, target_train, target_test = train_test_split(
features, target, random_state=0)
# Create logistic regression
classifier = LogisticRegression()
# Train model and make predictions
model = classifier.fit(features_train, target_train)
target_predicted = model.predict(features_test)
# Create a classification report
print(classification_report(target_test,
target_predicted,
target_names=class_names))
precision recall f1-score support
setosa 1.00 1.00 1.00 16
versicolor 1.00 0.91 0.95 11
virginica 0.92 1.00 0.96 11
accuracy 0.97 38
macro avg 0.97 0.97 0.97 38
weighted avg 0.98 0.97 0.97 38
讨论
classification_report
提供了一个快速查看一些常见评估指标(包括精确度、召回率和 F[1] 分数,详见 Recipe 11.4)的方法。支持是每个类别中的观测数量。
参见
11.13 可视化超参数值效果
问题
你想要了解模型在某些超参数值变化时的性能变化。
解决方案
绘制超参数与模型准确性的图表(验证曲线):
# Load libraries
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import validation_curve
# Load data
digits = load_digits()
# Create feature matrix and target vector
features, target = digits.data, digits.target
# Create range of values for parameter
param_range = np.arange(1, 250, 2)
# Calculate accuracy on training and test set using range of parameter values
train_scores, test_scores = validation_curve(
# Classifier
RandomForestClassifier(),
# Feature matrix
features,
# Target vector
target,
# Hyperparameter to examine
param_name="n_estimators",
# Range of hyperparameter's values
param_range=param_range,
# Number of folds
cv=3,
# Performance metric
scoring="accuracy",
# Use all computer cores
n_jobs=-1)
# Calculate mean and standard deviation for training set scores
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
# Calculate mean and standard deviation for test set scores
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)
# Plot mean accuracy scores for training and test sets
plt.plot(param_range, train_mean, label="Training score", color="black")
plt.plot(param_range, test_mean, label="Cross-validation score",
color="dimgrey")
# Plot accuracy bands for training and test sets
plt.fill_between(param_range, train_mean - train_std,
train_mean + train_std, color="gray")
plt.fill_between(param_range, test_mean - test_std,
test_mean + test_std, color="gainsboro")
# Create plot
plt.title("Validation Curve With Random Forest")
plt.xlabel("Number Of Trees")
plt.ylabel("Accuracy Score")
plt.tight_layout()
plt.legend(loc="best")
plt.show()
讨论
大多数训练算法(包括本书涵盖的许多算法)在开始训练过程之前必须选择的超参数。例如,随机森林分类器 创建一个“森林”由决策树组成,每棵树对观测的预测类进行投票。随机森林分类器的一个超参数是森林中的树的数量。通常在模型选择过程中选择超参数值(参见 第十二章)。然而,偶尔可视化模型性能随着超参数值的变化而变化是有用的。在我们的解决方案中,我们绘制了随机森林分类器在训练集和交叉验证中随着树的数量增加而准确性的变化。当我们有少量树时,训练和交叉验证分数都很低,表明模型欠拟合。随着树的数量增加到 250,两者的准确性趋于稳定,表明在训练大量森林的计算成本上可能没有太多价值。
在 scikit-learn 中,我们可以使用 validation_curve
计算验证曲线,其中包含三个重要参数:
param_name
要变化的超参数名称
param_range
要使用的超参数的值
scoring
评估模型的评估指标
参见
第十二章:模型选择
12.0 引言
在机器学习中,我们使用训练算法通过最小化某个损失函数来学习模型的参数。然而,许多学习算法(例如支持向量分类器和随机森林)有额外的 超参数,由用户定义,并影响模型学习其参数的方式。正如我们在本书的前面提到的,参数(有时也称为模型权重)是模型在训练过程中学习的内容,而超参数是我们手动提供的(用户提供的)内容。
例如,随机森林是决策树的集合(因此有 森林 一词);然而,森林中决策树的数量并非由算法学习,而必须在拟合之前设置好。这通常被称为 超参数调优、超参数优化 或 模型选择。此外,我们可能希望尝试多个学习算法(例如尝试支持向量分类器和随机森林,看哪种学习方法产生最佳模型)。
尽管在这个领域术语广泛变化,但在本书中,我们将选择最佳学习算法及其最佳超参数称为模型选择。原因很简单:想象我们有数据,并且想要训练一个支持向量分类器,有 10 个候选超参数值,以及一个随机森林分类器,有 10 个候选超参数值。结果是我们尝试从一组 20 个候选模型中选择最佳模型。在本章中,我们将介绍有效地从候选集中选择最佳模型的技术。
在本章中,我们将提到特定的超参数,比如 C(正则化强度的倒数)。如果你不知道超参数是什么,不要担心。我们将在后面的章节中介绍它们。相反,只需将超参数视为在开始训练之前必须选择的学习算法的设置。通常,找到能够产生最佳性能的模型和相关超参数是实验的结果——尝试各种可能性并找出最佳的那个。
12.1 使用穷举搜索选择最佳模型
问题
你想通过搜索一系列超参数来选择最佳模型。
解决方案
使用 scikit-learn 的 GridSearchCV
:
# Load libraries
import numpy as np
from sklearn import linear_model, datasets
from sklearn.model_selection import GridSearchCV
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create logistic regression
logistic = linear_model.LogisticRegression(max_iter=500, solver='liblinear')
# Create range of candidate penalty hyperparameter values
penalty = ['l1','l2']
# Create range of candidate regularization hyperparameter values
C = np.logspace(0, 4, 10)
# Create dictionary of hyperparameter candidates
hyperparameters = dict(C=C, penalty=penalty)
# Create grid search
gridsearch = GridSearchCV(logistic, hyperparameters, cv=5, verbose=0)
# Fit grid search
best_model = gridsearch.fit(features, target)
# Show the best model
print(best_model.best_estimator_)
LogisticRegression(C=7.742636826811269, max_iter=500, penalty='l1',
solver='liblinear')
讨论
GridSearchCV
是一种使用交叉验证进行模型选择的蛮力方法。具体来说,用户定义一个或多个超参数可能的值集合,然后 GridSearchCV
使用每个值和/或值组合来训练模型。选择具有最佳性能得分的模型作为最佳模型。
例如,在我们的解决方案中,我们使用逻辑回归作为我们的学习算法,并调整了两个超参数:C 和正则化惩罚。我们还指定了另外两个参数,解算器和最大迭代次数。如果您不知道这些术语的含义也没关系;我们将在接下来的几章中详细讨论它们。只需意识到 C 和正则化惩罚可以取一系列值,这些值在训练之前必须指定。对于 C,我们定义了 10 个可能的值:
np.logspace(0, 4, 10)
array([1.00000000e+00, 2.78255940e+00, 7.74263683e+00, 2.15443469e+01,
5.99484250e+01, 1.66810054e+02, 4.64158883e+02, 1.29154967e+03,
3.59381366e+03, 1.00000000e+04])
类似地,我们定义了两个正则化惩罚的可能值:['l1', 'l2']
。对于每个 C 和正则化惩罚值的组合,我们训练模型并使用 k 折交叉验证进行评估。在我们的解决方案中,C 有 10 个可能的值,正则化惩罚有 2 个可能的值,并且使用 5 折交叉验证。它们创建了 10 × 2 × 5 = 100 个候选模型,其中选择最佳模型。
一旦完成GridSearchCV
,我们可以看到最佳模型的超参数:
# View best hyperparameters
print('Best Penalty:', best_model.best_estimator_.get_params()['penalty'])
print('Best C:', best_model.best_estimator_.get_params()['C'])
Best Penalty: l1
Best C: 7.742636826811269
默认情况下,确定了最佳超参数后,GridSearchCV
会在整个数据集上重新训练一个模型(而不是留出一个折用于交叉验证)。我们可以像对待其他 scikit-learn 模型一样使用该模型来预测值:
# Predict target vector
best_model.predict(features)
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
GridSearchCV
的一个参数值得注意:verbose
。虽然大多数情况下不需要,但在长时间搜索过程中,接收到搜索正在进行中的指示可能会让人放心。verbose
参数确定了搜索过程中输出消息的数量,0
表示没有输出,而1
到3
表示额外的输出消息。
参见
12.2 使用随机搜索选择最佳模型
问题
您希望选择最佳模型的计算成本较低的方法。
解决方案
使用 scikit-learn 的RandomizedSearchCV
:
# Load libraries
from scipy.stats import uniform
from sklearn import linear_model, datasets
from sklearn.model_selection import RandomizedSearchCV
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create logistic regression
logistic = linear_model.LogisticRegression(max_iter=500, solver='liblinear')
# Create range of candidate regularization penalty hyperparameter values
penalty = ['l1', 'l2']
# Create distribution of candidate regularization hyperparameter values
C = uniform(loc=0, scale=4)
# Create hyperparameter options
hyperparameters = dict(C=C, penalty=penalty)
# Create randomized search
randomizedsearch = RandomizedSearchCV(
logistic, hyperparameters, random_state=1, n_iter=100, cv=5, verbose=0,
n_jobs=-1)
# Fit randomized search
best_model = randomizedsearch.fit(features, target)
# Print best model
print(best_model.best_estimator_)
LogisticRegression(C=1.668088018810296, max_iter=500, penalty='l1',
solver='liblinear')
讨论
在 Recipe 12.1 中,我们使用GridSearchCV
在用户定义的一组超参数值上搜索最佳模型,根据评分函数。比GridSearchCV
的蛮力搜索更高效的方法是从用户提供的分布(例如正态分布、均匀分布)中随机组合一定数量的超参数值进行搜索。scikit-learn 使用RandomizedSearchCV
实现了这种随机搜索技术。
使用RandomizedSearchCV
,如果我们指定一个分布,scikit-learn 将从该分布中随机抽样且不重复地抽取超参数值。例如,这里我们从范围为 0 到 4 的均匀分布中随机抽取 10 个值作为一般概念的示例:
# Define a uniform distribution between 0 and 4, sample 10 values
uniform(loc=0, scale=4).rvs(10)
array([3.95211699, 0.30693116, 2.88237794, 3.00392864, 0.43964702,
1.46670526, 0.27841863, 2.56541664, 2.66475584, 0.79611958])
或者,如果我们指定一个值列表,例如两个正则化惩罚超参数值['l1', 'l2']
,RandomizedSearchCV
将从列表中进行带替换的随机抽样。
就像GridSearchCV
一样,我们可以看到最佳模型的超参数值:
# View best hyperparameters
print('Best Penalty:', best_model.best_estimator_.get_params()['penalty'])
print('Best C:', best_model.best_estimator_.get_params()['C'])
Best Penalty: l1
Best C: 1.668088018810296
就像使用GridSearchCV
一样,在完成搜索后,RandomizedSearchCV
会使用最佳超参数在整个数据集上拟合一个新模型。我们可以像使用 scikit-learn 中的任何其他模型一样使用这个模型;例如,进行预测:
# Predict target vector
best_model.predict(features)
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2,
2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
超参数组合的采样数量(即训练的候选模型数量)由n_iter
(迭代次数)设置指定。值得注意的是,RandomizedSearchCV
并不比GridSearchCV
更快,但通常在较短时间内通过测试更少的组合来实现与GridSearchCV
可比较的性能。
参见
12.3 从多个学习算法中选择最佳模型
问题
通过在一系列学习算法及其相应的超参数上进行搜索,您可以选择最佳模型。
解决方案
创建一个包含候选学习算法及其超参数的字典,作为GridSearchCV
的搜索空间:
# Load libraries
import numpy as np
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
# Set random seed
np.random.seed(0)
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create a pipeline
pipe = Pipeline([("classifier", RandomForestClassifier())])
# Create dictionary with candidate learning algorithms and their hyperparameters
search_space = [{"classifier": [LogisticRegression(max_iter=500,
solver='liblinear')],
"classifier__penalty": ['l1', 'l2'],
"classifier__C": np.logspace(0, 4, 10)},
{"classifier": [RandomForestClassifier()],
"classifier__n_estimators": [10, 100, 1000],
"classifier__max_features": [1, 2, 3]}]
# Create grid search
gridsearch = GridSearchCV(pipe, search_space, cv=5, verbose=0)
# Fit grid search
best_model = gridsearch.fit(features, target)
# Print best model
print(best_model.best_estimator_)
Pipeline(steps=[('classifier',
LogisticRegression(C=7.742636826811269, max_iter=500,
penalty='l1', solver='liblinear'))])
讨论
在前两个示例中,我们通过搜索学习算法的可能超参数值来找到最佳模型。但是,如果我们不确定要使用哪种学习算法怎么办?scikit-learn 允许我们将学习算法作为搜索空间的一部分。在我们的解决方案中,我们定义了一个搜索空间,其中包含两个学习算法:逻辑回归和随机森林分类器。每个学习算法都有自己的超参数,并且我们使用classifier__[*hyperparameter name*]
的格式定义其候选值。例如,对于我们的逻辑回归,为了定义可能的正则化超参数空间C
的可能值集合以及潜在的正则化惩罚类型penalty
,我们创建一个字典:
{'classifier': [LogisticRegression(max_iter=500, solver='liblinear')],
'classifier__penalty': ['l1', 'l2'],
'classifier__C': np.logspace(0, 4, 10)}
我们也可以为随机森林的超参数创建一个类似的字典:
{'classifier': [RandomForestClassifier()],
'classifier__n_estimators': [10, 100, 1000],
'classifier__max_features': [1, 2, 3]}
完成搜索后,我们可以使用 best_estimator_
查看最佳模型的学习算法和超参数:
# View best model
print(best_model.best_estimator_.get_params()["classifier"])
LogisticRegression(C=7.742636826811269, max_iter=500, penalty='l1',
solver='liblinear')
就像前面两个示例一样,一旦我们完成了模型选择搜索,我们就可以像使用任何其他 scikit-learn 模型一样使用这个最佳模型:
# Predict target vector
best_model.predict(features)
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
12.4 在预处理时选择最佳模型
问题
您希望在模型选择过程中包含一个预处理步骤。
解决方案
创建一个包含预处理步骤及其任何参数的管道:
# Load libraries
import numpy as np
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# Set random seed
np.random.seed(0)
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create a preprocessing object that includes StandardScaler features and PCA
preprocess = FeatureUnion([("std", StandardScaler()), ("pca", PCA())])
# Create a pipeline
pipe = Pipeline([("preprocess", preprocess),
("classifier", LogisticRegression(max_iter=1000,
solver='liblinear'))])
# Create space of candidate values
search_space = [{"preprocess__pca__n_components": [1, 2, 3],
"classifier__penalty": ["l1", "l2"],
"classifier__C": np.logspace(0, 4, 10)}]
# Create grid search
clf = GridSearchCV(pipe, search_space, cv=5, verbose=0, n_jobs=-1)
# Fit grid search
best_model = clf.fit(features, target)
# Print best model
print(best_model.best_estimator_)
Pipeline(steps=[('preprocess',
FeatureUnion(transformer_list=[('std', StandardScaler()),
('pca', PCA(n_components=1))])),
('classifier',
LogisticRegression(C=7.742636826811269, max_iter=1000,
penalty='l1', solver='liblinear'))])
讨论
在使用数据训练模型之前,我们通常需要对数据进行预处理。在进行模型选择时,我们必须小心处理预处理。首先,GridSearchCV
使用交叉验证确定哪个模型性能最高。然而,在交叉验证中,我们实际上是在假装保留为测试集的折叠是不可见的,因此不是拟合任何预处理步骤的一部分(例如,缩放或标准化)。因此,我们不能对数据进行预处理然后运行GridSearchCV
。相反,预处理步骤必须成为GridSearchCV
采取的操作集的一部分。
这可能看起来很复杂,但 scikit-learn 让它变得简单。FeatureUnion
允许我们正确地组合多个预处理操作。在我们的解决方案中,我们使用FeatureUnion
来组合两个预处理步骤:标准化特征值(StandardScaler
)和主成分分析(PCA
)。该对象称为preprocess
,包含我们的两个预处理步骤。然后,我们将preprocess
包含在一个管道中与我们的学习算法一起。结果是,这使我们能够将拟合、转换和训练模型与超参数组合的正确(而令人困惑的)处理外包给 scikit-learn。
第二,一些预处理方法有它们自己的参数,通常需要用户提供。例如,使用 PCA 进行降维需要用户定义要使用的主成分数量,以产生转换后的特征集。理想情况下,我们会选择产生在某个评估测试指标下性能最佳模型的组件数量。
幸运的是,scikit-learn 使这变得容易。当我们在搜索空间中包含候选组件值时,它们被视为要搜索的任何其他超参数。在我们的解决方案中,我们在搜索空间中定义了features__pca__n_components': [1, 2, 3]
,以指示我们要发现一个、两个或三个主成分是否产生最佳模型。
在模型选择完成后,我们可以查看产生最佳模型的预处理值。例如,我们可以查看最佳的主成分数量:
# View best n_components
best_model.best_estimator_.get_params()['preprocess__pca__n_components']
1
12.5 使用并行化加快模型选择速度
问题
需要加快模型选择速度。
解决方案
通过设置n_jobs=-1
来利用机器中的所有核心,从而使您能够同时训练多个模型:
# Load libraries
import numpy as np
from sklearn import linear_model, datasets
from sklearn.model_selection import GridSearchCV
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create logistic regression
logistic = linear_model.LogisticRegression(max_iter=500, solver='liblinear')
# Create range of candidate regularization penalty hyperparameter values
penalty = ["l1", "l2"]
# Create range of candidate values for C
C = np.logspace(0, 4, 1000)
# Create hyperparameter options
hyperparameters = dict(C=C, penalty=penalty)
# Create grid search
gridsearch = GridSearchCV(logistic, hyperparameters, cv=5, n_jobs=-1, verbose=1)
# Fit grid search
best_model = gridsearch.fit(features, target)
# Print best model
print(best_model.best_estimator_)
Fitting 5 folds for each of 2000 candidates, totalling 10000 fits
LogisticRegression(C=5.926151812475554, max_iter=500, penalty='l1',
solver='liblinear')
讨论
在本章的示例中,我们将候选模型的数量保持较少,以使代码迅速完整。但是,在现实世界中,我们可能有成千上万甚至成千上万个模型要训练。因此,找到最佳模型可能需要花费很多小时。
为了加快这一过程,scikit-learn 允许我们同时训练多个模型。不深入技术细节,scikit-learn 可以同时训练多达机器上的核心数量的模型。现代大多数笔记本电脑至少有四个核心,因此(假设您当前使用的是笔记本电脑),我们可以同时训练四个模型。这将大大增加我们模型选择过程的速度。参数 n_jobs
定义了并行训练的模型数量。
在我们的解决方案中,我们将 n_jobs
设置为 -1
,这告诉 scikit-learn 使用 所有 核心。然而,默认情况下 n_jobs
被设置为 1
,这意味着它只使用一个核心。为了演示这一点,如果我们像在解决方案中一样运行相同的 GridSearchCV
,但使用 n_jobs=1
,我们可以看到找到最佳模型要花费显著更长的时间(确切时间取决于您的计算机):
# Create grid search using one core
clf = GridSearchCV(logistic, hyperparameters, cv=5, n_jobs=1, verbose=1)
# Fit grid search
best_model = clf.fit(features, target)
# Print best model
print(best_model.best_estimator_)
Fitting 5 folds for each of 2000 candidates, totalling 10000 fits
LogisticRegression(C=5.926151812475554, max_iter=500, penalty='l1',
solver='liblinear')
12.6 使用算法特定方法加速模型选择
问题
您需要加速模型选择,但不使用额外的计算资源。
解决方案
如果您正在使用一些特定的学习算法,请使用 scikit-learn 的模型特定的交叉验证超参数调整,例如 LogisticRegressionCV
:
# Load libraries
from sklearn import linear_model, datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create cross-validated logistic regression
logit = linear_model.LogisticRegressionCV(Cs=100, max_iter=500,
solver='liblinear')
# Train model
logit.fit(features, target)
# Print model
print(logit)
LogisticRegressionCV(Cs=100, max_iter=500, solver='liblinear')
讨论
有时候学习算法的特性使我们能够比蛮力或随机模型搜索方法显著更快地搜索最佳超参数。在 scikit-learn 中,许多学习算法(例如岭回归、套索回归和弹性网络回归)都有一种特定于算法的交叉验证方法,以利用这一点。例如,LogisticRegression
用于进行标准的逻辑回归分类器,而 LogisticRegressionCV
实现了一个高效的交叉验证逻辑回归分类器,可以识别超参数 C 的最佳值。
scikit-learn 的 LogisticRegressionCV
方法包括参数 Cs
。如果提供了一个列表,Cs
包含要从中选择的候选超参数值。如果提供了一个整数,参数 Cs
将生成该数量的候选值列表。候选值从 0.0001 到 10,0000 的对数范围内抽取(这是 C 的合理值范围)。
然而,LogisticRegressionCV
的一个主要缺点是它只能搜索 C 的一系列值。在 配方 12.1 中,我们的可能超参数空间包括 C 和另一个超参数(正则化惩罚范数)。这种限制是许多 scikit-learn 模型特定的交叉验证方法的共同特点。
参见
12.7 在模型选择后评估性能
问题
您希望评估通过模型选择找到的模型的性能。
解决方案
使用嵌套交叉验证以避免偏倚评估:
# Load libraries
import numpy as np
from sklearn import linear_model, datasets
from sklearn.model_selection import GridSearchCV, cross_val_score
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create logistic regression
logistic = linear_model.LogisticRegression(max_iter=500, solver='liblinear')
# Create range of 20 candidate values for C
C = np.logspace(0, 4, 20)
# Create hyperparameter options
hyperparameters = dict(C=C)
# Create grid search
gridsearch = GridSearchCV(logistic, hyperparameters, cv=5, n_jobs=-1, verbose=0)
# Conduct nested cross-validation and output the average score
cross_val_score(gridsearch, features, target).mean()
0.9733333333333334
讨论
在模型选择过程中的嵌套交叉验证对许多人来说是一个难以理解的概念。请记住,在 k 折交叉验证中,我们在数据的 k-1 折上训练模型,使用该模型对剩余的一折进行预测,然后评估我们的模型预测与真实值的比较。然后我们重复这个过程 k 次。
在本章描述的模型选择搜索中(即 GridSearchCV
和 RandomizedSearchCV
),我们使用交叉验证来评估哪些超参数值产生了最佳模型。然而,一个微妙且通常被低估的问题出现了:因为我们使用数据来选择最佳的超参数值,所以我们不能再使用同样的数据来评估模型的性能。解决方案是?将用于模型搜索的交叉验证包装在另一个交叉验证中!在嵌套交叉验证中,“内部”交叉验证选择最佳模型,而“外部”交叉验证提供了模型性能的无偏评估。在我们的解决方案中,内部交叉验证是我们的 GridSearchCV
对象,然后我们使用 cross_val_score
将其包装在外部交叉验证中。
如果你感到困惑,可以尝试一个简单的实验。首先,设置 verbose=1
,这样我们可以看到发生了什么:
gridsearch = GridSearchCV(logistic, hyperparameters, cv=5, verbose=1)
接下来,运行 gridsearch.fit(features, target)
,这是我们用来找到最佳模型的内部交叉验证:
best_model = gridsearch.fit(features, target)
Fitting 5 folds for each of 20 candidates, totalling 100 fits
从输出中可以看出,内部交叉验证训练了 20 个候选模型五次,总计 100 个模型。接下来,将 clf
嵌套在一个新的交叉验证中,默认为五折:
scores = cross_val_score(gridsearch, features, target)
Fitting 5 folds for each of 20 candidates, totalling 100 fits
Fitting 5 folds for each of 20 candidates, totalling 100 fits
Fitting 5 folds for each of 20 candidates, totalling 100 fits
Fitting 5 folds for each of 20 candidates, totalling 100 fits
Fitting 5 folds for each of 20 candidates, totalling 100 fits
输出显示,内部交叉验证训练了 20 个模型五次,以找到最佳模型,然后使用外部五折交叉验证评估了该模型,总共训练了 500 个模型。
第十三章:线性回归
13.0 引言
线性回归是我们工具箱中最简单的监督学习算法之一。如果您曾经在大学里修过入门统计课程,很可能您最后学到的主题就是线性回归。线性回归及其扩展在当目标向量是定量值(例如房价、年龄)时继续是一种常见且有用的预测方法。在本章中,我们将涵盖多种线性回归方法(及其扩展)来创建性能良好的预测模型。
13.1 拟合一条线
问题
您希望训练一个能够表示特征和目标向量之间线性关系的模型。
解决方案
使用线性回归(在 scikit-learn 中,LinearRegression
):
# Load libraries
from sklearn.linear_model import LinearRegression
from sklearn.datasets import make_regression
# Generate features matrix, target vector
features, target = make_regression(n_samples = 100,
n_features = 3,
n_informative = 2,
n_targets = 1,
noise = 0.2,
coef = False,
random_state = 1)
# Create linear regression
regression = LinearRegression()
# Fit the linear regression
model = regression.fit(features, target)
讨论
线性回归假设特征与目标向量之间的关系大致是线性的。也就是说,特征对目标向量的效果(也称为系数、权重或参数)是恒定的。为了解释起见,在我们的解决方案中,我们只使用了三个特征来训练我们的模型。这意味着我们的线性模型将是:
这里, 是我们的目标, 是单个特征的数据,,和是通过拟合模型确定的系数,是误差。在拟合模型后,我们可以查看每个参数的值。例如,,也称为偏差或截距,可以使用intercept_
查看:
# View the intercept
model.intercept_
-0.009650118178816669
而coef_
显示了和:
# View the feature coefficients
model.coef_
array([1.95531234e-02, 4.42087450e+01, 5.81494563e+01])
在我们的数据集中,目标值是一个随机生成的连续变量:
# First value in the target vector
target[0]
-20.870747595269407
使用predict
方法,我们可以根据输入特征预测输出:
# Predict the target value of the first observation
model.predict(features)[0]
-20.861927709296808
不错!我们的模型只偏离了约 0.01!
线性回归的主要优势在于其可解释性,这在很大程度上是因为模型的系数是目标向量一单位变化的影响。我们模型的第一个特征的系数约为~–0.02,这意味着我们在第一个特征每增加一个单位时目标的变化。
使用score
函数,我们还可以看到我们的模型在数据上的表现:
# Print the score of the model on the training data
print(model.score(features, target))
0.9999901732607787
scikit learn 中线性回归的默认得分是 R²,范围从 0.0(最差)到 1.0(最好)。正如我们在这个例子中所看到的,我们非常接近完美值 1.0。然而值得注意的是,我们是在模型已经见过的数据(训练数据)上评估该模型,而通常我们会在一个独立的测试数据集上进行评估。尽管如此,在实际情况下,这样高的分数对我们的模型是个好兆头。
13.2 处理交互效应
问题
你有一个特征,其对目标变量的影响取决于另一个特征。
解决方案
创建一个交互项来捕获这种依赖关系,使用 scikit-learn 的 PolynomialFeatures
:
# Load libraries
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.datasets import make_regression
# Generate features matrix, target vector
features, target = make_regression(n_samples = 100,
n_features = 2,
n_informative = 2,
n_targets = 1,
noise = 0.2,
coef = False,
random_state = 1)
# Create interaction term
interaction = PolynomialFeatures(
degree=3, include_bias=False, interaction_only=True)
features_interaction = interaction.fit_transform(features)
# Create linear regression
regression = LinearRegression()
# Fit the linear regression
model = regression.fit(features_interaction, target)
讨论
有时,一个特征对目标变量的影响至少部分依赖于另一个特征。例如,想象一个简单的基于咖啡的例子,我们有两个二进制特征——是否加糖(sugar
)和是否搅拌(stirred
)——我们想预测咖啡是否甜。仅仅加糖(sugar=1, stirred=0
)不会使咖啡变甜(所有的糖都在底部!),仅仅搅拌咖啡而不加糖(sugar=0, stirred=1
)也不会使其变甜。实际上,是将糖放入咖啡并搅拌(sugar=1, stirred=1
)才能使咖啡变甜。sugar
和 stirred
对甜味的影响是相互依赖的。在这种情况下,我们称 sugar
和 stirred
之间存在交互效应。
我们可以通过包含一个新特征来考虑交互效应,该特征由交互特征的相应值的乘积组成:
其中 和 分别是 sugar
和 stirred
的值, 表示两者之间的交互作用。
在我们的解决方案中,我们使用了一个只包含两个特征的数据集。以下是每个特征的第一个观察值:
# View the feature values for first observation
features[0]
array([0.0465673 , 0.80186103])
要创建一个交互项,我们只需为每个观察值将这两个值相乘:
# Import library
import numpy as np
# For each observation, multiply the values of the first and second feature
interaction_term = np.multiply(features[:, 0], features[:, 1])
我们可以看到第一次观察的交互项:
# View interaction term for first observation
interaction_term[0]
0.037340501965846186
然而,虽然我们经常有充分的理由相信两个特征之间存在交互作用,但有时我们也没有。在这些情况下,使用 scikit-learn 的 PolynomialFeatures
为所有特征组合创建交互项会很有用。然后,我们可以使用模型选择策略来识别产生最佳模型的特征组合和交互项。
要使用PolynomialFeatures
创建交互项,我们需要设置三个重要的参数。最重要的是,interaction_only=True
告诉PolynomialFeatures
仅返回交互项(而不是多项式特征,我们将在 Recipe 13.3 中讨论)。默认情况下,PolynomialFeatures
会添加一个名为bias的包含 1 的特征。我们可以通过include_bias=False
来防止这种情况发生。最后,degree
参数确定从中创建交互项的特征的最大数量(以防我们想要创建的交互项是三个特征的组合)。我们可以通过检查我们的解决方案中PolynomialFeatures
的输出,看看第一个观察值的特征值和交互项值是否与我们手动计算的版本匹配:
# View the values of the first observation
features_interaction[0]
array([0.0465673 , 0.80186103, 0.0373405 ])
13.3 拟合非线性关系
问题
您希望对非线性关系进行建模。
解决方案
通过在线性回归模型中包含多项式特征来创建多项式回归:
# Load library
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.datasets import make_regression
# Generate features matrix, target vector
features, target = make_regression(n_samples = 100,
n_features = 3,
n_informative = 2,
n_targets = 1,
noise = 0.2,
coef = False,
random_state = 1)
# Create polynomial features x² and x³
polynomial = PolynomialFeatures(degree=3, include_bias=False)
features_polynomial = polynomial.fit_transform(features)
# Create linear regression
regression = LinearRegression()
# Fit the linear regression
model = regression.fit(features_polynomial, target)
讨论
到目前为止,我们只讨论了建模线性关系。线性关系的一个例子是建筑物的层数与建筑物的高度之间的关系。在线性回归中,我们假设层数和建筑物高度的影响大致是恒定的,这意味着一个 20 层的建筑物大致会比一个 10 层的建筑物高出两倍,而一个 5 层的建筑物大致会比一个 10 层的建筑物高出两倍。然而,许多感兴趣的关系并不严格是线性的。
我们经常希望建模非线性关系,例如学生学习时间与她在考试中得分之间的关系。直觉上,我们可以想象,对于一个小时的学习和没有学习的学生之间的考试成绩差异很大。然而,在学习时间增加到 99 小时和 100 小时之间时,学生的考试成绩差异就会变得很小。随着学习小时数的增加,一个小时的学习对学生考试成绩的影响逐渐减小。
多项式回归是线性回归的扩展,允许我们建模非线性关系。要创建多项式回归,将我们在 Recipe 13.1 中使用的线性函数转换为多项式函数:
通过添加多项式特征将线性回归模型扩展为多项式函数:
其中是多项式的次数。我们如何能够对非线性函数使用线性回归?答案是我们不改变线性回归拟合模型的方式,而只是添加多项式特征。也就是说,线性回归并不“知道”是的二次转换,它只是将其视为另一个变量。
可能需要更实际的描述。为了建模非线性关系,我们可以创建将现有特征 提升到某个幂次的新特征: 、 等。我们添加的这些新特征越多,模型创建的“线”就越灵活。为了更加明确,想象我们想要创建一个三次多项式。为了简单起见,我们将专注于数据集中的第一个观察值:
# View first observation
features[0]
array([-0.61175641])
要创建一个多项式特征,我们将第一个观察值的值提升到二次方,:
# View first observation raised to the second power, x²
features[0]**2
array([0.37424591])
这将是我们的新功能。然后,我们还将第一个观察值的值提升到三次方,:
# View first observation raised to the third power, x³
features[0]**3
array([-0.22894734])
通过在我们的特征矩阵中包含所有三个特征(、 和 )并运行线性回归,我们进行了多项式回归:
# View the first observation's values for x, x², and x³
features_polynomial[0]
array([-0.61175641, 0.37424591, -0.22894734])
PolynomialFeatures
有两个重要参数。首先,degree
确定多项式特征的最大次数。例如,degree=3
会生成 和 。其次,默认情况下 PolynomialFeatures
包括一个只包含 1 的特征(称为偏差)。我们可以通过设置 include_bias=False
来删除它。
13.4 通过正则化减少方差
问题
您希望减少线性回归模型的方差。
解决方案
使用包含收缩惩罚(也称为正则化)的学习算法,例如岭回归和拉索回归:
# Load libraries
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_regression
# Generate features matrix, target vector
features, target = make_regression(n_samples = 100,
n_features = 3,
n_informative = 2,
n_targets = 1,
noise = 0.2,
coef = False,
random_state = 1)
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create ridge regression with an alpha value
regression = Ridge(alpha=0.5)
# Fit the linear regression
model = regression.fit(features_standardized, target)
讨论
在标准线性回归中,模型训练以最小化真实值()与预测值()目标值或残差平方和(RSS)之间的平方误差:
正则化回归学习者类似,除了它们试图最小化 RSS 和 系数值总大小的某种惩罚,称为收缩惩罚,因为它试图“收缩”模型。线性回归的两种常见类型的正则化学习者是岭回归和拉索。唯一的形式上的区别是使用的收缩惩罚类型。在岭回归中,收缩惩罚是一个调整超参数,乘以所有系数的平方和:
其中是第个特征的系数,是一个超参数(接下来会讨论)。Lasso则类似,只是收缩惩罚是一个调整的超参数,乘以所有系数的绝对值的和:
其中是观察数。那么我们应该使用哪一个?作为一个非常一般的经验法则,岭回归通常比 lasso 产生稍微更好的预测,但 lasso(我们将在 Recipe 13.5 中讨论原因)产生更可解释的模型。如果我们希望在岭回归和 lasso 的惩罚函数之间取得平衡,我们可以使用弹性网,它只是一个包含两种惩罚的回归模型。无论我们使用哪一个,岭回归和 lasso 回归都可以通过将系数值包括在我们试图最小化的损失函数中来对大或复杂的模型进行惩罚。
超参数让我们控制对系数的惩罚程度,较高的值会创建更简单的模型。理想的值应像其他超参数一样进行调整。在 scikit-learn 中,可以使用alpha
参数设置。
scikit-learn 包含一个RidgeCV
方法,允许我们选择理想的值:
# Load library
from sklearn.linear_model import RidgeCV
# Create ridge regression with three alpha values
regr_cv = RidgeCV(alphas=[0.1, 1.0, 10.0])
# Fit the linear regression
model_cv = regr_cv.fit(features_standardized, target)
# View coefficients
model_cv.coef_
array([1.29223201e-02, 4.40972291e+01, 5.38979372e+01])
我们可以轻松查看最佳模型的值:
# View alpha
model_cv.alpha_
0.1
最后一点:因为在线性回归中系数的值部分由特征的尺度确定,在正则化模型中所有系数都被合并在一起,因此在训练之前必须确保对特征进行标准化。
13.5 使用 Lasso 回归减少特征
问题
您希望通过减少特征来简化您的线性回归模型。
解决方案
使用 lasso 回归:
# Load library
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_regression
# Generate features matrix, target vector
features, target = make_regression(n_samples = 100,
n_features = 3,
n_informative = 2,
n_targets = 1,
noise = 0.2,
coef = False,
random_state = 1)
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create lasso regression with alpha value
regression = Lasso(alpha=0.5)
# Fit the linear regression
model = regression.fit(features_standardized, target)
讨论
lasso 回归惩罚的一个有趣特征是它可以将模型的系数收缩到零,有效减少模型中的特征数。例如,在我们的解决方案中,我们将alpha
设置为0.5
,我们可以看到许多系数为 0,意味着它们对应的特征未在模型中使用:
# View coefficients
model.coef_
array([-0\. , 43.58618393, 53.39523724])
然而,如果我们将增加到一个更高的值,我们会看到几乎没有特征被使用:
# Create lasso regression with a high alpha
regression_a10 = Lasso(alpha=10)
model_a10 = regression_a10.fit(features_standardized, target)
model_a10.coef_
array([-0\. , 32.92181899, 42.73086731])
这种效果的实际好处在于,我们可以在特征矩阵中包含 100 个特征,然后通过调整 lasso 的 α 超参数,生成仅使用最重要的 10 个特征之一的模型(例如)。这使得我们能够在提升模型的可解释性的同时减少方差(因为更少的特征更容易解释)。
第十四章:树和森林
14.0 引言
基于树的学习算法是一类广泛且流行的非参数化监督方法,既适用于分类又适用于回归。基于树的学习器的基础是决策树,其中一系列决策规则(例如,“如果一个人的信用评分大于 720…”)被链接起来。结果看起来略像一个倒置的树形,顶部是第一个决策规则,下面是后续的决策规则分支开展。在决策树中,每个决策规则出现在一个决策节点,规则创建通向新节点的分支。末端没有决策规则的分支称为叶节点。
树模型之所以受欢迎的一个原因是它们的可解释性。事实上,决策树可以以完整形式绘制出来(参见配方 14.3),以创建一个高度直观的模型。从这个基本的树系统中产生了多种扩展,从随机森林到堆叠。在本章中,我们将讨论如何训练、处理、调整、可视化和评估多种基于树的模型。
14.1 训练决策树分类器
问题
你需要使用决策树训练一个分类器。
解决方案
使用 scikit-learn 的 DecisionTreeClassifier
:
# Load libraries
from sklearn.tree import DecisionTreeClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create decision tree classifier object
decisiontree = DecisionTreeClassifier(random_state=0)
# Train model
model = decisiontree.fit(features, target)
讨论
决策树学习器试图找到一个决策规则,在节点处产生最大的不纯度减少。虽然有多种不纯度的测量方法,但默认情况下,DecisionTreeClassifier
使用基尼不纯度:
其中 是节点 处的基尼不纯度, 是节点 处类别 的观察比例。这个找到减少不纯度的决策规则并创建分裂的过程会递归重复,直到所有叶节点是纯净的(即只包含一个类别)或达到某个任意的截止点。
在 scikit-learn 中,DecisionTreeClassifier
的操作类似于其他学习方法;在使用 fit
训练模型之后,我们可以使用模型预测观察的类别:
# Make new observation
observation = [[ 5, 4, 3, 2]]
# Predict observation's class
model.predict(observation)
array([1])
我们还可以看到观察的预测类别概率:
# View predicted class probabilities for the three classes
model.predict_proba(observation)
array([[0., 1., 0.]])
最后,如果我们想使用不同的不纯度测量,我们可以使用 criterion
参数:
# Create decision tree classifier object using entropy
decisiontree_entropy = DecisionTreeClassifier(
criterion='entropy', random_state=0)
# Train model
model_entropy = decisiontree_entropy.fit(features, target)
参见
14.2 训练决策树回归器
问题
你需要使用决策树训练一个回归模型。
解决方案
使用 scikit-learn 的 DecisionTreeRegressor
:
# Load libraries
from sklearn.tree import DecisionTreeRegressor
from sklearn import datasets
# Load data with only two features
diabetes = datasets.load_diabetes()
features = diabetes.data
target = diabetes.target
# Create decision tree regressor object
decisiontree = DecisionTreeRegressor(random_state=0)
# Train model
model = decisiontree.fit(features, target)
讨论
决策树回归与决策树分类类似;但是,它不是减少基尼不纯度或熵,而是默认情况下测量潜在分裂如何减少均方误差(MSE):
其中 是目标的真实值,而 是平均值。在 scikit-learn 中,可以使用 DecisionTreeRegressor
进行决策树回归。一旦我们训练好一个决策树,就可以用它来预测观测值的目标值:
# Make new observation
observation = [features[0]]
# Predict observation's value
model.predict(observation)
array([151.])
就像使用 DecisionTreeClassifier
一样,我们可以使用 criterion
参数来选择所需的分裂质量测量。例如,我们可以构建一个树,其分裂减少平均绝对误差(MAE):
# Create decision tree classifier object using MAE
decisiontree_mae = DecisionTreeRegressor(criterion="absolute_error",
random_state=0)
# Train model
model_mae = decisiontree_mae.fit(features, target)
参见
14.3 可视化决策树模型
问题
需要可视化由决策树学习算法创建的模型。
解决方案
将决策树模型导出为 DOT 格式,然后进行可视化:
# Load libraries
import pydotplus
from sklearn.tree import DecisionTreeClassifier
from sklearn import datasets
from IPython.display import Image
from sklearn import tree
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create decision tree classifier object
decisiontree = DecisionTreeClassifier(random_state=0)
# Train model
model = decisiontree.fit(features, target)
# Create DOT data
dot_data = tree.export_graphviz(decisiontree,
out_file=None,
feature_names=iris.feature_names,
class_names=iris.target_names)
# Draw graph
graph = pydotplus.graph_from_dot_data(dot_data)
# Show graph
Image(graph.create_png())
讨论
决策树分类器的一个优点是,我们可以可视化整个训练好的模型,使决策树成为机器学习中最具可解释性的模型之一。在我们的解决方案中,我们将训练好的模型导出为 DOT 格式(一种图形描述语言),然后用它来绘制图形。
如果我们看根节点,我们可以看到决策规则是,如果花瓣宽度小于或等于 0.8 厘米,则进入左分支;否则,进入右分支。我们还可以看到基尼不纯度指数(0.667)、观测数量(150)、每个类中的观测数量([50,50,50])以及如果我们在该节点停止,观测将被预测为的类别(setosa)。我们还可以看到在该节点,学习者发现单个决策规则(花瓣宽度(厘米)<= 0.8
)能够完美识别所有 setosa 类观测。此外,再增加一个相同特征的决策规则(花瓣宽度(厘米)<= 1.75
),决策树能够正确分类 150 个观测中的 144 个。这使得花瓣宽度成为非常重要的特征!
如果我们想在其他应用程序或报告中使用决策树,可以将可视化导出为 PDF 或 PNG 图像:
# Create PDF
graph.write_pdf("iris.pdf")
True
# Create PNG
graph.write_png("iris.png")
True
虽然这个解决方案可视化了决策树分类器,但同样可以轻松用于可视化决策树回归器。
注意:macOS 用户可能需要安装 Graphviz 的可执行文件才能运行上述代码。可以使用 Homebrew 命令 brew install graphviz
完成安装。有关 Homebrew 安装说明,请访问 Homebrew 的网站。
参见
14.4 训练随机森林分类器
问题
要使用随机决策树“森林”训练分类模型。
解决方案
使用 scikit-learn 的 RandomForestClassifier
训练随机森林分类模型。
# Load libraries
from sklearn.ensemble import RandomForestClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create random forest classifier object
randomforest = RandomForestClassifier(random_state=0, n_jobs=-1)
# Train model
model = randomforest.fit(features, target)
讨论
决策树的一个常见问题是它们往往过度拟合训练数据。这促使了一种称为随机森林的集成学习方法的广泛使用。在随机森林中,训练许多决策树,但每棵树只接收一个自举样本的观测集(即使用替换的原始观测数量的随机样本),并且在确定最佳分裂时,每个节点只考虑特征的一个子集。这些随机化决策树的森林(因此得名)投票以确定预测类别。
通过将这个解决方案与配方 14.1 进行比较,我们可以看到 scikit-learn 的RandomForestClassifier
与DecisionTreeClassifier
类似:
# Make new observation
observation = [[ 5, 4, 3, 2]]
# Predict observation's class
model.predict(observation)
array([1])
RandomForestClassifier
也使用与DecisionTreeClassifier
许多相同的参数。例如,我们可以改变用于分裂质量的测量:
# Create random forest classifier object using entropy
randomforest_entropy = RandomForestClassifier(
criterion="entropy", random_state=0)
# Train model
model_entropy = randomforest_entropy.fit(features, target)
然而,作为一组森林而不是单个决策树,RandomForestClassifier
有一些参数是随机森林特有的或特别重要的。首先,max_features
参数确定在每个节点考虑的最大特征数,并接受多个参数,包括整数(特征数量)、浮点数(特征百分比)和sqrt
(特征数量的平方根)。默认情况下,max_features
设置为auto
,与sqrt
相同。其次,bootstrap
参数允许我们设置是否使用替换创建考虑树的观测子集(默认设置)或不使用替换。第三,n_estimators
设置森林中包含的决策树数量。最后,虽然不特定于随机森林分类器,因为我们实际上在训练许多决策树模型,通常通过设置n_jobs=-1
来使用所有可用核心是很有用的。
参见
14.5 训练随机森林回归器
问题
你希望使用“森林”中的随机决策树来训练回归模型。
解决方案
使用 scikit-learn 的RandomForestRegressor
训练随机森林回归模型:
# Load libraries
from sklearn.ensemble import RandomForestRegressor
from sklearn import datasets
# Load data with only two features
diabetes = datasets.load_diabetes()
features = diabetes.data
target = diabetes.target
# Create random forest regressor object
randomforest = RandomForestRegressor(random_state=0, n_jobs=-1)
# Train model
model = randomforest.fit(features, target)
讨论
就像我们可以制作一组决策树分类器的森林一样,我们也可以制作一组决策树回归器,其中每棵树都使用一个自举样本集合,并且在每个节点处,决策规则只考虑特征的一个子集。与RandomForestClassifier
一样,我们有一些重要的参数:
max_features
设置在每个节点考虑的最大特征数的最大值。默认为p个特征,其中p是总特征数。
bootstrap
设置是否使用替换采样。默认为True
。
n_estimators
设置要构建的决策树数量。默认为10
。
参见
14.6 使用袋外错误评估随机森林
问题
您需要在不使用交叉验证的情况下评估随机森林模型。
解决方案
计算模型的袋外得分:
# Load libraries
from sklearn.ensemble import RandomForestClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create random forest classifier object
randomforest = RandomForestClassifier(
random_state=0, n_estimators=1000, oob_score=True, n_jobs=-1)
# Train model
model = randomforest.fit(features, target)
# View out-of-bag-error
randomforest.oob_score_
0.9533333333333334
讨论
在随机森林中,每棵决策树使用自举子样本集进行训练。这意味着对于每棵树,都有一个单独的未用于训练该树的观察子集。这些称为袋外观察。我们可以使用袋外观察作为测试集来评估我们的随机森林的性能。
对于每个观察值,学习算法将观察的真实值与未使用该观察训练的树的预测进行比较。计算总体分数并提供随机森林性能的单一度量。袋外得分估计是交叉验证的一种替代方法。
在 scikit-learn 中,我们可以通过在随机森林对象(即 RandomForestClassifier
)中设置 oob_score=True
来计算随机森林的袋外得分。可以使用 oob_score_
属性来检索得分。
14.7 随机森林中重要特征的识别
问题
您需要知道随机森林模型中哪些特征最重要。
解决方案
通过检查模型的 feature_importances_
属性计算和可视化每个特征的重要性:
# Load libraries
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create random forest classifier object
randomforest = RandomForestClassifier(random_state=0, n_jobs=-1)
# Train model
model = randomforest.fit(features, target)
# Calculate feature importances
importances = model.feature_importances_
# Sort feature importances in descending order
indices = np.argsort(importances)[::-1]
# Rearrange feature names so they match the sorted feature importances
names = [iris.feature_names[i] for i in indices]
# Create plot
plt.figure()
# Create plot title
plt.title("Feature Importance")
# Add bars
plt.bar(range(features.shape[1]), importances[indices])
# Add feature names as x-axis labels
plt.xticks(range(features.shape[1]), names, rotation=90)
# Show plot
plt.show()
讨论
决策树的一个主要优点是可解释性。具体而言,我们可以可视化整个模型(参见 Recipe 14.3)。然而,随机森林模型由数十、数百甚至数千棵决策树组成。这使得对随机森林模型进行简单直观的可视化变得不切实际。尽管如此,还有另一种选择:我们可以比较(和可视化)每个特征的相对重要性。
在 Recipe 14.3 中,我们可视化了一个决策树分类器模型,并看到基于花瓣宽度的决策规则能够正确分类许多观察结果。直观地说,这意味着花瓣宽度在我们的分类器中是一个重要特征。更正式地说,具有分裂平均不纯度(例如分类器中的基尼不纯度或熵以及回归器中的方差)更大的特征被认为更重要。
但是,有两件事情需要注意关于特征重要性。首先,scikit-learn 要求我们将名义分类特征拆分为多个二进制特征。这会使得该特征的重要性分布在所有二进制特征上,即使原始的名义分类特征非常重要,也会使得每个特征看起来不重要。其次,如果两个特征高度相关,一个特征将会获得大部分重要性,使另一个特征看起来不太重要,这对解释有影响如果不考虑。
在 scikit-learn 中,分类和回归决策树以及随机森林可以使用 feature_importances_
方法报告每个特征的相对重要性:
# View feature importances
model.feature_importances_
array([0.09090795, 0.02453104, 0.46044474, 0.42411627])
数值越高,特征越重要(所有重要性得分总和为 1)。通过绘制这些值,我们可以为我们的随机森林模型增加可解释性。
14.8 在随机森林中选择重要特征
问题
您需要对随机森林进行特征选择。
解决方案
确定重要特征并仅使用最重要的特征重新训练模型:
# Load libraries
from sklearn.ensemble import RandomForestClassifier
from sklearn import datasets
from sklearn.feature_selection import SelectFromModel
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create random forest classifier
randomforest = RandomForestClassifier(random_state=0, n_jobs=-1)
# Create object that selects features with importance greater
# than or equal to a threshold
selector = SelectFromModel(randomforest, threshold=0.3)
# Create new feature matrix using selector
features_important = selector.fit_transform(features, target)
# Train random forest using most important features
model = randomforest.fit(features_important, target)
讨论
有些情况下,我们可能希望减少模型中特征的数量。例如,我们可能希望减少模型的方差,或者我们可能希望通过只包含最重要的特征来提高可解释性。
在 scikit-learn 中,我们可以使用一个简单的两阶段工作流程来创建一个具有减少特征的模型。首先,我们使用所有特征训练一个随机森林模型。然后,我们使用这个模型来识别最重要的特征。接下来,我们创建一个只包含这些特征的新特征矩阵。在我们的解决方案中,我们使用 SelectFromModel
方法创建一个包含重要性大于或等于某个 threshold
值的特征的特征矩阵。最后,我们使用这些特征创建一个新模型。
我们必须注意这种方法的两个限制。首先,已经进行了一次独热编码的名义分类特征会导致特征重要性在二元特征中被稀释。其次,高度相关特征的特征重要性将有效地分配给一个特征,而不是均匀分布在两个特征之间。
另请参阅
14.9 处理不平衡类别
问题
您有一个目标向量,其中包含高度不平衡的类别,并希望训练一个随机森林模型。
解决方案
使用 class_weight="balanced"
训练决策树或随机森林模型:
# Load libraries
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Make class highly imbalanced by removing first 40 observations
features = features[40:,:]
target = target[40:]
# Create target vector indicating if class 0, otherwise 1
target = np.where((target == 0), 0, 1)
# Create random forest classifier object
randomforest = RandomForestClassifier(
random_state=0, n_jobs=-1, class_weight="balanced")
# Train model
model = randomforest.fit(features, target)
讨论
在实际进行机器学习时,不平衡类别是一个常见问题。如果不加以解决,不平衡类别的存在会降低模型的性能。我们将在预处理过程中讨论如何处理不平衡类别 Recipe 17.5。然而,在 scikit-learn 中的许多学习算法都具有用于纠正不平衡类别的内置方法。我们可以使用 class_weight
参数将 RandomForestClassifier
设置为纠正不平衡类别。如果提供了一个字典,其形式为类别名称和所需权重(例如 {"male": 0.2, "female": 0.8}
),RandomForestClassifier
将相应地加权类别。然而,更常见的参数是 balanced
,其中类别的权重自动与其在数据中出现的频率成反比:
其中是类的权重,是观察次数,是类中的观察次数,是总类数。例如,在我们的解决方案中,有 2 个类别(),110 次观察(),分别有 10 和 100 次观察在每个类中()。如果我们使用class_weight="balanced"
来加权类别,则较小的类别被加权更多:
# Calculate weight for small class
110/(2*10)
5.5
而较大的类别被加权更少:
# Calculate weight for large class
110/(2*100)
0.55
14.10 控制树的大小
问题
您希望手动确定决策树的结构和大小。
解决方案
在 scikit-learn 基于树的学习算法中使用树结构参数:
# Load libraries
from sklearn.tree import DecisionTreeClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create decision tree classifier object
decisiontree = DecisionTreeClassifier(random_state=0,
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0,
max_leaf_nodes=None,
min_impurity_decrease=0)
# Train model
model = decisiontree.fit(features, target)
讨论
scikit-learn 的基于树的学习算法有多种控制决策树大小的技术。这些通过参数访问:
max_depth
树的最大深度。如果为None
,则树会生长直到所有叶子节点都是纯净的。如果是整数,则树被“修剪”到该深度。
min_samples_split
在一个节点上分裂之前所需的最小观察次数。如果提供整数作为参数,则确定原始最小值,如果提供浮点数,则最小值是总观察次数的百分比。
min_samples_leaf
在叶子节点上所需的最小观察次数。与min_samples_split
相同的参数。
max_leaf_nodes
最大叶子节点数。
min_impurity_split
在执行分割之前所需的最小不纯度减少。
虽然知道这些参数存在是有用的,但最可能我们只会使用max_depth
和min_impurity_split
,因为更浅的树(有时称为树桩)是更简单的模型,因此具有较低的方差。
14.11 通过增强提高性能
问题
您需要比决策树或随机森林性能更好的模型。
解决方案
使用AdaBoostClassifier
或AdaBoostRegressor
训练增强模型:
# Load libraries
from sklearn.ensemble import AdaBoostClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create adaboost tree classifier object
adaboost = AdaBoostClassifier(random_state=0)
# Train model
model = adaboost.fit(features, target)
讨论
在随机森林中,随机化决策树的集合预测目标向量。另一种常用且通常更强大的方法称为增强。在增强的一种形式中称为 AdaBoost,我们迭代地训练一系列弱模型(通常是浅决策树,有时称为树桩),每次迭代都优先考虑前一个模型预测错误的观察结果。更具体地,在 AdaBoost 中:
-
为每个观察值 分配初始权重值 ,其中 是数据中观察总数。
-
在数据上训练一个“弱”模型。
-
对于每个观察:
-
如果弱模型正确预测 ,则 减少。
-
如果弱模型错误预测 ,则 增加。
-
-
训练一个新的弱模型,其中具有较大 的观察值优先考虑。
-
重复步骤 4 和 5,直到数据完全预测或训练了预设数量的弱模型。
结果是一个聚合模型,个体弱模型专注于更难(从预测角度)的观察。在 scikit-learn 中,我们可以使用 AdaBoostClassifier
或 AdaBoostRegressor
实现 AdaBoost。最重要的参数是 base_estimator
、n_estimators
、learning_rate
和 loss
:
base_estimator
base_estimator
是用于训练弱模型的学习算法。与 AdaBoost 一起使用的最常见的学习器是决策树,默认参数。
n_estimators
n_estimators
是要迭代训练的模型数量。
learning_rate
learning_rate
是每个模型对权重的贡献,默认为 1
。减小学习率意味着权重将略微增加或减少,迫使模型训练速度变慢(但有时会导致更好的性能分数)。
loss
loss
仅适用于 AdaBoostRegressor
,设置更新权重时使用的损失函数。默认为线性损失函数,但可以更改为 square
或 exponential
。
参见
14.12 训练 XGBoost 模型
问题
您需要训练具有高预测能力的基于树的模型。
解决方案
使用 xgboost
Python 库:
# Load libraries
import xgboost as xgb
from sklearn import datasets, preprocessing
from sklearn.metrics import classification_report
from numpy import argmax
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create dataset
xgb_train = xgb.DMatrix(features, label=target)
# Define parameters
param = {
'objective': 'multi:softprob',
'num_class': 3
}
# Train model
gbm = xgb.train(param, xgb_train)
# Get predictions
predictions = argmax(gbm.predict(xgb_train), axis=1)
# Get a classification report
print(classification_report(target, predictions))
precision recall f1-score support
0 1.00 1.00 1.00 50
1 1.00 0.96 0.98 50
2 0.96 1.00 0.98 50
accuracy 0.99 150
macro avg 0.99 0.99 0.99 150
weighted avg 0.99 0.99 0.99 150
讨论
XGBoost(即极端梯度提升)是机器学习领域中非常流行的梯度提升算法。尽管它不总是基于树的模型,但经常应用于决策树集成中。由于在机器学习竞赛网站 Kaggle 上取得了广泛成功,XGBoost 因其卓越的性能提升而备受青睐,已成为提高性能的可靠算法,超越了典型随机森林或梯度增强机器的性能。
尽管 XGBoost 因计算密集而闻名,但过去几年的计算性能优化(例如 GPU 支持)显著简化了与 XGBoost 的快速迭代,它仍然是在统计性能要求时的常见选择算法。
另请参阅
14.13 使用 LightGBM 提升实时性能
问题
您需要训练一个在计算上优化的基于梯度提升树的模型。
解决方案
使用梯度提升机库 lightgbm
:
# Load libraries
import lightgbm as lgb
from sklearn import datasets, preprocessing
from sklearn.metrics import classification_report
from numpy import argmax
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create dataset
lgb_train = lgb.Dataset(features, target)
# Define parameters
params = {
'objective': 'multiclass',
'num_class': 3,
'verbose': -1,
}
# Train model
gbm = lgb.train(params, lgb_train)
# Get predictions
predictions = argmax(gbm.predict(features), axis=1)
# Get a classification report
print(classification_report(target, predictions))
precision recall f1-score support
0 1.00 1.00 1.00 50
1 1.00 1.00 1.00 50
2 1.00 1.00 1.00 50
accuracy 1.00 150
macro avg 1.00 1.00 1.00 150
weighted avg 1.00 1.00 1.00 150
讨论
lightgbm
库用于梯度提升机,在训练时间、推断和 GPU 支持方面都经过高度优化。由于其计算效率高,常用于生产环境和大规模设置中。尽管 scikit-learn 模型通常更易于使用,但某些库如 lightgbm
在受限于大数据或严格的模型训练/服务时间时会更实用。
另请参阅
第十五章:K 近邻算法
15.0 简介
k-最近邻(KNN)分类器是监督机器学习中最简单但最常用的分类器之一。KNN 通常被认为是一种惰性学习器;它不会技术上训练一个模型来进行预测。相反,一个观测值被预测为与 k 个最近观测值中最大比例的类相同。
例如,如果一个具有未知类的观测值被一个类为 1 的观测值所包围,则该观测值将被分类为类 1。在本章中,我们将探讨如何使用 scikit-learn 创建和使用 KNN 分类器。
15.1 寻找一个观测值的最近邻居
问题
您需要找到一个观测值的 k 个最近邻居。
解决方案
使用 scikit-learn 的 NearestNeighbors
:
# Load libraries
from sklearn import datasets
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler
# Load data
iris = datasets.load_iris()
features = iris.data
# Create standardizer
standardizer = StandardScaler()
# Standardize features
features_standardized = standardizer.fit_transform(features)
# Two nearest neighbors
nearest_neighbors = NearestNeighbors(n_neighbors=2).fit(features_standardized)
# Create an observation
new_observation = [ 1, 1, 1, 1]
# Find distances and indices of the observation's nearest neighbors
distances, indices = nearest_neighbors.kneighbors([new_observation])
# View the nearest neighbors
features_standardized[indices]
array([[[1.03800476, 0.55861082, 1.10378283, 1.18556721],
[0.79566902, 0.32841405, 0.76275827, 1.05393502]]])
讨论
在我们的解决方案中,我们使用了鸢尾花数据集。我们创建了一个观测值,new_observation
,具有一些值,然后找到了最接近我们观测值的两个观测值。 indices
包含了最接近我们数据集中的观测值的位置,所以 X[indices]
显示了这些观测值的值。直观地,距离可以被看作是相似性的度量,因此两个最接近的观测值是与我们创建的花最相似的两朵花。
我们如何衡量距离?scikit-learn 提供了多种距离度量方式,,包括欧几里得距离:
和曼哈顿距离:
默认情况下,NearestNeighbors
使用闵可夫斯基距离:
其中和是我们计算距离的两个观测值。闵可夫斯基距离包括一个超参数,其中=1 是曼哈顿距离,=2 是欧几里得距离,等等。在 scikit-learn 中,默认情况下=2。
我们可以使用 metric
参数设置距离度量:
# Find two nearest neighbors based on Euclidean distance
nearestneighbors_euclidean = NearestNeighbors(
n_neighbors=2, metric='euclidean').fit(features_standardized)
我们创建的 distance
变量包含了到两个最近邻居的实际距离测量:
# View distances
distances
array([[0.49140089, 0.74294782]])
此外,我们可以使用 kneighbors_graph
创建一个矩阵,指示每个观测值的最近邻居:
# Find each observation's three nearest neighbors
# based on Euclidean distance (including itself)
nearestneighbors_euclidean = NearestNeighbors(
n_neighbors=3, metric="euclidean").fit(features_standardized)
# List of lists indicating each observation's three nearest neighbors
# (including itself)
nearest_neighbors_with_self = nearestneighbors_euclidean.kneighbors_graph(
features_standardized).toarray()
# Remove 1s marking an observation is a nearest neighbor to itself
for i, x in enumerate(nearest_neighbors_with_self):
x[i] = 0
# View first observation's two nearest neighbors
nearest_neighbors_with_self[0]
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
当我们寻找最近邻居或使用基于距离的任何学习算法时,重要的是要转换特征,使它们处于相同的尺度上。这是因为距离度量将所有特征都视为处于相同的尺度上,但如果一个特征是以百万美元计算的,而第二个特征是以百分比计算的,那么计算出的距离将偏向于前者。在我们的解决方案中,我们通过使用 StandardScaler
对特征进行了标准化,以解决这个潜在的问题。
15.2 创建 K 近邻分类器
问题
给定一个未知类别的观测值,你需要根据其邻居的类别预测其类别。
解答
如果数据集不太大,使用 KNeighborsClassifier
:
# Load libraries
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn import datasets
# Load data
iris = datasets.load_iris()
X = iris.data
y = iris.target
# Create standardizer
standardizer = StandardScaler()
# Standardize features
X_std = standardizer.fit_transform(X)
# Train a KNN classifier with 5 neighbors
knn = KNeighborsClassifier(n_neighbors=5, n_jobs=-1).fit(X_std, y)
# Create two observations
new_observations = [[ 0.75, 0.75, 0.75, 0.75],
[ 1, 1, 1, 1]]
# Predict the class of two observations
knn.predict(new_observations)
array([1, 2])
讨论
在 KNN 中,给定一个观测值,,其目标类别未知,算法首先基于某种距离度量(例如欧几里得距离)确定最近的 个观测值(有时称为 的 邻域),然后这些 个观测值基于它们的类别“投票”,获胜的类别就是 的预测类别。更正式地,某个类别 的概率 为:
其中 ν 是 个观测值在 的邻域中, 是第 i 个观测值的类别, 是一个指示函数(即,1 为真,0 其他)。在 scikit-learn 中,我们可以使用 predict_proba
查看这些概率:
# View probability that each observation is one of three classes
knn.predict_proba(new_observations)
array([[0\. , 0.6, 0.4],
[0\. , 0\. , 1\. ]])
概率最高的类别成为预测类别。例如,在前面的输出中,第一观测值应该是类别 1 (Pr = 0.6),而第二观测值应该是类别 2 (Pr = 1),这正是我们所看到的:
knn.predict(new_observations)
array([1, 2])
KNeighborsClassifier
包含许多重要参数需要考虑。首先,metric
设置使用的距离度量。其次,n_jobs
决定使用计算机的多少核心。因为做出预测需要计算一个点与数据中每个点的距离,推荐使用多个核心。第三,algorithm
设置计算最近邻的方法。虽然算法之间存在实际差异,默认情况下 KNeighborsClassifier
尝试自动选择最佳算法,因此通常不需要担心这个参数。第四,默认情况下 KNeighborsClassifier
的工作方式与我们之前描述的相同,每个邻域中的观测值获得一个投票;然而,如果我们将 weights
参数设置为 distance
,则更靠近的观测值的投票比更远的观测值更重要。直观上这很有道理,因为更相似的邻居可能会告诉我们更多关于一个观测值类别的信息。
最后,由于距离计算将所有特征视为在相同尺度上,因此在使用 KNN 分类器之前,标准化特征是很重要的。
15.3 确定最佳邻域大小
问题
你想在 k 最近邻分类器中选择最佳的 k 值。
解答
使用模型选择技术如 GridSearchCV
:
# Load libraries
from sklearn.neighbors import KNeighborsClassifier
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.model_selection import GridSearchCV
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create standardizer
standardizer = StandardScaler()
# Create a KNN classifier
knn = KNeighborsClassifier(n_neighbors=5, n_jobs=-1)
# Create a pipeline
pipe = Pipeline([("standardizer", standardizer), ("knn", knn)])
# Create space of candidate values
search_space = [{"knn__n_neighbors": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}]
# Create grid search
classifier = GridSearchCV(
pipe, search_space, cv=5, verbose=0).fit(features_standardized, target)
讨论
k 的大小在 KNN 分类器中有着真实的影响。在机器学习中,我们试图在偏差和方差之间找到一个平衡点,k 的值在其中的显性展示是极其重要的。如果 k = n,其中 n 是观测数量,那么我们具有高偏差但低方差。如果 k = 1,我们将具有低偏差但高方差。最佳模型将通过找到能平衡这种偏差-方差权衡的k的值来获得。在我们的解决方案中,我们使用GridSearchCV
对具有不同k值的 KNN 分类器进行了五折交叉验证。完成后,我们可以看到产生最佳模型的k值:
# Best neighborhood size (k)
classifier.best_estimator_.get_params()["knn__n_neighbors"]
6
15.4 创建基于半径的最近邻分类器
问题
给定一个未知类的观测值,您需要基于一定距离内所有观测值的类来预测其类别。
解决方案
使用 RadiusNeighborsClassifier
:
# Load libraries
from sklearn.neighbors import RadiusNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create standardizer
standardizer = StandardScaler()
# Standardize features
features_standardized = standardizer.fit_transform(features)
# Train a radius neighbors classifier
rnn = RadiusNeighborsClassifier(
radius=.5, n_jobs=-1).fit(features_standardized, target)
# Create two observations
new_observations = [[ 1, 1, 1, 1]]
# Predict the class of two observations
rnn.predict(new_observations)
array([2])
讨论
在 KNN 分类中,一个观测的类别是根据其 k 个邻居的类别预测的。一种不太常见的技术是基于半径的最近邻(RNN)分类器,其中一个观测的类别是根据给定半径 r 内所有观测的类别预测的。
在 scikit-learn 中,RadiusNeighborsClassifier
与KNeighborsClassifier
非常相似,只有两个参数例外。首先,在RadiusNeighborsClassifier
中,我们需要指定用于确定观测是否为邻居的固定区域半径使用radius
。除非有设置radius
到某个值的实质性原因,否则最好在模型选择过程中像其他超参数一样进行调整。第二个有用的参数是outlier_label
,它指示如果半径内没有观测值,则给出一个观测的标签—这本身可以是一个用于识别异常值的有用工具。
15.5 寻找近似最近邻
问题
您希望在低延迟下获取大数据的最近邻:
解决方案
使用 近似最近邻(ANN)搜索,使用 Facebook 的 faiss
库:
# Load libraries
import faiss
import numpy as np
from sklearn import datasets
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler
# Load data
iris = datasets.load_iris()
features = iris.data
# Create standardizer
standardizer = StandardScaler()
# Standardize features
features_standardized = standardizer.fit_transform(features)
# Set faiss parameters
n_features = features_standardized.shape[1]
nlist = 3
k = 2
# Create an IVF index
quantizer = faiss.IndexFlatIP(n_features)
index = faiss.IndexIVFFlat(quantizer, n_features, nlist)
# Train the index and add feature vectors
index.train(features_standardized)
index.add(features_standardized)
# Create an observation
new_observation = np.array([[ 1, 1, 1, 1]])
# Search the index for the 2 nearest neighbors
distances, indices = index.search(new_observation, k)
# Show the feature vectors for the two nearest neighbors
np.array([list(features_standardized[i]) for i in indices[0]])
array([[1.03800476, 0.55861082, 1.10378283, 1.18556721],
[0.79566902, 0.32841405, 0.76275827, 1.05393502]])
讨论
KNN 是在小数据集中找到最相似观测的一个很好的方法。然而,随着数据集的增大,计算任意观测与数据集中所有其他点之间距离所需的时间也会增加。大规模的 ML 系统如搜索或推荐引擎通常使用某种形式的向量相似度测量来检索相似的观测。但在实时规模中,我们需要在不到 100 毫秒内获得结果,KNN 变得不可行。
ANN 通过牺牲精确最近邻搜索的一些质量以换取速度来帮助我们克服这个问题。换句话说,虽然 ANN 搜索的前 10 个最近邻的顺序和项可能与精确 KNN 搜索的前 10 个结果不匹配,但我们能更快地得到这前 10 个最近邻。
在这个例子中,我们使用了一种名为倒排文件索引(IVF)的 ANN 方法。这种方法通过使用聚类来限制最近邻搜索的范围。IVF 使用 Voronoi 镶嵌将我们的搜索空间划分为多个不同的区域(或聚类)。当我们去查找最近邻时,我们访问了有限数量的聚类以找到相似的观测值,而不是对数据集中的每一个点进行比较。
如何从数据中创建 Voronoi 镶嵌最好通过简单的数据可视化。例如,取随机数据的散点图在二维中可视化,如图 15-1 所示。
图 15-1. 一组随机生成的二维数据的散点图
使用 Voronoi 镶嵌,我们可以创建多个子空间,每个子空间只包含我们想要搜索的总观测的一个小子集,如图 15-2 所示。
图 15-2. 将随机生成的二维数据分割成多个不同子空间
在Faiss
库中,nlist
参数允许我们定义要创建的聚类数。还可以在查询时使用一个额外的参数nprobe
来定义要搜索的聚类数,以检索给定观测值的最近邻。增加nlist
和nprobe
都可以提高邻居的质量,但会增加计算成本,从而导致 IVF 索引的运行时间更长。减少这些参数会产生相反的效果,您的代码将运行得更快,但可能返回质量较低的结果。
注意,此示例返回与本章第一个配方完全相同的输出。这是因为我们处理的是非常小的数据,并且仅使用了三个聚类,这使得我们的 ANN 结果与我们的 KNN 结果没有显著差异。
参见
15.6 评估近似最近邻
问题
您想看看您的 ANN 与精确最近邻(KNN)的比较情况:
解决方案
计算 ANN 相对于 KNN 的 Recall @k 最近邻
# Load libraries
import faiss
import numpy as np
from sklearn import datasets
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler
# Number of nearest neighbors
k = 10
# Load data
iris = datasets.load_iris()
features = iris.data
# Create standardizer
standardizer = StandardScaler()
# Standardize features
features_standardized = standardizer.fit_transform(features)
# Create KNN with 10 NN
nearest_neighbors = NearestNeighbors(n_neighbors=k).fit(features_standardized)
# Set faiss parameters
n_features = features_standardized.shape[1]
nlist = 3
# Create an IVF index
quantizer = faiss.IndexFlatIP(n_features)
index = faiss.IndexIVFFlat(quantizer, n_features, nlist)
# Train the index and add feature vectors
index.train(features_standardized)
index.add(features_standardized)
index.nprobe = 1
# Create an observation
new_observation = np.array([[ 1, 1, 1, 1]])
# Find distances and indices of the observation's exact nearest neighbors
knn_distances, knn_indices = nearest_neighbors.kneighbors(new_observation)
# Search the index for the two nearest neighbors
ivf_distances, ivf_indices = index.search(new_observation, k)
# Get the set overlap
recalled_items = set(list(knn_indices[0])) & set(list(ivf_indices[0]))
# Print the recall
print(f"Recall @k={k}: {len(recalled_items)/k * 100}%")
Recall @k=10: 100.0%
讨论
Recall @k 最简单的定义是 ANN 在某个k个最近邻处返回的项目数,这些项目同时也出现在相同k个精确最近邻中,除以k。在这个例子中,在 10 个最近邻处,我们有 100%的召回率,这意味着我们的 ANN 返回的索引与我们的 KNN 在 k=10 时是相同的(尽管不一定是相同的顺序)。
在评估 ANN 与精确最近邻时,Recall 是一个常用的度量标准。
参见
第十六章:Logistic 回归
16.0 引言
尽管其名称中带有“回归”,逻辑回归 实际上是一种广泛使用的监督分类技术。逻辑回归(及其扩展,如多项式逻辑回归)是一种直接、被理解的方法,用于预测观察值属于某个类别的概率。在本章中,我们将涵盖在 scikit-learn 中使用逻辑回归训练各种分类器的过程。
16.1 训练一个二元分类器
问题
您需要训练一个简单的分类器模型。
解决方案
使用 LogisticRegression
在 scikit-learn 中训练一个逻辑回归模型:
# Load libraries
from sklearn.linear_model import LogisticRegression
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
# Load data with only two classes
iris = datasets.load_iris()
features = iris.data[:100,:]
target = iris.target[:100]
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create logistic regression object
logistic_regression = LogisticRegression(random_state=0)
# Train model
model = logistic_regression.fit(features_standardized, target)
讨论
尽管其名称中带有“回归”,逻辑回归实际上是一种广泛使用的二元分类器(即目标向量只能取两个值)。在逻辑回归中,线性模型(例如β[0] + β[1]x)包含在逻辑(也称为 sigmoid)函数中,,使得:
其中是第个观察目标值为类别 1 的概率;是训练数据;和是待学习的参数;是自然常数。逻辑函数的效果是将函数的输出值限制在 0 到 1 之间,因此可以解释为概率。如果大于 0.5,则预测为类别 1;否则,预测为类别 0。
在 scikit-learn 中,我们可以使用 LogisticRegression
训练一个逻辑回归模型。一旦训练完成,我们可以使用该模型预测新观察的类别:
# Create new observation
new_observation = [[.5, .5, .5, .5]]
# Predict class
model.predict(new_observation)
array([1])
在这个例子中,我们的观察被预测为类别 1。此外,我们可以看到观察为每个类的成员的概率:
# View predicted probabilities
model.predict_proba(new_observation)
array([[0.17738424, 0.82261576]])
我们的观察有 17.7%的机会属于类别 0,82.2%的机会属于类别 1。
16.2 训练一个多类分类器
问题
如果超过两个类别,则需要训练一个分类器模型。
解决方案
使用 LogisticRegression
在 scikit-learn 中训练一个逻辑回归,使用一对多或多项式方法:
# Load libraries
from sklearn.linear_model import LogisticRegression
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create one-vs-rest logistic regression object
logistic_regression = LogisticRegression(random_state=0, multi_class="ovr")
# Train model
model = logistic_regression.fit(features_standardized, target)
讨论
单独来看,逻辑回归只是二元分类器,意味着它不能处理目标向量超过两个类。然而,逻辑回归的两个巧妙扩展可以做到。首先,在 一对多 逻辑回归(OvR)中,为每个预测的类别训练一个单独的模型,无论观察结果是否属于该类(从而将其转化为二元分类问题)。它假设每个分类问题(例如,类别 0 或非类别 0)是独立的。
或者,多项式逻辑回归(MLR)中,我们在 配方 16.1 中看到的逻辑函数被 softmax 函数取代:
其中 是第 个观察目标值 属于类别 的概率, 是总类别数。MLR 的一个实际优势是,使用 predict_proba
方法预测的概率更可靠(即更好地校准)。
当使用 LogisticRegression
时,我们可以选择我们想要的两种技术之一,OvR (ovr
) 是默认参数。我们可以通过设置参数为 multinomial
切换到 MLR。
16.3 通过正则化减少方差
问题
你需要减少逻辑回归模型的方差。
解决方案
调整正则化强度超参数 C
:
# Load libraries
from sklearn.linear_model import LogisticRegressionCV
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create decision tree regression object
logistic_regression = LogisticRegressionCV(
penalty='l2', Cs=10, random_state=0, n_jobs=-1)
# Train model
model = logistic_regression.fit(features_standardized, target)
讨论
正则化 是一种惩罚复杂模型以减少其方差的方法。具体来说,是向我们试图最小化的损失函数中添加一个惩罚项,通常是 L1 和 L2 惩罚。在 L1 惩罚中:
其中 是正在学习的第 个特征的参数, 是表示正则化强度的超参数。使用 L2 惩罚时:
较高的 值增加了较大参数值的惩罚(即更复杂的模型)。scikit-learn 遵循使用 而不是 的常见方法,其中 是正则化强度的倒数:。为了在使用逻辑回归时减少方差,我们可以将 视为一个超参数,用于调整以找到创建最佳模型的 的值。在 scikit-learn 中,我们可以使用 LogisticRegressionCV
类来高效地调整 。LogisticRegressionCV
的参数 Cs
可以接受一个值范围供 搜索(如果提供一个浮点数列表作为参数),或者如果提供一个整数,则会在对数尺度的 -10,000 到 10,000 之间生成相应数量的候选值列表。
不幸的是,LogisticRegressionCV
不允许我们在不同的惩罚项上搜索。为了做到这一点,我们必须使用在 第十二章 讨论的效率较低的模型选择技术。
16.4 在非常大的数据上训练分类器
问题
您需要在非常大的数据集上训练一个简单的分类器模型。
解决方案
使用 stochastic average gradient(SAG)求解器在 scikit-learn 中训练逻辑回归:
# Load libraries
from sklearn.linear_model import LogisticRegression
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create logistic regression object
logistic_regression = LogisticRegression(random_state=0, solver="sag")
# Train model
model = logistic_regression.fit(features_standardized, target)
讨论
scikit-learn 的 LogisticRegression
提供了一些训练逻辑回归的技术,称为 solvers。大多数情况下,scikit-learn 会自动为我们选择最佳的求解器,或者警告我们无法使用某个求解器来做某事。然而,有一个特定的情况我们应该注意。
尽管详细解释超出了本书的范围(更多信息请参见 Mark Schmidt 在本章 “参见” 部分的幻灯片),随机平均梯度下降使我们能够在数据非常大时比其他求解器更快地训练模型。然而,它对特征缩放非常敏感,因此标准化我们的特征特别重要。我们可以通过设置 solver="sag"
来让我们的学习算法使用这个求解器。
参见
16.5 处理不平衡的类
问题
您需要训练一个简单的分类器模型。
解决方案
使用 scikit-learn 中的 LogisticRegression
训练逻辑回归模型:
# Load libraries
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Make class highly imbalanced by removing first 40 observations
features = features[40:,:]
target = target[40:]
# Create target vector indicating if class 0, otherwise 1
target = np.where((target == 0), 0, 1)
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create decision tree regression object
logistic_regression = LogisticRegression(random_state=0, class_weight="balanced")
# Train model
model = logistic_regression.fit(features_standardized, target)
讨论
就像 scikit-learn
中许多其他学习算法一样,LogisticRegression
自带处理不平衡类别的方法。如果我们的类别高度不平衡,在预处理过程中没有处理它,我们可以使用 class_weight
参数来加权这些类别,以确保每个类别的混合平衡。具体地,balanced
参数将自动根据其频率的倒数加权类别:
其中 是类别 的权重, 是观测数量, 是类别 中的观测数量, 是总类别数量。
第十七章:支持向量机
17.0 引言
要理解支持向量机,我们必须了解超平面。形式上,超平面是n - 1维空间中的一个* n * -维子空间。尽管听起来复杂,但实际上相当简单。例如,如果我们想要划分一个二维空间,我们会使用一维超平面(即,一条线)。如果我们想要划分一个三维空间,我们会使用二维超平面(即,一张平面或一张床单)。超平面只是将该概念推广到n维空间的一种方式。
支持向量机通过找到在训练数据中最大化类之间间隔的超平面来对数据进行分类。在一个二维示例中,我们可以将超平面看作是分开两个类的最宽的直线“带”(即,具有间隔的线)。
在本章中,我们将涵盖在各种情况下训练支持向量机,并深入了解如何扩展该方法以解决常见问题。
17.1 训练线性分类器
问题
您需要训练一个模型来对观察结果进行分类。
解决方案
使用支持向量分类器(SVC)找到最大化类之间间隔的超平面:
# Load libraries
from sklearn.svm import LinearSVC
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
import numpy as np
# Load data with only two classes and two features
iris = datasets.load_iris()
features = iris.data[:100,:2]
target = iris.target[:100]
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create support vector classifier
svc = LinearSVC(C=1.0)
# Train model
model = svc.fit(features_standardized, target)
讨论
scikit-learn 的LinearSVC
实现了一个简单的 SVC。为了理解 SVC 正在做什么,让我们绘制出数据和超平面的图像。虽然 SVC 在高维度下工作得很好,但在我们的解决方案中,我们只加载了两个特征并取了一部分观察结果,使得数据只包含两个类。这将让我们可以可视化模型。回想一下,当我们只有两个维度时,SVC 试图找到具有最大间隔的超平面—一条线—来分离类。在下面的代码中,我们在二维空间中绘制了两个类,然后绘制了超平面:
# Load library
from matplotlib import pyplot as plt
# Plot data points and color using their class
color = ["black" if c == 0 else "lightgrey" for c in target]
plt.scatter(features_standardized[:,0], features_standardized[:,1], c=color)
# Create the hyperplane
w = svc.coef_[0]
a = -w[0] / w[1]
xx = np.linspace(-2.5, 2.5)
yy = a * xx - (svc.intercept_[0]) / w[1]
# Plot the hyperplane
plt.plot(xx, yy)
plt.axis("off"), plt.show();
在这个可视化中,所有类 0 的观察结果都是黑色的,而类 1 的观察结果是浅灰色的。超平面是决策边界,决定了如何对新的观察结果进行分类。具体来说,线上方的任何观察结果将被分类为类 0,而线下方的任何观察结果将被分类为类 1。我们可以通过在可视化的左上角创建一个新的观察结果来证明这一点,这意味着它应该被预测为类 0:
# Create new observation
new_observation = [[ -2, 3]]
# Predict class of new observation
svc.predict(new_observation)
array([0])
关于 SVC 有几点需要注意。首先,为了可视化的目的,我们将示例限制为二元示例(即,只有两个类);但是,SVC 可以很好地处理多类问题。其次,正如我们的可视化所示,超平面在定义上是线性的(即,不是曲线的)。在这个示例中这是可以的,因为数据是线性可分的,意味着有一个能够完美分离两个类的超平面。不幸的是,在现实世界中,这种情况很少见。
更典型地,我们将无法完全分开类别。在这些情况下,支持向量分类器在最大化超平面间隔和最小化误分类之间存在平衡。在 SVC 中,后者由超参数 C 控制。C 是 SVC 学习器的参数,是对误分类数据点的惩罚。当 C 较小时,分类器可以接受误分类的数据点(高偏差但低方差)。当 C 较大时,分类器对误分类数据点进行严格惩罚,因此竭尽全力避免任何误分类数据点(低偏差但高方差)。
在 scikit-learn 中,C 是由参数 C
确定的,默认为 C=1.0
。我们应该将 C 视为我们学习算法的超参数,通过模型选择技术在 第十二章 中进行调优。
17.2 使用核处理线性不可分类
问题
你需要训练一个支持向量分类器,但你的类别是线性不可分的。
解决方案
使用核函数训练支持向量机的扩展,以创建非线性决策边界:
# Load libraries
from sklearn.svm import SVC
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
import numpy as np
# Set randomization seed
np.random.seed(0)
# Generate two features
features = np.random.randn(200, 2)
# Use an XOR gate (you don't need to know what this is) to generate
# linearly inseparable classes
target_xor = np.logical_xor(features[:, 0] > 0, features[:, 1] > 0)
target = np.where(target_xor, 0, 1)
# Create a support vector machine with a radial basis function kernel
svc = SVC(kernel="rbf", random_state=0, gamma=1, C=1)
# Train the classifier
model = svc.fit(features, target)
讨论
对支持向量机的全面解释超出了本书的范围。但是,简短的解释可能有助于理解支持向量机和核。出于最好在其他地方学习的原因,支持向量分类器可以表示为:
其中 是偏差, 是所有支持向量观测的集合, 是待学习的模型参数, 是两个支持向量观测 和 的对。最重要的是, 是一个核函数,用于比较 和 之间的相似性。如果你不理解核函数也不用担心。对于我们的目的,只需意识到:(1) 决定了用于分离我们类别的超平面类型,(2)我们通过使用不同的核函数创建不同的超平面。例如,如果我们想要一个类似于我们在 配方 17.1 中创建的基本线性超平面,我们可以使用线性核:
其中 是特征数。然而,如果我们想要一个非线性决策边界,我们可以将线性核替换为多项式核:
其中是多项式核函数的阶数。或者,我们可以使用支持向量机中最常见的核函数之一,径向基函数核:
其中是一个超参数,必须大于零。上述解释的主要观点是,如果我们有线性不可分的数据,我们可以用替代核函数替换线性核函数,从而创建非线性超平面决策边界。
我们可以通过可视化一个简单的例子来理解核函数的直觉。这个函数基于 Sebastian Raschka 的一个函数,绘制了二维空间的观测和决策边界超平面。您不需要理解这个函数的工作原理;我在这里包含它,以便您自己进行实验:
# Plot observations and decision boundary hyperplane
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
def plot_decision_regions(X, y, classifier):
cmap = ListedColormap(("red", "blue"))
xx1, xx2 = np.meshgrid(np.arange(-3, 3, 0.02), np.arange(-3, 3, 0.02))
Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
Z = Z.reshape(xx1.shape)
plt.contourf(xx1, xx2, Z, alpha=0.1, cmap=cmap)
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1],
alpha=0.8, c=cmap(idx),
marker="+", label=cl)
在我们的解决方案中,我们有包含两个特征(即两个维度)和一个目标向量的数据。重要的是,这些类别被分配得线性不可分。也就是说,我们无法画出一条直线来分隔这两类数据。首先,让我们创建一个带有线性核函数的支持向量机分类器:
# Create support vector classifier with a linear kernel
svc_linear = SVC(kernel="linear", random_state=0, C=1)
# Train model
svc_linear.fit(features, target)
SVC(C=1, kernel='linear', random_state=0)
接下来,由于我们只有两个特征,我们是在二维空间中工作,可以可视化观测、它们的类别以及我们模型的线性超平面:
# Plot observations and hyperplane
plot_decision_regions(features, target, classifier=svc_linear)
plt.axis("off"), plt.show();
我们可以看到,我们的线性超平面在分隔这两类数据时表现非常糟糕!现在,让我们将线性核函数替换为径向基函数核,并用它来训练一个新模型:
# Create a support vector machine with a radial basis function kernel
svc = SVC(kernel="rbf", random_state=0, gamma=1, C=1)
# Train the classifier
model = svc.fit(features, target)
然后可视化观测和超平面:
# Plot observations and hyperplane
plot_decision_regions(features, target, classifier=svc)
plt.axis("off"), plt.show();
通过使用径向基函数核,我们可以创建一个决策边界,它能够比线性核函数更好地分离这两类数据。这就是支持向量机中使用核函数的动机。
在 scikit-learn 中,我们可以通过使用kernel
参数来选择要使用的核函数。选择核函数后,我们需要指定适当的核选项,如多项式核中的阶数(使用degree
参数)和径向基函数核中的γ值(使用gamma
参数)。我们还需要设置惩罚参数C
。在训练模型时,大多数情况下,我们应该将所有这些视为超参数,并使用模型选择技术来确定它们值的组合,以生成性能最佳的模型。
17.3 创建预测概率
问题
您需要知道观测的预测类别概率。
解决方案
当使用 scikit-learn 的SVC
时,设置probability=True
,训练模型,然后使用predict_proba
来查看校准概率:
# Load libraries
from sklearn.svm import SVC
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
import numpy as np
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create support vector classifier object
svc = SVC(kernel="linear", probability=True, random_state=0)
# Train classifier
model = svc.fit(features_standardized, target)
# Create new observation
new_observation = [[.4, .4, .4, .4]]
# View predicted probabilities
model.predict_proba(new_observation)
array([[0.00541761, 0.97348825, 0.02109414]])
讨论
我们讨论过的许多监督学习算法使用概率估计来预测类别。例如,在 k 最近邻算法中,一个观测的k个邻居的类别被视为投票,以创建该观测属于该类别的概率。然后预测具有最高概率的类别。支持向量机使用超平面来创建决策区域,并不会自然输出一个观测属于某个类别的概率估计。但是,事实上我们可以在一些条件下输出校准的类别概率。在具有两个类别的支持向量机中,可以使用Platt 缩放,首先训练支持向量机,然后训练一个单独的交叉验证逻辑回归,将支持向量机的输出映射到概率:
其中和是参数向量,是第个观测到超平面的有符号距离。当我们有超过两个类别时,会使用 Platt 缩放的扩展。
更实际地说,创建预测概率有两个主要问题。首先,因为我们使用交叉验证训练第二个模型,生成预测概率可能会显著增加训练模型的时间。其次,因为预测概率是使用交叉验证创建的,它们可能并不总是与预测类别匹配。也就是说,一个观测可能被预测为类别 1,但其预测的概率小于 0.5。
在 scikit-learn 中,必须在训练模型时生成预测概率。我们可以通过将SVC
的probability
设置为True
来实现这一点。模型训练完成后,可以使用predict_proba
输出每个类别的估计概率。
17.4 识别支持向量
问题
需要确定哪些观测是决策超平面的支持向量。
解决方案
训练模型,然后使用support_vectors_
:
# Load libraries
from sklearn.svm import SVC
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
import numpy as np
# Load data with only two classes
iris = datasets.load_iris()
features = iris.data[:100,:]
target = iris.target[:100]
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create support vector classifier object
svc = SVC(kernel="linear", random_state=0)
# Train classifier
model = svc.fit(features_standardized, target)
# View support vectors
model.support_vectors_
array([[-0.5810659 , 0.42196824, -0.80497402, -0.50860702],
[-1.52079513, -1.67737625, -1.08231219, -0.86427627],
[-0.89430898, -1.4674418 , 0.30437864, 0.38056609],
[-0.5810659 , -1.25750735, 0.09637501, 0.55840072]])
讨论
支持向量机得名于这个事实:决策超平面由相对较少的被称为支持向量的观测决定。直观地说,可以将超平面看作由这些支持向量“支持”。因此,这些支持向量对我们的模型非常重要。例如,如果从数据中删除一个非支持向量的观测,模型不会改变;但是,如果删除一个支持向量,超平面将不会具有最大间隔。
在我们训练了支持向量机之后,scikit-learn 提供了多种选项来识别支持向量。在我们的解决方案中,我们使用了support_vectors_
来输出我们模型中四个支持向量的实际观测特征。或者,我们可以使用support_
查看支持向量的索引:
model.support_
array([23, 41, 57, 98], dtype=int32)
最后,我们可以使用n_support_
来查找属于每个类的支持向量的数量:
model.n_support_
array([2, 2], dtype=int32)
17.5 处理不平衡类
问题
在存在类不平衡的情况下,您需要训练支持向量机分类器。
解决方案
使用class_weight
增加误分类较小类别的惩罚:
# Load libraries
from sklearn.svm import SVC
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
import numpy as np
# Load data with only two classes
iris = datasets.load_iris()
features = iris.data[:100,:]
target = iris.target[:100]
# Make class highly imbalanced by removing first 40 observations
features = features[40:,:]
target = target[40:]
# Create target vector indicating if class 0, otherwise 1
target = np.where((target == 0), 0, 1)
# Standardize features
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)
# Create support vector classifier
svc = SVC(kernel="linear", class_weight="balanced", C=1.0, random_state=0)
# Train classifier
model = svc.fit(features_standardized, target)
讨论
在支持向量机中,是一个超参数,用于确定误分类观察的惩罚。处理支持向量机中的不平衡类的一种方法是通过按类别加权,使得:
其中是错误分类的惩罚,是与类频率成反比的权重,是类的值。总体思路是增加对误分类少数类的惩罚,以防止它们被多数类“淹没”。
在 scikit-learn 中,当使用SVC
时,可以通过设置class_weight="balanced"
来自动设置的值。balanced
参数会自动加权各个类别,以便:
其中是类的权重,是观察次数,是类中的观察次数,是总类数。
第十八章:朴素贝叶斯
18.0 引言
贝叶斯定理是理解某些事件概率的首选方法,如在给定一些新信息 和对事件概率的先验信念 的情况下,事件 的概率。
贝叶斯方法在过去十年中的流行度急剧上升,越来越多地在学术界、政府和企业中与传统的频率学应用竞争。在机器学习中,贝叶斯定理在分类问题上的一种应用是朴素贝叶斯分类器。朴素贝叶斯分类器将多种实用的机器学习优点结合到一个单一的分类器中。这些优点包括:
-
一种直观的方法
-
能够处理少量数据
-
训练和预测的低计算成本
-
在各种设置中通常能够产生可靠的结果
具体来说,朴素贝叶斯分类器基于:
其中:
-
被称为后验概率,表示观察值为 特征时类别 的概率。
-
被称为似然,表示在给定类别 时,特征 的观察值的可能性。
-
被称为先验概率,表示在观察数据之前,类别 的概率信念。
-
被称为边缘概率。
在朴素贝叶斯中,我们比较每个可能类别的观测后验概率值。具体来说,因为边际概率在这些比较中是恒定的,我们比较每个类别后验的分子部分。对于每个观测,具有最大后验分子的类别成为预测类别,。
有两个关于朴素贝叶斯分类器需要注意的重要事项。首先,对于数据中的每个特征,我们必须假设似然的统计分布,。常见的分布包括正态(高斯)、多项式和伯努利分布。选择的分布通常由特征的性质(连续、二进制等)决定。其次,朴素贝叶斯之所以得名,是因为我们假设每个特征及其结果的似然是独立的。这种“朴素”的假设在实践中往往是错误的,但并不会阻止构建高质量的分类器。
在本章中,我们将介绍使用 scikit-learn 训练三种类型的朴素贝叶斯分类器,使用三种不同的似然分布。此后,我们将学习如何校准朴素贝叶斯模型的预测,使其可解释。
18.1 训练连续特征的分类器
问题
您只有连续特征,并且希望训练朴素贝叶斯分类器。
解决方案
在 scikit-learn 中使用高斯朴素贝叶斯分类器:
# Load libraries
from sklearn import datasets
from sklearn.naive_bayes import GaussianNB
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create Gaussian naive Bayes object
classifer = GaussianNB()
# Train model
model = classifer.fit(features, target)
讨论
最常见的朴素贝叶斯分类器类型是高斯朴素贝叶斯。在高斯朴素贝叶斯中,我们假设给定观测的特征值的似然,,属于类别,遵循正态分布:
其中和分别是特征对类别的方差和均值。由于正态分布的假设,高斯朴素贝叶斯最适合于所有特征均为连续的情况。
在 scikit-learn 中,我们像训练其他模型一样训练高斯朴素贝叶斯,使用fit
,然后可以对观测的类别进行预测:
# Create new observation
new_observation = [[ 4, 4, 4, 0.4]]
# Predict class
model.predict(new_observation)
array([1])
朴素贝叶斯分类器的一个有趣方面之一是,它们允许我们对目标类别分配先验信念。我们可以使用GaussianNB priors
参数来实现这一点,该参数接受目标向量每个类别的概率列表:
# Create Gaussian naive Bayes object with prior probabilities of each class
clf = GaussianNB(priors=[0.25, 0.25, 0.5])
# Train model
model = classifer.fit(features, target)
如果我们不向priors
参数添加任何参数,则根据数据调整先验。
最后,请注意,从高斯朴素贝叶斯获得的原始预测概率(使用predict_proba
输出)未经校准。也就是说,它们不应被信任。如果我们想要创建有用的预测概率,我们需要使用等渗回归或相关方法进行校准。
另请参阅
18.2 训练离散和计数特征的分类器
问题
给定离散或计数数据,您需要训练一个朴素贝叶斯分类器。
解决方案
使用多项式朴素贝叶斯分类器:
# Load libraries
import numpy as np
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer
# Create text
text_data = np.array(['I love Brazil. Brazil!',
'Brazil is best',
'Germany beats both'])
# Create bag of words
count = CountVectorizer()
bag_of_words = count.fit_transform(text_data)
# Create feature matrix
features = bag_of_words.toarray()
# Create target vector
target = np.array([0,0,1])
# Create multinomial naive Bayes object with prior probabilities of each class
classifer = MultinomialNB(class_prior=[0.25, 0.5])
# Train model
model = classifer.fit(features, target)
讨论
多项式朴素贝叶斯的工作方式与高斯朴素贝叶斯类似,但特征被假定为多项式分布。实际上,这意味着当我们有离散数据时(例如,电影评分从 1 到 5),这种分类器通常被使用。多项式朴素贝叶斯最常见的用途之一是使用词袋或方法进行文本分类(参见 Recipes 6.9 和 6.10)。
在我们的解决方案中,我们创建了一个包含三个观察结果的玩具文本数据集,并将文本字符串转换为词袋特征矩阵和相应的目标向量。然后,我们使用MultinomialNB
来训练一个模型,同时为两个类别(支持巴西和支持德国)定义了先验概率。
MultinomialNB
的工作方式类似于GaussianNB
;模型使用fit
进行训练,并且可以使用predict
进行预测:
# Create new observation
new_observation = [[0, 0, 0, 1, 0, 1, 0]]
# Predict new observation's class
model.predict(new_observation)
array([0])
如果未指定class_prior
,则使用数据学习先验概率。但是,如果我们想要使用均匀分布作为先验,可以设置fit_prior=False
。
最后,MultinomialNB
包含一个添加平滑的超参数alpha
,应该进行调节。默认值为1.0
,0.0
表示不进行平滑。
18.3 训练二元特征的朴素贝叶斯分类器
问题
您有二元特征数据,并需要训练一个朴素贝叶斯分类器。
解决方案
使用伯努利朴素贝叶斯分类器:
# Load libraries
import numpy as np
from sklearn.naive_bayes import BernoulliNB
# Create three binary features
features = np.random.randint(2, size=(100, 3))
# Create a binary target vector
target = np.random.randint(2, size=(100, 1)).ravel()
# Create Bernoulli naive Bayes object with prior probabilities of each class
classifer = BernoulliNB(class_prior=[0.25, 0.5])
# Train model
model = classifer.fit(features, target)
讨论
伯努利朴素贝叶斯分类器假设所有特征都是二元的,即它们只能取两个值(例如,已经进行了独热编码的名义分类特征)。与其多项式兄弟一样,伯努利朴素贝叶斯在文本分类中经常被使用,当我们的特征矩阵仅是文档中单词的存在或不存在时。此外,像MultinomialNB
一样,BernoulliNB
也有一个添加平滑的超参数alpha
,我们可以使用模型选择技术来调节。最后,如果我们想使用先验概率,可以使用class_prior
参数并将其设置为包含每个类的先验概率的列表。如果我们想指定均匀先验,可以设置fit_prior=False
:
model_uniform_prior = BernoulliNB(class_prior=None, fit_prior=False)
18.4 校准预测概率
问题
您希望校准朴素贝叶斯分类器的预测概率,以便能够解释它们。
解决方案
使用 CalibratedClassifierCV
:
# Load libraries
from sklearn import datasets
from sklearn.naive_bayes import GaussianNB
from sklearn.calibration import CalibratedClassifierCV
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create Gaussian naive Bayes object
classifer = GaussianNB()
# Create calibrated cross-validation with sigmoid calibration
classifer_sigmoid = CalibratedClassifierCV(classifer, cv=2, method='sigmoid')
# Calibrate probabilities
classifer_sigmoid.fit(features, target)
# Create new observation
new_observation = [[ 2.6, 2.6, 2.6, 0.4]]
# View calibrated probabilities
classifer_sigmoid.predict_proba(new_observation)
array([[0.31859969, 0.63663466, 0.04476565]])
讨论
类概率是机器学习模型中常见且有用的一部分。在 scikit-learn 中,大多数学习算法允许我们使用 predict_proba
来查看类成员的预测概率。例如,如果我们只想在模型预测某个类的概率超过 90%时预测该类,这将非常有用。然而,一些模型,包括朴素贝叶斯分类器,输出的概率不是基于现实世界的。也就是说,predict_proba
可能会预测一个观测属于某一类的概率是 0.70,而实际上可能是 0.10 或 0.99。具体来说,在朴素贝叶斯中,虽然对不同目标类的预测概率排序是有效的,但原始预测概率往往会取极端值,接近 0 或 1。
要获得有意义的预测概率,我们需要进行所谓的校准。在 scikit-learn 中,我们可以使用 CalibratedClassifierCV
类通过 k 折交叉验证创建良好校准的预测概率。在 CalibratedClassifierCV
中,训练集用于训练模型,测试集用于校准预测概率。返回的预测概率是 k 折交叉验证的平均值。
使用我们的解决方案,我们可以看到原始和良好校准的预测概率之间的差异。在我们的解决方案中,我们创建了一个高斯朴素贝叶斯分类器。如果我们训练该分类器,然后预测新观测的类概率,我们可以看到非常极端的概率估计:
# Train a Gaussian naive Bayes then predict class probabilities
classifer.fit(features, target).predict_proba(new_observation)
array([[2.31548432e-04, 9.99768128e-01, 3.23532277e-07]])
然而,如果在我们校准预测的概率之后(我们在我们的解决方案中完成了这一步),我们得到非常不同的结果:
# View calibrated probabilities
array([[0.31859969, 0.63663466, 0.04476565]])
array([[ 0.31859969, 0.63663466, 0.04476565]])
CalibratedClassifierCV
提供两种校准方法——Platt 的 sigmoid 模型和等温回归——由 method
参数定义。虽然我们没有空间详细讨论,但由于等温回归是非参数的,当样本量非常小时(例如 100 个观测),它往往会过拟合。在我们的解决方案中,我们使用了包含 150 个观测的鸢尾花数据集,因此使用了 Platt 的 sigmoid 模型。
第十九章:聚类
19.0 介绍
在本书的大部分内容中,我们已经研究了监督机器学习——我们既可以访问特征又可以访问目标的情况。不幸的是,这并不总是事实。经常情况下,我们遇到的情况是我们只知道特征。例如,想象一下我们有一家杂货店的销售记录,并且我们想要按照购物者是否是折扣俱乐部会员来分割销售记录。使用监督学习是不可能的,因为我们没有一个目标来训练和评估我们的模型。然而,还有另一种选择:无监督学习。如果杂货店的折扣俱乐部会员和非会员的行为实际上是不同的,那么两个会员之间的平均行为差异将小于会员和非会员购物者之间的平均行为差异。换句话说,会有两个观察结果的簇。
聚类算法的目标是识别那些潜在的观察结果分组,如果做得好,即使没有目标向量,我们也能够预测观察结果的类别。有许多聚类算法,它们有各种各样的方法来识别数据中的簇。在本章中,我们将介绍一些使用 scikit-learn 的聚类算法以及如何在实践中使用它们。
19.1 使用 K 均值进行聚类
问题
你想要将观察结果分成k组。
解决方案
使用k 均值聚类:
# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# Load data
iris = datasets.load_iris()
features = iris.data
# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)
# Create k-means object
cluster = KMeans(n_clusters=3, random_state=0, n_init="auto")
# Train model
model = cluster.fit(features_std)
讨论
k 均值聚类是最常见的聚类技术之一。在 k 均值聚类中,算法试图将观察结果分成k组,每组的方差大致相等。组数k由用户作为超参数指定。具体来说,在 k 均值聚类中:
-
在随机位置创建k聚类的“中心”点。
-
对于每个观察结果:
-
计算每个观察结果与k中心点的距离。
-
观察结果被分配到最近中心点的簇中。
-
-
中心点被移动到各自簇的平均值(即,中心)。
-
步骤 2 和 3 重复,直到没有观察结果在簇成员资格上发生变化。
在这一点上,算法被认为已经收敛并停止。
关于 k 均值聚类有三点需要注意。首先,k 均值聚类假设簇是凸形的(例如,圆形,球形)。其次,所有特征都是等比例缩放的。在我们的解决方案中,我们标准化了特征以满足这个假设。第三,各组是平衡的(即,观察结果的数量大致相同)。如果我们怀疑无法满足这些假设,我们可以尝试其他聚类方法。
在 scikit-learn 中,k-means 聚类是在 KMeans
类中实现的。最重要的参数是 n_clusters
,它设置聚类数 k。在某些情况下,数据的性质将决定 k 的值(例如,学校学生的数据将有一个班级对应一个聚类),但通常我们不知道聚类数。在这些情况下,我们希望基于某些准则选择 k。例如,轮廓系数(参见第 11.9 节)可以衡量聚类内部的相似性与聚类间的相似性。此外,由于 k-means 聚类计算开销较大,我们可能希望利用计算机的所有核心。我们可以通过设置 n_jobs=-1
来实现这一点。
在我们的解决方案中,我们有点作弊,使用了已知包含三个类别的鸢尾花数据。因此,我们设置 k = 3。我们可以使用 labels_
查看每个观测数据的预测类别:
# View predicted class
model.labels_
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2,
1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2,
2, 1, 2, 1, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 1, 1, 2, 2, 2, 2,
2, 1, 1, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1], dtype=int32)
如果我们将其与观测数据的真实类别进行比较,可以看到,尽管类标签有所不同(即 0
、1
和 2
),k-means 的表现还是相当不错的:
# View true class
iris.target
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
然而,正如你所想象的那样,如果我们选择了错误的聚类数,k-means 的性能将显著甚至可能严重下降。
最后,与其他 scikit-learn 模型一样,我们可以使用训练好的聚类模型来预测新观测数据的值:
# Create new observation
new_observation = [[0.8, 0.8, 0.8, 0.8]]
# Predict observation's cluster
model.predict(new_observation)
array([2], dtype=int32)
预测观测数据属于距离其最近的聚类中心点。我们甚至可以使用 cluster_centers_
查看这些中心点:
# View cluster centers
model.cluster_centers_
array([[-1.01457897, 0.85326268, -1.30498732, -1.25489349],
[-0.01139555, -0.87600831, 0.37707573, 0.31115341],
[ 1.16743407, 0.14530299, 1.00302557, 1.0300019 ]])
参见
19.2 加速 K-Means 聚类
问题
您希望将观测数据分组成 k 组,但 k-means 太耗时。
解决方案
使用 mini-batch k-means:
# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import MiniBatchKMeans
# Load data
iris = datasets.load_iris()
features = iris.data
# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)
# Create k-mean object
cluster = MiniBatchKMeans(n_clusters=3, random_state=0, batch_size=100,
n_init="auto")
# Train model
model = cluster.fit(features_std)
讨论
Mini-batch k-means 类似于讨论中的 k-means 算法(见第 19.1 节)。不详细讨论的话,两者的区别在于,mini-batch k-means 中计算成本最高的步骤仅在随机抽样的观测数据上进行,而不是全部观测数据。这种方法可以显著减少算法找到收敛(即拟合数据)所需的时间,只有少量的质量损失。
MiniBatchKMeans
类似于 KMeans
,但有一个显著的区别:batch_size
参数。batch_size
控制每个批次中随机选择的观测数据数量。批次大小越大,训练过程的计算成本越高。
19.3 使用均值漂移进行聚类
问题
您希望在不假设聚类数或形状的情况下对观测数据进行分组。
解决方案
使用均值漂移聚类:
# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import MeanShift
# Load data
iris = datasets.load_iris()
features = iris.data
# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)
# Create mean shift object
cluster = MeanShift(n_jobs=-1)
# Train model
model = cluster.fit(features_std)
讨论
我们之前讨论过 k-means 聚类的一个缺点是在训练之前需要设置聚类数 k,并且该方法对聚类形状作出了假设。一种无此限制的聚类算法是均值漂移。
均值漂移是一个简单的概念,但有些难以解释。因此,通过类比可能是最好的方法。想象一个非常雾蒙蒙的足球场(即,二维特征空间),上面站着 100 个人(即,我们的观察结果)。因为有雾,一个人只能看到很短的距离。每分钟,每个人都会四处张望,并朝着能看到最多人的方向迈出一步。随着时间的推移,人们开始团结在一起,重复向更大的人群迈步。最终的结果是围绕场地的人群聚类。人们被分配到他们最终停留的聚类中。
scikit-learn 的实际均值漂移实现,MeanShift
,更为复杂,但遵循相同的基本逻辑。MeanShift
有两个重要的参数我们应该注意。首先,bandwidth
设置了观察使用的区域(即核心)的半径,以确定向何处移动。在我们的类比中,bandwidth 代表一个人透过雾能看到的距离。我们可以手动设置此参数,但默认情况下会自动估算一个合理的带宽(计算成本显著增加)。其次,在均值漂移中有时没有其他观察在观察的核心中。也就是说,在我们的足球场上,一个人看不到其他人。默认情况下,MeanShift
将所有这些“孤儿”观察分配给最近观察的核心。但是,如果我们希望排除这些孤儿,我们可以设置cluster_all=False
,其中孤儿观察被赋予标签-1
。
参见
19.4 使用 DBSCAN 进行聚类
问题
您希望将观察结果分组为高密度的聚类。
解决方案
使用 DBSCAN 聚类:
# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN
# Load data
iris = datasets.load_iris()
features = iris.data
# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)
# Create DBSCAN object
cluster = DBSCAN(n_jobs=-1)
# Train model
model = cluster.fit(features_std)
讨论
DBSCAN的动机在于,聚类将是许多观察结果密集堆积的区域,并且不对聚类形状做出假设。具体来说,在 DBSCAN 中:
-
选择一个随机观察结果,x[i]。
-
如果x[i]有足够数量的近邻观察,我们认为它是聚类的一部分。
-
步骤 2 递归地重复对x[i]的所有邻居,邻居的邻居等的处理。这些是聚类的核心观察结果。
-
一旦步骤 3 耗尽附近的观察,就会选择一个新的随机点(即,在步骤 1 重新开始)。
一旦完成这一步骤,我们就得到了多个聚类的核心观察结果集。最终,任何靠近聚类但不是核心样本的观察被认为是聚类的一部分,而不靠近聚类的观察则被标记为离群值。
DBSCAN
有三个主要的参数需要设置:
eps
一个观察到另一个观察的最大距离,以便将其视为邻居。
min_samples
小于eps
距离的观察数目最少的观察,被认为是核心观察结果。
metric
eps
使用的距离度量——例如,minkowski
或euclidean
(注意,如果使用 Minkowski 距离,参数p
可以用来设置 Minkowski 度量的幂)。
如果我们查看我们的训练数据中的集群,我们可以看到已经识别出两个集群,0
和1
,而异常值观测被标记为-1
:
# Show cluster membership
model.labels_
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1,
0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, -1, -1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1,
-1, 1, 1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-1, 1, -1, 1, 1, 1, 1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1,
1, 1, 1, -1, -1, -1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1,
-1, 1, 1, -1, 1, 1, -1, 1, 1, 1, -1, -1, -1, 1, 1, 1, -1,
-1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, 1])
另请参阅
19.5 使用分层合并进行聚类
问题
您想使用集群的层次结构对观测进行分组。
解决方案
使用聚类:
# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import AgglomerativeClustering
# Load data
iris = datasets.load_iris()
features = iris.data
# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)
# Create agglomerative clustering object
cluster = AgglomerativeClustering(n_clusters=3)
# Train model
model = cluster.fit(features_std)
讨论
凝聚式聚类是一种强大、灵活的分层聚类算法。在凝聚式聚类中,所有观测都开始作为自己的集群。接下来,满足一些条件的集群被合并。这个过程重复进行,直到达到某个结束点为止。在 scikit-learn 中,AgglomerativeClustering
使用linkage
参数来确定最小化合并策略:
-
合并集群的方差(
ward
) -
来自成对集群的观察之间的平均距离(
average
) -
来自成对集群的观察之间的最大距离(
complete
)
还有两个有用的参数需要知道。首先,affinity
参数确定用于linkage
的距离度量(minkowski
、euclidean
等)。其次,n_clusters
设置聚类算法将尝试找到的聚类数。也就是说,集群被连续合并,直到只剩下n_clusters
。
与我们讨论过的其他聚类算法一样,我们可以使用labels_
来查看每个观察被分配到的集群:
# Show cluster membership
model.labels_
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,
1, 1, 1, 1, 1, 1, 0, 0, 0, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 0, 2, 0,
2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 2, 0, 0, 2,
2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
第二十章:PyTorch 中的张量
20.0 简介
就像 NumPy 是机器学习堆栈中数据操作的基础工具一样,PyTorch 是深度学习堆栈中处理张量的基础工具。在深入学习之前,我们应该熟悉 PyTorch 张量,并创建许多与 NumPy 中执行的操作类似的操作(见 第一章)。
虽然 PyTorch 只是多个深度学习库之一,但在学术界和工业界都非常流行。PyTorch 张量与 NumPy 数组非常相似。然而,它们还允许我们在 GPU(专门用于深度学习的硬件)上执行张量操作。在本章中,我们将熟悉 PyTorch 张量的基础知识和许多常见的低级操作。
20.1 创建张量
问题
您需要创建一个张量。
解决方案
使用 PyTorch 创建张量:
# Load library
import torch
# Create a vector as a row
tensor_row = torch.tensor([1, 2, 3])
# Create a vector as a column
tensor_column = torch.tensor(
[
[1],
[2],
[3]
]
)
讨论
PyTorch 中的主要数据结构是张量,在许多方面,张量与多维 NumPy 数组(见 第一章)完全相同。就像向量和数组一样,这些张量可以水平(即行)或垂直(即列)表示。
参见
20.2 从 NumPy 创建张量
问题
您需要从 NumPy 数组创建 PyTorch 张量。
解决方案
使用 PyTorch 的 from_numpy
函数:
# Import libraries
import numpy as np
import torch
# Create a NumPy array
vector_row = np.array([1, 2, 3])
# Create a tensor from a NumPy array
tensor_row = torch.from_numpy(vector_row)
讨论
正如我们所看到的,PyTorch 在语法上与 NumPy 非常相似。此外,它还允许我们轻松地将 NumPy 数组转换为可以在 GPU 和其他加速硬件上使用的 PyTorch 张量。在撰写本文时,PyTorch 文档中频繁提到 NumPy,并且 PyTorch 本身甚至提供了一种使 PyTorch 张量和 NumPy 数组可以共享内存以减少开销的方式。
参见
20.3 创建稀疏张量
问题
给定数据,其中非零值非常少,您希望以张量的方式高效表示它。
解决方案
使用 PyTorch 的 to_sparse
函数:
# Import libraries
import torch
# Create a tensor
tensor = torch.tensor(
[
[0, 0],
[0, 1],
[3, 0]
]
)
# Create a sparse tensor from a regular tensor
sparse_tensor = tensor.to_sparse()
讨论
稀疏张量是表示由大多数 0 组成的数据的内存高效方法。在 第一章 中,我们使用 scipy
创建了一个压缩稀疏行(CSR)矩阵,它不再是 NumPy 数组。
torch.Tensor
类允许我们使用同一个对象创建常规矩阵和稀疏矩阵。如果我们检查刚刚创建的两个张量的类型,我们可以看到它们实际上都属于同一类:
print(type(tensor))
print(type(sparse_tensor))
<class 'torch.Tensor'>
<class 'torch.Tensor'>
参见
20.4 在张量中选择元素
问题
我们需要选择张量的特定元素。
解决方案
使用类似于 NumPy 的索引和切片返回元素:
# Load library
import torch
# Create vector tensor
vector = torch.tensor([1, 2, 3, 4, 5, 6])
# Create matrix tensor
matrix = torch.tensor(
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
)
# Select third element of vector
vector[2]
tensor(3)
# Select second row, second column
matrix[1,1]
tensor(5)
讨论
像 NumPy 数组和 Python 中的大多数内容一样,PyTorch 张量也是从零开始索引的。索引和切片也都受支持。一个关键区别是,对 PyTorch 张量进行索引以返回单个元素仍然会返回一个张量,而不是对象本身的值(该值将是整数或浮点数)。切片语法也与 NumPy 相同,并将以张量对象的形式返回:
# Select all elements of a vector
vector[:]
array([1, 2, 3, 4, 5, 6])
# Select everything up to and including the third element
vector[:3]
tensor([1, 2, 3])
# Select everything after the third element
vector[3:]
tensor([4, 5, 6])
# Select the last element
vector[-1]
tensor(6)
# Select the first two rows and all columns of a matrix
matrix[:2,:]
tensor([[1, 2, 3],
[4, 5, 6]])
# Select all rows and the second column
matrix[:,1:2]
tensor([[2],
[5],
[8]])
一个关键区别是,PyTorch 张量在切片时不支持负步长。因此,尝试使用切片反转张量会产生错误:
# Reverse the vector
vector[::-1]
ValueError: step must be greater than zero
相反,如果我们希望反转张量,我们可以使用 flip
方法:
vector.flip(dims=(-1,))
tensor([6, 5, 4, 3, 2, 1])
参见
20.5 描述张量
问题
您想描述张量的形状、数据类型和格式以及它所使用的硬件。
解决方案
检查张量的 shape
、dtype
、layout
和 device
属性:
# Load library
import torch
# Create a tensor
tensor = torch.tensor([[1,2,3], [1,2,3]])
# Get the shape of the tensor
tensor.shape
torch.Size([2, 3])
# Get the data type of items in the tensor
tensor.dtype
torch.int64
# Get the layout of the tensor
tensor.layout
torch.strided
# Get the device being used by the tensor
tensor.device
device(type='cpu')
讨论
PyTorch 张量提供了许多有用的属性,用于收集关于给定张量的信息,包括:
形状
返回张量的维度
Dtype
返回张量中对象的数据类型
布局
返回内存布局(最常见的是用于稠密张量的 strided
)
设备
返回张量存储的硬件(CPU/GPU)
再次,张量与数组的主要区别在于 设备 这样的属性,因为张量为我们提供了像 GPU 这样的硬件加速选项。
20.6 对元素应用操作
问题
您想对张量中的所有元素应用操作。
解决方案
利用 PyTorch 进行 广播:
# Load library
import torch
# Create a tensor
tensor = torch.tensor([1, 2, 3])
# Broadcast an arithmetic operation to all elements in a tensor
tensor * 100
tensor([100, 200, 300])
讨论
PyTorch 中的基本操作将利用广播并行化,使用像 GPU 这样的加速硬件。这对于 Python 中支持的数学运算符(+、-、×、/)和 PyTorch 内置函数是真实的。与 NumPy 不同,PyTorch 不包括用于在张量上应用函数的 vectorize
方法。然而,PyTorch 配备了所有必要的数学工具,以分发和加速深度学习工作流程中所需的常规操作。
参见
20.7 查找最大值和最小值
问题
您需要在张量中找到最大值或最小值。
解决方案
使用 PyTorch 的 max
和 min
方法:
# Load library
import torch
# Create a tensor
torch.tensor([1,2,3])
# Find the largest value
tensor.max()
tensor(3)
# Find the smallest value
tensor.min()
tensor(1)
讨论
张量的 max
和 min
方法帮助我们找到该张量中的最大值或最小值。这些方法在多维张量上同样适用:
# Create a multidimensional tensor
tensor = torch.tensor([[1,2,3],[1,2,5]])
# Find the largest value
tensor.max()
tensor(5)
20.8 改变张量的形状
问题
您希望改变张量的形状(行数和列数)而不改变元素值。
解决方案
使用 PyTorch 的 reshape
方法:
# Load library
import torch
# Create 4x3 tensor
tensor = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12]])
# Reshape tensor into 2x6 tensor
tensor.reshape(2, 6)
tensor([[ 1, 2, 3, 4, 5, 6],
[ 7, 8, 9, 10, 11, 12]])
讨论
在深度学习领域中,操作张量的形状可能很常见,因为神经网络中的神经元通常需要具有非常特定形状的张量。由于给定神经网络中的神经元之间所需的张量形状可能会发生变化,因此了解深度学习中输入和输出的低级细节是很有好处的。
20.9 转置张量
问题
您需要转置一个张量。
解决方案
使用 mT
方法:
# Load library
import torch
# Create a two-dimensional tensor
tensor = torch.tensor([[[1,2,3]]])
# Transpose it
tensor.mT
tensor([[1],
[2],
[3]])
讨论
使用 PyTorch 进行转置与 NumPy 略有不同。用于 NumPy 数组的 T
方法仅支持二维张量,在 PyTorch 中对于其他形状的张量时,该方法在写作时已被弃用。用于转置批处理张量的 mT
方法更受欢迎,因为它适用于超过两个维度的张量。
除了使用 permute
方法之外,还可以使用 PyTorch 中的另一种方式来转置任意形状的张量:
tensor.permute(*torch.arange(tensor.ndim - 1, -1, -1))
tensor([[1],
[2],
[3]])
这种方法也适用于一维张量(其中转置张量的值与原始张量相同)。
20.10 张量展平
问题
您需要将张量转换为一维。
解决方案
使用 flatten
方法:
# Load library
import torch
# Create tensor
tensor = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Flatten tensor
tensor.flatten()
tensor([1, 2, 3, 4, 5, 6, 7, 8, 9])
讨论
张量展平是将多维张量降维为一维的一种有用技术。
20.11 计算点积
问题
您需要计算两个张量的点积。
解决方案
使用 dot
方法:
# Load library
import torch
# Create one tensor
tensor_1 = torch.tensor([1, 2, 3])
# Create another tensor
tensor_2 = torch.tensor([4, 5, 6])
# Calculate the dot product of the two tensors
tensor_1.dot(tensor_2)
tensor(32)
讨论
计算两个张量的点积是深度学习空间以及信息检索空间中常用的操作。您可能还记得本书中我们使用两个向量的点积执行基于余弦相似度的搜索。在 PyTorch 上使用 GPU(而不是在 CPU 上使用 NumPy 或 scikit-learn)执行此操作可以在信息检索问题上获得显著的性能优势。
参见
20.12 乘法张量
问题
您需要将两个张量相乘。
解决方案
使用基本的 Python 算术运算符:
# Load library
import torch
# Create one tensor
tensor_1 = torch.tensor([1, 2, 3])
# Create another tensor
tensor_2 = torch.tensor([4, 5, 6])
# Multiply the two tensors
tensor_1 * tensor_2
tensor([ 4, 10, 18])
讨论
PyTorch 支持基本算术运算符,如 ×、+、- 和 /。虽然在深度学习中,张量乘法可能是最常用的操作之一,但了解张量也可以进行加法、减法和除法是很有用的。
将一个张量加到另一个张量中:
tensor_1+tensor_2
tensor([5, 7, 9])
从一个张量中减去另一个张量:
tensor_1-tensor_2
tensor([-3, -3, -3])
将一个张量除以另一个张量:
tensor_1/tensor_2
tensor([0.2500, 0.4000, 0.5000])
第二十一章:神经网络
21.0 引言
基本神经网络的核心是单元(也称为节点或神经元)。一个单元接收一个或多个输入,将每个输入乘以一个参数(也称为权重),将加权输入的值与一些偏置值(通常为 0)求和,然后将值馈送到激活函数中。然后,该输出被发送到神经网络中更深层的其他神经元(如果存在)。
神经网络可以被视为一系列连接的层,形成一个网络,将观察的特征值连接在一端,目标值(例如,观察的类)连接在另一端。前馈神经网络—也称为多层感知器—是任何实际设置中使用的最简单的人工神经网络。名称“前馈”来自于这样一个事实:观察的特征值被“前向”传递到网络中,每一层逐渐地转换特征值,目标是输出与目标值相同(或接近)。
具体而言,前馈神经网络包含三种类型的层。在神经网络的开始处是输入层,每个单元包含单个特征的观察值。例如,如果一个观察有 100 个特征,输入层有 100 个单元。在神经网络的末端是输出层,它将中间层(称为隐藏层)的输出转换为对任务有用的值。例如,如果我们的目标是二元分类,可以使用一个输出层,其中一个单元使用 sigmoid 函数将自己的输出缩放到 0 到 1 之间,表示预测的类概率。
在输入层和输出层之间是所谓的隐藏层。这些隐藏层逐步转换从输入层获取的特征值,以使其在被输出层处理后类似于目标类。具有许多隐藏层(例如,10、100、1,000)的神经网络被认为是“深”网络。训练深度神经网络的过程称为深度学习。
神经网络通常是用高斯或正态均匀分布中的小随机值初始化所有参数。一旦观察到(或更频繁地说是一组称为批量的观察),通过网络,输出的值与观察到的真实值使用损失函数进行比较。这称为前向传播。接下来,算法通过网络“向后”传播,识别每个参数在预测值和真实值之间误差中的贡献,这个过程称为反向传播。在每个参数处,优化算法确定每个权重应该调整多少以改善输出。
神经网络通过重复进行前向传播和反向传播的过程来学习,每个观察结果都会多次(每次所有观察结果都通过网络称为epoch,训练通常包含多个 epoch),通过使用梯度下降过程来逐步优化参数值,从而优化给定输出的参数值。
在本章中,我们将使用上一章节中使用的同一 Python 库 PyTorch 来构建、训练和评估各种神经网络。PyTorch 是深度学习领域内流行的工具,因为其良好编写的 API 和直观表示低级张量操作的能力。PyTorch 的一个关键特性被称为autograd,它在前向传播和反向传播后自动计算和存储用于优化网络参数的梯度。
使用 PyTorch 代码创建的神经网络可以使用 CPU(例如,您的笔记本电脑)和 GPU(例如,专门的深度学习计算机)进行训练。在现实世界中使用真实数据时,通常需要使用 GPU 来训练神经网络,因为对于大数据和复杂网络,使用 GPU 比使用 CPU 快数个数量级。然而,本书中的所有神经网络都足够小和简单,可以仅使用 CPU 在几分钟内训练。只需注意,当我们有更大的网络和更多的训练数据时,使用 CPU 训练比使用 GPU 训练显著慢。
21.1 使用 PyTorch 的 Autograd
问题
您希望在前向传播和反向传播后使用 PyTorch 的自动微分功能来计算和存储梯度。
解决方案
使用requires_grad
选项设置为True
创建张量:
# Import libraries
import torch
# Create a torch tensor that requires gradients
t = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
# Perform a tensor operation simulating "forward propagation"
tensor_sum = t.sum()
# Perform back propagation
tensor_sum.backward()
# View the gradients
t.grad
tensor([1., 1., 1.])
讨论
自动微分是 PyTorch 的核心特性之一,也是其作为深度学习库受欢迎的重要因素之一。能够轻松计算、存储和可视化梯度使得 PyTorch 对于从头构建神经网络的研究人员和爱好者来说非常直观。
PyTorch 使用有向无环图(DAG)来记录在数据上执行的所有数据和计算操作。这非常有用,但也意味着我们在尝试应用需要梯度的 PyTorch 数据的操作时需要小心。在使用自动微分时,我们不能轻松地将张量转换为 NumPy 数组,也不能将其转换回来,而不会“破坏图”,这是用来描述不支持自动微分的操作的术语:
import torch
tensor = torch.tensor([1.0,2.0,3.0], requires_grad=True)
tensor.numpy()
RuntimeError: Can't call numpy() on Tensor that requires grad. Use
tensor.detach().numpy() instead.
要将此张量转换为 NumPy 数组,我们需要在其上调用detach()
方法,这将中断计算图,从而无法自动计算梯度。虽然这确实有用,但值得注意的是,分离张量将阻止 PyTorch 自动计算梯度。
参见
21.2 为神经网络预处理数据
问题
你想为神经网络预处理数据。
解决方案
使用 scikit-learn 的StandardScaler
标准化每个特征:
# Load libraries
from sklearn import preprocessing
import numpy as np
# Create feature
features = np.array([[-100.1, 3240.1],
[-200.2, -234.1],
[5000.5, 150.1],
[6000.6, -125.1],
[9000.9, -673.1]])
# Create scaler
scaler = preprocessing.StandardScaler()
# Convert to a tensor
features_standardized_tensor = torch.from_numpy(features)
# Show features
features_standardized_tensor
tensor([[-100.1000, 3240.1000],
[-200.2000, -234.1000],
[5000.5000, 150.1000],
[6000.6000, -125.1000],
[9000.9000, -673.1000]], dtype=torch.float64)
讨论
尽管这个配方与配方 4.2 非常相似,但由于对神经网络的重要性,值得重复。通常情况下,神经网络的参数被初始化(即创建)为小的随机数。当特征值远远大于参数值时,神经网络的表现通常不佳。此外,由于观察的特征值在通过各个单元时被合并,因此重要的是所有特征具有相同的尺度。
出于这些原因,最佳实践是(虽然不总是必要;例如,当所有特征都是二进制时)标准化每个特征,使得特征值具有均值为 0 和标准差为 1。使用 scikit-learn 的StandardScaler
可以轻松实现这一点。
然而,如果你需要在创建了requires_grad=True
的张量之后执行此操作,则需要在 PyTorch 中原生地执行,以避免破坏图形。虽然通常会在开始训练网络之前标准化特征,但了解如何在 PyTorch 中完成相同的事情也是值得的:
# Load library
import torch
# Create features
torch_features = torch.tensor([[-100.1, 3240.1],
[-200.2, -234.1],
[5000.5, 150.1],
[6000.6, -125.1],
[9000.9, -673.1]], requires_grad=True)
# Compute the mean and standard deviation
mean = torch_features.mean(0, keepdim=True)
standard_deviation = torch_features.std(0, unbiased=False, keepdim=True)
# Standardize the features using the mean and standard deviation
torch_features_standardized = torch_features - mean
torch_features_standardized /= standard_deviation
# Show standardized features
torch_features_standardized
tensor([[-1.1254, 1.9643],
[-1.1533, -0.5007],
[ 0.2953, -0.2281],
[ 0.5739, -0.4234],
[ 1.4096, -0.8122]], grad_fn=<DivBackward0>)
21.3 设计一个神经网络
问题
你想设计一个神经网络。
解决方案
使用 PyTorch 的nn.Module
类定义一个简单的神经网络架构:
# Import libraries
import torch
import torch.nn as nn
# Define a neural network
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.fc1 = nn.Linear(10, 16)
self.fc2 = nn.Linear(16, 16)
self.fc3 = nn.Linear(16, 1)
def forward(self, x):
x = nn.functional.relu(self.fc1(x))
x = nn.functional.relu(self.fc2(x))
x = nn.functional.sigmoid(self.fc3(x))
return x
# Initialize the neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
loss_criterion = nn.BCELoss()
optimizer = torch.optim.RMSprop(network.parameters())
# Show the network
network
SimpleNeuralNet(
(fc1): Linear(in_features=10, out_features=16, bias=True)
(fc2): Linear(in_features=16, out_features=16, bias=True)
(fc3): Linear(in_features=16, out_features=1, bias=True)
)
讨论
神经网络由多层单元组成。然而,关于层类型及其如何组合形成网络架构有很多不同的选择。虽然有一些常用的架构模式(我们将在本章中介绍),但选择正确的架构大多是一门艺术,并且是大量研究的主题。
要在 PyTorch 中构建一个前馈神经网络,我们需要就网络架构和训练过程做出许多选择。请记住,每个隐藏层中的每个单元:
-
接收若干个输入。
-
通过参数值加权每个输入。
-
将所有加权输入与一些偏差(通常为 0)相加。
-
最常见的是应用一些函数(称为激活函数)。
-
将输出发送到下一层的单元。
首先,对于隐藏层和输出层中的每一层,我们必须定义包括在该层中的单元数和激活函数。总体来说,一个层中有更多的单元,我们的网络就能够学习更复杂的模式。然而,更多的单元可能会使我们的网络过度拟合训练数据,从而损害测试数据的性能。
对于隐藏层,一个流行的激活函数是修正线性单元(ReLU):
其中是加权输入和偏差的总和。正如我们所见,如果大于 0,则激活函数返回;否则,函数返回 0。这个简单的激活函数具有许多理想的特性(其讨论超出了本书的范围),这使其成为神经网络中的热门选择。然而,我们应该注意,存在许多十几种激活函数。
第二步,我们需要定义网络中要使用的隐藏层的数量。更多的层允许网络学习更复杂的关系,但需要计算成本。
第三步,我们必须定义输出层激活函数(如果有的话)的结构。输出函数的性质通常由网络的目标确定。以下是一些常见的输出层模式:
二元分类
一个带有 sigmoid 激活函数的单元
多类别分类
k个单元(其中k是目标类别的数量)和 softmax 激活函数
回归
一个没有激活函数的单元
第四步,我们需要定义一个损失函数(衡量预测值与真实值匹配程度的函数);同样,这通常由问题类型决定:
二元分类
二元交叉熵
多类别分类
分类交叉熵
回归
均方误差
第五步,我们需要定义一个优化器,直观上可以将其视为我们在损失函数上“漫步”以找到产生最低误差的参数值的策略。常见的优化器选择包括随机梯度下降、带动量的随机梯度下降、均方根传播以及自适应矩估计(有关这些优化器的更多信息,请参见“参考文献”)。
第六步,我们可以选择一个或多个指标来评估性能,如准确性。
在我们的例子中,我们使用torch.nn.Module
命名空间来组成一个简单的顺序神经网络,可以进行二元分类。在 PyTorch 中,标准的方法是创建一个子类,继承torch.nn.Module
类,在__init__
方法中实例化网络架构,并在类的forward
方法中定义我们希望在每次前向传递中执行的数学操作。在 PyTorch 中定义网络的方法有很多种,虽然在本例中我们使用了函数式方法作为我们的激活函数(如nn.functional.relu
),我们也可以将这些激活函数定义为层。如果我们希望将网络中的所有东西组成一层,我们可以使用Sequential
类:
# Import libraries
import torch
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
# Instantiate and view the network
SimpleNeuralNet()
SimpleNeuralNet(
(sequential): Sequential(
(0): Linear(in_features=10, out_features=16, bias=True)
(1): ReLU()
(2): Linear(in_features=16, out_features=16, bias=True)
(3): ReLU()
(4): Linear(in_features=16, out_features=1, bias=True)
(5): Sigmoid()
)
)
在这两种情况下,网络本身都是一个两层神经网络(当计算层数时,不包括输入层,因为它没有任何要学习的参数),使用 PyTorch 的顺序模型进行定义。每一层都是“密集的”(也称为“全连接的”),意味着前一层中的所有单元都连接到下一层中的所有单元。
在第一个隐藏层中,我们设置 out_features=16
,意味着该层包含 16 个单元。这些单元在我们类的 forward
方法中使用 ReLU 激活函数定义为 x = nn.functional.relu(self.fc1(x))
。我们网络的第一层大小为 (10, 16)
,这告诉第一层期望从输入数据中每个观测值有 10 个特征值。这个网络设计用于二元分类,因此输出层只包含一个单元,使用 sigmoid 激活函数将输出约束在 0 到 1 之间(表示观测为类别 1 的概率)。
另请参阅
21.4 训练二元分类器
问题
您希望训练一个二元分类器神经网络。
解决方案
使用 PyTorch 构建一个前馈神经网络并对其进行训练:
# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 3
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
print("Epoch:", epoch+1, "\tLoss:", loss.item())
# Evaluate neural network
with torch.no_grad():
output = network(x_test)
test_loss = criterion(output, y_test)
test_accuracy = (output.round() == y_test).float().mean()
print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
test_accuracy.item())
Epoch: 1 Loss: 0.19006995856761932
Epoch: 2 Loss: 0.14092367887496948
Epoch: 3 Loss: 0.03935524448752403
Test Loss: 0.06877756118774414 Test Accuracy: 0.9700000286102295
讨论
在 Recipe 21.3 中,我们讨论了如何使用 PyTorch 的顺序模型构建神经网络。在这个配方中,我们使用了来自 scikit-learn 的 make_classification
函数生成的具有 10 个特征和 1,000 个观测值的假分类数据集来训练该神经网络。
我们使用的神经网络与 Recipe 21.3 中的相同(详见该配方进行详细解释)。不同之处在于,我们只是创建了神经网络,而没有对其进行训练。
最后,我们使用 with torch.no_grad()
来评估网络。这表示我们不应计算在代码这一部分中进行的任何张量操作的梯度。由于我们只在模型训练过程中使用梯度,因此我们不希望为在其外部发生的操作(如预测或评估)存储新梯度。
epochs
变量定义了在训练数据时使用的 epochs 数量。batch_size
设置了在更新参数之前要通过网络传播的观测值数量。
然后,我们迭代多个 epochs,通过网络进行前向传递使用 forward
方法,然后反向传递以更新梯度。结果是一个经过训练的模型。
21.5 训练多类分类器
问题
您希望训练一个多类分类器神经网络。
解决方案
使用 PyTorch 构建一个具有 softmax 激活函数输出层的前馈神经网络:
# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
N_CLASSES=3
EPOCHS=3
# Create training and test sets
features, target = make_classification(n_classes=N_CLASSES, n_informative=9,
n_redundant=0, n_features=10, n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.nn.functional.one_hot(torch.from_numpy(target_train).long(),
num_classes=N_CLASSES).float()
x_test = torch.from_numpy(features_test).float()
y_test = torch.nn.functional.one_hot(torch.from_numpy(target_test).long(),
num_classes=N_CLASSES).float()
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16,3),
torch.nn.Softmax()
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.CrossEntropyLoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
for epoch in range(EPOCHS):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
print("Epoch:", epoch+1, "\tLoss:", loss.item())
# Evaluate neural network
with torch.no_grad():
output = network(x_test)
test_loss = criterion(output, y_test)
test_accuracy = (output.round() == y_test).float().mean()
print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
test_accuracy.item())
Epoch: 1 Loss: 0.8022041916847229
Epoch: 2 Loss: 0.775616466999054
Epoch: 3 Loss: 0.7751263380050659
Test Loss: 0.8105319142341614 Test Accuracy: 0.8199999928474426
讨论
在这个解决方案中,我们创建了一个类似于上一个示例中的二元分类器的神经网络,但是有一些显著的改变。在我们生成的分类数据中,我们设置了N_CLASSES=3
。为了处理多类分类问题,我们还使用了nn.CrossEntropyLoss()
,该函数期望目标是独热编码的。为了实现这一点,我们使用了torch.nn.functional.one_hot
函数,最终得到一个独热编码的数组,其中1.
的位置表示给定观察的类别:
# View target matrix
y_train
tensor([[1., 0., 0.],
[0., 1., 0.],
[1., 0., 0.],
...,
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 1.]])
因为这是一个多类分类问题,我们使用了大小为 3 的输出层(每个类别一个)并包含 softmax 激活函数。Softmax 激活函数将返回一个数组,其中的 3 个值相加为 1。这 3 个值表示一个观察结果属于每个类别的概率。
如本文提到的,我们使用了适合多类分类的损失函数,即分类交叉熵损失函数:nn.CrossEntropyLoss()
。
21.6 训练回归器
问题
您希望为回归训练一个神经网络。
解决方案
使用 PyTorch 构建一个只有一个输出单元且没有激活函数的前馈神经网络:
# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
EPOCHS=5
# Create training and test sets
features, target = make_regression(n_features=10, n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1,1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1,1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16,1),
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.MSELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
for epoch in range(EPOCHS):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
print("Epoch:", epoch+1, "\tLoss:", loss.item())
# Evaluate neural network
with torch.no_grad():
output = network(x_test)
test_loss = float(criterion(output, y_test))
print("Test MSE:", test_loss)
Epoch: 1 Loss: 10764.02734375
Epoch: 2 Loss: 1356.510009765625
Epoch: 3 Loss: 504.9664306640625
Epoch: 4 Loss: 199.11314392089844
Epoch: 5 Loss: 191.20834350585938
Test MSE: 162.24497985839844
讨论
完全可以创建一个神经网络来预测连续值,而不是类概率。在我们的二元分类器的情况下(Recipe 21.4),我们使用了一个具有单个单元和 sigmoid 激活函数的输出层,以生成观察是类 1 的概率。重要的是,sigmoid 激活函数将输出值限制在 0 到 1 之间。如果我们去除这种约束,即没有激活函数,我们允许输出为连续值。
此外,因为我们正在训练回归模型,我们应该使用适当的损失函数和评估指标,在我们的情况下是均方误差:
其中是观察数量;是我们试图预测的目标的真实值,对于观察;是模型对的预测值。
最后,由于我们使用了使用 scikit-learn 的make_regression
生成的模拟数据,我们不需要对特征进行标准化。然而,需要注意的是,在几乎所有实际情况下,标准化是必要的。
21.7 进行预测
问题
您希望使用神经网络进行预测。
解决方案
使用 PyTorch 构建一个前馈神经网络,然后使用forward
进行预测:
# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 3
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
print("Epoch:", epoch+1, "\tLoss:", loss.item())
# Evaluate neural network
with torch.no_grad():
predicted_class = network.forward(x_train).round()
predicted_class[0]
Epoch: 1 Loss: 0.19006995856761932
Epoch: 2 Loss: 0.14092367887496948
Epoch: 3 Loss: 0.03935524448752403
tensor([1.])
讨论
在 PyTorch 中进行预测非常容易。一旦我们训练了神经网络,我们可以使用 forward
方法(已作为训练过程的一部分使用),该方法接受一组特征作为输入,并通过网络进行前向传递。在我们的解决方案中,神经网络被设置为二元分类,因此预测的输出是属于类 1 的概率。预测值接近 1 的观察结果高度可能属于类 1,而预测值接近 0 的观察结果高度可能属于类 0。因此,我们使用 round
方法将这些值转换为二元分类器中的 1 和 0。
21.8 可视化训练历史
问题
您希望找到神经网络损失和/或准确率得分的“甜蜜点”。
解决方案
使用 Matplotlib 可视化每个 epoch 中测试集和训练集的损失:
# Load libraries
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
import numpy as np
import matplotlib.pyplot as plt
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 8
train_losses = []
test_losses = []
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
with torch.no_grad():
train_output = network(x_train)
train_loss = criterion(output, target)
train_losses.append(train_loss.item())
test_output = network(x_test)
test_loss = criterion(test_output, y_test)
test_losses.append(test_loss.item())
# Visualize loss history
epochs = range(0, epochs)
plt.plot(epochs, train_losses, "r--")
plt.plot(epochs, test_losses, "b-")
plt.legend(["Training Loss", "Test Loss"])
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.show();
讨论
当我们的神经网络是新的时,它的性能会比较差。随着神经网络在训练数据上的学习,模型在训练集和测试集上的错误通常会减少。然而,在某个点上,神经网络可能会开始“记忆”训练数据并过拟合。当这种情况发生时,训练错误可能会减少,而测试错误则开始增加。因此,在许多情况下,存在一个“甜蜜点”,在这个点上测试错误(我们主要关心的错误)达到最低点。这种效果可以在解决方案中看到,我们可视化了每个 epoch 的训练和测试损失。请注意,测试错误在第 6 个 epoch 左右达到最低点,此后训练损失趋于平稳,而测试损失开始增加。从这一点开始,模型开始过拟合。
21.9 使用权重正则化来减少过拟合
问题
您希望通过正则化网络的权重来减少过拟合。
解决方案
尝试对网络参数进行惩罚,也称为 weight regularization:
# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(network.parameters(), lr=1e-4, weight_decay=1e-5)
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 100
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# Evaluate neural network
with torch.no_grad():
output = network(x_test)
test_loss = criterion(output, y_test)
test_accuracy = (output.round() == y_test).float().mean()
print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
test_accuracy.item())
Test Loss: 0.4030887186527252 Test Accuracy: 0.9599999785423279
讨论
抑制过拟合神经网络的一种策略是通过对神经网络的参数(即权重)施加惩罚,使它们趋向于较小的值,从而创建一个不容易过拟合的简单模型。这种方法称为权重正则化或权重衰减。具体而言,在权重正则化中,将惩罚项添加到损失函数中,如 L2 范数。
在 PyTorch 中,我们可以通过在优化器中包含 weight_decay=1e-5
来添加权重正则化,在这里正则化发生。在这个例子中,1e-5
决定了我们对较高参数值施加的惩罚程度。大于 0 的数值表示在 PyTorch 中使用 L2 正则化。
21.10 使用早停策略来减少过拟合
问题
您希望通过在训练和测试得分发散时停止训练来减少过拟合。
解决方案
使用 PyTorch Lightning 实现一种名为 early stopping 的策略:
# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
import lightning as pl
from lightning.pytorch.callbacks.early_stopping import EarlyStopping
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
class LightningNetwork(pl.LightningModule):
def __init__(self, network):
super().__init__()
self.network = network
self.criterion = nn.BCELoss()
self.metric = nn.functional.binary_cross_entropy
def training_step(self, batch, batch_idx):
# training_step defines the train loop.
data, target = batch
output = self.network(data)
loss = self.criterion(output, target)
self.log("val_loss", loss)
return loss
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=1e-3)
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Initialize neural network
network = LightningNetwork(SimpleNeuralNet())
# Train network
trainer = pl.Trainer(callbacks=[EarlyStopping(monitor="val_loss", mode="min",
patience=3)], max_epochs=1000)
trainer.fit(model=network, train_dataloaders=train_loader)
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
| Name | Type | Params
----------------------------------------------
0 | network | SimpleNeuralNet | 465
1 | criterion | BCELoss | 0
----------------------------------------------
465 Trainable params
0 Non-trainable params
465 Total params
0.002 Total estimated model params size (MB)
/usr/local/lib/python3.10/site-packages/lightning/pytorch/trainer/
connectors/data_connector.py:224: PossibleUserWarning:
The dataloader, train_dataloader, does not have many workers which
may be a bottleneck. Consider increasing the value of the `num_workers`
argument (try 7 which is the number of cpus on this machine)
in the `DataLoader` init to improve performance.
rank_zero_warn(
/usr/local/lib/python3.10/site-packages/lightning/pytorch/trainer/
trainer.py:1609: PossibleUserWarning: The number of training batches (9)
is smaller than the logging interval Trainer(log_every_n_steps=50).
Set a lower value for log_every_n_steps if you want to see logs
for the training epoch.
rank_zero_warn(
Epoch 23: 100%|███████████████| 9/9 [00:00<00:00, 59.29it/s, loss=0.147, v_num=5]
讨论
正如我们在 Recipe 21.8 中讨论的,通常在最初的几个训练 epoch 中,训练和测试错误都会减少,但是在某个时候,网络将开始“记忆”训练数据,导致训练错误继续减少,而测试错误开始增加。因此,对抗过拟合最常见且非常有效的方法之一是监控训练过程,并在测试错误开始增加时停止训练。这种策略称为早期停止。
在 PyTorch 中,我们可以将早期停止作为回调函数来实现。回调函数是在训练过程的特定阶段应用的函数,例如在每个 epoch 结束时。然而,PyTorch 本身并没有为您定义一个早期停止的类,因此在这里我们使用流行的库lightning
(即 PyTorch Lightning)来使用现成的早期停止功能。PyTorch Lightning 是一个为 PyTorch 提供大量有用功能的高级库。在我们的解决方案中,我们包括了 PyTorch Lightning 的EarlyStopping(monitor="val_loss", mode="min", patience=3)
,以定义我们希望在每个 epoch 监控测试(验证)损失,并且如果经过三个 epoch(默认值)后测试损失没有改善,则中断训练。
如果我们没有包含EarlyStopping
回调,模型将在完整的 1,000 个最大 epoch 中训练而不会自行停止:
# Train network
trainer = pl.Trainer(max_epochs=1000)
trainer.fit(model=network, train_dataloaders=train_loader)
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
| Name | Type | Params
----------------------------------------------
0 | network | SimpleNeuralNet | 465
1 | criterion | BCELoss | 0
----------------------------------------------
465 Trainable params
0 Non-trainable params
465 Total params
0.002 Total estimated model params size (MB)
Epoch 999: 100%|████████████| 9/9 [00:01<00:00, 7.95it/s, loss=0.00188, v_num=6]
`Trainer.fit` stopped: `max_epochs=1000` reached.
Epoch 999: 100%|████████████| 9/9 [00:01<00:00, 7.80it/s, loss=0.00188, v_num=6]
21.11 使用 Dropout 减少过拟合
问题
您希望减少过拟合。
解决方案
使用 dropout 在您的网络架构中引入噪声:
# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Dropout(0.1), # Drop 10% of neurons
torch.nn.Sigmoid(),
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 3
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
print("Epoch:", epoch+1, "\tLoss:", loss.item())
# Evaluate neural network
with torch.no_grad():
output = network(x_test)
test_loss = criterion(output, y_test)
test_accuracy = (output.round() == y_test).float().mean()
print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
test_accuracy.item())
Epoch: 1 Loss: 0.18791493773460388
Epoch: 2 Loss: 0.17331615090370178
Epoch: 3 Loss: 0.1384529024362564
Test Loss: 0.12702330946922302 Test Accuracy: 0.9100000262260437
讨论
Dropout 是一种相对常见的正则化较小神经网络的方法。在 dropout 中,每次为训练创建一批观察时,一个或多个层中的单位比例被乘以零(即被删除)。在此设置中,每个批次都在相同的网络上训练(例如相同的参数),但是每个批次都面对稍微不同版本的该网络的架构。
Dropout 被认为是有效的,因为通过在每个批次中不断随机删除单位,它强制单位学习能够在各种网络架构下执行的参数值。也就是说,它们学会了对其他隐藏单元中的干扰(即噪声)具有鲁棒性,从而防止网络简单地记住训练数据。
可以将 dropout 添加到隐藏层和输入层中。当输入层被删除时,其特征值在该批次中不会被引入网络中。
在 PyTorch 中,我们可以通过在网络架构中添加一个nn.Dropout
层来实现 dropout。每个nn.Dropout
层将在每个批次中删除前一层中用户定义的超参数单位。
21.12 保存模型训练进度
问题
鉴于神经网络训练时间较长,您希望在训练过程中保存进度以防中断。
解决方案
使用 torch.save
函数在每个 epoch 后保存模型:
# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Dropout(0.1), # Drop 10% of neurons
torch.nn.Sigmoid(),
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 5
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# Save the model at the end of every epoch
torch.save(
{
'epoch': epoch,
'model_state_dict': network.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss,
},
"model.pt"
)
print("Epoch:", epoch+1, "\tLoss:", loss.item())
Epoch: 1 Loss: 0.18791493773460388
Epoch: 2 Loss: 0.17331615090370178
Epoch: 3 Loss: 0.1384529024362564
Epoch: 4 Loss: 0.1435958743095398
Epoch: 5 Loss: 0.17967987060546875
讨论
在现实世界中,神经网络通常需要训练几个小时甚至几天。在此期间,可能发生很多问题:计算机断电、服务器崩溃,或者不体贴的研究生关掉你的笔记本电脑。
我们可以使用 torch.save
函数来缓解这个问题,通过在每个 epoch 后保存模型。具体来说,在每个 epoch 后,我们将模型保存到位置 model.pt
,这是 torch.save
函数的第二个参数。如果我们只包含一个文件名(例如 model.pt),那么该文件将在每个 epoch 都被最新的模型覆盖。
正如你可以想象的,我们可以引入额外的逻辑,在每几个 epochs 保存模型,仅在损失减少时保存模型等。我们甚至可以将这种方法与 PyTorch Lightning 的提前停止方法结合起来,以确保无论训练在哪个 epoch 结束,我们都能保存模型。
21.13 调整神经网络
问题
您想自动选择神经网络的最佳超参数。
解决方案
使用 PyTorch 的 ray
调优库:
# Load libraries
from functools import partial
import numpy as np
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim import RMSprop
from torch.utils.data import random_split, DataLoader, TensorDataset
from ray import tune
from ray.tune import CLIReporter
from ray.tune.schedulers import ASHAScheduler
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self, layer_size_1=10, layer_size_2=10):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, layer_size_1),
torch.nn.ReLU(),
torch.nn.Linear(layer_size_1, layer_size_2),
torch.nn.ReLU(),
torch.nn.Linear(layer_size_2, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
config = {
"layer_size_1": tune.sample_from(lambda _: 2 ** np.random.randint(2, 9)),
"layer_size_2": tune.sample_from(lambda _: 2 ** np.random.randint(2, 9)),
"lr": tune.loguniform(1e-4, 1e-1),
}
scheduler = ASHAScheduler(
metric="loss",
mode="min",
max_t=1000,
grace_period=1,
reduction_factor=2
)
reporter = CLIReporter(
parameter_columns=["layer_size_1", "layer_size_2", "lr"],
metric_columns=["loss"]
)
# # Train neural network
def train_model(config, epochs=3):
network = SimpleNeuralNet(config["layer_size_1"], config["layer_size_2"])
criterion = nn.BCELoss()
optimizer = optim.SGD(network.parameters(), lr=config["lr"], momentum=0.9)
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
tune.report(loss=(loss.item()))
result = tune.run(
train_model,
resources_per_trial={"cpu": 2},
config=config,
num_samples=1,
scheduler=scheduler,
progress_reporter=reporter
)
best_trial = result.get_best_trial("loss", "min", "last")
print("Best trial config: {}".format(best_trial.config))
print("Best trial final validation loss: {}".format(
best_trial.last_result["loss"]))
best_trained_model = SimpleNeuralNet(best_trial.config["layer_size_1"],
best_trial.config["layer_size_2"])
== Status ==
Current time: 2023-03-05 23:31:33 (running for 00:00:00.07)
Memory usage on this node: 1.7/15.6 GiB
Using AsyncHyperBand: num_stopped=0
Bracket: Iter 512.000: None | Iter 256.000: None | Iter 128.000: None |
Iter 64.000: None | Iter 32.000: None | Iter 16.000: None |
Iter 8.000: None | Iter 4.000: None | Iter 2.000: None |
Iter 1.000: None
Resources requested: 2.0/7 CPUs, 0/0 GPUs, 0.0/8.95 GiB heap,
0.0/4.48 GiB objects
Result logdir: /root/ray_results/train_model_2023-03-05_23-31-33
Number of trials: 1/1 (1 RUNNING)
...
讨论
在第 12.1 和 12.2 节中,我们介绍了使用 scikit-learn 的模型选择技术来识别 scikit-learn 模型的最佳超参数。尽管一般来说,scikit-learn 的方法也可以应用于神经网络,但 ray
调优库提供了一个复杂的 API,允许您在 CPU 和 GPU 上调度实验。
模型的超参数很重要,应该仔细选择。然而,运行实验来选择超参数可能会成本高昂且耗时。因此,神经网络的自动超参数调整并非万能药,但在特定情况下是一个有用的工具。
在我们的解决方案中,我们对不同的参数进行了搜索,包括层大小和优化器的学习率。best_trial.config
显示了在我们的 ray
调优配置中导致最低损失和最佳实验结果的参数。
21.14 可视化神经网络
问题
您想快速可视化神经网络的架构。
解决方案
使用 torch_viz
的 make_dot
函数:
# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from torchviz import make_dot
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using Sequential
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Sigmoid()
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 3
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
make_dot(output.detach(), params=dict(
list(
network.named_parameters()
)
)
).render(
"simple_neural_network",
format="png"
)
'simple_neural_network.png'
如果我们打开保存到我们机器上的图像,我们可以看到以下内容:
讨论
torchviz
库提供了简单的实用函数,可以快速可视化我们的神经网络并将其输出为图像。
第二十二章:神经网络用于非结构化数据
22.0 介绍
在前一章中,我们专注于适用于结构化数据的神经网络配方,即表格数据。实际上,过去几年中最大的进展大部分涉及使用神经网络和深度学习处理非结构化数据,例如文本或图像。与处理结构化数据源不同,处理这些非结构化数据集有所不同。
深度学习在非结构化数据领域尤为强大,而“经典”的机器学习技术(如提升树)通常无法捕捉文本数据、音频、图像、视频等中存在的所有复杂性和细微差别。在本章中,我们将专门探讨将深度学习用于文本和图像数据。
在文本和图像的监督学习空间中,存在许多子任务或“类型”学习。以下是一些示例(尽管这不是一个全面的列表):
-
文本或图像分类(例如:分类一张照片是否是热狗的图像)
-
迁移学习(例如:使用预训练的上下文模型如 BERT,并在一个任务上进行微调以预测电子邮件是否为垃圾邮件)
-
目标检测(例如:识别和分类图像中的特定对象)
-
生成模型(例如:基于给定输入生成文本的模型,如 GPT 模型)
随着深度学习的普及和越来越普遍,处理这些用例的开源和企业解决方案变得更加易于访问。在本章中,我们将利用几个关键库作为我们进入执行这些深度学习任务的起点。特别是,我们将使用 PyTorch、Torchvision 和 Transformers Python 库来完成跨文本和图像 ML 数据的一系列任务。
22.1 训练神经网络进行图像分类
问题
您需要训练一个图像分类神经网络。
解决方案
在 PyTorch 中使用卷积神经网络:
# Import libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
# Define the convolutional neural network architecture
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.dropout1 = nn.Dropout2d(0.25)
self.dropout2 = nn.Dropout2d(0.5)
self.fc1 = nn.Linear(64 * 14 * 14, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = nn.functional.relu(self.conv1(x))
x = nn.functional.relu(self.conv2(x))
x = nn.functional.max_pool2d(self.dropout1(x), 2)
x = torch.flatten(x, 1)
x = nn.functional.relu(self.fc1(self.dropout2(x)))
x = self.fc2(x)
return nn.functional.log_softmax(x, dim=1)
# Set the device to run on
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Define the data preprocessing steps
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
# Load the MNIST dataset
train_dataset = datasets.MNIST('./data', train=True, download=True,
transform=transform)
test_dataset = datasets.MNIST('./data', train=False, transform=transform)
# Create data loaders
batch_size = 64
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size,
shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size,
shuffle=True)
# Initialize the model and optimizer
model = Net().to(device)
optimizer = optim.Adam(model.parameters())
# Compile the model using torch 2.0's optimizer
model = torch.compile(model)
# Define the training loop
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = nn.functional.nll_loss(output, target)
loss.backward()
optimizer.step()
# Define the testing loop
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
# get the index of the max log-probability
test_loss += nn.functional.nll_loss(
output, target, reduction='sum'
).item() # sum up batch loss
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
讨论
卷积神经网络通常用于图像识别和计算机视觉任务。它们通常包括卷积层、池化层和全连接层。
卷积层的目的是学习可以用于当前任务的重要图像特征。卷积层通过对图像的特定区域(卷积的大小)应用滤波器来工作。这一层的权重然后学习识别在分类任务中关键的特定图像特征。例如,如果我们在训练一个识别人手的模型,滤波器可能学会识别手指。
池化层的目的通常是从前一层的输入中减少维度。该层还使用应用于输入部分的滤波器,但没有激活。相反,它通过执行最大池化(选择具有最高值的滤波器中的像素)或平均池化(取输入像素的平均值来代替)来减少输入的维度。
最后,全连接层可以与类似 softmax 的激活函数一起用于创建一个二元分类任务。
参见
22.2 训练用于文本分类的神经网络
问题
您需要训练一个神经网络来对文本数据进行分类。
解决方案
使用一个 PyTorch 神经网络,其第一层是您的词汇表的大小:
# Import libraries
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# Load the 20 newsgroups dataset
cats = ['alt.atheism', 'sci.space']
newsgroups_data = fetch_20newsgroups(subset='all', shuffle=True,
random_state=42, categories=cats)
# Split the dataset into training and test sets
X_train, X_test, y_train, y_test = train_test_split(newsgroups_data.data,
newsgroups_data.target, test_size=0.2, random_state=42)
# Vectorize the text data using a bag-of-words approach
vectorizer = CountVectorizer(stop_words='english')
X_train = vectorizer.fit_transform(X_train).toarray()
X_test = vectorizer.transform(X_test).toarray()
# Convert the data to PyTorch tensors
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.long)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.long)
# Define the model
class TextClassifier(nn.Module):
def __init__(self, num_classes):
super(TextClassifier, self).__init__()
self.fc1 = nn.Linear(X_train.shape[1], 128)
self.fc2 = nn.Linear(128, num_classes)
def forward(self, x):
x = nn.functional.relu(self.fc1(x))
x = self.fc2(x)
return nn.functional.log_softmax(x, dim=1)
# Instantiate the model and define the loss function and optimizer
model = TextClassifier(num_classes=len(cats))
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# Compile the model using torch 2.0's optimizer
model = torch.compile(model)
# Train the model
num_epochs = 1
batch_size = 10
num_batches = len(X_train) // batch_size
for epoch in range(num_epochs):
total_loss = 0.0
for i in range(num_batches):
# Prepare the input and target data for the current batch
start_idx = i * batch_size
end_idx = (i + 1) * batch_size
inputs = X_train[start_idx:end_idx]
targets = y_train[start_idx:end_idx]
# Zero the gradients for the optimizer
optimizer.zero_grad()
# Forward pass through the model and compute the loss
outputs = model(inputs)
loss = loss_function(outputs, targets)
# Backward pass through the model and update the parameters
loss.backward()
optimizer.step()
# Update the total loss for the epoch
total_loss += loss.item()
# Compute the accuracy on the test set for the epoch
test_outputs = model(X_test)
test_predictions = torch.argmax(test_outputs, dim=1)
test_accuracy = accuracy_score(y_test, test_predictions)
# Print the epoch number, average loss, and test accuracy
print(f"Epoch: {epoch+1}, Loss: {total_loss/num_batches}, Test Accuracy:"
"{test_accuracy}")
讨论
不像图像,文本数据本质上是非数值的。在训练模型之前,我们需要将文本转换为模型可以使用的数值表示,以便学习哪些单词和单词组合对于当前分类任务是重要的。在这个例子中,我们使用 scikit-learn 的CountVectorizer
将词汇表编码为一个大小为整个词汇表的向量,其中每个单词被分配到向量中的特定索引,该位置的值是该单词在给定段落中出现的次数。在这种情况下,我们可以通过查看我们的训练集来看到词汇表的大小:
X_train.shape[1]
25150
我们在神经网络的第一层使用相同的值来确定输入层的大小:self.fc1 = nn.Linear(X_train.shape[1], 128)
。这允许我们的网络学习所谓的词嵌入,即从像本配方中的监督学习任务学习到的单词的向量表示。这个任务将允许我们学习大小为 128 的词嵌入,尽管这些嵌入主要对这个特定的任务和词汇表有用。
22.3 对图像分类进行微调预训练模型
问题
您希望使用从预训练模型中学到的知识来训练图像分类模型。
解决方案
使用transformers
库和torchvision
在您的数据上对预训练模型进行微调:
# Import libraries
import torch
from torchvision.transforms import(
RandomResizedCrop, Compose, Normalize, ToTensor
)
from transformers import Trainer, TrainingArguments, DefaultDataCollator
from transformers import ViTFeatureExtractor, ViTForImageClassification
from datasets import load_dataset, load_metric, Image
# Define a helper function to convert the images into RGB
def transforms(examples):
examples["pixel_values"] = [_transforms(img.convert("RGB")) for img in
examples["image"]]
del examples["image"]
return examples
# Define a helper function to compute metrics
def compute_metrics(p):
return metric.compute(predictions=np.argmax(p.predictions, axis=1),
references=p.label_ids)
# Load the fashion mnist dataset
dataset = load_dataset("fashion_mnist")
# Load the processor from the VIT model
image_processor = ViTFeatureExtractor.from_pretrained(
"google/vit-base-patch16-224-in21k"
)
# Set the labels from the dataset
labels = dataset['train'].features['label'].names
# Load the pretrained model
model = ViTForImageClassification.from_pretrained(
"google/vit-base-patch16-224-in21k",
num_labels=len(labels),
id2label={str(i): c for i, c in enumerate(labels)},
label2id={c: str(i) for i, c in enumerate(labels)}
)
# Define the collator, normalizer, and transforms
collate_fn = DefaultDataCollator()
normalize = Normalize(mean=image_processor.image_mean,
std=image_processor.image_std)
size = (
image_processor.size["shortest_edge"]
if "shortest_edge" in image_processor.size
else (image_processor.size["height"], image_processor.size["width"])
)
_transforms = Compose([RandomResizedCrop(size), ToTensor(), normalize])
# Load the dataset we'll use with transformations
dataset = dataset.with_transform(transforms)
# Use accuracy as our metric
metric = load_metric("accuracy")
# Set the training args
training_args = TrainingArguments(
output_dir="fashion_mnist_model",
remove_unused_columns=False,
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=0.01,
per_device_train_batch_size=16,
gradient_accumulation_steps=4,
per_device_eval_batch_size=16,
num_train_epochs=1,
warmup_ratio=0.1,
logging_steps=10,
load_best_model_at_end=True,
metric_for_best_model="accuracy",
push_to_hub=False,
)
# Instantiate a trainer
trainer = Trainer(
model=model,
args=training_args,
data_collator=collate_fn,
compute_metrics=compute_metrics,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
tokenizer=image_processor,
)
# Train the model, log and save metrics
train_results = trainer.train()
trainer.save_model()
trainer.log_metrics("train", train_results.metrics)
trainer.save_metrics("train", train_results.metrics)
trainer.save_state()
讨论
在像文本和图像这样的非结构化数据领域,通常会使用在大型数据集上训练过的预训练模型作为起点,而不是从头开始,尤其是在我们没有太多标记数据的情况下。利用来自更大模型的嵌入和其他信息,我们可以调整我们自己的模型以适应新任务,而不需要大量标记信息。此外,预训练模型可能具有我们训练数据中未完全捕获的信息,从而导致整体性能的提升。这个过程被称为迁移学习。
在这个例子中,我们加载了来自 Google 的 ViT(Vision Transformer)模型的权重。然后,我们使用transformers
库对其进行微调,以在时尚 MNIST 数据集上进行分类任务,这是一个简单的服装项目数据集。这种方法可以应用于增加任何计算机视觉数据集的性能,并且transformers
库提供了一个高级接口,我们可以使用它来微调我们自己的模型,而无需编写大量代码。
参见
22.4 对预训练模型进行文本分类的微调
问题
你想使用预训练模型的学习成果来训练一个文本分类模型。
解决方案
使用transformers
库:
# Import libraries
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
from transformers import (
AutoModelForSequenceClassification, TrainingArguments, Trainer
)
import evaluate
import numpy as np
# Load the imdb dataset
imdb = load_dataset("imdb")
# Create a tokenizer and collator
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
# Tokenize the imdb dataset
tokenized_imdb = imdb.map(
lambda example: tokenizer(
example["text"], padding="max_length", truncation=True
),
batched=True,
)
# User the accuracy metric
accuracy = evaluate.load("accuracy")
# Define a helper function to produce metrics
def compute_metrics(eval_pred):
predictions, labels = eval_pred
predictions = np.argmax(predictions, axis=1)
return accuracy.compute(predictions=predictions, references=labels)
# Create dictionaries to map indices to labels and vice versa
id2label = {0: "NEGATIVE", 1: "POSITIVE"}
label2id = {"NEGATIVE": 0, "POSITIVE": 1}
# Load a pretrained model
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2, id2label=id2label,
label2id=label2id
)
# Specify the training arguments
training_args = TrainingArguments(
output_dir="my_awesome_model",
learning_rate=2e-5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=2,
weight_decay=0.01,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)
# Instantiate a trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_imdb["train"],
eval_dataset=tokenized_imdb["test"],
tokenizer=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics,
)
# Train the model
trainer.train()
讨论
就像使用预训练图像模型一样,预训练语言模型包含了大量关于语言的上下文信息,因为它们通常是在各种开放互联网来源上进行训练的。当我们从一个预训练模型基础开始时,我们通常做的是将现有网络的分类层替换为我们自己的分类层。这使我们能够修改已经学习的网络权重以适应我们的特定任务。
在这个例子中,我们正在对一个 DistilBERT 模型进行微调,以识别 IMDB 电影评论是积极的(1)还是消极的(0)。预训练的 DistilBERT 模型为每个单词提供了大量的语境信息,以及从先前的训练任务中学到的神经网络权重。迁移学习使我们能够利用所有用于训练 DistilBERT 模型的初始工作,并将其重新用于我们的用例,即对电影评论进行分类。
参见
第二十三章:保存、加载和提供训练好的模型
23.0 简介
在过去的 22 章和大约 200 个示例中,我们已经涵盖了如何使用机器学习从原始数据创建性能良好的预测模型。然而,为了使我们所有的工作变得有价值,最终我们需要对我们的模型采取行动,比如将其集成到现有的软件应用程序中。为了实现这个目标,我们需要能够在训练后保存我们的模型,在应用程序需要时加载它们,然后请求该应用程序获取预测结果。
机器学习模型通常部署在简单的 Web 服务器上,旨在接收输入数据并返回预测结果。这使得模型能够在同一网络上的任何客户端中使用,因此其他服务(如 UI、用户等)可以实时使用 ML 模型进行预测。例如,在电子商务网站上使用 ML 进行商品搜索时,将提供一个 ML 模型,该模型接收关于用户和列表的数据,并返回用户购买该列表的可能性。搜索结果需要实时可用,并且可供负责接收用户搜索并协调用户结果的电子商务应用程序使用。
23.1 保存和加载 scikit-learn 模型
问题
您有一个训练好的 scikit-learn 模型,想要在其他地方保存和加载它。
解决方案
将模型保存为 pickle 文件:
# Load libraries
import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn import datasets
# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create decision tree classifer object
classifer = RandomForestClassifier()
# Train model
model = classifer.fit(features, target)
# Save model as pickle file
joblib.dump(model, "model.pkl")
['model.pkl']
一旦模型保存完成,我们可以在目标应用程序(例如 Web 应用程序)中使用 scikit-learn 加载该模型:
# Load model from file
classifer = joblib.load("model.pkl")
并使用它进行预测:
# Create new observation
new_observation = [[ 5.2, 3.2, 1.1, 0.1]]
# Predict observation's class
classifer.predict(new_observation)
array([0])
讨论
将模型用于生产环境的第一步是将该模型保存为可以被另一个应用程序或工作流加载的文件。我们可以通过将模型保存为 pickle 文件来实现这一点,pickle 是一种 Python 特定的数据格式,使我们能够序列化 Python 对象并将其写入文件。具体来说,为了保存模型,我们使用 joblib
,这是一个扩展 pickle 的库,用于处理我们在 scikit-learn 中经常遇到的大型 NumPy 数组。
在保存 scikit-learn 模型时,请注意保存的模型可能在不同版本的 scikit-learn 之间不兼容;因此,在文件名中包含使用的 scikit-learn 版本可能会有所帮助:
# Import library
import sklearn
# Get scikit-learn version
scikit_version = sklearn.__version__
# Save model as pickle file
joblib.dump(model, "model_{version}.pkl".format(version=scikit_version))
['model_1.2.0.pkl']
23.2 保存和加载 TensorFlow 模型
问题
您有一个训练好的 TensorFlow 模型,想要在其他地方保存和加载它。
解决方案
使用 TensorFlow 的 saved_model
格式保存模型:
# Load libraries
import numpy as np
from tensorflow import keras
# Set random seed
np.random.seed(0)
# Create model with one hidden layer
input_layer = keras.Input(shape=(10,))
hidden_layer = keras.layers.Dense(10)(input_layer)
output_layer = keras.layers.Dense(1)(input_layer)
model = keras.Model(input_layer, output_layer)
model.compile(optimizer="adam", loss="mean_squared_error")
# Train the model
x_train = np.random.random((1000, 10))
y_train = np.random.random((1000, 1))
model.fit(x_train, y_train)
# Save the model to a directory called `save_model`
model.save("saved_model")
32/32 [==============================] - 1s 8ms/step - loss: 0.2056
INFO:tensorflow:Assets written to: saved_model/assets
然后我们可以在另一个应用程序中加载该模型,或用于进一步的训练:
# Load neural network
model = keras.models.load_model("saved_model")
讨论
虽然在本书的整个过程中我们并没有大量使用 TensorFlow,但了解如何保存和加载 TensorFlow 模型仍然是有用的。与使用 Python 原生的 pickle
格式不同,TensorFlow 提供了自己的保存和加载模型的方法。saved_model
格式创建一个存储模型和加载所需所有信息的目录,以便以协议缓冲区格式(使用 .pb 文件扩展名)加载模型并进行预测:
ls saved_model
assets fingerprint.pb keras_metadata.pb saved_model.pb variables
虽然我们不会深入探讨这种格式,但这是在 TensorFlow 中保存、加载和提供训练模型的标准方式。
参见
23.3 保存和加载 PyTorch 模型
问题
如果你有一个训练好的 PyTorch 模型,并希望在其他地方保存和加载它。
解决方案
使用 torch.save
和 torch.load
函数:
# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
features, target, test_size=0.1, random_state=1)
# Set random seed
torch.manual_seed(0)
np.random.seed(0)
# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)
# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Dropout(0.1), # Drop 10% of neurons
torch.nn.Sigmoid(),
)
def forward(self, x):
x = self.sequential(x)
return x
# Initialize neural network
network = SimpleNeuralNet()
# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())
# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)
# Compile the model using torch 2.0's optimizer
network = torch.compile(network)
# Train neural network
epochs = 5
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# Save the model after it's been trained
torch.save(
{
'epoch': epoch,
'model_state_dict': network.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss,
},
"model.pt"
)
# Reinitialize neural network
network = SimpleNeuralNet()
state_dict = torch.load(
"model.pt",
map_location=torch.device('cpu')
)["model_state_dict"]
network.load_state_dict(state_dict, strict=False)
network.eval()
SimpleNeuralNet(
(sequential): Sequential(
(0): Linear(in_features=10, out_features=16, bias=True)
(1): ReLU()
(2): Linear(in_features=16, out_features=16, bias=True)
(3): ReLU()
(4): Linear(in_features=16, out_features=1, bias=True)
(5): Dropout(p=0.1, inplace=False)
(6): Sigmoid()
)
)
讨论
尽管我们在 第二十一章 中使用了类似的公式来检查点我们的训练进度,但在这里我们看到相同的方法如何用于将模型加载回内存以进行预测。我们保存模型的 model.pt
实际上只是一个包含模型参数的字典。我们在字典键 model_state_dict
中保存了模型状态;为了将模型加载回来,我们重新初始化我们的网络,并使用 network.load_state_dict
加载模型的状态。
参见
23.4 提供 scikit-learn 模型
问题
你想要使用 Web 服务器提供你训练好的 scikit-learn 模型。
解决方案
构建一个 Python Flask 应用程序,加载本章早期训练的模型:
# Import libraries
import joblib
from flask import Flask, request
# Instantiate a flask app
app = Flask(__name__)
# Load the model from disk
model = joblib.load("model.pkl")
# Create a predict route that takes JSON data, makes predictions, and
# returns them
@app.route("/predict", methods = ["POST"])
def predict():
print(request.json)
inputs = request.json["inputs"]
prediction = model.predict(inputs)
return {
"prediction" : prediction.tolist()
}
# Run the app
if __name__ == "__main__":
app.run()
确保已安装 Flask:
python3 -m pip install flask==2.2.3 joblib==1.2.0 scikit-learn==1.2.0
然后运行应用程序:
python3 app.py
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
现在,我们可以通过向端点提交数据点来对应用程序进行预测,并通过 curl
获取结果:
curl -X POST http://127.0.0.1:5000/predict -H 'Content-Type: application/json'
-d '{"inputs":[[5.1, 3.5, 1.4, 0.2]]}'
{"prediction":[0]}
讨论
在本例中,我们使用了 Flask,这是一个流行的用于构建 Python Web 框架的开源库。我们定义了一个路由 /predict
,该路由接受 POST 请求中的 JSON 数据,并返回包含预测结果的字典。尽管这个服务器并非准备用于生产环境(请参阅 Flask 关于使用开发服务器的警告),我们可以很容易地使用更适合生产环境的 Web 框架扩展和提供此代码以将其移至生产环境。
23.5 提供 TensorFlow 模型
问题
你想要使用 Web 服务器提供你训练好的 TensorFlow 模型。
解决方案
使用开源 TensorFlow Serving 框架和 Docker:
docker run -p 8501:8501 -p 8500:8500 \
--mount type=bind,source=$(pwd)/saved_model,target=/models/saved_model/1 \
-e MODEL_NAME=saved_model -t tensorflow/serving
讨论
TensorFlow Serving 是一个针对 TensorFlow 模型优化的开源服务解决方案。通过简单提供模型路径,我们就能获得一个 HTTP 和 gRPC 服务器,并附带开发者所需的额外有用功能。
docker run
命令使用公共 tensorflow/serving
镜像运行容器,并将我们当前工作目录的 saved_model
路径 ($(pwd)/saved_model
) 挂载到容器内部的 /models/saved_model/1
。这样就会自动将我们之前在本章保存的模型加载到正在运行的 Docker 容器中,我们可以向其发送预测查询。
如果你在网络浏览器中转到 http://localhost:8501/v1/models/saved_model,你应该看到这里显示的 JSON 结果:
{
"model_version_status": [
{
"version": "1",
"state": "AVAILABLE",
"status": {
"error_code": "OK",
"error_message": ""
}
}
]
}
在 http://localhost:8501/v1/models/saved_model/metadata 的 /metadata
路由将返回有关模型的更多信息:
{
"model_spec":{
"name": "saved_model",
"signature_name": "",
"version": "1"
}
,
"metadata": {"signature_def": {
"signature_def": {
"serving_default": {
"inputs": {
"input_8": {
"dtype": "DT_FLOAT",
"tensor_shape": {
"dim": [
{
"size": "-1",
"name": ""
},
{
"size": "10",
"name": ""
}
],
"unknown_rank": false
},
"name": "serving_default_input_8:0"
}
},
"outputs": {
"dense_11": {
"dtype": "DT_FLOAT",
"tensor_shape": {
"dim": [
{
"size": "-1",
"name": ""
},
{
"size": "1",
"name": ""
}
],
"unknown_rank": false
},
"name": "StatefulPartitionedCall:0"
}
},
"method_name": "tensorflow/serving/predict"
},
"__saved_model_init_op": {
"inputs": {},
"outputs": {
"__saved_model_init_op": {
"dtype": "DT_INVALID",
"tensor_shape": {
"dim": [],
"unknown_rank": true
},
"name": "NoOp"
}
},
"method_name": ""
}
}
}
}
}
我们可以使用 curl
向 REST 端点进行预测,并传递变量(此神经网络使用 10 个特征):
curl -X POST http://localhost:8501/v1/models/saved_model:predict
-d '{"inputs":[[1,2,3,4,5,6,7,8,9,10]]}'
{
"outputs": [
[
5.59353495
]
]
}
参见
23.6 在 Seldon 中为 PyTorch 模型提供服务
问题
您希望为实时预测提供经过训练的 PyTorch 模型。
解决方案
使用 Seldon Core Python 包装器提供模型服务:
# Import libraries
import torch
import torch.nn as nn
import logging
# Create a PyTorch model class
class SimpleNeuralNet(nn.Module):
def __init__(self):
super(SimpleNeuralNet, self).__init__()
self.sequential = torch.nn.Sequential(
torch.nn.Linear(10, 16),
torch.nn.ReLU(),
torch.nn.Linear(16,16),
torch.nn.ReLU(),
torch.nn.Linear(16, 1),
torch.nn.Dropout(0.1), # Drop 10% of neurons
torch.nn.Sigmoid(),
)
# Create a Seldon model object with the name `MyModel`
class MyModel(object):
# Loads the model
def __init__(self):
self.network = SimpleNeuralNet()
self.network.load_state_dict(
torch.load("model.pt")["model_state_dict"],
strict=False
)
logging.info(self.network.eval())
# Makes a prediction
def predict(self, X, features_names=None):
return self.network.forward(X)
并使用 Docker 运行它:
docker run -it -v $(pwd):/app -p 9000:9000 kylegallatin/seldon-example
seldon-core-microservice MyModel --service-type MODEL
2023-03-11 14:40:52,277 - seldon_core.microservice:main:578 -
INFO: Starting microservice.py:main
2023-03-11 14:40:52,277 - seldon_core.microservice:main:579 -
INFO: Seldon Core version: 1.15.0
2023-03-11 14:40:52,279 - seldon_core.microservice:main:602 -
INFO: Parse JAEGER_EXTRA_TAGS []
2023-03-11 14:40:52,287 - seldon_core.microservice:main:605 -
INFO: Annotations: {}
2023-03-11 14:40:52,287 - seldon_core.microservice:main:609 -
INFO: Importing MyModel
2023-03-11 14:40:55,901 - root:__init__:25 - INFO: SimpleNeuralNet(
(sequential): Sequential(
(0): Linear(in_features=10, out_features=16, bias=True)
(1): ReLU()
(2): Linear(in_features=16, out_features=16, bias=True)
(3): ReLU()
(4): Linear(in_features=16, out_features=1, bias=True)
(5): Dropout(p=0.1, inplace=False)
(6): Sigmoid()
)
)
2023-03-11 14:40:56,024 - seldon_core.microservice:main:640 -
INFO: REST gunicorn microservice running on port 9000
2023-03-11 14:40:56,028 - seldon_core.microservice:main:655 -
INFO: REST metrics microservice running on port 6000
2023-03-11 14:40:56,029 - seldon_core.microservice:main:665 -
INFO: Starting servers
2023-03-11 14:40:56,029 - seldon_core.microservice:start_servers:80 -
INFO: Using standard multiprocessing library
2023-03-11 14:40:56,049 - seldon_core.microservice:server:432 -
INFO: Gunicorn Config: {'bind': '0.0.0.0:9000', 'accesslog': None,
'loglevel': 'info', 'timeout': 5000, 'threads': 1, 'workers': 1,
'max_requests': 0, 'max_requests_jitter': 0, 'post_worker_init':
<function post_worker_init at 0x7f5aee2c89d0>, 'worker_exit':
functools.partial(<function worker_exit at 0x7f5aee2ca170>,
seldon_metrics=<seldon_core.metrics.SeldonMetrics object at
0x7f5a769f0b20>), 'keepalive': 2}
2023-03-11 14:40:56,055 - seldon_core.microservice:server:504 -
INFO: GRPC Server Binding to 0.0.0.0:5000 with 1 processes.
2023-03-11 14:40:56,090 - seldon_core.wrapper:_set_flask_app_configs:225 -
INFO: App Config: <Config {'ENV': 'production', 'DEBUG': False,
'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None,
'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/',
'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None,
'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None,
'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': None,
'JSON_SORT_KEYS': None, 'JSONIFY_PRETTYPRINT_REGULAR': None,
'JSONIFY_MIMETYPE': None, 'TEMPLATES_AUTO_RELOAD': None,
'MAX_COOKIE_SIZE': 4093}>
2023-03-11 14:40:56,091 - seldon_core.wrapper:_set_flask_app_configs:225 -
INFO: App Config: <Config {'ENV': 'production', 'DEBUG': False,
'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None,
'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/',
'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None,
'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None,
'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': None,
'JSON_SORT_KEYS': None, 'JSONIFY_PRETTYPRINT_REGULAR': None,
'JSONIFY_MIMETYPE': None, 'TEMPLATES_AUTO_RELOAD': None,
'MAX_COOKIE_SIZE': 4093}>
2023-03-11 14:40:56,096 - seldon_core.microservice:_run_grpc_server:466 - INFO:
Starting new GRPC server with 1 threads.
[2023-03-11 14:40:56 +0000] [23] [INFO] Starting gunicorn 20.1.0
[2023-03-11 14:40:56 +0000] [23] [INFO] Listening at: http://0.0.0.0:6000 (23)
[2023-03-11 14:40:56 +0000] [23] [INFO] Using worker: sync
[2023-03-11 14:40:56 +0000] [30] [INFO] Booting worker with pid: 30
[2023-03-11 14:40:56 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2023-03-11 14:40:56 +0000] [1] [INFO] Listening at: http://0.0.0.0:9000 (1)
[2023-03-11 14:40:56 +0000] [1] [INFO] Using worker: sync
[2023-03-11 14:40:56 +0000] [34] [INFO] Booting worker with pid: 34
2023-03-11 14:40:56,217 - seldon_core.gunicorn_utils:load:103 - INFO:
Tracing not active
讨论
虽然我们可以使用多种方式为 PyTorch 模型提供服务,但在这里我们选择了 Seldon Core Python 包装器。Seldon Core 是一个流行的用于在生产环境中为模型提供服务的框架,具有许多有用的功能,使其比 Flask 应用程序更易于使用和更可扩展。它允许我们编写一个简单的类(上面我们使用 MyModel
),而 Python 库则负责所有服务器组件和端点。然后我们可以使用 seldon-core-microservice
命令运行服务,该命令启动一个 REST 服务器、gRPC 服务器,甚至公开一个指标端点。要向服务进行预测,我们可以在端口 9000 上调用以下端点:
curl -X POST http://127.0.0.1:9000/predict -H 'Content-Type: application/json'
-d '{"data": {"ndarray":[[0, 0, 0, 0, 0, 0, 0, 0, 0]]}}'
您应该看到以下输出:
{"data":{"names":["t:0","t:1","t:2","t:3","t:4","t:5","t:6","t:7","t:8"],
"ndarray":[[0,0,0,0,0,0,0,0,0]]},"meta":{}}