Spark 机器学习快速启动指南(二)
原文:
annas-archive.org/md5/38444d9e78402dd0977c5a58c40c8efd译者:飞龙
第六章:使用 Apache Spark 进行自然语言处理
在本章中,我们将研究和实现常用的自然语言处理(NLP)算法,这些算法可以帮助我们开发能够自动分析和理解人类文本和语音的机器。具体来说,我们将研究和实现以下几类与 NLP 相关的计算机科学算法:
-
特征转换器,包括以下内容:
-
分词
-
词干提取
-
词形还原
-
规范化
-
-
特征提取器,包括以下内容:
-
词袋模型
-
词频-逆文档频率
-
特征转换器
自然语言处理背后的基本概念是将人类文本和语音视为数据——就像我们在本书中迄今为止遇到的结构化和非结构化数值和分类数据源一样——同时保留其上下文。然而,自然语言是出了名的难以理解,即使是对于人类来说也是如此,更不用说机器了!自然语言不仅包括数百种不同的口语语言,具有不同的书写系统,而且还提出了其他挑战,如不同的语调、屈折、俚语、缩写、隐喻和讽刺。特别是书写系统和通信平台为我们提供了可能包含拼写错误、非传统语法和结构松散的句子的文本。
因此,我们的第一个挑战是将自然语言转换为机器可以使用的、同时保留其潜在上下文的数据。此外,当应用于机器学习时,我们还需要将自然语言转换为特征向量,以便训练机器学习模型。好吧,有两种广泛的计算机科学算法帮助我们应对这些挑战——特征提取器,它帮助我们从自然语言数据中提取相关特征,以及特征转换器,它帮助我们缩放、转换和/或修改这些特征,以便为后续建模做准备。在本小节中,我们将讨论特征转换器以及它们如何帮助我们将自然语言数据转换为更容易处理的结构。首先,让我们介绍一些 NLP 中的常见定义。
文档
在 NLP 中,文档代表文本的逻辑容器。容器本身可以是任何在您的用例上下文中有意义的东西。例如,一个文档可以指一篇单独的文章、记录、社交媒体帖子或推文。
语料库
一旦你定义了你的文档代表什么,语料库就被定义为一系列逻辑上的文档集合。使用之前的例子,语料库可以代表一系列文章(例如,一本杂志或博客)或一系列推文(例如,带有特定标签的推文)。
预处理管道
在自然语言处理中涉及的基本任务之一是尝试尽可能标准化来自不同来源的文档,以进行预处理。预处理不仅帮助我们标准化文本,通常还能减少原始文本的大小,从而降低后续过程和模型计算复杂度。以下小节描述了可能构成典型有序预处理管道的常见预处理技术。
分词
分词是指将文本分割成单个标记或术语的技术。正式来说,一个标记被定义为代表原始文本子集的字符序列。非正式来说,标记通常是组成原始文本的不同单词,并且这些单词是通过使用空白和其他标点符号进行分割的。例如,句子“使用 Apache Spark 的机器学习”可能产生一个以数组或列表形式持久化的标记集合,表示为["Machine", "Learning", "with", "Apache", "Spark"]。
停用词
停用词是在给定语言中常用的单词,用于结构化句子语法,但它们在确定其潜在意义或情感方面不一定有帮助。例如,在英语中,常见的停用词包括and、I、there、this和with。因此,一个常见的预处理技术是通过基于特定语言的停用词查找来过滤这些单词,从而从标记集合中移除它们。使用我们之前的例子,我们的过滤标记列表将是["Machine", "Learning", "Apache", "Spark"]。
词干提取
词干提取是指将单词还原到共同基础或词干的技术。例如,单词“connection”、“connections”、“connective”、“connected”和“connecting”都可以还原到它们的共同词干“connect”。词干提取不是一个完美的过程,词干提取算法可能会出错。然而,为了减少数据集的大小以训练机器学习模型,它是一种有价值的技巧。使用我们之前的例子,我们的过滤词干列表将是["Machin", "Learn", "Apach", "Spark"]。
词形还原
虽然词干提取可以快速将单词还原到基本形式,但它并没有考虑到上下文,因此不能区分在句子或上下文中位置不同而具有不同意义的单词。词形还原并不是简单地基于共同词干来还原单词,而是旨在仅移除屈折词尾,以便返回一个称为词元的单词的词典形式。例如,单词am、is、being和was可以被还原为词元be,而词干提取器则无法推断出这种上下文意义。
虽然词形还原可以在更大程度上保留上下文和意义,但它是以额外的计算复杂度和处理时间为代价的。因此,使用我们之前的例子,我们的过滤词元列表可能看起来像 ["Machine", "Learning", "Apache", "Spark"]。
正规化
最后,正规化指的是一系列常用的技术,用于标准化文本。典型的正规化技术包括将所有文本转换为小写,删除选定的字符、标点符号和其他字符序列(通常使用正则表达式),以及通过应用特定于语言的常用缩写和俚语词典来扩展缩写。
图 6.1 展示了一个典型的有序预处理管道,该管道用于标准化原始书面文本:
图 6.1:典型的预处理管道
特征提取器
我们已经看到特征转换器如何通过预处理管道将我们的文档进行转换、修改和标准化,从而将原始文本转换为一系列标记。特征提取器将这些标记提取出来,并生成特征向量,这些向量可以用于训练机器学习模型。在 NLP 中使用的典型特征提取器的两个常见例子是词袋模型和词频-逆文档频率(TF-IDF)算法。
词袋模型
词袋模型方法简单地计算每个独特单词在原始或标记文本中出现的次数。例如,给定文本 "Machine Learning with Apache Spark, Apache Spark's MLlib and Apache Kafka",词袋模型将为我们提供一个以下数值特征向量:
| Machine | Learning | with | Apache | Spark | MLlib | Kafka |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 3 | 2 | 1 | 1 |
注意,每个独特的单词都是一个特征或维度,而词袋模型方法是一种简单的技术,通常用作基准模型,以比较更高级特征提取器的性能。
词频-逆文档频率
TF-IDF 旨在通过提供每个词在整个语料库中出现的频率的重要性指标来改进词袋模型方法。
让我们用 TF(t, d) 来表示词频,即一个词 t 在文档 d 中出现的次数。我们还可以用 DF(t, D) 来表示文档频率,即包含该词 t 的文档数量,在我们的语料库 D 中。然后我们可以定义逆文档频率 IDF(t, D) 如下:
IDF 为我们提供了一个衡量一个词重要性的度量,考虑到该词在整个语料库中出现的频率,其中*|D|是我们语料库中文档的总数,D。在语料库中不那么常见的词具有更高的 IDF 度量。然而,请注意,由于使用了对数,如果一个词出现在所有文档中,其 IDF 变为 0——即log(1)*。因此,IDF 提供了一个度量标准,更重视描述文档中重要但罕见的词。
最后,为了计算 TF–IDF 度量,我们只需将词频乘以逆文档频率,如下所示:
这意味着 TF–IDF 度量与一个词在文档中出现的次数成比例增加,同时抵消了该词在整个语料库中的频率。这一点很重要,因为仅凭词频可能突出显示像“a”、“I”和“the”这样的词,这些词在特定文档中非常常见,但并不能帮助我们确定文本的潜在含义或情感。通过使用 TF–IDF,我们可以减少这些类型词语对我们分析的影响。
案例研究 – 情感分析
现在我们将这些特征转换器和特征提取器应用于一个非常现代的真实世界用例——情感分析。在情感分析中,目标是分类潜在的文本情感——例如,作者对文本主题是积极、中立还是消极。对许多组织来说,情感分析是一项重要的技术,用于更好地了解他们的客户和目标市场。例如,零售商可以使用情感分析来衡量公众对特定产品的反应,或政治家可以评估公众对政策或新闻条目的情绪。在我们的案例研究中,我们将研究关于航空公司的推文,以预测客户是否对他们表示正面或负面的评论。我们的分析然后可以被航空公司用来通过关注那些被分类为负面情感的推文来改善他们的客户服务。
我们用于案例研究的推文语料库已从Figure Eight下载,这是一家为商业提供高质量真实世界机器学习训练数据集的公司。Figure Eight 还提供了一个“数据人人共享”平台,包含可供公众下载的开放数据集,网址为www.figure-eight.com/data-for-everyone/。
如果你从本书附带的 GitHub 仓库或 Figure Eight 的 Data for Everyone 平台上的任何文本编辑器打开 twitter-data/airline-tweets-labelled-corpus.csv,你将找到一组 14,872 条关于主要航空公司的推文,这些推文是在 2015 年 2 月从 Twitter 上抓取的。这些推文也已经为我们预先标记,包括正面、负面或中性的情感分类。该数据集中的相关列在以下表中描述:
| 列名 | 数据类型 | 描述 |
|---|---|---|
unit_id | Long | 唯一标识符(主键) |
airline_sentiment | String | 情感分类——正面、中性或负面 |
airline | String | 航空公司名称 |
text | String | 推文的文本内容 |
我们的目标将是使用这个推文语料库来训练一个机器学习模型,以预测关于特定航空公司的未来推文对该航空公司的情感是正面还是负面。
NLP 流程
在我们查看案例研究的 Python 代码之前,让我们可视化我们将构建的端到端 NLP 流程。本案例研究的 NLP 流程如图 6.2 所示:
图 6.2:端到端 NLP 流程
Apache Spark 中的 NLP
截至 Spark 2.3.2 版本,标记化(tokenization)和停用词去除(stop-word removal)功能转换器(以及其他众多功能),以及 TF–IDF 特征提取器在 MLlib 中原生支持。尽管在 Spark 2.3.2 中可以通过对 Spark 数据框上的转换(通过用户定义函数(UDFs)和应用于 RDDs 的映射函数)间接实现词干提取(stemming)、词形还原(lemmatization)和标准化,但我们将使用一个名为 spark-nlp 的第三方 Spark 库来执行这些特征转换。这个第三方库已被设计用来通过提供一个易于使用的 API 来扩展 MLlib 中已有的功能,以便在 Spark 数据框上进行大规模的分布式 NLP 标注。要了解更多关于 spark-nlp 的信息,请访问 nlp.johnsnowlabs.com/。最后,我们将使用 MLlib 中已经原生支持的估计器和转换器——正如我们在前面的章节中所见——来训练我们的最终机器学习分类模型。
注意,通过使用 MLlib 内置的特征转换器和提取器,然后使用第三方 spark-nlp 库提供的特征转换器,最后应用本地的 MLlib 估算器,我们将在我们的流程中需要显式定义和开发数据转换阶段,以符合两个不同库期望的底层数据结构。虽然这由于其低效性不推荐用于生产级流程,但本节的一个目的就是演示如何使用这两个库进行 NLP。读者将能够根据所讨论用例的要求选择合适的库。
根据您的环境设置,有几种方法可以用来安装 spark-nlp,具体描述见nlp.johnsnowlabs.com/quickstart.html。然而,根据我们在第二章中配置的本地开发环境* 设置本地开发环境*,我们将使用 pip 来安装 spark-nlp,这是 Anaconda 分发版中捆绑的另一个常用 Python 包管理器(截至写作时,spark-nlp 通过 conda 仓库不可用,因此我们将使用 pip)。要为我们的 Python 环境安装 spark-nlp,只需执行以下命令,这将安装 spark-nlp 的 1.7.0 版本(这是截至写作时的最新版本,并且与 Spark 2.x 兼容):
> pip install spark-nlp==1.7.0
然后,我们需要告诉 Spark 它可以在哪里找到 spark-nlp 库。我们可以通过在 {SPARK_HOME}/conf/spark-defaults.conf 中定义一个额外的参数,或者在实例化 Spark 上下文时在我们的代码中设置 spark.jars 配置来实现,如下所示:
conf = SparkConf().set("spark.jars", '/opt/anaconda3/lib/python3.6/sitepackages/sparknlp/lib/sparknlp.jar')
.setAppName("Natural Language Processing - Sentiment Analysis")
sc = SparkContext(conf=conf)
请参阅第二章,设置本地开发环境,以获取有关定义 Apache Spark 配置的更多详细信息。请注意,在多节点 Spark 集群中,所有第三方 Python 包要么需要在所有 Spark 节点上安装,要么您的 Spark 应用程序本身需要打包成一个包含所有第三方依赖项的自包含文件。然后,这个自包含文件将被分发到 Spark 集群的所有节点上。
我们现在已准备好在 Apache Spark 中开发我们的 NLP 流程,以便对航空推文语料库进行情感分析!让我们按以下步骤进行:
以下小节描述了对应于本用例的 Jupyter 笔记本中相关的每个单元格,该笔记本称为 chp06-01-natural-language-processing.ipynb。它可以在本书附带的 GitHub 仓库中找到。
- 除了导入标准的 PySpark 依赖项之外,我们还需要导入相关的
spark-nlp依赖项,包括其Tokenizer、Stemmer和Normalizer类,如下所示:
import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
from pyspark.sql import SQLContext
from pyspark.sql.functions import *
from pyspark.sql.types import StructType, StructField
from pyspark.sql.types import LongType, DoubleType, IntegerType, StringType, BooleanType
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.feature import StringIndexer
from pyspark.ml.feature import Tokenizer
from pyspark.ml.feature import StopWordsRemover
from pyspark.ml.feature import HashingTF, IDF
from pyspark.ml import Pipeline, PipelineModel
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.mllib.evaluation import MulticlassMetrics
from sparknlp.base import *
from sparknlp.annotator import Tokenizer as NLPTokenizer
from sparknlp.annotator import Stemmer, Normalizer
- 接下来,我们像往常一样实例化一个
SparkContext。请注意,然而,在这种情况下,我们明确告诉 Spark 使用spark-jars配置参数在哪里找到spark-nlp库。然后,我们可以调用我们的SparkContext实例上的getConf()方法来查看当前的 Spark 配置,如下所示:
conf = SparkConf().set("spark.jars", '/opt/anaconda3/lib/python3.6/site-packages/sparknlp/lib/sparknlp.jar')
.setAppName("Natural Language Processing - Sentiment Analysis")
sc = SparkContext(conf=conf)
sqlContext = SQLContext(sc)
sc.getConf().getAll()
- 在将我们的航空公司推文语料库从
twitter-data/airline-tweets-labelled-corpus.csv加载到名为airline_tweets_df的 Spark 数据框之后,我们生成一个新的标签列。现有的数据集已经包含一个名为airline_sentiment的标签列,该列基于手动预分类,可以是"positive"、"neutral"或"negative"。尽管积极的信息自然是始终受欢迎的,但在现实中,最有用的信息通常是负面的。通过自动识别和研究负面信息,组织可以更有效地关注如何根据负面反馈改进他们的产品和服务。因此,我们将创建一个名为negative_sentiment_label的新标签列,如果基础情感被分类为"negative"则为"true",否则为"false",如下面的代码所示:
airline_tweets_with_labels_df = airline_tweets_df
.withColumn("negative_sentiment_label",
when(col("airline_sentiment") == "negative", lit("true"))
.otherwise(lit("false")))
.select("unit_id", "text", "negative_sentiment_label")
- 我们现在准备构建并应用我们的预处理管道到我们的原始推文语料库!在这里,我们展示了如何利用 Spark 的
MLlib自带的特征转换器,即其Tokenizer和StopWordsRemover转换器。首先,我们使用Tokenizer转换器对每条推文的原始文本内容进行分词,从而得到一个包含解析后的标记列表的新列。然后,我们将包含标记的该列传递给StopWordsRemover转换器,该转换器从列表中移除英语语言(默认)的停用词,从而得到一个包含过滤后标记列表的新列。在下一单元格中,我们将展示如何利用spark-nlp第三方库中可用的特征转换器。然而,spark-nlp需要一个string类型的列作为其初始输入,而不是标记列表。因此,最终的语句将过滤后的标记列表重新连接成一个空格分隔的string列,如下所示:
filtered_df = airline_tweets_with_labels_df
.filter("text is not null")
tokenizer = Tokenizer(inputCol="text", outputCol="tokens_1")
tokenized_df = tokenizer.transform(filtered_df)
remover = StopWordsRemover(inputCol="tokens_1",
outputCol="filtered_tokens")
preprocessed_part_1_df = remover.transform(tokenized_df)
preprocessed_part_1_df = preprocessed_part_1_df
.withColumn("concatenated_filtered_tokens",
concat_ws(" ", col("filtered_tokens")))
- 我们现在可以展示如何利用
spark-nlp第三方库中可用的功能转换器和标注器,即其DocumentAssember转换器和Tokenizer、Stemmer、Normalizer标注器。首先,我们从字符串列创建标注文档,这些文档作为spark-nlp管道的初始输入。然后,我们应用spark-nlp的Tokenizer和Stemmer标注器,将我们的过滤令牌列表转换为词根列表。最后,我们应用其Normalizer标注器,该标注器默认将词根转换为小写。所有这些阶段都在一个管道中定义,正如我们在第四章中看到的,使用 Apache Spark 进行监督学习,这是一个有序的机器学习和数据转换步骤列表,在 Spark 数据帧上执行。
我们在我们的数据集上执行我们的管道,得到一个新的数据帧preprocessed_df,我们只保留后续分析和建模所需的相关列,即unit_id(唯一记录标识符)、text(推文的原始原始文本内容)、negative_sentiment_label(我们新的标签)和normalised_stems(作为预处理管道结果的一个过滤、词根化和归一化的spark-nlp数组),如下面的代码所示:
document_assembler = DocumentAssembler()
.setInputCol("concatenated_filtered_tokens")
tokenizer = NLPTokenizer()
.setInputCols(["document"]).setOutputCol("tokens_2")
stemmer = Stemmer().setInputCols(["tokens_2"])
.setOutputCol("stems")
normalizer = Normalizer()
.setInputCols(["stems"]).setOutputCol("normalised_stems")
pipeline = Pipeline(stages=[document_assembler, tokenizer, stemmer,
normalizer])
pipeline_model = pipeline.fit(preprocessed_part_1_df)
preprocessed_df = pipeline_model.transform(preprocessed_part_1_df)
preprocessed_df.select("unit_id", "text",
"negative_sentiment_label", "normalised_stems")
- 在我们能够使用
MLlib的本地特征提取器从我们的词根令牌数组创建特征向量之前,还有一个最后的预处理步骤。包含我们的词根令牌的列,即normalised_stems,将这些令牌持久化在专门的spark-nlp数组结构中。我们需要将这个spark-nlp数组转换回标准的令牌列表,以便我们可以应用MLlib的本地 TF-IDF 算法。我们通过首先分解spark-nlp数组结构来实现这一点,这会产生一个新数据帧观察结果,对应于数组中的每个元素。然后,我们在unit_id上对 Spark 数据帧进行分组,这是每个唯一推文的键,在将词根使用空格分隔符聚合到一个新的字符串列tokens之前。最后,我们应用split函数到这个列上,将聚合的字符串转换为字符串列表或令牌,如下面的代码所示:
exploded_df = preprocessed_df
.withColumn("stems", explode("normalised_stems"))
.withColumn("stems", col("stems").getItem("result"))
.select("unit_id", "negative_sentiment_label", "text", "stems")
aggregated_df = exploded_df.groupBy("unit_id")
.agg(concat_ws(" ", collect_list(col("stems"))),
first("text"), first("negative_sentiment_label"))
.toDF("unit_id", "tokens", "text", "negative_sentiment_label")
.withColumn("tokens", split(col("tokens"), " ")
.cast("array<string>"))
- 我们现在准备好从我们的过滤、词干提取和归一化的标记列表中生成特征向量了!正如所讨论的,我们将使用 TF–IDF 特征提取器来生成特征向量,而不是基本的词袋方法。TF–IDF 特征提取器是
MLlib的本地功能,分为两部分。首先,我们通过将我们的标记列表传递到MLlib的HashingTF转换器中来生成词频(TF)特征向量。然后,我们将MLlib的逆文档频率(IDF)估计器拟合到包含词频特征向量的数据框中,如下面的代码所示。结果是包含在名为features的列中的新的 Spark 数据框,其中包含我们的 TF–IDF 特征向量:
hashingTF = HashingTF(inputCol="tokens", outputCol="raw_features",
numFeatures=280)
features_df = hashingTF.transform(aggregated_df)
idf = IDF(inputCol="raw_features", outputCol="features")
idf_model = idf.fit(features_df)
scaled_features_df = idf_model.transform(features_df)
- 正如我们在第四章,使用 Apache Spark 进行监督学习中所见,由于我们的标签列本质上是分类的,我们需要将其应用到
MLlib的StringIndexer中,以便识别和索引所有可能的分类。结果是包含索引标签列的新 Spark 数据框,名为"label",如果negative_sentiment_label为true,则其值为 0.0,如果negative_sentiment_label为false,则其值为 1.0,如下面的代码所示:
indexer = StringIndexer(inputCol = "negative_sentiment_label",
outputCol = "label").fit(scaled_features_df)
scaled_features_indexed_label_df = indexer.transform(scaled_features_df)
- 我们现在准备好创建训练和测试数据框,以便训练和评估后续的机器学习模型。我们像往常一样使用
randomSplit方法(如下面的代码所示)来实现这一点,但在这个案例中,90%的所有观察结果将进入我们的训练数据框,剩下的 10%将进入我们的测试数据框:
train_df, test_df = scaled_features_indexed_label_df
.randomSplit([0.9, 0.1], seed=12345)
- 在这个例子中,我们将训练一个监督决策树分类器(参见第四章,使用 Apache Spark 进行监督学习),以便帮助我们判断给定的推文是正面情绪还是负面情绪。正如第四章,使用 Apache Spark 进行监督学习中所述,我们将
MLlib的DecisionTreeClassifier估计器拟合到我们的训练数据框中,以训练我们的分类树,如下面的代码所示:
decision_tree = DecisionTreeClassifier(featuresCol = 'features',
labelCol = 'label')
decision_tree_model = decision_tree.fit(train_df)
- 现在我们已经训练好了一个分类树,我们可以将其应用到我们的测试数据框中,以便对测试推文进行分类。正如我们在第四章,使用 Apache Spark 进行监督学习中所做的那样,我们使用
transform()方法(如下面的代码所示)将我们的训练好的分类树应用到测试数据框中,然后研究其预测的分类:
test_decision_tree_predictions_df = decision_tree_model
.transform(test_df)
print("TEST DATASET PREDICTIONS AGAINST ACTUAL LABEL: ")
test_decision_tree_predictions_df.select("prediction", "label",
"text").show(10, False)
例如,我们的决策树分类器预测了以下来自我们的测试数据框的推文是负面情绪:
-
"我需要你...成为一个更好的航空公司。^LOL"
-
"如果不能保证家长会和孩子一起坐,就不要承诺卖票"
-
"解决了,我很烦等你们。我想退款,并想和某人谈谈这件事。"
-
"我本想回复你的网站,直到我看到那个真的很长的形式。在商业中,新座位很糟糕"
人类也可能将这些推文归类为负面情感!但更重要的是,航空公司可以使用这个模型以及它识别的推文来关注改进的领域。根据这个推文样本,这些领域可能包括网站可用性、票务营销以及处理退款所需的时间。
- 最后,为了量化我们训练的分类树的准确性,让我们使用以下代码在测试数据上计算其混淆矩阵:
predictions_and_label = test_decision_tree_predictions_df
.select("prediction", "label").rdd
metrics = MulticlassMetrics(predictions_and_label)
print("N = %g" % test_decision_tree_predictions_df.count())
print(metrics.confusionMatrix())
得到的混淆矩阵如下所示:
| **预测 y = 0 (负面) | 预测 y = 1 (非负) | |
|---|---|---|
| 实际 y = 0**(负面)** | 725 | 209 |
| 实际 y = 1**(非负)** | 244 | 325 |
我们可以这样解释这个混淆矩阵——在总共 1,503 条测试推文中,我们的模型表现出以下特性:
-
正确地将 725 条实际上是情感负面的推文分类为负面情感
-
正确地将 325 条实际上是情感非负的推文分类为非负情感
-
错误地将 209 条实际上是负情感的推文分类为非负情感
-
错误地将 244 条实际上是情感非负的推文分类为负面情感
-
总体准确率 = 70%
-
总体错误率 = 30%
-
灵敏度 = 57%
-
特异性 = 78%
因此,基于默认的阈值值 0.5(在这个案例研究中是合适的,因为我们没有对哪种错误更好有偏好),我们的决策树分类器有 70%的整体准确率,这相当不错!
- 为了完整性,让我们训练一个决策树分类器,但使用从词袋算法中导出的特征向量。请注意,当我们应用
HashingTF转换器到我们的预处理语料库以计算词频(TF)特征向量时,我们已经计算了这些特征向量。因此,我们只需重复我们的机器学习流程,但仅基于HashingTF转换器的输出,如下所示:
# Create Training and Test DataFrames based on the Bag of Words Feature Vectors
bow_indexer = StringIndexer(inputCol = "negative_sentiment_label",
outputCol = "label").fit(features_df)
bow_features_indexed_label_df = bow_indexer.transform(features_df)
.withColumnRenamed("raw_features", "features")
bow_train_df, bow_test_df = bow_features_indexed_label_df
.randomSplit([0.9, 0.1], seed=12345)
# Train a Decision Tree Classifier using the Bag of Words Feature Vectors
bow_decision_tree = DecisionTreeClassifier(featuresCol =
'features', labelCol = 'label')
bow_decision_tree_model = bow_decision_tree.fit(bow_train_df)
# Apply the Bag of Words Decision Tree Classifier to the Test DataFrame and generate the Confusion Matrix
bow_test_decision_tree_predictions_df = bow_decision_tree_model
.transform(bow_test_df)
bow_predictions_and_label = bow_test_decision_tree_predictions_df
.select("prediction", "label").rdd
bow_metrics = MulticlassMetrics(bow_predictions_and_label)
print("N = %g" % bow_test_decision_tree_predictions_df.count())
print(bow_metrics.confusionMatrix())
注意,得到的混淆矩阵与我们使用IDF估计器在缩放特征向量上训练的决策树分类器时得到的混淆矩阵完全相同。这是因为我们的推文语料库相对较小,有 14,872 个文档,因此基于语料库中词频的缩放词频(TF)特征向量将对这个特定模型的预测质量产生微乎其微的影响。
MLlib提供的一个非常有用的功能是将训练好的机器学习模型保存到磁盘上以供以后使用。我们可以通过将训练好的决策树分类器保存到单节点开发环境的本地磁盘上,来利用这个功能。在多节点集群中,训练好的模型也可以简单地通过使用相关的文件系统前缀(例如hdfs://<HDFS NameNode URL>/<HDFS Path>)保存到分布式文件系统,如 Apache Hadoop 分布式文件系统(参见第一章,大数据生态系统),如下面的代码所示:
bow_decision_tree_model.save('<Target filesystem path to save MLlib Model>')
我们用于对航空公司推文进行情感分析的训练好的决策树分类器也已推送到本书配套的 GitHub 仓库中,可以在 chapter06/models/airline-sentiment-analysis-decision-tree-classifier 中找到。
摘要
在本章中,我们研究了、实现了并评估了在自然语言处理中常用的算法。我们使用特征转换器对文档语料库进行了预处理,并使用特征提取器从处理后的语料库中生成了特征向量。我们还将这些常见的 NLP 算法应用于机器学习。我们训练并测试了一个情感分析模型,该模型用于预测推文的潜在情感,以便组织可以改进其产品和服务的提供。在第八章,使用 Apache Spark 的实时机器学习中,我们将扩展我们的情感分析模型,使其能够使用 Spark Streaming 和 Apache Kafka 在实时环境中运行。
在下一章中,我们将亲身体验探索令人兴奋且前沿的深度学习世界!
第七章:使用 Apache Spark 进行深度学习
在本章中,我们将亲身体验深度学习这个激动人心且前沿的世界!我们将结合使用第三方深度学习库和 Apache Spark 的MLlib来执行精确的光学字符识别(OCR),并通过以下类型的人工神经网络和机器学习算法自动识别和分类图像:
-
多层感知器
-
卷积神经网络
-
迁移学习
人工神经网络
如我们在第三章《人工智能与机器学习》中所研究的,人工神经网络(ANN)是一组连接的人工神经元,它们被聚合为三种类型的链接神经网络层——输入层、零个或多个隐藏层和输出层。单层ANN 仅由输入节点和输出节点之间的一个层链接组成,而多层ANN 的特点是人工神经元分布在多个链接层中。
信号仅沿一个方向传播的人工神经网络——也就是说,信号被输入层接收并转发到下一层进行处理——被称为前馈网络。信号可能被传播回已经处理过该信号的输入神经元或神经层的 ANN 被称为反馈网络。
反向传播是一种监督学习过程,通过这个过程多层 ANN 可以学习——也就是说,推导出一个最优的权重系数集。首先,所有权重都最初设置为随机,然后计算网络的输出。如果预测输出与期望输出不匹配,则输出节点的总误差会通过整个网络反向传播,以尝试重新调整网络中的所有权重,以便在输出层减少误差。换句话说,反向传播通过迭代权重调整过程来最小化实际输出和期望输出之间的差异。
多层感知器
单层感知器(SLP)是一种基本的人工神经网络(ANN),它仅由两层节点组成——一个包含输入节点的输入层和一个包含输出节点的输出层。然而,多层感知器(MLP)在输入层和输出层之间引入了一个或多个隐藏层,这使得它们能够学习非线性函数,如图 7.1 所示:
图 7.1:多层感知器神经网络架构
MLP 分类器
Apache Spark 的机器学习库MLlib提供了一个现成的多层感知器分类器(MLPC),可以应用于需要从k个可能的类别中进行预测的分类问题。
输入层
在MLlib的 MLPC 中,输入层的节点代表输入数据。让我们将这个输入数据表示为一个具有m个特征的向量,X,如下所示:
隐藏层
然后将输入数据传递到隐藏层。为了简化,让我们假设我们只有一个隐藏层h¹,并且在这个隐藏层中,我们有n个神经元,如下所示:
对于这些隐藏神经元中的每一个,激活函数的净输入z是输入数据集向量X乘以一个权重集向量W^n(对应于分配给隐藏层中n个神经元的权重集),其中每个权重集向量W^n 包含m个权重(对应于我们输入数据集向量X中的m个特征),如下所示:
在线性代数中,将一个向量乘以另一个向量的乘积称为点积,它输出一个由z表示的标量(即一个数字),如下所示:
偏差,如第三章《人工智能与机器学习》中所示,并在图 3.5中展示,是一个独立的常数,类似于回归模型中的截距项,并且可以添加到前馈神经网络的非输出层。它被称为独立,因为偏差节点没有连接到前面的层。通过引入一个常数,我们可以允许激活函数的输出向左或向右移动该常数,从而增加人工神经网络学习模式的有效性,提供基于数据移动决策边界的功能。
注意,在一个包含n个隐藏神经元的单隐藏层中,将计算n个点积运算,如图图 7.2所示:
图 7.2:隐藏层净输入和输出
在MLlib的 MLPC 中,隐藏神经元使用sigmoid激活函数,如下公式所示:
正如我们在第三章《人工智能与机器学习》中看到的,Sigmoid(或逻辑)函数在 0 和 1 之间有界,并且对所有实数输入值都有平滑的定义。通过使用 sigmoid 激活函数,隐藏层中的节点实际上对应于一个逻辑回归模型。如果我们研究 sigmoid 曲线,如图图 7.3所示,我们可以声明,如果净输入z是一个大的正数,那么 sigmoid 函数的输出,以及我们隐藏神经元的激活函数,将接近 1。相反,如果净输入 z 是一个具有大绝对值的负数,那么 sigmoid 函数的输出将接近 0:
图 7.3:sigmoid 函数
在所有情况下,每个隐藏神经元都会接收净输入,即z,它是输入数据X和权重集W^n的点积,再加上一个偏置,并将其应用于 sigmoid 函数,最终输出一个介于 0 和 1 之间的数字。在所有隐藏神经元计算了它们的激活函数的结果之后,我们就会从隐藏层h¹中得到n个隐藏输出,如下所示:
输出层
隐藏层的输出随后被用作输入,以计算输出层的最终输出。在我们的例子中,我们只有一个隐藏层,即h¹,其输出。这些输出随后成为输出层的n个输入。
输出层神经元的激活函数的净输入是隐藏层计算出的这些n个输入,乘以一个权重集向量,即W^h,其中每个权重集向量W^h包含n个权重(对应于n个隐藏层输入)。为了简化,让我们假设我们输出层只有一个输出神经元。因此,这个神经元的权重集向量如下所示:
再次强调,由于我们是在乘以向量,我们使用点积计算,这将计算以下表示我们的净输入z的标量:
在MLlib的 MLPC 中,输出神经元使用 softmax 函数作为激活函数,它通过预测k个类别而不是标准的二分类来扩展逻辑回归。此函数具有以下形式:
因此,输出层的节点数对应于你希望预测的可能类别数。例如,如果你的用例有五个可能的类别,那么你将训练一个输出层有五个节点的 MLP。因此,激活函数的最终输出是相关输出神经元所做的预测,如图7.4所示:
图 7.4:输出层净输入和输出
注意,图 7.4 说明了 MLP 的初始正向传播,其中输入数据传播到隐藏层,隐藏层的输出传播到输出层,在那里计算最终输出。MLlib 的 MLPC 随后使用反向传播来训练神经网络并学习模型,通过迭代权重调整过程最小化实际输出和期望输出之间的差异。MLPC 通过寻求最小化损失函数来实现这一点。损失函数计算了关于分类问题的不准确预测所付出的代价的度量。MLPC 使用的特定损失函数是逻辑损失函数,其中具有高置信度预测的惩罚较小。要了解更多关于损失函数的信息,请访问en.wikipedia.org/wiki/Loss_functions_for_classification。
案例研究 1 – OCR
证明 MLP 强大功能的一个很好的实际案例是 OCR。在 OCR 中,挑战是识别人类书写,将每个手写符号分类为字母。在英文字母的情况下,有 26 个字母。因此,当应用于英语时,OCR 实际上是一个具有k=26 个可能类别的分类问题!
我们将要使用的这个数据集是从加州大学(加州大学)的机器学习仓库中提取的,该仓库位于archive.ics.uci.edu/ml/index.php。我们将使用的特定字母识别数据集,可以从本书附带的 GitHub 仓库以及archive.ics.uci.edu/ml/datasets/letter+recognition获取,由 Odesta 公司的 David J. Slate 创建;地址为 1890 Maple Ave;Suite 115;Evanston, IL 60201,并在 P. W. Frey 和 D. J. Slate 合著的论文《使用荷兰风格自适应分类器的字母识别》(来自《机器学习》第 6 卷第 2 期,1991 年 3 月)中使用。
图 7.5 展示了该数据集的视觉示例。我们将训练一个 MLP 分类器来识别和分类每个符号,例如图 7.5中所示,将其识别为英文字母:
图 7.5:字母识别数据集
输入数据
在我们进一步探讨我们特定数据集的架构之前,让我们首先了解 MLP 如何真正帮助我们解决这个问题。首先,正如我们在第五章中看到的,“使用 Apache Spark 进行无监督学习”,在研究图像分割时,图像可以被分解为像素强度值(用于灰度图像)或像素 RGB 值(用于彩色图像)的矩阵。然后可以生成一个包含(m x n)数值元素的单一向量,对应于图像的像素高度(m)和宽度(n)。
训练架构
现在,想象一下,我们想要使用我们的整个字母识别数据集来训练一个 MLP,如图图 7.6所示:
图 7.6:用于字母识别的多层感知器
在我们的 MLP 中,输入层有p(= m x n)个神经元,它们代表图像中的p个像素强度值。一个单一的隐藏层有n个神经元,输出层有 26 个神经元,代表英语字母表中的 26 个可能的类别或字母。在训练这个神经网络时,由于我们最初不知道应该分配给每一层的权重,我们随机初始化权重并执行第一次前向传播。然后我们迭代地使用反向传播来训练神经网络,从而得到一组经过优化的权重,使得输出层做出的预测/分类尽可能准确。
隐藏层中的模式检测
隐藏层中神经元的任务是学习在输入数据中检测模式。在我们的例子中,隐藏层中的神经元将检测构成更广泛符号的某些子结构。这如图图 7.7所示,我们假设隐藏层中的前三个神经元分别学会了识别正斜杠、反斜杠和水平线类型的模式:
图 7.7:隐藏层中的神经元检测模式和子结构
输出层的分类
在我们的神经网络中,输出层中的第一个神经元被训练来决定给定的符号是否是大写英文字母A。假设隐藏层中的前三个神经元被激活,我们预计输出层中的第一个神经元将被激活,而剩下的 25 个神经元不会被激活。这样,我们的 MLP 就会将这个符号分类为字母A!
注意,我们的训练架构仅使用单个隐藏层,这只能学习非常简单的模式。通过添加更多隐藏层,人工神经网络可以学习更复杂的模式,但这会以计算复杂性、资源和训练运行时间的增加为代价。然而,随着分布式存储和处理技术的出现,正如在第一章“大数据生态系统”中讨论的,其中大量数据可以存储在内存中,并且可以在分布式方式下对数据进行大量计算,今天我们能够训练具有大量隐藏层和隐藏神经元的极其复杂的神经网络。这种复杂的神经网络目前正在应用于广泛的领域,包括人脸识别、语音识别、实时威胁检测、基于图像的搜索、欺诈检测和医疗保健的进步。
Apache Spark 中的 MLP
让我们回到我们的数据集,并在 Apache Spark 中训练一个 MLP 来识别和分类英文字母。如果你在任何文本编辑器中打开 ocr-data/letter-recognition.data,无论是来自本书配套的 GitHub 仓库还是来自 UCI 的机器学习仓库,你将找到 20,000 行数据,这些数据由以下模式描述:
| 列名 | 数据类型 | 描述 |
|---|---|---|
lettr | 字符串 | 英文字母(26 个值之一,从 A 到 Z) |
x-box | 整数 | 矩形水平位置 |
y-box | 整数 | 矩形垂直位置 |
width | 整数 | 矩形宽度 |
high | 整数 | 矩形高度 |
onpix | 整数 | 颜色像素总数 |
x-bar | 整数 | 矩形内颜色像素的 x 均值 |
y-bar | 整数 | 矩形内颜色像素的 y 均值 |
x2bar | 整数 | x 的方差平均值 |
y2bar | 整数 | y 的方差平均值 |
xybar | 整数 | x 和 y 的相关平均值 |
x2ybr | 整数 | x 和 y 的平均值 |
xy2br | 整数 | x 和 y 的平方平均值 |
x-ege | 整数 | 从左到右的平均边缘计数 |
xegvy | 整数 | x-ege 与 y 的相关性 |
y-ege | 整数 | 从下到上的平均边缘计数 |
yegvx | 整数 | y-ege 与 x 的相关性 |
此数据集描述了 16 个数值属性,这些属性基于扫描字符图像的像素分布的统计特征,如 图 7.5 中所示。这些属性已经标准化并线性缩放到 0 到 15 的整数范围内。对于每一行,一个名为 lettr 的标签列表示它所代表的英文字母,其中没有特征向量映射到多个类别——也就是说,每个特征向量只映射到英文字母表中的一个字母。
你会注意到我们没有使用原始图像本身的像素数据,而是使用从像素分布中得到的统计特征。然而,当我们从第五章,“使用 Apache Spark 进行无监督学习”中学习到的知识时,我们特别关注将图像转换为数值特征向量时,我们将在下一刻看到的相同步骤可以遵循来使用原始图像本身训练 MLP 分类器。
让我们现在使用这个数据集来训练一个 MLP 分类器以识别符号并将它们分类为英文字母表中的字母:
以下小节描述了对应于本用例的 Jupyter 笔记本中每个相关的单元格,该笔记本称为chp07-01-multilayer-perceptron-classifier.ipynb。这个笔记本可以在本书附带的 GitHub 仓库中找到。
- 首先,我们像往常一样导入必要的 PySpark 库,包括
MLlib的MultilayerPerceptronClassifier分类器和MulticlassClassificationEvaluator评估器,如下面的代码所示:
import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
from pyspark.sql import SQLContext
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import MultilayerPerceptronClassifier
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
- 在实例化 Spark 上下文之后,我们现在准备好将我们的数据集导入 Spark 数据框中。请注意,在我们的情况下,我们已经将数据集预处理为 CSV 格式,其中我们将
lettr列从string数据类型转换为表示英文字母表中 26 个字符之一的numeric数据类型。这个预处理好的 CSV 文件可以在本书附带的 GitHub 仓库中找到。一旦我们将这个 CSV 文件导入 Spark 数据框中,我们就使用VectorAssembler生成特征向量,包含 16 个特征列,就像通常那样。因此,生成的 Spark 数据框,称为vectorised_df,包含两个列——表示英文字母表中 26 个字符之一的数值label列,以及包含我们的特征向量的features列:
letter_recognition_df = sqlContext.read
.format('com.databricks.spark.csv')
.options(header = 'true', inferschema = 'true')
.load('letter-recognition.csv')
feature_columns = ['x-box','y-box','width','high','onpix','x-bar',
'y-bar','x2bar','y2bar','xybar','x2ybr','xy2br','x-ege','xegvy',
'y-ege','yegvx']
vector_assembler = VectorAssembler(inputCols = feature_columns,
outputCol = 'features')
vectorised_df = vector_assembler.transform(letter_recognition_df)
.withColumnRenamed('lettr', 'label').select('label', 'features')
- 接下来,我们使用以下代码以 75%到 25%的比例将我们的数据集分为训练集和测试集:
train_df, test_df = vectorised_df
.randomSplit([0.75, 0.25], seed=12345)
-
现在,我们已经准备好训练我们的 MLP 分类器。首先,我们必须定义我们神经网络各自层的尺寸。我们通过定义一个包含以下元素的 Python 列表来完成此操作:
-
第一个元素定义了输入层的尺寸。在我们的情况下,我们的数据集中有 16 个特征,因此我们将此元素设置为
16。 -
下一个元素定义了中间隐藏层的尺寸。我们将定义两个隐藏层,分别大小为
8和4。 -
最后一个元素定义了输出层的尺寸。在我们的情况下,我们有 26 个可能的类别,代表英文字母表中的 26 个字母,因此我们将此元素设置为
26:
-
layers = [16, 8, 4, 26]
- 现在我们已经定义了我们的神经网络架构,我们可以使用
MLlib的MultilayerPerceptronClassifier分类器来训练一个 MLP,并将其拟合到训练数据集上,如下面的代码所示。记住,MLlib的MultilayerPerceptronClassifier分类器为隐藏神经元使用 sigmoid 激活函数,为输出神经元使用 softmax 激活函数:
multilayer_perceptron_classifier = MultilayerPerceptronClassifier(
maxIter = 100, layers = layers, blockSize = 128, seed = 1234)
multilayer_perceptron_classifier_model =
multilayer_perceptron_classifier.fit(train_df)
- 我们现在可以将我们的训练好的 MLP 分类器应用到测试数据集上,以预测 16 个与像素相关的数值特征代表英语字母表中的 26 个字母中的哪一个,如下所示:
test_predictions_df = multilayer_perceptron_classifier_model
.transform(test_df)
print("TEST DATASET PREDICTIONS AGAINST ACTUAL LABEL: ")
test_predictions_df.select("label", "features", "probability",
"prediction").show()
TEST DATASET PREDICTIONS AGAINST ACTUAL LABEL:
+-----+--------------------+--------------------+----------+
|label| features| probability|prediction|
+-----+--------------------+--------------------+----------+
| 0|[1.0,0.0,2.0,0.0,...|[0.62605849526384...| 0.0|
| 0|[1.0,0.0,2.0,0.0,...|[0.62875656935176...| 0.0|
| 0|[1.0,0.0,2.0,0.0,...|[0.62875656935176...| 0.0|
+-----+--------------------+--------------------+----------+
- 接下来,我们使用以下代码计算我们训练好的 MLP 分类器在测试数据集上的准确性。在我们的案例中,它的表现非常糟糕,准确率仅为 34%。我们可以得出结论,在我们的数据集中,具有大小分别为 8 和 4 的两个隐藏层的 MLP 在识别和分类扫描图像中的字母方面表现非常糟糕:
prediction_and_labels = test_predictions_df
.select("prediction", "label")
accuracy_evaluator = MulticlassClassificationEvaluator(
metricName = "accuracy")
precision_evaluator = MulticlassClassificationEvaluator(
metricName = "weightedPrecision")
recall_evaluator = MulticlassClassificationEvaluator(
metricName = "weightedRecall")
print("Accuracy on Test Dataset = %g" % accuracy_evaluator
.evaluate(prediction_and_labels))
print("Precision on Test Dataset = %g" % precision_evaluator
.evaluate(prediction_and_labels))
print("Recall on Test Dataset = %g" % recall_evaluator
.evaluate(prediction_and_labels))
Accuracy on Test Dataset = 0.339641
Precision on Test Dataset = 0.313333
Recall on Test Dataset = 0.339641
- 我们如何提高我们神经网络分类器的准确性?为了回答这个问题,我们必须重新审视我们对隐藏层功能的定义。记住,隐藏层中神经元的任务是学习在输入数据中检测模式。因此,在我们的神经网络架构中定义更多的隐藏神经元应该会增加我们的神经网络检测更多模式并具有更高分辨率的能力。为了测试这个假设,我们将我们两个隐藏层中的神经元数量分别增加到 16 和 12,如下面的代码所示。然后,我们重新训练我们的 MLP 分类器并将其重新应用到测试数据集上。这导致了一个性能远更好的模型,准确率达到 72%:
new_layers = [16, 16, 12, 26]
new_multilayer_perceptron_classifier =
MultilayerPerceptronClassifier(maxIter = 400,
layers = new_layers, blockSize = 128, seed = 1234)
new_multilayer_perceptron_classifier_model =
new_multilayer_perceptron_classifier.fit(train_df)
new_test_predictions_df =
new_multilayer_perceptron_classifier_model.transform(test_df)
print("New Accuracy on Test Dataset = %g" % accuracy_evaluator
.evaluate(new_test_predictions_df
.select("prediction", "label")))
卷积神经网络
我们已经看到,MLPs 可以通过一个或多个中间隐藏层对单个输入向量进行转换来识别和分类小图像,如 OCR 中的字母和数字。然而,MLP 的一个局限性是它们在处理较大图像时的扩展能力,这不仅要考虑单个像素强度或 RGB 值,还要考虑图像本身的高度、宽度和深度。
卷积神经网络(CNNs)假设输入数据具有网格状拓扑结构,因此它们主要用于识别和分类图像中的对象,因为图像可以被表示为像素的网格。
端到端神经网络架构
卷积神经网络的端到端架构如图7.8所示:
图 7.8:卷积神经网络架构
在以下小节中,我们将描述构成卷积神经网络(CNN)的每一层和变换。
输入层
由于卷积神经网络(CNN)主要用于图像分类,因此输入 CNN 的数据是具有维度h(像素高度)、w(像素宽度)和d(深度)的图像矩阵。在 RGB 图像的情况下,深度将是三个相应的颜色通道,即红色、绿色和蓝色(RGB)。这如图 7.9 所示:
图 7.9:图像矩阵维度
卷积层
CNN 中接下来发生的转换是在卷积层中处理的。卷积层的目的是在图像中检测特征,这是通过使用滤波器(也称为核)实现的。想象一下拿一个放大镜观察一个图像,从图像的左上角开始。当我们从左到右和从上到下移动放大镜时,我们检测到放大镜移动过的每个位置的不同特征。在高层上,这就是卷积层的工作,其中放大镜代表滤波器或核,滤波器每次移动的步长大小,通常是像素级,被称为步长大小。卷积层的输出称为特征图。
让我们通过一个例子来更好地理解卷积层中进行的处理过程。想象一下,我们有一个 3 像素(高度)乘以 3 像素(宽度)的图像。为了简化,我们将在例子中忽略代表图像深度的第三维度,但请注意,现实世界的卷积对于 RGB 图像是在三个维度上计算的。接下来,想象一下我们的滤波器是一个 2 像素(高度)乘以 2 像素(宽度)的矩阵,并且我们的步长大小是 1 像素。
这些相应的矩阵在图 7.10中展示:
图 7.10:图像矩阵和滤波器矩阵
首先,我们将我们的滤波器矩阵放置在图像矩阵的左上角,并在该位置进行两个矩阵的矩阵乘法。然后,我们将滤波器矩阵向右移动我们的步长大小——1 个像素,并在该位置进行矩阵乘法。我们继续这个过程,直到滤波器矩阵穿越整个图像矩阵。结果的特征图矩阵在图 7.11中展示:
图 7.11:特征图
注意,特征图的维度比卷积层的输入矩阵小。为了确保输出维度与输入维度匹配,通过一个称为填充的过程添加了一个零值像素层。此外,滤波器必须具有与输入图像相同的通道数——因此,在 RGB 图像的情况下,滤波器也必须具有三个通道。
那么,卷积是如何帮助神经网络学习的呢?为了回答这个问题,我们必须回顾一下过滤器概念。过滤器本身是训练用来检测图像中特定模式的权重矩阵,不同的过滤器可以用来检测不同的模式,如边缘和其他特征。例如,如果我们使用一个预先训练用来检测简单边缘的过滤器,当我们把这个过滤器移动到图像上时,如果存在边缘,卷积计算将输出一个高值实数(作为矩阵乘法和求和的结果),如果不存在边缘,则输出一个低值实数。
当过滤器完成对整个图像的遍历后,输出是一个特征图矩阵,它表示该过滤器在图像所有部分的卷积。通过在每一层的不同卷积中使用不同的过滤器,我们得到不同的特征图,这些特征图构成了卷积层的输出。
矩形线性单元
与其他神经网络一样,激活函数定义了节点的输出,并用于使我们的神经网络能够学习非线性函数。请注意,我们的输入数据(构成图像的 RGB 像素)本身是非线性的,因此我们需要一个非线性激活函数。矩形线性单元(ReLU)在 CNN 中常用,其定义如下:
换句话说,ReLU 函数对其输入数据中的每个负值返回 0,对其输入数据中的每个正值返回其本身值。这如图 7.12 所示:
图 7.12:ReLU 函数
ReLU 函数可以绘制如图 7.13 所示:
图 7.13:ReLU 函数图
池化层
CNN 中接下来发生的变换在池化层中处理。池化层的目标是在保持原始输入数据的空间方差的同时,减少卷积层输出的特征图维度(但不是深度)。换句话说,通过减小数据的大小,可以减少计算复杂性、内存需求和训练时间,同时克服过拟合,以便在测试数据中检测到训练期间检测到的模式,即使它们的形状有所变化。给定一个特定的窗口大小,有各种池化算法可用,包括以下几种:
-
最大池化:取每个窗口中的最大值
-
平均池化:取每个窗口的平均值
-
求和池化:取每个窗口中值的总和
图 7.14显示了使用 2x2 窗口大小对 4x4 特征图执行最大池化的效果:
图 7.14:使用 2x2 窗口对 4x4 特征图进行最大池化
全连接层
在经过一系列卷积和池化层将 3-D 输入数据转换后,一个全连接层将最后一个卷积和池化层输出的特征图展平成一个长的 1-D 特征向量,然后将其用作一个常规 ANN 的输入数据,在这个 ANN 中,每一层的所有神经元都与前一层的所有神经元相连。
输出层
在这个人工神经网络(ANN)中,输出神经元使用诸如 softmax 函数(如 MLP 分类器中所示)这样的激活函数来分类输出,从而识别和分类输入图像数据中的对象!
案例研究 2 - 图像识别
在这个案例研究中,我们将使用一个预训练的 CNN 来识别和分类它以前从未遇到过的图像中的对象。
通过 TensorFlow 使用 InceptionV3
我们将使用的预训练 CNN 被称为 Inception-v3。这个深度 CNN 是在 ImageNet 图像数据库(一个包含大量标记图像的计算机视觉算法学术基准,覆盖了广泛的名词)上训练的,可以将整个图像分类为日常生活中发现的 1,000 个类别,例如“披萨”、“塑料袋”、“红葡萄酒”、“桌子”、“橙子”和“篮球”,仅举几例。
Inception-v3 深度 CNN 是由 TensorFlow (TM),一个最初在 Google 的 AI 组织内部开发的开源机器学习框架和软件库,用于高性能数值计算,开发和训练的。
要了解更多关于 TensorFlow、Inception-v3 和 ImageNet 的信息,请访问以下链接:
-
ImageNet:
www.image-net.org/ -
TensorFlow:
www.tensorflow.org/ -
Inception-v3:
www.tensorflow.org/tutorials/images/image_recognition
Apache Spark 的深度学习管道
在这个案例研究中,我们将通过一个名为 sparkdl 的第三方 Spark 包来访问 Inception-v3 TensorFlow 深度 CNN。这个 Spark 包是由 Apache Spark 的原始创建者成立的公司 Databricks 开发的,并为 Apache Spark 中的可扩展深度学习提供了高级 API。
要了解更多关于 Databricks 和 sparkdl 的信息,请访问以下链接:
-
Databricks:
databricks.com/
图像库
我们将用于测试预训练的 Inception-v3 深度卷积神经网络(CNN)的图像已从 Open Images v4 数据集中选取,这是一个包含超过 900 万张图片的集合,这些图片是在 Creative Common Attribution 许可下发布的,并且可以在 storage.googleapis.com/openimages/web/index.html 找到。
在本书配套的 GitHub 仓库中,您可以找到 30 张鸟类图像(image-recognition-data/birds)和 30 张飞机图像(image-recognition-data/planes)。图 7.15 显示了您可能在这些测试数据集中找到的一些图像示例:
图 7.15:Open Images v4 数据集的示例图像
在本案例研究中,我们的目标将是将预训练的 Inception-v3 深度 CNN 应用到这些测试图像上,并量化训练好的分类器模型在区分单个测试数据集中鸟类和飞机图像时的准确率。
PySpark 图像识别应用程序
注意,为了本案例研究的目的,我们不会使用 Jupyter notebook 进行开发,而是使用具有 .py 文件扩展名的标准 Python 代码文件。本案例研究提供了一个关于如何开发和执行生产级管道的初步了解;而不是在我们的代码中显式实例化 SparkContext,我们将通过 Linux 命令行将我们的代码及其所有依赖项提交给 spark-submit(包括任何第三方 Spark 包,如 sparkdl)。
现在,让我们看看如何通过 PySpark 使用 Inception-v3 深度 CNN 来对测试图像进行分类。在我们的基于 Python 的图像识别应用程序中,我们执行以下步骤(编号与 Python 代码文件中的编号注释相对应):
以下名为 chp07-02-convolutional-neural-network-transfer-learning.py 的 Python 代码文件,可以在本书配套的 GitHub 仓库中找到。
- 首先,使用以下代码,我们导入所需的 Python 依赖项,包括来自第三方
sparkdl包的相关模块和MLlib内置的LogisticRegression分类器:
from sparkdl import DeepImageFeaturizer
from pyspark.sql.functions import *
from pyspark.sql import SparkSession
from pyspark.ml.image import ImageSchema
from pyspark.ml import Pipeline
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
- 与我们的 Jupyter notebook 案例研究不同,我们没有必要实例化一个
SparkContext,因为当我们通过命令行执行 PySpark 应用程序时,这会为我们完成。在本案例研究中,我们将创建一个SparkSession,如下所示,它作为 Spark 执行环境(即使它已经在运行)的入口点,它包含 SQLContext。因此,我们可以使用SparkSession来执行与之前所见相同的类似 SQL 的操作,同时仍然使用 Spark Dataset/DataFrame API:
spark = SparkSession.builder.appName("Convolutional Neural Networks - Transfer Learning - Image Recognition").getOrCreate()
- 截至 2.3 版本,Spark 通过其
MLlibAPI 提供了对图像数据源的原生支持。在此步骤中,我们通过在MLlib的ImageSchema类上调用readImages方法,将我们的鸟类和飞机测试图像从本地文件系统加载到名为birds_df和planes_df的 Spark 数据帧中。然后,我们用0文字标签所有鸟类图像,用1文字标签所有飞机图像,如下所示:
path_to_img_directory = 'chapter07/data/image-recognition-data'
birds_df = ImageSchema.readImages(path_to_img_directory + "/birds")
.withColumn("label", lit(0))
planes_df = ImageSchema.readImages(path_to_img_directory +
"/planes").withColumn("label", lit(1))
- 现在我们已经将测试图像加载到分别以它们的标签区分的单独 Spark 数据框中,我们相应地将它们合并为单个训练和测试数据框。我们通过使用 Spark 数据框 API 的
unionAll方法来实现这一点,该方法简单地将一个数据框附加到另一个数据框上,如下面的代码所示:
planes_train_df, planes_test_df = planes_df
.randomSplit([0.75, 0.25], seed=12345)
birds_train_df, birds_test_df = birds_df
.randomSplit([0.75, 0.25], seed=12345)
train_df = planes_train_df.unionAll(birds_train_df)
test_df = planes_test_df.unionAll(birds_test_df)
- 与之前的案例研究一样,我们需要从我们的输入数据生成特征向量。然而,我们不会从头开始训练一个深度 CNN——即使有分布式技术,这也可能需要好几天——我们将利用预训练的 Inception-v3 深度 CNN。为此,我们将使用称为迁移学习的过程。在这个过程中,解决一个机器学习问题获得的知识被应用于不同但相关的问题。为了在我们的案例研究中使用迁移学习,我们采用第三方
sparkdlSpark 包的DeepImageFeaturizer模块。DeepImageFeaturizer不仅将我们的图像转换为数值特征,还通过剥离预训练神经网络的最后一层来执行快速迁移学习,然后使用所有先前层的输出作为标准分类算法的特征。在我们的案例中,DeepImageFeaturizer将剥离预训练的 Inception-v3 深度 CNN 的最后一层,如下所示:
featurizer = DeepImageFeaturizer(inputCol = "image",
outputCol = "features", modelName = "InceptionV3")
- 现在我们已经通过迁移学习从预训练的 Inception-v3 深度 CNN 的所有先前层中提取了特征,我们将它们输入到分类算法中。在我们的案例中,我们将使用
MLlib的LogisticRegression分类器,如下所示:
logistic_regression = LogisticRegression(maxIter = 20,
regParam = 0.05, elasticNetParam = 0.3, labelCol = "label")
- 要执行迁移学习和逻辑回归模型训练,我们构建一个标准的
pipeline并将该管道拟合到我们的训练数据框中,如下所示:
pipeline = Pipeline(stages = [featurizer, logistic_regression])
model = pipeline.fit(train_df)
- 现在我们已经训练了一个分类模型,使用由 Inception-v3 深度 CNN 推导出的特征,我们将我们的训练逻辑回归模型应用于测试数据框以进行正常预测,如下面的代码所示:
test_predictions_df = model.transform(test_df)
test_predictions_df.select("image.origin", "prediction")
.show(truncate=False)
- 最后,我们使用
MLlib的MulticlassClassificationEvaluator在测试数据框上量化我们模型的准确性,如下所示:
accuracy_evaluator = MulticlassClassificationEvaluator(
metricName = "accuracy")
print("Accuracy on Test Dataset = %g" % accuracy_evaluator
.evaluate(test_predictions_df.select("label", "prediction")))
Spark 提交
我们现在可以运行我们的图像识别应用程序了!由于它是一个 Spark 应用程序,我们可以在 Linux 命令行通过spark-submit来执行它。为此,导航到我们安装 Apache Spark 的目录(见第二章,设置本地开发环境)。然后,我们可以通过传递以下命令行参数来执行spark-submit程序:
-
--master: Spark Master 的 URL。 -
--packages: Spark 应用程序运行所需的第三方库和依赖项。在我们的案例中,我们的图像识别应用程序依赖于sparkdl第三方库的可用性。 -
--py-files:由于我们的图像识别应用程序是一个 PySpark 应用程序,我们将传递应用程序依赖的任何 Python 代码文件的文件系统路径。在我们的情况下,由于我们的图像识别应用程序包含在一个单独的代码文件中,因此没有其他依赖项需要传递给spark-submit。 -
最后一个参数是包含我们的 Spark 驱动程序的 Python 代码文件的路径,即
chp07-02-convolutional-neural-network-transfer-learning.py。
因此,执行的最后命令如下:
> cd {SPARK_HOME}
> bin/spark-submit --master spark://192.168.56.10:7077 --packages databricks:spark-deep-learning:1.2.0-spark2.3-s_2.11 chapter07/chp07-02-convolutional-neural-network-transfer-learning.py
图像识别结果
假设图像识别应用程序运行成功,你应该会在控制台看到以下结果输出:
| Origin | Prediction |
|---|---|
planes/plane-005.jpg | 1.0 |
planes/plane-008.jpg | 1.0 |
planes/plane-009.jpg | 1.0 |
planes/plane-016.jpg | 1.0 |
planes/plane-017.jpg | 0.0 |
planes/plane-018.jpg | 1.0 |
birds/bird-005.jpg | 0.0 |
birds/bird-008.jpg | 0.0 |
birds/bird-009.jpg | 0.0 |
birds/bird-016.jpg | 0.0 |
birds/bird-017.jpg | 0.0 |
birds/bird-018.jpg | 0.0 |
Origin列指的是图像的绝对文件系统路径,Prediction列中的值如果是我们的模型预测图像中的物体是飞机,则为1.0;如果是鸟,则为0.0。当在测试数据集上运行时,我们的模型具有惊人的 92%的准确率。我们的模型唯一的错误是在plane-017.jpg上,如图7.16所示,它被错误地分类为鸟,而实际上它是一架飞机:
图 7.16:plane-017.jpg 的错误分类
如果我们查看图 7.16中的plane-017.jpg,我们可以快速理解模型为什么会犯这个错误。尽管它是一架人造飞机,但它被物理建模成鸟的样子,以提高效率和空气动力学性能。
在这个案例研究中,我们使用预训练的 CNN 对图像进行特征提取。然后,我们将得到的特征传递给标准的逻辑回归算法,以预测给定图像是鸟还是飞机。
案例研究 3 – 图像预测
在案例研究 2(图像识别)中,我们在训练最终的逻辑回归分类器之前,仍然明确地为我们的测试图像进行了标注。在这个案例研究中,我们将简单地发送随机图像到预训练的 Inception-v3 深度 CNN,而不对其进行标注,并让 CNN 本身对图像中包含的物体进行分类。同样,我们将利用第三方sparkdl Spark 包来访问预训练的 Inception-v3 CNN。
我们将使用的随机图像再次从Open Images v4 数据集下载,可以在本书附带的 GitHub 仓库中的image-recognition-data/assorted找到。图 7.17显示了测试数据集中可能找到的一些典型图像:
图 7.17:随机图像组合
PySpark 图像预测应用程序
在我们的基于 Python 的图像预测应用程序中,我们按照以下步骤进行(编号与 Python 代码文件中的注释编号相对应):
以下名为chp07-03-convolutional-neural-network-image-predictor.py的 Python 代码文件,可以在本书附带的 GitHub 存储库中找到。
- 首先,我们像往常一样导入所需的 Python 依赖项,包括来自第三方
sparkdlSpark 包的DeepImagePredictor类,如下所示:
from sparkdl import DeepImagePredictor
from pyspark.sql import SparkSession
from pyspark.ml.image import ImageSchema
- 接下来,我们创建一个
SparkSession,它作为 Spark 执行环境的入口点,如下所示:
spark = SparkSession.builder.appName("Convolutional Neural Networks - Deep Image Predictor").getOrCreate()
- 然后,我们使用我们在上一个案例研究中首次遇到的
ImageSchema类的readImages方法将我们的随机图像组合加载到 Spark 数据框中,如下所示:
assorted_images_df = ImageSchema.readImages(
"chapter07/data/image-recognition-data/assorted")
- 最后,我们将包含我们的随机图像组合的 Spark 数据框传递给
sparkdl的DeepImagePredictor,它将应用指定的预训练神经网络来对图像中的对象进行分类。在我们的案例中,我们将使用预训练的 Inception-v3 深度 CNN。我们还告诉DeepImagePredictor按置信度降序返回每个图像的前 10 个(topK=10)预测分类,如下所示:
deep_image_predictor = DeepImagePredictor(inputCol = "image",
outputCol = "predicted_label", modelName = "InceptionV3",
decodePredictions = True, topK = 10)
predictions_df = deep_image_predictor.transform(assorted_images_df)
predictions_df.select("image.origin", "predicted_label")
.show(truncate = False)
要运行此 PySpark 图像预测应用程序,我们再次通过命令行调用spark-submit,如下所示:
> cd {SPARK_HOME}
> bin/spark-submit --master spark://192.168.56.10:7077 --packages databricks:spark-deep-learning:1.2.0-spark2.3-s_2.11 chapter07/chp07-03-convolutional-neural-network-image-predictor.py
图像预测结果
假设图像预测应用程序运行成功,您应该在控制台看到以下结果输出:
| 原始 | 首次预测标签 |
|---|---|
assorted/snowman.jpg | 泰迪熊 |
assorted/bicycle.jpg | 山地自行车 |
assorted/house.jpg | 图书馆 |
assorted/bus.jpg | 有轨电车 |
assorted/banana.jpg | 香蕉 |
assorted/pizza.jpg | 披萨 |
assorted/toilet.jpg | 马桶座圈 |
assorted/knife.jpg | 大刀 |
assorted/apple.jpg | 红富士(苹果) |
assorted/pen.jpg | 圆珠笔 |
assorted/lion.jpg | 狮子 |
assorted/saxophone.jpg | 萨克斯风 |
assorted/zebra.jpg | 斑马 |
assorted/fork.jpg | 勺子 |
assorted/car.jpg | 敞篷车 |
如您所见,预训练的 Inception-v3 深度 CNN 具有惊人的识别和分类图像中对象的能力。尽管本案例研究中提供的图像相对简单,但 Inception-v3 CNN 在 ImageNet 图像数据库上的前五错误率——即模型未能将其正确答案预测为其前五个猜测之一的情况——仅为 3.46%。请记住,Inception-v3 CNN 试图将整个图像分类到 1,000 个类别中,因此仅 3.46%的前五错误率确实令人印象深刻,并且清楚地展示了卷积神经网络以及一般的人工神经网络在检测和学习模式时的学习能力和力量!
摘要
在本章中,我们亲身体验了激动人心且前沿的深度学习世界。我们开发了能够以惊人的准确率识别和分类图像中的应用程序,并展示了人工神经网络在检测和学习输入数据中的模式方面的真正令人印象深刻的学习能力。
在下一章中,我们将扩展我们的机器学习模型部署,使其超越批量处理,以便从数据中学习并在实时中进行预测!
第八章:使用 Apache Spark 进行实时机器学习
在本章中,我们将扩展我们的机器学习模型部署,使其超越批量处理,以便从数据中学习、做出预测和实时识别趋势!我们将开发并部署一个由以下高级技术组成的实时流处理和机器学习应用程序:
-
Apache Kafka 生产者应用程序
-
Apache Kafka 消费者应用程序
-
Apache Spark 的 Structured Streaming 引擎
-
Apache Spark 的机器学习库,
MLlib
分布式流平台
到目前为止,在这本书中,我们一直在执行批量处理——也就是说,我们被提供了有界原始数据文件,并将这些数据作为一个组进行处理。正如我们在第一章,“大数据生态系统”中看到的,流处理与批量处理的不同之处在于数据是按需或单个数据单元(或流)到达时处理的。我们还在第一章,“大数据生态系统”中看到,Apache Kafka作为一个分布式*流平台,通过以下组件的逻辑流架构,以容错和可靠的方式在系统和应用程序之间移动实时数据:
-
生产者:生成并发送消息的应用程序
-
消费者:订阅并消费消息的应用程序
-
主题:属于特定类别并存储为有序且不可变记录序列的记录流,这些记录在分布式集群中分区和复制
-
流处理器:以特定方式处理消息的应用程序,例如数据转换和机器学习模型
该逻辑流架构的简化示意图如图8.1所示:
图 8.1:Apache Kafka 逻辑流架构
分布式流处理引擎
Apache Kafka 使我们能够在系统和应用程序之间可靠地移动实时数据。但是,我们仍然需要一个某种处理引擎来处理和转换这些实时数据,以便根据特定用例从中提取价值。幸运的是,有多个流处理引擎可供我们使用,包括但不限于以下:
-
Apache Spark:
spark.apache.org/ -
Apache Storm:
storm.apache.org/ -
Apache Flink:
flink.apache.org/ -
Apache Samza:
samza.apache.org/ -
Apache Kafka(通过其 Streams API):
kafka.apache.org/documentation/
尽管对可用的流处理引擎进行详细比较超出了本书的范围,但鼓励读者探索前面的链接并研究可用的不同架构。为了本章的目的,我们将使用 Apache Spark 的 Structured Streaming 引擎作为我们选择的流处理引擎。
使用 Apache Spark 进行流式传输
在撰写本文时,Spark 中提供了两个流处理 API:
-
Spark Streaming (DStreams):
spark.apache.org/docs/latest/streaming-programming-guide.html -
Structured Streaming:
spark.apache.org/docs/latest/structured-streaming-programming-guide.html
Spark Streaming (DStreams)
Spark Streaming (DStreams) 扩展了核心 Spark API,通过将实时数据流划分为 输入批次,然后由 Spark 的核心 API 处理,从而生成最终的 处理批次 流,如图 8.2 所示。一系列 RDD 构成了所谓的 离散流(或 DStream),它代表了数据的连续流:
图 8.2:Spark Streaming (DStreams)
Structured Streaming
另一方面,Structured Streaming 是一个基于 Spark SQL 引擎构建的较新的且高度优化的流处理引擎,其中可以使用 Spark 的 Dataset/DataFrame API 存储和处理流数据(参见第一章,大数据生态系统)。截至 Spark 2.3,Structured Streaming 提供了使用微批处理和连续处理两种方式处理数据流的能力,微批处理的延迟低至 100 毫秒,连续处理的延迟低至 1 毫秒(从而提供真正的实时处理)。Structured Streaming 通过将数据流建模为一个不断追加的无界表来工作。当对这个无界表执行转换或其他类型的查询时,将生成一个结果表,该表代表了那个时刻的数据。
在可配置的触发间隔之后,数据流中的新数据被建模为追加到这个无界表的新行,随后结果表被更新,如图 8.3 所示:
图 8.3:Spark Structured Streaming 逻辑模型
由于流数据通过 Dataset/DataFrame API 暴露,因此可以轻松地在实时数据流上执行 SQL-like 操作(包括聚合和连接)和 RDD 操作(包括 map 和过滤)。此外,结构化流提供了针对迟到数据的处理、流查询的管理和监控以及从故障中恢复的能力。因此,结构化流是一种极其灵活、高效且可靠的流数据处理方式,具有极低的延迟,是我们将在本章剩余部分使用的流处理引擎。
通常建议开发者使用这个较新且高度优化的引擎而不是 Spark Streaming(DStreams)。然而,由于这是一个较新的 API,截至 Spark 2.3.2,可能某些功能尚未提供,这意味着在开发新 API 的同时,DStreams RDD-based 方法仍会偶尔使用。
流处理管道
在本节中,我们将开发一个端到端流处理管道,它能够从生成连续数据的数据源系统中流式传输数据,然后能够将这些流发布到 Apache Kafka 分布式集群。我们的流处理管道将使用 Apache Spark 从 Apache Kafka 中消费数据,使用其结构化流引擎,并将训练好的机器学习模型应用于这些流,以使用MLlib实时提取洞察。我们将开发的端到端流处理管道如图 8.4 所示:
图 8.4:我们的端到端流处理管道
案例研究 – 实时情感分析
在本章的案例研究中,我们将扩展我们在第六章,“使用 Apache Spark 进行自然语言处理”中开发的情感分析模型,使其能够在实时环境中运行。在第六章“使用 Apache Spark 进行自然语言处理”中,我们训练了一个决策树分类器,根据关于航空公司的历史推文训练数据集来预测和分类推文的潜在情感。在本章中,我们将应用这个训练好的决策树分类器来处理实时推文,以便预测它们的情感并识别负面推文,以便航空公司能够尽快采取行动。
因此,我们的端到端流处理管道可以扩展,如图 8.5 所示:
图 8.5:我们的端到端流处理管道,用于实时情感分析
我们用于实时情感分析的流处理管道的核心阶段如下:
-
Kafka 生产者: 我们将开发一个 Python 应用程序,使用我们在第二章,“设置本地开发环境”中安装的
pykafka(一个 Python 的 Apache Kafka 客户端)和tweepy(一个用于访问 Twitter API 的 Python 库)库,以捕获实时发布的关于航空公司的推文,并将这些推文发布到名为twitter的 Apache Kafka 主题。 -
Kafka 消费者: 然后,我们将开发一个 Spark 应用程序,使用其 Structured Streaming API,订阅并从
twitter主题消费推文到 Spark 数据框。 -
流处理器和
MLlib: 然后,我们将使用我们在第六章,“使用 Apache Spark 进行自然语言处理”中研究和开发的相同管道中的特征转换器和特征提取器,对存储在此 Spark 数据框中的推文的原始文本内容进行预处理,这些特征转换器和特征提取器包括分词、去除停用词、词干提取和归一化——在应用 HashingTF 转换器生成实时特征向量之前。 -
训练好的决策树分类器: 接下来,我们将加载我们在第六章,“使用 Apache Spark 进行自然语言处理”中训练的决策树分类器,并将其持久化到我们的单个开发节点的本地文件系统。一旦加载,我们将应用这个训练好的决策树分类器到包含我们从实时推文中提取的预处理特征向量的 Spark 数据框,以预测和分类其潜在的情感。
-
输出目标: 最后,我们将把对实时推文应用的情感分析模型的结果输出到目标目的地,称为输出目标。在我们的案例中,输出目标将是控制台目标,这是 Structured Streaming API 原生提供的内置输出目标之一。通过使用此目标,每次触发时输出都会打印到控制台/标准输出(stdout)。从此控制台,我们将能够读取原始推文的原始文本内容和来自我们模型的预测情感分类,即负面或非负面。要了解更多关于可用的各种输出目标,请访问
spark.apache.org/docs/latest/structured-streaming-programming-guide.html#output-sinks。
以下小节将描述我们将遵循的技术步骤来开发、部署和运行我们的端到端流处理管道,以进行实时情感分析。
注意,对于本案例研究的目的,我们不会使用 Jupyter 笔记本进行开发。这是因为需要为单独的组件编写单独的代码文件,如前所述。因此,本案例研究提供了另一个了解如何开发和执行生产级管道的视角。我们不会在笔记本中显式实例化SparkContext,而是将通过 Linux 命令行将我们的 Python 代码文件及其所有依赖项提交给spark-submit。
启动 Zookeeper 和 Kafka 服务器
第一步是确保我们的单节点 Kafka 集群正在运行。如第二章中所述,“设置本地开发环境”,请执行以下命令以启动 Apache Kafka:
> cd {KAFKA_HOME}
> bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
> bin/kafka-server-start.sh -daemon config/server.properties
Kafka 主题
接下来,我们需要创建一个 Kafka 主题,我们的 Python Kafka 生产者应用程序(我们将在稍后开发)将发布有关航空公司的实时推文。在我们的案例中,我们将该主题称为twitter。如第二章中所示,“设置本地开发环境”,可以通过以下方式实现:
> bin/kafka-topics.sh --create --zookeeper 192.168.56.10:2181 --replication-factor 1 --partitions 1 --topic twitter
Twitter 开发者账户
为了让我们的 Python Kafka 生产者应用程序实时捕获推文,我们需要访问 Twitter API。截至 2018 年 7 月,除了普通 Twitter 账户外,还必须创建并批准一个 Twitter 开发者账户,才能访问其 API。为了申请开发者账户,请访问apps.twitter.com/,点击“申请开发者账户”按钮,并填写所需详细信息。
Twitter 应用程序和 Twitter API
一旦您创建了 Twitter 开发者账户,为了使用 Twitter API,必须创建一个 Twitter 应用程序。Twitter 应用程序根据您打算创建的应用程序的具体目的,提供认证和授权访问 Twitter API。为了创建用于我们实时情感分析模型的 Twitter 应用程序,请按照以下说明(截至撰写时有效)进行操作:
-
点击“创建应用程序”按钮。
-
提供以下必填的应用程序详细信息:
- App Name (max 32 characters) e.g. "Airline Sentiment Analysis"
- Application Description (max 200 characters) e.g. "This App will collect tweets about airlines and apply our previously trained decision tree classifier to predict and classify the underlying sentiment of those tweets in real-time"
- Website URL (for attribution purposes only - if you do not have a personal website, then use the URL to your Twitter page, such as https://twitter.com/PacktPub)
- Tell us how this app will be used (min 100 characters) e.g. "Internal training and development purposes only, including the deployment of machine learning models in real-time. It will not be visible to customers or 3rd parties."
-
点击“创建”按钮创建您的 Twitter 应用程序。
-
一旦您的 Twitter 应用程序创建完成,导航到“密钥和令牌”选项卡。
-
分别记下您的消费者 API 密钥和消费者 API 密钥字符串。
-
然后点击“访问令牌 & 访问令牌密钥”下的“创建”按钮,为您的 Twitter 应用程序生成访问令牌。将访问级别设置为只读,因为此 Twitter 应用程序将只读取推文,不会生成任何自己的内容。
-
记下生成的 访问令牌和访问令牌密钥字符串。
消费者 API 密钥和访问令牌将被用于授权我们的基于 Python 的 Kafka 生产者应用程序以只读方式访问通过 Twitter API 获取的实时推文流,因此您需要将它们记录下来。
应用程序配置
现在我们已经准备好开始开发我们的端到端流处理管道!首先,让我们创建一个 Python 配置文件,该文件将存储与我们的管道和本地开发节点相关的所有环境和应用程序级别的选项,如下所示:
以下 Python 配置文件,称为config.py,可以在伴随本书的 GitHub 存储库中找到。
#!/usr/bin/python
""" config.py: Environmental and Application Settings """
""" ENVIRONMENT SETTINGS """
# Apache Kafka
bootstrap_servers = '192.168.56.10:9092'
data_encoding = 'utf-8'
""" TWITTER APP SETTINGS """
consumer_api_key = 'Enter your Twitter App Consumer API Key here'
consumer_api_secret = 'Enter your Twitter App Consumer API Secret Key here'
access_token = 'Enter your Twitter App Access Token here'
access_token_secret = 'Enter your Twitter App Access Token Secret here'
""" SENTIMENT ANALYSIS MODEL SETTINGS """
# Name of an existing Kafka Topic to publish tweets to
twitter_kafka_topic_name = 'twitter'
# Keywords, Twitter Handle or Hashtag used to filter the Twitter Stream
twitter_stream_filter = '@British_Airways'
# Filesystem Path to the Trained Decision Tree Classifier
trained_classification_model_path = '..chapter06/models/airline-sentiment-analysis-decision-tree-classifier'
此 Python 配置文件定义了以下相关选项:
-
bootstrap_servers:Kafka 代理的主机名/IP 地址和端口号配对的逗号分隔列表。在我们的案例中,这是默认情况下位于端口9092的单节点开发环境的主机名/IP 地址。 -
consumer_api_key:在此处输入与您的 Twitter 应用程序关联的消费者 API 密钥。 -
consumer_api_secret:在此处输入与您的 Twitter 应用程序关联的消费者 API 密钥。 -
access_token:在此处输入与您的 Twitter 应用程序关联的访问令牌。 -
access_token_secret:在此处输入与您的 Twitter 应用程序关联的访问令牌密钥。 -
twitter_kafka_topic_name:我们的 Kafka 生产者将发布的 Kafka 主题名称,以及我们的结构化流 Spark 应用程序将从中消费推文的主题。 -
twitter_stream_filter:一个关键字、Twitter 用户名或标签,用于过滤从 Twitter API 捕获的实时推文流。在我们的案例中,我们正在过滤针对@British_Airways的实时推文。 -
trained_classification_model_path:我们保存我们的训练决策树分类器(在第六章中介绍,使用 Apache Spark 进行自然语言处理)的绝对路径。
Kafka Twitter 生产者应用程序
现在我们已经准备好开发我们的基于 Python 的 Kafka 生产者应用程序,该程序将捕获有关航空公司实时推文的推文,并将这些推文发布到我们之前创建的 Apache Kafka twitter 主题。在开发我们的 Kafka 生产者时,我们将使用以下两个 Python 库:
-
tweepy:这个库允许我们使用 Python 和之前生成的消费者 API 密钥和访问令牌以编程方式访问 Twitter API。 -
pykafka:这个库允许我们实例化一个基于 Python 的 Apache Kafka 客户端,通过它可以与我们的单节点 Kafka 集群进行通信和交易。
以下 Python 代码文件,称为kafka_twitter_producer.py,可以在伴随本书的 GitHub 存储库中找到。
关于我们的基于 Python 的 Kafka 生产者应用程序,我们执行以下步骤(编号与 Python 代码文件中的编号注释相对应):
- 首先,我们分别从
tweepy和pykafka库中导入所需的模块,如下面的代码所示。我们还导入了我们之前创建的config.py文件中的配置:
import config
import tweepy
from tweepy import OAuthHandler
from tweepy import Stream
from tweepy.streaming import StreamListener
import pykafka
- 接下来,我们使用
config.py中定义的消费者 API 密钥和访问令牌实例化一个tweepyTwitter API 包装器,如下所示,以提供我们认证和授权的程序访问 Twitter API:
auth = OAuthHandler(config.consumer_api_key,
config.consumer_api_secret)
auth.set_access_token(config.access_token,
config.access_token_secret)
api = tweepy.API(auth)
- 然后,我们在 Python 中定义了一个名为
KafkaTwitterProducer的类,一旦实例化,它就为我们提供了一个pykafka客户端到我们的单节点 Apache Kafka 集群,如下面的代码所示。当这个类被实例化时,它最初执行__init__函数中定义的代码,使用引导服务器创建一个pykafka客户端,这些服务器的位置可以在config.py中找到。然后它创建了一个与config.py中定义的twitter_kafka_topic_nameKafka 主题关联的生产者。当我们的pykafka生产者捕获数据时,会调用on_data函数,该函数将数据物理发布到 Kafka 主题。
如果我们的 pykafka 生产者遇到错误,则调用 on_error 函数,在我们的情况下,它只是将错误打印到控制台并继续处理下一个消息:
class KafkaTwitterProducer(StreamListener):
def __init__(self):
self.client = pykafka.KafkaClient(config.bootstrap_servers)
self.producer = self.client.topics[bytes(
config.twitter_kafka_topic_name,
config.data_encoding)].get_producer()
def on_data(self, data):
self.producer.produce(bytes(data, config.data_encoding))
return True
def on_error(self, status):
print(status)
return True
- 接下来,我们使用
tweepy库的Stream模块实例化一个 Twitter 流。为此,我们只需将我们的 Twitter 应用程序认证详情和KafkaTwitterProducer类的实例传递给Stream模块:
print("Instantiating a Twitter Stream and publishing to the '%s'
Kafka Topic..." % config.twitter_kafka_topic_name)
twitter_stream = Stream(auth, KafkaTwitterProducer())
- 现在我们已经实例化了一个 Twitter 流,最后一步是根据
config.py中的twitter_stream_filter选项过滤流,以传递感兴趣的推文,如下面的代码所示:
print("Filtering the Twitter Stream based on the query '%s'..." %
config.twitter_stream_filter)
twitter_stream.filter(track=[config.twitter_stream_filter])
我们现在可以运行我们的 Kafka 生产者应用程序了!由于它是一个 Python 应用程序,运行它的最简单方法就是使用 Linux 命令行,导航到包含 kafka_twitter_producer.py 的目录,并按以下方式执行:
> python kafka_twitter_producer.py
$ Instantiating a Twitter Stream and publishing to the 'twitter'
Kafka Topic...
$ Filtering the Twitter Stream based on the query
'@British_Airways'...
为了验证它实际上正在捕获并将实时推文发布到 Kafka,如第二章所述,设置本地开发环境,你可以启动一个命令行消费者应用程序来从 Twitter 主题中消费消息并将它们打印到控制台,如下所示:
> cd {KAFKA_HOME}
> bin/kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --topic twitter
希望你能看到实时打印到控制台上的推文。在我们的例子中,这些推文都是指向 "@British_Airways" 的。
推文本身是通过 Twitter API 以 JSON 格式捕获的,不仅包含推文的原始文本内容,还包含相关的元数据,如推文 ID、推文者的用户名、时间戳等。有关 JSON 模式的完整描述,请访问 developer.twitter.com/en/docs/tweets/data-dictionary/overview/tweet-object.html。
预处理和特征向量化流程
如前所述,为了能够将我们训练好的决策树分类器应用于这些实时推文,我们首先需要像在第六章《使用 Apache Spark 的自然语言处理》中处理我们的训练和测试数据集那样对它们进行预处理和向量化。然而,我们不会在 Kafka 消费者应用程序本身中重复预处理和向量化流程的逻辑,而是将在一个独立的 Python 模块和 Python 函数中定义我们的流程逻辑。这样,每次我们需要像在第六章《使用 Apache Spark 的自然语言处理》中那样预处理文本时,我们只需调用相关的 Python 函数,从而避免在不同 Python 代码文件中重复相同的代码。
以下名为model_pipelines.py的 Python 代码文件,可以在本书配套的 GitHub 仓库中找到。
在以下 Python 模块中,我们定义了两个函数。第一个函数应用了我们在第六章《使用 Apache Spark 的自然语言处理》中学习的MLlib和spark-nlp特征转换器的相同流程,以预处理推文的原始文本内容。第二个函数随后对预处理后的 Spark 数据框应用HashingTF转换器,以根据词频生成特征向量,正如我们在第六章《使用 Apache Spark 的自然语言处理》中所学习的那样。结果是包含原始推文文本的 Spark 数据框,该文本位于名为text的列中,以及位于名为features的列中的词频特征向量:
#!/usr/bin/python
""" model_pipelines.py: Pre-Processing and Feature Vectorization Spark Pipeline function definitions """
from pyspark.sql.functions import *
from pyspark.ml.feature import Tokenizer
from pyspark.ml.feature import StopWordsRemover
from pyspark.ml.feature import HashingTF
from pyspark.ml import Pipeline, PipelineModel
from sparknlp.base import *
from sparknlp.annotator import Tokenizer as NLPTokenizer
from sparknlp.annotator import Stemmer, Normalizer
def preprocessing_pipeline(raw_corpus_df):
# Native MLlib Feature Transformers
filtered_df = raw_corpus_df.filter("text is not null")
tokenizer = Tokenizer(inputCol = "text", outputCol = "tokens_1")
tokenized_df = tokenizer.transform(filtered_df)
remover = StopWordsRemover(inputCol = "tokens_1",
outputCol = "filtered_tokens")
preprocessed_part_1_df = remover.transform(tokenized_df)
preprocessed_part_1_df = preprocessed_part_1_df
.withColumn("concatenated_filtered_tokens", concat_ws(" ",
col("filtered_tokens")))
# spark-nlp Feature Transformers
document_assembler = DocumentAssembler()
.setInputCol("concatenated_filtered_tokens")
tokenizer = NLPTokenizer()
.setInputCols(["document"]).setOutputCol("tokens_2")
stemmer =
Stemmer().setInputCols(["tokens_2"]).setOutputCol("stems")
normalizer = Normalizer().setInputCols(["stems"])
.setOutputCol("normalised_stems")
preprocessing_pipeline = Pipeline(stages = [document_assembler,
tokenizer, stemmer, normalizer])
preprocessing_pipeline_model = preprocessing_pipeline
.fit(preprocessed_part_1_df)
preprocessed_df = preprocessing_pipeline_model
.transform(preprocessed_part_1_df)
preprocessed_df.select("id", "text", "normalised_stems")
# Explode and Aggregate
exploded_df = preprocessed_df
.withColumn("stems", explode("normalised_stems"))
.withColumn("stems", col("stems").getItem("result"))
.select("id", "text", "stems")
aggregated_df = exploded_df.groupBy("id")
.agg(concat_ws(" ", collect_list(col("stems"))), first("text"))
.toDF("id", "tokens", "text")
.withColumn("tokens", split(col("tokens"), " ")
.cast("array<string>"))
# Return the final processed DataFrame
return aggregated_df
def vectorizer_pipeline(preprocessed_df):
hashingTF = HashingTF(inputCol = "tokens", outputCol = "features",
numFeatures = 280)
features_df = hashingTF.transform(preprocessed_df)
# Return the final vectorized DataFrame
return features_df
Kafka Twitter 消费者应用程序
我们最终准备好使用 Spark Structured Streaming 引擎开发我们的 Kafka 消费者应用程序,以便将我们的训练好的决策树分类器应用于实时推文流,以提供实时情感分析!
以下名为kafka_twitter_consumer.py的 Python 代码文件,可以在本书配套的 GitHub 仓库中找到。
关于我们的基于 Spark Structured-Streaming 的 Kafka 消费者应用程序,我们执行以下步骤(编号与 Python 代码文件中的注释编号相对应):
- 首先,我们从
config.py文件中导入配置。我们还导入了我们之前创建的包含预处理和向量化流程逻辑的 Python 函数,如下所示:
import config
import model_pipelines
- 与我们的 Jupyter 笔记本案例研究不同,没有必要显式实例化一个
SparkContext,因为这将在我们通过命令行中的spark-submit执行 Kafka 消费者应用程序时为我们完成。在本案例研究中,我们创建了一个SparkSession,如下面的代码所示,它作为 Spark 执行环境的入口点——即使它已经在运行——并且它包含了SQLContext。因此,我们可以使用SparkSession来执行与之前看到的相同类型的 SQL 操作,同时仍然使用 Spark Dataset/DataFrame API:
spark = SparkSession.builder.appName("Stream Processing - Real-Time Sentiment Analysis").getOrCreate()
- 在这一步,我们将我们在第六章自然语言处理使用 Apache Spark 中训练的决策树分类器(使用了 HashingTF 特征提取器)从本地文件系统加载到一个
DecisionTreeClassificationModel对象中,以便我们可以在以后应用它,如下面的代码所示。注意,训练好的决策树分类器的绝对路径已在config.py中定义:
decision_tree_model = DecisionTreeClassificationModel.load(
config.trained_classification_model_path)
- 我们几乎准备好开始从我们的单节点 Kafka 集群中消费消息了。然而,在这样做之前,我们必须注意 Spark 还不支持自动推断和解析 JSON 键值到 Spark 数据框列。因此,我们必须明确定义 JSON 架构,或者我们希望保留的 JSON 架构的子集,如下所示:
schema = StructType([
StructField("created_at", StringType()),
StructField("id", StringType()),
StructField("id_str", StringType()),
StructField("text", StringType()),
StructField("retweet_count", StringType()),
StructField("favorite_count", StringType()),
StructField("favorited", StringType()),
StructField("retweeted", StringType()),
StructField("lang", StringType()),
StructField("location", StringType())
])
- 现在我们已经定义了我们的 JSON 架构,我们准备开始消费消息。为此,我们在我们的
SparkSession实例上调用readStream方法来消费流数据。我们指定流数据的来源将是一个 Kafka 集群,使用format方法,之后我们定义 Kafka 启动服务器和我们要订阅的 Kafka 主题的名称,这两个都在config.py中定义过。最后,我们调用load方法将twitter主题中消费的最新消息流式传输到一个无界的 Spark 数据框tweets_df,如下面的代码所示:
tweets_df = spark.readStream.format("kafka")
.option("kafka.bootstrap.servers", config.bootstrap_servers)
.option("subscribe", config.twitter_kafka_topic_name).load()
- 存储在 Kafka 主题中的记录以二进制格式持久化。为了处理代表我们的推文的 JSON,这些推文存储在名为
value的 Kafka 记录字段下,我们必须首先将value的内容CAST为字符串。然后我们将定义的架构应用于这个 JSON 字符串并提取感兴趣的字段,如下面的代码所示。在我们的例子中,我们只对存储在名为id的 JSON 键中的推文 ID 和存储在名为text的 JSON 键中的原始文本内容感兴趣。因此,生成的 Spark 数据框将有两个字符串列,id和text,包含这些感兴趣的相应字段:
tweets_df = tweets_df.selectExpr(
"CAST(key AS STRING)", "CAST(value AS STRING) as json")
.withColumn("tweet", from_json(col("json"), schema=schema))
.selectExpr("tweet.id_str as id", "tweet.text as text")
- 现在我们已经从我们的 Kafka 主题中消费了原始推文并将它们解析为 Spark 数据框,我们可以像在第六章中做的那样应用我们的预处理管道,使用 Apache Spark 进行自然语言处理。然而,我们不是将第六章中使用 Apache Spark 进行自然语言处理的相同代码复制到我们的 Kafka 消费者应用程序中,而是简单地调用我们在
model_pipelines.py中定义的相关函数,即preprocessing_pipeline(),如下面的代码所示。这个预处理管道将原始文本分词,去除停用词,应用词干提取算法,并规范化生成的标记:
preprocessed_df = model_pipelines.preprocessing_pipeline(tweets_df)
- 接下来,我们生成这些标记的特征向量,就像在第六章中使用 Apache Spark 进行自然语言处理所做的那样。我们调用
model_pipelines.py中的vectorizer_pipeline()函数来根据词频生成特征向量,如下面的代码所示。生成的 Spark 数据框,称为features_df,包含三个相关列,即id(原始推文 ID)、text(原始推文文本)和features(词频特征向量):
features_df = model_pipelines.vectorizer_pipeline(preprocessed_df)
- 现在我们已经从我们的推文流中生成了特征向量,我们可以将我们的训练好的决策树分类器应用到这个流中,以便预测和分类其潜在的 sentiment。我们像平常一样这样做,通过在
features_df数据框上调用transform方法,结果生成一个新的 Spark 数据框,称为predictions_df,包含与之前相同的id和text列,以及一个新的名为prediction的列,其中包含我们的预测分类,如下面的代码所示。正如在第六章中使用 Apache Spark 进行自然语言处理所描述的,预测值为1表示非负 sentiment,预测值为0表示负 sentiment:
predictions_df = decision_tree_model.transform(features_df)
.select("id", "text", "prediction")
- 最后,我们将我们的预测结果数据框写入输出接收器。在我们的例子中,我们将输出接收器定义为简单地是用于执行 Kafka 消费者 PySpark 应用程序的控制台——即执行
spark-submit的控制台。我们通过在相关的 Spark 数据框上调用writeStream方法并指定console作为选择的format来实现这一点。我们通过调用start方法启动输出流,并调用awaitTermination方法,这告诉 Spark 无限期地继续处理我们的流处理管道,直到它被明确中断并停止,如下所示:
query = predictions_df.writeStream
.outputMode("complete")
.format("console")
.option("truncate", "false")
.start()
query.awaitTermination()
注意,outputMode方法定义了写入输出接收器的内容,可以取以下选项之一:
-
complete:将整个(更新后的)结果表写入输出接收器 -
append:仅写入自上次触发以来添加到结果表的新行到输出接收器 -
update: 只有自上次触发以来在结果表中更新的行会被写入输出接收器
我们现在可以运行我们的 Kafka 消费者应用程序了!由于它是一个 Spark 应用程序,我们可以在 Linux 命令行上通过 spark-submit 来执行它。为此,导航到我们安装 Apache Spark 的目录(见第二章,设置本地开发环境)。然后我们可以通过传递以下命令行参数来执行 spark-submit 程序:
-
--master: Spark 主机 URL。 -
--packages: 给定 Spark 应用程序运行所需的第三方库和依赖项。在我们的例子中,我们的 Kafka 消费者应用程序依赖于两个第三方库的可用性,即spark-sql-kafka(Spark Kafka 集成)和spark-nlp(自然语言处理算法,如第六章所述,使用 Apache Spark 进行自然语言处理)。 -
--py-files: 由于我们的 Kafka 消费者是一个 PySpark 应用程序,我们可以使用这个参数来传递一个以逗号分隔的文件系统路径列表,这些路径包含我们的应用程序所依赖的任何 Python 代码文件。在我们的例子中,我们的 Kafka 消费者应用程序分别依赖于config.py和model_pipelines.py。 -
最后一个参数是我们 Spark Structured Streaming 驱动程序的 Python 代码文件的路径,在我们的例子中,是
kafka_twitter_consumer.py
因此,最终的命令如下所示:
> cd {SPARK_HOME}
> bin/spark-submit --master spark://192.168.56.10:7077 --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.3.2,JohnSnowLabs:spark-nlp:1.7.0 --py-files chapter08/config.py,chapter08/model_pipelines.py chapter08/kafka_twitter_consumer.py
假设基于 Python 的 Kafka 生产者应用程序也在运行,结果表应该定期写入控制台,并包含来自全球 Twitter API 消费并由真实 Twitter 用户编写的航班推文流背后的实时预测和分类!以下表格显示了在过滤使用 "@British_Airways" 时处理的真实世界分类推文选择:
| 推文原始内容 | 预测情感 |
|---|---|
| @British_Airways @HeathrowAirport 我习惯了在设得兰的寒冷,但这是完全不同的一种寒冷!! | 非负面 |
| @British_Airways 刚刚使用该应用程序为我们的航班 bari 到 lgw 办理登机手续,但应用程序显示没有行李托运 | 负面 |
| 她在夜晚看起来更美 | A380 在伦敦希思罗机场起飞 @HeathrowAviYT @HeathrowAirport @British_Airways | 非负面 |
| The @British_Airways #B747 landing into @HeathrowAirport | 非负面 |
| @British_Airways 正在尝试为明天的航班在线登机,但收到一条消息“很抱歉,我们无法为此次航班提供在线登机服务”。有什么想法吗?? | 负面 |
摘要
在本章中,我们开发了 Apache Kafka 的生产者和消费者应用程序,并利用 Spark 的 Structured Streaming 引擎处理从 Kafka 主题中消费的流数据。在我们的实际案例研究中,我们设计、开发和部署了一个端到端流处理管道,该管道能够消费全球各地发布的真实推文,并使用机器学习对这些推文背后的情感进行分类,所有这些都是在实时完成的。
在这本书中,我们经历了一次理论与实践相结合的旅程,探索了支撑当今行业数据智能革命的一些最重要和最激动人心的技术和框架。我们首先描述了一种新型分布式和可扩展的技术,这些技术使我们能够存储、处理和分析大量结构化、半结构化和非结构化数据。以这些技术为基础,我们建立了人工智能的背景及其与机器学习和深度学习的关系。然后,我们继续探讨了机器学习的关键概念及其应用,包括监督学习、无监督学习、自然语言处理和深度学习。我们通过大量相关且令人兴奋的用例来阐述这些关键概念,这些用例都是使用我们选择的 Apache Spark 大数据处理引擎实现的。最后,鉴于及时决策对许多现代企业和组织至关重要,我们将机器学习模型的部署从批量处理扩展到了实时流应用!