Spark 深度学习秘籍(二)
原文:
annas-archive.org/md5/0b821d0906a13124b659b6b5635b3940译者:飞龙
第五章:使用 Spark ML 预测消防部门呼叫
本章将涵盖以下内容:
-
下载旧金山消防部门呼叫数据集
-
确定逻辑回归模型的目标变量
-
为逻辑回归模型准备特征变量
-
应用逻辑回归模型
-
评估逻辑回归模型的准确性
介绍
分类模型是预测定义的类别结果的常用方法。我们经常使用分类模型的输出。每当我们去电影院看电影时,我们都关心电影是否符合预期?数据科学领域中最常用的分类模型之一是逻辑回归。逻辑回归模型产生一个由 sigmoid 函数激活的响应。sigmoid 函数使用模型的输入,并产生一个介于 0 和 1 之间的输出。这个输出通常是一个概率分数。许多深度学习模型也用于分类目的。常见的是,逻辑回归模型与深度学习模型一起使用,以帮助建立基准,以此为基础来评估深度学习模型。sigmoid 激活函数是深度神经网络中使用的多种激活函数之一,在深度学习中用于产生概率输出。我们将利用 Spark 中的内置机器学习库来构建一个逻辑回归模型,预测旧金山消防部门的来电是否与火灾有关,而不是其他事件。
下载旧金山消防部门呼叫数据集
旧金山市在收集辖区内消防部门的呼叫服务方面做得非常好。正如他们网站上所述,每条记录都包含呼叫号、事件号、地址、单位标识符、呼叫类型和处置方式。包含旧金山消防部门呼叫数据的官方网站可以通过以下链接访问:
data.sfgov.org/Public-Safety/Fire-Department-Calls-for-Service/nuek-vuh3
有关数据集的一些常规信息,包含列和行的数量,如下图所示:
该数据集最后更新于 2018 年 3 月 26 日,包含大约 460 万个数据行和 34 个列。
准备工作
数据集以.csv格式提供,可以下载到本地计算机上,然后导入 Spark 中。
如何实现...
本节将介绍如何下载并导入.csv文件到我们的 Jupyter notebook 中。
- 从网站下载数据集,通过选择“导出”然后选择 CSV,如下图所示:
-
如果尚未这样做,请将下载的数据集命名为
Fire_Department_Calls_for_Service.csv -
将数据集保存到任何本地目录中,理想情况下,它应该保存在与本章中使用的 Spark 笔记本相同的文件夹中,如下图所示:
- 一旦数据集保存到与笔记本相同的目录中,执行以下
pyspark脚本,将数据集导入到 Spark 中,并创建一个名为df的数据框:
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.master("local") \
.appName("Predicting Fire Dept Calls") \
.config("spark.executor.memory", "6gb") \
.getOrCreate()
df = spark.read.format('com.databricks.spark.csv')\
.options(header='true', inferschema='true')\
.load('Fire_Department_Calls_for_Service.csv')
df.show(2)
它是如何工作的……
数据集被保存到与 Jupyter 笔记本相同的目录中,便于导入到 Spark 会话中。
-
通过从
pyspark.sql导入SparkSession,初始化本地pyspark会话。 -
通过读取 CSV 文件并使用
header = 'true'和inferschema = 'true'选项,创建一个名为df的数据框。 -
最后,理想情况下,始终运行一个脚本,显示已通过数据框导入 Spark 的数据,以确认数据已成功加载。脚本的结果,显示了旧金山消防部门来电数据集的前两行,如下图所示:
请注意,当我们将文件读取到 Spark 时,我们使用 .load() 将 .csv 文件导入到 Jupyter 笔记本中。对于我们的目的,这种方式是可以的,因为我们使用的是本地集群,但如果我们使用 Hadoop 的集群,这种方法将不可行。
还有更多……
数据集附带一个数据字典,定义了 34 列中每一列的标题。可以通过以下链接从同一网站访问该数据字典:
另见
旧金山政府网站允许在线可视化数据,可以用来快速进行数据分析。通过选择网站上的“可视化”下拉菜单,可以访问该可视化应用程序,如下图所示:
确定逻辑回归模型的目标变量
逻辑回归模型作为一种分类算法,旨在预测二元结果。在本节中,我们将指定数据集中最合适的列,用于预测接收到的来电是否与火灾或非火灾事件有关。
准备工作
本节中我们将可视化许多数据点,这将需要以下内容:
-
通过在命令行执行
pip install matplotlib,确保已安装matplotlib。 -
运行
import matplotlib.pyplot as plt,并通过运行%matplotlib inline确保图表能够在单元格中显示。
此外,pyspark.sql中的一些函数也需要进行操作,这需要importing functions as F。
如何操作...
本节将演示如何可视化来自旧金山消防部门的数据。
- 执行以下脚本,以获取
Call Type Group列中唯一值的初步识别:
df.select('Call Type Group').distinct().show()
-
有五个主要类别:
-
报警。 -
潜在生命威胁。 -
非生命威胁。 -
火灾。 -
null。
-
-
不幸的是,其中一个类别是
null值。获取每个唯一值的行计数,将有助于确定数据集中的null值数量。执行以下脚本生成Call Type Group列中每个唯一值的行计数:
df.groupBy('Call Type Group').count().show()
- 不幸的是,超过 280 万行数据没有与
Call Type Group关联的值。这占总可用数据(460 万行)的 60%以上。执行以下脚本,以查看空值不平衡的条形图:
df2 = df.groupBy('Call Type Group').count()
graphDF = df2.toPandas()
graphDF = graphDF.sort_values('count', ascending=False)
import matplotlib.pyplot as plt
%matplotlib inline
graphDF.plot(x='Call Type Group', y = 'count', kind='bar')
plt.title('Call Type Group by Count')
plt.show()
- 可能需要选择另一个指标来确定目标变量。相反,我们可以分析
Call Type,以识别与火灾相关的呼叫与其他所有呼叫的区别。执行以下脚本来分析Call Type:
df.groupBy('Call Type').count().orderBy('count', ascending=False).show(100)
- 与
Call Type Group不同,似乎没有任何null值。Call Type共有 32 个独特类别;因此,它将作为火灾事件的目标变量。执行以下脚本,标记Call Type中包含Fire的列:
from pyspark.sql import functions as F
fireIndicator = df.select(df["Call Type"],F.when(df["Call Type"].like("%Fire%"),1)\
.otherwise(0).alias('Fire Indicator'))
fireIndicator.show()
- 执行以下脚本,以检索
Fire Indicator的不同计数:
fireIndicator.groupBy('Fire Indicator').count().show()
- 执行以下脚本,将
Fire Indicator列添加到原始数据框df中:
df = df.withColumn("fireIndicator",\
F.when(df["Call Type"].like("%Fire%"),1).otherwise(0))
- 最后,将
fireIndicator列添加到数据框df中,并通过执行以下脚本确认:
df.printSchema()
它是如何工作的...
构建成功的逻辑回归模型的关键步骤之一是确定一个二元目标变量,该变量将用于预测结果。本节将讲解如何选择我们的目标变量:
- 通过识别
Call Type Group列的唯一值,进行潜在目标列的数据分析。我们可以查看Call Type Group列中的唯一值,如以下截图所示:
-
目标是识别
Call Type Group列中是否存在缺失值,以及如何处理这些缺失值。有时,列中的缺失值可以直接删除,而其他时候则需要进行处理以填充这些值。 -
以下截图显示了有多少
null值:
- 此外,我们还可以绘制
null值的数量,以更直观地了解值的分布情况,如以下截图所示:
-
由于在
Call Type Group中有超过 280 万行缺失数据,如df.groupBy脚本和条形图所示,删除这些值没有意义,因为它们占数据集中总行数的 60%以上。因此,需要选择另一个列作为目标指标。 -
在分析
Call Type列时,我们发现 32 个唯一的可能值中没有空值。这使得Call Type成为逻辑回归模型的一个更好的目标变量。以下是Call Type列分析的截图:
- 由于逻辑回归在二元结果下效果最佳,因此在
df数据框中使用withColumn()操作符创建了一个新列,用于表示某个呼叫是否与火灾相关事件有关(0 或 1)。这个新列名为fireIndicator,可以在以下截图中看到:
- 我们可以通过执行
groupBy().count()来识别火灾呼叫与其他呼叫的普遍程度,如下图所示:
- 最佳实践是在执行新修改数据框的
printSchema()脚本后确认新列已经附加到现有数据框中。新架构的输出可以在以下截图中看到:
还有更多内容...
在这一部分中,使用了pyspark.sql模块进行了一些列操作。withColumn()操作符通过添加一个新列或修改现有的同名列来返回一个新的数据框或修改现有的数据框。这个操作符不应与withColumnRenamed()操作符混淆,后者同样返回一个新的数据框,但通过修改现有列的名称来更改为新列。最后,我们需要执行一些逻辑操作,将与Fire相关的值转换为 0,而没有Fire的转换为 1。这需要使用pyspark.sql.functions模块,并结合where函数,相当于 SQL 中的case语句。这个函数通过以下语法创建了一个case语句公式:
CASE WHEN Call Type LIKE %Fire% THEN 1 ELSE 0 END
新数据集的结果中,Call Type和fireIndicator两列的内容如下所示:
另请参见
若要了解有关 Spark 中pyspark.sql模块的更多信息,请访问以下网站:
spark.apache.org/docs/2.2.0/api/python/pyspark.sql.html
准备逻辑回归模型的特征变量
在前一节中,我们确定了将用于预测火灾报警的目标变量,并将其用于逻辑回归模型。本节将重点介绍如何识别所有有助于模型确定目标变量的特征。这就是所谓的特征选择。
准备就绪
本节将需要从pyspark.ml.feature导入StringIndexer。为了确保正确的特征选择,我们需要将字符串类型的列映射为索引列。这将帮助为分类变量生成不同的数字值,便于机器学习模型处理独立变量,从而预测目标结果。
如何操作...
本节将介绍如何为我们的模型准备特征变量的步骤。
- 执行以下脚本,以仅选择与任何火灾指标无关的字段,从而更新数据框
df:
df = df.select('fireIndicator',
'Zipcode of Incident',
'Battalion',
'Station Area',
'Box',
'Number of Alarms',
'Unit sequence in call dispatch',
'Neighborhooods - Analysis Boundaries',
'Fire Prevention District',
'Supervisor District')
df.show(5)
- 下一步是识别数据框中是否有空值,如果存在则将其删除。执行以下脚本以识别包含空值的行数:
print('Total Rows')
df.count()
print('Rows without Null values')
df.dropna().count()
print('Row with Null Values')
df.count()-df.dropna().count()
- 有 16,551 行包含缺失值。执行以下脚本以更新数据框,删除所有包含空值的行:
df = df.dropna()
- 执行以下脚本以检索更新后的
fireIndicator目标计数:
df.groupBy('fireIndicator').count().orderBy('count', ascending = False).show()
- 从
pyspark.ml.feature导入StringIndexer类,为每个分类变量分配数值,如下脚本所示:
from pyspark.ml.feature import StringIndexer
- 使用以下脚本创建一个包含所有将用于模型的特征变量的 Python 列表:
column_names = df.columns[1:]
- 执行以下脚本以指定输出列格式
outputcol,该列将通过从输入列inputcol中的特征列表进行stringIndexed来生成:
categoricalColumns = column_names
indexers = []
for categoricalCol in categoricalColumns:
stringIndexer = StringIndexer(inputCol=categoricalCol, outputCol=categoricalCol+"_Index")
indexers += [stringIndexer]
- 执行以下脚本以创建一个
model,该模型将用于拟合输入列,并将新定义的输出列添加到现有的数据框df:
models = []
for model in indexers:
indexer_model = model.fit(df)
models+=[indexer_model]
for i in models:
df = i.transform(df)
- 执行以下脚本以定义数据框
df中最终选择的特征,这些特征将用于模型:
df = df.select(
'fireIndicator',
'Zipcode of Incident_Index',
'Battalion_Index',
'Station Area_Index',
'Box_Index',
'Number of Alarms_Index',
'Unit sequence in call dispatch_Index',
'Neighborhooods - Analysis Boundaries_Index',
'Fire Prevention District_Index',
'Supervisor District_Index')
它是如何工作的...
本节将解释准备特征变量步骤背后的逻辑。
- 只有数据框中真正与火灾指示无关的指标才会被选择用来构建预测结果的逻辑回归模型。这样做的原因是为了去除数据集中的潜在偏差,这些偏差可能已经揭示了预测的结果。这样可以最大限度地减少人为干预对最终结果的影响。更新后的数据框输出可以在以下截图中查看:
请注意,Neighborhooods - Analysis of Boundaries 列最初是在我们提取的数据中拼写错误的。为了保持一致性,我们将继续使用错误拼写,直到本章结束。然而,可以通过 Spark 中的 withColumnRenamed() 函数将列名重命名。
-
最终选择的列如下所示:
-
火警指示器 -
事件的邮政编码 -
营 -
站点区域 -
箱 -
警报次数 -
呼叫调度中的单元序列 -
Neighborhooods - Analysis Boundaries -
消防预防区 -
监管区
-
-
这些列被选择是为了避免在建模中出现数据泄漏。数据泄漏在建模中很常见,它可能导致无效的预测模型,因为它可能包括那些直接影响我们要预测的结果的特征。理想情况下,我们希望使用与结果真正独立的特征。有几个列看起来存在泄漏,因此它们被从我们的数据框和模型中移除。
-
所有缺失或为空的行都会被识别并移除,以便从模型中获得最佳性能,而不夸大或低估关键特征。可以计算并显示缺失值的行数,结果为 16,551,如以下脚本所示:
- 我们可以查看火灾相关的呼叫频率与非火灾呼叫的比较,以下截图展示了这一情况:
StringIndexer被导入以帮助将多个类别或字符串特征转换为数值,以便在逻辑回归模型中进行计算。特征的输入需要是向量或数组格式,这对数值型数据是理想的。以下截图显示了将用于模型的所有特征列表:
- 为每个类别变量构建一个索引器,指定模型中将使用的输入列(
inputCol)和输出列(outputCol)。数据框中的每一列都经过调整或转换,重建一个新的输出,并使用更新后的索引,范围从 0 到该列的唯一值的最大计数。新的列在末尾添加了_Index。在创建更新后的列时,原始列仍然保留在数据框中,如以下截图所示:
- 我们可以查看其中一个新创建的列,并将其与原始列进行比较,看看字符串是如何被转换为数值类别的。以下截图展示了
Neighborhooods - Analysis Boundaries和Neighborhooods - Analysis Boundaries_Index的比较:
-
然后,数据框被裁剪,只保留数值型数据,并去除已经转换的原始类别变量。从建模的角度来看,非数值型数据不再起作用,因此被从数据框中删除。
-
新列已打印出来,以确认数据框中每个值的类型是双精度或整数,如下图所示:
还有更多...
最后查看经过修改的数据框,您会发现它只包含数值型数据,如下图所示:
另请参见
要了解更多关于StringIndexer的信息,请访问以下网站:spark.apache.org/docs/2.2.0/ml-features.html#stringindexer。
应用逻辑回归模型
现在已经为将模型应用到数据框做好准备。
准备工作
本节将重点介绍应用一种非常常见的分类模型——逻辑回归,这将涉及从 Spark 中导入以下一些内容:
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.ml.classification import LogisticRegression
如何操作...
本节将逐步介绍应用模型和评估结果的过程。
- 执行以下脚本,将数据框中的所有特征变量放入一个名为
features的列表中:
features = df.columns[1:]
- 执行以下操作以导入
VectorAssembler并配置将分配给特征向量的字段,通过指定inputCols和outputCol:
from pyspark.ml.feature import VectorAssembler
feature_vectors = VectorAssembler(
inputCols = features,
outputCol = "features")
- 执行以下脚本,使用
transform函数将VectorAssembler应用到数据框中:
df = feature_vectors.transform(df)
- 修改数据框,移除除
fireIndicator和features之外的所有列,如下脚本所示:
df = df.drop( 'Zipcode of Incident_Index',
'Battalion_Index',
'Station Area_Index',
'Box_Index',
'Number of Alarms_Index',
'Unit sequence in call dispatch_Index',
'Neighborhooods - Analysis Boundaries_Index',
'Fire Prevention District_Index',
'Supervisor District_Index')
- 修改数据框,将
fireIndicator重命名为label,如下脚本所示:
df = df.withColumnRenamed('fireIndicator', 'label')
- 将整个数据框
df按 75:25 的比例拆分为训练集和测试集,并设置随机种子为12345,如下脚本所示:
(trainDF, testDF) = df.randomSplit([0.75, 0.25], seed = 12345)
- 从
pyspark.ml.classification导入LogisticRegression库,并配置以从数据框中引入label和features,然后在训练数据集trainDF上进行拟合,如下脚本所示:
from pyspark.ml.classification import LogisticRegression
logreg = LogisticRegression(labelCol="label", featuresCol="features", maxIter=10)
LogisticRegressionModel = logreg.fit(trainDF)
- 转换测试数据框
testDF,应用逻辑回归模型。新的数据框包含预测结果的分数,命名为df_predicted,如下脚本所示:
df_predicted = LogisticRegressionModel.transform(testDF)
它是如何工作的...
本节将解释应用模型和评估结果步骤背后的逻辑。
-
分类模型在将所有特征合并为一个单一向量进行训练时效果最佳。因此,我们通过将所有特征收集到一个名为
features的单一列表中,开始向量化过程。由于我们的标签是数据框的第一列,因此我们将其排除,并将之后的每一列作为特征列或特征变量。 -
向量化过程继续进行,将
features列表中的所有变量转换为一个输出到features列的单一向量。这个过程需要从pyspark.ml.feature导入VectorAssembler。 -
应用
VectorAssembler会通过创建一个名为features的新列来转换数据框,具体见下图:
-
在这一点上,模型中唯一需要使用的列是标签列
fireIndicator和features列。其他所有列可以从数据框中删除,因为它们在建模过程中不再需要。 -
此外,为了帮助逻辑回归模型,我们将把名为
fireIndicator的列更名为label。df.show()脚本的输出可以在以下截图中看到,其中包括新重命名的列:
- 为了减少过拟合,数据框将被拆分为测试数据集和训练数据集,以便在训练数据集
trainDF上训练模型,并在测试数据集testDF上进行测试。设置了一个随机种子12345,以确保每次执行该单元时随机性的一致性。我们可以在以下截图中看到数据拆分的行数:
-
然后,从
pyspark.ml.classification导入逻辑回归模型LogisticRegression,并配置以输入数据框中与特征和标签相关的列名。此外,逻辑回归模型会被分配给一个名为logreg的变量,接着对我们的数据集trainDF进行训练。 -
创建了一个新的数据框
predicted_df,基于对测试数据框testDF的变换,逻辑回归模型对其评分后,该模型为predicted_df创建了三个额外的列。根据评分,额外的三个列是rawPrediction、probability和prediction,具体见下图:
- 最后,可以对
df_predicted中的新列进行分析,具体见下图:
还有更多...
有一件重要的事情需要记住,因为它最初可能看起来有些直觉不合常理,那就是我们的概率阈值在数据框中设置为 50%。任何概率大于或等于 0.500 的调用都会被预测为 0.0,而任何概率小于 0.500 的调用则会被预测为 1.0。这个设置是在管道开发过程中完成的,只要我们知道阈值是什么,并且了解预测是如何分配的,我们就能很好地应对。
另请参阅
要了解更多关于VectorAssembler的信息,请访问以下网站:
spark.apache.org/docs/latest/ml-features.html#vectorassembler
评估逻辑回归模型的准确性
现在我们准备好评估预测是否正确分类为火灾事件的性能。
准备开始
我们将进行模型分析,这需要导入以下内容:
from sklearn import metrics
如何操作...
本节讲解了评估模型性能的步骤。
- 使用
.crosstab()函数创建混淆矩阵,如以下脚本所示:
df_predicted.crosstab('label', 'prediction').show()
- 从
sklearn导入metrics模块来帮助使用以下脚本测量准确性:
from sklearn import metrics
- 创建两个变量,分别代表数据框中的
actual和predicted列,这些列将用于衡量准确性,使用以下脚本:
actual = df_predicted.select('label').toPandas()
predicted = df_predicted.select('prediction').toPandas()
- 使用以下脚本计算准确率预测分数:
metrics.accuracy_score(actual, predicted)
它是如何工作的...
本节解释了如何评估模型的性能。
- 为了计算模型的准确率,重要的是能够识别我们的预测有多准确。通常,使用混淆矩阵交叉表来可视化这一点,它展示了正确和错误的预测分数。我们使用
df_predicted数据框中的crosstab()函数创建了一个混淆矩阵,显示我们有 964,980 个负类正确预测(标签为 0),以及 48,034 个正类正确预测(标签为 1),如下图所示:
-
从本节前面的内容我们知道,
testDF数据框总共有 1,145,589 行;因此,我们可以使用以下公式计算模型的准确性:(TP + TN) / 总数。准确率为 88.4%。 -
需要注意,并非所有的假分数都具有相同的意义。例如,从火灾安全的角度来看,将一次通话错误地归类为与火灾无关,而实际上它与火灾有关,比将其错误地归类为火灾事件要更为严重。这种情况称为假阴性。为了衡量假阴性(FN),有一个叫做召回率的指标。
-
虽然我们可以像最后一步那样手动计算准确率,但最好能够自动计算准确率。这可以通过导入
sklearn.metrics模块轻松实现,它是一个常用于评分和模型评估的模块。 -
sklearn.metrics接受两个参数:我们用于标签的实际结果和从逻辑回归模型中得出的预测值。因此,创建了两个变量,actual和predicted,并使用accuracy_score()函数计算准确率,如下图所示:
- 准确率与我们手动计算的结果相同,为 88.4%。
还有更多…
我们现在知道我们的模型能够以 88.4%的准确率准确预测来电是否与火灾相关。乍一听,这似乎是一个强有力的预测;然而,始终需要将其与基准得分进行比较,其中每个电话都被预测为非火灾电话。预测的数据框架df_predicted中标签1和0的分布情况如下图所示:
我们可以在同一个数据框架上运行一些统计信息,使用df_predicted.describe('label').show()脚本来计算标签值1的均值。该脚本的输出可以在下面的截图中看到:
一个基础模型在预测值为1时的预测率为 14.94%,换句话说,它的预测率为100 - 14.94%,即为 0 时的预测率为 85.06%。因此,由于 85.06%小于模型预测率 88.4%,该模型相比于盲目猜测(是否为火灾相关的电话)提供了更好的改进。
另请参见
要了解更多关于准确度与精确度的知识,请访问以下网站:
www.mathsisfun.com/accuracy-precision.html
第六章:在生成网络中使用 LSTMs
阅读完本章后,您将能够完成以下任务:
-
下载将作为输入文本使用的小说/书籍
-
准备和清理数据
-
句子标记化
-
训练并保存 LSTM 模型
-
使用模型生成相似的文本
简介
由于循环神经网络(RNNs)在反向传播方面的缺点,长短期记忆单元(LSTMs)和门控循环单元(GRUs)近年来在学习序列输入数据时越来越受欢迎,因为它们更适合解决梯度消失和梯度爆炸的问题。
下载将作为输入文本使用的小说/书籍
在本食谱中,我们将按步骤进行操作,下载我们将作为输入文本使用的小说/书籍,以执行本食谱。
准备工作
-
将输入数据以
.txt文件的形式放入工作目录。 -
输入可以是任何类型的文本,如歌词、小说、杂志文章或源代码。
-
大多数经典文本不再受版权保护,可以免费下载安装并用于实验。获取免费书籍的最佳地方是古腾堡计划。
-
在本章中,我们将使用鲁德亚德·吉卜林的《丛林之书》作为输入,训练我们的模型并生成统计上相似的文本作为输出。以下截图显示了如何下载必要的
.txt格式文件:
- 访问网站并搜索所需的书籍后,点击 Plain Text UTF-8 并下载。UTF-8 基本上指定了编码类型。文本可以复制粘贴或直接保存到工作目录,方法是点击链接。
如何实现...
在开始之前,查看并分析数据总是有帮助的。通过查看数据,我们可以看到数据中有很多标点符号、空格、引号、大写字母和小写字母。在对数据进行任何分析或将其输入 LSTM 网络之前,我们需要先准备数据。我们需要一些库来简化数据的处理:
- 通过执行以下命令导入必要的库:
from keras.preprocessing.text import Tokenizer
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense, lSTM, Dropout, Embedding
import numpy as np
from pickle import dump
import string
- 上述命令的输出如下截图所示:
- 总是检查当前工作目录并选择所需的文件夹作为工作目录是个好主意。在我们的例子中,
.txt文件名为junglebook.txt,它位于名为Chapter 8的文件夹中。因此,我们将选择该文件夹作为整个章节的工作目录。这可以通过以下截图所示的方式完成:
- 接下来,通过定义名为
load_document的函数将文件加载到程序内存中,可以通过执行以下命令完成此操作:
def load_document(name):
file = open(name, 'r')
text = file.read()
file.close()
return text
- 使用先前定义的函数将文档加载到内存中,并使用以下脚本打印文本文件的前
2000个字符:
input_filename = 'junglebook.txt'
doc = load_document(input_filename)
print(doc[:2000])
- 运行前述函数以及命令会产生以下截图所示的输出:
上述代码的输出显示在这里的截图中:
以下截图是之前输出的继续:
- 如前面截图所示,
.txt文件中的前2000个字符被打印出来。通常,分析数据时先查看它的内容是一个好主意,这样可以更好地了解如何进行后续的预处理步骤。
它的工作原理...
-
array函数将用于处理以数组形式表示的数据。numpy库提供了这个函数。 -
由于我们的数据仅为文本数据,我们将需要字符串库来处理所有输入数据为字符串形式,然后将单词编码为整数,便于输入。
-
tokenizer函数将用于将所有句子拆分为标记,其中每个标记表示一个单词。 -
为了将字典保存到 pickle 文件中,需要使用
dump函数,因此需要 pickle 库。 -
keras库中的to_categorical函数将类向量(整数)转换为二进制类矩阵,例如,用于categorical_crossentropy,我们在后面的步骤中将需要它,以便将标记映射到唯一的整数,并反向操作。 -
本章中所需的其他 Keras 层包括 LSTM 层、全连接层、丢弃层和嵌入层。模型将按顺序定义,因此我们需要从
keras库中导入顺序模型。
还有更多...
-
你还可以使用相同的模型处理不同类型的文本,如网站上的客户评价、推文、结构化文本(如源代码)、数学理论等。
-
本章的目的是理解 LSTM 如何学习长期依赖性,以及与循环神经网络相比,它们在处理顺序数据时如何表现得更好。
-
另一个好主意是将宝可梦的名称输入模型,并尝试生成你自己的宝可梦名称。
另见
有关使用的不同库的更多信息,请访问以下链接:
数据的准备和清理
本章节的这一部分将讨论在将数据输入模型之前的各种数据准备和文本预处理步骤。我们如何准备数据,实际上取决于我们打算如何建模,进而决定了我们如何使用它。
准备中
语言模型将基于统计数据,预测给定输入文本序列的每个单词的概率。预测出的单词将作为输入反馈给模型,从而生成下一个单词。
一个关键决策是输入序列的长度应该是多少。它们需要足够长,以便模型能够学习单词预测的上下文。这个输入长度还将决定在我们使用模型时,用于生成新序列的种子文本的长度。
为了简化起见,我们将随意选择一个 50 个单词的长度作为输入序列的长度。
如何操作...
基于对文本的回顾(我们之前已进行过),以下是可以对输入文件中的文本进行清理和预处理的一些操作。我们已呈现了一些关于文本预处理的选项。然而,作为练习,你可能想要探索更多清理操作:
-
将破折号
–替换为空格,以便更好地拆分单词 -
根据空格拆分单词
-
删除输入文本中的所有标点符号,以减少输入模型的文本中唯一字符的数量(例如,Why? 变为 Why)
-
删除所有非字母单词,以去除独立的标点符号标记和表情符号
-
将所有单词从大写转换为小写,以进一步减少标记总数的大小,并消除任何不一致和数据冗余
词汇表的大小是语言建模中的决定性因素,并且会影响模型的训练时间。较小的词汇表能使模型更高效,训练更快。虽然在某些情况下较小的词汇表更好,但在其他情况下,为了防止过拟合,较大的词汇表有其作用。为了预处理数据,我们需要一个函数,它接受整个输入文本,基于空格将其拆分,删除所有标点符号,规范化所有字母大小写,并返回一个标记序列。为此,定义 clean_document 函数,执行以下命令:
import string
def clean_document(doc):
doc = doc.replace('--', ' ')
tokens = doc.split()
table = str.maketrans('', '', string.punctuation)
tokens = [w.translate(table) for w in tokens]
tokens = [word for word in tokens if word.isalpha()]
tokens = [word.lower() for word in tokens]
return tokens
- 之前定义的函数基本上会将加载的文档/文件作为参数,并返回一个干净的标记数组,如以下截图所示:
- 接下来,打印出一些标记和统计数据,以便更好地理解
clean_document函数的作用。此步骤通过执行以下命令完成:
tokens = clean_document(doc)
print(tokens[:200])
print('Total Tokens: %d' % len(tokens))
print('Total Unique Tokens: %d' % len(set(tokens)))
- 上述命令组的输出会打印前两百个标记,具体内容如以下截图所示:
- 接下来,使用以下命令将所有这些标记组织成序列,每个序列包含 50 个单词(随意选择)。
length = 50 + 1
sequences = list()
for i in range(length, len(tokens)):
seq = tokens[i-sequence_length:i]
line = ' '.join(seq)
sequences.append(line)
print('Total Sequences: %d' % len(sequences))
可以通过打印文档生成的所有序列来查看总数,如下图所示:
- 使用以下命令,定义
save_doc函数,将所有生成的标记以及序列保存到工作目录中的文件:
def save_document(lines, name):
data = '\n'.join(lines)
file = open(name, 'w')
file.write(data)
file.close()
要保存序列,请使用以下两个命令:
output_filename = 'junglebook_sequences.txt'
save_document(sequences, output_filename)
- 该过程在下图中有所示例:
- 接下来,使用
load_document函数将保存的文档(包含所有保存的标记和序列)加载到内存中,load_document函数定义如下:
def load_document(name):
file = open(name, 'r')
text = file.read()
file.close()
return text
# function to load document and split based on lines
input_filename = 'junglebook_sequences.txt'
doc = load_document(input_filename)
lines = doc.split('\n')
它的工作原理...
-
clean_document函数删除所有空格、标点符号、大写字母和引号,并将整个文档分割成标记,每个标记是一个单词。 -
通过打印文档中标记的总数和唯一标记的总数,我们会发现
clean_document函数生成了 51,473 个标记,其中 5,027 个标记(或单词)是唯一的。 -
save_document函数将所有这些标记以及生成每个包含 50 个单词的序列所需的唯一标记进行保存。请注意,通过循环遍历所有生成的标记,我们能够生成一个包含 51,422 个序列的长列表。这些序列将作为输入用于训练语言模型。 -
在训练所有 51,422 个序列之前,最好先将标记和序列保存到文件中。保存后,可以使用定义的
load_document函数将文件重新加载到内存中。 -
序列被组织为 50 个输入标记和一个输出标记(这意味着每个序列有 51 个标记)。为了预测每个输出标记,前 50 个标记将作为模型的输入。我们可以通过从第 51 个标记开始遍历标记列表,并将前 50 个标记作为一个序列来实现这一点,然后重复这个过程,直到所有标记的列表结束。
另见
访问以下链接,了解使用各种函数进行数据准备的更好方法:
对句子进行标记化
在定义并将数据输入到 LSTM 网络之前,重要的是将数据转换为神经网络能够理解的形式。计算机理解所有内容都是二进制代码(0 和 1),因此文本或字符串格式的数据需要转换为一热编码变量。
准备就绪
要了解一热编码如何工作,请访问以下链接:
-
machinelearningmastery.com/how-to-one-hot-encode-sequence-data-in-python/ -
scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html -
stackoverflow.com/questions/37292872/how-can-i-one-hot-encode-in-python -
hackernoon.com/what-is-one-hot-encoding-why-and-when-do-you-have-to-use-it-e3c6186d008f
如何操作...
完成前面的部分后,你应该能够清理整个语料库并分割句子。接下来的步骤,涉及一热编码和句子标记化,可以按照以下方式进行:
-
一旦标记和序列被保存到文件并加载到内存中,它们必须被编码为整数,因为模型中的词嵌入层期望输入序列由整数而非字符串组成。
-
这是通过将词汇表中的每个单词映射到一个唯一的整数并对输入序列进行编码来实现的。稍后,在进行预测时,可以将预测结果转换(或映射)回数字,以便在相同的映射中查找相关单词,并将其从整数反向映射回单词。
-
要执行这种编码,利用 Keras API 中的
Tokenizer类。在编码之前,必须先对整个数据集进行训练,以便它能找到所有独特的标记,并为每个标记分配一个独特的整数。执行这些命令的方法如下:
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
sequences = tokenizer.texts_to_sequences(lines)
-
你还需要在稍后定义嵌入层之前计算词汇表的大小。这是通过计算映射字典的大小来确定的。
-
因此,在指定嵌入层的词汇表大小时,应该将其指定为比实际词汇表大 1。词汇表大小因此定义如下:
vocab_size = len(tokenizer.word_index) + 1
print('Vocabulary size : %d' % vocab_size)
-
一旦输入序列被编码,它们需要被分成输入和输出元素,这可以通过数组切片来实现。
-
分离后,将输出单词进行独热编码。这意味着将其从整数转换为一个 n 维向量,其中每个维度对应词汇表中的一个单词,并且用 1 表示该单词在词汇表中整数值对应的索引。Keras 提供了
to_categorical()函数,可以用来为每个输入-输出序列对对输出单词进行独热编码。 -
最后,指定给嵌入层输入序列的长度。我们知道有 50 个单词,因为模型是通过指定序列长度为 50 来设计的,但一种好的通用方式是使用输入数据形状的第二维(列数)来指定序列长度。
-
这可以通过执行以下命令来完成:
sequences = array(sequences)
Input, Output = sequences[:,:-1], sequences[:,-1]
Output = to_categorical(Output, num_classes=vocab_size)
sequence_length = Input.shape[1]
它是如何工作的...
本节将描述您在执行前一节命令时必须看到的输出:
- 在运行分词命令并计算词汇表长度后,您必须看到如下所示的输出:
-
单词从 1 开始分配值,直到总词数(例如,本例中为 5,027)。嵌入层需要为词汇表中从索引 1 到最大索引的每个单词分配一个向量表示。词汇表末尾的单词的索引将是 5,027;这意味着数组的长度必须是 5,027 + 1。
-
数组切片和将句子分割成每个序列 50 个单词的输出应如下所示:
to_categorical()函数用于让模型学习预测下一个单词的概率分布。
还有更多...
有关在 Python 中重塑数组的更多信息,请参阅以下链接:
-
docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html -
machinelearningmastery.com/index-slice-reshape-numpy-arrays-machine-learning-python/
训练并保存 LSTM 模型
现在,您可以从准备好的数据中训练一个统计语言模型。
将要训练的模型是一个神经语言模型。它有一些独特的特点:
-
它使用分布式表示法来表示单词,这样具有相似含义的不同单词将具有相似的表示。
-
它在学习模型的同时学习表示
-
它学会使用前 50 个单词的上下文来预测下一个单词的概率
具体而言,您将使用一个嵌入层来学习单词的表示,并使用长短期记忆(LSTM)递归神经网络来学习基于上下文预测单词。
正在准备
学到的嵌入需要知道词汇表的大小和输入序列的长度,如前面所述。它还有一个参数,用来指定将使用多少维度来表示每个词。即嵌入向量空间的大小。
常见的值为 50、100 和 300。我们这里使用 100,但可以考虑测试更小或更大的值,并评估这些值的指标。
网络将包含以下内容:
-
两个 LSTM 隐藏层,每个隐藏层有 200 个记忆单元。更多的记忆单元和更深的网络可能会获得更好的结果。
-
一个丢弃层(dropout),丢弃率为 0.3 或 30%,有助于网络减少对每个神经元/单元的依赖,减少过拟合数据。
-
一个包含 200 个神经元的密集全连接层连接到 LSTM 隐藏层,用于解释从序列中提取的特征。
-
输出层,它预测下一个词作为一个大小为词汇表的向量,并为词汇表中的每个词分配一个概率。
-
第二个密集或全连接层使用 softmax 分类器,以确保输出具有归一化概率的特征(例如在 0 和 1 之间)。
如何做...
- 模型通过以下命令定义,并在下方截图中进行说明:
model = Sequential()
model.add(Embedding(vocab_size, 100, input_length=sequence_length))
model.add(LSTM(200, return_sequences=True))
model.add(LSTM(200))
model.add(Dropout(0.3))
model.add(Dense(200, activation='relu'))
model.add(Dense(vocab_size, activation='softmax'))
print(model.summary())
-
打印模型摘要,仅确保模型按照预期构建。
-
编译模型,指定需要的类别交叉熵损失以适配模型。将训练的周期数设为 75,并且使用批量大小为 250 的小批量训练。这是通过以下命令实现的:
model.compile(loss='categorical_crossentropy', optimizer='adam',
metrics=['accuracy'])
model.fit(Input, Output, batch_size=250, epochs=75)
- 前面命令的输出结果如以下截图所示:
- 一旦模型编译完成,使用以下命令将其保存:
model.save('junglebook_trained.h5')
dump(tokenizer, open('tokenizer.pkl', 'wb'))
它是如何工作的...
-
模型是在 Keras 框架中使用
Sequential()函数构建的。模型中的第一层是一个嵌入层,它将词汇表的大小、向量维度和输入序列长度作为参数。 -
接下来的两层是 LSTM 层,每层有 200 个记忆单元。可以尝试更多的记忆单元和更深的网络,检查是否能够提高准确性。
-
下一个层是一个丢弃层,丢弃概率为 30%,这意味着在训练过程中有 30%的机会某个记忆单元不被使用。这样可以防止数据过拟合。同样,丢弃概率可以进行调整和优化。
-
最后的两层是两个全连接层。第一层具有
relu激活函数,第二层是 softmax 分类器。打印模型摘要以检查模型是否按照要求构建。 -
注意,在这种情况下,总的可训练参数数量为 2,115,228。模型摘要还显示了每一层将在模型中训练的参数数量。
-
模型以 250 的迷你批次,在 75 个周期内进行训练,以最小化训练时间。在我们的案例中,将周期数增加到 100 以上,并在训练时使用较小的批次,能显著提高模型的准确度,同时减少损失。
-
在训练过程中,你将看到性能摘要,包括每个批次更新结束时根据训练数据评估的损失和准确度。在我们的案例中,经过 75 个周期后,我们获得了接近 40%的准确率。
-
模型的目标不是以 100%的准确率记住文本,而是捕捉输入文本的特性,比如在自然语言和句子中存在的长期依赖关系和结构。
-
训练完成后,模型会保存在名为
junglebook_trained.h5的工作目录中。 -
我们还需要在以后将模型加载到内存中进行预测时,将单词映射到整数。这些信息存储在
Tokenizer对象中,并通过Pickle库中的dump()函数进行保存。
还有更多...
Jason Brownlee 在 Machine Learning Mastery 网站上的博客包含了大量有关开发、训练和调整用于自然语言处理的机器学习模型的有用信息。你可以通过以下链接访问它们:
machinelearningmastery.com/deep-learning-for-nlp/
machinelearningmastery.com/lstms-with-python/
machinelearningmastery.com/blog/
另见
进一步的信息,关于不同的 keras 层和本节中使用的其他函数,可以通过以下链接找到:
使用模型生成相似文本
现在你已经有了一个训练好的语言模型,可以开始使用它。在这个例子中,你可以用它生成具有与源文本相同统计特性的全新文本序列。虽然这在实际应用中并不实用(至少对于这个示例而言),但它提供了一个具体的例子,展示了语言模型学到的内容。
准备工作
- 重新加载训练序列。你可以通过使用我们最初开发的
load_document()函数来实现。代码如下:
def load_document(name):
file = open(name, 'r')
text = file.read()
file.close()
return text
# load sequences of cleaned text
input_filename = 'junglebook_sequences.txt'
doc = load_document(input_filename)
lines = doc.split('\n')
上述代码的输出如下截图所示:
-
请注意,输入文件名现在是
'junglebook_sequences.txt',它将把保存的训练序列加载到内存中。我们需要这些文本,以便选择一个源序列作为输入,供模型生成新的文本序列。 -
模型将需要 50 个单词作为输入。
后续,需要指定期望的输入长度。这可以通过计算加载数据的一行的长度来确定,并减去 1 作为期望的输出单词(该单词也位于同一行),如下面所示:
sequence_length = len(lines[0].split()) - 1 -
接下来,通过执行以下命令将训练好的模型加载到内存中:
from keras.models import load_model
model = load_model('junglebook.h5')
- 生成文本的第一步是准备一个种子输入。为此,从输入文本中选择一行随机文本。选择后,打印它,以便你了解所使用的内容。此操作如下所示:
from random import randint
seed_text = lines[randint(0,len(lines))]
print(seed_text + '\n')
如何做……
-
现在,你已经准备好逐个生成新单词。首先,使用训练模型时所用的相同分词器将种子文本编码为整数,可以通过以下代码完成:
encoded = tokenizer.texts_to_sequences([seed_text])[0]
- 模型可以通过调用
model.predict_classes()直接预测下一个单词,该方法将返回具有最高概率的单词索引:
prediction = model.predict_classes(encoded, verbose=0)
- 查找 Tokenizers 映射中的索引,以获取相关的单词,如下面的代码所示:
out_word = ''
for word, index in tokenizer.word_index.items():
if index == prediction:
out_word = word
break
- 将此单词附加到种子文本并重复此过程。重要的是,输入序列将变得太长。我们可以在输入序列被编码为整数后,对其进行截断至所需长度。Keras 提供了
pad_sequences()函数,可以用来执行此截断,如下所示:
encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
- 将所有这些内容封装成一个名为
generate_sequence()的函数,该函数接受模型、分词器、输入序列长度、种子文本和要生成的单词数作为输入。然后,它返回由模型生成的单词序列。你可以使用以下代码来实现:
from random import randint
from pickle import load
from keras.models import load_model
from keras.preprocessing.sequence import pad_sequences
def load_document(filename):
file = open(filename, 'r')
text = file.read()
file.close()
return text
def generate_sequence(model, tokenizer, sequence_length, seed_text, n_words):
result = list()
input_text = seed_text
for _ in range(n_words):
encoded = tokenizer.texts_to_sequences([input_text])[0]
encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
prediction = model.predict_classes(encoded, verbose=0)
out_word = ''
for word, index in tokenizer.word_index.items():
if index == prediction:
out_word = word
break
input_text += ' ' + out_word
result.append(out_word)
return ' '.join(result)
input_filename = 'junglebook_sequences.txt'
doc = load_document(input_filename)
lines = doc.split('\n')
seq_length = len(lines[0].split()) - 1
它是如何工作的……
现在,我们已经准备好生成新的单词序列,前提是我们有一些种子文本:
- 首先,使用以下命令再次将模型加载到内存中:
model = load_model('junglebook.h5')
- 接下来,通过输入以下命令加载分词器:
tokenizer = load(open('tokenizer.pkl', 'rb'))
- 使用以下命令随机选择一段种子文本:
seed_text = lines[randint(0,len(lines))]
print(seed_text + '\n')
- 最后,通过以下命令生成新的序列:
generated = generate_sequence(model, tokenizer, sequence_length, seed_text, 50)
print(generated)
- 打印生成的序列时,你将看到类似于以下截图的输出:
-
模型首先打印出 50 个随机种子文本单词,然后是 50 个生成的文本单词。在这种情况下,随机种子文本如下所示:
篮子里装满了干草,并将蚂蚱放进去,或者抓住两只螳螂让它们打斗,或者串起一条红黑相间的丛林坚果项链,或者看着蜥蜴在岩石上晒太阳,或者看蛇在泥坑附近捕捉青蛙,然后他们唱着长长的歌。
模型生成的 50 个单词的文本如下:
在评论的结尾带有奇怪的本地音调,以及他曾见证过真相的鬣狗,它们因周围的噪音而感到不安,仿佛看到峡谷尽头的画面,闻着被咬伤的味道,最好的公牛在黎明时分是本地的。
-
注意模型输出了它根据从输入文本中学到的内容生成的随机单词序列。你还会注意到,模型在模仿输入文本和生成自己的故事方面做得相当不错。尽管文本并不完全有意义,但它为我们提供了有价值的见解,说明了模型如何学习将统计上相似的单词排列在一起。
还有更多...
-
更改设置的随机种子后,网络生成的输出也会发生变化。你可能不会得到与前面示例完全相同的输出文本,但它会与用于训练模型的输入非常相似。
-
以下是通过多次运行生成文本片段获得的不同结果的一些截图:
- 模型甚至生成了它自己版本的《古腾堡计划许可证》,如下所示的截图:
-
通过将训练周期从大约 100 次增加到 200 次,模型的准确性可以提高到大约 60%。另一种提高学习效果的方法是通过以大约 50 到 100 的迷你批次进行训练。尝试调整不同的超参数和激活函数,看看哪些对结果的影响最好。
-
通过在定义模型时包含更多的 LSTM 层和丢弃层,模型也可以变得更密集。然而,要知道,如果模型更复杂并且训练周期更长,它将只会增加训练时间。
-
经过多次实验,理想的批量大小被确定为 50 到 100 之间,训练模型的理想周期次数被确定为 100 到 200 之间。
-
执行上述任务没有确定的方式。你还可以尝试不同的文本输入,如推文、客户评论或 HTML 代码。
-
其他可以执行的任务包括使用简化的词汇表(例如去除所有停用词)来进一步增强字典中的独特词汇;调节嵌入层的大小和隐藏层中记忆单元的数量;并扩展模型以使用预训练模型,如 Google 的 Word2Vec(预训练词汇模型),以查看它是否能产生更好的模型。
另见
有关本章最后部分使用的各种函数和库的更多信息,可以通过访问以下链接找到:
第七章:使用 TF-IDF 进行自然语言处理
本章将涉及以下内容:
-
下载治疗机器人会话文本数据集
-
分析治疗机器人会话数据集
-
可视化数据集中的词频
-
计算文本的情感分析
-
从文本中去除停用词
-
训练 TF-IDF 模型
-
评估 TF-IDF 模型性能
-
将模型性能与基准分数进行比较
介绍
自然语言处理(NLP)最近在新闻中频繁出现,如果你问五个人,你可能会得到十个不同的定义。最近,NLP 被用来帮助识别互联网上的机器人或水军,这些人试图传播假新闻,甚至更糟,像网络欺凌这样的战术。事实上,最近在西班牙发生了一起案件,一名学生在社交媒体账号上遭遇网络欺凌,这对学生的健康产生了严重影响,老师们开始介入。学校联系了研究人员,他们利用 NLP 方法,如 TF-IDF,帮助识别出几个可能的网络水军来源。最终,潜在的学生名单被提交给学校,并且在对质时,实际嫌疑人承认了自己是施害者。这一故事被发表在题为《监督式机器学习用于 Twitter 社交网络中水军档案的检测:应用于一起真实的网络欺凌案例》的论文中,作者为 Patxi Galan-García、Jose Gaviria de la Puerta、Carlos Laorden Gomez、Igor Santos 和 Pablo García Bringas。
本文重点介绍了利用多种方法分析文本并开发类人语言处理的能力。正是这种方法论将自然语言处理(NLP)融入到机器学习、深度学习和人工智能中。让机器能够理解文本数据并从中做出可能的决策,是自然语言处理的核心。用于自然语言处理的算法有很多,如下所示:
-
TF-IDF
-
Word2Vec
-
N-grams
-
潜在狄利克雷分配(LDA)
-
长短期记忆(LSTM)
本章将重点讨论一个包含个体与在线治疗网站上的聊天机器人对话的数据集。该聊天机器人的目的是识别需要立即转交给个体而不是继续与聊天机器人讨论的对话。最终,我们将重点使用 TF-IDF 算法对数据集进行文本分析,以确定聊天对话是否需要被分类为需要升级给个体处理的情况。TF-IDF代表词频-逆文档频率。这是一种常用于算法中的技术,用于识别一个单词在文档中的重要性。此外,TF-IDF 特别容易计算,尤其是在处理文档中的高词汇量时,并且能够衡量一个词的独特性。在处理聊天机器人数据时,这一点非常有用。主要目标是快速识别出一个独特的词,触发升级到个体的处理,从而提供即时支持。
下载治疗机器人会话文本数据集
本节将重点介绍下载和设置将用于本章自然语言处理(NLP)的数据集。
准备工作
本章使用的数据集基于治疗机器人与在线治疗网站访客之间的互动。它包含 100 次互动,每次互动都被标记为escalate或do_not_escalate。如果讨论需要更严肃的对话,机器人将标记讨论为escalate,转交给个体处理。否则,机器人将继续与用户讨论。
它是如何工作的……
本节将介绍下载聊天机器人数据集的步骤。
-
通过以下 GitHub 仓库访问数据集:
github.com/asherif844/ApacheSparkDeepLearningCookbook/tree/master/CH07/data -
到达仓库后,右键点击下图所示的文件:
-
下载
TherapyBotSession.csv并保存到与 Jupyter 笔记本SparkSession相同的本地目录中。 -
通过 Jupyter 笔记本使用以下脚本访问数据集,以构建名为
spark的SparkSession,并将数据集分配给 Spark 中的一个数据框(dataframe),名为df:
spark = SparkSession.builder \
.master("local") \
.appName("Natural Language Processing") \
.config("spark.executor.memory", "6gb") \
.getOrCreate()
df = spark.read.format('com.databricks.spark.csv')\
.options(header='true', inferschema='true')\
.load('TherapyBotSession.csv')
如何操作……
本节解释了聊天机器人数据如何进入我们的 Jupyter 笔记本。
- 可以通过点击仓库中的 TherapyBotSession.csv 查看数据集的内容,如下图所示:
- 一旦数据集下载完成,它就可以上传并转换成数据框
df。可以通过执行df.show()来查看数据框,如下图所示:
-
数据框中有 3 个主要字段是我们特别关注的:
-
id:每次访客与网站聊天机器人之间交易的唯一 ID。 -
label:由于这是一个监督学习方法,我们已经知道我们试图预测的结果,因此每笔交易已被分类为escalate或do_not_escalate。这个字段将在建模过程中使用,用来训练文本识别哪些单词会被归类为这两种情况之一。 -
chat:最后,我们有网站访客的chat文本,模型将对其进行分类。
-
还有更多...
数据框df有一些额外的列,_c3、_c4、_c5和_c6,这些列在模型中不会使用,因此可以使用以下脚本将其从数据集中排除:
df = df.select('id', 'label', 'chat')
df.show()
脚本的输出可以在下图中看到:
分析治疗机器人会话数据集
在对数据集应用模型之前,首先分析数据集总是很重要的。
准备工作
本节需要从pyspark.sql导入functions,以便在我们的数据框上执行。
import pyspark.sql.functions as F
如何操作...
以下部分将演示如何对文本数据进行剖析。
- 执行以下脚本以对
label列进行分组并生成计数分布:
df.groupBy("label") \
.count() \
.orderBy("count", ascending = False) \
.show()
- 使用以下脚本将一个新列
word_count添加到数据框df中:
import pyspark.sql.functions as F
df = df.withColumn('word_count', F.size(F.split(F.col('response_text'),' ')))
- 使用以下脚本按
label聚合平均单词数,avg_word_count:
df.groupBy('label')\
.agg(F.avg('word_count').alias('avg_word_count'))\
.orderBy('avg_word_count', ascending = False) \
.show()
它是如何工作的...
以下部分解释了通过分析文本数据获得的反馈。
- 收集跨多行的数据并按维度对结果进行分组是非常有用的。在这种情况下,维度是
label。使用df.groupby()函数来衡量按label分布的 100 次治疗交易的计数。我们可以看到,do_not_escalate与escalate的比例为65:35,如下图所示:
- 创建了一个新的列,
word_count,用于计算聊天机器人与在线访客之间 100 次交易中使用的单词数量。新创建的列word_count可以在下图中看到:
- 由于
word_count现已添加到数据框中,可以对其进行聚合,按label计算平均单词数。完成此操作后,我们可以看到,escalate会话的平均长度是do_not_escalate会话的两倍多,如下图所示:
可视化数据集中的单词计数
一张图片胜过千言万语,本节将证明这一点。不幸的是,从版本 2.2 开始,Spark 没有任何内建的绘图能力。为了在数据框中绘制值,我们必须转换为pandas。
准备工作
本节需要导入matplotlib用于绘图:
import matplotlib.pyplot as plt
%matplotlib inline
如何操作...
本部分详细说明了如何将 Spark 数据框转换为可在 Jupyter notebook 中查看的可视化图表。
- 使用以下脚本将 Spark 数据框转换为
pandas数据框:
df_plot = df.select('id', 'word_count').toPandas()
- 使用以下脚本绘制数据框:
import matplotlib.pyplot as plt
%matplotlib inline
df_plot.set_index('id', inplace=True)
df_plot.plot(kind='bar', figsize=(16, 6))
plt.ylabel('Word Count')
plt.title('Word Count distribution')
plt.show()
它是如何工作的...
本部分解释了如何将 Spark 数据框转换为 pandas,然后进行绘制。
-
Spark 数据框的一个子集被收集并使用 Spark 中的
toPandas()方法转换为pandas。 -
然后使用 matplotlib 绘制该数据子集,将 y 值设置为
word_count,将 x 值设置为id,如下图所示:
另见
除了 matplotlib 之外,Python 还有其他绘图库,如 bokeh、plotly 和 seaborn。
要了解更多关于 bokeh 的信息,请访问以下网站:
要了解更多关于 plotly 的信息,请访问以下网站:
要了解更多关于 seaborn 的信息,请访问以下网站:
计算文本的情感分析
情感分析是从单词或一系列单词中提取语气和情感的能力。本部分将利用 Python 中的技术计算数据集中 100 个事务的情感分析得分。
准备工作
本部分将需要使用 PySpark 中的函数和数据类型。此外,我们还将导入用于情感分析的 TextBlob 库。为了在 PySpark 中使用 SQL 和数据类型函数,必须导入以下内容:
from pyspark.sql.types import FloatType
此外,为了使用 TextBlob,必须导入以下库:
from textblob import TextBlob
如何实现...
以下部分将详细介绍如何将情感评分应用于数据集。
- 使用以下脚本创建一个情感评分函数
sentiment_score:
from textblob import TextBlob
def sentiment_score(chat):
return TextBlob(chat).sentiment.polarity
-
使用以下脚本将
sentiment_score应用于数据框中的每个对话回复: -
创建一个名为
sentiment_score_udf的lambda函数,将sentiment_score映射到 Spark 中的用户定义函数udf,并指定输出类型为FloatType(),如下脚本所示:
from pyspark.sql.types import FloatType
sentiment_score_udf = F.udf(lambda x: sentiment_score(x), FloatType())
- 如下脚本所示,将函数
sentiment_score_udf应用于数据框中的每个chat列:
df = df.select('id', 'label', 'chat','word_count',
sentiment_score_udf('chat').alias('sentiment_score'))
- 使用以下脚本按
label计算平均情感得分avg_sentiment_score:
df.groupBy('label')\
.agg(F.avg('sentiment_score').alias('avg_sentiment_score'))\
.orderBy('avg_sentiment_score', ascending = False) \
.show()
它是如何工作的...
本部分解释了如何将 Python 函数转换为 Spark 中的用户定义函数 udf,以便将情感分析得分应用于数据框中的每一列。
-
Textblob是一个 Python 中的情感分析库。它可以通过一个名为sentiment.polarity的方法计算情感得分,得分范围为 -1(非常负面)到 +1(非常正面),0 则表示中立。此外,Textblob还可以衡量主观性,范围从 0(非常客观)到 1(非常主观);不过,本章不会涉及主观性测量。 -
将 Python 函数应用到 Spark 数据框有几个步骤:
-
Textblob被导入,并且对chat列应用了一个名为sentiment_score的函数,以生成每个聊天对话的情感极性,结果会生成一个新的列,亦称为sentiment_score。 -
在 Spark 数据框上直接应用 Python 函数之前,必须首先通过 Spark 中的用户自定义函数转换
udf。 -
此外,函数的输出也必须明确声明,无论是整数还是浮动数据类型。在我们的例子中,我们明确声明函数的输出将使用来自
pyspark.sql.types的FloatType()。最后,情感得分会通过lambda函数应用到每一行数据中,这个lambda函数是在udf中调用的,称为sentiment_score_udf。
-
-
可以通过执行
df.show()来查看更新后的数据框,其中包含新创建的字段sentiment_score,如下所示的屏幕截图:
- 现在已经计算出了每个聊天对话的
sentiment_score,我们可以为每一行的情感极性指定一个从 -1(非常负面)到 +1(非常正面)的数值范围。就像我们处理词汇计数和平均词数一样,我们可以比较escalate对话和do_not_escalate对话的情感得分,看看它们是否有显著差异。我们可以按label计算一个平均情感得分avg_sentiment_score,如下所示的屏幕截图:
- 初步推测,
escalate的对话情感极性得分应该比do_not_escalate更负面。但实际上我们发现,escalate的情感极性比do_not_escalate稍微正面一些;然而,二者的情感极性都相当中立,接近 0。
另见
要了解更多关于 TextBlob 库的信息,请访问以下网站:
textblob.readthedocs.io/en/dev/
从文本中去除停用词
停用词是英语中常见的单词,通常在常见的自然语言处理(NLP)技术中被移除,因为它们可能会干扰分析。常见的停用词包括如 the 或 and 这样的词。
准备工作
本节需要导入以下库:
from pyspark.ml.feature import StopWordsRemover
from pyspark.ml import Pipeline
如何操作...
本节将演示如何去除停用词。
- 执行以下脚本,将
chat中的每个词提取到一个数组中的字符串:
df = df.withColumn('words',F.split(F.col('chat'),' '))
- 将一组常见的词语分配给一个变量
stop_words,并使用以下脚本将其视为停用词:
stop_words = ['i','me','my','myself','we','our','ours','ourselves',
'you','your','yours','yourself','yourselves','he','him',
'his','himself','she','her','hers','herself','it','its',
'itself','they','them','their','theirs','themselves',
'what','which','who','whom','this','that','these','those',
'am','is','are','was','were','be','been','being','have',
'has','had','having','do','does','did','doing','a','an',
'the','and','but','if','or','because','as','until','while',
'of','at','by','for','with','about','against','between',
'into','through','during','before','after','above','below',
'to','from','up','down','in','out','on','off','over','under',
'again','further','then','once','here','there','when','where',
'why','how','all','any','both','each','few','more','most',
'other','some','such','no','nor','not','only','own','same',
'so','than','too','very','can','will','just','don','should','now']
- 执行以下脚本来从 PySpark 导入
StopWordsRemover函数,并配置输入和输出列words和word without stop:
from pyspark.ml.feature import StopWordsRemover
stopwordsRemovalFeature = StopWordsRemover(inputCol="words",
outputCol="words without stop").setStopWords(stop_words)
- 执行以下脚本导入管道并定义停用词转换过程的
阶段,这些将应用到数据框:
from pyspark.ml import Pipeline
stopWordRemovalPipeline = Pipeline(stages=[stopwordsRemovalFeature])
pipelineFitRemoveStopWords = stopWordRemovalPipeline.fit(df)
- 最后,使用以下脚本将停用词移除转换
pipelineFitRemoveStopWords应用到数据框df:
df = pipelineFitRemoveStopWords.transform(df)
它是如何工作的...
本节解释了如何从文本中删除停用词。
-
正如我们在对
chat数据进行分析和探索时所做的那样,我们也可以调整chat对话的文本,将每个词分割成一个单独的数组。这将用于隔离停用词并将其移除。 -
提取的每个词作为字符串的新列称为
words,可以在以下截图中看到:
-
有多种方法可以将一组词汇分配给停用词列表。这些词汇中的一些可以通过一个名为
nltk的 Python 库自动下载和更新,nltk代表自然语言工具包。为了我们的目的,我们将利用一个包含 124 个常见停用词的列表来生成我们自己的列表。额外的词汇可以轻松地手动添加或删除。 -
停用词对文本没有任何价值,因此会通过指定
outputCol="words without stop"从新创建的列中删除。此外,通过指定inputCol = "words"来设置作为转换源的列。 -
我们创建了一个管道
stopWordRemovalPipeline,用来定义将转换数据的步骤或阶段序列。在这种情况下,用于转换数据的唯一阶段是特征stopwordsRemover。 -
管道中的每个阶段都可以有一个转换角色和一个估算角色。估算角色
pipeline.fit(df)被调用以生成一个名为pipelineFitRemoveStopWords的转换器函数。最后,调用transform(df)函数对数据框进行转换,以生成一个新的数据框,其中包含一个名为words without stop的新列。我们可以将两个列并排比较,检查它们之间的差异,如以下截图所示:
- 新列
words without stop不包含原始列words中被视为停用词的任何字符串。
另见:
要了解更多关于 nltk 停用词的信息,请访问以下网站:
要了解更多关于 Spark 机器学习管道的信息,请访问以下网站:
spark.apache.org/docs/2.2.0/ml-pipeline.html
要了解更多关于 PySpark 中 StopWordsRemover 特征的信息,请访问以下网站:
spark.apache.org/docs/2.2.0/api/python/pyspark.ml.html#pyspark.ml.feature.StopWordsRemover
训练 TF-IDF 模型
我们现在准备训练我们的 TF-IDF 自然语言处理(NLP)模型,并看看我们是否能将这些事务分类为 escalate 或 do_not_escalate。
准备工作
本节需要从 spark.ml.feature 和 spark.ml.classification 导入内容。
如何操作...
以下部分将逐步讲解训练 TF-IDF 模型的步骤。
- 创建一个新的用户定义函数
udf,用以下脚本为label列定义数值:
label = F.udf(lambda x: 1.0 if x == 'escalate' else 0.0, FloatType())
df = df.withColumn('label', label('label'))
- 执行以下脚本,以设置词向量化的 TF 和 IDF 列:
import pyspark.ml.feature as feat
TF_ = feat.HashingTF(inputCol="words without stop",
outputCol="rawFeatures", numFeatures=100000)
IDF_ = feat.IDF(inputCol="rawFeatures", outputCol="features")
- 设置管道
pipelineTFIDF,按以下脚本设置TF_和IDF_阶段的顺序:
pipelineTFIDF = Pipeline(stages=[TF_, IDF_])
- 使用以下脚本,将 IDF 估算器拟合并转换为数据框
df:
pipelineFit = pipelineTFIDF.fit(df)
df = pipelineFit.transform(df)
- 使用以下脚本将数据框按 75:25 的比例拆分,以进行模型评估:
(trainingDF, testDF) = df.randomSplit([0.75, 0.25], seed = 1234)
- 使用以下脚本导入并配置分类模型
LogisticRegression:
from pyspark.ml.classification import LogisticRegression
logreg = LogisticRegression(regParam=0.25)
- 将逻辑回归模型
logreg拟合到训练数据框trainingDF上。基于逻辑回归模型的transform()方法,会创建一个新的数据框predictionDF,如下脚本所示:
logregModel = logreg.fit(trainingDF)
predictionDF = logregModel.transform(testDF)
它是如何工作的...
以下部分解释了如何有效地训练一个 TF-IDF 自然语言处理模型。
- 理想情况下,标签应以数字格式而非类别形式出现,因为模型能够解释数字值,并在 0 和 1 之间进行输出分类。因此,
label列下的所有标签都会转换为数值形式的label,其值为 0.0 或 1.0,如下图所示:
-
TF-IDF 模型需要采用两步法,通过从
pyspark.ml.feature导入HashingTF和IDF来处理不同的任务。第一步仅涉及导入HashingTF和IDF,并为输入和输出列分配相应的值。numfeatures参数被设置为 100,000,以确保它大于数据框中不同词汇的数量。如果numfeatures小于词汇的不同数量,模型将不准确。 -
如前所述,管道的每个步骤都包含转换过程和估算过程。管道
pipelineTFIDF被配置为顺序排列各个步骤,其中IDF将紧随HashingTF。 -
HashingTF用于将“去除停用词后的词汇”转换为向量,存储在一个新的列rawFeatures中。随后,rawFeatures会被IDF处理,以估算大小并拟合数据框,生成一个新的列features,如下图所示:
-
为了进行训练,我们的数据框将以
75:25的比例保守地拆分,并且随机种子设置为1234。 -
由于我们的主要目标是将每个对话分类为
escalate(升级)或do_not_escalate(继续与机器人对话),因此我们可以使用传统的分类算法,如来自 PySpark 库的逻辑回归模型。逻辑回归模型配置了一个正则化参数regParam,其值为 0.025。我们使用该参数通过在稍微增加偏差的情况下最小化过拟合,从而稍微改善模型。 -
逻辑回归模型在
trainingDF上训练并拟合,然后创建一个新的数据框predictionDF,其中包含新转换的字段prediction,如以下截图所示:
还有更多...
虽然我们确实使用了用户定义函数 udf 来手动创建数值标签列,但我们也可以使用 PySpark 中的内置特性 StringIndexer 来为分类标签分配数值。要查看 StringIndexer 的实际操作,请访问 第五章,使用 Spark ML 预测消防部门电话。
另见
要了解更多关于 PySpark 中的 TF-IDF 模型,请访问以下网站:
spark.apache.org/docs/latest/mllib-feature-extraction.html#tf-idf
评估 TF-IDF 模型性能
此时,我们准备好评估模型的性能
准备工作
本节需要导入以下库:
-
metrics来自sklearn -
来自
pyspark.ml.evaluation的BinaryClassificationEvaluator
如何执行...
本节介绍了评估 TF-IDF NLP 模型的步骤。
- 使用以下脚本创建混淆矩阵:
predictionDF.crosstab('label', 'prediction').show()
- 使用来自 sklearn 的
metrics评估模型,使用以下脚本:
from sklearn import metrics
actual = predictionDF.select('label').toPandas()
predicted = predictionDF.select('prediction').toPandas()
print('accuracy score: {}%'.format(round(metrics.accuracy_score(actual, predicted),3)*100))
- 使用以下脚本计算 ROC 分数:
from pyspark.ml.evaluation import BinaryClassificationEvaluator
scores = predictionDF.select('label', 'rawPrediction')
evaluator = BinaryClassificationEvaluator()
print('The ROC score is {}%'.format(round(evaluator.evaluate(scores),3)*100))
它是如何工作的...
本节解释了我们如何使用评估计算来确定模型的准确性。
- 混淆矩阵有助于快速总结实际结果和预测结果之间的准确性。由于我们使用了 75:25 的拆分,因此应该从我们的训练数据集中看到 25 个预测结果。我们可以使用以下脚本来构建混淆矩阵:
predictionDF.crosstab('label', 'prediction').show()。脚本的输出可以在以下截图中看到:
- 我们现在处于评估模型准确性的阶段,通过将
prediction值与实际的label值进行比较。sklearn.metrics接受两个参数,分别是与label列关联的actual(实际)值和从逻辑回归模型中得出的predicted(预测)值。
请注意,我们再次使用 toPandas() 方法将 Spark 数据框的列值转换为 Pandas 数据框。
- 创建了两个变量,
actual和predicted,并使用metrics.accuracy_score()函数计算了 91.7%的准确率,如下截图所示:
- ROC(接收者操作特征)通常与测量真阳性率与假阳性率之间的曲线相关。曲线下的面积越大越好。与曲线相关的 ROC 得分是另一个可以用来衡量模型性能的指标。我们可以使用
BinaryClassificationEvaluator计算ROC,如以下截图所示:
另见
要了解更多关于 PySpark 中BinaryClassificationEvaluator的信息,请访问以下网站:
将模型性能与基准得分进行比较
虽然我们的模型准确率为 91.7%,这已经是很棒的结果,但与基准得分进行比较同样重要。本节将深入探讨这个概念。
如何做到这一点...
本节详细介绍了计算基准准确率的步骤。
- 执行以下脚本以从
describe()方法中检索平均值:
predictionDF.describe('label').show()
- 通过减去
1 - 平均值得分来计算基准准确率。
它是如何工作的...
本节解释了基准准确率背后的概念,以及如何利用它来理解我们模型的效果。
-
如果每个
chat对话都被标记为do_not_escalate,反之亦然,会不会使我们的基准准确率超过 91.7%?找出这个问题最简单的方法是运行predictionDF的label列上的describe()方法,使用以下脚本:predictionDF.describe('label').show() -
脚本的输出可以在下方的截图中看到:
-
label的平均值为 0.2083,约为 21%,这意味着label为 1 的情况仅占 21%。因此,如果我们将每个对话标记为do_not_escalate,我们将大约 79%的时间是正确的,这比我们模型的准确率 91.7%还低。 -
因此,我们可以说我们的模型表现优于盲目基准性能模型。
另见
要了解有关 PySpark 数据框中describe()方法的更多信息,请访问以下网站:
spark.apache.org/docs/2.2.0/api/python/pyspark.sql.html#pyspark.sql.DataFrame.describe
第八章:使用 XGBoost 进行房地产价值预测
房地产市场是定价竞争最激烈的市场之一。价格会因多种因素而大幅波动,例如位置、房产年龄、大小等。因此,准确预测房产(尤其是住房市场)的价格,已成为现代社会的一项挑战,目的是做出更好的投资决策。本章将专门讨论这一问题。
完成本章后,你将能够:
-
下载 King County 房屋销售数据集
-
执行探索性分析与可视化
-
绘制价格与其他特征之间的相关性
-
预测房屋价格
下载 King County 房屋销售数据集
我们无法在没有数据集的情况下建立模型。我们将在本节中下载数据。
准备工作
Kaggle (www.kaggle.com/) 是一个用于预测建模和分析竞赛的平台,统计学家和数据挖掘人员在其中竞争,旨在为公司和用户上传的数据集生成最优模型进行预测和描述。King County 房屋销售数据集包含了 1900 年至 2015 年间,在纽约 King County 销售的 21,613 套房屋的记录。数据集还包含了 21 个不同的变量,如位置、邮政编码、卧室数量、居住空间面积等,每个房屋都有这些信息。
如何操作...
-
可以从以下网址访问数据集:
www.kaggle.com/harlfoxem/housesalesprediction。该数据集来自 King County 的公共记录,免费下载并可用于任何分析。 -
一旦进入网站,你可以点击下载按钮,如下图所示:
King County 房屋销售数据集
-
从压缩的下载文件
housesalesprediction.zip中会出现一个名为kc_house_data.csv的文件。 -
将名为
kc_house_data.csv的文件保存在当前工作目录中,因为这将是我们的数据集。该文件将被加载到 IPython 笔记本中进行分析和预测。
它是如何工作的...
- 使用以下代码安装本章所需的库:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import mpl_toolkits
from sklearn import preprocessing
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.feature_selection import RFE
from sklearn import linear_model
from sklearn.cross_validation import train_test_split %matplotlib inline
- 上一步应产生一个输出,示例如下图所示:
- 始终建议检查当前工作目录,并将其设置为存储数据集的目录。示例如下图所示:
在我们的案例中,名为 Chapter 10 的文件夹被设为当前工作目录。
- 文件中的数据通过
read_csv()函数读取到名为dataframe的 Pandas 数据框中,并使用list(dataframe)命令列出特征/头部信息,示例如下图所示:
正如您可能已经注意到的那样,数据集包含 21 个不同的变量,如 id、日期、价格、卧室、浴室等。
更多内容...
本章使用的库及其函数如下:
-
Numpy,用于整理数组形式的数据以及以数组形式存储名称列表 -
Pandas,用于所有数据整理和以数据帧形式管理数据 -
Seaborn,这是一个用于探索性分析和绘图所需的可视化库 -
MPL_Toolkits,其中包含Matplotlib所需的多个函数和依赖项 -
本章需要的科学和统计库
Scikit Learn的函数 -
我们还需要一些其他库,如
XGBoost,但在构建模型时会根据需要导入
另请参阅
可以通过访问以下链接找到有关不同库的进一步文档:
执行探索性分析和可视化
在预测诸如 price 这样的变量时,通过可视化数据并查明依赖变量如何受其他变量影响,可以帮助分析。探索性分析提供了许多通过查看数据无法立即获得的见解。本章节将描述如何从大数据中可视化并得出见解。
准备工作
- 可以使用
dataframe.head()函数打印dataframe的头部,该函数产生一个输出,如下截图所示:
- 同样地,可以使用
dataframe.tail()函数打印dataframe的尾部,该函数产生一个输出,如下截图所示:
dataframe.describe()函数用于获取每列的最大、最小和平均值等一些基本统计数据。如下截图所示:
dataframe.describe() 函数的输出
-
正如您所见,数据集包含 21,613 条记录,这些记录是在 1900 年至 2015 年之间售出的房屋。
-
仔细查看统计数据后,我们意识到大多数售出的房屋平均有约三个卧室。我们还可以看到,房屋中卧室数量最少的是 0,而最大的房屋有 33 间卧室和 13,540 平方英尺的生活空间。
如何做...
- 让我们绘制整个数据集中卧室数量的分布,看看三卧室房屋与二卧室或一卧室房屋相比如何。这可以通过以下代码完成:
dataframe['bedrooms'].value_counts().plot(kind='bar') plt.title('No. of bedrooms')
plt.xlabel('Bedrooms')
plt.ylabel('Count')
sns.despine
- 我们还可以使用以下命令绘制相同数据的饼图:
dataframe['bedrooms'].value_counts().plot(kind='pie')
plt.title('No. of bedrooms')
- 接下来,让我们尝试查看在金县最常售出的房屋的楼层数。这可以通过绘制柱状图来完成,命令如下:
dataframe['floors'].value_counts().plot(kind='bar') plt.title('Number of floors')
plt.xlabel('No. of floors')
plt.ylabel('Count')
sns.despine
- 接下来,我们需要了解哪些位置的房屋销售数量最多。我们可以通过使用数据集中的
latitude和longitude变量来实现,代码如下:
plt.figure(figsize=(20,20))
sns.jointplot(x=dataframe.lat.values, y=dataframe.long.values, size=9)
plt.xlabel('Longitude', fontsize=10)
plt.ylabel('Latitude', fontsize=10)
plt.show()
sns.despine()
- 让我们通过执行以下命令,看看不同卧室数量的房屋价格如何比较:
plt.figure(figsize=(20,20))
sns.jointplot(x=dataframe.lat.values, y=dataframe.long.values, size=9)
plt.xlabel('Longitude', fontsize=10)
plt.ylabel('Latitude', fontsize=10)
plt.show()
sns.despine()
- 使用以下命令,我们可以得到房价与卧室数量之间的图表:
plt.figure(figsize=(20,20))
sns.jointplot(x=dataframe.lat.values, y=dataframe.long.values, size=9)
plt.xlabel('Longitude', fontsize=10)
plt.ylabel('Latitude', fontsize=10)
plt.show()
sns.despine()
- 同样,让我们看看价格与所有已售房屋的居住面积之间的关系。这可以通过使用以下命令来实现:
plt.figure(figsize=(8,8))
plt.scatter(dataframe.price, dataframe.sqft_living)
plt.xlabel('Price')
plt.ylabel('Square feet')
plt.show()
- 售出房屋的状况也为我们提供了一些重要信息。让我们将其与价格进行对比,来更好地了解一般的趋势。这可以通过以下命令实现:
plt.figure(figsize=(5,5))
plt.bar(dataframe.condition, dataframe.price)
plt.xlabel('Condition')
plt.ylabel('Price')
plt.show()
- 我们可以使用以下命令查看金县哪些邮政编码区域的房屋销售最多:
plt.figure(figsize=(8,8))
plt.scatter(dataframe.zipcode, dataframe.price)
plt.xlabel('Zipcode')
plt.ylabel('Price')
plt.show()
- 最后,绘制每套房屋的等级与价格的关系,以便通过使用以下命令找出基于房屋等级的销售趋势:
plt.figure(figsize=(10,10))
plt.scatter(dataframe.grade, dataframe.price)
plt.xlabel('Grade')
plt.ylabel('Price')
plt.show()
它是如何工作的...
- 卧室数量的图表应该生成如下输出:
-
很明显,三卧室房屋是销售最多的,其次是四卧室房屋,然后是二卧室房屋,出乎意料的是,五卧室和六卧室房屋的销售量也不低。
-
卧室数量的饼图生成的输出如下所示:
-
你会注意到,三卧室房屋大约占金县所有已售房屋的 50%。看起来大约 25%是四卧室房屋,剩下的 25%则由二卧室、五卧室、六卧室房屋等组成。
-
运行脚本以根据楼层数对大多数出售的房屋进行分类时,我们会看到以下输出:
-
很明显,单层房屋销售最多,其次是二层房屋。三层以上的房屋数量相对较少,这或许反映了金县居民的家庭规模和收入水平。
-
检查不同位置售出房屋的密度后,我们得到如下输出。从中可以明显看出,一些位置的房屋销售密度高于其他位置:
-
从前面图表观察到的趋势来看,很容易注意到,在经度 -122.2 到 -122.4 之间销售的房屋更多。同样,经度 47.5 到 47.8 之间销售的房屋密度比其他经度更高。这可能是对比其他社区更安全和居住质量更好的指示。
-
当绘制房屋价格与房屋卧室数量的关系时,我们意识到关于房屋卧室数量的趋势与房价成正比,直到六个卧室,然后成为反比,如下面的截图所示:
- 绘制每个房屋的生活区域与价格的关系图表明,随着房屋面积的增加,价格也在增加。最昂贵的房屋似乎有 12,000 平方英尺的生活面积,如下面的截图所示:
更多内容...
- 当绘制房屋条件与价格的关系时,我们再次注意到一个预期的趋势:随着条件评级的提高,房价也在增加,如下面的截图所示。有趣的是,五卧房的价格比四卧房略低,这可能是因为对这么大的房子的购买者较少:
- 房屋的邮政编码与价格的关系图显示了不同邮政编码地区房价的趋势。你可能已经注意到,像 98100 到 98125 这样的一些邮政编码区域,销售的房屋密度比其他地区高,而像 98040 这样的邮政编码地区的房价高于平均价格,可能表明这是一个更富裕的社区,如下面的截图所示:
- 房屋等级与价格的关系图显示了随着等级的增加,价格稳步增长的趋势。两者之间似乎存在明确的线性关系,正如下面截图中的结果所观察到的:
另请参阅
下面的链接很好地解释了为什么在对数据运行任何模型之前,数据可视化如此重要:
-
www.slideshare.net/Centerline_Digital/the-importance-of-data-visualization -
www.techchange.org/2015/05/19/data-visualization-analysis-international-development/
绘制价格与其他特征之间的相关性
现在,初步的探索性分析已经完成,我们对不同变量如何影响每栋房子的价格有了更好的了解。然而,我们并不知道每个变量在预测价格时的重要性。由于我们有 21 个变量,单纯将所有变量放入一个模型中构建会变得困难。因此,可能需要丢弃或忽略某些变量,特别是那些在预测中比其他变量重要性较低的变量。
准备工作
相关系数在统计学中用于衡量两个变量之间关系的强度。特别是,在执行线性回归时,Pearson 相关系数是最常用的系数。相关系数通常在 -1 到 +1 之间取值:
-
相关系数为 1 表示当一个变量增加时,另一个变量也会按照固定比例增加。例如,鞋码几乎与脚长呈完全正相关。
-
相关系数为 -1 表示当一个变量增加时,另一个变量会按照固定比例减少。例如,油箱中的油量与加速或换档机制几乎呈完全负相关(与第四档相比,在第一档行驶更长时间会消耗更多的油)。
-
零表示对于每一个增加,既没有正向增加也没有负向增加。这两个变量之间没有关系。
如何进行...
- 首先使用以下命令删除数据集中的
id和date特征。我们在预测中不会使用它们,因为 ID 变量都是唯一的,且在我们的分析中没有值,而日期需要用不同的函数来正确处理。这个任务留给读者完成:
x_df = dataframe.drop(['id','date',], axis = 1)
x_df
- 使用以下命令将因变量(本例中的房价)复制到一个新的
dataframe中:
y = dataframe[['price']].copy()
y_df = pd.DataFrame(y)
y_df
- 可以使用以下脚本手动找到价格与其他变量之间的相关性:
print('Price Vs Bedrooms: %s' % x_df['price'].corr(x_df['bedrooms']))
print('Price Vs Bathrooms: %s' % x_df['price'].corr(x_df['bathrooms']))
print('Price Vs Living Area: %s' % x_df['price'].corr(x_df['sqft_living']))
print('Price Vs Plot Area: %s' % x_df['price'].corr(x_df['sqft_lot']))
print('Price Vs No. of floors: %s' % x_df['price'].corr(x_df['floors']))
print('Price Vs Waterfront property: %s' % x_df['price'].corr(x_df['waterfront']))
print('Price Vs View: %s' % x_df['price'].corr(x_df['view']))
print('Price Vs Grade: %s' % x_df['price'].corr(x_df['grade']))
print('Price Vs Condition: %s' % x_df['price'].corr(x_df['condition']))
print('Price Vs Sqft Above: %s' % x_df['price'].corr(x_df['sqft_above']))
print('Price Vs Basement Area: %s' % x_df['price'].corr(x_df['sqft_basement']))
print('Price Vs Year Built: %s' % x_df['price'].corr(x_df['yr_built']))
print('Price Vs Year Renovated: %s' % x_df['price'].corr(x_df['yr_renovated']))
print('Price Vs Zipcode: %s' % x_df['price'].corr(x_df['zipcode']))
print('Price Vs Latitude: %s' % x_df['price'].corr(x_df['lat']))
print('Price Vs Longitude: %s' % x_df['price'].corr(x_df['long']))
-
除了前面的方法,还有一种更简单的方法可以通过以下一行代码来查找一个变量与所有其他变量(或列)之间的相关性:
x_df.corr().iloc[:,-19] -
可以使用
seaborn库和以下脚本绘制相关变量:
sns.pairplot(data=x_df,
x_vars=['price'],
y_vars=['bedrooms', 'bathrooms', 'sqft_living',
'sqft_lot', 'floors', 'waterfront','view',
'grade','condition','sqft_above','sqft_basement',
'yr_built','yr_renovated','zipcode','lat','long'],
size = 5)
它是如何工作的...
- 在删除了
id和date变量后,新的dataframe(命名为x_df)包含 19 个变量或列,如以下截图所示。为了本书的目的,只打印出前十个条目:
输出的前 10 个条目
- 创建一个只包含因变量(价格)的新
dataframe时,您将看到如下输出。这个新dataframe命名为y_df。再次说明,这里只打印了价格列的前十条记录,供参考:
- 价格与其他变量的相关性如下图所示:
- 你可能已经注意到,
sqft_living变量与价格的相关性最高,其相关系数为 0.702035。下一个最相关的变量是grade,其相关系数为 0.667434,其次是sqft_above,其相关系数为 0.605567。Zipcode是与价格最不相关的变量,相关系数为-0.053202。
更多内容…
- 使用简化代码找到的相关系数给出了完全相同的值,但也给出了价格与其自身的相关系数,结果是 1.0000,正如预期的那样。以下截图展示了这一点:
- 使用
seaborn库绘制的相关系数如下图所示。请注意,每个图的 x 轴上是价格:
相关系数的绘制
另见
以下链接对 Pearson 相关系数及其手动计算方法提供了很好的解释:
en.wikipedia.org/wiki/Pearson_correlation_coefficient
www.statisticshowto.com/probability-and-statistics/correlation-coefficient-formula/
预测房价
本节将介绍如何使用当前dataframe中的所有特征来构建一个简单的线性模型以预测房价。然后,我们将评估该模型,并尝试在本节后半部分通过使用更复杂的模型来提高准确性。
准备工作
访问以下链接,了解线性回归是如何工作的以及如何在 Scikit Learn 库中使用线性回归模型:
en.wikipedia.org/wiki/Linear_regression
www.stat.yale.edu/Courses/1997-98/101/linreg.htm
newonlinecourses.science.psu.edu/stat501/node/251/
scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html
scikit-learn.org/stable/modules/linear_model.html
如何做到...
- 从
x_df数据框中删除Price列,并使用以下脚本将其保存到一个名为x_df2的新数据框中:
x_df2 = x_df.drop(['price'], axis = 1)
- 声明一个名为
reg的变量,并将其等式赋值给 Scikit Learn 库中的LinearRegression()函数,使用以下脚本:
reg=linear_model.LinearRegression()
- 使用以下脚本将数据集拆分为测试集和训练集:
x_train,x_test,y_train,y_test = train_test_split(x_df2,y_df,test_size=0.4,random_state=4)
- 使用以下脚本在训练集上拟合模型:
reg.fit(x_train,y_train)
-
使用
reg.coef_命令打印应用线性回归到训练集和测试集后生成的系数。 -
使用以下脚本查看模型生成的预测列:
predictions=reg.predict(x_test)
predictions
- 使用以下命令打印模型的准确性:
reg.score(x_test,y_test)
它是如何工作的...
- 在将回归模型拟合到训练集后,输出应如下所示:
reg.coeff_命令生成 18 个系数,每个变量对应一个系数,如下截图所示:
-
特征/变量的系数中,值最正的那些对价格预测的影响更大,相比之下,值为负的特征/变量系数对价格预测的影响较小。这就是回归系数的重要性所在。
-
打印预测结果时,您必须看到一个输出,这是一个从 1 到 21,612 的值数组,每一行代表数据集中的一个值,截图如下所示:
- 最后,在打印模型的准确性时,我们得到了 70.37%的准确率,对于一个线性模型来说,这个结果还不错。以下是该截图:
还有更多...
线性模型在第一次尝试时表现尚可,但如果我们希望模型更准确,我们需要使用一个更复杂的模型,带有一些非线性部分,以便更好地拟合所有数据点。XGBoost 是我们在本节中将使用的模型,目的是通过线性回归提高准确性。方法如下:
-
使用
import xgboost命令导入XGBoost库。 -
如果出现错误,您需要通过终端使用
pip install安装该库。可以通过打开一个新的终端窗口并执行以下命令来完成此操作:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 在此阶段,您应该会看到类似以下截图的输出:
- 在此阶段,系统会提示您输入密码。安装 homebrew 后,您将看到类似以下截图的输出:
-
接下来,使用以下命令安装 Python:
brew install python -
使用
brew doctor命令检查您的安装,并根据 homebrew 的建议进行操作。 -
一旦
Homebrew安装完成,使用以下命令进行 XGBoost 的 pip 安装:pip install xgboost -
安装完成后,您应该能够将 XGBoost 导入到 IPython 环境中。
一旦 XGBoost 成功导入到 Jupyter 环境中,您将能够使用库中的函数来声明并存储模型。可以按照以下步骤进行:
- 声明一个名为
new_model的变量来存储模型,并使用以下命令声明所有超参数:
new_model = xgboost.XGBRegressor(n_estimators=750, learning_rate=0.09, gamma=0, subsample=0.65, colsample_bytree=1, max_depth=7)
- 前述命令的输出必须看起来像以下截图所示:
- 将数据拆分为测试集和训练集,并使用以下命令将新模型拟合到拆分的数据上:
from sklearn.model_selection import train_test_split
traindf, testdf = train_test_split(x_train, test_size = 0.2)
new_model.fit(x_train,y_train)
- 此时,您将看到类似以下截图的输出:
- 最后,使用新训练的模型来预测房价,并使用以下命令评估新模型:
from sklearn.metrics import explained_variance_score
predictions = new_model.predict(x_test)
print(explained_variance_score(predictions,y_test))
- 执行前述命令后,您应该会看到类似以下截图的输出:
-
请注意,新模型的准确率现在是 87.79%,大约为 88%。这被认为是最优的。
-
在这种情况下,
number of estimators设置为 750。经过在 100 到 1,000 之间的实验后,确定 750 个估算器能够提供最佳的准确率。learning rate设置为 0.09。Subsample rate设置为 65%。Max_depth设置为 7。似乎max_depth对模型准确性的影响不大。然而,使用较慢的学习率时,准确率有所提高。通过调整各种超参数,我们进一步将准确率提高到 89%。 -
接下来的步骤包括对卧室、浴室、楼层、邮政编码等变量进行独热编码,并在模型拟合之前对所有变量进行标准化。尝试调优超参数,如 XGBoost 模型中的学习率、估算器数量、子采样率等,观察它们如何影响模型的准确性。这部分留给读者作为练习。
-
此外,您可能希望尝试在 XGBoost 中使用交叉验证,以找出模型中树的最佳数量,这将进一步提高准确性。
-
另一个可以尝试的练习是在训练过程中使用不同大小的测试和训练数据集,并在训练中加入
date变量。在我们的案例中,我们已将其拆分为 80%的训练数据和 20%的测试数据。尝试将测试集增加到 40%,并查看模型准确度的变化。
另请参阅
访问以下链接了解如何调整 XGBoost 模型中的超参数以及如何在 XGBoost 中实施交叉验证:
xgboost.readthedocs.io/en/latest/python/index.html
xgboost.readthedocs.io/en/latest/get_started/
第九章:使用 LSTM 预测苹果股票市场成本
股票市场预测已经进行多年,并且催生了一个完整的预测行业。考虑到如果预测准确,它可以带来可观的利润,这一点不应令人惊讶。理解何时是买入或卖出股票的最佳时机是掌握华尔街的关键。本章将重点介绍如何使用 Keras 上的 LSTM 创建深度学习模型,以预测苹果公司(AAPL)的股市报价。
本章将介绍以下食谱:
-
下载苹果公司的股市数据
-
探索并可视化苹果公司的股市数据
-
准备股票数据以供模型性能评估
-
构建 LSTM 模型
-
评估 LSTM 模型
下载苹果公司的股市数据
有许多资源可以下载苹果公司的股市数据。为了我们的目的,我们将使用 Yahoo! Finance 网站。
准备开始
本部分需要初始化一个 Spark 集群,供本章所有食谱使用。可以通过终端使用sparknotebook来初始化 Spark 笔记本,如下图所示:
可以通过以下脚本在 Jupyter 笔记本中初始化SparkSession:
spark = SparkSession.builder \
.master("local") \
.appName("StockMarket") \
.config("spark.executor.memory", "6gb") \
.getOrCreate()
如何操作...
以下部分将介绍如何下载苹果公司历史股市数据的步骤。
-
访问以下网站,以追踪苹果公司(股票代码:AAPL)的每日历史调整收盘股价:
finance.yahoo.com/quote/AAPL/history -
在“历史数据”标签中设置并应用以下参数:
-
时间范围:2000 年 1 月 1 日 - 2018 年 4 月 30 日。
-
显示:历史价格。
-
频率:每日。
-
-
点击下载数据链接,将数据集下载为
.csv文件,如下图所示:
- 下载文件
AAPL.csv,然后使用以下脚本将相同的数据集上传到 Spark 数据框:
df =spark.read.format('com.databricks.spark.csv')\
.options(header='true', inferschema='true')\
.load('AAPL.csv')
如何操作...
以下部分将解释如何将股市数据整合到 Jupyter 笔记本中。
-
Yahoo! Finance 是一个提供上市公司股市报价的好来源。苹果公司的股市报价(AAPL)在纳斯达克交易,历史报价可以用于模型开发和分析。Yahoo! Finance 提供了按日、周或月捕捉股市报价的选项。
-
本章的目的是按日预测股票,因为这将拉入最多的数据来训练我们的模型。我们可以通过追溯数据从 2000 年 1 月 1 日到 2018 年 4 月 30 日来实现这一点。
-
一旦我们设置好下载的参数,我们就可以从 Yahoo! Finance 获取格式良好的 CSV 文件,该文件可以轻松地转换为 Spark 数据框,几乎没有问题。
-
数据框将允许我们查看股票的日期、开盘价、最高价、最低价、收盘价、调整后收盘价和成交量。数据框中的列跟踪当天的开盘和收盘股价,以及当天交易的最高和最低股价。当天交易的股票数量也会被记录下来。执行
df.show()可以显示 Spark 数据框df的输出,具体如以下截图所示:
还有更多...
Python 曾经有股票市场 API,允许你自动连接并获取像苹果公司这样的上市公司股票报价。你需要输入参数并检索可以存储在数据框中的数据。然而,从 2018 年 4 月起,Yahoo! Finance API 已不再运行,因此不再是提取本章数据的可靠解决方案。
另见
Pandas_datareader是一个非常强大的库,用于从 Yahoo! Finance 等网站提取数据。要了解更多关于该库的信息,以及它如何在 Yahoo! Finance 重新上线后连接到该网站,请访问以下网站:
github.com/pydata/pandas-datareader
探索和可视化苹果公司股票市场数据
在对数据进行任何建模和预测之前,首先探索并可视化手头的数据,以发现任何潜在的亮点是非常重要的。
准备就绪
本节将对数据框进行转换和可视化操作。这将需要在 Python 中导入以下库:
-
pyspark.sql.functions -
matplotlib
如何操作...
以下部分介绍了探索和可视化股票市场数据的步骤。
- 使用以下脚本转换数据框中的
Date列,通过去除时间戳:
import pyspark.sql.functions as f
df = df.withColumn('date', f.to_date('Date'))
- 创建一个 for 循环,向数据框中添加三个额外的列。该循环将
date字段拆分为year、month和day,如下脚本所示:
date_breakdown = ['year', 'month', 'day']
for i in enumerate(date_breakdown):
index = i[0]
name = i[1]
df = df.withColumn(name, f.split('date', '-')[index])
-
使用以下脚本将 Spark 数据框的子集保存到名为
df_plot的pandas数据框中:df_plot = df.select('year', 'Adj Close').toPandas()。 -
使用以下脚本在笔记本中绘制并可视化
pandas数据框df_plot:
from matplotlib import pyplot as plt
%matplotlib inline
df_plot.set_index('year', inplace=True)
df_plot.plot(figsize=(16, 6), grid=True)
plt.title('Apple stock')
plt.ylabel('Stock Quote ($)')
plt.show()
-
使用以下脚本计算我们的 Spark 数据框的行数和列数:
df.toPandas().shape。 -
执行以下脚本来确定数据框中的空值:
df.dropna().count()。 -
执行以下脚本以提取
Open、High、Low、Close和Adj Close的统计数据:
df.select('Open', 'High', 'Low', 'Close', 'Adj Close').describe().show()
它是如何工作的...
以下部分解释了探索性数据分析中使用的技术和获得的洞察。
- 数据框中的
date列更像是一个日期时间列,所有时间值都以00:00:00结尾。对于我们在建模过程中所需的内容,这是不必要的,因此可以从数据集中删除。幸运的是,PySpark 有一个to_date函数,可以轻松地完成这项操作。数据框df使用withColumn()函数进行转换,现在只显示没有时间戳的日期列,如以下截图所示:
- 为了分析目的,我们希望从
date列中提取day、month和year。我们可以通过遍历自定义列表date_breakdown,以-分隔日期,然后使用withColumn()函数为年份、月份和日期添加新列。添加新列后的更新数据框可以在以下截图中看到:
一个重要的结论是,PySpark也有一个 SQL 函数用于日期,可以从日期时间戳中提取天、月或年。例如,如果我们要向数据框添加一个月份列,可以使用以下脚本:df.withColumn("month",f.month("date")).show()。这突出了在 Spark 中转换数据的多种方法。
-
Spark 数据框在可视化功能上不如
pandas数据框。因此,我们将从 Spark 数据框df中提取两列,并将它们转换为pandas数据框,用于绘制线性图或时间序列图。y 轴将是股票的调整后收盘价,x 轴将是日期的年份。 -
pandas 数据框
df_plot准备好用于绘制图表,一旦设置了一些格式化功能,如网格可见性、图表大小和标题及坐标轴标签。此外,我们明确指定数据框的索引需要指向年份列。否则,默认的索引会出现在 x 轴上,而不是年份。最终的时间序列图可以在以下截图中看到:
-
苹果在过去 18 年经历了广泛的增长。虽然有几年出现了下滑,但总体趋势是稳定向上的,近几年股价徘徊在175 之间。
-
到目前为止,我们已经对数据框做了一些更改,因此重要的是要获取行和列的总数,这会影响数据集稍后如何分割用于测试和训练。如以下截图所示,我们总共有 10 列和 4,610 行:
-
执行
df.dropna().count()时,我们可以看到行数仍为 4,610,这与上一步的行数相同,表明没有任何行包含空值。 -
最后,我们可以对每个将用于模型的列进行行数、均值、标准差、最小值和最大值的统计。这有助于识别数据中是否存在异常。需要注意的一点是,所有五个将在模型中使用的字段的标准差都高于均值,这表明数据分布较广,并非都集中在均值附近。以下截图显示了 Open、High、Low、Close 和 Adj Close 的统计数据:
还有更多内容…
虽然 Spark 中的数据框没有pandas数据框那样的本地可视化功能,但有些公司提供企业级 Spark 管理服务,允许通过笔记本进行高级可视化,而无需使用matplotlib等库。Databricks 就是其中一家提供此功能的公司。
以下是使用 Databricks 笔记本内置功能进行可视化的示例:
另见
要了解更多关于 Databricks 的信息,请访问以下网站:databricks.com/。
要了解更多关于 Databricks 笔记本中可视化的信息,请访问以下网站:docs.databricks.com/user-guide/visualizations/index.html。
要了解如何通过 Microsoft Azure 订阅访问 Databricks,请访问以下网站:
azure.microsoft.com/en-us/services/databricks/
为模型性能准备股票数据
我们几乎准备好为苹果股票的价值表现构建预测算法了。剩下的任务是以确保最佳预测结果的方式准备数据。
准备就绪
在本节中,我们将对数据框执行转换和可视化操作。这需要导入以下 Python 库:
-
numpy -
MinMaxScaler()
如何操作…
本节将介绍为我们的模型准备股票市场数据的步骤。
- 执行以下脚本,通过
Adj Close计数对年份列进行分组:
df.groupBy(['year']).agg({'Adj Close':'count'})\
.withColumnRenamed('count(Adj Close)', 'Row Count')\
.orderBy(["year"],ascending=False)\
.show()
- 执行以下脚本以创建两个新的数据框,用于训练和测试:
trainDF = df[df.year < 2017]
testDF = df[df.year > 2016]
- 使用以下脚本将两个新的数据框转换为
pandas数据框,以便使用toPandas()获取行列计数:
trainDF.toPandas().shape
testDF.toPandas().shape
- 正如我们之前对
df做的那样,我们使用以下脚本对trainDF和testDF进行可视化:
trainDF_plot = trainDF.select('year', 'Adj Close').toPandas()
trainDF_plot.set_index('year', inplace=True)
trainDF_plot.plot(figsize=(16, 6), grid=True)
plt.title('Apple Stock 2000-2016')
plt.ylabel('Stock Quote ($)')
plt.show()
testDF_plot = testDF.select('year', 'Adj Close').toPandas()
testDF_plot.set_index('year', inplace=True)
testDF_plot.plot(figsize=(16, 6), grid=True)
plt.title('Apple Stock 2017-2018')
plt.ylabel('Stock Quote ($)')
plt.show()
- 我们根据数据框(日期列除外)创建两个新数组,
trainArray和testArray,使用以下脚本:
import numpy as np
trainArray = np.array(trainDF.select('Open', 'High', 'Low', 'Close','Volume', 'Adj Close' ).collect())
testArray = np.array(testDF.select('Open', 'High', 'Low', 'Close','Volume', 'Adj Close' ).collect())
- 为了将数组缩放到 0 和 1 之间,导入
sklearn中的MinMaxScaler,并使用以下脚本创建一个名为MinMaxScale的函数:
from sklearn.preprocessing import MinMaxScaler
minMaxScale = MinMaxScaler()
- 然后,将
MinMaxScaler应用于trainArray,并使用以下脚本创建两个经过缩放的新数组:
minMaxScale.fit(trainArray)
testingArray = minMaxScale.transform(testArray)
trainingArray = minMaxScale.transform(trainArray)
- 使用以下脚本将
testingArray和trainingArray都拆分为特征x和标签y:
xtrain = trainingArray[:, 0:-1]
xtest = testingArray[:, 0:-1]
ytrain = trainingArray[:, -1:]
ytest = testingArray[:, -1:]
- 执行以下脚本来检索所有四个数组形状的最终清单:
print('xtrain shape = {}'.format(xtrain.shape))
print('xtest shape = {}'.format(xtest.shape))
print('ytrain shape = {}'.format(ytrain.shape))
print('ytest shape = {}'.format(ytest.shape))
- 执行以下脚本绘制报价
open、high、low和close的训练数组:
plt.figure(figsize=(16,6))
plt.plot(xtrain[:,0],color='red', label='open')
plt.plot(xtrain[:,1],color='blue', label='high')
plt.plot(xtrain[:,2],color='green', label='low')
plt.plot(xtrain[:,3],color='purple', label='close')
plt.legend(loc = 'upper left')
plt.title('Open, High, Low, and Close by Day')
plt.xlabel('Days')
plt.ylabel('Scaled Quotes')
plt.show()
- 此外,我们使用以下脚本绘制
volume的训练数组:
plt.figure(figsize=(16,6))
plt.plot(xtrain[:,4],color='black', label='volume')
plt.legend(loc = 'upper right')
plt.title('Volume by Day')
plt.xlabel('Days')
plt.ylabel('Scaled Volume')
plt.show()
它是如何工作的...
本节解释了模型中所需的数据转换。
- 构建模型的第一步是将数据分割成训练数据集和测试数据集,以进行模型评估。我们的目标是使用 2000 年到 2016 年的所有股票报价来预测 2017-2018 年的股票趋势。我们从之前的部分得知,我们总共有 4,610 天的股票报价,但我们并不确切知道每年有多少数据。我们可以在数据框中使用
groupBy()函数来获取每年股票报价的唯一计数,详情请参见以下截图:
-
2016 年和 2017 年的合并数据大约占总数据的 7%,这个测试数据集的比例稍显较小。然而,出于此模型的目的,这应该足够。剩余的 93% 数据集将用于 2000 到 2016 年之间的训练。因此,通过过滤器创建了两个数据框,以确定是否包含或排除 2016 年之前或之后的行。
-
我们现在可以看到,测试数据集
testDF包含 333 行,而训练数据集trainDF包含 4,277 行。当两者合并时,我们得到了来自原始数据框df的总行数 4,610。最后,我们看到testDF仅由 2017 年和 2018 年的数据组成,其中 2017 年有 251 行,2018 年有 82 行,总共 333 行,详情请参见以下截图:
请注意,每当我们将 Spark 数据框转换为 pandas 数据框时,它可能并不总是适合大数据。虽然在我们使用相对较小的数据集时它可以正常工作,但转换为 pandas 数据框意味着所有数据都将加载到主驱动程序的内存中。一旦发生此转换,数据不会存储在 Spark 工作节点中,而是传输到主驱动节点。这并不是最优的,可能会导致内存溢出错误。如果你发现需要从 Spark 转换为 pandas 数据框来可视化数据,建议从 Spark 中抽取一个随机样本,或将 Spark 数据聚合成一个更易处理的数据集,然后在 pandas 中进行可视化。
- 一旦数据的子集通过
toPandas()转换,我们就可以使用matplotlib来可视化测试和训练数据框,利用pandas内建的图形功能。将数据框并排显示可以展示出,在未缩放的情况下,调整后的收盘价的 y 轴看起来相似。实际上,我们可以看到trainDF_plot接近于 0 开始,而testDF_plot则从接近 110 的位置开始,如下两张截图所示:
- 目前我们的股票值并不适合深度学习建模,因为没有用于归一化或标准化的基准。在使用神经网络时,最好将值保持在 0 和 1 之间,以匹配在用于激活的 sigmoid 或 step 函数中找到的结果。为了实现这一点,我们必须首先将
pyspark数据框trainDF和testDF转换为numpy数组,即trainArray和testArray。由于它们现在是数组而不是数据框,我们将不使用日期列,因为神经网络只对数值感兴趣。每个数组的第一个值可以在以下截图中看到:
-
缩放数组值到 0 和 1 之间有很多种方法。它涉及使用以下公式:
scaled array value = (array value - min array value) / (max array value - min array value)。幸运的是,我们无需手动进行这个计算。我们可以利用sklearn中的MinMaxScaler()函数来缩放这两个数组。 -
MinMaxScaler()函数会在训练数组trainArray上进行拟合,然后应用于创建两个新的数组,trainingArray和testingArray,它们的值会被缩放到 0 到 1 之间。每个数组的第一行可以在以下截图中看到:
- 现在,我们准备通过将数组切分成 x 和 y 来设置标签和特征变量,供训练和测试使用。数组中的前五个元素是特征或 x 值,最后一个元素是标签或 y 值。特征包括 Open、High、Low、Close 和 Volume 的值。标签由 Adj Close 组成。
trainingArray的第一行分解可以在以下截图中看到:
- 查看我们将在模型中使用的四个数组的形状,可以确认我们有 4,227 行训练数据、333 行测试数据、5 个特征元素(
x)和 1 个标签元素(y),如以下截图所示:
xtrain训练数组的值,包括开盘价、最低价、最高价和收盘价,可以使用新的调整后的 0 到 1 之间的比例绘制,如下图所示:
- 此外,体积也可以使用归一化后的体积得分(在 0 和 1 之间)进行绘制,如下图所示:
还有更多内容...
尽管我们确实使用了来自sklearn的MinMaxScaler,但同样重要的是要理解,pyspark.ml.feature中也有一个MinMaxScaler函数。它的功能完全相同,都是将每个特征重新缩放到 0 到 1 之间。如果我们在本章中通过 PySpark 原生的机器学习库进行预测,我们将使用来自pyspark.ml.feature的MinMaxScaler。
另见
要了解更多关于sklearn中MinMaxScaler的信息,请访问以下网站:
scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html.
要了解更多关于pyspark中MinMaxScaler的信息,请访问以下网站:
spark.apache.org/docs/2.2.0/ml-features.html#minmaxscaler.
构建 LSTM 模型
数据现在已转换为与 Keras 中的 LSTM 模型开发兼容的格式。因此,我们将在本节中设置并配置深度学习模型,以预测 2017 年和 2018 年苹果公司的股票报价。
准备工作
我们将在本节中对模型进行管理和超参数调优。这将需要在 Python 中导入以下库:
from keras import models
from keras import layers
如何操作...
本节将引导您完成设置和调优 LSTM 模型的步骤。
- 使用以下脚本从
keras导入以下库:
from keras import models, layers
- 使用以下脚本构建一个
Sequential模型:
model = models.Sequential()
model.add(layers.LSTM(1, input_shape=(1,5)))
model.add(layers.Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
- 使用以下脚本将测试集和训练集转换为三维数组:
xtrain = xtrain.reshape((xtrain.shape[0], 1, xtrain.shape[1]))
xtest = xtest.reshape((xtest.shape[0], 1, xtest.shape[1]))
- 使用一个名为
loss的变量通过以下脚本拟合model:
loss = model.fit(xtrain, ytrain, batch_size=10, epochs=100)
- 使用以下脚本创建一个新的数组
predicted:
predicted = model.predict(xtest)
- 使用以下脚本将
predicted和ytest数组合并为一个统一的数组combined_array:
combined_array = np.concatenate((ytest, predicted), axis = 1)
它是如何工作的...
本节解释了如何配置 LSTM 神经网络模型以在我们的数据集上进行训练。
-
构建 LSTM 模型时,大部分功能来自于
keras的models和layers模块。 -
已构建的
LSTM模型将使用Sequential类进行定义,这种类非常适合处理具有序列依赖的时间序列数据。LSTM 模型的input_shape = (1,5)表示我们的训练数据集中有一个因变量和五个自变量。由于我们希望保持模型简单,因此只会使用一个Dense层来定义神经网络。编译 keras 模型时需要指定损失函数,因为我们正在处理一个循环神经网络,使用mean_squared_error(均方误差)计算最能反映预测值与实际值之间的差距。最后,模型编译时还需要定义优化器,以调整神经网络中的权重。adam优化器给出了很好的结果,尤其是在与循环神经网络配合使用时。 -
我们当前的数组
xtrain和xtest是二维数组;然而,为了将它们整合进 LSTM 模型,它们需要使用reshape()转换为三维数组,具体如以下截图所示:
- LSTM 模型使用
xtrain和ytrain进行拟合,批量大小设定为 10,训练 100 个 epochs。批量大小是定义一起训练的对象数量的设置。我们可以根据需要设置较低或较高的批量大小,但需要注意的是,批量数越少,所需的内存越多。此外,epoch 是衡量模型通过整个数据集的次数。最终,这些参数可以根据时间和内存分配进行调整。
每个 epoch 中的均方误差损失都被捕获并可视化。经过第五或第六个 epoch 后,我们可以看到损失逐渐减小,如以下截图所示:
- 我们现在可以创建一个新的数组
predicted,它基于应用于xtest的拟合模型,然后将其与ytest合并,进行并排比较,以便进行准确性验证。
另请参见
要了解更多关于 keras 中参数调整模型的内容,请访问以下网站:keras.io/models/model/
评估模型
现在到了关键时刻:我们将看看我们的模型是否能够为 2017 年和 2018 年的 AAPL 股票提供准确的预测。
准备工作
我们将使用均方误差进行模型评估。因此,我们需要导入以下库:
import sklearn.metrics as metrics
如何操作...
本节演示了如何可视化并计算 2017 年和 2018 年苹果股票的实际与预测股价。
- 使用以下脚本绘制
实际与预测股票的并排比较图,以便比较趋势:
plt.figure(figsize=(16,6))
plt.plot(combined_array[:,0],color='red', label='actual')
plt.plot(combined_array[:,1],color='blue', label='predicted')
plt.legend(loc = 'lower right')
plt.title('2017 Actual vs. Predicted APPL Stock')
plt.xlabel('Days')
plt.ylabel('Scaled Quotes')
plt.show()
- 使用以下脚本计算实际
ytest与predicted(预测的)股票之间的均方误差:
import sklearn.metrics as metrics
np.sqrt(metrics.mean_squared_error(ytest,predicted))
它是如何工作的...
本节解释了 LSTM 模型评估的结果。
- 从图形的角度来看,我们可以看到我们的预测与 2017 年至 2018 年的实际股票报价非常接近,如下面的屏幕截图所示:
- 我们的模型显示,预测值与 2017 年和 2018 年较早的实际值更接近。总体而言,虽然我们预测的分数与实际分数非常接近,但最好还是进行均方误差计算,以了解两者之间的偏差。正如我们所看到的,我们的均方误差为 0.05841,或约为 5.8%:
另请参阅
为了更好地了解在 sklearn 中如何计算均方误差,请访问以下网站:
scikit-learn.org/stable/modules/generated/sklearn.metrics.mean_squared_error.html.