Python-数据科学-全-
Python 数据科学(全)
原文:
annas-archive.org/md5/b306e51c73948c57f772d5af5f61eb39
译者:飞龙
第一章:第一章
数据科学与数据预处理简介
学习目标
到本章结束时,您将能够:
-
使用各种 Python 机器学习库
-
处理缺失数据和异常值
-
执行数据集成,将来自不同来源的数据汇集在一起
-
执行数据转换,将数据转换为机器可读的形式
-
对数据进行缩放,以避免不同量级值所带来的问题
-
将数据拆分为训练集和测试集
-
描述不同类型的机器学习
-
描述机器学习模型的不同性能评估指标
本章介绍了数据科学,并涵盖了构建机器学习模型中涉及的各种过程,特别关注预处理部分。
介绍
我们生活在一个充满数据的世界中。因此,能够理解和处理数据是绝对必要的。
数据科学是一个处理数据描述、分析和预测的领域。举个我们日常生活中的例子:每天,我们都会在手机上使用多个社交媒体应用。这些应用收集并处理数据,以便为每个用户创造更个性化的体验——例如,向我们展示可能感兴趣的新闻文章,或根据我们的位置信息调整搜索结果。数据科学的这一分支被称为机器学习。
机器学习是计算机通过程序化学习和统计表示来完成任务的过程,无需人工干预。换句话说,它是通过模式和推断来教计算机自己执行任务,而不是依赖于明确的指令。一些常见的机器学习算法应用包括电子邮件过滤、计算机视觉和计算语言学。
本书将专注于使用 Python 进行机器学习及数据科学的其他方面。Python 是数据科学中一种流行的编程语言,因为它多功能且相对易用。它还拥有多个现成的库,非常适合用于数据处理。
Python 库
在本书中,我们将使用各种 Python 库,包括 pandas、Matplotlib、Seaborn 和 scikit-learn。
pandas
pandas 是一个开源包,具有许多用于加载和处理数据的功能,以便为机器学习任务做好准备。它还具有用于分析和操纵数据的工具。可以使用 pandas 从多种格式读取数据。本书主要使用 CSV 数据。要读取 CSV 数据,您可以使用 read_csv()
函数,并将 filename.csv
作为参数传入。以下是示例代码:
>>> import pandas as pd
>>> pd.read_csv("data.csv")
在前面的代码中,pd
是给 pandas 起的别名。为 pandas 起别名并非强制要求。要可视化一个 pandas DataFrame,您可以使用 head()
函数来列出前五行数据。接下来的练习中会展示这一点。
注意
请访问以下链接了解更多关于 pandas 的信息:pandas.pydata.org/pandas-docs/stable/
。
NumPy
NumPy 是 Python 提供的主要包之一。它主要用于科学计算相关的实践,特别是在进行数学运算时。它包含的工具使我们能够处理数组和数组对象。
Matplotlib
Matplotlib 是一个数据可视化包。它对于在二维空间中绘制数据点非常有用,可以借助 NumPy 实现。
Seaborn
Seaborn 也是一个基于 matplotlib 的数据可视化库。使用 Seaborn 创建的可视化在图形效果上比 matplotlib 更具吸引力。
scikit-learn
scikit-learn 是一个用于机器学习的 Python 包。它的设计方式使其可以与 Python 中的其他数值和科学库互操作,从而实现算法的实现。
这些现成可用的库吸引了开发者的兴趣,特别是在数据科学领域。现在我们已经介绍了 Python 中的各种库,接下来我们将探索构建机器学习模型的路线图。
构建机器学习模型的路线图
构建机器学习模型的路线图非常直接,包含五个主要步骤,下面进行解释:
-
数据预处理
这是构建机器学习模型的第一步。数据预处理是指在将数据输入模型之前对数据进行的转换。它处理的是将无法使用的原始数据转化为干净、可靠数据的技术。
由于数据收集通常不是以受控方式进行的,原始数据通常包含异常值(例如,年龄=120)、无意义的数据组合(例如,模型:自行车,类型:四轮车)、缺失值、尺度问题等。因此,原始数据不能直接输入机器学习模型,因为它可能会影响结果的质量。因此,这是数据科学过程中最重要的一步。
-
模型学习
在对数据进行预处理并将其拆分为训练集/测试集(稍后会详细介绍)之后,我们进入建模阶段。模型只是一些被称为算法的明确定义的方法,这些算法使用预处理过的数据来学习模式,之后可以用来做预测。学习算法有不同的类型,包括监督学习、半监督学习、无监督学习和强化学习。这些将在后面讨论。
-
模型评估
在此阶段,模型通过特定的性能指标进行评估。通过这些指标,我们可以进一步调整模型的超参数以改进模型。这个过程称为超参数优化。我们将重复这一步骤,直到对模型的性能满意为止。
-
预测
一旦我们对评估步骤的结果满意,我们将进入预测阶段。当训练好的模型接触到新的数据集时,它会进行预测。在商业环境中,这些预测可以与决策者共享,以做出有效的商业决策。
-
模型部署
机器学习的整个过程不仅仅止于模型构建和预测。它还涉及利用模型构建带有新数据的应用程序。根据商业需求,部署可能是一个报告,或者是一些需要执行的重复性数据科学步骤。部署后,模型需要定期进行适当的管理和维护,以确保其正常运行。
本章主要关注预处理部分。我们将讨论数据预处理中的不同任务,如数据表示、数据清理等。
数据表示
机器学习的主要目标是构建能够理解数据并发现潜在模式的模型。为了实现这一目标,将数据以计算机能够理解的方式进行输入是非常重要的。为了将数据输入到模型中,它必须以表格或矩阵的形式表示,且具有所需的维度。在预处理能够正确开始之前,将数据转换为正确的表格形式是第一步。
以表格形式表示的数据
数据应该安排在由行和列组成的二维空间中。这种数据结构使得理解数据和发现问题变得容易。以下是一些存储为 CSV(逗号分隔值)文件的原始数据示例:
图 1.1:CSV 格式的原始数据
相同数据在表格中的表示如下:
图 1.2:表格格式的 CSV 数据
如果你对比 CSV 格式和表格格式中的数据,你会发现两者都有缺失值。我们将在本章后续部分讨论如何处理这些缺失值。要加载 CSV 文件并将其作为表格处理,我们使用 pandas 库。这里的数据被加载到称为 DataFrame 的表格中。
注意
要了解更多关于 pandas 的信息,请访问以下链接:pandas.pydata.org/pandas-docs/version/0.15/tutorials.html
。
自变量和目标变量
我们使用的数据框(DataFrame)包含可以分为两类的变量或特征。这些是自变量(也称为预测变量)和因变量(也称为目标变量)。自变量用于预测目标变量。顾名思义,自变量应该彼此独立。如果它们不独立,需要在预处理(清理)阶段加以处理。
自变量
这些是 DataFrame 中所有的特征,除了 目标变量。它们的尺寸是 (m, n),其中 m 是观测值的数量,n 是特征的数量。这些变量必须服从正态分布,并且不应包含以下内容:
-
缺失值或 NULL 值
-
高度分类的数据特征或高基数特征(这些术语将在后面详细讨论)
-
异常值
-
不同尺度的数据
-
人为错误
-
多重共线性(相互关联的自变量)
-
非常大的独立特征集(自变量过多,难以管理)
-
稀疏数据
-
特殊字符
特征矩阵和目标向量
一条数据称为标量。多个标量组成一个向量,多个向量组成一个矩阵。矩阵是通过行和列来表示的。特征矩阵数据由独立的列组成,而目标向量则依赖于特征矩阵的列。为了更好地理解这一点,我们来看一下以下表格:
图 1.3:包含汽车详细信息的表格
正如你在表格中看到的,有多个列:汽车型号、汽车容量、汽车品牌和汽车价格。除了汽车价格之外,所有列都是自变量,代表特征矩阵。汽车价格是因变量,它依赖于其他列(汽车型号、汽车容量和汽车品牌)。它是一个目标向量,因为它依赖于特征矩阵的数据。在下一部分,我们将通过一个基于特征和目标矩阵的练习来全面理解。
注意
所有练习和活动将主要在 Jupyter Notebook 中进行开发。建议为不同的作业保持一个单独的 notebook,除非有特别的要求。另外,为了加载样本数据集,将使用 pandas 库,因为它能以表格形式显示数据。其他加载数据的方法将在后续章节中解释。
练习 1:加载样本数据集并创建特征矩阵和目标矩阵
在本次练习中,我们将把 House_price_prediction
数据集加载到 pandas DataFrame 中,并创建特征矩阵和目标矩阵。House_price_prediction
数据集来自 UCI 机器学习库。该数据收集自美国的多个郊区,包含 5,000 条记录和与房屋相关的 6 个特征。按照以下步骤完成此练习:
注意
House_price_prediction
数据集可以在以下位置找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/USA_Housing.csv。
-
打开一个 Jupyter notebook,并添加以下代码来导入 pandas:
import pandas as pd
-
现在我们需要将数据集加载到 pandas DataFrame 中。由于数据集是一个 CSV 文件,我们将使用
read_csv()
函数来读取数据。添加以下代码来实现:dataset = "https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/USA_Housing.csv" df = pd.read_csv(dataset, header = 0)
正如你在上面的代码中看到的,数据存储在名为
df
的变量中。 -
要打印数据框的所有列名,我们将使用
df.columns
命令。在笔记本中编写以下代码:df.columns
上述代码生成了以下输出:
图 1.4:数据框中存在的列列表
-
数据集包含 n 个数据点。我们可以使用以下命令找到行数的总数:
df.index
上述代码生成了以下输出:
图 1.5:数据框中的总索引
如前图所示,我们的数据集包含 5000 行,索引从 0 到 5000。
注意
你可以在 pandas 中使用
set_index()
函数将某一列转换为数据框中行的索引。这有点像使用该列中的值作为行标签。Dataframe.set_index('column name', inplace = True')'
-
让我们将
Address
列设置为索引,然后将其重置回原始数据框。pandas 库提供了set_index()
方法,将列转换为数据框中的行索引。添加以下代码来实现:df.set_index('Address', inplace=True) df
上述代码生成了以下输出:
图 1.6:带有索引的地址列的数据框
set_index()
函数中的inplace
参数默认为False
。如果将其值更改为True
,那么我们执行的任何操作都会直接修改数据框的内容,而不会创建副本。 -
要重置给定对象的索引,我们使用
reset_index()
函数。编写以下代码来实现:df.reset_index(inplace=True) df
上述代码生成了以下输出:
图 1.7:重置索引的数据框
注意
索引就像是给行和列起的名字。行和列都有索引。你可以通过行/列号或者行/列名来索引。
-
我们可以使用行号和列号来检索前四行和前三列数据。这可以通过 pandas 中的
iloc
索引器来完成,该索引器通过索引位置检索数据。添加以下代码来实现:df.iloc[0:4 , 0:3]
图 1.8:包含四行和三列的数据集
-
要使用标签检索数据,我们使用
loc
索引器。添加以下代码来检索收入和年龄列的前五行:df.loc[0:4 , ["Avg. Area Income", "Avg. Area House Age"]]
图 1.9:包含五行和两列的数据集
-
现在创建一个名为
X
的变量来存储独立特征。在我们的数据集中,我们将把除了价格以外的所有特征视为独立变量,并使用drop()
函数将它们包括进来。完成后,打印出X
变量的前五个实例。可以添加以下代码来实现:X = df.drop('Price', axis=1) X.head()
前面的代码生成了以下输出:
图 1.10:显示特征矩阵前五行的数据集
注意
默认情况下,获取头部的实例数量为五,因此如果你没有指定数量,它将默认输出五个观测值。前面的截图中的 axis 参数表示你是否想从行(axis = 0)或列(axis = 1)中删除标签。
-
使用
X.shape
命令打印你新创建的特征矩阵的形状。添加以下代码来实现这一功能:X.shape
前面的代码生成了以下输出:
图 1.11:特征矩阵的形状
在前面的图中,第一个值表示数据集中观测值的数量(5000),第二个值表示特征的数量(6)。
-
同样,我们将创建一个名为
y
的变量,用来存储目标值。我们将使用索引来获取目标列。索引允许你访问一个更大元素的部分。在这种情况下,我们想从df
数据框中获取名为 Price 的列。然后,我们希望打印出该变量的前 10 个值。添加以下代码来实现这一功能:y = df['Price'] y.head(10)
前面的代码生成了以下输出:
图 1.12:显示目标矩阵前 10 行的数据集
-
使用
y.shape
命令打印你新变量的形状。形状应该是一维的,长度仅等于观测值的数量(5000)。添加以下代码来实现这一功能:y.shape
前面的代码生成了以下输出:
图 1.13:目标矩阵的形状
你已成功创建了数据集的特征矩阵和目标矩阵。你已完成建立预测模型过程中的第一步。这个模型将从特征矩阵(X
中的列)中学习模式,并了解它们如何与目标向量(y
)中的值相匹配。这些模式可以用于基于新房屋的特征,从新数据中预测房价。
在下一部分,我们将探讨更多的数据预处理步骤。
数据清理
数据清理包括填补缺失值和处理不一致的过程。它可以检测到损坏的数据,并进行替换或修改。
缺失值
如果你想掌握成功管理和理解数据的技能,理解缺失值的概念是非常重要的。让我们看看下图:
图 1.14:银行客户信用数据
正如你所看到的,这些数据来自一家银行;每一行代表一个独立的客户,每一列包含他们的详细信息,如年龄和信用额度。有些单元格中要么是NA,要么是空白。这些都是缺失数据。银行需要知道每个客户的每一项信息。如果任何信息缺失,银行就难以预测是否能够向该客户提供贷款。
处理缺失数据
智能地处理缺失数据将帮助构建一个强健的模型,能够应对复杂的任务。处理缺失数据有多种方法。接下来我们将看看其中的一些方法。
删除数据
检查缺失值是数据预处理中的第一步,也是最重要的一步。模型无法接受包含缺失值的数据。这是一个非常简单且常用的方法来处理缺失值:如果缺失值出现在某行中的位置,就删除该行;或者如果某一列的缺失数据超过 70%-75%,就删除该列。同样,阈值不是固定的,取决于你希望修复多少数据。
这种方法的好处在于它快速且简单,并且在很多情况下,缺少数据比错误的数据更好。缺点是,你可能会丢失重要的信息,因为你是基于几个缺失值删除整个特征的。
练习 2:删除缺失数据
在这个练习中,我们将把Banking_Marketing.csv
数据集加载到 pandas DataFrame 中,并处理缺失数据。这个数据集与葡萄牙一家银行的直接营销活动有关。营销活动包括打电话给客户,尝试让他们订阅某一特定产品。数据集包含了每个被联系客户的详细信息,以及他们是否订阅了该产品。请按照以下步骤完成这个练习:
注意
Banking_Marketing.csv
数据集可以在此位置找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Banking_Marketing.csv。
-
打开一个 Jupyter 笔记本。插入一个新单元格,并添加以下代码以导入 pandas 并获取
Banking_Marketing.csv
数据集:import pandas as pd dataset = 'https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Banking_Marketing.csv' #reading the data into the dataframe into the object data df = pd.read_csv(dataset, header=0)
-
一旦获取了数据集,打印出每一列的数据类型。为此,可以使用 pandas DataFrame 的
dtypes
属性:df.dtypes
上述代码会生成以下输出:
图 1.15:每个特征的数据类型
-
现在,我们需要找到每一列的缺失值。为此,我们使用 pandas 提供的
isna()
函数:df.isna().sum()
上述代码会生成以下输出:
图 1.16:数据集中每列的缺失值
在上面的图中,我们可以看到有三列数据缺失,分别是
age
、contact
和duration
。age列有两个缺失值,contact列有六个缺失值,duration列有七个缺失值。 -
一旦你弄清楚所有缺失的细节,我们就从 DataFrame 中删除所有缺失的行。为此,我们使用
dropna()
函数:#removing Null values data = data.dropna()
-
为了检查缺失值是否仍然存在,可以使用
isna()
函数:df.isna().sum()
上面的代码会生成以下输出:
图 1.17:数据集中每列没有缺失值
你已经成功地从 DataFrame 中删除了所有缺失数据。在接下来的章节中,我们将介绍第二种处理缺失数据的方法,即使用填补法。
均值/中位数/众数填补
对于数值数据,我们可以计算其均值或中位数,并用结果替换缺失值。对于类别(非数值)数据,我们可以计算其众数来替代缺失值。这就是所谓的填补法(imputation)。
使用填补法(imputation)而不是直接删除数据的好处在于,它可以防止数据丢失。缺点是你无法知道在特定情况下,使用均值、中位数或众数的准确性如何。
让我们来看一个练习,使用填补法来解决缺失数据问题。
练习 3:填补缺失数据
在这个练习中,我们将把Banking_Marketing.csv
数据集加载到 pandas 的 DataFrame 中,并处理缺失数据。我们将使用填补法。请按照以下步骤完成这个练习:
注意
Banking_Marketing.csv
数据集可以在以下位置找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Banking_Marketing.csv。
-
打开一个 Jupyter 笔记本并添加一个新单元格,将数据集加载到 pandas 的 DataFrame 中。添加以下代码来实现这一点:
import pandas as pd dataset = 'https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Banking_Marketing.csv' df = pd.read_csv(dataset, header=0)
-
使用
age
列的均值填补缺失的数值数据。为此,首先使用 pandas 的mean()
函数找到age
列的均值,然后打印出来:mean_age = df.age.mean() print(mean_age)
上面的代码会生成以下输出:
图 1.18:年龄列的均值
-
一旦完成,使用
fillna()
函数用均值填补缺失数据。可以通过以下代码来完成:df.age.fillna(mean_age, inplace=True)
-
现在我们用持续时间列的中位数填补缺失的数值数据。为此,首先使用 pandas 的
median()
函数找到持续时间列的中位数。添加以下代码来实现这一点:median_duration = df.duration.median() print(median_duration)
图 1.19:持续时间的中位数
-
使用
fillna()
函数,用持续时间的中位数填补缺失数据。df. duration.fillna(median_duration,inplace=True)
-
使用
mode()
函数,将contact
列的类别数据填补为其众数。为此,首先使用 pandas 的mode()
函数找到contact
列的众数。添加以下代码来实现这一点:mode_contact = df.contact.mode()[0] print(mode_contact)
图 1.20:联系方式的众数
-
使用
fillna()
函数用众数填充联系方式列的缺失数据。添加以下代码来实现:df.contact.fillna(mode_contact,inplace=True)
与均值和中位数不同,列中可能有多个众数。因此,我们只取第一个众数,其索引为 0。
你已经成功地以不同的方式填充了缺失数据,并使数据完整且清洁。
数据清理的另一个部分是处理异常值,我们将在下一节讨论。
异常值
异常值是指与其他数据的分布相比,非常大或非常小的值。我们只能在数值数据中找到异常值。箱型图是一种有效的识别数据集异常值的方法,如下图所示:
图 1.21:箱型图中的异常值示例
注意
异常值不一定是坏数据!借助商业理解和客户互动,你可以判断是否要移除或保留异常值。
让我们通过一个简单的例子来学习如何查找异常值。考虑一个地方在不同时间的温度样本数据集:
71, 70, 90, 70, 70, 60, 70, 72, 72, 320, 71, 69
现在我们可以执行以下操作:
-
首先,我们对数据进行排序:
60,69, 70, 70, 70, 70, 71, 71, 72, 72, 90, 320
-
接下来,我们计算中位数(Q2)。中位数是排序后的中间数据。
在这里,排序后中间的项是 70 和 71。
中位数是(70 + 71) / 2 = 70.5
-
然后我们将计算下四分位数(Q1)。Q1 是数据集前半部分的中位数。
数据的前半部分 =
60, 69, 70, 70, 70, 70
下 6 个数据点的第 3 和第 4 个都等于 70。
平均值是(70 + 70) / 2 = 70
Q1 = 70
-
然后计算上四分位数(Q3)。
Q3 是数据集后半部分的中位数。
数据的后半部分 =
71, 71, 72, 72, 90, 320
上 6 个数据点的第 3 和第 4 个是 72 和 72。
平均值是(72 + 72) / 2 = 72
Q3 = 72
-
然后我们找出四分位距(IQR)。
IQR = Q3 – Q1 = 72 – 70
IQR = 2
-
接下来,我们找出上限和下限。
下限 = Q1 – 1.5 (IQR) = 70 – 1.5(2) = 67
上限 = Q3 + 1.5 (IQR) = 71.5 + 1.5(2) = 74.5
我们的边界值 = 67 和 74.5
任何低于下限并大于上限的数据点都是异常值。因此,我们例子中的异常值是 60、90 和 320。
练习 4:查找和移除数据中的异常值
在本练习中,我们将加载german_credit_data.csv
数据集到 pandas DataFrame 中,并去除异常值。该数据集包含 1,000 条记录和 20 个分类/符号属性,由 Hofmann 教授准备。每条记录代表一个从银行借贷的人。根据这些属性,每个人被分类为良好或不良信用风险。按照以下步骤完成本练习:
注意
german_credit_data.csv
数据集的链接可以在这里找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/german_credit_data.csv。
-
打开一个 Jupyter 笔记本并添加一个新单元格。编写以下代码来导入必要的库:pandas、NumPy、matplotlib 和 seaborn。获取数据集并将其加载到 pandas DataFrame 中。添加以下代码来实现:
import pandas as pd import numpy as np %matplotlib inline import seaborn as sbn dataset = 'https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/german_credit_data.csv' #reading the data into the dataframe into the object data df = pd.read_csv(dataset, header=0)
在前面的代码中,
%matplotlib inline
是一个魔法函数,如果我们希望图表在笔记本中可见,它是必不可少的。 -
这个数据集包含一个
Age
列。让我们绘制Age
列的箱型图。为此,使用 seaborn 库中的boxplot()
函数:sbn.boxplot(df['Age'])
前面的代码生成了以下输出:
图 1.22:年龄列的箱型图
我们可以在箱型图中看到一些数据点是异常值。
-
箱型图使用 IQR 方法来显示数据和异常值(数据的形状)。但是,为了打印出异常值,我们使用数学公式来检索它。添加以下代码来使用 IQR 方法查找
Age
列的异常值:Q1 = df["Age"].quantile(0.25) Q3 = df["Age"].quantile(0.75) IQR = Q3 - Q1 print(IQR) >>> 15.0
在前面的代码中,Q1 是第一四分位数,Q3 是第三四分位数。
-
现在通过添加以下代码,我们可以找到上边界和下边界,并打印所有高于上边界和低于下边界的数据。添加以下代码来实现:
Lower_Fence = Q1 - (1.5 * IQR) Upper_Fence = Q3 + (1.5 * IQR) print(Lower_Fence) print(Upper_Fence) >>> 4.5 >>> 64.5
-
为了打印所有高于上边界和低于下边界的数据,添加以下代码:
df[((df["Age"] < Lower_Fence) |(df["Age"] > Upper_Fence))]
前面的代码生成了以下输出:
图 1.23:基于年龄列的异常数据
-
筛选掉异常数据并只打印潜在数据。为此,只需使用
~
运算符否定前面的结果:df = df[~((df ["Age"] < Lower_Fence) |(df["Age"] > Upper_Fence))] df
前面的代码生成了以下输出:
图 1.24:基于年龄列的潜在数据
你已经成功地使用 IQR 找到了异常值。在接下来的部分,我们将探讨另一种预处理方法——数据集成。
数据集成
到目前为止,我们已经确保移除数据中的杂质,使其变得干净。现在,下一步是将来自不同来源的数据合并,以获得一个统一的结构,包含更有意义和有价值的信息。如果数据被拆分到不同的来源,这通常是必需的。为了简单起见,假设我们有一些 CSV 格式的数据,存放在不同的地方,所有这些数据都描述的是相同的场景。比如,我们有一些关于某个员工的数据存储在一个数据库中。我们不能指望所有关于员工的数据都存在同一个表格中。可能员工的个人信息在一个表格中,员工的项目历史在第二个表格中,员工的考勤数据在另一个表格中,等等。因此,如果我们想对员工进行分析,我们需要将所有员工数据整合到一个共同的地方。这一过程被称为数据整合。为了进行数据整合,我们可以使用 merge
函数合并多个 pandas DataFrame。
让我们通过一个数据整合的练习来清晰地理解它。
练习 5:数据整合
在本练习中,我们将合并来自两个数据集(student.csv
和 marks.csv
)的学生信息。student
数据集包含诸如 Age
(年龄)、Gender
(性别)、Grade
(年级)和 Employed
(是否就业)等列。marks.csv
数据集包含 Mark
(分数)和 City
(城市)等列。Student_id
列在两个数据集之间是共同的。按照以下步骤完成此练习:
注意
student.csv
数据集可以在此位置找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/student.csv。
marks.csv
数据集可以在此位置找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/mark.csv。
-
打开一个 Jupyter notebook 并添加一个新单元格。编写以下代码以导入 pandas 并将
student.csv
和marks.csv
数据集加载到df1
和df2
pandas DataFrame 中:import pandas as pd dataset1 = "https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/student.csv" dataset2 = "https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/mark.csv" df1 = pd.read_csv(dataset1, header = 0) df2 = pd.read_csv(dataset2, header = 0)
-
要打印第一个 DataFrame 的前五行,请添加以下代码:
df1.head()
上述代码生成以下输出:
图 1.25:第一个 DataFrame 的前五行
-
要打印第二个 DataFrame 的前五行,请添加以下代码:
df2.head()
上述代码生成以下输出:
图 1.26:第二个 DataFrame 的前五行
-
Student_id
在两个数据集中都是共同的。使用pd.merge()
函数对两个 DataFrame 进行数据整合,基于Student_id
列,然后打印新 DataFrame 的前 10 个值:df = pd.merge(df1, df2, on = 'Student_id') df.head(10)
图 1.27:合并后的 DataFrame 的前 10 行
这里,df1
数据框的数据与 df2
数据框的数据进行了合并。合并后的数据存储在一个名为 df
的新数据框中。
我们现在已经学习了如何进行数据集成。在下一部分中,我们将探索另一个预处理任务——数据转换。
数据转换
之前,我们已经看到如何将来自不同来源的数据合并成一个统一的数据框。现在,我们有很多列,包含不同类型的数据。我们的目标是将数据转换为机器学习可以处理的格式。所有机器学习算法都是基于数学的。因此,我们需要将所有列转换为数值格式。在此之前,让我们看一下我们所拥有的不同类型的数据。
从更广泛的角度来看,数据可以分为数值型数据和分类数据:
-
数值型:顾名思义,这些是可以量化的数字数据。
-
分类数据:数据是字符串或非数值型数据,具有定性特征。
数值型数据进一步分为以下几类:
-
离散型:简单来说,任何可以计数的数值数据称为离散型数据,例如家庭中的人数或班级中的学生人数。离散型数据只能取某些特定值(如 1、2、3、4 等)。
-
连续型:任何可测量的数值数据都称为连续型数据,例如一个人的身高或到达某地所花费的时间。连续型数据可以取几乎任何值(例如,1.25、3.8888 和 77.1276)。
分类数据进一步分为以下几类:
-
有序型:任何有顺序的分类数据称为有序型分类数据,例如电影评分(优秀、好、差、最差)和反馈(满意、不差、差)。你可以将有序数据看作是可以在一个量表上标记的内容。
-
名义型:任何没有顺序的分类数据称为名义型分类数据。例如,性别和国家。
在这些不同类型的数据中,我们将重点讨论分类数据。在下一部分中,我们将讨论如何处理分类数据。
处理分类数据
有些算法可以很好地处理分类数据,例如决策树。但大多数机器学习算法不能直接处理分类数据。这些算法要求输入和输出都必须是数值型的。如果需要预测的输出是分类数据,那么在预测之后,我们将其从数值数据转换回分类数据。让我们讨论一下处理分类数据时面临的一些关键挑战:
-
高基数:基数指的是数据的唯一性。在这种情况下,数据列将包含大量不同的值。一个很好的例子是用户 ID——在一个包含 500 个不同用户的表格中,用户 ID 列将有 500 个独特的值。
-
稀有出现:这些数据列可能包含非常少见的变量,因此它们对模型的影响不足。
-
频繁出现:数据列中可能存在一个类别,它出现的次数很多,但方差很小,这样的类别对模型没有影响。
-
无法适应:这些未处理的类别数据无法适应我们的模型。
编码
为了解决与类别数据相关的问题,我们可以使用编码。这是将类别变量转换为数字形式的过程。在这里,我们将介绍三种简单的编码类别数据的方法。
替换
这是一种将类别数据替换为数字的技术。这是一个简单的替换过程,不涉及太多逻辑处理。让我们通过一个练习更好地理解这一点。
练习 6:将类别数据简单替换为数字
在本练习中,我们将使用前面看到的student
数据集。我们将数据加载到 pandas 数据框中,并将所有类别数据简单地替换为数字。按照以下步骤完成此练习:
注意
student
数据集可以在以下位置找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/student.csv。
-
打开一个 Jupyter notebook 并添加一个新单元格。写入以下代码以导入 pandas,并将数据集加载到 pandas 数据框中:
import pandas as pd import numpy as np dataset = "https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/student.csv" df = pd.read_csv(dataset, header = 0)
-
找到类别列,并将其与其他数据分开。为此,使用 pandas 中的
select_dtypes()
函数:df_categorical = df.select_dtypes(exclude=[np.number]) df_categorical
上述代码生成的输出如下:
图 1.28:数据框的类别列
-
找到
Grade
列中的不同唯一值。为此,使用 pandas 中的unique()
函数,指定列名:df_categorical['Grade'].unique()
上述代码生成的输出如下:
图 1.29:Grade 列中的唯一值
-
找到每个类别列的频率分布。为此,使用
value_counts()
函数对每一列进行处理。此函数返回对象中唯一值的计数:df_categorical.Grade.value_counts()
这一步的输出结果如下:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_01_30.jpg)
图 1.30:Grade 列中每个唯一值的总计数
-
对于
Gender
列,写入以下代码:df_categorical.Gender.value_counts()
这段代码的输出结果如下:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_01_31.jpg)
图 1.31:Gender 列中每个唯一值的总计数
-
类似地,对于
Employed
列,写入以下代码:df_categorical.Employed.value_counts()
这段代码的输出结果如下:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_01_32.jpg)
图 1.32:Employed 列中每个唯一值的总计数
-
替换
Grade
列中的条目。将1st class
替换为1
,2nd class
替换为2
,3rd class
替换为3
。为此,使用replace()
函数:df_categorical.Grade.replace({"1st Class":1, "2nd Class":2, "3rd Class":3}, inplace= True)
-
替换
Gender
列中的条目。将Male
替换为0
,将Female
替换为1
。为此,使用replace()
函数:df_categorical.Gender.replace({"Male":0,"Female":1}, inplace= True)
-
替换
Employed
列中的条目。将no
替换为0
,将yes
替换为1
。为此,使用replace()
函数:df_categorical.Employed.replace({"yes":1,"no":0}, inplace = True)
-
一旦完成三列的所有替换操作,我们需要打印数据框。添加以下代码:
df_categorical.head()
图 1.33:替换后的数值数据
你已经成功地使用简单的手动替换方法将分类数据转换为数值数据。接下来我们将看看另一种编码分类数据的方法。
标签编码
这是一种技术,其中我们将分类列中的每个值替换为从 0 到 N-1 的数字。例如,假设我们在一列中有一份员工名字列表。进行标签编码后,每个员工名字将分配一个数值标签。但这对于所有情况可能不适用,因为模型可能会将数值视为分配给数据的权重。标签编码是处理有序数据的最佳方法。scikit-learn 库提供了LabelEncoder()
,可以帮助进行标签编码。接下来我们将在下一节中查看一个练习。
练习 7:使用标签编码将分类数据转换为数值数据
在本次练习中,我们将把Banking_Marketing.csv
数据集加载到 pandas 数据框中,并使用标签编码将分类数据转换为数值数据。请按照以下步骤完成此练习:
注意
Banking_Marketing.csv
数据集可以在以下位置找到:https://github.com/TrainingByPackt/Master-Data-Science-with-Python/blob/master/Chapter 1/Data/Banking_Marketing.csv。
-
打开一个 Jupyter 笔记本并添加一个新单元格。编写代码导入 pandas 并将数据集加载到 pandas 数据框中:
import pandas as pd import numpy as np dataset = 'https://github.com/TrainingByPackt/Master-Data-Science-with-Python/blob/master/Chapter%201/Data/Banking_Marketing.csv' df = pd.read_csv(dataset, header=0)
-
在进行编码之前,删除所有缺失的数据。为此,使用
dropna()
函数:df = df.dropna()
-
使用以下代码选择所有非数值类型的列:
data_column_category = df.select_dtypes(exclude=[np.number]).columns data_column_category
要了解选择的内容,请参见以下截图:
图 1.34:数据框中的非数值列
-
打印新数据框的前五行。添加以下代码来执行此操作:
df[data_column_category].head()
上述代码生成了以下输出:
图 1.35:列中的非数值类型
-
遍历
category
列,并使用LabelEncoder()
将其转换为数值数据。为此,导入sklearn.preprocessing
包并使用LabelEncoder()
类来转换数据:#import the LabelEncoder class from sklearn.preprocessing import LabelEncoder #Creating the object instance label_encoder = LabelEncoder() for i in data_column_category: df[i] = label_encoder.fit_transform(df[i]) print("Label Encoded Data: ") df.head()
上述代码生成了以下输出:
图 1.36:非数值列转换为数值形式
在上面的截图中,我们可以看到所有的值已经从类别数据转换为数值数据。这里,原始值已被转换并替换为新编码的数据。
你已经成功使用 LabelEncoder
方法将类别数据转换为数值数据。在接下来的章节中,我们将探讨另一种编码方法:独热编码。
独热编码
在标签编码中,类别数据被转换为数值数据,并为每个值分配标签(如 1、2 和 3)。使用这些数值数据进行分析的预测模型有时可能会误认为这些标签具有某种顺序(例如,模型可能认为标签 3 比标签 1 "更好",这是错误的)。为了避免这种混淆,我们可以使用独热编码。在这里,标签编码的数据进一步被分成 n 列,其中 n 表示标签编码时生成的唯一标签的总数。例如,假设通过标签编码生成了三个新标签。那么,在执行独热编码时,列将分为三部分。所以,n 的值为 3。让我们通过一个练习来进一步澄清。
练习 8:使用独热编码将类别数据转换为数值数据
在本练习中,我们将把 Banking_Marketing.csv
数据集加载到 pandas 数据框中,并使用独热编码将类别数据转换为数值数据。按照以下步骤完成此练习:
注意
Banking_Marketing
数据集可以在这里找到:github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Banking_Marketing.csv
。
-
打开 Jupyter 笔记本并添加一个新单元格。编写代码导入 pandas 并将数据集加载到 pandas 数据框中:
import pandas as pd import numpy as np from sklearn.preprocessing import OneHotEncoder dataset = 'https://github.com/TrainingByPackt/Master-Data-Science-with-Python/blob/master/Chapter%201/Data/Banking_Marketing.csv' #reading the data into the dataframe into the object data df = pd.read_csv(dataset, header=0)
-
在进行编码之前,移除所有缺失数据。为此,使用
dropna()
函数:df = df.dropna()
-
使用以下代码选择所有非数值型的列:
data_column_category = df.select_dtypes(exclude=[np.number]).columns data_column_category
上述代码生成以下输出:
图 1.37:数据框的非数值列
-
打印新数据框的前五行。添加以下代码来实现:
df[data_column_category].head()
上述代码生成以下输出:
图 1.38:列的非数值值
-
迭代这些类别列,并使用
OneHotEncoder
将其转换为数值数据。为此,导入sklearn.preprocessing
包,并使用OneHotEncoder()
类进行转换。在执行独热编码之前,我们需要先进行标签编码:#performing label encoding from sklearn.preprocessing import LabelEncoder label_encoder = LabelEncoder() for i in data_column_category: df[i] = label_encoder.fit_transform(df[i]) print("Label Encoded Data: ") df.head()
上述代码生成以下输出:
图 1.39:非数值列转换为数值数据
-
一旦我们完成了标签编码,就可以执行一热编码。添加以下代码来实现这一操作:
#Performing Onehot Encoding onehot_encoder = OneHotEncoder(sparse=False) onehot_encoded = onehot_encoder.fit_transform(df[data_column_category])
-
现在我们创建一个包含编码数据的新数据框,并打印前五行。添加以下代码来实现这一操作:
#Creating a dataframe with encoded data with new column name onehot_encoded_frame = pd.DataFrame(onehot_encoded, columns = onehot_encoder.get_feature_names(data_column_category)) onehot_encoded_frame.head()
上述代码生成了以下输出:
图 1.40:具有一热编码值的列
-
由于一热编码,新的数据框架中的列数增加了。为了查看和打印所有创建的列,请使用
columns
属性:onehot_encoded_frame.columns
上述代码生成了以下输出:
图 1.41:经过一热编码生成的新列列表
-
对于每个级别或类别,会创建一个新的列。为了在列名前加上类别名,你可以使用另一种方法来创建一热编码。为了在列名前加上类别名,可以写下以下代码:
df_onehot_getdummies = pd.get_dummies(df[data_column_category], prefix=data_column_category) data_onehot_encoded_data = pd.concat([df_onehot_getdummies,df[data_column_number]],axis = 1) data_onehot_encoded_data.columns
上述代码生成了以下输出:
图 1.42:包含类别的新列列表
你已经成功地使用OneHotEncoder
方法将分类数据转换为数值数据。
现在我们将进入另一个数据预处理步骤——如何处理数据中的不同量级范围。
不同尺度的数据
在现实生活中,数据集中的值可能具有不同的量级、范围或尺度。使用距离作为参数的算法可能不会以相同的方式加权这些特征。存在多种数据转换技术,用于将数据特征转换到相同的尺度、量级或范围。这确保了每个特征对模型预测的影响是合适的。
我们数据中的某些特征可能具有较大的数值(例如,年薪),而其他特征的数值可能相对较小(例如,在公司工作的年数)。仅仅因为某些数据的值较小并不意味着它不重要。因此,为了确保我们的预测不会因为数据中特征的不同量级而有所不同,我们可以进行特征缩放、标准化或归一化(这三种方式都用于处理数据中的量级问题)。
练习 9:使用标准缩放器方法实现缩放
在本练习中,我们将把Wholesale customer's data.csv
数据集加载到 pandas 数据框中,并使用标准缩放器方法进行缩放。该数据集指的是某批发分销商的客户信息,包含了在不同产品类别上每年花费的货币数额。按照以下步骤完成本练习:
注意
Wholesale customer
数据集可以在此找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Wholesale customers data.csv。
-
打开一个 Jupyter 笔记本并添加一个新单元格。编写代码以导入 pandas 并将数据集加载到 pandas 数据框中:
import pandas as pd dataset = 'https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Wholesale%20customers%20data.csv' df = pd.read_csv(dataset, header=0)
-
检查是否有任何缺失数据。如果有,删除缺失数据:
null_ = df.isna().any() dtypes = df.dtypes info = pd.concat([null_,dtypes],axis = 1,keys = ['Null','type']) print(info)
上述代码生成了以下输出:
图 1.43:数据框的不同列
如我们所见,数据框中有八列,都是
int64
类型。由于空值为False
,这意味着在任何列中都没有空值。因此,无需使用dropna()
函数。 -
现在执行标准缩放,并打印新数据集的前五行。为此,使用
sklearn.preprocessing
中的StandardScaler()
类,并实现fit_transform()
方法:from sklearn import preprocessing std_scale = preprocessing.StandardScaler().fit_transform(df) scaled_frame = pd.DataFrame(std_scale, columns=df.columns) scaled_frame.head()
上述代码生成了以下输出:
图 1.44:特征数据缩放为统一单位
使用StandardScaler
方法,我们已将数据缩放为所有列的统一单位。正如前面的表格所示,所有特征的值已转换为相同范围和尺度的统一值。因此,模型更容易进行预测。
你已经成功使用StandardScaler
方法对数据进行了缩放。在接下来的部分,我们将尝试一个练习,使用MinMax
缩放器方法来实现数据缩放。
练习 10:使用 MinMax 缩放器方法实现缩放
在本练习中,我们将把Wholesale customers data.csv
数据集加载到 pandas 数据框中,并使用MinMax
缩放器方法进行缩放。按照以下步骤完成此练习:
注意
Whole customers data.csv
数据集可以在此找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Wholesale customers data.csv。
-
打开一个 Jupyter 笔记本并添加一个新单元格。编写以下代码以导入 pandas 库并将数据集加载到 pandas 数据框中:
import pandas as pd dataset = 'https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Wholesale%20customers%20data.csv' df = pd.read_csv(dataset, header=0)
-
检查是否有任何缺失数据。如果有,删除缺失数据:
null_ = df.isna().any() dtypes = df.dtypes info = pd.concat([null_,dtypes],axis = 1,keys = ['Null','type']) print(info)
上述代码生成了以下输出:
图 1.45:数据框的不同列
如我们所见,数据框中有八列,都是
int64
类型。由于空值为False
,这意味着在任何列中都没有空值。因此,无需使用dropna()
函数。 -
执行
MinMax
缩放,并打印新数据集的前五个值。为此,使用sklearn.preprocessing
中的MinMaxScaler()
类,并实现fit_transform()
方法。添加以下代码以实现此功能:from sklearn import preprocessing minmax_scale = preprocessing.MinMaxScaler().fit_transform(df) scaled_frame = pd.DataFrame(minmax_scale,columns=df.columns) scaled_frame.head()
上述代码生成了以下输出:
图 1.46:特征数据被缩放到统一的单位
使用 MinMaxScaler
方法,我们再次将所有列的数据缩放到统一的单位。正如在上述表格中所看到的,所有特征的值已经被转换为相同范围的统一尺度。你已经成功地使用 MinMaxScaler
方法缩放了数据。
在下一部分,我们将探讨另一个预处理任务:数据离散化。
数据离散化
到目前为止,我们已经通过编码处理了类别数据,并通过缩放处理了数值数据。
数据离散化是将连续数据通过分组转换为离散桶的过程。离散化还因数据的易维护性而闻名。与使用连续数据训练模型相比,使用离散数据训练模型更加快速和有效。尽管连续值数据包含更多信息,但大量数据会拖慢模型的运行速度。在这里,离散化可以帮助我们在两者之间找到平衡。数据离散化的一些著名方法包括分箱和使用直方图。尽管数据离散化有用,但我们需要有效地选择每个桶的范围,这是一项挑战。
离散化的主要挑战是选择区间或桶的数量,以及如何决定它们的宽度。
这里我们使用一个叫做 pandas.cut()
的函数。这个函数对于实现数据的分桶和排序非常有用。
练习 11:连续数据的离散化
在这个练习中,我们将加载 Student_bucketing.csv
数据集并执行分桶操作。该数据集包含学生的详细信息,如 Student_id
、Age
、Grade
、Employed
和 marks
。按照以下步骤完成此练习:
注意
Student_bucketing.csv
数据集可以在这里找到:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Student_bucketing.csv。
-
打开一个 Jupyter Notebook,添加一个新单元格。编写以下代码以导入所需的库并将数据集加载到 pandas 数据框中:
import pandas as pd dataset = "https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Student_bucketing.csv" df = pd.read_csv(dataset, header = 0)
-
一旦加载数据框,显示数据框的前五行。添加以下代码以完成此操作:
df.head()
上述代码生成以下输出:
图 1.47:数据框的前五行
-
使用
pd.cut()
函数对marks
列进行分桶,并显示前 10 列。cut()
函数接受如x
、bins
和labels
等参数。在这里,我们只使用了三个参数。添加以下代码以实现此功能:df['bucket']=pd.cut(df['marks'],5,labels=['Poor','Below_average','Average','Above_Average','Excellent']) df.head(10)
上述代码生成以下输出:
图 1.48:具有五个离散桶的 marks
列
在上述代码中,第一个参数表示一个数组。在这里,我们已从数据框中选择 marks
列作为数组。5
表示要使用的箱数。由于我们设置了箱数为 5
,因此需要相应地填充标签,使用五个值:Poor
, Below_average
, Average
, Above_average
和 Excellent
。在上述图中,我们可以看到整个连续的 marks 列被放入了五个离散的桶中。我们已经学会了如何进行分桶。
我们现在已经涵盖了所有预处理中涉及的主要任务。在下一节中,我们将详细讨论如何训练和测试您的数据。
训练和测试数据
一旦您将数据预处理为可以被模型使用的格式,您需要将数据分割为训练集和测试集。这是因为您的机器学习算法将使用训练集中的数据来学习所需的内容。然后,它将使用所学内容对测试集中的数据进行预测。您随后可以将此预测与测试集中的实际目标变量进行比较,以查看模型的准确性。下一节的练习将更加详细地解释这一点。
我们将按比例进行训练/测试分割。数据分割的较大部分将是训练集,较小部分将是测试集。这将有助于确保您使用足够的数据来准确训练您的模型。
通常情况下,我们按照 80:20 的比例进行训练-测试分割,遵循帕累托原则。帕累托原则指出:"对于许多事件,大约 80% 的效果来自于 20% 的原因。" 但是,如果您有一个大型数据集,无论是 80:20 还是 90:10 或者 60:40 都无关紧要。(如果我们的过程计算密集型,使用较小的训练集可能会更好,但这可能会导致过拟合问题 – 这将在本书的后续部分进行讨论。)
练习 12: 将数据分割为训练集和测试集
在此练习中,我们将加载 USA_Housing.csv
数据集(您之前看到的)到 pandas 数据框中,并执行训练/测试分割。按照以下步骤完成此练习:
注意
USA_Housing.csv
数据集可在此处获取:https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/USA_Housing.csv.
-
打开一个 Jupyter 笔记本,并添加一个新的单元格来导入 pandas 并将数据集加载到 pandas 中:
import pandas as pd dataset = 'https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/USA_Housing.csv' df = pd.read_csv(dataset, header=0)
-
创建一个名为
X
的变量来存储独立特征。使用drop()
函数来包括所有特征,留出依赖或目标变量,本例中名为Price
。然后,打印出该变量的前五个实例。添加以下代码来完成这一步骤:X = df.drop('Price', axis=1) X.head()
上述代码生成以下输出:
图 1.49: 包含独立变量的数据框
-
使用
X.shape
命令打印出你新创建的特征矩阵的形状:X.shape
上面的代码生成了以下输出:
图 1.50:X 变量的形状
在前面的图中,第一个值表示数据集中的观察数(5000),第二个值表示特征数(6)。
-
同样地,我们将创建一个名为
y
的变量来存储目标值。我们将使用索引来获取目标列。索引允许我们访问更大元素的一部分。在本例中,我们希望从df
数据框中提取名为 Price 的列,并打印出前 10 个值。添加以下代码来实现:y = df['Price'] y.head(10)
上面的代码生成了以下输出:
图 1.51:y 变量的前 10 个值
-
使用
y.shape
命令打印出你新创建的变量的形状:y.shape
上面的代码生成了以下输出:
图 1.52:y 变量的形状
数据集的形状应为一维,长度等于观察数量(5000)。
-
将数据集按 80:20 的比例分成训练集和测试集。为此,请使用来自
sklearn.model_selection
包的train_test_split()
函数。添加以下代码来实现:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
在上面的代码中,
test_size
是一个浮动值,定义了测试数据的大小。如果值为 0.2,则为 80:20 的分割比例。test_train_split
会将数组或矩阵随机分割成训练集和测试集。每次我们运行代码而不指定random_state
时,都会得到不同的结果。 -
打印出
X_train
、X_test
、y_train
和y_test
的形状。添加以下代码来实现:print("X_train : ",X_train.shape) print("X_test : ",X_test.shape) print("y_train : ",y_train.shape) print("y_test : ",y_test.shape)
上面的代码生成了以下输出:
图 1.53:训练集和测试集的数据形状
你已经成功地将数据分割成训练集和测试集。
在接下来的章节中,你将完成一项活动,其中你将对数据集进行预处理。
活动 1:使用银行营销订阅数据集进行预处理
在这个活动中,我们将对 Bank Marketing Subscription
数据集进行各种预处理任务。该数据集与葡萄牙银行机构的直接营销活动相关。通过电话营销新产品,并记录每个客户是否订阅了该产品。
按照以下步骤完成此活动:
注意
Bank Marketing Subscription
数据集可以在此处找到: github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Banking_Marketing.csv
。
-
从给定的链接加载数据集到 pandas 数据框中。
-
通过找到行数和列数、列出所有列、找到所有列的基本统计信息(可以使用
describe().transpose()
函数)和列出列的基本信息(可以使用info()
函数),探索数据的特征。 -
检查是否有任何缺失(或 NULL)值,并找出每列有多少个缺失值。
-
删除任何缺失值。
-
打印
education
列的频率分布。 -
数据集的
education
列有许多类别。减少类别以进行更好的建模。 -
选择并执行适合数据的编码方法。
-
将数据分为训练集和测试集。目标数据在
y
列中,独立数据在其余列中。将数据以 80%的训练集和 20%的测试集分割。注意
此活动的解决方案可以在第 324 页找到。
现在我们已经涵盖了各种数据预处理步骤,让我们更详细地看看数据科学家可用的不同类型的机器学习。
监督学习
监督学习是一种使用标记数据(目标变量已知的数据)进行训练的学习系统。模型学习如何将特征矩阵中的模式映射到目标变量。当训练后的机器用新数据集进行输入时,它可以利用所学内容预测目标变量。这也可以称为预测建模。
监督学习广泛分为两类。这些类别如下:
分类 主要处理分类目标变量。分类算法有助于预测数据点属于哪个组或类。
当预测处于两个类之间时,称为二元分类。例如,预测客户是否会购买产品(在这种情况下,类别为是和否)。
如果预测涉及超过两个目标类,则称为多分类;例如,预测客户将购买的所有物品。
回归 处理数值目标变量。回归算法基于训练数据集预测目标变量的数值。
线性回归 测量一个或多个 预测变量 与一个 结果变量 之间的关联。例如,线性回归可以帮助列举年龄、性别和饮食(预测变量)对身高(结果变量)的相对影响。
时间序列分析,顾名思义,处理的是随时间分布的数据,即按时间顺序排列的数据。股市预测和客户流失预测是时间序列数据的两个例子。根据需求或必要性,时间序列分析可以是回归或分类任务。
无监督学习
与有监督学习不同,无监督学习过程涉及的数据既没有分类也没有标记。算法将对数据进行分析,而不需要指导。机器的任务是根据数据中的相似性将未分组的信息进行分组。其目标是让模型在数据中发现模式,从而为我们提供数据的洞察力并进行预测。
一个例子是,使用一整批未标记的客户数据来寻找模式,将客户分成不同的群体。然后可以针对不同的群体推销不同的产品,以实现最大的利润。
无监督学习大致分为两类:
-
聚类:聚类过程有助于发现数据中的内在模式。
-
关联:关联规则是一种独特的方式,用来发现与大量数据相关的模式,例如当某人购买产品 1 时,他们也倾向于购买产品 2。
强化学习
强化学习是机器学习的一个广泛领域,在这个领域中,机器通过观察已执行操作的结果来学习如何在环境中执行下一步操作。强化学习没有固定的答案,学习代理根据任务的要求决定应该做什么。它从以前的经验中学习。这种学习方式涉及奖励和惩罚。
无论你使用何种机器学习方法,你都希望能够衡量你的模型效果。你可以通过各种性能指标来做到这一点。你将在本书的后续章节中看到这些如何使用,但这里简要概述了其中一些最常见的指标。
性能指标
机器学习中有不同的评估指标,这些指标依赖于数据的类型和需求。以下是一些常见的指标:
-
混淆矩阵
-
精确度
-
召回
-
准确率
-
F1 分数
混淆矩阵
混淆矩阵是一个表格,用来定义分类模型在测试数据上的表现,其中实际值是已知的。为了更好地理解这一点,请看下图,展示了预测值与实际值:
图 1.54:预测值与实际值
让我们详细了解混淆矩阵及其指标,TP、TN、FP 和 FN。假设你正在构建一个预测怀孕的模型:
-
True
。 -
True
,这是不可能发生的。它是一种错误,称为类型 1 错误。 -
False
,这也是一种错误,称为类型 2 错误。 -
False
;即真正负类。
类型 1 错误比类型 2 错误更危险。根据问题的不同,我们必须弄清楚是需要减少类型 1 错误还是类型 2 错误。
精确度
精确度是 TP 结果与模型预测的所有正向结果的比例。精确度衡量我们模型的准确性,如下所示:
图 1.55:精度公式
召回率
召回率计算我们模型预测出的 TP(真正例)结果所占的比例:
图 1.56:召回率公式
准确率
准确率计算模型在所有预测中正确预测正例的比例:
图 1.57:准确率公式
F1 得分
F1 得分是另一种准确度衡量方法,但它允许我们在精度和召回率之间寻求平衡:
图 1.58:F1 得分
在考虑模型表现时,我们必须理解预测误差的另外两个重要概念:偏差和方差。
什么是偏差?
偏差是指预测值与实际值之间的差距。高偏差意味着模型非常简单,无法捕捉到数据的复杂性,导致所谓的欠拟合。
什么是方差?
高方差是指模型在训练数据集上表现得过好。这会导致过拟合,使得模型对训练数据过于特定,也就是说,模型在测试数据上表现不佳。
图 1.59:高方差
假设你正在构建一个线性回归模型,目的是预测一个国家中汽车的市场价格。假设你有关于汽车及其价格的大量数据集,但仍有一些汽车的价格需要预测。
当我们用数据集训练模型时,我们希望模型仅仅在数据集中找到那个模式,仅此而已,因为如果超出这个范围,它就会开始记住训练集中的数据。
我们可以通过调整模型的超参数来改进模型——书中的后续章节会详细介绍这一点。我们通过使用另一个数据集,即验证集,来努力最小化误差并最大化准确率。第一个图表显示模型还没有学到足够的知识来在测试集上进行良好的预测。第三个图表显示模型已经记住了训练数据集,这意味着准确率将是 100,误差为 0。但如果我们在测试数据上进行预测,中间的模型表现会优于第三个。
总结
在本章中,我们介绍了数据科学的基础,并探讨了使用科学方法、过程和算法从数据中提取潜在信息的过程。接着我们进入了数据预处理,包括数据清洗、数据集成、数据转换和数据离散化。
我们看到在使用机器学习算法构建模型时,如何将预处理过的数据分为训练集和测试集。我们还讨论了监督学习、无监督学习和强化学习算法。
最后,我们介绍了不同的度量标准,包括混淆矩阵、精度、召回率和准确率。
在下一章中,我们将介绍数据可视化。
序言
"开发者是当今商业中最重要、最有价值的群体,无论在哪个行业。" | ||
---|---|---|
--斯蒂芬·奥格雷迪,《新王 makers》一书的作者 |
首先,我要感谢并祝贺你,亲爱的读者,做出决定,投资宝贵的时间来阅读这本书。在接下来的章节中,我将带领你从开发者的角度来发现或重新发现数据科学,并阐述本书的主题,即数据科学是一项团队运动,如果它要成功,开发者将在不久的将来扮演更重要的角色,并且与数据科学家更好地合作。然而,为了使数据科学更加包容各行各业的人,我们首先需要通过让数据变得简单和可访问来让它民主化——这实际上就是本书的核心内容。
我为什么要写这本书?
正如我将在第一章中详细解释的那样,编程与数据科学 – 一套新工具,我首先是一名开发者,拥有超过 20 年的多样化软件组件构建经验;包括前端、后端、中间件等等。回顾这段时光,我意识到,每当我思考时,正确的算法总是放在首位;数据总是别人的问题。我很少需要分析数据或从中提取洞察。充其量,我只是设计合适的数据结构,以便以一种更高效、更优雅且可复用的方式加载数据,进而使我的算法运行得更加顺畅。
然而,随着人工智能和数据科学革命的展开,我很明显地意识到像我这样的开发者需要参与其中,因此,7 年前的 2011 年,我抓住机会成为 IBM Watson 核心平台 UI 与工具的首席架构师。当然,我并不自称已成为机器学习或自然语言处理领域的专家,远非如此。通过实践学习并不能替代获得正式的学术背景。
然而,我在本书中想要展示的一个重要观点是,凭借正确的工具和方法,拥有合适数学基础的人(我这里只谈高中的微积分概念)可以迅速成为这一领域的优秀从业者。成功的关键之一是尽可能简化构建数据管道的不同步骤;从获取、加载、清洗数据,到可视化和探索,再到构建和部署机器学习模型。
正是出于让数据更简单、让数据科学家之外的社区也能接触到数据的想法,三年前,我在 IBM Watson 数据平台团队中担任领导角色,致力于扩大开发者社区,专注于教育和为开发者争取权益。在那个时期,作为首席开发者倡导者,我开始公开讨论开发者和数据科学家在解决复杂数据问题时需要更好地协作。
注意
注意:在会议和聚会上讨论时,我有时会与数据科学家产生矛盾,因为他们误解了我的叙述,以为我在说数据科学家不是优秀的软件开发人员。我想澄清一下,包括对你——数据科学家的读者——说,这绝对不是我的意思。
大多数数据科学家都是优秀的软件开发人员,具备全面的计算机科学知识。然而,他们的主要目标是解决复杂的数据问题,这需要快速、反复试验新事物,而不是编写优雅、可重用的组件。
但我不想只是空谈,我还希望有所行动,便启动了 PixieDust 开源项目,作为我为解决这个重要问题所做的微薄贡献。随着 PixieDust 的工作进展顺利,叙述变得更加简洁易懂,开发者和数据科学家们都能为之兴奋。
当我被提供机会写一本关于这个故事的书时,由于两个主要原因,我犹豫了很久才开始这次冒险:
-
我在博客、文章和教程中广泛写过我作为数据科学从业者使用 Jupyter Notebooks 的经验。我还在各种会议中担任过演讲者和工作坊主持人。一个很好的例子是我在 2017 年的 ODSC 伦敦大会上发表的主题演讲,《数据科学的未来:少一些权力的游戏,多一些联盟》(
odsc.com/training/portfolio/future-data-science-less-game-thrones-alliances
)。然而,我从未写过书,完全不知道这会是多么大的承诺,尽管许多曾经写过书的朋友多次提醒过我。 -
我希望这本书能够包容不同的读者,平等地面向开发者、数据科学家以及业务线用户,但我在寻找合适的内容和语气时感到困惑。
最终,决定踏上这段冒险之旅变得相当容易。经过两年的 PixieDust 项目工作,我觉得我们在这个项目中取得了非常好的进展,做出了许多有趣的创新,引起了开源社区的广泛关注,写一本书将会很好地补充我们在帮助开发者参与数据科学方面的倡导工作。
作为旁注,对于那些有写书打算并有类似顾虑的读者,我只能大声地建议:“是的,去做吧。”当然,这是一项需要付出巨大承诺的工作,需要大量的牺牲,但只要你有一个好的故事要讲,且内容扎实,这绝对值得付出努力。
本书适合谁阅读
本书将帮助有志于提升技能的初学数据科学家和开发者,或任何希望成为专业数据科学家的读者。由于书中引入了 PixieDust 的创造者,这本书还将成为已经成就非凡的数据科学家的优秀桌面伴侣。
无论个人的兴趣程度如何,简洁易读的文本和现实生活中的案例都适合那些对该领域有一般兴趣的人,因为他们可以在 Jupyter Notebooks 中运行 Python 代码进行实践。
要制作一个功能完备的 PixieDust 仪表盘,只需要掌握少量的 HTML 和 CSS。数据解读和可视化的流利性也是必不可少的,因为本书面向的数据专业人士包括商业和一般数据分析师。后面的章节也有很多内容值得学习。
本书内容概述
本书分为两个逻辑部分,长度大致相等。在前半部分,我阐述了本书的主题,即需要弥合数据科学与工程之间的鸿沟,详细介绍了我提出的 Jupyter + PixieDust 解决方案。后半部分则专注于将我们在前半部分学到的内容应用于四个行业案例。
第一章, 编程与数据科学——一套新工具,在这一章中,我试图通过自己的经验来定义数据科学,建立一个对 Twitter 帖子进行情感分析的数据管道。我为这个观点辩护——数据科学是一项团队运动,通常,数据科学团队和工程团队之间存在隔阂,这会导致不必要的摩擦、低效,最终无法充分发挥其潜力。我还提出数据科学将会长期存在,并且最终它将成为今天所称计算机科学的一个组成部分(我喜欢认为,总有一天会出现一些新术语,比如计算机数据科学,来更好地体现这一双重性)。
第二章,用 Python 和 Jupyter Notebooks 推动数据分析,我开始深入探讨流行的数据科学工具,如 Python 及其专注于数据科学的开源库生态系统,当然还有 Jupyter Notebooks。我解释了为什么我认为 Jupyter Notebooks 将在未来几年成为大赢家。我还介绍了 PixieDust 开源库的功能,从简单的display()
方法开始,用户可以通过构建引人入胜的图表,在交互式用户界面中直观地探索数据。通过此 API,用户可以选择多个渲染引擎,如 Matplotlib、Bokeh、Seaborn 和 Mapbox。display()
功能是 PixieDust MVP(最小可行产品)中唯一的特性,但随着时间的推移,随着我与许多数据科学从业者的互动,我添加了新功能,迅速形成了 PixieDust 工具箱:
-
sampleData():一个简单的 API,用于轻松地将数据加载到 pandas 和 Apache Spark 数据框中。
-
wrangle_data():一个简单的 API,用于清理和处理数据集。此功能包括使用正则表达式将列拆解为新列,以提取非结构化文本中的内容。
wrangle_data()
API 还可以根据预定义的模式提供建议。 -
包管理器:允许用户在 Python Notebook 中安装第三方 Apache Spark 包。
-
Scala 桥接:使用户能够在 Python Notebook 中运行 Scala 代码。在 Python 端定义的变量可以在 Scala 中访问,反之亦然。
-
Spark 作业进度监控:让你通过实时进度条跟踪 Spark 作业的状态,进度条直接显示在执行代码的输出单元中。
-
PixieApp:提供以 HTML/CSS 为中心的编程模型,允许开发者构建复杂的仪表板,以将 Notebook 中构建的分析操作化。PixieApps 可以直接在 Jupyter Notebook 中运行,也可以使用 PixieGateway 微服务作为分析 Web 应用程序进行部署。PixieGateway 是 PixieDust 的开源配套项目。
以下图表总结了 PixieDust 的发展历程,包括最近添加的 PixieGateway 和 PixieDebugger,后者是 Jupyter Notebooks 中的第一个可视化 Python 调试器:
PixieDust 历程
从本章中要记住的一个关键点是,PixieDust 首先是一个开源项目,它通过开发者社区的贡献而生生不息。与无数开源项目一样,我们可以预期 PixieDust 在未来会加入更多突破性的功能。
第三章,使用 Python 库加速数据分析,我将带领读者深入了解 PixieApp 编程模型,沿途通过一个分析 GitHub 数据的示例应用程序来阐明每个概念。我从 PixieApp 的基本结构入手,描述其生命周期和执行流程,并结合路由概念进行讲解。接着,我介绍开发者如何使用常规的 HTML 和 CSS 代码片段构建仪表盘的 UI,实现与分析结果的无缝交互,并利用 PixieDust 的display()
API 添加复杂的图表。
PixieApp 编程模型是连接数据科学与工程之间的工具策略的基石,因为它简化了将分析成果转化为可操作应用的过程,从而促进了数据科学家与开发者之间的协作,并减少了应用的上市时间。
第四章,将数据分析发布到 Web - PixieApp 工具,我讨论了 PixieGateway 微服务,它使开发者能够将 PixieApps 发布为分析型 Web 应用程序。我首先展示了如何在本地和云端快速部署 PixieGateway 微服务实例,作为 Kubernetes 容器。然后,我介绍了 PixieGateway 管理员控制台的功能,包括各种配置文件以及如何实时监控已部署的 PixieApps 实例和相关的 Python 后端内核。我还介绍了 PixieGateway 的图表共享功能,允许用户将使用 PixieDust 的display()
API 创建的图表转换为网页,供团队中的任何人访问。
PixieGateway 是一项突破性的创新,具有显著加速分析操作化的潜力——这是当今非常需要的——以充分实现数据科学的潜力。它代表了一个开源替代方案,可以与市场上已有的类似产品竞争,如 R-Studio 的 Shiny Server(shiny.rstudio.com/deploy
)和 Plotly 的 Dash(dash.plot.ly
)。
第五章,Python 和 PixieDust 最佳实践与高级概念,我通过深入探讨 PixieApp 编程模型的高级概念,完成了对 PixieDust 工具箱的深度剖析:
-
@captureOutput 装饰器:默认情况下,PixieApp 路由要求开发者提供一个将被注入到应用 UI 中的 HTML 片段。当我们想调用一个第三方 Python 库,而该库不了解 PixieApp 架构并直接生成输出到 Notebook 时,就会出现问题。
@captureOutput
通过自动重定向第三方 Python 库生成的内容并将其封装成一个合适的 HTML 片段来解决这个问题。 -
利用 Python 类继承提高模块化和代码重用性:将 PixieApp 代码拆解成逻辑类,并利用 Python 类继承功能将其组合在一起。我还展示了如何通过
pd_app
自定义属性调用外部 PixieApp。 -
PixieDust 对流式数据的支持:展示了 PixieDust 的
display()
和 PixieApp 如何处理流式数据。 -
通过 PixieApp 事件实现仪表盘深入分析:提供了一种机制,让 PixieApp 组件能够发布和订阅当用户与 UI 交互时生成的事件(例如,图表和按钮)。
-
为 PixieDust display() API 构建自定义显示渲染器:演示了一个简单渲染器的代码,扩展了 PixieDust 菜单。该渲染器展示了一个自定义的 HTML 表格,显示所选数据。
-
调试技巧:介绍 PixieDust 提供的各种调试技巧,包括名为 PixieDebugger 的可视化 Python 调试器,以及用于显示 Python 日志消息的
%%PixiedustLog
魔法命令。 -
运行 Node.js 代码的能力:我们讨论了
pixiedust_node
扩展,它管理一个 Node.js 进程的生命周期,该进程负责直接从 Python Notebook 中执行任意的 Node.js 脚本。
多亏了开放源代码模型,凭借其透明的开发过程和日益增长的用户社区提供的一些宝贵反馈,我们能够随着时间的推移优先实施许多先进的功能。我想表达的关键点是,遵循一个合适的开源模型并采用适当的许可证(PixieDust 使用的是 Apache 2.0 许可证,详情请见 www.apache.org/licenses/LICENSE-2.0
)是非常有效的。它帮助我们壮大了用户社区,而社区又为我们提供了必要的反馈,帮助我们优先开发那些我们知道具有高价值的新特性,在某些情况下,甚至贡献了代码,形式为 GitHub 拉取请求。
第六章,分析研究:AI 与图像识别与 TensorFlow,我深入讲解了四个行业案例中的第一个。我从机器学习的高级介绍开始,接着介绍了深度学习——机器学习的一个子领域——以及使得构建神经网络模型更简单的 TensorFlow 框架。然后,我继续构建一个图像识别示例应用程序,并在四个部分中包括相关的 PixieApp:
-
第一部分:使用预训练的 ImageNet 模型构建一个图像识别的 TensorFlow 模型。通过《TensorFlow for poets》教程,我展示了如何构建分析功能来加载和评分神经网络模型。
-
第二部分:创建一个 PixieApp,将第一部分中创建的分析功能投入实际应用。这个 PixieApp 从用户提供的网页 URL 中抓取图像,将它们与 TensorFlow 模型进行评分,并以图形方式展示结果。
-
第三部分:我展示了如何将 TensorBoard 图形可视化组件直接集成到 Notebook 中,以提供调试神经网络模型的能力。
-
第四部分:我展示了如何使用自定义训练数据重新训练模型,并更新 PixieApp 以展示来自两个模型的结果。
我决定以深度学习图像识别为主题开始这一系列示例应用程序,因为这是一个日益流行的重要用例,展示了我们如何构建模型并将其部署到同一个 Notebook 中的应用程序,这一做法为弥合数据科学与工程之间的鸿沟提供了有力的支持。
第七章,分析研究:NLP 与大数据——Twitter 情感分析,我谈到了在 Twitter 规模上进行自然语言处理。在这一章中,我展示了如何使用 IBM Watson 自然语言理解的云服务对推文进行情感分析。这一点非常重要,因为它提醒读者,有时候复用托管服务而不是自己构建能力,可能是一个更具吸引力的选择。
我首先介绍了 Apache Spark 并行计算框架,然后分四部分开始构建应用程序:
-
第一部分:使用 Spark 结构化流处理获取 Twitter 数据。
-
第二部分:通过提取文本中的情感和最相关的实体来丰富数据。
-
第三部分:通过创建一个实时仪表盘 PixieApp 来实现分析功能。
-
第四部分:这是一个可选部分,重新实现该应用程序,使用 Apache Kafka 和 IBM Streaming Designer 托管服务,展示如何提高可扩展性。
我认为读者,尤其是那些不熟悉 Apache Spark 的读者,会喜欢这一章,因为它比前一章稍微容易跟随。关键的收获是如何构建可扩展的分析,利用连接到 Spark 集群的 Jupyter Notebooks。
第八章,分析研究:预测 - 财务时间序列分析与预测,我谈到了时间序列分析,这是数据科学中一个非常重要的领域,在工业界有许多实际应用。我从深入探讨 NumPy 库开始,NumPy 是许多其他库(如 pandas 和 SciPy)的基础。接着,我开始构建示例应用程序,分析一个由历史股票数据组成的时间序列,分为两个部分:
-
第一部分:提供了时间序列的统计探索,包括各种图表,如自相关函数(ACF)和偏自相关函数(PACF)。
-
第二部分:基于 ARIMA 算法使用
statsmodels
Python 库构建预测模型。
时间序列分析是数据科学中一个非常重要的领域,我认为它被低估了。在写这章时,我个人学到了很多东西。我真心希望读者也能喜欢它,并且阅读它能够激发对这个伟大主题的兴趣。如果真是这样,我也希望你会被说服,尝试在下次学习时间序列分析时使用 Jupyter 和 PixieDust。
第九章,分析研究:图算法 - 美国国内航班数据分析,我通过研究图形算法来完成这一系列的行业应用案例。我选择了一个分析航班延误的示例应用程序,因为数据是现成的,而且非常适合使用图算法(老实说,我也可能是因为我已经写过一个类似的应用程序,基于天气数据预测航班延误,那个应用程序使用了 Apache Spark MLlib:developer.ibm.com/clouddataservices/2016/08/04/predict-flight-delays-with-apache-spark-mllib-flightstats-and-weather-data
)。
我首先介绍了图形及其相关的图算法,包括一些最流行的图算法,如广度优先搜索(Breadth First Search)和深度优先搜索(Depth First Search)。接着,我介绍了用于构建示例应用程序的 networkx
Python 库。
该应用程序由四个部分组成:
-
第一部分:展示了如何将美国国内航班数据加载到图中。
-
第二部分:创建
USFlightsAnalysis
PixieApp,允许用户选择起点和目的地机场,然后根据选定的中心性展示两机场之间的最短路径的 Mapbox 地图。 -
第三部分:为 PixieApp 添加数据探索功能,包括针对每个航司的各种统计数据,统计的是从所选起点机场起飞的航班。
-
第四部分:使用在第八章,分析学习:预测 – 财务时间序列分析与预测 中学到的技术,构建一个 ARIMA 模型来预测航班延误。
图论也是数据科学中一个重要且不断发展的领域,本章很好地总结了这一系列内容,我希望它提供了一个多样且具有代表性的行业案例集。对于那些特别感兴趣于使用图算法处理大数据的读者,我建议查看 Apache Spark GraphX (spark.apache.org/graphx
),它通过一个非常灵活的 API 实现了许多图算法。
第十章,数据分析的未来及如何发展你的技能,我通过简要总结并解释 Drew Conway 的 Venn 图来结束本书。接着我会谈到人工智能和数据科学的未来,以及公司如何为人工智能和数据科学的革命做好准备。此外,我还列出了一些有助于进一步学习的优秀参考资料。
附录,PixieApp 快速参考,是为开发者提供的快速参考指南,概述了所有 PixieApp 属性。这本指南通过适当的示例解释了各种注释、自定义 HTML 属性和方法。
但介绍部分说得够多了:让我们从第一章开始,标题为 编程与数据科学 – 新工具集。
为了最大程度地从本书中受益
-
大部分跟随本书示例所需的软件是开源的,因此可以免费下载安装。书中提供了详细的安装说明,首先从安装 Anaconda 开始,Anaconda 包含了 Jupyter Notebook 服务器。
-
在第七章,分析学习:NLP 和大数据与 Twitter 情感分析,示例应用程序需要使用 IBM Watson 云服务,包括 NLU 和 Streams Designer。这些服务提供免费的计划层,足以让你跟随本书的示例。
下载示例代码文件
你可以从 www.packtpub.com
账户下载本书的示例代码文件。如果你是从其他地方购买的这本书,可以访问 www.packtpub.com/support
,注册后文件会直接通过电子邮件发送给你。
你可以按照以下步骤下载代码文件:
-
登录或注册
www.packtpub.com
-
选择支持标签。
-
点击代码下载与勘误。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用以下最新版本解压或提取文件夹:
-
Windows 版 WinRAR / 7-Zip
-
Mac 版 Zipeg / iZip / UnRarX
-
Linux 版 7-Zip / PeaZip
本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Thoughtful-Data-Science
。我们还有其他来自丰富书籍和视频目录的代码包,地址是github.com/PacktPublishing/
。快来看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,里面有本书中使用的截图/图表的彩色图片。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/ThoughtfulDataScience_ColorImages.pdf
。
使用的约定
本书中使用了多种文本约定。
CodeInText
:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“你可以使用{%if ...%}...{%elif ...%}...{%else%}…{%endif%}
符号来有条件地输出文本。”
代码块的格式如下:
import pandas
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_df = pandas.read_csv(data_url)
building_df
当我们希望特别提醒你注意代码块中的某一部分时,相关行或项会以粗体显示:
import pandas
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_df = pandas.read_csv(data_url)
building_df
任何命令行输入或输出都如下所示:
jupyter notebook --generate-config
粗体:表示一个新术语、重要词汇或你在屏幕上看到的词汇,例如在菜单或对话框中,也会在文本中像这样出现。例如:“下一步是创建一个新的路由,该路由接受用户值并返回结果。这个路由将通过提交查询按钮触发。”
注意
警告或重要提示如下所示。
提示
提示和技巧如下所示。
联系我们
我们非常欢迎读者的反馈。
一般反馈:电子邮件<feedback@packtpub.com>
,并在邮件主题中提到书籍的标题。如果你有关于本书的任何问题,请通过电子邮件联系我们<questions@packtpub.com>
。
勘误:尽管我们已尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在本书中发现错误,我们将非常感激你能报告给我们。请访问www.packtpub.com/submit-errata
,选择你的书籍,点击勘误提交表单链接,输入相关细节。
盗版:如果您在互联网上发现任何形式的非法复制我们作品的情况,我们将非常感激您能提供该材料的地址或网站名称。请通过<copyright@packtpub.com>
与我们联系,并附上相关链接。
如果您有兴趣成为作者:如果您在某个领域有专长,并且有兴趣写书或为书籍贡献内容,请访问 authors.packtpub.com
。
评论
请留下评论。在阅读和使用本书后,为什么不在您购买它的网站上留下评论呢?潜在的读者可以看到并参考您的客观意见来做出购买决策,我们在 Packt 也能了解您对我们产品的看法,作者们也能看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章 编程与数据科学——一种新工具集
“数据是宝贵的,它将比系统本身存在得更久。”
– 蒂姆·伯纳斯-李,万维网的发明人
(en.wikipedia.org/wiki/Tim_Berners-Lee
)
在这一章节的开始,我将尝试回答一些基础问题,希望这些问题能为本书的后续内容提供背景和清晰度:
-
什么是数据科学以及为什么它正在兴起
-
为什么数据科学将长久存在
-
为什么开发人员需要参与数据科学
结合我作为开发人员和最近的数据科学实践者的经验,我将讨论一个具体的数据管道项目,以及从这个项目中衍生出的一种数据科学策略,这个策略由三大支柱组成:数据、服务和工具。最后,我将介绍 Jupyter Notebooks,它是我在本书中提出的解决方案的核心。
什么是数据科学
如果你在网上搜索“数据科学”的定义,你一定会找到很多不同的解释。这反映了数据科学对不同的人意味着不同的东西。对于数据科学家究竟做什么以及他们必须接受什么样的训练,实际上并没有达成一致意见;这完全取决于他们要完成的任务,例如数据收集与清洗、数据可视化等。
现在,我会尝试使用一个普遍且希望得到共识的定义:数据科学指的是分析大量数据,以提取知识和见解,从而做出可操作的决策。不过这仍然有些模糊;人们可以问,我们到底在谈论什么样的知识、见解和可操作的决策?
为了引导话题,让我们将数据科学的领域范围缩小到三个领域:
-
描述性分析:数据科学与信息检索和数据收集技术相关,目的是重建过去的事件,以识别模式并发现有助于理解发生了什么以及是什么导致其发生的见解。一个例子是查看销售数据和按地区划分的人口统计信息,以便对客户偏好进行分类。这部分需要熟悉统计学和数据可视化技术。
-
预测性分析:数据科学是一种预测当前正在发生或将来会发生某些事件的可能性的方法。在这种情况下,数据科学家会查看过去的数据,找出解释变量,并构建可以应用于其他数据点的统计模型,以预测其结果。例如,实时预测信用卡交易是否存在欺诈行为。这部分通常与机器学习领域相关。
-
规范性分析:在这种情况下,数据科学被看作是一种做出更好决策的方法,或者我应该说是基于数据的决策。这个想法是,查看多个选项,并使用模拟技术量化并最大化结果,例如,通过优化供应链来最小化运营成本。
从本质上讲,描述性数据科学回答了什么(数据告诉我什么),预测性数据科学回答了为什么(数据为什么以某种方式表现),而规范性数据科学回答了如何(我们如何将数据优化以实现特定目标)。
数据科学会一直存在吗?
让我们从一开始就直截了当地说:我坚信答案是肯定的。
然而,情况并非总是如此。几年前,当我第一次听说数据科学作为一个概念时,我最初以为这只是另一个营销术语,用来描述行业中已经存在的活动:商业智能(BI)。作为一名开发者和架构师,主要解决复杂的系统集成问题,我很容易说服自己不需要直接参与数据科学项目,尽管显然它们的数量在不断增加,原因是开发者传统上将数据管道视为可以通过明确定义的 API 访问的黑箱。然而,在过去的十年里,我们看到数据科学在学术界和工业界的兴趣呈指数级增长,直到它变得非常清楚,这种模型将无法持续下去。
随着数据分析在公司运营过程中扮演着越来越重要的角色,开发者的角色也扩展到了更接近算法的部分,并构建能够在生产环境中运行它们的基础设施。数据科学已成为新的淘金热的另一个证据就是数据科学家职位的非凡增长,这些职位连续两年在 Glassdoor 上排名第一(www.prnewswire.com/news-releases/glassdoor-reveals-the-50-best-jobs-in-america-for-2017-300395188.html
),并且在 Indeed 上经常是雇主发布最多的职位。猎头也在 LinkedIn 和其他社交媒体平台上活跃,向任何拥有数据科学技能资料的人发送大量招聘信息。
所有这些新技术背后投资的主要原因之一,是希望它们能为企业带来显著的改进和更高的效率。然而,尽管这是一个正在发展的领域,今天企业中的数据科学仍然局限于实验,而没有像大家预期的那样成为核心活动。这使得许多人开始怀疑,数据科学是否只是一个短暂的潮流,最终会消退,成为又一个技术泡沫,最终破灭,留下许多人被抛在后头。
这些都是很好的观点,但我很快意识到这不仅仅是一个昙花一现的潮流;我所领导的越来越多的项目开始将数据分析融入到核心产品功能中。最终,当 IBM Watson 问答系统在危险边缘游戏中战胜了两位经验丰富的冠军时,我深信数据科学、云计算、大数据和人工智能(AI)将会长久存在,并最终改变我们对计算机科学的理解。
为什么数据科学在崛起?
数据科学的迅速崛起涉及多个因素。
首先,收集的数据量正在以指数级的速度增长。根据 IBM 营销云的最新市场研究(www-01.ibm.com/common/ssi/cgi-bin/ssialias?htmlfid=WRL12345GBEN
),每天大约会产生 2.5 万亿字节的数据(为了让你更清楚它有多大,这相当于 25 亿亿字节),但这些数据中只有极小的一部分会被分析,导致错失了大量的机会。
其次,我们正处于一场几年前开始的认知革命中;几乎所有行业都在纷纷加入 AI 的浪潮,这其中包括自然语言处理(NLP)和机器学习。尽管这些领域已经存在了很长时间,但最近它们获得了重新的关注,以至于现在它们已经成为大学中最受欢迎的课程之一,并且占据了开源活动的大部分份额。显然,如果企业想要生存下去,它们需要变得更加敏捷,更快地行动,并转型为数字化企业。而随着决策时间的缩短,几乎接近实时,它们必须完全依赖数据。再加上 AI 算法需要高质量(并且大量的)数据才能正常工作,我们可以开始理解数据科学家所扮演的关键角色。
第三,随着云技术的进步和平台即服务(PaaS)的发展,访问海量计算引擎和存储变得前所未有的简单且廉价。曾经只有大型企业才能承担的大数据工作负载,现在对小型组织或任何拥有信用卡的个人都可用;这反过来又推动了各领域创新的增长。
正因如此,我毫不怀疑,类似于人工智能革命,数据科学将长期存在,而且它的增长将持续很长时间。但我们也不能忽视数据科学尚未充分发挥其潜力,并且未能产生预期的结果,特别是在帮助公司转型为数据驱动型组织方面。最常见的挑战是实现下一步,即将数据科学和分析转化为核心业务活动,最终推动清晰、智能的“赌命”决策。
这与开发者有什么关系?
这是一个非常重要的问题,我们将在接下来的章节中花费大量时间探讨。让我先回顾一下我的职业生涯;我大部分时间作为开发者,回溯到 20 多年前,参与了计算机科学的多个方面。
我开始通过构建各种工具来帮助软件国际化,自动化将用户界面翻译成多种语言的过程。随后,我在 Eclipse 中开发了一个 LotusScript(Lotus Notes 的脚本语言)编辑器,该编辑器可以直接与底层编译器进行交互。这个编辑器提供了第一流的开发功能,例如内容提示(提供建议)、实时语法错误报告等等。接着,我花了几年时间为 Lotus Domino 服务器构建基于 Java EE 和 OSGI 的中间件组件(www.osgi.org
)。在此期间,我带领团队通过将其引入当时可用的最新技术来现代化 Lotus Domino 编程模型。我对软件开发的各个方面都很熟悉,包括前端、中间件、后端数据层、工具等;我可以说是一个全栈开发者。
直到我看到 IBM Watson 问答系统的演示,2011 年它在《危险边缘!》节目中击败了长期冠军 Brad Rutter 和 Ken Jennings。哇!这真是一个突破性进展,一个能够回答自然语言问题的计算机程序。我非常感兴趣,在做了一些研究,拜访了几个参与该项目的研究人员,并了解了构建此系统所用的技术,如 NLP、机器学习和通用数据科学后,我意识到,如果将这项技术应用到商业的其他领域,它将有巨大的潜力。
几个月后,我有机会加入 IBM 新成立的 Watson 部门,领导一个工具团队,任务是为 Watson 系统构建数据摄取和准确性分析功能。我们的一个重要需求是确保工具对客户易于使用,这也是为什么回头看,将这个责任交给一个开发团队是正确的决定。从我的角度来看,接手这个职位既具挑战性又充满收获。我离开了一个熟悉的世界,在那里我擅长设计基于常见模式的架构,实施前端、中间件或后端软件组件,进入了一个主要聚焦于大数据工作的世界:获取数据、清洗数据、分析数据、可视化数据并构建模型。我花了前六个月像从火管中喝水一样,阅读和学习自然语言处理(NLP)、机器学习、信息检索和统计数据科学,至少足以能够参与我所构建的功能。
正是在那个时候,我与研究团队合作将这些算法推向市场,我意识到开发人员和数据科学家需要更好地协作。传统的方法是让数据科学家单独解决复杂的数据问题,然后将结果“抛给”开发人员,由他们来实现这些结果。但考虑到数据处理量不断呈指数级增长,以及市场所需时间日益缩短,这种做法是不可持续的,且无法扩展。
相反,他们的角色需要转变为作为一个团队共同工作,这意味着数据科学家必须像软件开发人员一样思考和工作,反之亦然。事实上,这在理论上看起来非常好:一方面,数据科学家将从成熟的软件开发方法论中受益,例如敏捷开发——其快速迭代和频繁反馈的方式——同时也能从严谨的软件开发生命周期中获益,带来符合企业需求的合规性,例如安全性、代码审查、源代码控制等。另一方面,开发人员将开始以全新的方式思考数据:将其视为用于发现洞察的分析,而不仅仅是具有查询和CRUD(即创建、读取、更新、删除)API 的持久层。
将这些概念付诸实践
在担任 Watson 核心工具首席架构师并为 Watson 问答系统构建自助工具四年后,我加入了 IBM Watson 数据平台组织的开发者倡导团队,后者的使命是创建一个平台,将数据和认知服务的产品组合带到 IBM 公共云。我们的任务相当简单:赢得开发者的心,并帮助他们在数据和 AI 项目中取得成功。
这项工作有多个维度:教育、宣传和激进主义。前两者比较直接,但激进主义的概念与这次讨论相关,并且值得详细解释。顾名思义,激进主义是关于在需要改变的地方带来变革。对于我们 15 位开发者倡导者的团队来说,这意味着要站在开发者的角度思考他们在处理数据时的体验——无论他们是刚刚开始,还是已经在应用高级算法——感受他们的痛点并识别应该解决的空白。为此,我们构建并开源了多个带有真实场景案例的数据管道示例。
至少,这些项目需要满足三个基本要求:
-
用作输入的原始数据必须是公开可用的
-
提供清晰的指令,确保数据管道可以在合理时间内部署到云端
-
开发者应该能够将该项目作为类似场景的起点,即代码必须具备高度的可定制性和可重用性
从这些练习中获得的经验和洞察是无价的:
-
理解哪些数据科学工具最适合每项任务
-
最佳实践框架和语言
-
部署和操作分析的最佳实践架构
指导我们选择的指标有很多:准确性、可扩展性、代码重用性,但最重要的是,改善数据科学家与开发者之间的协作。
深入探讨一个具体的例子
最初,我们希望构建一个数据管道,通过对包含特定标签的推文进行情感分析,提取 Twitter 中的洞察,并将结果部署到实时仪表板上。这个应用程序对我们来说是一个完美的起点,因为数据科学分析并不复杂,且该应用程序涵盖了许多现实场景的方面:
-
高流量、高吞吐量的流式数据
-
通过情感分析 NLP 进行数据增强
-
基本数据聚合
-
数据可视化
-
部署到实时仪表板
为了尝试这个方案,第一次实现是一个简单的 Python 应用程序,使用了 tweepy 库(Python 的官方 Twitter 库:pypi.python.org/pypi/tweepy
)来连接 Twitter,获取一系列的推文流,以及 textblob 库(用于基本 NLP 的简单 Python 库:pypi.python.org/pypi/textblob
)进行情感分析的丰富。
然后,结果被保存到 JSON 文件中进行分析。这个原型是启动项目并快速实验的一个绝佳方式,但经过几轮迭代后,我们迅速意识到,需要认真构建一个满足企业需求的架构。
数据管道蓝图
从高层次来看,数据管道可以通过以下通用蓝图来描述:
数据管道工作流
数据管道的主要目标是将数据科学分析结果转化为可操作的(即提供直接的业务价值)输出,并且在可扩展、可重复的过程中实现高度自动化。分析的例子可以是一个推荐引擎,用于激励消费者购买更多产品,例如,亚马逊的推荐列表,或者一个展示关键绩效指标(KPI)的仪表板,帮助 CEO 为公司的未来决策提供依据。
构建数据管道的过程中涉及多个角色:
-
数据工程师:他们负责设计和运营信息系统。换句话说,数据工程师负责与数据源对接,获取原始数据,然后进行处理(有些人称之为数据清洗),直到数据准备好进行分析。在亚马逊推荐系统的例子中,他们会实现一个流处理管道,捕捉并聚合来自电商系统的特定消费者交易事件,并将这些数据存储到数据仓库中。
-
数据科学家:他们分析数据并构建提取洞察的分析模型。在亚马逊推荐系统的例子中,他们可能会使用 Jupyter Notebook 连接到数据仓库,加载数据集,并使用例如协同过滤算法来构建推荐引擎(
en.wikipedia.org/wiki/Collaborative_filtering
)。 -
开发人员:他们负责将分析功能转化为面向业务用户(如业务分析师、高层管理人员、最终用户等)的应用程序。在亚马逊推荐系统中,开发人员将在用户完成购买后或通过定期电子邮件呈现推荐的产品列表。
-
业务用户:指的是所有使用数据科学分析输出的用户,例如,业务分析师分析仪表板以监控业务健康状况,或最终用户使用一个提供推荐的应用程序来决定下一步购买什么。
注意
在现实生活中,通常同一个人可能会扮演多个角色;这意味着一个人在与数据管道互动时可能有多个不同的需求。
正如前面的图示所示,构建数据科学管道是一个迭代过程,并遵循一个明确定义的流程:
-
获取数据:此步骤包括从各种来源获取原始数据:结构化(关系数据库、记录系统等)或非结构化(网页、报告等):
-
数据清洗:检查数据完整性,填补缺失数据,修复不正确的数据,以及数据预处理
-
数据准备:丰富数据,检测/移除异常值,并应用业务规则
-
-
分析:此步骤结合了描述性(理解数据)和规范性(构建模型)活动:
-
探索:发现统计特性,例如集中趋势、标准差、分布,以及变量识别,如单变量和双变量分析、变量之间的相关性等。
-
可视化:这个步骤对于正确分析数据和形成假设至关重要。可视化工具应该提供合理的互动性,以便帮助理解数据。
-
构建模型:应用推断统计学来形成假设,例如为模型选择特征。这个步骤通常需要专家领域知识,并且有很大的解释空间。
-
-
部署:将分析阶段的结果转化为可操作的实际应用:
-
沟通:生成报告和仪表板,清晰地传达分析结果,以供业务用户(如高层管理人员、业务分析师等)使用
-
发现:设定一个聚焦于发现新见解和商业机会的业务目标,这些见解和机会可能会带来新的收入来源
-
实施:为最终用户创建应用程序
-
-
测试:这个活动应该贯穿于每一步,但在这里我们讨论的是创建一个来自实际应用的反馈循环:
-
创建衡量模型准确性的指标
-
优化模型,例如获取更多数据、寻找新特征等
-
成为数据科学家需要哪些技能?
在行业中,现实情况是,数据科学如此新兴,以至于公司还没有为其制定出明确的职业发展路径。那么,如何才能获得数据科学家的职位呢?需要多少年的经验?需要具备哪些技能?数学、统计学、机器学习、信息技术、计算机科学,还有什么?
其实,答案可能是“稍微懂一点儿各个领域”再加上一项关键技能:领域特定的专业知识。
目前有一种争论,关于是否在没有深入了解数据意义的情况下,将通用的数据科学技术应用于任何数据集,能否实现期望的商业结果。许多公司倾向于确保数据科学家具有足够的领域专业知识,其理由是,没有这一点,你可能会在任何步骤中无意间引入偏差,比如在数据清洗阶段填补空缺时,或在特征选择过程中,最终构建的模型可能非常适合某个数据集,但依然没有实际价值。想象一下,一个没有化学背景的数据科学家,正在为一家制药公司研究不需要的分子相互作用,帮助其开发新药。或许这也是为什么我们看到越来越多专门针对某一领域的统计学课程,例如生物学的生物统计学,或是供应链分析,专注于分析与供应链相关的运营管理等。
总结来说,一名数据科学家在理论上应该对以下领域有所熟练掌握:
-
数据工程 / 信息检索
-
计算机科学
-
数学与统计学
-
机器学习
-
数据可视化
-
商业智能
-
领域专业知识
注意事项
如果你正在考虑获取这些技能,但没有时间参加传统的课堂学习,我强烈推荐使用在线课程。
我特别推荐这门课程:www.coursera.org/
: www.coursera.org/learn/data-science-course
。
Drew 的 Conway 维恩图提供了一个关于什么是数据科学以及为什么数据科学家有点像独角兽的优秀可视化图示:
Drew 的 Conway 数据科学维恩图
到目前为止,我希望你已经明白,符合上述描述的完美数据科学家更像是例外而非常态,而且通常这个角色涉及多个身份。没错,我想表达的观点是,数据科学是一项团队运动,这个观点将在本书中反复出现。
IBM Watson DeepQA
有一个项目很好地说明了数据科学是一项团队运动的观点,那就是 IBM DeepQA 研究项目。这个项目最初是 IBM 发起的一个重大挑战,目的是构建一个能够回答自然语言问题的人工智能系统,依据预定的领域知识。问答(QA)系统应该足够好,能够与《危险边缘!》这档受欢迎的电视游戏节目中的人类选手竞争。
众所周知,这个名为 IBM Watson 的系统在 2011 年赢得了比赛,战胜了两位最有经验的危险边缘!冠军:Ken Jennings 和 Brad Rutter。以下照片来自 2011 年 2 月播出的实际比赛:
IBM Watson 与 Ken Jennings 和 Brad Rutter 在《危险边缘!》上的对决!
来源:https://upload.wikimedia.org/wikipedia/e
正是在与构建 IBM Watson QA 计算机系统的研究团队互动时,我才得以更深入地了解 DeepQA 项目架构,并亲身体验到许多数据科学领域实际上是如何被应用的。
以下图示描述了 DeepQA 数据管道的高层次架构:
Watson DeepQA 架构图
来源:https://researcher.watson.ibm.com/researcher/files/us-mi
如上图所示,回答问题的数据管道由以下高层次步骤组成:
-
问题与主题分析(自然语言处理):此步骤使用一个深度解析组件,检测构成问题的单词之间的依赖关系和层次结构。目标是更深入地理解问题,并提取出基本属性,例如:
-
重点:这个问题到底是在问什么?
-
词汇答案类型(LAT):期望答案的类型,例如一个人,一个地方等等。在候选答案的评分过程中,这些信息非常重要,因为它为与 LAT 不匹配的答案提供了早期过滤。
-
命名实体解析:将一个实体解析为标准化的名称,例如,将“Big Apple”解析为“New York”。
-
指代解析:将代词与问题中的先前术语连接起来,例如,在句子“1715 年 9 月 1 日,路易十四在这座城市去世,他建造了这座宏伟的宫殿”中,代词“他”指代的是路易十四。
-
关系检测:检测问题中的关系,例如,“她在 1954 年与乔·迪马吉奥离婚”,其中的关系是“乔·迪马吉奥娶了 X”。这类关系(主语->谓语->宾语)可以用于查询三元组存储,并得到高质量的候选答案。
-
问题分类:将问题映射到Jeopardy!中预定义的类型之一,例如,事实类问题、多项选择题、谜题等。
-
-
初步搜索与假设生成(信息检索):这一步骤主要依赖于问题分析步骤的结果,构建一组适应不同答案来源的查询。答案来源的一些例子包括各种全文搜索引擎,如 Indri(
www.lemurproject.org/indri.php
)和 Apache Lucene/Solr(lucene.apache.org/solr
),面向文档和标题的搜索(Wikipedia),三元组存储等等。搜索结果随后用于生成候选答案。例如,面向标题的结果将直接作为候选答案,而文档搜索则需要对段落进行更详细的分析(再次使用 NLP 技术)以提取可能的候选答案。 -
假设与证据评分(NLP 和信息检索):对于每个候选答案,进行另一次搜索以寻找更多支持证据,并使用不同的评分技术。此步骤还充当筛选测试,某些不符合步骤 1 中计算的 LAT 的候选答案将被淘汰。此步骤的输出是一组与找到的支持证据对应的机器学习特征。这些特征将作为输入,传递给一组机器学习模型,用于对候选答案进行评分。
-
最终合并与评分(机器学习):在最后一步中,系统识别相同答案的变体并将它们合并。它还使用机器学习模型,根据步骤 3 中生成的特征,选择由各自得分排名的最佳答案。这些机器学习模型已在一组代表性问题上进行训练,使用正确答案和预先摄取的文档语料库进行对比。
在继续讨论数据科学和人工智能如何改变计算机科学领域时,我认为有必要看看最先进的技术。IBM Watson 是这些旗舰项目之一,它为我们自它击败 Ken Jennings 和 Brad Rutter 参加Jeopardy!(危险游戏)以来的进步铺平了道路。
回到我们的 Twitter 话题情感分析项目
我们构建的快速数据管道原型使我们对数据有了更好的理解,但接下来我们需要设计一个更强大的架构,并使我们的应用程序准备好进入企业级。我们的主要目标仍然是积累构建数据分析的经验,而不是在数据工程部分花费太多时间。这就是为什么我们尽可能利用开源工具和框架的原因:
-
Apache Kafka (
kafka.apache.org
): 这是一个可扩展的流处理平台,用于以可靠和容错的方式处理大量的推文。 -
Apache Spark (
spark.apache.org
): 这是一个内存计算集群框架。Spark 提供了一个编程接口,抽象了并行计算的复杂性。 -
Jupyter Notebooks (
jupyter.org
): 这些基于 Web 的交互式文档(Notebooks)允许用户远程连接到计算环境(内核),以创建先进的数据分析。Jupyter 内核支持多种编程语言(Python、R、Java/Scala 等)以及多个计算框架(Apache Spark、Hadoop 等)。
对于情感分析部分,我们决定用 Watson Tone Analyzer 服务 (www.ibm.com/watson/services/tone-analyzer
) 替换我们使用 textblob Python 库编写的代码。Watson Tone Analyzer 是一个基于云的 REST 服务,提供包括情感、语言和社交语气检测在内的高级情感分析。尽管 Tone Analyzer 不是开源的,但 IBM 云上提供了一个可供开发和试用的免费版本 (www.ibm.com/cloud
)。
我们的架构现在如下所示:
Twitter 情感分析数据管道架构
在上述图中,我们可以将工作流程分解为以下几个步骤:
-
产生一系列推文并将其发布到 Kafka 主题中,Kafka 主题可以看作是将事件聚集在一起的通道。接收组件可以订阅该主题/通道以消费这些事件。
-
通过情感、语言和社交语气得分来丰富推文:使用 Spark Streaming 订阅来自组件1的 Kafka 主题,并将文本发送到 Watson Tone Analyzer 服务。生成的语气得分将被添加到数据中,供下游进一步处理。此组件是使用 Scala 实现的,为了方便起见,它是通过 Jupyter Scala Notebook 运行的。
-
数据分析和探索:对于这一部分,我们决定使用 Python Notebook,因为 Python 提供了更具吸引力的库生态系统,尤其是在数据可视化方面。
-
将结果发布回 Kafka。
-
实现一个实时仪表板作为 Node.js 应用。
在三人团队的协作下,我们花了大约 8 周的时间,成功将仪表板与实时 Twitter 情感数据对接。这个看似较长的时间有多个原因:
-
一些框架和服务,比如 Kafka 和 Spark Streaming,对我们来说是全新的,我们必须学习如何使用它们的 API。
-
仪表板前端是作为一个独立的 Node.js 应用构建的,使用了 Mozaïk 框架(
github.com/plouc/mozaik
),它使得构建强大的实时仪表板变得简单。然而,我们发现代码中有一些局限性,这迫使我们深入实现,编写补丁,因此对整体进度造成了一定延迟。
结果展示在以下截图中:
Twitter 情感分析实时仪表板
从构建我们第一个企业级数据管道中学到的经验教训
利用开源框架、库和工具,的确帮助我们在实现数据管道时提高了效率。例如,Kafka 和 Spark 部署相对简单,使用也非常方便,而且当我们遇到瓶颈时,我们可以随时依赖开发者社区的帮助,例如通过使用像stackoverflow.com
这样的问题解答网站。
使用基于云的托管服务进行情感分析步骤,比如 IBM Watson Tone Analyzer(www.ibm.com/watson/services/tone-analyzer
),也是一个积极的做法。它让我们可以抽象出训练和部署模型的复杂性,使得整个步骤更可靠,显然比我们自己实现要准确得多。
它也非常容易集成,因为我们只需要发送一个 REST 请求(也称为 HTTP 请求,关于 REST 架构的更多信息,请参考en.wikipedia.org/wiki/Representational_state_transfer
)就能获得我们的答案。现在,大多数现代 Web 服务都遵循 REST 架构,然而,我们仍然需要了解每个 API 的规范,这通常需要很长时间才能正确实现。通过使用 SDK 库,这一步通常会变得更简单,SDK 库通常是免费的,并且支持大多数流行的编程语言,如 Python、R、Java 和 Node.js。SDK 库通过抽象出生成 REST 请求的代码,提供了更高级的程序化访问服务。SDK 通常会提供一个类来表示服务,每个方法封装一个 REST API,并处理用户认证和其他请求头。
在工具方面,我们对 Jupyter Notebooks 印象深刻,它提供了许多出色的功能,例如协作和完全交互性(我们稍后会更详细地介绍 Notebooks)。
当然,并非一切都很顺利,我们在一些关键领域遇到了困难:
-
在选择编程语言方面,对于一些关键任务,如数据丰富和数据分析,我们最终选择了 Scala 和 Python,尽管团队经验不多,主要是因为它们在数据科学家中非常受欢迎,而且我们也希望学习它们。
-
创建数据探索的可视化图表耗费了太多时间。使用可视化库(如 Matplotlib 或 Bokeh)编写一个简单的图表需要写大量的代码。这反过来又拖慢了我们对快速实验的需求。
-
将分析功能操作化为实时仪表板的过程非常困难,难以扩展。如前所述,我们需要编写一个完整的独立 Node.js 应用程序,该应用程序从 Kafka 消费数据,并且需要作为云平台应用程序(
www.cloudfoundry.org
)部署在 IBM Cloud 上。可以理解的是,这个任务第一次完成时需要相当长的时间,但我们还发现,更新它也很困难。写入 Kafka 的分析功能需要与仪表板应用程序上的更改同步。
数据科学战略
如果数据科学要继续增长并成为核心业务活动,公司必须找到一种方法,在整个组织的所有层面上扩展它,并克服我们之前讨论的所有困难挑战。为了实现这一目标,我们确定了三大重要支柱,架构师在规划数据科学战略时应重点关注这三点:数据、服务和工具:
大规模数据科学的三大支柱
-
数据是你最宝贵的资源:你需要一个合适的数据策略,确保数据科学家可以轻松访问所需的精心策划的内容。通过对数据进行适当的分类、设定合适的治理政策,并使元数据可搜索,将减少数据科学家获取数据并请求使用权限的时间。这不仅能提高他们的生产力,还能提升他们的工作满意度,因为他们会有更多的时间从事实际的数据科学工作。
制定一个数据策略,使数据科学家能够轻松访问与其相关的高质量数据,能够提高生产力和士气,并最终带来更高的成功率。
-
服务:每个规划数据科学的架构师都应该考虑采用面向服务架构(SOA)。与传统的单体应用程序不同,后者将所有功能捆绑在一起进行单一部署,面向服务的系统将功能拆解为若干服务,每个服务只负责做少数几件事,但做得非常出色,且具备高性能和可扩展性。这些系统可以独立部署和维护,相互之间没有依赖,从而为整个应用基础设施提供可扩展性和可靠性。例如,你可以有一个运行算法创建深度学习模型的服务,另一个则负责持久化模型并允许应用程序运行它来对客户数据进行预测,等等。
优势显而易见:高度的可重用性、更容易的维护、更短的上市时间、可扩展性等等。此外,这种方法还非常适合融入云策略,当你的工作负载超出现有容量时,它能够提供增长路径。你还需要优先考虑开源技术,并尽可能标准化开放协议。
将流程拆分为更小的功能模块,能够为系统注入可扩展性、可靠性和重复性。
-
工具确实很重要! 没有合适的工具,一些任务将变得极其难以完成(至少这是我用来解释自己为什么修不好家里东西的理由)。然而,你也需要保持工具的简单性、标准化,并合理地集成它们,以便即便是技术水平较低的用户也能使用(即使我得到了正确的工具,我也不确定自己能否完成家务修理任务,除非它足够简单)。一旦你降低了这些工具的学习曲线,非数据科学家的用户将会感到更加舒适使用它们。
简化工具的使用有助于打破信息孤岛,并增强数据科学、工程和业务团队之间的协作。
Jupyter Notebooks 是我们策略的核心
本质上,Notebooks 是由可编辑单元格组成的网页文档,允许你与后端引擎进行交互式命令操作。正如其名称所示,我们可以将其视为纸质便签的数字版,用于记录实验的笔记和结果。这个概念既强大又简单:用户可以在自己选择的编程语言中输入代码(大多数 Notebooks 实现支持多种语言,如 Python、Scala、R 等),运行单元并在单元下方的输出区域互动式地查看结果,这些结果会成为文档的一部分。结果可以是任何类型:文本、HTML 和图像,非常适合数据图表化。这就像是在传统的 REPL(读取-评估-打印-循环)程序上使用增强版,因为 Notebook 可以连接到强大的计算引擎(例如 Apache Spark(spark.apache.org
)或 Python Dask(dask.pydata.org
)集群),如果需要,可以进行大数据实验。
在 Notebooks 中,任何在单元格中创建的类、函数或变量都可以在下方的单元格中看到,允许你逐步编写复杂的分析,迭代地测试假设并在进入下一个阶段之前解决问题。此外,用户还可以使用流行的 Markdown 语言编写富文本,或者使用 LaTeX(www.latex-project.org/
)编写数学表达式,来描述他们的实验供其他人阅读。
下图展示了一个示例 Jupyter Notebook 的部分内容,其中包含一个 Markdown 单元,解释实验的内容,一个用 Python 编写的代码单元用于创建 3D 图表,以及实际的 3D 图表结果:
样本 Jupyter Notebook
为什么 Notebooks 如此受欢迎?
在过去的几年里,Notebooks 作为数据科学相关活动的首选工具,迎来了爆炸式的增长。解释这一现象的原因有很多,但我认为主要原因是其多功能性,使得它不仅是数据科学家不可或缺的工具,也是参与构建数据管道的大多数角色,包括业务分析师和开发人员的必备工具。
对于数据科学家来说,Notebooks 非常适合迭代实验,因为它使得他们能够快速加载、探索和可视化数据。Notebooks 也是一个出色的协作工具;它们可以导出为 JSON 文件,并轻松地在团队之间共享,允许实验在需要时能够精确重复和调试。此外,由于 Notebooks 还是网页应用,它们可以轻松集成到基于云的多用户环境中,从而提供更好的协作体验。
这些环境还可以通过使用像 Apache Spark 这样的框架,将笔记本与机器集群连接,从而按需提供大规模计算资源。这些基于云的笔记本服务器的需求正在迅速增长,因此,我们看到越来越多的SaaS(即软件即服务)解决方案,不仅包括商业产品,如 IBM 数据科学体验(datascience.ibm.com
)或 DataBricks(databricks.com/try-databricks
),还包括开源解决方案,如 JupyterHub(jupyterhub.readthedocs.io/en/latest
)。
对于商业分析师,笔记本可以作为演示工具,在大多数情况下,凭借其对 Markdown 的支持,提供足够的功能来取代传统的 PowerPoint。生成的图表和表格可以直接用于有效地传达复杂分析的结果;不再需要复制粘贴,并且算法的更改会自动反映在最终演示中。例如,某些笔记本实现(如 Jupyter)提供了将单元格布局自动转换为幻灯片的功能,使得整个体验更加无缝。
注意
作为参考,以下是在 Jupyter 笔记本中制作这些幻灯片的步骤:
-
使用视图 | 单元工具栏 | 幻灯片放映,首先通过选择幻灯片、子幻灯片、片段、跳过或笔记来标注每个单元格。
-
使用
nbconvert jupyter
命令将笔记本转换为 Reveal.js 驱动的 HTML 幻灯片: -
可选地,你可以启动一个 web 应用服务器,以便在线访问这些幻灯片:
jupyter nbconvert <pathtonotebook.ipynb> --to slides
jupyter nbconvert <pathtonotebook.ipynb> --to slides –post serve
对于开发人员来说,情况要复杂得多。一方面,开发人员喜欢 REPL 编程,而笔记本提供了交互式 REPL 的所有优势,此外,它还可以连接到远程后端。由于它是在浏览器中运行的,结果可以包含图形,而且由于它们可以被保存,整个笔记本或其部分内容可以在不同的场景中重复使用。因此,对于开发人员来说,只要你选择的语言可用,笔记本提供了一种很好的方式来尝试和测试不同的内容,比如微调算法或集成新的 API。另一方面,尽管开发人员最终负责将分析结果转化为满足客户需求的应用程序,但他们对笔记本的采纳率很低,尤其是在数据科学活动中,这些活动本可以补充数据科学家的工作。
为了改善软件开发生命周期并缩短价值实现的时间,开发者需要开始使用与数据科学家相同的工具、编程语言和框架,包括 Python 及其丰富的库生态系统,以及已经成为数据科学重要工具的 Notebooks。尽管开发者必须在数据科学的理论和概念上与数据科学家站在一起并迅速掌握,但根据我的经验,我强烈推荐使用MOOCs(大规模在线开放课程),如 Coursera(www.coursera.org
)或 EdX(www.edx.org
),这些平台提供各种各样的课程,适合各个层次的学习者。
然而,尽管 Notebook 功能强大,但它们主要是为数据科学家设计的,开发者需要面临陡峭的学习曲线。它们还缺乏开发者所需的应用开发功能,这对开发者至关重要。正如我们在Twitter 话题标签情感分析项目中所看到的,基于 Notebook 中创建的分析构建应用程序或仪表板可能非常困难,需要一种架构,而这种架构不仅难以实现,而且对基础设施的负担较重。
正是为了解决这些问题,我决定创建 PixieDust(github.com/ibm-watson-data-lab/pixiedust
)库并将其开源。如我们在接下来的章节中将看到的,PixieDust 的主要目标是通过提供简单的 API 来加载和可视化数据,从而降低新用户(无论是数据科学家还是开发者)的入门成本。PixieDust 还提供了一个开发者框架,带有 API,方便开发者构建可以直接在 Notebook 中运行并部署为 Web 应用程序的应用、工具和仪表板。
总结
在本章中,我从开发者的角度分享了我对数据科学的看法,讨论了为什么我认为数据科学与人工智能(AI)和云计算(Cloud)一起,具有定义下一代计算时代的潜力。我还讨论了在数据科学充分实现其潜力之前,必须解决的许多问题。尽管本书并不打算提供一个解决所有这些问题的“魔法食谱”,但它确实尝试回答一个既困难又关键的问题,那就是如何实现数据科学的民主化,特别是如何弥合数据科学家和开发者之间的鸿沟。
在接下来的几章中,我们将深入探讨 PixieDust 开源库,了解它如何帮助 Jupyter Notebook 用户在处理数据时提高效率。我们还将深入研究 PixieApp 应用开发框架,该框架使开发者能够利用 Notebook 中实现的分析来构建应用程序和仪表板。
在接下来的章节中,我们将深入探讨许多示例,展示数据科学家和开发者如何有效地协作,构建端到端的数据管道,迭代分析,并将其在更短的时间内部署到最终用户。这些示例应用将涵盖多个行业用例,如图像识别、社交媒体和金融数据分析,其中包括数据科学用例,如描述性分析、机器学习、自然语言处理和流数据。
我们不会深入讨论示例应用中涵盖的所有算法背后的理论(这超出了本书的范围,且需要一本书来讲解),而是将重点强调如何利用开源生态系统快速完成手头的任务(如模型构建、可视化等),并将结果转化为应用程序和仪表板。
注意
提供的示例应用主要使用 Python 编写,并附带完整的源代码。代码经过广泛测试,已经准备好可以在您的项目中重复使用和定制。
第二章 Python 和 Jupyter Notebooks 助力你的数据分析
“最好的代码是你不必写的那一行!”
– 未知
在上一章中,我从开发者的角度,基于真实经验探讨了数据科学,并讨论了成功部署企业所需的三个战略支柱:数据、服务和工具。我还讨论了一个观点,即数据科学不仅仅是数据科学家的专属领域,它实际上是一个团队合作的过程,其中开发者扮演着特殊的角色。
在本章中,我将介绍一个解决方案——基于 Jupyter Notebooks、Python 和 PixieDust 开源库——它专注于三个简单的目标:
-
通过降低非数据科学家的准入门槛,实现数据科学的普及
-
增加开发者与数据科学家之间的合作
-
让数据科学分析更易于操作化
注意
该解决方案仅聚焦于工具层面,而不涉及数据和服务,尽管我们将在第六章 数据分析研究:使用 TensorFlow 的 AI 与图像识别中讨论一些内容,数据和服务应独立实现。
为什么选择 Python?
像许多开发者一样,在构建数据密集型项目时,使用 Python 并不是我的首选。说实话,经过多年的 Java 工作经验后,最初 Scala 对我来说更具吸引力,尽管学习曲线相当陡峭。Scala 是一种非常强大的语言,它优雅地结合了面向对象编程和函数式编程,而这些在 Java 中是极为缺乏的(至少直到 Java 8 开始引入 Lambda 表达式)。
Scala 还提供了非常简洁的语法,这转化为更少的代码行、更高的生产力,最终也减少了错误。这在你大部分工作都涉及数据操作时非常有用。喜欢 Scala 的另一个原因是,当使用像 Apache Spark 这样的“大数据”框架时,它的 API 支持更好,而这些框架本身就是用 Scala 编写的。还有很多其他理由使得 Scala 更受青睐,比如它是一个强类型系统,能够与 Java 互操作,拥有在线文档,并且性能高。
因此,对于像我这样的开发者,刚开始涉足数据科学时,Scala 似乎是一个更自然的选择,但实际上,剧透一下,我们最终选择了 Python。这一选择有多个原因:
-
Python 作为一种语言也有很多优点。它是一种动态编程语言,具有与 Scala 类似的优势,比如函数式编程和简洁的语法等。
-
在过去几年里,Python 在数据科学家中迅速崛起,超过了长期竞争对手 R,成为数据科学领域的首选语言,这一点通过在 Google Trends 上搜索
Python Data Science
、Python Machine Learning
、R Data Science
和R Machine Learning
可以看到:2017 年的兴趣趋势
在良性循环中,Python 日益增长的流行度推动了一个庞大而不断扩展的生态系统,涵盖了各种各样的库,这些库可以通过 pip Python 包安装器轻松导入到项目中。数据科学家现在可以使用许多强大的开源 Python 库进行数据处理、数据可视化、统计学、数学、机器学习和自然语言处理。
即使是初学者,也可以通过流行的 scikit-learn 包 (scikit-learn.org
) 快速构建机器学习分类器,而不需要成为机器学习专家,或者使用 Matplotlib (matplotlib.org
) 或 Bokeh (bokeh.pydata.org
) 快速绘制丰富的图表。
此外,Python 也成为了开发者的顶级语言之一,正如 IEEE Spectrum 2017 年调查所示 (spectrum.ieee.org/computing/software/the-2017-top-programming-languages
):
按编程语言划分的使用统计
这一趋势在 GitHub 上也得到了确认,Python 现在在所有仓库的总数中排名第三,仅次于 Java 和 JavaScript:
按编程语言划分的 GitHub 仓库统计
上面的图表展示了一些有趣的统计数据,说明了 Python 开发者社区的活跃程度。在 GitHub 上,Python 相关的活跃仓库是第三大,拥有健康的总代码推送和每个仓库的开启问题数量。
Python 还在网页开发中变得无处不在,许多知名网站都采用了如 Django (www.djangoproject.com
)、Tornado (www.tornadoweb.org
)和 TurboGears (turbogears.org
)等网页开发框架。最近,Python 也开始渗透到云服务领域,所有主要云服务提供商都将其作为某种程度的服务纳入其产品中。
显然,Python 在数据科学领域有着光明的前景,特别是当与强大的工具如 Jupyter Notebooks 一起使用时,Jupyter Notebooks 在数据科学家社区中已经变得非常流行。Notebook 的价值主张在于,它们非常容易创建,非常适合快速运行实验。此外,Notebook 支持多种高保真度的序列化格式,可以捕捉指令、代码和结果,然后很容易与团队中的其他数据科学家分享,或作为开源项目供所有人使用。例如,我们看到越来越多的 Jupyter Notebooks 在 GitHub 上被分享,数量已超过 250 万,并且还在不断增加。
下图显示了在 GitHub 上搜索所有扩展名为 .ipynb
的文件的结果,这是最流行的 Jupyter Notebooks 序列化格式(JSON 格式):
在 GitHub 上搜索 Jupyter Notebooks 的结果
这很好,但 Jupyter Notebooks 常常被认为仅仅是数据科学家的工具。我们将在接下来的章节中看到,它们可以做得更多,而且可以帮助所有类型的团队解决数据问题。例如,它们可以帮助业务分析师快速加载和可视化数据集,使开发者能够直接在 Notebook 中与数据科学家合作,利用他们的分析并构建强大的仪表盘,或允许 DevOps 将这些仪表盘轻松地部署到可扩展的企业级微服务中,这些微服务可以作为独立的 Web 应用程序运行,或者作为可嵌入的组件。正是基于将数据科学工具带给非数据科学家的愿景,才创建了 PixieDust 开源项目。
介绍 PixieDust
小贴士
有趣的事实
我经常被问到我如何想出“PixieDust”这个名字,对此我回答说,我只是想让 Notebook 对非数据科学家来说变得简单,就像魔法一样。
PixieDust (github.com/ibm-watson-data-lab/pixiedust
) 是一个开源项目,主要由三个组件组成,旨在解决本章开头提到的三个目标:
-
一个为 Jupyter Notebooks 提供的辅助 Python 库,提供简单的 API 来将数据从各种来源加载到流行的框架中,如 pandas 和 Apache Spark DataFrame,然后交互式地可视化和探索数据集。
-
一种基于 Python 的简单编程模型,使开发者能够通过创建强大的仪表盘(称为 PixieApps)将分析“产品化”到 Notebook 中。正如我们将在接下来的章节中看到的,PixieApps 与传统的 BI(即 商业智能)仪表盘不同,因为开发者可以直接使用 HTML 和 CSS 创建任意复杂的布局。此外,他们还可以在其业务逻辑中嵌入对 Notebook 中创建的任何变量、类或函数的访问。
-
一种名为 PixieGateway 的安全微服务 Web 服务器,可以将 PixieApps 作为独立的 Web 应用程序运行,或作为可以嵌入任何网站的组件。PixieApps 可以通过 Jupyter Notebook 使用图形向导轻松部署,并且无需任何代码更改。此外,PixieGateway 支持将任何由 PixieDust 创建的图表作为可嵌入的网页共享,使数据科学家可以轻松地将结果传达给 Notebook 外部的受众。
需要注意的是,PixieDust display()
API 主要支持两种流行的数据处理框架:
-
pandas (
pandas.pydata.org
): 迄今为止最受欢迎的 Python 数据分析包,pandas 提供了两种主要的数据结构:DataFrame 用于处理类似二维表格的数据集,Series 用于处理一维列状数据集。注意
目前,PixieDust
display()
只支持 pandas DataFrame。 -
Apache Spark DataFrame (
spark.apache.org/docs/latest/sql-programming-guide.html
): 这是一个高层数据结构,用于操作 Spark 集群中分布式数据集。Spark DataFrame 建立在低层的 RDD(即 Resilient Distributed Dataset)之上,并且增加了支持 SQL 查询的功能。
另一种 PixieDust display()
支持的较少使用的格式是 JSON 对象数组。在这种情况下,PixieDust 会使用这些值来构建行,并将键作为列,例如如下所示:
my_data = [
{"name": "Joe", "age": 24},
{"name": "Harry", "age": 35},
{"name": "Liz", "age": 18},
...
]
此外,PixieDust 在数据处理和渲染层面都具有高度的可扩展性。例如,你可以向可视化框架添加新的数据类型,或者如果你特别喜欢某个绘图库,你可以轻松地将其添加到 PixieDust 支持的渲染器列表中(更多细节请参见接下来的章节)。
你还会发现,PixieDust 包含了一些与 Apache Spark 相关的附加工具,例如以下内容:
-
PackageManager:这使你可以在 Python Notebook 中安装 Spark 包。
-
Scala 桥接:这使你可以在 Python Notebook 中直接使用 Scala,通过
%%scala
魔法命令。变量会自动从 Python 转移到 Scala,反之亦然。 -
Spark 作业进度监控器:通过在单元格输出中显示进度条,跟踪任何 Spark 作业的状态。
在我们深入了解 PixieDust 的三个组件之前,建议先获取一个 Jupyter Notebook,可以通过注册云端托管解决方案(例如,Watson Studio,网址:datascience.ibm.com
)或在本地机器上安装开发版来实现。
注意
你可以按照以下说明在本地安装 Notebook 服务器:jupyter.readthedocs.io/en/latest/install.html
。
要在本地启动 Notebook 服务器,只需从终端运行以下命令:
jupyter notebook --notebook-dir=<<directory path where notebooks are stored>>
Notebook 主页将自动在浏览器中打开。有许多配置选项可以控制 Notebook 服务器的启动方式。这些选项可以添加到命令行或持久化到 Notebook 配置文件中。如果你想尝试所有可能的配置选项,可以使用--generate-config
选项生成一个配置文件,如下所示:
jupyter notebook --generate-config
这将生成以下 Python 文件<home_directory>/.jupyter/jupyter_notebook_config.py
,其中包含一组已禁用的自动文档选项。例如,如果你不希望 Jupyter Notebook 启动时自动打开浏览器,找到包含sc.NotebookApp.open_browser
变量的行,取消注释并将其设置为False
:
## Whether to open in a browser after starting. The specific browser used is
# platform dependent and determined by the python standard library 'web browser'
# module, unless it is overridden using the --browser (NotebookApp.browser)
# configuration option.
c.NotebookApp.open_browser = False
在做完更改后,只需保存jupyter_notebook_config.py
文件并重新启动 Notebook 服务器。
下一步是使用pip
工具安装 PixieDust 库:
-
从 Notebook 本身,输入以下命令在单元格中执行:
!pip install pixiedust
注意
注意:感叹号语法是 Jupyter Notebook 特有的,表示后续的命令将作为系统命令执行。例如,你可以使用
!ls
列出当前工作目录下的所有文件和目录。 -
使用单元格 | 运行单元格菜单或工具栏上的运行图标来运行单元格。你也可以使用以下键盘快捷键来运行单元格:
-
Ctrl + Enter:运行并保持当前单元格选中
-
Shift + Enter:运行并选中下一个单元格
-
Alt + Enter:运行并在下方创建一个新的空单元格
-
-
重新启动内核以确保
pixiedust
库已正确加载到内核中。
以下截图显示了首次安装pixiedust
后的结果:
在 Jupyter Notebook 上安装 PixieDust 库
提示
我强烈推荐使用 Anaconda(anaconda.org
),它提供了优秀的 Python 包管理功能。如果你像我一样喜欢尝试不同版本的 Python 和库依赖,我建议你使用 Anaconda 虚拟环境。
它们是轻量级的 Python 沙箱,创建和激活非常简单(请参见conda.io/docs/user-guide/tasks/manage-environments.html
):
-
创建新环境:
conda create --name env_name
-
列出所有环境:
conda env list
-
激活环境:
source activate env_name
我还推荐你可以选择性地熟悉源代码,源代码可以在github.com/ibm-watson-data-lab/pixiedust
和github.com/ibm-watson-data-lab/pixiegateway
找到。
我们现在准备好在下一节中探索 PixieDust API,从sampleData()
开始。
SampleData – 一个简单的数据加载 API
将数据加载到 Notebook 中是数据科学家最常做的重复性任务之一,但根据使用的框架或数据源,编写代码可能会很困难且耗时。
让我们以一个具体的例子来说明,尝试从一个开放数据网站(例如 data.cityofnewyork.us
)加载 CSV 文件到 pandas 和 Apache Spark DataFrame。
注意
注意:接下来的所有代码都假定在 Jupyter Notebook 中运行。
对于 pandas,代码相当简单,因为它提供了一个直接从 URL 加载的 API:
import pandas
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_df = pandas.read_csv(data_url)
building_df
最后一条语句,调用building_df,
,将在输出单元中打印其内容。由于 Jupyter 会将单元格最后一条调用变量的语句作为打印指令,因此可以在不显式使用 print
的情况下实现此操作:
pandas DataFrame 的默认输出
然而,对于 Apache Spark,我们首先需要将数据下载到文件中,然后使用 Spark CSV 连接器将其加载到 DataFrame 中:
#Spark CSV Loading
from pyspark.sql import SparkSession
try:
from urllib import urlretrieve
except ImportError:
#urlretrieve package has been refactored in Python 3
from urllib.request import urlretrieve
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
urlretrieve (data_url, "building.csv")
spark = SparkSession.builder.getOrCreate()
building_df = spark.read\
.format('org.apache.spark.sql.execution.datasources.csv.CSVFileFormat')\
.option('header', True)\
.load("building.csv")
building_df
输出略有不同,因为 building_df
现在是一个 Spark DataFrame:
Spark DataFrame 的默认输出
尽管这段代码并不复杂,但每次都需要重复执行,并且很可能需要花时间做 Google 搜索来记住正确的语法。数据也可能采用不同的格式,例如 JSON,这需要调用不同的 API,无论是对于 pandas 还是 Spark。数据可能也不是很规范,可能在 CSV 文件中有错误行,或者 JSON 语法不正确。所有这些问题不幸的是并不罕见,并且符合数据科学的 80/20 法则,该法则表明数据科学家平均花费 80% 的时间来获取、清洗和加载数据,仅有 20% 的时间用于实际分析。
PixieDust 提供了一个简单的 sampleData
API 来帮助改善这种情况。当不带参数调用时,它会显示一个预先策划的、准备好进行分析的数据集列表:
import pixiedust
pixiedust.sampleData()
结果如下所示:
PixieDust 内置数据集
预构建的策划数据集列表可以根据组织的需要进行自定义,这是朝着我们数据支柱迈出的好步骤,如上一章所述。
用户可以简单地再次调用 sampleData
API,传入预构建数据集的 ID,如果 Jupyter 内核中有 Spark 框架可用,则会获得一个 Spark DataFrame;如果不可用,则会回退为 pandas DataFrame。
在下面的示例中,我们在与 Spark 连接的 Notebook 上调用 sampleData()
。我们还调用 enableSparkJobProgressMonitor()
来显示涉及操作的 Spark 作业的实时信息。
注意
注意:Spark 作业是在 Spark 集群中的特定节点上运行的进程,处理特定子集的数据。对于从数据源加载大量数据的情况,每个 Spark 作业会分配一个特定的子集来处理(实际大小取决于集群中的节点数和总体数据的大小),并与其他作业并行运行。
在一个单独的单元格中,我们运行以下代码来启用 Spark 作业进度监视器:
pixiedust.enableSparkJobProgressMonitor()
结果如下:
Successfully enabled Spark Job Progress Monitor
接着,我们调用 sampleData
来加载 cars
数据集:
cars = pixiedust.sampleData(1)
结果如下所示:
使用 PixieDust sampleData API 加载内置数据集
用户还可以传入指向可下载文件的任意 URL;PixieDust 目前支持 JSON 和 CSV 文件。在这种情况下,PixieDust 会自动下载该文件,缓存到临时区域,检测格式,并根据 Notebook 中是否可用 Spark 将其加载到 Spark 或 pandas DataFrame 中。请注意,即使 Spark 可用,用户也可以通过使用 forcePandas
关键字参数强制加载到 pandas 中:
import pixiedust
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_dataframe = pixiedust.sampleData(data_url, forcePandas=True)
结果如下:
Downloading 'https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD' from https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD
Downloaded 13672351 bytes
Creating pandas DataFrame for 'https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD'. Please wait...
Loading file using 'pandas'
Successfully created pandas DataFrame for 'https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD'
sampleData()
API 足够智能,能够识别指向 ZIP 和 GZ 类型压缩文件的 URL。在这种情况下,它会自动解压原始二进制数据,并加载归档中包含的文件。对于 ZIP 文件,它会查看归档中的第一个文件,而对于 GZ 文件,它会简单地解压内容,因为 GZ 文件不是归档文件,不包含多个文件。sampleData()
API 然后会从解压后的文件加载 DataFrame。
例如,我们可以直接从伦敦开放数据网站提供的 ZIP 文件加载区信息,并使用 display()
API 将结果显示为饼图,具体如下:
import pixiedust
london_info = pixiedust.sampleData("https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip")
结果如下(假设你的 Notebook 已连接到 Spark,否则将加载一个 pandas DataFrame):
Downloading 'https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip' from https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip
Extracting first item in zip file...
File extracted: london-borough-profiles.csv
Downloaded 948147 bytes
Creating pySpark DataFrame for 'https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip'. Please wait...
Loading file using 'com.databricks.spark.csv'
Successfully created pySpark DataFrame for 'https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip'
然后我们可以在 london_info
DataFrame 上调用 display()
,如图所示:
display(london_info)
我们在图表菜单中选择 饼图,在 选项 对话框中,将 Area name
列拖放到 键 区域,将 Crime rates per thousand population 2014/15
拖放到 值 区域,如下图所示:
用于可视化 london_info DataFrame 的图表选项
在 选项 对话框中点击 确定 按钮后,我们得到以下结果:
从指向压缩文件的 URL 创建饼图
许多时候,您可能找到了一个很棒的数据集,但文件中包含错误,或者对您重要的数据格式不正确,或者被埋在一些非结构化文本中需要提取到自己的列中。这个过程也被称为数据整理,可能非常耗时。在接下来的部分中,我们将看到一个名为pixiedust_rosie
的 PixieDust 扩展,它提供了一个wrangle_data
方法,可以帮助处理这个过程。
使用 pixiedust_rosie 整理数据
在受控实验中工作,大多数情况下与在真实世界中工作并不相同。我的意思是,在开发过程中,我们通常会选择(或者我应该说制造)一个设计良好、符合模式规范、没有数据缺失等特性的样本数据集。目标是专注于验证假设并构建算法,而不是数据清洗,这可能非常痛苦和耗时。然而,在开发过程的早期尽可能接近真实数据确实有不可否认的好处。为了帮助完成这项任务,我与两位 IBM 同事 Jamie Jennings 和 Terry Antony 合作,他们志愿构建了一个名为pixiedust_rosie
的 PixieDust 扩展。
这个 Python 包实现了一个简单的wrangle_data()
方法来自动清理原始数据。pixiedust_rosie
包目前支持 CSV 和 JSON 格式,但未来将添加更多格式。底层数据处理引擎使用了Rosie 模式语言(RPL)开源组件,这是一个专为开发人员设计、更高效、可扩展到大数据的正则表达式引擎。您可以在这里找到更多关于 Rosie 的信息:rosie-lang.org
。
要开始使用,您需要使用以下命令安装pixiedust_rosie
包:
!pip install pixiedust_rosie
pixiedust_rosie
包依赖于pixiedust
和rosie
,如果系统上尚未安装,将自动下载。
wrangle_data()
方法与sampleData()
API 非常相似。如果不带参数调用,它将显示预先筛选数据集的列表,如下所示:
import pixiedust_rosie
pixiedust_rosie.wrangle_data()
这将产生以下结果:
预先筛选的数据集列表可用于wrangle_data()
。
您还可以通过预先筛选的数据集的 ID 或 URL 链接来调用它,例如:
url = "https://github.com/ibm-watson-data-lab/pixiedust_rosie/raw/master/sample-data/Healthcare_Cost_and_Utilization_Project__HCUP__-_National_Inpatient_Sample.csv"
pixiedust_rosie.wrangle_data(url)
在上面的代码中,我们在由url
变量引用的 CSV 文件上调用wrangle_data()
。该函数首先下载文件到本地文件系统,并对数据的一个子集执行自动化数据分类,以推断数据架构。随后启动一个架构编辑器 PixieApp,提供一组向导屏幕,允许用户配置架构。例如,用户将能够删除和重命名列,更重要的是,通过提供 Rosie 模式将现有列解构为新列。
工作流在以下图示中进行说明:
wrangle_data()
工作流
wrangle_data()
向导的第一个屏幕显示了 Rosie 数据分类器推断出的架构,如下图所示:
wrangle_data()
架构编辑器
上面的架构小部件显示了列名、Rosie 类型
(特定于 Rosie 的高级类型表示)和列类型
(映射到支持的 pandas 类型)。每一行还包含三个操作按钮:
-
删除列:这将从架构中删除列。此列将不会出现在最终的 pandas DataFrame 中。
-
重命名列:这将改变列的名称。
-
转换列:通过将列解构为新列来转换列。
用户随时可以预览数据(如上面的 SampleData 小部件所示),以验证架构配置是否按预期运行。
当用户点击转换列按钮时,显示一个新屏幕,允许用户指定用于构建新列的模式。在某些情况下,数据分类器能够自动检测这些模式,在这种情况下,会添加一个按钮询问用户是否应用这些建议。
下图显示了带有自动化建议的转换选定列屏幕:
转换列屏幕
此屏幕显示四个小部件,包含以下信息:
-
Rosie 模式输入框是您可以输入自定义 Rosie 模式的位置,用于表示此列的数据。然后,您使用提取变量按钮告诉架构编辑器应该将模式中的哪一部分提取到新列中(更多细节稍后解释)。
-
有一个帮助小部件,提供指向 RPL 文档的链接。
-
显示当前列的数据预览。
-
显示应用了 Rosie 模式的数据预览。
当用户点击提取变量按钮时,部件将更新为如下:
将 Rosie 变量提取到列中
此时,用户可以选择编辑定义,然后点击创建列按钮,将新列添加到架构中。新列示例小部件随后会更新,显示数据预览。如果模式定义包含错误语法,则此小部件会显示错误:
应用模式定义后的新列预览
当用户点击提交列按钮时,主架构编辑器界面会再次显示,新增的列会被添加进来,如下图所示:
带有新列的架构编辑器
最后一步是点击完成按钮,将架构定义应用到原始文件中,并创建一个 pandas 数据框,该数据框将作为变量在笔记本中使用。此时,用户将看到一个对话框,其中包含一个可以编辑的默认变量名,如下图所示:
编辑结果 Pandas 数据框的变量名称
点击完成按钮后,pixiedust_rosie
会遍历整个数据集,应用架构定义。完成后,它会在当前单元格下方创建一个新单元格,其中包含生成的代码,调用display()
API 来显示新生成的 pandas 数据框,如下所示:
#Code generated by pixiedust_rosie
display(wrangled_df)
运行前面的单元格将让你探索和可视化新数据集。
我们在本节中探讨的wrangle_data()
功能是帮助数据科学家减少数据清理时间、更多时间进行数据分析的第一步。在下一节中,我们将讨论如何帮助数据科学家进行数据探索和可视化。
Display – 一个简单的交互式数据可视化 API
数据可视化是数据科学中另一个非常重要的任务,它在探索和形成假设中是不可或缺的。幸运的是,Python 生态系统拥有许多强大的库,专门用于数据可视化,例如以下这些流行的例子:
-
Matplotlib:
matplotlib.org
-
Seaborn:
seaborn.pydata.org
-
Bokeh:
bokeh.pydata.org
-
Brunel:
brunelvis.org
然而,类似于数据加载和清理,在笔记本中使用这些库可能会很困难且耗时。每个库都有自己独特的编程模型,API 学习和使用起来并不总是容易,特别是如果你不是一个有经验的开发者。另一个问题是,这些库没有提供一个高层次的接口来与常用的数据处理框架(如 pandas(也许 Matplotlib 除外)或 Apache Spark)进行对接,因此,在绘制数据之前,需要进行大量的数据准备工作。
为了帮助解决这个问题,PixieDust 提供了一个简单的display()
API,使得 Jupyter Notebook 用户可以通过交互式图形界面绘制数据,无需编写任何代码。这个 API 并不直接创建图表,而是通过调用渲染器的 API 来处理数据准备工作,根据用户的选择委托给相应的渲染器。
display()
API 支持多种数据结构(如 pandas、Spark 和 JSON)以及多种渲染器(如 Matplotlib、Seaborn、Bokeh 和 Brunel)。
举个例子,让我们使用内置的汽车性能数据集,开始通过调用display()
API 来可视化数据:
import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True) #car performance data
display(cars)
当命令首次在单元格中调用时,系统会显示一个表格视图,随着用户在菜单中的导航,所选的选项会以 JSON 格式存储在单元格的元数据中,确保下次运行该单元格时可以重新使用。所有可视化的输出布局遵循相同的模式:
-
有一个可扩展的顶级菜单,用于在不同的图表之间切换。
-
有一个下载菜单,允许将文件下载到本地计算机。
-
有一个过滤切换按钮,允许用户通过过滤数据来精炼他们的探索。我们将在过滤部分讨论过滤功能。
-
有一个展开/折叠 Pixiedust 输出按钮,用于折叠或展开输出内容。
-
有一个选项按钮,点击后会弹出一个对话框,包含当前可视化的特定配置。
-
有一个分享按钮,可以让你将可视化结果发布到网络上。
注
注:此按钮仅在你部署了 PixieGateway 后可用,详细内容将在第四章,将数据分析发布到网络 - PixieApp 工具中讨论。
-
在可视化的右侧有一组上下文相关的选项。
-
有主可视化区域。
表格渲染器的可视化输出布局
要开始创建图表,首先在菜单中选择合适的类型。PixieDust 默认支持六种类型的图表:柱状图、折线图、散点图、饼图、地图和直方图。正如我们在第五章,Python 和 PixieDust 最佳实践及高级概念中看到的,PixieDust 还提供 API,允许你通过添加新的菜单项或为现有菜单添加选项来自定义这些菜单:
PixieDust 图表菜单
当首次调用图表菜单时,将显示一个选项对话框,用于配置一组基本的配置选项,例如使用X轴和Y轴的内容、聚合类型等。为了节省时间,对话框将预填充 PixieDust 从 DataFrame 自动推测的数据架构。
在以下示例中,我们将创建一个条形图,显示按马力划分的平均油耗:
条形图对话框选项
点击OK将在单元格输出区域显示交互式界面:
条形图可视化
画布显示图表在中心区域,并在侧边展示与所选图表类型相关的上下文选项。例如,我们可以在Cluster By下拉框中选择origin字段,按原产国展示细分:
聚类条形图可视化
如前所述,PixieDust 的display()
实际上并不创建图表,而是根据选定的选项准备数据,并且通过渲染引擎的 API 调用做重载工作,使用正确的参数。这种设计的目标是让每种图表类型支持多种渲染器,无需额外编程,尽可能为用户提供自由的探索空间。
开箱即用,PixieDust 支持以下渲染器,前提是已安装相应的库。对于未安装的库,PixieDust 日志中将生成警告,并且对应的渲染器将不会在菜单中显示。我们将在第五章中详细介绍 PixieDust 日志,Python 和 PixieDust 最佳实践与高级概念。
-
Matplotlib (
matplotlib.org
) -
Seaborn (
seaborn.pydata.org
)注意
该库需要使用以下命令安装:
!pip install seaborn.
-
Bokeh (
bokeh.pydata.org
)注意
该库需要使用以下命令安装:
!pip install bokeh.
-
Brunel (
brunelvis.org
)注意
该库需要使用以下命令安装:
!pip install brunel.
-
Google Map (
developers.google.com/maps
) -
Mapbox (
www.mapbox.com
)注意
注意:Google Map 和 Mapbox 需要 API 密钥,您可以在各自的站点上获取。
你可以使用Renderer下拉框在不同的渲染器之间切换。例如,如果我们想要更多的交互性来探索图表(如缩放和平移),我们可以使用 Bokeh 渲染器,而不是 Matplotlib,后者仅提供静态图像:
使用 Bokeh 渲染器的簇状条形图
另一个值得一提的图表类型是 Map,当你的数据包含地理空间信息时,它特别有趣,例如经度、纬度或国家/州信息。PixieDust 支持多种类型的地理映射渲染引擎,包括流行的 Mapbox 引擎。
注意
在使用 Mapbox 渲染器之前,建议从 Mapbox 网站获取 API 密钥,网址如下:(www.mapbox.com/help/how-access-tokens-work
)。不过,如果没有密钥,PixieDust 将提供一个默认密钥。
为了创建一个地图图表,下面我们使用东北马萨诸塞百万美元住宅销售数据集:
import pixiedust
homes = pixiedust.sampleData(6, forcePandas=True) #Million dollar home sales in NE Mass
display(homes)
首先,在图表下拉菜单中选择Map,然后在选项对话框中,选择LONGITUDE
和LATITUDE
作为键,并在提供的输入框中输入 Mapbox 访问令牌。你可以在Values区域添加多个字段,它们将作为工具提示显示在地图上:
Mapbox 图表的选项对话框
当点击OK按钮时,你将获得一个交互式地图,你可以使用样式(简单、分区图或密度图)、颜色和底图(亮色、卫星图、暗色和户外)选项来自定义该地图:
交互式 Mapbox 可视化
每种图表类型都有自己的一套上下文选项,这些选项不难理解,在此我鼓励你尝试每一个选项。如果遇到问题或有改进的想法,你可以随时在 GitHub 上创建一个新问题,网址为github.com/ibm-watson-data-lab/pixiedust/issues
,或者更好的是,提交一个包含代码更改的拉取请求(关于如何做这件事的更多信息可以在这里找到:help.github.com/articles/creating-a-pull-request
)。
为了避免每次单元格运行时重新配置图表,PixieDust 将图表选项存储为 JSON 对象在单元格元数据中,并最终保存到 Notebook 中。你可以通过选择View | Cell Toolbar | Edit Metadata菜单手动检查这些数据,如下所示的截图:
显示编辑元数据按钮
一个Edit Metadata按钮将显示在单元格顶部,点击该按钮后会显示 PixieDust 的配置:
编辑单元格元数据对话框
当我们在下一节讨论 PixieApps 时,这个 JSON 配置将变得非常重要。
过滤
为了更好地探索数据,PixieDust 还提供了一个内置的简单图形界面,可以让你快速筛选正在可视化的数据。你可以通过点击顶级菜单中的筛选切换按钮快速调出筛选器。为了简化操作,筛选器只支持基于单一列构建谓词,这在大多数情况下足以验证简单假设(根据反馈,未来可能会增强此功能,支持多个谓词)。筛选器的 UI 会自动让你选择要筛选的列,并根据其类型显示不同的选项:
-
数值类型:用户可以选择一个数学比较符并输入操作数的值。为了方便,UI 还会显示与所选列相关的统计值,这些值可以在选择操作数时使用:
对汽车数据集中的 mpg 数值列进行筛选
-
字符串类型:用户可以输入一个表达式来匹配列值,可以是正则表达式或普通字符串。为了方便,UI 还会显示有关如何构建正则表达式的基本帮助:
对汽车数据集中的 name 字符串类型列进行筛选
点击 应用 按钮时,当前的可视化将更新,以反映筛选器的配置。需要注意的是,筛选器适用于整个单元格,而不仅仅是当前的可视化。因此,在切换图表类型时,筛选器仍然会继续应用。筛选器配置也会保存在单元格元数据中,因此在保存笔记本并重新运行单元格时,筛选器配置会被保留。
例如,以下截图将 cars
数据集可视化为一个柱状图,显示 mpg
大于 23
的行,根据统计框,23 是数据集的均值,并按年份聚集。在选项对话框中,我们选择 mpg
列作为键,origin
作为值:
筛选后的汽车数据集柱状图
总结一下,在这一节中,我们讨论了 PixieDust 如何帮助解决三项复杂且耗时的数据科学任务:数据加载、数据清洗和数据可视化。接下来,我们将看到 PixieDust 如何帮助增强数据科学家与开发人员之间的协作。
使用 PixieApps 弥合开发人员和数据科学家之间的鸿沟
解决难度较大的数据问题只是数据科学团队任务的一部分。他们还需要确保数据科学结果能正确地投入实际应用,为组织创造商业价值。数据分析的操作化非常依赖于使用案例。例如,这可能意味着创建一个仪表板,用于为决策者整合见解,或者将一个机器学习模型(如推荐引擎)集成到一个网页应用中。
在大多数情况下,这就是数据科学与软件工程相遇的地方(或者有人会说,关键时刻)。团队之间的持续合作——而不是一次性的交接——是任务成功完成的关键。往往他们还需要应对不同的编程语言和平台,导致软件工程团队进行大量的代码重写。
我们在Twitter 标签情感分析项目中亲身体验到了这一点,当时我们需要构建一个实时仪表盘来可视化结果。数据分析部分是用 Python 编写的,使用了 pandas、Apache Spark 和一些绘图库,如 Matplotlib 和 Bokeh,而仪表盘则是用 Node.js(nodejs.org
)和 D3(d3js.org
)编写的。
我们还需要在分析和仪表盘之间构建数据接口,而且由于我们需要系统是实时的,我们选择使用 Apache Kafka 来流式传输格式化的分析结果事件。
以下图示概括了一种我称之为交接模式的方法,在这种模式下,数据科学团队构建分析并将结果部署在数据接口层中。然后,应用程序将使用这些结果。数据层通常由数据工程师处理,这也是我们在第一章中讨论的角色之一,编程与数据科学——一套新工具:
数据科学与工程之间的交接
这种交接模式的问题在于,它不利于快速迭代。数据层的任何更改都需要与软件工程团队进行同步,以避免破坏应用程序。PixieApps 的理念是,在构建应用程序的同时,尽量保持与数据科学环境的接近,而在我们的案例中,数据科学环境就是 Jupyter Notebook。通过这种方式,分析结果直接从 PixieApp 中调用,PixieApp 嵌入在 Jupyter Notebook 中运行,因此数据科学家和开发人员可以轻松合作并迭代,从而实现快速改进。
PixieApp 定义了一个简单的编程模型,用于构建具有直接访问 IPython Notebook 内核(即运行 Notebook 代码的 Python 后台进程)的单页面应用程序。本质上,PixieApp 是一个 Python 类,封装了展示和业务逻辑。展示部分由一组特殊的方法组成,称为路由,这些方法返回任意的 HTML 片段。每个 PixieApp 都有一个默认路由,返回启动页面的 HTML 片段。开发人员可以使用自定义 HTML 属性来调用其他路由并动态更新页面的全部或部分内容。例如,一个路由可以调用一个从 Notebook 中创建的机器学习算法,或者使用 PixieDust 显示框架生成图表。
以下图示展示了 PixieApps 如何与 Jupyter Notebook 客户端前端和 IPython 内核交互的高层架构:
PixieApp 与 Jupyter 内核的交互
作为 PixieApp 外观的预览,下面是一个hello world示例应用程序,具有一个按钮,显示我们在前一节中创建的汽车 DataFrame 的条形图:
#import the pixieapp decorators
from pixiedust.display.app import *
#Load the cars dataframe into the Notebook
cars = pixiedust.sampleData(1)
@PixieApp #decorator for making the class a PixieApp
class HelloWorldApp():
#decorator for making a method a
#route (no arguments means default route)
@route()
def main_screen(self):
return """
<button type="submit" pd_options="show_chart=true" pd_target="chart">Show Chart</button>
<!--Placeholder div to display the chart-->
<div id="chart"></div>
"""
@route(show_chart="true")
def chart(self):
#Return a div bound to the cars dataframe
#using the pd_entity attribute
#pd_entity can refer a class variable or
#a global variable scoped to the notebook
return """
<div pd_render_onload pd_entity="cars">
<pd_options>
{
"title": "Average Mileage by Horsepower",
"aggregation": "AVG",
"clusterby": "origin",
"handlerId": "barChart",
"valueFields": "mpg",
"rendererId": "bokeh",
"keyFields": "horsepower"
}
</pd_options>
</div>
"""
#Instantiate the application and run it
app = HelloWorldApp()
app.run()
当上面的代码在 Notebook 单元格中运行时,我们会得到以下结果:
Hello World PixieApp
你可能对上面的代码有很多疑问,但不用担心。在接下来的章节中,我们将涵盖所有关于 PixieApp 的技术细节,包括如何在端到端的管道中使用它们。
操作化数据科学分析的架构
在前一节中,我们展示了 PixieApps 与 PixieDust 显示框架结合,提供了一种便捷的方法来构建强大的仪表板,直接连接到您的数据分析,允许算法和用户界面之间的快速迭代。这非常适合快速原型设计,但 Notebook 不适合用于生产环境,其中目标用户是业务线用户。一个明显的解决方案是使用传统的三层 Web 应用架构重写 PixieApp,例如,如下所示:
-
用于展示层的 React (
reactjs.org
) -
Web 层的 Node.js
-
一个面向机器学习评分或运行其他分析任务的 Web 分析层的数据访问库
然而,这只会比现有流程提供微小的改进,在这种情况下,现有流程仅包含通过 PixieApp 进行迭代实现的能力。
一个更好的解决方案是直接将 PixieApps 作为 Web 应用进行部署和运行,包括将分析嵌入周围的 Notebook 中,并且在此过程中,无需任何代码更改。
使用这种模型,Jupyter Notebooks 将成为简化开发生命周期的核心工具,如下图所示:
数据科学流水线开发生命周期
-
数据科学家使用 Python Notebook 来加载、丰富和分析数据,并创建分析(机器学习模型、统计分析等)
-
在同一个 Notebook 中,开发人员创建 PixieApp 来实现这些分析。
-
一旦准备好,开发人员将 PixieApp 发布为 Web 应用,业务用户可以轻松地通过交互方式使用它,而无需访问 Notebooks。
PixieDust 提供了一个实现该解决方案的组件 PixieGateway。PixieGateway 是一个 Web 应用服务器,负责加载和运行 PixieApps。它建立在 Jupyter 内核网关之上(github.com/jupyter/kernel_gateway
),而 Jupyter 内核网关本身是建立在 Tornado Web 框架之上的,因此遵循如下所示的架构:
PixieGateway 架构图
-
PixieApp 直接从 Notebook 发布到 PixieGateway 服务器,并生成一个 URL。在后台,PixieGateway 为 PixieApp 分配一个 Jupyter 内核来运行。根据配置,PixieApp 可以与其他应用共享内核实例,或者根据需求拥有专用内核。PixieGateway 中间件可以通过管理多个内核实例的生命周期进行横向扩展,这些内核实例可以是本地服务器上的,或者是集群上的远程内核。
注意
注意:远程内核必须是 Jupyter 内核网关。
使用发布向导,用户可以选择性地为应用定义安全性。提供多种选项,包括基本认证、OAuth 2.0 和 Bearer Token。
-
业务用户通过浏览器使用第一步生成的 URL 访问应用。
-
PixieGateway 提供了一个全面的管理控制台,用于管理服务器,包括配置应用程序、配置和监控内核、访问日志进行故障排除等。
-
PixieGateway 为每个活跃用户管理会话,并使用 IPython 消息协议(
jupyter-client.readthedocs.io/en/latest/messaging.html
)通过 WebSocket 或 ZeroMQ 调度请求到相应的内核进行执行,具体取决于内核是本地的还是远程的。
在将分析产品化时,这种解决方案相比经典的三层 Web 应用架构提供了显著的改进,因为它将 Web 层和数据层合并为一个Web 分析层,如下图所示:
经典三层与 PixieGateway 网络架构的比较
在经典的三层架构中,开发人员必须维护多个 REST 接口,这些接口调用数据层的分析功能,并对数据进行处理,以满足展示层的要求,从而正确显示数据。因此,必须在这些接口中添加大量工程工作,增加了开发和代码维护的成本。相比之下,在 PixieGateway 的两层架构中,开发人员不需要担心创建接口,因为服务器负责通过内置的通用接口将请求分发到适当的内核。换句话说,PixieApp 的 Python 方法会自动成为展示层的接口,而无需任何代码更改。这种模型有利于快速迭代,因为 Python 代码的任何变化都能在重新发布后直接反映到应用程序中。
PixieApps 非常适合快速构建单页面应用和仪表板。然而,你可能还希望生成更简单的一页报告并与用户分享。为此,PixieGateway 还允许你通过 共享 按钮共享由 display()
API 生成的图表,生成一个链接到包含图表的网页的 URL。反过来,用户可以通过复制并粘贴为该页面生成的代码,将图表嵌入到网站或博客文章中。
注释
注:我们将在第四章中详细介绍 PixieGateway,将数据分析发布到 Web - PixieApp 工具,包括如何在本地和云端安装新实例。
为了演示此功能,我们使用之前创建的车辆 DataFrame:
分享图表对话框
如果共享成功,下一页将显示生成的 URL 和嵌入到网页或博客文章中的代码片段:
确认共享图表
点击链接将会带你到该页面:
将图表显示为网页
总结
在本章中,我们讨论了为什么我们的数据科学工具策略以 Python 和 Jupyter Notebook 为中心。我们还介绍了 PixieDust 的功能,通过以下特点提高用户生产力:
-
数据加载与清洗
-
无需编码即可进行数据可视化和探索
-
一个基于 HTML 和 CSS 的简单编程模型,称为 PixieApp,用于构建与 Notebook 直接交互的工具和仪表板
-
一种点选机制,将图表和 PixieApp 直接发布到网页
在下一章中,我们将深入探讨 PixieApp 编程模型,讨论 API 的各个方面,并附上大量代码示例。
第三章:使用 Python 库加速数据分析
“每一个愿景都是笑话,直到第一个人实现它;一旦实现,它就变得平凡。”
– 罗伯特·戈达德
在本章中,我们将深入探讨 PixieApp 框架的技术细节。你将能够将以下信息既作为入门教程,也作为 PixieApp 编程模型的参考文档。
我们将从 PixieApp 的基本结构入手,然后再深入探讨其基础概念,如路由和请求。为了帮助大家跟进,我们将逐步构建一个GitHub 跟踪示例应用程序,应用介绍的功能和最佳实践,从构建数据分析开始,到将其集成到 PixieApp 中。
到本章结束时,你应该能够将学到的知识应用到自己的实际案例中,包括编写自己的 PixieApp。
PixieApp 的结构
注意
注意:PixieApp 编程模型不要求有 JavaScript 的先验经验,但期望读者熟悉以下内容:
-
Python (
www.python.org
) -
HTML5 (
www.w3schools.com/html
) -
CSS3 (
www.w3schools.com/css
)
PixieApp一词代表Pixie 应用程序,旨在强调其与 PixieDust 功能的紧密集成,特别是display()
API。其主要目标是使开发者能够轻松构建可以调用 Jupyter Notebook 中实现的数据分析的用户界面。
一个 PixieApp 遵循单页应用程序(SPA)设计模式 (en.wikipedia.org/wiki/Single-page_application
),用户会看到一个欢迎页面,并根据用户的交互动态更新。更新可以是部分刷新,例如用户点击控件后更新图表,或是完全刷新,比如在多步骤过程中显示新页面。在每种情况下,更新都由服务器端的路由控制,路由通过特定机制触发,我们将在后面讨论。当触发时,路由会执行代码处理请求,然后返回一个 HTML 片段,该片段会应用到客户端的目标 DOM 元素上 (www.w3schools.com/js/js_htmldom.asp
)。
以下时序图展示了在运行 PixieApp 时,客户端和服务器端是如何相互交互的:
显示 PixieApp 信息流的时序图
当 PixieApp 启动时(通过调用run
方法),默认路由会被调用,并返回相应的 HTML 片段。当用户与应用交互时,会执行更多请求,触发关联的路由,并相应地刷新 UI。
从实现的角度来看,PixieApp 只是一个常规的 Python 类,已经使用了@PixieApp
装饰器。在幕后,PixieApp
装饰器为类添加了运行应用所需的方法和字段,例如 run
方法。
注意
更多关于 Python 装饰器的信息可以在这里找到:
wiki.python.org/moin/PythonDecorators
为了开始,以下代码展示了一个简单的 Hello World PixieApp:
#import the pixieapp decorators
from pixiedust.display.app import *
@PixieApp #decorator for making the class a PixieApp
class HelloWorldApp():
@route() #decorator for making a method a route (no arguments means default route)
def main_screen(self):
return """<div>Hello World</div>"""
#Instantiate the application and run it
app = HelloWorldApp()
app.run()
注意
你可以在这里找到代码:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode1.py
上述代码展示了 PixieApp 的结构,如何定义路由,以及如何实例化并运行应用。由于 PixieApps 是常规的 Python 类,因此它们可以继承其他类,包括其他 PixieApp,这对于大型项目来说,能够使代码模块化和可重用。
路由
路由用于动态更新客户端屏幕的全部或部分内容。可以通过在任何类方法上使用@route
装饰器来轻松定义路由,依据以下规则:
-
路由方法需要返回一个字符串,表示用于更新的 HTML 片段。
注意
注意:在片段中允许使用 CSS 和 JavaScript。
-
@route
装饰器可以有一个或多个关键字参数,这些参数必须是字符串类型。可以将这些关键字参数视为请求参数,PixieApp 框架内部使用它们将请求分发到最匹配的路由,依据以下规则:-
参数最多的路由总是会被优先评估。
-
所有参数必须匹配,才能选择一个路由。
-
如果未找到路由,则会选择默认路由作为备用。
-
路由可以使用通配符
*
进行配置,在这种情况下,任何状态参数的值都会匹配。以下是一个示例:
@route(state1="value1", state2="value2")
-
-
PixieApp 必须有且仅有一个默认路由,即没有参数的路由,即
@route()
。
配置路由时避免冲突非常重要,特别是当你的应用有层次化状态时。例如,关联 state1="load"
的路由可能负责加载数据,然后关联 (state1="load", state2="graph")
的第二个路由可能负责绘制数据。在这种情况下,带有 state1
和 state2
两个指定参数的请求将匹配第二个路由,因为路由评估是从最具体到最不具体的顺序进行的,直到找到第一个匹配的路由为止。
为了更清楚,以下图示展示了请求与路由是如何匹配的:
请求与路由的匹配
定义为路由的方法的预期合同是返回一个 HTML 片段,其中可以包含 Jinja2 模板构造。Jinja2 是一个强大的 Python 模板引擎,提供了一套丰富的功能来动态生成文本,包括访问 Python 变量、方法和控制结构,如 if...else
、for
循环等。覆盖所有功能超出了本书的范围,但我们将讨论一些常用的重要构造:
注意
注意:如果你想了解更多关于 Jinja2 的内容,可以在这里阅读完整文档:
jinja.pocoo.org/docs/templates
-
变量:你可以使用双花括号来访问作用域中的变量,例如,
"<div>这是我的变量 {{my_var}}</div>"
。在渲染时,my_var
变量将被其实际值替换。你还可以使用.
(点)符号来访问复杂对象,例如,"<div>这是一个嵌套的值 {{my_var.sub_value}}</div>"
。 -
for 循环:你可以使用
{%for ...%}...{%endfor%}
语法来通过迭代一系列项目(如列表、元组、字典等)动态生成文本,如下例所示:{%for message in messages%} <li>{{message}}</li> {%endfor%}
-
if 语句:你可以使用
{%if ...%}...{%elif ...%}...{%else%}…{%endif%}
语法来有条件地输出文本,如下例所示:{%if status.error%} <div class="error">{{status.error}}</div> {%elif status.warning%} <div class="warning">{{status.warning}}</div> {%else%} <div class="ok">{{status.message}}</div> {%endif%}
了解变量和方法如何进入 Jinja2 模板字符串的作用域也非常重要。PixieApp 会自动提供三种类型的变量和方法供你访问:
-
类变量和方法:可以使用
this
关键字访问这些内容。注意
注意:我们没有使用更符合 Python 风格的
self
关键字,因为不幸的是,Jinja2 本身已经占用了这个关键字。 -
方法参数:当路由参数使用
*
值,并且你希望在运行时访问该值时,这个功能非常有用。在这种情况下,你可以在方法中添加与路由参数中定义的名称相同的参数,PixieApp 框架会自动传递正确的值。注意
注意:参数的顺序实际上无关紧要。你也不必使用路由中定义的每个参数,这样如果你只关心使用其中的一部分参数会更加方便。
该变量也会在 Jinja2 模板字符串的作用域内,如下例所示:
@route(state1="*", state2="*") def my_method(self, state1, state2): return "<div>State1 is {{state1}}. State2 is {{state2}}</div>"
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode2.py
-
方法的局部变量:PixieApp 将自动把在方法中定义的所有局部变量放入 Jinja2 模板字符串的作用域中,前提是你在方法中添加了
@templateArgs
装饰器,如下例所示:@route() @templateArgs def main_screen(self): var1 = self.compute_something() var2 = self.compute_something_else() return "<div>var1 is {{var1}}. var2 is {{var2}}</div>"
注意
你可以在这里找到代码:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode3.py
生成请求到路由
如前所述,PixieApp 遵循 SPA 设计模式。在第一次加载屏幕后,所有与服务器的后续交互都是通过动态请求进行的,而不是像多页面 Web 应用程序那样使用 URL 链接。有三种方法可以生成到路由的内核请求:
-
使用
pd_options
自定义属性定义要传递给服务器的状态列表,如下例所示:pd_options="state1=value1;state2=value2;..;staten=valuen"
-
如果你已经有一个包含
pd_options
值的 JSON 对象——例如在调用display()
时——你需要将其转换为pd_options
HTML 属性所期望的格式,这可能会非常耗时。在这种情况下,更方便的方法是将pd_options
指定为子元素,这样就能直接将选项作为 JSON 对象传递(避免了转换数据的额外工作),如下例所示:<div> <pd_options> {"state1":"value1","state2":"value2",..., "staten":"valuen"} </pd_options> </div>
-
通过调用
invoke_route
方法以编程方式,如下例所示:self.invoke_route(self.route_method, state1='value1', state2='value2')
注意
注意:记得使用this
,而不是self
,如果你是从 Jinja2 模板字符串中调用此方法,因为self
已经被 Jinja2 本身使用。
当pd_options
中传递的状态值需要根据用户选择动态计算时,你需要使用$val(arg)
特殊指令,该指令作为宏,在内核请求执行时解析。
$val(arg)
指令接受一个参数,该参数可以是以下之一:
-
页面上 HTML 元素的 ID,例如输入框或组合框,如下例所示:
<div> <pd_options> {"state1":"$val(my_element_id)","state2":"value2"} <pd_options> </div>
-
一个必须返回期望值的 JavaScript 函数,如下例所示:
<script> function resValue(){ return "my_query"; } </script> ... <div pd_options="state1=$val(resValue)"></div>
注意
注意:使用$val
指令的动态值被大多数 PixieDust 自定义属性支持。
一个 GitHub 项目追踪示例应用程序
让我们将迄今为止学到的内容应用到实现示例应用程序中。为了试验,我们将使用 GitHub Rest API (developer.github.com/v3
) 来搜索项目并将结果加载到 pandas DataFrame 中进行分析。
初始代码显示欢迎界面,包含一个简单的输入框用于输入 GitHub 查询,并有一个按钮提交请求:
from pixiedust.display.app import *
@PixieApp
class GitHubTracking():
@route()
def main_screen(self):
return """
<style>
div.outer-wrapper {
display: table;width:100%;height:300px;
}
div.inner-wrapper {
display: table-cell;vertical-align: middle;height: 100%;width: 100%;
}
</style>
<div class="outer-wrapper">
<div class="inner-wrapper">
<div class="col-sm-3"></div>
<div class="input-group col-sm-6">
<input id="query{{prefix}}" type="text" class="form-control" placeholder="Search projects on GitHub">
<span class="input-group-btn">
<button class="btn btn-default" type="button">Submit Query</button>
</span>
</div>
</div>
</div>
"""
app = GitHubTracking()
app.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode4.py
从上面的代码中有几点需要注意:
-
Bootstrap CSS 框架 (
getbootstrap.com/docs/3.3
) 和 jQuery JS 框架 (jquery.com
) 已由 Jupyter Notebook 提供。我们可以直接在代码中使用它们,无需安装。 -
Font Awesome 图标 (
fontawesome.com
) 在 Notebook 中默认可用。 -
PixieApp 代码可以在 Notebook 的多个单元格中执行。由于我们依赖于 DOM 元素 ID,因此确保两个元素没有相同的 ID 是很重要的,否则会导致不希望出现的副作用。为此,建议始终包括由 PixieDust 框架提供的唯一标识符
{{prefix}}
,例如"query{{prefix}}"
。
结果显示在以下截图中:
我们 GitHub 跟踪应用的欢迎界面
下一步是创建一个新路由,接收用户输入并返回结果。此路由将通过 提交查询 按钮调用。
为了简化,以下代码没有使用与 GitHub 交互的 Python 库,如 PyGithub (pygithub.readthedocs.io/en/latest
),而是直接调用 GitHub 网站中文档化的 REST API:
注意
注意:当你看到以下符号 [[GitHubTracking]]
时,表示该代码应添加到 GitHubTracking
PixieApp 类中,为了避免重复代码,已省略周围的代码。在有疑问时,你可以参考本节末尾指定的完整 Notebook。
import requests
import pandas
[[GitHubTracking]]
@route(query="*")
@templateArgs
def do_search(self, query):
response = requests.get( "https://api.github.com/search/repositories?q={}".format(query))
frames = [pandas.DataFrame(response.json()['items'])]
while response.ok and "next" in response.links:
response = requests.get(response.links['next']['url'])
frames.append(pandas.DataFrame(response.json()['items']))
pdf = pandas.concat(frames)
response = requests.get( "https://api.github.com/search/repositories?q={}".format(query))
if not response.ok:
return "<div>An Error occurred: {{response.text}}</div>"
return """<h1><center>{{pdf|length}} repositories were found</center></h1>"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode5.py
在上述代码中,我们创建了一个名为do_search
的路由,它接受一个名为query
的参数,我们用这个参数来构建 GitHub 的 API URL。使用requests
Python 模块(docs.python-requests.org
)向这个 URL 发出 GET 请求,我们得到一个 JSON 数据包,并将其转换为一个 pandas DataFrame。根据 GitHub 文档,搜索 API 会进行分页,下一页的链接会存储在响应头中。代码使用while
循环遍历每个链接,并将下一页加载到新的 DataFrame 中。然后,我们将所有的 DataFrame 合并为一个名为pdf
的 DataFrame。接下来,我们只需构建一个 HTML 片段来显示结果。该片段使用 Jinja2 语法{{...}}
来访问定义为局部变量的pdf
,这只因为我们在do_search
方法中使用了@templateArgs
装饰器。请注意,我们还使用了一个 Jinja2 过滤器length
来显示找到的仓库数量:{{pdf|length}}
。
注意
有关过滤器的更多信息,请访问以下链接:
jinja.pocoo.org/docs/templates/#filters
当用户点击提交查询按钮时,我们仍然需要调用do_search
路由。为此,我们在<button>
元素中添加pd_options
属性,如下所示:
<div class="input-group col-sm-6">
<input id="query{{prefix}}" type="text"
class="form-control"
placeholder="Search projects on GitHub">
<span class="input-group-btn">
<button class="btn btn-default" type="button" pd_options="query=$val(query{{prefix}})">
Submit Query
</button>
</span>
</div>
我们在pd_options
属性中使用了$val()
指令,动态获取 ID 为"query{{prefix}}"
的输入框的值,并将其存储在query
参数中。
在表格中显示搜索结果
上述代码一次性加载所有数据,这并不推荐,因为我们可能会有大量数据。类似地,一次性展示所有数据会导致界面变得缓慢且不实用。幸运的是,我们可以通过以下步骤轻松构建一个分页表格,而无需太多努力:
-
创建一个名为
do_retrieve_page
的路由,接受一个 URL 作为参数,并返回表格主体的 HTML 片段 -
将
first
、previous
、next
和last
的 URL 作为字段保存在 PixieApp 类中 -
创建一个分页控件(我们将使用 Bootstrap,因为它是现成的),包括
First
、Prev
、Next
和Last
按钮 -
创建一个表格占位符,显示需要显示的列标题
现在,我们将更新do_search
方法,如下所示:
注意
注意:以下代码引用了do_retrieve_page
方法,我们稍后会定义它。在你添加do_retrieve_page
方法之前,请不要尝试运行此代码。
[[GitHubTracking]]
@route(query="*")
@templateArgs
def do_search(self, query):
self.first_url = "https://api.github.com/search/repositories?q={}".format(query)
self.prev_url = None
self.next_url = None
self.last_url = None
response = requests.get(self.first_url)
if not response.ok:
return "<div>An Error occurred: {{response.text}}</div>"
total_count = response.json()['total_count']
self.next_url = response.links.get('next', {}).get('url', None)
self.last_url = response.links.get('last', {}).get('url', None)
return """
<h1><center>{{total_count}} repositories were found</center></h1>
<ul class="pagination">
<li><a href="#" pd_options="page=first_url" pd_target="body{{prefix}}">First</a></li>
<li><a href="#" pd_options="page=prev_url" pd_target="body{{prefix}}">Prev</a></li>
<li><a href="#" pd_options="page=next_url" pd_target="body{{prefix}}">Next</a></li>
<li><a href="#" pd_options="page=last_url" pd_target="body{{prefix}}">Last</a></li>
</ul>
<table class="table">
<thead>
<tr>
<th>Repo Name</th>
<th>Lastname</th>
<th>URL</th>
<th>Stars</th>
</tr>
</thead>
<tbody id="body{{prefix}}">
{{this.invoke_route(this.do_retrieve_page, page='first_url')}}
</tbody>
</table>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode6.py
上面的代码示例展示了 PixieApps 的一个非常重要的特性,那就是你可以通过简单地将数据存储到类变量中来保持应用程序生命周期中的状态。在这个例子中,我们使用了 self.first_url
、self.prev_url
、self.next_url
和 self.last_url
变量。这些变量为每个分页控件中的按钮使用 pd_options
属性,并在每次调用 do_retrieve_page
路由时更新。do_search
返回的片段现在返回一个表格,其中包含一个由 body{{prefix}}
标识的占位符,成为每个按钮的 pd_target
。我们还使用 invoke_route
方法来确保在首次显示表格时获取第一页。
我们之前看到,路由返回的 HTML 片段用于替换整个页面,但在前面的代码中,我们使用了 pd_target="body{{prefix}}"
属性来表示 HTML 片段将被注入到具有 body{{prefix}}
ID 的表格主体元素中。如果需要,你还可以通过创建一个或多个 <target>
元素作为可点击源元素的子元素,来定义多个目标以响应用户操作。每个 <target>
元素本身可以使用所有 PixieApp 自定义属性来配置内核请求。
这是一个示例:
<button type="button">Multiple Targets
<target pd_target="elementid1" pd_options="state1=value1"></target>
<target pd_target="elementid2" pd_options="state2=value2"></target>
</button>
回到我们的 GitHub 示例应用程序,do_retrieve_page
方法现在看起来像这样:
[[GitHubTracking]]
@route(page="*")
@templateArgs
def do_retrieve_page(self, page):
url = getattr(self, page)
if url is None:
return "<div>No more rows</div>"
response = requests.get(url)
self.prev_url = response.links.get('prev', {}).get('url', None)
self.next_url = response.links.get('next', {}).get('url', None)
items = response.json()['items']
return """
{%for row in items%}
<tr>
<td>{{row['name']}}</td>
<td>{{row.get('owner',{}).get('login', 'N/A')}}</td>
<td><a href="{{row['html_url']}}" target="_blank">{{row['html_url']}}</a></td>
<td>{{row['stargazers_count']}}</td>
</tr>
{%endfor%}
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode7.py
page
参数是一个字符串,包含我们想要显示的 url
类变量的名称。我们使用标准的 getattr
Python 函数 (docs.python.org/2/library/functions.html#getattr
) 来从页面中获取 url
值。然后,我们对 GitHub API 的 url
发出 GET 请求,获取以 JSON 格式返回的有效载荷,并将其传递给 Jinja2 模板,以生成将注入到表格中的一组行。为此,我们使用 Jinja2 中的 {%for…%}
循环控制结构 (jinja.pocoo.org/docs/templates/#for
) 来生成一系列 <tr>
和 <td>
HTML 标签。
以下截图展示了查询 pixiedust
的搜索结果:
显示来自查询的 GitHub 仓库列表的屏幕
注意
在第一部分中,我们展示了如何创建 GitHubTracking
PixieApp,调用 GitHub 查询 REST API,并使用分页在表格中显示结果。你可以在这里找到包含源代码的完整笔记本:
https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/GitHub%20Tracking%20Application/GitHub%20Sample%20Application%20-%20Part%201.ipynb
在接下来的部分,我们将探索更多 PixieApp 的功能,这些功能将帮助我们通过让用户深入某个特定的仓库,并可视化该仓库的各种统计数据,来改进应用程序。
第一步是为搜索结果表格的每一行添加一个按钮,该按钮触发一个新路由,用于可视化所选仓库的统计信息。
以下代码是 do_search
函数的一部分,并在表头中添加了一个新列:
<thead>
<tr>
<th>Repo Name</th>
<th>Lastname</th>
<th>URL</th>
<th>Stars</th>
<th>Actions</th>
</tr>
</thead>
为了完成表格,我们更新了 do_retrieve_page
方法,添加了一个新单元格,其中包含一个 <button>
元素,带有与新路由匹配的 pd_options
参数:analyse_repo_owner
和 analyse_repo_name
。这些参数的值是从用于遍历从 GitHub 请求中接收到的有效负载的 row
元素中提取的:
{%for row in items%}
<tr>
<td>{{row['name']}}</td>
<td>{{row.get('owner',{}).get('login', 'N/A')}}</td>
<td><a href="{{row['html_url']}}" target="_blank">{{row['html_url']}}</a></td>
<td>{{row['stargazers_count']}}</td>
<td>
<button pd_options=
"analyse_repo_owner={{row["owner"]["login"]}};
analyse_repo_name={{row['name']}}"
class="btn btn-default btn-sm" title="Analyze Repo">
<i class="fa fa-line-chart"></i>
</button>
</td>
</tr>
{%endfor%}
在这个简单的代码更改完成后,通过重新运行单元格来重启 PixieApp,现在我们可以看到每个仓库的按钮,尽管我们还没有实现相应的路由,接下来我们将实现该路由。提醒一下,当没有找到匹配的路由时,默认路由将被触发。
以下截图展示了添加按钮后的表格:
为每一行添加操作按钮
下一步是创建与仓库可视化页面关联的路由。该页面的设计相当简单:用户从下拉框中选择他们想要在页面上可视化的数据类型。GitHub REST API 提供了多种类型的数据访问,但对于这个示例应用程序,我们将使用提交活动数据,这是统计类别的一部分(请参阅 developer.github.com/v3/repos/statistics/#get-the-last-year-of-commit-activity-data
以获取此 API 的详细说明)。
提示
作为练习,您可以通过添加其他类型 API 的可视化效果来改进这个示例应用程序,例如流量 API(developer.github.com/v3/repos/traffic
)。
同时需要注意的是,尽管大多数 GitHub API 可以在没有认证的情况下工作,但如果未提供凭证,服务器可能会限制响应。要进行请求认证,您需要使用 GitHub 密码或通过在 GitHub 设置页面中选择开发者设置菜单,点击个人访问令牌菜单,然后点击生成新令牌按钮来生成个人访问令牌。
在一个单独的 Notebook 单元格中,我们将为 GitHub 用户 ID 和令牌创建两个变量:
github_user = "dtaieb"
github_token = "XXXXXXXXXX"
这些变量稍后将在请求认证中使用。请注意,尽管这些变量是在各自的单元格中创建的,但它们对整个 Notebook 可见,包括 PixieApp 代码。
为了提供良好的代码模块化和复用性,我们将在一个新类中实现 Repo Visualization 页面,并让主 PixieApp 类继承该类并自动复用它的路由。这是一个模式,当你开始处理大型项目并希望将其拆分为多个类时,需要记住。
Repo Visualization 页面的主路由返回一个包含下拉菜单和 <div>
占位符的 HTML 片段。下拉菜单使用 Bootstrap dropdown
类创建(www.w3schools.com/bootstrap/bootstrap_dropdowns.asp
)。为了使代码更易于维护,菜单项是通过 Jinja2 {%for..
%}
循环生成的,该循环遍历一个包含元组的数组(docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences
),数组名为 analyses
,它包含一个描述和一个用于将数据加载到 pandas DataFrame 中的函数。这里,我们再次在自己的单元格中创建这个数组,并将在 PixieApp 类中引用它:
analyses = [("Commit Activity", load_commit_activity)]
注意
注意:load_commit_activity
函数将在本节后面讨论。
对于这个示例应用程序,数组仅包含一个与提交活动相关的元素,但将来你添加的任何元素都会自动被 UI 捕捉到。
do_analyse_repo
路由有两个参数:analyse_repo_owner
和 analyse_repo_name
,这些应该足以访问 GitHub APIs。我们还需要将这些参数保存为类变量,因为它们将在生成可视化的路由中使用:
@PixieApp
class RepoAnalysis():
@route(analyse_repo_owner="*", analyse_repo_name="*")
@templateArgs
def do_analyse_repo(self, analyse_repo_owner, analyse_repo_name):
self._analyse_repo_owner = analyse_repo_owner
self._analyse_repo_name = analyse_repo_name
return """
<div class="container-fluid">
<div class="dropdown center-block col-sm-2">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
Select Repo Data Set
<span class="caret"></span>
</button>
<ul class="dropdown-menu" style="list-style:none;margin:0px;padding:0px">
{%for analysis,_ in this.analyses%}
<li>
<a href="#" pd_options="analyse_type={{analysis}}" pd_target="analyse_vis{{prefix}}"
style="text-decoration: none;background-color:transparent">
{{analysis}}
</a>
</li>
{%endfor%}
</ul>
</div>
<div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
</div>
"""
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode8.py
注意
上述代码中需要注意两件事:
-
Jinja2 模板使用
this
关键字引用analyses
数组,尽管analyses
变量并未定义为类变量。之所以能够这样工作,是因为 PixieApp 的另一个重要特性:在 Notebook 中定义的任何变量都可以像类变量一样引用。 -
我将
analyse_repo_owner
和analyse_repo_name
作为类变量存储,并使用不同的名称,例如_analyse_repo_owner
和_analyse_repo_name
。这很重要,因为使用相同的名称会对路由匹配算法产生副作用,算法也会查看类变量来查找参数。使用相同的名称会导致该路由始终被找到,这显然不是我们想要的效果。
动作按钮链接由 <a>
标签定义,并使用 pd_options
访问一个包含名为 analyse_type
的参数的路由,此外 pd_target
指向同一 HTML 片段下方定义的 "analyse_vis{{prefix}}"
占位符 <div>
。
使用pd_entity
属性调用 PixieDust 的 display() API
当使用pd_options
属性创建内核请求时,PixieApp 框架会将当前的 PixieApp 类作为目标。但是,你可以通过指定pd_entity
属性来更改这个目标。例如,你可以指向另一个 PixieApp,或者更有趣的是,指向display()
API 支持的数据结构,比如 pandas 或 Spark DataFrame。在这种情况下,只要你包含display()
API 所期望的正确选项,生成的输出将是图表本身(在 Matplotlib 的情况下是图像,在 Mapbox 的情况下是 Iframe,在 Bokeh 的情况下是 SVG)。获取正确选项的一种简单方法是,在自己的单元格中调用display()
API,使用菜单配置所需的图表,然后复制通过点击编辑元数据按钮获得的单元格元数据 JSON 片段。(你可能需要先通过菜单视图 | 单元格工具栏 | 编辑元数据来启用该按钮)。
你也可以在不指定任何值的情况下指定pd_entity
。在这种情况下,PixieApp 框架将使用传递给run
方法的第一个参数作为实体,该方法用于启动 PixieApp 应用程序。例如,my_pixieapp.run(cars)
,其中cars
是通过pixiedust.sampleData()
方法创建的 pandas 或 Spark DataFrame。pd_entity
的值也可以是返回实体的函数调用。当你想在渲染实体之前动态计算实体时,这很有用。与其他变量一样,pd_entity
的作用范围可以是 PixieApp 类,也可以是 Notebook 中声明的任何变量。
例如,我们可以在一个单元格中创建一个函数,该函数以前缀作为参数并返回一个 pandas DataFrame。然后我们将其用作我的 PixieApp 中的pd_entity
值,如下代码所示:
def compute_pdf(key):
return pandas.DataFrame([
{"col{}".format(i): "{}{}-{}".format(key,i,j) for i in range(4)} for j in range(10)
])
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode9.py
在上述代码中,我们使用了 Python 列表推导式(docs.python.org/2/tutorial/datastructures.html#list-comprehensions
)来快速生成基于key
参数的模拟数据。
注意
Python 列表推导式是我最喜欢的 Python 语言特性之一,因为它让你能够以简洁而富有表现力的语法创建、转换和提取数据。
然后,我可以创建一个 PixieApp,使用compute_pdf
函数作为pd_entity
来将数据呈现为表格:
from pixiedust.display.app import *
@PixieApp
class TestEntity():
@route()
def main_screen(self):
return """
<h1><center>
Simple PixieApp with dynamically computed dataframe
</center></h1>
<div pd_entity="compute_pdf('prefix')" pd_options="handlerId=dataframe" pd_render_onload></div>
"""
test = TestEntity()
test.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode10.py
在前面的代码中,为了简便起见,我将键硬编码为'prefix'
,接下来留给您作为练习,使用输入控件和$val()
指令使其可以由用户定义。
另一个需要注意的重要事项是,在显示图表的 div 中使用了pd_render_onload
属性。这个属性告诉 PixieApp,在元素加载到浏览器 DOM 中后,立即执行由该元素定义的内核请求。
以下截图展示了前面 PixieApp 的结果:
在 PixieApp 中动态创建 DataFrame
回到我们的GitHub 跟踪应用程序,现在让我们将pd_entity
值应用到从 GitHub 统计 API 加载的 DataFrame 中。我们创建一个名为load_commit_activity
的方法,负责将数据加载到 pandas DataFrame 并返回它和显示图表所需的pd_options
:
from datetime import datetime
import requests
import pandas
def load_commit_activity(owner, repo_name):
response = requests.get(
"https://api.github.com/repos/{}/{}/stats/commit_activity".format(owner, repo_name),
auth=(github_user, github_token)
).json()
pdf = pandas.DataFrame([
{"total": item["total"], "week":datetime.fromtimestamp(item["week"])} for item in response
])
return {
"pdf":pdf,
"chart_options": {
"handlerId": "lineChart",
"keyFields": "week",
"valueFields": "total",
"aggregation": "SUM",
"rendererId": "bokeh"
}
}
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode11.py
前面的代码向 GitHub 发送了一个 GET 请求,并使用在笔记本开始时设置的github_user
和github_token
变量进行身份验证。响应是一个 JSON 负载,我们将使用它来创建一个 pandas DataFrame。在创建 DataFrame 之前,我们需要将 JSON 负载转换为正确的格式。目前,负载看起来像这样:
[
{"days":[0,0,0,0,0,0,0],"total":0,"week":1485046800},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1485651600},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1486256400},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1486861200}
...
]
我们需要删除days
键,因为它不需要用于显示图表,而且为了正确显示图表,我们需要将week
键的值(Unix 时间戳)转换为 Python 的datetime
对象。此转换通过 Python 列表推导和一行简单代码完成:
[{"total": item["total"], "week":datetime.fromtimestamp(item["week"])} for item in response]
在当前实现中,load_commit_activity
函数定义在它自己的单元格中,但我们也可以将其定义为 PixieApp 的成员方法。作为最佳实践,使用自己的单元格非常方便,因为我们可以对函数进行单元测试并快速迭代,而无需每次都运行整个应用程序,避免了额外的开销。
要获取pd_options
值,我们只需运行该函数,使用示例仓库信息,然后在单独的单元格中调用display()
API:
在单独的单元格中使用 display()获取可视化配置
要获取前面的图表,您需要选择折线图,然后在选项对话框中,将week
列拖到键框中,将total
列拖到值框中。您还需要选择 Bokeh 作为渲染器。完成后,注意到 PixieDust 会自动检测到x轴是一个日期时间,并相应地调整渲染。
使用编辑元数据按钮,我们现在可以复制图表选项的 JSON 片段:
捕获 display() JSON 配置
并将其返回到load_commit_activity
数据负载中:
return {
"pdf":pdf,
"chart_options": {
"handlerId": "lineChart",
"keyFields": "week",
"valueFields": "total",
"aggregation": "SUM",
"rendererId": "bokeh"
}
}
我们现在准备在RepoAnalysis
类中实现do_analyse_type
路由,如以下代码所示:
[[RepoAnalysis]]
@route(analyse_type="*")
@templateArgs
def do_analyse_type(self, analyse_type):
fn = [analysis_fn for a_type,analysis_fn in analyses if a_type == analyse_type]
if len(fn) == 0:
return "No loader function found for {{analyse_type}}"
vis_info = fn0
self.pdf = vis_info["pdf"]
return """
<div pd_entity="pdf" pd_render_onload>
<pd_options>{{vis_info["chart_options"] | tojson}}</pd_options>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode12.py
该路由有一个名为analyse_type
的参数,我们用它作为键来查找analyses
数组中的 load 函数(注意,我再次使用列表推导式来快速执行查找)。然后,我们调用这个函数,并传入仓库的所有者和名称,以获取vis_info
JSON 数据包,并将 pandas DataFrame 存储在一个名为pdf
的类变量中。返回的 HTML 片段将使用pdf
作为pd_entity
的值,并使用vis_info["chart_options"]
作为pd_options
。在这里,我使用了tojson
Jinja2 过滤器(jinja.pocoo.org/docs/templates/#list-of-builtin-filters
)以确保它在生成的 HTML 中被正确转义。尽管vis_info
变量在栈上声明,但由于我为该函数使用了@templateArgs
装饰器,因此仍然可以使用它。
在测试我们改进后的应用程序之前,最后一步是确保主GitHubTracking
PixieApp 类继承自RepoAnalysis
PixieApp:
@PixieApp
class GitHubTracking(RepoAnalysis):
@route()
def main_screen(self):
<<Code omitted here>>
@route(query="*")
@templateArgs
def do_search(self, query):
<<Code omitted here>>
@route(page="*")
@templateArgs
def do_retrieve_page(self, page):
<<Code omitted here>>
app = GitHubTracking()
app.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode13.py
下面显示的是 Repo 分析页面的截图:
GitHub 仓库提交活动可视化
注意
如果你想进一步实验,可以在这里找到完整的 Notebook,针对GitHub 跟踪应用程序第二部分:
使用 pd_script 调用任意 Python 代码
在这一部分,我们将查看pd_script
自定义属性,它可以让你在每次触发内核请求时运行任意的 Python 代码。执行 Python 代码时有几个规则需要遵循:
-
代码可以通过
self
关键字访问 PixieApp 类,以及 Notebook 中定义的任何变量、函数和类,如以下示例所示:<button type="submit" pd_script="self.state='value'">Click me</button>
-
如果指定了
pd_target
,则任何使用print
函数的语句都会输出到target
元素中。如果没有pd_target
,则不会这样做。换句话说,你不能使用pd_script
来进行页面的完全刷新(你需要使用pd_options
属性),例如以下示例:from pixiedust.display.app import * def call_me(): print("Hello from call_me") @PixieApp class Test(): @route() def main_screen(self): return """ <button type="submit" pd_script="call_me()" pd_target="target{{prefix}}">Click me</button> <div id="target{{prefix}}"></div> """ Test().run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode14.py
-
如果代码包含多行,建议使用
pd_script
子元素,这样可以使用多行编写 Python 代码。在使用这种形式时,确保代码遵循 Python 语言的缩进规则,如以下示例所示:@PixieApp class Test(): @route() def main_screen(self): return """ <button type="submit" pd_script="call_me()" pd_target="target{{prefix}}"> <pd_script> self.name="some value" print("This is a multi-line pd_script") </pd_script> Click me </button> <div id="target{{prefix}}"></div> """ Test().run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode15.py
pd_script
的一个常见使用场景是在触发内核请求之前更新服务器上的某些状态。让我们通过添加一个复选框,在我们的GitHub Tracking应用中切换数据的可视化方式,从线图切换为统计数据摘要。
在do_analyse_repo
返回的 HTML 片段中,我们添加了一个复选框元素,用于在图表和统计摘要之间切换:
[[RepoAnalysis]]
...
return """
<div class="container-fluid">
<div class="col-sm-2">
<div class="dropdown center-block">
<button class="btn btn-primary
dropdown-toggle" type="button"
data-toggle="dropdown">
Select Repo Data Set
<span class="caret"></span>
</button>
<ul class="dropdown-menu"
style="list-style:none;margin:0px;padding:0px">
{%for analysis,_ in this.analyses%}
<li>
<a href="#"
pd_options="analyse_type={{analysis}}"
pd_target="analyse_vis{{prefix}}"
style="text-decoration: none;background-color:transparent">
{{analysis}}
</a>
</li>
{%endfor%}
</ul>
</div>
<div class="checkbox">
<label>
<input id="show_stats{{prefix}}" type="checkbox"
pd_script="self.show_stats=('$val(show_stats{{prefix}})' == 'true')">
Show Statistics
</label>
</div>
</div>
<div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
</div>
"""
在checkbox
元素中,我们包含了一个pd_script
属性,该属性根据checkbox
元素的状态修改服务器上的变量状态。我们使用$val()
指令来获取show_stats_{{prefix}}
元素的值,并将其与true string
进行比较。当用户点击复选框时,状态会立即在服务器上改变,下一次用户点击菜单时,数据显示的是统计数据而不是图表。
现在我们需要更改do_analyse_type
路由,以动态配置pd_entity
和chart_options
:
[[RepoAnalysis]]
@route(analyse_type="*")
@templateArgs
def do_analyse_type(self, analyse_type):
fn = [analysis_fn for a_type,analysis_fn in analyses if a_type == analyse_type]
if len(fn) == 0:
return "No loader function found for {{analyse_type}}"
vis_info = fn0
self.pdf = vis_info["pdf"]
chart_options = {"handlerId":"dataframe"} if self.show_stats else vis_info["chart_options"]
return """
<div pd_entity="get_pdf()" pd_render_onload>
<pd_options>{{chart_options | tojson}}</pd_options>
</div>
"""
注意
你可以在这里找到文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode16.py
chart_options
现在是一个本地变量,包含了在show_stats
为true
时以表格形式显示的选项,以及在不为true
时作为常规折线图选项的显示方式。
pd_entity
现在被设置为get_pdf()
方法,该方法负责根据show_stats
变量返回相应的 DataFrame:
def get_pdf(self):
if self.show_stats:
summary = self.pdf.describe()
summary.insert(0, "Stat", summary.index)
return summary
return self.pdf
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode17.py
我们使用 pandas 的describe()
方法(pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.describe.html
),该方法返回一个包含统计摘要的 DataFrame,诸如计数、均值、标准差等。我们还确保这个 DataFrame 的第一列包含统计数据的名称。
我们需要进行的最后一次修改是初始化show_stats
变量,因为如果我们不这样做,那么第一次检查它时会得到AttributeError
异常。
由于使用@PixieApp
装饰器的内部机制,你不能使用__init__
方法来初始化变量;相反,PixieApp 编程模型要求你使用一个名为setup
的方法,该方法在应用程序启动时保证会被调用:
@PixieApp
class RepoAnalysis():
def setup(self):
self.show_stats = False
...
注意
注意:如果你有一个类继承自其他 PixieApps,那么 PixieApp 框架将自动按其出现顺序调用所有基类的setup
函数。
以下截图显示了统计摘要的展示:
GitHub 仓库的统计摘要
注意
你可以在这里找到完整的GitHub 跟踪应用程序第三部分的笔记本:
使用pd_refresh
使应用程序更具响应性
我们希望通过让显示统计按钮直接展示统计表格来改善用户体验,而不是让用户再次点击菜单。类似于加载提交活动的菜单,我们可以为复选框添加pd_options
属性,并将pd_target
属性指向analyse_vis{{prefix}}
元素。我们可以将pd_options
添加到analyse_vis{{prefix}}
一次,而不是在每个触发新显示的控件中重复添加,然后通过pd_refresh
属性让它自行更新。
以下图示显示了两种设计之间的差异:
带有和不带pd_refresh
的时序图
在两种情况下,第一步都是更新服务器端的某些状态。在第 2 步中由控件调用的路由的情况下,请求规范存储在控件本身中,触发第 3 步,即生成 HTML 片段并将其注入目标元素。使用pd_refresh
时,控件不知道pd_options
来调用路由,相反,它只是使用pd_refresh
来通知目标元素,而后者将调用路由。在这种设计中,我们只需要在目标元素中指定一次请求,用户控件只需在触发刷新之前更新状态。这使得实现更容易维护。
为了更好地理解这两种设计之间的差异,我们来对比RepoAnalysis
类中的两种实现。
对于分析菜单,变更如下:
之前,控件触发了analyse_type
路由,将{{analysis}}
选择作为内核请求的一部分,目标是analyse_vis{{prefix}}
:
<a href="#" pd_options="analyse_type={{analysis}}"
pd_target="analyse_vis{{prefix}}"
style="text-decoration: none;background-color:transparent">
{{analysis}}
</a>
之后,控件现在将选择状态存储为类字段,并请求analyse_vis{{prefix}}
元素刷新自身:
<a href="#" pd_script="self.analyse_type='{{analysis}}'"
pd_refresh="analyse_vis{{prefix}}"
style="text-decoration: none;background-color:transparent">
{{analysis}}
</a>
同样,显示统计信息复选框的变更如下:
之前,复选框只是简单地在类中设置show_stats
状态;用户必须再次点击菜单才能获取可视化:
<div class="checkbox">
<label>
<input type="checkbox"
id="show_stats{{prefix}}"
pd_script="self.show_stats='$val(show_stats{{prefix}})'=='true'">
Show Statistics
</label>
</div>
之后,感谢pd_refresh
属性,复选框一旦被选中,可视化就会立即更新:
<div class="checkbox">
<label>
<input type="checkbox"
id="show_stats{{prefix}}"
pd_script="self.show_stats='$val(show_stats{{prefix}})'=='true'"
pd_refresh="analyse_vis{{prefix}}">
Show Statistics
</label>
</div>
最后,analyse_vis{{prefix}}
元素的变更如下:
之前,元素不知道如何更新自己,它依赖其他控件将请求定向到合适的路由:
<div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
之后,元素携带内核配置以更新自身;现在,任何控件都可以更改状态并调用刷新:
<div id="analyse_vis{{prefix}}" class="col-sm-10"
pd_options="display_analysis=true"
pd_target="analyse_vis{{prefix}}">
</div>
注意
你可以在此处找到本节的完整 Notebook,适用于GitHub 跟踪应用程序第四部分:
创建可重用的小部件
PixieApp 编程模型提供了一种机制,将复杂 UI 构造的 HTML 和逻辑封装成一个小部件,可以轻松地从其他 PixieApp 中调用。创建小部件的步骤如下:
-
创建一个包含小部件的 PixieApp 类。
-
创建一个带有特殊
widget
属性的路由,如示例所示:@route(widget="my_widget")
它将是小部件的起始路由。
-
创建一个消费者 PixieApp 类,继承自小部件 PixieApp 类。
-
通过使用
pd_widget
属性从<div>
元素调用小部件。
下面是创建小部件和消费者 PixieApp 类的示例:
from pixiedust.display.app import *
@PixieApp
class WidgetApp():
@route(widget="my_widget")
def widget_main_screen(self):
return "<div>Hello World Widget</div>"
@PixieApp
class ConsumerApp(WidgetApp):
@route()
def main_screen(self):
return """<div pd_widget="my_widget"></div>"""
ConsumerApp.run()
注意
你可以在此处找到代码:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/sampleCode18.py
总结
在本章中,我们涵盖了 PixieApp 编程模型的基础构建模块,使您能够直接在 Notebook 中创建强大的工具和仪表板。
我们还通过展示如何构建GitHub Tracking示例应用程序(包括详细的代码示例)来说明了 PixieApp 的概念和技术。关于最佳实践和更高级的 PixieApp 概念将在第五章中介绍,Python and PixieDust Best Practices and Advanced Concepts,包括事件、流处理和调试。
现在,您应该已经对 Jupyter Notebooks、PixieDust 和 PixieApps 如何通过使数据科学家和开发人员能够在单一工具(如 Jupyter Notebook)中进行协作有了一个良好的理解。
在下一章中,我们将展示如何将 PixieApp 从 Notebook 中解放出来,并使用 PixieGateway 微服务服务器将其发布为 Web 应用程序。
第四章:将您的数据分析发布到 Web - PixieApp 工具
"我认为数据是讲故事最强大的机制之一。我拿一大堆数据,试图让它讲述故事。"
– 史蒂文·列维特,《怪诞经济学》合著者
在上一章中,我们讨论了如何通过将 PixieDust 与 Jupyter Notebooks 结合使用,借助简单的 API 加速您的数据科学项目,这些 API 可以让您加载、清理和可视化数据,而无需编写大量代码,还能通过 PixieApps 实现数据科学家和开发人员之间的协作。在本章中,我们将展示如何通过使用 PixieGateway 服务器,将您的 PixieApps 和相关的数据分析从 Jupyter Notebook 中“解放”出来,发布为 Web 应用程序。这种 Notebook 的操作化特别吸引那些业务用户(如商业分析师、高层管理人员等)使用,他们希望使用 PixieApps,但与数据科学家或开发人员不同,他们可能不太愿意使用 Jupyter Notebooks。相反,他们更愿意将其作为经典的 Web 应用程序访问,或者像 YouTube 视频一样,将其嵌入到博客文章或 GitHub 页面中。通过网站或博客文章,您可以更轻松地传达从数据分析中提取的有价值的见解和其他结果。
到本章结束时,您将能够在本地安装和配置 PixieGateway 服务器实例进行测试,或者在云端的 Kubernetes 容器中进行生产部署。对于那些不熟悉 Kubernetes 的读者,我们将在下一节中介绍其基础知识。
本章将介绍 PixieGateway 服务器的另一个主要功能,即轻松共享使用 PixieDust display()
API 创建的图表。我们将展示如何通过单击一个按钮,将其发布为一个网页,供您的团队访问。最后,我们将介绍 PixieGateway 管理控制台,它可以让您管理应用程序、图表、内核、服务器日志,并在 Python 控制台中执行对内核的即席代码请求。
注意
注意:PixieGateway 服务器是 PixieDust 的一个子组件,其源代码可以在此找到:
github.com/pixiedust/pixiegateway
Kubernetes 概述
Kubernetes (kubernetes.io
) 是一个可扩展的开源系统,用于自动化和编排容器化应用程序的部署和管理,这些应用程序在云服务提供商中非常流行。它通常与 Docker 容器(www.docker.com
)一起使用,尽管也支持其他类型的容器。在开始之前,您需要访问一组已经配置为 Kubernetes 集群的计算机;您可以在此处找到如何创建该集群的教程:kubernetes.io/docs/tutorials/kubernetes-basics
。
如果您没有足够的计算资源,一个不错的解决方案是使用提供 Kubernetes 服务的公共云服务商,例如 Amazon AWS EKS(aws.amazon.com/eks
)、Microsoft Azure(azure.microsoft.com/en-us/services/container-service/kubernetes
)或 IBM Cloud Kubernetes Service(www.ibm.com/cloud/container-service
)。
为了更好地理解 Kubernetes 集群是如何工作的,让我们看看以下图示的高层架构:
Kubernetes 高层架构
在架构的顶部,我们有kubectl
命令行工具,它允许用户通过向Kubernetes 主节点发送命令来管理 Kubernetes 集群。kubectl
命令使用以下语法:
kubectl [command] [TYPE] [NAME] [flags]
其中:
-
command
:指定操作,例如create
、get
、describe
和delete
。 -
TYPE
:指定资源类型,例如pods
、nodes
和services
。 -
NAME
:指定资源的名称。 -
flags
:指定特定操作的可选标志。
注意
欲了解如何使用kubectl
,请访问以下链接:
kubernetes.io/docs/reference/kubectl/overview
工作节点中的另一个重要组件是kubelet,它通过从kube API Server读取 pod 配置来控制 pod 的生命周期。它还负责与主节点的通信。kube-proxy 根据主节点指定的策略提供所有 pods 之间的负载均衡功能,从而确保整个应用程序的高可用性。
在下一部分,我们将讨论安装和配置 PixieGateway 服务器的不同方法,包括使用 Kubernetes 集群的其中一种方法。
安装和配置 PixieGateway 服务器
在我们深入技术细节之前,部署一个 PixieGateway 服务器实例来进行尝试会是一个好主意。
主要有两种安装类型:本地安装和服务器安装。
本地安装:使用这种方法进行测试和开发。
对于这一部分,我强烈推荐使用 Anaconda 虚拟环境(conda.io/docs/user-guide/tasks/manage-environments.html
),因为它们提供了良好的环境隔离,允许您在不同版本和配置的 Python 包之间进行实验。
如果您管理多个环境,您可以使用以下命令获取所有可用环境的列表:
conda env list
首先,通过在终端使用以下命令选择您选择的环境:
source activate <<my_env>>
你应该能在终端中看到你的环境名称,这表明你已正确激活它。
接下来,通过运行以下命令从 PyPi 安装pixiegateway
包:
pip install pixiegateway
注意
注意:你可以在 PyPi 上找到更多关于pixiegateway
包的信息:
pypi.python.org/pypi/pixiegateway
安装完所有依赖项后,你可以开始启动服务器。假设你想使用8899 端口
,可以通过以下命令启动 PixieGateway 服务器:
jupyter pixiegateway --port=8899
示例输出应如下所示:
(dashboard) davids-mbp-8:pixiegateway dtaieb$ jupyter pixiegateway --port=8899
Pixiedust database opened successfully
Pixiedust version 1.1.10
[PixieGatewayApp] Jupyter Kernel Gateway at http://127.0.0.1:8899
注意
注意:要停止 PixieGateway 服务器,只需在终端中按Ctrl + C。
现在,你可以通过以下 URL 打开 PixieGateway 管理控制台:http://localhost:8899/admin
。
注意
注意:在遇到挑战时,使用admin
作为用户名,密码留空(即没有密码)。我们将在本章稍后通过PixieGateway 服务器配置部分介绍如何配置安全性和其他属性。
使用 Kubernetes 和 Docker 安装服务器:如果你需要在生产环境中运行 PixieGateway,并且希望通过 Web 向多个用户提供已部署的 PixieApps 访问权限,请使用此安装方法。
以下说明将使用 IBM Cloud Kubernetes 服务,但也可以很容易地适应其他提供商:
-
如果你还没有 IBM Cloud 账户,请创建一个,并从目录中创建一个容器服务实例。
注意
注意:有一个适用于测试的免费轻量版计划。
-
下载并安装 Kubernetes CLI(
kubernetes.io/docs/tasks/tools/install-kubectl
)和 IBM Cloud CLI(console.bluemix.net/docs/cli/reference/bluemix_cli/get_started.html#getting-started
)。注意
注意:关于 Kubernetes 容器的其他入门文章可以在此找到:
console.bluemix.net/docs/containers/container_index.html#container_index
-
登录到 IBM Cloud 后,定向到你的 Kubernetes 实例所在的组织和空间。安装并初始化
container-service
插件:bx login -a https://api.ng.bluemix.net bx target -o <YOUR_ORG> -s <YOUR_SPACE></YOUR_SPACE> bx plugin install container-service -r Bluemix bx cs init
-
检查你的集群是否已创建,如果没有,创建一个:
bx cs clusters bx cs cluster-create --name my-cluster
-
下载稍后将用于
kubectl
命令的集群配置文件,该命令将在你的本地计算机上执行:bx cs cluster-config my-cluster
上述命令将生成一个临时的 YML 文件,包含集群信息和一个环境变量导出语句,你必须在开始使用
kubectl
命令之前运行,如示例所示:export KUBECONFIG=/Users/dtaieb/.bluemix/plugins/container- service/clusters/davidcluster/kube-config-hou02-davidcluster.yml
注意
注意:YAML 是一种非常流行的数据序列化格式,通常用于系统配置。你可以在此找到更多信息:
-
现在,你可以使用
kubectl
为 PixieGateway 服务器创建部署和服务。为了方便,PixieGateway GitHub 仓库中已经有一个通用版本的deployment.yml
和service.yml
,你可以直接引用。我们将在本章后面PixieGateway 服务器配置部分回顾如何为 Kubernetes 配置这些文件:kubectl create -f https://github.com/ibm-watson-data-lab/pixiegateway/raw/master/etc/deployment.yml kubectl create -f https://github.com/ibm-watson-data-lab/pixiegateway/raw/master/etc/service.yml
-
使用
kubectl get
命令验证集群的状态是一个不错的主意:kubectl get pods kubectl get nodes kubectl get services
-
最后,你需要服务器的公共 IP 地址,你可以通过在终端使用以下命令获取的输出中的
Public IP
列来找到它:bx cs workers my-cluster
-
如果一切顺利,你现在可以通过在
http://<server_ip>>:32222/admin
打开管理员控制台来测试你的部署。这次,管理员控制台的默认凭据是admin
/changeme
,我们将在下一节中演示如何更改这些凭据。
Kubernetes 安装说明中使用的deployment.yml
文件引用了一个 Docker 镜像,该镜像已预安装并配置了 PixieGateway 二进制文件及其所有依赖项。PixieGateway Docker 镜像可以通过以下链接获取:hub.docker.com/r/dtaieb/pixiegateway-python35
。
在本地工作时,推荐的方法是遵循之前描述的本地安装步骤。然而,对于那些喜欢使用 Docker 镜像的读者,可以尝试在没有 Kubernetes 的情况下直接在本地笔记本电脑上使用简单的 Docker 命令安装 PixieGateway Docker 镜像。
docker run -p 9999:8888 dtaieb/pixiegateway-python35
上述命令假设你已经安装了 Docker,并且它当前正在本地机器上运行。如果没有,你可以从以下链接下载安装程序:docs.docker.com/engine/installation
。
如果本地没有现成的 Docker 镜像,它将自动拉取该镜像,容器将启动,并在本地端口8888
上启动 PixieGateway 服务器。命令中的-p
选项将本地的8888 端口
映射到主机机器的9999 端口
。通过这个配置,你可以通过以下 URL 访问 PixieGateway 服务器的 Docker 实例:http://localhost:9999/admin
。
注意
你可以在这里找到更多关于 Docker 命令行的信息:
docs.docker.com/engine/reference/commandline/cli
注意
注意:使用这种方法的另一个原因是为 PixieGateway 服务器提供你自定义的 Docker 镜像。如果你为 PixieGateway 构建了扩展,并希望将其作为已经配置好的 Docker 镜像提供给用户,这会很有用。如何从基础镜像构建 Docker 镜像的讨论超出了本书的范围,但你可以在这里找到详细信息:
docs.docker.com/engine/reference/commandline/image_build
PixieGateway 服务器配置
配置 PixieGateway 服务器与配置 Jupyter Kernel Gateway 非常相似。大多数选项都是通过 Python 配置文件进行配置的;为了开始,你可以使用以下命令生成一个模板配置文件:
jupyter kernelgateway --generate-config
jupyter_kernel_gateway_config.py
模板文件将在~/.jupyter
目录下生成(~
表示用户主目录)。你可以在这里找到更多关于标准 Jupyter Kernel Gateway 选项的信息:jupyter-kernel-gateway.readthedocs.io/en/latest/config-options.html
。
当你在本地工作并且可以轻松访问文件系统时,使用jupyter_kernel_gateway_config.py
文件是可以的。对于使用 Kubernetes 安装时,建议将选项配置为环境变量,你可以通过在deployment.yml
文件中使用预定义的env
类别直接设置这些变量。
现在让我们来看一下 PixieGateway 服务器的每个配置选项。这里提供了一个列表,包含了 Python 方法和环境方法:
注意
注意:提醒一下,Python 方法是指在jupyter_kernel_gateway_config.py
Python 配置文件中设置参数,而环境方法是指在 Kubernetes 的deployment.yml
文件中设置参数。
-
管理员控制台凭证:配置管理员控制台的用户 ID/密码:
-
Python:
PixieGatewayApp.admin_user_id
,PixieGatewayApp.admin_password
-
环境:
ADMIN_USERID
和ADMIN_PASSWORD
-
-
存储连接器: 配置一个持久化存储以保存各种资源,如图表和笔记本。默认情况下,PixieGateway 使用本地文件系统;例如,它会将发布的笔记本保存在
~/pixiedust/gateway
目录下。使用本地文件系统对于本地测试环境可能是可以的,但当使用 Kubernetes 安装时,你将需要明确地使用持久卷(kubernetes.io/docs/concepts/storage/persistent-volumes
),这可能会比较难以使用。如果没有配置持久化策略,当容器重启时,已持久化的文件将被删除,所有已发布的图表和 PixieApps 也会消失。PixieGateway 提供了另一种选项,可以配置一个存储连接器,让你使用你选择的机制和后端来持久化数据。要配置图表的存储连接器,你必须在以下配置变量中的任何一个中指定一个完全限定的类名:
-
Python:
SingletonChartStorage.chart_storage_class
-
环境:
PG_CHART_STORAGE
引用的连接器类必须继承自
pixiegateway.chartsManager
包中定义的ChartStorage
抽象类(实现可以在这里找到:github.com/ibm-watson-data-lab/pixiegateway/blob/master/pixiegateway/chartsManager.py
)。PixieGateway 提供一个开箱即用的连接器,用于连接 Cloudant/CouchDB NoSQL 数据库 (
couchdb.apache.org
)。要使用此连接器,你需要将连接器类设置为pixiegateway.chartsManager.CloudantChartStorage
。你还需要指定其他配置变量来指定服务器和凭证信息(我们展示了 Python/环境变量形式):-
CloudantConfig.host / PG_CLOUDANT_HOST
-
CloudantConfig.port / PG_CLOUDANT_PORT
-
CloudantConfig.protocol / PG_CLOUDANT_PROTOCOL
-
CloudantConfig.username / PG_CLOUDANT_USERNAME
-
CloudantConfig.password / PG_CLOUDANT_PASSWORD
-
-
远程内核:指定远程 Jupyter Kernel Gateway 的配置。
目前,此配置选项仅在 Python 模式下受支持。你需要使用的变量名是
ManagedClientPool.remote_gateway_config
。预期的值是一个包含服务器信息的 JSON 对象,可以通过两种方式指定:-
protocol
,host
,和port
-
notebook_gateway
指定服务器的完全限定 URL
根据内核配置,安全性也可以通过两种方式提供:
-
auth_token
-
user
和password
这可以在以下示例中看到:
c.ManagedClientPool.remote_gateway_config={ 'protocol': 'http', 'host': 'localhost', 'port': 9000, 'auth_token':'XXXXXXXXXX' } c.ManagedClientPool.remote_gateway_config={ 'notebook_gateway': 'https://YYYYY.us-south.bluemix.net:8443/gateway/default/jkg/', 'user': 'clsadmin', 'password': 'XXXXXXXXXXX' }
注意
注意,在前面的示例中,你需要在变量前加上
c.
。这是来自底层 Jupyter/IPython 配置机制的要求。作为参考,以下是使用 Python 和 Kubernetes 环境变量格式的完整配置示例文件:
-
-
以下是
jupyter_kernel_gateway_config.py
的内容:c.PixieGatewayApp.admin_password = "password" c.SingletonChartStorage.chart_storage_class = "pixiegateway.chartsManager.CloudantChartStorage" c.CloudantConfig.host="localhost" c.CloudantConfig.port=5984 c.CloudantConfig.protocol="http" c.CloudantConfig.username="admin" c.CloudantConfig.password="password" c.ManagedClientPool.remote_gateway_config={ 'protocol': 'http', 'host': 'localhost', 'port': 9000, 'auth_token':'XXXXXXXXXX' }
-
以下是
deployment.yml
的内容:apiVersion: extensions/v1beta1 kind: Deployment metadata: name: pixiegateway-deployment spec: replicas: 1 template: metadata: labels: app: pixiegateway spec: containers: - name: pixiegateway image: dtaieb/pixiegateway-python35 imagePullPolicy: Always env: - name: ADMIN_USERID value: admin - name: ADMIN_PASSWORD value: changeme - name: PG_CHART_STORAGE value: pixiegateway.chartsManager.CloudantChartStorage - name: PG_CLOUDANT_HOST value: XXXXXXXX-bluemix.cloudant.com - name: PG_CLOUDANT_PORT value: "443" - name: PG_CLOUDANT_PROTOCOL value: https - name: PG_CLOUDANT_USERNAME value: YYYYYYYYYYY-bluemix - name: PG_CLOUDANT_PASSWORD value: ZZZZZZZZZZZZZ
PixieGateway 架构图
现在是时候再次查看在第二章中展示的 PixieGateway 架构图,使用 Python 和 Jupyter Notebooks 为你的数据分析提供动力。服务器作为 Jupyter Kernel Gateway 的自定义扩展(称为 Personality)实现 (github.com/jupyter/kernel_gateway
)。
反过来,PixieGateway 服务器提供了扩展点,以自定义一些行为,我们将在本章稍后讨论。
PixieGateway 服务器的高级架构图如下所示:
PixieGateway 架构图
如图所示,PixieGateway 为三种类型的客户端提供 REST 接口:
-
Jupyter Notebook 服务器:调用一组专门的 REST API 来共享图表和发布 PixieApps 作为 Web 应用程序
-
浏览器客户端运行 PixieApp:一个特殊的 REST API 管理着与之关联的内核中 Python 代码的执行
-
浏览器客户端运行管理控制台:一组专门的 REST API 用于管理各种服务器资源和统计信息,例如 PixieApps 和内核实例
在后台,PixieGateway 服务器管理一个或多个负责运行 PixieApps 的 Jupyter 内核实例的生命周期。在运行时,每个 PixieApp 都会使用一组特定的步骤在内核实例上进行部署。下图展示了在服务器上运行的所有 PixieApp 用户实例的典型拓扑结构:
运行中的 PixieApp 实例拓扑结构
当 PixieApp 部署在服务器上时,Jupyter Notebook 中每个单元的代码都会被分析并分为两部分:
-
预热代码:这是在所有主 PixieApp 定义之上的所有单元中定义的代码。此代码仅在 PixieApp 应用程序第一次在内核上启动时运行一次,除非内核重新启动,或者显式从运行代码中调用,否则不会再次运行。这一点很重要,因为它有助于优化性能;例如,您应该始终将那些加载大量数据且变化不大或者初始化可能需要较长时间的代码放在预热部分。
-
运行代码:这是每个用户会话中在其独立实例中运行的代码。运行代码通常是从包含 PixieApp 类声明的单元中提取的。发布者通过对 Python 代码进行静态分析,自动发现这个单元,并特别查找以下两个条件,这两个条件必须同时满足:
-
该单元包含一个带有
@PixieApp
注解的类 -
该单元实例化该类并调用其
run()
方法@PixieApp class MyApp(): @route() def main_screen(self): return "<div>Hello World</div>" app = MyApp() app.run()
例如,以下代码必须放在单独的单元中,才能符合作为运行代码的条件:
正如我们在 第三章 加速使用 Python 库进行数据分析 中看到的那样,可以在同一个笔记本中声明多个 PixieApps,作为子 PixieApp 或作为主 PixieApp 的基类。在这种情况下,我们需要确保它们定义在自己的单元中,并且不要尝试实例化它们并调用其
run()
方法。规则是,只有一个主 PixieApp 类可以调用
run()
方法,包含此代码的单元会被 PixieGateway 视为运行代码。注意
注意:在 PixieGateway 服务器进行静态分析时,未标记为代码的单元(如 Markdown、Raw NBConvert 或 Heading)会被忽略。因此,您可以安全地将它们保留在您的笔记本中。
-
对于每个客户端会话,PixieGateway 将使用运行代码(如上图中的彩色六边形表示)实例化主 PixieApp 类的一个实例。根据当前负载,PixieGateway 将决定在特定内核实例中应该运行多少个 PixieApp,如果需要,自动创建一个新的内核来服务额外的用户。例如,如果五个用户正在使用同一个 PixieApp,三个实例可能在特定的内核实例中运行,而另外两个将会在另一个内核实例中运行。PixieGateway 会不断监控使用模式,通过负载均衡将 PixieApp 实例分配到多个内核之间,从而优化工作负载分配。
为了帮助理解笔记本代码是如何被分解的,下面的图表展示了如何从笔记本中提取热身代码和运行代码,并将其转化,以确保多个实例能够在同一内核中和平共存:
注意
提醒一下,包含主 PixieApp 的单元格也必须有代码来实例化它并调用run()
方法。
PixieApp 生命周期:热身代码与运行代码
因为一个给定的内核实例可以托管多个包含主 PixieApp 的笔记本,我们需要确保在执行两个主 PixieApp 的热身代码时不会发生意外的名称冲突。例如,title
变量可能在两个 PixieApp 中都有使用,如果不加以处理,第二个 PixieApp 的值将覆盖第一个的值。为了避免这种冲突,所有热身代码中的变量名都会通过注入命名空间来使其唯一。
title = 'some string'
语句在发布后会变成ns1_title = 'some string'
。PixieGateway 发布程序还会更新代码中所有title
的引用,以反映新的名称。所有这些重命名操作都是在运行时自动完成的,开发者无需做任何特别的事情。
我们稍后将在介绍PixieApp 详细信息页面时展示真实的代码示例。
提示
如果你已经将主 PixieApp 的代码打包为一个在笔记本中导入的 Python 模块,你仍然需要声明一个封装的 PixieApp 代码,该代码继承自主 PixieApp。这是因为 PixieGateway 会进行静态代码分析,查找@PixieApp
注释,如果没有找到,主 PixieApp 将无法被正确识别。
例如,假设你有一个名为AwesomePixieApp
的 PixieApp,它是从awesome package
中导入的。在这种情况下,你需要将以下代码放入它自己的单元格中:
from awesome import AwesomePixieApp
@PixieApp
class WrapperAwesome(AwesomePixieApp):
pass
app = WrapperAwesome()
app.run()
发布应用程序
在本节中,我们将发布我们在第三章中创建的GitHub 跟踪应用程序,使用 Python 库加速数据分析,并将其发布到 PixieGateway 实例中。
注意
你可以从这个 GitHub 位置使用已完成的笔记本:
从笔记本中,像往常一样运行应用程序,并使用位于单元输出左上方的发布按钮,开始该过程:
调用发布对话框
发布对话框包含多个标签菜单:
-
选项:
-
PixieGateway 服务器:例如,
http://localhost:8899
-
页面标题:一个简短的描述,将作为浏览器中显示时的页面标题
-
-
安全性:通过网页访问时,请配置 PixieApp 的安全性:
-
无安全性
-
令牌:必须将安全令牌作为查询参数添加到 URL 中,例如,
http://localhost:8899/GitHubTracking?token=941b3990d5c0464586d67e48705b9deb
。
注意
注意:此时,PixieGateway 并未提供任何身份验证/授权机制。第三方授权,如 OAuth 2.0(
oauth.net/2
)、JWT(jwt.io
)等,将在未来添加。 -
-
导入:显示 PixieDust 发布器自动检测到的 Python 包依赖列表。这些导入的包将自动安装,如果目标内核中没有这些包的话。当检测到某个特定的依赖项时,PixieDust 会查看当前系统的版本和安装位置,例如,PyPi 或者自定义的安装 URL(如 GitHub 仓库等)。
-
内核规格:在这里,您可以为 PixieApp 选择内核规格。默认情况下,PixieDust 会选择 PixieGateway 服务器上可用的默认内核,但如果您的笔记本依赖于 Apache Spark,例如,您应该能够选择一个支持该功能的内核。此选项也可以在 PixieApp 部署后,通过管理员控制台进行更改。
以下是 PixieApp 发布对话框的示例截图:
PixieApp 发布对话框
单击 发布 按钮将启动发布过程。完成后(根据笔记本的大小,通常非常快速),您将看到以下屏幕:
成功发布屏幕
然后,您可以通过单击提供的链接来测试该应用程序,您可以复制该链接并与团队中的其他用户共享。以下截图显示了 GitHub Tracking 应用程序作为 Web 应用程序在 PixieGateway 上运行的三个主要屏幕:
PixieApp 作为 Web 应用程序运行
现在你已经知道如何发布 PixieApp,接下来我们来回顾一些开发者最佳实践和规则,这些规则将帮助你优化那些打算发布为 Web 应用的 PixieApp:
-
每个用户会话都会创建一个 PixieApp 实例,因此为了提高性能,确保其中不包含长时间运行的代码或加载大量静态数据(不常更改的数据)。相反,将其放在热身代码部分,并根据需要从 PixieApp 中引用。
-
不要忘记在同一个单元格中添加运行 PixieApp 的代码。如果没有这样做,运行时会在网页上显示一个空白页面。作为一种良好的实践,建议将 PixieApp 实例分配到一个单独的变量中。例如,可以这样做:
app = GitHubTracking() app.run()
这是替代以下代码的做法
GitHubTracking().run()
-
你可以在同一个 Notebook 中声明多个 PixieApp 类,如果你使用子 PixieApp 或 PixieApp 继承,这会是必要的。但是,只有其中一个可以是主 PixieApp,它是 PixieGateway 运行的那个。这个类包含了额外的代码,用于实例化并运行 PixieApp。
-
为你的 PixieApp 类添加文档字符串(
www.python.org/dev/peps/pep-0257
)是一个好主意,它简要描述应用程序的功能。正如我们在本章稍后的PixieGateway 管理控制台部分中所看到的,这个文档字符串将在 PixieGateway 管理控制台中显示,如以下示例所示:@PixieApp class GitHubTracking(RepoAnalysis): """ GitHub Tracking Sample Application """ @route() def main_screen(self): return """ ...
在 PixieApp URL 中编码状态
在某些情况下,你可能希望将 PixieApp 的状态捕捉到 URL 中作为查询参数,以便可以书签化和/或与其他人共享。这个思路是,当使用查询参数时,PixieApp 不会从主屏幕开始,而是自动激活与参数相对应的路由。例如,在GitHub Tracking应用中,你可以使用http://localhost:8899/pixieapp/GitHubTracking?query=pixiedust
来跳过初始屏幕,直接进入显示与给定查询匹配的仓库列表的表格。
你可以通过在路由中添加persist_args
特殊参数,使查询参数在路由激活时自动添加到 URL 中。
对于do_search()
路由,它看起来应该是这样的:
@route(query="*", persist_args='true')
@templateArgs
def do_search(self, query):
self.first_url = "https://api.github.com/search/repositories?q={}".format(query)
self.prev_url = None
self.next_url = None
self.last_url = None
...
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%204/sampleCode1.py
persist_args
关键字参数不会影响路由的激活方式。它只是为了在路由激活时自动将适当的查询参数添加到 URL 中。你可以尝试在 Notebook 中做出这个简单的更改,重新发布 PixieApp 到 PixieGateway 服务器并进行测试。当你在第一个屏幕上点击提交按钮时,你会发现 URL 会自动更新,包含查询参数。
注意
注意:persist_args
参数在 Notebook 中运行时也能工作,尽管实现方式不同,因为我们没有 URL。相反,参数会通过 pixieapp
键添加到单元格元数据中,如下图所示:
显示 PixieApp 参数的单元格元数据
如果您使用 persist_args
功能,可能会发现,在进行迭代开发时,每次都去单元格元数据中移除参数会变得很麻烦。作为快捷方式,PixieApp 框架在右上角的工具栏中添加了一个主页按钮,单击即可重置参数。
作为替代方案,您也可以完全避免在 Notebook 中运行时将路由参数保存到单元格元数据中(但在 Web 上运行时仍然保存)。为此,您需要使用 web
作为 persist_args
参数的值,而不是 true
:
@route(query="*", persist_args='web')
…
通过将图表发布为网页进行分享
在本节中,我们展示了如何轻松分享通过 display()
API 创建的图表,并将其发布为网页。
使用 第二章中的示例,使用 Python 和 Jupyter Notebooks 来增强数据分析,让我们加载汽车性能数据集并使用 display()
创建一个图表:
import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True) #car performance data
display(cars)
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%204/sampleCode2.py
在 PixieDust 输出界面中,选择 柱状图 菜单,然后在选项对话框中选择 horsepower
作为 Keys 和 mpg
作为 Values,如图所示:
PixieDust 图表选项
然后,我们使用分享按钮来调用图表分享对话框,如下图所示,使用 Bokeh 作为渲染器:
注意
注意:图表分享适用于任何渲染器,我鼓励您尝试使用其他渲染器,如 Matplotlib 和 Mapbox。
调用分享图表对话框
在分享图表对话框中,您可以指定 PixieGateway 服务器和图表的可选描述:
注意
请注意,作为一种便捷功能,PixieDust 会自动记住上次使用的设置。
分享图表对话框
单击 分享 按钮将启动发布过程,图表内容会传送到 PixieGateway,然后返回一个指向网页的唯一 URL。与 PixieApp 类似,您可以将此 URL 分享给团队:
图表分享确认对话框
确认对话框包含图表的唯一 URL 以及一个 HTML 片段,让您将图表嵌入到自己的网页中,比如博客文章或仪表板。
点击链接将显示以下 PixieGateway 页面:
图表页面
前面的页面显示了图表的元数据,例如作者、描述和日期,以及嵌入的 HTML 片段。请注意,如果图表具有交互性(如 Bokeh、Brunel 或 Mapbox),则它会在 PixieGateway 页面中得到保留。
例如,在前面的截图中,用户仍然可以使用滚轮缩放、框选缩放和拖动来探索图表或将图表下载为 PNG 文件。
将图表嵌入到您自己的页面中也非常简单。只需将嵌入的 HTML 片段复制到 HTML 的任何位置,如下例所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Example page with embedded chart</title>
</head>
<body>
<h1> Embedded a PixieDust Chart in a custom HTML Page</h1>
<div>
<object type="text/html" width="600" height="400"
data="http://localhost:8899/embed/04089782-7543-42a6-8dd1-e4d1cb06596a/600/400">
<a href="http://localhost:8899/embed/04089782-7543-42a6-8dd1-e4d1cb06596a">View Chart</a>
</object>
</div>
</body>
</html>
注意
您可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%204/sampleCode3.html
提示
嵌入的图表对象必须使用与浏览器相同级别或更高的安全性。如果不符合,浏览器将抛出混合内容错误。例如,如果主机页面是通过 HTTPS 加载的,则嵌入的图表也必须通过 HTTPS 加载,这意味着您需要在 PixieGateway 服务器上启用 HTTPS。您还可以访问jupyter-kernel-gateway.readthedocs.io/en/latest/config-options.html
为 PixieGateway 服务器配置 SSL/TLS 证书。另一种更容易维护的解决方案是为提供 TLS 终止的 Kubernetes 集群配置 Ingress 服务。
为了方便起见,我们在此提供了一个 PixieGateway 服务的模板入口 YAML 文件:github.com/ibm-watson-data-lab/pixiegateway/blob/master/etc/ingress.yml
。您需要使用提供的 TLS 主机和密钥更新此文件。例如,如果您使用的是 IBM Cloud Kubernetes 服务,只需在<your cluster name>
占位符中输入集群名称。有关如何将 HTTP 重定向到 HTTPS 的更多信息,请参阅:console.bluemix.net/docs/containers/cs_annotations.html#redirect-to-https
。入口服务是提高安全性、可靠性并防止 DDOS 攻击的好方法。例如,您可以设置各种限制,例如允许每个唯一 IP 地址每秒的请求/连接次数或最大带宽限制。有关更多信息,请参见kubernetes.io/docs/concepts/services-networking/ingress
。
PixieGateway 管理控制台
管理控制台是管理资源和进行故障排除的好工具。你可以通过/admin
网址访问它。请注意,你需要使用你配置的用户名/密码进行身份验证(关于如何配置用户名/密码的说明,请参见本章中的PixieGateway 服务器配置部分;默认情况下,用户名是admin
,密码为空)。
管理控制台的用户界面由多个菜单组成,集中在特定任务上。让我们逐一查看:
-
PixieApps:
-
关于所有已部署 PixieApps 的信息:网址、描述等
-
安全管理
-
操作,例如,删除和下载
管理控制台 PixieApp 管理页面
-
-
图表:
-
关于所有已发布图表的信息:链接、预览等
-
操作,例如,删除、下载和嵌入片段
管理控制台图表管理页面
-
-
内核统计:
以下截图显示了内核统计屏幕:
管理控制台内核统计页面
此屏幕显示 PixieGateway 中当前运行的所有内核的实时表格。每一行包含以下信息:
-
内核名称:这是带有深入链接的内核名称,点击后会显示内核规格、日志和Python 控制台。
-
状态:这显示状态为
空闲
或忙碌
。 -
忙碌比率:这是一个介于 0 和 100%之间的值,表示自启动以来内核的利用率。
-
正在运行的应用:这是一个正在运行的 PixieApps 列表。每个 PixieApp 都是一个深入链接,显示该 PixieApp 的预热代码并运行代码。这对于故障排除非常有用,因为你可以查看 PixieGateway 正在运行的代码。
-
用户数量:这是在该内核中有活动会话的用户数量。
-
-
服务器日志:
完整访问龙卷风服务器日志以进行故障排除
管理控制台服务器日志页面
Python 控制台
通过点击内核统计屏幕中的内核链接来调用 Python 控制台。管理员可以使用它来执行任何针对内核的代码,这对于故障排除非常有用。
例如,以下截图展示了如何调用 PixieDust 日志:
从 PixieGateway 管理员 Python 控制台显示 PixieDust 日志
显示 PixieApp 的预热和运行代码
当加载页面时发生执行错误时,PixieGateway 将在浏览器中显示完整的 Python 回溯。然而,错误可能很难找到,因为其根本原因可能在于 PixieApp 启动时执行一次的预热代码。一项重要的调试技巧是查看 PixieGateway 执行的预热和运行代码,以发现任何异常。
如果错误仍然不明显,你可以例如将热身和运行代码复制到一个临时笔记本中,然后尝试从那里运行,期望能够重现错误并找出问题。
你可以通过点击Kernel Stats屏幕上的 PixieApp 链接来访问热身和运行代码,点击后会进入如下屏幕:
显示热身和运行代码
请注意,热身和运行代码没有原始的代码格式,因此可能较难阅读。你可以通过将其复制并粘贴到临时笔记本中,再重新格式化来缓解这个问题。
总结
阅读完本章后,你应该能够安装、配置和管理 PixieGateway 微服务服务器,将图表发布为网页,并将 PixieApp 从笔记本部署到网页应用程序。无论你是从事 Jupyter 笔记本中分析的数据显示科学家,还是为业务用户编写和部署应用程序的开发者,本章展示了 PixieDust 如何帮助你更高效地完成任务,减少将分析运用到生产中的时间。
在下一章中,我们将讨论与 PixieDust 和 PixieApp 编程模型相关的高级主题和最佳实践,这些内容在后续章节中讨论行业用例和示例数据管道时将非常有用。
第五章:Python 和 PixieDust 最佳实践与高级概念
“我们信仰上帝,其他的都带上数据。”
– W. Edwards Deming
本书的剩余章节中,我们将深入探讨行业用例的架构,包括示范数据管道的实现,广泛应用我们到目前为止学到的技术。在开始查看代码之前,让我们通过一些最佳实践和高级 PixieDust 概念来完善我们的工具箱,这些将有助于我们实现示例应用:
-
使用
@captureOutput
装饰器调用第三方 Python 库 -
提高 PixieApp 的模块化和代码复用性
-
PixieDust 对流数据的支持
-
使用 PixieApp 事件添加仪表盘钻取功能
-
使用自定义显示渲染器扩展 PixieDust
-
调试:
-
使用 pdb 调试在 Jupyter Notebook 中运行的逐行 Python 代码
-
使用 PixieDebugger 进行可视化调试
-
使用 PixieDust 日志框架排查问题
-
客户端 JavaScript 调试技巧
-
-
在 Python Notebook 中运行 Node.js
使用@captureOutput
装饰器集成第三方 Python 库的输出
假设你希望将自己的 PixieApp 在已经使用一段时间的第三方库中复用,以执行某个任务,例如,使用 scikit-learn 机器学习库(scikit-learn.org
)进行集群计算并将其作为图形显示。问题是,大多数情况下,你调用的是一个高级方法,它并不会返回数据,而是直接在单元格输出区域绘制某些内容,比如图表或报告表格。从 PixieApp 路由调用此方法将不起作用,因为路由的合同要求返回一个 HTML 片段字符串,该字符串将由框架处理。在这种情况下,该方法很可能没有返回任何内容,因为它将结果直接写入单元格输出区域。解决方案是在路由方法中使用@captureOutput
装饰器——这是 PixieApp 框架的一部分。
使用@captureOutput 创建词云图像
为了更好地说明前面描述的@captureOutput
场景,让我们以一个具体示例为例,在这个示例中,我们想要构建一个 PixieApp,使用wordcloud
Python 库(pypi.python.org/pypi/wordcloud
)从用户通过 URL 提供的文本文件生成词云图像。
我们首先通过在自己的单元格中运行以下命令来安装wordcloud
库:
!pip install wordcloud
注意
注意:确保在安装完wordcloud
库后重新启动内核。
PixieApp 的代码如下所示:
from pixiedust.display.app import *
import requests
from wordcloud import WordCloud
import matplotlib.pyplot as plt
@PixieApp
class WordCloudApp():
@route()
def main_screen(self):
return """
<div style="text-align:center">
<label>Enter a url: </label>
<input type="text" size="80" id="url{{prefix}}">
<button type="submit"
pd_options="url=$val(url{{prefix}})"
pd_target="wordcloud{{prefix}}">
Go
</button>
</div>
<center><div id="wordcloud{{prefix}}"></div></center>
"""
@route(url="*")
@captureOutput
def generate_word_cloud(self, url):
text = requests.get(url).text
plt.axis("off")
plt.imshow(
WordCloud(max_font_size=40).generate(text),
interpolation='bilinear'
)
app = WordCloudApp()
app.run()
注意
你可以在这里找到代码:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode1.py
注意,通过简单地在 generate_word_cloud
路由上添加 @captureOutput
装饰器,我们不再需要返回 HTML 片段字符串。我们可以直接调用 Matplotlib 的 imshow()
函数,将图像发送到系统输出。PixieApp 框架将负责捕获输出,并将其打包为 HTML 片段字符串,然后注入到正确的 div 占位符中。结果如下:
注意
注意:我们使用以下来自 GitHub 上 wordcloud
仓库的输入 URL:
github.com/amueller/word_cloud/blob/master/examples/constitution.txt
注意
另一个可以使用的好链接是:
raw.githubusercontent.com/amueller/word_cloud/master/examples/a_new_hope.txt
简单的 PixieApp,生成来自文本的词云
任何直接绘制到单元格输出的函数都可以与 @captureOutput
装饰器一起使用。例如,你可以使用 Matplotlib 的 show()
方法或 IPython 的 display()
方法与 HTML 或 JavaScript 类一起使用。你甚至可以使用 display_markdown()
方法,通过 Markdown 标记语言输出富文本,如下代码所示:
from pixiedust.display.app import *
from IPython.display import display_markdown
@PixieApp
class TestMarkdown():
@route()
@captureOutput
def main_screen(self):
display_markdown("""
# Main Header:
## Secondary Header with bullet
1\. item1
2\. item2
3\. item3
Showing image of the PixieDust logo
.png "PixieDust Logo")
""", raw=True)
TestMarkdown().run()
这将产生以下结果:
PixieApp 使用 @captureOutput 与 Markdown
增加模块化和代码重用性
将你的应用程序拆分为较小的、自包含的组件始终是一种良好的开发实践,因为这样可以使代码更具可重用性,并且更易于维护。PixieApp 框架提供了两种创建和运行可重用组件的方法:
-
动态调用其他 PixieApp 使用
pd_app
属性 -
将应用程序的一部分打包为可重用的小部件
使用 pd_app
属性,你可以通过完全限定的类名动态调用另一个 PixieApp(从现在开始我们称之为子 PixieApp)。子 PixieApp 的输出将放置在宿主 HTML 元素(通常是一个 div 元素)中,或者通过使用 runInDialog=true
选项放入对话框中。你还可以使用 pd_options
属性初始化子 PixieApp,在这种情况下,框架将调用相应的路由。
为了更好地理解 pd_app
的工作原理,让我们通过将生成 WordCloud
图像的代码重构为其自己的 PixieApp,称为 WCChildApp
,来重写我们的 WordCloud
应用程序。
以下代码实现了 WCChildApp
作为常规 PixieApp,但请注意,它不包含默认路由。它只有一个名为 generate_word_cloud
的路由,应该由另一个 PixieApp 使用 url
参数来调用:
from pixiedust.display.app import *
import requests
from wordcloud import WordCloud
import matplotlib.pyplot as plt
@PixieApp
class WCChildApp():
@route(url='*')
@captureOutput
def generate_word_cloud(self, url):
text = requests.get(url).text
plt.axis("off")
plt.imshow(
WordCloud(max_font_size=40).generate(text),
interpolation='bilinear'
)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode2.py
现在,我们可以构建主 PixieApp,当用户指定 URL 后点击Go按钮时,它将调用WCChildApp
:
@PixieApp
class WordCloudApp():
@route()
def main_screen(self):
return """
<div style="text-align:center">
<label>Enter a url: </label>
<input type="text" size="80" id="url{{prefix}}">
<button type="submit"
pd_options="url=$val(url{{prefix}})"
pd_app="WCChildApp"
pd_target="wordcloud{{prefix}}">
Go
</button>
</div>
<center><div id="wordcloud{{prefix}}"></div></center>
"""
app = WordCloudApp()
app.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode3.py
在上述代码中,Go
按钮具有以下属性:
-
pd_app="WCChildApp"
:使用子 PixieApp 的类名。请注意,如果你的子 PixieApp 位于一个导入的 Python 模块中,那么你需要使用完整限定名。 -
pd_options="url=$val(url{{prefix}})"
:将用户输入的 URL 作为子 PixieApp 的初始化选项进行存储。 -
pd_target="wordcloud{{prefix}}"
:告诉 PixieDust 将子 PixieApp 的输出放置在 ID 为wordcloud{{prefix}}
的 div 中。
pd_app
属性是通过封装组件的逻辑和展示来模块化代码的强大方式。pd_widget
属性提供了另一种实现类似结果的方式,不过这次组件不是由外部调用,而是通过继承来调用。
每种方法都有其优缺点:
-
pd_widget
技术作为一个路由实现,肯定比pd_app
更加轻量化,因为pd_app
需要创建一个全新的 PixieApp 实例。请注意,pd_widget
和pd_app
(通过parent_pixieapp
变量)都可以访问宿主应用程序中的所有变量。 -
pd_app
属性提供了更清晰的组件分离,并且比小部件具有更多灵活性。例如,你可以有一个按钮,根据某些用户选择动态调用多个 PixieApps。注意
注意:正如我们将在本章后面看到的,这实际上就是 PixieDust 显示选项对话框时使用的方式。
如果你发现自己需要在 PixieApp 中有多个相同组件的副本,请问问自己该组件是否需要将其状态保持在类变量中。如果是这样,最好使用pd_app
,如果不是,那么使用pd_widget
也可以。
使用pd_widget
创建小部件
创建小部件可以按照以下步骤进行:
-
创建一个 PixieApp 类,该类包含一个带有特殊参数
widget
的路由 -
使主类继承自 PixieApp 小部件
-
使用
pd_widget
属性在 div 元素中调用小部件
再次举例说明,让我们用小部件重写WordCloud
应用程序:
from pixiedust.display.app import *
import requests
from word cloud import WordCloud
import matplotlib.pyplot as plt
@PixieApp
class WCChildApp():
@route(widget='wordcloud')
@captureOutput
def generate_word_cloud(self):
text = requests.get(self.url).text if self.url else ""
plt.axis("off")
plt.imshow(
WordCloud(max_font_size=40).generate(text),
interpolation='bilinear'
)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode4.py
注意前面的代码中,url
现在被引用为一个类变量,因为我们假设基类会提供它。代码必须测试 url
是否为 None
,因为在启动时,它的值正是 None
。我们之所以这样实现,是因为 pd_widget
是一个无法轻松动态生成的属性(你需要使用一种二级途径来生成带有 pd_widget
属性的 div 片段)。
现在主要的 PixieApp 类看起来是这样的:
@PixieApp
class WordCloudApp(WCChildApp):
@route()
def main_screen(self):
self.url=None
return """
<div style="text-align:center">
<label>Enter a url: </label>
<input type="text" size="80" id="url{{prefix}}">
<button type="submit"
pd_script="self.url = '$val(url{{prefix}})'"
pd_refresh="wordcloud{{prefix}}">
Go
</button>
</div>
<center><div pd_widget="wordcloud" id="wordcloud{{prefix}}"></div></center>
"""
app = WordCloudApp()
app.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode5.py
包含 pd_widget
属性的 div 在启动时被渲染出来,但由于 url
仍然是 None
,因此实际上并没有生成词云。Go
按钮有一个 pd_script
属性,该属性将 self.url
设置为用户提供的值。它还有一个 pd_refresh
属性,该属性设置为 pd_widget
div,这将再次调用 wordcloud
小部件,但这一次 URL 会被初始化为正确的值。
在这一节中,我们已经看到了两种将代码模块化以便重用的方法,以及它们各自的优缺点。我强烈建议你自己动手实验这些代码,感受一下在何时使用每种技巧。如果你觉得这部分内容还是有些模糊,不用担心;希望在接下来的章节中使用这些技巧时,它会变得更加清晰。
在下一节中,我们将转向并研究 PixieDust 中的流式数据支持。
PixieDust 对流式数据的支持
随着物联网(Internet of Things)设备的兴起,能够分析和可视化实时数据流变得越来越重要。例如,你可以有像温度计这样的传感器,或者像心脏起搏器这样的便携式医疗设备,持续地将数据流传输到像 Kafka 这样的流式服务中。PixieDust 通过提供简单的集成 API,简化了在 Jupyter Notebook 中与实时数据的交互,使得 PixieApp
和 display()
框架的集成更加便捷。
在可视化层面,PixieDust 使用 Bokeh (bokeh.pydata.org
) 支持高效的数据源更新,将流式数据绘制到实时图表中(请注意,目前仅支持折线图和散点图,但未来会添加更多图表类型)。display()
框架还支持使用 Mapbox 渲染引擎进行流式数据的地理空间可视化。
要启用流式可视化,你需要使用一个继承自 StreamingDataAdapter
的类,这是 PixieDust API 中的一个抽象类。这个类充当了流式数据源和可视化框架之间的通用桥梁。
注意
注意:我建议你花些时间查看这里的 StreamingDataAdapter
代码:
以下图表展示了StreamingDataAdapter
数据结构如何融入display()
框架:
StreamingDataAdapter 架构
在实现StreamingDataAdapter
的子类时,必须重写基类提供的doGetNextData()
方法,该方法会被重复调用以获取新数据并更新可视化。你还可以选择性地重写getMetadata()
方法,将上下文传递给渲染引擎(稍后我们将使用此方法配置 Mapbox 渲染)。
doGetNextData()
的抽象实现如下:
@abstractmethod
def doGetNextData(self):
"""Return the next batch of data from the underlying stream.
Accepted return values are:
1\. (x,y): tuple of list/numpy arrays representing the x and y axis
2\. pandas dataframe
3\. y: list/numpy array representing the y axis. In this case, the x axis is automatically created
4\. pandas serie: similar to #3
5\. json
6\. geojson
7\. url with supported payload (json/geojson)
"""
Pass
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode6.py
上面的文档字符串解释了doGetNextData()
方法允许返回的不同类型的数据。
作为一个示例,我们希望可视化一个虚构无人机在地图上实时地绕地球游荡的位置。其当前位置由一个 REST 服务提供,网址为:wanderdrone.appspot.com
。
负载使用 GeoJSON(geojson.org
),例如:
{
"geometry": {
"type": "Point",
"coordinates": [
-93.824908715741202, 10.875051131034805
]
},
"type": "Feature",
"properties": {}
}
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode7.json
为了实时渲染我们的无人机位置,我们创建了一个DroneStreamingAdapter
类,继承自StreamingDataAdapter
,并在doGetNextData()
方法中返回无人机位置服务的 URL,如下代码所示:
from pixiedust.display.streaming import *
class DroneStreamingAdapter(StreamingDataAdapter):
def getMetadata(self):
iconImage = "rocket-15"
return {
"layout": {"icon-image": iconImage, "icon-size": 1.5},
"type": "symbol"
}
def doGetNextData(self):
return "https://wanderdrone.appspot.com/"
adapter = DroneStreamingAdapter()
display(adapter)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode8.py
在getMetadata()
方法中,我们返回了特定于 Mapbox 的样式属性(如文档中所述:www.mapbox.com/mapbox-gl-js/style-spec
),该样式使用火箭 Maki 图标(www.mapbox.com/maki-icons
)作为无人机的符号。
通过几行代码,我们能够创建一个实时的无人机位置地理空间可视化,结果如下:
无人机的实时地理空间映射
注意
你可以在 PixieDust 仓库的以下位置找到此示例的完整笔记本:
github.com/pixiedust/pixiedust/blob/master/notebook/pixieapp-streaming/Mapbox%20Streaming.ipynb
将流媒体功能添加到您的 PixieApp
在下一个示例中,我们展示如何使用 PixieDust 提供的开箱即用的 MessageHubStreamingApp
PixieApp 来可视化来自 Apache Kafka 数据源的流数据:github.com/pixiedust/pixiedust/blob/master/pixiedust/apps/messageHub/messageHubApp.py
。
注意
注意:MessageHubStreamingApp
与 IBM Cloud Kafka 服务 Message Hub(console.bluemix.net/docs/services/MessageHub/index.html#messagehub
)一起使用,但它可以很容易地适配任何其他 Kafka 服务。
如果您不熟悉 Apache Kafka,不用担心,我们将在第七章中介绍相关内容,分析研究:使用 Twitter 情感分析的 NLP 和大数据。
该 PixieApp 允许用户选择与服务实例关联的 Kafka 主题,并实时显示事件。假设所选主题的事件有效负载使用 JSON 格式,它会展示从事件数据采样推断出的模式。用户随后可以选择一个特定字段(必须是数值型),并显示该字段随时间变化的平均值的实时图表。
流媒体数据的实时可视化
提供流媒体功能的关键 PixieApp 属性是 pd_refresh_rate,
它在指定的间隔执行特定的内核请求(拉取模型)。在前面的应用中,我们使用它来更新实时图表,如下所示,由 showChart
路由返回的 HTML 片段:
@route(topic="*",streampreview="*",schemaX="*")
def showChart(self, schemaX):
self.schemaX = schemaX
self.avgChannelData = self.streamingData.getStreamingChannel(self.computeAverages)
return """
<div class="well" style="text-align:center">
<div style="font-size:x-large">Real-time chart for {{this.schemaX}}(average).</div>
</div>
<div pd_refresh_rate="1000" pd_entity="avgChannelData"></div>
"""
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode9.py
上述 div 通过 pd_entity
属性与 avgChannelData
实体绑定,并负责创建每秒更新的实时图表(pd_refresh_rate=1000 ms)。反过来,avgChannelData
实体通过调用 getStreamingChannel(),
创建,并传递给 self
。computeAverage
函数负责更新所有流数据的平均值。需要注意的是,avgChannelData
是一个继承自 StreamingDataAdapter
的类,因此可以传递给 display()
框架,用于构建实时图表。
最后一步是让 PixieApp 返回displayHandler
,这是display()
框架所需要的。可以通过如下方式重写newDisplayHandler()
方法来实现:
def newDisplayHandler(self, options, entity):
if self.streamingDisplay is None:
self.streamingDisplay = LineChartStreamingDisplay(options, entity)
else:
self.streamingDisplay.options = options
return self.streamingDisplay
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode10.py
在前面的代码中,我们使用它创建了一个由 PixieDust 提供的LineChartStreamingDisplay
实例,该实例位于pixiedust.display.streaming.bokeh
包中(github.com/pixiedust/pixiedust/blob/master/pixiedust/display/streaming/bokeh/lineChartStreamingDisplay.py
),并传入了avgChannelData
实体。
如果你想看到这个应用的实际效果,你需要在 IBM Cloud 上创建一个消息中心服务实例(console.bluemix.net/catalog/services/message-hub
),并使用其凭证在 Notebook 中调用此 PixieApp,代码如下:
from pixiedust.apps.messageHub import *
MessageHubStreamingApp().run(
credentials={
"username": "XXXX",
"password": "XXXX",
"api_key" : "XXXX",
"prod": True
}
)
如果你有兴趣了解更多关于 PixieDust 流媒体的内容,你可以在这里找到其他流媒体应用示例:
-
一个简单的 PixieApp 示例,演示如何从随机生成的数据创建流媒体可视化:
github.com/pixiedust/pixiedust/blob/master/notebook/pixieapp-streaming/PixieApp%20Streaming-Random.ipynb
- 显示如何构建实时股票行情可视化的 PixieApp:
github.com/pixiedust/pixiedust/blob/master/notebook/pixieapp-streaming/PixieApp%20Streaming-Stock%20Ticker.ipynb
接下来的主题将介绍 PixieApp 事件,它可以让你在应用程序的不同组件之间添加交互性。
- 显示如何构建实时股票行情可视化的 PixieApp:
使用 PixieApp 事件添加仪表盘钻取功能
PixieApp 框架支持使用浏览器中可用的发布-订阅模式在不同组件之间发送和接收事件。使用这种模式的巨大优势在于,它借鉴了松耦合模式(en.wikipedia.org/wiki/Loose_coupling
),使得发送和接收组件可以彼此独立。这样,它们的实现可以相互独立执行,并且不受需求变化的影响。这在你的 PixieApp 使用来自不同团队的不同 PixieApp 组件时非常有用,或者当事件来自用户与图表交互(例如,点击地图)时,你希望提供钻取功能。
每个事件都携带一个包含任意键值对的 JSON 负载。负载必须至少包含以下一个键(或两者):
-
targetDivId
:标识发送事件元素的 DOM ID -
type
:标识事件类型的字符串
发布者可以通过两种方式触发事件:
-
声明式:使用
pd_event_payload
属性来指定负载内容。该属性遵循与pd_options
相同的规则:-
每个键值对必须使用
key=value
的表示法进行编码 -
事件将由点击或变化事件触发
-
必须支持
$val()
指令,以动态注入用户输入的内容 -
使用
<pd_event_payload>
子元素输入原始 JSON<button type="submit" pd_event_payload="type=topicA;message=Button clicked"> Send event A </button>
<button type="submit"> <pd_event_payload> { "type":"topicA", "message":"Button Clicked" } </pd_event_payload> Send event A </button>
示例:
或者,我们可以使用以下方法:
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode11.html
-
-
编程式:在某些情况下,你可能想通过 JavaScript 直接触发事件。在这种情况下,你可以使用
pixiedust
全局对象的sendEvent(payload, divId)
方法。divId
是一个可选参数,指定事件的来源。如果省略divId
参数,则默认为当前发送事件的元素的divId
。因此,通常情况下,你应使用pixiedust.sendEvent
而不带divId
,来自用户事件的 JavaScript 处理程序,例如点击和悬停。示例:
<table onclick="pixiedust.sendEvent({type:'topicB',text:event.srcElement.innerText})"> <tr><td>Row 1</td></tr> <tr><td>Row 2</td></tr> <tr><td>Row 3</td></tr> </table>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode12.html
订阅者可以通过声明一个<pd_event_handler>
元素来监听事件,该元素可以接受 PixieApp 内核执行属性中的任何一个,如pd_options
和pd_script
。它还必须使用pd_source
属性来筛选它们想要处理的事件。pd_source
属性可以包含以下值之一:
-
targetDivId
:只接受来自指定 ID 元素的事件 -
type
:只有指定类型的事件才会被接受 -
"*"
:表示接受任何事件
示例:
<div class="col-sm-6" id="listenerA{{prefix}}">
Listening to button event
<pd_event_handler
pd_source="topicA"
pd_script="print(eventInfo)"
pd_target="listenerA{{prefix}}">
</pd_event_handler>
</div>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode13.html
下图展示了组件如何相互交互:
组件之间的事件发送/接收
在以下代码示例中,我们通过构建两个发布者,一个按钮元素和一个表格来说明 PixieDust 事件系统,其中每一行都是一个事件源。我们还有两个监听器,作为 div 元素实现:
from pixiedust.display.app import *
@PixieApp
class TestEvents():
@route()
def main_screen(self):
return """
<div>
<button type="submit">
<pd_event_payload>
{
"type":"topicA",
"message":"Button Clicked"
}
</pd_event_payload>
Send event A
</button>
<table onclick="pixiedust.sendEvent({type:'topicB',text:event.srcElement.innerText})">
<tr><td>Row 1</td></tr>
<tr><td>Row 2</td></tr>
<tr><td>Row 3</td></tr>
</table>
</div>
<div class="container" style="margin-top:30px">
<div class="row">
<div class="col-sm-6" id="listenerA{{prefix}}">
Listening to button event
<pd_event_handler pd_source="topicA" pd_script="print(eventInfo)" pd_target="listenerA{{prefix}}">
</pd_event_handler>
</div>
<div class="col-sm-6" id="listenerB{{prefix}}">
Listening to table event
<pd_event_handler pd_source="topicB" pd_script="print(eventInfo)" pd_target="listenerB{{prefix}}">
</pd_event_handler>
</div>
</div>
</div>
"""
app = TestEvents()
app.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode14.py
上述代码会生成以下结果:
PixieApp 事件的用户交互流程
PixieApp 事件使你能够创建具有钻取功能的复杂仪表板。还要知道,你可以利用 display()
框架为某些图表自动发布的事件。例如,内置渲染器(如 Google Maps、Mapbox 和 Table)将在用户点击图表上的某个地方时自动生成事件。这对于快速构建各种具有钻取功能的互动仪表板非常有用。
在下一个主题中,我们将讨论如何使用 PixieDust 可扩展性 API 创建自定义可视化。
扩展 PixieDust 可视化
PixieDust 被设计为高度可扩展。你可以创建自己的可视化并控制它何时可以被调用,这取决于正在显示的实体。PixieDust 框架提供了多个可扩展性层。最底层也是最强大的那一层让你创建自己的 Display
类。然而,大多数可视化具有许多共同的属性,例如标准选项(聚合、最大行数、标题等),或者是一个缓存机制,用来防止在用户仅选择了一个不需要重新处理数据的小选项时重新计算所有内容。
为了防止用户每次都从头开始,PixieDust 提供了第二层可扩展性,称为 renderer,它包含了这里描述的所有功能。
以下图示说明了不同的层级:
PixieDust 扩展层
要开始使用 Display 扩展层,你需要通过创建一个继承自 pixiedust.display.DisplayHandlerMeta
的类,将你的可视化显示在菜单中。此类包含两个需要重写的方法:
-
getMenuInfo(self,entity,dataHandler)
:如果传入的实体参数不被支持,返回一个空数组,否则返回一个包含一组 JSON 对象的数组,其中包含菜单信息。每个 JSON 对象必须包含以下信息:-
id
:一个唯一的字符串,用于标识你的工具。 -
categoryId
:一个唯一的字符串,用于标识菜单类别或组。稍后会提供所有内置类别的完整列表。 -
title
:一个任意字符串,用于描述菜单。 -
icon
:一个字体图标的名称,或者一个图片的 URL。
-
-
newDisplayHandler(self,options,entity)
:当用户激活菜单时,将调用newDisplayHandler()
方法。该方法必须返回一个继承自pixiedust.display.Display
的类实例。要求该类实现doRender()
方法,该方法负责创建可视化效果。
让我们以为 pandas DataFrame 创建自定义表格渲染为例。我们首先创建DisplayHandlerMeta
类来配置菜单和工厂方法:
from pixiedust.display.display import *
import pandas
@PixiedustDisplay()
class SimpleDisplayMeta(DisplayHandlerMeta):
@addId
def getMenuInfo(self,entity,dataHandler):
if type(entity) is pandas.core.frame.DataFrame:
return [
{"categoryId": "Table", "title": "Simple Table", "icon": "fa-table", "id": "simpleTest"}
]
return []
def newDisplayHandler(self,options,entity):
return SimpleDisplay(options,entity)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode15.py
请注意,前面的SimpleDisplayMeta
类需要使用@PixiedustDisplay
进行装饰,这对于将此类添加到 PixieDust 插件的内部注册表是必需的。在getMenuInfo()
方法中,我们首先检查实体类型是否为pandas DataFrame,如果不是,则返回一个空数组,表示此插件不支持当前实体,因此不会对菜单做出任何贡献。如果类型正确,我们将返回一个包含菜单信息的 JSON 对象的数组。
工厂方法newDisplayHandler()
接受options
和entity
作为参数。options
参数是一个包含用户选择的各种键/值对的字典。如我们稍后将看到的,可视化效果可以定义任意的键/值对来反映其功能,PixieDust 框架将自动将其保存在单元格元数据中。
例如,你可以为在 UI 中将 HTTP 链接显示为可点击的选项添加一个功能。在我们的示例中,我们返回一个定义好的SimpleDisplay
实例,如下所示:
class SimpleDisplay(Display):
def doRender(self, handlerId):
self._addHTMLTemplateString("""
<table class="table table-striped">
<thead>
{%for column in entity.columns.tolist()%}
<th>{{column}}</th>
{%endfor%}
</thead>
<tbody>
{%for _, row in entity.iterrows()%}
<tr>
{%for value in row.tolist()%}
<td>{{value}}</td>
{%endfor%}
</tr>
{%endfor%}
</tbody>
</table>
""")
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode16.py
如前所述,SimpleDisplay
类必须继承自 Display
类,并实现 doRender()
方法。在该方法的实现中,您可以访问 self.entity
和 self.options
变量,以调整如何在屏幕上呈现信息。在前面的示例中,我们使用 self._addHTMLTemplateString()
方法来创建将渲染可视化的 HTML 片段。与 PixieApp 路由类似,传递给 self._addHTMLTemplateString()
的字符串可以利用 Jinja2 模板引擎,并自动访问如 entity
等变量。如果您不想在 Python 文件中硬编码模板字符串,您可以将其提取到一个独立的文件中,该文件必须放置在名为 templates
的目录中,并且该目录必须与调用 Python 文件位于同一目录下。然后,您需要使用 self._addHTMLTemplate()
方法,该方法将文件名作为参数(无需指定 templates
目录)。
注意
将 HTML 片段外部化为独立文件的另一个好处是,您在进行更改时不必每次都重启内核,这可以节省很多时间。由于 Python 的工作方式,如果 HTML 片段嵌入在源代码中,情况就不同了,在这种情况下,您必须重新启动内核才能使 HTML 片段的任何更改生效。
还需要注意的是,self._addHTMLTemplate()
和 self._addHTMLTemplateString()
接受关键字参数,这些参数将传递给 Jinja2 模板。例如:
self._addHTMLTemplate('simpleTable.html', custom_arg = "Some value")
现在我们可以运行一个单元格,显示例如 cars
数据集:
注意
注意:简单表格扩展只适用于 pandas,而不适用于 Spark DataFrame。因此,如果您的 Notebook 连接到 Spark,您需要在调用 sampleData()
时使用 forcePandas = True
。
在 pandas DataFrame 上运行自定义可视化插件
如 PixieDust 扩展层架构图所示,您还可以使用 渲染器扩展层 来扩展 PixieDust,渲染器扩展层比 显示扩展层 更具规定性,但开箱即用提供了更多的功能,如选项管理和中间数据计算缓存。从用户界面的角度来看,用户可以通过图表区域右上角的 渲染器 下拉菜单在不同的渲染器之间切换。
PixieDust 附带了一些内置渲染器,如 Matplotlib、Seaborn、Bokeh、Mapbox、Brunel 和 Google Maps,但它并不声明对底层可视化库(包括 Bokeh、Brunel 或 Seaborn)的硬依赖。因此,用户必须手动安装这些库,否则它们将不会出现在菜单中。
以下截图展示了在给定图表上切换渲染器的机制:
切换渲染器
添加新的渲染器类似于添加显示可视化(使用相同的 API),尽管实际上更简单,因为您只需构建一个类(无需构建元数据类)。以下是您需要遵循的步骤:
-
创建一个从专门的
BaseChartDisplay class
继承的 Display 类。实现所需的doRenderChart()
方法。 -
使用
@PixiedustRenderer
装饰器注册rendererId
(在所有渲染器中必须是唯一的)和正在渲染的图表类型。注意,相同的
rendererId
可以用于渲染器中包含的所有图表。PixieDust 提供了一组核心图表类型:-
tableView
-
barChart
-
lineChart
-
scatterPlot
-
pieChart
-
mapView
-
histogram
-
-
(可选) 使用
@commonChartOptions
装饰器创建一组动态选项。 -
(可选) 通过覆盖
get_options_dialog_pixieapp()
方法来自定义选项对话框,返回pixiedust.display.chart.options.baseOptions
包中继承自BaseOptions
类的 PixieApp 类的完全限定名称。
例如,让我们使用扩展层的渲染器重新编写前述的自定义 SimpleDisplay
表可视化:
from pixiedust.display.chart.renderers import PixiedustRenderer
from pixiedust.display.chart.renderers.baseChartDisplay import BaseChartDisplay
@PixiedustRenderer(rendererId="simpletable", id="tableView")
class SimpleDisplayWithRenderer(BaseChartDisplay):
def get_options_dialog_pixieapp(self):
return None #No options needed
def doRenderChart(self):
return self.renderTemplateString("""
<table class="table table-striped">
<thead>
{%for column in entity.columns.tolist()%}
<th>{{column}}</th>
{%endfor%}
</thead>
<tbody>
{%for _, row in entity.iterrows()%}
<tr>
{%for value in row.tolist()%}
<td>{{value}}</td>
{%endfor%}
</tr>
{%endfor%}
</tbody>
</table>
""")
注意
您可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode17.py
我们使用 @PixiedustRenderer
装饰器装饰这个类,指定一个名为 simpletable
的唯一 rendererId
,并将其与 PixieDust 框架定义的 tableView
图表类型关联。我们在 get_options_dialog_pixieapp()
方法中返回 None
,表示此扩展不支持自定义选项。因此,选项按钮将不会显示。在 doRenderChart()
方法中,我们返回 HTML 片段。由于我们想使用 Jinja2,我们需要使用 self.renderTemplateString
方法进行渲染。
现在,我们可以使用 cars
数据集测试这个新的渲染器。
注意
再次运行代码时,请确保将 cars
数据集加载为 pandas DataFrame。如果您已经运行了简单表的第一个实现,并且正在重用笔记本电脑,则可能仍会看到旧的简单表菜单。如果是这种情况,请重新启动内核并重试。
下图显示了作为渲染器的简单表可视化:
测试简单表的渲染器实现
您可以在这里找到更多关于此主题的材料:pixiedust.github.io/pixiedust/develop.html
。希望到目前为止,您已经对您可以编写的类型自定义有了一个很好的了解,以集成到 display()
框架中。
在接下来的章节中,我们将讨论开发者非常重要的一个主题:调试。
调试
能够快速调试应用程序对于项目的成功至关重要。如果没有这样做,我们在打破数据科学与工程之间的壁垒所取得的生产力和协作上的进展,大部分(如果不是全部)将会丧失。还需要注意的是,我们的代码在不同的位置运行,即 Python 在服务器端,JavaScript 在客户端,调试必须在这两个地方进行。对于 Python 代码,让我们来看两种排查编程错误的方法。
在 Jupyter Notebook 中使用 pdb 调试
pdb(docs.python.org/3/library/pdb.html
)是一个交互式命令行 Python 调试器,每个 Python 发行版中都默认包含。
调用调试器的方式有多种:
-
启动时,从命令行:
python -m pdb <script_file>
-
在代码中以编程方式:
import pdb pdb.run("<insert a valid python statement here>")
-
通过在代码中设置显式的断点,使用
set_trace()
方法:import pdb def my_function(arg1, arg2): pdb.set_trace() do_something_here()
注意
您可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode18.py
-
在发生异常后,通过调用
pdb.pm()
进行事后调试。
在交互式调试器中,您可以调用命令、检查变量、运行语句、设置断点等。
注意
完整的命令列表可以在此处找到:
docs.python.org/3/library/pdb.html
好消息是 Jupyter Notebook 对交互式调试器提供了一流的支持。要调用调试器,只需使用%pdb
单元魔法命令来启用/禁用它,并且如果触发异常,调试器将自动在出错的行停止执行。
魔法命令(ipython.readthedocs.io/en/stable/interactive/magics.html
)是特定于 IPython 内核的构造。它们与语言无关,因此理论上可以在内核支持的任何语言中使用(例如 Python、Scala 和 R)。
有两种类型的魔法命令:
-
行魔法:语法为
%<magic_command_name> [optional arguments]
,例如%matplotlib inline
,它配置 Matplotlib 将图表以内联方式输出到 Notebook 的输出单元格中。它们可以在单元格代码中的任何位置调用,甚至可以返回值,赋值给 Python 变量,例如:
#call the pwd line magic to get the current working directory #and assign the result into a Python variable called pwd pwd = %pwd print(pwd)
注意
您可以在此处找到所有行魔法命令的列表:
ipython.readthedocs.io/en/stable/interactive/magics.html#line-magics
-
单元魔法:语法为
%%<magic_command_name> [optional arguments]
。例如,我们可以调用 HTML 单元魔法在输出单元格中显示 HTML:%%html <div>Hello World</div>
单元格魔法命令必须位于单元格的顶部;如果放在其他位置将导致执行错误。单元格魔法命令下方的所有内容都会作为参数传递给处理程序,并根据单元格魔法命令的规范进行解释。例如,HTML 单元格魔法命令期望单元格的其余部分是 HTML 格式。
以下代码示例调用了一个引发ZeroDivisionError
异常的函数,并且激活了pdb
的自动调用:
注意
注意:一旦启用pdb
,它将保持开启,直到整个 Notebook 会话结束。
交互式命令行调试
这里有一些可以用来排查问题的重要pdb
命令:
-
s(tep)
:进入被调用的函数并停在下一行语句处。 -
n(ext)
:继续执行到下一行,而不进入嵌套函数。 -
l(list)
:列出当前行周围的代码。 -
c(ontinue)
:继续运行程序并停在下一个断点,或者当其他异常被触发时停下。 -
d(own)
:向下移动堆栈帧。 -
u(p)
:向上移动堆栈帧。 -
<any expression>
:在当前框架上下文中评估并显示一个表达式。例如,你可以使用locals()
来获取当前框架作用域内的所有局部变量列表。
如果发生了异常,而且你没有设置自动调用pdb
,你仍然可以在事后通过在另一个单元格中使用%debug
魔法命令来调用调试器,如下图所示:
使用%debug 进行事后调试会话
类似于普通的 Python 脚本,你也可以使用pdb.set_trace()
方法显式地设置一个断点。然而,建议使用由 IPython 核心模块提供的增强版set_trace()
,它支持语法高亮:
显式断点
在下一个主题中,我们将介绍一个由 PixieDust 提供的增强版 Python 调试器。
使用 PixieDebugger 进行可视化调试
使用标准命令行调试工具 Python 的 pdb 来调试代码是一个不错的工具,但它有两个主要的局限性:
-
它是命令行导向的,这意味着命令必须手动输入,结果会按顺序附加到单元格输出中,这使得它在进行高级调试时不太实用。
-
它不能与 PixieApps 一起使用
PixieDebugger 的功能解决了这两个问题。你可以在 Jupyter Notebook 的任何 Python 代码中使用它来进行可视化调试。要在单元格中启用 PixieDebugger,只需在单元格顶部添加%%pixie_debugger
单元格魔法命令。
注意
注意:如果你还没有这么做,请记得在尝试使用%%pixie_debugger
之前,先在单独的单元格中导入pixiedust
。
例如,以下代码尝试计算cars
数据集中名为chevrolet
的汽车数量:
%%pixie_debugger
import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True)
def count_cars(name):
count = 0
for row in cars.itertuples():
if name in row.name:
count += 1
return count
count_cars('chevrolet')
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode19.py
运行前面的代码单元将触发以下截图所示的可视化调试器。用户界面让你逐行调试代码,具备检查局部变量、评估 Python 表达式和设置断点的功能。代码执行工具栏提供了用于管理代码执行的按钮:恢复执行、跳过当前行、进入代码中的特定函数、运行到当前函数的末尾,以及上下显示栈帧一层:
PixieDebugger 在工作中
没有参数时,pixie_debugger
单元格魔法将会在代码中的第一个可执行语句处停止。你可以通过使用-b
开关轻松配置它在特定位置停止,后面跟着一个断点列表,这些断点可以是行号或方法名。
从前面的示例代码开始,让我们在count_cars()
方法和第 11 行添加断点:
%%pixie_debugger -b count_cars 11
import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True)
def count_cars(name):
count = 0
for row in cars.itertuples():
if name in row.name:
count += 1
return count
count_cars('chevrolet')
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode20.py
运行前面的代码将触发 PixieDebugger,在count_cars()
方法的第一个可执行语句处停止。它还在第 11 行添加了一个断点,如果用户恢复执行,执行流程将在此停止,如下图所示:
带有预定义断点的 PixieDebugger
注意
注意:要运行到特定的代码行而不设置显式断点,只需在左侧面板的行号区域悬停,然后点击出现的图标。
像%debug
行魔法一样,你还可以使用%pixie_debugger
行魔法来调用 PixieDebugger 进行事后调试。
使用 PixieDebugger 调试 PixieApp 路由
PixieDebugger 完全集成到 PixieApp 框架中。每当触发路由时发生异常,生成的回溯信息将会增加两个额外的按钮:
-
事后调试:调用 PixieDebugger 开始事后故障排除会话,允许你检查变量并分析堆栈帧
-
调试路线:回放当前路线,在 PixieDebugger 中停止在第一个可执行语句处
例如,以下是实现一个 PixieApp 的代码,允许用户通过提供列名和查询条件来搜索cars
数据集:
from pixiedust.display.app import *
import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True)
@PixieApp
class DisplayCars():
@route()
def main_screen(self):
return """
<div>
<label>Column to search</label>
<input id="column{{prefix}}" value="name">
<label>Query</label>
<input id="search{{prefix}}">
<button type="submit" pd_options="col=$val(column{{prefix}});query=$val(search{{prefix}})"
pd_target="target{{prefix}}">
Search
</button>
</div>
<div id="target{{prefix}}"></div>
"""
@route(col="*", query="*")
def display_screen(self, col, query):
self.pdf = cars.loc[cars[col].str.contains(query)]
return """
<div pd_render_onload pd_entity="pdf">
<pd_options>
{
"handlerId": "tableView",
"table_noschema": "true",
"table_nosearch": "true",
"table_nocount": "true"
}
</pd_options>
</div>
"""
app = DisplayCars()
app.run()
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode21.py
搜索列的默认值为name
,但如果用户输入的列名不存在,将会生成如下的回溯信息:
增强的回溯信息,带有用于调用 PixieDebugger 的按钮
点击Debug Route将自动启动 PixieDebugger,并在路由的第一个可执行语句处停下来,如下图所示:
调试 PixieApp 路由
你也可以通过使用debug_route
关键字参数来让 PixieDebugger 在display_screen()
路由处停下来,而无需等待回溯信息的生成,方法如下:
...
app = DisplayCars()
app.run(debug_route="display_screen")
PixieDebugger 是第一个为 Jupyter Notebook 提供的可视化 Python 调试器,提供了 Jupyter 用户社区长期要求的功能。然而,实时调试并不是开发者使用的唯一工具。在接下来的部分,我们将通过检查日志记录消息来进行调试。
使用 PixieDust 日志记录进行故障排查
习惯上最好在代码中使用日志记录消息,而 PixieDust 框架提供了一种简便的方式,可以直接从 Jupyter Notebook 创建和读取日志消息。首先,你需要通过调用getLogger()
方法创建一个日志记录器,方法如下:
import pixiedust
my_logger = pixiedust.getLogger(__name__)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode22.py
你可以将任何内容作为getLogger()
方法的参数。然而,为了更好地识别特定消息的来源,建议使用__name__
变量,它返回当前模块的名称。my_logger
变量是一个标准的 Python 日志记录器对象,提供各种级别的日志记录方法:
-
debug(msg, *args, **kwargs)
:以DEBUG
级别记录一条消息。 -
info(msg, *args, **kwargs)
:以INFO
级别记录一条消息。 -
warning(msg, *args, **kwargs)
:以WARNING
级别记录一条消息。 -
error(msg, *args, **kwargs)
:以ERROR
级别记录一条消息。 -
critical(msg, *args, **kwargs)
:以CRITICAL
级别记录一条消息。 -
exception(msg, *args, **kwargs)
:以EXCEPTION
级别记录一条消息。此方法仅应在异常处理程序中调用。
注意
注意:你可以在这里找到更多关于 Python 日志框架的信息:
docs.python.org/2/library/logging.html
然后你可以通过%pixiedustLog
单元魔法直接从 Jupyter Notebook 查询日志消息,该魔法需要以下参数:
-
-l
: 按日志级别过滤,例如CRITICAL
、FATAL
、ERROR
、WARNING
、INFO
和DEBUG
-
-f
: 过滤包含特定字符串的消息,例如Exception
-
-m
: 返回的最大日志消息数
在以下示例中,我们使用 %pixiedustLog
魔法来显示所有调试消息,将这些消息限制为最后五条:
显示最后五条日志消息
为了方便使用,在处理 Python 类时,你还可以使用 @Logger
装饰器,它会自动创建一个以类名为标识符的日志记录器。
这是一个使用 @Logger
装饰器的代码示例:
from pixiedust.display.app import *
from pixiedust.utils import Logger
@PixieApp
@Logger()
class AppWithLogger():
@route()
def main_screen(self):
self.info("Calling default route")
return "<div>hello world</div>"
app = AppWithLogger()
app.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode23.py
在单元格中运行前述 PixieApp 后,你可以调用 %pixiedustLog
魔法来显示消息:
使用特定术语查询日志
这完成了我们关于服务器端调试的讨论。在下一节中,我们将探讨一种执行客户端调试的技术
客户端调试
PixieApp 编程模型的设计原则之一是尽量减少开发者编写 JavaScript 的需要。框架将通过监听用户输入事件(如点击或更改事件)自动触发内核请求。然而,在某些情况下,编写少量的 JavaScript 是不可避免的。这些 JavaScript 片段通常是特定路由 HTML 片段的一部分,并动态注入到浏览器中,这使得调试变得非常困难。
一种流行的技巧是在 JavaScript 代码中加入 console.log
调用,以便将消息打印到浏览器的开发者控制台。
注意
注意:每种浏览器都有自己调用开发者控制台的方式。例如,在 Google Chrome 中,你可以使用 查看 | 开发者 | JavaScript 控制台,或 Command + Alt + J 快捷键。
另一个我特别喜欢的调试技巧是通过在 JavaScript 代码中编程插入一个断点,使用 debugger;
语句。除非浏览器开发者工具已打开并启用了源代码调试,否则此语句没有任何效果。在这种情况下,执行将自动在 debugger;
语句处中断。
以下是一个 PixieApp 示例,使用 JavaScript 函数解析 $val()
指令引用的动态值:
from pixiedust.display.app import *
@PixieApp
class TestJSDebugger():
@route()
def main_screen(self):
return """
<script>
function FooJS(){
debugger;
return "value"
}
</script>
<button type="submit" pd_options="state=$val(FooJS)">Call route</button>
"""
@route(state="*")
def my_route(self, state):
return "<div>Route called with state <b>{{state}}</b></div>"
app = TestJSDebugger()
app.run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode24.py
在上面的代码中,按钮动态地使用包含调试器语句的FooJS
JavaScript 函数设置状态的值。执行应用程序并在开发者工具打开时点击按钮,将自动在浏览器中启动调试会话:
在客户端使用调试器调试 JavaScript 代码;语句
在 Python Notebook 中运行 Node.js
尽管我在本书开头已经明确表示,Python 已成为数据科学领域的明确领导者,但它在开发者社区中的使用仍然处于边缘地位,而传统语言(如 Node.js)仍然是首选。认识到对于一些开发者而言,学习像 Python 这样的新语言可能是进入数据科学的成本太高,我与我的 IBM 同事 Glynn Bird 合作,构建了一个名为pixiedust_node
的 PixieDust 扩展库(github.com/pixiedust/pixiedust_node
),让开发者可以在 Python Notebook 的单元格中运行 Node.js/JavaScript 代码。这个库的目标是通过让开发者可以重用他们喜欢的 Node.js 库(例如,用于加载和处理现有数据源中的数据)来帮助他们更容易地进入 Python 世界。
要安装pixiedust_node
库,只需在自己的单元格中运行以下命令:
!pip install pixiedust_node
注意
注意:安装完成后,不要忘记重启内核。
重要:你需要确保在与 Jupyter Notebook Server 同一台机器上安装 Node.js 运行时版本 6 或更高版本。
一旦内核重启,我们导入pixiedust_node
模块:
import pixiedust_node
你应该在输出中看到关于 PixieDust 和pixiedust_node
的信息,如下所示:
pixiedust_node 欢迎输出
当导入pixiedust_node
时,Python 端会创建一个 Node 子进程,并启动一个特殊线程来读取子进程的输出,将其传递给 Python 端,以便在当前执行的 Notebook 单元格中显示。这个子进程负责启动REPL会话(读取-求值-打印循环:en.wikipedia.org/wiki/Read-eval-print_loop
),它将执行所有从 Notebook 发送的脚本,并使所有创建的类、函数和变量在所有执行中可重用。
它还定义了一组旨在与 Notebook 和 PixieDust display()
API 交互的函数:
-
print(data)
:在当前执行的 Notebook 单元格中输出 data 的值。 -
display(data)
:调用 PixieDust 的display()
API,使用从数据转换的 pandas DataFrame。如果数据无法转换为 pandas DataFrame,则默认使用print
方法。 -
html(data)
:以 HTML 格式在当前执行的 Notebook 单元格中显示数据。 -
image(data)
:期望数据是一个指向图像的 URL,并在当前执行的单元格中显示图像。 -
help()
:显示所有前述方法的列表。
此外,pixiedust_node
使两个变量,npm
和 node
,在笔记本中全局可用:
-
node.cancel()
:停止当前在 Node.js 子进程中执行的代码。 -
node.clear()
:重置 Node.js 会话;所有现有变量将被删除。 -
npm.install(package)
:安装一个 npm 包并使其在 Node.js 会话中可用。该包在会话之间保持持久。 -
npm.uninstall(package)
:从系统和当前 Node.js 会话中删除 npm 包。 -
npm.list()
:列出当前安装的所有 npm 包。
pixiedust_node
创建一个单元格魔法,允许你运行任意 JavaScript 代码。只需在单元格顶部使用 %%node
魔法并像往常一样运行,代码将被执行在 Node.js 子进程的 REPL 会话中。
以下代码使用 JavaScript Date
对象(www.w3schools.com/Jsref/jsref_obj_date.asp
)显示一个包含当前日期时间的字符串:
%%node
var date = new Date()
print("Today's date is " + date)
这将输出以下内容:
"Today's date is Sun May 27 2018 20:36:35 GMT-0400 (EDT)"
以下图示说明了前述单元格的执行流程:
Node.js 脚本执行的生命周期
JavaScript 代码由 pixiedust_node
魔法处理并发送到 Node 子进程执行。在代码执行过程中,其输出将由特殊线程读取并显示回当前在笔记本中执行的单元格。请注意,JavaScript 代码可能会进行异步调用,在这种情况下,执行将立即返回,而异步调用可能还没有完成。在这种情况下,笔记本会显示单元格代码已完成,即使异步代码可能稍后会生成更多输出。无法确定异步代码何时完成。因此,开发人员必须小心地管理此状态。
pixiedust_node
还具有在 Python 和 JavaScript 之间共享变量的能力,反之亦然。因此,你可以声明一个 Python 变量(例如整数数组),在 JavaScript 中应用转换(也许使用你喜欢的库),然后再返回 Python 中处理。
以下代码在两个单元格中运行,一个纯 Python 单元格声明一个整数数组,另一个 JavaScript 单元格将每个元素乘以 2:
反向方向也同样有效。以下代码首先在 JavaScript 的 node 单元格中创建一个 JSON 变量,然后在 Python 单元格中创建并显示一个 pandas DataFrame:
%%node
data = {
"name": ["Bob","Alice","Joan","Christian"],
"age": [20, 25, 19, 45]
}
print(data)
结果如下:
{"age": [20, 25, 19, 45], "name": ["Bob", "Alice", "Joan", "Christian"]}
然后,在 Python 单元格中,我们使用 PixieDust 的 display()
:
df = pandas.DataFrame(data)
display(df)
使用以下选项:
从 Node 单元创建的数据的 display() 选项
我们得到以下结果:
从 Node 单元创建的数据生成的柱状图
我们也可以直接从 Node 单元使用pixiedust_node
提供的 display()
方法,达到相同的结果,如下所示:
%%node
data = {
"name": ["Bob","Alice","Joan","Christian"],
"age": [20, 25, 19, 45]
}
display(data)
如果你想了解更多关于pixiedust_node
的信息,我强烈推荐阅读这篇博客文章:medium.com/ibm-watson-data-lab/nodebooks-node-js-data-science-notebooks-aa140bea21ba
。像往常一样,我鼓励读者通过贡献代码或提出改进意见来参与这些工具的改进。
总结
在本章中,我们探索了各种高级概念、工具和最佳实践,增加了更多工具到我们的工具箱中,涵盖了从 PixieApps(流式处理、如何通过将第三方库与@captureOutput
集成来实现路由、PixieApp 事件、以及通过pd_app
实现更好的模块化)到开发者必备工具 PixieDebugger 的内容。我们还详细介绍了如何使用 PixieDust display()
API 创建自定义可视化。我们还讨论了pixiedust_node
,它是 PixieDust 框架的扩展,允许那些更熟悉 JavaScript 的开发者在他们喜爱的语言中处理数据。
在本书的剩余部分,我们将利用这些学到的知识,构建行业应用数据管道,从第六章,数据分析研究:使用 TensorFlow 进行 AI 和图像识别中的 深度学习视觉识别 应用开始。
本书结尾提供了关于 PixieApp 编程模型的开发者快速参考指南,详见附录,PixieApp 快速参考。
第六章:分析学习:使用 TensorFlow 进行 AI 和图像识别
“人工智能、深度学习、机器学习——无论你在做什么,如果你不理解它——就学习它。因为不然,你将在 3 年内变成恐龙。” | ||
---|---|---|
--马克·库班 |
这是一个涵盖流行行业用例的系列示例应用中的第一章,毫无疑问,我从一个与机器学习,特别是通过图像识别示例应用的深度学习相关的用例开始。这几年,人工智能(AI)领域经历了加速增长,许多实际应用已成为现实,比如自动驾驶汽车,具备高级自动语音识别的聊天机器人,这些技术在某些任务中完全能够替代人工操作员,而越来越多的人,无论是学术界还是产业界,开始参与其中。然而,仍然存在一种看法,认为进入的门槛很高,并且掌握机器学习背后的数学概念是前提条件。在本章中,我们尝试通过示例来演示,事实并非如此。
我们将以简要介绍机器学习开始本章,并介绍其一个子集——深度学习。接着我们将介绍一个非常流行的深度学习框架——TensorFlow,我们将利用它来构建一个图像识别模型。在本章的第二部分,我们将展示如何通过实现一个名为 PixieApp 的示例应用来将我们构建的模型投入实际使用,用户可以输入一个网站链接,获取该网站的所有图片,并将其作为输入传递给模型进行分类。
在本章结束时,你应该确信,即使没有机器学习博士学位,也完全可以构建有意义的应用并将其投入实际使用。
什么是机器学习?
我认为很好地捕捉到机器学习直觉的一个定义来自斯坦福大学的副教授 Andrew Ng,在他的 Coursera 课程机器学习中提到(www.coursera.org/learn/machine-learning
):
机器学习是让计算机通过学习来完成任务,而不是通过显式编程。
上述定义中的关键词是学习,在此上下文中,学习的含义与我们人类的学习方式非常相似。继续这一类比,从小开始,我们就被教导如何通过示范或者通过自身的试错过程完成一项任务。广义来说,机器学习算法可以分为两种类型,这两种类型对应于人类学习的两种方式:
-
监督学习:算法从已正确标注的示例数据中学习。这些数据也叫做训练数据,或者有时被称为地面真实。
-
无监督学习:算法能够从未标记的数据中自行学习。
下面的表格概述了每个类别中最常用的机器学习算法及其解决的问题类型:
机器学习算法列表
这些算法的输出被称为模型,并用于对从未见过的新输入数据进行预测。构建和部署这些模型的整个端到端过程在不同类型的算法中是非常一致的。
下图展示了这个过程的高层次工作流:
机器学习模型工作流
和往常一样,工作流从数据开始。在监督学习的情况下,数据将作为示例使用,因此必须正确标记答案。然后,输入数据被处理以提取内在特性,称为特征,我们可以将它们看作是代表输入数据的数值。随后,这些特征被输入到一个机器学习算法中,构建出一个模型。在典型设置中,原始数据会被拆分为训练数据、测试数据和盲数据。在模型构建阶段,测试数据和盲数据用于验证和优化模型,以确保模型不会过度拟合训练数据。过度拟合发生在模型参数过于紧密地跟随训练数据,导致在使用未见过的数据时出现错误。当模型达到预期的准确度时,它会被部署到生产环境中,并根据宿主应用的需求对新数据进行预测。
在本节中,我们将提供一个非常高层次的机器学习介绍,配以简化的数据流水线工作流,足以让你理解模型是如何构建和部署的。如果你是初学者,我强烈推荐 Andrew Ng 在 Coursera 上的机器学习课程(我自己也时常回顾)。在接下来的部分,我们将介绍机器学习的一个分支——深度学习,我们将用它来构建图像识别示例应用。
什么是深度学习?
让计算机学习、推理和思考(做决策)是一门被称为认知计算的科学,其中机器学习和深度学习是重要组成部分。下图展示了这些领域如何与 AI 这一广泛领域相关:
深度学习在 AI 中的位置
正如图示所示,深度学习是机器学习算法的一种类型。或许不为人所广知的是,深度学习领域已经存在相当长的时间,但直到最近才被广泛应用。兴趣的复燃是由于近年来计算机、云计算和存储技术的巨大进步,这些技术推动了人工智能的指数增长,并催生了许多新的深度学习算法,每个算法都特别适合解决特定问题。
正如我们在本章稍后讨论的,深度学习算法特别擅长学习复杂的非线性假设。它们的设计实际上是受到人脑工作方式的启发,例如,输入数据通过多个计算单元层进行处理,以将复杂的模型表示(例如图像)分解为更简单的表示,然后将结果传递到下一层,依此类推,直到到达负责输出结果的最终层。这些层的组合也被称为神经网络,构成一层的计算单元被称为神经元。本质上,一个神经元负责接收多个输入,并将其转换为单一输出,然后这个输出可以输入到下一层的其他神经元。
以下图示表示了一个用于图像分类的多层神经网络:
图像分类的神经网络高级表示
上述神经网络也被称为前馈网络,因为每个计算单元的输出作为输入传递到下一层,从输入层开始。中间层被称为隐藏层,包含由网络自动学习的中间特征。在我们的图像示例中,某些神经元可能负责检测角落,而其他神经元则可能专注于边缘,依此类推。最终的输出层负责为每个输出类别分配一个置信度(得分)。
一个重要的问题是,神经元的输出是如何从输入生成的?在不深入探讨涉及的数学内容的前提下,每个人工神经元会对其输入的加权和应用激活函数 ,以决定它是否应该激活。
以下公式计算加权和:
其中 是层 i 和 i + 1 之间的权重矩阵。这些权重是在稍后讨论的训练阶段中计算得出的。
注意
注意:前面公式中的偏置表示偏置神经元的权重,它是每一层中添加的一个额外神经元,其 x 值为 +1。偏置神经元很特殊,因为它贡献了下一层的输入,但与上一层没有连接。然而,它的权重仍然像其他神经元一样被正常学习。偏置神经元的直觉是,它为线性回归方程提供了常数项 b:
当然,应用神经元激活函数 在 A 上,不能简单地产生一个二进制(0 或 1)值,因为如果多个类别都被赋予了 1 的分数,我们就无法正确地排序最终的候选答案。相反,我们使用提供 0 到 1 之间非离散分数的激活函数,并设置一个阈值(例如 0.5)来决定是否激活神经元。
最常用的激活函数之一是 sigmoid 函数:
下图展示了如何使用 sigmoid 激活函数根据输入和权重计算神经元的输出:
使用 sigmoid 函数计算神经元输出
其他常用的激活函数包括双曲正切 和 修正线性单元(ReLu):
。当有很多层时,ReLu 的表现更好,因为它提供了稀疏的激活神经元,从而减少噪音并加快学习速度。
前馈传播用于模型评分时,但在训练神经网络的权重矩阵时,一种常用的方法叫做反向传播(en.wikipedia.org/wiki/Backpropagation
)。
以下高层步骤描述了训练是如何进行的:
-
随机初始化权重矩阵(最好使用较小的值,例如
)。
-
使用之前描述的前向传播方法,对所有训练样本进行计算,使用你选择的激活函数计算每个神经元的输出。
-
为你的神经网络实现一个成本函数。成本函数量化了与训练样本的误差。可以与反向传播算法一起使用的成本函数有多种,例如均方误差(
en.wikipedia.org/wiki/Mean_squared_error
)和交叉熵(en.wikipedia.org/wiki/Cross_entropy
)。 -
使用反向传播来最小化你的成本函数并计算权重矩阵。反向传播的基本思想是从输出层的激活值开始,计算与训练数据的误差,并将这些误差反向传递到隐藏层。然后,这些误差会被调整,以最小化步骤 3 中实现的成本函数。
注意
注意:详细解释这些成本函数以及它们如何被优化超出了本书的范围。若想深入了解,我强烈推荐阅读 MIT 出版社的《深度学习》一书(Ian Goodfellow、Yoshua Bengio 和 Aaron Courville)。
在本节中,我们从高层次讨论了神经网络的工作原理以及它们是如何训练的。当然,我们只触及了这项激动人心的技术的皮毛,但希望你应该能大致了解它们的工作方式。在接下来的部分,我们将开始研究 TensorFlow,这是一个帮助抽象实现神经网络底层复杂性的编程框架。
开始使用 TensorFlow
除了 TensorFlow (www.tensorflow.org
) 之外,我还可以选择多个开源深度学习框架用于这个示例应用程序。
以下是一些最流行的框架:
-
PyTorch (
pytorch.org
) -
Caffee2 (
caffe2.ai
) -
MXNet (
mxnet.apache.org
) -
Keras (
keras.io
):一个高级神经网络抽象 API,能够运行其他深度学习框架,如 TensorFlow、CNTK (github.com/Microsoft/cntk
) 和 Theano (github.com/Theano/Theano
)
TensorFlow API 支持多种语言:Python、C++、Java、Go,最近还包括 JavaScript。我们可以将 API 分为两类:高级和低级,具体如下图所示:
TensorFlow 高级 API 架构
为了开始使用 TensorFlow API,让我们构建一个简单的神经网络,学习 XOR 转换。
提醒一下,XOR 运算符只有四个训练样本:
X | Y | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
有趣的是,线性分类器 (en.wikipedia.org/wiki/Linear_classifier
) 无法学习 XOR 转换。然而,我们可以通过一个简单的神经网络来解决这个问题,该网络有两个输入层神经元、一个隐藏层(包含两个神经元)和一个输出层(包含一个神经元,进行二分类),如下所示:
XOR 神经网络
注意
注意:你可以通过以下命令直接从 Notebook 安装 TensorFlow:
!pip install tensorflow
像往常一样,在成功安装任何内容后,别忘了重启内核。
为了创建输入层和输出层的张量,我们使用tf.placeholder
API,如下代码所示:
import tensorflow as tf
x_input = tf.placeholder(tf.float32)
y_output = tf.placeholder(tf.float32)
然后,我们使用tf.Variable
API (www.tensorflow.org/programmers_guide/variables
) 初始化矩阵的随机值!TensorFlow 入门 和 ,分别对应隐藏层和输出层:
eps = 0.01
W1 = tf.Variable(tf.random_uniform([2,2], -eps, eps))
W2 = tf.Variable(tf.random_uniform([2,1], -eps, eps))
对于激活函数,我们使用 sigmoid 函数:
注意
注意:为了简化,我们省略了偏置的介绍。
layer1 = tf.sigmoid(tf.matmul(x_input, W1))
output_layer = tf.sigmoid(tf.matmul(layer1, W2))
对于损失函数,我们使用MSE(即均方误差):
cost = tf.reduce_mean(tf.square(y_output - output_layer))
在图中的所有张量就位后,我们可以使用tf.train.GradientDescentOptimizer
,学习率为0.05
,来最小化我们的损失函数,开始训练:
train = tf.train.GradientDescentOptimizer(0.05).minimize(cost)
training_data = ([[0,0],[0,1],[1,0],[1,1]], [[0],[1],[1],[0]])
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(5000):
sess.run(train,
feed_dict={x_input: training_data[0], y_output: training_data[1]})
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode1.py
上述代码首次引入了 TensorFlow Session
的概念,这是框架的基础部分。实际上,任何 TensorFlow 操作必须在Session
的上下文中执行,使用其run
方法。会话还维护需要显式释放的资源,通过close
方法来释放。为了方便,Session
类通过提供__enter__
和__exit__
方法支持上下文管理协议。这允许调用者使用with
语句 (docs.python.org/3/whatsnew/2.6.html#pep-343-the-with-statement
) 来调用 TensorFlow 操作,并自动释放资源。
以下伪代码展示了一个典型的 TensorFlow 执行结构:
with tf.Session() as sess:
with-block statement with TensorFlow operations
在本节中,我们快速探讨了低级 TensorFlow API,构建了一个简单的神经网络,学习了 XOR 转换。在下一节中,我们将探讨提供高级抽象层的更高层次的估计器 API。
使用 DNNClassifier 进行简单的分类
注意
注意:本节讨论了一个示例 PixieApp 的源代码。如果你想跟着操作,可能更容易直接下载完整的 Notebook 文件,位于这个位置:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/TensorFlow%20classification.ipynb
在我们开始使用低级 TensorFlow API 中的张量、图和会话之前,先熟悉一下 Estimators
包提供的高级 API 是很有帮助的。在这一部分,我们构建了一个简单的 PixieApp,它接受 pandas DataFrame 作为输入,并训练一个具有类别输出的分类模型。
注意
注意:分类输出基本上有两种类型:类别型和连续型。在类别型分类模型中,输出只能从有限的预定义值列表中选择,且可能有或没有逻辑顺序。我们通常称二分类为只有两个类别的分类模型。另一方面,连续输出可以有任何数值。
用户首先需要选择一个数值列进行预测,然后使用数据框中所有其他数值列训练一个分类模型。
注意
注意:这个示例应用的一些代码改编自 github.com/tensorflow/models/tree/master/samples/core/get_started
。
对于这个示例,我们将使用内置的示例数据集 #7:波士顿犯罪数据,两周的样本数据,但你也可以使用任何其他数据集,只要它有足够的数据和数值列。
提醒一下,你可以使用以下代码浏览 PixieDust 内置的数据集:
import pixiedust
pixiedust.sampleData()
PixieDust 中的内置数据集列表
以下代码使用 sampleData()
API 加载 波士顿犯罪 数据集:
import pixiedust
crimes = pixiedust.sampleData(7, forcePandas=True)
和往常一样,我们首先通过 display()
命令探索数据。这里的目标是寻找一个合适的列进行预测:
display(crimes)
犯罪数据集的表格视图
看起来 nonviolent
是一个适合二分类的良好候选项。现在让我们展示一个条形图,以确保该列的数据分布良好:
在选项对话框中选择非暴力列
点击 OK 会生成以下图表:
非暴力犯罪分布
不幸的是,数据倾向于非暴力犯罪,但我们有接近 2,000 个暴力犯罪的数据点,对于这个示例应用程序来说,应该足够了。
我们现在准备创建 do_training
方法,使用 tf.estimator.DNNClassifier
创建一个分类模型。
注意
注意:你可以在这里找到更多关于 DNNClassifier
和其他高级 TensorFlow 估算器的信息:
www.tensorflow.org/api_docs/python/tf/estimator
DNNClassifier
构造函数有很多可选参数。在我们的示例应用中,我们只会使用其中三个,但我鼓励你查看文档中的其他参数:
-
feature_columns
:feature_column._FeatureColumn
模型输入的可迭代对象。在我们的例子中,我们可以使用 Python 推导式仅通过 pandas DataFrame 的数值列创建一个数组。 -
hidden_units
:每个单元隐藏层数的可迭代对象。在这里,我们只使用两个层,每个层有 10 个节点。 -
n_classes
:标签类别的数量。我们将通过对预测列进行分组并计算行数来推断此数字。
这是do_training
方法的代码:
def do_training(train, train_labels, test, test_labels, num_classes):
#set TensorFlow logging level to INFO
tf.logging.set_verbosity(tf.logging.INFO)
# Build 2 hidden layer DNN with 10, 10 units respectively.
classifier = tf.estimator.DNNClassifier(
# Compute feature_columns from dataframe keys using a list comprehension
feature_columns =
[tf.feature_column.numeric_column(key=key) for key in train.keys()],
hidden_units=[10, 10],
n_classes=num_classes)
# Train the Model
classifier.train(
input_fn=lambda:train_input_fn(train, train_labels,100),
steps=1000
)
# Evaluate the model
eval_result = classifier.evaluate(
input_fn=lambda:eval_input_fn(test, test_labels,100)
)
return (classifier, eval_result)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode2.py
classifier.train
方法使用一个train_input_fn
方法,负责以小批量的形式提供训练输入数据(即真实标签),返回一个tf.data.Dataset
或(features, labels)
元组。我们的代码还通过classifier.evaluate
进行模型评估,通过对测试数据集进行评分并将结果与给定标签进行比较来验证准确性。结果随后作为函数输出的一部分返回。
此方法需要一个与train_input_fn
类似的eval_input_fn
方法,唯一的区别是在评估过程中我们不使数据集可重复。由于这两个方法共享大部分相同的代码,我们使用一个名为input_fn
的辅助方法,该方法由两个方法调用,并带有适当的标志:
def input_fn(features, labels, batch_size, train):
# Convert the inputs to a Dataset and shuffle.
dataset = tf.data.Dataset.from_tensor_slices((dict(features), labels)).shuffle(1000)
if train:
#repeat only for training
dataset = dataset.repeat()
# Return the dataset in batch
return dataset.batch(batch_size)
def train_input_fn(features, labels, batch_size):
return input_fn(features, labels, batch_size, train=True)
def eval_input_fn(features, labels, batch_size):
return input_fn(features, labels, batch_size, train=False)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode3.py
下一步是构建 PixieApp,它将从传递给run
方法的 pandas DataFrame 创建分类器。主屏幕构建了所有数值列的下拉列表,并要求用户选择一个将用作分类器输出的列。这是通过以下代码完成的,使用 Jinja2 {%for ...%}
循环遍历作为输入传递的 DataFrame,DataFrame 通过pixieapp_entity
变量引用。
注意
注意:以下代码使用[[SimpleClassificationDNN]]
符号表示它是指定类的不完整代码。请勿尝试运行此代码,直到提供完整实现为止。
[[SimpleClassificationDNN]]
from pixiedust.display.app import *
@PixieApp
class SimpleClassificationDNN():
@route()
def main_screen(self):
return """
<h1 style="margin:40px">
<center>The classificiation model will be trained on all the numeric columns of the dataset</center>
</h1>
<style>
div.outer-wrapper {
display: table;width:100%;height:300px;
}
div.inner-wrapper {
display: table-cell;vertical-align: middle;height: 100%;width: 100%;
}
</style>
<div class="outer-wrapper">
<div class="inner-wrapper">
<div class="col-sm-3"></div>
<div class="input-group col-sm-6">
<select id="cols{{prefix}}" style="width:100%;height:30px" pd_options="predictor=$val(cols{{prefix}})">
<option value="0">Select a predictor column</option>
{%for col in this.pixieapp_entity.columns.values.tolist()%}
<option value="{{col}}">{{col}}</option>
{%endfor%}
</select>
</div>
</div>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode4.py
使用crimes
数据集,我们通过以下代码运行 PixieApp:
app = SimpleClassificationDNN()
app.run(crimes)
注意
注意:此时 PixieApp 代码尚不完整,但我们仍然可以看到欢迎页面的结果,如下图所示:
显示输入 pandas DataFrame 列表的主屏幕
当用户选择预测列(例如 nonviolent
)时,通过属性 pd_options="predictor=$val(cols{{prefix}})"
会触发一个新的 prepare_training
路由。该路由将显示两个条形图,分别显示训练集和测试集的输出类别分布,这些数据是通过从原始数据集中以 80/20 的比例随机选取得到的。
注意
注意:我们在训练集和测试集之间使用 80/20 的分割比例,从我的经验来看,这种做法很常见。当然,这不是绝对规则,根据具体情况可以进行调整。
屏幕片段还包括一个按钮,用于启动训练分类器。
prepare_training
路由的代码如下所示:
[[SimpleClassificationDNN]]
@route(predictor="*")
@templateArgs
def prepare_training(self, predictor):
#select only numerical columns
self.dataset = self.pixieapp_entity.dropna(axis=1).select_dtypes(
include=['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
)
#Compute the number of classed by counting the groups
self.num_classes = self.dataset.groupby(predictor).size().shape[0]
#Create the train and test feature and labels
self.train_x=self.dataset.sample(frac=0.8)
self.full_train = self.train_x.copy()
self.train_y = self.train_x.pop(predictor)
self.test_x=self.dataset.drop(self.train_x.index)
self.full_test = self.test_x.copy()
self.test_y=self.test_x.pop(predictor)
bar_chart_options = {
"rowCount": "100",
"keyFields": predictor,
"handlerId": "barChart",
"noChartCache": "true"
}
return """
<div class="container" style="margin-top:20px">
<div class="row">
<div class="col-sm-5">
<h3><center>Train set class distribution</center></h3>
<div pd_entity="full_train" pd_render_onload>
<pd_options>{{bar_chart_options|tojson}}</pd_options>
</div>
</div>
<div class="col-sm-5">
<h3><center>Test set class distribution</center></h3>
<div pd_entity="full_test" pd_render_onload>
<pd_options>{{bar_chart_options|tojson}}</pd_options>
</div>
</div>
</div>
</div>
<div style="text-align:center">
<button class="btn btn-default" type="submit" pd_options="do_training=true">
Start Training
</button>
</div>
"""
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode5.py
注意:由于我们计算了 bar_chart_options
变量一次,并且在 Jinja2 模板中使用它,所以使用了 @templateArgs
。
选择 nonviolent
预测列将给我们以下截图结果:
预训练屏幕
开始训练 按钮通过属性 pd_options="do_training=true",
调用 do_training
路由,该路由触发我们之前创建的 do_training
方法。注意,我们使用了 @captureOutput
装饰器,因为我们将 TensorFlow 日志级别设置为 INFO
,所以我们希望捕获日志消息并将其显示给用户。这些日志消息会通过 stream 模式返回到浏览器,PixieDust 会自动将它们显示为专门创建的 <div>
元素,并随着数据的到达动态追加到该元素中。当训练完成时,路由返回一个 HTML 片段,生成一个表格,显示 do_training
方法返回的评估指标,如下所示的代码:
[[SimpleClassificationDNN]]
@route(do_training="*")
@captureOutput
def do_training_screen(self):
self.classifier, self.eval_results = \
do_training(
self.train_x, self.train_y, self.test_x, self.test_y, self.num_classes
)
return """
<h2>Training completed successfully</h2>
<table>
<thead>
<th>Metric</th>
<th>Value</th>
</thead>
<tbody>
{%for key,value in this.eval_results.items()%}
<tr>
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
{%endfor%}
</tbody>
</table>
"""
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode6.py
以下截图显示了模型成功创建后的结果,并包括分类模型的评估指标表,准确率为 87%:
显示成功训练结果的最终屏幕
这个 PixieApp 使用 crimes
数据集作为参数运行,如下所示的代码所示:
app = SimpleClassificationDNN()
app.run(crimes)
一旦模型成功训练,你可以通过在 app.classifier
变量上调用 predict
方法来分类新数据。与 train
和 evaluate
方法类似,predict
也接受一个 input_fn
,用于构造输入特征。
注意
注意:有关 predict
方法的更多细节,请参见此处:
www.tensorflow.org/api_docs/python/tf/estimator/DNNClassifier#predict
这个示例应用程序通过使用高层次的估算器 API,为熟悉 TensorFlow 框架提供了一个很好的起点。
注意
注意:此示例应用程序的完整笔记本可以在这里找到:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/TensorFlow%20classification.ipynb
在下一部分,我们将开始使用低级 TensorFlow API(包括张量、图和会话)构建我们的图像识别示例应用程序。
图像识别示例应用程序
当谈到构建一个开放式应用程序时,你应该从定义MVP(即最小可行产品)版本的需求开始,该版本仅包含足够的功能,足以使其对用户有用且有价值。在做技术决策时,确保你能够尽快获得一个完整的端到端实现,而不会投入过多时间,这是一个非常重要的标准。其核心思想是,你需要从小做起,这样你可以快速迭代并改进应用程序。
对于我们图像识别示例应用程序的 MVP,我们将使用以下要求:
-
不要从头开始构建模型;而是重用公开可用的预训练通用卷积神经网络(CNN:
en.wikipedia.org/wiki/Convolutional_neural_network
)模型,如 MobileNet。我们可以稍后使用迁移学习(en.wikipedia.org/wiki/Transfer_learning
)用自定义训练图像重新训练这些模型。 -
对于 MVP,我们虽然只关注评分而不涉及训练,但仍应确保应用程序对用户有吸引力。所以让我们构建一个 PixieApp,允许用户输入网页的 URL,并显示从页面中抓取的所有图片,包括我们的模型推断的分类输出。
-
由于我们正在学习深度学习神经网络和 TensorFlow,如果我们能够在 Jupyter Notebook 中直接显示 TensorBoard 图形可视化(
www.tensorflow.org/programmers_guide/graph_viz
),而不强迫用户使用其他工具,那将会非常棒。这将提供更好的用户体验,并增强用户与应用程序的互动。
注意
注意:本节中的应用程序实现是根据以下教程改编的:
codelabs.developers.google.com/codelabs/tensorflow-for-poets
第一部分 – 加载预训练的 MobileNet 模型
注意
注意:你可以下载完成的 Notebook 来跟进本节讨论:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%201.ipynb
有很多公开可用的图像分类模型,使用 CNNs,且在如 ImageNet 等大型图像数据库上进行了预训练。ImageNet 发起了多个公开挑战,如ImageNet 大规模视觉识别挑战赛(ILSVRC)或 Kaggle 上的ImageNet 物体定位挑战(www.kaggle.com/c/imagenet-object-localization-challenge
),并取得了非常有趣的结果。
这些挑战催生了多个模型,如 ResNet、Inception、SqueezeNet、VGGNet 或 Xception,每个模型都使用不同的神经网络架构。详细讲解每个架构超出了本书的范围,但即使你还不是机器学习专家(我也绝对不是),我也鼓励你在网上阅读相关内容。为了这个示例应用,我选择了 MobileNet 模型,因为它小巧、快速且非常准确。它提供了一个包含 1,000 个类别的图像分类模型,足以满足此示例应用的需求。
为了确保代码的稳定性,我已在 GitHub 仓库中创建了模型的副本:github.com/DTAIEB/Thoughtful-Data-Science/tree/master/chapter%206/Visual%20Recognition/mobilenet_v1_0.50_224
。
在这个目录中,你可以找到以下文件:
-
frozen_graph.pb
:TensorFlow 图的序列化二进制版本 -
labels.txt
:包含 1,000 个图像类别及其索引的文本文件 -
quantized_graph.pb
:采用 8 位定点表示的模型图的压缩形式
加载模型的过程包括构建一个tf.graph
对象及相关标签。由于未来可能会加载多个模型,因此我们首先定义一个字典,用来提供有关模型的元数据:
models = {
"mobilenet": {
"base_url":"https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%206/Visual%20Recognition/mobilenet_v1_0.50_224",
"model_file_url": "frozen_graph.pb",
"label_file": "labels.txt",
"output_layer": "MobilenetV1/Predictions/Softmax"
}
}
注意
你可以在这里找到文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode7.py
在前面的models
字典中,每个键代表特定模型的元数据:
-
base_url
:指向文件存储的 URL -
model_file_url
:假定相对于base_url
的模型文件名称 -
label_file
:假定相对于base_url
的标签文件名称 -
output_layer
:提供每个类别最终得分的输出层名称
我们实现了一个get_model_attribute
辅助方法,以便从model
元数据中读取内容,这在我们整个应用程序中都非常有用:
# helper method for reading attributes from the model metadata
def get_model_attribute(model, key, default_value = None):
if key not in model:
if default_value is None:
raise Exception("Require model attribute {} not found".format(key))
return default_value
return model[key]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode8.py
为了加载图形,我们下载二进制文件,使用ParseFromString
方法将其加载到tf.GraphDef
对象中,然后我们调用tf.import_graph_def
方法,将图形作为当前内容管理器:
import tensorflow as tf
import requests
# Helper method for resolving url relative to the selected model
def get_url(model, path):
return model["base_url"] + "/" + path
# Download the serialized model and create a TensorFlow graph
def load_graph(model):
graph = tf.Graph()
graph_def = tf.GraphDef()
graph_def.ParseFromString(
requests.get( get_url( model, model["model_file_url"] ) ).content
)
with graph.as_default():
tf.import_graph_def(graph_def)
return graph
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode9.py
加载标签的方法返回一个 JSON 对象或一个数组(稍后我们会看到这两者都需要)。以下代码使用 Python 列表推导式迭代requests.get
调用返回的行。然后,它使用as_json
标志将数据格式化为适当的形式:
# Load the labels
def load_labels(model, as_json = False):
labels = [line.rstrip() \
for line in requests.get(get_url(model, model["label_file"]) ).text.split("\n") if line != ""]
if as_json:
return [{"index": item.split(":")[0],"label":item.split(":")[1]} for item in labels]
return labels
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode10.py
下一步是调用模型来分类图像。为了简化操作并可能提高其价值,我们要求用户提供一个包含待分类图像的 HTML 页面的 URL。我们将使用 BeautifulSoup4 库来帮助解析页面。要安装 BeautifulSoup4,只需运行以下命令:
!pip install beautifulsoup4
注意
注意:像往常一样,安装完成后不要忘记重启内核。
以下get_image_urls
方法接受一个 URL 作为输入,下载 HTML,实例化一个 BeautifulSoup 解析器,并提取所有<img>
元素和background-image
样式中找到的图像。BeautifulSoup 提供了一个非常优雅且易于使用的 API 来解析 HTML。在这里,我们只使用find_all
方法来查找所有的<img>
元素,并使用select
方法选择所有具有内联样式的元素。读者很快会注意到,我们没有探索通过 HTML 创建图像的其他方式,例如,作为 CSS 类声明的图像。像往常一样,如果你有兴趣和时间改进它,我非常欢迎你在 GitHub 仓库中提交拉取请求(关于如何创建拉取请求,请参阅此处:help.github.com/articles/creating-a-pull-request
)。
get_image_urls
的代码如下:
from bs4 import BeautifulSoup as BS
import re
# return an array of all the images scraped from an html page
def get_image_urls(url):
# Instantiate a BeautifulSoup parser
soup = BS(requests.get(url).text, "html.parser")
# Local helper method for extracting url
def extract_url(val):
m = re.match(r"url\((.*)\)", val)
val = m.group(1) if m is not None else val
return "http:" + val if val.startswith("//") else val
# List comprehension that look for <img> elements and backgroud-image styles
return [extract_url(imgtag['src']) for imgtag in soup.find_all('img')] + [ \
extract_url(val.strip()) for key,val in \
[tuple(selector.split(":")) for elt in soup.select("[style]") \
for selector in elt["style"].strip(" ;").split(";")] \
if key.strip().lower()=='background-image' \
]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode11.py
对于发现的每一张图片,我们还需要一个辅助函数来下载这些图片,这些图片将作为输入传递给模型进行分类。
以下download_image
方法将图片下载到临时文件:
import tempfile
def download_image(url):
response = requests.get(url, stream=True)
if response.status_code == 200:
with tempfile.NamedTemporaryFile(delete=False) as f:
for chunk in response.iter_content(2048):
f.write(chunk)
return f.name
else:
raise Exception("Unable to download image: {}".format(response.status_code))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode12.py
给定一张本地路径的图片,我们现在需要通过调用tf.image
包中的正确解码方法将其解码为张量,也就是.png
文件需要使用decode_png
方法。
注意
注意:在数学中,张量是向量的一个推广,向量由方向和大小定义,张量则支持更高的维度。向量是 1 阶张量,同样,标量是 0 阶张量。直观地讲,我们可以把 2 阶张量看作一个二维数组,其中的值是通过乘以两个向量得到的结果。在 TensorFlow 中,张量是 n 维数组。
在对图片读取器张量进行一些转换(转换为正确的十进制表示、调整大小和归一化)之后,我们在归一化器张量上调用tf.Session.run
以执行之前定义的步骤,如以下代码所示:
# decode a given image into a tensor
def read_tensor_from_image_file(model, file_name):
file_reader = tf.read_file(file_name, "file_reader")
if file_name.endswith(".png"):
image_reader = tf.image.decode_png(file_reader, channels = 3,name='png_reader')
elif file_name.endswith(".gif"):
image_reader = tf.squeeze(tf.image.decode_gif(file_reader,name='gif_reader'))
elif file_name.endswith(".bmp"):
image_reader = tf.image.decode_bmp(file_reader, name='bmp_reader')
else:
image_reader = tf.image.decode_jpeg(file_reader, channels = 3, name='jpeg_reader')
float_caster = tf.cast(image_reader, tf.float32)
dims_expander = tf.expand_dims(float_caster, 0);
# Read some info from the model metadata, providing default values
input_height = get_model_attribute(model, "input_height", 224)
input_width = get_model_attribute(model, "input_width", 224)
input_mean = get_model_attribute(model, "input_mean", 0)
input_std = get_model_attribute(model, "input_std", 255)
resized = tf.image.resize_bilinear(dims_expander, [input_height, input_width])
normalized = tf.divide(tf.subtract(resized, [input_mean]), [input_std])
sess = tf.Session()
result = sess.run(normalized)
return result
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode13.py
在所有部分准备好后,我们现在可以实现score_image
方法,该方法接受tf.graph
、模型元数据和图片的 URL 作为输入参数,并根据置信度分数返回前五个候选分类,包括它们的标签:
import numpy as np
# classify an image given its url
def score_image(graph, model, url):
# Get the input and output layer from the model
input_layer = get_model_attribute(model, "input_layer", "input")
output_layer = get_model_attribute(model, "output_layer")
# Download the image and build a tensor from its data
t = read_tensor_from_image_file(model, download_image(url))
# Retrieve the tensors corresponding to the input and output layers
input_tensor = graph.get_tensor_by_name("import/" + input_layer + ":0");
output_tensor = graph.get_tensor_by_name("import/" + output_layer + ":0");
with tf.Session(graph=graph) as sess:
results = sess.run(output_tensor, {input_tensor: t})
results = np.squeeze(results)
# select the top 5 candidate and match them to the labels
top_k = results.argsort()[-5:][::-1]
labels = load_labels(model)
return [(labels[i].split(":")[1], results[i]) for i in top_k]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode14.py
我们现在可以使用以下步骤来测试代码:
-
选择
mobilenet
模型并加载对应的图 -
获取从 Flickr 网站抓取的图片 URL 列表
-
对每个图片 URL 调用
score_image
方法并打印结果
代码如下所示:
model = models['mobilenet']
graph = load_graph(model)
image_urls = get_image_urls("https://www.flickr.com/search/?text=cats")
for url in image_urls:
results = score_image(graph, model, url)
print("Result for {}: \n\t{}".format(url, results))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode15.py
结果非常准确(除了第一张是空白图片),如以下截图所示:
对与猫相关的 Flickr 页面上发现的图片进行分类
我们的图像识别示例应用程序的第一部分现已完成;您可以在以下位置找到完整的 Notebook:github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%201.ipynb
。
在接下来的部分中,我们将通过构建 PixieApp 的用户界面来构建一个更加用户友好的体验。
第二部分 – 创建一个 PixieApp 用于我们的图像识别示例应用程序
注意
注意:您可以在此下载完成的 Notebook,以便跟随本部分的讨论:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%202.ipynb
提醒一下,PixieApp 的setup
方法(如果定义的话)会在应用程序开始运行之前执行。我们用它来选择模型并初始化图形:
from pixiedust.display.app import *
@PixieApp
class ScoreImageApp():
def setup(self):
self.model = models["mobilenet"]
self.graph = load_graph( self.model )
...
注意
您可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode16.py
在 PixieApp 的主屏幕上,我们使用一个输入框让用户输入网页的 URL,如下所示的代码片段所示:
[[ScoreImageApp]]
@route()
def main_screen(self):
return """
<style>
div.outer-wrapper {
display: table;width:100%;height:300px;
}
div.inner-wrapper {
display: table-cell;vertical-align: middle;height: 100%;width: 100%;
}
</style>
<div class="outer-wrapper">
<div class="inner-wrapper">
<div class="col-sm-3"></div>
<div class="input-group col-sm-6">
<input id="url{{prefix}}" type="text" class="form-control"
value="https://www.flickr.com/search/?text=cats"
placeholder="Enter a url that contains images">
<span class="input-group-btn">
<button class="btn btn-default" type="button" pd_options="image_url=$val(url{{prefix}})">Go</button>
</span>
</div>
</div>
</div>
"""
注意
您可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode17.py
为了方便起见,我们将输入文本初始化为默认值https://www.flickr.com/search/?text=cats
。
我们现在可以使用以下代码来运行并测试主屏幕:
app = ScoreImageApp()
app.run()
主屏幕看起来是这样的:
图像识别 PixieApp 的主屏幕
注意
注意:这对于测试是好的,但我们应该记住,do_process_url
路由尚未实现,因此,点击Go按钮将会回退到默认路由。
现在让我们实现do_process_url
路由,它会在用户点击Go按钮时触发。该路由首先调用get_image_urls
方法获取图像 URL 列表。然后,我们使用 Jinja2 构建一个 HTML 片段,显示所有图像。对于每个图像,我们异步调用do_score_url
路由,运行模型并显示结果。
以下代码展示了do_process_url
路由的实现:
[[ScoreImageApp]]
@route(image_url="*")
@templateArgs
def do_process_url(self, image_url):
image_urls = get_image_urls(image_url)
return """
<div>
{%for url in image_urls%}
<div style="float: left; font-size: 9pt; text-align: center; width: 30%; margin-right: 1%; margin-bottom: 0.5em;">
<img src="img/{{url}}" style="width: 100%">
<div style="display:inline-block" pd_render_onload pd_options="score_url={{url}}">
</div>
</div>
{%endfor%}
<p style="clear: both;">
</div>
"""
注意
您可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode18.py
注意@templateArgs
装饰器的使用,它允许 Jinja2 片段引用本地的image_urls
变量。
最后,在 do_score_url
路由中,我们调用 score_image
并将结果以列表形式显示:
[[ScoreImageApp]]
@route(score_url="*")
@templateArgs
def do_score_url(self, score_url):
results = score_image(self.graph, self.model, score_url)
return """
<ul style="text-align:left">
{%for label, confidence in results%}
<li><b>{{label}}</b>: {{confidence}}</li>
{%endfor%}
</ul>
"""
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode19.py
以下截图展示了包含猫咪图像的 Flickr 页面结果:
猫咪的图像分类结果
注意
提醒您,您可以在此位置找到完整的 Notebook:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%202.ipynb
我们的 MVP 应用程序几乎完成。在下一节中,我们将直接在 Notebook 中集成 TensorBoard 图形可视化。
第三部分 – 集成 TensorBoard 图形可视化
注意
注意:本节中描述的部分代码改编自位于此处的 deepdream
notebook:
github.com/tensorflow/tensorflow/blob/master/tensorflow/examples/tutorials/deepdream/deepdream.ipynb
您可以在这里下载完整的 Notebook 来跟随本节内容:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%203.ipynb
TensorFlow 提供了一套非常强大的可视化工具,帮助调试和优化应用程序性能。请花点时间在这里探索 TensorBoard 的功能:www.tensorflow.org/programmers_guide/summaries_and_tensorboard
。
这里的一个问题是,将 TensorBoard 服务器配置为与您的 Notebook 一起使用可能会很困难,特别是当您的 Notebooks 托管在云端时,且您几乎无法访问底层操作系统。在这种情况下,配置和启动 TensorBoard 服务器可能会变得几乎不可能。在本节中,我们展示了如何通过将模型图形可视化直接集成到 Notebook 中来解决这个问题,无需任何配置。为了提供更好的用户体验,我们希望将 TensorBoard 可视化功能添加到我们的 PixieApp 中。我们通过将主布局更改为选项卡布局,并将 TensorBoard 可视化分配到单独的选项卡中来实现这一点。方便的是,PixieDust 提供了一个名为 TemplateTabbedApp
的基础 PixieApp,它负责构建选项卡布局。当使用 TemplateTabbedApp
作为基类时,我们需要在 setup
方法中配置选项卡,如下所示:
[[ImageRecoApp]]
from pixiedust.apps.template import TemplateTabbedApp
@PixieApp
class ImageRecoApp(TemplateTabbedApp):
def setup(self):
self.apps = [
{"title": "Score", "app_class": "ScoreImageApp"},
{"title": "Model", "app_class": "TensorGraphApp"},
{"title": "Labels", "app_class": "LabelsApp"}
]
self.model = models["mobilenet"]
self.graph = self.load_graph(self.model)
app = ImageRecoApp()
app.run()
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode20.py
需要注意的是,在前面的代码中,我们已经将LabelsApp
子 PixieApp 添加到了标签页列表中,尽管它尚未实现。因此,正如预期的那样,如果直接运行这段代码,Labels
标签将会失败。
self.apps
包含一个对象数组,用于定义标签页:
-
title
:标签页标题 -
app_class
: 选中标签时运行的 PixieApp
在ImageRecoApp
中,我们配置了三个与三个子 PixieApps 相关联的标签页:我们在第二部分 – 为图像识别示例应用创建 PixieApp中已经创建的ScoreImageApp
,用于显示模型图的TensorGraphApp
,以及用于显示模型中所有标注类别的表格的LabelsApp
。
结果显示在以下截图中:
包含 Score、Model 和 Labels 的标签布局
使用TemplateTabbedApp
超类的另一个优点是,子 PixieApps 是分开定义的,这使得代码更易于维护和重用。
首先来看一下TensorGraphApp
PixieApp。它的主路由返回一个 HTML 片段,该片段从https://tensorboard.appspot.com
的 Iframe 加载tf-graph-basic.build.html
,并使用 JavaScript 加载监听器应用通过tf.Graph.as_graph_def
方法计算得到的序列化图定义。为了确保图定义保持在合理的大小,并避免在浏览器客户端上不必要的性能下降,我们调用strip_consts
方法删除具有大尺寸常量值的张量。
TensorGraphApp
的代码如下所示:
@PixieApp
class TensorGraphApp():
"""Visualize TensorFlow graph."""
def setup(self):
self.graph = self.parent_pixieapp.graph
@route()
@templateArgs
def main_screen(self):
strip_def = self.strip_consts(self.graph.as_graph_def())
code = """
<script>
function load() {{
document.getElementById("{id}").pbtxt = {data};
}}
</script>
<link rel="import" href="https://tensorboard.appspot.com/tf-graph-basic.build.html" onload=load()>
<div style="height:600px">
<tf-graph-basic id="{id}"></tf-graph-basic>
</div>
""".format(data=repr(str(strip_def)), id='graph'+ self.getPrefix()).replace('"', '"')
return """
<iframe seamless style="width:1200px;height:620px;border:0" srcdoc="{{code}}"></iframe>
"""
def strip_consts(self, graph_def, max_const_size=32):
"""Strip large constant values from graph_def."""
strip_def = tf.GraphDef()
for n0 in graph_def.node:
n = strip_def.node.add()
n.MergeFrom(n0)
if n.op == 'Const':
tensor = n.attr['value'].tensor
size = len(tensor.tensor_content)
if size > max_const_size:
tensor.tensor_content = "<stripped {} bytes>".format(size).encode("UTF-8")
return strip_def
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode21.py
注意:子 PixieApps 可以通过self.parent_pixieapp
变量访问其父 PixieApp。
TensorGraphApp
子 PixieApp 的结果屏幕如以下截图所示。它提供了选定模型的 TensorFlow 图的交互式可视化,允许用户浏览不同的节点,并深入探索模型。然而,重要的是要注意,整个可视化是在浏览器内运行的,而没有使用 TensorBoard 服务器。因此,TensorBoard 中的一些功能,如运行时统计信息,是禁用的。
显示 MobileNet V1 的模型图
在LabelsApp
PixieApp 中,我们只是将标签作为 JSON 格式加载,并使用handlerId=tableView
选项在 PixieDust 表格中显示它。
[[LabelsApp]]
@PixieApp
class LabelsApp():
def setup(self):
self.labels = self.parent_pixieapp.load_labels(
self.parent_pixieapp.model, as_json=True
)
@route()
def main_screen(self):
return """
<div pd_render_onload pd_entity="labels">
<pd_options>
{
"table_noschema": "true",
"handlerId": "tableView",
"rowCount": "10000"
}
</pd_options>
</div>
"""
注意
您可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode22.py
注意:我们通过将table_noschema
设置为true
来配置表格,以避免显示模式架构,但为了方便起见,我们保留了搜索栏。
结果如下截图所示:
可搜索的模型类别表格
我们的 MVP 图像识别示例应用程序现在已经完成;您可以在此找到完整的 Notebook:github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%203.ipynb
。
在下一节中,我们将通过允许用户使用自定义图像重新训练模型来改进应用程序。
第四部分 – 使用自定义训练数据重新训练模型
注意
注意:您可以在此下载完整的 Notebook 以便跟随本节的讨论:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%204.ipynb
本节中的代码相当广泛,部分与主题无关的辅助函数将被省略。然而,和往常一样,更多关于代码的信息请参阅 GitHub 上的完整 Notebook。
在本节中,我们将使用自定义训练数据重新训练 MobileNet 模型,并用它来分类那些在通用模型中得分较低的图像。
注意
注意:本节中的代码改编自TensorFlow for poets教程:
github.com/googlecodelabs/tensorflow-for-poets-2/blob/master/scripts/retrain.py
正如大多数时候一样,获取高质量的训练数据可能是最具挑战性且耗时的任务。在我们的示例中,我们需要为每个要训练的类别获取大量图像。为了简便和可复现性,我们使用了 ImageNet 数据库,该数据库方便地提供了获取 URL 和相关标签的 API。我们还将下载的文件限制为.jpg
格式。当然,如果需要,您也可以自行获取训练数据。
我们首先从 2011 年秋季发布的版本下载所有图片 URL 的列表,链接在这里:image-net.org/imagenet_data/urls/imagenet_fall11_urls.tgz
,并将文件解压到你选择的本地目录(例如,我选择了/Users/dtaieb/Downloads/fall11_urls.txt
)。我们还需要下载所有synsets
的 WordNet ID 与单词的映射文件,链接在这里:image-net.org/archive/words.txt
,这个文件将帮助我们找到包含我们需要下载的 URL 的 WordNet ID。
以下代码将分别加载两个文件到 pandas DataFrame 中:
import pandas
wnid_to_urls = pandas.read_csv('/Users/dtaieb/Downloads/fall11_urls.txt',
sep='\t', names=["wnid", "url"],
header=0, error_bad_lines=False,
warn_bad_lines=False, encoding="ISO-8859-1")
wnid_to_urls['wnid'] = wnid_to_urls['wnid'].apply(lambda x: x.split("_")[0])
wnid_to_urls = wnid_to_urls.dropna()
wnid_to_words = pandas.read_csv('/Users/dtaieb/Downloads/words.txt',
sep='\t', names=["wnid", "description"],
header=0, error_bad_lines=False,
warn_bad_lines=False, encoding="ISO-8859-1")
wnid_to_words = wnid_to_words.dropna()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode23.py
请注意,我们需要清理wnid_to_urls
数据集中的wnid
列,因为它包含一个后缀,表示该图片在类别中的索引。
然后我们可以定义一个方法get_url_for_keywords
,它返回一个字典,字典的键是类别,值是包含 URL 的数组:
def get_url_for_keywords(keywords):
results = {}
for keyword in keywords:
df = wnid_to_words.loc[wnid_to_words['description'] == keyword]
row_list = df['wnid'].values.tolist()
descriptions = df['description'].values.tolist()
if len(row_list) > 0:
results[descriptions[0]] = \
wnid_to_urls.loc[wnid_to_urls['wnid'] == \
row_list[0]]["url"].values.tolist()
return results
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode24.py
我们可以使用 PixieDust 的display
轻松查看数据分布。和往常一样,随时可以自己进行更多探索:
按类别分布的图片
现在我们可以构建代码来下载与我们选择的类别列表对应的图片。在我们的例子中,我们选择了水果:["apple", "orange", "pear", "banana"]
。这些图片将下载到 PixieDust 主目录的子目录中(使用 PixieDust 的Environment
助手类,来自pixiedust.utils
包),并限制下载图片的数量为500
,以提高速度:
注意
注意:以下代码使用了 Notebook 中先前定义的方法和导入内容。在尝试运行以下代码之前,请确保先运行相应的单元格。
from pixiedust.utils.environment import Environment
root_dir = ensure_dir_exists(os.path.join(Environment.pixiedustHome, "imageRecoApp")
image_dir = root_dir
image_dict = get_url_for_keywords(["apple", "orange", "pear", "banana"])
with open(os.path.join(image_dir, "retrained_label.txt"), "w") as f_label:
for key in image_dict:
f_label.write(key + "\n")
path = ensure_dir_exists(os.path.join(image_dir, key))
count = 0
for url in image_dict[key]:
download_image_into_dir(url, path)
count += 1
if count > 500:
break;
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode25.py
代码的下一部分处理训练集中的每张图片,使用以下步骤:
注意
注意:如前所述,代码比较长,并且部分代码被省略,这里仅解释了重要部分。请不要直接运行以下代码,完整实现请参阅完整的 Notebook。
-
使用以下代码解码
.jpeg
文件:def add_jpeg_decoding(model): input_height = get_model_attribute(model, "input_height") input_width = get_model_attribute(model, "input_width") input_depth = get_model_attribute(model, "input_depth") input_mean = get_model_attribute(model, "input_mean", 0) input_std = get_model_attribute(model, "input_std", 255) jpeg_data = tf.placeholder(tf.string, name='DecodeJPGInput') decoded_image = tf.image.decode_jpeg(jpeg_data, channels=input_depth) decoded_image_as_float = tf.cast(decoded_image, dtype=tf.float32) decoded_image_4d = tf.expand_dims( decoded_image_as_float, 0) resize_shape = tf.stack([input_height, input_width]) resize_shape_as_int = tf.cast(resize_shape, dtype=tf.int32) resized_image = tf.image.resize_bilinear( decoded_image_4d, resize_shape_as_int) offset_image = tf.subtract(resized_image, input_mean) mul_image = tf.multiply(offset_image, 1.0 / input_std) return jpeg_data, mul_image
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode26.py
-
创建瓶颈值(根据需要进行缓存),通过调整图像大小和缩放来标准化图像。这是在以下代码中完成的:
def run_bottleneck_on_image(sess, image_data, image_data_tensor,decoded_image_tensor, resized_input_tensor,bottleneck_tensor): # First decode the JPEG image, resize it, and rescale the pixel values. resized_input_values = sess.run(decoded_image_tensor, {image_data_tensor: image_data}) # Then run it through the recognition network. bottleneck_values = sess.run( bottleneck_tensor, {resized_input_tensor: resized_input_values}) bottleneck_values = np.squeeze(bottleneck_values) return bottleneck_values
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode27.py
-
使用
add_final_training_ops
方法添加最终训练操作,放在一个公共命名空间下,方便在可视化图时进行操作。训练步骤如下:-
使用
tf.truncated_normal
API 生成随机权重:initial_value = tf.truncated_normal( [bottleneck_tensor_size, class_count], stddev=0.001) layer_weights = tf.Variable( initial_value, name='final_weights')
-
添加偏置,初始化为零:
layer_biases = tf.Variable(tf.zeros([class_count]), name='final_biases')
-
计算加权和:
logits = tf.matmul(bottleneck_input, layer_weights) + layer_biases
-
添加
cross_entropy
成本函数:cross_entropy = tf.nn.softmax_cross_entropy_with_logits( labels=ground_truth_input, logits=logits) with tf.name_scope('total'): cross_entropy_mean = tf.reduce_mean( cross_entropy)
-
最小化成本函数:
optimizer = tf.train.GradientDescentOptimizer( learning_rate) train_step = optimizer.minimize(cross_entropy_mean)
-
为了可视化重新训练后的图,我们首先需要更新TensorGraphApp
PixieApp,让用户选择可视化的模型:通用的 MobileNet 还是自定义模型。通过在主路由中添加<select>
下拉菜单并附加pd_script
元素来更新状态:
[[TensorGraphApp]]
return """
{%if this.custom_graph%}
<div style="margin-top:10px" pd_refresh>
<pd_script>
self.graph = self.custom_graph if self.graph is not self.custom_graph else self.parent_pixieapp.graph
</pd_script>
<span style="font-weight:bold">Select a model to display:</span>
<select>
<option {%if this.graph!=this.custom_graph%}selected{%endif%} value="main">MobileNet</option>
<option {%if this.graph==this.custom_graph%}selected{%endif%} value="custom">Custom</options>
</select>
{%endif%}
<iframe seamless style="width:1200px;height:620px;border:0" srcdoc="{{code}}"></iframe>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode28.py
重新运行我们的ImageReco
PixieApp 生成以下截图:
可视化重新训练后的图
点击火车节点将显示运行反向传播算法的嵌套操作,以最小化前面add_final_training_ops
中指定的cross_entropy_mean
成本函数:
with tf.name_scope('cross_entropy'):
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(
labels=ground_truth_input, logits=logits)
with tf.name_scope('total'):
cross_entropy_mean = tf.reduce_mean(cross_entropy)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode29.py
以下截图展示了train命名空间的详细信息:
训练过程中的反向传播
类似地,我们可以在LabelsApp
中添加下拉切换,以切换通用 MobileNet 和自定义模型之间的可视化:
[[LabelsApp]]
@PixieApp
class LabelsApp():
def setup(self):
...
@route()
def main_screen(self):
return """
{%if this.custom_labels%}
<div style="margin-top:10px" pd_refresh>
<pd_script>
self.current_labels = self.custom_labels if self.current_labels is not self.custom_labels else self.labels
</pd_script>
<span style="font-weight:bold">
Select a model to display:</span>
<select>
<option {%if this.current_labels!=this.labels%}selected{%endif%} value="main">MobileNet</option>
<option {%if this.current_labels==this.custom_labels%}selected{%endif%} value="custom">Custom</options>
</select>
{%endif%}
<div pd_render_onload pd_entity="current_labels">
<pd_options>
{
"table_noschema": "true",
"handlerId": "tableView",
"rowCount": "10000",
"noChartCache": "true"
}
</pd_options>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode30.py
结果显示在以下截图中:
显示每个模型的标签信息
第四部分 MVP 的最后一步是更新 score_image
方法,使其同时使用两个模型对图像进行分类,并将结果以字典形式存储,其中每个模型有一个条目。我们定义了一个本地方法 do_score_image
,该方法返回前 5 个候选答案。
该方法会为每个模型调用,并将结果填充到一个字典中,字典的键是模型名称:
# classify an image given its url
def score_image(graph, model, url):
# Download the image and build a tensor from its data
t = read_tensor_from_image_file(model, download_image(url))
def do_score_image(graph, output_layer, labels):
# Retrieve the tensors corresponding to the input and output layers
input_tensor = graph.get_tensor_by_name("import/" +
input_layer + ":0");
output_tensor = graph.get_tensor_by_name( output_layer +
":0");
with tf.Session(graph=graph) as sess:
# Initialize the variables
sess.run(tf.global_variables_initializer())
results = sess.run(output_tensor, {input_tensor: t})
results = np.squeeze(results)
# select the top 5 candidates and match them to the labels
top_k = results.argsort()[-5:][::-1]
return [(labels[i].split(":")[1], results[i]) for i in top_k]
results = {}
input_layer = get_model_attribute(model, "input_layer",
"input")
labels = load_labels(model)
results["mobilenet"] = do_score_image(graph, "import/" +
get_model_attribute(model, "output_layer"), labels)
if "custom_graph" in model and "custom_labels" in model:
with open(model["custom_labels"]) as f:
labels = [line.rstrip() for line in f.readlines() if line != ""]
custom_labels = ["{}:{}".format(i, label) for i,label in zip(range(len(labels)), labels)]
results["custom"] = do_score_image(model["custom_graph"],
"final_result", custom_labels)
return results
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode31.py
由于我们修改了 score_image
方法返回的值,我们需要调整 ScoreImageApp
中返回的 HTML 片段,以便遍历 results
字典中的所有模型条目:
@route(score_url="*")
@templateArgs
def do_score_url(self, score_url):
scores_dict = score_image(self.graph, self.model, score_url)
return """
{%for model, results in scores_dict.items()%}
<div style="font-weight:bold">{{model}}</div>
<ul style="text-align:left">
{%for label, confidence in results%}
<li><b>{{label}}</b>: {{confidence}}</li>
{%endfor%}
</ul>
{%endfor%}
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/sampleCode32.py
在这些更改生效后,PixieApp 将会自动调用可用的自定义模型,并且如果存在自定义模型,它会显示两个模型的结果。
下图显示了与 香蕉 相关的图像的结果:
使用通用的 MobileNet 和自定义训练模型进行评分
读者会注意到自定义模型的分数相当低。一个可能的解释是,训练数据获取是完全自动化的,并且没有人工筛选。对这个示例应用程序的一个可能改进是,将训练数据获取和再训练步骤移到一个独立的 PixieApp 标签页中。我们还应当给用户机会验证图像,并拒绝质量差的图像。让用户重新标注错误分类的图像也是一个不错的主意。
注意
第四部分的完整 Notebook 可以在这里找到:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%206/Tensorflow%20VR%20Part%204.ipynb
本节讨论了使用 TensorFlow 在 Jupyter Notebook 中构建图像识别示例应用程序的增量方法,特别关注如何通过 PixieApps 实现算法的操作化。我们首先通过 TensorFlow DNNClassifier
估计器,从 pandas DataFrame 中构建了一个简单的分类模型。接着,我们将图像识别示例应用程序的 MVP 版本分为四部分来构建:
-
我们加载了预训练的 MobileNet 模型
-
我们为我们的图像识别示例应用程序创建了一个 PixieApp
-
我们将 TensorBoard 图形可视化集成到 PixieApp 中
-
我们使用户能够使用来自 ImageNet 的自定义训练数据重新训练模型
概述
机器学习是一个庞大的领域,享有巨大的增长,无论是在研究还是开发方面。在本章中,我们只探讨了与机器学习算法相关的极小一部分前沿技术,具体来说,是使用深度学习神经网络进行图像识别。对于一些刚刚开始接触机器学习的读者,示例 PixieApp 及其关联的算法代码可能一次性难以消化。然而,底层的目标是展示如何逐步构建一个应用程序,并利用机器学习模型。我们恰好使用了一个卷积神经网络模型进行图像识别,但任何其他模型都可以使用。
希望你已经对 PixieDust 和 PixieApp 编程模型如何帮助你完成自己的项目有了一个不错的了解,我强烈建议你以这个示例应用程序为起点,使用你选择的机器学习方法来构建自己的自定义应用程序。我还推荐将你的 PixieApp 部署为一个 web 应用程序,并通过 PixieGateway 微服务进行测试,看看它是否是一个可行的解决方案。
在下一章,我们将介绍另一个与大数据和自然语言处理相关的重要行业应用案例。我们将构建一个示例应用程序,通过自然语言理解服务分析社交媒体趋势。
第七章:分析研究:Twitter 情感分析与 NLP 及大数据
“数据是新的石油。” | ||
---|---|---|
--未知 |
本章我们将探讨人工智能和数据科学的两个重要领域:自然语言处理(NLP)和大数据分析。为了支持示例应用程序,我们重新实现了第一章中描述的Twitter 标签情感分析项目,编程与数据科学——一种新工具集,但这次我们利用 Jupyter Notebooks 和 PixieDust 构建实时仪表盘,分析来自与特定实体(例如公司提供的某个产品)相关的推文流中的数据,提供情感信息以及从相同推文中提取的其他趋势实体的信息。在本章的结尾,读者将学习如何将基于云的 NLP 服务,如IBM Watson 自然语言理解,集成到他们的应用程序中,并使用像 Apache Spark 这样的框架在(Twitter)规模上执行数据分析。
一如既往,我们将展示如何通过实现一个作为 PixieApp 的实时仪表盘,直接在 Jupyter Notebook 中运行来使分析工作可操作化。
开始使用 Apache Spark
大数据这一术语常常给人模糊不清和不准确的感觉。什么样的数据集才算是大数据呢?是 10 GB、100 GB、1 TB 还是更多?我喜欢的一个定义是:大数据是当数据无法完全装入单个机器的内存时。多年来,数据科学家被迫对大数据集进行抽样处理,以便能够在单台机器上处理,但随着并行计算框架的出现,这些框架能够将数据分布到多台机器的集群中,使得可以在整个数据集上进行工作,当然,前提是集群有足够的机器。与此同时,云技术的进步使得可以按需提供适合数据集大小的机器集群。
目前,有多种框架(大多数通常以开源形式提供)可以提供强大且灵活的并行计算能力。最受欢迎的一些包括 Apache Hadoop (hadoop.apache.org
)、Apache Spark (spark.apache.org
) 和 Dask (dask.pydata.org
)。对于我们的Twitter 情感分析应用程序,我们将使用 Apache Spark,它在可扩展性、可编程性和速度方面表现出色。此外,许多云服务提供商提供了某种形式的 Spark 即服务,能够在几分钟内按需创建一个合适大小的 Spark 集群。
一些 Spark 即服务的云服务提供商包括:
-
Microsoft Azure:
azure.microsoft.com/en-us/services/hdinsight/apache-spark
-
亚马逊网络服务:
aws.amazon.com/emr/details/spark
-
Google Cloud:
cloud.google.com/dataproc
-
Databricks:
databricks.com
-
IBM Cloud:
www.ibm.com/cloud/analytics-engine
注意
注意:Apache Spark 也可以轻松地在本地机器上安装用于测试,在这种情况下,集群节点通过线程进行模拟。
Apache Spark 架构
下图展示了 Apache Spark 框架的主要组件:
Spark 高层架构
-
Spark SQL:该组件的核心数据结构是 Spark DataFrame,使得熟悉 SQL 语言的用户能够轻松地处理结构化数据。
-
Spark Streaming:用于处理流式数据的模块。正如我们稍后所看到的,我们将在示例应用中使用该模块,特别是 Spark 2.0 引入的结构化流处理(Structured Streaming)。
-
MLlib:提供一个功能丰富的机器学习库,在 Spark 规模上运行。
-
GraphX:用于执行图并行计算的模块。
如下图所示,主要有两种方式可以与 Spark 集群工作:
与 Spark 集群工作的两种方式
-
spark-submit:用于在集群上启动 Spark 应用的 Shell 脚本
-
Notebooks:与 Spark 集群交互式执行代码语句
本书不涵盖 spark-submit
shell 脚本的内容,但可以在以下网址找到官方文档:spark.apache.org/docs/latest/submitting-applications.html
。在本章的其余部分,我们将重点介绍通过 Jupyter Notebooks 与 Spark 集群进行交互。
配置 Notebooks 以便与 Spark 一起使用
本节中的说明仅涵盖在本地安装 Spark 用于开发和测试。手动在集群中安装 Spark 超出了本书的范围。如果需要真正的集群,强烈建议使用基于云的服务。
默认情况下,本地 Jupyter Notebooks 会安装普通的 Python 内核。为了与 Spark 一起使用,用户必须执行以下步骤:
-
从
spark.apache.org/downloads.html
下载二进制分发包,安装 Spark 到本地。 -
使用以下命令在临时目录中生成内核规范:
ipython kernel install --prefix /tmp
注意
注意:上述命令可能会生成警告消息,只要显示以下信息,这些警告可以安全忽略:
已在/tmp/share/jupyter/kernels/python3 中安装 kernelspec python3
-
转到
/tmp/share/jupyter/kernels/python3
,编辑kernel.json
文件,向 JSON 对象中添加以下键(将<<spark_root_path>>
替换为你安装 Spark 的目录路径,将<<py4j_version>>
替换为你系统上安装的版本):"env": { "PYTHONPATH": "<<spark_root_path>>/python/:<<spark_root_path>>/python/lib/py4j-<<py4j_version>>-src.zip", "SPARK_HOME": "<<spark_root_path>>", "PYSPARK_SUBMIT_ARGS": "--master local[10] pyspark-shell", "SPARK_DRIVER_MEMORY": "10G", "SPARK_LOCAL_IP": "127.0.0.1", "PYTHONSTARTUP": "<<spark_root_path>>/python/pyspark/shell.py" }
-
你可能还想自定义
display_name
键,以使其在 Juptyer 界面中独特且易于识别。如果你需要查看现有内核的列表,可以使用以下命令:jupyter kernelspec list
前述命令将为你提供内核名称和相关路径的列表。从路径中,你可以打开
kernel.json
文件,访问display_name
值。例如:Available kernels: pixiedustspark16 /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark16 pixiedustspark21 /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark21 pixiedustspark22 /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark22 pixiedustspark23 /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark23
-
使用以下命令安装带有编辑文件的内核:
jupyter kernelspec install /tmp/share/jupyter/kernels/python3
注意
注意:根据环境不同,你可能在运行前述命令时会遇到“权限拒绝”的错误。在这种情况下,你可能需要使用管理员权限运行该命令,使用
sudo
或者按如下方式使用--user
开关:jupyter kernelspec install --user /tmp/share/jupyter/kernels/python3
如需了解更多安装选项的信息,可以使用
-h
开关。例如:jupyter kernelspec install -h
-
重启 Notebook 服务器并开始使用新的 PySpark 内核。
幸运的是,PixieDust 提供了一个 install
脚本来自动化前述的手动步骤。
注意
你可以在这里找到该脚本的详细文档:
pixiedust.github.io/pixiedust/install.html
简而言之,使用自动化 PixieDust install
脚本需要发出以下命令并按照屏幕上的说明操作:
jupyter pixiedust install
本章稍后会深入探讨 Spark 编程模型,但现在让我们在下一节定义我们 Twitter 情感分析 应用的 MVP 要求。
Twitter 情感分析应用
和往常一样,我们首先定义 MVP 版本的要求:
-
连接 Twitter,获取由用户提供的查询字符串过滤的实时推文流
-
丰富推文,添加情感信息和从文本中提取的相关实体
-
使用实时图表显示有关数据的各种统计信息,并在指定的时间间隔内更新图表
-
系统应该能够扩展到 Twitter 数据规模
以下图示展示了我们应用架构的第一个版本:
Twitter 情感分析架构版本 1
对于第一个版本,应用将完全在一个 Python Notebook 中实现,并调用外部服务处理 NLP 部分。为了能够扩展,我们肯定需要将一些处理外部化,但对于开发和测试,我发现能够将整个应用封装在一个 Notebook 中显著提高了生产力。
至于库和框架,我们将使用 Tweepy(www.tweepy.org
)连接到 Twitter,使用 Apache Spark 结构化流处理(spark.apache.org/streaming
)处理分布式集群中的流数据,使用 Watson Developer Cloud Python SDK(github.com/watson-developer-cloud/python-sdk
)访问 IBM Watson 自然语言理解(www.ibm.com/watson/services/natural-language-understanding
)服务。
第一部分 – 使用 Spark 结构化流处理获取数据
为了获取数据,我们使用 Tweepy,它提供了一个优雅的 Python 客户端库来访问 Twitter API。Tweepy 支持的 API 非常广泛,详细介绍超出了本书的范围,但你可以在 Tweepy 官方网站找到完整的 API 参考:tweepy.readthedocs.io/en/v3.6.0/cursor_tutorial.html
。
你可以直接通过 PyPi 安装 Tweepy 库,使用pip install
命令。以下命令展示了如何通过 Notebook 使用!
指令安装:
!pip install tweepy
注意
注意:当前使用的 Tweepy 版本是 3.6.0。安装完库后,别忘了重启内核。
数据管道架构图
在深入了解数据管道的每个组件之前,最好先了解其整体架构并理解计算流。
如下图所示,我们首先创建一个 Tweepy 流,将原始数据写入 CSV 文件。然后,我们创建一个 Spark Streaming 数据框,读取 CSV 文件并定期更新新数据。从 Spark Streaming 数据框中,我们使用 SQL 创建一个 Spark 结构化查询,并将其结果存储在 Parquet 数据库中:
流计算流程
Twitter 身份验证
在使用任何 Twitter API 之前,建议先进行身份验证。最常用的身份验证机制之一是 OAuth 2.0 协议(oauth.net
),该协议使第三方应用程序能够访问网络服务。你需要做的第一件事是获取一组密钥字符串,这些字符串由 OAuth 协议用于对你进行身份验证:
-
消费者密钥:唯一标识客户端应用程序的字符串(即 API 密钥)。
-
消费者密钥:仅应用程序和 Twitter OAuth 服务器知道的密钥字符串。可以将其视为密码。
-
访问令牌:用于验证请求的字符串。该令牌也在授权阶段用于确定应用程序的访问级别。
-
访问令牌密钥:与消费者密钥类似,这是与访问令牌一起发送的密码字符串,用作密码。
要生成前面的密钥字符串,您需要访问apps.twitter.com
,使用您的常规 Twitter 用户 ID 和密码进行身份验证,并按照以下步骤操作:
-
使用创建新应用按钮创建一个新的 Twitter 应用。
-
填写应用程序详情,同意开发者协议,然后点击创建您的 Twitter 应用按钮。
提示
注意:确保您的手机号码已添加到个人资料中,否则在创建 Twitter 应用时会出现错误。
您可以为必填项网站输入提供一个随机 URL,并将URL输入留空,因为这是一个可选的回调 URL。
-
点击密钥和访问令牌标签以获取消费者和访问令牌。您可以随时使用页面上提供的按钮重新生成这些令牌。如果您这么做,您还需要在应用程序代码中更新这些值。
为了更容易进行代码维护,我们将把这些令牌放在 Notebook 顶部的单独变量中,并创建我们稍后将使用的tweepy.OAuthHandler
类:
from tweepy import OAuthHandler
# Go to http://apps.twitter.com and create an app.
# The consumer key and secret will be generated for you after
consumer_key="XXXX"
consumer_secret="XXXX"
# After the step above, you will be redirected to your app's page.
# Create an access token under the "Your access token" section
access_token="XXXX"
access_token_secret="XXXX"
auth = OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
创建 Twitter 流
为了实现我们的应用程序,我们只需要使用这里文档化的 Twitter 流 API:tweepy.readthedocs.io/en/v3.5.0/streaming_how_to.html
。在此步骤中,我们创建一个 Twitter 流,将传入的数据存储到本地文件系统中的 CSV 文件中。通过继承自tweepy.streaming.StreamListener
的自定义RawTweetsListener
类完成此操作。通过重写on_data
方法来处理传入数据的自定义处理。
在我们的案例中,我们希望使用标准 Python csv
模块中的DictWriter
将传入的 JSON 数据转换为 CSV 格式。由于 Spark Streaming 文件输入源仅在输入目录中创建新文件时触发,因此我们不能简单地将数据追加到现有文件中。相反,我们将数据缓冲到一个数组中,并在缓冲区达到容量时将其写入磁盘。
注意
为了简化,实施中没有包括处理完文件后的清理工作。另一个小的限制是,我们目前等待缓冲区填满后再写入文件,理论上如果没有新推文出现,这可能需要很长时间。
RawTweetsListener
的代码如下所示:
from six import iteritems
import json
import csv
from tweepy.streaming import StreamListener
class RawTweetsListener(StreamListener):
def __init__(self):
self.buffered_data = []
self.counter = 0
def flush_buffer_if_needed(self):
"Check the buffer capacity and write to a new file if needed"
length = len(self.buffered_data)
if length > 0 and length % 10 == 0:
with open(os.path.join( output_dir,
"tweets{}.csv".format(self.counter)), "w") as fs:
self.counter += 1
csv_writer = csv.DictWriter( fs,
fieldnames = fieldnames)
for data in self.buffered_data:
csv_writer.writerow(data)
self.buffered_data = []
def on_data(self, data):
def transform(key, value):
return transformskey if key in transforms else value
self.buffered_data.append(
{key:transform(key,value) \
for key,value in iteritems(json.loads(data)) \
if key in fieldnames}
)
self.flush_buffer_if_needed()
return True
def on_error(self, status):
print("An error occured while receiving streaming data: {}".format(status))
return False
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode1.py
从前面的代码中有几个重要的地方需要注意:
-
每条来自 Twitter API 的推文都包含大量数据,我们使用
field_metadata
变量选择保留的字段。我们还定义了一个全局变量fieldnames
,它保存了要从流中捕获的字段列表,以及一个transforms
变量,它包含一个字典,字典的键是所有具有变换函数的字段名,值是变换函数本身:from pyspark.sql.types import StringType, DateType from bs4 import BeautifulSoup as BS fieldnames = [f["name"] for f in field_metadata] transforms = { item['name']:item['transform'] for item in field_metadata if "transform" in item } field_metadata = [ {"name": "created_at","type": DateType()}, {"name": "text", "type": StringType()}, {"name": "source", "type": StringType(), "transform": lambda s: BS(s, "html.parser").text.strip() } ]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode2.py
-
CSV 文件被写入定义在自己的变量中的
output_dir
目录。在启动时,我们首先删除该目录及其内容:import shutil def ensure_dir(dir, delete_tree = False): if not os.path.exists(dir): os.makedirs(dir) elif delete_tree: shutil.rmtree(dir) os.makedirs(dir) return os.path.abspath(dir) root_dir = ensure_dir("output", delete_tree = True) output_dir = ensure_dir(os.path.join(root_dir, "raw"))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode3.py
-
field_metadata
包含了 Spark DataType,我们稍后将在创建 Spark 流查询时使用它来构建模式。 -
field_metadata
还包含一个可选的变换lambda
函数,用于在将值写入磁盘之前清理数据。作为参考,Python 中的 lambda 函数是一个内联定义的匿名函数(请参见docs.python.org/3/tutorial/controlflow.html#lambda-expressions
)。我们在此使用它来处理常常以 HTML 片段形式返回的源字段。在这个 lambda 函数中,我们使用了 BeautifulSoup 库(它也在上一章中使用过)来提取只有文本的内容,如以下代码片段所示:lambda s: BS(s, "html.parser").text.strip()
现在,RawTweetsListener
已经创建,我们定义了一个start_stream
函数,稍后将在 PixieApp 中使用。此函数接受一个搜索词数组作为输入,并使用filter
方法启动一个新的流:
from tweepy import Stream
def start_stream(queries):
"Asynchronously start a new Twitter stream"
stream = Stream(auth, RawTweetsListener())
stream.filter(track=queries, async=True)
return stream
注意
注意到传递给stream.filter
的async=True
参数。这是必要的,确保该函数不会阻塞,这样我们就可以在 Notebook 中运行其他代码。
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode4.py
以下代码启动了一个流,它将接收包含单词baseball
的推文:
stream = start_stream(["baseball"])
当运行上述代码时,Notebook 中不会生成任何输出。然而,你可以在输出目录(即../output/raw
)中看到生成的文件(如tweets0.csv
、tweets1.csv
等),这些文件位于 Notebook 运行的路径下。
要停止流,我们只需调用disconnect
方法,如下所示:
stream.disconnect()
创建一个 Spark Streaming DataFrame
根据架构图,下一步是创建一个 Spark Streaming DataFrame tweets_sdf
,该 DataFrame 使用 output_dir
作为源文件输入。我们可以把 Streaming DataFrame 看作一个没有边界的表格,随着新数据从流中到达,新的行会不断被添加进来。
注意
注意:Spark Structured Streaming 支持多种类型的输入源,包括文件、Kafka、Socket 和 Rate。(Socket 和 Rate 仅用于测试。)
以下图表摘自 Spark 网站,能够很好地解释新数据是如何被添加到 Streaming DataFrame 中的:
Streaming DataFrame 流程
来源: spark.apache.org/docs/latest/img/structured-streaming-stream-as-a-table.png
Spark Streaming Python API 提供了一种优雅的方式来使用 spark.readStream
属性创建 Streaming DataFrame,该属性会创建一个新的 pyspark.sql.streamingreamReader
对象,方便你链式调用方法,并能让代码更加清晰(有关此模式的更多细节,请参见 en.wikipedia.org/wiki/Method_chaining
)。
例如,要创建一个 CSV 文件流,我们调用 format
方法并传入 csv
,接着链式调用适用的选项,并通过指定目录路径调用 load
方法:
schema = StructType(
[StructField(f["name"], f["type"], True) for f in field_metadata]
)
csv_sdf = spark.readStream\
.format("csv")\
.option("schema", schema)\
.option("multiline", True)\
.option("dateFormat", 'EEE MMM dd kk:mm:ss Z y')\
.option("ignoreTrailingWhiteSpace", True)\
.option("ignoreLeadingWhiteSpace", True)\
.load(output_dir)
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode5.py
spark.readStream
还提供了一个方便的高阶 csv
方法,它将路径作为第一个参数,并为选项提供关键字参数:
csv_sdf = spark.readStream \
.csv(
output_dir,
schema=schema,
multiLine = True,
dateFormat = 'EEE MMM dd kk:mm:ss Z y',
ignoreTrailingWhiteSpace = True,
ignoreLeadingWhiteSpace = True
)
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode6.py
你可以通过调用 isStreaming
方法来验证 csv_sdf
DataFrame 是否真的是一个 Streaming DataFrame,返回值应为 true
。以下代码还添加了 printSchema
方法的调用,以验证 schema 是否按照 field_metadata
配置如预期那样:
print(csv_sdf.isStreaming)
csv_sdf.printSchema()
返回值:
root
|-- created_at: date (nullable = true)
|-- text: string (nullable = true)
|-- source: string (nullable = true)
在继续下一步之前,理解csv_sdf
流数据框如何适应结构化流编程模型及其局限性非常重要。从本质上讲,Spark 的低级 API 定义了弹性分布式数据集(RDD)数据结构,它封装了管理分布式数据的所有底层复杂性。像容错(集群节点因任何原因崩溃时,框架会自动重启节点,无需开发者干预)等特性都由框架自动处理。RDD 操作有两种类型:转换和动作。转换是对现有 RDD 的逻辑操作,直到调用动作操作时,转换才会在集群上立即执行(懒执行)。转换的输出是一个新的 RDD。内部,Spark 维护一个 RDD 有向无环图(DAG),记录所有生成 RDD 的血统,这在从服务器故障恢复时非常有用。常见的转换操作包括map
、flatMap
、filter
、sample
和distinct
。对数据框的转换(数据框在内部由 RDD 支持)也适用,且它们具有包括 SQL 查询的优点。另一方面,动作不会生成其他 RDD,而是对实际分布式数据执行操作,返回非 RDD 值。常见的动作操作包括reduce
、collect
、count
和take
。
如前所述,csv_sdf
是一个流式数据框(Streaming DataFrame),这意味着数据会持续被添加到其中,因此我们只能对其应用转换,而不能执行操作。为了解决这个问题,我们必须先使用csv_sdf.writeStream
创建一个流查询,这是一个pyspark.sql.streaming.DataStreamWriter
对象。流查询负责将结果发送到输出接收器。然后,我们可以通过start()
方法运行流查询。
Spark Streaming 支持多种输出接收器类型:
-
文件:支持所有经典文件格式,包括 JSON、CSV 和 Parquet
-
Kafka:直接写入一个或多个 Kafka 主题
-
Foreach:对集合中的每个元素执行任意计算
-
控制台:将输出打印到系统控制台(主要用于调试)
-
内存:输出存储在内存中
在下一节中,我们将创建并运行一个结构化查询,针对csv_sdf
使用输出接收器将结果存储为 Parquet 格式。
创建并运行结构化查询
使用tweets_sdf
流数据框,我们创建一个流查询tweet_streaming_query
,该查询将数据以append输出模式写入 Parquet 格式。
注意
注意:Spark 流查询支持三种输出模式:complete,每次触发时写入整个表;append,只写入自上次触发以来的增量行;以及update,只写入已修改的行。
Parquet 是一种列式数据库格式,提供了高效、可扩展的分布式分析存储。你可以在这里找到有关 Parquet 格式的更多信息:parquet.apache.org
。
以下代码创建并启动 tweet_streaming_query
流式查询:
tweet_streaming_query = csv_sdf \
.writeStream \
.format("parquet") \
.option("path", os.path.join(root_dir, "output_parquet")) \
.trigger(processingTime="2 seconds") \
.option("checkpointLocation", os.path.join(root_dir, "output_chkpt")) \
.start()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode7.py
类似地,你可以使用 stop()
方法来停止流式查询,如下所示:
tweet_streaming_query.stop()
在上述代码中,我们使用 path
选项指定 Parquet 文件的位置,并使用 checkpointLocation
指定在服务器故障时用于恢复的数据位置。我们还指定了从流中读取新数据并将新行添加到 Parquet 数据库的触发间隔。
出于测试目的,你也可以使用 console
sink 来查看每次生成新原始 CSV 文件时从 output_dir
目录读取的新行:
tweet_streaming_query = csv_sdf.writeStream\
.outputMode("append")\
.format("console")\
.trigger(processingTime='2 seconds')\
.start()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode8.py
你可以在 Spark 集群主节点的系统输出中查看结果(你需要物理访问主节点机器并查看日志文件,因为不幸的是,由于操作在不同的进程中执行,输出不会显示在笔记本中。日志文件的位置取决于集群管理软件;有关更多信息,请参阅具体的文档)。
以下是特定批次显示的示例结果(标识符已被屏蔽):
-------------------------------------------
Batch: 17
-------------------------------------------
+----------+--------------------+-------------------+
|created_at| text| source|
+----------+--------------------+-------------------+
|2018-04-12|RT @XXXXXXXXXXXXX...|Twitter for Android|
|2018-04-12|RT @XXXXXXX: Base...| Twitter for iPhone|
|2018-04-12|That's my roommat...| Twitter for iPhone|
|2018-04-12|He's come a long ...| Twitter for iPhone|
|2018-04-12|RT @XXXXXXXX: U s...| Twitter for iPhone|
|2018-04-12|Baseball: Enid 10...| PushScoreUpdates|
|2018-04-12|Cubs and Sox aren...| Twitter for iPhone|
|2018-04-12|RT @XXXXXXXXXX: T...| RoundTeam|
|2018-04-12|@XXXXXXXX that ri...| Twitter for iPhone|
|2018-04-12|RT @XXXXXXXXXX: S...| Twitter for iPhone|
+----------+--------------------+-------------------+
监控活动流式查询
当流式查询启动时,Spark 会分配集群资源。因此,管理和监控这些查询非常重要,以确保你不会耗尽集群资源。随时可以通过以下代码获取所有正在运行的查询列表:
print(spark.streams.active)
结果:
[<pyspark.sql.streaming.StreamingQuery object at 0x12d7db6a0>, <pyspark.sql.streaming.StreamingQuery object at 0x12d269c18>]
然后,你可以通过使用以下查询监控属性来深入了解每个查询的细节:
-
id
:返回查询的唯一标识符,该标识符在重启时仍会保留(从检查点数据恢复) -
runId
:返回为当前会话生成的唯一 ID -
explain()
:打印查询的详细解释 -
recentProgress
:返回最近的进度更新数组 -
lastProgress
:返回最新的进度
以下代码打印每个活动查询的最新进度:
import json
for query in spark.streams.active:
print("-----------")
print("id: {}".format(query.id))
print(json.dumps(query.lastProgress, indent=2, sort_keys=True))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode9.py
第一个查询的结果显示如下:
-----------
id: b621e268-f21d-4eef-b6cd-cb0bc66e53c4
{
"batchId": 18,
"durationMs": {
"getOffset": 4,
"triggerExecution": 4
},
"id": "b621e268-f21d-4eef-b6cd-cb0bc66e53c4",
"inputRowsPerSecond": 0.0,
"name": null,
"numInputRows": 0,
"processedRowsPerSecond": 0.0,
"runId": "d2459446-bfad-4648-ae3b-b30c1f21be04",
"sink": {
"description": "org.apache.spark.sql.execution.streaming.ConsoleSinkProvider@586d2ad5"
},
"sources": [
{
"description": "FileStreamSource[file:/Users/dtaieb/cdsdev/notebookdev/Pixiedust/book/Chapter7/output/raw]",
"endOffset": {
"logOffset": 17
},
"inputRowsPerSecond": 0.0,
"numInputRows": 0,
"processedRowsPerSecond": 0.0,
"startOffset": {
"logOffset": 17
}
}
],
"stateOperators": [],
"timestamp": "2018-04-12T21:40:10.004Z"
}
作为读者的练习,构建一个 PixieApp,它提供一个实时仪表盘,显示每个活跃流查询的更新详情,会很有帮助。
注意
注意:我们将在第三部分 – 创建实时仪表盘 PixieApp中展示如何构建这个 PixieApp。
从 Parquet 文件创建批处理 DataFrame
注意
注意:在本章的其余部分,我们将批处理 Spark DataFrame 定义为经典 Spark DataFrame,即非流式的。
这个流计算流程的最后一步是创建一个或多个批处理 DataFrame,我们可以用来构建分析和数据可视化。我们可以将这最后一步视为对数据进行快照,以便进行更深层次的分析。
有两种方法可以通过编程方式从 Parquet 文件加载批处理 DataFrame:
-
使用
spark.read
(注意,我们不再像之前那样使用spark.readStream
):parquet_batch_df = spark.read.parquet(os.path.join(root_dir, "output_parquet"))
-
使用
spark.sql
:parquet_batch_df = spark.sql( "select * from parquet.'{}'".format( os.path.join(root_dir, "output_parquet") ) )
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode10.py
这种方法的好处是,我们可以使用任何 ANSI SQL 查询来加载数据,而不必像第一种方法那样使用等效的低级 DataFrame API。
然后,我们可以通过重新运行前面的代码并重新创建 DataFrame 来定期刷新数据。我们现在可以为数据创建进一步的分析,例如,通过在数据上运行 PixieDust 的display()
方法来生成可视化图表:
import pixiedust
display(parquet_batch_df)
我们选择条形图菜单,并将source
字段拖到Keys字段区域。由于我们只想显示前 10 条推文,因此我们在要显示的行数字段中设置这个值。下图显示了 PixieDust 选项对话框:
显示前 10 个推文来源的选项对话框
点击确定后,我们会看到以下结果:
展示与棒球相关的推文数量按来源分类的图表
在这一部分中,我们已经展示了如何使用 Tweepy 库创建 Twitter 流,清洗原始数据并将其存储在 CSV 文件中,创建 Spark Streaming DataFrame,在其上运行流查询并将输出存储在 Parquet 数据库中,从 Parquet 文件创建批处理 DataFrame,并使用 PixieDust 的display()
方法进行数据可视化。
注意
第一部分 – 使用 Spark 结构化流获取数据的完整笔记本可以在这里找到:
在下一部分中,我们将探讨如何使用 IBM Watson 自然语言理解服务,丰富数据中的情感分析和实体提取。
第二部分 - 使用情感和最相关的提取实体丰富数据
在这一部分,我们将推特数据与情感信息进行丰富处理,例如,正面,负面和中性。我们还希望从推文中提取出最相关的实体,例如,运动,组织和地点。这些额外的信息将通过我们在下一部分构建的实时仪表板进行分析和可视化。从非结构化文本中提取情感和实体所使用的算法属于计算机科学和人工智能领域,称为自然语言处理(NLP)。网上有许多教程提供了提取情感的算法示例。例如,您可以在 scikit-learn 的 GitHub 仓库找到一个全面的文本分析教程,链接为github.com/scikit-learn/scikit-learn/blob/master/doc/tutorial/text_analytics/working_with_text_data.rst
。
然而,对于这个示例应用程序,我们不会构建自己的 NLP 算法。而是选择一个提供文本分析(如情感和实体提取)的云服务。当您的需求比较通用,不需要训练自定义模型时,这种方法效果很好,尽管即便如此,许多服务提供商现在也提供了相关工具来完成此类任务。使用云服务提供商相比自己创建模型具有显著优势,比如节省开发时间、提高准确性和性能。通过简单的 REST 调用,我们可以生成所需数据并将其集成到应用程序流程中。如果需要,切换服务提供商也非常容易,因为与服务接口的代码已经很好地隔离。
对于这个示例应用程序,我们将使用IBM Watson 自然语言理解(NLU)服务,它是 IBM Watson 认知服务家族的一部分,并且可以在 IBM Cloud 上使用。
开始使用 IBM Watson 自然语言理解服务
为新服务提供资源的过程对于每个云服务提供商通常都是相同的。登录后,您将进入服务目录页面,在那里可以搜索特定的服务。
要登录到 IBM Cloud,只需访问console.bluemix.net
,如果还没有 IBM 账户,可以创建一个免费的账户。进入仪表板后,有多种方式可以搜索 IBM Watson NLU 服务:
-
点击左上角菜单,选择Watson,点击浏览服务,然后在服务列表中找到自然语言理解条目。
-
点击右上角的创建资源按钮进入目录。一旦进入目录,你可以在搜索栏中搜索
Natural Language Understanding
,如以下截图所示:在服务目录中搜索 Watson NLU
然后,你可以点击自然语言理解来配置一个新实例。云服务提供商通常会为一些服务提供免费的或基于试用的计划,幸运的是,Watson NLU 也提供了这样的计划,但有限制,你只能训练一个自定义模型,每月最多处理 30,000 个 NLU 项目(对于我们的示例应用足够了)。选择Lite(免费)计划并点击创建按钮后,新配置的实例将出现在仪表盘上,并准备好接受请求。
注意
注意:创建服务后,你可能会被重定向到 NLU 服务的入门文档。如果是这种情况,只需返回仪表盘,应该能看到新创建的服务实例。
下一步是通过在笔记本中发出 REST 调用来测试服务。每个服务都会提供详细的文档,说明如何使用,包括 API 参考。在笔记本中,我们可以使用 requests 包根据 API 参考发出 GET、POST、PUT 或 DELETE 请求,但强烈建议检查服务是否提供具有高级编程访问功能的 SDK。
幸运的是,IBM Watson 提供了watson_developer_cloud
开源库,其中包含多个支持流行编程语言(包括 Java、Python 和 Node.js)的开源 SDK。对于本项目,我们将使用 Python SDK,源代码和示例代码可以在此找到:github.com/watson-developer-cloud/python-sdk
。
以下pip
命令直接从 Jupyter Notebook 安装watson_developer_cloud
包:
!pip install Watson_developer_cloud
注意
请注意命令前的!
,它表示这是一个 shell 命令。
注意:安装完成后,别忘了重新启动内核。
大多数云服务提供商使用一种通用模式,允许用户通过服务控制台仪表盘生成一组凭证,然后将其嵌入到客户端应用程序中。要生成凭证,只需点击 Watson NLU 实例的服务凭证标签,然后点击新建凭证按钮。
这将生成一组新的凭证,格式为 JSON,如下截图所示:
为 Watson NLU 服务生成新凭证
现在我们已经拥有了服务的凭据,我们可以创建一个 NaturalLanguageUnderstandingV1
对象,它将提供对 REST API 的编程访问,如下所示的代码所示:
from watson_developer_cloud import NaturalLanguageUnderstandingV1
from watson_developer_cloud.natural_language_understanding_v1 import Features, SentimentOptions, EntitiesOptions
nlu = NaturalLanguageUnderstandingV1(
version='2017-02-27',
username='XXXX',
password='XXXX'
)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode11.py
注意:在前面的代码中,将 XXXX
文本替换为服务凭据中的适当用户名和密码。
version
参数指的是 API 的特定版本。要了解最新版本,请访问此处的官方文档页面:
www.ibm.com/watson/developercloud/natural-language-understanding/api/v1
在继续构建应用程序之前,让我们花点时间了解 Watson 自然语言服务所提供的文本分析功能,包括:
-
情感
-
实体
-
概念
-
类别
-
情感
-
关键词
-
关系
-
语义角色
在我们的应用程序中,Twitter 数据的丰富化发生在 RawTweetsListener
中,我们在其中创建了一个 enrich
方法,该方法将从 on_data
处理程序方法中调用。在这个方法中,我们使用 Twitter 数据和仅包含情感和实体的特征列表调用 nlu.analyze
方法,如下所示的代码所示:
注意
注意:[[RawTweetsListener]]
符号表示以下代码是一个名为 RawTweetsListener
的类的一部分,用户不应尝试在没有完整类的情况下直接运行代码。像往常一样,您可以参考完整的笔记本进行查看。
[[RawTweetsListener]]
def enrich(self, data):
try:
response = nlu.analyze(
text = data['text'],
features = Features(
sentiment=SentimentOptions(),
entities=EntitiesOptions()
)
)
data["sentiment"] = response["sentiment"]["document"]["label"]
top_entity = response["entities"][0] if len(response["entities"]) > 0 else None
data["entity"] = top_entity["text"] if top_entity is not None else ""
data["entity_type"] = top_entity["type"] if top_entity is not None else ""
return data
except Exception as e:
self.warn("Error from Watson service while enriching data: {}".format(e))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode12.py
结果将存储在 data
对象中,随后会写入 CSV 文件。我们还会防范意外异常,跳过当前推文并记录警告信息,而不是让异常冒泡,从而停止 Twitter 流。
注意
注意:最常见的异常发生在推文数据使用该服务不支持的语言时。
我们使用在第五章中描述的 @Logger
装饰器,Python 和 PixieDust 最佳实践与高级概念,通过 PixieDust 日志框架记录日志消息。提醒一下,您可以使用来自另一个单元的 %pixiedustLog
魔法命令来查看日志消息。
我们仍然需要更改模式元数据以包括新的字段,如下所示:
field_metadata = [
{"name": "created_at", "type": DateType()},
{"name": "text", "type": StringType()},
{"name": "source", "type": StringType(),
"transform": lambda s: BS(s, "html.parser").text.strip()
},
{"name": "sentiment", "type": StringType()},
{"name": "entity", "type": StringType()},
{"name": "entity_type", "type": StringType()}
]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode13.py
最后,我们更新on_data
处理程序以调用enrich
方法,如下所示:
def on_data(self, data):
def transform(key, value):
return transformskey if key in transforms else value
data = self.enrich(json.loads(data))
if data is not None:
self.buffered_data.append(
{key:transform(key,value) \
for key,value in iteritems(data) \
if key in fieldnames}
)
self.flush_buffer_if_needed()
return True
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode14.py
当我们重新启动 Twitter 流并创建 Spark Streaming DataFrame 时,我们可以通过以下代码验证我们是否有正确的模式:
schema = StructType(
[StructField(f["name"], f["type"], True) for f in field_metadata]
)
csv_sdf = spark.readStream \
.csv(
output_dir,
schema=schema,
multiLine = True,
dateFormat = 'EEE MMM dd kk:mm:ss Z y',
ignoreTrailingWhiteSpace = True,
ignoreLeadingWhiteSpace = True
)
csv_sdf.printSchema()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode15.py
这将显示如下结果,如预期:
root
|-- created_at: date (nullable = true)
|-- text: string (nullable = true)
|-- source: string (nullable = true)
|-- sentiment: string (nullable = true)
|-- entity: string (nullable = true)
|-- entity_type: string (nullable = true)
类似地,当我们使用console
接收器运行结构化查询时,数据将按批次显示在 Spark 主节点的控制台中,如下所示:
-------------------------------------------
Batch: 2
-------------------------------------------
+----------+---------------+---------------+---------+------------+-------------+
|created_at| text| source|sentiment| entity| entity_type|
+----------+---------------+---------------+---------+------------+-------------+
|2018-04-14|Some little ...| Twitter iPhone| positive| Drew| Person|d
|2018-04-14|RT @XXXXXXXX...| Twitter iPhone| neutral| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXXXXX...| Twitter iPhone| neutral| baseball| Sport|
|2018-04-14|RT @XXXXXXXX...| Twitter Client| neutral| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXXXXX...| Twitter Client| positive| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXX: I...|Twitter Android| positive| Greg XXXXXX| Person|
|2018-04-14|RT @XXXXXXXX...| Twitter iPhone| positive| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXX: I...|Twitter Android| positive| Greg XXXXXX| Person|
|2018-04-14|Congrats to ...|Twitter Android| positive| softball| Sport|
|2018-04-14|translation:...| Twitter iPhone| neutral| null| null|
+----------+---------------+---------------+---------+------------+-------------+
最后,我们使用 Parquet 的output
接收器运行结构化查询,创建一个批量 DataFrame,并使用 PixieDust 的display()
探索数据,展示例如按情感(positive
,negative
,neutral
)聚类的推文计数,如下图所示:
显示按情感分类的推文数的条形图,按实体聚类
注意
完整的笔记本《第二部分——通过情感和最相关的提取实体丰富数据》位于此处:
如果你正在运行它,我鼓励你通过向模式添加更多字段、运行不同的 SQL 查询,并使用 PixieDust 的display()
来可视化数据进行实验。
在接下来的部分,我们将构建一个展示 Twitter 数据多个指标的仪表盘。
第三部分——创建实时仪表盘 PixieApp
一如既往,我们首先需要定义 MVP 版本仪表盘的需求。这次我们将借用敏捷方法中的一个工具,称为用户故事,它从用户的角度描述我们希望构建的功能。敏捷方法还要求我们通过将不同的用户分类为角色,充分理解与软件互动的用户的背景。在我们的案例中,我们只使用一个角色:Frank,市场营销总监,想要实时了解消费者在社交媒体上讨论的内容。
用户故事是这样的:
-
Frank 输入类似产品名称的搜索查询
-
然后,展示一个仪表板,显示一组图表,展示有关用户情绪(正面、负面、中立)的度量
-
仪表板还包含一个展示所有在推文中提到的实体的词云
-
此外,仪表板还提供了一个选项,可以显示当前所有活跃的 Spark Streaming 查询的实时进度
注意
注意:最后一个功能对于 Frank 来说并不是必需的,但我们还是在这里展示它,作为之前练习的示例实现。
将分析功能重构为独立的方法
在开始之前,我们需要将启动 Twitter 流和创建 Spark Streaming 数据框的代码重构为独立的方法,并在 PixieApp 中调用这些方法。
start_stream,
start_streaming_dataframe
和 start_parquet_streaming_query
方法如下:
def start_stream(queries):
"Asynchronously start a new Twitter stream"
stream = Stream(auth, RawTweetsListener())
stream.filter(track=queries, languages=["en"], async=True)
return stream
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode16.py
def start_streaming_dataframe(output_dir):
"Start a Spark Streaming DataFrame from a file source"
schema = StructType(
[StructField(f["name"], f["type"], True) for f in field_metadata]
)
return spark.readStream \
.csv(
output_dir,
schema=schema,
multiLine = True,
timestampFormat = 'EEE MMM dd kk:mm:ss Z yyyy',
ignoreTrailingWhiteSpace = True,
ignoreLeadingWhiteSpace = True
)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode17.py
def start_parquet_streaming_query(csv_sdf):
"""
Create and run a streaming query from a Structured DataFrame
outputing the results into a parquet database
"""
streaming_query = csv_sdf \
.writeStream \
.format("parquet") \
.option("path", os.path.join(root_dir, "output_parquet")) \
.trigger(processingTime="2 seconds") \
.option("checkpointLocation", os.path.join(root_dir, "output_chkpt")) \
.start()
return streaming_query
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode18.py
作为准备工作的一部分,我们还需要管理 PixieApp 将要创建的不同流的生命周期,并确保在用户重新启动仪表板时,底层资源被正确停止。为此,我们创建了一个StreamsManager
类,封装了 Tweepy 的twitter_stream
和 CSV 流数据框。这个类有一个reset
方法,它会停止twitter_stream
,停止所有活动的流查询,删除先前查询创建的所有输出文件,并使用新的查询字符串启动一个新的流。如果reset
方法在没有查询字符串的情况下被调用,我们将不会启动新的流。
我们还创建了一个全局的streams_manager
实例,它将跟踪当前状态,即使仪表板被重新启动。由于用户可以重新运行包含全局streams_manager
的单元,我们需要确保在当前全局实例被删除时,reset
方法会自动调用。为此,我们重写了对象的__del__
方法,这是 Python 实现析构函数的一种方式,并调用reset
。
StreamsManager
的代码如下:
class StreamsManager():
def __init__(self):
self.twitter_stream = None
self.csv_sdf = None
def reset(self, search_query = None):
if self.twitter_stream is not None:
self.twitter_stream.disconnect()
#stop all the active streaming queries and re_initialize the directories
for query in spark.streams.active:
query.stop()
# initialize the directories
self.root_dir, self.output_dir = init_output_dirs()
# start the tweepy stream
self.twitter_stream = start_stream([search_query]) if search_query is not None else None
# start the spark streaming stream
self.csv_sdf = start_streaming_dataframe(output_dir) if search_query is not None else None
def __del__(self):
# Automatically called when the class is garbage collected
self.reset()
streams_manager = StreamsManager()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode19.py
创建 PixieApp
如同在第六章,分析研究:TensorFlow 下的 AI 和图像识别,我们再次使用 TemplateTabbedApp
类来创建一个包含两个 PixieApp 的标签布局:
-
TweetInsightApp
:允许用户指定查询字符串并显示与之关联的实时仪表盘 -
StreamingQueriesApp
:监控活动结构化查询的进度
在 TweetInsightApp
的默认路由中,我们返回一个片段,提示用户输入查询字符串,如下所示:
from pixiedust.display.app import *
@PixieApp
class TweetInsightApp():
@route()
def main_screen(self):
return """
<style>
div.outer-wrapper {
display: table;width:100%;height:300px;
}
div.inner-wrapper {
display: table-cell;vertical-align: middle;height: 100%;width: 100%;
}
</style>
<div class="outer-wrapper">
<div class="inner-wrapper">
<div class="col-sm-3"></div>
<div class="input-group col-sm-6">
<input id="query{{prefix}}" type="text" class="form-control"
value=""
placeholder="Enter a search query (e.g. baseball)">
<span class="input-group-btn">
<button class="btn btn-default" type="button"
pd_options="search_query=$val(query{{prefix}})">
Go
</button>
</span>
</div>
</div>
</div>
"""
TweetInsightApp().run()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode20.py
以下截图显示了运行上述代码后的结果:
注意
注意:稍后我们会创建主 TwitterSentimentApp
PixieApp,它具有标签布局,并包含此类。在此之前,我们只展示 TweetInsightApp
子应用程序的独立功能。
Twitter 情感仪表盘的欢迎界面
在 Go
按钮中,我们通过用户提供的查询字符串调用 search_query
路由。在这个路由中,我们首先启动各种流并创建一个批量数据框,该数据框从 Parquet 数据库所在的输出目录中存储为一个类变量,命名为 parquet_df
。然后,我们返回由三个小部件组成的 HTML 片段,展示以下指标:
-
按照实体分组的三种情感的柱状图
-
显示推文情感分布的折线图子图
-
用于实体的词云
每个小部件都在使用 pd_refresh_rate
属性定期调用特定的路由,相关文档可以参考第五章,Python 和 PixieDust 最佳实践与高级概念。我们还确保重新加载 parquet_df
变量,以获取自上次加载以来到达的新数据。该变量随后在 pd_entity
属性中引用,用于显示图表。
以下代码展示了 search_query
路由的实现:
import time
[[TweetInsightApp]]
@route(search_query="*")
def do_search_query(self, search_query):
streams_manager.reset(search_query)
start_parquet_streaming_query(streams_manager.csv_sdf)
while True:
try:
parquet_dir = os.path.join(root_dir,
"output_parquet")
self.parquet_df = spark.sql("select * from parquet.'{}'".format(parquet_dir))
break
except:
time.sleep(5)
return """
<div class="container">
<div id="header{{prefix}}" class="row no_loading_msg"
pd_refresh_rate="5000" pd_target="header{{prefix}}">
<pd_script>
print("Number of tweets received: {}".format(streams_manager.twitter_stream.listener.tweet_count))
</pd_script>
</div>
<div class="row" style="min-height:300px">
<div class="col-sm-5">
<div id="metric1{{prefix}}" pd_refresh_rate="10000"
class="no_loading_msg"
pd_options="display_metric1=true"
pd_target="metric1{{prefix}}">
</div>
</div>
<div class="col-sm-5">
<div id="metric2{{prefix}}" pd_refresh_rate="12000"
class="no_loading_msg"
pd_options="display_metric2=true"
pd_target="metric2{{prefix}}">
</div>
</div>
</div>
<div class="row" style="min-height:400px">
<div class="col-sm-offset-1 col-sm-10">
<div id="word_cloud{{prefix}}" pd_refresh_rate="20000"
class="no_loading_msg"
pd_options="display_wc=true"
pd_target="word_cloud{{prefix}}">
</div>
</div>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode21.py
从上述代码中有多个需要注意的地方:
-
当我们尝试加载
parquet_df
批数据框时,Parquet 文件的输出目录可能尚未准备好,这会导致异常。为了解决这个时序问题,我们将代码包裹在try...except
语句中,并使用time.sleep(5)
等待 5 秒钟。 -
我们还在页头显示当前推文的数量。为此,我们添加了一个每 5 秒刷新一次的
<div>
元素,并且在该元素中使用<pd_script>
来打印当前的推文数量,使用streams_manager.twitter_stream.listener.tweet_count
变量,它是我们在RawTweetsListener
类中添加的变量。我们还更新了on_data()
方法,以便每次新推文到达时增加tweet_count
变量,以下代码展示了这一过程:[[TweetInsightApp]] def on_data(self, data): def transform(key, value): return transformskey if key in transforms else value data = self.enrich(json.loads(data)) if data is not None: self.tweet_count += 1 self.buffered_data.append( {key:transform(key,value) \ for key,value in iteritems(data) \ if key in fieldnames} ) self.flush_buffer_if_needed() return True
同时,为了避免闪烁,我们通过在
<div>
元素中使用class="no_loading_msg"
来阻止显示 加载旋转图标 图像。 -
我们调用了三个不同的路由(
display_metric1
,display_metric2
和display_wc
),它们分别负责显示三个小部件。display_metric1
和display_metric2
路由非常相似。它们返回一个包含parquet_df
作为pd_entity
的div
,以及一个自定义的<pd_options>
子元素,该元素包含传递给 PixieDustdisplay()
层的 JSON 配置。
以下代码展示了 display_metric1
路由的实现:
[[TweetInsightApp]]
@route(display_metric1="*")
def do_display_metric1(self, display_metric1):
parquet_dir = os.path.join(root_dir, "output_parquet")
self.parquet_df = spark.sql("select * from parquet.'{}'".format(parquet_dir))
return """
<div class="no_loading_msg" pd_render_onload pd_entity="parquet_df">
<pd_options>
{
"legend": "true",
"keyFields": "sentiment",
"clusterby": "entity_type",
"handlerId": "barChart",
"rendererId": "bokeh",
"rowCount": "10",
"sortby": "Values DESC",
"noChartCache": "true"
}
</pd_options>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode22.py
display_metric2
路由遵循类似的模式,但使用了不同的一组 pd_options
属性。
最后一条路由是 display_wc
,负责显示实体的词云。该路由使用 wordcloud
Python 库,你可以通过以下命令安装它:
!pip install wordcloud
注意
注意:一如既往,安装完成后不要忘记重启内核。
我们使用了在第五章中记录的 @captureOutput
装饰器,Python 和 PixieDust 最佳实践与高级概念,如以下所示:
import matplotlib.pyplot as plt
from wordcloud import WordCloud
[[TweetInsightApp]]
@route(display_wc="*")
@captureOutput
def do_display_wc(self):
text = "\n".join(
[r['entity'] for r in self.parquet_df.select("entity").collect() if r['entity'] is not None]
)
plt.figure( figsize=(13,7) )
plt.axis("off")
plt.imshow(
WordCloud(width=750, height=350).generate(text),
interpolation='bilinear'
)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode23.py
传递给 WordCloud
类的文本是通过收集 parquet_df
批处理 DataFrame 中的所有实体生成的。
以下截图展示了在使用搜索查询 baseball
创建的 Twitter 流运行一段时间后的仪表盘:
用于搜索查询“baseball”的 Twitter 情感仪表盘
第二个 PixieApp 用于监控正在积极运行的流查询。主路由返回一个 HTML 片段,该片段包含一个 <div>
元素,该元素定期(每 5000 毫秒)调用 show_progress
路由,如以下代码所示:
@PixieApp
class StreamingQueriesApp():
@route()
def main_screen(self):
return """
<div class="no_loading_msg" pd_refresh_rate="5000" pd_options="show_progress=true">
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode24.py
在show_progress
路由中,我们使用了本章之前描述的query.lastProgress
监控 API,通过 Jinja2 {%for%}
循环遍历 JSON 对象,并如以下代码所示在表格中显示结果:
@route(show_progress="true")
def do_show_progress(self):
return """
{%for query in this.spark.streams.active%}
<div>
<div class="page-header">
<h1>Progress Report for Spark Stream: {{query.id}}</h1>
<div>
<table>
<thead>
<tr>
<th>metric</th>
<th>value</th>
</tr>
</thead>
<tbody>
{%for key, value in query.lastProgress.items()%}
<tr>
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
{%endfor%}
</tbody>
</table>
{%endfor%}
"""
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode25.py
以下截图显示了 PixieApp 的流查询监控:
实时监控活动的 Spark 流查询
最后一步是使用TemplateTabbedApp
类来集成完整的应用程序,如下所示的代码:
from pixiedust.display.app import *
from pixiedust.apps.template import TemplateTabbedApp
@PixieApp
class TwitterSentimentApp(TemplateTabbedApp):
def setup(self):
self.apps = [
{"title": "Tweets Insights", "app_class": "TweetInsightApp"},
{"title": "Streaming Queries", "app_class": "StreamingQueriesApp"}
]
app = TwitterSentimentApp()
app.run()
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode26.py
我们的示例应用程序第三部分现已完成;您可以在这里找到完整的 Notebook:
注意
在下一部分,我们将讨论如何通过使用 Apache Kafka 进行事件流处理和 IBM Streams Designer 对流数据进行数据增强来使应用程序的数据管道更加可扩展。
第四部分 – 使用 Apache Kafka 和 IBM Streams Designer 增加可扩展性
注意
注意:本节为可选部分。它演示了如何通过使用基于云的流服务重新实现数据管道的部分,以实现更大的可扩展性。
在单个 Notebook 中实现整个数据管道使我们在开发和测试过程中具有很高的生产力。我们可以快速实验代码并测试更改,且占用的资源非常小。由于我们使用的是相对较小的数据量,性能也很合理。然而,显然我们不会在生产环境中使用这种架构,接下来我们需要问自己的是,随着来自 Twitter 的流数据量急剧增加,哪些瓶颈会阻碍应用程序的扩展。
在本节中,我们确定了两个改进的方向:
-
在 Tweepy 流中,传入的数据会通过
on_data
方法发送到RawTweetsListener
实例进行处理。我们需要确保在此方法中尽量减少时间消耗,否则随着传入数据量的增加,系统将会落后。在当前的实现中,数据是通过外部调用 Watson NLU 服务同步丰富的,然后将数据缓冲,最终写入磁盘。为了解决这个问题,我们将数据发送到 Kafka 服务,这是一个高可扩展性、容错的流平台,使用发布/订阅模式来处理大量数据。我们还使用了 Streaming Analytics 服务,它将从 Kafka 消费数据并通过调用 Watson NLU 服务来丰富数据。两个服务都可以在 IBM Cloud 上使用。注意
注意:我们可以使用其他开源框架来处理流数据,例如 Apache Flink(
flink.apache.org
)或 Apache Storm(storm.apache.org
)。 -
在当前实现中,数据以 CSV 文件形式存储,我们使用输出目录作为源创建一个 Spark Streaming DataFrame。这个步骤会消耗 Notebook 和本地环境的时间和资源。相反,我们可以让 Streaming Analytics 将丰富后的事件写回到不同的主题,并创建一个以 Message Hub 服务作为 Kafka 输入源的 Spark Streaming DataFrame。
下图展示了我们示例应用程序的更新架构:
使用 Kafka 和 Streams Designer 扩展架构
在接下来的几个部分中,我们将实现更新后的架构,首先将推文流式传输到 Kafka。
将原始推文流式传输到 Kafka
在 IBM Cloud 上配置 Kafka / Message Hub 服务实例的过程与我们配置 Watson NLU 服务时的步骤相同。首先,我们在目录中找到并选择该服务,选择定价计划后点击 创建。然后,我们打开服务仪表板,选择 服务凭证 标签以创建新的凭证,如下图所示:
为 Message Hub 服务创建新的凭证
与 IBM Cloud 上的所有服务一样,凭证以 JSON 对象的形式提供,我们需要将其存储在 Notebook 中的一个变量里,代码如下所示(同样,别忘了将 XXXX
替换为您的用户名和服务凭证中的密码):
message_hub_creds = {
"instance_id": "XXXXX",
"mqlight_lookup_url": "https://mqlight-lookup-prod02.messagehub.services.us-south.bluemix.net/Lookup?serviceId=XXXX",
"api_key": "XXXX",
"kafka_admin_url": "https://kafka-admin-prod02.messagehub.services.us-south.bluemix.net:443",
"kafka_rest_url": "https://kafka-rest-prod02.messagehub.services.us-south.bluemix.net:443",
"kafka_brokers_sasl": [
"kafka03-prod02.messagehub.services.us-south.bluemix.net:9093",
"kafka01-prod02.messagehub.services.us-south.bluemix.net:9093",
"kafka02-prod02.messagehub.services.us-south.bluemix.net:9093",
"kafka05-prod02.messagehub.services.us-south.bluemix.net:9093",
"kafka04-prod02.messagehub.services.us-south.bluemix.net:9093"
],
"user": "XXXX",
"password": "XXXX"
}
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode27.py
关于与 Kafka 的接口,我们可以选择多个优秀的客户端库。我尝试了很多,但最终我使用得最多的是kafka-python
(github.com/dpkp/kafka-python
),它的优势是纯 Python 实现,因此更容易安装。
要从 Notebook 安装它,请使用以下命令:
!pip install kafka-python
注
注:像往常一样,在安装任何库之后,不要忘记重启内核。
kafka-python
库提供了一个KafkaProducer
类,用于将数据作为消息写入服务,我们需要用之前创建的凭证来配置它。Kafka 有多个配置选项,涵盖所有选项超出了本书的范围。所需的选项与身份验证、主机服务器和 API 版本相关。
以下代码实现了RawTweetsListener
类的__init__
构造函数。它创建了一个KafkaProducer
实例并将其存储为类变量:
[[RawTweetsListener]]
context = ssl.create_default_context()
context.options &= ssl.OP_NO_TLSv1
context.options &= ssl.OP_NO_TLSv1_1
kafka_conf = {
'sasl_mechanism': 'PLAIN',
'security_protocol': 'SASL_SSL',
'ssl_context': context,
"bootstrap_servers": message_hub_creds["kafka_brokers_sasl"],
"sasl_plain_username": message_hub_creds["user"],
"sasl_plain_password": message_hub_creds["password"],
"api_version":(0, 10, 1),
"value_serializer" : lambda v: json.dumps(v).encode('utf-8')
}
self.producer = KafkaProducer(**kafka_conf)
注
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode28.py
我们为value_serializer
键配置了一个 lambda 函数,用于序列化 JSON 对象,这是我们将用于数据的格式。
注
注:我们需要指定api_version
键,否则库会尝试自动发现其值,这会导致由于kafka-python
库中的一个 bug(只在 Mac 上可复现)引发NoBrokerAvailable
异常。编写本书时,尚未提供该 bug 的修复。
现在,我们需要更新on_data
方法,通过使用tweets
主题将推文数据发送到 Kafka。Kafka 主题就像一个频道,应用程序可以发布或订阅它。在尝试向主题写入之前,确保该主题已经创建,否则会引发异常。此操作在以下ensure_topic_exists
方法中完成:
import requests
import json
def ensure_topic_exists(topic_name):
response = requests.post(
message_hub_creds["kafka_rest_url"] +
"/admin/topics",
data = json.dumps({"name": topic_name}),
headers={"X-Auth-Token": message_hub_creds["api_key"]}
)
if response.status_code != 200 and \
response.status_code != 202 and \
response.status_code != 422 and \
response.status_code != 403:
raise Exception(response.json())
注
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode29.py
在前面的代码中,我们向路径/admin/topic
发出了一个 POST 请求,载荷为包含我们想要创建的主题名称的 JSON 数据。请求必须使用凭证中提供的 API 密钥和X-Auth-Token
头进行身份验证。我们还确保忽略 HTTP 错误码 422 和 403,它们表示该主题已经存在。
on_data
方法的代码现在看起来简单得多,如下所示:
[[RawTweetsListener]]
def on_data(self, data):
self.tweet_count += 1
self.producer.send(
self.topic,
{key:transform(key,value) \
for key,value in iteritems(json.loads(data)) \
if key in fieldnames}
)
return True
注
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode30.py
如我们所见,通过这段新代码,我们在on_data
方法中所花费的时间最少,这是我们想要实现的目标。推文数据现在正在流入 Kafka 的tweets
主题,准备通过我们将在下一节讨论的流式分析服务进行丰富化。
使用流式分析服务丰富推文数据
在这一步,我们需要使用 Watson Studio,这是一个集成的基于云的 IDE,提供多种数据处理工具,包括机器学习/深度学习模型、Jupyter Notebooks、流式数据流等。Watson Studio 是 IBM Cloud 的一个配套工具,可以通过datascience.ibm.com
访问,因此无需额外注册。
登录到 Watson Studio 后,我们创建一个新的项目,命名为Thoughtful Data Science
。
注意
注意:创建项目时,选择默认选项是可以的。
然后,我们进入设置标签页创建一个流式分析服务,它将成为驱动我们丰富化过程的引擎,并将其与项目关联。请注意,我们也可以像为本章中其他服务一样,在 IBM Cloud 目录中创建该服务,但由于我们仍然需要将其与项目关联,最好也在 Watson Studio 中进行创建。
在设置标签页中,我们向下滚动到关联服务部分,点击添加服务下拉菜单,选择流式分析。在接下来的页面中,您可以选择现有和新建。选择新建并按照步骤创建服务。创建完成后,新创建的服务应已与项目关联,如下图所示:
注意
注意:如果有多个免费选项,可以任选其一。
将流式分析服务与项目关联
现在我们准备创建定义推文数据丰富处理的流式数据流。
我们进入资源标签页,向下滚动到流式数据流部分,点击新建流式数据流按钮。在接下来的页面中,我们为其命名,选择流式分析服务,选择手动并点击创建按钮。
现在我们在流式设计器中,它由左侧的操作符调色板和一个可以用来图形化构建流式数据流的画布组成。对于我们的示例应用程序,我们需要从调色板中选择三个操作符并将它们拖放到画布上:
-
调色板中的源部分的消息中心:我们数据的输入源。进入画布后,我们将其重命名为
Source Message Hub
(通过双击进入编辑模式)。 -
处理和分析部分的代码:它将包含调用 Watson NLU 服务的数据丰富化 Python 代码。我们将操作符重命名为
Enrichment
。 -
来自调色板的目标部分中的 Message Hub:丰富数据的输出源。我们将其重命名为
目标 Message Hub
。
接下来,我们创建源 Message Hub与丰富之间,以及丰富与目标 Message Hub之间的连接。要创建两个操作符之间的连接,只需将第一个操作符末尾的输出端口拖动到另一个操作符的输入端口。请注意,源操作符右侧只有一个输出端口,表示它仅支持外部连接,而目标操作符左侧只有一个输入端口,表示它仅支持内部连接。处理与分析部分的任何操作符都有左右两个端口,因为它们同时接受和发送连接。
以下截图显示了完整的画布:
推文丰富流处理
现在让我们看一下这三个操作符的配置。
注意
注意:要完成此部分,请确保运行生成主题的代码,并将其发送到我们在前一部分讨论过的 Message Hub 实例。否则,Message Hub 实例将为空,且无法检测到任何模式。
点击源 Message Hub。右侧会出现一个动画窗格,提供选择包含推文的 Message Hub 实例的选项。第一次使用时,您需要创建与 Message Hub 实例的连接。选择tweets
作为主题。点击编辑输出模式,然后点击检测模式,以从数据中自动填充模式。您还可以使用显示预览按钮预览实时流数据,如下图所示:
设置模式并预览实时流数据
现在选择代码操作符,执行调用 Watson NLU 的代码。右侧的动画上下文窗格包含一个 Python 代码编辑器,其中包含所需实现的模板代码,分别是init(state)
和process(event, state)
函数。
在init
方法中,我们实例化了NaturalLanguageUnderstandingV1
实例,如下代码所示:
import sys
from watson_developer_cloud import NaturalLanguageUnderstandingV1
from watson_developer_cloud.natural_language_understanding_v1 import Features, SentimentOptions, EntitiesOptions
# init() function will be called once on pipeline initialization
# @state a Python dictionary object for keeping state. The state object is passed to the process function
def init(state):
# do something once on pipeline initialization and save in the state object
state["nlu"] = NaturalLanguageUnderstandingV1(
version='2017-02-27',
username='XXXX',
password='XXXX'
)
注意
您可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode31.py
注意:我们需要通过位于 Python 编辑器窗口上方的Python 包链接安装Watson_developer_cloud
库,如下图所示:
将 watson_cloud_developer 包添加到流处理中
每次事件数据都会调用该过程方法。我们使用它来调用 Watson NLU,并将额外的信息添加到事件对象中,如下代码所示:
# @event a Python dictionary object representing the input event tuple as defined by the input schema
# @state a Python dictionary object for keeping state over subsequent function calls
# return must be a Python dictionary object. It will be the output of this operator.
# Returning None results in not submitting an output tuple for this invocation.
# You must declare all output attributes in the Edit Schema window.
def process(event, state):
# Enrich the event, such as by:
# event['wordCount'] = len(event['phrase'].split())
try:
event['text'] = event['text'].replace('"', "'")
response = state["nlu"].analyze(
text = event['text'],
features=Features(sentiment=SentimentOptions(), entities=EntitiesOptions())
)
event["sentiment"] = response["sentiment"]["document"]["label"]
top_entity = response["entities"][0] if len(response["entities"]) > 0 else None
event["entity"] = top_entity["text"] if top_entity is not None else ""
event["entity_type"] = top_entity["type"] if top_entity is not None else ""
except Exception as e:
return None
return event
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode32.py
注意:我们还必须通过使用编辑输出架构链接声明所有输出变量,如下截图所示:
声明所有输出变量用于代码操作符
最后,我们配置目标 Message Hub 以使用enriched_tweets
主题。请注意,首次需要手动创建该主题,方法是进入 IBM Cloud 上的 Message Hub 实例的仪表板并点击添加主题按钮。
然后我们使用主工具栏中的保存按钮保存流。流中的任何错误,无论是代码中的编译错误、服务配置错误还是其他任何错误,都将在通知面板中显示。在确保没有错误后,我们可以使用运行按钮运行流,该按钮将带我们进入流数据监控屏幕。此屏幕由多个面板组成。主面板显示不同的操作符,数据以小球的形式流动在操作符之间的虚拟管道中。我们可以点击管道,在右侧面板中显示事件负载。这对于调试非常有用,因为我们可以可视化数据如何在每个操作符中进行转换。
注意
注意:Streams Designer 还支持在代码操作符中添加 Python 日志消息,然后可以将其下载到本地机器进行分析。你可以在这里了解更多关于此功能的信息:
dataplatform.cloud.ibm.com/docs/content/streaming-pipelines/downloading_logs.html
下图显示了流式数据监控屏幕:
Twitter 情感分析流数据的实时监控屏幕
现在,我们的丰富推文数据已经通过enriched_tweets
主题流入 Message Hub 实例。在下一节中,我们将展示如何使用 Message Hub 实例作为输入源创建 Spark Streaming DataFrame。
使用 Kafka 输入源创建 Spark Streaming DataFrame
在最后一步中,我们创建一个 Spark Streaming DataFrame,它从 enriched_tweets
Kafka 主题中消费经过增强的推文,这个主题属于 Message Hub 服务。为此,我们使用内置的 Spark Kafka 连接器,并在 subscribe
选项中指定我们想要订阅的主题。同时,我们还需要在 kafka.bootstrap.servers
选项中指定 Kafka 服务器的列表,这些信息通过读取我们之前创建的全局 message_hub_creds
变量来获取。
注意
注意:你可能已经注意到,不同的系统为此选项使用不同的名称,这使得它更容易出错。幸运的是,如果拼写错误,异常将显示一个明确的根本原因信息。
上述选项是针对 Spark Streaming 的,我们仍然需要配置 Kafka 凭证,以便较低级别的 Kafka 消费者可以与 Message Hub 服务进行正确的身份验证。为了正确地将这些消费者属性传递给 Kafka,我们不使用 .option
方法,而是创建一个 kafka_options
字典,并将其作为参数传递给加载方法,代码如下所示:
def start_streaming_dataframe():
"Start a Spark Streaming DataFrame from a Kafka Input source"
schema = StructType(
[StructField(f["name"], f["type"], True) for f in field_metadata]
)
kafka_options = {
"kafka.ssl.protocol":"TLSv1.2",
"kafka.ssl.enabled.protocols":"TLSv1.2",
"kafka.ssl.endpoint.identification.algorithm":"HTTPS",
'kafka.sasl.mechanism': 'PLAIN',
'kafka.security.protocol': 'SASL_SSL'
}
return spark.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", ",".join(message_hub_creds["kafka_brokers_sasl"])) \
.option("subscribe", "enriched_tweets") \
.load(**kafka_options)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode33.py
你可能认为代码到此为止就完成了,因为 Notebook 的其他部分应该与 第三部分 – 创建实时仪表板 PixieApp 一致。这个想法是正确的,直到我们运行 Notebook 并开始看到 Spark 抛出异常,提示 Kafka 连接器无法找到。这是因为 Kafka 连接器并不包含在 Spark 的核心发行版中,必须单独安装。
不幸的是,这类基础设施层面的问题并不直接与手头的任务相关,然而它们经常发生,我们最终花费大量时间去修复它们。在 Stack Overflow 或其他技术网站搜索通常能够快速找到解决方案,但有时答案并不显而易见。在这种情况下,由于我们是在 Notebook 中运行,而不是在 spark-submit
脚本中运行,因此没有太多现成的帮助,我们只能自己尝试直到找到解决方法。要安装 spark-sql-kafka
,我们需要编辑本章前面讨论过的 kernel.json
文件,并将以下选项添加到 "PYSPARK_SUBMIT_ARGS"
项中:
--packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.3.0
当内核重启时,这个配置将自动下载依赖并将其缓存到本地。
现在应该可以正常工作了吧?嗯,暂时还不行。我们仍然需要配置 Kafka 的安全性,以使用我们 Message Hub 服务的凭证,而该服务使用 SASL 作为安全协议。为此,我们需要提供一个JAAS(即Java 认证和授权服务)配置文件,其中包含服务的用户名和密码。Kafka 的最新版本提供了一种灵活的机制,允许使用名为sasl.jaas.config
的消费者属性以编程方式配置安全性。不幸的是,Spark 的最新版本(截至写作时为 2.3.0)尚未更新为 Kafka 的最新版本。因此,我们必须退回到另一种配置 JAAS 的方式,即设置一个名为java.security.auth.login.config
的 JVM 系统属性,并指向一个jaas.conf
配置文件的路径。
我们首先在选择的目录中创建jaas.conf
文件,并将以下内容添加到其中:
KafkaClient {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="XXXX"
password="XXXX";
};
在上述内容中,将XXXX
替换为从 Message Hub 服务凭证中获得的用户名和密码。
然后,我们将以下配置添加到kernel.json
中的"PYSPARK_SUBMIT_ARGS"
条目:
--driver-java-options=-Djava.security.auth.login.config=<<jaas.conf path>>
作为参考,这里是一个包含这些配置的示例kernel.json
:
{
"language": "python",
"env": {
"SCALA_HOME": "/Users/dtaieb/pixiedust/bin/scala/scala-2.11.8",
"PYTHONPATH": "/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7/python/:/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7/python/lib/py4j-0.10.6-src.zip",
"SPARK_HOME": "/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7",
"PYSPARK_SUBMIT_ARGS": "--driver-java-options=-Djava.security.auth.login.config=/Users/dtaieb/pixiedust/jaas.conf --jars /Users/dtaieb/pixiedust/bin/cloudant-spark-v2.0.0-185.jar --driver-class-path /Users/dtaieb/pixiedust/data/libs/* --master local[10] --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.3.0 pyspark-shell",
"PIXIEDUST_HOME": "/Users/dtaieb/pixiedust",
"SPARK_DRIVER_MEMORY": "10G",
"SPARK_LOCAL_IP": "127.0.0.1",
"PYTHONSTARTUP": "/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7/python/pyspark/shell.py"
},
"display_name": "Python with Pixiedust (Spark 2.3)",
"argv": [
"python",
"-m",
"ipykernel",
"-f",
"{connection_file}"
]
}
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%207/sampleCode34.json
注意:我们在修改kernel.json
时,应该始终重启 Notebook 服务器,以确保所有新配置能够正确重新加载。
其余的 Notebook 代码没有变化,PixieApp 仪表盘应该依然能够正常工作。
注意
我们现在已经完成了示例应用的第四部分;您可以在这里找到完整的笔记本:
在本节末尾我们编写的额外代码提醒我们,与数据打交道的旅程永远不会是一条直线。我们必须准备好应对不同性质的障碍:可能是依赖库中的错误,或外部服务的限制。克服这些障碍不必让项目停滞太久。由于我们主要使用开源组件,我们可以借助像 Stack Overflow 这样的社交网站上志同道合的开发者社区,获取新的想法和代码示例,并在 Jupyter Notebook 中快速实验。
总结
在本章中,我们构建了一个数据管道,用于分析包含非结构化文本的大量流数据,并应用来自外部云服务的 NLP 算法来提取情感和文本中发现的其他重要实体。我们还构建了一个 PixieApp 仪表板,显示从推文中提取的实时指标和洞察。我们还讨论了多种分析大规模数据的技术,包括 Apache Spark 结构化流处理、Apache Kafka 和 IBM Streaming Analytics。像往常一样,这些示例应用程序的目标是展示如何构建数据管道,特别关注如何利用现有框架、库和云服务的可能性。
在下一章中,我们将讨论时间序列分析,这是另一个具有广泛行业应用的数据科学话题,我们将通过构建一个金融投资组合分析应用程序来说明这一点。
第八章 分析学习:预测 - 金融时间序列分析与预测
“在做出重要决策时,依靠直觉是可以的,但始终要用数据进行验证”
– David Taieb
时间序列的研究是数据科学中一个非常重要的领域,广泛应用于各行各业,包括天气、医学、销售以及金融等。它是一个广泛且复杂的主题,详尽讨论超出了本书的范围,但我们会在本章中简要提及一些重要的概念,并保持足够的高层次内容,不要求读者具备特定的知识。我们还将展示 Python 如何特别适合进行时间序列分析,使用诸如 pandas (pandas.pydata.org
)进行数据分析、NumPy (www.numpy.org
)进行科学计算,Matplotlib (matplotlib.org
)和 Bokeh (bokeh.pydata.org
)进行数据可视化。
本章从 NumPy 库的介绍开始,介绍其最重要的 API,这些 API 将在构建描述性分析时得到充分应用,分析代表股票历史金融数据的时间序列。我们将使用 Python 库,如statsmodels
(www.statsmodels.org/stable/index.html
),展示如何进行统计探索,找到如平稳性、自相关函数(ACF)和偏自相关函数(PACF)等属性,这些对于发现数据趋势并创建预测模型非常有用。接着,我们将通过构建一个 PixieApp 来操作这些分析,汇总股票历史金融数据的重要统计信息和可视化。
在第二部分,我们将尝试构建一个时间序列预测模型,预测股票的未来趋势。我们将使用自回归整合滑动平均模型,简称ARIMA,通过使用时间序列中的先前值来预测下一个值。ARIMA 是目前最流行的模型之一,尽管基于递归神经网络的新模型开始逐渐获得人气。
和往常一样,我们将在本章结束时,结合在StockExplorer
PixieApp 中构建 ARIMA 时间序列预测模型。
开始使用 NumPy
NumPy 库是 Python 在数据科学家社区中获得广泛关注的主要原因之一。它是一个基础库,许多流行库的构建都依赖于它,例如 pandas (pandas.pydata.org
)、Matplotlib (matplotlib.org
)、SciPy (www.scipy.org
)和 scikit-learn (scikit-learn.org
)。
NumPy 提供的关键功能包括:
-
一个功能强大的多维 NumPy 数组,称为 ndarray,具有非常高效的数学运算性能(至少与常规 Python 列表和数组相比)
-
通用函数,也简称
ufunc
,用于提供非常高效且易于使用的逐元素操作,适用于一个或多个 ndarray -
强大的 ndarray 切片和选择功能
-
广播函数,使得在遵守某些规则的前提下,可以对不同形状的 ndarray 进行算术运算
在开始探索 NumPy APIs 之前,有一个 API 是绝对必须了解的:lookfor()
。使用此方法,你可以通过查询字符串查找函数,这在 NumPy 提供的数百个强大 API 中非常有用。
例如,我可以查找一个计算数组平均值的函数:
import numpy as np
np.lookfor("average")
结果如下:
Search results for 'average'
----------------------------
numpy.average
Compute the weighted average along the specified axis.
numpy.irr
Return the Internal Rate of Return (IRR).
numpy.mean
Compute the arithmetic mean along the specified axis.
numpy.nanmean
Compute the arithmetic mean along the specified axis, ignoring NaNs.
numpy.ma.average
Return the weighted average of array over the given axis.
numpy.ma.mean
Returns the average of the array elements along given axis.
numpy.matrix.mean
Returns the average of the matrix elements along the given axis.
numpy.chararray.mean
Returns the average of the array elements along given axis.
numpy.ma.MaskedArray.mean
Returns the average of the array elements along given axis.
numpy.cov
Estimate a covariance matrix, given data and weights.
numpy.std
Compute the standard deviation along the specified axis.
numpy.sum
Sum of array elements over a given axis.
numpy.var
Compute the variance along the specified axis.
numpy.sort
Return a sorted copy of an array.
numpy.median
Compute the median along the specified axis.
numpy.nanstd
Compute the standard deviation along the specified axis, while
numpy.nanvar
Compute the variance along the specified axis, while ignoring NaNs.
numpy.nanmedian
Compute the median along the specified axis, while ignoring NaNs.
numpy.partition
Return a partitioned copy of an array.
numpy.ma.var
Compute the variance along the specified axis.
numpy.apply_along_axis
Apply a function to 1-D slices along the given axis.
numpy.ma.apply_along_axis
Apply a function to 1-D slices along the given axis.
numpy.ma.MaskedArray.var
Compute the variance along the specified axis.
在几秒钟内,我可以找到几个候选函数,而不必离开我的 Notebook 去查阅文档。在之前的例子中,我可以找到一些有趣的函数——np.average
和np.mean
——但我仍然需要了解它们的参数。同样,我不想花时间查阅文档,打断我的工作流,因此我使用 Jupyter Notebooks 的一个不太为人知的功能,提供函数的签名和文档字符串内联显示。要调用函数的内联帮助,只需将光标定位在函数末尾并使用Shift + Tab组合键。第二次按Shift + Tab时,将展开弹出窗口,显示更多文本,如下截图所示:
注意
注意:Shift + Tab 仅适用于函数。
在 Jupyter Notebook 中提供内联帮助。
使用此方法,我可以快速地遍历候选函数,直到找到适合我需求的那个。
需要注意的是,np.lookfor()
不仅限于查询 NumPy 模块;你也可以在其他模块中进行搜索。例如,以下代码在statsmodels
包中搜索与acf
(自相关函数)相关的方法:
import statsmodels
np.lookfor("acf", module = statsmodels)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode1.py
这将产生以下结果:
Search results for 'acf'
------------------------
statsmodels.tsa.vector_ar.var_model.var_acf
Compute autocovariance function ACF_y(h) up to nlags of stable VAR(p)
statsmodels.tsa.vector_ar.var_model._var_acf
Compute autocovariance function ACF_y(h) for h=1,...,p
statsmodels.tsa.tests.test_stattools.TestPACF
Set up for ACF, PACF tests.
statsmodels.sandbox.tsa.fftarma.ArmaFft.acf2spdfreq
not really a method
statsmodels.tsa.stattools.acf
Autocorrelation function for 1d arrays.
statsmodels.tsa.tests.test_stattools.TestACF_FFT
Set up for ACF, PACF tests.
...
创建一个 NumPy 数组
有很多方法可以创建 NumPy 数组。以下是最常用的方法:
-
从 Python 列表或元组使用
np.array()
,例如,np.array([1, 2, 3, 4])
。 -
来自 NumPy 工厂函数之一:
-
np.random
:一个提供非常丰富函数集的模块,用于随机生成值。该模块由以下几类组成:简单的随机数据:
rand
、randn
、randint
等排列:
shuffle
、permutation
分布:
geometric
、logistic
等注意
你可以在这里找到关于
np.random
模块的更多信息:docs.scipy.org/doc/numpy-1.14.0/reference/routines.random.html
-
np.arange
:返回一个在给定区间内均匀分布的 ndarray。函数签名:
numpy.arange([start, ]stop, [step, ]dtype=None)
例如:
np.arange(1, 100, 10)
结果:
array([ 1, 11, 21, 31, 41, 51, 61, 71, 81, 91])
-
np.linspace
:与np.arange
类似,返回一个在给定区间内均匀分布的 ndarray,不同之处在于,使用linspace
时你指定的是想要的样本数量,而不是步长数量。例如:
np.linspace(1,100,8, dtype=int)
结果:
array([ 1, 15, 29, 43, 57, 71, 85, 100])
-
np.full
,np.full_like
,np.ones
,np.ones_like
,np.zeros
,np.zeros_like
:创建一个用常数值初始化的 ndarray。例如:
np.ones( (2,2), dtype=int)
结果:
array([[1, 1], [1, 1]])
-
np.eye
,np.identity
,np.diag
:创建一个对角线元素为常数值的 ndarray:例如:
np.eye(3,3)
结果:
array([[1, 0, 0],[0, 1, 0],[0, 0, 1]])
注意
注意:当未提供
dtype
参数时,NumPy 会尝试从输入参数中推断数据类型。然而,可能会发生返回的类型不是正确的情况;例如,当应该是整数时,返回了浮动类型。此时,你应当使用dtype
参数强制指定数据类型。例如:np.arange(1, 100, 10, dtype=np.integer)
为什么 NumPy 数组比 Python 的列表和数组更快?
如前所述,NumPy 数组的操作比 Python 对应的操作要快得多。这是因为 Python 是动态语言,它不知道所处理数据的类型,因此必须不断查询与数据类型相关的元数据,以便将操作分派到正确的方法。而 NumPy 经过高度优化,能够处理大型多维数组,尤其是通过将 CPU 密集型操作委托给外部经过高度优化的 C 库,这些 C 库已被预编译。
为了能够做到这一点,NumPy 对 ndarray 施加了两个重要的约束:
-
-
ndarray 是不可变的:因此,如果你想改变一个 ndarray 的形状或大小,或者想添加/删除元素,你必须总是创建一个新的 ndarray。例如,下面的代码使用
arange()
函数创建一个包含均匀分布值的一维数组,然后将其重塑为一个 4x5 的矩阵:ar = np.arange(20) print(ar) print(ar.reshape(4,5))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode2.py
结果如下:
before: [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19] after: [[ 0 1 2 3 4] [ 5 6 7 8 9] [10 11 12 13 14] [15 16 17 18 19]]
-
ndarray 中的元素必须是相同类型:ndarray 在其
dtype
成员中存储元素类型。当使用nd.array()
函数创建新的 ndarray 时,NumPy 会自动推断一个适合所有元素的类型。例如:
np.array([1,2,3]).dtype
将返回dtype('int64')
。np.array([1,2,'3']).dtype
将返回dtype('<U21')
,其中<
表示小端字节序(见en.wikipedia.org/wiki/Endianness
),U21
表示 21 个字符的 Unicode 字符串。
注意
注意:你可以在这里找到有关所有支持的数据类型的详细信息:
docs.scipy.org/doc/numpy/reference/arrays.dtypes.html
对 ndarray 的操作
通常,我们需要对 ndarray 进行数据汇总。幸运的是,NumPy 提供了一套非常丰富的函数(也称为归约函数),可以直接对 ndarray 或 ndarray 的轴进行汇总。
作为参考,NumPy 的轴对应数组的一个维度。例如,一个二维 ndarray 有两个轴:一个沿行方向,称为轴 0;一个沿列方向,称为轴 1。
以下图示展示了二维数组中的轴:
二维数组中的轴
接下来我们讨论的大多数归约函数都将接受轴作为参数。它们分为以下几类:
-
数学函数:
-
三角函数:
np.sin
,np.cos
,等等 -
双曲线函数:
np.sinh
,np.cosh
,等等 -
四舍五入:
np.around
,np.floor
,等等 -
和、积、差:
np.sum
,np.prod
,np.cumsum
,等等 -
指数和对数:
np.exp
,np.log
,等等 -
算术运算:
np.add
,np.multiply
,等等 -
杂项:
np.sqrt
,np.absolute
,等等
注意
注意:所有这些一元函数(只接受一个参数的函数)都直接作用于 ndarray。例如,我们可以使用
np.square
一次性对数组中的所有值进行平方:代码:
np.square(np.arange(10))
结果:
array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81])
你可以在这里找到更多关于 NumPy 数学函数的信息:
-
-
统计函数:
-
顺序统计:
np.amin
,np.amax
,np.percentile
,等等 -
平均数和方差:
np.median
,np.var
,np.std
,等等 -
相关性:
np.corrcoef
,np.correlate
,np.cov
,等等 -
直方图:
np.histogram
,np.bincount
,等等
-
注意
注意:pandas 与 NumPy 紧密集成,让你能够在 pandas 的 DataFrame 上应用这些 NumPy 操作。在本章接下来的时间序列分析中,我们将充分利用这一功能。
以下代码示例创建一个 pandas DataFrame 并对所有列进行平方运算:
将 NumPy 操作应用于 pandas DataFrame
NumPy 数组的选择
NumPy 数组支持与 Python 数组和列表类似的切片操作。因此,使用 np.arrange()
方法创建的 ndarray,我们可以执行以下操作:
sample = np.arange(10)
print("Sample:", sample)
print("Access by index: ", sample[2])
print("First 5 elements: ", sample[:5])
print("From 8 to the end: ", sample[8:])
print("Last 3 elements: ", sample[-3:])
print("Every 2 elements: ", sample[::2])
注意
您可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode3.py
这将产生以下结果:
Sample: [0 1 2 3 4 5 6 7 8 9]
Access by index: 2
First 5 elements: [0 1 2 3 4]
From index 8 to the end: [8 9]
Last 3 elements: [7 8 9]
Every 2 elements: [0 2 4 6 8]
使用切片进行选择也适用于具有多个维度的 NumPy 数组。我们可以对数组的每个维度使用切片操作。而这在 Python 数组和列表中不可行,它们仅允许使用整数或切片进行索引。
注意
注意:作为参考,Python 中的切片语法如下:
start:end:step
作为示例,让我们创建一个形状为 (3,4)
的 NumPy 数组,即 3 行 * 4 列:
my_nparray = np.arange(12).reshape(3,4)
print(my_nparray)
返回:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
假设我想选择矩阵的中间部分,即 [5, 6]。我可以简单地对行和列应用切片,例如,[1:2]
选择第二行,[1:3]
选择第二行的第二和第三个值:
print(my_nparray[1:2, 1:3])
返回:
array([[5, 6]])
另一个有趣的 NumPy 特性是我们还可以使用谓词来用布尔值对 ndarray 进行索引。
例如:
print(sample > 5 )
返回:
[False False False False False False True True True True]
然后,我们可以使用布尔 ndarray 以简单优雅的语法选择数据的子集。
例如:
print( sample[sample > 5] )
返回:
[6 7 8 9]
注意
这是 NumPy 所有选择能力的一个小预览。有关更多 NumPy 选择的信息,您可以访问:
docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html
广播
广播是 NumPy 的一个非常方便的特性,它允许你对具有不同形状的 ndarrays 执行算术运算。广播这个术语来源于这样一个事实:较小的数组会自动被复制以适应较大的数组,从而使它们具有兼容的形状。然而,有一套规则决定了广播如何工作。
注意
您可以在这里找到更多关于广播的信息:
docs.scipy.org/doc/numpy/user/basics.broadcasting.html
NumPy 广播的最简单形式是标量广播,它允许你在 ndarray 和标量(即数字)之间执行逐元素的算术操作。
例如:
my_nparray * 2
返回:
array([[ 0, 2, 4, 6],
[ 8, 10, 12, 14],
[16, 18, 20, 22]])
注意
注意:在接下来的讨论中,我们假设我们要操作的两个 ndarray 具有不同的维度。
广播操作对于较小的数组仅需要遵循一个规则:其中一个数组必须至少有一个维度等于 1。其思想是沿着不匹配的维度复制较小的数组,直到它们的维度一致。
以下图示来自 www.scipy-lectures.org/
网站,生动地展示了两数组相加的不同情况:
广播流的解释
来源:www.scipy-lectures.org/_images/numpy_broadcasting.png
上图展示的三种使用情况是:
-
数组的维度匹配:按常规执行元素级的求和。
-
较小的数组只有 1 行:复制行,直到维度与第一个数组匹配。如果较小的数组只有 1 列,则会使用相同的算法。
-
第一个数组只有 1 列,第二个数组只有 1 行:
-
将第一个数组中的列复制,直到列数与第二个数组相同。
-
将第二个数组中的行复制,直到行数与第一个数组相同。
-
以下代码示例展示了 NumPy 广播的实际应用:
my_nparray + np.array([1,2,3,4])
结果:
array([[ 1, 3, 5, 7],
[ 5, 7, 9, 11],
[ 9, 11, 13, 15]])
本节中,我们提供了 NumPy 的基础介绍,至少足够让我们入门并跟随接下来章节中涵盖的代码示例。在下一节中,我们将开始讨论时间序列,并通过统计数据探索来寻找模式,帮助我们识别数据中的潜在结构。
时间序列的统计探索
对于示例应用,我们将使用 Quandl 数据平台金融 API 提供的股票历史金融数据 (www.quandl.com/tools/api
) 和 quandl
Python 库 (www.quandl.com/tools/python
)。
要开始使用,我们需要通过在独立的单元格中运行以下命令来安装 quandl
库:
!pip install quandl
注意
注意:和往常一样,安装完成后不要忘记重启内核。
访问 Quandl 数据是免费的,但每天限 50 次请求,不过你可以通过创建一个免费账户并获取 API 密钥来绕过此限制:
-
访问
www.quandl.com
,通过点击右上角的 SIGN UP 按钮创建一个新账户。 -
在注册向导的三步中填写表单。(我选择了 Personal,但根据你的情况,你可能想选择 Business 或 Academic。)
-
完成过程后,你应该会收到一封包含激活账户链接的电子邮件确认。
-
账户激活后,登录 Quandl 平台网站,在右上角菜单点击 Account Settings,然后转到 API KEY 标签。
-
复制本页面提供的 API 密钥。此值将用于在
quandl
Python 库中通过编程方式设置密钥,如以下代码所示:import quandl quandl.ApiConfig.api_key = "YOUR_KEY_HERE"
quandl
库主要由两个 API 组成:
-
quandl.get(dataset, **kwargs)
:这将返回一个 pandas DataFrame 或一个 NumPy 数组,表示请求的数据集。dataset
参数可以是一个字符串(单一数据集)或一个字符串列表(多个数据集)。每个数据集遵循database_code/dataset_code
的语法,其中database_code
是数据发布者,dataset_code
与资源相关。(接下来我们将介绍如何获取所有database_code
和dataset_code
的完整列表)。关键字参数使你能够精细化查询。你可以在 GitHub 上的
quandl
代码中找到支持的所有参数的完整列表:github.com/quandl/quandl-python/blob/master/quandl/get.py
。一个有趣的关键字参数
returns
控制方法返回的数据结构,它可以取以下两个值:-
pandas
:返回一个 pandas DataFrame -
numpy
:返回一个 NumPy 数组
-
-
quandl.get_table(datatable_code, **kwargs)
:返回一个非时间序列数据集(称为datatable
),用于描述某个资源。在本章中我们不会使用这个方法,但你可以通过查看代码了解更多:github.com/quandl/quandl-python/blob/master/quandl/get_table.py
。
为了获取database_code
的列表,我们使用 Quandl REST API:https://www.quandl.com/api/v3/databases?api_key=YOUR_API_KEY&page=n
,它使用了分页功能。
注意
注意:在前面的 URL 中,将YOUR_API_KEY
值替换为你实际的 API 密钥。
返回的 payload 是以下 JSON 格式:
{
"databases": [{
"id": 231,
"name": "Deutsche Bundesbank Data Repository",
"database_code": "BUNDESBANK",
"description": "Data on the German economy, ...",
"datasets_count": 49358,
"downloads": 43209922,
"premium": false,
"image": "https://quandl--upload.s3.amazonaws/...thumb_bundesbank.png",
"favorite": false,
"url_name": "Deutsche-Bundesbank-Data-Repository"
},...
],
"meta": {
"query": "",
"per_page": 100,
"current_page": 1,
"prev_page": null,
"total_pages": 3,
"total_count": 274,
"next_page": 2,
"current_first_item": 1,
"current_last_item": 100
}
}
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode4.json
我们使用while
循环加载所有可用的页面,依赖于payload['meta']['next_page']
值来判断何时停止。在每次迭代中,我们将database_code
信息的列表追加到一个名为databases
的数组中,如下所示:
import requests
databases = []
page = 1
while(page is not None):
payload = requests.get("https://www.quandl.com/api/v3/databases?api_key={}&page={}"\
.format(quandl.ApiConfig.api_key, page)).json()
databases += payload['databases']
page = payload['meta']['next_page']
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode5.py
databases
变量现在包含一个包含每个database_code
元数据的 JSON 对象数组。我们使用 PixieDust 的display()
API 以漂亮的可搜索表格形式查看数据:
import pixiedust
display(databases)
在下面的 PixieDust 表格截图中,我们使用第二章中描述的筛选按钮,用 Python 和 Jupyter Notebook 驱动数据分析,来查看每个数据库中可用数据集的统计信息,例如最小值、最大值和均值:
Quandl 数据库代码列表
在寻找包含 纽约证券交易所(NYSE)股票信息的数据库时,我找到了 XNYS
数据库,如下所示:
注意
注意:确保在图表选项对话框中将显示的值数量增加到 300
,以便所有结果都能在表格中显示。
寻找包含纽约证券交易所(NYSE)股票数据的数据库
不幸的是,XNYS
数据库是私有的,需要付费订阅。我最终使用了 WIKI
数据库代码,尽管它没有出现在之前 API 请求返回的列表中,但我在一些代码示例中找到了它。
我随后使用 https://www.quandl.com/api/v3/databases/{database_code}/codes
REST API 获取数据集列表。幸运的是,这个 API 返回的是一个压缩在 ZIP 文件中的 CSV 文件,PixieDust 的 sampleData()
方法可以轻松处理,如下代码所示:
codes = pixiedust.sampleData( "https://www.quandl.com/api/v3/databases/WIKI/codes?api_key=" + quandl.ApiConfig.api_key)
display(codes)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode6.py
在 PixieDust 表格界面中,我们点击 选项 对话框,将显示的值数量增加到 4000
,以便可以显示整个数据集(共有 3,198 条),并使用搜索框查找特定的股票,如下图所示:
注意
注意:搜索框只会搜索浏览器中显示的行,当数据集过大时,这可能是一个较小的子集。由于本例中数据集过大,增加显示的行数并不实际;因此,建议使用 筛选器,它可以确保查询整个数据集。
quandl
API 返回的 CSV 文件没有表头,但 PixieDust.sampleData()
期望文件中包含表头。这是当前的一个限制,将来会进行改进。
WIKI 数据库的数据集列表
在接下来的部分中,我们加载了微软(Microsoft)股票(股票代码 MSFT)过去几年的历史时间序列数据,并开始探索其统计属性。在以下代码中,我们使用 quandl.get()
来获取 WIKI/MSFT
数据集。我们添加了一个名为 daily_spread
的列,通过调用 pandas 的 diff()
方法计算每日涨跌,这个方法返回当前调整后的收盘价与前一个收盘价之间的差异。请注意,返回的 pandas DataFrame 使用日期作为索引,但 PixieDust 当前不支持按索引绘制时间序列图表。因此,在以下代码中,我们调用 reset_index()
将 DateTime
索引转换为一个名为 Date
的新列,其中包含日期信息:
msft = quandl.get('WIKI/MSFT')
msft['daily_spread'] = msft['Adj. Close'].diff()
msft = msft.reset_index()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode7.py
对于我们的第一次数据探索,我们使用display()
创建了一个基于 Bokeh 渲染器的股票调整后收盘价随时间变化的折线图。
以下截图显示了选项配置及其生成的折线图:
MSFT 股价随时间变化,已调整股息分配、股票拆分及其他公司行为
我们还可以生成一个图表,显示该期间每天的收益,以下截图展示了该图表:
MSFT 股票的每日收益
假设投资
作为练习,我们尝试创建一个图表,显示在所选股票(MSFT)中,假设投资 10,000 美元随着时间的推移会如何变化。为此,我们必须计算一个数据框,包含每一天的总投资价值,考虑到我们在上一段计算的每日收益,并使用 PixieDust 的display()
API 来可视化数据。
我们利用 pandas 的能力,根据日期条件选择行,首先过滤数据框,只选择我们感兴趣的时间段内的数据点。然后通过将初始投资 10,000 美元除以该期间第一天的收盘价来计算购买的股票数量,并加上初始投资价值。所有这些计算都变得非常简单,得益于 pandas 高效的系列计算和底层的 NumPy 基础库。我们使用np.cumsum()
方法(docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.cumsum.html
)来计算所有每日收益的累积和,并加上初始投资金额 10,000 美元。
最后,我们通过使用resample()
方法将频率从每日转换为每月,并使用该月的平均值计算新值,从而使图表更易于阅读。
以下代码计算了从 2016 年 5 月开始的增长数据框(DataFrame):
import pandas as pd
tail = msft[msft['Date'] > '2016-05-16']
investment = np.cumsum((10000 / tail['Adj. Close'].values[0]) * tail['daily_spread']) + 10000
investment = investment.astype(int)
investment.index = tail['Date']
investment = investment.resample('M').mean()
investment = pd.DataFrame(investment).reset_index()
display(investment)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode8.py
以下截图显示了display()
API 生成的图表,包括配置选项:
假设投资组合增长
自相关函数(ACF)和偏自相关函数(PACF)
在尝试生成预测模型之前,了解时间序列是否具有可识别的模式(如季节性或趋势)是至关重要的。一种常见的技术是查看数据点如何根据指定的时间滞后与前一个数据点进行关联。直观地说,自相关性会揭示内部结构,例如识别高相关性(正相关或负相关)发生的时期。你可以尝试不同的滞后值(也就是对于每个数据点,你会考虑多少个之前的数据点)来找到合适的周期性。
计算自相关函数通常需要计算一组数据点的皮尔逊相关系数(en.wikipedia.org/wiki/Pearson_correlation_coefficient
),这并不是一件简单的事情。好消息是,statsmodels
Python 库提供了一个tsa
包(tsa代表时间序列分析),它提供了紧密集成于 pandas Series 的辅助方法,用于计算自相关函数(ACF)。
注意
注意:如果尚未完成,请使用以下命令安装statsmodels
包,并在完成后重启内核:
!pip install statsmodels
以下代码使用来自tsa.api.graphics
包的plot_acf()
函数来计算并可视化 MSFT 股票时间序列的自相关函数(ACF):
import statsmodels.tsa.api as smt
import matplotlib.pyplot as plt
smt.graphics.plot_acf(msft['Adj. Close'], lags=100)
plt.show()
以下是结果:
滞后为 100 时的 MSFT 自相关函数(ACF)
上面的图表展示了数据在多个前期数据点(滞后)上的自相关性,这些滞后值由x坐标轴表示。因此,在滞后为0
时,你总是会有一个自相关值为1.0
(你总是与你自己完全相关),滞后1
显示的是与前一个数据点的自相关性,滞后2
显示的是与两个数据点之前的自相关性。我们可以清楚地看到,随着滞后的增加,自相关性逐渐下降。在上面的图表中,我们只使用了 100 个滞后,并且看到自相关性仍然在 0.9 左右,表明数据间的长时间间隔并没有关联。这表明数据存在趋势,从整体价格图上来看,这一点非常明显。
为了验证这个假设,我们绘制了一个具有更大lags
参数的 ACF 图,例如1000
(考虑到我们的数据序列有超过 10,000 个数据点,这并不为过),如下图所示:
滞后为 1000 时的 MSFT 自相关函数(ACF)
我们现在可以清楚地看到,自相关性在约600
个滞后值时低于显著性水平。
为了更好地说明 ACF 如何工作,让我们生成一个周期性的时间序列,该序列没有趋势,看看我们可以学到什么。例如,我们可以在用 np.linspace()
生成的一系列均匀间隔的点上使用 np.cos()
:
smt.graphics.plot_acf(np.cos(np.linspace(0, 1000, 100)), lags=50)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode9.py
结果如下:
没有趋势的周期性序列的 ACF
在前面的图表中,我们可以看到自相关再次在固定间隔(大约每 5 个滞后期)处出现峰值,清晰地显示了周期性(在处理真实世界数据时,这也称为季节性)。
使用 ACF 来检测时间序列中的结构有时会导致问题,尤其是当你有强周期性时。在这种情况下,无论你试图多么往回计算自相关,总会看到自相关在周期的倍数处出现一个峰值,这可能会导致错误的解释。为了解决这个问题,我们使用 PACF,它使用较短的滞后期,且与 ACF 不同,它不会重复使用之前在较短时间段内发现的相关性。ACF 和 PACF 的数学原理相当复杂,但读者只需要理解其背后的直觉,并愉快地使用像 statsmodels
这样的库来进行繁重的计算。我获取有关 ACF 和 PACF 更多信息的一个资源可以在这里找到:www.mathworks.com/help/econ/autocorrelation-and-partial-autocorrelation.html
。
回到我们的 MSFT 股票时间序列,以下代码展示了如何使用 smt.graphics
包绘制其 PACF:
import statsmodels.tsa.api as smt
smt.graphics.plot_pacf(msft['Adj. Close'], lags=50)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode10.py
结果显示在下面的截图中:
MSFT 股票时间序列的偏自相关
我们将在本章后面讨论 ARIMA 模型进行时间序列预测时,再回到 ACF 和 PACF。
在本节中,我们讨论了多种探索数据的方法。当然,这些方法绝非详尽无遗,但我们可以看到像 Jupyter、pandas、NumPy 和 PixieDust 等工具如何让实验变得更容易,并在必要时快速失败。在下一节中,我们将构建一个 PixieApp,将所有这些图表汇聚在一起。
将一切整合在一起,使用 StockExplorer PixieApp
对于我们 StockExplorer
PixieApp 的第一个版本,我们希望实现用户选择的股票数据时间序列的数据探索。与我们构建的其他 PixieApps 类似,第一个屏幕有一个简单的布局,包含一个输入框,用户可以在其中输入以逗号分隔的股票代码列表,并有一个 Explore 按钮来开始数据探索。主屏幕由一个垂直导航栏组成,每个数据探索类型都有一个菜单。为了使 PixieApp 代码更加模块化,并且更易于维护和扩展,我们将在自己的子 PixieApp 中实现每个数据探索屏幕,这些子 PixieApp 通过垂直导航栏触发。同时,每个子 PixieApp 都继承自一个名为 BaseSubApp
的基类,提供所有子类共有的功能。以下图显示了整体 UI 布局以及所有子 PixieApps 的类图:
股票探索 PixieApp 的 UI 布局
我们先来看一下欢迎屏幕的实现。它是在 StockExplorer
PixieApp 类的默认路由中实现的。以下代码显示了 StockExplorer
类的部分实现,仅包括默认路由。
注意
注意:在提供完整实现之前,不要尝试运行此代码。
@PixieApp
class StockExplorer():
@route()
def main_screen(self):
return """
<style>
div.outer-wrapper {
display: table;width:100%;height:300px;
}
div.inner-wrapper {
display: table-cell;vertical-align: middle;height: 100%;width: 100%;
}
</style>
<div class="outer-wrapper">
<div class="inner-wrapper">
<div class="col-sm-3"></div>
<div class="input-group col-sm-6">
<input id="stocks{{prefix}}" type="text"
class="form-control"
value="MSFT,AMZN,IBM"
placeholder="Enter a list of stocks separated by comma e.g MSFT,AMZN,IBM">
<span class="input-group-btn">
<button class="btn btn-default" type="button" pd_options="explore=true">
<pd_script>
self.select_tickers('$val(stocks{{prefix}})'.split(','))
</pd_script>
Explore
</button>
</span>
</div>
</div>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode11.py
上述代码与我们迄今为止看到的其他示例 PixieApps 非常相似。Explore 按钮包含以下两个 PixieApp 属性:
-
一个
pd_script
子元素,它调用一个 Python 代码片段来设置股票代码。我们还使用$val
指令来获取用户输入的股票代码值:<pd_script> self.select_tickers('$val(stocks{{prefix}})'.split(',')) </pd_script>
-
pd_options
属性,它指向explore
路由:pd_options="explore=true"
select_tickers
辅助方法将股票代码列表存储在字典成员变量中,并选择第一个作为活动股票代码。出于性能考虑,我们只在需要时加载数据,也就是在第一次设置活动股票代码时,或者当用户在 UI 中点击特定股票代码时。
注意
注意:与前几章一样,[[StockExplorer]]
表示以下代码是 StockExplorer
类的一部分。
[[StockExplorer]]
def select_tickers(self, tickers):
self.tickers = {ticker.strip():{} for ticker in tickers}
self.set_active_ticker(tickers[0].strip())
def set_active_ticker(self, ticker):
self.active_ticker = ticker
if 'df' not in self.tickers[ticker]:
self.tickers[ticker]['df'] = quandl.get('WIKI/{}'.format(ticker))
self.tickers[ticker]['df']['daily_spread'] = self.tickers[ticker]['df']['Adj. Close'] - self.tickers[ticker]['df']['Adj. Open']
self.tickers[ticker]['df'] = self.tickers[ticker]['df'].reset_index()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode12.py
特定股票代码的数据懒加载到 pandas DataFrame 是在set_active_ticker()
中完成的。我们首先检查 DataFrame 是否已经加载,通过查看df
键是否存在,如果不存在,我们调用quandl
API,传入dataset_code
:'WIKI/{ticker}'
。我们还添加了一个列,用于计算股票的每日波动,这将在基本探索界面中显示。最后,我们需要调用reset_index()
(pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.reset_index.html
)对 DataFrame 进行处理,将索引(DateTimeIndex
)转换为名为Date
的列。原因是 PixieDust 的display()
尚不支持可视化包含DateTimeIndex
的 DataFrame。
在explore
路由中,我们返回一个构建整个屏幕布局的 HTML 片段。如前面的示意图所示,我们使用btn-group-vertical
和btn-group-toggle
的 Bootstrap 类来创建垂直导航栏。菜单列表及其关联的子 PixieApp 定义在tabs
Python 变量中,并且我们使用 Jinja2 的{%for loop%}
来构建内容。我们还添加了一个占位符<div>
元素,id ="analytic_screen{{prefix}}"
,它将成为子 PixieApp 屏幕的接收容器。
explore
路由的实现如下所示:
[[StockExplorer]]
@route(explore="*")
@templateArgs
def stock_explore_screen(self):
tabs = [("Explore","StockExploreSubApp"),
("Moving Average", "MovingAverageSubApp"),
("ACF and PACF", "AutoCorrelationSubApp")]
return """
<style>
.btn:active, .btn.active {
background-color:aliceblue;
}
</style>
<div class="page-header">
<h1>Stock Explorer PixieApp</h1>
</div>
<div class="container-fluid">
<div class="row">
<div class="btn-group-vertical btn-group-toggle col-sm-2"
data-toggle="buttons">
{%for title, subapp in tabs%}
<label class="btn btn-secondary {%if loop.first%}active{%endif%}"
pd_options="show_analytic={{subapp}}"
pd_target="analytic_screen{{prefix}}">
<input type="radio" {%if loop.first%}checked{%endif%}>
{{title}}
</label>
{%endfor%}
</div>
<div id="analytic_screen{{prefix}}" class="col-sm-10">
</div>
</div>
"""
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode13.py
在前面的代码中,请注意我们使用了@templateArgs
装饰器,因为我们想在 Jinja2 模板中使用方法实现局部创建的tabs
变量。
垂直导航栏中的每个菜单都指向相同的analytic_screen{{prefix}}
目标,并通过{{subapp}}
引用的选定子 PixieApp 类名调用show_analytic
路由。
反过来,show_analytic
路由仅返回一个包含<div>
元素的 HTML 片段,该元素具有pd_app
属性,引用子 PixieApp 类名。我们还使用pd_render_onload
属性,要求 PixieApp 在浏览器 DOM 加载时立即渲染<div>
元素的内容。
以下代码用于show_analytic
路由:
@route(show_analytic="*")
def show_analytic_screen(self, show_analytic):
return """
<div pd_app="{{show_analytic}}" pd_render_onload></div>
"""
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode14.py
BaseSubApp – 所有子 PixieApp 的基类
现在让我们来看看每个子 PixieApp 的实现,以及如何使用基类BaseSubApp
来提供共享功能。对于每个子 PixieApp,我们希望用户能够通过标签页界面选择股票代码,如下图所示:
MSFT、IBM、AMZN 股票代码的选项卡小部件
为了避免为每个子 PixieApp 重复 HTML 片段,我们使用了一种我特别喜欢的技术,即创建一个名为add_ticker_selection_markup
的 Python 装饰器,它动态改变函数的行为(有关 Python 装饰器的更多信息,请参见 wiki.python.org/moin/PythonDecorators
)。这个装饰器是在BaseSubApp
类中创建的,并且会自动为路由预先附加选项卡选择小部件的 HTML 标记,代码如下所示:
[[BaseSubApp]]
def add_ticker_selection_markup(refresh_ids):
def deco(fn):
def wrap(self, *args, **kwargs):
return """
<div class="row" style="text-align:center">
<div class="btn-group btn-group-toggle"
style="border-bottom:2px solid #eeeeee"
data-toggle="buttons">
{%for ticker, state in this.parent_pixieapp.tickers.items()%}
<label class="btn btn-secondary {%if this.parent_pixieapp.active_ticker == ticker%}active{%endif%}"
pd_refresh=\"""" + ",".join(refresh_ids) + """\" pd_script="self.parent_pixieapp.set_active_ticker('{{ticker}}')">
<input type="radio" {%if this.parent_pixieapp.active_ticker == ticker%}checked{%endif%}>
{{ticker}}
</label>
{%endfor%}
</div>
</div>
""" + fn(self, *args, **kwargs)
return wrap
return deco
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode15.py
初看之下,上面的代码可能看起来很难读,因为add_ticker_selection_markup
装饰器方法包含了两层匿名嵌套方法。我们来尝试解释它们的目的,包括主要的add_ticker_selection_markup
装饰器方法:
-
add_ticker_selection_markup
:这是主要的装饰器方法,它接受一个名为refresh_ids
的参数,这个参数将在生成的标记中使用。该方法返回一个名为deco
的匿名函数,deco
接收一个函数参数。 -
deco
:这是一个包装方法,它接受一个名为fn
的参数,该参数是指向原始函数的指针,装饰器应用于该函数。当用户代码中调用该函数时,该方法返回一个名为wrap
的匿名函数,wrap
将在原始函数的地方被调用。 -
wrap
:这是最终的包装方法,它接受三个参数:-
self
:指向函数所属类的指针 -
*args
:原方法定义的任意可变参数(可能为空) -
**kwargs
:原方法定义的任意关键字参数(可能为空)
wrap
方法可以通过 Python 闭包机制访问其作用域外的变量。在这种情况下,它使用refresh_ids
生成选项卡小部件的标记,然后用self
、args
和kwargs
参数调用fn
函数。 -
注意
注意:如果上述解释看起来依然让你感到困惑,即使你读了多次,也不用担心。你现在只需使用这个装饰器,它不会影响你理解本章的其他内容。
StockExploreSubApp – 第一个子 PixieApp
现在我们可以实现第一个子 PixieApp,名为StockExploreSubApp
。在主屏幕中,我们创建了两个<div>
元素,每个元素都有一个pd_options
属性,该属性调用show_chart
路由,并将Adj. Close
和daily_spread
作为值。然后,show_chart
路由返回一个<div>
元素,其中pd_entity
属性指向parent_pixieapp.get_active_df()
方法,并且包含一个<pd_options>
元素,里面包含一个 JSON 负载,用于显示一个 Bokeh 线图,Date
作为* x 轴,任何作为参数传递的值作为 y *轴的列。我们还用BaseSubApp.add_ticker_selection_markup
装饰器装饰了该路由,使用前面两个<div>
元素的 ID 作为refresh_ids
参数。
以下代码显示了StockExplorerSubApp
子 PixieApp 的实现:
@PixieApp
class StockExploreSubApp(BaseSubApp):
@route()
@BaseSubApp.add_ticker_selection_markup(['chart{{prefix}}', 'daily_spread{{prefix}}'])
def main_screen(self):
return """
<div class="row" style="min-height:300px">
<div class="col-xs-6" id="chart{{prefix}}" pd_render_onload pd_options="show_chart=Adj. Close">
</div>
<div class="col-xs-6" id="daily_spread{{prefix}}" pd_render_onload pd_options="show_chart=daily_spread">
</div>
</div>
"""
@route(show_chart="*")
def show_chart_screen(self, show_chart):
return """
<div pd_entity="parent_pixieapp.get_active_df()" pd_render_onload>
<pd_options>
{
"handlerId": "lineChart",
"valueFields": "{{show_chart}}",
"rendererId": "bokeh",
"keyFields": "Date",
"noChartCache": "true",
"rowCount": "10000"
}
</pd_options>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode16.py
在前面的show_chart
路由中,pd_entity
使用了parent_pixieapp
中定义的get_active_df()
方法,该方法在StockExplorer
主类中定义,代码如下:
[[StockExplorer]]
def get_active_df(self):
return self.tickers[self.active_ticker]['df']
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode17.py
提醒一下,StockExploreSubApp
通过StockExplorer
路由中的Explore
路由内声明的tabs
数组变量中的元组与菜单关联:
tabs = [("Explore","StockExploreSubApp"), ("Moving Average", "MovingAverageSubApp"),("ACF and PACF", "AutoCorrelationSubApp")]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode18.py
下图显示了StockExploreSubApp
:
StockExploreSubApp 主屏幕
MovingAverageSubApp – 第二个子 PixieApp
第二个子 PixieApp 是MovingAverageSubApp
,它展示了所选股票代码的移动平均线图,并且可以通过滑块控件配置延迟。与股票选择标签类似,延迟滑块将在另一个子 PixieApp 中使用。我们可以使用与股票选择标签控件相同的装饰器技术,但这里我们希望能将延迟滑块放置在页面的任何位置。因此,我们将使用一个在BaseSubApp
类中定义的pd_widget
控件,名为lag_slider
,并返回一个用于滑块控件的 HTML 片段。它还添加了一个<script>
元素,使用 jQuery UI 模块中可用的 jQuery slider
方法(有关更多信息,请参见api.jqueryui.com/slider
)。我们还添加了一个change
事件处理函数,当用户选择新值时会被调用。在这个事件处理程序中,我们调用pixiedust.sendEvent
函数,发布一个类型为lagSlider
的事件,并包含新的延迟值的有效载荷。调用者有责任添加一个<pd_event_handler>
元素来监听该事件并处理有效载荷。
以下代码展示了lag_slider
pd_widget
的实现
[[BaseSubApp]]
@route(widget="lag_slider")
def slider_screen(self):
return """
<div>
<label class="field">Lag:<span id="slideval{{prefix}}">50</span></label>
<i class="fa fa-info-circle" style="color:orange"
data-toggle="pd-tooltip"
title="Selected lag used to compute moving average, ACF or PACF"></i>
<div id="slider{{prefix}}" name="slider" data-min=30
data-max=300
data-default=50 style="margin: 0 0.6em;">
</div>
</div>
<script>
$("[id^=slider][id$={{prefix}}]").each(function() {
var sliderElt = $(this)
var min = sliderElt.data("min")
var max = sliderElt.data("max")
var val = sliderElt.data("default")
sliderElt.slider({
min: isNaN(min) ? 0 : min,
max: isNaN(max) ? 100 : max,
value: isNaN(val) ? 50 : val,
change: function(evt, ui) {
$("[id=slideval{{prefix}}]").text(ui.value);
pixiedust.sendEvent({type:'lagSlider',value:ui.value})
},
slide: function(evt, ui) {
$("[id=slideval{{prefix}}]").text(ui.value);
}
});
})
</script>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode19.py
在MovingAverageSubApp
中,我们在默认路由中使用add_ticker_selection_markup
装饰器,并以chart{{prefix}}
作为参数,添加股票选择标签,并添加一个名为lag_slider
的pd_widget
的<div>
元素,包括一个<pd_event_handler>
来设置self.lag
变量并刷新chart
div。chart
div 使用pd_entity
属性,并调用get_moving_average_df()
方法,该方法调用来自所选 pandas DataFrame 的 pandas Series 上的rolling
方法(pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.rolling.html
)并对其调用mean()
方法。由于 PixieDust 的display()
尚不支持 pandas Series,我们使用 series 索引构建一个 pandas DataFrame,作为一个名为x
的列,并在get_moving_average_df()
方法中返回它。
以下代码展示了MovingAverageSubApp
子 PixieApp 的实现
@PixieApp
class MovingAverageSubApp(BaseSubApp):
@route()
@BaseSubApp.add_ticker_selection_markup(['chart{{prefix}}'])
def main_screen(self):
return """
<div class="row" style="min-height:300px">
<div class="page-header text-center">
<h1>Moving Average for {{this.parent_pixieapp.active_ticker}}</h1>
</div>
<div class="col-sm-12" id="chart{{prefix}}" pd_render_onload pd_entity="get_moving_average_df()">
<pd_options>
{
"valueFields": "Adj. Close",
"keyFields": "x",
"rendererId": "bokeh",
"handlerId": "lineChart",
"rowCount": "10000"
}
</pd_options>
</div>
</div>
<div class="row">
<div pd_widget="lag_slider">
<pd_event_handler
pd_source="lagSlider"
pd_script="self.lag = eventInfo['value']"
pd_refresh="chart{{prefix}}">
</pd_event_handler>
</div>
</div>
"""
def get_moving_average_df(self):
ma = self.parent_pixieapp.get_active_df()['Adj. Close'].rolling(window=self.lag).mean()
ma_df = pd.DataFrame(ma)
ma_df["x"] = ma_df.index
return ma_df
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode20.py
以下截图展示了MovingAverageSubApp
所显示的图表:
MovingAverageSubApp 截图
AutoCorrelationSubApp – 第三个子 PixieApp
对于第三个子 PixieApp,我们调用了AutoCorrelationSubApp
;我们展示了所选股票 DataFrame 的 ACF 和 PACF,它们是使用statsmodels
包计算得出的。
以下代码展示了AutoCorrelationSubApp
的实现,它还使用了add_ticker_selection_markup
装饰器和名为lag_slider
的pd_widget
:
import statsmodels.tsa.api as smt
@PixieApp
class AutoCorrelationSubApp(BaseSubApp):
@route()
@BaseSubApp.add_ticker_selection_markup(['chart_acf{{prefix}}', 'chart_pacf{{prefix}}'])
def main_screen(self):
return """
<div class="row" style="min-height:300px">
<div class="col-sm-6">
<div class="page-header text-center">
<h1>Auto-correlation Function</h1>
</div>
<div id="chart_acf{{prefix}}" pd_render_onload pd_options="show_acf=true">
</div>
</div>
<div class="col-sm-6">
<div class="page-header text-center">
<h1>Partial Auto-correlation Function</h1>
</div>
<div id="chart_pacf{{prefix}}" pd_render_onload pd_options="show_pacf=true">
</div>
</div>
</div>
<div class="row">
<div pd_widget="lag_slider">
<pd_event_handler
pd_source="lagSlider"
pd_script="self.lag = eventInfo['value']"
pd_refresh="chart_acf{{prefix}},chart_pacf{{prefix}}">
</pd_event_handler>
</div>
</div>
"""
@route(show_acf='*')
@captureOutput
def show_acf_screen(self):
smt.graphics.plot_acf(self.parent_pixieapp.get_active_df()['Adj. Close'], lags=self.lag)
@route(show_pacf='*')
@captureOutput
def show_pacf_screen(self):
smt.graphics.plot_pacf(self.parent_pixieapp.get_active_df()['Adj. Close'], lags=self.lag)
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode21.py
在前面的代码中,我们定义了两个路由:show_acf
和 show_pacf
,分别调用smt.graphics
包中的plot_acf
和plot_pacf
方法。我们还使用@captureOutput
装饰器,告诉 PixieApp 框架捕获由plot_acf
和plot_pacf
生成的输出。
以下截图展示了AutoCorrelationSubApp
所显示的图表:
AutoCorrelationSubApp 截图
在本节中,我们展示了如何组合一个简单的 PixieApp,用于对时间序列进行基本的数据探索,并显示各种统计图表。完整的 Notebook 可以在此找到:github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/StockExplorer%20-%20Part%201.ipynb
。
在接下来的部分中,我们尝试使用一个非常流行的模型来构建一个时间序列预测模型,称为自回归积分滑动平均(ARIMA)。
使用 ARIMA 模型的时间序列预测
ARIMA 是最流行的时间序列预测模型之一,正如其名字所示,它由三个部分组成:
-
AR:代表自回归,即应用线性回归算法,使用一个观测值及其自身的滞后观测值作为训练数据。
AR 模型使用以下公式:
其中
是从先前的观测中学习到的模型权重,
是观测值t的残差误差。
我们还称p为自回归模型的阶数,定义为前述公式中包含的滞后观测值的数量。
例如:
AR(2) 定义为:
AR(1) 定义为:
-
I:代表积分。为了使 ARIMA 模型有效,假设时间序列是平稳的,或者可以被转化为平稳的。如果一个序列的均值和方差随时间变化不大,我们称之为平稳序列 (
en.wikipedia.org/wiki/Stationary_process
)。注意
注意:严格平稳性也有一个概念,要求一组观测值的联合概率分布在时间平移时不发生变化。
使用数学符号,严格平稳性转化为:
和
对于任意的t, m 和 k 都是相同的,F是联合概率分布。
实际操作中,这个条件太严格,前述较弱的定义通常更为常用。
我们可以通过使用观测值与其前一个值之间的对数差分来对时间序列进行平稳化,如下方的公式所示:
可能在将时间序列真正转化为平稳序列之前,需要进行多次对数差分转换。我们称d为我们使用对数差分转换序列的次数。
例如:
I(0) 定义为不需要对数差分(该模型称为 ARMA 模型)。
I(1) 定义为需要 1 次对数差分。
I(2) 定义为需要 2 次对数差分。
注意
注意:在预测一个值之后,记得做与转换次数相同的反向转换。
-
MA:代表滑动平均。MA 模型使用当前观测值的均值的残差误差和滞后观测值的加权残差误差。我们可以使用以下公式来定义该模型:
其中
是时间序列的均值,
是序列中的残差误差,
是滞后残差误差的权重。
我们称q为滑动平均窗口的大小。
例如:
MA(0) 定义为不需要滑动平均(该模型称为 AR 模型)。
MA(1) 定义为使用 1 的滑动平均窗口。公式为:
根据前述定义,我们使用符号ARIMA(p,d,q)来定义一个 ARIMA 模型,其中p为自回归模型的阶数,d为积分/差分的阶数,q为滑动平均窗口的大小。
实现构建 ARIMA 模型的所有代码可能非常耗时。幸运的是,statsmodels
库在statsmodels.tsa.arima_model
包中实现了一个ARIMA
类,提供了训练模型所需的所有计算,使用fit()
方法来训练模型,使用predict()
方法来预测值。它还处理对数差分,使时间序列变得平稳。诀窍是找到用于构建最佳 ARIMA 模型的参数p、d和q。为此,我们使用以下的 ACF 和 PACF 图:
-
p值对应于 ACF 图首次越过统计显著性阈值的滞后数(在x坐标轴上)。
-
同样,q值对应于 PACF 图首次越过统计显著性阈值的滞后数(在x坐标轴上)。
构建 MSFT 股票时间序列的 ARIMA 模型
提醒一下,MSFT 股票时间序列的价格图表如下所示:
MSFT 股票序列图
在我们开始构建模型之前,让我们首先保留最后 14 天的数据用于测试,其余部分用于训练。
以下代码定义了两个新变量:train_set
和test_set
:
train_set, test_set = msft[:-14], msft[-14:]
注意
注意:如果你仍然不熟悉前面的切片表示法,请参考本章开头关于 NumPy 的部分。
从前面的图表中,我们可以清楚地观察到自 2012 年起的增长趋势,但没有明显的季节性。因此,我们可以放心地假设没有平稳性。我们首先尝试应用一次对数差分变换,并绘制相应的 ACF 和 PACF 图。
在以下代码中,我们通过对Adj. Close
列使用np.log()
来构建logmsft
pandas Series,然后使用logmsft
与滞后 1(使用shift()
方法)的差异来构建logmsft_diff
pandas DataFrame。像之前一样,我们还调用reset_index()
将Date
索引转换为列,以便 PixieDust 的display()
可以处理它:
logmsft = np.log(train_set['Adj. Close'])
logmsft.index = train_set['Date']
logmsft_diff = pd.DataFrame(logmsft - logmsft.shift()).reset_index()
logmsft_diff.dropna(inplace=True)
display(logmsft_diff)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode22.py
结果如下面的截图所示:
对数差分后的 MSFT 股票序列
从前面的图形来看,我们可以合理地认为,我们已成功将时间序列平稳化,且其均值为 0。我们还可以使用更严格的方法,通过使用 Dickey-Fuller 检验(en.wikipedia.org/wiki/Dickey%E2%80%93Fuller_test
)来检验平稳性,该检验测试AR(1)模型中是否存在单位根的原假设。
注意
注意:在统计学中,统计假设检验是通过取样来挑战假设是否成立,并判断该假设是否成立。我们查看 p 值(en.wikipedia.org/wiki/P-value
),它有助于判断结果的显著性。有关统计假设检验的更多细节可以在这里找到:
en.wikipedia.org/wiki/Statistical_hypothesis_testing
以下代码使用 statsmodels.tsa.stattools
包中的 adfuller
方法:
from statsmodels.tsa.stattools import adfuller
import pprint
ad_fuller_results = adfuller(
logmsft_diff['Adj. Close'], autolag = 'AIC', regression = 'c'
)
labels = ['Test Statistic','p-value','#Lags Used','Number of Observations Used']
pp = pprint.PrettyPrinter(indent=4)
pp.pprint({labels[i]: ad_fuller_results[i] for i in range(4)})
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode23.py
我们使用 pprint
包,它对 漂亮打印 任意 Python 数据结构非常有用。有关 pprint
的更多信息可以在这里找到:
docs.python.org/3/library/pprint.html
结果(在以下链接详细解释:www.statsmodels.org/devel/generated/statsmodels.tsa.stattools.adfuller.html
)显示在这里:
{
'Number of lags used': 3,
'Number of Observations Used': 8057,
'Test statistic': -48.071592138591136,
'MacKinnon's approximate p-value': 0.0
}
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode24.json
p 值低于显著性水平;因此,我们可以拒绝零假设,即 AR(1) 模型中存在单位根,这使我们有信心认为时间序列是平稳的。
然后我们绘制 ACF 和 PACF 图,这将为我们提供 ARIMA 模型的 p 和 q 参数:
以下代码构建了 ACF 图:
import statsmodels.tsa.api as smt
smt.graphics.plot_acf(logmsft_diff['Adj. Close'], lags=100)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode25.py
结果显示在以下截图中:
对数差分 MSFT 数据框的 ACF
从前面的 ACF 图中,我们可以看到相关性首次超过统计显著性阈值时,滞后值为 1。因此,我们将使用 p = 1 作为 ARIMA 模型的 AR 顺序。
我们对 PACF 做相同的操作:
smt.graphics.plot_pacf(logmsft_diff['Adj. Close'], lags=100)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode26.py
结果显示在以下截图中:
对数差分 MSFT 数据框的 PACF
从之前的 PACF 图中,我们还可以看到在滞后为 1 时,相关性首次越过了统计显著性阈值。因此,我们将使用q = 1作为 ARIMA 模型的 MA 阶数。
我们还只应用了对数差分转换一次。因此,我们将使用d = 1作为 ARIMA 模型的积分部分。
注意
注意:当调用ARIMA
类时,如果使用d = 0,你可能需要手动进行对数差分,在这种情况下,你需要自己在预测值上恢复转换。如果不进行此操作,statsmodels
包会在返回预测值之前自动恢复转换。
以下代码使用p = 1、d = 1、q = 1作为ARIMA
构造函数的顺序元组参数,对train_set
时间序列训练 ARIMA 模型。接着我们调用fit()
方法进行训练并获取模型:
from statsmodels.tsa.arima_model import ARIMA
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
arima_model_class = ARIMA(train_set['Adj. Close'], dates=train_set['Date'], order=(1,1,1))
arima_model = arima_model_class.fit(disp=0)
print(arima_model.resid.describe())
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode27.py
注意:我们使用warnings
包来避免在使用较旧版本的 NumPy 和 pandas 时可能出现的多个弃用警告。
在之前的代码中,我们使用train_set['Adj. Close']
作为ARIMA
构造函数的参数。由于我们使用的是 Series 数据,因此我们还需要传递train_set['Date']
系列作为dates
参数。请注意,如果我们传递的是带有DateIndex
索引的 pandas DataFrame,那么就不需要使用dates
参数了。ARIMA
构造函数的最后一个参数是order
参数,它是一个包含p、d和q的元组,正如本节开头所讨论的那样。
然后,我们调用fit()
方法,它返回实际的 ARIMA 模型,我们将使用该模型来预测数值。为了展示目的,我们使用arima_model.resid.describe()
打印模型的残差误差统计信息。
结果如下所示:
count 8.061000e+03
mean -5.785533e-07
std 4.198119e-01
min -5.118915e+00
25% -1.061133e-01
50% -1.184452e-02
75% 9.848486e-02
max 5.023380e+00
dtype: float64
平均残差误差是 ,该误差非常接近零,因此表明模型可能存在过拟合训练数据的情况。
现在我们已经有了模型,接下来尝试对其进行诊断。我们定义了一个名为plot_predict
的方法,它接受一个模型、一个日期系列和一个数字,表示我们想要回溯的时间段。然后我们调用 ARIMA 的plot_predict()
方法,绘制一个包含预测值和观察值的图表。
以下代码展示了plot_predict()
方法的实现,包括两次调用100
和10
:
def plot_predict(model, dates_series, num_observations):
fig = plt.figure(figsize = (12,5))
model.plot_predict(
start = str(dates_series[len(dates_series)-num_observations]),
end = str(dates_series[len(dates_series)-1])
)
plt.show()
plot_predict(arima_model, train_set['Date'], 100)
plot_predict(arima_model, train_set['Date'], 10)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode28.py
结果如下所示:
观察值与预测值图表
上述图表展示了预测值与训练集实际观察值的接近程度。现在我们使用之前被保留的测试集,进一步诊断模型。对于这一部分,我们使用forecast()
方法来预测下一个数据点。对于test_set
的每个值,我们从一个叫做 history 的观察数组中构建一个新的 ARIMA 模型,这个数组包含了训练数据并增添了每个预测值。
以下代码展示了compute_test_set_predictions()
方法的实现,该方法接收train_set
和test_set
作为参数,并返回一个包含forecast
列(包含所有预测值)和test
列(包含相应实际观察值)的 pandas DataFrame:
def compute_test_set_predictions(train_set, test_set):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
history = train_set['Adj. Close'].values
forecast = np.array([])
for t in range(len(test_set)):
prediction = ARIMA(history, order=(1,1,0)).fit(disp=0).forecast()
history = np.append(history, test_set['Adj. Close'].iloc[t])
forecast = np.append(forecast, prediction[0])
return pd.DataFrame(
{"forecast": forecast,
"test": test_set['Adj. Close'],
"Date": pd.date_range(start=test_set['Date'].iloc[len(test_set)-1], periods = len(test_set))
}
)
results = compute_test_set_predictions(train_set, test_set)
display(results)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode29.py
以下截图展示了结果图表:
预测值与实际值的图表
我们可以使用流行的mean_squared_error
方法来衡量误差,该方法来自 scikit-learn 包(en.wikipedia.org/wiki/Mean_squared_error
)并定义如下:
其中 是实际值,
是预测值。
以下代码定义了一个compute_mean_squared_error
方法,该方法接收一个测试集和一个预测集,并返回均方误差的值:
from sklearn.metrics import mean_squared_error
def compute_mean_squared_error(test_series, forecast_series):
return mean_squared_error(test_series, forecast_series)
print('Mean Squared Error: {}'.format(
compute_mean_squared_error( test_set['Adj. Close'], results.forecast))
)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode30.py
结果如下所示:
Mean Squared Error: 6.336538843075749
StockExplorer PixieApp 第二部分 - 使用 ARIMA 模型添加时间序列预测
在本节中,我们通过添加一个菜单来改进StockExplorer
PixieApp,该菜单为选定的股票代码提供基于 ARIMA 模型的时间序列预测。我们创建了一个名为ForecastArimaSubApp
的新类,并更新了主StockExplorer
类中的tabs
变量。
[[StockExplorer]]
@route(explore="*")
@templateArgs
def stock_explore_screen(self):
tabs = [("Explore","StockExploreSubApp"),
("Moving Average", "MovingAverageSubApp"),
("ACF and PACF", "AutoCorrelationSubApp"),
("Forecast with ARIMA", "ForecastArimaSubApp")]
...
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode31.py
ForecastArimaSubApp
子 PixieApp 由两个屏幕组成。第一个屏幕显示时间序列图表以及 ACF 和 PACF 图表。这个屏幕的目标是为用户提供必要的数据探索,帮助他们确定 ARIMA 模型的p、d和q阶数,如前一节所解释的那样。通过查看时间序列图表,我们可以判断时间序列是否平稳(提醒一下,这是构建 ARIMA 模型的前提条件)。如果不是,用户可以点击添加差分按钮,尝试通过对数差分转换使 DataFrame 平稳。然后,三个图表会使用转换后的 DataFrame 进行更新。
以下代码展示了ForecastArimaSubApp
子 PixieApp 的默认路由:
from statsmodels.tsa.arima_model import ARIMA
@PixieApp
class ForecastArimaSubApp(BaseSubApp):
def setup(self):
self.entity_dataframe = self.parent_pixieapp.get_active_df().copy()
self.differencing = False
def set_active_ticker(self, ticker):
BaseSubApp.set_active_ticker(self, ticker)
self.setup()
@route()
@BaseSubApp.add_ticker_selection_markup([])
def main_screen(self):
return """
<div class="page-header text-center">
<h2>1\. Data Exploration to test for Stationarity
<button class="btn btn-default"
pd_script="self.toggle_differencing()" pd_refresh>
{%if this.differencing%}Remove differencing{%else%}Add differencing{%endif%}
</button>
<button class="btn btn-default"
pd_options="do_forecast=true">
Continue to Forecast
</button>
</h2>
</div>
<div class="row" style="min-height:300px">
<div class="col-sm-10" id="chart{{prefix}}" pd_render_onload pd_options="show_chart=Adj. Close">
</div>
</div>
<div class="row" style="min-height:300px">
<div class="col-sm-6">
<div class="page-header text-center">
<h3>Auto-correlation Function</h3>
</div>
<div id="chart_acf{{prefix}}" pd_render_onload pd_options="show_acf=true">
</div>
</div>
<div class="col-sm-6">
<div class="page-header text-center">
<h3>Partial Auto-correlation Function</h3>
</div>
<div id="chart_pacf{{prefix}}" pd_render_onload pd_options="show_pacf=true">
</div>
</div>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode32.py
上面的代码遵循了我们现在应该熟悉的模式:
-
定义一个
setup
方法,确保 PixieApp 启动时会调用。在这个方法中,我们从父级 PixieApp 复制选中的 DataFrame,并维护一个名为self.differencing
的变量,用于跟踪用户是否点击了添加差分按钮。 -
我们创建了一个默认路由,显示由以下组件组成的第一个屏幕:
-
一个带有两个按钮的头部:
添加差分
(用于使时间序列平稳)和继续预测
(用于显示第二个屏幕,稍后我们将讨论)。当差分已应用时,添加差分
按钮会切换为移除差分
。 -
一个
<div>
元素,它调用show_chart
路由来显示时间序列图表。 -
一个
<div>
元素,它调用show_acf
路由来显示 ACF 图表。 -
一个
<div>
元素,它调用show_pacf
路由来显示 PACF 图表。
-
-
我们使用一个空数组
[]
作为@BaseSubApp.add_ticker_selection_markup
装饰器的参数,确保当用户选择另一个股票代码时,整个屏幕会刷新,并且从第一个屏幕重新开始。我们还需要重置内部变量。为此,我们对add_ticker_selection_markup
进行了修改,定义了BaseSubApp
中的一个新方法set_active_ticker
,它是父级 PixieApp 中的set_active_ticker
方法的封装。这个设计的目的是让子类能够重写这个方法,并根据需要注入额外的代码。我们还修改了pd_script
属性,用于当用户选择新的股票代码时调用该方法,如下代码所示:[[BaseSubApp]] def add_ticker_selection_markup(refresh_ids): def deco(fn): def wrap(self, *args, **kwargs): return """ <div class="row" style="text-align:center"> <div class="btn-group btn-group-toggle" style="border-bottom:2px solid #eeeeee" data-toggle="buttons"> {%for ticker, state in this.parent_pixieapp.tickers.items()%} <label class="btn btn-secondary {%if this.parent_pixieapp.active_ticker == ticker%}active{%endif%}" pd_refresh=\"""" + ",".join(refresh_ids) + """\" pd_script="self.set_active_ticker('{{ticker}}')"> <input type="radio" {%if this.parent_pixieapp.active_ticker == ticker%}checked{%endif%}> {{ticker}} </label> {%endfor%} </div> </div> """ + fn(self, *args, **kwargs) return wrap return deco def set_active_ticker(self, ticker): self.parent_pixieapp.set_active_ticker(ticker)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode33.py
在ForecastArimaSubApp
子 PixieApp 中,我们覆盖了set_active_tracker
方法,首先调用父类方法,然后调用self.setup()
来重新初始化内部变量:
[[ForecastArimaSubApp]]
def set_active_ticker(self, ticker):
BaseSubApp.set_active_ticker(self, ticker)
self.setup()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode34.py
第一个预测屏幕的路由实现非常简单。Add differencing
/ Remove differencing
按钮具有pd_script
属性,调用self.toggle_differencing()
方法,并且具有pd_refresh
属性来更新整个页面。它还定义了三个<div>
元素,分别调用show_chart
、show_acf
和show_pacf
路由,如以下代码所示:
[[ForecastArimaSubApp]]
@route()
@BaseSubApp.add_ticker_selection_markup([])
def main_screen(self):
return """
<div class="page-header text-center">
<h2>1\. Data Exploration to test for Stationarity
<button class="btn btn-default"
pd_script="self.toggle_differencing()" pd_refresh>
{%if this.differencing%}Remove differencing{%else%}Add differencing{%endif%}
</button>
<button class="btn btn-default" pd_options="do_forecast=true">
Continue to Forecast
</button>
</h2>
</div>
<div class="row" style="min-height:300px">
<div class="col-sm-10" id="chart{{prefix}}" pd_render_onload pd_options="show_chart=Adj. Close">
</div>
</div>
<div class="row" style="min-height:300px">
<div class="col-sm-6">
<div class="page-header text-center">
<h3>Auto-correlation Function</h3>
</div>
<div id="chart_acf{{prefix}}" pd_render_onload pd_options="show_acf=true">
</div>
</div>
<div class="col-sm-6">
<div class="page-header text-center">
<h3>Partial Auto-correlation Function</h3>
</div>
<div id="chart_pacf{{prefix}}" pd_render_onload pd_options="show_pacf=true">
</div>
</div>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode35.py
toggle_differencing()
方法通过self.differencing
变量跟踪当前差分状态,并且要么从parent_pixieapp
复制活动数据框,要么对self.entity_dataframe
变量应用对数差分转换,如以下代码所示:
def toggle_differencing(self):
if self.differencing:
self.entity_dataframe = self.parent_pixieapp.get_active_df().copy()
self.differencing = False
else:
log_df = np.log(self.entity_dataframe['Adj. Close'])
log_df.index = self.entity_dataframe['Date']
self.entity_dataframe = pd.DataFrame(log_df - log_df.shift()).reset_index()
self.entity_dataframe.dropna(inplace=True)
self.differencing = True
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode36.py
show_acf
和show_pacf
路由实现非常简单。它们分别调用smt.graphics.plot_acf
和smt.graphics.plot_pacf
方法。它们还使用@captureOutput
装饰器将图表图像传递到目标小部件:
@route(show_acf='*')
@captureOutput
def show_acf_screen(self):
smt.graphics.plot_acf(self.entity_dataframe['Adj. Close'], lags=50)
@route(show_pacf='*')
@captureOutput
def show_pacf_screen(self):
smt.graphics.plot_pacf(self.entity_dataframe['Adj. Close'], lags=50)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode37.py
以下截图展示了没有差分的预测子 PixieApp 的数据探索页面:
没有应用差分的第一个预测屏幕
正如预期的那样,图表与非平稳的时间序列一致。当用户点击Add differencing按钮时,显示以下屏幕:
应用差分的第一个预测屏幕
下一步是实现 do_forecast
路由,它由 继续预测 按钮触发。这个路由负责构建 ARIMA 模型;它首先展示一个配置页面,其中有三个输入框,允许用户输入 p、d 和 q 顺序,这些顺序是通过查看数据探索界面中的图表推断得出的。我们添加了一个 Go
按钮来继续使用 build_arima_model
路由构建模型,稍后我们会在本节中讨论这个路由。页面头部还有一个 Diagnose Model
按钮,触发另一个页面,用于评估模型的准确性。
do_forecast
路由的实现如下所示。请注意,我们使用 add_ticker_selection_markup
并传递一个空数组,以便在用户选择另一个股票代码时刷新整个页面:
[[ForecastArimaSubApp]]
@route(do_forecast="true")
@BaseSubApp.add_ticker_selection_markup([])
def do_forecast_screen(self):
return """
<div class="page-header text-center">
<h2>2\. Build Arima model
<button class="btn btn-default"
pd_options="do_diagnose=true">
Diagnose Model
</button>
</h2>
</div>
<div class="row" id="forecast{{prefix}}">
<div style="font-weight:bold">Enter the p,d,q order for the ARIMA model you want to build</div>
<div class="form-group" style="margin-left: 20px">
<label class="control-label">Enter the p order for the AR model:</label>
<input type="text" class="form-control"
id="p_order{{prefix}}"
value="1" style="width: 100px;margin-left:10px">
<label class="control-label">Enter the d order for the Integrated step:</label>
<input type="text" class="form-control"
id="d_order{{prefix}}" value="1"
style="width: 100px;margin-left:10px">
<label class="control-label">Enter the q order for the MA model:</label>
<input type="text" class="form-control"
id="q_order{{prefix}}" value="1"
style="width: 100px;margin-left:10px">
</div>
<center>
<button class="btn btn-default"
pd_target="forecast{{prefix}}"
pd_options="p_order=$val(p_order{{prefix}});d_order=$val(p_order{{prefix}});q_order=$val(p_order{{prefix}})">
Go
</button>
</center>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode38.py
以下截图显示了 构建 ARIMA 模型 页面配置的界面:
构建 ARIMA 模型页面的配置页面
Go 按钮有一个 pd_options
属性,该属性调用一个有三个状态的路由:p_order
、d_order
和 q_order
,这些值取自与每个属性相关的三个输入框。
构建 ARIMA 模型的路由在下面的代码中显示。它首先将活动数据框(DataFrame)拆分为训练集和测试集,保留 14 个观测值作为测试集。然后它构建模型并计算残差误差。一旦模型成功构建,我们返回一个包含图表的 HTML 标记,图表显示训练集的预测值与实际训练集的值对比。这是通过调用 plot_predict
路由实现的。最后,我们还通过创建一个 <div>
元素,并为其设置 pd_entity
属性指向残差变量,使用 <pd_options>
子元素配置所有统计数据的表格视图,来显示模型的残差误差统计信息。
显示预测与实际训练集的对比图表使用了 plot_predict
路由,该路由调用了我们在笔记本中之前创建的 plot_predict
方法。我们还使用了 @captureOutput
装饰器,将图表图像发送到正确的组件。
plot_predict
路由的实现如下所示:
@route(plot_predict="true")
@captureOutput
def plot_predict(self):
plot_predict(self.arima_model, self.train_set['Date'], 100)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode39.py
build_arima_model
路由的实现如下所示:
@route(p_order="*",d_order="*",q_order="*")
def build_arima_model_screen(self, p_order, d_order, q_order):
#Build the arima model
self.train_set = self.parent_pixieapp.get_active_df()[:-14]
self.test_set = self.parent_pixieapp.get_active_df()[-14:]
self.arima_model = ARIMA(
self.train_set['Adj. Close'], dates=self.train_set['Date'],
order=(int(p_order),int(d_order),int(q_order))
).fit(disp=0)
self.residuals = self.arima_model.resid.describe().to_frame().reset_index()
return """
<div class="page-header text-center">
<h3>ARIMA Model succesfully created</h3>
<div>
<div class="row">
<div class="col-sm-10 col-sm-offset-3">
<div pd_render_onload pd_options="plot_predict=true">
</div>
<h3>Predicted values against the train set</h3>
</div>
</div>
<div class="row">
<div pd_render_onload pd_entity="residuals">
<pd_options>
{
"handlerId": "tableView",
"table_noschema": "true",
"table_nosearch": "true",
"table_nocount": "true"
}
</pd_options>
</div>
<h3><center>Residual errors statistics</center></h3>
<div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode40.py
以下截图显示了 构建 ARIMA 模型 页面结果:
模型构建页面
预测子应用的最终页面是由 do_diagnose
路由调用的 诊断模型 页面。在这个页面中,我们只是简单地显示了一个由 compute_test_set_predictions
方法返回的 DataFrame 的折线图,这个方法我们之前在 Notebook 中使用 train_set
和 test_set
变量创建过。这个图表的 <div>
元素使用了 pd_entity
属性,调用了一个中介类方法 compute_test_set_predictions
。它还有一个 <pd_options>
子元素,包含显示折线图的 display()
选项。
以下代码展示了 do_diagnose_screen
路由的实现:
def compute_test_set_predictions(self):
return compute_test_set_predictions(self.train_set, self.test_set)
@route(do_diagnose="true")
@BaseSubApp.add_ticker_selection_markup([])
def do_diagnose_screen(self):
return """
<div class="page-header text-center"><h2>3\. Diagnose the model against the test set</h2></div>
<div class="row">
<div class="col-sm-10 center" pd_render_onload pd_entity="compute_test_set_predictions()">
<pd_options>
{
"keyFields": "Date",
"valueFields": "forecast,test",
"handlerId": "lineChart",
"rendererId": "bokeh",
"noChartCache": "true"
}
</pd_options>
</div>
</div>
"""
注意
你可以在此找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/sampleCode41.py
以下截图显示了诊断页面的结果:
模型诊断页面
在这一部分中,我们展示了如何改进 StockExplorer
示例 PixieApp,加入使用 ARIMA 模型的预测能力。顺便提一下,我们演示了如何使用 PixieApp 编程模型创建一个三步向导,首先进行一些数据探索,然后配置模型的参数并构建模型,最后对模型进行测试集的诊断。
注意
完整的 Notebook 实现可以在此找到:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%208/StockExplorer%20-%20Part%202.ipynb
总结
在本章中,我们触及了时间序列分析和预测的话题。当然,我们只是略微触及了表面,实际上还有更多内容需要探索。这也是一个对行业非常重要的领域,特别是在金融领域,相关研究非常活跃。例如,我们看到越来越多的数据科学家尝试基于循环神经网络(en.wikipedia.org/wiki/Recurrent_neural_network
)算法构建时间序列预测模型,并取得了巨大成功。我们还展示了如何将 Jupyter Notebooks 与 PixieDust 以及 pandas
、numpy
和 statsmodels
等库的生态系统结合使用,帮助加速分析开发,并将其操作化为可以被业务用户使用的应用。
在下一章,我们将探讨另一个重要的数据科学应用场景:图形。我们将构建一个与航班旅行相关的示例应用,并讨论我们何时以及如何应用图算法来解决数据问题。
第九章:分析学习:图算法 - 美国国内航班数据分析
“在没有数据之前进行理论推测是一个严重的错误。”
福尔摩斯
本章我们将重点介绍一种基本的计算机科学数据模型——图,以及常用于图的各种算法。作为数据科学家或开发者,熟悉图并能迅速识别何时使用图来解决特定数据问题是非常重要的。例如,图非常适合于基于 GPS 的应用,如 Google Maps,用于寻找从 A 点到 B 点的最佳路线,考虑到各种参数,包括用户是开车、步行还是乘坐公共交通工具,或者用户是否希望选择最短路径,还是最大化使用高速公路而不考虑总体距离。这些参数中的一些也可以是实时参数,如交通状况和天气。另一个使用图的应用重要类别是社交网络,如 Facebook 或 Twitter,其中顶点代表个人,边表示关系,例如是朋友和关注。
本章将从图论和相关图算法的高层次介绍开始。然后我们将介绍networkx
,这是一个 Python 库,可以轻松加载、操作和可视化图数据结构,并提供丰富的图算法集合。接下来,我们将通过构建样本分析,使用各种图算法分析美国航班数据,其中机场作为顶点,航班作为边。像往常一样,我们还将通过构建一个简单的仪表板 PixieApp 来操作这些分析。最后,我们将通过建立一个预测模型,应用在第八章中学到的时间序列技术,来分析历史航班数据。
图论简介
图论的引入及相关理论广泛归功于 1736 年莱昂哈德·欧拉(Leonhard Euler),当时他研究了哥尼斯堡七桥问题(en.wikipedia.org/wiki/Seven_Bridges_of_K%C3%B6nigsberg
)。
这座城市被普雷格尔河分开,河流形成了两座岛屿,并根据以下图示建造了七座桥。问题是找到一种方法,使一个人能够走过每座桥一次且仅一次,并回到起点。欧拉证明了这个问题没有解,并在此过程中创立了图论。其基本思路是将城市的布局转化为图,其中每块陆地是一个顶点,每座桥是连接两个顶点(即陆地)的边。问题被简化为找到一条路径,这条路径是一个包含每座桥仅一次的连续边和顶点的序列。
以下图示展示了欧拉是如何将 哥尼斯堡七桥问题 简化为图论问题的:
将哥尼斯堡七桥问题简化为图论问题
使用更正式的定义,图是表示对象之间成对关系(称为边)的数据结构(对象称为顶点或节点)。通常使用以下符号来表示图:G = (V, E),其中 V 是顶点集,E 是边集。
图的表示主要有两大类:
-
有向图(称为有向图):在成对关系中,顺序是重要的,即从顶点 A 到顶点 B 的边(A-B)与从顶点 B 到顶点 A 的边(B-A)是不同的。
-
无向图:在成对关系中,顺序无关,即边(A-B)与边(B-A)是相同的。
以下图示展示了一个样本图的表示方式,既有无向图(边没有箭头),也有有向图(边有箭头):
图形表示
表示图的方式主要有两种:
-
邻接矩阵:使用 n 乘 n 矩阵(我们称之为 A)来表示图,其中 n 是图中顶点的数量。顶点通过 1 到 n 的整数进行索引。我们使用
来表示顶点 i 和顶点 j 之间存在边,使用
来表示顶点 i 和顶点 j 之间不存在边。在无向图的情况下,由于顺序无关,通常会有
。然而,在有向图中,由于顺序重要,A**[i,j] 可能与 A**[j,i] 不同。以下示例展示了如何在邻接矩阵中表示一个样本图,既适用于有向图,也适用于无向图:
图的邻接矩阵表示(包括有向和无向图)
需要注意的是,邻接矩阵表示法具有恒定的空间复杂度,其为
,其中 n 是顶点的数量,但它具有 O(1) 的时间复杂度,即常数时间可以判断两个顶点是否通过边连接。当图是密集的(即边很多)时,高空间复杂度可能是可以接受的,但当图是稀疏的时,这种方式可能会浪费空间,在这种情况下,我们可能会更倾向于使用下面的邻接表表示法。
注意
注意:大 O 符号(
en.wikipedia.org/wiki/Big_O_notation
)常用于代码分析中,通过评估算法在输入大小增长时的表现来表示算法的性能。它用于评估运行时间(运行算法所需的指令数量)和空间需求(随着时间推移所需的存储量)。 -
邻接表:对于每个顶点,我们维护一个与之相连的所有顶点的列表。在无向图中,每条边被表示两次,分别对应两个端点,而有向图则不同,边的顺序是有意义的。
下图展示了图的邻接表表示方法,包括有向图和无向图:
图的邻接表表示(包括有向图和无向图)
与邻接矩阵表示法相反,邻接表表示法具有较小的空间复杂度,时间复杂度为 O (m + n),其中 m 是边的数量,n 是顶点的数量。然而,时间复杂度相较于邻接矩阵的 O(1) 增加到 O(m)。因此,当图是稀疏连接时(即边较少),使用邻接表表示法更为合适。
正如前面讨论中所提示的,使用哪种图表示方法很大程度上依赖于图的密度,同时也取决于我们计划使用的算法类型。在接下来的章节中,我们将讨论最常用的图算法。
图算法
以下是最常用的图算法列表:
-
搜索:在图的上下文中,搜索意味着查找两个顶点之间的路径。路径被定义为一系列连续的边和顶点。图中搜索路径的动机可能有多种;它可能是你希望根据某些预定义的距离标准找到最短路径,例如最少的边数(例如,GPS 路线规划),或者你仅仅想知道两个顶点之间是否存在路径(例如,确保网络中每台机器都可以与其他机器互联)。一种通用的路径搜索算法是从给定的顶点开始,发现与其连接的所有顶点,标记已发现的顶点为已探索(以防止重复发现),并对每个已发现的顶点继续相同的探索,直到找到目标顶点,或者所有顶点都已被遍历。这个搜索算法有两种常用的变种:广度优先搜索(BFS)和深度优先搜索(DFS),它们各自有适用的场景,适合不同的使用情况。这两种算法的区别在于它们寻找未探索顶点的方式:
-
广度优先搜索(BFS):首先探索直接邻居的未探索节点。当一个层次的邻居已被探索完毕,开始探索每个节点的邻域,直到遍历完整个图。由于我们首先探索所有直接连接的顶点,这个算法保证能够找到最短路径,该路径与发现的邻居数量相对应。BFS 的一个扩展是著名的 Dijkstra 最短路径算法,其中每条边都与一个非负权重关联。在这种情况下,最短路径可能不是跳数最少的路径,而是最小化所有权重之和的路径。Dijkstra 最短路径的一个应用示例是查找地图上两点之间的最短路径。
-
深度优先搜索(DFS):对于每个直接邻居顶点,首先深入探索它的邻居,尽可能深入,直到没有更多邻居为止,然后开始回溯。DFS 的应用示例包括查找有向图的拓扑排序和强连通分量。作为参考,拓扑排序是顶点的线性排序,使得每个顶点在排序中按照边的方向依次排列(即,不会逆向)。更多信息请参见
en.wikipedia.org/wiki/Topological_sorting
。
下图展示了 BFS 和 DFS 在寻找未探索节点时的差异:
BFS 和 DFS 中未探索顶点的发现顺序
-
-
连通分量和强连通分量:图的连通分量是指一组顶点,其中任意两个顶点之间都有路径。请注意,定义只要求存在路径,这意味着两个顶点之间不必有边,只要有路径就行。对于有向图,连通分量被称为强连通分量,这是因为有额外的方向约束要求,不仅任何顶点 A 应该有路径通向任意其他顶点 B,而且 B 也必须有路径通向 A。
以下图示展示了强连通分量或一个示例有向图:
有向图的强连通分量
-
中心性:顶点的中心性指标提供了一个关于该顶点相对于图中其他顶点的重要性的衡量。中心性指数有多个重要应用。例如,识别社交网络中最有影响力的人,或根据页面的重要性对网页搜索结果进行排名,等等。
中心性有多个指标,但我们将专注于本章稍后使用的以下四个:
-
度数:顶点的度数是该顶点作为端点的边的数量。在有向图的情况下,它是该顶点作为源或目标的边的数量,我们称入度为该顶点作为目标的边的数量,出度为该顶点作为源的边的数量。
-
PageRank:这是由谷歌创始人拉里·佩奇和谢尔盖·布林开发的著名算法。PageRank 用于通过提供一个衡量网站重要性的指标来排名搜索结果,其中包括计算指向该网站的其他网站的链接数量。它还考虑了这些链接的质量估计(即,指向你网站的链接站点的可信度)。
-
接近度:接近度中心性与给定顶点与图中所有其他顶点之间最短路径的平均长度成反比。直观上,顶点距离其他所有节点越近,它就越重要。
接近度中心性可以通过以下简单公式计算:
(来源:https://en.wikipedia.org/wiki/Centrality#Closeness_centrality)
其中 d(y,x) 是节点 x 和 y 之间的边的长度。
-
最短路径介入度:这个度量是基于给定顶点在任意两节点之间最短路径上出现的次数。直观上,顶点对最短路径的贡献越大,它就越重要。最短路径介入度的数学公式如下所示:
(来源:https://en.wikipedia.org/wiki/Centrality#Betweenness_centrality)
其中
是从顶点 s 到顶点 t 的所有最短路径的总数,! 图算法 是经过 v 的
的子集。
注意
注意:更多关于中心性的详细信息可以在这里找到:
-
图与大数据
目前我们的图讨论主要集中在可以适应单台机器的数据上,但当我们有包含数十亿个顶点和边的大图时,如何处理呢?如果将整个数据加载到内存中是不可能的,如何解决呢?一个自然的解决方案是将数据分布到多个节点的集群中,在这些节点上并行处理数据,并将各自的结果合并形成最终答案。幸运的是,已经有多个框架提供了这种图并行计算能力,并且它们几乎都实现了大多数常用图算法。流行的开源框架包括 Apache Spark GraphX(spark.apache.org/graphx
)和 Apache Giraph(giraph.apache.org
),后者目前被 Facebook 用于分析其社交网络。
不深入细节,重要的是要知道,这些框架都受到大规模同步并行(BSP)分布式计算模型的启发(en.wikipedia.org/wiki/Bulk_synchronous_parallel
),该模型通过机器间的消息传递来查找集群中的顶点。需要记住的关键点是,这些框架通常非常易于使用,例如,使用 Apache Spark GraphX 编写本章的分析将相对简单。
在本节中,我们只回顾了所有图算法中的一小部分,深入探讨将超出本书的范围。自己实现这些算法将花费大量时间,但幸运的是,现有许多开源库提供了相当完整的图算法实现,并且易于使用和集成到应用程序中。在本章的其余部分,我们将使用networkx
开源 Python 库。
开始使用 networkx 图库
在我们开始之前,如果尚未完成,需要使用pip
工具安装networkx
库。请在单独的单元格中执行以下代码:
!pip install networkx
注意
注意:和往常一样,安装完成后不要忘记重启内核。
networkx
提供的大多数算法可以直接从主模块调用。因此,用户只需要以下的import
语句:
import networkx as nx
创建图
作为起点,让我们回顾一下networkx
支持的不同类型的图及其创建空图的构造函数:
-
Graph
:一个无向图,顶点之间只允许有一条边。允许自环边。构造函数示例:G = nx.Graph()
-
Digraph
:Graph
的子类,表示有向图。构造函数示例:G = nx.DiGraph()
-
MultiGraph
:允许顶点之间有多条边的无向图。构造函数示例:G = nx.MultiGraph()
-
MultiDiGraph
:允许顶点之间有多条边的有向图。构造函数示例:G = nx.MultiDiGraph()
Graph
类提供了许多方法用于添加和删除顶点及边。以下是可用方法的子集:
-
add_edge(u_of_edge, v_of_edge, **attr)
:在顶点u
和顶点v
之间添加一条边,并可选地添加附加属性,这些属性将与边相关联。如果顶点u
和v
在图中不存在,它们会自动创建。 -
remove_edge(u, v)
:移除顶点u
和v
之间的边。 -
add_node(self, node_for_adding, **attr)
:向图中添加一个节点,并可选地添加额外属性。 -
remove_node(n)
:移除由给定参数n
标识的节点。 -
add_edges_from(ebunch_to_add, **attr)
:批量添加多条边,并可选地添加额外属性。边必须以二元组(u,v)
或三元组(u,v,d)
的形式给出,其中d
是包含边数据的字典。 -
add_nodes_from(self, nodes_for_adding, **attr)
:批量添加多个节点,并可选地添加额外属性。节点可以作为列表、字典、集合、数组等提供。
作为练习,让我们从一开始就构建我们使用的有向图示例:
要通过编程方式使用 networkx 创建的示例图
以下代码通过创建一个 DiGraph()
对象开始,使用 add_nodes_from()
方法一次性添加所有节点,然后使用 add_edge()
和 add_edges_from()
的组合(作为示例)开始添加边:
G = nx.DiGraph()
G.add_nodes_from(['A', 'B', 'C', 'D', 'E'])
G.add_edge('A', 'B')
G.add_edge('B', 'B')
G.add_edges_from([('A', 'E'),('A', 'D'),('B', 'C'),('C', 'E'),('D', 'C')])
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode1.py
Graph
类还通过变量类视图提供了方便访问其属性的方法。例如,你可以使用 G.nodes
和 G.edges
遍历图的顶点和边,但也可以通过以下表示法访问单个边:G.edges[u,v]
。
以下代码遍历图的节点并打印它们:
for n in G.nodes:
print(n)
networkx
库还提供了一套丰富的预构建图生成器,这些生成器对于测试你的算法非常有用。例如,你可以通过 complete_graph()
生成器轻松生成一个完全图,如以下代码所示:
G_complete = nx.complete_graph(10)
注意
你可以在这里找到所有可用图生成器的完整列表:
networkx.github.io/documentation/networkx-2.1/reference/generators.html#generators
可视化一个图
NetworkX 支持多种渲染引擎,包括 Matplotlib、Graphviz AGraph (pygraphviz.github.io
) 和 Graphviz with pydot (github.com/erocarrera/pydot
)。尽管 Graphviz 提供了非常强大的绘图功能,但我发现它的安装非常困难。然而,Matplotlib 已经在 Jupyter Notebooks 中预装,可以让你快速开始。
核心绘图功能是 draw_networkx
,它接受一个图作为参数,并有一组可选的关键字参数,允许你对图进行样式设置,例如颜色、宽度,以及节点和边的标签字体。图形绘制的整体布局通过 pos
关键字参数传递 GraphLayout
对象来配置。默认布局是 spring_layout
(使用基于力的算法),但 NetworkX 支持许多其他布局,包括 circular_layout
、random_layout
和 spectral_layout
。你可以在这里查看所有可用布局的列表:networkx.github.io/documentation/networkx-2.1/reference/drawing.html#module-networkx.drawing.layout
。
为了方便,networkx
将每个布局封装成其高层绘图方法,调用合理的默认值,这样调用者就不需要处理每个布局的复杂性。例如,draw()
方法将使用 spring_layout
绘制图形,draw_circular()
使用 circular_layout
,draw_random()
使用 random_layout
。
在下面的示例代码中,我们使用 draw()
方法来可视化我们之前创建的 G_complete
图:
%matplotlib inline
import matplotlib.pyplot as plt
nx.draw(G_complete, with_labels=True)
plt.show()
注
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode2.py
结果显示在以下输出中:
绘制一个包含 10 个节点的完整图
使用 networkx
绘制图形既简单又有趣,并且由于它使用了 Matplotlib,你可以进一步美化它们,利用 Matplotlib 的绘图功能。我鼓励读者进一步实验,通过在笔记本中可视化不同的图形。在下一部分中,我们将开始实现一个示例应用程序,使用图算法分析航班数据。
第一部分 – 将美国国内航班数据加载到图中
为了初始化笔记本,接下来我们运行以下代码,在它自己的单元格中导入本章中将频繁使用的包:
import pixiedust
import networkx as nx
import pandas as pd
import matplotlib.pyplot as plt
我们还将使用 2015 年航班延误和取消 数据集,该数据集可以在 Kaggle 网站上找到,位置为:www.kaggle.com/usdot/datasets
。该数据集由三个文件组成:
-
airports.csv
:所有美国机场的列表,包括它们的IATA代码(国际航空运输协会:openflights.org/data.html
)、城市、州、经度和纬度。 -
airlines.csv
:美国航空公司的列表,包括它们的 IATA 代码。 -
flights.csv
:2015 年发生的航班列表。该数据包括日期、起点和终点机场、计划和实际时间以及延误情况。
flights.csv
文件包含接近 600 万条记录,需要清理,移除所有起点或终点机场没有 IATA 三字代码的航班。我们还需要删除ELAPSED_TIME
列中缺失值的行。如果不这样做,在将数据加载到图形结构时会出现问题。另一个问题是数据集包含一些时间列,比如DEPARTURE_TIME
和ARRIVAL_TIME
,为了节省空间,这些列只存储HHMM
格式的时间,实际日期存储在YEAR
、MONTH
和DAY
列中。我们将在本章进行的一个分析中需要完整的DEPARTURE_TIME
日期时间,因为进行这个转换是一个耗时的操作,所以我们现在就进行转换,并将处理后的版本存储在将上传到 GitHub 的flights.csv
文件中。此操作使用 pandas 的apply()
方法,调用to_datetime()
函数并设置axis=1
(表示该转换应用于每一行)。
另一个问题是我们想将文件存储在 GitHub 上,但 GitHub 对文件大小有 100MB 的最大限制。因此,为了将文件大小缩小到 100MB 以下,我们还删除了一些在我们构建分析中不需要的列,然后将文件压缩后再存储到 GitHub 上。当然,另一个好处是,较小的文件会使 DataFrame 加载得更快。
在从 Kaggle 网站下载文件后,我们运行以下代码,该代码首先将 CSV 文件加载到 pandas DataFrame 中,移除不需要的行和列,然后将数据写回到文件中:
注意
注意:原始数据存储在名为flights.raw.csv
的文件中。
运行以下代码可能需要一些时间,因为文件非常大,包含 600 万条记录。
import pandas as pd
import datetime
import numpy as np
# clean up the flights data in flights.csv
flights = pd.read_csv('flights.raw.csv', low_memory=False)
# select only the rows that have a 3 letter IATA code in the ORIGIN and DESTINATION airports
mask = (flights["ORIGIN_AIRPORT"].str.len() == 3) & (flights["DESTINATION_AIRPORT"].str.len() == 3)
flights = flights[ mask ]
# remove the unwanted columns
dropped_columns=["SCHEDULED_DEPARTURE","SCHEDULED_TIME",
"CANCELLATION_REASON","DIVERTED","DIVERTED","TAIL_NUMBER",
"TAXI_OUT","WHEELS_OFF","WHEELS_ON",
"TAXI_IN","SCHEDULED_ARRIVAL", "ARRIVAL_TIME", "AIR_SYSTEM_DELAY","SECURITY_DELAY",
"AIRLINE_DELAY","LATE_AIRCRAFT_DELAY", "WEATHER_DELAY"]
flights.drop(dropped_columns, axis=1, inplace=True)
# remove the row that have NA in the ELAPSED_TIME column
flights.dropna(subset=["ELAPSED_TIME"], inplace=True)
# remove the row that have NA in the DEPARTURE_TIME column
flights.dropna(subset=["ELAPSED_TIME"], inplace=True)
# Create a new DEPARTURE_TIME columns that has the actual datetime
def to_datetime(row):
departure_time = str(int(row["DEPARTURE_TIME"])).zfill(4)
hour = int(departure_time[0:2])
return datetime.datetime(year=row["YEAR"], month=row["MONTH"],
day=row["DAY"],
hour = 0 if hour >= 24 else hour,
minute=int(departure_time[2:4])
)
flights["DEPARTURE_TIME"] = flights.apply(to_datetime, axis=1)
# write the data back to file without the index
flights.to_csv('flights.csv', index=False)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode3.py
注意
注意:根据pandas.read_csv
文档(pandas.pydata.org/pandas-docs/version/0.23/generated/pandas.read_csv.html
),我们使用关键字参数low_memory=False
来确保数据不是按块加载,这样可以避免在类型推断时出现问题,特别是在处理非常大的文件时。
为了方便起见,三个文件存储在以下 GitHub 位置:github.com/DTAIEB/Thoughtful-Data-Science/tree/master/chapter%209/USFlightsAnalysis
。
以下代码使用 pixiedust.sampleData()
方法将数据加载到三个 pandas DataFrame 中,分别对应 airlines
、airports
和 flights
:
airports = pixiedust.sampleData("https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%209/USFlightsAnalysis/airports.csv")
airlines = pixiedust.sampleData("https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%209/USFlightsAnalysis/airlines.csv")
flights = pixiedust.sampleData("https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%209/USFlightsAnalysis/flights.zip")
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode4.py
注意:GitHub URL 使用了 /raw/
部分,表示我们希望下载原始文件,而不是相应 GitHub 页面上的 HTML。
下一步是使用 flights
DataFrame 作为 edge
列表,并将 ELAPSED_TIME
列的值作为权重,加载数据到 networkx
有向加权图对象中。我们首先通过使用 pandas.groupby()
方法按多重索引对所有起始地和目的地相同的航班进行去重,其中 ORIGIN_AIRPORT
和 DESTINATION_AIRPORT
为键。接着,我们从 DataFrameGroupBy
对象中选择 ELAPSED_TIME
列,并使用 mean()
方法对结果进行聚合。这将为我们提供一个新的 DataFrame,其中包含每个具有相同起始地和目的地机场的航班的平均 ELAPSED_TIME
:
edges = flights.groupby(["ORIGIN_AIRPORT","DESTINATION_AIRPORT"]) [["ELAPSED_TIME"]].mean()
edges
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode5.py
结果如下截图所示:
按起始地和目的地分组的航班,以及平均的 ELAPSED_TIME
在使用这个 DataFrame 创建有向图之前,我们需要将索引从多重索引重置为常规的单一索引,并将索引列转换为常规列。为此,我们只需使用 reset_index()
方法,如下所示:
edges = edges.reset_index()
edges
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode6.py
我们现在有了一个形状正确的 DataFrame,准备用于创建有向图,如下截图所示:
按起始地和目的地分组的航班,计算平均 ELAPSED_TIME,并使用单一索引
为了创建有向加权图,我们使用 NetworkX 的 from_pandas_edgelist()
方法,该方法以 pandas DataFrame 作为输入源。我们还指定了源列和目标列,以及权重列(在我们的例子中是 ELAPSED_TIME
)。最后,我们通过使用 create_using
关键字参数,传入 DiGraph
的实例,告诉 NetworkX 我们想要创建一个有向图。
以下代码展示了如何调用 from_pandas_edgelist()
方法:
flight_graph = nx.from_pandas_edgelist(
flights, "ORIGIN_AIRPORT","DESTINATION_AIRPORT",
"ELAPSED_TIME",
create_using = nx.DiGraph() )
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode7.py
注意:NetworkX 支持通过转换多种格式(包括字典、列表、NumPy 和 SciPy 矩阵以及 pandas)来创建图形。你可以在这里找到关于这些转换功能的更多信息:
networkx.github.io/documentation/networkx-2.1/reference/convert.html
我们可以通过直接打印图的节点和边来快速验证图的值是否正确:
print("Nodes: {}".format(flight_graph.nodes))
print("Edges: {}".format(flight_graph.edges))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode8.py
这会生成以下输出(已截断):
Nodes: ['BOS', 'TYS', 'RKS', 'AMA', 'BUF', 'BHM', 'PPG', …, 'CWA', 'DAL', 'BFL']
Edges: [('BOS', 'LAX'), ('BOS', 'SJC'), ..., ('BFL', 'SFO'), ('BFL', 'IAH')]
我们还可以使用 networkx
中内置的绘图 API 来创建更好的可视化效果,这些 API 支持多个渲染引擎,包括 Matplotlib、Graphviz AGraph (pygraphviz.github.io
) 和带有 pydot 的 Graphviz (github.com/erocarrera/pydot
)。
为了简化,我们将使用 NetworkX 的 draw()
方法,该方法使用现成的 Matplotlib 引擎。为了美化可视化效果,我们配置了合适的宽度和高度 (12, 12)
,并添加了一个色彩图,色彩鲜艳(我们使用了 matplotlib.cm
中的 cool
和 spring
色彩图,参见:matplotlib.org/2.0.2/examples/color/colormaps_reference.html
)。
以下代码展示了图形可视化的实现:
import matplotlib.cm as cm
fig = plt.figure(figsize = (12,12))
nx.draw(flight_graph, arrows=True, with_labels=True,
width = 0.5,style="dotted",
node_color=range(len(flight_graph)),
cmap=cm.get_cmap(name="cool"),
edge_color=range(len(flight_graph.edges)),
edge_cmap=cm.get_cmap(name="spring")
)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode9.py
这会生成以下结果:
使用 Matplotlib 快速可视化我们的有向图
在前面的图表中,节点是通过一种名为spring_layout
的默认图布局进行定位的,这是一种力导向布局。这种布局的一个优点是它能够快速显示出图中连接最多边的节点,这些节点位于图的中心。我们可以通过在调用draw()
方法时使用pos
关键字参数来更改图的布局。networkx
还支持其他类型的布局,包括circular_layout
、random_layout
、shell_layout
和spectral_layout
。
例如,使用random_layout
:
import matplotlib.cm as cm
fig = plt.figure(figsize = (12,12))
nx.draw(flight_graph, arrows=True, with_labels=True,
width = 0.5,style="dotted",
node_color=range(len(flight_graph)),
cmap=cm.get_cmap(name="cool"),
edge_color=range(len(flight_graph.edges)),
edge_cmap=cm.get_cmap(name="spring"),
pos = nx.random_layout(flight_graph)
)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode10.py
我们得到以下结果:
使用random_layout
的航班数据图
注意
注意:你可以在这里找到更多关于这些布局的信息:
networkx.github.io/documentation/networkx-2.1/reference/drawing.html
图的中心性
接下来,图的一个有趣分析点是其中心性指标,这可以帮助我们发现哪些节点是最重要的顶点。作为练习,我们将计算四种类型的中心性指标:度数、PageRank、接近度和最短路径中介中心性。然后,我们将扩展机场数据框,添加每个中心性指标的列,并使用 PixieDust 的display()
在 Mapbox 地图中可视化结果。
计算有向图的度数非常简单,只需使用networkx
的degree
属性,像这样:
print(flight_graph.degree)
这将输出一个元组数组,每个元组包含机场代码和度数索引,如下所示:
[('BMI', 14), ('RDM', 8), ('SBN', 13), ('PNS', 18), ………, ('JAC', 26), ('MEM', 46)]
现在我们想要在机场数据框中添加一个DEGREE
列,其中包含前述数组中每个机场行的度数值。为了做到这一点,我们需要创建一个包含两个列的新数据框:IATA_CODE
和DEGREE
,并在IATA_CODE
上执行 pandas 的merge()
操作。
合并操作在下图中展示:
合并度数数据框到机场数据框
以下代码展示了如何实现上述步骤。我们首先通过遍历flight_path.degree
输出创建一个 JSON 负载,并使用pd.DataFrame()
构造函数创建数据框。然后我们使用pd.merge()
,将airports
和degree_df
作为参数传入。我们还使用on
参数,其值为IATA_CODE
,这是我们要进行连接的键列:
degree_df = pd.DataFrame([{"IATA_CODE":k, "DEGREE":v} for k,v in flight_graph.degree], columns=["IATA_CODE", "DEGREE"])
airports_centrality = pd.merge(airports, degree_df, on='IATA_CODE')
airports_centrality
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode11.py
结果如下所示:
增加了 DEGREE 列的机场数据框
为了在 Mapbox 地图中可视化数据,我们只需对 airport_centrality
数据框使用 PixieDust.display()
:
display(airports_centrality)
以下截图显示了选项对话框:
Mapbox 显示机场的选项
在选项对话框点击 OK 后,我们得到以下结果:
显示具有度中心性的机场
对于其他中心性指标,我们可以注意到,相关的计算函数都返回一个 JSON 输出(与度属性的数组不同),其中 IATA_CODE
作为机场代码,中心性指数作为值。
例如,如果我们使用以下代码计算 PageRank:
nx.pagerank(flight_graph)
我们得到以下结果:
{'ABE': 0.0011522441195896051,
'ABI': 0.0006671948649909588,
...
'YAK': 0.001558809391270303,
'YUM': 0.0006214341604372096}
考虑到这一点,我们可以实现一个通用的函数 compute_centrality()
,而不是重复为 degree
所做的相同步骤。该函数接受计算中心性的函数和列名作为参数,创建一个包含计算中心性值的临时数据框,并将其与 airports_centrality
数据框合并。
以下代码展示了 compute_centrality()
的实现:
from six import iteritems
def compute_centrality(g, centrality_df, compute_fn, col_name, *args, **kwargs):
# create a temporary DataFrame that contains the computed centrality values
temp_df = pd.DataFrame(
[{"IATA_CODE":k, col_name:v} for k,v in iteritems(compute_fn(g, *args, **kwargs))],
columns=["IATA_CODE", col_name]
)
# make sure to remove the col_name from the centrality_df is already there
if col_name in centrality_df.columns:
centrality_df.drop([col_name], axis=1, inplace=True)
# merge the 2 DataFrame on the IATA_CODE column
centrality_df = pd.merge(centrality_df, temp_df, on='IATA_CODE')
return centrality_df
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode12.py
我们现在可以简单地调用 compute_centrality()
方法,并使用三个计算函数 nx.pagerank()
、nx.closeness_centrality()
和 nx.betweenness_centrality()
,并分别将 PAGE_RANK
、CLOSENESS
和 BETWEENNESS
作为列,如下所示的代码:
airports_centrality = compute_centrality(flight_graph, airports_centrality, nx.pagerank, "PAGE_RANK")
airports_centrality = compute_centrality(flight_graph, airports_centrality, nx.closeness_centrality, "CLOSENESS")
airports_centrality = compute_centrality(
flight_graph, airports_centrality, nx.betweenness_centrality, "BETWEENNESS", k=len(flight_graph))
airports_centrality
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode13.py
airports_centrality
数据框现在增加了额外的列,如下所示的输出:
增加了 PAGE_RANK、CLOSENESS 和 BETWEENNESS 值的机场数据框
作为练习,我们可以验证四个中心性指数对排名前的机场提供一致的结果。使用 pandas 的 nlargest()
方法,我们可以获得四个指数的前 10 个机场,如下所示的代码:
for col_name in ["DEGREE", "PAGE_RANK", "CLOSENESS", "BETWEENNESS"]:
print("{} : {}".format(
col_name,
airports_centrality.nlargest(10, col_name)["IATA_CODE"].values)
)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode14.py
这将产生以下结果:
DEGREE : ['ATL' 'ORD' 'DFW' 'DEN' 'MSP' 'IAH' 'DTW' 'SLC' 'EWR' 'LAX']
PAGE_RANK : ['ATL' 'ORD' 'DFW' 'DEN' 'MSP' 'IAH' 'DTW' 'SLC' 'SFO' 'LAX']
CLOSENESS : ['ATL' 'ORD' 'DFW' 'DEN' 'MSP' 'IAH' 'DTW' 'SLC' 'EWR' 'LAX']
BETWEENNESS : ['ATL' 'DFW' 'ORD' 'DEN' 'MSP' 'SLC' 'DTW' 'ANC' 'IAH' 'SFO']
正如我们所看到的,亚特兰大机场在所有中心性指标中排名第一。作为一个练习,让我们创建一个通用方法 visualize_neighbors()
,用来可视化给定节点的所有邻居,并通过亚特兰大节点调用它。在这个方法中,我们通过从父节点到所有邻居添加边,创建一个以父节点为中心的子图。我们使用 NetworkX 的 neighbors()
方法获取特定节点的所有邻居。
以下代码展示了 visualize_neighbors()
方法的实现:
import matplotlib.cm as cm
def visualize_neighbors(parent_node):
fig = plt.figure(figsize = (12,12))
# Create a subgraph and add an edge from the parent node to all its neighbors
graph = nx.DiGraph()
for neighbor in flight_graph.neighbors(parent_node):
graph.add_edge(parent_node, neighbor)
# draw the subgraph
nx.draw(graph, arrows=True, with_labels=True,
width = 0.5,style="dotted",
node_color=range(len(graph)),
cmap=cm.get_cmap(name="cool"),
edge_color=range(len(graph.edges)),
edge_cmap=cm.get_cmap(name="spring"),
)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode15.py
然后我们在 ATL
节点上调用 visualize_neighbors()
方法:
visualize_neighbors("ATL")
该方法生成如下输出:
可视化顶点 ATL 及其邻居
我们通过计算使用著名的 Dijkstra 算法在两个节点之间的最短路径来完成 第一部分,该算法的详细信息请参见(en.wikipedia.org/wiki/Dijkstra%27s_algorithm
)。我们将尝试不同的权重属性,看看是否能得到不同的结果。
例如,让我们使用 NetworkX 的 dijkstra_path()
方法计算从马萨诸塞州波士顿洛根机场(BOS
)到华盛顿帕斯科三城市机场(PSC
)之间的最短路径(networkx.github.io/documentation/networkx-2.1/reference/algorithms/generated/networkx.algorithms.shortest_paths.weighted.dijkstra_path.html
)。
我们首先将 ELAPSED_TIME
列作为权重属性:
注意
注意:提醒一下,ELAPSED_TIME
是我们在本节早些时候计算的,从相同出发地和目的地机场的每个航班的平均飞行时间。
nx.dijkstra_path(flight_graph, "BOS", "PSC", weight="ELAPSED_TIME")
该方法返回:
['BOS', 'MSP', 'PSC']
不幸的是,我们之前计算的中心性指标不属于flight_graph
数据框,因此将其用作weight
属性的列名是行不通的。然而,dijkstra_path()
也允许我们使用一个函数来动态计算权重。由于我们希望尝试不同的中心性指标,我们需要创建一个工厂方法(en.wikipedia.org/wiki/Factory_method_pattern
),该方法会为传入的中心性指标创建一个函数。这个参数作为闭包传递给一个嵌套的包装函数,符合dijkstra_path()
方法的weight
参数要求。我们还使用了一个cache
字典来记住计算出的某个机场的权重,因为算法会对同一个机场多次调用该函数。如果权重不在缓存中,我们会在airports_centrality
数据框中使用centrality_indice_col
参数查找。最终的权重通过获取中心性值的倒数来计算,因为 Dijkstra 算法偏向于选择较短路径。
以下代码展示了compute_weight
工厂方法的实现:
# use a cache so we don't recompute the weight for the same airport every time
cache = {}
def compute_weight(centrality_indice_col):
# wrapper function that conform to the dijkstra weight argument
def wrapper(source, target, attribute):
# try the cache first and compute the weight if not there
source_weight = cache.get(source, None)
if source_weight is None:
# look up the airports_centrality for the value
source_weight = airports_centrality.loc[airports_centrality["IATA_CODE"] == source][centrality_indice_col].values[0]
cache[source] = source_weight
target_weight = cache.get(target, None)
if target_weight is None:
target_weight = airports_centrality.loc[airports_centrality["IATA_CODE"] == target][centrality_indice_col].values[0]
cache[target] = target_weight
# Return weight is inversely proportional to the computed weighted since
# the Dijkstra algorithm give precedence to shorter distances
return float(1/source_weight) + float(1/target_weight)
return wrapper
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode16.py
我们现在可以针对每个中心性指标调用 NetworkX 的dijkstra_path()
方法。请注意,我们没有使用 BETWEENNESS,因为一些值为零,不能用作权重。在调用dijkstra_path()
方法之前,我们还需要清除缓存,因为使用不同的中心性指标会为每个机场生成不同的值。
以下代码展示了如何计算每个中心性指标的最短路径:
for col_name in ["DEGREE", "PAGE_RANK", "CLOSENESS"]:
#clear the cache
cache.clear()
print("{} : {}".format(
col_name,
nx.dijkstra_path(flight_graph, "BOS", "PSC",
weight=compute_weight(col_name))
))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode17.py
下面是产生的结果:
DEGREE : ['BOS', 'DEN', 'PSC']
PAGE_RANK : ['BOS', 'DEN', 'PSC']
CLOSENESS : ['BOS', 'DEN', 'PSC']
有趣的是,正如预期的那样,计算出的最短路径对于三个中心性指标是相同的,都经过丹佛机场,这是一座重要的枢纽机场。然而,它与使用ELAPSED_TIME
权重计算的路径不同,后者会让我们经过明尼阿波利斯。
在本节中,我们展示了如何将美国航班数据加载到图数据结构中,计算不同的中心性指标,并利用这些指标计算机场之间的最短路径。我们还讨论了不同的图数据可视化方法。
注意
第一部分的完整笔记本可以在这里找到:
在接下来的部分,我们将创建USFlightsAnalysis
PixieApp,将这些分析功能实现并投入使用。
第二部分 – 创建 USFlightsAnalysis PixieApp
在我们USFlightsAnalysis
的第一次迭代中,我们希望实现一个简单的用户故事,利用第一部分创建的分析功能:
-
欢迎屏幕将显示两个下拉控制,用于选择出发机场和目的地机场。
-
当选择一个机场时,我们会显示一个图表,展示所选机场及其邻近的机场。
-
当两个机场都被选择时,用户点击分析按钮,显示一个包含所有机场的 Mapbox 地图。
-
用户可以选择一个中心性指标(作为复选框)来根据选定的中心性显示最短的飞行路径。
首先让我们来看一下欢迎屏幕的实现,它是在USFlightsAnalysis
PixieApp 的默认路由中实现的。以下代码定义了USFlightsAnalysis
类,并用@PixieApp
装饰器将其标记为 PixieApp。它包含一个用@route()
装饰器装饰的main_screen()
方法,将其设置为默认路由。该方法返回一个 HTML 片段,该片段将在 PixieApp 启动时作为欢迎屏幕使用。HTML 片段由两部分组成:一部分显示选择出发机场的下拉控制,另一部分包含选择目的地机场的下拉控制。我们使用 Jinja2 的{%for...%}
循环遍历每个机场(由get_airports()
方法返回),生成一组<options>
元素。在每个控制下方,我们添加一个占位符<div>
元素,当选择机场时,这个<div>
将承载图表可视化。
注意
注意:和往常一样,我们使用[[USFlightsAnalysis]]
符号来表示代码只显示了部分实现,因此读者在完整实现提供之前不应尝试直接运行。
我们稍后会解释为什么USFlightsAnalysis
类继承自MapboxBase
类。
[[USFlightsAnalysis]]
from pixiedust.display.app import *
from pixiedust.apps.mapboxBase import MapboxBase
from collections import OrderedDict
@PixieApp
class USFlightsAnalysis(MapboxBase):
...
@route()
def main_screen(self):
return """
<style>
div.outer-wrapper {
display: table;width:100%;height:300px;
}
div.inner-wrapper {
display: table-cell;vertical-align: middle;height: 100%;width: 100%;
}
</style>
<div class="outer-wrapper">
<div class="inner-wrapper">
<div class="col-sm-6">
<div class="rendererOpt" style="font-weight:bold">
Select origin airport:
</div>
<div>
<select id="origin_airport{{prefix}}"
pd_refresh="origin_graph{{prefix}}">
<option value="" selected></option>
{%for code, airport in this.get_airports() %}
<option value="{{code}}">{{code}} - {{airport}}</option>
{%endfor%}
</select>
</div>
<div id="origin_graph{{prefix}}" pd_options="visualize_graph=$val(origin_airport{{prefix}})"></div>
</div>
<div class="input-group col-sm-6">
<div class="rendererOpt" style="font-weight:bold">
Select destination airport:
</div>
<div>
<select id="destination_airport{{prefix}}"
pd_refresh="destination_graph{{prefix}}">
<option value="" selected></option>
{%for code, airport in this.get_airports() %}
<option value="{{code}}">{{code}} - {{airport}}</option>
{%endfor%}
</select>
</div>
<div id="destination_graph{{prefix}}"
pd_options="visualize_graph=$val(destination_airport{{prefix}})">
</div>
</div>
</div>
</div>
<div style="text-align:center">
<button class="btn btn-default" type="button"
pd_options="org_airport=$val(origin_airport{{prefix}});dest_airport=$val(destination_airport{{prefix}})">
<pd_script type="preRun">
if ($("#origin_airport{{prefix}}").val() == "" || $("#destination_airport{{prefix}}").val() == ""){
alert("Please select an origin and destination airport");
return false;
}
return true;
</pd_script>
Analyze
</button>
</div>
"""
def get_airports(self):
return [tuple(l) for l in airports_centrality[["IATA_CODE", "AIRPORT"]].values.tolist()]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode18.py
当用户选择出发机场时,会触发一个pd_refresh
,该操作针对 ID 为origin_graph{{prefix}}
的占位符<div>
元素。进而,该<div>
元素会触发一个路由,使用状态:visualize_graph=$val(origin_airport{{prefix}})
。作为提醒,$val()
指令在运行时解析,通过获取origin_airport{{prefix}}
下拉菜单元素的机场值来实现。目的地机场的实现也类似。
visualize_graph
路由的代码提供如下。它简单地调用了我们在第一部分中实现的visualize_neighbors()
方法,并在第二部分中稍作修改,增加了一个可选的图形大小参数,以适应主机<div>
元素的大小。作为提醒,我们还使用了@captureOutput
装饰器,因为visualize_neighbors()
方法直接写入选定单元格的输出:
[[USFlightsAnalysis]]
@route(visualize_graph="*")
@captureOutput
def visualize_graph_screen(self, visualize_graph):
visualize_neighbors(visualize_graph, (5,5))
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode19.py
Analyze
按钮触发了compute_path_screen()
路由,该路由与org_airport
和dest_airport
状态参数相关联。我们还希望在允许compute_path_screen()
路由继续之前,确保两个机场都已选择。为此,我们使用一个<pd_script>
子元素,type="preRun"
,其中包含将在触发路由之前执行的 JavaScript 代码。该代码的契约是:如果我们希望让路由继续执行,它返回布尔值true
,否则返回false
。
对于Analyze
按钮,我们检查两个下拉菜单是否都有值,如果是,则返回true
,否则抛出错误信息并返回false
:
<button class="btn btn-default" type="button" pd_options="org_airport=$val(origin_airport{{prefix}});dest_airport=$val(destination_airport{{prefix}})">
<pd_script type="preRun">
if ($("#origin_airport{{prefix}}").val() == "" || $("#destination_airport{{prefix}}").val() == ""){
alert("Please select an origin and destination airport");
return false;
}
return true;
</pd_script>
Analyze
</button>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode20.html
以下输出显示了选择 BOS 作为出发机场,PSC 作为目的地时的最终结果:
欢迎界面,已选择两个机场
现在让我们看看compute_path_screen()
路由的实现,它负责显示所有机场的 Mapbox 地图,以及基于所选的中心性指标作为图层的最短路径,这是一个附加的可视化层,叠加在整体地图上。
以下代码展示了其实现:
[[USFlightsAnalysis]]
@route(org_airport="*", dest_airport="*")
def compute_path_screen(self, org_airport, dest_airport):
return """
<div class="container-fluid">
<div class="form-group col-sm-2" style="padding-right:10px;">
<div><strong>Centrality Indices</strong></div>
{% for centrality in this.centrality_indices.keys() %}
<div class="rendererOpt checkbox checkbox-primary">
<input type="checkbox"
pd_refresh="flight_map{{prefix}}"
pd_script="self.compute_toggle_centrality_layer('{{org_airport}}', '{{dest_airport}}', '{{centrality}}')">
<label>{{centrality}}</label>
</div>
{%endfor%}
</div>
<div class="form-group col-sm-10">
<h1 class="rendererOpt">Select a centrality index to show the shortest flight path
</h1>
<div id="flight_map{{prefix}}" pd_entity="self.airports_centrality" pd_render_onload>
<pd_options>
{
"keyFields": "LATITUDE,LONGITUDE",
"valueFields": "AIRPORT,DEGREE,PAGE_RANK,ELAPSED_TIME,CLOSENESS",
"custombasecolorsecondary": "#fffb00",
"colorrampname": "Light to Dark Red",
"handlerId": "mapView",
"quantiles": "0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0",
"kind": "choropleth",
"rowCount": "1000",
"numbins": "5",
"mapboxtoken": "pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4M29iazA2Z2gycXA4N2pmbDZmangifQ.-g_vE53SD2WrJ6tFX7QHmA",
"custombasecolor": "#ffffff"
}
</pd_options>
</div>
</div>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode21.py
该界面的中央<div>
元素是 Mapbox 地图,默认显示所有机场的 Mapbox 地图。如上面的代码所示,<pd_options>
子元素直接取自相应单元格的元数据,其中我们在第一部分中配置了地图。
在左侧,我们通过 Jinja2 的{%for …%}
循环,针对每个中心性指数生成一组复选框,循环的目标是centrality_indices
变量。我们在USFlightsAnalysis
PixieApp 的setup()
方法中初始化了这个变量,该方法在 PixieApp 启动时必定会被调用。此变量是一个 OrderedDict(docs.python.org/3/library/collections.html#collections.OrderedDict
),键是中心性指数,值是将用于 Mapbox 渲染的颜色方案:
[[USFlightsAnalysis]]
def setup(self):
self.centrality_indices = OrderedDict([
("ELAPSED_TIME","rgba(256,0,0,0.65)"),
("DEGREE", "rgba(0,256,0,0.65)"),
("PAGE_RANK", "rgba(0,0,256,0.65)"),
("CLOSENESS", "rgba(128,0,128,0.65)")
])
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode22.py
以下输出显示了未选择中心性指数的分析界面:
未选择中心性指数的分析界面
我们现在到达了用户选择中心性指数以触发最短路径搜索的步骤。每个复选框都有一个pd_script
属性,调用compute_toggle_centrality_layer()
方法。该方法负责调用 NetworkX 的dijkstra_path()
方法,并传入一个weight
参数,该参数通过调用我们在第一部分中讨论的compute_weight()
方法生成。此方法返回一个包含构成最短路径的每个机场的数组。利用该路径,我们创建一个包含 GeoJSON 有效负载的 JSON 对象,该有效负载作为一组要显示在地图上的线路。
此时,值得暂停讨论一下什么是图层。图层是使用 GeoJSON 格式(geojson.org
)定义的,我们在第五章中简要讨论过,Python 和 PixieDust 最佳实践与高级概念。提醒一下,GeoJSON 有效负载是一个具有特定模式的 JSON 对象,其中包括定义绘制对象形状的geometry
元素等内容。
例如,我们可以使用LineString
类型和一个包含线路两端经纬度坐标的数组来定义一条线:
{
"geometry": {
"type": "LineString",
"coordinates": [
[-93.21692, 44.88055],
[-119.11903000000001, 46.26468]
]
},
"type": "Feature",
"properties": {}
}
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode23.json
假设我们能够从最短路径生成这个 GeoJSON 有效载荷,我们可能会想知道如何将其传递给 PixieDust Mapbox 渲染器,以便显示。其实,机制非常简单:Mapbox 渲染器会检查主机 PixieApp 中是否有符合特定格式的类变量,并利用它生成要显示的 Mapbox 图层。为了帮助遵循这一机制,我们使用了之前简要介绍的MapboxBase
工具类。该类有一个get_layer_index()
方法,接受一个唯一的名称(我们使用centrality
索引)作为参数并返回其索引。它还接受一个额外的可选参数,在图层不存在时创建该图层。然后,我们调用toggleLayer()
方法,传递图层索引作为参数来打开和关闭图层。
以下代码展示了compute_toggle_centrality_layer()
方法的实现,该方法实现了上述步骤:
[[USFlightsAnalysis]]
def compute_toggle_centrality_layer(self, org_airport, dest_airport, centrality):
cache.clear()
cities = nx.dijkstra_path(flight_graph, org_airport, dest_airport, weight=compute_weight(centrality))
layer_index = self.get_layer_index(centrality, {
"name": centrality,
"geojson": {
"type": "FeatureCollection",
"features":[
{"type":"Feature",
"properties":{"route":"{} to {}".format(cities[i], cities[i+1])},
"geometry":{
"type":"LineString",
"coordinates":[
self.get_airport_location(cities[i]),
self.get_airport_location(cities[i+1])
]
}
} for i in range(len(cities) - 1)
]
},
"paint":{
"line-width": 8,
"line-color": self.centrality_indices[centrality]
}
})
self.toggleLayer(layer_index)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode24.py
几何对象中的坐标是使用get_airport_location()
方法计算的,该方法查询了我们在第一部分中创建的airports_centrality
数据框,如下代码所示:
[[USFlightsAnalysis]]
def get_airport_location(self, airport_code):
row = airports_centrality.loc[airports["IATA_CODE"] == airport_code]
if row is not None:
return [row["LONGITUDE"].values[0], row["LATITUDE"].values[0]]
return None
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode25.py
传递给get_layer_index()
方法的图层对象具有以下属性:
-
name
:唯一标识图层的字符串。 -
geojson
:GeoJSON 对象,定义了图层的特征和几何形状。 -
url
:仅在geojson
不存在时使用。指向一个返回 GeoJSON 有效载荷的 URL。 -
paint
:Mapbox 规范中定义的可选附加属性,指定图层数据的样式,例如颜色、宽度和透明度。 -
layout
:Mapbox 规范中定义的可选附加属性,指定图层数据的绘制方式,例如填充、可见性和符号。
注意
注意:你可以在这里找到更多关于 Mapbox 布局和绘制属性的信息:
www.mapbox.com/mapbox-gl-js/style-spec/#layers
在上面的代码中,我们指定了额外的paint
属性来配置line-width
和line-color
,这些属性来自在setup()
方法中定义的centrality_indices
JSON 对象。
以下输出显示了从BOS
到PSC
的最短飞行路径,使用了ELAPSED_TIME(红色)和DEGREE(绿色)中心性指标:
使用 ELAPSED_TIME 和 DEGREE 中心性指标显示从 BOS 到 PSC 的最短路径
在这一部分中,我们构建了一个 PixieApp,它使用 PixieDust Mapbox 渲染器可视化两个机场之间的最短路径。我们展示了如何使用MapboxBase
工具类创建新图层,以丰富地图信息。
注意
你可以在这里找到完成的 Notebook 文件,第二部分:
在接下来的部分,我们将添加与航班延误和相关航空公司有关的额外数据探索内容。
第三部分 – 向 USFlightsAnalysis PixieApp 添加数据探索功能
在这一部分中,我们希望扩展USFlightsAnalysis
PixieApp 的路线分析界面,添加两张图表,展示从选定起始机场起飞的每个航空公司的历史到达延误情况:一张显示所有从该机场起飞的航班,另一张则显示所有航班(不管机场在哪里)。这将使我们能够直观地比较特定机场的延误情况与其他机场的延误情况。
我们从实现一个方法开始,该方法选择给定航空公司的航班。我们还添加了一个可选的机场参数,可以用来控制是包括所有航班,还是仅包括从该机场起飞的航班。返回的 DataFrame 应该包含两列:DATE
和ARRIVAL_DELAY
。
以下代码展示了此方法的实现:
def compute_delay_airline_df(airline, org_airport=None):
# create a mask for selecting the data
mask = (flights["AIRLINE"] == airline)
if org_airport is not None:
# Add the org_airport to the mask
mask = mask & (flights["ORIGIN_AIRPORT"] == org_airport)
# Apply the mask to the Pandas dataframe
df = flights[mask]
# Convert the YEAR, MONTH and DAY column into a DateTime
df["DATE"] = pd.to_datetime(flights[['YEAR','MONTH', 'DAY']])
# Select only the columns that we need
return df[["DATE", "ARRIVAL_DELAY"]]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode26.py
我们可以通过使用它来测试前面的代码,选择波士顿的 Delta 航班。然后,我们可以调用 PixieDust 的display()
方法,创建一个线性图表,在 PixieApp 中使用:
bos_delay = compute_delay_airline_df("DL", "BOS")
display(bos_delay)
在 PixieDust 输出中,我们选择折线图菜单,并按如下方式配置选项对话框:
为生成波士顿出发的 Delta 航班到达延误线性图表的选项对话框
点击确定后,我们得到以下图表:
展示所有从波士顿起飞的 Delta 航班的延误情况
由于我们将在 PixieApp 中使用此图表,因此从编辑单元格元数据对话框复制 JSON 配置是个不错的主意:
PixieDust display() 配置,用于延误图表,需要复制到 PixieApp 中
现在我们知道如何生成延迟图表了,可以开始设计 PixieApp。我们首先通过更改主屏幕的布局,使用 TemplateTabbedApp
辅助类,这样就能免费得到标签式布局。整体分析屏幕现在由 RouteAnalysisApp
子类 PixieApp 驱动,包含两个标签:一个是与 SearchShortestRouteApp
子类 PixieApp 相关的 Search Shortest Route
标签,另一个是与 AirlinesApp
子类 PixieApp 相关的 Explore Airlines
标签。
以下图表提供了新布局中涉及的所有类的高层次流程:
新的标签式布局类图
RouteAnalysisApp
的实现非常直接,使用 TemplateTabbedApp
,如下代码所示:
from pixiedust.apps.template import TemplateTabbedApp
@PixieApp
class RouteAnalysisApp(TemplateTabbedApp):
def setup(self):
self.apps = [
{"title": "Search Shortest Route",
"app_class": "SearchShortestRouteApp"},
{"title": "Explore Airlines",
"app_class": "AirlinesApp"}
]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode27.py
SearchShortestRouteApp
子类 PixieApp 基本上是我们在第二部分中创建的主要 PixieApp 类的副本。唯一的不同是它是 RouteAnalysisApp
的子类 PixieApp,而 RouteAnalysisApp
本身又是 USFlightsAnalysis
主要 PixieApp 的子类。因此,我们需要一种机制将起始和目的地机场传递给各自的子类 PixieApp。为此,我们在实例化 RouteAnalysisApp
子类 PixieApp 时使用 pd_options
属性。
在 USFlightAnalysis
类中,我们将 analyze_route
方法更改为返回一个简单的 <div>
元素,该元素触发 RouteAnalysisApp
。我们还添加了一个包含 org_airport
和 dest_airport
的 pd_options
属性,如以下代码所示:
[[USFlightsAnalysis]]
@route(org_airport="*", dest_airport="*")
def analyze_route(self, org_airport, dest_airport):
return """
<div pd_app="RouteAnalysisApp"
pd_options="org_airport={{org_airport}};dest_airport={{dest_airport}}"
pd_render_onload>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode28.py
相反,在 SearchShortestRouteApp
子类 PixieApp 的 setup()
方法中,我们从 parent_pixieapp
的 options 字典中读取 org_airport
和 dest_airport
的值,如以下代码所示:
[[SearchShortestRouteApp]]
from pixiedust.display.app import *
from pixiedust.apps.mapboxBase import MapboxBase
from collections import OrderedDict
@PixieApp
class SearchShortestRouteApp(MapboxBase):
def setup(self):
self.org_airport = self.parent_pixieapp.options.get("org_airport")
self.dest_airport = self.parent_pixieapp.options.get("dest_airport")
self.centrality_indices = OrderedDict([
("ELAPSED_TIME","rgba(256,0,0,0.65)"),
("DEGREE", "rgba(0,256,0,0.65)"),
("PAGE_RANK", "rgba(0,0,256,0.65)"),
("CLOSENESS", "rgba(128,0,128,0.65)")
])
...
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode29.py
注意:由于 SearchShortestRouteApp
的其余实现与 第二部分 完全相同,因此已省略。若要访问实现,请参阅完整的 第三部分 Notebook。
最后要实现的 PixieApp 类是AirlinesApp
,它将显示所有的延误图表。与SearchShortestRouteApp
类似,我们从parent_pixieapp
选项字典中存储了org_airport
和dest_airport
。我们还计算了一个元组列表(代码和名称),列出了所有从给定org_airport
起飞的航空公司。为了实现这一点,我们使用 pandas 的groupby()
方法对AIRLINE
列进行分组,并获取索引值列表,代码如下所示:
[[AirlinesApp]]
@PixieApp
class AirlinesApp():
def setup(self):
self.org_airport = self.parent_pixieapp.options.get("org_airport")
self.dest_airport = self.parent_pixieapp.options.get("dest_airport")
self.airlines = flights[flights["ORIGIN_AIRPORT"] == self.org_airport].groupby("AIRLINE").size().index.values.tolist()
self.airlines = [(a, airlines.loc[airlines["IATA_CODE"] == a]["AIRLINE"].values[0]) for a in self.airlines]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode30.py
在AirlinesApp
的主屏幕上,我们使用 Jinja2 的{%for...%}
循环为每个航空公司生成一组行。在每一行中,我们添加两个<div>
元素,用于显示给定航空公司的延误折线图:一个用于显示从起始机场出发的航班,另一个用于显示该航空公司的所有航班。每个<div>
元素都有一个pd_options
属性,其中org_airport
和dest_airport
作为状态属性,触发delay_airline_screen
路由。我们还添加了一个delay_org_airport
布尔状态属性,用于表示我们想要显示哪种类型的延误图表。为了确保<div>
元素能够立即渲染,我们还添加了pd_render_onload
属性。
以下代码展示了AirlinesApp
默认路由的实现:
[[AirlinesApp]]
@route()
def main_screen(self):
return """
<div class="container-fluid">
{%for airline_code, airline_name in this.airlines%}
<div class="row" style="max-e">
<h1 style="color:red">{{airline_name}}</h1>
<div class="col-sm-6">
<div pd_render_onload pd_options="delay_org_airport=true;airline_code={{airline_code}};airline_name={{airline_name}}"></div>
</div>
<div class="col-sm-6">
<div pd_render_onload pd_options="delay_org_airport=false;airline_code={{airline_code}};airline_name={{airline_name}}"></div>
</div>
</div>
{%endfor%}
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode31.py
delay_airline_screen()
路由有三个参数:
-
delay_org_airport
:如果我们只想显示从起始机场出发的航班,则为true
;如果我们想显示该航空公司的所有航班,则为false
。我们使用这个标志来构建过滤航班 DataFrame 数据的掩码。 -
airline_code
:给定航空公司的 IATA 代码。 -
airline_name
:航空公司的全名。在构建 Jinja2 模板的 UI 时我们会使用这个值。
在delay_airline_screen()
方法的主体中,我们还计算了所选数据的平均延误,并将结果保存在average_delay
局部变量中。提醒一下,为了在 Jinja2 模板中使用此变量,我们使用了@templateArgs
装饰器,它会自动使所有局部变量在 Jinja2 模板中可用。
承载图表的 <div>
元素具有一个 pd_entity
属性,使用我们在本节开头创建的 compute_delay_airline_df()
方法。然而,由于参数发生了变化,我们需要将此方法重写为类的成员:org_airport
现在是一个类变量,delay_org_airport
现在是一个字符串布尔值。我们还添加了一个 <pd_options>
子元素,其中包含我们从 编辑单元格元数据 对话框复制的 PixieDust display()
JSON 配置。
以下代码展示了 delay_airline_screen()
路由的实现:
[[AirlinesApp]]
@route(delay_org_airport="*",airline_code="*", airline_name="*")
@templateArgs
def delay_airline_screen(self, delay_org_airport, airline_code, airline_name):
mask = (flights["AIRLINE"] == airline_code)
if delay_org_airport == "true":
mask = mask & (flights["ORIGIN_AIRPORT"] == self.org_airport)
average_delay = round(flights[mask]["ARRIVAL_DELAY"].mean(), 2)
return """
{%if delay_org_airport == "true" %}
<h4>Delay chart for all flights out of {{this.org_airport}}</h4>
{%else%}
<h4>Delay chart for all flights</h4>
{%endif%}
<h4 style="margin-top:5px">Average delay: {{average_delay}} minutes</h4>
<div pd_render_onload pd_entity="compute_delay_airline_df('{{airline_code}}', '{{delay_org_airport}}')">
<pd_options>
{
"keyFields": "DATE",
"handlerId": "lineChart",
"valueFields": "ARRIVAL_DELAY",
"noChartCache": "true"
}
</pd_options>
</div>
"""
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode32.py
compute_delay_airline_df()
方法有两个参数:对应 IATA 代码的航空公司和 delay_org_airport
字符串布尔值。我们已经介绍了该方法的实现,但这里提供了更新后的代码:
[[AirlinesApp]]
def compute_delay_airline_df(self, airline, delay_org_airport):
mask = (flights["AIRLINE"] == airline)
if delay_org_airport == "true":
mask = mask & (flights["ORIGIN_AIRPORT"] == self.org_airport)
df = flights[mask]
df["DATE"] = pd.to_datetime(flights[['YEAR','MONTH', 'DAY']])
return df[["DATE", "ARRIVAL_DELAY"]]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode33.py
运行 USFlightsAnalysis
PixieApp,将 BOS 和 PSC 分别设置为起点和终点机场时,我们点击 探索航空公司 标签。
结果如下图所示:
显示所有从波士顿机场提供服务的航空公司的延误线图
本节中,我们提供了另一个示例,展示如何使用 PixieApp 编程模型构建强大的仪表盘,提供可视化和分析结果的洞察,展示 Notebook 中开发的分析输出。
注意
完整的 Part 3 Notebook 可在以下位置找到:
在接下来的部分,我们将构建一个 ARIMA 模型,尝试预测航班延误。
第四部分 - 创建一个 ARIMA 模型来预测航班延误
在第八章,分析研究:预测 - 财务时间序列分析与预测中,我们使用时间序列分析构建了一个预测金融股票的预测模型。我们实际上可以使用相同的技术来分析航班延误,因为毕竟我们这里处理的也是时间序列,因此,在本节中,我们将遵循完全相同的步骤。对于每个目的地机场和可选航空公司,我们将构建一个包含匹配航班信息的 pandas DataFrame。
注意
注意:我们将再次使用statsmodels
库。如果你还没有安装它,请确保先安装,并参考第八章,分析学习:预测 - 金融时间序列分析与预测,获取更多信息。
作为例子,让我们关注所有以BOS
为目的地的 Delta(DL
)航班:
df = flights[(flights["AIRLINE"] == "DL") & (flights["ORIGIN_AIRPORT"] == "BOS")]
使用ARRIVAL_DELAY
列作为时间序列的值,我们绘制 ACF 和 PACF 图来识别趋势和季节性,如下代码所示:
import statsmodels.tsa.api as smt
smt.graphics.plot_acf(df['ARRIVAL_DELAY'], lags=100)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode34.py
结果如下所示:
ARRIVAL_DELAY 数据的自相关函数
同样,我们使用以下代码绘制偏自相关函数:
import statsmodels.tsa.api as smt
smt.graphics.plot_pacf(df['ARRIVAL_DELAY'], lags=50)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode35.py
结果如下所示:
ARRIVAL_DELAY 数据的偏自相关
从前面的图表中,我们可以推测数据具有趋势和/或季节性,并且它不是平稳的。使用我们在第八章,分析学习:预测 - 金融时间序列分析与预测中解释的对数差分技术,我们转换该序列,并使用 PixieDust 的display()
方法进行可视化,如以下代码所示:
注意
注意:我们还确保通过首先调用replace()
方法将np.inf
和-np.inf
替换为np.nan
,然后调用dropna()
方法移除所有包含np.nan
值的行,从而移除包含 NA 和无限值的行。
import numpy as np
train_set, test_set = df[:-14], df[-14:]
train_set.index = train_set["DEPARTURE_TIME"]
test_set.index = test_set["DEPARTURE_TIME"]
logdf = np.log(train_set['ARRIVAL_DELAY'])
logdf.index = train_set['DEPARTURE_TIME']
logdf_diff = pd.DataFrame(logdf - logdf.shift()).reset_index()
logdf_diff.replace([np.inf, -np.inf], np.nan, inplace=True)
logdf_diff.dropna(inplace=True)
display(logdf_diff)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode36.py
以下截图显示了 PixieDust 选项对话框:
ARRIVAL_DELAY 数据的对数差分选项对话框
点击确定后,我们得到以下结果:
注意
注意:在运行上述代码时,你可能不会得到与以下截图完全相同的图表。这是因为我们在选项对话框中将显示行数配置为100
,这意味着 PixieDust 将在创建图表之前从中抽取 100 个样本。
ARRIVAL_DELAY 数据的对数差分折线图
前面的图看起来是平稳的;我们可以通过对对数差分重新绘制 ACF 和 PACF 来强化这个假设,如下代码所示:
smt.graphics.plot_acf(logdf_diff["ARRIVAL_DELAY"], lags=100)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode37.py
结果如下所示:
ARRIVAL_DELAY 数据的对数差分 ACF 图
在以下代码中,我们对 PACF 进行了相同的操作:
smt.graphics.plot_pacf(logdf_diff["ARRIVAL_DELAY"], lags=100)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode38.py
结果如下所示:
ARRIVAL_DELAY 数据的对数差分 PACF 图
提醒一下,参考第八章,分析研究:预测 - 财务时间序列分析与预测,ARIMA 模型由三个阶数组成:p、d和q。从前面的两张图,我们可以推断出我们要构建的 ARIMA 模型的这些阶数:
-
自回归阶数 p 为 1:对应 ACF 首次穿越显著性水平的时刻
-
差分阶数 d 为 1:我们进行了 1 次对数差分
-
移动平均阶数 q 为 1:对应 PACF 首次穿越显著性水平的时刻
根据这些假设,我们可以使用statsmodels
包构建 ARIMA 模型,并获取其残差误差信息,如下代码所示:
from statsmodels.tsa.arima_model import ARIMA
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
arima_model_class = ARIMA(train_set['ARRIVAL_DELAY'],
dates=train_set['DEPARTURE_TIME'],
order=(1,1,1))
arima_model = arima_model_class.fit(disp=0)
print(arima_model.resid.describe())
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode39.py
结果如下所示:
count 13882.000000
mean 0.003116
std 48.932043
min -235.439689
25% -17.446822
50% -5.902274
75% 6.746263
max 1035.104295
dtype: float64
如我们所见,均值误差仅为 0.003,非常好,因此我们可以准备使用train_set
中的数据运行模型,并将结果与实际值的差异可视化。
以下代码使用 ARIMA 的plot_predict()
方法创建图表:
def plot_predict(model, dates_series, num_observations):
fig,ax = plt.subplots(figsize = (12,8))
model.plot_predict(
start = dates_series[len(dates_series)-num_observations],
end = dates_series[len(dates_series)-1],
ax = ax
)
plt.show()
plot_predict(arima_model, train_set['DEPARTURE_TIME'], 100)
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode40.py
结果如下所示:
预测与实际值对比
在前面的图表中,我们可以清晰地看到,预测线比实际值平滑得多。这是有道理的,因为实际上,航班延误总是会有一些意外的原因,这些原因可能会被视为离群值,因此很难进行建模。
我们仍然需要使用test_set
来验证模型,使用的是模型尚未见过的数据。以下代码创建了一个compute_test_set_predictions()
方法,用于比较预测数据和测试数据,并通过 PixieDust 的display()
方法可视化结果:
def compute_test_set_predictions(train_set, test_set):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
history = train_set['ARRIVAL_DELAY'].values
forecast = np.array([])
for t in range(len(test_set)):
prediction = ARIMA(history, order=(1,1,0)).fit(disp=0).forecast()
history = np.append(history, test_set['ARRIVAL_DELAY'].iloc[t])
forecast = np.append(forecast, prediction[0])
return pd.DataFrame(
{"forecast": forecast,
"test": test_set['ARRIVAL_DELAY'],
"Date": pd.date_range(start=test_set['DEPARTURE_TIME'].iloc[len(test_set)-1], periods = len(test_set))
}
)
results = compute_test_set_predictions(train_set, test_set)
display(results)
注释
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode41.py
这里显示的是 PixieDust 选项对话框:
预测与测试对比折线图的选项对话框
点击确定后,我们得到以下结果:
预测值与测试数据的折线图
现在我们准备将这个模型集成到我们的USFlightsAnalysis
PixieApp 中,通过在RouteAnalysisApp
主界面添加一个新的选项卡,名为航班延误预测
。该选项卡将由一个名为PredictDelayApp
的新子 PixieApp 驱动,用户可以选择使用 Dijkstra 最短路径算法计算出的最短路径航班段,DEGREE
作为中心性指标。用户还可以选择航空公司,在这种情况下,训练数据将仅限于由所选航空公司运营的航班。
在以下代码中,我们创建了PredictDelayApp
子 PixieApp 并实现了setup()
方法,计算所选起点和终点机场的 Dijkstra 最短路径:
[[PredictDelayApp]]
import warnings
import numpy as np
from statsmodels.tsa.arima_model import ARIMA
@PixieApp
class PredictDelayApp():
def setup(self):
self.org_airport = self.parent_pixieapp.options.get("org_airport")
self.dest_airport = self.parent_pixieapp.options.get("dest_airport")
self.airlines = flights[flights["ORIGIN_AIRPORT"] == self.org_airport].groupby("AIRLINE").size().index.values.tolist()
self.airlines = [(a, airlines.loc[airlines["IATA_CODE"] == a]["AIRLINE"].values[0]) for a in self.airlines]
path = nx.dijkstra_path(flight_graph, self.org_airport, self.dest_airport, weight=compute_weight("DEGREE"))
self.paths = [(path[i], path[i+1]) for i in range(len(path) - 1)]
在PredictDelayApp
的默认路由中,我们使用 Jinja2 的{%for..%}
循环构建了两个下拉框,显示航班段和航空公司,如下所示:
[[PredictDelayApp]]
@route()
def main_screen(self):
return """
<div class="container-fluid">
<div class="row">
<div class="col-sm-6">
<div class="rendererOpt" style="font-weight:bold">
Select a flight segment:
</div>
<div>
<select id="segment{{prefix}}" pd_refresh="prediction_graph{{prefix}}">
<option value="" selected></option>
{%for start, end in this.paths %}
<option value="{{start}}:{{end}}">{{start}} -> {{end}}</option>
{%endfor%}
</select>
</div>
</div>
<div class="col-sm-6">
<div class="rendererOpt" style="font-weight:bold">
Select an airline:
</div>
<div>
<select id="airline{{prefix}}" pd_refresh="prediction_graph{{prefix}}">
<option value="" selected></option>
{%for airline_code, airline_name in this.airlines%}
<option value="{{airline_code}}">{{airline_name}}</option>
{%endfor%}
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div id="prediction_graph{{prefix}}"
pd_options="flight_segment=$val(segment{{prefix}});airline=$val(airline{{prefix}})">
</div>
</div>
</div>
</div>
"""
注释
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode42.py
这两个下拉框具有pd_refresh
属性,该属性指向 ID 为prediction_graph{{prefix}}
的<div>
元素。当触发时,该<div>
元素会调用predict_screen()
路由,并使用flight_segment
和airline
的状态属性。
在predict_screen()
路由中,我们使用flight_segment
和airline
参数来创建训练数据集,建立一个 ARIMA 模型来进行预测,并通过折线图展示预测值与实际值的对比。
注释
时间序列预测模型的限制在于只能预测接近实际数据的结果,由于我们只有 2015 年的数据,因此无法使用该模型预测更新的数据。当然,在生产应用中,假设我们有当前的航班数据,因此这不会是一个问题。
以下代码展示了predict_screen()
路由的实现:
[[PredictDelayApp]]
@route(flight_segment="*", airline="*")
@captureOutput
def predict_screen(self, flight_segment, airline):
if flight_segment is None or flight_segment == "":
return "<div>Please select a flight segment</div>"
airport = flight_segment.split(":")[1]
mask = (flights["DESTINATION_AIRPORT"] == airport)
if airline is not None and airline != "":
mask = mask & (flights["AIRLINE"] == airline)
df = flights[mask]
df.index = df["DEPARTURE_TIME"]
df = df.tail(50000)
df = df[~df.index.duplicated(keep='first')]
with warnings.catch_warnings():
warnings.simplefilter("ignore")
arima_model_class = ARIMA(df["ARRIVAL_DELAY"], dates=df['DEPARTURE_TIME'], order=(1,1,1))
arima_model = arima_model_class.fit(disp=0)
fig, ax = plt.subplots(figsize = (12,8))
num_observations = 100
date_series = df["DEPARTURE_TIME"]
arima_model.plot_predict(
start = str(date_series[len(date_series)-num_observations]),
end = str(date_series[len(date_series)-1]),
ax = ax
)
plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode43.py
在以下代码中,我们还希望确保数据集索引去重,以避免在绘制结果时发生错误。通过使用df = df[~df.index.duplicated(keep='first')]
来过滤重复的索引。
最后要做的事情是将PredictDelayApp
子应用 PixieApp 接入到RouteAnalysisApp
,如下代码所示:
from pixiedust.apps.template import TemplateTabbedApp
@PixieApp
class RouteAnalysisApp(TemplateTabbedApp):
def setup(self):
self.apps = [
{"title": "Search Shortest Route",
"app_class": "SearchShortestRouteApp"},
{"title": "Explore Airlines",
"app_class": "AirlinesApp"},
{"title": "Flight Delay Prediction",
"app_class": "PredictDelayApp"}
]
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%209/sampleCode44.py
当我们运行USFlightsAnalysis
PixieApp 并使用 BOS 和 PSC,如前几节所做的那样,在航班延误预测标签页中,选择BOS->DEN航班段。
结果如下所示:
波士顿到丹佛航段的预测
在本节中,我们展示了如何使用时间序列预测模型基于历史数据来预测航班延误。
注意
你可以在这里找到完整的 Notebook:
提醒一下,虽然这只是一个示例应用,仍有很多改进空间,但使用 PixieApp 编程模型将数据分析转化为实际应用的技巧,在任何其他项目中也同样适用。
总结
在本章中,我们讨论了图形及其相关的图论,探索了图的结构和算法。我们还简要介绍了networkx
Python 库,它提供了一套丰富的 API,用于操作和可视化图形。然后,我们将这些技巧应用于构建一个示例应用,该应用通过将航班数据视为图问题(机场为顶点,航班为边)来进行分析。像往常一样,我们还展示了如何将这些分析转化为一个简单而强大的仪表盘,该仪表盘可以直接在 Jupyter Notebook 中运行,然后可以选择性地作为 Web 分析应用通过 PixieGateway 微服务部署。
本章完成了一系列涵盖许多重要行业应用的示例。在下一章中,我将对本书的主题做一些最终的思考,主题是通过简化和使数据工作变得对所有人可及,架起数据科学与工程之间的桥梁。
第十一章:第二章
数据可视化
学习目标
本章结束时,您将能够:
-
使用函数式方法创建和自定义折线图、条形图、直方图、散点图和箱线图
-
开发程序化的、描述性的图表标题
-
描述使用面向对象方法创建 Matplotlib 图表的优势
-
创建一个包含单个轴或多个轴的可调用图表对象
-
调整大小并保存包含多个子图的图表对象
-
使用 Matplotlib 创建和自定义常见的图表类型。
本章将涵盖属于数据可视化范畴的各种概念。
简介
数据可视化是一种强大的工具,允许用户快速消化大量数据。有不同类型的图表用于各种目的。在业务中,折线图和条形图通常用于显示随时间变化的趋势和比较不同组之间的指标。而统计学家可能更感兴趣的是使用散点图或相关矩阵来检查变量之间的相关性。他们还可以使用直方图检查变量的分布或使用箱线图检查异常值。在政治中,饼图广泛用于比较不同类别之间的总数据。数据可视化可以非常复杂和创造性,仅限于个人的想象力。
Python 库 Matplotlib 是一个文档良好的二维绘图库,可以用来创建各种强大的数据可视化,其宗旨是“...让简单的事情变得简单,让困难的事情变得可能”(https://matplotlib.org/index.html)。
Matplotlib 创建图表的两种方法分别是函数式方法和面向对象方法。
在函数式方法中,创建一个包含单个图表的图形。通过一系列顺序函数来创建和自定义图表。但是,函数式方法不允许我们将图表保存到我们的环境中作为一个对象;这可以通过面向对象的方法实现。在面向对象的方法中,我们创建一个图形对象,为一个图表或多个子图分配一个轴或多个轴。然后,我们可以自定义轴或轴,并通过调用图形对象来调用单个图表或多个图表集合。
在本章中,我们将使用函数式方法来创建和自定义折线图、条形图、直方图、散点图和箱线图。然后,我们将学习如何使用面向对象的方法来创建和自定义单轴和多轴图。
函数式方法
在 Matplotlib 中使用函数式方法绘图是一种快速生成单轴图形的方式。通常,这是教给初学者的方法。函数式方法允许用户自定义并将图形保存为所选目录中的图像文件。在以下的练习和活动中,你将学习如何使用函数式方法绘制线形图、条形图、直方图、箱线图和散点图。
练习 13:函数式方法 – 线形图
要开始使用 Matplotlib,我们将从创建一个线形图开始,并对其进行自定义:
-
使用以下代码生成一个水平轴的数字数组,范围从 0 到 10,共 20 个均匀分布的值:
import numpy as np x = np.linspace(0, 10, 20)
-
创建一个数组并将其保存为对象
y
。以下代码片段将x
的值立方并保存到数组y
中:y = x**3
-
按如下方式创建图形:
import matplotlib.pyplot as plt plt.plot(x, y) plt.show()
参见这里的结果输出:
图 2.1:y 与 x 的线形图
-
使用以下代码添加一个 x 轴标签,内容为'
Linearly Spaced Numbers
':plt.xlabel('Linearly Spaced Numbers')
-
使用以下代码添加一个 y 轴标签,内容为'
y Value
':plt.ylabel('y Value')
-
使用以下代码添加一个标题,内容为'
x by x cubed
':plt.title('x by x Cubed')
-
通过在
plt.plot()
函数中将颜色参数指定为k
,将线条颜色更改为黑色:plt.plot(x, y, 'k')
使用
plt.show()
将图形打印到控制台。查看以下截图,查看结果输出:
图 2.2:带有标签轴和黑色线条的线形图
-
将线条字符改为菱形;使用字符参数(即 D)结合颜色字符(即 k),如下所示:
plt.plot(x, y, 'Dk')
查看下图,查看结果输出:
图 2.3:带有未连接的黑色菱形标记的线形图
-
使用以下代码通过在
D
和k
之间添加'-
'来连接菱形,形成实线:plt.plot(x, y, 'D-k')
参见下图查看输出:
图 2.4:带有连接的黑色菱形标记的线形图
-
使用
plt.title()
函数中的fontsize
参数来增大标题的字体大小,如下所示:plt.title('x by x Cubed', fontsize=22)
-
使用以下代码将图形打印到控制台:
plt.show()
-
输出可以在下图中看到:
图 2.5:带有较大标题的线形图
在这里,我们使用函数式方法创建了一个单线的线形图,并对其进行了样式化,使其更具美感。然而,在同一个图中比较多个趋势并不罕见。因此,接下来的练习将详细讲解如何在同一线形图中绘制多条线并创建图例来区分这些线条。
练习 14:函数式方法 – 向线形图添加第二条线
Matplotlib 通过简单地指定另一个 plt.plot()
实例,使得在折线图中添加另一条线变得非常容易。在本练习中,我们将使用单独的线绘制 x 的立方和 x 的平方:
-
创建另一个 y 对象,就像我们为第一个 y 对象所做的那样,不过这次是对 x 进行平方,而不是立方,如下所示:
y2 = x**2
-
现在,通过在现有图表中添加
plt.plot(x, y2)
,将y2
绘制在与 y 相同的图表上。参考此处的输出:
图 2.6:y 和 y2 随 x 的多条折线图
-
使用以下代码将
y2
的颜色更改为红色虚线:plt.plot(x, y2, '--r')
输出显示在以下图示中:
图 2.7:y2 为红色虚线的多条折线图
-
要创建图例,我们必须首先使用
plt.plot()
函数中的 label 参数为我们的线条创建标签。 -
要将 y 标记为 '
x 的立方
',使用以下代码:plt.plot(x, y, 'D-k', label='x cubed')
-
使用以下代码将
y2
标记为 'x 的平方
':plt.plot(x, y2, '--r', label='x squared')
-
使用
plt.legend(loc='upper left')
来指定图例的位置。请查看以下截图以查看最终输出:
图 2.8:带有图例的多条折线图
-
为了在新行开始时将一行文本分割成多行,我们使用字符串中的'
\n
'。因此,使用以下代码,我们可以创建此处显示的标题:plt.title('As x increases, \nx Cubed (black) increases \nat a Greater Rate than \nx Squared (red)', fontsize=22)
请查看以下截图中的输出:
图 2.9:带有多行标题的多条折线图
-
要更改图表的尺寸,我们需要在
plt
实例的顶部添加plt.figure(figsize=(10,5))
。figsize
参数中的 10 和 5 分别指定了图表的宽度和高度。要查看输出,请参考以下图示:
图 2.10:增加图表尺寸的多条折线图
在本练习中,我们学习了如何使用函数式方法在 Matplotlib 中创建和设置单条和多条折线图。为了巩固我们的学习,我们将绘制另一张稍微不同样式的单条折线图。
活动 2:折线图
在本活动中,我们将创建一个折线图,分析从一月到六月每月销售商品的趋势。趋势将是正向的并且呈线性增长,并将使用带星形标记的蓝色虚线表示。x 轴将标记为 '月份
',y 轴将标记为 '销售商品数
'。标题将显示为 '销售商品数呈线性增长:'
-
创建一个包含六个字符串的 x 列表,表示一月到六月的月份。
-
创建一个包含六个值的 y 列表,这些值表示 '
销售商品数
',从 1000 开始,每个值增加 200,直到最后一个值为 2000。 -
生成上述描述的图表。
查看以下截图以获取结果输出:
图 2.11:按月份销售的商品线形图
注意
我们可以参考第 333 页的本活动解决方案。
到目前为止,我们已经获得了很多创建和自定义线形图的实践。线形图通常用于显示趋势。然而,在比较各组之间和/或组内的值时,条形图通常是首选的可视化方式。在接下来的练习中,我们将探讨如何创建条形图。
练习 15:创建条形图
在本练习中,我们将按商品类型显示销售收入:
-
创建一个商品类型的列表,并使用以下代码将其保存为
x
:x = ['Shirts', 'Pants','Shorts','Shoes']
-
创建一个销售收入的列表,并按如下方式将其保存为
y
:y = [1000, 1200, 800, 1800]
-
要创建条形图并将其打印到控制台,请参考以下代码:
import matplotlib.pyplot as plt plt.bar(x, y) plt.show()
以下截图显示了结果输出:
图 2.12:按商品类型划分的销售收入条形图
-
使用以下代码添加标题“
Sales Revenue by Item Type
”:plt.title('Sales Revenue by Item Type')
-
使用以下代码创建一个显示“
Item Type
”的 x 轴标签:plt.xlabel('Item Type')
-
使用以下代码添加一个 y 轴标签,显示“
Sales Revenue ($)
”:plt.ylabel('Sales Revenue ($)')
以下截图显示了输出:
图 2.13:带有自定义轴和标题的条形图
-
我们将创建一个标题,该标题会根据绘制的数据进行更改。对于这个示例,标题将是“
Shoes Produce the Most Sales Revenue
”。首先,我们将找到 y 中最大值的索引,并使用以下代码将其保存为index_of_max_y
对象:index_of_max_y = y.index(max(y))
-
使用以下代码,将列表
x
中与index_of_max_y
索引相等的项保存到most_sold_item
对象:most_sold_item = x[index_of_max_y]
-
按照以下方式使标题具有程序化功能:
plt.title('{} Produce the Most Sales Revenue'.format(most_sold_item))
查看以下输出:
图 2.14:带有程序化标题的条形图
-
如果我们希望将图表转换为水平条形图,可以通过将
plt.bar(x, y)
替换为plt.barh(x, y)
来实现。以下截图显示了输出:
图 2.15:带有错误标注轴的水平条形图
注意
记住,当条形图从垂直转为水平时,x 轴和 y 轴需要互换。
-
将 x 和 y 的标签分别从
plt.xlabel('Item Type')
和plt.ylabel('Sales Revenue ($)')
互换为plt.xlabel('Sales Revenue ($)')
和plt.ylabel('Item Type')
。
查看以下输出,获取最终的条形图:
图 2.16:带有正确标注轴的水平条形图
在前一个练习中,我们学习了如何创建条形图。使用 Matplotlib 创建条形图非常简单。在接下来的活动中,我们将继续练习构建条形图。
活动 3:条形图
在本活动中,我们将创建一个条形图,比较五支获得最多冠军头衔的 NBA 球队的冠军数量。该图将按标题数从大到小排序,拥有最多头衔的球队在左侧,最少的在右侧。柱状条将为红色,x 轴将命名为 'NBA 球队
',y 轴将命名为 '冠军数量
',标题将是程序化的,解释哪支球队拥有最多冠军及其数量。在进行此活动之前,请确保在线查找所需的 NBA 球队数据。此外,我们将使用 plt.xticks(rotation=45)
将 x 轴刻度标签旋转 45 度,以避免重叠,并将图表保存到当前目录:
-
为
x
创建一个包含五个字符串的列表,这些字符串是获得最多冠军头衔的 NBA 球队名称。 -
为
y
创建一个包含五个值的列表,这些值对应于x
中的字符串,表示 '获得的冠军头衔
'。 -
将 x 和 y 放入一个数据框,其中列名分别为 '
队伍
' 和 '冠军头衔
'。 -
按 '
标题
' 进行降序排序数据框。 -
创建一个程序化的标题并将其保存为
title
。 -
生成描述的图形。
注意
我们可以参考第 334 页的解答来完成此活动。
折线图和条形图是两种非常常见且有效的可视化方式,分别用于报告趋势和比较不同组。然而,若要进行更深入的统计分析,生成能揭示特征特性(而这些特征在折线图和条形图中无法显现)的图形就显得尤为重要。因此,在接下来的练习中,我们将学习创建常见的统计图表。
练习 16:函数式方法 – 直方图
在统计学中,在进行任何类型的分析之前,了解连续变量的分布是非常重要的。为了展示分布,我们将使用直方图。直方图通过给定数组的区间显示频率:
-
为了展示如何创建直方图,我们将生成一个包含 100 个符合正态分布的值的数组,均值为 0,标准差为 0.1,并使用以下代码将其保存为 y:
import numpy as np y = np.random.normal(loc=0, scale=0.1, size=100)
-
导入 Matplotlib 后,使用以下代码创建直方图:
plt.hist(y, bins=20)
-
使用以下代码为 x 轴创建一个名为 '
y 值
' 的标签:plt.xlabel('y Value')
-
使用以下代码将 y 轴命名为 '
频率
':plt.ylabel('Frequency')
-
使用
plt.show()
将其打印到控制台: -
查看以下截图中的输出:
图 2.17:带有标注坐标轴的 y 直方图
注意
当我们查看直方图时,我们通常会判断分布是否为正态分布。有时,一个分布看起来是正态分布,但实际上不是;而有时,一个分布看起来不是正态分布,但实际上是正态的。为了检测正态性,有一个叫做 Shapiro-Wilk 检验的方法。Shapiro-Wilk 检验的原假设是数据是正态分布的。因此,p 值 < 0.05 表示非正态分布,而 p 值 > 0.05 表示正态分布。我们将使用 Shapiro-Wilk 检验的结果来创建一个程序化标题,告知读者分布是否为正态分布。
-
使用元组解包,将 Shapiro-Wilk 检验中的 W 统计量和 p 值分别保存到
shap_w
和shap_p
对象中,代码如下:from scipy.stats import shapiro shap_w, shap_p = shapiro(y)
-
我们将使用 if-else 语句来判断数据是否为正态分布,并将适当的字符串存储在
normal_YN
对象中。if shap_p > 0.05: normal_YN = 'Fail to reject the null hypothesis. Data is normally distributed.' else: normal_YN = 'Null hypothesis is rejected. Data is not normally distributed.'
-
使用
plt.title(normal_YN)
将normal_YN
赋值给我们的图表,并使用plt.show()
将其打印到控制台。请查看此截图中的最终输出:
图 2.18:带有程序化标题的 y 的直方图
如前所述,直方图用于显示数组的分布。另一个常用的统计图表用于探索数值特征的是箱线图,也称为箱型图。
箱线图显示了一个数组的分布,基于最小值、第一个四分位数、中位数、第三个四分位数和最大值,但它们主要用于表示分布的偏斜程度并识别离群值。
练习 17:函数式方法 – 箱线图
在本练习中,我们将学习如何创建箱线图,并在标题中呈现关于分布形状和离群值数量的信息:
-
使用以下代码生成一个均值为 0,标准差为 0.1 的 100 个正态分布的数字数组,并将其保存为 y:
import numpy as np y = np.random.normal(loc=0, scale=0.1, size=100)
-
如下创建并显示图表:
import matplotlib.pyplot as plt plt.boxplot(y) plt.show()
有关输出,请参见下图:
图 2.19:y 的箱线图
注意
该图显示了一个框,表示四分位距(IQR)。框的顶部是第 25 百分位(即 Q1),框的底部是第 75 百分位(即 Q3)。穿过盒子的橙色线是中位数。框上方和下方延伸的两条线是“胡须”。上胡须的顶部是“最大”值,通过 Q1 - 1.5IQR 计算得出。下胡须的底部是“最小”值,通过 Q3 + 1.5IQR 计算得出。离群值(或边缘离群值)以点的形式显示在“最大”胡须上方或“最小”胡须下方。
-
使用以下代码将 Shapiro W 和 p 值从
shapiro
函数保存:from scipy.stats import shapiro shap_w, shap_p = shapiro(y)
-
参考以下代码将
y
转换为 z 得分:from scipy.stats import zscore y_z_scores = zscore(y)
注意
这是衡量数据的一种方式,显示每个数据点与均值的标准差差异。
-
使用以下代码迭代
y_z_scores
数组,以找到离群值的数量:total_outliers = 0 for i in range(len(y_z_scores)): if abs(y_z_scores[i]) >= 3: total_outliers += 1
注释
由于生成的数组 y 是正态分布的,因此我们可以预期数据中没有离群值。
-
生成一个标题,传达数据是否呈正态分布,以及离群值的数量。如果
shap_p
大于 0.05,则我们的数据呈正态分布。如果小于 0.05,则数据不呈正态分布。我们可以通过以下逻辑设置并包含离群值的数量:if shap_p > 0.05: title = 'Normally distributed with {} outlier(s).'.format(total_outliers) else: title = 'Not normally distributed with {} outlier(s).'.format(total_outliers)
-
使用
plt.title
(标题)设置我们的图表标题,并通过以下方式打印到控制台:plt.show()
-
在下图中检查最终输出:
图 2.20:带程序生成标题的 y 的箱线图
直方图和箱线图在探索数值数组特征时非常有效。然而,它们并不能提供数组之间关系的信息。在下一个练习中,我们将学习如何创建散点图——一种常见的可视化方式,用于展示两个连续数组之间的关系。
练习 18:散点图
在本练习中,我们将创建一个体重与身高的散点图。我们将再次创建一个标题来解释该图所展示的信息:
-
使用以下方法生成一个表示身高的数字列表,并将其保存为 y:
y = [5, 5.5, 5, 5.5, 6, 6.5, 6, 6.5, 7, 5.5, 5.25, 6, 5.25]
-
使用以下方法生成一个表示体重的数字列表,并将其保存为 x:
x = [100, 150, 110, 140, 140, 170, 168, 165, 180, 125, 115, 155, 135]
-
使用以下代码创建一个基本的散点图,体重为 x 轴,身高为 y 轴:
import matplotlib.pyplot as plt plt.scatter(x, y)
-
将 x 轴标记为 '
体重
',如下所示:plt.xlabel('Weight')
-
将 y 轴标记为 '
身高
',如下所示:plt.ylabel('Height')
-
使用
plt.show()
将图表打印到控制台。我们的输出应类似于以下内容:
图 2.21:按体重绘制的身高散点图
-
我们希望图表标题能告知读者关系的强度和 Pearson 相关系数。因此,我们将计算 Pearson 相关系数,并在标题中解释该系数的值。计算 Pearson 相关系数的代码如下:
from scipy.stats import pearsonr correlation_coeff, p_value = pearsonr(x, y)
-
Pearson 相关系数是一个指标,用于衡量两个连续数组之间线性关系的强度和方向。使用 if-else 逻辑,我们将通过以下代码返回相关系数的解释:
if correlation_coeff == 1.00: title = 'There is a perfect positive linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff >= 0.8: title = 'There is a very strong, positive linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff >= 0.6: title = 'There is a strong, positive linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff >= 0.4: title = 'There is a moderate, positive linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff >= 0.2: title = 'There is a weak, positive linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff > 0: title = 'There is a very weak, positive linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff == 0: title = 'There is no linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff <= -0.8: title = 'There is a very strong, negative linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff <= -0.6: title = 'There is a strong, negative linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff <= -0.4: title = 'There is a moderate, negative linear relationship (r = {0:0.2f}).'.format(correlation_coeff) elif correlation_coeff <= -0.2: title = 'There is a weak, negative linear relationship (r = {0:0.2f}).'.format(correlation_coeff) else: title = 'There is a very weak, negative linear relationship (r = {0:0.2f}).'.format(correlation_coeff) print(title)
-
现在,我们可以使用新创建的标题对象作为标题,方法是使用
plt.title(title)
。请参阅下图以了解结果输出:
图 2.22:按体重绘制的身高散点图,带程序生成的标题
到目前为止,我们已经学习了如何使用函数式方法为不同目的创建和设置各种图表样式。虽然这种绘图方法非常有效,可以快速生成可视化图表,但它无法创建多个子图,也无法将图表保存为我们环境中的对象。为了将图表保存为对象,我们必须使用面向对象的方法,这将在接下来的练习和活动中进行介绍。
使用子图的面向对象方法
使用 Matplotlib 的函数式绘图方法不允许用户将图表保存为我们环境中的对象。在面向对象的方法中,我们创建一个图形对象,作为一个空画布,然后我们将一组坐标轴或子图添加到图形中。该图形对象是可调用的,如果被调用,将返回该图形到控制台。我们将通过绘制与练习 13中相同的 x 和 y 对象来演示这一过程。
练习 19:使用子图绘制单条线
当我们学习 Matplotlib 的函数式绘图方法时,我们从创建和自定义一条线性图开始。在本次练习中,我们将使用函数式绘图方法创建并设置样式的线性图:
-
使用以下代码将
x
保存为从 0 到 10 的数组,间隔为 20 个线性步长:import numpy as np x = np.linspace(0, 10, 20)
使用以下代码将
y
保存为 x 的三次方:y = x**3
-
按以下步骤创建图形和坐标轴:
import matplotlib.pyplot as plt fig, axes = plt.subplots() plt.show()
查看以下屏幕截图以查看输出结果:
图 2.23:可调用的图形和坐标轴集
注意
fig
对象现在是可调用的,并返回我们可以绘制的坐标轴。 -
使用以下代码绘制 y(即 x 的平方)与 x 的关系:
axes.plot(x, y)
下图显示了输出结果:
图 2.24:按 x 绘制 y 的可调用线性图
-
将图表样式设置与练习 13中的样式相同。首先,按照以下步骤更改线条颜色和标记:
axes.plot(x, y, 'D-k')
-
使用以下代码将 x 轴标签设置为'
线性间隔数字
':axes.set_xlabel('Linearly Spaced Numbers')
-
使用以下代码将 y 轴设置为'
y 值
':axes.set_ylabel('y Value')
-
使用以下代码将标题设置为'
随着 x 的增加,y 按 x 的三次方增加
':axes.set_title('As x increases, y increases by x cubed')
下图显示了输出结果:
图 2.25:按 x 绘制 y 的可调用样式线图
在本次练习中,我们创建了一个与练习 13中的第一个图表非常相似的图表,但现在它是一个可调用的对象。使用面向对象的绘图方法的另一个优势是可以在单个图形对象上创建多个子图。
在某些情况下,我们希望并排比较不同的数据视图。我们可以在 Matplotlib 中使用子图来实现这一点。
练习 20:使用子图绘制多条线
因此,在本练习中,我们将绘制与练习 14 中相同的线条,但我们将在同一个可调用的图形对象中绘制两个子图。子图是通过网格格式布局的,并可以通过 [行,列] 索引访问。例如,如果我们的图形对象包含四个子图,排列成两行两列,我们可以通过 axes[0,0]
引用左上角的图形,使用 axes[1,1]
引用右下角的图形,如下图所示。
图 2.26:坐标轴索引引用
在剩余的练习和活动中,我们将有很多机会练习生成子图并访问各种坐标轴。在本练习中,我们将使用子图绘制多个线图:
-
首先,使用以下代码创建
x
、y
和y2
:import numpy as np x = np.linspace(0, 10, 20) y = x**3 y2 = x**2
-
创建一个包含两个坐标轴(即子图)的图像,它们并排显示(即 1 行 2 列),如下所示:
import matplotlib.pyplot as plt fig, axes = plt.subplots(nrows=1, ncols=2)
结果输出如下所示:
图 2.27:包含两个子图的图像
-
要访问左侧的子图,请将其引用为
axes[0]
。要访问右侧的图形,请将其引用为axes[1]
。在左侧坐标轴上,使用以下代码绘制 y 对 x 的图像:axes[0].plot(x, y)
-
使用以下内容添加标题:
axes[0].set_title('x by x Cubed')
-
使用以下代码行生成 x 轴标签:
axes[0].set_xlabel('Linearly Spaced Numbers')
-
使用以下代码创建 y 轴标签:
axes[0].set_ylabel('y Value')
结果输出如下所示:
图 2.28:包含两个子图的图像,左侧已创建
-
在右侧坐标轴上,使用以下代码绘制
y2
对x
的图像:axes[1].plot(x, y2)
-
使用以下代码添加标题:
axes[1].set_title('x by x Squared')
-
使用以下代码生成 x 轴标签:
axes[1].set_xlabel('Linearly Spaced Numbers')
-
使用以下代码创建 y 轴标签:
axes[1].set_ylabel('y Value')
以下截图显示了输出结果:
图 2.29:包含两个子图的图像
-
我们已经成功创建了两个子图。然而,看起来右侧的图的 y 轴与左侧图重叠。为了防止图形重叠,可以使用
plt.tight_layout()
。这里显示的是输出结果:
图 2.30:包含两个不重叠子图的图像
使用面向对象的方法,我们只需调用 fig
对象即可显示两个子图。我们将在活动 4 中进一步练习面向对象的绘图方法。
活动 4:使用子图绘制多种图类型
到目前为止,我们已经学习了如何使用函数式方法构建、定制和编程线性图、条形图、直方图、散点图和箱形图。在第 19 个练习中,我们介绍了面向对象的方法,而在第 20 个练习中,我们学习了如何使用子图创建包含多个图的图形。因此,在本活动中,我们将利用子图创建一个包含多个图表和图表类型的图形。我们将创建一个包含六个子图的图形。子图将按三行两列的布局显示(见图 2.31):
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_02_31.jpg)
图 2.31:子图布局
一旦我们生成了六个子图的图形,我们可以通过'行,列'索引访问每个子图(见图 2.32):
图 2.32:坐标轴索引引用
因此,要访问线性图(即左上角),使用axes[0, 0]
。要访问直方图(即中右),使用axes[1, 1]
。我们将在接下来的活动中练习这一点:
-
从 GitHub 导入'
Items_Sold_by_Week.csv
'和'Weight_by_Height.csv
',并生成一个符合正态分布的数字数组。 -
生成一个包含六个空子图的图形,使用三行两列的布局,确保子图不重叠。
-
设置图形标题,确保六个子图排列成三行两列且不重叠。
-
在'
Line
'、'Bar
'和'Horizontal Bar
'坐标轴上,使用'Items_Sold_by_Week.csv
'绘制按'Week
'排序的'Items_Sold
'数据。 -
在'
Histogram
'和'Box-and-Whisker
'坐标轴上,绘制包含 100 个符合正态分布的数字的数组。 -
在'
Scatter
'坐标轴上,使用'Weight_by_Height.csv
'绘制按身高排序的体重图。 -
为每个子图标注 x 轴和 y 轴。
-
增加图形的大小并保存。
注意
本活动的解决方案可以在第 338 页找到。
总结
在本章中,我们使用了 Python 绘图库 Matplotlib 来创建、定制和保存图表,采用了函数式方法。然后,我们讲解了描述性标题的重要性,并创建了我们自己的描述性编程标题。然而,函数式方法并不创建可调用的图形对象,也不返回子图。因此,为了创建一个可调用的图形对象并能够包含多个子图,我们采用了面向对象的方法来创建、定制和保存图表。绘图需求因分析而异,因此在本章中覆盖所有可能的图表并不实际。为了创建满足每个分析需求的强大图表,熟悉 Matplotlib 文档页面中的文档和示例至关重要。
在下一章中,我们将应用一些这些绘图技巧,进入使用 scikit-learn 的机器学习部分。
第十章. 数据分析的未来及技能发展方向
“我们正在创建和招聘以填补‘新领衔’职位——这些完全是新的职位,涉及如网络安全、数据科学、人工智能和认知商业等领域。”
– Ginni Rometty,IBM 董事长兼首席执行官
再次感谢并祝贺你,亲爱的读者,完成了这些长篇章节的阅读,并且可能尝试了一些或所有提供的示例代码。我尽力在深入探讨某个特定主题(例如深度学习或时间序列分析)的基础上,提供给实际操作人员的综合示例代码之间找到一个良好的平衡。我特别希望你能发现将数据科学分析与 PixieApp 应用编程模型紧密集成到一个 Jupyter Notebook 中的想法既有趣又新颖。但最重要的是,我希望你觉得它是有用的,并且是你可以在自己的项目和团队中重用的内容。
在第一章 编程与数据科学 – 新工具集的开头,我使用了 Drew 的 Conway 数据科学韦恩图(这是我最喜欢的图表之一)来表示什么是数据科学,以及为什么数据科学家通常被认为是独角兽。虽然我非常尊重 Drew Conway,但我想扩展这个图表,以表明开发者在数据科学领域中日益重要和不断增长的角色,如下图所示:
Drew 的 Conway 数据科学韦恩图,现在包括了开发者
我现在想利用最后一章来提供我对未来的看法,以及在人工智能和数据科学方面的预期。
前瞻性思维——人工智能和数据科学的未来预期
这一部分我非常喜欢,因为我可以表达前瞻性的观点,而不必为其准确性负责,因为从定义上讲,这些只是我的个人观点!前瞻性思维——人工智能和数据科学的未来预期。
正如我在第一章 编程与数据科学 – 新工具集中所解释的那样,我相信人工智能和数据科学将会持续存在,并且它们将在可预见的未来继续对现有行业产生颠覆性影响,最可能以加速的速度进行。这无疑会影响到总体的就业数量,类似于我们过去所见过的其他技术革命(农业革命、工业革命、信息革命等),一些岗位将会消失,而新的岗位也将被创造出来。
2016 年,IBM 董事长兼首席执行官 Ginny Rometty 在给唐纳德·特朗普总统的信中(www.ibm.com/blogs/policy/ibm-ceo-ginni-romettys-letter-u-s-president-elect
)谈到了通过创造她所称之为“新领带”类型的工作,来更好地为人工智能革命做准备,以下是信中的一段摘录:
“在今天的 IBM 找到一份工作并不总是需要大学学位;在我们美国的部分中心,甚至有三分之一的员工没有四年制大学学位。最重要的是相关技能,有时这些技能是通过职业培训获得的。此外,我们还在创造和招聘‘新领带’岗位——全新的角色,涵盖网络安全、数据科学、人工智能和认知业务等领域。”
这些“新领带”工作只有在我们成功实现数据科学的民主化时才能大规模创造,因为数据科学是人工智能的生命线,每个人都需要在某种程度上参与其中;开发人员、业务线用户、数据工程师等等。可以想象,这些新型工作的需求将如此之高,以至于传统的学术轨道将无法满足这些需求。相反,行业将负有责任通过创建新项目来填补这一空白,这些项目旨在重新培训所有那些工作可能面临被淘汰风险的现有员工。像苹果的人人都能编程项目(www.apple.com/everyone-can-code
)这样的新项目将会涌现;也许会有类似任何人都能做数据科学的项目。我还认为,MOOCs(即大规模在线开放课程)将在未来扮演更加重要的角色,正如我们今天已经看到的,Coursera 和 edX 等主要 MOOC 平台与 IBM 等公司之间的合作伙伴关系越来越多(见www.coursera.org/ibm
)。
公司还可以做其他事情,以更好地为人工智能和数据科学革命做好准备。在第一章,编程与数据科学——一种新的工具集中,我谈到了可以帮助我们实现这一雄心壮志目标的数据科学战略的三大支柱:数据、服务和工具。
在服务方面,公共云的高速增长大大推动了多个领域内高质量服务的整体提升:数据持久化、认知、流媒体等等。像亚马逊、Facebook、谷歌、IBM 和微软等提供商正引领着基于强大平台的服务优先的创新能力建设,为开发人员提供一致的体验。这一趋势将继续加速,越来越多强大的服务将在越来越快的速度下发布。
一个很好的例子是谷歌的自学习 AI——AlphaZero(en.wikipedia.org/wiki/AlphaZero
),它在 4 小时内自学会了国际象棋,并最终打败了一款冠军象棋程序。另一个很好的例子来自 IBM 最近宣布的项目辩论者(www.research.ibm.com/artificial-intelligence/project-debater
),它是第一个可以与人类在复杂话题上辩论的 AI 系统。这些类型的进展将持续推动越来越强大的服务的可用性,这些服务可以被包括开发者在内的每个人访问。聊天机器人是另一个成功实现民主化的服务示例,开发者现在比以往任何时候都更容易创建具有对话能力的应用程序。我相信,随着时间的推移,消费这些服务将变得越来越容易,从而使开发者能够构建我们今天甚至无法想象的令人惊叹的新应用程序。
在数据方面,我们需要使访问高质量数据变得比现在更容易。我想到的一个模型来自一部叫做24 小时的电视剧。完全坦白;我喜欢看电视剧并且狂看一整季,我认为其中一些电视剧能很好地反映技术的发展方向。在24 小时中,反恐特工杰克·鲍尔有 24 小时的时间阻止坏人制造灾难性事件。看这部剧时,我总是对数据如何轻松地从指挥中心的分析员传输到杰克·鲍尔的手机感到惊讶,或者当数据问题只有几分钟时间来解决时,分析员如何能从不同的系统(卫星图像、记录系统等)中召唤数据,精准锁定坏人;例如,我们正在寻找在过去两个月内购买过这种化学品并且位于某个特定半径范围内的人。哇!从我的角度看,这就是数据科学家访问和处理数据应该是如此简单和无摩擦的过程。我相信,我们正朝着这个目标取得巨大进展,工具如 Jupyter Notebooks 正充当着连接数据源与服务和分析的控制平面。Jupyter Notebooks 将工具带到数据面前,而不是相反,从而大大降低了任何希望参与数据科学的人的入门成本。
参考文献
-
DeepQA(IBM):
researcher.watson.ibm.com/researcher/view_group_subpage.php?id=2159
-
沃森中的深度解析,麦考德,穆多克,博古拉耶夫:
brenocon.com/watson_special_issue/03%20Deep%20parsing.pdf
-
Jupyter 数据科学, Dan Toomey, Packt Publishing:
www.packtpub.com/big-data-and-business-intelligence/jupyter-data-science
-
PixieDust 文档:
pixiedust.github.io/pixiedust/
-
你一直想要的 Jupyter Notebook 可视化 Python 调试器, David Taieb:
medium.com/ibm-watson-data-lab/the-visual-python-debugger-for-jupyter-notebooks-youve-always-wanted-761713babc62
-
在 Web 上分享你的 Jupyter Notebook 图表, David Taieb:
medium.com/ibm-watson-data-lab/share-your-jupyter-notebook-charts-on-the-web-43e190df4adb
-
使用 PixieDust 1.1 版本将您的分析部署为 Web 应用, David Taieb:
medium.com/ibm-watson-data-lab/deploy-your-analytics-as-web-apps-using-pixiedusts-1-1-release-d08067584a14
-
Kubernetes:
kubernetes.io/docs/home/
-
WordCloud:
amueller.github.io/word_cloud/index.html
-
神经网络与深度学习, Michael Nielsen:
neuralnetworksanddeeplearning.com/index.html
-
深度学习, Ian Goodfellow, Yoshua Bengio 和 Aaron Courville, 麻省理工学院出版社书籍:
www.deeplearningbook.org/
-
TensorFlow 文档网站:
www.tensorflow.org/
-
TensorFlow 为诗人:
codelabs.developers.google.com/codelabs/tensorflow-for-poets
-
Tensorflow 和深度学习 - 不需要博士学位,Martin Görner:
www.youtube.com/watch?v=vq2nnJ4g6N0
-
Apache Spark:
spark.apache.org/
-
Tweepy 库文档:
tweepy.readthedocs.io/en/latest/
-
Watson 开发者云 Python SDK:
github.com/watson-developer-cloud/python-sdk
-
Kafka-Python:
kafka-python.readthedocs.io/en/master/usage.html
-
使用 Spark 对 Twitter 标签进行情感分析, David Taieb:
medium.com/ibm-watson-data-lab/real-time-sentiment-analysis-of-twitter-hashtags-with-spark-7ee6ca5c1585
-
使用统计与机器学习模型进行时间序列预测,杰弗里·姚:
www.youtube.com/watch?v=_vQ0W_qXMxk
-
时间序列预测理论,分析大学:
www.youtube.com/watch?v=Aw77aMLj9uM
-
时间序列分析 - PyCon 2017,艾琳·尼尔森:
www.youtube.com/watch?v=zmfe2RaX-14
-
Quandl Python 文档:
docs.quandl.com/docs/python
-
Statsmodels 文档:
www.statsmodels.org/stable/index.html
-
NetworkX:
networkx.github.io/documentation/networkx-2.1/index.html
GeoJSON 规范:geojson.org/
-
Beautiful Soup 文档:
www.crummy.com/software/BeautifulSoup/bs4/doc
附录 A. PixieApp 快速参考
本附录是开发者的快速参考指南,提供了所有 PixieApp 属性的汇总。
注解
-
@PixieApp
:类注解,必须添加到任何 PixieApp 类上。参数:无
示例:
from pixiedust.display.app import * @PixieApp class MyApp(): pass
-
@route
:方法注解,必须加到一个方法上,以表示该方法(方法名可以随意)与一个路由相关联。参数:
**kwargs
。表示路由定义的关键字参数(键值对)。PixieApp 调度程序会根据以下规则将当前内核请求与路由进行匹配:-
参数数量最多的路由将最先被评估。
-
所有参数必须匹配,路由才会被选中。参数值可以使用
*
来表示匹配任何值。 -
如果找不到路由,则会选择默认路由(没有参数的那个)。
-
路由参数的每个键可以是临时状态(由
pd_options
属性定义)或持久化状态(PixieApp 类的字段,直到明确更改之前都会保留)。 -
方法可以有任意数量的参数。在调用方法时,PixieApp 调度程序将尝试根据参数名将方法参数与路由参数进行匹配。
返回值:该方法必须返回一个 HTML 片段(除非使用了
@captureOutput
注解),该片段将被注入到前端。方法可以利用 Jinja2 模板语法生成 HTML。HTML 模板可以访问一些特定的变量:-
this:引用 PixieApp 类(注意,我们使用
this
而不是self
,因为self
已经被 Jinja2 框架本身使用) -
prefix:一个字符串 ID,唯一标识 PixieApp 实例
-
entity:请求的当前数据实体
-
方法参数:方法的所有参数都可以作为变量在 Jinja2 模板中访问。
from pixiedust.display.app import * @PixieApp class MyApp(): @route(key1=”value1”, key2=”*”) def myroute_screen(self, key1, key2): return “<div>fragment: Key1 = {{key1}} - Key2 = {{key2}}”
示例:
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode25.py
-
-
@templateArgs
:注解,允许在 Jinja2 模板中使用任何本地变量。注意,@templateArgs
不能与@captureOutput
一起使用:参数:无
示例:
from pixiedust.display.app import * @PixieApp class MyApp(): @route(key1=”value1”, key2=”*”) @templateArgs def myroute_screen(self, key1, key2): local_var = “some value” return “<div>fragment: local_var = {{local_var}}”
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode26.py
-
@captureOutput
:注解用于改变路由方法的契约,使得方法不再需要返回 HTML 片段。相反,方法体可以像在 Notebook 单元中那样直接输出结果。框架会捕获输出并以 HTML 形式返回。注意,在这种情况下你不能使用 Jinja2 模板。参数:无
示例:
from pixiedust.display.app import * import matplotlib.pyplot as plt @PixieApp class MyApp(): @route() @captureOutput def main_screen(self): plt.plot([1,2,3,4]) plt.show()
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode27.py
-
@Logger
:通过向类添加日志方法来添加日志功能:debug
、warn
、info
、error
、critical
、exception
。参数:无
示例:
from pixiedust.display.app import * from pixiedust.utils import Logger @PixieApp @Logger() class MyApp(): @route() def main_screen(self): self.debug(“In main_screen”) return “<div>Hello World</div>”
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode28.py
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode28.py
自定义 HTML 属性
这些可以与任何常规 HTML 元素一起使用,用于配置内核请求。当元素接收到点击或更改事件时,或者在 HTML 片段完成加载后,PixieApp 框架可以触发这些请求。
-
pd_options
:定义内核请求瞬态状态的键值对列表,格式如下:pd_options=”key1=value1;key2=value2;...”
。当与pd_entity
属性结合使用时,pd_options
属性会调用 PixieDust 的display()
API。在这种情况下,你可以从另一个 Notebook 单元格的元数据中获取值,在该单元格中你已经使用了display()
API。建议在display()
模式下使用pd_options
时,为了方便起见,可以通过创建一个名为<pd_options>
的子元素,并将 JSON 值作为文本包含在其中,来使用pd_options
的 JSON 表示法。使用
pd_options
作为子元素调用display()
的示例:<div pd_entity> <pd_options> { “mapboxtoken”: “XXXXX”, “chartsize”: “90”, “aggregation”: “SUM”, “rowCount”: “500”, “handlerId”: “mapView”, “rendererId”: “mapbox”, “valueFields”: “IncidntNum”, “keyFields”: “X,Y”, “basemap”: “light-v9” } </pd_options> </div>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode29.html
使用
pd_options
作为 HTML 属性的示例:<!-- Invoke a route that displays a chart --> <button type=”submit” pd_options=”showChart=true” pd_target=”chart{{prefix}}”> Show Chart </button>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode30.html
-
pd_entity
:仅用于在特定数据上调用display()
API。必须与pd_options
一起使用,其中的键值对将作为参数传递给display()
。如果没有为pd_entity
属性指定值,则假定它是传递给启动 PixieApp 的run
方法的实体。pd_entity
的值可以是 Notebook 中定义的变量,也可以是 PixieApp 的字段(例如,pd_entity=”df”
),或者是使用点符号表示法指向对象的字段(例如,pd_entity=”obj_instance.df”
)。 -
pd_target
:默认情况下,内核请求的输出会被注入到整体输出单元格或对话框中(如果你将runInDialog="true"
作为run
方法的参数)。但是,你可以使用pd_target="elementId"
来指定一个接收输出的目标元素。(请注意,elementId
必须在当前视图中存在。)示例:
<div id=”chart{{prefix}}”> <button type=”submit” pd_options=”showChart=true” pd_target=”chart{{prefix}}”> Show Chart </button> </div>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode31.html
-
pd_script
:此属性调用任意的 Python 代码作为内核请求的一部分。可以与其他属性如pd_entity
和pd_options
结合使用。需要注意的是,必须遵守 Python 的缩进规则(docs.python.org/2.0/ref/indentation.html
),以避免运行时错误。如果 Python 代码包含多行,建议将
pd_script
作为子元素使用,并将代码存储为文本。示例:
<!-- Invoke a method to load a dataframe before visualizing it --> <div id=”chart{{prefix}}”> <button type=”submit” pd_entity=”df” pd_script=”self.df = self.load_df()” pd_options=”handlerId=dataframe” pd_target=”chart{{prefix}}”> Show Chart </button> </div>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode32.html
-
pd_app
:此属性通过其完全限定类名动态调用一个独立的 PixieApp。可以使用pd_options
属性传递路由参数,以调用 PixieApp 的特定路由。示例:
<div pd_render_onload pd_option=”show_route_X=true” pd_app=”some.package.RemoteApp”> </div>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode33.html
-
pd_render_onload
:此属性应在页面加载时触发内核请求,而不是在用户点击某个元素或发生变化事件时触发。pd_render_onload
属性可以与定义请求的其他属性结合使用,如pd_options
或pd_script
。请注意,此属性只能与 div 元素一起使用。示例:
<div pd_render_onload> <pd_script> print(‘hello world rendered on load’) </pd_script> </div>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode34.html
-
pd_refresh
:此属性用于强制 HTML 元素执行内核请求,即使没有发生任何事件(点击或变化事件)。如果没有指定值,则刷新当前元素;否则,刷新值中指定 ID 的元素。示例:
<!-- Update state before refreshing a chart --> <button type=”submit” pd_script=”self.show_line_chart()” pd_refresh=”chart{{prefix}}”> Show line chart </button>
注意
你可以在这里找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode35.html
-
pd_event_payload
:此属性用于发出一个带有指定负载内容的 PixieApp 事件。该属性遵循与pd_options
相同的规则:-
每个键值对必须使用
key=value
表示法进行编码 -
事件将在点击或变化事件时触发
-
支持使用
$val()
指令动态注入用户输入 -
使用
<pd_event_payload>
子元素输入原始 JSON。<button type=”submit” pd_event_payload=”type=topicA;message=Button clicked”> Send event A </button> <button type=”submit”> <pd_event_payload> { “type”:”topicA”, “message”:”Button Clicked” } </pd_event_payload> Send event A </button>
示例:
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode36.html
-
-
pd_event_handler
:订阅者可以通过声明<pd_event_handler>
子元素来监听事件,该元素可以接受任何 PixieApp 内核执行属性,如pd_options
和pd_script
。该元素必须使用pd_source
属性来过滤其想要处理的事件。pd_source
属性可以包含以下值之一:-
targetDivId
:只有来自指定 ID 元素的事件才会被接受 -
type
:只有指定类型的事件才会被接受<div class=”col-sm-6” id=”listenerA{{prefix}}”> Listening to button event <pd_event_handler pd_source=”topicA” pd_script=”print(eventInfo)” pd_target=”listenerA{{prefix}}”> </pd_event_handler> </div>
示例:
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode37.html
注意:使用
*
作为pd_source
表示将接受所有事件。 -
-
pd_refresh_rate
:用于在指定的时间间隔内(以毫秒为单位)重复执行一个元素。这对于需要轮询特定变量状态并在 UI 中显示结果的场景非常有用。示例:
<div pd_refresh_rate=”3000” pd_script=”print(self.get_status())”> </div>
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode38.html
方法
-
setup
:这是 PixieApp 实现的可选方法,用于初始化其状态。在 PixieApp 运行之前会自动调用。参数:无
示例:
def setup(self): self.var1 = “some initial value” self.pandas_dataframe = pandas.DataFrame(data)
注意
你可以在此处找到代码文件:
github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%205/sampleCode39.py
-
run
:用于启动 PixieApp。参数:
-
实体:[可选] 作为输入传递给 PixieApp 的数据集。可以通过
pd_entity
属性或直接作为名为pixieapp_entity
的字段引用。 -
****kwargs**:要传递给 PixieApp 的关键字参数。例如,使用
runInDialog="true"
将以对话框方式启动 PixieApp。app = MyPixieApp() app.run(runInDialog=”true”)
示例:
-
-
invoke_route
:用于以编程方式调用路由。参数:
-
路由方法:需要调用的方法。
-
****kwargs**:要传递给路由方法的关键字参数。
app.invoke_route(app.route_method, arg1 = “value1”, arg2 = “value2”)
示例:
-
-
getPixieAppEntity
:用于检索调用run()
方法时传递的当前 PixieApp 实体(可能为 None)。getPixieAppEntity()
通常在 PixieApp 内部调用,即:self.getPixieAppEntity()
第十三章:第三章
通过 Scikit-Learn 进行机器学习简介
学习目标
本章结束时,你将能够:
-
为不同类型的有监督学习模型准备数据。
-
使用网格搜索调整模型的超参数。
-
从调整过的模型中提取特征重要性。
-
评估分类和回归模型的表现。
在本章中,我们将讲解处理数据并使数据准备好进行分析的关键概念。
介绍
scikit-learn 是一个免费的开源库,专为 Python 构建,包含一系列有监督和无监督的机器学习算法。此外,scikit-learn 提供数据预处理、超参数调优和模型评估的功能,我们将在接下来的章节中涉及到这些内容。它简化了模型构建过程,并且易于在各种平台上安装。scikit-learn 起步于 2007 年,由 David Corneapeau 在 Google Summer of Code 项目中创建,经过一系列的开发和发布,scikit-learn 已经发展成学术界和专业人士广泛使用的机器学习工具之一。
本章中,我们将学习构建一系列广泛使用的建模算法,即线性回归和逻辑回归、支持向量机(SVM)、决策树和随机森林。首先,我们将介绍线性回归和逻辑回归。
线性回归与逻辑回归简介
在回归分析中,使用一个或多个自变量来预测单一的因变量或结果变量。回归的应用场景包括但不限于以下预测:
-
根据各种球队统计数据预测球队的胜率
-
根据家族病史及一系列身体和心理特征预测患心脏病的风险
-
根据多个气候指标预测降雪的可能性
由于线性回归和逻辑回归具有易于解释和透明的特点,并且能够对未在训练数据中出现的值进行外推,因此它们是预测此类结果的流行选择。线性回归的最终目标是绘制一条直线,使得这条直线与观测值之间的绝对距离最小(即最佳拟合线)。因此,在线性回归中,假设特征与连续因变量之间的关系是线性的。线的形式通常是斜截式(即 y = a + bx),其中 a 为截距(即当 x 为 0 时 y 的值),b 为斜率,x 为自变量。本章将介绍两种线性回归:简单线性回归和多重线性回归。
简单线性回归
简单线性回归模型通过y = α + β**x定义一个特征与连续结果变量之间的关系。这个方程类似于斜截式,其中y表示因变量的预测值,α表示截距,β(贝塔)代表斜率,x是自变量的值。给定x值后,回归模型计算出能够最小化预测的y值(即ŷ)与实际y值之间绝对差异的α和β的值。
例如,如果我们使用身高(米)作为唯一预测变量来预测一个人的体重(千克),并且简单线性回归模型计算出α的值为 1.5,β的系数为 50,那么该模型可以解释为:每增加 1 米身高,体重大约增加 50 千克。因此,我们可以预测身高为 1.8 米的人的体重大约是 91.5 千克,计算公式为 y = 1.5 + (50 x 1.8)。在接下来的练习中,我们将演示如何使用 scikit-learn 进行简单线性回归。
练习 21:为线性回归模型准备数据
为了准备我们的数据以用于简单的线性回归模型,我们将使用一个随机子集,该子集来自 2006 至 2016 年间匈牙利塞格德市的天气数据集。该数据集包含从 2006 年 4 月 1 日到 2016 年 9 月 9 日的每小时天气测量数据。经过处理的数据以.csv
文件形式提供(https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter02/weather.csv),并包含 10,000 条观察记录,涵盖 8 个变量:
-
Temperature_c
:温度,单位为摄氏度 -
Humidity
:湿度比例 -
Wind_Speed_kmh
:风速,单位为公里每小时 -
Wind_Bearing_Degrees
:风向,按顺时针方向从正北开始的角度 -
Visibility_km
:能见度,单位为公里 -
Pressure_millibars
:大气压力,单位为毫巴 -
Rain
:雨=1,雪=0 -
Description
:温暖、正常或寒冷
-
使用以下代码导入
weather.csv
数据集:import pandas as pd df = pd.read_csv('weather.csv')
-
使用
df.info()
探索数据:](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_03_01.jpg)
图 3.1:描述 df 的信息
-
Description
列是df
中的唯一类别变量。可以按如下方式检查Description
中的层级数量:levels = len(pd.value_counts(df['Description'])) print('There are {} levels in the Description column'.format(levels))
层级的数量如下图所示:
图 3.2:'Description'列中的层级数量
注意
多类类别变量必须通过一种称为“虚拟编码”的过程转换为虚拟变量。对多类类别变量进行虚拟编码会创建 n-1 个新的二元特征,这些特征对应于类别变量中的层级。例如,一个具有三个层级的多类类别变量将创建两个二元特征。经过虚拟编码后,必须删除原始特征。
-
要对所有多分类的类别变量进行虚拟编码,请参考以下代码:
import pandas as pd df_dummies = pd.get_dummies(df, drop_first=True)
注意
原始的 DataFrame
df
包含八列,其中一列(即Description
)是一个具有三个级别的多分类类别变量。 -
在第 4 步中,我们将这个特征转换为 n-1(即 2)个分开的虚拟变量,并删除了原始特征
Description
。因此,df_dummies
现在应该包含比df
多一列(即 9 列)。使用以下代码检查这一点:print('There are {} columns in df_dummies' .format(df_dummies.shape[1]))
图 3.3:虚拟编码后列的数量
-
为了消除数据中可能存在的顺序效应,良好的实践是先对数据的行进行洗牌,然后再将数据拆分为特征(
X
)和结果(y
)。要对df_dummies
中的行进行洗牌,请参考以下代码:from sklearn.utils import shuffle df_shuffled = shuffle(df_dummies, random_state=42)
-
现在数据已经被洗牌,我们将把数据的行拆分为特征(
X
)和因变量(y
)。注意
线性回归用于预测连续的结果。因此,在本次练习中,我们假设连续变量
Temperature_c
(摄氏温度)是因变量,我们正在准备数据来拟合一个线性回归模型。 -
将
df_shuffled
拆分为X
和y
如下所示:DV = 'Temperature_c' X = df_shuffled.drop(DV, axis=1) y = df_shuffled[DV]
-
使用以下代码将
X
和y
拆分为测试数据和训练数据:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
现在,数据已经进行了虚拟编码、洗牌、拆分成
X
和y
,并进一步划分为测试数据集和训练数据集,准备好用于线性回归或逻辑回归模型。这里的截图展示了
X_train
的前五行:
图 3.4:X_train
的前五行
练习 22:拟合简单线性回归模型并确定截距和系数
在本次练习中,我们将继续使用练习 21 中准备的数据,通过简单线性回归模型来预测摄氏温度与湿度之间的关系。
从练习 21 继续,执行以下步骤:
-
要实例化一个线性回归模型,请参考以下代码:
from sklearn.linear_model import LinearRegression model = LinearRegression()
-
使用以下代码将模型拟合到训练数据中的
Humidity
列:model.fit(X_train[['Humidity']], y_train)
图 3.5:拟合简单线性回归模型的输出
-
使用以下代码提取截距的值:
intercept = model.intercept_
-
使用以下代码提取
coefficient
的值:coefficient = model.coef_
-
现在,我们可以使用以下代码打印出预测摄氏温度的公式:
print('Temperature = {0:0.2f} + ({1:0.2f} x Humidity)'.format(intercept, coefficient[0]))
图 3.6:使用简单线性回归从湿度预测摄氏温度的公式
干得好!根据这个简单线性回归模型,一个湿度值为 0.78 的日子预测温度为 10.56 摄氏度。现在我们已经熟悉了提取简单线性回归模型的截距和系数,是时候生成预测并评估模型在未见过的测试数据上的表现了。
教学提示
练习在不同湿度水平下计算温度。
练习 23:生成预测并评估简单线性回归模型的表现
监督学习的核心目的是使用现有的标记数据生成预测。因此,本练习将演示如何在测试特征上生成预测,并通过将预测与实际值进行比较来生成模型性能指标。
从练习 22继续,执行以下步骤:
-
使用以下代码在测试数据上生成预测:
predictions = model.predict(X_test[['Humidity']])
注意
评估模型性能的一种常见方法是使用散点图检查预测值与实际值之间的相关性。散点图展示了实际值与预测值之间的关系。一个完美的回归模型将在预测值和实际值之间显示一条直线。预测值与实际值之间的关系可以通过皮尔逊 r 相关系数来量化。在接下来的步骤中,我们将创建一个预测值与实际值的散点图。
-
如果相关系数显示在图表标题中会更有帮助。以下代码将演示如何做到这一点:
import matplotlib.pyplot as plt from scipy.stats import pearsonr plt.scatter(y_test, predictions) plt.xlabel('Y Test (True Values)') plt.ylabel('Predicted Values') plt.title('Predicted vs. Actual Values (r = {0:0.2f})'.format(pearsonr(y_test, predictions)[0], 2)) plt.show()
这是生成的输出结果:
图 3.7:简单线性回归模型的预测值与实际值对比
注意
皮尔逊 r 值为 0.62,表明预测值与实际值之间存在中等程度的正向线性相关性。一个完美的模型会使散点图中的所有点都在一条直线上,并且 r 值为 1.0。
-
一个与数据拟合得非常好的模型,其残差应该呈正态分布。要创建残差的密度图,请参照以下代码:
import seaborn as sns from scipy.stats import shapiro sns.distplot((y_test - predictions), bins = 50) plt.xlabel('Residuals') plt.ylabel('Density') plt.title('Histogram of Residuals (Shapiro W p-value = {0:0.3f})'.format(shapiro(y_test - predictions)[1])) plt.show()
请参阅此处的输出结果:
图 3.8:简单线性回归模型的残差直方图
注意
直方图显示残差呈负偏态,标题中的 Shapiro W p 值告诉我们该分布不是正态分布。这进一步证明我们的模型还有改进空间。
-
最后,我们将计算平均绝对误差、均方误差、均方根误差和 R 平方值,并使用以下代码将它们放入一个数据框中:
from sklearn import metrics import numpy as np metrics_df = pd.DataFrame({'Metric': ['MAE', 'MSE', 'RMSE', 'R-Squared'], 'Value': [metrics.mean_absolute_error(y_test, predictions), metrics.mean_squared_error(y_test, predictions), np.sqrt(metrics.mean_squared_error(y_test, predictions)), metrics.explained_variance_score(y_test, predictions)]}).round(3) print(metrics_df)
请参阅生成的输出结果:
图 3.9:简单线性回归模型的模型评估指标
平均绝对误差(MAE)是预测值与实际值之间的平均绝对差。均方误差(MSE)是预测值与实际值之间差的平方的平均值。均方根误差(RMSE)是 MSE 的平方根。R 方告诉我们可以由模型解释的因变量方差的比例。因此,在这个简单线性回归模型中,湿度仅能解释温度方差的 38.9%。此外,我们的预测值在±6.052 摄氏度范围内。
在这里,我们成功地使用 scikit-learn 拟合和评估了一个简单线性回归模型。这是成为机器学习专家之旅中的第一步。接下来,我们将继续扩展我们对回归的知识,并通过探索多元线性回归来改进这个模型。
多元线性回归
多元线性回归模型通过 y = α + β1x**i1 + β2x**i2 + … + βp-1x**i,p-1 定义了两个或更多特征与连续结果变量之间的关系。再次说明,α 表示截距,β 表示模型中每个特征(x)的斜率。因此,如果我们使用身高(m)、总胆固醇(mg/dL)和每日心血管运动分钟数来预测个体的体重(kg),且多元线性回归模型计算出 α 为 1.5,β**1 为 50,β**2 为 0.1,β**3 为-0.4,这个模型可以解释为:在控制模型中的所有其他特征的情况下,每增加 1 m 的身高,体重增加 50 kg。此外,每增加 1 mg/dL 的总胆固醇,体重增加 0.1 kg。最后,每天每增加 1 分钟的心血管运动,体重减少 0.4 kg。因此,我们可以预测一个身高为 1.8 m、总胆固醇为 200 mg/dL,并每天完成 30 分钟心血管运动的个体的体重为 99.5 kg,使用 y = 1.5 + (0.1 x 50) + (200 x 0.5) + (30 x -0.4)。在下一项练习中,我们将演示如何使用 scikit-learn 进行多元线性回归。
练习 24:拟合多元线性回归模型并确定截距和系数
在这个练习中,我们将继续使用我们在练习 21中准备的数据,拟合一个多元线性回归模型,以预测数据中所有特征对应的温度(摄氏度)。
继续自练习 23,执行以下步骤:
-
要实例化一个线性回归模型,请参考以下代码:
from sklearn.linear_model import LinearRegression model = LinearRegression()
-
使用以下代码将模型拟合到训练数据中:
model.fit(X_train, y_train)
图 3.10:拟合多元线性回归模型的输出结果
-
使用以下代码提取截距值:
intercept = model.intercept_
-
按照以下方法提取系数值:
coefficients = model.coef_
-
现在,我们可以打印出一个消息,显示使用此代码预测摄氏温度的公式:
print('Temperature = {0:0.2f} + ({1:0.2f} x Humidity) + ({2:0.2f} x Wind Speed) + ({3:0.2f} x Wind Bearing Degrees) + ({4:0.2f} x Visibility) + ({5:0.2f} x Pressure) + ({6:0.2f} x Rain) + ({7:0.2f} x Normal Weather) + ({8:0.2f} x Warm Weather)'.format(intercept, coefficients[0], coefficients[1], coefficients[2], coefficients[3], coefficients[4], coefficients[5], coefficients[6], coefficients[7]))
我们的输出应如下所示:
图 3.11:使用多元线性回归根据湿度预测摄氏温度的公式
做得好!根据这个多元回归模型,一天湿度为 0.78,风速为 5.0 km/h,风向为自正北顺时针 81 度,能见度为 3 km,气压为 1000 毫巴,没有降雨,且天气被描述为正常,那么该天预测的摄氏温度为 5.72 度。现在我们已经熟悉了如何提取多元线性回归模型的截距和系数,我们可以生成预测并评估模型在测试数据上的表现。
活动 5:生成预测并评估多元线性回归模型的性能
在练习 23中,生成预测并评估简单线性回归模型的性能,我们学习了如何使用多种方法生成预测并评估简单线性回归模型的性能。为了减少代码的冗余,我们将使用练习 23中步骤 4的指标来评估多元线性回归模型的性能,并确定多元线性回归模型相对于简单线性回归模型的表现是更好还是更差。
从练习 24 开始,执行以下步骤:
-
使用所有特征在测试数据上生成预测。
-
使用散点图绘制预测值与实际值的关系。
-
绘制残差的分布图。
-
计算均值绝对误差、均方误差、均方根误差和 R 平方,并将结果放入 DataFrame 中。
-
确定多元线性回归模型相较于简单线性回归模型的表现是更好还是更差。
注意
此活动的解决方案可以在第 343 页找到。
你应该发现,相较于简单线性回归模型,多元线性回归模型在每个指标上表现得更好。最显著的是,在简单线性回归模型中,模型只能解释温度方差的 38.9%。然而,在多元线性回归模型中,特征的组合解释了 86.6% 的温度方差。此外,我们的简单线性回归模型预测的温度平均误差为 ± 6.052 度,而我们的多元线性回归模型预测的温度平均误差为 ± 2.861 度。
截距和回归系数的透明性使得线性回归模型非常易于解释。在商业中,通常要求数据科学家解释某一特征对结果的影响。因此,线性回归提供了可用的指标,从而合理回应了之前的业务询问。
然而,大多数情况下,问题要求数据科学家预测一个不是连续的,而是分类的结果变量。例如,在保险领域,给定客户的某些特征,预测该客户是否会续保的概率是多少?在这种情况下,数据中的特征与结果变量之间不存在线性关系,因此线性回归方法会失败。对于分类因变量,进行回归分析的一个可行选择是逻辑回归。
逻辑回归
逻辑回归使用分类变量和连续变量来预测分类结果。当所选因变量有两个分类结果时,分析称为二元逻辑回归。然而,如果结果变量有多个水平,则该分析称为多项式逻辑回归。本章的学习将集中在前者上。
在预测二元结果时,特征与结果变量之间并不存在线性关系,这违反了线性回归的假设。因此,为了以线性方式表达非线性关系,我们必须使用对数转换来转换数据。因此,逻辑回归使我们能够预测在给定模型中特征的情况下二元结果发生的概率。
对于具有 1 个预测变量的逻辑回归,逻辑回归方程如下所示:
图 3.12:具有 1 个预测变量的逻辑回归公式
在上图中,P(Y) 是结果发生的概率,e 是自然对数的底数,α 是截距,β 是回归系数,x 是预测变量的值。该方程可以扩展到多个预测变量,使用以下公式:
图 3.13:具有多个预测变量的逻辑回归公式
因此,使用逻辑回归来建模事件发生的概率,就相当于拟合一个线性回归模型,只是连续的结果变量已被替换为成功的对数比(另一种表达概率的方式),用于二元结果变量。在线性回归中,我们假设预测变量与结果变量之间存在线性关系。而逻辑回归则假设预测变量与 p/(1-p) 的自然对数之间存在线性关系,其中 p 是事件发生的概率。
在接下来的练习中,我们将使用 weather.csv
数据集,演示如何构建一个逻辑回归模型,利用我们数据中的所有特征来预测降雨的概率。
练习 25:拟合逻辑回归模型并确定截距和系数
为了使用数据中的所有特征来建模降雨的概率(而非雪),我们将使用 weather.csv
文件,并将二元变量 Rain
作为结果测量。
-
使用以下代码导入数据:
import pandas as pd df = pd.read_csv('weather.csv')
-
如下所示对
Description
变量进行虚拟编码:import pandas as pd df_dummies = pd.get_dummies(df, drop_first=True)
-
使用此处的代码对
df_dummies
进行洗牌:from sklearn.utils import shuffle df_shuffled = shuffle(df_dummies, random_state=42)
-
如下所示,将特征和结果分别拆分为
X
和y
:DV = 'Rain' X = df_shuffled.drop(DV, axis=1) y = df_shuffled[DV]
-
使用以下代码将特征和结果拆分为训练数据和测试数据:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
-
使用以下代码实例化一个逻辑回归模型:
from sklearn.linear_model import LogisticRegression model = LogisticRegression()
-
使用
model.fit(X_train, y_train)
将逻辑回归模型拟合到训练数据中。我们应该得到以下输出:图 3.14:拟合逻辑回归模型的输出
-
使用以下代码获取截距:
intercept = model.intercept_
-
使用以下代码提取系数:
coefficients = model.coef_
-
如下所示,将系数放入列表:
coef_list = list(coefficients[0,:])
-
将特征与它们的系数匹配,放入 DataFrame 中,并按如下方式将 DataFrame 打印到控制台:
coef_df = pd.DataFrame({'Feature': list(X_train.columns), 'Coefficient': coef_list}) print(coef_df)
请参考此处的输出:
图 3.15:逻辑回归模型的特征及其系数
温度的系数可以解释为:每增加 1 度温度,降雨的对数几率增加 5.69,同时控制模型中的其他特征。为了生成预测,我们可以将对数几率转换为几率,再将几率转换为概率。然而,scikit-learn 提供了生成预测概率以及预测类别的功能。
练习 26:生成预测并评估逻辑回归模型的表现
在练习 25中,我们学习了如何拟合一个逻辑回归模型,并提取生成预测所需的元素。然而,scikit-learn 通过提供函数来预测结果的概率以及预测结果的类别,使我们的工作变得更加轻松。在本练习中,我们将学习如何生成预测的概率和类别,并使用混淆矩阵和分类报告评估模型的表现。
从练习 25 开始,执行以下步骤:
-
使用以下代码生成预测概率:
predicted_prob = model.predict_proba(X_test)[:,1]
-
使用以下代码生成预测类别:
predicted_class = model.predict(X_test)
-
使用混淆矩阵按如下方式评估表现:
from sklearn.metrics import confusion_matrix import numpy as np cm = pd.DataFrame(confusion_matrix(y_test, predicted_class)) cm['Total'] = np.sum(cm, axis=1) cm = cm.append(np.sum(cm, axis=0), ignore_index=True) cm.columns = ['Predicted No', 'Predicted Yes', 'Total'] cm = cm.set_index([['Actual No', 'Actual Yes', 'Total']]) print(cm)
请参考此处的输出:
图 3.16:我们逻辑回归模型的混淆矩阵
注意
从混淆矩阵中,我们可以看到,在 383 个未被分类为“雨天”的观测值中,377 个被正确分类;在 2917 个被分类为“雨天”的观测值中,2907 个被正确分类。为了进一步检查我们模型的性能,我们将使用精度、召回率和
f1
得分等指标生成分类报告。 -
使用以下代码生成分类报告:
from sklearn.metrics import classification_report print(classification_report(y_test, predicted_class))
请参考结果输出:
图 3.17:我们逻辑回归模型生成的分类报告
从我们的混淆矩阵和分类报告中可以看出,我们的模型表现非常好,可能很难进一步改进。然而,机器学习模型,包括逻辑回归,包含许多可以调整的超参数,调优这些超参数可以进一步提高模型性能。在下一个练习中,我们将学习如何找到超参数的最佳组合,以最大化模型性能。
练习 27:调优多重逻辑回归模型的超参数
在练习 25的步骤 7中,我们拟合了一个逻辑回归模型,随后该模型的输出显示在图 3.14 中。LogisticRegression()
函数中的每个参数都设置为默认超参数。为了调优模型,我们将使用 scikit-learn 的网格搜索功能,它会为每个可能的超参数值组合拟合模型,并确定每个超参数的最佳值,从而得到最好的模型。在本练习中,我们将学习如何使用网格搜索来调优模型。
继续从练习 26:
-
数据已经为我们准备好了(见练习 26);因此,我们可以直接开始实例化一个可能的超参数值网格,如下所示:
import numpy as np grid = {'penalty': ['l1', 'l2'], 'C': np.linspace(1, 10, 10), 'solver': ['liblinear']}
-
实例化一个网格搜索模型,寻找具有最大
f1
得分(即精度和召回率的调和平均数)的模型,如下所示:from sklearn.model_selection import GridSearchCV from sklearn.linear_model import LogisticRegression model = GridSearchCV(LogisticRegression(solver='liblinear'), grid, scoring='f1', cv=5)
-
使用
model.fit(X_train, y_train)
在训练集上拟合模型(请记住,这可能需要一些时间),并找到此处的结果输出:图 3.18:我们逻辑回归网格搜索模型的输出
-
我们可以如下所示返回超参数的最佳组合,以字典形式表示:
best_parameters = model.best_params_ print(best_parameters)
请参考此处的结果输出:
图 3.19:我们逻辑回归网格搜索模型调优后的超参数
我们已经找到了最大化f1
得分的超参数组合。请记住,仅仅使用练习 25中的默认超参数就能得到一个在测试数据上表现非常好的模型。因此,在接下来的活动中,我们将评估调优过的超参数模型在测试数据上的表现。
活动 6:生成预测并评估调优后的逻辑回归模型性能
一旦找到了最佳的超参数组合,我们需要像在练习 25 中一样评估模型的性能。
从练习 27 继续:
-
生成预测的降雨概率。
-
生成预测的降雨类别。
-
使用混淆矩阵评估性能,并将其存储为数据框。
-
打印分类报告。
注意
此活动的解决方案可以在第 346 页找到。
通过调整逻辑回归模型的超参数,我们能够改进已经表现非常良好的逻辑回归模型。我们将在接下来的练习和活动中继续扩展调整不同类型的模型。
最大边界分类使用 SVM
SVM 是一种监督学习算法,可以解决分类和回归问题。然而,SVM 最常用于分类问题,因此,在本章中,我们将重点讨论 SVM 作为二分类器。SVM 的目标是确定超平面的最佳位置,在多维空间中创建数据点之间的类别边界。为帮助澄清这一概念,请参见图 3.20。
图 3.20:超平面(蓝色)在三维空间中将圆形与方形分开
在图 3.20 中,方形和圆形是同一数据框中的观测值,代表不同的类别。在该图中,超平面由一个半透明的蓝色边界表示,位于圆形和方形之间,划分出两个不同的类别。在这个例子中,观测点被认为是线性可分的。
超平面的位置是通过找到在两类之间创建最大分隔(即边界)的位置来确定的。因此,这被称为最大边界超平面(MMH),并提高了点保持在超平面边界正确一侧的可能性。可以通过每个类中最接近 MMH 的点来表达 MMH。这些点被称为支持向量,每个类至少有 1 个。图 3.21 直观地展示了支持向量与 MMH 在二维空间中的关系:
图 3.21:支持向量与 MMH 的关系
实际上,大多数数据是不可线性分割的。在这种情况下,SVM 使用松弛变量,创建一个软边界(与最大边界相对),允许一些观测点落在边界的错误一侧。请参见以下图示:
图 3.22:两个观测值(用灰色阴影和希腊字母Χi 表示)位于软边界线的错误一侧
对误分类的数据点应用一个成本值,算法的目标不是找到最大边界,而是最小化总成本。随着成本参数的增加,SVM 优化将更加严格,目标是 100%的分类分离,这可能导致过拟合训练数据。相反,较低的成本参数则强调更宽的边界,可能导致欠拟合训练数据。因此,为了创建在测试数据上表现良好的 SVM 模型,重要的是确定一个平衡过拟合和欠拟合的成本参数。
此外,非线性可分的数据可以通过使用核技巧转换到更高维度的空间。在映射到高维空间后,原本的非线性关系可能变得线性。通过对原始数据进行转换,SVM 可以发现原始特征中没有明确体现的关联。scikit-learn 默认使用高斯 RBF 核,但也提供了常用的核函数,如线性核、多项式核和 sigmoid 核。为了最大化 SVM 分类器模型的性能,必须确定核函数和成本函数的最佳组合。幸运的是,可以通过网格搜索超参数调整轻松实现这一点,正如练习 27 中介绍的那样。在接下来的练习和活动中,我们将学习如何实现这一目标。
练习 28:为支持向量分类器(SVC)模型准备数据
在为预测二元结果变量(在此案例中为雨或雪)拟合 SVM 分类器模型之前,我们必须先准备数据。由于 SVM 是一个“黑箱”模型,即输入和输出之间的过程不明确,因此我们不需要担心模型的可解释性。因此,在拟合模型之前,我们将数据中的特征转换为 z 分数。以下步骤将展示如何进行:
-
使用以下代码导入
weather.csv
:import pandas as pd df = pd.read_csv('weather.csv')
-
将分类特征
Description
进行虚拟编码,方法如下:import pandas as pd df_dummies = pd.get_dummies(df, drop_first=True)
-
使用以下代码对
df_dummies
进行洗牌,以去除任何排序效应:from sklearn.utils import shuffle df_shuffled = shuffle(df_dummies, random_state=42)
-
使用以下代码将
df_shuffled
拆分为X
和y
:DV = 'Rain' X = df_shuffled.drop(DV, axis=1) y = df_shuffled[DV]
-
使用以下代码将
X
和y
划分为测试数据和训练数据:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
-
为了防止数据泄漏,通过拟合一个缩放器模型到
X_train
并分别将其转换为 z 分数,按如下方式对X_train
和X_test
进行缩放:from sklearn.preprocessing import StandardScaler model = StandardScaler() X_train_scaled = model.fit_transform(X_train) X_test_scaled = model.transform(X_test)
现在,我们的数据已经正确地划分为特征和结果变量,分为测试数据和训练数据,并且已分别进行了缩放,因此我们可以使用网格搜索调整 SVC 模型的超参数。
练习 29:使用网格搜索调整 SVC 模型
之前,我们讨论了确定 SVM 分类器模型的最佳成本函数和核函数的重要性。在练习 27 中,我们学习了如何使用 scikit-learn 的网格搜索功能找到超参数的最佳组合。在本练习中,我们将演示如何使用网格搜索来找到最佳的成本函数和核函数组合。
从练习 28继续:
-
使用以下代码实例化要搜索的网格:
import numpy as np grid = {'C': np.linspace(1, 10, 10), 'kernel': ['linear', 'poly', 'rbf', 'sigmoid']}
-
使用
gamma
超参数设置为auto
来避免警告,并将概率设置为True
,以便我们可以提取雨的概率,如下所示:from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC model = GridSearchCV(SVC(gamma='auto'), grid, scoring='f1', cv=5)
-
使用
model.fit(X_train_scaled, y_train)
拟合网格搜索模型:图 3.23:拟合 SVC 网格搜索模型的输出
-
使用以下代码打印最佳参数:
best_parameters = model.best_params_ print(best_parameters)
参见下面的结果输出:
图 3.24:我们 SVC 网格搜索模型的调优超参数
一旦确定了超参数的最佳组合,就可以开始生成预测,并随后评估我们的模型在未见测试数据上的表现。
活动 7:生成预测并评估 SVC 网格搜索模型的性能
在之前的练习/活动中,我们学习了如何生成预测并评估分类器模型的性能。在本活动中,我们将再次评估模型的性能,通过生成预测、创建混淆矩阵并打印分类报告。
从练习 29 继续:
-
提取预测的类别。
-
创建并打印混淆矩阵。
-
生成并打印分类报告。
注意
本活动的解决方案可以在第 348 页找到。
在这里,我们展示了如何通过网格搜索调优 SVC 模型的超参数。调优后的 SVC 模型在预测雨/雪方面的表现不如调优后的逻辑回归模型。此外,SVC 模型是一个黑箱,因为它们无法提供特征对结果的贡献的洞察。在接下来的决策树部分,我们将介绍一种不同的算法——决策树,它采用“分而治之”的方法生成预测,并提供特征重要性属性,用于确定每个特征对结果的影响。
决策树
假设我们正在考虑换工作。我们正在权衡潜在工作机会的利弊,在当前职位待了几年后,我们开始意识到对我们来说重要的东西。然而,职业的各个方面并非都同等重要。事实上,在工作几年后,我们决定职位最重要的方面是我们对所做项目的兴趣,其次是薪酬,再然后是工作压力,接着是通勤时间,最后是福利。我们刚刚创建了一个认知决策树的框架。我们可以进一步详细说明,我们希望找到一份工作,其中分配的项目我们非常感兴趣,年薪至少为$55k,工作压力较低,通勤时间不超过 30 分钟,并且有良好的牙科保险。创建心理决策树是我们天生就会利用的一种决策过程,也是决策树成为当今最广泛使用的机器学习算法之一的原因。
在机器学习中,决策树使用基尼不纯度或熵信息增益作为衡量拆分质量的标准。首先,决策树算法确定能够最大化拆分质量值的特征。这被称为根节点,因为它是数据中最重要的特征。在前面提到的工作机会中,对潜在项目非常感兴趣会被视为根节点。考虑到根节点,工作机会被分为那些有非常有趣项目的和那些没有非常有趣项目的。
接下来,在考虑前一个特征的基础上,将这两个类别细分为下一个最重要的特征,以此类推,直到潜在工作被识别为感兴趣或不感兴趣。
这种方法被称为递归划分,或称为"分而治之",因为它持续不断地拆分和细分数据,直到算法判断数据子集已足够同质化,或者:
-
对应节点的几乎所有观察结果都属于同一类别(即纯度)。
-
数据中没有进一步的特征可供拆分。
-
树已达到事先决定的大小限制。
例如,如果纯度是通过熵来决定的,我们必须理解熵是衡量一组值内部随机性的指标。决策树通过选择最小化熵(随机性)的切分来运作,进而最大化信息增益。信息增益是通过计算切分与所有后续切分之间的熵差值来确定的。然后,计算总熵的方法是将每个分区的熵加权求和,权重是该分区中观察值的比例。幸运的是,scikit-learn 提供了一个函数来帮助我们完成这些操作。在接下来的练习和活动中,我们将实现决策树分类器模型,用以预测是否在下雨或下雪,使用熟悉的weather.csv
数据集。
活动 8:为决策树分类器准备数据
在本活动中,我们将为决策树分类器模型准备数据。执行以下步骤完成活动:
-
导入
weather.csv
并将其存储为 DataFrame -
为多层次的分类特征
Summary
编写虚拟代码 -
打乱数据,以去除可能的顺序效应
-
将数据拆分为特征和结果
-
进一步将特征和结果拆分为测试数据和训练数据
-
使用以下代码缩放
X_train
和X_test
:from sklearn.preprocessing import StandardScaler model = StandardScaler() X_train_scaled = model.fit_transform(X_train) X_test_scaled = model.transform(X_test)
注意
本活动的解决方案可以在第 349 页找到
在接下来的练习中,我们将学习如何调优和拟合决策树分类器模型。
练习 30:使用网格搜索调优决策树分类器
在本练习中,我们将实例化一个超参数空间,并使用网格搜索来调优决策树分类器的超参数。
继续进行活动 8,执行以下步骤:
-
按如下方式指定超参数空间:
import numpy as np grid = {'criterion': ['gini', 'entropy'], 'min_weight_fraction_leaf': np.linspace(0.0, 0.5, 10), 'min_impurity_decrease': np.linspace(0.0, 1.0, 10), 'class_weight': [None, 'balanced'], 'presort': [True, False]}Instantiate the GridSearchCV model
-
使用以下代码实例化网格搜索模型:
from sklearn.model_selection import GridSearchCV from sklearn.tree import DecisionTreeClassifier model = GridSearchCV(DecisionTreeClassifier(), grid, scoring='f1', cv=5)
-
使用以下方法拟合训练集:
model.fit(X_train_scaled, y_train)
查看此处显示的结果输出:
图 3.25:拟合我们的决策树分类器网格搜索模型的输出
-
打印调优后的参数:
best_parameters = model.best_params_ print(best_parameters)
查看下面的结果输出:
图 3.26:我们决策树分类器网格搜索模型的调优超参数
我们可以从图 3.26 中看到,它使用了gini不纯度作为切分质量的标准。关于超参数的进一步解释超出了本章范围,但可以在决策树分类器的 scikit-learn 文档中找到。
记住,在实际应用中,决策者常常会询问不同特征如何影响预测。在线性回归和逻辑回归中,截距和系数使得模型预测非常透明。
注意
决策树也非常容易解释,因为我们可以看到做出决策的地方,但这需要安装并正确配置 Graphviz,并且特征需要未经过缩放。
与接下来的练习不同,我们将探索 scikit-learn 树形模型算法中找到的一个属性 'feature_importances_
',该属性返回一个包含每个特征相对重要性值的数组。需要注意的是,这个属性在网格搜索模型中不可用。因此,在下一个练习中,我们将学习如何从 best_parameters
字典中程序化提取值,并重新拟合调优后的决策树模型,从而访问决策树分类器函数提供的属性。
练习 31:程序化地从决策树分类器网格搜索模型中提取调优后的超参数
在前一个练习中,我们将调优后的超参数保存为 best_parameters
字典中的键值对。这使我们能够以编程方式访问这些值并将它们分配给决策树分类器模型的相应超参数。通过拟合调优后的决策树模型,我们将能够访问 scikit-learn 决策树分类器函数提供的属性。
继续进行 练习 30,执行以下步骤:
-
证明我们可以通过以下方式访问 '
Tree_criterion
' 的值:print(best_parameters['criterion'])
在这里查看结果输出:
图 3.27:在
best_parameters
字典中分配给 'Tree_criterion' 键的值 -
实例化决策树分类器模型,并将相应的超参数值分配如下:
from sklearn.tree import DecisionTreeClassifier model = DecisionTreeClassifier(class_weight=best_parameters['class_weight'], criterion=best_parameters['criterion'], min_impurity_decrease=best_parameters['min_impurity_decrease'], min_weight_fraction_leaf=best_parameters['min_weight_fraction_leaf'], presort=best_parameters['presort'])
-
使用以下代码将网格搜索模型拟合到标准化的训练数据:
model.fit(X_train_scaled, y_train)
图 3.28:使用调优后的超参数拟合决策树分类器模型的输出
-
使用以下代码提取
feature_importances
属性:print(model.feature_importances_) The resultant output is shown below:
图 3.29:我们调优后的决策树分类器模型的特征重要性数组
从图 3.29 的数组中,我们可以看到第一个特征在特征重要性上完全主导了其他变量。
-
使用以下代码可视化:
import pandas as pd import matplotlib.pyplot as plt df_imp = pd.DataFrame({'Importance': list(model.feature_importances_)}, index=X.columns) df_imp_sorted = df_imp.sort_values(by=('Importance'), ascending=True) df_imp_sorted.plot.barh(figsize=(5,5)) plt.title('Relative Feature Importance') plt.xlabel('Relative Importance') plt.ylabel('Variable') plt.legend(loc=4) plt.show()
在这里查看结果输出:
图 3.30:调优后的决策树分类器模型的特征重要性
看起来摄氏温度是这个分类问题的唯一驱动因素。由于结果衡量标准是 rain ('Rain'=1)
或 snow ('Rain'=0)
,而决策树通过“分而治之”的方式做出划分决策,因此算法使用温度来判断测量时是否有降雨或降雪,这也有其合理性。在接下来的活动中,我们将评估模型的表现。
活动 9:生成预测并评估决策树分类器模型的性能
在之前的练习和活动中,我们已经生成了预测并评估了模型性能。在本次活动中,我们将采取相同的方法来评估我们调整过的决策树分类器模型的性能。
从第 31 次练习继续,执行以下步骤:
-
生成降水的预测概率。
-
生成降水的预测类别。
-
生成并打印混淆矩阵。
-
打印分类报告。
注意
本活动的解决方案可以在第 350 页找到。
你应该会发现只有一个观测值被误分类。因此,通过在我们的 weather.csv
数据集上调整决策树分类器模型,我们能够高精度地预测降雨(或降雪)。我们可以看到,唯一的驱动特征是摄氏温度。这是有道理的,因为决策树使用递归分割的方法来进行预测。
有时候,经过评估后,单一模型是一个弱学习器,表现不佳。然而,通过组合多个弱学习器,我们可以创建一个更强的学习器。将多个弱学习器组合成一个强学习器的方式称为集成。随机森林模型将多个决策树模型组合在一起,形成一个更强的集成模型。随机森林可以用于分类或回归问题。
随机森林
如前所述,随机森林是决策树的集成,可以用来解决分类或回归问题。随机森林使用数据的一小部分来拟合每棵树,因此能够处理非常大的数据集,而且相较于其他算法,它们对“维度灾难”的敏感性较低。维度灾难是指数据中有大量特征时,模型的性能会下降。随机森林的预测是通过将每棵树的预测结果结合起来得到的。与支持向量机(SVM)一样,随机森林是一个黑箱,其输入和输出无法解释。
在接下来的练习和活动中,我们将使用网格搜索调整并拟合随机森林回归器,以预测摄氏温度。然后,我们将评估模型的性能。
练习 32:为随机森林回归器准备数据
首先,我们将为随机森林回归器准备数据,使用 'Temperature_c
' 作为因变量,就像我们在第 21 次练习中做的那样:
-
导入 '
weather.csv
' 并使用以下代码将其保存为df
:import pandas as pd df = pd.read_csv('weather.csv')
-
对多类分类变量
Description
进行虚拟编码,方法如下:import pandas as pd df_dummies = pd.get_dummies(df, drop_first=True)
-
使用以下代码通过打乱
df_dummies
来去除可能的排序效应:from sklearn.utils import shuffle df_shuffled = shuffle(df_dummies, random_state=42)
-
使用以下代码将
df_shuffled
划分为X
和y
:DV = 'Temperature_c' X = df_shuffled.drop(DV, axis=1) y = df_shuffled[DV]
-
将
X
和y
按照如下方式划分为测试数据和训练数据:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
-
使用以下代码对
X_train
和X_test
进行缩放:from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)
现在,我们已经导入、打乱、将数据分为特征(X
)和因变量(y
),将X
和y
拆分为测试数据和训练数据,并对X_train
和X_test
进行缩放,我们将使用网格搜索调优随机森林回归模型。
活动 10:调优随机森林回归模型
数据已准备好用于随机森林回归模型。现在,我们必须设置超参数空间,并使用网格搜索找到超参数的最佳组合。
从练习 32 继续,执行以下步骤:
-
指定超参数空间。
-
实例化
GridSearchCV
模型以优化解释方差。 -
将网格搜索模型拟合到训练集。
-
打印调优后的参数。
注意
该活动的解决方案可以在第 351 页找到。
在对我们的随机森林回归超参数进行网格搜索后,我们需要使用调优后的超参数拟合一个随机森林回归模型。我们将编程提取best_parameters
字典中的值,并将它们分配给随机森林回归函数中的相应超参数,以便我们可以访问来自该函数的属性。
练习 33:从随机森林回归网格搜索模型中编程提取调优的超参数并确定特征重要性
通过从best_parameters
字典中的键值对提取值,我们消除了手动错误的可能性,并且使我们的代码更加自动化。在本练习中,我们将复制练习 31中的步骤,但将代码调整为适应随机森林回归模型。
从活动 10继续,执行以下步骤:
-
实例化一个随机森林回归模型,将
best_parameters
字典中每个键的值分配给相应的超参数:from sklearn.ensemble import RandomForestRegressor model = RandomForestRegressor(criterion=best_parameters['criterion'], max_features=best_parameters['max_features'], min_impurity_decrease=best_parameters['min_impurity_decrease'], bootstrap=best_parameters['bootstrap'], warm_start=best_parameters['warm_start'])
-
使用以下代码在训练数据上拟合模型:
model.fit(X_train_scaled, y_train)
在此处查找结果输出:
图 3.31:使用调优超参数拟合随机森林回归模型的输出
-
使用以下代码按降序绘制特征重要性:
import pandas as pd import matplotlib.pyplot as plt df_imp = pd.DataFrame({'Importance': list(model.feature_importances_)}, index=X.columns) df_imp_sorted = df_imp.sort_values(by=('Importance'), ascending=True) df_imp_sorted.plot.barh(figsize=(5,5)) plt.title('Relative Feature Importance') plt.xlabel('Relative Importance') plt.ylabel('Variable') plt.legend(loc=4) plt.show()
在此处查看结果输出:
图 3.32:来自调优超参数的随机森林回归模型的特征重要性
从图 3.32 中,我们可以看到'Description_Warm
'虚拟变量和'Humidity
'是摄氏温度的主要驱动因素。与此同时,'Visibility_km
'和'Wind_Bearing_degrees
'对温度的影响较小。接下来,我们将检查我们的模型在测试数据上的表现。
活动 11:生成预测并评估调优后的随机森林回归模型的性能
在练习 23和活动 5中,我们学习了如何生成预测并评估回归模型在预测连续结果时的性能。在本次活动中,我们将采用相同的方法来评估我们随机森林回归模型在预测摄氏温度时的性能。
继续执行练习 33中的步骤,进行以下操作:
-
在测试数据上生成预测。
-
绘制预测值与实际值的相关性图。
-
绘制残差的分布图。
-
计算指标,然后将其放入数据框并打印出来。
注意
这个活动的解决方案可以在第 352 页找到。
随机森林回归模型似乎表现不如多元线性回归,这一点通过更大的 MAE、MSE 和 RMSE 值以及较低的解释方差得到了证实。此外,预测值与实际值之间的相关性较弱,残差也远未呈正态分布。然而,通过利用随机森林回归器的集成方法,我们构建了一个模型,该模型解释了 75.8%的温度方差,并预测摄氏温度±3.781 度。
总结
本章介绍了 Python 的开源机器学习库 scikit-learn。你学习了如何预处理数据,以及如何调整和拟合几种不同的回归和分类算法。最后,你学习了如何快速有效地评估分类和回归模型的性能。这是对 scikit-learn 库的全面介绍,在这里使用的策略可以应用于构建 scikit-learn 提供的众多其他算法。
在下一章,你将学习降维和无监督学习。
第十四章:第四章
降维和无监督学习
学习目标
在本章结束时,您将能够:
-
比较层次聚类分析(HCA)和 k-means 聚类
-
进行层次聚类分析(HCA)并解读输出结果
-
调整 k-means 聚类的聚类数
-
选择一个最佳的主成分数进行降维
-
使用线性判别分析(LDA)进行监督式降维
本章将介绍降维和无监督学习下的各种概念。
介绍
在无监督学习中,描述性模型用于探索性分析,以揭示未标记数据中的模式。无监督学习任务的例子包括聚类算法和降维算法。在聚类中,观察值被分配到组中,其中组内同质性高而组间异质性大。简而言之,观察值被归类到与其他非常相似的观察值的样本群体中。聚类算法的应用场景广泛。例如,分析师通过根据顾客的购物行为将顾客分开,进而为特定顾客群体定向营销广告和促销活动,从而提高销售额。
注意
此外,层次聚类已被应用于学术神经科学和运动行为研究(https://www.researchgate.net/profile/Ming-Yang_Cheng/project/The-Effect-of-SMR-Neurofeedback-Training-on-Mental-Representation-and-Golf-Putting-Performance/attachment/57c8419808aeef0362ac36a5/AS:401522300080128@1472741784217/download/Schack+-+2012+-+Measuring+mental+representations.pdf?context=ProjectUpdatesLog),而 k-means 聚类已被用于欺诈检测(https://www.semanticscholar.org/paper/Fraud-Detection-in-Credit-Card-by-Clustering-Tech/3e98a9ac78b5b89944720c2b428ebf3e46d9950f)。
然而,在构建描述性或预测性模型时,确定哪些特征应纳入模型以提高模型效果,以及哪些特征应排除因为它们会削弱模型,可能是一个挑战。特征过多可能会导致问题,因为模型中变量的数量越多,多重共线性和模型过拟合的可能性就越高。此外,过多的特征增加了模型的复杂性,并延长了模型调优和拟合的时间。
当数据集变大时,这会变得更加棘手。幸运的是,另一种无监督学习的应用场景是通过创建原始特征的组合来减少数据集中的特征数量。减少数据中的特征数量有助于消除多重共线性,并汇聚出一组特征组合,从而生成一个在未见测试数据上表现良好的模型。
注意
多重共线性是指至少有两个变量之间存在相关性。这在回归模型中是一个问题,因为它无法孤立地描述每个独立变量与结果变量之间的关系。因此,系数和 p 值变得不稳定,且精确度较低。
在本章中,我们将介绍两种广泛使用的无监督聚类算法:层次聚类分析(HCA)和k 均值聚类。此外,我们还将探索使用主成分分析(PCA)进行降维,并观察降维如何改善模型性能。最后,我们将实现线性判别函数分析(LDA)用于监督式降维。
层次聚类分析(HCA)
层次聚类分析(HCA)最适用于用户不知道预先聚类数量的情况。因此,HCA 常作为其他聚类技术的前驱方法,其中建议使用预定数量的聚类。HCA 的工作原理是将相似的观测值合并成聚类,并继续合并距离最近的聚类,直到所有观测值合并为一个单一的聚类。
HCA 通过计算观测值之间的欧几里得距离来确定相似性,并根据两个点之间的距离建立链接。
使用特征数* n *表示,欧几里得距离的计算公式如下:
图 4.1: 欧几里得距离
在计算了观测值和聚类之间的距离后,通过树状图显示所有观测值之间的关系。树状图是类似树的结构,通过水平线表示链接之间的距离。
Thomas Schack 博士(链接)将这种结构与人脑相联系,在人脑中,每个观测值都是一个节点,观测值之间的链接则是神经元。
这创建了一个层次结构,其中关系紧密的项目会被“打包”到一起,形成聚类。这里展示了一个示例树状图:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_04_02.jpg)
图 4.2: 一个示例树状图
y 轴表示欧几里得距离,x 轴表示每个观察值的行索引。水平线表示观察值之间的连接,越接近 x 轴的连接表示越短的距离及其更紧密的关系。在此示例中,似乎有三个聚类。第一个聚类包含绿色标记的观察值,第二个聚类包含红色标记的观察值,第三个聚类包含青绿色标记的观察值。
练习 34:构建 HCA 模型
为了演示 HCA,我们将使用加利福尼亚大学欧文分校(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter04)提供的适配版本的玻璃数据集。该数据集包含 218 个观察值和 9 个特征,对应于玻璃中各种氧化物的质量百分比:
-
RI:折射率
-
Na:钠的质量百分比
-
Mg:镁的质量百分比
-
Al:铝的质量百分比
-
Si:硅的质量百分比
-
K:钾的质量百分比
-
Ca:钙的质量百分比
-
Ba:钡的质量百分比
-
Fe:铁的质量百分比
在这个练习中,我们将使用折射率(RI)和每种氧化物的质量百分比来对玻璃类型进行分段。
-
首先,我们将导入 pandas 并使用以下代码读取
glass.csv
文件:import pandas as pd df = pd.read_csv('glass.csv')
-
通过使用以下代码打印
df.info()
到控制台,查看一些基本的 DataFrame 信息:print(df.info()):
图 4.3:DataFrame 信息
-
为了去除数据中的可能的顺序效应,我们将在构建任何模型之前打乱行,并将其保存为新的数据框对象,如下所示:
from sklearn.utils import shuffle df_shuffled = shuffle(df, random_state=42)
-
通过拟合并转换打乱后的数据,使用以下方法将每个观察值转换为 z 分数:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() scaled_features = scaler.fit_transform(df_shuffled)
-
使用
scaled_features
执行分层聚类,使用连接函数。以下代码将向你展示如何操作:from scipy.cluster.hierarchy import linkage model = linkage(scaled_features, method='complete')
恭喜!你已成功构建 HCA 模型。
练习 35:绘制 HCA 模型并分配预测标签
现在 HCA 模型已经构建完成,我们将继续分析,使用树状图可视化聚类,并利用该可视化生成预测。
-
通过绘制连接模型来显示树状图,如下所示:
import matplotlib.pyplot as plt from scipy.cluster.hierarchy import dendrogram plt.figure(figsize=(10,5)) plt.title('Dendrogram for Glass Data') dendrogram(model, leaf_rotation=90, leaf_font_size=6) plt.show()
图 4.4:玻璃数据的树状图
注意
数据集中的每个观察值或行的索引在 x 轴上。欧几里得距离在 y 轴上。水平线表示观察值之间的连接。默认情况下,scipy 会为它找到的不同聚类进行颜色编码。
现在我们已经得到了预测的观察值聚类,我们可以使用
fcluster
函数生成一个标签数组,该数组与df_shuffled
中的行对应。 -
使用以下代码生成预测的标签,表示一个观察值属于哪个聚类:
from scipy.cluster.hierarchy import fcluster labels = fcluster(model, t=9, criterion='distance')
-
使用以下代码将标签数组作为一列添加到打乱的数据中,并预览前五行:
df_shuffled['Predicted_Cluster'] = labels print(df_shuffled.head(5))
-
查看下图中的输出:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_04_05.jpg)
图 4.5:在预测与观察值匹配后,df_shuffled 的前五行数据。
我们已经成功地学习了监督学习与无监督学习的区别,如何构建 HCA 模型,如何可视化和解释 HCA 的树状图,以及如何将预测的聚类标签分配给相应的观察值。
在这里,我们使用 HCA 将数据分成了三组,并将观察值与其预测的聚类匹配起来。HCA 模型的一些优点包括:
-
它们容易构建
-
无需提前指定聚类的数量
-
可视化结果容易理解
然而,HCA 的一些缺点包括:
-
终止标准的模糊性(即何时最终确定聚类数量)
-
一旦做出聚类决策,算法无法进行调整
-
在大数据集且特征较多的情况下,构建 HCA 模型可能会非常耗费计算资源
接下来,我们将为您介绍另一种聚类算法——K-means 聚类。该算法通过能够在初始生成聚类后进行调整,解决了 HCA 的一些不足。它比 HCA 更加节省计算资源。
K-means 聚类
与 HCA 相似,K-means 也使用距离将未标记数据的观察值分配到聚类中。然而,与 HCA 将观察值相互连接不同,K-means 会将观察值分配到 k(用户定义数量)个聚类中。
为了确定每个观察值所属的聚类,K-means 会随机生成聚类中心,并将观察值分配到其欧几里得距离与聚类中心最接近的聚类中。类似于人工神经网络中的起始权重,聚类中心是随机初始化的。在聚类中心被随机生成后,算法分为两个阶段:
-
指派阶段
-
更新阶段
注意
随机生成的聚类中心非常重要,我们将在本章稍后进行讨论。有人认为这种随机生成聚类中心的方式是算法的弱点,因为在对相同数据拟合相同模型时,结果会有所不同,且无法确保将观察值分配到合适的聚类。我们可以通过利用循环的强大功能将其转化为优势。
在指派阶段,观察值会被分配到距离它最近的聚类,如下图所示:
图 4.6:观察值的散点图以及用星号、三角形和菱形表示的聚类中心。
接下来,在更新阶段,聚类中心会移动到该聚类中各点的均值位置。这些聚类均值被称为质心,如下图所示:
图 4.7:聚类中心向质心移动。
然而,一旦计算出质心,由于某些观察比以前的聚类中心更接近新的质心,这些观察将被重新分配到不同的聚类。因此,模型必须再次更新其质心。此过程在下图中展示:
图 4.8:观察重新分配后质心的更新。
更新质心的过程持续进行,直到没有进一步的观察重新分配。最终的质心如下图所示:
图 4.9:最终质心位置和聚类分配。
使用与练习 34中相同的玻璃数据集,构建 HCA 模型,我们将拟合一个具有用户定义聚类数量的 K-Means 模型。接下来,由于质心的选择具有随机性,我们将通过构建一个具有给定聚类数量的 K-Means 模型集成,并将每个观察分配给预测聚类的众数,从而提高我们预测的可靠性。之后,我们将通过监控平均惯性(或聚类内平方和)随聚类数量的变化,来调节最佳的聚类数,并找出增加聚类数时,惯性减少的临界点。
练习 36:拟合 K-Means 模型并分配预测
由于我们的数据已经准备好(请参见练习 34,构建 HCA 模型),并且我们理解 K-Means 算法背后的概念,我们将学习如何轻松地拟合 K-Means 模型,生成预测,并将这些预测分配到相应的观察。
在导入、打乱和标准化玻璃数据集后:
-
使用以下代码实例化一个 KMeans 模型,假设聚类数为任意数,这里为两个聚类:
from sklearn.cluster import KMeans model = KMeans(n_clusters=2)
-
使用以下代码行将模型拟合到
scaled_features
:model.fit(scaled_features)
-
使用以下代码将模型的聚类标签保存到数组 labels 中:
labels = model.labels_
-
生成标签的频率表:
import pandas as pd pd.value_counts(labels)
为了更好地理解,请参见以下截图:
图 4.10:两个聚类的频率表
使用两个聚类,将 61 个观察分配到第一个聚类,157 个观察分配到第二个聚类。
-
使用以下代码将标签数组作为 '
预测聚类
' 列添加到df_shuffled
数据框中,并预览前五行:df_shuffled['Predicted_Cluster'] = labels print(df_shuffled.head(5))
-
查看以下图中的输出:
图 4.11:df_shuffled
的前五行
活动 12:集成 K-Means 聚类与预测计算
当算法使用随机性作为寻找最佳解决方案的一部分时(即在人工神经网络和 k-means 聚类中),在相同数据上运行相同的模型可能会得出不同的结论,从而限制我们对预测结果的信心。因此,建议多次运行这些模型,并使用所有模型的汇总度量(即均值、中位数和众数)生成预测。在本活动中,我们将构建 100 个 k-means 聚类模型的集成。
在导入、打乱和标准化玻璃数据集之后(请参见 练习 34,构建 HCA 模型):
-
实例化一个空数据框架,以便为每个模型附加标签,并将其保存为新的数据框架对象
labels_df
。 -
使用 for 循环,迭代 100 个模型,在每次迭代时将预测标签作为新列附加到
labels_df
中。计算labels_df
中每行的众数,并将其作为新列保存到labels_df
中。输出应如下所示:
图 4.12:labels_df 的前五行
注意
本活动的解答可以在第 356 页找到。
通过迭代多个模型并在每次迭代中保存预测结果,我们极大地提高了对预测结果的信心,并将最终预测结果作为这些预测的众数。然而,这些预测是由使用预定聚类数的模型生成的。除非我们事先知道聚类数,否则我们需要发现最佳的聚类数来分割我们的观测数据。
练习 37:通过 n_clusters 计算平均惯性
k-means 算法通过最小化簇内平方和(或惯性)来将观测数据分组到不同的簇中。因此,为了提高我们对 k-means 模型的聚类数调优的信心,我们将把在 活动 12,集成 k-means 聚类与计算预测 中创建的循环(经过少许调整)放入另一个循环中,后者将迭代一个 n_clusters
范围。这会创建一个嵌套循环,迭代 10 个 n_clusters
的可能值,并在每次迭代时构建 100 个模型。在 100 次内层迭代中的每一次中,模型惯性将被计算出来。对于 10 次外层迭代中的每一次,将计算 100 个模型的平均惯性,从而得出每个 n_clusters
值的平均惯性值。
在导入、打乱和标准化玻璃数据集之后(请参见 练习 34,构建 HCA 模型):
-
如下所示,在循环外部导入我们所需的包:
from sklearn.cluster import KMeans import numpy as np
-
从内到外构建和理解嵌套循环会更容易。首先,实例化一个空列表
inertia_list
,我们将在内部循环的每次迭代后将惯性值附加到该列表,如下所示:inertia_list = []
-
在 for 循环中,我们将使用以下代码迭代 100 个模型:
for i in range(100):
-
在循环内部,构建一个
KMeans
模型,使用n_clusters=x
,如下所示:model = KMeans(n_clusters=x)
注意
x 的值由外部 for 循环确定,我们还没有讲解这个部分,但很快会详细介绍。
-
如下所示,将模型拟合到
scaled_features
:model.fit(scaled_features)
-
获取惯性值并将其保存为对象惯性,如下所示:
inertia = model.inertia_
-
使用以下代码将惯性添加到
inertia_list
中:inertia_list.append(inertia)
-
移动到外部循环,实例化另一个空列表来存储平均惯性值,如下所示:
mean_inertia_list = []
-
使用以下代码循环遍历
n_clusters
从 1 到 10 的值:for x in range(1, 11):
-
在内部 for 循环运行完 100 次迭代,并将 100 个模型的惯性值添加到
inertia_list
后,计算该列表的均值并保存为对象mean_inertia
,如下面所示:mean_inertia = np.mean(inertia_list)
-
如下所示,将
mean_inertia
添加到mean_inertia_list
中:mean_inertia_list.append(mean_inertia)
-
在完成 100 次迭代并进行 10 次,合计 1000 次迭代后,
mean_inertia_list
包含 10 个值,这些值是每个n_clusters
值的平均惯性值。 -
按如下代码打印
mean_inertia_list
。这些值在下图中显示:print(mean_inertia_list)
图 4.13:mean_inertia_list
练习 38:按 n_clusters 绘制均值惯性
继续进行练习 38:
现在我们已经为每个 n_clusters
值生成了 100 个模型的均值惯性,接下来我们将按 n_clusters
绘制均值惯性。然后,我们将讨论如何从视觉上评估选择 n_clusters
的最佳值。
-
首先,按如下方式导入 matplotlib:
import matplotlib.pyplot as plt
-
创建一个数字列表并将其保存为对象 x,以便在 x 轴上绘制,如下所示:
x = list(range(1, len(mean_inertia_list)+1))
-
将
mean_inertia_list
保存为对象 y,如下所示:y = mean_inertia_list
-
如下所示,按聚类数量绘制均值惯性:
plt.plot(x, y)
-
使用以下代码将图表标题设置为 '
Mean Inertia by n_clusters
':plt.title('Mean Inertia by n_clusters')
-
使用
plt.xlabel('n_clusters')
将 x 轴标签标记为 'n_clusters
',使用以下代码将 y 轴标签标记为 'Mean Inertia
':plt.ylabel ('Mean Inertia')
-
使用以下代码将 x 轴的刻度标签设置为 x 中的值:
plt.xticks(x)
-
使用
plt.show()
显示图表。为了更好地理解,参见以下代码:plt.plot(x, y) plt.title('Mean Inertia by n_clusters') plt.xlabel('n_clusters') plt.xticks(x) plt.ylabel('Mean Inertia') plt.show()
对于结果输出,请参见以下截图:
图 4.14:按 n_clusters 计算的均值惯性
为了确定最佳的 n_clusters
数量,我们将使用“肘部法则”。即在图中,随着聚类数量增加,新增聚类带来的复杂度增加,而均值惯性减少的幅度逐渐减缓。从图 4.14 中可以看出,从 n_clusters
为 1 到 3 时,均值惯性急剧下降。而当 n_clusters
等于 3 后,均值惯性的下降似乎变得缓慢,并且惯性减少可能不足以抵消增加额外聚类的复杂性。因此,在这种情况下,合适的 n_clusters
数量是 3。
然而,如果数据的维度过多,k-means 算法可能会受到维度灾难的影响,因为欧几里得距离膨胀,导致结果错误。因此,在拟合 k-Means 模型之前,建议使用降维策略。
降低维度有助于消除多重共线性,并减少拟合模型的时间。主成分分析(PCA)是一种常见的降维方法,通过发现数据中一组潜在的线性变量来减少维度。
主成分分析(PCA)
从高层次来看,PCA 是一种通过原始特征创建不相关线性组合的技术,这些组合被称为主成分。在主成分中,第一个成分解释了数据中最大比例的方差,而后续的成分则逐渐解释较少的方差。
为了演示 PCA,我们将:
-
使用所有主成分拟合 PCA 模型
-
通过设置解释方差的阈值来调整主成分的数量,以便保留数据中的信息。
-
将这些成分拟合到 k-means 聚类分析中,并比较 PCA 转换前后 k-means 的性能
练习 39:拟合 PCA 模型
在本练习中,您将学习如何使用我们在练习 34:构建 HCA 模型中准备的数据和 PCA 简要说明来拟合一个通用的 PCA 模型。
-
按照以下方式实例化 PCA 模型:
from sklearn.decomposition import PCA model = PCA()
-
将 PCA 模型拟合到
scaled_features
,如下代码所示:model.fit(scaled_features)
-
获取数据中每个主成分的解释方差比例,将数组保存为对象
explained_var_ratio
,并将值打印到控制台,如下所示:explained_var_ratio = model.explained_variance_ratio_ print(explained_var_ratio)
-
对于结果输出,请参阅以下屏幕截图:
图 4.15:每个主成分的数据解释方差
每个主成分解释了数据中的一部分方差。在本练习中,第一个主成分解释了数据中 35%的方差,第二个主成分解释了 25%,第三个主成分解释了 13%,依此类推。总的来说,这九个成分解释了数据中 100%的方差。降维的目标是减少数据中的维度,以限制过拟合和后续模型拟合的时间。因此,我们不会保留所有九个成分。然而,如果我们保留的成分太少,数据中的解释方差比例将很低,后续模型将出现欠拟合。因此,数据科学家的挑战在于确定最小化过拟合和欠拟合的n_components
数量。
练习 40:使用解释方差阈值选择 n_components
在练习 39,拟合 PCA 模型中,你学会了用所有可用的主成分拟合 PCA 模型。然而,保留所有主成分并不会减少数据的维度。在本练习中,我们将通过保留解释一定方差阈值的主成分来减少数据的维度。
-
通过计算每个主成分解释的方差的累计和,确定最少 95%数据方差由多少个主成分解释。让我们看以下代码,看看它是如何实现的:
import numpy as np cum_sum_explained_var = np.cumsum(model.explained_variance_ratio_) print(cum_sum_explained_var)
对于结果输出,请参考以下截图:
图 4.16:每个主成分的解释方差的累计和
-
将数据中保持的方差百分比阈值设置为 95%,如下所示:
threshold = .95
-
使用这个阈值,我们将遍历累计解释方差的列表,看看它们是否解释了数据中不少于 95%的方差。由于我们将循环遍历
cum_sum_explained_var
的索引,因此我们将使用以下方式实例化我们的循环:for i in range(len(cum_sum_explained_var)):
-
检查
cum_sum_explained_var
中的项是否大于或等于 0.95,如下所示:if cum_sum_explained_var[i] >= threshold:
-
如果满足该逻辑,则我们将在该索引上加 1(因为我们不能有 0 个主成分),将值保存为一个对象,并退出循环。为此,我们将在 if 语句中使用
best_n_components = i+1
,并在下一行执行 break。看看以下代码,了解如何操作:best_n_components = i+1 break
if 语句中的最后两行指示循环如果逻辑不满足时不做任何操作:
else: pass
-
使用以下代码打印一条消息,详细说明最佳的主成分数量:
print('The best n_components is {}'.format(best_n_components))
查看上一行代码的输出:
图 4.17:显示组件数量的输出信息
best_n_components
的值为 6。我们可以用n_components = 6
重新拟合一个 PCA 模型,将数据转换为主成分,并在新的 k-means 模型中使用这些主成分来降低惯性值。此外,我们可以将使用 PCA 变换后的数据构建的模型与使用未经 PCA 变换的数据构建的模型在n_clusters
值上的惯性值进行比较。
活动 13:通过 PCA 变换后按聚类评估平均惯性
现在我们知道了保留至少 95%数据方差的主成分数量,知道如何将特征转换为主成分,并且有了一个方法来通过嵌套循环调整 k-means 聚类的最优聚类数量,接下来我们将在这个活动中将它们整合起来。
接着进行练习 40:
-
使用
n_components
参数等于best_n_components
的值实例化 PCA 模型(也就是说,记住,best_n_components = 6
)。 -
将模型拟合到
scaled_features
并将其转换为前六个主成分 -
使用嵌套循环,计算在
n_clusters
值从 1 到 10 之间的 100 个模型的均值惯性(参见练习 40,使用方差解释阈值选择 n_components)。
图 4.18:mean_inertia_list_PCA
现在,就像在练习 38中所做的那样,按 n_clusters 绘制均值惯性,我们有每个n_clusters
值(1 到 10)的均值惯性值。然而,mean_inertia_list_PCA
包含 PCA 变换后每个n_clusters
值的均值惯性值。但是,我们如何知道 PCA 变换后 k-means 模型的表现是否更好呢?在下一个练习中,我们将视觉对比每个n_clusters
值下 PCA 变换前后的均值惯性。
注意
本活动的解答可以在第 357 页找到。
练习 41:按 n_clusters 进行惯性视觉对比
为了视觉对比 PCA 变换前后的均值惯性,我们将稍微修改在练习 38,按 n_clusters 绘制均值惯性中创建的图表,方法如下:
-
向图表中添加一条显示 PCA 变换后按
n_clusters
绘制的均值惯性曲线 -
创建一个图例来区分各条线
-
更改标题
注意
为了让这个可视化正常工作,练习 38中的
mean_inertia_list
,按 n_clusters 绘制均值惯性,必须仍然存在于环境中。
继续进行活动 13:
-
使用以下代码导入 Matplotlib:
import matplotlib.pyplot as plt
-
创建一个数字列表并将其保存为对象 x,以便我们可以在 x 轴上绘制,方法如下:
x = list(range(1,len(mean_inertia_list_PCA)+1))
-
使用以下代码将
mean_inertia_list_PCA
保存为对象 y:y = mean_inertia_list_PCA
-
使用以下代码将
mean_inertia_list
保存为对象 y2:y2 = mean_inertia_list
-
使用以下代码按聚类数绘制 PCA 变换后的均值惯性:
plt.plot(x, y, label='PCA')
使用以下方法在 PCA 变换之前,按聚类数添加我们的第二条均值惯性线:
plt.plot(x, y2, label='No PCA)
-
将图表标题设置为'
Mean Inertia by n_clusters for Original Features and PCA Transformed Features
',如下所示:plt.title('Mean Inertia by n_clusters for Original Features and PCA Transformed Features')
-
使用以下代码将 x 轴标签标记为'
n_clusters
':plt.xlabel('n_clusters')
-
使用以下代码将 y 轴标签标记为'
Mean Inertia
':plt.ylabel('Mean Inertia')
-
使用
plt.xticks(x)
将 x 轴的刻度标签设置为 x 中的值。 -
使用并显示图表如下所示,显示图例:
plt.legend() plt.show()
图 4.19:原始特征(橙色)和 PCA 变换特征(蓝色)按 n_clusters 的均值惯性
从图表中,我们可以看到,在模型中使用 PCA 转换后的特征时,每个聚类数的惯性较低。这表明,在 PCA 转换后,相比于转换前,各聚类的组质心与观测值之间的距离更小。因此,通过对原始特征进行 PCA 转换,我们能够减少特征数量,并同时通过减少组内平方和(即惯性)来改进我们的模型。
HCA 和 k-means 聚类是两种广泛使用的无监督学习技术,用于数据分割。PCA 可用于帮助减少数据维度,并以无监督的方式改进模型。而线性判别函数分析(LDA)则是一种监督方法,通过数据压缩来减少维度。
使用线性判别分析(LDA)进行监督数据压缩
如前所述,PCA 将特征转换为一组最大化特征间方差的变量。在 PCA 中,输出标签在拟合模型时不被考虑。与此同时,LDA 使用因变量来帮助将数据压缩成能最好区分结果变量类别的特征。在这一部分中,我们将演示如何使用 LDA 作为监督数据压缩技术。
为了演示如何使用 LDA 作为监督的降维压缩技术,我们将:
-
使用所有可能的
n_components
拟合一个 LDA 模型 -
将我们的特征转换为
n_components
-
调整
n_components
的数量
练习 42:拟合 LDA 模型
为了使用 LDA 算法的默认参数将模型作为监督学习者拟合,我们将使用一个稍有不同的玻璃数据集,glass_w_outcome.csv
。(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter04)此数据集包含与玻璃数据集相同的九个特征,但还包括一个结果变量 Type,表示玻璃的类型。Type 被标记为 1、2 和 3,分别对应建筑窗户浮法处理、建筑窗户非浮法处理和车灯。
-
导入
glass_w_outcome.csv
文件,并使用以下代码将其保存为对象 df:import pandas as pd df = pd.read_csv(‘glass_w_outcome.csv’)
-
打乱数据以消除任何顺序效应,并将其保存为数据框
df_shuffled
,如下所示:from sklearn.utils import shuffle df_shuffled = shuffle(df, random_state=42)
-
将
Type
保存为DV
(即,因变量),如下所示:DV = ‘Type’
-
使用
X = df_shuffled.drop(DV, axis=1)
和y = df_shuffled[DV]
将打乱后的数据拆分为特征(即 X)和结果(即 y)。 -
按照如下方式将 X 和 y 拆分为测试集和训练集:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
-
使用以下代码分别对
X_train
和X_test
进行缩放:from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.fit_transform(X_test)
-
实例化 LDA 模型并将其保存为模型。以下将向您展示如何操作。
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis -model = LinearDiscriminantAnalysis()
注意
通过实例化一个不带参数
for n_components
的 LDA 模型,我们将返回所有可能的成分。 -
使用以下代码将模型拟合到训练数据:
model.fit(X_train_scaled, y_train)
-
请参见下面的结果输出:![图 4.20:线性判别函数分析的拟合结果输出]
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_04_20.jpg)
图 4.20:拟合线性判别分析函数后的输出
-
与 PCA 类似,我们可以返回每个组件解释的方差百分比。
model.explained_variance_ratio_
输出如下图所示。
图 4.21:按组件解释的方差。
注意
第一个组件解释了数据中 95.86% 的方差,第二个组件解释了数据中 4.14% 的方差,总共为 100%。
我们成功地将 LDA 模型拟合到数据中,将数据从九个特征压缩到两个特征。将特征减少到两个可以减少调优和拟合机器学习模型的时间。然而,在将这些特征应用于分类器模型之前,我们必须将训练和测试特征转换为其两个组成部分。在下一个练习中,我们将展示如何实现这一过程。
练习 43:在分类模型中使用 LDA 转换的组件
通过监督式数据压缩,我们将把训练和测试特征(即 X_train_scaled
和 X_test_scaled
)转换为其组成部分,并在其上拟合 RandomForestClassifier
模型。
继续进行练习 42:
-
将
X_train_scaled
压缩为其组成部分,如下所示:X_train_LDA = model.transform(X_train_scaled)
-
使用以下方式将
X_test
压缩为其组成部分:X_test_LDA = model.transform(X_test_scaled)
-
按如下方式实例化一个
RandomForestClassifier
模型:from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier()
注意
我们将使用
RandomForestClassifier
模型的默认超参数,因为超参数调优超出了本章的范围。 -
使用以下代码将模型拟合到压缩后的训练数据:
model.fit(X_train_LDA, y_train)
请参见下方的结果输出:
图 4.22:拟合随机森林分类器模型后的输出
-
使用以下代码在
X_test_LDA
上生成预测并将其保存为数组 predictions:predictions = model.predict(X_test_LDA)
-
通过将预测与
y_test
进行比较,使用混淆矩阵评估模型性能。要生成并打印混淆矩阵,请参见以下代码:from sklearn.metrics import confusion_matrix import pandas as pd import numpy as np cm = pd.DataFrame(confusion_matrix(y_test, predictions)) cm[‘Total’] = np.sum(cm, axis=1) cm = cm.append(np.sum(cm, axis=0), ignore_index=True) cm.columns = [‘Predicted 1’, ‘Predicted 2’, ‘Predicted 3’, ‘Total’] cm = cm.set_index([[‘Actual 1’, ‘Actual 2’, ‘Actual 3’, ‘Total’]]) print(cm)
输出如下图所示:
图 4.23:使用 LDA 压缩数据评估随机森林分类器模型性能的 3x3 混淆矩阵
总结
本章向你介绍了两种广泛使用的无监督聚类算法——HCA 和 k-means 聚类。在学习 k-means 聚类时,我们利用循环的力量创建了多个模型集,以调节聚类的数量,并提高我们预测的可靠性。在 PCA 部分,我们确定了用于降维的主成分数量,并将这些成分拟合到 k-means 模型中。此外,我们比较了 PCA 转换前后 k-means 模型性能的差异。我们还介绍了一种算法——LDA,它以监督方式减少维度。最后,我们通过遍历所有可能的成分值,并通过编程返回使得随机森林分类器模型获得最佳准确度得分的值,来调节 LDA 中的成分数量。现在,你应该已经对降维和无监督学习技术感到得心应手。
本章简要介绍了如何创建图表;然而,在下一章,我们将学习结构化数据以及如何使用 XGboost 和 Keras 库。
第十五章:第五章
掌握结构化数据
学习目标
在本章结束时,你将能够:
-
使用结构化数据创建高精度模型
-
使用 XGBoost 库训练提升模型
-
使用 Keras 库训练神经网络模型
-
微调模型参数以获得最佳准确性
-
使用交叉验证
-
保存并加载你的训练模型
本章将介绍如何创建高精度的结构化数据模型的基础知识。
简介
数据主要分为两种类型:结构化数据和非结构化数据。结构化数据指的是具有定义格式的数据,通常以表格形式存储,例如存储在 Excel 表格或关系型数据库中的数据。非结构化数据没有预定义的模式,任何无法存储在表格中的数据都属于这一类。例如语音文件、图片和 PDF 文件等。
在本章中,我们将重点讨论结构化数据,并使用 XGBoost 和 Keras 创建机器学习模型。XGBoost 算法因其能够快速交付高精度模型以及其分布式特性而被业界专家和研究人员广泛使用。分布式特性指的是能够并行处理数据和训练模型;这使得训练速度更快,数据科学家的周转时间更短。另一方面,Keras 让我们能够创建神经网络模型。在某些情况下,神经网络的效果远胜于提升算法,但找到合适的网络和网络配置是非常困难的。以下主题将帮助你熟悉这两个库,确保你能应对数据科学旅程中的任何结构化数据。
提升算法
提升方法是一种提高任何学习算法准确性的方式。提升方法通过将粗略的、高层次的规则结合成一个比任何单一规则更准确的预测。它通过迭代地将训练数据集的一个子集输入到一个“弱”算法中来生成一个弱模型。这些弱模型随后被结合在一起,形成最终的预测。两种最有效的提升算法是梯度提升机和 XGBoost。
梯度提升机(GBM)
GBM 利用分类树作为弱算法。结果通过使用可微分的损失函数来改进这些弱模型的估算。该模型通过考虑前一棵树的净损失来拟合连续的树;因此,每棵树都部分地参与了最终的解决方案。因此,提升树会降低算法的速度,但它们提供的透明度能带来更好的结果。GBM 算法有很多参数,而且对噪声和极端值非常敏感。同时,GBM 会发生过拟合,因此需要一个合适的停止点,但它通常是最佳的模型。
XGBoost(极端梯度提升)
XGBoost 是全球研究人员在构建结构化数据模型时的首选算法。XGBoost 同样使用树作为弱学习算法。那么,为什么当数据科学家看到结构化数据时,XGBoost 是他们首先想到的算法呢?XGBoost 是可移植和分布式的,这意味着它可以在不同的架构中轻松使用,并且可以利用多个核心(单台机器)或多台机器(集群)。作为额外的好处,XGBoost 库是用 C++编写的,这使它非常快速。当处理大数据集时,它特别有用,因为它允许将数据存储在外部磁盘上,而不必将所有数据加载到内存中。
注意
你可以在这里阅读更多关于 XGBoost 的内容:arxiv.org/abs/1603.02754
练习 44:使用 XGBoost 库进行分类
在本练习中,我们将使用 Python 的 XGBoost 库对批发客户数据集进行分类(github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter05/data
)。该数据集包含批发分销商客户的购买数据,包括在各种产品类别上的年度花费。我们将根据不同产品的年度支出预测渠道。这里的“渠道”描述了客户是酒店/餐厅/咖啡馆(horeca)客户还是零售客户。
-
打开你虚拟环境中的 Jupyter Notebook。
-
导入 XGBoost、Pandas 和 sklearn,这些是我们将用于计算准确度的函数。准确度对于理解我们的模型性能至关重要。
import pandas as pd import xgboost as xgb from sklearn.metrics import accuracy_score
-
使用 pandas 读取批发客户数据集,并通过以下命令检查数据是否成功加载:
data = pd.read_csv("data/wholesale-data.csv")
-
使用
head()
命令检查数据集的前五个条目。输出如下面的截图所示:data.head()
图 5.1:显示数据集前五个元素的截图
-
现在,“
data
”数据框包含所有数据。它有目标变量,在我们的例子中是“Channel
”,以及预测变量。因此,我们将数据分为特征(预测变量)和标签(目标变量)。X = data.copy()X.drop("Channel", inplace = True, axis = 1)Y = data.Channel
-
按照前面章节的讨论,创建训练集和测试集。在这里,我们使用 80:20 的比例进行分割,因为数据集中的数据点较少。你可以尝试不同的分割比例。
X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values
-
将 pandas 数据框转换为 DMatrix,这是 XGBoost 用于存储训练和测试数据集的内部数据结构。
train = xgb.DMatrix(X_train, label=Y_train)test = xgb.DMatrix(X_test, label=Y_test)
-
指定训练参数并训练模型。
注意
param = {'max_depth':6, 'eta':0.1, 'silent':1, 'objective':'multi:softmax', 'num_class': 3}num_round = 5 model = xgb.train(param, train, num_round)
注意
默认情况下,XGBoost 会使用所有可用线程进行多线程处理。要限制线程数,你可以使用
nthread
参数。有关更多信息,请参阅下一节。 -
使用我们刚刚创建的模型预测测试集的“
Channel
”值。preds = model.predict(test)
-
获取我们为测试数据集训练的模型的准确度。
acc = accuracy_score(Y_test, preds)print("Accuracy: %.2f%%" % (acc * 100.0))
输出截图如下:
图 5.2:最终准确度
恭喜!你刚刚使用大约 90%的准确率创建了你的第一个 XGBoost 模型,而且几乎没有进行调优!
XGBoost 库
我们用于执行上述分类的库名为 XGBoost。该库通过许多参数提供了大量的自定义选项。在接下来的章节中,我们将深入了解 XGBoost 库的不同参数和功能。
注意
欲了解更多关于 XGBoost 的信息,请访问网站:https://xgboost.readthedocs.io
训练
影响任何 XGBoost 模型训练的参数如下所示。
-
booster
: 尽管我们在介绍中提到 XGBoost 的基础学习器是回归树,但使用此库时,我们也可以将线性回归作为弱学习器使用。另一种弱学习器,DART 增强器,是一种新的树增强方法,它通过随机丢弃树来防止过拟合。使用树增强时,传递"gbtree
"(默认);使用线性回归时,传递"gblinear
";若要进行带有丢弃的树增强,传递"dart
"。注意
你可以通过这篇论文了解更多关于 DART 的信息:http://www.jmlr.org/proceedings/papers/v38/korlakaivinayak15.pdf
-
silent
: 0 打印训练日志,而 1 则为静默模式。 -
nthread
: 这表示要使用的并行线程数。默认情况下,它是系统中可用的最大线程数。注意
参数
silent
已被弃用,现已被verbosity
替代,verbosity
可以取以下值:0(静默)、1(警告)、2(信息)、3(调试)。 -
seed
: 这是随机数生成器的种子值。在此处设置一个常量值,可以得到可重复的结果。默认值是 0。 -
objective
: 这是模型尝试最小化的函数。接下来的几个点将介绍目标函数。reg:linear
: 线性回归应该用于连续型目标变量(回归问题)。 (默认)binary:logistic
: 用于二分类的逻辑回归。它输出概率而非类别。binary:hinge
: 这是二分类,它输出的是 0 或 1 的预测,而不是概率。当你不关心概率时,使用此方法。multi:softmax
: 如果你想进行多类别分类,使用此方法来执行基于 softmax 目标的分类。必须将num_class
参数设置为类别数。multi:softprob
: 这与 softmax 的作用相同,但输出的是每个数据点的概率,而不是预测类别。 -
eval_metric
: 需要在验证集上观察模型的表现(如在第一章,数据科学与数据预处理介绍中讨论)。此参数用于指定验证数据的评估指标。默认的评估指标根据目标函数选择(回归使用rmse
,分类使用logloss
)。你可以使用多个评估指标。rmse
:均方根误差(RMSE)对较大误差的惩罚更重。因此,当误差为 1 比误差为 3 更为严重时,它是合适的。mae
:平均绝对误差(MAE)适用于当误差为 1 和误差为 3 的情况类似时。以下图表显示了随着实际值与预测值之间差异的增加,误差也随之增加。图示如下:
图 5.3:实际值与预测值的差异
图 5.4:随着误差变化,惩罚的变化;|X| 为 mae,X² 为 rmse
logloss
:模型的负对数似然(logloss
)等同于最大化模型的准确度。它在数学上被定义为:
图 5.5:Logloss 方程示意图
这里,N 是数据点的数量,M 是类别的数量,1 或 0 取决于预测是否正确,表示的是预测数据点 i 为标签 j 的概率。
AUC
:曲线下面积在二分类中广泛使用。如果你的数据集存在类别不平衡问题,你应该始终使用它。类别不平衡问题发生在数据未能均匀分配到不同类别中;例如,如果类别 A 占数据的 90%,而类别 B 占 10%。我们将在“处理不平衡数据集”一节中进一步讨论类别不平衡问题。
aucpr
:精准率-召回率(PR)曲线下的面积与 AUC 曲线相同,但在处理高度不平衡的数据集时应优先使用。我们将在“处理不平衡数据集”一节中讨论这一点。
注意
在处理二分类数据集时,应作为经验法则使用 AUC 或 AUCPR。
树提升器
特定于基于树的模型的参数如下所示:
-
eta
:这是学习率。修改此值以防止过拟合,正如我们在第一章,数据科学与数据预处理简介中讨论的那样。学习率决定了每一步更新权重的幅度。权重的梯度会与学习率相乘,并加到权重上。默认值为 0.3,最大值为 1,最小值为 0。 -
gamma
:这是进行划分所需的最小损失减少量。gamma 值越大,算法越保守。更加保守有助于防止过拟合。该值依赖于数据集和使用的其他参数。其范围从 0 到无限大,默认值为 0。较小的值会导致树较浅,较大的值则会导致树更深。注意
gamma 值大于 1 通常不会得到好的结果。
-
max_depth
:这是任何树的最大深度,如第三章《通过 Sklearn 介绍机器学习》中所讨论的。增加最大深度将使模型更容易发生过拟合。0 表示没有限制。默认值为 6。 -
subsample
:将其设置为 0.5 将导致算法在生成树之前随机抽取一半的训练数据。这可以防止过拟合。每次提升迭代时会进行一次子采样,默认值为 1,这意味着模型会使用完整的数据集,而不是样本。 -
lambda
:这是 L2 正则化项。L2 正则化将系数的平方值作为惩罚项添加到损失函数中。增大该值可以防止过拟合。其默认值为 1。 -
alpha
:这是 L1 正则化项。L1 正则化将系数的绝对值作为惩罚项添加到损失函数中。增大该值可以防止过拟合。其默认值为 0。 -
scale_pos_weight
:当类别极度不平衡时非常有用。我们将在接下来的章节中学习更多关于不平衡数据的内容。一个典型的考虑值是:负实例的总和 / 正实例的总和。其默认值为 1。 -
predictor
:有两个预测器。cpu_predictor
使用 CPU 进行预测,默认使用。gpu_predictor
使用 GPU 进行预测。注意
获取所有参数的列表,请访问:https://xgboost.readthedocs.io/en/latest/parameter.html
控制模型过拟合
如果你观察到训练数据集上的准确度很高,但在测试数据集上的准确度较低,那么你的模型已经对训练数据过拟合,如第一章《数据科学和数据预处理简介》中所示。XGBoost 中有两种主要方法可以限制过拟合:
-
在监控训练和测试指标的同时调整
max_depth
、min_child_weight
和gamma
,以便获得最佳模型而不对训练数据集过拟合。你将在接下来的章节中学到更多相关内容。 -
colsample_bytree
的作用与子采样相同,但它是对列进行采样,而不是对行进行采样。
为了更好地理解,请参见下图中的训练和准确度图表:
图 5.6:训练和准确度图表
要理解具有过拟合和适当拟合模型的数据集的概念化,请参阅下图:
图 5.7:具有过拟合和适当拟合模型的数据集示意图
注意
黑线表示模型适配良好,而红线表示的模型已经对数据集过拟合。
处理不平衡数据集
不平衡数据集给数据科学家带来了许多问题。一个不平衡数据集的例子是信用卡欺诈数据。在这种情况下,大约 95%的交易是合法的,只有 5%是欺诈性的。在这种情况下,一个将所有交易预测为正确的模型会得到 95%的准确率,但实际上这是一个非常糟糕的模型。为了查看类别的分布情况,你可以使用以下函数:
data['target_variable'].value_counts()
输出结果如下:
图 5.8:类别分布
为了处理不平衡数据集,你可以使用以下方法:
-
对记录数较多的类别进行欠采样:以信用卡欺诈为例,你可以通过随机抽取合法交易样本,使其记录数与欺诈记录相等。这样就能实现欺诈类别和合法类别的均衡分布。
-
对记录数较少的类别进行过采样:以信用卡欺诈为例,你可以通过添加新数据点或复制现有数据点来增加欺诈交易样本的数量。这样就能实现欺诈类别和合法类别的均衡分布,欺诈和合法。
-
使用 scale_pos_weight 来平衡正负权重:你可以通过该参数为数据点较少的类别分配更高的权重,从而人为地平衡各类别。该参数的值可以是:
图 5.9:值参数方程
你可以使用以下代码查看各类别的分布情况:
positive = sum(Y == 1)
negative = sum(Y == 0)
scale_pos_weight = negative/positive
- 使用 AUC 或 AUCPR 进行评估:如前所述,AUC 和 AUCPR 指标对于不平衡数据集非常敏感,不像准确率那样,它会给出一个高值,尽管模型通常预测的是多数类别。AUC 仅适用于二分类问题。它表示的是在不同阈值(0、0.01、0.02...1)下的真正例率与假正例率的关系。如下图所示:
图 5.10:TPR 和 FPR 方程
该指标是绘制TPR 和 FPR 后所得到的曲线下面积。在处理高度偏斜的数据集时,AUCPR 能提供更好的结果,因此它是首选。AUCPR 表示在不同阈值下的精确度和召回率。
图 5.11:精确度和召回率方程
作为经验法则,当处理不平衡类别时,应使用 AUC 或 AUCPR 作为评估指标,因为它能提供更清晰的模型表现。
注意
机器学习算法无法轻松处理作为字符串表示的字符串或类别变量,因此我们必须将它们转换为数字。
活动 14:训练并预测一个人的收入
在本次活动中,我们将尝试预测一个人的收入是否超过$50,000。成人收入数据集(链接)的数据来源于 1994 年人口普查数据集(链接),包含个人收入、教育资格和职业等信息。让我们来看一个场景:你在一家汽车公司工作,你需要创建一个系统,让公司的销售代表能够判断应该向哪个人推荐哪种汽车。
为此,你需要创建一个机器学习模型来预测潜在买家的收入,从而为销售人员提供正确的信息,以便销售正确的汽车。
-
使用 pandas 加载收入数据集(
adult-data.csv
)。 -
数据应该如下所示:
图 5.12:显示人口普查数据集五个元素的截图
使用以下代码来指定列名:
data = pd.read_csv("../data/adult-data.csv", names=['age', 'workclass','education-num', 'occupation', 'capital-gain', 'capital-loss', 'hoursper-week', 'income'])
-
使用 sklearn 将所有分类变量从字符串转换为整数。
-
使用 XGBoost 库进行预测,并进行参数调优,使准确率超过 80%。
我们成功地使用数据集预测了收入,准确率约为 83%。
注意
该活动的解决方案可以在第 360 页找到。
外部内存使用
当你有一个异常大的数据集,无法将其加载到内存中时,XGBoost 库的外部内存功能将帮助你。这个功能将训练 XGBoost 模型,而无需将整个数据集加载到内存中。
使用这个功能几乎不需要额外努力;你只需要在文件名的末尾添加缓存前缀。
train = xgb.DMatrix('data/wholesale-data.dat.train#train.cache')
该功能仅支持libsvm
文件。因此,我们现在将把一个已加载到 pandas 的数据集转换为libsvm
文件,以便使用外部内存功能。
注意
你可能需要根据数据集的大小分批进行处理。
from sklearn.datasets import dump_svmlight_file
dump_svmlight_file(X_train, Y_train, 'data/wholesale-data.dat.train', zero_based=True, multilabel=False)
这里,X_train
和Y_train
分别是预测变量和目标变量。libsvm
文件将保存在数据文件夹中。
交叉验证
交叉验证是一种帮助数据科学家评估模型在未见数据上的表现的技术。当数据集不够大,无法创建三个划分(训练集、测试集和验证集)时,交叉验证特别有用。交叉验证通过给模型呈现同一数据集的不同划分,帮助模型避免过拟合。它通过在每次交叉验证的过程中,向模型提供不同的训练集和验证集来实现。10 折交叉验证是最常用的方式,数据集被分成 10 个完全不同的子集,并对每个子集进行训练,最后平均各项指标,以获得模型的准确预测性能。在每一轮交叉验证中,我们进行以下操作:
-
将数据集打乱并将其分成 k 个不同的组(k=10 用于 10 折交叉验证)。
-
在 k-1 个组上训练模型,并在 1 个组上进行测试。
-
评估模型并存储结果。
-
重复步骤 2 和 3,使用不同的组,直到所有 k 种组合都训练完毕。
-
最终指标是不同轮次生成的指标的平均值。
图 5.13:交叉验证数据集示意图
XGBoost 库内置了一个进行交叉验证的函数。本节将帮助你熟悉如何使用它。
练习 45:使用交叉验证找到最佳超参数
在本练习中,我们将使用 XGBoost 库在 Python 中为上一个活动中的成人数据集找到最佳超参数。为此,我们将利用该库的交叉验证功能。
-
从活动 14加载人口普查数据集,并执行所有预处理步骤。
import pandas as pd import numpy as np data = pd.read_csv("../data/adult-data.csv", names=['age', 'workclass', 'fnlwgt', 'education-num', 'occupation', 'capital-gain', 'capital-loss', 'hours-per-week', 'income'])
使用 sklearn 中的 Label Encoder 对字符串进行编码。首先,导入 Label Encoder,然后逐个编码所有字符串类型的列。
from sklearn.preprocessing import LabelEncoder data['workclass'] = LabelEncoder().fit_transform(data['workclass']) data['occupation'] = LabelEncoder().fit_transform(data['occupation']) data['income'] = LabelEncoder().fit_transform(data['income'])
-
从数据中创建训练集和测试集,并将数据转换为 D 矩阵。
import xgboost as xgb X = data.copy() X.drop("income", inplace = True, axis = 1) Y = data.income X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values train = xgb.DMatrix(X_train, label=Y_train) test = xgb.DMatrix(X_test, label=Y_test)
-
不使用 train 函数,而是使用以下代码进行 10 折交叉验证,并将结果存储在
model_metrics
数据框中。for 循环遍历不同的树深度值,以找到最适合我们数据集的深度。test_error = {} for i in range(20): param = {'max_depth':i, 'eta':0.1, 'silent':1, 'objective':'binary:hinge'} num_round = 50 model_metrics = xgb.cv(param, train, num_round, nfold = 10) test_error[i] = model_metrics.iloc[-1]['test-error-mean']
-
使用 Matplotlib 可视化结果。
import matplotlib.pyplot as plt plt.scatter(test_error.keys(),test_error.values()) plt.xlabel('Max Depth') plt.ylabel('Test Error') plt.show()
图 5.14:最大深度与测试误差的图表
从图中我们可以看到,9 的最大深度对于我们的数据集效果最好,因为它的测试误差最低。
-
找到最佳学习率。运行这段代码会花费一些时间,因为它会遍历多个学习率,每个学习率运行 500 轮。
for i in range(1,100,5): param = {'max_depth':9, 'eta':0.001*i, 'silent':1, 'objective':'binary:hinge'} num_round = 500 model_metrics = xgb.cv(param, train, num_round, nfold = 10) test_error[i] = model_metrics.iloc[-1]['test-error-mean']
-
可视化结果。
lr = [0.001*(i) for i in test_error.keys()] plt.scatter(temp,test_error.values()) plt.xlabel('Learning Rate') plt.ylabel('Error') plt.show()
图 5.15:学习率与测试误差的图表
从图中可以看到,约 0.01 的学习率对我们的模型效果最好,因为它的误差最小。
-
让我们可视化学习率为 0.01 时每一轮的训练和测试误差。
param = {'max_depth':9, 'eta':0.01, 'silent':1, 'objective':'binary:hinge'} num_round = 500 model_metrics = xgb.cv(param, train, num_round, nfold = 10) plt.scatter(range(500),model_metrics['test-error-mean'], s = 0.7, label = 'Test Error') plt.scatter(range(500),model_metrics['train-error-mean'], s = 0.7, label = 'Train Error') plt.legend() plt.show()
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_05_16.jpg)
图 5.16:训练和测试误差随轮次变化的图表
注意
list(model_metrics['test-error-mean']).index(min(model_metrics['test-error-mean']))
-
要理解这一点,可以查看输出结果。
图 5.17:最小误差
注意
对于该数据集,效果最好的最终模型参数为:
最大深度 = 9
学习率 = 0.01
轮次数 = 496
保存和加载模型
掌握结构化数据的最后一步是能够保存和加载你已经训练和微调的模型。每次需要预测时都重新训练一个新模型会浪费大量时间,因此能够保存已训练的模型对于数据科学家来说至关重要。保存的模型使我们能够复制结果,并创建使用机器学习模型的应用程序和服务。步骤如下:
-
要保存 XGBoost 模型,你需要调用
save_model
函数。model.save_model('wholesale-model.model')
-
要加载之前保存的模型,您需要在初始化的 XGBoost 变量上调用
load_model
。loaded_model = xgb.Booster({'nthread': 2}) loaded_model.load_model('wholesale-model.model')
注意
如果您让 XGBoost 获取所有可用的线程,您的计算机在训练或预测时可能会变慢。
现在你已经准备好使用 XGBoost 库来开始建模你的结构化数据集了!
练习 46:创建一个基于实时输入进行预测的 Python 脚本
在这个练习中,我们将首先创建一个模型并保存它。然后,我们将创建一个 Python 脚本,利用这个保存的模型对用户输入的数据进行预测。
-
将活动 14 中的收入数据集加载为 pandas 数据框。
import pandas as pd import numpy as np data = pd.read_csv("../data/adult-data.csv", names=['age', 'workclass', 'education-num', 'occupation', 'capital-gain', 'capital-loss', 'hours-per-week', 'income'])
-
去除所有尾随空格。
data[['workclass', 'occupation', 'income']] = data[['workclass', 'occupation', 'income']].apply(lambda x: x.str.strip())
-
使用 scikit 将所有分类变量从字符串转换为整数。
from sklearn.preprocessing import LabelEncoder from collections import defaultdict label_dict = defaultdict(LabelEncoder) data[['workclass', 'occupation', 'income']] = data[['workclass', 'occupation', 'income']].apply(lambda x: label_dict[x.name].fit_transform(x))
-
将标签编码器保存到一个 pickle 文件中,以供将来使用。pickle 文件存储 Python 对象,以便我们在需要时可以访问它们。
import pickle with open( 'income_labels.pkl', 'wb') as f: pickle.dump(label_dict, f, pickle.HIGHEST_PROTOCOL)
-
将数据集拆分为训练集和测试集并创建模型。
-
将模型保存到文件。
model.save_model('income-model.model')
-
在 Python 脚本中,加载模型和标签编码器。
import xgboost as xgb loaded_model = xgb.Booster({'nthread': 8}) loaded_model.load_model('income-model.model') def load_obj(file): with open(file + '.pkl', 'rb') as f: return pickle.load(f) label_dict = load_obj('income_labels')
-
从用户那里读取输入。
age = input("Please enter age: ") workclass = input("Please enter workclass: ") education_num = input("Please enter education_num: ") occupation = input("Please enter occupation: ") capital_gain = input("Please enter capital_gain: ") capital_loss = input("Please enter capital_loss: ") hours_per_week = input("Please enter hours_per_week: ")
-
创建一个数据框来存储这些数据。
data_list = [age, workclass, education_num, occupation, capital_gain, capital_loss, hours_per_week] data = pd.DataFrame([data_list]) data.columns = ['age', 'workclass', 'education-num', 'occupation', 'capital-gain', 'capital-loss', 'hours-per-week']
-
预处理数据。
data[['workclass', 'occupation']] = data[['workclass', 'occupation']].apply(lambda x: label_dict[x.name].transform(x))
-
转换为 Dmatrix 并使用模型进行预测。
data = data.astype(int) data_xgb = xgb.DMatrix(data) pred = loaded_model.predict(data_xgb)
-
执行逆变换以获取结果。
income = label_dict['income'].inverse_transform([int(pred[0])])
输出如下:
图 5.18:逆变换输出
注意
确保你输入的 workclass
和 occupation
值在训练数据中存在,否则脚本会抛出错误。当 LabelEncoder
遇到它之前未见过的新值时,就会发生此错误。
恭喜!你构建了一个使用用户输入数据进行预测的脚本。现在你可以将你的模型部署到任何你想要的地方。
活动 15:预测客户流失
在本活动中,我们将尝试预测一个客户是否会转移到另一个电信提供商。数据来自 IBM 示例数据集。让我们来看以下场景:你在一家电信公司工作,最近,许多用户开始转向其他提供商。现在,为了能够给流失的客户提供折扣,你需要预测哪些客户最有可能流失。为此,你需要创建一个机器学习模型,预测哪些客户会流失。
-
使用 pandas 加载电信流失(
telco-churn.csv
)数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter05/data)。该数据集包含有关电信服务提供商客户的信息。数据集的原始来源链接为: https://www.ibm.com/communities/analytics/watson-analytics-blog/predictive-insights-in-the-telco-customer-churn-data-set/。数据集中包含多个字段,如费用、服务期限、流媒体信息,以及一个变量表示客户是否流失。前几行数据应该如下所示:图 5.19:显示电信流失数据集前五个元素的截图
-
删除不必要的变量。
-
使用 scikit 将所有类别变量从字符串转换为整数。你可以使用以下代码:
data.TotalCharges = pd.to_numeric(data.TotalCharges, errors='coerce')
-
修复使用 pandas 加载时的数据类型不匹配问题。
-
使用 XGBoost 库进行预测,并通过交叉验证进行参数调优,提升准确率至超过 80%。
-
保存你的模型以供将来使用。
注意
本活动的解决方案可以在第 361 页找到。
神经网络
神经网络是数据科学家使用的最流行的机器学习算法之一。它在需要图像或数字媒体来解决问题的场景中,始终优于传统的机器学习算法。在拥有足够数据的情况下,它在结构化数据问题中也超越了传统的机器学习算法。拥有超过两层的神经网络被称为深度神经网络,而利用这些“深度”网络解决问题的过程被称为深度学习。为了处理非结构化数据,神经网络有两种主要类型:卷积神经网络(CNN)可以用于处理图像,递归神经网络(RNN)可以用于处理时间序列和自然语言数据。我们将在第六章《解码图像》和第七章《处理人类语言》中深入讨论 CNN 和 RNN。接下来,让我们看看一个普通的神经网络是如何工作的。在这一部分,我们将简要讲解神经网络的不同部分,接下来的章节会详细解释每个话题。
什么是神经网络?
神经网络的基本单元是神经元。神经网络的灵感来源于生物大脑,因此神经元这一名称也由此得来。神经网络中的所有连接,就像大脑中的突触一样,可以将信息从一个神经元传递到另一个神经元。在神经网络中,输入信号的加权组合会被汇聚,然后经过一个函数处理后将输出信号传递出去。这个函数是一个非线性激活函数,代表着神经元的激活阈值。多个相互连接的神经元层组成了一个神经网络。神经网络中只有非输出层包含偏置单元。与每个神经元相关的权重和这些偏置共同决定了整个网络的输出;因此,这些就是我们在训练过程中为了拟合数据而修改的参数。
图 5.20:单层神经网络的表示
神经网络的第一层包含的节点数与数据集中的独立变量数量相等。这个层被称为输入层,接下来是多个隐藏层,最后是输出层。输入层的每个神经元接收数据集中的一个独立变量。输出层则输出最终的预测结果。如果是回归问题,这些输出可以是连续的(如 0.2、0.6、0.8);如果是分类问题,则输出为分类标签(如 2、4、5)。神经网络的训练通过调整网络的权重和偏置来最小化误差,这个误差是期望值与输出值之间的差异。权重与输入相乘,偏置值则与这些权重的组合相加,最终得到输出。
图 5.21:神经元输出
在这里,y 是神经元的输出,x 是输入,w 和 b 分别是权重和偏置,f 是激活函数,我们将在后面进一步学习它。
优化算法
为了最小化模型的误差,我们训练神经网络以最小化预定义的损失函数,使用优化算法。对于这个优化算法,有许多选择,具体选择哪个取决于你的数据和模型。在本书的大部分内容中,我们将使用随机梯度下降(SGD),它在大多数情况下表现良好,但我们会在需要时解释其他优化器。SGD 通过迭代地计算梯度来工作,梯度是权重相对于误差的变化。在数学上,它是相对于输入的偏导数。它找到的梯度有助于最小化给定的函数,在我们这个案例中就是损失函数。当我们接近解决方案时,梯度的大小会减小,从而防止我们超越最优解。
理解 SGD(随机梯度下降)最直观的方式是将其视为下坡的过程。最初,我们会采取陡峭的下降,然后当我们接近谷底时,坡度会变小。
图 5.22:梯度下降的直观理解(k 表示梯度的大小)
超参数
一个决定训练模型所需时间的重要参数叫做学习率,它本质上是我们进行下降时采取的步长。步长过小,模型需要很长时间才能找到最优解;步长过大,则会超越最优解。为了解决这个问题,我们从较大的学习率开始,并在进行几步后降低学习率。这有助于我们更快地达到最小值,并且由于步长的减小,防止模型超越解决方案。
接下来是权重的初始化。我们需要对神经网络的权重进行初始化,以便从一个起始点开始,然后调整权重以最小化误差。初始化在防止消失梯度和爆炸梯度问题中起着重要作用。
消失梯度问题指的是每经过一层,梯度逐渐变小,因为任何小于 1 的数相乘会变得更小,因此经过多层后,这个值会变为 0。
爆炸梯度问题发生在较大的误差梯度累加时,导致模型更新过大。如果模型的损失变为 NaN,这可能是一个问题。
使用Xavier 初始化,我们可以防止这些问题,因为它在初始化权重时考虑了网络的大小。Xavier 初始化通过从以 0 为中心的截断正态分布中抽取权重进行初始化,标准差为
图 5.23:Xavier 初始化使用的标准差。
其中 xi 是该层输入神经元的数量,yi 是输出神经元的数量。
这确保了即使网络中的层数非常大,输入和输出的方差仍然保持不变。
损失函数
另一个需要考虑的超参数是损失函数。根据问题的类型,分类或回归,使用不同的损失函数。对于分类,我们选择交叉熵、铰链等损失函数;对于回归,我们使用均方误差、平均绝对误差(MAE)和 Huber 损失函数。不同的损失函数适用于不同的数据集。我们将在使用过程中逐步介绍它们。
激活函数
在创建神经网络层时,您需要定义一个激活函数,这取决于该层是隐藏层还是输出层。对于隐藏层,我们将使用 ReLU 或 tanh 激活函数。激活函数帮助神经网络模型实现非线性函数。几乎没有实际问题可以通过线性模型来解决。除了这些,不同的激活函数具有不同的特点。Sigmoid 输出的值在 0 和 1 之间,而 tanh 则将输出围绕 0 进行中心化,有助于更好的学习。ReLU 则可以防止梯度消失问题,并且在计算上更为高效。下面是 ReLU 图形的表示。
图 5.24:ReLU 激活函数的表示
Softmax 输出的是概率,用于多分类问题,而 sigmoid 输出的是介于 0 和 1 之间的值,仅用于二分类问题。线性激活通常用于解决回归问题的模型。下图展示了 sigmoid 激活函数的表示:
图 5.25:Sigmoid 激活函数的表示
上一部分介绍了很多新信息;如果您感到困惑,不必担心。我们将在接下来的章节中实践所有这些概念,这将加强对这些主题的理解。
Keras
Keras 是一个用 Python 编写的开源高级神经网络 API,能够在 TensorFlow、微软认知工具包(CNTK)或 Theano 上运行。Keras 的开发旨在快速实验,从而帮助加速应用程序开发。使用 Keras,用户可以以最短的延迟从创意到结果。由于强大的社区支持,Keras 支持几乎所有最新的数据科学模型,尤其是与神经网络相关的模型。它包含了多种常用构建块的实现,如层、批量归一化、Dropout、目标函数、激活函数和优化器。同时,Keras 允许用户为智能手机(Android 和 iOS)、Web 或 Java 虚拟机 (JVM) 创建模型。使用 Keras,您可以在 GPU 上训练模型,而无需更改代码。
鉴于 Keras 的所有这些特性,数据科学家必须学习如何使用该库的各个方面。掌握 Keras 的使用将极大地帮助你成为一名优秀的数据科学家。为了展示 Keras 的强大功能,我们将安装它并创建一个单层神经网络模型。
注意
你可以在这里阅读更多关于 Keras 的信息:keras.io/
练习 47:为 Python 安装 Keras 库并使用它进行分类
在本练习中,我们将使用 Python 的 Keras 库对批发客户数据集(我们在练习 44中使用的那个)进行分类。
-
在虚拟环境中运行以下命令以安装 Keras。
pip3 install keras
-
从你的虚拟环境中打开 Jupyter Notebook。
-
导入 Keras 和其他所需的库。
import pandas as pd from keras.models import Sequential from keras.layers import Dense import numpy as np from sklearn.metrics import accuracy_score
-
使用 pandas 读取批发客户数据集,并使用以下命令检查数据是否成功加载:
data = pd.read_csv("data/wholesale-data.csv") data.head()
输出应该是这样的:
图 5.26:显示数据集前五个元素的截图
-
将数据拆分为特征和标签。
X = data.copy()X.drop("Channel", inplace = True, axis = 1)Y = data.Channel
-
创建训练集和测试集。
X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values
-
创建神经网络模型。
model = Sequential() model.add(Dense(units=8, activation='relu', input_dim=7)) model.add(Dense(units=16, activation='relu')) model.add(Dense(units=1, activation='sigmoid'))
在这里,我们创建了一个四层网络,其中包含一个输入层、两个隐藏层和一个输出层。隐藏层使用 ReLU 激活函数,输出层使用 softmax 激活函数。
-
编译并训练模型。我们使用的是二元交叉熵损失函数,这与我们之前讨论的 logloss 相同;我们选择了随机梯度下降作为优化器。我们将训练运行五个 epoch,批量大小为八。
model.compile(loss='binary_crossentropy', optimizer='sgd', metrics=['accuracy']) model.fit(X_train, Y_train, epochs=5, batch_size=8)
注意
你将看到模型训练日志。Epoch 表示训练的迭代次数,352 表示数据集的大小除以批量大小。在进度条之后,你可以看到一次迭代所用的时间。接下来,你会看到训练每个批次所需的平均时间。接下来是模型的损失,这里是二元交叉熵损失,然后是迭代后的准确度。这些术语中的一些是新的,但我们将在接下来的章节中逐一了解它们。
图 5.27:模型训练日志截图
-
预测测试集的值。
preds = model.predict(X_test, batch_size=128)
-
获取模型的准确度。
accuracy = accuracy_score(Y_test, preds.astype(int)) print("Accuracy: %.2f%%" % (accuracy * 100.0))
输出结果如下:
图 5.28:输出准确度
恭喜!你刚刚成功创建了第一个神经网络模型,准确率约为 81%,而且没有进行任何微调!你会注意到,与 XGBoost 相比,这个准确率相当低。在接下来的章节中,你将学习如何提高这个准确度。准确率较低的一个主要原因是数据的大小。为了让神经网络模型真正发挥作用,它必须有一个大型的数据集来进行训练,否则会出现过拟合。
Keras 库
Keras 使得模块化成为可能。所有的初始化器、损失函数、优化器、层、正则化器和激活函数都是独立的模块,可以用于任何类型的数据和网络架构。你会发现几乎所有最新的函数都已经在 Keras 中实现。这使得代码的可重用性和快速实验成为可能。作为数据科学家,你不会受到内建模块的限制;创建自己的自定义模块并与其他内建模块一起使用是非常简单的。这促进了研究并有助于不同的应用场景。例如,你可能需要编写一个自定义的损失函数来最大化汽车销量,并对利润更高的汽车赋予更大的权重,从而提高整体利润。
所有你需要创建神经网络的不同种类的层在 Keras 中都有定义。我们将在使用它们时进行探讨。在 Keras 中创建神经模型的主要方式有两种,顺序模型和功能性 API。
顺序模型:顺序模型是一个层的线性堆叠。这是使用 Keras 创建神经网络模型的最简单方式。下面给出了该模型的代码片段:
model = Sequential()model.add(Dense(128, input_dim=784))model.add(Activation('relu'))
model.add(Dense(10))model.add(Activation('softmax'))
功能性 API:功能性 API 是构建复杂模型的方式。由于顺序模型是线性的,无法创建复杂模型。功能性 API 让你能够创建模型的多个部分,然后将它们合并在一起。通过功能性 API 创建的相同模型如下所示:
inputs = Input(shape=(784,))
x = Dense(128, activation='relu')(inputs)prediction = Dense(10, activation='softmax')(x)model = Model(inputs=inputs, outputs=prediction)
Keras 的一个强大功能是回调函数。回调函数允许你在训练过程的任何阶段使用函数。这对于获取统计信息和在不同阶段保存模型非常有用。它还可以用来对学习率应用自定义衰减,并执行提前停止。
filepath="model-weights-{epoch:02d}-{val_loss:.2f}.hdf5"
model_ckpt = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=True, mode='auto')
callbacks = [model_ckpt]
要保存你在 Keras 上训练的模型,你只需要使用以下代码:
model.save('Path to model')
要从文件中加载模型,使用以下代码:
keras.models.load_model('Path to model')
提前停止是一个可以通过回调函数实现的有用功能。提前停止有助于节省训练模型的时间。如果指定的指标变化小于设定的阈值,训练过程将停止。
EarlyStopping(monitor='val_loss', min_delta=0.01, patience=5, verbose=1, mode='auto')
上述回调函数将在验证损失的变化小于 0.01 且持续五个周期时停止训练。
注意
始终使用 ModelCheckpoint
来存储模型状态。对于大型数据集和更大的网络,这一点尤为重要。
练习 48:使用神经网络预测鳄梨价格
让我们应用本节所学的知识,创建一个优秀的神经网络模型,用于预测不同种类鳄梨的价格。数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter05/data)包含了如产物的平均价格、产物的体积、鳄梨生产区域、以及所使用袋子大小等信息。它还包含一些未知变量,可能对我们的模型有所帮助。
注意
原始来源网站:www.hassavocadoboard.com/retail/volume-and-price-data
-
导入鳄梨数据集并观察列。你将看到如下内容:
import pandas as pd import numpy as np data = pd.read_csv('data/avocado.csv') data.T
图 5.29:展示鳄梨数据集的截图
-
浏览数据并将日期列拆分为天和月。这将帮助我们捕捉季节性变化,同时忽略年份。现在,删除日期和未命名的列。
data['Day'], data['Month'] = data.Date.str[:2], data.Date.str[3:5] data = data.drop(['Unnamed: 0', 'Date'], axis = 1)
-
使用
LabelEncoder
对分类变量进行编码,以便 Keras 可以使用它来训练模型。from sklearn.preprocessing import LabelEncoder from collections import defaultdict label_dict = defaultdict(LabelEncoder) data[['region', 'type', 'Day', 'Month', 'year']] = data[['region', 'type', 'Day', 'Month', 'year']].apply(lambda x: label_dict[x.name].fit_transform(x))
-
将数据分为训练集和测试集。
from sklearn.model_selection import train_test_split X = data y = X.pop('AveragePrice') X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=9)
-
使用回调函数在损失改善时保存模型,并在模型表现不佳时进行早停。
from keras.callbacks import ModelCheckpoint, EarlyStopping filepath="avocado-{epoch:02d}-{val_loss:.2f}.hdf5" model_ckpt = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=True, mode='auto') es = EarlyStopping(monitor='val_loss', min_delta=1, patience=5, verbose=1) callbacks = [model_ckpt, es]
-
创建神经网络模型。这里,我们使用与之前相同的模型。
from keras.models import Sequential from keras.layers import Dense model = Sequential() model.add(Dense(units=16, activation='relu', input_dim=13)) model.add(Dense(units=8, activation='relu')) model.add(Dense(units=1, activation='linear')) model.compile(loss='mse', optimizer='adam')
-
训练并评估模型,以获取模型的 MSE。
model.fit(X_train, y_train, validation_data = (X_test, y_test), epochs=40, batch_size=32) model.evaluate(X_test, y_test)
-
查看下方截图中的最终输出:
图 5.30:模型的 MSE
恭喜!你刚刚训练了你的神经网络,得到了合理的误差值,应用于鳄梨数据集。上面显示的值是模型的均方误差。修改一些超参数并使用剩余数据,看看能否获得更好的误差分数。利用前面章节提供的信息。
注意
MSE(均方误差)下降是理想的。最优值将取决于具体情况。例如,在预测汽车速度时,低于 100 的值是理想的,而在预测一个国家的 GDP 时,1000 的 MSE 已经足够好。
分类变量
分类变量是其值可以表示为不同类别的变量。例如,球的颜色、狗的品种和邮政编码等。将这些分类变量映射到单一维度会导致它们之间相互依赖,这是不正确的。尽管这些分类变量没有顺序或依赖关系,但将它们作为单一特征输入神经网络时,神经网络会根据顺序在这些变量之间创建依赖关系,而实际上,顺序并不代表任何意义。在本节中,我们将学习如何解决这个问题并训练有效的模型。
独热编码
映射分类变量的最简单且最广泛使用的方法是使用独热编码。使用此方法,我们将一个分类特征转换为等于特征中类别数量的多个特征。
图 5.31:分类特征转换
使用以下步骤将分类变量转换为独热编码变量:
-
如果数据类型不是整数,则将数据转换为数字。有两种方法可以做到这一点。
-
你可以直接使用 sklearn 的
LabelEncoder
方法。 -
创建箱子以减少类别的数量。类别的数量越高,模型的难度越大。你可以选择一个整数来表示每个箱子。请记住,进行这种操作会导致信息的丧失,并可能导致模型效果不佳。你可以使用以下规则进行直方图分箱:
如果分类列的数量少于 25,使用 5 个箱子。
如果分类列的数量在 25 到 100 之间,使用 n/5 个箱子,其中 n 是分类列的数量;如果超过 100,使用 10 * log (n)个箱子。
注意
你可以将频率小于 5%的类别合并为一个类别。
-
使用 pandas 的
get_dummies
函数将步骤 1 中的数值数组转换为一热向量。
图 5.32:get_dummies 函数的输出
一热编码不是使用分类数据的最佳方法,主要有两个原因:
-
假设分类变量的不同值是完全独立的。这会导致它们之间关系信息的丧失。
-
拥有许多类别的分类变量会导致一个计算成本非常高的模型。数据集越宽广,生成有意义的模型所需的数据点就越多。这就是所谓的维度灾难。
为了解决这些问题,我们将使用实体嵌入。
实体嵌入
实体嵌入将分类特征表示在一个多维空间中。这确保了网络学习不同类别之间的正确关系。这个多维空间的维度没有特定的意义;它可以是模型认为合适学习的任何内容。例如,在一周的日子中,一维可以表示是否是工作日,另一维可以表示与工作日的距离。这种方法的灵感来源于自然语言处理中的词嵌入,用于学习词汇和短语之间的语义相似性。创建嵌入可以帮助教会神经网络如何区分星期五和星期三,或者小狗和狗的区别。例如,一周七天的四维嵌入矩阵可能如下所示:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_05_33.jpg)
图 5.33:四维嵌入矩阵
从上面的矩阵中,你可以看到嵌入学习了类别之间的依赖关系:它知道周六和周日比周四和周五更相似,因为周六和周日的向量是相似的。当数据集中有大量分类变量时,实体嵌入会提供巨大的优势。要在 Keras 中创建实体嵌入,你可以使用嵌入层。
注意
总是尽量使用词嵌入,因为它能提供最佳的结果。
练习 49:使用实体嵌入预测鳄梨价格
让我们利用实体嵌入的知识,通过创建一个更好的神经网络模型来预测鳄梨价格。我们将使用之前的鳄梨数据集。
-
导入鳄梨价格数据集并检查是否有空值。将日期列拆分为月份和日期列。
import pandas as pd import numpy as np data = pd.read_csv('data/avocado.csv') data['Day'], data['Month'] = data.Date.str[:2], data.Date.str[3:5] data = data.drop(['Unnamed: 0', 'Date'], axis = 1) data = data.dropna()
-
对分类变量进行编码。
from sklearn.preprocessing import LabelEncoder from collections import defaultdict label_dict = defaultdict(LabelEncoder) data[['region', 'type', 'Day', 'Month', 'year']] = data[['region', 'type', 'Day', 'Month', 'year']].apply(lambda x: label_dict[x.name].fit_transform(x))
-
将数据拆分为训练集和测试集。
from sklearn.model_selection import train_test_split X = data y = X.pop('AveragePrice') X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=9)
-
创建一个字典,将分类列名映射到其中的唯一值。
cat_cols_dict = {col: list(data[col].unique()) for col in ['region', 'type', 'Day', 'Month', 'year'] }
-
接下来,获取嵌入神经网络可以接受的输入数据格式。
train_input_list = [] test_input_list = [] for col in cat_cols_dict.keys(): raw_values = np.unique(data[col]) value_map = {} for i in range(len(raw_values)): value_map[raw_values[i]] = i train_input_list.append(X_train[col].map(value_map).values) test_input_list.append(X_test[col].map(value_map).fillna(0).values) other_cols = [col for col in data.columns if (not col in cat_cols_dict.keys())] train_input_list.append(X_train[other_cols].values) test_input_list.append(X_test[other_cols].values)
在这里,我们要做的是创建一个包含所有变量的数组列表。
-
接下来,创建一个字典,用于存储嵌入层的输出维度。这是变量所表示的值的数量。你必须通过反复试验来确定正确的数字。
cols_out_dict = { 'region': 12, 'type': 1, 'Day': 10, 'Month': 3, 'year': 1 }
-
现在,为分类变量创建嵌入层。在循环的每次迭代中,我们为分类变量创建一个嵌入层。
from keras.models import Model from keras.layers import Input, Dense, Concatenate, Reshape, Dropout from keras.layers.embeddings import Embedding inputs = [] embeddings = [] for col in cat_cols_dict.keys(): inp = Input(shape=(1,), name = 'input_' + col) embedding = Embedding(cat_cols_dict[col], cols_out_dict[col], input_length=1, name = 'embedding_' + col)(inp) embedding = Reshape(target_shape=(cols_out_dict[col],))(embedding) inputs.append(inp) embeddings.append(embedding)
-
现在,将连续变量添加到网络中并完成模型。
input_numeric = Input(shape=(8,)) embedding_numeric = Dense(16)(input_numeric) inputs.append(input_numeric) embeddings.append(embedding_numeric) x = Concatenate()(embeddings) x = Dense(16, activation='relu')(x) x = Dense(4, activation='relu')(x) output = Dense(1, activation='linear')(x) model = Model(inputs, output) model.compile(loss='mse', optimizer='adam')
-
用我们在第 5 步创建的 train_input_list 训练模型,训练 50 个 epoch。
model.fit(train_input_list, y_train, validation_data = (test_input_list, y_test), epochs=50, batch_size=32)
-
现在,从嵌入层获取权重以可视化嵌入。
embedding_region = model.get_layer('embedding_region').get_weights()[0]
-
执行 PCA,并使用区域标签绘制输出(你可以通过对我们之前创建的字典执行逆变换获得区域标签)。PCA 通过将维度降至二维,将相似的数据点聚集在一起。在这里,我们仅绘制前 25 个区域。
如果你想的话,可以绘制所有它们。
import matplotlib.pyplot as plt from sklearn.decomposition import PCA pca = PCA(n_components=2) Y = pca.fit_transform(embedding_region[:25]) plt.figure(figsize=(8,8)) plt.scatter(-Y[:, 0], -Y[:, 1]) for i, txt in enumerate((label_dict['region'].inverse_transform(cat_cols_dict['region']))[:25]): plt.annotate(txt, (-Y[i, 0],-Y[i, 1]), xytext = (-20, 8), textcoords = 'offset points') plt.show()
图 5.34:使用实体嵌入表示鳄梨生长区域的图形
恭喜!你通过使用实体嵌入提高了模型的准确性。从嵌入图中可以看出,模型能够识别出平均价格高和低的区域。你可以绘制其他变量的嵌入,看看网络从数据中得出了什么关系。另外,尝试通过超参数调优来提高模型的准确性。
活动 16:预测顾客的购买金额
在本活动中,我们将尝试预测顾客在某一产品类别上的消费金额。数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter05/data)包含零售店中的交易记录。让我们来看一个场景:你在一家大型零售连锁店工作,想预测哪种类型的顾客会在某个特定产品类别上花费多少钱。这样做将帮助你的前线员工向顾客推荐合适的产品,从而提高销售额和顾客满意度。为此,你需要创建一个机器学习模型,预测每笔交易的购买金额。
-
使用 pandas 加载 Black Friday 数据集。该数据集是一个零售店的交易记录集合。它包含的信息包括客户的年龄、城市、婚姻状况、购买商品的类别以及账单金额。前几行应该像这样:
图 5.35:显示 Black Friday 数据集前五个元素的截图
移除不必要的变量和空值。移除
Product_Category_2
和Product_Category_3
列。 -
对所有分类变量进行编码。
-
通过使用 Keras 库创建神经网络进行预测。利用实体嵌入并进行超参数调优。
-
保存你的模型以便日后使用。
注意
本活动的解决方案可以在第 364 页找到。
总结
在这一章中,我们学习了如何创建高精度的结构化数据模型,了解了 XGBoost 是什么,以及如何使用该库来训练模型。在开始之前,我们曾经想知道什么是神经网络,以及如何使用 Keras 库来训练模型。了解了神经网络后,我们开始处理分类数据。最后,我们学习了什么是交叉验证以及如何使用它。
现在你已经完成了这一章的内容,你可以处理任何类型的结构化数据,并用它创建机器学习模型。在下一章,你将学习如何为图像数据创建神经网络模型。
第十六章:第六章
解码图像
学习目标
到本章结束时,你将能够:
-
创建能够将图像分类为不同类别的模型
-
使用 Keras 库训练图像的神经网络模型
-
在不同的商业场景中利用图像增强的概念
-
从图像中提取有意义的信息
本章将涵盖如何读取和处理图像的各种概念。
介绍
到目前为止,我们只处理过数字和文本。在本章中,我们将学习如何使用机器学习解码图像并提取有意义的信息,比如图像中存在的物体类型或图像中写的数字。你有没有停下来思考过我们的大脑是如何解读它们从眼睛接收到的图像的?经过数百万年的进化,我们的大脑已经变得非常高效和准确,能够从眼睛接收到的图像中识别物体和模式。我们已经能够通过相机复制眼睛的功能,但让计算机识别图像中的模式和物体却是一项非常艰巨的任务。与理解图像中存在的内容相关的领域被称为计算机视觉。计算机视觉领域在过去几年里经历了巨大的研究和进展。卷积神经网络(CNN)和在 GPU 上训练神经网络的能力是其中最重大的突破之一。如今,CNN 被广泛应用于任何计算机视觉问题中,例如自动驾驶汽车、人脸识别、物体检测、物体追踪以及创建完全自主的机器人。在本章中,我们将学习这些 CNN 是如何工作的,并了解它们相比传统方法有哪些显著的改进。
图像
我们今天使用的数码相机将图像存储为一个巨大的数字矩阵,这些就是我们所说的数字图像。矩阵中的每个数字代表图像中的一个像素。每个数字代表该像素的颜色强度。对于灰度图像,这些值的范围是 0 到 255,其中 0 是黑色,255 是白色。对于彩色图像,这个矩阵是三维的,每个维度对应红色、绿色和蓝色的值。矩阵中的值代表各自颜色的强度。我们将这些值作为输入,用于我们的计算机视觉程序或数据科学模型,以进行预测和识别。
现在,有两种方法可以让我们使用这些像素来创建机器学习模型:
-
将单独的像素作为不同的输入变量输入神经网络
-
使用卷积神经网络
创建一个将单个像素值作为输入变量的全连接神经网络是目前最简单、最直观的方法,因此我们将从创建这个模型开始。在下一节中,我们将学习 CNN,并了解它们在处理图像时的优势。
练习 50:使用全连接神经网络分类 MNIST
在本次练习中,我们将对 修改后的国家标准与技术研究所数据库(MNIST)数据集进行分类。MNIST 是一个手写数字数据集,已被规范化以适应 28 x 28 像素的边界框。该数据集包含 60,000 张训练图像和 10,000 张测试图像。在完全连接的网络中,我们将单个像素作为特征输入到网络中,然后像训练第一个神经网络一样训练它,就像在 第五章 掌握结构化数据 中训练的第一个神经网络一样。
完成此练习,请执行以下步骤:
-
加载所需的库,如下所示:
import numpy as np import matplotlib.pyplot as plt from sklearn.preprocessing import LabelBinarizer from keras.datasets import mnist from keras.models import Sequential from keras.layers import Dense
-
使用 Keras 库加载 MNIST 数据集:
(x_train, y_train), (x_test, y_test) = mnist.load_data()
-
从数据集的形状,你可以推测数据是以 2D 格式呈现的。第一个元素是可用图像的数量,接下来的两个元素是图像的宽度和高度:
x_train.shape
输出结果如下:
图 6.1:图像的宽度和高度
-
绘制第一张图像,查看你正在处理的数据类型:
plt.imshow(x_test[0], cmap=plt.get_cmap(‘gray')) plt.show()
图 6.2:MNIST 数据集的样本图像
-
将 2D 数据转换为 1D 数据,以便我们的神经网络可以将其作为输入(28 x 28 像素 = 784):
x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784)
-
将目标变量转换为 one-hot 向量,这样我们的网络就不会在不同的目标变量之间形成不必要的连接:
label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test)
-
创建模型。建立一个小型的两层网络;你可以尝试其他架构。接下来你将学习更多关于交叉熵损失的内容:
model = Sequential() model.add(Dense(units=32, activation='relu', input_dim=784)) model.add(Dense(units=32, activation='relu')) model.add(Dense(units=10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) model.summary()
图 6.3:稠密网络的模型架构
-
训练模型并检查最终准确率:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=40, batch_size=32) score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))
输出结果如下:
图 6.4:模型准确率
恭喜!你现在已经创建了一个能够以 93.57% 的准确率预测图像上的数字的模型。你可以使用以下代码绘制不同的测试图像,并查看网络的结果。更改图像变量的值来获取不同的图像:
image = 6
plt.imshow(x_test[image].reshape(28,28),
cmap=plt.get_cmap(‘gray'))
plt.show()
y_pred = model.predict(x_test)
print(“Prediction: {0}”.format(np.argmax(y_pred[image])))
图 6.5:带有稠密网络预测的 MNIST 图像
你可以仅可视化错误的预测,以了解你的模型在哪些地方失败:
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
image = 4
plt.imshow(x_test[incorrect_indices[image]].reshape(28,28),
cmap=plt.get_cmap(‘gray'))
plt.show()
print(“Prediction: {0}”.format(np.argmax(y_pred[incorrect_indices[image]])))
图 6.6:稠密网络错误分类的示例
如你在之前的截图中所见,模型失败了,因为我们预测的类别是 2,而正确的类别是 3。
卷积神经网络
卷积神经网络(CNN)是指具有卷积层的神经网络。这些卷积层借助卷积滤波器高效地处理原始图像的高维度。CNN 使我们能够识别图像中的复杂模式,这是简单神经网络无法做到的。CNN 还可以用于自然语言处理。
CNN 的前几层是卷积层,网络在这些层中应用不同的滤波器来寻找图像中的有用模式;接着是池化层,它们有助于下采样卷积层的输出。激活层控制信号从一层流向下一层,模拟我们大脑中的神经元。网络中的最后几层是全连接层;这些层与我们在之前练习中使用的层相同。
卷积层
卷积层由多个滤波器组成,它们在看到初始层中的特征、边缘或颜色时会激活,最终能够识别出面孔、蜂窝图案和车轮等。这些滤波器就像我们常用的 Instagram 滤镜一样。滤镜通过以某种方式改变像素来改变图像的外观。以一个检测水平边缘的滤波器为例。
图 6.7:水平边缘检测滤波器
如前面截图所示,滤波器将图像转换成另一幅图像,其中水平线被突出显示。为了得到这种转换,我们将图像的部分区域与滤波器逐一相乘。首先,我们取图像的左上角 3x3 区域,并与滤波器进行矩阵乘法,得到转换后的第一个左上角像素。然后,我们将滤波器向右移动一个像素,得到转换后的第二个像素,依此类推。转换后的图像是一幅只突出显示水平线部分的图像。滤波器参数的值(此处为 9)是卷积层在训练过程中学习的权重或参数。有些滤波器可能学会检测水平线,有些则是垂直线,或者是 45 度角的线。随后的层将学习到更复杂的结构,比如车轮或人脸的模式。
这里列出了一些卷积层的超参数:
-
滤波器:这是网络中每层的滤波器数量。这个数字也反映了转换的维度,因为每个滤波器将导致输出的一个维度。
-
滤波器大小:这是网络将学习的卷积滤波器的大小。这个超参数将决定输出转换的大小。
-
步长:在前述水平边缘示例中,我们每次通过都将滤波器移动一个像素。这就是步长。它指的是滤波器每次通过时移动的量。这个超参数还决定了输出转换的大小。
-
填充:这是一个超参数,使网络在图像的所有边缘填充零。在某些情况下,这有助于保留边缘信息,并确保输入和输出的大小相同。
注意
如果进行填充,则得到的图像大小与卷积操作的输出相同或更大。如果不进行填充,则图像大小将会减小。
池化层
池化层 将输入图像的大小减小,以减少网络中的计算量和参数。池化层周期性地插入到卷积层之间,以控制过拟合。最常见的池化变体是 2 x 2 最大池化,步长为 2。此变体通过下采样输入,保留输出中四个像素的最大值。深度维度保持不变。
图 6.8:最大池化操作
在过去,我们也进行过平均池化,但是现在更常见的是使用最大池化,因为在实践中已经证明其效果更好。许多数据科学家不喜欢使用池化层,仅仅是因为池化操作会伴随信息的丢失。关于这个主题已经有一些研究,发现在某些时候,简单的没有池化层的架构能够超越最先进的模型。为了减少输入的大小,建议偶尔在卷积层中使用更大的步长。
注意
研究论文 Striving for Simplicity: The All Convolutional Net 评估具有池化层的模型,发现在有足够数据可用时,池化层并不总是能够提高网络的性能。有关更多信息,请阅读 Striving for Simplicity: The All Convolutional Net 论文:https://arxiv.org/abs/1412.6806
Adam 优化器
优化器通过损失函数更新权重。选择错误的优化器或优化器的错误超参数可能会导致在找到问题的最优解时延迟。
Adam 的名称源自自适应矩估计。Adam 是专门设计用于训练深度神经网络的优化器。由于其快速接近最优解的速度,Adam 在数据科学社区中被广泛使用。因此,如果您想要快速收敛,请使用 Adam 优化器。但是,Adam 并不总是导致最优解;在这种情况下,带有动量的 SGD 有助于实现最先进的结果。以下是参数:
-
学习率:这是优化器的步长。较大的值(0.2)会导致更快的初始学习速度,而较小的值(0.00001)会在训练过程中减慢学习速度。
-
Beta 1:这是梯度均值估计的指数衰减率。
-
Beta 2:这是梯度的未中心化方差估计的指数衰减率。
-
Epsilon:这是一个非常小的数值,用于防止除零错误。
对于深度学习问题,一个好的起始点是学习率 = 0.001,Beta 1 = 0.9,Beta 2 = 0.999,Epsilon = 10^-8。
注意
欲了解更多信息,请阅读 Adam 论文:arxiv.org/abs/1412.6980v8
交叉熵损失
交叉熵损失用于分类问题中,其中每个类别的输出是介于 0 和 1 之间的概率值。这里的损失随着模型偏离实际值而增加;它遵循一个负对数图形。当模型预测的概率远离实际值时,这种损失尤为有效。例如,如果真实标签的概率是 0.05,我们会给模型一个很大的惩罚损失。另一方面,如果真实标签的概率是 0.40,我们则给予它较小的惩罚损失。
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_06_09.jpg)
图 6.9:对数损失与概率的关系图
上面的图表显示,当预测值远离真实标签时,损失会呈指数增长。交叉熵损失遵循的公式如下:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_06_12.jpg)
图 6.10:交叉熵损失公式
M是数据集中的类别数(对于 MNIST 来说是 10),y是真实标签,p是该类别的预测概率。我们偏好使用交叉熵损失来进行分类,因为随着我们接近真实值,权重更新会变得越来越小。交叉熵损失只会惩罚正确类别的概率。
练习 51:使用 CNN 对 MNIST 进行分类
在这个练习中,我们将使用 CNN 对修改后的国家标准与技术研究院(MNIST)数据集进行分类,而不是像练习 50中那样使用全连接层。我们将完整的图像作为输入,得到图像上的数字作为输出:
-
使用 Keras 库加载 MNIST 数据集:
from keras.datasets import mnist (x_train, y_train), (x_test, y_test) = mnist.load_data()
-
将二维数据转换为三维数据,第三维只有一层,这是 Keras 要求的输入格式:
x_train = x_train.reshape(-1, 28, 28, 1) x_test = x_test.reshape(-1, 28, 28, 1)
-
将目标变量转换为一个独热编码向量,这样我们的网络就不会在不同的目标变量之间形成不必要的连接:
from sklearn.preprocessing import LabelBinarizer label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test)
-
创建模型。这里,我们构建了一个小型 CNN。你可以尝试其他架构:
from keras.models import Model, Sequential from keras.layers import Dense, Conv2D, MaxPool2D, Flatten model = Sequential()
添加卷积层:
model.add(Conv2D(32, kernel_size=3, padding=”same”,input_shape=(28, 28, 1), activation = ‘relu')) model.add(Conv2D(32, kernel_size=3, activation = ‘relu'))
添加池化层:
model.add(MaxPool2D(pool_size=(2, 2)))
-
将二维矩阵展平成一维向量:
model.add(Flatten())
-
使用全连接层作为模型的最后几层:
model.add(Dense(128, activation = “relu”)) model.add(Dense(10, activation = “softmax”)) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) model.summary()
为了完全理解这一点,请查看模型输出的以下截图:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_06_10.jpg)
图 6.11:CNN 的模型架构
-
训练模型并检查最终的准确度:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=10, batch_size=1024) score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))
输出如下:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_06_12.jpg)
图 6.12:最终模型准确率
恭喜你!现在你已经创建了一个能够以 98.62% 的准确率预测图像上数字的模型。你可以使用练习 50中提供的代码绘制不同的测试图像,并查看你的网络结果。还可以绘制错误预测,看看模型哪里出错:
import numpy as np
import matplotlib.pyplot as plt
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
image = 4
plt.imshow(x_test[incorrect_indices[image]].reshape(28,28),
cmap=plt.get_cmap(‘gray'))
plt.show()
print(“Prediction: {0}”.format(np.argmax(y_pred[incorrect_indices[image]])))
图 6.13:模型的错误预测;真实标签为 2
如你所见,模型在预测模糊的图像时遇到了困难。你可以尝试调整层和超参数,看看是否能获得更好的准确率。尝试用更高步幅的卷积层替代池化层,正如前面一节中建议的那样。
正则化
正则化是一种通过修改学习算法帮助机器学习模型更好地泛化的技术。它有助于防止过拟合,并使我们的模型在训练过程中未见过的数据上表现得更好。在本节中,我们将学习可用的不同正则化方法。
丢弃层
丢弃(Dropout)是一种我们用来防止神经网络模型过拟合的正则化技术。在训练过程中,我们随机忽略网络中的神经元。这样可以防止这些神经元的激活信号继续传播下去,且在反向传播时这些神经元的权重更新不会被应用。神经元的权重被调节来识别特定的特征,而与它们相邻的神经元则变得依赖于这些特征,这可能会导致过拟合,因为这些神经元可能会过于专门化于训练数据。当神经元被随机丢弃时,相邻的神经元会介入并学习这些表示,从而使网络学习到多种不同的表示。这使得网络能更好地进行泛化,并防止模型过拟合。一个需要注意的重要事项是,当你进行预测或测试模型时,不应使用丢弃层。这会使模型失去宝贵的信息,并导致性能下降。Keras 会自动处理这个问题。
在使用丢弃层时,建议创建更大的网络,因为这能为模型提供更多学习的机会。我们通常使用 0.2 到 0.5 之间的丢弃概率。该概率指的是神经元在训练过程中被丢弃的概率。每层之后使用丢弃层通常能得到较好的效果,因此你可以从每层之后放置一个丢弃层,概率设置为 0.2,然后从那里进行微调。
要在 Keras 中创建一个丢弃层(dropout layer),且其概率为 0.5,你可以使用以下函数:
keras.layers.Dropout(0.5)
图 6.14:在密集神经网络中可视化丢弃
L1 和 L2 正则化
L2 是最常见的正则化类型,其次是 L1。这些正则化器通过向模型的损失中添加一个项来工作,以获得最终的代价函数。这一额外的项会导致模型的权重减少,从而使模型具有良好的泛化能力。
L1 正则化的代价函数如下所示:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_06_15.jpg)
图 6.15:L1 正则化的代价函数
这里,λ 是正则化参数。L1 正则化会导致权重非常接近零。这使得应用 L1 正则化的神经元仅依赖于最重要的输入,并忽略噪声输入。
L2 正则化的代价函数如下所示:
图 6.16:L2 正则化的代价函数
L2 正则化对高权重向量进行重罚,并偏好扩散的权重。L2 正则化也被称为 权重衰减,因为它迫使网络的权重衰减到接近零,但与 L1 正则化不同,L2 正则化并不会完全将权重压缩到零。我们可以将 L1 和 L2 正则化结合使用。要实现这些正则化器,您可以在 Keras 中使用以下函数:
keras.regularizers.l1(0.01)
keras.regularizers.l2(0.01)
keras.regularizers.l1_l2(l1=0.01, l2=0.01)
批量归一化
在第一章,数据科学与数据预处理简介中,我们学习了如何进行归一化,以及它如何帮助加速我们机器学习模型的训练。在这里,我们将对神经网络的每一层应用相同的归一化方法。批量归一化允许各层独立学习,而不受其他层的影响。它通过将层的输入标准化,使其具有固定的均值和方差来实现这一点;这可以防止前一层的参数变化对当前层的输入产生过大影响。它还有一定的正则化作用;类似于 dropout,它防止过拟合,但它是通过在小批量的值中引入噪声来实现的。在使用批量归一化时,请确保使用较低的 dropout,这样更好,因为 dropout 会导致信息丢失。然而,不要完全依赖批量归一化而去除 dropout,因为两者结合使用效果更好。使用批量归一化时,可以使用较高的学习率,因为它确保了不会有动作过大或过小。
图 6.17:批量归一化方程
这里,(xi) 是层的输入,y 是标准化后的输入。μ 是批量均值,σ2 是批量的标准差。批量归一化引入了两个新的(x_i)̂损失。
要在 Keras 中创建批量归一化层,您可以使用以下函数:
keras.layers.BatchNormalization()
练习 52:使用正则化改进图像分类,使用 CIFAR-10 图像
在本次练习中,我们将对加拿大高级研究院(CIFAR-10)数据集进行分类。该数据集包含 60,000 张 32 x 32 的彩色图像,分为 10 类。这 10 类分别是:鸟类、飞机、猫、汽车、青蛙、鹿、狗、卡车、船和马。它是机器学习研究中最广泛使用的数据集之一,主要用于卷积神经网络(CNN)领域。由于图像的分辨率较低,模型可以在这些图像上更快速地训练。我们将使用该数据集实现我们在上一节中学到的一些正则化技术:
注
若要获取原始 CIFAR-10 文件和 CIFAR-100 数据集,请访问 https://www.cs.toronto.edu/~kriz/cifar.html。
-
使用 Keras 库加载 CIFAR-10 数据集:
from keras.layers import Dense, Conv2D, MaxPool2D, Flatten, Dropout, BatchNormalization from keras.datasets import cifar10 (x_train, y_train), (x_test, y_test) = cifar10.load_data()
-
检查数据的维度:
x_train.shape
输出结果如下:
图 6.18:x 的维度
相似的维度,针对
y
:y_train.shape
输出结果如下:
图 6.19:y 的维度
由于这些是彩色图像,它们有三个通道。
-
将数据转换为 Keras 所需的格式:
x_train = x_train.reshape(-1, 32, 32, 3) x_test = x_test.reshape(-1, 32, 32, 3)
-
将目标变量转换为 one-hot 向量,以确保网络在不同的目标变量之间不会形成不必要的连接:
from sklearn.preprocessing import LabelBinarizer label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test)
-
创建模型。在这里,我们首先创建一个不带正则化的小型 CNN:
from keras.models import Sequential model = Sequential()
添加卷积层:
model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32,32,3))) model.add(Conv2D(32, (3, 3), activation='relu'))
添加池化层:
model.add(MaxPool2D(pool_size=(2, 2)))
-
将 2D 矩阵展平为 1D 向量:
model.add(Flatten())
-
使用密集层作为模型的最终层并编译模型:
model.add(Dense(512, activation='relu')) model.add(Dense(10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc'])
-
训练模型并检查最终准确度:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=10, batch_size=512)
-
现在检查模型的准确度:
score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))
输出结果如下:
图 6.20:模型的准确度
-
现在创建相同的模型,但加入正则化。你也可以尝试其他架构:
model = Sequential()
添加卷积层:
model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32,32,3))) model.add(Conv2D(32, (3, 3), activation='relu'))
添加池化层:
model.add(MaxPool2D(pool_size=(2, 2)))
-
添加批量归一化层和 Dropout 层:
model.add(BatchNormalization()) model.add(Dropout(0.10))
-
将 2D 矩阵展平为 1D 向量:
model.add(Flatten())
-
使用密集层作为模型的最终层并编译模型:
model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc']) model.summary()
图 6.21:带正则化的 CNN 架构
-
训练模型并检查最终准确度:
model.fit(x_train, y_train, validation_data = (x_test, y_test), epochs=10, batch_size=512) score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))
输出结果如下:
图 6.22:最终准确度输出
恭喜!你通过使用正则化使得模型比以前表现得更好。如果你的模型没有看到改善,尝试将训练时间延长,增加更多的训练轮数。你也会发现,可以训练更多的轮次而不必担心过拟合。
你可以绘制不同的测试图像,并使用练习 50 中给出的代码查看网络的结果。同时,绘制错误预测,看看模型哪里出错:
import numpy as np
import matplotlib.pyplot as plt
y_pred = model.predict(x_test)
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
labels = [‘airplane', ‘automobile', ‘bird', ‘cat', ‘deer', ‘dog', ‘frog', ‘horse', ‘ship', ‘truck']
image = 3
plt.imshow(x_test[incorrect_indices[image]].reshape(32,32,3))
plt.show()
print(“Prediction: {0}”.format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
图 6.23:模型的错误预测
如你所见,模型在预测模糊图像时遇到了困难。真实标签是马。你可以尝试调整层和超参数,看看是否能提高准确率。尝试创建更复杂的模型并进行正则化,训练更长时间。
图像数据预处理
本节将介绍数据科学家可以用来预处理图像的一些技术。首先,我们将介绍图像归一化,然后学习如何将彩色图像转换为灰度图像。最后,我们将探讨如何将数据集中的所有图像调整为相同尺寸。预处理图像是必要的,因为数据集中的图像大小不同,我们需要将它们转换为标准大小,以便在其上训练机器学习模型。一些图像预处理技术通过简化模型识别重要特征或通过减少维度(如灰度图像的情况)来帮助减少模型的训练时间。
归一化
对于图像而言,像素的规模在同一量级,范围是 0 到 255。因此,这一步归一化是可选的,但它可能有助于加速学习过程。再重申一下,数据中心化并将其缩放到相同的量级,有助于确保梯度不会失控。神经网络共享参数(神经元)。如果输入数据没有缩放到相同的量级,那么网络学习将变得困难。
转换为灰度图像
根据数据集和问题的不同,你可以将图像从 RGB 转换为灰度图像。这有助于网络更快地工作,因为它需要学习的参数要少得多。根据问题类型,你可能不希望这样做,因为这会导致丢失图像颜色所提供的信息。要将 RGB 图像转换为灰度图像,可以使用Pillow库:
from PIL import Image
image = Image.open(‘rgb.png').convert(‘LA')
image.save(‘greyscale.png')
图 6.24:转换为灰度的汽车图像
将所有图像调整为相同大小
在处理现实生活中的数据集时,你会经常遇到一个主要的挑战,那就是数据集中的所有图像大小可能不相同。你可以根据情况执行以下步骤来解决这个问题:
resize
函数是用于获取调整大小后新像素的算法。双三次插值算法速度较快,是上采样时最好的像素重采样算法之一。
图 6.25:上采样的汽车图像
resize
函数是用于获取调整大小后新像素的算法,如前所述。抗锯齿算法有助于平滑像素化的图像。它比双三次插值算法效果更好,但速度较慢。抗锯齿是最适合下采样的像素重采样算法之一。
图 6.26:下采样的汽车图像
-
裁剪:将所有图像裁剪为相同大小的另一种方法是裁剪它们。如前所述,可以使用不同的中心来防止信息丢失。你可以使用以下代码裁剪图像:
area = (1000, 500, 2500, 2000) cropped_img = img.crop(area)
图 6.27:裁剪后的汽车图像
-
填充:填充是指在图像周围添加一层零或一的边界,以增加图像的大小。执行填充时,请使用以下代码:
size = (2000,2000) back = Image.new(“RGB”, size, “white”) offset = (250, 250) back.paste(cropped_img, offset)
图 6.28:裁剪后的填充汽车图像
其他有用的图像操作
Pillow 库提供了许多用于修改和创建新图像的功能。这些功能将帮助我们从现有的训练数据中创建新图像。
要翻转图像,可以使用以下代码:
img.transpose(Image.FLIP_LEFT_RIGHT)
图 6.29:翻转后的裁剪汽车图像
要将图像旋转 45 度,可以使用以下代码:
img.rotate(45)
图 6.30:旋转 45 度后的裁剪汽车图像
要将图像平移 1,000 像素,可以使用以下代码:
import PIL
width, height = img.size
image = PIL.ImageChops.offset(img, 1000, 0)
image.paste((0), (0, 0, 1000, height))
图 6.31:旋转后的裁剪汽车图像
活动 17:预测图像是猫还是狗
在本活动中,我们将尝试预测提供的图像是猫还是狗。微软提供的猫狗数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter06)包含了 25,000 张猫和狗的彩色图像。假设你在一家兽医诊所工作,诊所里有两位兽医,一位专门治疗狗,另一位专门治疗猫。你希望通过判断下一位客户是狗还是猫,来自动安排兽医的预约。为此,你创建了一个 CNN 模型:
-
加载狗与猫数据集并预处理图像。
-
使用图像文件名找到每个图像的猫或狗标签。第一张图像应该是这样的:
图 6.32:狗与猫类别的第一张图像
-
获取形状正确的图像以进行训练。
-
创建一个使用正则化的 CNN。
注意
本活动的解决方案可以在第 369 页找到。
你应该发现该模型的测试集准确率为 70.4%。训练集准确率非常高,约为 96%。这意味着模型已经开始出现过拟合。改进模型以获得最佳准确率的任务留给你作为练习。你可以使用前面练习中的代码绘制错误预测的图像,从而了解模型的表现:
import matplotlib.pyplot as plt
y_pred = model.predict(x_test)
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
labels = [‘dog', ‘cat']
image = 5
plt.imshow(x_test[incorrect_indices[image]].reshape(50,50), cmap=plt.get_cmap(‘gray'))
plt.show()
print(“Prediction: {0}”.format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
图 6.33:常规 CNN 模型错误地预测为狗
数据增强
在训练机器学习模型时,我们数据科学家经常遇到类别不平衡和训练数据不足的问题。这导致模型性能不佳,在实际应用中表现差强人意。应对这些问题的一种简单方法是数据增强。数据增强有多种方式,例如旋转图像、平移物体、裁剪图像、剪切扭曲图像、放大图像的某部分,以及更复杂的方法,如使用生成对抗网络(GANs)生成新图像。GAN 只是两个相互竞争的神经网络。生成器网络试图生成与已有图像相似的图像,而判别器网络则尝试判断图像是生成的还是原始数据的一部分。训练完成后,生成器网络能够创造出并非原始数据的一部分,但与真实拍摄的图像相似,几乎可以误认为是摄像机拍摄的图像。
注意
你可以在这篇论文中了解更多关于 GAN 的信息:https://arxiv.org/abs/1406.2661。
图 6.34:左侧是一个由生成对抗网络(GAN)生成的假图像,而右侧是一个真实人物的图像
注意
来源:http://www.whichfaceisreal.com
回到传统的图像增强方法,我们执行之前提到的操作,如翻转图像,然后在原始图像和变换后的图像上训练我们的模型。假设我们有以下左侧的翻转猫图像:
图 6.35:右侧是猫的正常图像,左侧是翻转后的图像
现在,一个在左侧图像上训练的机器学习模型可能会很难将右侧翻转后的图像识别为猫的图像,因为它朝向相反。这是因为卷积层被训练成只检测朝左看的猫图像。它已经对身体的不同特征位置建立了规则。
因此,我们在所有增强后的图像上训练我们的模型。数据增强是获得 CNN 模型最佳结果的关键。我们利用 Keras 中的ImageDataGenerator
类轻松执行图像增强。你将在下一节中了解更多关于生成器的内容。
生成器
在上一章中,我们讨论了大数据集如何由于 RAM 的限制而导致训练问题。当处理图像时,这个问题会更严重。Keras 实现了生成器,帮助我们在训练时动态获取输入图像及其相应标签。这些生成器还帮助我们在训练前对图像进行数据增强。首先,我们将看看如何利用ImageDataGenerator
类为我们的模型生成增强后的图像。
为了实现数据增强,我们只需要稍微修改我们的练习 3 代码。我们将用以下代码替代model.fit()
:
BATCH_SIZE = 32
aug = ImageDataGenerator(rotation_range=20,
width_shift_range=0.2, height_shift_range=0.2,
shear_range=0.15, zoom_range=0.15,
horizontal_flip=True, vertical_flip=True,
fill_mode=”nearest”)
log = model.fit_generator(
aug.flow(x_train, y_train, batch_size= BATCH_SIZE),
validation_data=( x_test, y_test), steps_per_epoch=len(x_train) // BATCH_SIZE, epochs=10)
现在我们来看一下ImageDataGenerator
实际在做什么:
-
rotation_range
:此参数定义图像可以旋转的最大角度。旋转是随机的,可以小于该值的任何数值。这确保了没有两张图像是相同的。 -
width_shift_range
/height_shift_range
:该值定义了图像可以移动的范围。如果值小于 1,则认为它是总宽度的一个比例;如果大于 1,则表示像素数。范围将在(-shift_range
,+ shift_range
)区间内。 -
shear_range
:这是剪切角度,单位为度(逆时针方向)。 -
zoom_range
:这里的值可以是[lower_range
,upper_range
],或者是浮动值,表示[1-zoom_range
,1+zoom_range
],这是随机缩放的范围。 -
horizontal_flip
/vertical_flip
:此处的布尔值为真时,生成器会随机水平或垂直翻转图像。 -
fill_mode
:这帮助我们决定在旋转和剪切过程中产生的空白区域应填充什么内容。constant
:此选项将用常数值填充空白区域,常数值需要通过cval
参数定义。nearest
:这会用最近的像素填充空白区域。reflect
:这会产生反射效果,就像镜子一样。wrap
:这会使图像环绕并填充空白区域。
生成器会随机应用前述操作到它遇到的所有图像上。这确保了模型不会看到相同的图像两次,从而减轻过拟合问题。在使用生成器时,我们需要使用fit_generator()
函数,而不是fit()
函数。我们根据训练时可用的内存大小,向生成器传递合适的批处理大小。
默认的 Keras 生成器有一些内存开销;为了去除这些开销,你可以创建自己的生成器。为此,你需要确保实现生成器的以下四个部分:
-
读取输入图像(或任何其他数据)。
-
读取或生成标签。
-
对图像进行预处理或增强。
注意
确保随机增强图像。
-
以 Keras 所期望的形式生成输出。
这里提供了一个示例代码,帮助你创建自己的生成器:
def custom_image_generator(images, labels, batch_size = 128):
while True:
# Randomly select images for the batch batch_images = np.random.choice(images,
size = batch_size) batch_input = [] batch_output = []
# Read image, perform preprocessing and get labels
for image in batch_images:
# Function that reads and returns the image
input = get_input(image)
# Function that gets the label of the image
output = get_output(image,labels =labels)
# Function that pre-processes and augments the image
input = preprocess_image(input)
batch_input += [input] batch_output += [output]
batch_x = np.array( batch_input ) batch_y = np.array( batch_output )
# Return a tuple of (images,labels) to feed the network yield(batch_x, batch_y)
实现 get_input
、get_output
和 preprocess_image
被留作练习。
练习 53:使用图像增强对 CIFAR-10 图像进行分类
在本练习中,我们将对 CIFAR-10(加拿大高级研究院)数据集进行分类,类似于练习 52。在这里,我们将使用生成器来增强训练数据。我们将随机旋转、平移和翻转图像:
-
使用 Keras 库加载 CIFAR-10 数据集:
from keras.datasets import cifar10 (x_train, y_train), (x_test, y_test) = cifar10.load_data()
-
将数据转换为 Keras 所需的格式:
x_train = x_train.reshape(-1, 32, 32, 3) x_test = x_test.reshape(-1, 32, 32, 3)
-
将目标变量转换为 one-hot 向量,以便我们的网络不会在不同的目标变量之间形成不必要的连接:
from sklearn.preprocessing import LabelBinarizer label_binarizer = LabelBinarizer() label_binarizer.fit(range(10)) y_train = label_binarizer.transform(y_train) y_test = label_binarizer.transform(y_test)
-
创建模型。我们将使用练习 3中的网络:
from keras.models import Sequential model = Sequential()
添加卷积层:
from keras.layers import Dense, Dropout, Conv2D, MaxPool2D, Flatten, BatchNormalization model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32,32,3))) model.add(Conv2D(32, (3, 3), activation='relu'))
添加池化层:
model.add(MaxPool2D(pool_size=(2, 2)))
添加批归一化层,并附加一个丢弃层:
model.add(BatchNormalization()) model.add(Dropout(0.10))
-
将 2D 矩阵展平为 1D 向量:
model.add(Flatten())
-
使用全连接层作为模型的最后一层:
model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(10, activation='softmax'))
-
使用以下代码编译模型:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = [‘acc'])
-
创建数据生成器并传递所需的增强方式:
from keras.preprocessing.image import ImageDataGenerator datagen = ImageDataGenerator( rotation_range=45, width_shift_range=0.2, height_shift_range=0.2, horizontal_flip=True)
-
训练模型:
BATCH_SIZE = 128 model_details = model.flow(datagen.flow(x_train, y_train, batch_size = BATCH_SIZE), steps_per_epoch = len(x_train) // BATCH_SIZE, epochs = 10, validation_data= (x_test, y_test), verbose=1)
-
检查模型的最终准确度:
score = model.evaluate(x_test, y_test) print(“Accuracy: {0:.2f}%”.format(score[1]*100))
输出如下所示:
图 6.36:模型准确度输出
恭喜!你已经使用数据增强让你的模型识别更广泛的图像。你一定注意到模型的准确率下降了。这是因为我们训练模型的 epochs 数量较少。使用数据增强的模型需要更多的 epochs 来训练。你还会看到,即使训练更多 epochs 也不用担心过拟合。这是因为每个 epoch,模型看到的数据都是新图像,数据集中的图像很少重复,甚至几乎不重复。如果你训练更多 epochs,一定会看到进展。试着尝试更多的架构和增强方式。
这里你可以看到一个错误分类的图像。通过检查错误识别的图像,你可以评估模型的表现,并找出其表现不佳的地方。
y_pred = model.predict(x_test)
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
labels = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
image = 2
plt.imshow(x_test[incorrect_indices[image]].reshape(32,32,3))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
请查看以下截图以检查错误预测:
图 6.37:基于增强数据训练的 CNN 模型的错误预测
活动 18:识别和增强图像
在本活动中,我们将尝试预测图像是猫还是狗,类似于活动 17。不过这次我们将使用生成器来处理图像,并对它们进行数据增强,以获得更好的结果:
-
创建函数以获取每个图像和每个图像标签。然后,创建一个函数来预处理加载的图像并对其进行增强。最后,创建一个数据生成器(如生成器部分所示),利用上述函数在训练期间将数据传递给 Keras。
-
加载未增强的测试数据集。使用活动 17中的函数。
-
创建一个 CNN 模型,用于识别给定的图像是猫还是狗。确保使用正则化。
注意
本活动的解决方案可以在第 373 页找到。
你应该会发现该模型的测试集准确度大约为 72%,相比活动 17中的模型有所提升。你还会观察到训练集的准确度非常高,约为 98%。这意味着该模型开始出现过拟合,就像活动 17中的模型一样。这可能是由于数据增强不足造成的。尝试更改数据增强参数,看看准确度是否有所变化。或者,你可以修改神经网络的架构,以获得更好的结果。你可以绘制出错误预测的图像,了解模型的表现如何。
import matplotlib.pyplot as plt
y_pred = model.predict(validation_data[0])
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(validation_data[1],axis=1))[0]
labels = ['dog', 'cat']
image = 7
plt.imshow(validation_data[0][incorrect_indices[image]].reshape(50,50), cmap=plt.get_cmap('gray'))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
下图展示了一个例子:
图 6.38:数据增强 CNN 模型错误预测为猫
总结
本章中,我们学习了数字图像是什么以及如何使用它们创建机器学习模型。然后,我们讲解了如何使用 Keras 库训练图像的神经网络模型。我们还介绍了什么是正则化,如何在神经网络中使用正则化,什么是图像增强,以及如何使用它。我们探讨了 CNN 是什么以及如何实现 CNN。最后,我们讨论了各种图像预处理技术。
现在你已经完成了本章内容,你将能够处理任何类型的数据来创建机器学习模型。在下一章中,我们将学习如何处理人类语言。
第十七章:第七章
处理人类语言
学习目标
到本章结束时,您将能够:
-
为文本数据创建机器学习模型
-
使用 NLTK 库对文本进行预处理
-
使用正则表达式清洗和分析字符串
-
使用 Word2Vec 模型创建词向量
本章将介绍处理人类语言的相关概念。
介绍
人工智能(AI)最重要的目标之一是理解人类语言,以执行任务。拼写检查、情感分析、问答、聊天机器人和虚拟助手(如 Siri 和 Google 助手)都具有自然语言处理(NLP)模块。NLP 模块使虚拟助手能够处理人类语言并根据语言执行相应的操作。例如,当我们说“OK Google,设定一个早上 7 点的闹钟”时,语音首先被转换为文本,然后由 NLP 模块处理。处理完毕后,虚拟助手会调用闹钟/时钟应用程序的适当 API。处理人类语言有其自身的挑战,因为语言具有歧义性,词汇的意义取决于其所处的上下文。这是 AI 语言处理中的最大痛点。
另一个重要原因是完整信息的缺乏。在交流时,我们往往省略大部分信息;这些信息通常是常识,或者是普遍真实或错误的事情。例如,句子“I saw a man on a hill with a telescope”根据上下文信息的不同,可以有不同的含义。例如,它可能意味着“我看到一个手持望远镜的男人站在山上”,也可能意味着“我通过望远镜看到一个站在山上的男人”。计算机很难跟踪这些信息,因为大部分是上下文性的。由于深度学习的进步,今天的自然语言处理(NLP)比我们以前使用传统方法(如聚类和线性模型)时更为有效。这也是我们将使用深度学习来处理文本语料库解决 NLP 问题的原因。像其他任何机器学习问题一样,NLP 也有两个主要部分:数据处理和模型创建。在接下来的内容中,我们将学习如何处理文本数据,随后我们将学习如何使用这些处理过的数据来创建机器学习模型,以解决我们的实际问题。
文本数据处理
在我们开始为文本数据构建机器学习模型之前,我们需要对数据进行处理。首先,我们将学习不同的方法来理解数据的组成。这有助于我们了解数据的真正内容,并决定下一步要使用的预处理技术。接下来,我们将学习有助于预处理数据的技术。这一步有助于减少数据的大小,从而缩短训练时间,并帮助我们将数据转换为机器学习算法更易于提取信息的形式。最后,我们将学习如何将文本数据转换为数字,以便机器学习算法可以实际使用这些数据来创建模型。我们通过词嵌入来实现这一点,就像我们在第五章:“掌握结构化数据”中进行的实体嵌入一样。
正则表达式
在我们开始处理文本数据之前,我们需要了解正则表达式(RegEx)。正则表达式并不真正属于预处理技术,而是一串定义字符串中搜索模式的字符。正则表达式是处理文本数据时的强大工具,它帮助我们在文本集合中查找特定的序列。正则表达式由元字符和普通字符组成。
图 7.1:包含在正则表达式中使用的元字符的表格,以及一些示例
使用正则表达式,我们可以在文本中搜索复杂的模式。例如,我们可以用它来从文本中删除 URL。我们可以使用 Python 的re
模块删除 URL,如下所示:
re.sub(r"https?\://\S+\s", '', "https://www.asfd.com hello world")
re.sub
接受三个参数:第一个是正则表达式,第二个是你想要替换匹配模式的表达式,第三个是它应该搜索该模式的文本。
命令的输出如下:
图 7.2:输出命令
注意
记住所有正则表达式的约定很困难,因此在使用正则表达式时,参考备忘单是个不错的选择,例如: (http://www.pyregex.com/)。
练习 54:使用正则表达式进行字符串清理
在这个练习中,我们将使用 Python 的re
模块来修改和分析字符串。在本练习中,我们将简单地学习如何使用正则表达式,接下来的部分我们将展示如何使用正则表达式来预处理数据。我们将使用 IMDB 电影评论数据集中的一条评论(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter07),稍后我们将在本章中利用它来创建情感分析模型。该数据集已经经过处理,一些单词已被删除。处理预构建数据集时,通常会出现这种情况,因此在开始工作之前,了解你所使用的数据集是非常重要的。
-
在这个练习中,我们将使用来自 IMDB 的电影评论。将评论文本保存到一个变量中,如以下代码所示。你也可以使用任何其他段落的文本进行这个练习:
string = "first think another Disney movie, might good, it's kids movie. watch it, can't help enjoy it. ages love movie. first saw movie 10 8 years later still love it! Danny Glover superb could play part better. Christopher Lloyd hilarious perfect part. Tony Danza believable Mel Clark. can't help, enjoy movie! give 10/10!<br /><br />- review Jamie Robert Ward (http://www.invocus.net)"
-
计算评论的长度,以确定我们需要减少多大的大小。我们将使用
len(string)
并获得输出,如以下代码所示:len(string)
输出长度如下:
图 7.3:字符串长度
-
有时,当你从网站抓取数据时,超链接也会被记录下来。大多数情况下,超链接不会为我们提供任何有用信息。通过使用复杂的正则表达式字符串(如"
https?\://\S+
"),删除数据中的任何超链接。这将选择任何包含https://
的子字符串:import re string = re.sub(r"https?\://\S+", '', string) string
去除超链接后的字符串如下:
图 7.4:去除超链接后的字符串
-
接下来,我们将从文本中删除
br
HTML 标签,这些标签是我们在读取字符串时观察到的。有时,这些 HTML 标签会被添加到抓取的数据中:string = re.sub(r'<br />', ' ', string) string
去除
br
标签后的字符串如下:图 7.5:去除 br 标签后的字符串
-
现在,我们将从文本中删除所有数字。当数字对我们没有意义时,这有助于减少数据集的大小:
string = re.sub('\d','', string) string
去除数字后的字符串如下所示:
图 7.6:去除数字后的字符串
-
接下来,我们将删除所有特殊字符和标点符号。根据你的问题,这些可能只是占用空间,且不会为机器学习算法提供相关信息。所以,我们使用以下正则表达式模式将它们移除:
string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', '', string) string
去除特殊字符和标点符号后的字符串如下所示:
图 7.7:没有特殊字符的字符串
-
现在,我们将把
can't
替换为cannot
,并将it's
替换为it is
。这有助于减少训练时间,因为唯一单词的数量减少了:string = re.sub(r"can\'t", "cannot", string) string = re.sub(r"it\'s", "it is", string) string
最终的字符串如下:
图 7.8:最终字符串
-
最后,我们将计算清理后字符串的长度:
len(string)
字符串的输出大小如下:
图 7.9:清理后的字符串长度
我们将评论的大小减少了 14%。
-
现在,我们将使用正则表达式分析数据,并获取所有以大写字母开头的单词:
注意
re.findall(r"[A-Z][a-z]*", string)
单词如下:
图 7.10:以大写字母开头的单词
-
要在文本中查找所有一字母和二字母的单词,请使用以下方法:
re.findall(r"\b[A-z]{1,2}\b", string)
输出如下:
图 7.11:一字母和二字母单词
恭喜你!你已经成功使用 re
模块和正则表达式修改并分析了评论字符串。
基本特征提取
基本特征提取帮助我们了解数据的组成。这有助于我们选择进行数据预处理的步骤。基本特征提取包括计算平均词数和特殊字符计数等操作。我们将在本节中使用 IMDB 电影评论数据集作为示例:
data = pd.read_csv('movie_reviews.csv', encoding='latin-1')
让我们看看数据集包含了什么:
data.iloc[0]
输出如下:
图 7.12:SentimentText 数据
SentimentText
变量包含实际评论,Sentiment
变量包含评论的情绪。1
表示正面情绪,0
表示负面情绪。让我们打印出第一条评论,以便了解我们处理的数据:
data.SentimentText[0]
第一条评论如下:
图 7.13:第一条评论
现在,我们将尝试通过获取数据集的关键统计数据来了解我们正在处理的数据类型。
词数
我们可以使用以下代码获取每条评论中的词数:
data['word_count'] = data['SentimentText'].apply(lambda x: len(str(x).split(" ")))
现在,DataFrame 中的 word_count
变量包含评论中的总词数。apply
函数会将 split
函数逐行应用于数据集。现在,我们可以获取每一类评论的平均词数,看看正面评论是否比负面评论有更多的词汇。
mean()
函数计算 pandas 中某列的平均值。对于负面评论,使用以下代码:
data.loc[data.Sentiment == 0, 'word_count'].mean()
负面情绪的平均词数如下:
图 7.14:负面情绪的总词数
对于正面评论,使用以下代码:
data.loc[data.Sentiment == 1, 'word_count'].mean()
正面情绪的平均词数如下:
图 7.15:正面情绪的总词数
我们可以看到,正负情绪的平均词数差异不大。
停用词
停用词是语言中最常见的词汇——例如,“I”、“me”、“my”、“yours”和“the”。大多数时候,这些词汇不会提供句子的实际信息,因此我们将它们从数据集中移除,以减少数据的大小。nltk
库提供了一个可以访问的英文停用词列表。
from nltk.corpus import stopwords
stop = stopwords.words('english')
为了获取这些停用词的计数,我们可以使用以下代码:
data['stop_count'] = data['SentimentText'].apply(lambda x: len([x for x in x.split() if x in stop]))
然后,我们可以使用以下代码查看每个类别的平均停用词数量:
data.loc[data.Sentiment == 0, 'stop_count'].mean()
负面情绪的平均停用词数量如下:
图 7.16:负面情绪的平均停用词数
现在,为了获取正面情绪的停用词数量,我们使用以下代码:
data.loc[data.Sentiment == 1, 'stop_count'].mean()
这里显示的是正面情感的停用词平均数量:
图 7.17:正面情感的停用词平均数量
特殊字符数量
根据你处理的问题类型,你可能需要保留诸如 @
、#
、$
和 *
等特殊字符,或者将它们移除。要做到这一点,你首先需要弄清楚数据集中出现了多少特殊字符。要获取数据集中 ^
、&
、*
、$
、@
和 #
的数量,可以使用以下代码:
data['special_count'] = data['SentimentText'].apply(lambda x: len(re.sub('[^\^&*$@#]+' ,'', x)))
文本预处理
现在我们已经知道了数据的组成部分,我们需要对其进行预处理,以便机器学习算法能够轻松地在文本中找到模式。在本节中,我们将介绍一些用于清理和减少我们输入机器学习算法的数据维度的技术。
小写化
我们执行的第一个预处理步骤是将所有数据转换为小写字母。这可以防止出现同一个词的多个副本。你可以使用以下代码轻松将所有文本转换为小写:
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join(x.lower() for x in x.split()))
apply 函数会将 lower
函数迭代地应用于数据集的每一行。
停用词移除
如前所述,停用词应从数据集中移除,因为它们提供的信息非常有限。停用词不会影响句子的情感。我们执行此步骤是为了去除停用词可能引入的偏差:
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join(x for x in x.split() if x not in stop))
常见词移除
停用词是一些常见的词汇,如 'a'、'an' 和 'the'。然而,在这一步,你将移除数据集中最常见的词。例如,可以从推文数据集中移除的词包括 RT
、@username
和 DM
。首先,找出最常见的词汇:
word_freq = pd.Series(' '.join(data['SentimentText']).split()).value_counts()
word_freq.head()
最常见的词汇是:
图 7.18:推文数据集中最常见的词汇
从输出结果中,我们得到一个提示:文本包含 HTML 标签,这些标签可以去除,从而大大减少数据集的大小。因此,我们首先去除所有 <br />
HTML 标签,然后去除诸如 'movie' 和 'film' 这样的词,这些词对情感分析器的影响不大:
data['SentimentText'] = data['SentimentText'].str.replace(r'<br />','')
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join(x for x in x.split() if x not in ['movie', 'film']))
标点符号和特殊字符移除
接下来,我们从文本中移除所有标点符号和特殊字符,因为它们对文本提供的信息很少。要移除标点符号和特殊字符,可以使用以下正则表达式:
punc_special = r"[^A-Za-z0-9\s]+"
data['SentimentText'] = data['SentimentText'].str.replace(punc_special,'')
正则表达式选择所有字母数字字符和空格。
拼写检查
有时,同一个词的拼写错误会导致我们拥有相同词汇的多个副本。通过使用自动纠正库进行拼写检查,可以纠正这种问题:
from autocorrect import spell
data['SentimentText'] = [' '.join([spell(i) for i in x.split()]) for x in data['SentimentText']]
词干提取
nltk
库:
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
data['SentimentText'] = data['SentimentText'].apply(lambda x: " ".join([stemmer.stem(word) for word in x.split()]))
注意
拼写检查、词干提取和词形还原的处理时间可能会很长,具体取决于数据集的大小,因此,在执行这些步骤之前,请先检查数据集,确保需要进行这些操作。
词形还原
提示
你应该更倾向于使用词形还原(lemmatization)而非词干提取(stemming),因为它更为有效。
nltk
库:
lemmatizer = nltk.stem.WordNetLemmatizer()
data['SentimentText'][:5].apply(lambda x: " ".join([lemmatizer.lemmatize(word) for word in x.split()]))
注意
我们正在减少数据集的维度,原因是“维度灾难”。随着维度(因变量)增加,数据集会变得稀疏。这会导致数据科学技术失效,因为很难对高维特征进行建模以得到正确的输出。随着数据集特征数量的增加,我们需要更多的数据点来进行建模。因此,为了克服高维数据的灾难,我们需要获取更多的数据,这样会增加处理这些数据所需的时间。
分词
nltk
库:
import nltk
nltk.word_tokenize("Hello Dr. Ajay. It's nice to meet you.")
标记后的列表如下:
图 7.19:分词后的列表
如你所见,它将标点符号与单词分开,并检测像“Dr.”这样的复杂词语。
练习 55:预处理 IMDB 电影评论数据集
在这个练习中,我们将预处理 IMDB 电影评论数据集,使其适用于任何机器学习算法。该数据集包含 25,000 条电影评论以及评论的情感(正面或负面)。我们希望通过评论预测情感,因此在进行预处理时需要考虑这一点。
-
使用 pandas 加载 IMDB 电影评论数据集:
import pandas as pd data = pd.read_csv('../../chapter 7/data/movie_reviews.csv', encoding='latin-1')
-
首先,我们将数据集中的所有字符转换为小写字母:
data.SentimentText = data.SentimentText.str.lower()
-
接下来,我们将编写一个
clean_str
函数,在其中使用re
模块清理评论:import re def clean_str(string): string = re.sub(r"https?\://\S+", '', string) string = re.sub(r'\<a href', ' ', string) string = re.sub(r'&', 'and', string) string = re.sub(r'<br />', ' ', string) string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', ' ', string) string = re.sub('\d','', string) string = re.sub(r"can\'t", "cannot", string) string = re.sub(r"it\'s", "it is", string) return string
注意
data.SentimentText = data.SentimentText.apply(lambda x: clean_str(str(x)))
使用 pandas 的 apply 函数对整个数据集进行评论清理。
-
接下来,使用以下代码检查数据集中的词语分布:
pd.Series(' '.join(data['SentimentText']).split()).value_counts().head(10)
排名前 10 的词汇出现频率如下:
图 7.20:排名前 10 的词语
-
从评论中移除停用词:
注意
这将通过首先对评论进行分词,然后移除从
nltk
库加载的停用词来完成。 -
我们将'
movie
'、'film
' 和 'time
' 加入停用词列表,因为它们在评论中出现频率很高,且对理解评论情感没有太大帮助:from nltk.corpus import stopwords from nltk.tokenize import word_tokenize,sent_tokenize stop_words = stopwords.words('english') + ['movie', 'film', 'time'] stop_words = set(stop_words) remove_stop_words = lambda r: [[word for word in word_tokenize(sente) if word not in stop_words] for sente in sent_tokenize(r)] data['SentimentText'] = data['SentimentText'].apply(remove_stop_words)
-
接下来,我们将分词后的内容转回为句子,并去除那些全部由停用词组成的评论:
def combine_text(text): try: return ' '.join(text[0]) except: return np.nan data.SentimentText = data.SentimentText.apply(lambda x: combine_text(x)) data = data.dropna(how='any')
-
下一步是将文本转换为词汇表中的标记,再转换为数字。我们将使用 Keras Tokenizer,因为它能同时完成这两个步骤:
from keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(num_words=250) tokenizer.fit_on_texts(list(data['SentimentText'])) sequences = tokenizer.texts_to_sequences(data['SentimentText'])
-
要获取词汇表的大小,使用以下代码:
word_index = tokenizer.word_index print('Found %s unique tokens.' % len(word_index))
唯一标记的数量如下:
图 7.21:唯一词汇的数量
-
为了减少模型的训练时间,我们将把评论的长度限制在 200 个单词以内。你可以调整这个数字,以找到最适合的准确率。
注意
from keras.preprocessing.sequence import pad_sequences reviews = pad_sequences(sequences, maxlen=200)
-
你应该保存分词器,以便在之后将评论转回为文本:
import pickle with open('tokenizer.pkl', 'wb') as handle: pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)
要预览清理过的评论,运行以下命令:
data.SentimentText[124]
一个清理过的评论如下所示:
图 7.22:已清理的评论
要获取下一步骤的实际输入,请运行以下命令:
reviews[124]
reviews
命令的下一步骤输入大致如下所示:
图 7.23:下一步骤的输入,已清理的评论
恭喜!你已经成功预处理了你的第一个文本数据集。评论数据现在是一个包含 25,000 行(即评论)和 200 列(即单词)的矩阵。接下来,我们将学习如何将这些数据转换为嵌入,以便更容易预测情感。
文本处理
现在我们已经清理好了数据集,将其转化为机器学习模型可以使用的形式。回顾一下第五章,结构化数据的掌握,我们讨论了神经网络无法处理单词,因此我们需要将单词表示为数字才能处理它们。因此,为了执行情感分析等任务,我们需要将文本转换为数字。
所以,我们首先讨论的方法是独热编码(one-hot encoding),它在处理单词时表现较差,因为单词之间存在某些关系,而独热编码却将单词当作彼此独立的来计算。例如,假设我们有三个单词:‘car’(车),‘truck’(卡车),和‘ship’(船)。现在,‘car’在相似度上更接近‘truck’,但它与‘ship’仍然有一定相似性。独热编码未能捕捉到这种关系。
词嵌入也是单词的向量表示,但它们捕捉了每个单词与其他单词之间的关系。获取词嵌入的不同方法将在以下部分中解释。
计数嵌入(Count Embedding)
计数嵌入(Count embedding) 是一种简单的词向量表示,依据单词在文本中出现的次数来构建。假设有一个数据集,其中包含 n 个唯一单词和 M 个不同记录。要获得计数嵌入,你需要创建一个 N x M 矩阵,其中每一行代表一个单词,每一列代表一个记录。矩阵中任何 (n,m) 位置的值将包含单词 n 在记录 m 中出现的次数。
TF-IDF 嵌入(TF-IDF Embedding)
TF-IDF 是一种衡量每个单词在一组单词或文档中重要性的方法。它代表词频-逆文档频率(term frequency-inverse document frequency)。在 TF-IDF 中,单词的重要性随着该单词的频率而增加,但这一重要性会被包含该单词的文档数量所抵消,从而有助于调整某些使用频率较高的单词。换句话说,单词的重要性是通过计算该单词在训练集中的一个数据点的频率来得出的。这一重要性会根据单词在训练集中的其他数据点的出现情况而增加或减少。
TF-IDF 生成的权重由两个术语组成:
- 词频(TF):单词在文档中出现的频率,如下图所示:
图 7.24:词频公式
其中 w 是单词。
- 逆文档频率(IDF):单词提供的信息量,如下图所示:
图 7.25:逆文档频率公式
权重是这两个项的乘积。在 TF-IDF 的情况下,我们用这个权重替代词频,在我们之前用于计数嵌入部分的 N x M 矩阵中。
连续词袋模型嵌入
连续词袋模型(CBOW)通过使用神经网络进行工作。当输入是某个单词的上下文单词时,它预测该单词。神经网络的输入是上下文单词的独热向量。输入单词的数量由窗口参数决定。网络只有一个隐藏层,输出层通过 softmax 激活函数来获取概率。层与层之间的激活函数是线性的,但更新梯度的方法与常规神经网络相同。
语料库的嵌入矩阵是隐藏层和输出层之间的权重。因此,这个嵌入矩阵的维度将是 N x H,其中 N 是语料库中唯一单词的数量,H 是隐藏层节点的数量。由于其概率性质和低内存需求,CBOW 比之前讨论的两种方法表现更好。
图 7.26:CBOW 网络的表示
Skip-gram 嵌入
使用神经网络,skip-gram 根据输入单词预测其周围的单词。这里的输入是单词的独热向量,输出是周围单词的概率。输出单词的数量由窗口参数决定。与 CBOW 类似,这种方法使用一个只有单层隐藏层的神经网络,且所有激活函数均为线性,除了输出层,我们使用 softmax 函数。一个重要的区别是误差的计算方式:为每个被预测的单词计算不同的误差,然后将所有误差加起来得到最终的误差。每个单词的误差是通过将输出概率向量与目标独热向量相减来计算的。
这里的嵌入矩阵是输入层和隐藏层之间的权重矩阵。因此,这个嵌入矩阵的维度将是 H x N,其中 N 是语料库中唯一单词的数量,H 是隐藏层节点的数量。对于较少频繁出现的单词,skip-gram 的表现远好于 CBOW,但通常较慢:
图 7.27:跳字模型的表示
提示
对于词汇较少但样本量大的数据集,使用 CBOW;对于词汇量较大且样本量较小的数据集,使用 skip-gram。
Word2Vec
gensim
库:
model = gensim.models.Word2Vec(
tokens,
iter=5
size=100,
window=5,
min_count=5,
workers=10,
sg=0)
为了训练模型,我们需要将令牌化的句子作为参数传递给gensim
的Word2Vec
类。iter
是训练的轮数,size
指的是隐藏层节点数,也决定了嵌入层的大小。window
是指在训练神经网络时,考虑的上下文单词数。min_count
是指某个单词至少出现多少次才能被考虑。workers
是训练时使用的线程数,sg
是指使用的训练算法,0代表 CBOW,1代表 skip-gram。
要获取训练好的词向量中的唯一词汇数量,可以使用以下代码:
vocab = list(model.wv.vocab)
len(vocab)
在使用这些词向量之前,我们需要确保它们是正确的。为此,我们通过查找相似的单词来验证:
model.wv.most_similar('fun')
输出结果如下:
图 7.28:相似单词
要将你的词向量保存到文件中,请使用以下代码:
model.wv.save_word2vec_format('movie_embedding.txt', binary=False)
要加载预训练的词向量,可以使用这个函数:
def load_embedding(filename, word_index , num_words, embedding_dim):
embeddings_index = {}
file = open(filename, encoding="utf-8")
for line in file:
values = line.split()
word = values[0]
coef = np.asarray(values[1:])
embeddings_index[word] = coef
file.close()
embedding_matrix = np.zeros((num_words, embedding_dim))
for word, pos in word_index.items():
if pos >= num_words:
continue
print(num_words)
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[pos] = embedding_vector
return embedding_matrix
该函数首先读取嵌入文件filename
,并获取文件中所有的嵌入向量。然后,它会创建一个嵌入矩阵,将这些嵌入向量堆叠在一起。num_words
参数限制了词汇表的大小,在 NLP 算法训练时间过长的情况下非常有用。word_index
是一个字典,键是语料库中的唯一单词,值是该单词的索引。embedding_dim
是训练时指定的嵌入向量的大小。
提示
有很多非常好的预训练词向量可供使用。一些流行的包括 GloVe: https://nlp.stanford.edu/projects/glove/ 和 fastText: https://fasttext.cc/docs/en/english-vectors.html
练习 56:使用 Gensim 创建词向量
在本练习中,我们将使用gensim
库创建我们自己的 Word2Vec 词向量。这个词向量将为我们正在使用的 IMDB 电影评论数据集创建。我们将从练习 55的结束部分继续。
-
评审变量包含的是以令牌形式表示的评论,但它们已经被转换成数字。
gensim
的 Word2Vec 要求令牌为字符串形式,因此我们回溯到步骤 6 中将令牌重新转换为句子的部分,即练习 55。data['SentimentText'] [0]
第一条评论的令牌如下:
图 7.29:第一条评论的令牌
-
现在,我们使用
pandas
的apply
函数将每一行的列表转换为单个列表,使用如下代码:data['SentimentText'] = data['SentimentText'].apply(lambda x: x[0])
-
现在,我们将这些预处理过的数据输入到 Word2Vec 中,以创建词向量:
from gensim.models import Word2Vec model = Word2Vec( data['SentimentText'], iter=50, size=100, window=5, min_count=5, workers=10)
-
让我们通过查看一些相似的单词来检查模型的表现:
model.wv.most_similar('insight')
数据集中与'
insight
'最相似的词语是:图 7.30:与'insight'相似的词语
-
要获得两个词之间的相似度,请使用:
model.wv.similarity(w1='violent', w2='brutal')
-
这里显示了相似度的输出结果:
图 7.31:相似度输出
相似度分数的范围是从
0
到1
,其中1
表示两个词完全相同,0
表示两个词完全不同,毫无关联。 -
将嵌入结果绘制在二维空间中,以理解哪些词语被发现彼此相似。
首先,使用 PCA 将嵌入转换为二维。我们将仅绘制前 200 个词。(如果愿意,你可以绘制更多的词。)
from sklearn.decomposition import PCA word_limit = 200 X = model[model.wv.vocab][: word_limit] pca = PCA(n_components=2) result = pca.fit_transform(X)
-
现在,使用
matplotlib
将结果绘制成散点图:import matplotlib.pyplot as plt plt.scatter(result[:, 0], result[:, 1]) words = list(model.wv.vocab)[: word_limit] for i, word in enumerate(words): plt.annotate(word, xy=(result[i, 0], result[i, 1])) plt.show()
你的输出结果应该如下所示:
图 7.32:使用 PCA 表示前 200 个词的嵌入
注意
在词嵌入的表示中,坐标轴没有任何特定含义。表示仅显示不同词语之间的相似度。
-
将嵌入保存到文件中,以便稍后检索:
model.wv.save_word2vec_format('movie_embedding.txt', binary=False)
恭喜!你刚刚创建了你的第一个词嵌入。你可以玩转这些嵌入,查看不同词语之间的相似性。
活动 19:预测电影评论的情感
在本活动中,我们将尝试预测电影评论的情感。数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter07)包含 25,000 条来自 IMDB 的电影评论及其情感(正面或负面)。让我们看看以下情景:你在一家 DVD 租赁公司工作,必须根据评论者的评价预测某部电影需要制作的 DVD 数量。为此,你创建一个机器学习模型,能够分析评论并判断电影的受欢迎程度。
-
读取并预处理电影评论。
-
创建评论的词嵌入。
-
创建一个完全连接的神经网络来预测情感,这与我们在第五章中创建的神经网络模型类似:掌握结构化数据。输入将是评论的词嵌入,而模型的输出将是
1
(正面情感)或0
(负面情感)。注意
该活动的解决方案可以在第 378 页找到。
输出结果有些晦涩,因为停用词和标点符号已经被移除,但你仍然可以理解评论的大致意思。
恭喜你!你刚刚创建了你的第一个 NLP 模块。你应该会发现该模型的准确率大约是 76%,这是比较低的。这是因为它是基于单个词来预测情感的;它无法理解评论的上下文。例如,它会将“not good”预测为积极情感,因为它看到了“good”这个词。如果它能看到多个词,就能理解这是负面情感。在接下来的章节中,我们将学习如何创建能够保留过去信息的神经网络。
递归神经网络(RNN)
到目前为止,我们讨论的所有问题都没有时间依赖性,这意味着预测不仅依赖于当前输入,还依赖于过去的输入。例如,在狗与猫分类器的案例中,我们只需要一张狗的图片就能将其分类为狗。不需要其他信息或图片。而如果你想创建一个分类器,用于预测狗是走路还是站着,你将需要一系列的图片或视频,以确定狗的行为。RNNs 就像我们之前讨论的完全连接的网络。唯一的区别是,RNN 具有存储关于之前输入的信息作为状态的记忆。隐藏层的输出作为下一个输入的输入。
图 7.33:递归神经网络的表示
从图像中,你可以理解隐藏层的输出是如何作为下一个输入的输入。这在神经网络中充当记忆元素。另一个需要注意的事项是,普通神经网络的输出是输入和网络权重的函数。
这使得我们能够随机输入任何数据点以获得正确的输出。然而,RNNs(递归神经网络)却不是这样。在 RNN 的情况下,我们的输出取决于之前的输入,因此我们需要按照正确的顺序输入数据。
图 7.34:递归层的表示
在前面的图像中,你可以看到左边“折叠”模型中的单个 RNN 层。U 是输入权重,V 是输出权重,W 是与记忆输入相关的权重。RNN 的记忆也被称为状态。右边的“展开”模型展示了 RNN 如何处理输入序列[xt-1, xt, xt+1]。该模型会根据应用的不同而有所变化。例如,在情感分析中,输入序列最终只需要一个输出。这个问题的展开模型如下所示:
图 7.35:用于情感分析的递归层展开表示
LSTM(长短期记忆网络)
长短期记忆(LSTM)单元是一种特殊的 RNN 单元,能够在长时间段内保持信息。Hochreiter 和 Schmidhuber 在 1997 年引入了 LSTM。RNN 遭遇了梯度消失问题。它们在长时间段内会丢失所检测到的信息。例如,如果我们在对一篇文本进行情感分析,第一句话说“我今天很高兴”,然后接下来的文本没有任何情感,RNN 就无法有效地检测到文本的情感是高兴的。长短期记忆(LSTM)单元通过长时间存储某些输入而不忘记它们,克服了这个问题。大多数现实世界的递归机器学习实现都使用 LSTM。RNN 单元和 LSTM 单元的唯一区别在于记忆状态。每个 RNN 层接受一个记忆状态作为输入,并输出一个记忆状态,而每个 LSTM 层则接受长期记忆和短期记忆作为输入,并输出这两者。长期记忆使得网络能够保留信息更长时间。
LSTM 单元在 Keras 中已经实现,你可以轻松地将 LSTM 层添加到模型中:
model.add(keras.layers.LSTM(units, activation='tanh', dropout=0.0, recurrent_dropout=0.0, return_sequences=False))
在这里,units
是层中节点的数量,activation
是该层使用的激活函数。recurrent_dropout
和 dropout
分别是递归状态和输入的丢弃概率。return_sequences
指定输出是否应包含序列;当你计划在当前层后面使用另一个递归层时,这一选项设置为 True
。
注意
LSTM 通常比 RNN 更有效。
练习 57:使用 LSTM 执行情感分析
在本次练习中,我们将修改之前为前一个活动创建的模型,使其使用 LSTM 单元。我们将继续使用之前处理过的 IMDB 电影评论数据集。大多数预处理步骤与活动 19中的步骤相似。
-
使用 pandas 在 Python 中读取 IMDB 电影评论数据集:
import pandas as pd data = pd.read_csv('../../chapter 7/data/movie_reviews.csv', encoding='latin-1')
-
将推文转换为小写,以减少唯一词汇的数量:
data.text = data.text.str.lower()
-
使用 RegEx 和
clean_str
函数清理评论:import re def clean_str(string): string = re.sub(r"https?\://\S+", '', string) string = re.sub(r'\<a href', ' ', string) string = re.sub(r'&', '', string) string = re.sub(r'<br />', ' ', string) string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', ' ', string) string = re.sub('\d','', string) string = re.sub(r"can\'t", "cannot", string) string = re.sub(r"it\'s", "it is", string) return string data.SentimentText = data.SentimentText.apply(lambda x: clean_str(str(x)))
-
接下来,去除评论中的停用词和其他频繁出现的不必要的词语。此步骤将字符串转换为标记(这在下一步中会有所帮助):
from nltk.corpus import stopwords from nltk.tokenize import word_tokenize,sent_tokenize stop_words = stopwords.words('english') + ['movie', 'film', 'time'] stop_words = set(stop_words) remove_stop_words = lambda r: [[word for word in word_tokenize(sente) if word not in stop_words] for sente in sent_tokenize(r)] data['SentimentText'] = data['SentimentText'].apply(remove_stop_words)
-
将这些标记组合成一个字符串,然后删除任何在去除停用词后内容为空的评论:
def combine_text(text): try: return ' '.join(text[0]) except: return np.nan data.SentimentText = data.SentimentText.apply(lambda x: combine_text(x)) data = data.dropna(how='any')
-
使用 Keras Tokenizer 对评论进行标记化,并将它们转换为数字:
from keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(num_words=5000) tokenizer.fit_on_texts(list(data['SentimentText'])) sequences = tokenizer.texts_to_sequences(data['SentimentText']) word_index = tokenizer.word_index
-
最后,将推文填充为最多 100 个单词。如果单词数少于 100,将补充 0,超过 100 则会删除多余的单词:
from keras.preprocessing.sequence import pad_sequences reviews = pad_sequences(sequences, maxlen=100)
-
使用
load_embedding
函数加载先前创建的嵌入,获取嵌入矩阵,该函数在文本处理部分中有讨论,使用以下代码:import numpy as np def load_embedding(filename, word_index , num_words, embedding_dim): embeddings_index = {} file = open(filename, encoding="utf-8") for line in file: values = line.split() word = values[0] coef = np.asarray(values[1:]) embeddings_index[word] = coef file.close() embedding_matrix = np.zeros((num_words, embedding_dim)) for word, pos in word_index.items(): if pos >= num_words: continue embedding_vector = embeddings_index.get(word) if embedding_vector is not None: embedding_matrix[pos] = embedding_vector return embedding_matrix embedding_matrix = load_embedding('movie_embedding.txt', word_index, len(word_index), 16)
-
将数据分为训练集和测试集,按 80:20 的比例划分。此比例可以调整,以找到最佳的划分方式:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(reviews, pd.get_dummies(data.Sentiment), test_size=0.2, random_state=9)
-
创建并编译包含一个 LSTM 层的 Keras 模型。你可以尝试不同的层和超参数:
from keras.models import Model from keras.layers import Input, Dense, Dropout, BatchNormalization, Embedding, Flatten, LSTM inp = Input((100,)) embedding_layer = Embedding(len(word_index), 16, weights=[embedding_matrix], input_length=100, trainable=False)(inp) model = Dropout(0.10)(embedding_layer) model = LSTM(128, dropout=0.2)(model) model = Dense(units=256, activation='relu')(model) model = Dense(units=64, activation='relu')(model) model = Dropout(0.3)(model) predictions = Dense(units=2, activation='softmax')(model) model = Model(inputs = inp, outputs = predictions) model.compile(loss='binary_crossentropy', optimizer='sgd', metrics = ['acc'])
-
使用以下代码训练模型 10 个 epoch,以查看其表现是否优于活动 1中的模型:
model.fit(X_train, y_train, validation_data = (X_test, y_test), epochs=10, batch_size=256)
-
检查模型的准确度:
from sklearn.metrics import accuracy_score preds = model.predict(X_test) accuracy_score(np.argmax(preds, 1), np.argmax(y_test.values, 1))
LSTM 模型的准确度为:
图 7.36:LSTM 模型准确度
-
绘制模型的混淆矩阵,以便更好地理解模型的预测:
y_actual = pd.Series(np.argmax(y_test.values, axis=1), name='Actual') y_pred = pd.Series(np.argmax(preds, axis=1), name='Predicted') pd.crosstab(y_actual, y_pred, margins=True)
图 7.37:模型的混淆矩阵(0 = 负面情感,1 = 正面情感)
-
使用以下代码检查模型的表现,通过查看随机评论的情感预测结果:
review_num = 110 print("Review: \n"+tokenizer.sequences_to_texts([X_test[review_num]])[0]) sentiment = "Positive" if np.argmax(preds[review_num]) else "Negative" print("\nPredicted sentiment = "+ sentiment) sentiment = "Positive" if np.argmax(y_test.values[review_num]) else "Negative" print("\nActual sentiment = "+ sentiment)
输出结果如下:
图 7.38:来自 IMDB 数据集的负面评论
恭喜!你刚刚实现了一个 RNN 来预测电影评论的情感。这个网络比我们之前创建的网络表现得稍微好一点。可以通过调整网络架构和超参数来提高模型的准确度。你还可以尝试使用来自 fastText 或 GloVe 的预训练词嵌入来提高模型的准确度。
活动 20:从推文中预测情感
在这个活动中,我们将尝试预测推文的情感。提供的数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter07)包含 150 万条推文及其情感(正面或负面)。让我们来看以下情境:你在一家大型消费者组织工作,该公司最近创建了一个 Twitter 账户。部分对公司有不满经历的顾客开始在 Twitter 上表达他们的情感,导致公司声誉下降。你被指派识别这些推文,以便公司可以与这些顾客取得联系,提供更好的支持。你通过创建一个情感预测器来完成这项任务,预测器可以判断推文的情感是正面还是负面。在将你的情感预测器应用到关于公司实际的推文之前,你将先在提供的推文数据集上进行测试。
-
读取数据并去除所有不必要的信息。
-
清理推文,进行分词,最后将其转换为数字。
-
加载 GloVe Twitter 嵌入并创建嵌入矩阵(https://nlp.stanford.edu/projects/glove/)。
-
创建一个 LSTM 模型来预测情感。
注意
该活动的解决方案可以在第 383 页找到。
恭喜!你刚刚创建了一个机器学习模块,用于从推文中预测情感。现在你可以使用 Twitter API 将其部署,用于实时推文情感分析。你可以尝试不同的 GloVe 和 fastText 嵌入,并查看模型能提高多少准确度。
总结
在本章中,我们学习了计算机如何理解人类语言。我们首先了解了什么是正则表达式(RegEx),以及它如何帮助数据科学家分析和清洗文本数据。接下来,我们了解了停用词,它们是什么,以及为什么要从数据中去除停用词以减少维度。接着,我们学习了句子切分及其重要性,然后是词嵌入。词嵌入是我们在第五章《掌握结构化数据》中讲解的主题;在这里,我们学习了如何创建词嵌入以提升我们的自然语言处理(NLP)模型的性能。为了创建更好的模型,我们研究了循环神经网络(RNN),这是一种特殊类型的神经网络,能够保留过去输入的记忆。最后,我们学习了 LSTM 单元及其为何优于普通 RNN 单元。
现在你已经完成了本章的学习,你已经能够处理文本数据并为自然语言处理创建机器学习模型。在下一章中,你将学习如何通过迁移学习和一些技巧加速模型的训练。
第十八章:第八章
行业技巧与窍门
学习目标
到本章结束时,你将能够:
-
通过迁移学习的帮助,更快地创建更好的深度学习模型
-
通过帮助分离的训练、开发和测试数据集,利用和使用更好的模型
-
使用真实数据集
-
利用 AutoML 找到最优的网络,几乎不需要任何工作
-
可视化神经网络模型
-
更好地使用训练日志
本章最后将描述迁移学习的概念,并展示如何有效地使用训练日志。
介绍
现在我们已经涵盖了你启动数据科学之旅所需的几乎所有主题,我们将介绍一些数据科学家用来提高效率、创建更好的机器学习系统的工具和技巧。你将首先学习迁移学习,它可以帮助你在数据不足时训练模型。然后,我们将介绍一些重要的工具和技巧,帮助你成为更好的数据科学家。
迁移学习
训练一个复杂的神经网络很困难且耗时,因为需要大量的数据进行训练。迁移学习帮助数据科学家将一个网络获得的部分知识转移到另一个网络上。这类似于人类如何将知识从一个人传递给另一个人,这样每个人就不必从头开始学习每一项新事物。迁移学习帮助数据科学家更快且用更少的数据点训练神经网络。根据情况,有两种方法可以执行迁移学习。具体如下:
-
使用预训练模型:在这种方法中,我们使用一个预训练的神经网络模型,并用它来解决当前的问题。预训练模型是为与当前任务不同的目的而创建的神经网络,已经在某个其他数据集上进行了训练,并且已被保存以供将来重用。预训练模型必须在类似或相同的数据集上进行训练,以获得合理的准确度。
-
创建一个模型:在这种方法中,我们在一个类似实际问题的数据集上训练神经网络模型。然后,我们使用这个模型执行与预训练模型方法相同的步骤。当实际数据集较小,且我们无法创建一个可接受的模型时,这种方法非常有用。
如第六章《解码图像》中所讨论的那样,神经网络的不同层次学习图像的不同特征。例如,第一层可能学习识别水平线,而几层后,网络可能学会识别眼睛。这也是为什么迁移学习在图像上有效的原因;我们得到的特征提取器可以用来从同一分布的新图像中提取信息。现在,你一定会好奇,为什么我们不在每个问题上都使用迁移学习呢?
让我们通过以下图示来理解这一点。这里,原始数据集指的是用于训练我们将要迁移知识的网络的数据集:
图 8.1: 不同条件下进行迁移学习的步骤
在图示中,有四个区域:
-
小数据集(类似于原始数据集):这是最常见的情况,也是迁移学习最有效的场景。由于当前数据集与用于训练预训练模型的数据集相似,我们可以使用预训练模型的各层,并根据问题类型只修改最后的全连接层部分。
-
大数据集(类似于原始数据集):这是最理想的情况。由于数据的可用性,建议从头开始训练模型,并且为了加速学习,我们可以使用预训练模型的权重作为起点。
-
小数据集(与原始数据集不同):这是迁移学习和深度学习中最糟糕的情况。面对这种情况,唯一的解决方案是找到一个类似当前数据集的数据集,在其上训练一个模型,然后再使用迁移学习。
-
大数据集(与原始数据集不同):由于数据集非常大,我们可以从头开始训练模型。为了加快训练速度,可以将预训练模型的权重作为起点,但这并不推荐。
迁移学习仅在两种类型的数据集上取得了成功——图像数据集和自然语言(文本数据)数据集。我们在 第七章 中讨论的词嵌入就是一个迁移学习的例子。接下来,我们将看看如何将迁移学习应用于图像数据。
图像数据的迁移学习
在本节中,我们将使用 Keras 加载一个预训练模型并进行迁移学习。你将学到如何处理数据集与预训练模型数据集相似的两种情况。要开始迁移学习,我们首先必须加载一个预训练模型。我们将使用 Keras 加载 Inception 模型:
import keras
base_model = keras.applications.inception_v3.InceptionV3(include_top=False, weights='imagenet')
include_top=False
去除了网络中的第一个全连接层,使我们可以输入任何大小的图像,而不依赖于原始数据集的图像大小。weights='imagenet'
确保加载预训练权重。如果没有传递给 weights
,则权重将随机初始化。Inception 模型在现有的 卷积神经网络(CNN)分类器上做出了巨大的改进。在 Inception 之前,最好的模型只是堆叠多个卷积层,希望能获得更好的性能。而 Inception 则不同,它采用了许多技巧,在提高准确率的同时,也减少了预测时间。
图 8.2:Inception 网络的单个细胞
我们将首先查看的案例是一个与原始数据集相似的小型数据集。在这种情况下,我们需要首先冻结预训练模型的层。为此,我们只需使基础模型的所有层不可训练:
for layer in base_model.layers:
layer.trainable = False
下一个案例是一个与原始数据集相似的大型数据集。在这种情况下,我们需要通过将预训练权重作为起点来训练模型。在这种情况下,我们不做任何修改,只是训练整个模型,该模型是base_model
与一些额外的全连接层的组合,具体取决于我们的任务。例如,如果任务是二分类问题,我们需要使最后一层全连接层有 2 个输出。我们在这种情况下还可以做的一件事是冻结前几层的权重,这样训练过程会更快。冻结前几层是有帮助的,因为这些层学习的是简单的形状,可以应用于任何类型的问题。要在 Keras 中冻结前五层,请使用以下代码:
for layer in base_model.layers[:5]: layer.trainable = False
练习 58:使用 InceptionV3 比较和分类图像
在这个练习中,我们将利用 Keras 提供的 InceptionV3 模型进行猫狗分类。我们将使用与第六章《解码图像》相同的数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter08),并比较我们的结果。我们将冻结 Inception 卷积层,这样就不需要重新训练它们:
-
首先,创建函数从文件名读取图像及其标签。这里,
PATH
变量包含训练数据集的路径:from PIL import Image def get_input(file): return Image.open(PATH+file) def get_output(file): class_label = file.split('.')[0] if class_label == 'dog': label_vector = [1,0] elif class_label == 'cat': label_vector = [0,1] return label_vector
-
设置图像的大小和通道:
SIZE = 200 CHANNELS = 3
-
然后,创建一个函数来预处理图像:
def preprocess_input(image): # Data preprocessing image = image.resize((SIZE,SIZE)) image = np.array(image).reshape(SIZE,SIZE,CHANNELS) # Normalize image image = image/255.0 return image
-
现在创建一个生成器函数,读取图像和标签并处理图像:
import numpy as np def custom_image_generator(images, batch_size = 128): while True: # Randomly select images for the batch batch_images = np.random.choice(images, size = batch_size) batch_input = [] batch_output = [] # Read image, perform preprocessing and get labels for file in batch_images: # Function that reads and returns the image input_image = get_input(file) # Function that gets the label of the image label = get_output(file) # Function that pre-processes and augments the image image = preprocess_input(input_image) batch_input.append(image) batch_output.append(label) batch_x = np.array(batch_input) batch_y = np.array(batch_output) # Return a tuple of (images,labels) to feed the network yield(batch_x, batch_y)
-
接下来,我们将读取验证数据。创建一个函数来读取图像及其标签:
from tqdm import tqdm def get_data(files): data_image = [] labels = [] for image in tqdm(files): label_vector = get_output(image) img = Image.open(PATH + image) img = img.resize((SIZE,SIZE)) labels.append(label_vector) img = np.asarray(img).reshape(SIZE,SIZE,CHANNELS) img = img/255.0 data_image.append(img) data_x = np.array(data_image) data_y = np.array(labels) return (data_x, data_y)
-
读取验证文件:
from random import shuffle files = os.listdir(PATH) random.shuffle(files) train = files[:7000] test = files[7000:] validation_data = get_data(test)
-
从数据集中绘制一些图像,查看是否正确加载了文件:
import matplotlib.pyplot as plt plt.figure(figsize=(20,10)) columns = 5 for i in range(columns): plt.subplot(5 / columns + 1, columns, i + 1) plt.imshow(validation_data[0][i])
样本图像如下:
图 8.3:从加载的数据集中提取的样本图像
-
加载 Inception 模型并传入输入图像的形状:
from keras.applications.inception_v3 import InceptionV3 base_model = InceptionV3(weights='imagenet', include_top=False, input_shape=(200,200,3))
-
冻结 Inception 模型层,使得训练过程中不会对它们进行训练:
for layer in base_model.layers: layer.trainable = False
-
现在根据我们的问题,添加输出的全连接层。这里,
keep_prob
是训练过程中保留节点的比例。因此,丢弃率将是1 – keep_prob
:from keras.layers import GlobalAveragePooling2D, Dense, Dropout from keras.models import Model x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(256, activation='relu')(x) keep_prob = 0.5 x = Dropout(rate = 1 - keep_prob)(x) predictions = Dense(2, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions)
-
接下来,编译模型,使其准备好进行训练:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = ['accuracy'])
然后进行模型训练:
EPOCHS = 5 BATCH_SIZE = 128 model_details = model.fit_generator(custom_image_generator(train, batch_size = BATCH_SIZE), steps_per_epoch = len(train) // BATCH_SIZE, epochs = EPOCHS, validation_data= validation_data, verbose=1)
-
评估模型并获取准确性:
score = model.evaluate(validation_data[0], validation_data[1]) print("Accuracy: {0:.2f}%".format(score[1]*100))
准确度如下:
图 8.4:模型的准确性
如前所示,模型的准确率为 97.8%,这比我们在第六章,解码图像中获得的 73%的准确率要高得多。你可以尝试修改我们附加到 Inception 模型上的模型,看看是否能提高准确率。你还可以绘制错误预测的图像,来了解模型的表现如何。
y_pred = model.predict(validation_data[0])
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(validation_data[1],axis=1))[0]
labels = ['dog', 'cat']
image = 5
plt.imshow(validation_data[0][incorrect_indices[image]].reshape(SIZE, SIZE, CHANNELS), cmap=plt.get_cmap('gray'))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
错误预测的图像如下所示:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_05.jpg)
图 8.5:错误预测的样本
活动 21:使用 InceptionV3 进行图像分类
在本活动中,我们将使用 Keras 提供的 InceptionV3 模型进行猫狗分类。我们将使用第六章,解码图像中使用的相同数据集并比较我们的结果。在这里,我们将训练整个模型,但我们将使用 Inception 预训练模型中的权重作为起点。这类似于我们刚才讨论的练习,但没有冻结层。
-
创建一个生成器来获取图像和标签。
-
创建一个函数来获取标签和图像。然后,创建一个函数来预处理图像并对其进行增强。
-
加载验证数据集,这些数据集不会进行增强处理。
-
加载 Inception 模型并添加最终的全连接层。训练整个网络。
你应该看到这个模型的准确率为 95.4%,这比我们在第六章,解码图像中获得的 73%的准确率要高得多。
你一定注意到前面的代码与练习 58相似,但这里我们没有冻结层。模型显然从 Inception 模型的权重中受益,将其作为起点。你可以绘制错误预测的图像,来了解模型的表现如何:
y_pred = model.predict(validation_data[0])
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(validation_data[1],axis=1))[0]
labels = ['dog', 'cat']
image = 5
plt.imshow(validation_data[0][incorrect_indices[image]].reshape(SIZE, SIZE, CHANNELS), cmap=plt.get_cmap('gray'))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
注意
本活动的解决方案可以在第 387 页找到。
错误预测的图像如下所示:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_06.jpg)
图 8.6:数据集中错误预测的样本
有用的工具和技巧
在本节中,你将首先了解数据集的不同拆分的重要性。之后,你将学到一些技巧,这些技巧在处理未经过处理的数据集时会派上用场。然后,我们将介绍像 pandas profiling 和 TensorBoard 这样的工具,它们通过提供简便的信息访问,使你的工作更加轻松。我们还将看看 AutoML,以及它如何在无需大量人工操作的情况下获得高性能模型。最后,我们将可视化我们的 Keras 模型并将模型图导出到文件。
训练集、开发集和测试集
我们在前几章中简要讨论了训练集、开发集和测试集。在这里,我们将深入探讨这个话题。
训练集(或训练数据集)是从数据集中获取的一个样本,我们用它来创建我们的机器学习模型。开发集(或验证集,也称为开发数据集)是一个样本,帮助我们调整已创建模型的超参数。测试集(或测试数据集)是我们用来最终评估模型的样本。拥有这三种数据集对模型开发至关重要。
数据集的分布
开发集和测试集应该来自相同的分布,并且应该代表你期望模型在未来接收到的数据。如果分布不同,模型将会调优到一个在未来看不见的分布,这会影响部署后的模型表现。由于训练集和测试/开发集之间的分布差异,模型可能会表现不佳。为了解决这个问题,你可以从测试/开发集获取一些数据点并将其引入到训练集中。确保原始图像主导各自的数据集,以防止出现不正确的结果。
如果训练集和开发集的分布不同,我们无法识别模型是否过拟合;在这种情况下,应引入一个新的训练-开发集来检查模型的过拟合情况。训练集和训练-开发集必须具有相同的分布。如果开发集和训练-开发集的误差差异很大,那么就存在数据不匹配的问题。为了解决这个问题,你需要进行手动误差分析,并在大多数情况下,收集更多的数据点。
注意
开发集与我们一直使用的验证集相同,我们有时称之为测试集,但那只是为了让你入门。还应该注意,我们仅在训练数据集上训练模型。
数据集的大小
开发集和测试集的大小应根据数据集的总体大小来确定。如果数据集的大小是 10,000 个数据点,那么 60%/20%/20%的划分会很好,因为测试集和开发集都有足够的数据点来准确衡量模型的性能。另一方面,如果数据集有 1,000,000 个数据点,那么 98%/1%/1%的划分就足够了,因为 10,000 个数据点已经足够评估模型的表现。
三个数据集的样本应该保持一致,以便我们在相同的环境中评估所有模型。为此,你可以在创建随机样本时设置一个“种子”值。设置随机数种子有助于每次运行实验时获得相同的随机数据划分。
处理未处理的数据集
当你开始处理更复杂且未经过多处理的数据集时,你会发现大多数情况下,你无法获得创建令人满意模型所需的所有数据。为了解决这个问题,你需要识别可以帮助你创建合格模型的外部数据集。你使用的附加数据可以有以下两种类型:
-
相同数据的更多数据点:当模型由于数据集较小而过拟合时,这非常有用。如果无法获取更多数据点,可以使用更简单的模型——例如层数较少的神经网络或线性模型。
-
来自不同来源的额外数据:有时候数据集中会缺少一些数据,例如数据集中城市的州或国家,或者数据集中列出的国家的宏观经济因素,如 GDP 和人均收入。此类数据可以轻松在互联网上找到,并可用于改进您创建的模型。
最佳实践是始终从探索性数据分析(EDA)开始。EDA 帮助我们深入了解数据集,有助于识别最佳模型及可用于机器学习的变量。EDA 的另一个重要方面是检查数据是否存在异常。这可以确保数据在传输过程中没有发生错误。EDA 的结果可以与利益相关者共享,以确认数据的有效性。数据科学家在项目中可能需要多次回顾 EDA 步骤。
另一个需要牢记的事项是模型的应用场景。了解您的模型是用于实时处理还是批处理非常重要。这将帮助您选择合适的工具和模型。例如,如果实时处理是优先考虑的目标,那么您可能会使用一个能在不到一秒钟内产生结果的模型;而如果应用程序需要批处理,那么您可以使用那些需要几秒钟以上才能产生预测的复杂神经网络模型。
接下来,我们将探讨一些处理训练和进行超参数调优的最佳实践。在将数据拆分为训练集和测试集之前,始终对数据进行洗牌。另一个有助于更快收敛的做法是在训练过程中对训练数据进行洗牌。Keras 的fit
函数有一个非常有用的参数,名为True
,它会在每个训练周期前洗牌训练数据。需要记住的一个重要参数是随机数种子;它帮助数据科学家即使在随机洗牌和拆分的情况下,也能创建可重复的结果。要为 Keras 设置种子,请使用以下方法:
from numpy.random import seed
seed(1)
from tensorflow import set_random_seed
set_random_seed(1)
前两行设置了 NumPy 的随机种子,接下来的两行则为 TensorFlow 设置了种子,这是 Keras 使用的后端。
如果您正在处理一个大型数据集,建议先使用数据的子集来创建模型。尝试通过加深网络或使其更复杂来使模型过拟合。您可以使用正则化来限制模型过拟合数据。当您对模型有信心时,使用完整的训练数据并调整已创建的模型,以提高模型的性能。
Dropout 是一种非常强大的正则化方法;你应该尝试不同的 dropout 率,因为最优的 dropout 率因数据集不同而不同。如果 dropout 概率太低,效果就不会显现。另一方面,如果概率过高,模型就会开始欠拟合。通常,20%到 50%之间的 dropout 率效果最好。
学习率是一个重要的超参数。学习率过大会导致模型越过最优解,而学习率过小则会导致模型学习非常缓慢。如第五章《掌握结构化数据》中提到的,我们可以从较高的学习率开始,并在经过几步后降低学习率。
这有助于我们更快地达到最优点,同时由于步长减小,避免了模型越过解的情况。为了实现学习率的减少,我们可以使用 Keras 中的ReduceLROnPlateau
回调函数。如果选择的指标停止改进,该回调函数会按照预定义的因子降低学习率。
注意
要进一步了解数据集,请参考文档:https://keras.io/callbacks/#reducelronplateau。
from keras.callbacks import ReduceLROnPlateau
ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=10, min_delta=0.0001, min_lr=0)
我们将要监控的数量传递给monitor
参数。factor
指的是学习率需要减少的倍数;新的学习率将等于当前学习率乘以这个因子。patience
是回调函数等待的 epochs 数量,等待后才会改变学习率。min_delta
表示衡量模型在监控指标上改进的阈值。min_lr
表示学习率的下限。
pandas Profiling
在前几章中,你学习了不同的探索结构化数据集的方法。EDA 在构建结构化数据模型时起着重要作用。执行 EDA 的步骤,如空值识别、相关性分析和计数唯一值等,通常不变,因此最好创建一个函数来完成这些任务,避免编写大量代码。pandas profiling 库正是做到了这一点:它接收一个 DataFrame,对数据进行分析并以交互式的输出形式展示结果。
输出包含相关列的以下信息:
-
基本信息:包含关于变量类型、唯一值和缺失值的信息。
-
分位数统计:包含关于最小值、Q1、中央値、Q3、最大值、范围和四分位距的信息。
-
描述性统计:包含关于均值、众数、标准差、总和、中位数绝对偏差和变异系数的信息。
-
最频繁的值:包含关于最常见值的计数以及其百分比频率的信息。
-
直方图:包含关于数据集不同特征值频率的图表信息。
-
相关性:这些突出显示了高度相关的变量,并建议将其移除。
要使用 pandas profiling,只需将数据框传递给 pandas_profiling
对象。使用以下代码:
import pandas_profiling
pandas_profiling.ProfileReport(df)
以下截图显示了我们在 第五章 中处理的电信流失数据集的 pandas profiling 输出的一部分,精通结构化数据
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_07.jpg)
图 8.7:pandas profiling 输出的截图
您可以使用这个功能来探索我们在前几章中处理的数据集。pandas profiling 提供交互式输出,鼓励您去操作并玩转这些输出。
TensorBoard
TensorBoard 是一个可以用来查看训练日志并可视化模型准确率和损失度量的 Web 应用。它最初是为了与 TensorFlow 一起使用而创建的,但我们可以通过 Keras 中的 TensorBoard 回调 来使用 TensorBoard。要开始可视化,请创建 Keras 回调。使用以下代码来实现:
import keras
keras.callbacks.TensorBoard(log_dir='./logs', update_freq='epoch')
记住您在此处指定的日志目录;稍后您会需要它。您可以在 update_freq
中传递 'batch
'、'epoch
' 或整数值;这表示日志应写入的频率。下一步是启动 TensorBoard;为此,打开终端并运行以下命令:
tensorboard --logdir logs --port 6607
现在开始训练。不要忘记将回调传递给 fit
函数。TensorBoard 的第一个标签页显示了模型的训练日志。您可以在日志文件夹内创建多个子文件夹,以便在同一图表上获取不同模型的日志进行比较:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_08.jpg)
图 8.8:显示 TensorBoard 仪表盘的截图
在第二个标签页中,您可以可视化您创建的模型。下图展示了我们在上一章的第一个活动中创建的模型:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_09.jpg)
图 8.9:Keras 解释的模型
在 Jupyter Notebook 中可视化训练日志的另一种方法是使用 Matplotlib 绘制它们:
import matplotlib.pyplot as plt
plt.plot(model_details.history['acc'])
plt.plot(model_details.history['val_acc'])
plt.title('Cats vs. Dogs model accuracy')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend(['Train set', 'Dev set'], loc='upper left')
plt.show()
下图显示了我们在 活动 1 中使用的猫狗模型的训练集和测试集的准确率图:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_10.jpg)
图 8.10:模型的准确率日志
上面的准确率日志显示了训练集和开发集的准确率在不同的 epoch 上是如何变化的。正如你所看到的,开发集的准确率比训练集的准确率更为波动。这是因为模型没有见过这些例子,在初期的 epochs 中波动较大,但随着我们在更多的 epochs 上进行训练,模型变得更稳健,准确率波动会减小。
plt.plot(model_details.history['loss'])
plt.plot(model_details.history['val_loss'])
plt.title('Cats vs. Dogs model loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train set', 'Test set'], loc='upper left')
plt.show()
下图显示了我们在 活动 21 中使用的猫狗模型的训练集和测试集的损失图:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_11.jpg)
图 8.11:模型的损失日志
类似于准确度日志,上面给出的损失日志显示了训练集和开发集的损失在不同 epochs 中的变化。第 19 个 epoch 附近的波峰表明,模型曾因过拟合于训练集而表现非常糟糕,但最终模型开始稳定,并在开发集上也给出了更好的结果。
如果你只关心模型日志,那么你可以使用之前给出的代码,在训练结束后绘制模型日志。然而,如果你正在训练一个需要较长时间的模型,建议使用 TensorBoard,因为它可以实时绘制训练损失和准确度。
AutoML
现在你已经创建了多个神经网络模型,你明白了构建表现良好的网络的两个主要组成部分。它们分别是:
-
神经网络的架构
-
神经网络的超参数
根据问题的不同,可能需要经过数十次迭代才能得到最佳网络。到目前为止,我们一直在手动创建架构并调优超参数。AutoML 可以帮助我们执行这些任务,它会为当前数据集搜索最优的网络和参数。Auto-Keras 是一个开源库,帮助我们在 Keras 上实现 AutoML。让我们通过一个练习来学习如何使用 Auto-Keras。
练习 59:使用 Auto-Keras 获取表现良好的网络
在本练习中,我们将利用 Auto-Keras 库,为 cats-vs-dogs 数据集(https://github.com/TrainingByPackt/Data-Science-with-Python/tree/master/Chapter08)找到最优的网络和参数。
-
首先,创建一个加载图像标签的函数:
def get_label(file): class_label = file.split('.')[0] if class_label == 'dog': label_vector = 0 elif class_label == 'cat': label_vector = 1 return label_vector
-
设置
SIZE
,它是输入图像的维度(宽高相等的图像)。SIZE = 50
-
然后创建一个函数来读取图像及其标签。此处
PATH
变量包含训练数据集的路径。import os from PIL import Image import numpy as np from random import shuffle def get_data(): data = [] files = os.listdir(PATH) for image in tqdm(files): label_vector = get_label(image) img = Image.open(PATH + image).convert('L') img = img.resize((SIZE,SIZE)) data.append([np.asarray(img),np.array(label_vector)]) shuffle(data) return data
-
加载数据并将其划分为训练集和测试集:
data = get_data() train = data[:7000] test = data[7000:] x_train = [data[0] for data in train] y_train = [data[1] for data in train] x_test = [data[0] for data in test] y_test = [data[1] for data in test] x_train = np.array(x_train).reshape(-1,SIZE,SIZE,1) x_test = np.array(x_test).reshape(-1,SIZE,SIZE,1)
-
现在,让我们开始使用 AutoML。
首先,创建一个数组来设置 AutoKeras 的训练时间。时间一到,它会停止寻找最佳模型的过程:
TRAINING_TIME = 60 * 60 * 1 # 1 hour
我们将给 AutoKeras 一小时的时间来寻找最佳方法。
-
使用 AutoKeras 创建一个图像分类模型,并进行前一步中指定时间的训练:
import autokeras as ak model = ak.ImageClassifier(verbose=True) model.fit(x_train, y_train, time_limit=TRAINING_TIME) model.final_fit(x_train, y_train, x_test, y_test, retrain=True)
-
输出将如下所示:
图 8.12:图像分类模型
-
接下来,我们保存模型,以便以后可以再次使用它:
model.export_autokeras_model("model.h5")
-
加载训练好的模型并使用它进行预测:
from autokeras.utils import pickle_from_file model = pickle_from_file("model.h5") predictions = model.predict(x_test)
-
评估由 AutoKeras 创建的模型的准确性:
score = model.evaluate(x_test, y_test) print("\nScore: {}".format(score))
-
模型的准确度如下:
图 8.13:模型最终准确度
-
我们成功地利用 autokeras 创建了一个图像分类器,用于检测提供的图像是猫还是狗。经过一小时的运行,这个模型的准确率为 72%,考虑到我们在第六章,解码图像,活动 22 中创建的模型准确率为 73%,这已经相当不错了。这显示了 AutoML 的强大功能,但有时我们在可接受的时间框架内可能无法获得足够好的结果。
使用 Keras 进行模型可视化
到目前为止,我们已经创建了一些神经网络模型,但还没有可视化它们。Keras 提供了一个非常实用的工具函数,可以绘制任何模型的图。要创建一个图形,首先定义模型,我们将使用第六章中的模型,解码图像,如下面的代码所示:
model = Sequential()
model.add(Conv2D(48, (3, 3), activation='relu', padding='same', input_shape=(50,50,1)))
model.add(Conv2D(48, (3, 3), activation='relu'))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.10))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(2, activation='softmax'))
model.summary()
然后使用 plot_model
将模型保存为图像,如以下代码所示。
from keras.utils import plot_model
plot_model(model, to_file='model.png', show_shapes=True)
show_shapes
参数会展示层的输入和输出形状。保存的图像如下:
图 8.14:由 Keras 创建的模型可视化
活动 22:使用迁移学习预测图像
我们将创建一个项目,使用迁移学习来预测给定图片是狗还是猫。你将使用的基准模型是 InceptionV3\。我们将对这个模型进行微调,以适应我们的数据集,从而将模型调整为区分猫和狗。我们将使用 TensorBoard 实时监控训练指标,并使用本章中讨论的最佳实践。确保结果是可复现的:
-
重复你在前一个活动中的第 1 步所做的一切。
-
加载开发集和测试集,这些数据集将不会进行数据增强。
-
加载 Inception 模型并添加最终的全连接层。训练整个网络。
-
使用所有有用的回调函数。
-
使用 TensorBoard 可视化训练过程。
注意
这个活动的解决方案可以在第 391 页找到。
你可以使用以下代码片段绘制预测错误的图像,以了解模型的性能:
y_pred = model.predict(test_data[0])
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(test_data[1],axis=1))[0]
labels = ['dog', 'cat']
image = 5
plt.imshow(test_data[0][incorrect_indices[image]].reshape(SIZE, SIZE, CHANNELS), cmap=plt.get_cmap('gray'))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
错误预测的图像如下:
图 8.15:错误预测的样本
总结
在本章中,我们介绍了迁移学习并利用它加速深度学习模型的创建。接着我们学习了训练集、开发集和测试集分开的重要性,并讨论了如何处理现实生活中的未处理数据集。之后,我们讲解了什么是 AutoML,以及如何以最少的工作量找到最优的网络。最后,我们学习了如何可视化神经网络模型和训练日志。
现在,你已经完成了本章的学习,具备了处理任何数据以创建机器学习模型的能力。
最后,完成本书后,你应该对数据科学的概念有一个深入的理解,并且能够使用 Python 语言处理不同的数据集来解决商业案例问题。你所学到的不同概念,包括预处理、数据可视化、图像增强和自然语言处理等,应该帮助你全面掌握如何处理数据。
第十九章:附录
关于
本节内容是为帮助您完成书中活动而包含的。它包括学生需要执行的详细步骤,以便完成并实现书中的目标。
第一章:数据科学与数据预处理简介
活动 1:使用银行营销订阅数据集进行预处理
解决方案
让我们对Bank Marketing Subscription
数据集进行各种预处理任务。我们还将把数据集分为训练数据和测试数据。按照以下步骤完成此活动:
-
打开一个 Jupyter 笔记本并添加一个新单元格,导入 pandas 库并将数据集加载到 pandas 数据框中。首先需要导入该库,然后使用
pd.read_csv()
函数,如下所示:import pandas as pd Link = 'https://github.com/TrainingByPackt/Data-Science-with-Python/blob/master/Chapter01/Data/Banking_Marketing.csv' #reading the data into the dataframe into the object data df = pd.read_csv(Link, header=0)
-
要查找数据集中的行数和列数,请添加以下代码:
#Finding number of rows and columns print("Number of rows and columns : ",df.shape)
上述代码生成以下输出:
图 1.60:数据集中的行数和列数
-
要打印所有列的列表,请添加以下代码:
#Printing all the columns print(list(df.columns))
上述代码生成以下输出:
图 1.61:数据集中包含的列列表
-
要概览每一列的基本统计数据,如计数、均值、中位数、标准差、最小值、最大值等,可以添加以下代码:
#Basic Statistics of each column df.describe().transpose()
上述代码生成以下输出:
图 1.62:每一列的基本统计数据
-
要打印每一列的基本信息,请添加以下代码:
#Basic Information of each column print(df.info())
上述代码生成以下输出:
图 1.63:每一列的基本信息
在上面的图中,可以看到没有任何列包含空值。此外,还提供了每一列的数据类型。
-
现在让我们检查缺失值和每个特征的类型。请添加以下代码来执行此操作:
#finding the data types of each column and checking for null null_ = df.isna().any() dtypes = df.dtypes sum_na_ = df.isna().sum() info = pd.concat([null_,sum_na_,dtypes],axis = 1,keys = ['isNullExist','NullSum','type']) info
查看下面图中的输出结果:
图 1.64:每一列的信息,包括空值的数量和数据类型
-
由于我们已经将数据集加载到
data
对象中,我们将从数据集中删除空值。要删除空值,可以添加以下代码:#removing Null values df = df.dropna() #Total number of null in each column print(df.isna().sum())# No NA
查看下面图中的输出结果:
图 1.65:无空值的数据集特征
-
现在我们检查数据集中
education
列的频率分布。使用value_counts()
函数来实现这一点:df.education.value_counts()
查看下面图中的输出结果:
图 1.66:education 列的频率分布
-
在前面的图中,我们可以看到数据集的
education
列有很多类别。我们需要减少这些类别,以便更好地建模。为了检查education
列中的各种类别,我们使用unique()
函数。输入以下代码来实现这一点:df.education.unique()
输出如下:
图 1.67:education 列的各种类别
-
现在让我们将
basic.4y
、basic.9y
和basic.6y
这些类别合并为一个basic
类别。为此,我们可以使用 pandas 的replace
函数:df.education.replace({"basic.9y":"Basic","basic.6y":"Basic","basic.4y":"Basic"},inplace=True)
-
要检查分组后的类别列表,添加以下代码:
df.education.unique()
图 1.68:education 列的各种类别
在前面的图中,你可以看到
basic.9y
、basic.6y
和basic.4y
被合并为Basic
。 -
现在我们选择并执行适当的数据编码方法。添加以下代码来实现这一点:
#Select all the non numeric data using select_dtypes function data_column_category = df.select_dtypes(exclude=[np.number]).columns
前面的代码生成了以下输出:
图 1.69:数据集的各个列
-
现在我们定义一个包含数据中所有类别特征名称的列表。此外,我们循环遍历列表中的每个变量,获取虚拟变量编码的输出。添加以下代码来实现这一点:
cat_vars=data_column_category for var in cat_vars: cat_list='var'+'_'+var cat_list = pd.get_dummies(df[var], prefix=var) data1=df.join(cat_list) df=data1 df.columns
前面的代码生成了以下输出:
图 1.70:数据中类别特征的列表
-
现在我们忽略已经进行了编码的类别列。我们将只选择数值列和已编码的类别列。添加代码以实现此操作:
#Categorical features cat_vars=data_column_category #All features data_vars=df.columns.values.tolist() #neglecting the categorical column for which we have done encoding to_keep = [] for i in data_vars: if i not in cat_vars: to_keep.append(i) #selecting only the numerical and encoded catergorical column data_final=df[to_keep] data_final.columns
前面的代码生成了以下输出:
图 1.71:数值列和已编码类别列的列表
-
最后,我们将数据拆分为训练集和测试集。添加以下代码来实现这一点:
#Segregating Independent and Target variable X=data_final.drop(columns='y') y=data_final['y'] from sklearn. model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) print("FULL Dateset X Shape: ", X.shape ) print("Train Dateset X Shape: ", X_train.shape ) print("Test Dateset X Shape: ", X_test.shape )
输出如下:
图 1.72:完整数据集、训练集和测试集的形状
第二章:数据可视化
活动 2:折线图
解决方案:
-
创建一个包含从一月到六月的六个字符串的列表,并将其保存为 x,使用以下代码:
x = ['January','February','March','April','May','June']
-
创建一个包含 6 个值的列表,代表'
Items Sold
',其值从 1000 开始,每次增加 200,直到最终值为 2000,并将其保存为 y,如下所示:y = [1000, 1200, 1400, 1600, 1800, 2000]
-
使用以下代码绘制 y('
Items Sold
')与 x('Month
')的折线图,线为虚线,标记为星形:plt.plot(x, y, '*:b')
-
使用以下代码将 x 轴设置为'
Month
':plt.xlabel('Month')
-
将 y 轴设置为'
Items Sold
',如下所示:plt.ylabel('Items Sold')
-
要将标题设置为'
Items Sold has been Increasing Linearly
',请参考以下代码:plt.title('Items Sold has been Increasing Linearly')
查看以下截图,查看结果输出:
图 2.33:按月销售项目的折线图
活动 3:条形图
解决方案:
-
使用以下代码为
x
创建一个包含五个字符串的列表,表示获得最多冠军的 NBA 球队名称:x = ['Boston Celtics','Los Angeles Lakers', 'Chicago Bulls', 'Golden State Warriors', 'San Antonio Spurs']
-
使用以下代码为
y
创建一个包含五个值的列表,这些值对应于x
中的字符串,表示'Titles Won
':y = [17, 16, 6, 6, 5]
-
将
x
和y
放入数据框中,列名分别为'Team
'和'Titles
',如下所示:import pandas as pd df = pd.DataFrame({'Team': x, 'Titles': y})
-
使用以下代码对数据框按'Titles'进行降序排序,并将结果保存为 df_sorted:
df_sorted = df.sort_values(by=('Titles'), ascending=False)
注意
如果我们使用
ascending=True
进行排序,图表中较大的值会出现在右侧。由于我们希望较大的值出现在左侧,我们将使用ascending=False
。 -
编写一个程序化的标题,并首先通过以下代码找到获得最多冠军的球队,并将其保存为
team_with_most_titles
对象:team_with_most_titles = df_sorted['Team'][0]
-
然后,使用以下代码检索获得最多冠军的球队的冠军数量:
most_titles = df_sorted['Titles'][0]
-
最后,使用以下代码创建一个字符串'
The Boston Celtics have the most titles with 17
':title = 'The {} have the most titles with {}'.format(team_with_most_titles, most_titles)
-
使用以下代码绘制一个条形图,显示各球队的冠军数量:
import matplotlib.pyplot as plt plt.bar(df_sorted['Team'], df_sorted['Titles'], color='red')
-
使用以下代码将 x 轴标签设置为'
Team
':plt.xlabel('Team')
-
使用以下代码将 y 轴标签设置为'
Number of Championships
':plt.ylabel('Number of Championships')
-
为了防止 x 轴刻度标签重叠,我们将它们旋转 45 度,请参考以下代码:
plt.xticks(rotation=45)
-
将图表的标题设置为我们创建的程序化
title
对象,如下所示:plt.title(title)
-
使用以下代码将图表保存到当前工作目录下,文件名为'
Titles_by_Team.png
':plt.savefig('Titles_by_Team)
-
使用
plt.show()
打印图表。为了更好地理解这一点,请查看以下输出截图:图 2.34:NBA 球队拥有的冠军数量的条形图
注意
当我们使用
plt.show()
将图表打印到控制台时,它会按预期显示;然而,当我们打开我们创建的名为'Titles_by_Team.png
'的文件时,我们会看到它裁剪了 x 轴刻度标签。下图显示了裁剪了 x 轴刻度标签的条形图。
图 2.35:裁剪了 x 轴刻度标签的'Titles_by_Team.png'
-
为了修复裁剪问题,在
plt.savefig()
中添加bbox_inches='tight'
作为参数,如下所示:plt.savefig('Titles_by_Team', bbox_inches='tight')
-
现在,当我们从工作目录中打开保存的'
Titles_by_Team.png
'文件时,我们看到 x 轴刻度标签没有被裁剪。查看以下输出,查看最终结果:
图 2.36:未裁剪 x 轴刻度标签的'Titles_by_Team.png'
活动 4:使用子图展示多种图表类型
解决方案:
-
导入'
Items_Sold_by_Week.csv
'文件,并使用以下代码将其保存为Items_by_Week
数据框对象:import pandas as pd Items_by_Week = pd.read_csv('Items_Sold_by_Week.csv')
-
导入
Weight_by_Height.csv
文件并将其保存为Weight_by_Height
数据框对象,如下所示:Weight_by_Height = pd.read_csv('Weight_by_Height.csv')
-
使用以下代码生成一个包含 100 个正态分布数字的数组,作为直方图和箱形图的数据,并将其保存为 y:
y = np.random.normal(loc=0, scale=0.1, size=100)
-
为了生成一个包含六个子图、按三行两列排列且不重叠的图形,请参考以下代码:
import matplotlib.pyplot as plt fig, axes = plt.subplots(nrows=3, ncols=2) plt.tight_layout()
-
使用以下代码将相应的坐标轴标题设置为与图 2.32 中相同:
axes[0,0].set_title('Line') axes[0,1].set_title('Bar') axes[1,0].set_title('Horizontal Bar') axes[1,1].set_title('Histogram') axes[2,0].set_title('Scatter') axes[2,1].set_title('Box-and-Whisker')
图 2.37:标题、无重叠的空子图
-
在
Line
、Bar
和Horizontal Bar
坐标轴上,使用以下代码绘制Items_Sold
与Week
的关系:axes[0,0].plot(Items_by_Week['Week'], Items_by_Week['Items_Sold']) axes[0,1].bar(Items_by_Week['Week'], Items_by_Week['Items_Sold']) axes[1,0].barh(Items_by_Week['Week'], Items_by_Week['Items_Sold'])
请在以下图中查看结果输出:
图 2.38:添加的折线图、条形图和水平条形图
-
在
Histogram
和Box-and-Whisker
坐标轴上,使用以下代码绘制包含 100 个正态分布数字的数组:axes[1,1].hist(y, bins=20)axes[2,1].boxplot(y)
结果输出如下所示:
图 2.39:添加的直方图和箱形图
-
使用以下代码在
Weight_by_Height
数据框的Scatterplot
坐标轴上绘制Weight
与Height
的关系:axes[2,0].scatter(Weight_by_Height['Height'], Weight_by_Height['Weight'])
请参阅这里的图示,以查看结果输出:
图 2.40:添加的散点图
-
使用
axes[row, column].set_xlabel('X-Axis Label')
和axes[row, column].set_ylabel('Y-Axis Label')
分别标注每个子图的 x 轴和 y 轴。请参阅这里的图示,以查看结果输出:
图 2.41:X 和 Y 轴已被标注
-
使用
figsize
参数在子图函数中增加图形的大小,如下所示:fig, axes = plt.subplots(nrows=3, ncols=2, figsize=(8,8))
-
使用以下代码将图形保存为当前工作目录中的
Six_Subplots
:fig.savefig('Six_Subplots')
以下图显示了
Six_Subplots.png
文件:
图 2.42:Six_Subplots.png 文件
第三章:通过 Scikit-Learn 介绍机器学习
活动 5:生成预测并评估多元线性回归模型的性能
解决方案:
-
使用以下代码在测试数据上生成预测:
predictions = model.predict(X_test) 2. Plot the predicted versus actual values on a scatterplot using the following code: import matplotlib.pyplot as plt from scipy.stats import pearsonr plt.scatter(y_test, predictions) plt.xlabel('Y Test (True Values)') plt.ylabel('Predicted Values') plt.title('Predicted vs. Actual Values (r = {0:0.2f})'.format(pearsonr(y_test, predictions)[0], 2)) plt.show()
请参阅这里的结果输出:
图 3.33:来自多元线性回归模型的预测值与实际值的散点图
注意
在多元线性回归模型中,预测值与实际值之间的线性相关性(r = 0.93)比简单线性回归模型(r = 0.62)强得多。
-
为了绘制残差的分布,请参考这里的代码:
import seaborn as sns from scipy.stats import shapiro sns.distplot((y_test - predictions), bins = 50) plt.xlabel('Residuals') plt.ylabel('Density') plt.title('Histogram of Residuals (Shapiro W p-value = {0:0.3f})'.format(shapiro(y_test - predictions)[1])) plt.show()
请参阅这里的结果输出:
图 3.34:多元线性回归模型的残差分布
注意
我们的残差呈负偏态且非正态分布,但相比简单线性模型,这种偏态较小。
-
计算平均绝对误差、均方误差、均方根误差和 R 方,并将它们放入 DataFrame,如下所示:
from sklearn import metrics import numpy as np metrics_df = pd.DataFrame({'Metric': ['MAE', 'MSE', 'RMSE', 'R-Squared'], 'Value': [metrics.mean_absolute_error(y_test, predictions), metrics.mean_squared_error(y_test, predictions), np.sqrt(metrics.mean_squared_error(y_test, predictions)), metrics.explained_variance_score(y_test, predictions)]}).round(3) print(metrics_df)
请参考结果输出:
图 3.35:多元线性回归模型的模型评估指标
与简单线性回归模型相比,多元线性回归模型在每个指标上表现更好。
活动 6:生成预测并评估调整后的逻辑回归模型性能
解决方案:
-
使用以下代码生成降雨的预测概率:
predicted_prob = model.predict_proba(X_test)[:,1]
-
使用
predicted_class = model.predict(X_test)
生成降雨的预测类别。 -
使用混淆矩阵评估性能,并使用以下代码将其保存为 DataFrame:
from sklearn.metrics import confusion_matrix import numpy as np cm = pd.DataFrame(confusion_matrix(y_test, predicted_class)) cm['Total'] = np.sum(cm, axis=1) cm = cm.append(np.sum(cm, axis=0), ignore_index=True) cm.columns = ['Predicted No', 'Predicted Yes', 'Total'] cm = cm.set_index([['Actual No', 'Actual Yes', 'Total']]) print(cm)
图 3.36:我们逻辑回归网格搜索模型的混淆矩阵
注意
很棒!我们将假阳性的数量从 6 降低到 2。此外,假阴性从 10 降低到了 4(请参见 练习 26)。请注意,结果可能会略有不同。
-
为了进一步评估,打印分类报告,如下所示:
from sklearn.metrics import classification_report print(classification_report(y_test, predicted_class))
图 3.37:我们逻辑回归网格搜索模型的分类报告
通过调整逻辑回归模型的超参数,我们能够在已经表现良好的逻辑回归模型基础上进一步优化。
活动 7:生成预测并评估 SVC 网格搜索模型的性能
解决方案:
-
使用以下代码提取降雨的预测类别:
predicted_class = model.predict(X_test)
-
使用以下代码创建并打印混淆矩阵:
from sklearn.metrics import confusion_matrix import numpy as np cm = pd.DataFrame(confusion_matrix(y_test, predicted_class)) cm['Total'] = np.sum(cm, axis=1) cm = cm.append(np.sum(cm, axis=0), ignore_index=True) cm.columns = ['Predicted No', 'Predicted Yes', 'Total'] cm = cm.set_index([['Actual No', 'Actual Yes', 'Total']]) print(cm)
请查看结果输出:
图 3.38:我们 SVC 网格搜索模型的混淆矩阵
-
生成并打印分类报告,如下所示:
from sklearn.metrics import classification_report print(classification_report(y_test, predicted_class))
请查看结果输出:
图 3.39:我们 SVC 网格搜索模型的分类报告
在这里,我们展示了如何使用网格搜索调整 SVC 模型的超参数。
活动 8:为决策树分类器准备数据
解决方案:
-
导入
weather.csv
并使用以下代码将其存储为 DataFrame:import pandas as pd df = pd.read_csv('weather.csv')
-
将
Description
列进行虚拟编码,如下所示:import pandas as pd df_dummies = pd.get_dummies(df, drop_first=True)
-
使用以下代码打乱
df_dummies
:from sklearn.utils import shuffle df_shuffled = shuffle(df_dummies, random_state=42)
-
按照以下步骤将
df_shuffled
分割为 X 和 y:DV = 'Rain' X = df_shuffled.drop(DV, axis=1) y = df_shuffled[DV]
-
将
X
和y
分割为测试数据和训练数据:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
-
使用以下代码缩放
X_train
和X_test
:from sklearn.preprocessing import StandardScaler model = StandardScaler() X_train_scaled = model.fit_transform(X_train) X_test_scaled = model.transform(X_test)
活动 9:生成预测并评估决策树分类器模型的表现
解决方案:
-
使用以下代码生成预测的降雨概率:
predicted_prob = model.predict_proba(X_test_scaled)[:,1]
-
使用以下代码生成降雨的预测类别:
predicted_class = model.predict(X_test)
-
使用以下代码生成并打印混淆矩阵:
from sklearn.metrics import confusion_matrix import numpy as np cm = pd.DataFrame(confusion_matrix(y_test, predicted_class)) cm['Total'] = np.sum(cm, axis=1) cm = cm.append(np.sum(cm, axis=0), ignore_index=True) cm.columns = ['Predicted No', 'Predicted Yes', 'Total'] cm = cm.set_index([['Actual No', 'Actual Yes', 'Total']]) print(cm)
请参考此处的结果输出:
图 3.40:经过调优的决策树分类器模型的混淆矩阵
-
按以下方式打印分类报告:
from sklearn.metrics import classification_report print(classification_report(y_test, predicted_class))
请参考此处的结果输出:
图 3.41:经过调优的决策树分类器模型的分类报告
只有一个被错误分类的观测值。因此,通过在 weather.csv
数据集上调优决策树分类器模型,我们能够以极高的准确度预测降雨(或降雪)。我们可以看到,唯一的驱动特征是摄氏度温度。考虑到决策树通过递归分区进行预测,这一点是有意义的。
活动 10:调优随机森林回归模型
解决方案:
-
按以下方式指定超参数空间:
import numpy as np grid = {'criterion': ['mse','mae'], 'max_features': ['auto', 'sqrt', 'log2', None], 'min_impurity_decrease': np.linspace(0.0, 1.0, 10), 'bootstrap': [True, False], 'warm_start': [True, False]}
-
实例化
GridSearchCV
模型,使用以下代码优化解释方差:from sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestRegressor model = GridSearchCV(RandomForestRegressor(), grid, scoring='explained_variance', cv=5)
-
使用以下代码将网格搜索模型拟合到训练集(注意这可能需要一些时间):
model.fit(X_train_scaled, y_train)
请参阅此处的输出:
图 3.42:经过调优的随机森林回归模型网格搜索的输出
-
按以下方式打印调优后的参数:
best_parameters = model.best_params_ print(best_parameters)
请参阅以下结果输出:
图 3.43:经过调优的随机森林回归模型网格搜索的超参数
活动 11:生成预测并评估调优后的随机森林回归模型的表现
解决方案:
-
使用以下代码在测试数据上生成预测:
predictions = model.predict(X_test_scaled)
-
使用以下代码绘制预测值与实际值的相关性:
import matplotlib.pyplot as plt from scipy.stats import pearsonr plt.scatter(y_test, predictions) plt.xlabel('Y Test (True Values)') plt.ylabel('Predicted Values') plt.title('Predicted vs. Actual Values (r = {0:0.2f})'.format(pearsonr(y_test, predictions)[0], 2)) plt.show()
请参考此处的结果输出:
图 3.44:经过调优超参数的随机森林回归模型的预测值与实际值的散点图
-
按照以下步骤绘制残差的分布:
import seaborn as sns from scipy.stats import shapiro sns.distplot((y_test - predictions), bins = 50) plt.xlabel('Residuals') plt.ylabel('Density') plt.title('Histogram of Residuals (Shapiro W p-value = {0:0.3f})'.format(shapiro(y_test - predictions)[1])) plt.show()
请参考此处的结果输出:
图 3.45:经过调优超参数的随机森林回归模型的残差直方图
-
使用以下代码计算指标,将其放入 DataFrame 中,并打印:
from sklearn import metrics import numpy as np metrics_df = pd.DataFrame({'Metric': ['MAE', 'MSE', 'RMSE', 'R-Squared'], 'Value': [metrics.mean_absolute_error(y_test, predictions), metrics.mean_squared_error(y_test, predictions), np.sqrt(metrics.mean_squared_error(y_test, predictions)), metrics.explained_variance_score(y_test, predictions)]}).round(3) print(metrics_df)
在此查看结果输出:
图 3.46:调优超参数后的随机森林回归模型的模型评估指标
与多元线性回归相比,随机森林回归模型似乎表现不佳,表现为较大的 MAE、MSE 和 RMSE 值,以及较低的解释方差。此外,预测值与实际值之间的相关性较弱,残差也远离正态分布。然而,通过利用集成方法使用随机森林回归器,我们构建了一个能够解释 75.8%温度方差并预测温度(摄氏度+3.781 度)的模型。
第四章:降维与无监督学习
活动 12:集成 k-means 聚类与预测计算
解决方案:
在玻璃数据集被导入、打乱并标准化之后(参见练习 58):
-
实例化一个空的数据框,附加每个模型,并使用以下代码将其保存为新的数据框对象
labels_df
:import pandas as pd labels_df = pd.DataFrame()
-
使用以下代码在循环外导入
KMeans
函数:from sklearn.cluster import KMeans
-
完成 100 次迭代,如下所示:
for i in range(0, 100):
-
使用以下代码保存一个具有两个簇(事先任意决定)的 KMeans 模型对象:
model = KMeans(n_clusters=2)
-
使用以下代码拟合模型至
scaled_features
:model.fit(scaled_features)
-
生成标签数组,并将其保存为标签对象,如下所示:
labels = model.labels_
-
使用以下代码将标签存储为
labels_df
中的一列,列名为迭代名称:labels_df['Model_{}_Labels'.format(i+1)] = labels
-
在为每个 100 个模型生成标签后(参见活动 21),使用以下代码计算每一行的众数:
row_mode = labels_df.mode(axis=1)
-
将
row_mode
分配给labels_df
中的新列,如下代码所示:labels_df['row_mode'] = row_mode
-
查看
labels_df
的前五行print(labels_df.head(5))
图 4.24:labels_df 的前五行
我们通过迭代多个模型,保存每次迭代的预测结果,并将最终的预测结果设为这些预测结果的众数,从而大幅提高了对预测的信心。然而,这些预测是由使用预定簇数的模型生成的。除非我们事先知道簇的数量,否则我们需要发现最优的簇数来对观察结果进行分割。
活动 13:PCA 变换后按簇评估平均惯性
解决方案:
-
实例化一个 PCA 模型,将
n_components
参数的值设置为best_n_components
(即,记住,best_n_components = 6
),如下所示:from sklearn.decomposition import PCA model = PCA(n_components=best_n_components)
-
将模型拟合到
scaled_features
并将其转化为六个组件,如下所示:df_pca = model.fit_transform(scaled_features)
-
使用以下代码将
numpy
和KMeans
函数导入循环外:from sklearn.cluster import KMeans import numpy as np
-
实例化一个空列表
inertia_list
,我们将在每次迭代后使用以下代码将惯性值附加到该列表中:inertia_list = []
-
在内部的 for 循环中,我们将遍历 100 个模型,如下所示:
for i in range(100):
-
使用以下代码构建我们的
KMeans
模型,n_clusters=x
:model = KMeans(n_clusters=x)
注意
x 的值将由外部循环决定,详细内容请参见此处。
-
如下所示,将模型拟合到
df_pca
:model.fit(df_pca)
-
获取惯性值并使用以下代码将其保存到对象惯性中:
inertia = model.inertia_
-
使用以下代码将惯性附加到
inertia_list
:inertia_list.append(inertia)
-
进入外部循环后,使用以下代码实例化另一个空列表,以存储平均惯性值:
mean_inertia_list_PCA = []
-
由于我们想要检查在
n_clusters
从 1 到 10 的 100 个模型中的平均惯性,我们将如下实例化外部循环:for x in range(1, 11):
-
在内部循环完成 100 次迭代后,将每个模型的惯性值附加到
inertia_list
,计算该列表的平均值,并使用以下代码将对象保存为mean_inertia
:mean_inertia = np.mean(inertia_list)
-
使用以下代码将
mean_inertia
附加到mean_inertia_list_PCA
:mean_inertia_list_PCA.append(mean_inertia)
-
使用以下代码将
mean_inertia_list_PCA
打印到控制台:print(mean_inertia_list_PCA)
-
注意以下屏幕截图中的输出:
图 4.25: mean_inertia_list_PCA
第五章: 精通结构化数据
活动 14: 训练和预测一个人的收入
解决方案:
-
导入库并使用 pandas 加载收入数据集。首先,导入 pandas,然后使用
read_csv
读取数据。import pandas as pd import xgboost as xgb import numpy as np from sklearn.metrics import accuracy_score data = pd.read_csv("../data/adult-data.csv", names=['age', 'workclass', 'education-num', 'occupation', 'capital-gain', 'capital-loss', 'hours-per-week', 'income'])
我们传递列名的原因是因为数据中没有这些列名。我们这样做是为了让我们的工作更轻松。
-
使用来自 sklearn 的 Label Encoder 对字符串进行编码。首先,导入
Label Encoder
,然后逐一编码所有字符串类别列。from sklearn.preprocessing import LabelEncoder data['workclass'] = LabelEncoder().fit_transform(data['workclass']) data['occupation'] = LabelEncoder().fit_transform(data['occupation']) data['income'] = LabelEncoder().fit_transform(data['income'])
在这里,我们对所有类别字符串数据进行了编码。我们还有另一种方法可以避免重复编写相同的代码。看看你能否找到它。
-
我们首先将因变量和自变量分开。
X = data.copy() X.drop("income", inplace = True, axis = 1) Y = data.income
-
然后,我们将其分为 80:20 的训练集和测试集。
X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values
-
接下来,我们将它们转换为 DMatrix,这是该库支持的数据结构。
train = xgb.DMatrix(X_train, label=Y_train) test = xgb.DMatrix(X_test, label=Y_test)
-
然后,我们使用以下参数通过 XGBoost 训练模型。
param = {'max_depth':7, 'eta':0.1, 'silent':1, 'objective':'binary:hinge'} num_round = 50 model = xgb.train(param, train, num_round)
-
检查模型的准确性。
preds = model.predict(test) accuracy = accuracy_score(Y[int(Y.shape[0]*0.8):].values, preds) print("Accuracy: %.2f%%" % (accuracy * 100.0))
输出如下:
图 5.36: 最终模型的准确性
活动 15: 预测客户流失
解决方案:
-
使用 pandas 加载收入数据集。首先,导入 pandas,然后使用
read_csv
读取数据。import pandas as pd import numpy as np data = data = pd.read_csv("data/telco-churn.csv")
-
customerID
变量不是必需的,因为任何未来的预测都会有一个唯一的customerID
,这使得该变量在预测中无用。data.drop('customerID', axis = 1, inplace = True)
-
使用 scikit 将所有类别变量转换为整数。下面给出一个例子。
from sklearn.preprocessing import LabelEncoder data['gender'] = LabelEncoder().fit_transform(data['gender'])
-
检查数据集中变量的数据类型。
data.dtypes
变量的数据类型将如下所示:
图 5.37:变量的数据类型
-
如你所见,
TotalCharges
是一个对象。所以,将TotalCharges
的数据类型从对象转换为数值型。coerce
会将缺失值设置为 null。data.TotalCharges = pd.to_numeric(data.TotalCharges, errors='coerce')
-
将数据框转换为 XGBoost 变量,并使用之前的练习作为参考,找到适合数据集的最佳参数。
import xgboost as xgb import matplotlib.pyplot as plt X = data.copy() X.drop("Churn", inplace = True, axis = 1) Y = data.Churn X_train, X_test = X[:int(X.shape[0]*0.8)].values, X[int(X.shape[0]*0.8):].values Y_train, Y_test = Y[:int(Y.shape[0]*0.8)].values, Y[int(Y.shape[0]*0.8):].values train = xgb.DMatrix(X_train, label=Y_train) test = xgb.DMatrix(X_test, label=Y_test) test_error = {} for i in range(20): param = {'max_depth':i, 'eta':0.1, 'silent':1, 'objective':'binary:hinge'} num_round = 50 model_metrics = xgb.cv(param, train, num_round, nfold = 10) test_error[i] = model_metrics.iloc[-1]['test-error-mean'] plt.scatter(test_error.keys(),test_error.values()) plt.xlabel('Max Depth') plt.ylabel('Test Error') plt.show()
查看以下截图中的输出:
图 5.38:电信流失数据集的最大深度与测试误差的关系图
从图中可以清楚地看到,最大深度为 4 时误差最小。因此,我们将使用这一深度来训练我们的模型。
-
使用我们在前一步中选择的
max_depth
参数创建模型。param = {'max_depth':4, 'eta':0.1, 'silent':1, 'objective':'binary:hinge'} num_round = 100 model = xgb.train(param, train, num_round) preds = model.predict(test) from sklearn.metrics import accuracy_score accuracy = accuracy_score(Y[int(Y.shape[0]*0.8):].values, preds) print("Accuracy: %.2f%%" % (accuracy * 100.0))
输出如下:
图 5.39:最终准确率
-
使用以下代码保存模型以供将来使用:
model.save_model('churn-model.model')
活动 16:预测客户的购买金额
解决方案:
-
使用 pandas 加载
黑色星期五
数据集。首先,导入pandas
,然后使用read_csv
读取数据。import pandas as pd import numpy as np data = data = pd.read_csv("data/BlackFriday.csv")
-
User_ID
变量不需要用于新用户 ID 的预测,因此我们将其删除。data.isnull().sum() data.drop(['User_ID', 'Product_Category_2', 'Product_Category_3'], axis = 1, inplace = True)
产品类别变量有较高的空值比例,因此我们也会将其删除。
-
使用 scikit-learn 将所有分类变量转换为整数。
from collections import defaultdict from sklearn.preprocessing import LabelEncoder, MinMaxScaler label_dict = defaultdict(LabelEncoder) data[['Product_ID', 'Gender', 'Age', 'Occupation', 'City_Category', 'Stay_In_Current_City_Years', 'Marital_Status', 'Product_Category_1']] = data[['Product_ID', 'Gender', 'Age', 'Occupation', 'City_Category', 'Stay_In_Current_City_Years', 'Marital_Status', 'Product_Category_1']].apply(lambda x: label_dict[x.name].fit_transform(x))
-
将数据拆分为训练集和测试集,并转换为嵌入层所需的格式。
from sklearn.model_selection import train_test_split X = data y = X.pop('Purchase') X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=9) cat_cols_dict = {col: list(data[col].unique()) for col in ['Product_ID', 'Gender', 'Age', 'Occupation', 'City_Category', 'Stay_In_Current_City_Years', 'Marital_Status', 'Product_Category_1']} train_input_list = [] test_input_list = [] for col in cat_cols_dict.keys(): raw_values = np.unique(data[col]) value_map = {} for i in range(len(raw_values)): value_map[raw_values[i]] = i train_input_list.append(X_train[col].map(value_map).values) test_input_list.append(X_test[col].map(value_map).fillna(0).values)
-
使用 Keras 中的嵌入层和全连接层创建网络,并进行超参数调整以获得最佳准确率。
from keras.models import Model from keras.layers import Input, Dense, Concatenate, Reshape, Dropout from keras.layers.embeddings import Embedding cols_out_dict = { 'Product_ID': 20, 'Gender': 1, 'Age': 2, 'Occupation': 6, 'City_Category': 1, 'Stay_In_Current_City_Years': 2, 'Marital_Status': 1, 'Product_Category_1': 9 } inputs = [] embeddings = [] for col in cat_cols_dict.keys(): inp = Input(shape=(1,), name = 'input_' + col) embedding = Embedding(len(cat_cols_dict[col]), cols_out_dict[col], input_length=1, name = 'embedding_' + col)(inp) embedding = Reshape(target_shape=(cols_out_dict[col],))(embedding) inputs.append(inp) embeddings.append(embedding)
-
现在,我们在嵌入层之后创建一个三层网络。
x = Concatenate()(embeddings) x = Dense(4, activation='relu')(x) x = Dense(2, activation='relu')(x) output = Dense(1, activation='relu')(x) model = Model(inputs, output) model.compile(loss='mae', optimizer='adam') model.fit(train_input_list, y_train, validation_data = (test_input_list, y_test), epochs=20, batch_size=128)
-
检查模型在测试集上的 RMSE。
from sklearn.metrics import mean_squared_error y_pred = model.predict(test_input_list) np.sqrt(mean_squared_error(y_test, y_pred))
RMSE 为:
图 5.40:RMSE 模型
-
可视化产品 ID 的嵌入。
import matplotlib.pyplot as plt from sklearn.decomposition import PCA embedding_Product_ID = model.get_layer('embedding_Product_ID').get_weights()[0] pca = PCA(n_components=2) Y = pca.fit_transform(embedding_Product_ID[:40]) plt.figure(figsize=(8,8)) plt.scatter(-Y[:, 0], -Y[:, 1]) for i, txt in enumerate(label_dict['Product_ID'].inverse_transform(cat_cols_dict['Product_ID'])[:40]): plt.annotate(txt, (-Y[i, 0],-Y[i, 1]), xytext = (-20, 8), textcoords = 'offset points') plt.show()
图表如下:
图 5.41:聚类模型的图
从图中可以看出,相似的产品已被模型聚集在一起。
-
保存模型以供将来使用。
model.save ('black-friday.model')
第六章:解码图像
活动 17:预测图像是猫还是狗
解决方案:
-
如果你查看数据集中的图像名称,你会发现狗的图像名称以 dog 开头,后面跟着'.'和数字,例如:"dog.123.jpg"。类似地,猫的图像名称以 cat 开头。让我们创建一个函数,从文件名中提取标签:
def get_label(file): class_label = file.split('.')[0] if class_label == 'dog': label_vector = [1,0] elif class_label == 'cat': label_vector = [0,1] return label_vector
然后,创建一个函数来读取、调整大小并预处理图像:
import os import numpy as np from PIL import Image from tqdm import tqdm from random import shuffle SIZE = 50 def get_data(): data = [] files = os.listdir(PATH) for image in tqdm(files): label_vector = get_label(image) img = Image.open(PATH + image).convert('L') img = img.resize((SIZE,SIZE)) data.append([np.asarray(img),np.array(label_vector)]) shuffle(data) return data
这里的
SIZE
指的是我们输入模型的最终方形图像的尺寸。我们将图像调整为长度和宽度均为SIZE
。注意
当运行
os.listdir(PATH)
时,你会发现所有猫的图像会先出现,紧接着是狗的图像。 -
为了让训练集和测试集中的两个类别具有相同的分布,我们将对数据进行洗牌。
-
定义图像的尺寸并读取数据。将加载的数据拆分为训练集和测试集:
data = get_data() train = data[:7000] test = data[7000:] x_train = [data[0] for data in train] y_train = [data[1] for data in train] x_test = [data[0] for data in test] y_test = [data[1] for data in test]
-
将列表转换为 numpy 数组,并将图像重塑为 Keras 接受的格式:
y_train = np.array(y_train) y_test = np.array(y_test) x_train = np.array(x_train).reshape(-1, SIZE, SIZE, 1) x_test = np.array(x_test).reshape(-1, SIZE, SIZE, 1)
-
创建一个使用正则化的 CNN 模型来进行训练:
from keras.models import Sequential from keras.layers import Dense, Dropout, Conv2D, MaxPool2D, Flatten, BatchNormalization model = Sequential()
添加卷积层:
model.add(Conv2D(48, (3, 3), activation='relu', padding='same', input_shape=(50,50,1))) model.add(Conv2D(48, (3, 3), activation='relu'))
添加池化层:
model.add(MaxPool2D(pool_size=(2, 2)))
-
使用以下代码添加批量归一化层和 dropout 层:
model.add(BatchNormalization()) model.add(Dropout(0.10))
-
将二维矩阵展平为一维向量:
model.add(Flatten())
-
使用密集层作为模型的最终层:
model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(2, activation='softmax'))
-
编译模型并使用训练数据进行训练:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = ['accuracy']) Define the number of epochs you want to train the model for: EPOCHS = 10 model_details = model.fit(x_train, y_train, batch_size = 128, epochs = EPOCHS, validation_data= (x_test, y_test), verbose=1)
-
打印模型在测试集上的准确度:
score = model.evaluate(x_test, y_test) print("Accuracy: {0:.2f}%".format(score[1]*100))
图 6.39:测试集上的模型准确度
-
打印模型在训练集上的准确度:
score = model.evaluate(x_train, y_train) print("Accuracy: {0:.2f}%".format(score[1]*100))
图 6.40:训练集上的模型准确度
该模型在测试集上的准确度为 70.4%。训练集的准确度非常高,达到了 96%。这意味着模型已经开始出现过拟合。提高模型以获得最佳的准确度是留给你作为练习的部分。你可以使用之前练习中的代码绘制错误预测的图像,以了解模型的表现:
import matplotlib.pyplot as plt
y_pred = model.predict(x_test)
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(y_test,axis=1))[0]
labels = ['dog', 'cat']
image = 5
plt.imshow(x_test[incorrect_indices[image]].reshape(50,50), cmap=plt.get_cmap('gray'))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
图 6.41:正则化的 CNN 模型错误预测狗
活动 18:识别和增强图像
解决方案:
-
创建获取数据集图像和标签的函数:
from PIL import Image def get_input(file): return Image.open(PATH+file) def get_output(file): class_label = file.split('.')[0] if class_label == 'dog': label_vector = [1,0] elif class_label == 'cat': label_vector = [0,1] return label_vector
-
创建预处理和增强图像的函数:
SIZE = 50 def preprocess_input(image): # Data preprocessing image = image.convert('L') image = image.resize((SIZE,SIZE)) # Data augmentation random_vertical_shift(image, shift=0.2) random_horizontal_shift(image, shift=0.2) random_rotate(image, rot_range=45) random_horizontal_flip(image) return np.array(image).reshape(SIZE,SIZE,1)
-
实现增强函数,在传入图像时随机执行增强,并返回增强后的图像结果。
这是水平翻转:
import random def random_horizontal_flip(image): toss = random.randint(1, 2) if toss == 1: return image.transpose(Image.FLIP_LEFT_RIGHT) else: return image
这是旋转:
def random_rotate(image, rot_range): value = random.randint(-rot_range,rot_range) return image.rotate(value)
这是图像平移:
import PIL def random_horizontal_shift(image, shift): width, height = image.size rand_shift = random.randint(0,shift*width) image = PIL.ImageChops.offset(image, rand_shift, 0) image.paste((0), (0, 0, rand_shift, height)) return image def random_vertical_shift(image, shift): width, height = image.size rand_shift = random.randint(0,shift*height) image = PIL.ImageChops.offset(image, 0, rand_shift) image.paste((0), (0, 0, width, rand_shift)) return image
-
最后,创建一个生成器,用于生成用于训练模型的图像批次:
import numpy as np def custom_image_generator(images, batch_size = 128): while True: # Randomly select images for the batch batch_images = np.random.choice(images, size = batch_size) batch_input = [] batch_output = [] # Read image, perform preprocessing and get labels for file in batch_images: # Function that reads and returns the image input_image = get_input(file) # Function that gets the label of the image label = get_output(file) # Function that pre-processes and augments the image image = preprocess_input(input_image) batch_input.append(image) batch_output.append(label) batch_x = np.array(batch_input) batch_y = np.array(batch_output) # Return a tuple of (images,labels) to feed the network yield(batch_x, batch_y)
-
创建加载测试数据集图像和标签的函数:
def get_label(file): class_label = file.split('.')[0] if class_label == 'dog': label_vector = [1,0] elif class_label == 'cat': label_vector = [0,1] return label_vector
这个
get_data
函数类似于我们在活动 1中使用的函数。这里的修改是,我们将图像列表作为输入参数,然后返回一个包含图像及其标签的元组:def get_data(files): data_image = [] labels = [] for image in tqdm(files): label_vector = get_label(image) img = Image.open(PATH + image).convert('L') img = img.resize((SIZE,SIZE)) labels.append(label_vector) data_image.append(np.asarray(img).reshape(SIZE,SIZE,1)) data_x = np.array(data_image) data_y = np.array(labels) return (data_x, data_y)
-
现在,创建训练集和测试集的划分,并加载测试数据集:
import os files = os.listdir(PATH) random.shuffle(files) train = files[:7000] test = files[7000:] validation_data = get_data(test)
-
创建模型并进行训练:
from keras.models import Sequential model = Sequential()
添加卷积层
from keras.layers import Input, Dense, Dropout, Conv2D, MaxPool2D, Flatten, BatchNormalization model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(50,50,1))) model.add(Conv2D(32, (3, 3), activation='relu'))
添加池化层:
model.add(MaxPool2D(pool_size=(2, 2)))
-
添加批量归一化层以及一个 dropout 层:
model.add(BatchNormalization()) model.add(Dropout(0.10))
-
将二维矩阵展平为一维向量:
model.add(Flatten())
-
使用密集层作为模型的最终层:
model.add(Dense(512, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(2, activation='softmax'))
-
编译模型并使用你创建的生成器进行训练:
EPOCHS = 10 BATCH_SIZE = 128 model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = ['accuracy']) model_details = model.fit_generator(custom_image_generator(train, batch_size = BATCH_SIZE), steps_per_epoch = len(train) // BATCH_SIZE, epochs = EPOCHS, validation_data= validation_data, verbose=1)
该模型的测试集准确率为 72.6%,相比于活动 21中的模型有所提升。你会发现训练准确率非常高,达到 98%。这意味着该模型已经开始过拟合,类似于活动 21中的模型。可能是由于数据增强不足所致。尝试更改数据增强参数,看看准确率是否有所变化。或者,你可以修改神经网络的架构以获得更好的结果。你可以绘制预测错误的图像,以了解模型的表现。
import matplotlib.pyplot as plt
y_pred = model.predict(validation_data[0])
incorrect_indices = np.nonzero(np.argmax(y_pred,axis=1) != np.argmax(validation_data[1],axis=1))[0]
labels = ['dog', 'cat']
image = 7
plt.imshow(validation_data[0][incorrect_indices[image]].reshape(50,50), cmap=plt.get_cmap('gray'))
plt.show()
print("Prediction: {0}".format(labels[np.argmax(y_pred[incorrect_indices[image]])]))
图 6.42:数据增强 CNN 模型错误预测一只猫
第七章:处理人类语言
活动 19:预测电影评论的情感
解决方案:
-
使用 pandas 在 Python 中读取 IMDB 电影评论数据集:
import pandas as pd data = pd.read_csv('../../chapter 7/data/movie_reviews.csv', encoding='latin-1')
-
将推文转换为小写字母,以减少唯一单词的数量:
data.text = data.text.str.lower()
注意:
请记住,"
Hello
" 和 "hellow
" 对计算机来说是不同的。 -
使用 RegEx 和
clean_str
函数清洗评论:import re def clean_str(string): string = re.sub(r"https?\://\S+", '', string) string = re.sub(r'\<a href', ' ', string) string = re.sub(r'&', '', string) string = re.sub(r'<br />', ' ', string) string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', ' ', string) string = re.sub('\d','', string) string = re.sub(r"can\'t", "cannot", string) string = re.sub(r"it\'s", "it is", string) return string data.SentimentText = data.SentimentText.apply(lambda x: clean_str(str(x)))
-
接下来,从评论中去除停用词和其他频繁出现的不必要的词:
注意:
要了解我们如何找到这些内容,单词请参见练习 51。
-
这一步将字符串转换为标记(在下一步中会派上用场):
from nltk.corpus import stopwords from nltk.tokenize import word_tokenize,sent_tokenize stop_words = stopwords.words('english') + ['movie', 'film', 'time'] stop_words = set(stop_words) remove_stop_words = lambda r: [[word for word in word_tokenize(sente) if word not in stop_words] for sente in sent_tokenize(r)] data['SentimentText'] = data['SentimentText'].apply(remove_stop_words)
-
使用前一步骤中创建的标记来创建评论的词嵌入。在这里,我们将使用 gensim 的 Word2Vec 来创建这些嵌入向量:
from gensim.models import Word2Vec model = Word2Vec( data['SentimentText'].apply(lambda x: x[0]), iter=10, size=16, window=5, min_count=5, workers=10) model.wv.save_word2vec_format('movie_embedding.txt', binary=False)
-
将标记合并为一个字符串,然后删除任何在去除停用词后为空的评论:
def combine_text(text): try: return ' '.join(text[0]) except: return np.nan data.SentimentText = data.SentimentText.apply(lambda x: combine_text(x)) data = data.dropna(how='any')
-
使用 Keras 的 Tokenizer 对评论进行分词,并将其转换为数字:
from keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(num_words=5000) tokenizer.fit_on_texts(list(data['SentimentText'])) sequences = tokenizer.texts_to_sequences(data['SentimentText']) word_index = tokenizer.word_index
-
最后,将推文填充到最多 100 个单词。这将删除超过 100 个单词限制后的任何单词,并在单词数少于 100 时填充 0:
from keras.preprocessing.sequence import pad_sequences reviews = pad_sequences(sequences, maxlen=100)
-
加载创建的嵌入,使用在文本处理部分中讨论的
load_embedding
函数获取嵌入矩阵:import numpy as np def load_embedding(filename, word_index , num_words, embedding_dim): embeddings_index = {} file = open(filename, encoding="utf-8") for line in file: values = line.split() word = values[0] coef = np.asarray(values[1:]) embeddings_index[word] = coef file.close() embedding_matrix = np.zeros((num_words, embedding_dim)) for word, pos in word_index.items(): if pos >= num_words: continue embedding_vector = embeddings_index.get(word) if embedding_vector is not None: embedding_matrix[pos] = embedding_vector return embedding_matrix embedding_matrix = load_embedding('movie_embedding.txt', word_index, len(word_index), 16)
-
使用 pandas 的
get_dummies
函数将标签转换为 one-hot 向量,并以 80:20 的比例将数据集分为测试集和训练集:from sklearn.model_selection import train_test_split labels = pd.get_dummies(data.Sentiment) X_train, X_test, y_train, y_test = train_test_split(reviews,labels, test_size=0.2, random_state=9)
-
创建神经网络模型,首先是输入层和嵌入层。该层将输入的单词转换为其嵌入向量:
from keras.layers import Input, Dense, Dropout, BatchNormalization, Embedding, Flatten from keras.models import Model inp = Input((100,)) embedding_layer = Embedding(len(word_index), 16, weights=[embedding_matrix], input_length=100, trainable=False)(inp)
-
使用 Keras 创建其余的全连接神经网络:
model = Flatten()(embedding_layer) model = BatchNormalization()(model) model = Dropout(0.10)(model) model = Dense(units=1024, activation='relu')(model) model = Dense(units=256, activation='relu')(model) model = Dropout(0.5)(model) predictions = Dense(units=2, activation='softmax')(model) model = Model(inputs = inp, outputs = predictions)
-
编译并训练模型 10 个 epoch。你可以修改模型和超参数,尝试获得更好的准确性:
model.compile(loss='binary_crossentropy', optimizer='sgd', metrics = ['acc']) model.fit(X_train, y_train, validation_data = (X_test, y_test), epochs=10, batch_size=256)
-
使用以下方法计算模型在测试集上的准确性,以查看我们的模型在先前未见数据上的表现如何:
from sklearn.metrics import accuracy_score preds = model.predict(X_test) accuracy_score(np.argmax(preds, 1), np.argmax(y_test.values, 1))
模型的准确率是:
图 7.39:模型准确性
-
绘制模型的混淆矩阵,以便更好地理解模型的预测效果:
y_actual = pd.Series(np.argmax(y_test.values, axis=1), name='Actual') y_pred = pd.Series(np.argmax(preds, axis=1), name='Predicted') pd.crosstab(y_actual, y_pred, margins=True)
检查以下内容:
图 7.40: 模型的混淆矩阵(0 = 负面情感,1 = 正面情感)
-
使用以下代码检查模型的表现,查看随机评论上的情感预测:
review_num = 111 print("Review: \n"+tokenizer.sequences_to_texts([X_test[review_num]])[0]) sentiment = "Positive" if np.argmax(preds[review_num]) else "Negative" print("\nPredicted sentiment = "+ sentiment) sentiment = "Positive" if np.argmax(y_test.values[review_num]) else "Negative" print("\nActual sentiment = "+ sentiment)
检查你是否收到以下输出:
图 7.41: 来自 IMDB 数据集的评论
活动 20: 从推文中预测情感
解决方案:
-
使用 pandas 读取推文数据集,并按照以下代码重命名列:
import pandas as pd data = pd.read_csv('tweet-data.csv', encoding='latin-1', header=None) data.columns = ['sentiment', 'id', 'date', 'q', 'user', 'text']
-
删除以下列,因为我们不会使用它们。如果你想在尝试提高准确性时分析和使用它们,可以自行操作:
data = data.drop(['id', 'date', 'q', 'user'], axis=1)
-
我们仅在数据的一个子集(400,000 条推文)上执行此活动,以节省时间。如果你愿意,可以处理整个数据集:
data = data.sample(400000).reset_index(drop=True)
-
将推文转换为小写,以减少唯一单词的数量。请记住,"
Hello
" 和 "hellow
" 对计算机来说是不同的:data.text = data.text.str.lower()
-
使用
clean_str
函数清洗推文:import re def clean_str(string): string = re.sub(r"https?\://\S+", '', string) string = re.sub(r"@\w*\s", '', string) string = re.sub(r'\<a href', ' ', string) string = re.sub(r'&', '', string) string = re.sub(r'<br />', ' ', string) string = re.sub(r'[_"\-;%()|+&=*%.,!?:#$@\[\]/]', ' ', string) string = re.sub('\d','', string) return string data.text = data.text.apply(lambda x: clean_str(str(x)))
-
移除推文中的所有停用词,就像在
文本预处理
部分所做的那样:from nltk.corpus import stopwords from nltk.tokenize import word_tokenize,sent_tokenize stop_words = stopwords.words('english') stop_words = set(stop_words) remove_stop_words = lambda r: [[word for word in word_tokenize(sente) if word not in stop_words] for sente in sent_tokenize(r)] data['text'] = data['text'].apply(remove_stop_words) def combine_text(text): try: return ' '.join(text[0]) except: return np.nan data.text = data.text.apply(lambda x: combine_text(x)) data = data.dropna(how='any')
-
使用 Keras Tokenizer 对推文进行分词,并将其转换为数字:
from keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(num_words=5000) tokenizer.fit_on_texts(list(data['text'])) sequences = tokenizer.texts_to_sequences(data['text']) word_index = tokenizer.word_index
-
最后,将推文填充为最多 50 个单词。这将删除超过 50 个单词后的任何单词,并在单词数少于 50 时添加 0:
from keras.preprocessing.sequence import pad_sequences tweets = pad_sequences(sequences, maxlen=50)
-
使用
load_embedding
函数从下载的 GloVe 嵌入文件中创建嵌入矩阵:import numpy as np def load_embedding(filename, word_index , num_words, embedding_dim): embeddings_index = {} file = open(filename, encoding="utf-8") for line in file: values = line.split() word = values[0] coef = np.asarray(values[1:]) embeddings_index[word] = coef file.close() embedding_matrix = np.zeros((num_words, embedding_dim)) for word, pos in word_index.items(): if pos >= num_words: continue embedding_vector = embeddings_index.get(word) if embedding_vector is not None: embedding_matrix[pos] = embedding_vector return embedding_matrix embedding_matrix = load_embedding('../../embedding/glove.twitter.27B.50d.txt', word_index, len(word_index), 50)
-
将数据集分割为训练集和测试集,比例为 80:20。你可以尝试不同的分割方式:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(tweets, pd.get_dummies(data.sentiment), test_size=0.2, random_state=9)
-
创建 LSTM 模型来预测情感。你可以修改它以创建自己的神经网络:
from keras.models import Sequential from keras.layers import Dense, Dropout, BatchNormalization, Embedding, Flatten, LSTM embedding_layer = Embedding(len(word_index), 50, weights=[embedding_matrix], input_length=50, trainable=False) model = Sequential() model.add(embedding_layer) model.add(Dropout(0.5)) model.add(LSTM(100, dropout=0.2)) model.add(Dense(2, activation='softmax')) model.compile(loss='binary_crossentropy', optimizer='sgd', metrics = ['acc'])
-
训练模型。此处我们仅训练 10 个 epochs。你可以增加 epochs 的数量以尝试获得更好的准确性:
model.fit(X_train, y_train, validation_data = (X_test, y_test), epochs=10, batch_size=256)
-
通过预测测试集中的几条推文的情感,检查模型的表现:
preds = model.predict(X_test) review_num = 1 print("Tweet: \n"+tokenizer.sequences_to_texts([X_test[review_num]])[0]) sentiment = "Positive" if np.argmax(preds[review_num]) else "Negative" print("\nPredicted sentiment = "+ sentiment) sentiment = "Positive" if np.argmax(y_test.values[review_num]) else "Negative" print("\nActual sentiment = "+ sentiment)
输出如下:
图 7.42: 正面(左)和负面(右)推文及其预测结果
第八章:行业技巧和窍门
活动 21: 使用 InceptionV3 进行图像分类
解决方案:
-
创建函数以获取图像和标签。此处的
PATH
变量包含训练数据集的路径。from PIL import Image def get_input(file): return Image.open(PATH+file) def get_output(file): class_label = file.split('.')[0] if class_label == 'dog': label_vector = [1,0] elif class_label == 'cat': label_vector = [0,1] return label_vector
-
设置
SIZE
和CHANNELS
。SIZE
是方形图像输入的维度。CHANNELS
是训练数据图像中的通道数。RGB 图像有 3 个通道。SIZE = 200 CHANNELS = 3
-
创建一个函数来预处理和增强图像:
def preprocess_input(image): # Data preprocessing image = image.resize((SIZE,SIZE)) image = np.array(image).reshape(SIZE,SIZE,CHANNELS) # Normalize image image = image/255.0 return image
-
最后,开发生成批次的生成器:
import numpy as np def custom_image_generator(images, batch_size = 128): while True: # Randomly select images for the batch batch_images = np.random.choice(images, size = batch_size) batch_input = [] batch_output = [] # Read image, perform preprocessing and get labels for file in batch_images: # Function that reads and returns the image input_image = get_input(file) # Function that gets the label of the image label = get_output(file) # Function that pre-processes and augments the image image = preprocess_input(input_image) batch_input.append(image) batch_output.append(label) batch_x = np.array(batch_input) batch_y = np.array(batch_output) # Return a tuple of (images,labels) to feed the network yield(batch_x, batch_y)
-
接下来,我们将读取验证数据。创建一个函数来读取图像及其标签:
from tqdm import tqdm def get_data(files): data_image = [] labels = [] for image in tqdm(files): label_vector = get_output(image) img = Image.open(PATH + image) img = img.resize((SIZE,SIZE)) labels.append(label_vector) img = np.asarray(img).reshape(SIZE,SIZE,CHANNELS) img = img/255.0 data_image.append(img) data_x = np.array(data_image) data_y = np.array(labels) return (data_x, data_y)
-
读取验证文件:
import os files = os.listdir(PATH) random.shuffle(files) train = files[:7000] test = files[7000:] validation_data = get_data(test) 7\. Plot a few images from the dataset to see whether you loaded the files correctly: import matplotlib.pyplot as plt plt.figure(figsize=(20,10)) columns = 5 for i in range(columns): plt.subplot(5 / columns + 1, columns, i + 1) plt.imshow(validation_data[0][i])
这里展示了一些图像的随机样本:
图 8.16: 加载数据集中的样本图像
-
加载 Inception 模型并传递输入图像的形状:
from keras.applications.inception_v3 import InceptionV3 base_model = InceptionV3(weights='imagenet', include_top=False, input_shape=(SIZE,SIZE,CHANNELS))
-
根据我们的问题添加输出的全连接层:
from keras.layers import GlobalAveragePooling2D, Dense, Dropout from keras.models import Model x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(256, activation='relu')(x) x = Dropout(0.5)(x) predictions = Dense(2, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions)
-
接下来,编译模型,使其准备好进行训练:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = ['accuracy']) And then perform the training of the model: EPOCHS = 50 BATCH_SIZE = 128 model_details = model.fit_generator(custom_image_generator(train, batch_size = BATCH_SIZE), steps_per_epoch = len(train) // BATCH_SIZE, epochs = EPOCHS, validation_data= validation_data, verbose=1)
-
评估模型并获取准确率:
score = model.evaluate(validation_data[0], validation_data[1]) print("Accuracy: {0:.2f}%".format(score[1]*100))
准确率如下:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_17.jpg)
图 8.17:模型准确率
活动 22:使用迁移学习预测图像
解决方案:
-
首先,设置随机数种子,以确保结果是可重复的:
from numpy.random import seed seed(1) from tensorflow import set_random_seed set_random_seed(1)
-
设置
SIZE
和CHANNELS
SIZE
是输入图像的维度,CHANNELS
是训练数据图像中的通道数。RGB 图像有 3 个通道。SIZE = 200 CHANNELS = 3
-
创建函数以获取图像和标签。这里的
PATH
变量包含训练数据集的路径。from PIL import Image def get_input(file): return Image.open(PATH+file) def get_output(file): class_label = file.split('.')[0] if class_label == 'dog': label_vector = [1,0] elif class_label == 'cat': label_vector = [0,1] return label_vector
-
创建一个函数来预处理和增强图像:
def preprocess_input(image): # Data preprocessing image = image.resize((SIZE,SIZE)) image = np.array(image).reshape(SIZE,SIZE,CHANNELS) # Normalize image image = image/255.0 return image
-
最后,创建生成器,它将生成批量数据:
import numpy as np def custom_image_generator(images, batch_size = 128): while True: # Randomly select images for the batch batch_images = np.random.choice(images, size = batch_size) batch_input = [] batch_output = [] # Read image, perform preprocessing and get labels for file in batch_images: # Function that reads and returns the image input_image = get_input(file) # Function that gets the label of the image label = get_output(file) # Function that pre-processes and augments the image image = preprocess_input(input_image) batch_input.append(image) batch_output.append(label) batch_x = np.array(batch_input) batch_y = np.array(batch_output) # Return a tuple of (images,labels) to feed the network yield(batch_x, batch_y)
-
接下来,我们将读取开发集和测试集数据。创建一个函数来读取图像及其标签:
from tqdm import tqdm def get_data(files): data_image = [] labels = [] for image in tqdm(files): label_vector = get_output(image) img = Image.open(PATH + image) img = img.resize((SIZE,SIZE)) labels.append(label_vector) img = np.asarray(img).reshape(SIZE,SIZE,CHANNELS) img = img/255.0 data_image.append(img) data_x = np.array(data_image) data_y = np.array(labels) return (data_x, data_y)
-
现在读取开发集和测试集文件。训练/开发/测试集的比例为
70%/15%/15%
。import random random.shuffle(files) train = files[:7000] development = files[7000:8500] test = files[8500:] development_data = get_data(development) test_data = get_data(test)
-
从数据集中绘制一些图像,检查是否正确加载了文件:
import matplotlib.pyplot as plt plt.figure(figsize=(20,10)) columns = 5 for i in range(columns): plt.subplot(5 / columns + 1, columns, i + 1) plt.imshow(validation_data[0][i])
查看以下截图中的输出:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_18.jpg)
图 8.18:加载数据集中的样本图像
-
加载 Inception 模型并传入输入图像的形状:
from keras.applications.inception_v3 import InceptionV3 base_model = InceptionV3(weights='imagenet', include_top=False, input_shape=(200,200,3)) 10\. Add the output dense layer according to our problem: from keras.models import Model from keras.layers import GlobalAveragePooling2D, Dense, Dropout x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(256, activation='relu')(x) keep_prob = 0.5 x = Dropout(rate = 1 - keep_prob)(x) predictions = Dense(2, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions)
-
这一次,我们将冻结模型的前五层,以帮助加快训练时间:
for layer in base_model.layers[:5]: layer.trainable = False
-
编译模型,使其准备好进行训练:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics = ['accuracy'])
-
为 Keras 创建回调函数:
from keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping, TensorBoard callbacks = [ TensorBoard(log_dir='./logs', update_freq='epoch'), EarlyStopping(monitor = "val_loss", patience = 18, verbose = 1, min_delta = 0.001, mode = "min"), ReduceLROnPlateau(monitor = "val_loss", factor = 0.2, patience = 8, verbose = 1, mode = "min"), ModelCheckpoint(monitor = "val_loss", filepath = "Dogs-vs-Cats-InceptionV3-{epoch:02d}-{val_loss:.2f}.hdf5", save_best_only=True, period = 1)]
注意
EPOCHS = 50 BATCH_SIZE = 128 model_details = model.fit_generator(custom_image_generator(train, batch_size = BATCH_SIZE), steps_per_epoch = len(train) // BATCH_SIZE, epochs = EPOCHS, callbacks = callbacks, validation_data= development_data, verbose=1)
以下是 TensorBoard 上的训练日志:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_19.jpg)
图 8.19:TensorBoard 上的训练集日志
-
现在,您可以通过将开发集的准确率作为指标来微调超参数。
TensorBoard 工具显示了开发集的日志:
图 8.20:TensorBoard 上的验证集日志
可以从下图中观察到学习率的下降:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_21.jpg)
图 8.21:TensorBoard 上的学习率日志
-
在测试集上评估模型并获取准确率:
score = model.evaluate(test_data[0], test_data[1]) print("Accuracy: {0:.2f}%".format(score[1]*100))
为了完全理解,请参考以下输出截图:
](https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/ds-py/img/C13322_08_22.jpg)
图 8.22:模型在测试集上的最终准确率
如您所见,模型在测试集上的准确率为 93.6%,这与开发集的准确率(TensorBoard 训练日志中的 93.3%)不同。早停回调函数在开发集的损失没有显著改善时停止了训练,这帮助我们节省了时间。学习率在九个 epoch 后减少,这有助于训练,正如以下所示:
图 8.23:模型训练日志片段
内容
-
前言
-
关于本书
-
关于作者
-
学习目标
-
读者群体
-
方法
-
最低硬件要求
-
软件要求
-
安装和设置
-
使用 Kaggle 加速实验
-
约定
-
安装代码包
-
-
-
第一章
-
数据科学和数据预处理简介
-
介绍
-
Python 库
-
建立机器学习模型的路线图
-
数据表示
-
自变量和目标变量
-
练习 1:加载示例数据集并创建特征矩阵和目标矩阵
-
-
数据清洗
-
练习 2:删除缺失数据
-
练习 3:填补缺失数据
-
练习 4:查找并删除数据中的异常值
-
-
数据集成
- 练习 5:集成数据
-
数据转换
-
处理分类数据
-
练习 6:用数字简单替换分类数据
-
练习 7:使用标签编码将分类数据转换为数值数据
-
练习 8:使用独热编码将分类数据转换为数值数据
-
-
不同规模的数据
-
练习 9:使用标准缩放器方法实现缩放
-
练习 10:使用 MinMax 缩放器方法实现缩放
-
-
数据离散化
- 练习 11:连续数据的离散化
-
训练和测试数据
-
练习 12:将数据分割为训练和测试集
-
活动 1:使用银行营销订阅数据集进行预处理
-
-
监督学习
-
无监督学习
-
强化学习
-
性能指标
-
总结
-
-
第二章
-
数据可视化
-
介绍
-
功能方法
-
练习 13:功能方法 – 线图
-
练习 14:功能方法 – 在线图中添加第二条线
-
活动 2:线图
-
练习 15:创建条形图
-
活动 3:条形图
-
练习 16:功能方法 – 直方图
-
练习 17:功能方法 – 箱线图
-
练习 18:散点图
-
-
使用子图的面向对象方法
-
练习 19:使用子图绘制单线图
-
练习 20:使用子图绘制多条线图
-
活动 4:使用子图绘制多种图表类型
-
-
总结
-
-
第三章
-
通过 Scikit-Learn 介绍机器学习
-
介绍
-
线性和逻辑回归介绍
-
简单线性回归
-
练习 21:为线性回归模型准备数据
-
练习 22:拟合简单线性回归模型并确定截距和系数
-
练习 23:生成预测并评估简单线性回归模型的性能
-
-
多元线性回归
-
练习 24:拟合多元线性回归模型并确定截距和系数
-
活动 5:生成预测并评估多元线性回归模型的性能
-
-
逻辑回归
-
练习 25:拟合逻辑回归模型并确定截距和系数
-
练习 26:生成预测并评估逻辑回归模型的性能
-
练习 27:调优多元逻辑回归模型的超参数
-
活动 6:生成预测并评估调优后的逻辑回归模型性能
-
-
最大间隔分类(SVM)
-
练习 28:为支持向量分类器(SVC)模型准备数据
-
练习 29:使用网格搜索调优 SVC 模型
-
活动 7:生成预测并评估 SVC 网格搜索模型的性能
-
-
决策树
-
活动 8:为决策树分类器准备数据
-
练习 30:使用网格搜索调优决策树分类器
-
练习 31:通过编程提取决策树分类器网格搜索模型中的调优超参数
-
活动 9:生成预测并评估决策树分类模型的性能
-
-
随机森林
-
练习 32:为随机森林回归模型准备数据
-
活动 10:调优随机森林回归模型
-
练习 33:通过编程提取调优后的超参数并确定随机森林回归模型网格搜索中的特征重要性
-
活动 11:生成预测并评估调优后的随机森林回归模型的性能
-
-
总结
-
-
第四章
-
降维与无监督学习
-
简介
-
层次聚类分析(HCA)
-
练习 34:构建 HCA 模型
-
练习 35:绘制 HCA 模型并分配预测
-
-
K-means 聚类
-
练习 36:拟合 k-means 模型并分配预测
-
活动 12:集成 k-means 聚类与计算预测
-
练习 37:通过 n_clusters 计算均值惯性
-
练习 38:通过 n_clusters 绘制均值惯性图
-
-
主成分分析(PCA)
-
练习 39:拟合 PCA 模型
-
练习 40:使用解释方差的阈值选择 n_components
-
活动 13:PCA 转换后评估每个聚类的均值惯性
-
练习 41:通过 n_clusters 的惯性进行视觉比较
-
-
使用线性判别分析(LDA)进行监督数据压缩
-
练习 42:拟合 LDA 模型
-
练习 43:在分类模型中使用 LDA 转换后的成分
-
-
总结
-
-
第五章
-
掌握结构化数据
-
介绍
-
提升算法
-
梯度提升机(GBM)
-
XGBoost(极端梯度提升)
-
练习 44:使用 XGBoost 库进行分类
-
-
XGBoost 库
-
控制模型过拟合
-
处理不平衡数据集
-
活动 14:训练并预测一个人的收入
-
-
外部内存使用
-
交叉验证
- 练习 45:使用交叉验证查找最佳超参数
-
保存和加载模型
-
练习 46:创建一个根据实时输入进行预测的 Python 脚本
-
活动 15:预测客户流失
-
-
神经网络
-
什么是神经网络?
-
优化算法
-
超参数
-
-
Keras
-
练习 47:安装 Keras 库并使用其进行分类
-
Keras 库
-
练习 48:使用神经网络预测鳄梨价格
-
-
分类变量
-
独热编码
-
实体嵌入
-
练习 49:使用实体嵌入预测鳄梨价格
-
活动 16:预测客户购买金额
-
-
总结
-
-
第六章
-
解码图像
-
介绍
-
图像
- 练习 50:使用全连接神经网络分类 MNIST
-
卷积神经网络
- 卷积层
-
池化层
-
Adam 优化器
-
交叉熵损失
- 练习 51:使用 CNN 分类 MNIST
-
正则化
-
Dropout 层
-
L1 和 L2 正则化
-
批量归一化
-
练习 52:使用正则化改进图像分类(使用 CIFAR-10 图像)
-
-
图像数据预处理
-
归一化
-
转换为灰度图
-
将所有图像调整为相同大小
-
其他有用的图像操作
-
活动 17:预测图像是猫还是狗
-
-
数据增强
-
生成器
-
练习 53:使用图像增强对 CIFAR-10 图像进行分类
-
活动 18:识别并增强图像
-
-
总结
-
-
第七章
-
处理人类语言
-
介绍
-
文本数据处理
-
正则表达式
-
练习 54:使用正则表达式进行字符串清理
-
基本特征提取
-
文本预处理
-
练习 55:预处理 IMDB 电影评论数据集
-
文本处理
-
练习 56:使用 Gensim 创建词向量
-
活动 19:预测电影评论情感
-
-
循环神经网络(RNN)
-
LSTM
-
练习 57:使用 LSTM 执行情感分析
-
活动 20:预测推文情感
-
-
总结
-
-
第八章
-
行业技巧与窍门
-
介绍
-
迁移学习
-
图像数据的迁移学习
-
练习 58:使用 InceptionV3 比较并分类图像
-
活动 21:使用 InceptionV3 进行图像分类
-
-
有用的工具和技巧
-
训练、开发和测试数据集
-
处理未处理的数据集
-
pandas Profiling
-
TensorBoard
-
-
AutoML
-
练习 59:使用 Auto-Keras 获取高性能网络
-
使用 Keras 进行模型可视化
-
活动 22:使用迁移学习预测图像
-
-
总结
-
-
附录
-
第一章:数据科学与数据预处理简介
- 活动 1:使用银行营销订阅数据集进行预处理
-
第二章:数据可视化
-
活动 2:折线图
-
活动 3:条形图
-
活动 4:使用子图的多种绘图类型
-
-
第三章:通过 Scikit-Learn 介绍机器学习
-
活动 5:生成预测并评估多元线性回归模型的性能
-
活动 6:生成预测并评估调优后的逻辑回归模型性能
-
活动 7:生成预测并评估 SVC 网格搜索模型的性能
-
活动 8:为决策树分类器准备数据
-
活动 9:生成预测并评估决策树分类器模型的性能
-
活动 10:调优随机森林回归模型
-
活动 11:生成预测并评估调优后的随机森林回归模型的性能
-
-
第四章:降维与无监督学习
-
活动 12:集成 k-means 聚类与预测计算
-
活动 13:PCA 转换后按簇评估均值惯性
-
-
第五章:掌握结构化数据
-
活动 14:训练并预测一个人的收入
-
活动 15:预测客户流失
-
活动 16:预测客户的购买金额
-
-
第六章:解码图像
-
活动 17:预测图像是猫还是狗
-
活动 18:识别并增强图像
-
-
第七章:处理人类语言
-
活动 19:预测电影评论的情感
-
活动 20:从推文中预测情感
-
-
第八章:行业技巧与窍门
-
活动 21:使用 InceptionV3 分类图像
-
活动 22:使用迁移学习预测图像
-
-
地标
-
封面
-
目录