精通-Azure-机器学习第二版-三-

49 阅读1小时+

精通 Azure 机器学习第二版(三)

原文:annas-archive.org/md5/4810ad92d2f87002f854999a7a373fce

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:第七章:使用 NLP 的高级特征提取

在前面的章节中,我们学习了在 Azure 机器学习服务中许多标准的转换和预处理方法,以及使用 Azure 机器学习数据标注服务进行典型标注技术的应用。在本章中,我们希望更进一步,从文本和分类数据中提取语义特征——这是用户在训练机器学习模型时经常遇到的问题。本章将描述使用自然语言处理NLP)进行特征提取的基础。这将帮助您在实际的机器学习管道中实现使用 NLP 的语义嵌入。

首先,我们将探讨文本分类名义有序数据之间的差异。这种分类将帮助您根据特征类型决定最佳的特征提取和转换技术。稍后,我们将查看分类值最常见的转换方法,即标签编码独热编码。这两种技术将被比较和测试,以了解这两种技术的不同用例和应用。

接下来,我们将处理文本数据的数值嵌入。为了实现这一点,我们将构建一个简单的词袋模型,使用计数向量器。为了净化输入,我们将构建一个包含分词器、停用词去除、词干提取词形还原的 NLP 管道。我们将逐步学习这些不同的技术如何影响样本数据集。

此后,我们将用一种更好的词频加权方法——词频-逆文档频率TF-IDF)算法来替换词计数方法。这将帮助您在给定整个文档集合的情况下,通过加权一个文档中术语的出现频率相对于文档集合中的频率来计算单词的重要性。此外,我们将探讨奇异值分解SVD)以减少术语字典的大小。作为下一步,我们将通过利用词义来提高术语嵌入的质量,并深入了解语义嵌入,如全局向量GloVe)和Word2Vec

在最后一节,我们将探讨基于序列到序列深度神经网络且超过一亿参数的当前最先进的语言模型。我们将使用长短期记忆LSTM)训练一个小的端到端模型,使用双向编码器表示从 TransformerBERT)进行词嵌入和情感分析,并将这两种自定义解决方案与 Azure 认知服务中的文本分析能力进行比较。

本章将涵盖以下主题:

  • 理解分类数据

  • 构建简单的词袋模型

  • 利用术语重要性和语义

  • 实现端到端语言模型

技术要求

在本章中,我们将使用以下 Python 库和版本来创建分类编码、创建语义嵌入、训练端到端模型以及执行经典的 NLP 预处理步骤:

  • azureml-sdk 1.34.0

  • azureml-widgets 1.34.0

  • tensorflow 2.6.0

  • numpy 1.19.5

  • pandas 1.3.2

  • scikit-learn 0.24.2

  • nltk 3.6.2

  • gensim 3.8.3

与前几章类似,您可以使用本地 Python 解释器或 Azure Machine Learning 中托管的笔记本环境来执行此代码。

本章中所有的代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-Azure-Machine-Learning-Second-Edition/tree/main/chapter07

理解分类数据

分类数据以多种形式、形状和意义存在。了解你正在处理的数据类型至关重要——它是一个字符串、文本还是伪装成分类值的数值?这些信息对于数据预处理、特征提取和模型选择至关重要。

在本节中,首先,我们将查看不同类型的分类数据——即顺序名义文本。根据类型,你可以使用不同的方法从中提取信息或其他有价值的数据。请记住,分类数据无处不在,无论是 ID 列、名义类别、顺序类别还是自由文本字段。值得一提的是,你对数据的了解越多,预处理就越容易。

接下来,我们将通过将其转换为数值来实际预处理顺序和名义分类数据。当你想要使用不能解释分类数据的机器学习算法时,这是一个必要的步骤,这对于大多数算法都是真实的,例如基于决策树的算法。大多数其他算法只能对数值值进行操作(例如,计算损失函数),因此需要进行转换。

比较文本、分类和顺序数据

许多机器学习算法,如支持向量机、神经网络、线性回归等,只能应用于数值数据。然而,在现实世界的数据集中,我们经常发现非数值列,例如包含文本数据的列。本章的目标是将文本数据转换为数值数据,作为高级特征提取步骤,这样我们就可以将处理后的数据插入到任何机器学习算法中。

当处理现实世界数据时,你将面临许多不同类型的文本和/或分类数据。为了优化机器学习算法,你需要了解这些差异,以便对不同的类型应用不同的预处理技术。但首先,让我们定义三种不同的文本数据类型:

  • 文本数据:自由文本

  • 分类名义数据:不可排序的类别

  • 有序类别数据:可排序的类别

文本数据和类别数据之间的区别在于,在文本数据中,我们想要捕捉语义相似性(即词语的意义相似性),而在类别数据中,我们想要区分少数几个变量。

有序类别数据和有序类别数据之间的区别在于,名义数据不能排序(所有类别具有相同的权重),而有序类别可以在有序尺度上逻辑排序。

图 7.1 展示了一个新闻文章评论的示例数据集,其中第一列,命名为 statement,是一个文本字段,名为 topic 的列是一个名义类别,而 rating 是一个有序类别:

图 7.1 – 比较不同的文本数据类型

图 7.1 – 比较不同的文本数据类型

理解这些数据表示之间的差异对于之后找到适当的嵌入技术至关重要。用有序数值尺度替换有序类别似乎很自然,将名义类别嵌入到正交空间中。相反,将文本数据嵌入到保留语义的数值空间中并不明显——这将在本章后面的部分中介绍,这部分内容涉及 NLP。

请注意,除了类别值之外,你还会看到表示类别信息的连续数值变量,例如来自维度或查找表的 ID。尽管这些是数值,但如果可能的话,你应该考虑将它们作为类别名义值处理。以下是一个示例数据集:

图 7.2 – 比较数值类别值

图 7.2 – 比较数值类别值

在这个例子中,我们可以看到 sensorId 值是一个数值,应该将其解释为类别名义值,而不是默认的数值,因为它没有数值意义。当你从 sensorId 1 减去 sensorId 2 时,你得到什么?sensorId 10sensorId 1 的 10 倍大吗?这些问题是发现和编码这些类别值的典型问题。我们将在 第九章使用 Azure 机器学习构建 ML 模型 中发现,通过指定这些值是类别数据,梯度提升树模型可以优化这些特征,而不是将它们作为连续变量处理。

将类别转换为数值

让我们先从将分类变量(序数和名义)转换为数值开始。在本节中,我们将探讨两种常见的分类编码技术:标签编码独热编码(也称为虚拟编码)。虽然标签编码用一个数值特征列替换分类特征列,独热编码则使用多个列(列的数量等于唯一值的数量)来编码一个单一特征。

这两种技术以相同的方式进行应用。在训练迭代过程中,这些技术会找到特征列中的所有唯一值,并给它们分配一个特定的数值(对于独热编码,是一个多维数值)。结果,一个定义这种替换的查找字典存储在编码器中。当应用编码器时,应用列中的值会使用查找字典进行转换(替换)。如果事先知道可能的值列表,大多数实现允许编码器直接从已知值的列表初始化查找字典,而不是在训练集中找到唯一值。这有利于指定字典中值的顺序,从而对编码值进行排序。

重要提示

请注意,通常可能存在某些分类特征值在测试集中没有出现在训练集中,因此没有存储在查找字典中。因此,你应该在你的编码器中添加一个默认类别,该类别也可以将未见过的值转换为数值。

现在,我们将使用两个不同的分类数据列,一个是序数类别,另一个是名义类别,来展示不同的编码。图 7.3 显示了一个名义特征topic,它可能代表一个新闻机构的文章列表:

图 7.3 – 名义分类数据

图 7.3 – 名义分类数据

图 7.4 包含了rating的序数类别;它可能代表一个网站购买文章的反馈表单:

图 7.4 – 序列分类数据

图 7.4 – 序列分类数据

为了保留类别的含义,我们需要为不同的分类数据类型采用不同的预处理技术。首先,我们来看一下标签编码器。标签编码器为特征列中的每个唯一分类值分配一个递增的值。因此,它将类别转换为介于0N-1之间的数值,其中N代表唯一值的数量。

让我们在第一个表中的topic列中测试标签编码器。我们在数据上训练编码器,并用数值主题 ID 替换topic列。以下是一个训练标签编码器并转换数据集的示例片段:

from sklearn import preprocessing
data = load_articles()
enc = preprocessing.LabelEncoder()
enc.fit(data)
enc.transform(data)

图 7.5 显示了先前转换的结果。每个主题都被编码为一个数值增量,topicId

图 7.5 – 标签编码的主题

图 7.5 – 标签编码的主题

topicId生成的查找表如图7.6所示。这个查找字典是在fit()方法期间由编码器学习到的,可以使用transform()方法应用于分类数据:

图 7.6 – 主题查找字典

图 7.6 – 主题的查找字典

如前几个截图所示,使用标签对名义数据进行编码既简单又直接。然而,生成的数值数据具有与不同的名义类别不同的数学属性。因此,让我们找出这种方法对有序数据是如何工作的。

在下一个例子中,我们天真地将标签编码器应用于评分数据集。编码器通过迭代训练数据来训练,以创建查找字典:

from sklearn import preprocessing
data = load_ratings()
enc = preprocessing.LabelEncoder()
enc.fit(data)
enc.transform(data)

图 7.7显示了编码后的评分结果作为ratingId,这与前面的例子非常相似。然而,在评分的情况下,评分数据的数值属性与分类评分的有序属性相似:

图 7.7 – 标签编码的评分

图 7.7 – 标签编码的评分

此外,让我们看看编码器从输入数据中学习到的查找字典,如图7.8所示:

图 7.8 – 评分的查找字典

图 7.8 – 评分的查找字典

你在自动生成的查找字典中看到什么奇怪的地方了吗?由于训练数据中分类值的顺序,我们按照以下顺序创建了一个数字列表:

good < very good < bad < average

这可能不是我们在将标签编码器应用于有序分类值时所预期的结果。我们希望寻找的顺序类似于以下内容:

very bad < bad < average < good < very good

为了创建具有正确顺序的标签编码器,我们可以将分类值的有序列表传递给编码器。这将创建一个更有意义的编码,如图7.9所示:

图 7.9 – 带有自定义顺序的标签编码的评分

图 7.9 – 带有自定义顺序的标签编码的评分

要在 Python 中实现这一点,我们必须使用 pandas 的分类顺序变量,这是一种特殊的标签编码器,它需要一个有序分类列表作为输入:

import pandas as pd
data = load_ratings()
categories = [
    'very bad', 'bad', 'average', 'good', 'very good']
data = pd.Categorical(data,
 categories=categories,
 ordered=True)
print(data.codes)

在幕后,我们通过直接将类别传递给编码器来隐式地创建了以下查找字典:

图 7.10 – 带有自定义顺序的评分查找字典

图 7.10 – 带有自定义顺序的评分的查找字典

如前例所示,标签编码器可以迅速应用于任何分类数据,无需过多思考。标签编码器的结果是单个数值特征和分类查找表。此外,我们还可以看到,在主题和评分的示例中,标签编码更适合有序数据。

重要提示

主要的收获是标签编码器非常适合编码有序分类数据。你也了解到元素的顺序很重要,因此将类别按正确顺序手动传递给编码器是一个好的实践。

使用独热编码的正交嵌入

在本节的第二部分,我们将探讨N的含义,其中N代表唯一值的数量。这个向量除了包含一个列值为1的列,代表这个特定值所在的列外,其余列都包含0。以下是一个代码片段,展示了如何将独热编码器应用于articles数据集:

from sklearn import preprocessing
data = [load_articles()]
enc = preprocessing.OneHotEncoder()
enc.fit(data)
enc.transform(data)

前面代码的输出显示在图 7.11中:

图 7.11 – 独热编码的文章

图 7.11 – 独热编码的文章

独热编码的查找字典有N+1列,其中N是编码列中唯一值的数量。正如我们在图 7.12中的查找字典中可以看到的那样,字典中的所有 N 维向量都是正交的,长度相等,为1

图 7.12 – 文章的查找字典

图 7.12 – 文章的查找字典

现在,让我们将这种技术与有序数据进行比较,并将独热编码应用于评分表。结果显示在图 7.13中:

图 7.13 – 独热编码的评分

图 7.13 – 独热编码的评分

在前面的图中,我们可以看到,即使原始的类别值是有序的,编码后的值也无法排序,因此,在数值编码后,这个属性就丢失了。因此,我们可以得出结论,独热编码非常适合唯一值数量较少的名称分类值。

到目前为止,我们已经学习了如何通过使用查找字典和一维或 N 维数值嵌入将名称和有序分类值嵌入到数值中。然而,我们发现它在许多方面都有一定的局限性,例如唯一类别的数量和嵌入自由文本的能力。在接下来的几节中,我们将学习如何使用简单的 NLP 管道提取单词。

语义和文本值

值得花时间去理解的是,分类值和文本值并不相同。尽管它们可能都存储为字符串,并且在你的数据集中可能有相同的数据类型,但通常,分类值代表一组有限的类别,而文本值可以包含任何文本信息。

那么,这种区分为什么很重要呢?一旦你预处理了分类数据并将其嵌入到数值空间中,名称类别通常会被实现为正交向量。你将无法自动计算类别 A 到类别 B 的距离或创建类别之间的语义意义。

然而,对于文本数据,通常您会采用不同的方法来开始特征提取,该方法假设您将在数据集样本的相同文本特征中找到相似术语。您可以使用这些信息来计算两个文本列之间的有意义相似度得分;例如,测量共同单词的数量。

因此,我们建议您彻底检查您有哪些类型的分类值以及您打算如何预处理它们。此外,一个很好的练习是计算两行之间的相似度,看看它是否与您的预测相符。让我们看看使用基于字典的词袋嵌入的简单文本预处理方法。

构建简单的词袋模型

在本节中,我们将探讨一个惊人的简单概念,即使用称为词袋的技术来解决标签编码在文本数据中的不足,这将为一个简单的 NLP 管道打下基础。当您阅读这些技术时,如果它们看起来太简单,请不要担心;我们将通过调整、优化和改进逐步构建现代 NLP 管道。

使用计数构建的简单词袋模型

在本节中,我们将构建的主要概念是词袋模型。这是一个非常简单的概念;也就是说,它涉及将任何文档建模为包含在给定文档中的单词集合,每个单词的频率。因此,我们丢弃句子结构、单词顺序、标点符号等,并将文档简化为单词的原始计数。在此基础上,我们可以将这个单词计数向量化为一个数值向量表示,然后可以用于机器学习、分析、文档比较等等。虽然这个单词计数模型听起来非常简单,但在路上我们将会遇到很多语言特定的障碍,我们需要解决。

让我们开始并定义一个示例文档,我们将在这个部分对其进行转换:

Almost before we knew it, we had left the ground. The unknown holds its grounds.

将简单的单词计数应用于文档为我们提供了我们的第一个(过于简单)词袋模型:

图 7.14 – 一个简单的词袋模型

图 7.14 - 一个简单的词袋模型

然而,像前面那样简单的方法有很多问题。我们混合了不同的标点符号、符号、名词、动词、副词和形容词的不同变形、屈折、时态和格。因此,我们必须构建一个管道来使用 NLP 清理和标准化数据。在本节中,在将数据输入到计数向量器之前,我们将构建以下清理步骤的管道,该向量器最终会计算单词出现次数并将它们收集到特征向量中。

分词 - 将字符串转换为单词列表

构建管道的第一步是将语料库分为文档,将文档分为单词。这个过程被称为nltk

from nltk.tokenize import word_tokenize
nltk.download('punkt')
tokens = word_tokenize(document)
print(tokens)

上一段代码将输出一个包含单词和标点符号的标记列表:

['Almost', 'before', 'we', 'knew', 'it', ',', 'we', 'had', 'left', 'the', 'ground', '.', 'The', 'unknown', 'holds', 'its', 'grounds', '.']

当你执行前面的代码片段时,nltk将下载预训练的标点模型以运行分词器。分词器的输出是单词和标点符号。

在下一步中,我们将移除标点符号,因为它们对于随后的词形还原过程不相关。然而,我们将在本节稍后将其恢复:

words = [word.lower() for word in tokens if word.isalnum()]
print(words)

结果将只包含原始文档中的单词,没有任何标点符号:

['almost', 'before', 'we', 'knew', 'it', 'we', 'had', 'left', 'the', 'ground', 'the', 'unknown', 'holds', 'its', 'grounds']

在前面的代码中,我们使用了word.isalnum()函数来仅提取字母数字标记并将它们全部转换为小写。前面的单词列表已经比最初的原始模型好得多。然而,它仍然包含许多不必要的词,如thewehad等,这些词不传达任何信息。

为了过滤掉特定语言的噪声,有道理移除那些经常出现在文本中且不增加任何语义意义的词。在 Python 中,移除这些词是常见的做法,使用nltk库:

from nltk.corpus import stopwords
stopword_set = set(stopwords.words('english'))
words = [word for word in words if word not in stopword_set]
print(words)

现在得到的列表只包含不是停用词的单词:

['almost', 'knew', 'left', 'ground', 'unknown', 'holds', 'grounds']

上述代码为我们提供了一个很好的管道,我们最终只得到具有语义意义的词。我们可以将这个词表带到下一步,并对每个词应用更复杂的转换/归一化。如果我们在这个阶段应用计数向量器,我们最终会得到如图 7.15 所示的简单词袋模型:

图 7.15 – 一个简单的词袋模型

图 7.15 – 一个简单的词袋模型

如前图所示,词袋模型中包含的术语列表已经比原始示例干净得多。这是因为它不包含任何标点符号或停用词。

你可能会问,除了在文本中相对频繁出现之外,什么使一个词成为停用词?嗯,这是一个非常好的问题!我们可以使用TF-IDF方法来衡量每个词在当前上下文中的重要性,与它在整个文本中的出现频率进行比较,这将在使用 TF-IDF 衡量词的重要性部分进行讨论。

词干提取 – 基于规则的词缀移除

在下一步中,我们想要归一化词缀——单词的结尾以创建复数和动词变位。你可以看到,随着每一步的进行,我们都在更深入地探讨单一语言的概念——在这个案例中,是英语。然而,当将这些步骤应用于不同的语言时,可能需要使用完全不同的转换。这就是为什么 NLP 是一个如此困难的领域。

移除单词的词缀以获得词根也称为词干提取。词干提取是指将每个单词的出现转换为它的词根的基于规则(启发式)方法。以下是一些预期的转换示例:

cars   -> car
saying -> say
flies  -> fli

如前例所示,这种针对词根的启发式方法必须为每种语言专门构建。这对于所有其他 NLP 算法也是普遍适用的。为了简洁起见,在这本书中,我们只将讨论英语示例。

英语中一个流行的词根化算法是 Porter 算法,它定义了五个连续的缩减规则,例如从单词末尾移除edingatetionenceance等。nltk库包含 Porter 词根化算法的实现:

from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
words = [stemmer.stem(word) for word in words]
print(words)

词根化后的单词列表看起来像这样:

['almost', 'knew', 'left', 'ground', 'unknown', 'hold', 'ground']

在前面的代码中,我们只是简单地将stemmer应用于分词文档中的每个单词。经过这一步骤后的词袋模型如图7.16所示:

图 7.16 – 词根化后的词袋模型

图 7.16 – 词根化后的词袋模型

虽然这个算法与词缀配合得很好,但它无法避免对动词的变形和时态进行规范化。这是我们接下来要解决的问题,我们将使用词形还原来解决。

词形还原 – 基于词典的词规范化

当查看词根化示例时,我们已能看出该方法的局限性。例如,对于像areamis这样的不规则动词变形,它们都应该被规范化为同一个词be,会发生什么?这正是词形还原试图通过使用预训练的词汇集和转换规则(称为词元)来解决的问题。词元存储在查找字典中,类似于以下转换:

are    -> be
is     -> be
taught -> teach
better -> good

讨论词形还原时,有一个非常重要的观点需要提出。每个词元都需要应用于正确的词型,因此名词、动词、形容词等都有词元。这样做的原因是,一个词可以是名词或动词的过去时。在我们的例子中,ground可能来自名词ground或动词grindleft可能是形容词或leave的过去时。因此,我们还需要从句子中的单词中提取词型——这个过程称为nltk库再次为我们提供了支持。为了估计正确的 POS 标签,我们还需要提供标点符号:

import nltk
nltk.download('averaged_perceptron_tagger')
tags = nltk.pos_tag(tokens)
print(tags)

这里是生成的 POS 标签:

[('Almost', 'RB'), ('before', 'IN'), ('we', 'PRP'), ('knew', 'VBD'), ('it', 'PRP'), (',', ','), ('we', 'PRP'), ('had', 'VBD'), ('left', 'VBN'), ('the', 'DT'), ('ground', 'NN'), ('.', '.'), ('The', 'DT'), ('unknown', 'JJ'), ('holds', 'VBZ'), ('its', 'PRP$'), ('grounds', 'NNS'), ('.', '.')]

POS 标签描述了文档中每个标记的词型。您可以使用nltk.help.upenn_tagset()命令找到完整的标签列表。以下是从命令行执行此操作的示例:

import nltk
nltk.download('tagsets')
nltk.help.upenn_tagset()

前面的命令将打印出 POS 标签列表:

CC: conjunction, coordinating
    & 'n and both but either et for less minus neither nor or 
    plus so therefore times v. versus vs. whether yet
CD: numeral, cardinal
    mid-1890 nine-thirty forty-two one-tenth ten million 0.5 
    one forty- seven 1987 twenty '79 zero two 78-degrees 
    eighty-four IX '60s .025 fifteen 271,124 dozen quintillion 
    DM2,000 ...
DT: determiner
    all an another any both del each either every half la many 
    much nary neither no some such that the them these this 
    those
EX: existential there
    there
FW: foreign word
    gemeinschaft hund ich jeux habeas Haementeria Herr K'ang-si 
    vous lutihaw alai je jour objets salutaris fille quibusdam 
    pas 
...

POS 标签还包括动词和其他非常有用的时态信息。然而,在本节的词形还原中,我们只需要知道单词类型——名词动词形容词副词。一个可能的词形还原器选择是nltk中的 WordNet 词形还原器。WordNet 是一个英语词汇数据库,它将单词分组到概念和词型组中。

要将词形还原器应用于词干分析的结果,我们需要通过标点符号和停用词过滤 POS 标签,类似于之前的预处理步骤。然后,我们可以使用结果单词的词标签。让我们使用nltk应用词形还原器:

from nltk.corpus import wordnet
from nltk.stem import WordNetLemmatizer
nltk.download('wordnet')       
lemmatizer = WordNetLemmatizer()
tag_dict = {
    "J": wordnet.ADJ,
    "N": wordnet.NOUN,
    "V": wordnet.VERB,
    "R": wordnet.ADV
}
pos = [tag_dict.get(t[0].upper(), wordnet.NOUN) \
        for t in zip(*tags)[1]]
words = [lemmatizer.lemmatize(w, pos=p) \
        for w, p in zip(words, pos)]
print(words)

代码输出了词形还原后的单词:

['almost', 'know', 'leave', 'ground', 'unknown', 'hold', 'ground']

上述单词列表看起来比我们在之前的模型中找到的干净得多。这是因为我们对动词的时态进行了归一化,并将它们转换成不定式形式。得到的词袋模型在图 7.17中显示:

图 7.17 – 词袋模型在词形还原后的样子

图 7.17 – 词形还原后的词袋模型

这种技术对于清理数据集中单词的不规则形式非常有帮助。然而,它基于规则——称为词元——因此,它只能用于有此类词元的语言和单词。

scikit-learn 中的词袋模型

最后,我们可以将我们之前的所有步骤结合起来,创建一个最先进的自然语言处理预处理流程,以归一化输入文档,并通过计数向量器运行它们,以便我们可以将它们转换成数值特征向量。对多个文档这样做,我们可以轻松地在数值空间中比较文档的语义。我们可以计算文档特征向量之间的余弦相似度来计算它们的相似度,将它们插入到监督分类方法中,或者对生成的文档概念进行聚类。

回顾一下,让我们看看简单词袋模型的最终流程。我想强调的是,这个模型只是我们使用自然语言处理进行特征提取旅程的开始。我们进行了以下步骤进行归一化:

  1. 分词

  2. 删除标点符号

  3. 删除停用词

  4. 词干提取

  5. 基于 POS 标签的词形还原

在最后一步中,我们在 scikit-learn 中应用了CountVectorizer。这将计算每个词的出现次数,创建一个全局单词语料库,并输出一个包含单词频率的稀疏特征向量。以下是将预处理数据从nltk传递到CountVectorizer的示例代码:

from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer()
data = [" ".join(words)]
X_train_counts = count_vect.fit_transform(data)
print(X_train_counts)

转换后的词袋模型包含坐标和计数:

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

坐标指的是(文档 ID,术语 ID)对,而计数指的是术语频率。为了更好地理解这个输出,我们还可以查看模型的内部词汇表。vocabulary_参数包含术语 ID 的查找字典:

print(count_vect.vocabulary_)

代码输出了模型的单词字典:

{'almost': 0, 'know': 3, 'leave': 4, 'ground': 1, 'unknown': 5, 'hold': 2}

在前面的例子中,我们在将其传递到CountVectorizer之前将预处理文档转换回字符串。这样做的原因是CountVectorizer自带一些可配置的预处理技术,例如分词、停用词删除等。对于这个演示,我们想将其应用于预处理数据。转换的输出是一个包含术语频率的稀疏特征向量。

让我们来找出如何将多个术语与语义概念相结合。

利用术语重要性和语义

我们到目前为止所做的一切都相对简单,并且基于词干或所谓的标记。词袋模型只不过是一个标记字典,它按字段统计标记的出现次数。在本节中,我们将探讨一种常见的技巧,通过术语的 n-gram 和 skip-gram 组合来进一步改进文档之间的匹配。

以多种方式组合术语将使你的词典爆炸。如果你有一个大语料库,比如一千万个单词,这就会变成一个问题。因此,我们将探讨一种常见的预处理技术,通过 SVD 来降低大型词典的维度。

虽然,现在,这种方法要复杂得多,但它仍然基于一个在大语料库上已经工作得很好的词袋模型。然而,当然,我们可以做得更好,并尝试理解词语的重要性。因此,我们将探讨 NLP 中另一种流行的技术来计算术语的重要性。

使用 n-gram 和 skip-gram 进行词语泛化

在之前的管道中,我们考虑了每个单词本身,没有任何上下文。然而,众所周知,上下文在语言中非常重要。有时,词语在一起才有意义,而不是单独存在。为了将这种上下文引入同类型的算法,我们将引入n-gramskip-gram。这两种技术都在 NLP 中广泛用于预处理数据集和从文本数据中提取相关特征。

让我们从 n-gram 开始。一个输入数据集的N个连续实体(即字符、单词或标记)。以下是一些在字符列表中计算 n-gram 的示例:

A, B, C, D -> 1-Gram: A, B, C, D
A, B, C, D -> 2-Gram: AB, BC, CD
A, B, C, D -> 3-Gram: ABC, BCD

这里是一个示例,使用 scikit-learn 的CountVectorizer中的内置ngram_range参数来为输入数据生成多个 n-gram:

from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer(ngram_range=(1,2))
X_train_counts = count_vect.fit_transform(data)
print(count_vect.vocabulary_)

如您所见,词汇现在包含每个术语的 1-gram 和 2-gram 表示:

{'almost': 0, 'before': 2, 'we': 24, 'knew': 15, 'it': 11, 'had': 7, 'left': 17, 'the': 19, 'ground': 4, 'unknown': 22, 'holds': 9, 'its': 13, 'grounds': 6, 'almost before': 1, 'before we': 3, 'we knew': 26, 'knew it': 16, 'it we': 12, 'we had': 25, 'had left': 8, 'left the': 18, 'the ground': 20, 'ground the': 5, 'the unknown': 21, 'unknown holds': 23, 'holds its': 10, 'its grounds': 14}

在前面的代码中,我们可以看到,我们现在在训练词汇中拥有两个连续词语的组合,而不是原始词语。

我们可以将 n-gram 的概念扩展到允许模型跳过词语。如果我们想要执行一个 2-gram,但其中一个样本中两个词语之间有一个形容词,而在另一个样本中这些词语是直接相邻的,这是一个很好的选项。为了实现这一点,我们需要一种方法来定义我们允许跳过多少个词语来找到匹配的词语。以下是一个使用之前相同字符的示例:

A, B, C, D -> 2-Gram (1 skip): AB, AC, BC, BD, CD
A, B, C, D -> 2-Gram (2 skip): AB, AC, AD, BC, BD, CD

幸运的是,我们在nltk中找到了 n-gram 的通用版本,即nltk.skipgrams方法。将跳过距离设置为0会导致传统的 n-gram 算法。我们可以将其应用于我们的原始数据集:

terms = list(nltk.skipgrams(document.split(' '), 2, 1))
print(terms)

与 2-gram 示例类似,该方法产生了一组成对术语的组合列表。然而,在这种情况下,我们在这些对之间允许存在一个跳过的单词:

[('Almost', 'before'), ('Almost', 'we'), ('before', 'we'), ('before', 'knew'), ('we', 'knew'), ('we', 'it,'), ('knew', 'it,'), ('knew', 'we'), ('it,', 'we'), ('it,', 'had'), ('we', 'had'), ('we', 'left'), ('had', 'left'), ('had', 'the'), ('left', 'the'), ('left', 'ground.'), ('the', 'ground.'), ('the', 'The'), ('ground.', 'The'), ('ground.', 'unknown'), ('The', 'unknown'), ('The', 'holds'), ('unknown', 'holds'), ('unknown', 'its'), ('holds', 'its'), ('holds', 'grounds.'), ('its', 'grounds.')]

在前面的代码中,我们可以观察到 skip-grams 可以为 NLP 模型生成大量的额外有用特征维度。在现实场景中,这两种技术通常都会使用,因为单个单词的顺序在语义中起着重要作用。

然而,如果输入文档是来自网络的所有网站或大型文档,新特征维度的爆炸可能会造成灾难。因此,我们还需要一种方法来避免维度爆炸,同时捕获输入数据中的所有语义。我们将在下一节中解决这个挑战。

使用 SVD 减小词字典大小

NLP 的一个常见问题是语料库中的单词数量庞大,因此字典大小会爆炸。在先前的例子中,我们看到字典的大小定义了正交项向量的大小。因此,20,000 个术语的字典大小将导致 20,000 维的特征向量。即使没有任何 n-gram 丰富,这个特征向量维度也太大,无法在标准 PC 上处理。

因此,我们需要一个算法来减小生成的CountVectorizer的维度,同时保留现有信息。理想情况下,我们只会从输入数据中移除冗余信息,并将其投影到低维空间,同时保留所有原始信息。

PCA 变换非常适合我们的解决方案,并帮助我们将输入数据转换成更低维度的线性无关维度。然而,计算特征值需要一个对称矩阵(行数和列数相同),在我们的情况下,我们没有这样的矩阵。因此,我们可以使用 SVD 算法,它将特征向量计算推广到非对称矩阵。由于其数值稳定性,它通常用于 NLP 和信息检索系统中。

SVD 在 NLP 应用中的使用也被称为潜在语义分析LSA),因为主成分可以解释为潜在特征空间中的概念。SVD 嵌入将高维特征向量转换成低维概念空间。概念空间中的每个维度都是由术语向量的线性组合构成的。通过丢弃方差最小的概念,我们也减小了结果概念空间的维度,使其变得小得多,更容易处理。典型的概念空间有 10 到 100 个维度,而单词字典通常有超过 100,000 个。

让我们通过 sklearnTruncatedSVD 实现来查看一个示例。SVD 被实现为一个转换器类,因此我们需要调用 fit_transform() 来拟合一个字典并使用相同的步骤进行转换。SVD 使用 n_components 参数配置为仅保留方差最高的成分:

from sklearn.decomposition import TruncatedSVD
svd = TruncatedSVD(n_components=5)
X_lsa = svd.fit_transform(X_train_counts) 

在前面的代码中,我们使用 SVD 对 X_train_counts 数据和 CountVectorizer 的输出进行 LSA。我们配置 SVD 只保留方差最高的前五个成分。

通过降低数据集的维度,你会丢失信息。幸运的是,我们可以使用训练好的 SVD 对象来计算剩余数据集中方差的数量,如下例所示:

Print(svd.explained_variance_ratio_.sum())

前面的命令将方差输出为一个介于 0 和 1 之间的数字,其中 1 表示 SVD 变换是原始数据到潜在空间的精确无损映射:

0.19693920498587408

在这种情况下,仅使用五个成分,SVD 保留了原始数据集 20% 的方差。

重要提示

根据任务的不同,我们通常的目标是在潜在变换后保留超过 80-90% 的原始方差。

在前面的代码示例中,我们计算了转换后保留的数据的方差。因此,我们现在可以增加或减少成分的数量,以保持转换数据中特定百分比的信息。这是一个非常有用的操作,并在许多实际的 NLP 应用中得到了使用。

注意,我们仍在使用词袋模型的原始单词字典。这个模型的一个特定缺点是,一个术语出现的频率越高,它的计数(以及因此的权重)就越高。这是一个问题,因为现在,任何不是停用词且在文本中频繁出现的术语都将获得高权重——无论该术语在特定文档中的重要性如何。因此,我们引入了另一个极其流行的预处理技术——TF-IDF

使用 TF-IDF 测量单词的重要性

词袋方法的一个特定缺点是,我们仅仅计算一个上下文中单词的绝对数量,而不检查该单词是否在所有文档中普遍出现。一个在所有文档中都出现的术语可能对我们模型来说并不相关,因为它包含的信息较少,并且更频繁地出现在其他文档中。因此,在文本挖掘中,计算给定上下文中某个单词的重要性是一项重要的技术。

因此,我们希望计算一个上下文中术语的相对数量,而不是上下文中术语的绝对计数。通过这样做,我们将给只出现在特定上下文中的术语赋予更高的权重,并减少给出现在许多不同文档中的术语的权重。这正是 TF-IDF 算法所做的事情。根据以下方程式,很容易计算文档中术语 (t) 的权重 (w):

公式 07_001

虽然词频 (ft) 计算了文档中的所有术语,但逆文档频率是通过将总文档数 (N) 除以所有文档中术语的计数 (fd*) 来计算的。IDF 术语通常进行对数变换,因为所有文档中术语的总数可能相当大。

在下面的示例中,我们不会直接使用 TF-IDF 函数。相反,我们将使用 TfidfVectorizer,它在一步中完成计数并将 TF-IDF 函数应用于结果。再次强调,该函数作为 sklearn 转换器实现,因此我们调用 fit_transform() 来训练和转换数据集:

from sklearn.feature_extraction.text import TfidfVectorizer
vect = TfidfVectorizer()
data = [" ".join(words)]
X_train_counts = vect.fit_transform(data)
print(X_train_counts)

结果的格式与前面的示例类似,包含 (document id, term id) 对及其 TF-IDF 值:

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

在前面的代码中,我们直接应用 TfidfVectorizer,它返回与使用 CountVectorizerTfidfTransformer 结合相同的结果。我们转换包含词袋模型中单词的数据集,并返回 TF-IDF 值。我们还可以为每个 TF-IDF 值返回术语:

print(vect.get_feature_names())

上述代码返回模型的词汇表:

['almost', 'ground', 'hold', 'know', 'leave', 'unknown']

在这个示例中,我们可以看到 ground 获得了 TF-IDF 值为 0.667,而所有其他术语的值均为 0.333。当向语料库中添加更多文档时,这个计数将相对缩放——因此,如果单词 hold 再次出现,TF-IDF 值将降低。

在任何实际的管道中,我们都会始终使用本章中介绍的所有技术——分词、停用词去除、词干提取、词形还原、n-gram/skip-gram、TF-IDF 和 SVD——结合在一个单一的管道中。结果将是一个由重要性加权的 n-gram/skip-gram 的标记的数值表示,并转换到潜在语义空间。使用这些技术进行你的第一个 NLP 管道将让你走得很远,因为你现在可以从你的文本数据中捕获大量信息。

到目前为止,我们已经学习了如何使用一维或 N 维标签、计数和加权词干和字符组合来数值化许多种类的分类和文本值。虽然许多这些方法在需要简单数值嵌入的许多情况下都表现良好,但它们都有一个严重的限制——它们不编码语义。让我们看看我们如何在同一个管道中提取文本的语义意义。

使用词嵌入提取语义

当计算新闻的相似性时,你会想象到像网球、一级方程式足球这样的主题在语义上比像政治、经济或科学这样的主题更相似。然而,在之前讨论的技术中,所有编码的分类都被视为在语义上是相同的。在本节中,我们将讨论一种简单的语义嵌入方法,这也可以称为词嵌入

之前讨论的管道使用 LSA 将多个文档转换为术语,然后将这些术语转换为可以与其他文档比较的语义概念。然而,语义意义基于术语出现和重要性——没有对单个术语之间的语义进行测量。

因此,我们寻找的是将术语嵌入到数值多维空间中的嵌入,这样每个单词就代表这个空间中的一个点。这使我们能够计算这个空间中多个单词之间的数值距离,以比较两个单词的语义意义。词嵌入最有趣的好处是,在词嵌入上的代数运算不仅数值上是可能的,而且是有意义的。考虑以下示例:

King – Man + Woman = Queen

我们可以通过将单词语料库映射到 N 维数值空间,并根据单词语义优化数值距离(例如,基于语料库中单词之间的距离)来创建这样的嵌入。结果优化输出语料库中单词及其 N 维数值表示的字典。在这个数值空间中,单词具有与语义空间中相同或至少相似的属性。一个巨大的好处是,这些嵌入可以无监督地训练,因此不需要标记的训练数据。

最早的嵌入之一被称为Word2Vec,它基于连续的词袋模型或连续的跳字模型来计数和测量窗口中的单词。让我们尝试这个功能,并使用 Word2Vec 进行语义词嵌入:

  1. 最好的 Python 词嵌入实现是Gensim,我们也将在这里使用它。我们需要将我们的标记输入到模型中以便训练它:

    from gensim.models import Word2Vec
    model = Word2Vec(words, size=100, window=5)
    vector = model.wv['ground']
    

在前面的代码中,我们加载了Word2Vec模型,并用之前章节中存储在words变量中的标记列表初始化它。size属性定义了结果向量的维度,window参数决定了我们应该考虑多少个单词作为每个窗口。一旦模型被训练,我们就可以简单地在该模型的字典中查找词嵌入。

代码将自动在我们提供的标记集上训练嵌入。结果模型将单词到向量的映射存储在wv属性中。理想情况下,我们还使用一个大型语料库或预训练模型,该模型由gensim或另一个 NLP 库(如NLTK)提供,以训练嵌入并使用较小的数据集进行微调。

  1. 接下来,我们可以使用训练好的模型通过 Word2Vec 嵌入将我们文档中的所有术语嵌入。然而,这将导致多个向量,因为每个单词都返回其自己的嵌入。因此,你需要使用所有嵌入的数学平均值将所有向量组合成一个单一的向量。这个过程与用于生成 LSA 中概念的类似过程非常相似。此外,还有其他可能的缩减技术;例如,使用 TF-IDF 值对单个嵌入向量进行加权:

    dim = len(model.wv.vectors[0])
    X = np.mean([model.wv[w] for w in words if w in model.wv] \
            or [np.zeros(dim)], axis=0)
    

在前面的函数中,我们计算所有术语的词嵌入向量的平均值——这被称为平均嵌入,它代表了文档在嵌入空间中的概念。如果一个单词在嵌入中未找到,我们需要在计算中将它替换为零。

您可以通过下载预训练嵌入,例如在维基百科语料库上,来使用此类语义嵌入为您的应用程序。然后,您可以遍历您的清洗过的输入标记,并在数字嵌入的字典中查找单词。

GloVe 是另一种流行的将单词编码为数值向量的技术,由斯坦福大学开发。与基于连续窗口的方法相比,它使用全局单词到单词共现统计来确定单词之间的线性关系:

  1. 让我们看看在维基百科和 Gigaword 新闻档案上训练的预训练 6B 标记嵌入:

    # download pre-trained dictionary from 
    # http://nlp.stanford.edu/data/glove.6B.zip
    glove = {}
    with open('glove.6B.100d.txt') as f:
      for line in f:
        word, coefs = line.split(maxsplit=1)
        coefs = np.fromstring(coefs, 'f', sep=' ')
        glove[word] = coefs
    

在前面的代码中,我们只打开并解析预训练的词嵌入,以便将单词和向量存储在查找字典中。

  1. 然后,我们使用这个字典在我们的训练数据中查找标记,并通过计算所有 GloVe 向量的平均值来合并它们:

    X = np.mean([glove[w] for w in words if w in glove] \
          or [np.zeros(dim)], axis=0)
    

前面的代码与之前非常相似,每个单词返回一个向量,最后通过取平均值进行聚合。再次强调,这与使用训练数据中所有标记的语义概念相对应。

Gensim 提供了其他流行的语义嵌入模型,如doc2wordfastTextGloVegensim Python 库是利用这些预训练嵌入或训练您自己的模型的绝佳场所。现在您可以用单词向量的平均嵌入替换您的词袋模型,以捕获词义。然而,您的管道仍然由许多可调组件构建。

在下一节中,我们将探讨构建端到端最先进的语言模型以及重用 Azure 认知服务中的一些语言特征。

实现端到端语言模型

在前面的章节中,我们训练和连接了多个部分以实现一个最终算法,其中大多数单个步骤也需要进行训练。词形还原包含一个转换规则字典。停用词存储在字典中。词干提取需要为每种语言和每个需要嵌入训练的单词制定规则——TF-IDF 和 SVD 仅在训练数据上计算,但彼此独立。

这是一个类似于传统计算机视觉方法的问题,我们将在第十章“在 Azure 上训练深度神经网络”中更深入地讨论,其中许多经典算法被组合成一个特征提取器和分类器的管道。类似于计算机视觉中通过梯度下降和反向传播训练的端到端模型的突破,深度神经网络——尤其是序列到序列模型——已经取代了手动执行每个转换和训练步骤的经典方法。

在本节中,首先,我们将查看如何通过自定义嵌入和 LSTM 实现来改进我们之前的模型,以对标记序列进行建模。这将帮助你更好地理解我们是如何从基于单个预处理器管道的个体方法过渡到使用深度学习的完整端到端方法的。

序列到序列模型是基于在可变输入集上训练的编码器和解码器模型。这种编码器/解码器架构用于各种任务,如机器翻译、图像标题和摘要。这些模型的优点之一是,你可以重用这个网络中的编码器部分,将一组输入转换为编码器的固定集数值表示。

接下来,我们将探讨最先进的语言表示模型,并讨论它们如何用于特征工程和文本数据的预处理。我们将使用 BERT 进行情感分析和数值嵌入。

最后,我们还将探讨如何重用 Azure 认知服务 API 进行文本分析,以执行高级建模和特征提取,例如文本或句子情感、关键词或实体识别。这是一个很好的方法,因为你可以利用微软的知识和大量训练数据,通过简单的 HTTP 请求来执行复杂的文本分析。

标记序列的端到端学习

我们不想将不同的算法片段连接成一个单一的管道,而是想要构建和训练一个端到端模型,该模型可以训练词嵌入、预形式潜在语义转换,并在单个模型中捕获文本中的顺序信息。

这种模型的优点是,每个处理步骤都可以在单个联合优化过程中针对用户的预测任务进行微调:

  1. 管道的第一部分将看起来与前几节非常相似。我们将构建一个标记化器,将文档转换为标记序列,然后将其转换为基于标记序列的数值模型。然后,我们将使用pad_sequences将所有文档对齐到相同的长度:

    from tensorflow.keras.preprocessing.text import Tokenizer
    from tensorflow.keras.preprocessing.sequence import \
           pad_sequences
    num_words = 1000
    tokenizer = Tokenizer(num_words=num_words)
    tokenizer.fit_on_texts(X_words)
    X = tokenizer.texts_to_sequences(X_words)
    X = pad_sequences(X, maxlen=2000)
    
  2. 在下一步中,我们将使用 Keras 构建一个简单的模型,包括一个嵌入层和一个 LSTM 层来捕获标记序列。嵌入层将执行类似于 GloVe 的操作,将单词嵌入到语义空间中。LSTM 单元将确保我们比较的是单词序列而不是单个单词。然后,我们将使用一个带有softmax激活函数的密集层来实现分类头:

    from tensorflow.keras.layers import Embedding, LSTM, Dense
    from tensorflow.keras.models import Sequential
    embed_dim = 128
    lstm_out = 196
    model = Sequential()
    model.add(Embedding(
        num_words, embed_dim, input_length=X.shape[1]))
    model.add(LSTM(
        lstm_out, recurrent_dropout=0.2, dropout=0.2))
    model.add(Dense(
        len(labels), activation='softmax'))
    model.compile(loss='categorical_crossentropy', 
                  optimizer='adam',
                  metrics=['categorical_crossentropy'])
    

如前所述的函数所示,我们使用三个层(即EmbeddingLSTMDense)和一个softmax激活函数构建了一个简单的神经网络,用于分类。这意味着为了训练此模型,我们还需要同时解决一个分类问题。因此,我们需要标记的训练数据来使用这种方法进行分析。在下一节中,我们将探讨序列到序列模型是如何在输入输出文本序列中用于学习隐式文本表示的。

最前沿的序列到序列模型

在近年来,另一种类型的模型已经取代了传统的 NLP 管道——基于 transformer 的模型。这些类型的模型是完全端到端的,并使用序列到序列映射、位置编码和多头注意力层。这使得模型能够在文本中向前和向后查看,关注特定模式,并完全端到端地学习任务。正如你可能已经猜到的,这些模型具有复杂的架构,通常有超过一亿或超过十亿的参数。

序列到序列模型现在在许多复杂的端到端自然语言处理(NLP)问题中处于最前沿,例如分类(例如,情感或文本分析)、语言理解(例如,实体识别)、翻译、文本生成、摘要等等。

一种流行的序列到序列模型是 BERT,今天,它存在许多不同的变体和配置。基于 BERT 架构的模型似乎表现特别出色,但已经被更新的架构、调整的参数或具有更多训练数据的模型所超越。

使用这些新的 NLP 模型的最简单方法是通过Hugging Facetransformers库,该库提供了端到端模型(或管道)以及预训练的标记器和模型。transformers库实现了TensorFlowPyTorch的所有模型架构。这些模型可以轻松地在应用程序中使用,从头开始训练,或者使用特定领域的自定义训练数据进行微调。

以下示例展示了如何使用默认的 sentiment-analysis 流程实现情感分析,该流程在撰写本文时使用 TFDistilBertForSequenceClassification 模型:

from transformers import pipeline
classifier = pipeline("sentiment-analysis")
result = classifier("Azure ML is quite good.")[0]
print("Label: %s, with score: %.2f" %
         (result['label'], result['score']))

如前例所示,使用预训练模型进行端到端预测任务非常简单。这三行代码可以轻松集成到你的特征提取流程中,以丰富你的训练数据中的情感。

除了端到端模型之外,NLP 的另一个流行应用是在预处理文本数据时提供语义嵌入。这也可以使用 transformers 库和许多支持的模型之一来实现。

要做到这一点,首先,我们初始化一个预训练的 BERT 分词器。这将帮助我们将输入数据分割成适合 BERT 模型的正确格式:

from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
inputs = tokenizer("Azure ML is quite good.",   
                   return_tensors="tf")

一旦我们将输入转换为标记序列,我们就可以评估 BERT 模型。要检索数值嵌入,我们需要理解编码器的潜在状态,我们可以使用 last_hidden_state 属性来检索:

from transformers import TFBertModel
model = TFBertModel.from_pretrained('bert-base-uncased')
outputs = model(**inputs)
print(outputs.last_hidden_state)

最后的隐藏层包含模型的潜在表示,我们现在可以用作模型中的语义数值表示:

<tf.Tensor: shape=(1, 10, 768), dtype=float32, numpy=
array([[[-0.30760652,  0.19552925,  0.1440584 , ...,  0.08283961,
          0.16151786,  0.23049755],…

这些模型的关键启示是它们使用基于编码器/解码器的架构,这使我们能够简单地借用编码器将文本嵌入到语义数值特征空间中。因此,一个常见的方法是下载预训练模型,并通过网络的编码器部分进行正向传递。现在,固定大小的数值输出可以用作任何其他模型的特征向量。这是一个常见的预处理步骤,也是使用最先进的语言模型进行数值嵌入的良好权衡。

使用 Azure 认知服务的文本分析

在许多工程学科中,一个好的方法是不重复造轮子,因为许多其他公司已经比你更好地解决了相同的问题。对于微软开发、实施和训练的基本文本分析和文本理解任务,现在作为服务提供的情况可能也是如此。

如果我告诉你,当使用 Azure 时,文本理解功能,如情感分析、关键词提取、语言检测、命名实体识别以及个人身份信息PII)的提取,只需一个请求即可?Azure 提供的 Text Analytics API 作为认知服务的一部分,将为您解决所有这些问题。

这并不能解决将文本转换为数值的需求,但它会使从文本中提取语义变得更加容易。一个例子就是使用认知服务作为额外的特征工程步骤,执行关键词提取或情感分析,而不是实现自己的 NLP 流程。

让我们实现一个函数,使用认知服务的文本分析 API 返回给定文档的情感。当你想在文本中添加额外的属性,如整体情感时,这非常棒。让我们首先设置所有需要调用认知服务 API 的参数:

import requests
region='westeurope'
language='en'
version='v3.1'
key = '<insert access key>'
url = "https://{region}.api.cognitive.microsoft.com" + \
    + "/text/analytics/{version}/sentiment".format(
           region=region, version=version)

接下来,我们定义请求的内容和元数据。我们创建一个包含单个文档和要分析的文本的payload对象:

params = {
    'showStats': False
}
headers = {
    'Content-Type': 'application/json',
    'Ocp-Apim-Subscription-Key': key
}
payload = {
    'documents': [{
        'id': '1',
        'text': 'This is some input text that I love.',
        'language': language   
    }]
}

最后,我们需要将 payload、头部和参数发送到认知服务 API:

response = requests.post(url,
                         json=payload,
                         params=params,
                         headers=headers)
result = response.json()
print(result)

之前的代码看起来与我们在第二章中看到的计算机视觉示例非常相似,即在 Azure 中选择合适的机器学习服务。事实上,它使用的是相同的 API,但只是为文本分析和在此情况下情感分析功能使用了不同的端点。让我们运行这段代码并查看输出,输出看起来与以下片段非常相似:

{
  'documents': [{
    'id': '1',
    'sentiment': 'positive',
    'confidenceScores': {
      'positive': 1.0,
      'neutral': 0.0,
      'negative': 0.0},
  ...}],
  ...
}

我们可以观察到,JSON 响应包含每个文档的情感分类(积极中性消极)以及每个类别的数字置信度分数。此外,你可以看到,生成的文档存储在一个数组中,并标记了一个id值。因此,你可以使用 ID 来标识每个文档,向此 API 发送多个文档。

使用自定义预训练的语言模型很棒,但对于标准化的文本分析,我们可以简单地重用认知服务。微软在研究和生产这些语言模型上投入了大量的资源,你可以用相对较少的费用为自己的数据管道使用这些模型。因此,如果你更喜欢使用托管服务而不是运行自己的客户 transformer 模型,你应该尝试这个文本分析 API。

摘要

在本章中,你学习了如何使用最先进的 NLP 技术对文本和分类的定名和有序数据进行预处理。

现在,你可以使用停用词去除、词形还原词干提取n-gram和计数词项出现来构建一个经典的 NLP 管道,使用词袋模型。我们使用SVD来降低结果特征向量的维度,并生成低维度的主题编码。对基于计数的词袋模型的一个重要调整是比较文档中术语的相对频率。你学习了TF-IDF函数,并可以使用它来计算一个词在文档中的重要性,与语料库相比。

在下一节中,我们探讨了Word2VecGloVe,它们是预训练的数字词嵌入字典。现在,你可以轻松地重用预训练的词嵌入,在商业 NLP 应用中实现显著的改进和准确性,这得益于词的语义嵌入。

最后,我们通过研究一种最先进的方法来结束这一章节,该方法使用端到端语言表示,例如 BERT 和基于 BERT 的架构,这些架构被训练为序列到序列模型。这些模型的好处是你可以重用编码器将一系列文本转换为数值表示,这在特征提取过程中是一个非常常见的任务。

在下一章中,我们将探讨如何使用 Azure Machine Learning 训练一个机器学习模型,应用我们迄今为止所学的一切。

第八章:第八章:Azure 机器学习管道

在上一章中,我们学习了高级预处理技术,如类别嵌入和 NLP,从文本特征中提取语义意义。在本章中,你将学习如何使用这些预处理和转换技术来构建可重用的机器学习管道。

首先,你将了解将你的代码分解成单个步骤并将它们包装成管道的好处。不仅可以通过模块化和参数化使你的代码块可重用,而且还可以控制单个步骤的计算目标。这有助于优化你的计算,节省成本,并同时提高性能。最后,你可以通过 HTTP 端点或通过定期或反应式调度来参数化和触发你的管道。

然后,我们将分几个步骤构建一个复杂的 Azure 机器学习管道。我们将从一个简单的管道开始,添加数据输入、输出以及步骤之间的连接,并将管道作为 Web 服务部署。你还将了解基于频率和变化数据的先进调度,以及如何并行化管道步骤以处理大量数据。

在最后一部分,你将学习如何将 Azure 机器学习管道集成到其他 Azure 服务中,例如 Azure 机器学习设计器、Azure 数据工厂和 Azure DevOps。这将帮助你了解不同管道和工作流程服务之间的共性和差异,以及你如何触发机器学习管道。

在本章中,我们将涵盖以下主题:

  • 在机器学习工作流程中使用管道

  • 构建和发布机器学习管道

  • 将管道与其他 Azure 服务集成

技术要求

在本章中,我们将使用以下 Python 库和版本来创建管道和管道步骤:

  • azureml-core 1.34.0

  • azureml-sdk 1.34.0

与前面的章节类似,你可以使用本地 Python 解释器或托管在 Azure 机器学习中的笔记本环境运行此代码。然而,所有脚本都需要在 Azure 中安排执行。

本章中所有的代码示例都可以在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Mastering-Azure-Machine-Learning-Second-Edition/tree/main/chapter08

在机器学习工作流程中使用管道

将你的工作流程分解成可重用和可配置的步骤,并将这些步骤组合成一个端到端管道,为实施端到端机器学习过程提供了许多好处。多个团队可以拥有并迭代单个步骤以改进管道,同时其他人可以轻松地将管道的每个版本集成到他们的当前设置中。

管道本身不仅将代码与执行分开,还将执行与编排分开。因此,你可以配置单个计算目标,用于优化你的执行并提供并行执行,而你无需接触 ML 代码。

我们将快速浏览 Azure 机器学习管道,并探讨为什么它们是实现 Azure 中 ML 工作流程的首选工具。在下一节“构建和发布 ML 管道”中,我们将深入探讨通过构建这样一个管道来探索其单个功能。

为什么构建管道?

对于一个主要进行实验、同时处理数据、基础设施和建模的单个开发者来说,管道并没有给开发者的工作流程带来很多好处。然而,一旦你在多个团队中进行企业级开发,这些团队在不同的 ML 系统部分进行迭代,那么你将极大地从将代码拆分为单个执行步骤的管道中受益。

这种模块化将为你带来极大的灵活性,多个团队能够高效协作。当你在迭代和构建管道的新版本时,团队能够集成你的模型和管道。通过使用版本化管道和管道参数,你可以控制如何调用你的数据或模型服务管道,并确保审计和可重复性。

使用工作流而不是在单个文件中运行所有内容的另一个重要好处是执行速度和成本的改进。你可以在不同的计算目标上单独运行和扩展步骤,而不是在同一个计算实例上运行单个脚本。这让你对潜在的成本节约有更大的控制,并能够更好地优化性能,你只需重试管道中失败的部分,而无需重试整个管道。

通过调度管道,你可以确保所有管道运行都无需手动干预即可执行。你只需定义触发器,例如新训练数据的存在,以执行你的管道。将代码执行与触发执行解耦为你带来了许多好处,例如轻松集成到许多其他服务中。

最后,你代码的模块化特性使得代码具有很高的可重用性。通过将脚本拆分为功能步骤,如清理、预处理、特征工程、训练和超参数调整,你可以为其他项目版本化和重用这些步骤。

因此,一旦你想从这些优势中获益,你就可以开始组织你的代码到管道中,这些管道可以部署、调度、版本化、扩展和重用。让我们看看如何在 Azure 机器学习中实现这一点。

Azure 机器学习管道是什么?

Azure 机器学习流水线是 Azure 机器学习中的可执行步骤工作流程,它构成了完整的机器学习工作流程。因此,您可以将数据导入、数据转换、特征工程、模型训练、优化以及部署作为流水线步骤。

流水线是您 Azure 机器学习工作空间中的资源,您可以创建、管理、版本控制、触发和部署。它们与所有其他 Azure 机器学习工作空间资源集成,例如用于加载数据的数据集和数据存储、计算实例、模型和端点。每个流水线运行都在您的 Azure 机器学习工作空间中作为一个实验执行,并为您提供我们在上一章中介绍过的相同好处,例如在灵活的计算集群上运行时跟踪文件、日志、模型、工件和图像。

在实现灵活和可重用的机器学习工作流程时,Azure 机器学习流水线应该是您的首选。通过使用流水线,您可以模块化代码为功能块和版本,并将这些块与其他项目共享。这使得与其他团队协作进行复杂的端到端机器学习工作流程变得容易。

Azure 机器学习流水线的另一个伟大集成是与工作空间中的端点和触发器的集成。通过一行代码,您可以将流水线发布为 Web 服务或 Web 服务端点,并使用此端点从任何地方配置和触发流水线。这为将 Azure 机器学习流水线与其他许多 Azure 和第三方服务集成打开了大门。

然而,如果您需要一个更复杂的触发器,例如基于源数据变化的持续调度或响应式触发,您也可以轻松地配置这些功能。使用流水线的额外好处是,所有编排功能都与您的训练代码完全解耦。

正如您所看到的,使用 Azure 机器学习流水线为您的机器学习工作流程带来了许多好处。然而,值得注意的是,这项功能确实带来了一些额外的开销,即在每个计算步骤中包装流水线步骤,添加流水线触发器,为每个步骤配置环境和计算目标,以及将参数作为流水线选项公开。让我们从构建我们的第一个流水线开始。

构建和发布机器学习流水线

让我们继续使用之前章节中学到的所有知识,构建一个数据处理流水线。我们将使用 Azure 机器学习 SDK for Python 定义所有流水线步骤为 Python 代码,以便它可以轻松管理、审查和作为编写脚本存入版本控制。

我们将定义一个管道为一系列管道步骤的线性序列。每个步骤都将有一个输入和输出,分别定义为管道数据汇和源。每个步骤都将关联到一个计算目标,该目标定义了执行环境以及执行所需的计算资源。我们将设置一个执行环境,作为一个包含所有必需 Python 库的 Docker 容器,并在 Azure Machine Learning 的训练集群上运行管道步骤。

管道作为你的 Azure Machine Learning 工作空间中的一个实验运行。我们可以将管道作为创作脚本的一部分提交,将其部署为 Web 服务并通过 webhook 触发,将其作为已发布的管道进行安排,类似于 cron 作业,或者从第三方服务(如 Logic Apps)触发。

在许多情况下,运行线性顺序的管道已经足够好。然而,当数据量增加且管道步骤变得越来越慢时,我们需要找到一种方法来加速这些大型计算。加快数据转换、模型训练和评分的常见解决方案是通过并行化。因此,我们将向我们的数据转换管道添加一个并行执行步骤。

正如我们在本章的第一节中学到的,将 ML 工作流程解耦到管道中的主要原因是模块化和可重用性。通过将工作流程拆分为单个步骤,我们为常见 ML 任务的可重用计算块奠定了基础,无论是通过可视化和特征重要性进行数据分析,还是通过 NLP 和第三方数据进行特征工程,或者简单地评分常见的 ML 任务,如通过目标检测进行自动图像标记。

在 Azure Machine Learning 管道中,我们可以使用模块从管道创建可重用的计算步骤。模块是在管道步骤之上的一个管理层,它允许你轻松地对管道步骤进行版本控制、部署、加载和重用。这个概念与对 ML 项目中的源代码或数据集进行版本控制非常相似。

对于任何企业级 ML 工作流程,管道的使用是必不可少的。它不仅帮助你解耦、扩展、触发和重用单个计算步骤,而且还为你的端到端工作流程提供了可审计性和可监控性。此外,将计算块拆分为管道步骤将为你成功过渡到 MLOps——ML 项目的**持续集成和持续部署(CI/CD)**过程打下基础。

让我们开始并实现我们的第一个 Azure Machine Learning 管道。

创建一个简单的管道

Azure 机器学习管道是一系列可以并行或顺序执行的独立计算步骤。Azure 机器学习在管道之上提供了额外的功能,例如计算图的可视化、步骤之间的数据传输以及将管道作为端点或已发布管道发布。在本节中,我们将创建一个简单的管道步骤并执行管道以探索 Azure 机器学习管道的功能。

根据计算类型,您可以在不同的计算目标上安排作业,例如 Azure 机器学习、Azure Batch、Databricks、Azure Synapse 等,或者运行自动机器学习HyperDrive实验。根据执行类型,您需要为每个步骤提供额外的配置。

让我们从只包含一个步骤的简单管道开始。我们将在后续章节中逐步添加更多功能和步骤。首先,我们需要定义我们的管道步骤的执行类型。虽然PipelineStep是管道中可以运行的任何执行的基类,但我们需要选择一个步骤实现。以下是在编写时可用的一些步骤:

  • AutoMLStep: 运行一个自动机器学习实验

  • AzureBatchStep: 在 Azure Batch 上运行一个脚本

  • DatabricksStep: 运行一个 Databricks 笔记本

  • DataTransferStep: 在 Azure 存储账户之间传输数据

  • HyperDriveStep: 运行一个 HyperDrive 实验

  • ModuleStep: 运行一个模块

  • MpiStep: 运行一个**消息传递接口 (MPI)**作业

  • ParallelRunStep: 并行运行一个脚本

  • PythonScriptStep: 运行一个 Python 脚本

  • RScriptStep: 运行一个 R 脚本

  • SynapseSparkStep: 在 Synapse 上运行一个 Spark 脚本

  • CommandStep: 运行一个脚本或命令

  • KustoStep: 在 Azure 数据探索器上运行一个 Kusto 查询

在我们的简单示例中,我们想在管道中运行一个单独的 Python 数据预处理脚本,因此我们将从前面列表中选择PythonScriptStep。我们正在构建与我们在前面的章节中看到的相同示例和代码示例。在这个第一个管道中,我们将执行一个步骤,该步骤将直接从脚本中加载数据——因此不需要将任何输入或输出传递给管道步骤。我们将在以下步骤中单独添加这些:

  1. 管道步骤都附加到 Azure 机器学习工作区。因此,我们首先加载工作区配置:

    from azureml.core import Workspace
    ws = Workspace.from_config()
    
  2. 接下来,我们需要一个计算目标,我们可以在其上执行我们的管道步骤。让我们创建一个自动扩展的 Azure 机器学习训练集群作为计算目标,类似于我们在前面的章节中创建的:

    # Create or get training cluster
    aml_cluster = get_aml_cluster(
      ws, cluster_name="cpu-cluster")
    aml_cluster.wait_for_completion(show_output=True)
    
  3. 此外,我们还需要一个运行配置,它定义了我们的训练环境和 Python 库:

    run_conf = get_run_config(['numpy', 'pandas',
      'scikit-learn', 'tensorflow'])
    
  4. 现在,我们可以定义PythonScriptStep,它为目标的机器学习训练脚本提供了所有必需的配置和入口点:

    from azureml.pipeline.steps import PythonScriptStep
    step = PythonScriptStep(name='Preprocessing',
                            script_name="preprocess.py",
                            source_directory="code",
                            runconfig=run_conf,
                            compute_target=aml_cluster)
    

如您在前面代码中所见,我们正在配置script_name和包含预处理脚本的source_directory参数。我们还传递了runconfig运行时配置和compute_target计算目标到PythonScriptStep

  1. 如果您还记得前面的章节,我们之前将ScriptRunConfig对象作为实验提交到 Azure 机器学习工作区。在管道的情况下,我们首先需要将管道步骤包装在Pipeline中,然后将管道作为实验提交。虽然一开始这似乎有些反直觉,但我们将看到我们如何参数化管道并添加更多的计算步骤。在下一个代码片段中,我们定义了管道:

    from azureml.pipeline.core import Pipeline
    pipeline = Pipeline(ws, steps=[step])
    

如您所见,管道通过一系列管道步骤简单地定义,并与工作区相关联。在我们的例子中,我们只定义了一个执行步骤。让我们也检查一下,我们是否在配置管道时没有犯任何错误,通过内置的管道验证:

pipeline.validate()
  1. 一旦管道成功验证,我们就可以准备执行了。可以通过将管道作为实验提交到 Azure 机器学习工作区来执行管道:

    from azureml.core import Experiment
    exp = Experiment(ws, "azureml-pipeline")
    run = exp.submit(pipeline)
    

恭喜!您刚刚运行了第一个非常简单的 Azure 机器学习管道。

重要提示

您可以在官方 Azure 存储库中找到许多使用 Azure 机器学习管道的完整和最新示例:github.com/Azure/MachineLearningNotebooks/blob/master/how-to-use-azureml/machine-learning-pipelines

一旦提交了管道,它将在管道部分以及实验部分中显示,如图 8.1 所示。管道被视为一个实验,其中每个管道运行就像一个实验运行。管道的每个步骤,以及其日志、图表和指标,都可以作为实验的子运行来访问:

图 8.1 – Azure 机器学习中的管道运行作为实验

图 8.1 – Azure 机器学习中的管道运行作为实验

虽然这个简单的管道在直接将脚本作为实验提交时并没有带来很多好处,但现在我们可以向管道中添加额外的步骤并配置数据输入和输出。让我们看看吧!

在步骤之间连接数据输入和输出

管道步骤是计算块,而管道定义了步骤执行的顺序。为了控制数据流,我们需要为管道定义输入和输出,并为单个步骤连接数据输入和输出。计算块之间的数据流最终将定义块的执行顺序,从而将一系列步骤转换为一个有向无环执行图。这正是我们将在本节中探讨的内容。

在大多数情况下,管道需要外部输入、各个块之间的连接以及持久化输出。在 Azure Machine Learning 管道中,我们将使用以下构建块来配置此数据流:

  • 预持久化管道输入:Dataset

  • 管道步骤之间的数据:PipelineData

  • 持续管道输出:PipelineData.as_dataset()

在本节中,我们将查看所有三种类型的数据输入和输出。首先,我们将查看如何将数据作为输入传递到管道中。

管道步骤的输入数据

让我们从向管道的第一步添加数据输入开始。为此 – 或者将任何预持久化数据传递给管道步骤 – 我们将使用数据集,这在第四章数据摄取和管理数据集中我们已经看到。在 Azure Machine Learning 中,数据集是在指定路径上存储的指定编码数据的抽象引用。存储系统本身被抽象为数据存储对象,它是物理系统的引用,包含有关位置、协议和访问权限的信息。

如果您还记得前面的章节,我们可以通过简单地按名称引用来访问之前在 Azure Machine Learning 工作区中注册的数据集:

from azureml.core.dataset import Dataset
dataset = Dataset.get_by_name(ws, 'titanic')

当数据最初被组织并注册为数据集时,前面的代码非常方便。作为管道开发者,我们不需要知道底层的数据格式(例如,CSV、ZIP、Parquet 和 JSON),以及数据存储在哪个 Azure Blob 存储或 Azure SQL 数据库上。管道开发者可以消费指定的数据,并专注于预处理、特征工程和模型训练。

然而,当将新数据传递到 Azure Machine Learning 管道时,我们通常没有将数据注册为数据集。在这些情况下,我们可以创建一个新的数据集引用。以下是如何从公开数据创建Dataset的示例:

path ='https://...windows.net/demo/Titanic.csv'
dataset = Dataset.Tabular.from_delimited_files(path)

将文件和表格数据转换为Dataset有多种方式。虽然这看起来像是额外的复杂工作,而不是直接将绝对路径传递给管道,但遵循此约定将带来许多好处。最重要的是,Azure Machine Learning 工作区中的所有计算实例都将能够访问、读取和解析数据,而无需任何额外配置。此外,Azure Machine Learning 将引用和跟踪每个实验运行所使用的数据集。

一旦我们获得了Dataset的引用,我们就可以将数据集作为输入传递给管道步骤。当将数据集传递给计算步骤时,我们可以配置以下附加配置:

  • 在脚本中对数据集引用的名称 – as_named_input()

  • FileDataset的访问类型 – as_download()as_mount()

首先,我们将表格数据集配置为命名输入:

from azureml.core.dataset import Dataset
dataset = Dataset.get_by_name(ws, 'titanic')
data_in = dataset.as_named_input('titanic')

接下来,我们将使用 PythonScriptStep,这将允许我们将参数传递给管道步骤。我们需要将数据集传递给两个参数——作为管道脚本的参数以及作为步骤的输入依赖。前者将允许我们将数据集传递给 Python 脚本,而后者将跟踪数据集作为此管道步骤的依赖项:

from azureml.pipeline.steps import PythonScriptStep
step = PythonScriptStep(name='Preprocessing',
                        script_name="preprocess_input.py",
                        source_directory="code",
                        arguments=["--input", data_in],
                        inputs=[data_in],
                        runconfig=run_conf,
                        compute_target=aml_cluster)

如前述示例所示,我们可以将一个(或多个)数据集作为inputs参数传递给管道步骤,以及作为脚本的参数。为这个数据集指定一个特定的名称将帮助我们区分管道中的多个输入。我们将更新预处理脚本以从命令行参数解析数据集,如下面的代码片段所示:

preprocess_input.py

import argparse
from azureml.core import Run, Dataset
run = Run.get_context()
ws = run.experiment.workspace
parser = argparse.ArgumentParser()
parser.add_argument("--input", type=str)
args = parser.parse_args()
dataset = Dataset.get_by_id(ws, id=args.input)
df = dataset.to_pandas_dataframe()

如前述代码所示,数据集作为数据集名称传递给 Python 脚本。我们可以使用 Dataset API 在运行时检索数据。

一旦我们提交管道以执行,我们可以在 Azure Machine Learning Studio 界面中看到管道的可视化,如图 8.2 所示。我们可以看到数据集作为titanic命名的输入传递给预处理步骤:

图 8.2 – 数据集作为管道步骤输入

图 8.2 – 数据集作为管道步骤输入

这是一种将功能块与其输入解耦的绝佳方式。我们将在下一节,通过模块化重用管道步骤中看到,如何将这些可重用块转换为共享模块。

重要提示

除了将数据集作为输入参数传递给管道步骤外,我们还可以使用运行上下文对象上的以下属性从运行上下文中访问命名输入——Run.get_context().input_datasets['titanic']。然而,将数据集设置为输入和输出参数可以更容易地在管道和其他实验中重用管道步骤和代码片段。

接下来,让我们了解如何设置单个管道步骤之间的数据流。

步骤间传递数据

当我们为管道步骤定义输入时,我们通常希望配置计算输出的输出。通过传递输入和输出定义,我们将管道步骤与预定义的数据存储分离,并避免在计算步骤中移动数据。

虽然预先持久化的输入被定义为Dataset对象,但管道步骤之间的数据连接(输入和输出)是通过PipelineData对象定义的。让我们看看一个PipelineData对象作为某个管道步骤的输出和另一个步骤的输入的示例:

from azureml.core import Datastore
from azureml.pipeline.core import PipelineData
datastore = Datastore.get(ws, datastore_name="mldata")
data_train = PipelineData('train', datastore=datastore)
data_test = PipelineData('test', datastore=datastore)

与前一个示例类似,我们将数据集作为参数传递,并作为outputs引用它们。前者将允许我们在脚本中检索数据集,而后者定义了步骤依赖项:

from azureml.pipeline.steps import PythonScriptStep
step_1 = PythonScriptStep(name='Preprocessing',
                          script_name= \
                            "preprocess_output.py",
                          source_directory="code",
                          arguments=[
                              "--input", data_in,
                              "--out-train", data_train,
                              "--out-test", data_test],
                          inputs=[data_in],
                          outputs=[data_train, data_test],
                          runconfig=run_conf,
                          compute_target=aml_cluster)

一旦我们将预期的输出路径传递给评分文件,我们需要解析命令行参数以检索路径。评分文件看起来如下,以便读取输出路径并将 pandas DataFrame 输出到期望的位置。我们首先需要在训练脚本中解析命令行参数:

preprocess_output.py

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--input", type=str)
parser.add_argument("--out-train", type=str)
parser.add_argument("--out-test", type=str)
args = parser.parse_args()

PipelineData参数在运行时被解释,并用挂载的数据集目录的本地路径替换。因此,我们可以简单地写入这个本地目录,数据将被自动注册到数据集中:

preprocess_output.py

import os
out_train = args.out_train
os.makedirs(os.path.dirname(out_train), exist_ok=True)
out_test = args.out_test
os.makedirs(os.path.dirname(out_test), exist_ok=True)
df_train, df_test = preprocess(...)
df_train.to_csv(out_train)
df_test.to_csv(out_test)

一旦我们将数据输出到PipelineData数据集,我们就可以将这些数据集传递给下一个管道步骤。传递数据集的方式与我们之前看到的完全相同 – 我们将它们作为参数传递并注册为inputs

from azureml.pipeline.steps import PythonScriptStep
step_2 = PythonScriptStep(name='Training',
                          script_name="train.py",
                          source_directory="code",
                          arguments=[
                              "--in-train", data_train,
                              "--in-test", data_test],
                          inputs=[data_train, data_test],
                          runconfig=run_conf,
                          compute_target=aml_cluster)

现在,我们可以在训练脚本中加载数据。如果你还记得前一个步骤,PipelineData在本地执行环境中被解释为路径。因此,我们可以从命令行参数中解释的路径读取数据:

train.py

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--in-train", type=str)
parser.add_argument("--in-test", type=str)
args = parser.parse_args()
...
df_train = pd.read_csv(args.in_train)
df_test = pd.read_csv(args.in_test)

最后,我们可以通过使用steps关键字传递步骤来将这两个步骤包装成一个Pipeline对象。这个pipeline对象可以被传递给 Azure Machine Learning 作为一个实验:

from azureml.pipeline.core import Pipeline
pipeline = Pipeline(ws, steps=[step_1, step_2])

如前一个示例所示,我们可以从命令行参数中读取输出路径,并在 Python 脚本中将其用作标准文件路径。因此,我们需要确保文件路径存在,并将一些表格数据输出到该位置。接下来,我们定义第二个验证步骤的输入,该步骤读取新创建的数据:

图 8.3 – 在管道步骤间传递数据

图 8.3 – 在管道步骤间传递数据

最后,我们将探讨如何将管道步骤的输出持久化以供管道外使用。

持久化数据输出

在本节的最后,我们将学习如何持久化管道的输出数据。对于管道来说,一个常见的任务是构建数据转换 – 因此我们通常期望管道输出数据。

在前一节中,我们学习了如何使用PipelineData从管道步骤创建输出,主要是为了将这些输出连接到后续步骤的输入。我们可以使用相同的方法来定义管道的最终持久化输出。

一旦你理解了如何创建、持久化和版本化数据集,这样做就非常简单了。原因是我们可以使用as_dataset()方法将PipelineData对象转换为数据集。一旦我们有了Dataset对象的引用,我们就可以继续将其导出到特定的数据存储或在工作区中将其注册为数据集。

这里是一个如何将作为管道步骤输出的PipelineData对象转换为数据集并在 Azure Machine Learning 工作区中注册的示例:

from azureml.data import OutputFileDatasetConfig
data_out = OutputFileDatasetConfig(name="predictions", 
  destination=(datastore, 'titanic/predictions')) 

通过调用前面的作者代码,您将能够将结果预测作为数据集访问任何与您的工作空间连接的计算实例:

图 8.4 – 作为管道步骤输出的数据集

图 8.4 – 作为管道步骤输出的数据集

接下来,我们将探讨触发管道执行的不同方法。

发布、触发和安排管道

在您创建了第一个简单的管道之后,您有多种运行管道的方式。我们已经看到的一个例子是将管道作为实验提交给 Azure Machine Learning。这将简单地从配置管道的同一作者脚本中执行管道。虽然这最初执行管道是一个好的开始,但还有其他触发、参数化和执行管道的方法。

执行管道的常见方法如下:

  • 将管道发布为 Web 服务。

  • 使用 webhook 触发已发布的管道。

  • 安排管道定期运行。

在本节中,我们将探讨所有三种方法,以帮助您轻松触发和执行您的管道。让我们首先从将管道作为 Web 服务发布和版本化开始。

将管道发布为 Web 服务

将机器学习工作流程拆分成可重用管道的常见原因是你可以根据需要对其进行参数化和触发,以执行各种任务。好的例子包括常见的预处理任务、特征工程步骤和批量评分。

因此,将管道转变为可参数化的 Web 服务,我们可以从任何其他应用程序中触发它,这是一种很好的部署我们的机器学习工作流程的方式。让我们开始,并将之前构建的管道作为 Web 服务进行打包和部署。

由于我们希望我们的已发布管道可以通过 HTTP 参数进行配置,我们需要首先创建这些参数引用。让我们创建一个参数来控制训练管道的学习率:

from azureml.pipeline.core.graph import PipelineParameter
lr_param = PipelineParameter(name="lr_arg",
                             default_value=0.01)

接下来,我们将通过将参数作为训练脚本的参数传递来将管道参数与管道步骤链接起来。我们扩展了上一节中的步骤:

data = mnist_dataset.as_named_input('mnist').as_mount()
args = ["--in-train", data, "--learning-rate", lr_param]
step = PythonScriptStep(name='Training',
	script_name="train.py",
	source_directory="code",
	arguments=args,
	inputs=[data_train],
	runconfig=run_conf,
	compute_target=aml_cluster)
                     arguments=args ,
                     estimator=estimator,
                     compute_target=cpu_cluster)

在前面的示例中,我们将学习率作为一个参数添加到了命令行参数列表中。在训练脚本中,我们可以解析命令行参数并读取参数:

score.py

parser = argparse.ArgumentParser()
parser.add_argument('--learning-rate', type=float, 
  dest='lr')
args = parser.parse_args()
# print learning rate 
print(args.lr)

现在,唯一剩下的步骤就是发布管道。为此,我们创建一个管道并调用publish()方法。我们需要为管道传递一个名称和版本,这样它现在将是一个版本化的已发布管道:

pipeline = Pipeline(ws, steps=[step])
service = pipeline.publish(name="CNN_Train_Service",
                           version="1.0")
service_id = service.id
service_endpoint = service.endpoint

这就是您需要公开的代码,以将管道作为具有身份验证的参数化 Web 服务。如果您想将您的已发布管道从特定的端点抽象出来——例如,在迭代管道的开发过程的同时,让其他团队将 Web 服务集成到他们的应用程序中——您还可以部署管道 webhooks 作为端点。

让我们看看一个示例,其中我们使用先前创建的管道服务并通过单独的端点公开它:

from azureml.pipeline.core import PipelineEndpoint
application = PipelineEndpoint.publish(ws,
  pipeline=service,
  name="CNN_Train_Endpoint")
service_id = application.id
service_endpoint = application.endpoint

我们已经部署并解耦了管道和管道端点。我们最终可以通过服务端点调用和触发端点。让我们在下一节中看看这个例子。

使用 webhook 触发已发布的管道

已发布的管道 web 服务需要认证。因此,在我们调用 web 服务之前,让我们首先检索一个 Azure Active Directory 令牌:

from azureml.core.authentication import AzureCliAuthentication
cli_auth = AzureCliAuthentication()
aad_token = cli_auth.get_authentication_header()

使用认证令牌,我们现在可以通过调用服务端点来触发和参数化管道。让我们通过使用requests库来查看一个示例。我们可以通过在上一节中定义的lr_arg参数配置学习率,并通过发送自定义 JSON 体来设置实验名称。如果你还记得,管道仍然会在你的 Azure 机器学习工作区中以实验的形式运行:

import requests
response = requests.post(service_endpoint,
  headers=aad_token,
  json={"ExperimentName": "mnist-train",
        "ParameterAssignments": {"lr_arg": 0.05}})

在前面的代码片段中,我们可以看到我们使用POST请求调用管道 webhook,并通过发送自定义 JSON 体来配置管道运行。对于认证,我们还需要通过 HTTP 头传递认证信息。

在这个例子中,我们使用 Python 脚本来触发 web 服务端点。然而,你现在可以通过 webhook 使用任何其他 Azure 服务来触发此管道,例如 Azure Logic Apps、Azure DevOps 中的 CI/CD 管道或任何其他自定义应用程序。如果你希望你的管道定期运行而不是手动触发,你可以设置管道计划。让我们在下一节中看看这个例子。

安排已发布的管道

在构建管道时,为工作流设置连续触发器是一个常见用例。这些触发器可以在每周或每天运行管道并重新训练模型,如果可用新数据。Azure 机器学习管道支持两种类型的调度技术——通过预定义频率的连续调度,以及通过轮询间隔的响应式调度和数据变更检测。在本节中,我们将探讨这两种方法。

在我们开始安排管道之前,我们将首先探索一种列出工作区中所有先前定义的管道的方法。为此,我们可以使用PublishedPipeline.list()方法,类似于我们的 Azure 机器学习工作区资源中的list()方法。让我们打印工作区中每个已发布管道的名称和 ID:

from azureml.pipeline.core import PublishedPipeline
for pipeline in PublishedPipeline.list(ws):
  print("name: %s, id: %s" % (pipeline.name, pipeline.id))

要为已发布的管道设置计划,我们需要将管道 ID 作为参数传递。我们可以从前面的代码片段中检索所需的管道 ID 并将其插入到计划声明中。

首先,我们将探讨连续计划,这些计划会以预定义的频率重新触发管道,类似于 cron 作业。为了定义计划频率,我们需要创建一个ScheduleRecurrence对象。以下是一个创建重复计划的示例片段:

from azureml.pipeline.core.schedule import \
  ScheduleRecurrence, Schedule
recurrence = ScheduleRecurrence(frequency="Minute", 
                                interval=15)
schedule = Schedule.create(ws, 
                           name="CNN_Train_Schedule", 
                           pipeline_id=pipeline_id,
                           experiment_name="mnist-train", 
                           recurrence=recurrence, 
                           pipeline_parameters={})

上述代码就是设置一个持续触发你的管道的重复调度的全部所需。管道将在你的 Azure Machine Learning 工作区中定义的实验中运行。使用pipeline_parameters参数,你可以将额外的参数传递给管道运行。

Azure Machine Learning 管道还支持另一种类型的重复调度,即轮询数据存储库中的更改。这种类型的调度被称为反应式调度,需要连接到数据存储库。它将在你的数据存储库中的数据更改时触发你的管道。以下是一个设置反应式调度的示例:

from azureml.core.datastore import Datastore
# use default datastore 'ws.get_default_datastore()'
# or load a custom registered datastore
datastore = Datastore.get(workspace, 'mldemodatastore')
# 5 min polling interval
polling_interval = 5
schedule = Schedule.create(
    ws, name="CNN_Train_OnChange", 
    pipeline_id=pipeline_id,
    experiment_name="mnist-train",
    datastore=datastore,
    data_path_parameter_name="mnist"
    polling_interval=polling_interval,
    pipeline_parameters={})

如此例所示,我们使用数据存储库引用和分钟级的轮询间隔来设置反应式调度。因此,调度将检查每个轮询间隔,以查看是否有任何 blob 已更改,并使用这些更改来触发管道。blob 名称将通过data_path_parameter_name参数传递给管道。类似于之前的调度,你也可以使用pipeline_parameters参数将额外的参数发送到管道。

最后,让我们看看如何一旦启用就程序性地停止一个调度。为此,我们需要一个调度对象的引用。我们可以通过获取特定工作区的调度来获取这个引用,类似于 Azure Machine Learning 中的任何其他资源:

for schedule in Schedule.list(ws):
  print(schedule.id)

我们可以使用调度对象上所有可用的属性来过滤这个列表。一旦我们找到了所需的调度,我们只需简单地禁用它:

schedule.disable(wait_for_provisioning=True)

使用额外的wait_for_provisioning参数,我们确保在调度真正禁用之前阻塞代码执行。你可以使用Schedule.enable方法轻松重新启用调度。现在,你可以创建重复和反应式调度,持续运行你的 Azure Machine Learning 管道,并在不再需要时禁用它们。接下来,我们将看看如何并行化执行步骤。

并行化步骤以加快大型管道

在许多情况下,随着时间的推移,管道不可避免地会处理越来越多的数据。为了并行化管道,你可以并行或顺序运行管道步骤,或者通过使用ParallelRunConfigParallelRunStep来并行化单个管道步骤的计算。

在我们深入讨论单个步骤执行的并行化之前,让我们首先讨论简单管道的控制流。我们将从一个使用多个步骤构建的简单管道开始,如下面的示例所示:

pipeline = Pipeline(ws, steps=[step1, step2, step3, step4])

当我们提交此管道时,这四个步骤将如何执行——是顺序执行、并行执行,还是甚至是无定义的顺序?为了回答这个问题,我们需要查看各个步骤的定义。如果所有步骤都是独立的,并且每个步骤的计算目标足够大,则所有步骤都会并行执行。然而,如果我们定义PipelineDatastep1的输出并将其输入到其他步骤中,则这些步骤只有在step1完成后才会执行:

图 8.5 – 带有并行步骤的管道

图 8.5 – 带有并行步骤的管道

管道步骤之间的数据连接隐式定义了步骤的执行顺序。如果没有步骤之间存在依赖关系,则所有步骤都会并行安排。

上述说法有一个例外,即在没有专用数据对象作为依赖项的情况下强制执行管道步骤的特定执行顺序。为了做到这一点,你可以手动定义这些依赖项,如下面的代码片段所示:

step3.run_after(step2)
step4.run_after(step3)

上述配置将首先并行执行step1step2,然后再安排step3,这要归功于你明确配置的依赖项。这在当你访问 Azure 机器学习工作区外的资源中的状态或数据时非常有用;因此,管道不能隐式创建依赖项:

图 8.6 – 带有自定义步骤顺序的管道

图 8.6 – 带有自定义步骤顺序的管道

一旦我们解决了步骤执行顺序的问题,我们想要了解如何并行执行单个步骤,而不是多个步骤。这个用例非常适合批量评分大量数据。你不想将输入数据分割成多个步骤的输入,而是希望将数据作为单个步骤的输入。然而,为了加快评分过程,你希望对单个步骤的评分进行并行执行。

在 Azure 机器学习管道中,你可以使用ParallelRunStep步骤来配置单个步骤的并行执行。为了配置数据分区和计算的并行化,你需要创建一个ParallelRunConfig对象。并行运行步骤是一个很好的选择,适用于任何类型的并行化计算,帮助我们将输入数据分割成更小的数据分区(也称为批量或小批量)。让我们通过一个示例来了解如何为单个管道步骤设置并行执行。我们将配置批量大小作为管道参数,该参数可以在调用管道步骤时设置:

from azureml.pipeline.core import PipelineParameter
from azureml.pipeline.steps import ParallelRunConfig
parallel_run_config = ParallelRunConfig(
  entry_script='score.py',
  mini_batch_size=PipelineParameter(
    name="batch_size", 
    default_value="10"),
  output_action="append_row",
  append_row_file_name="parallel_run_step.txt",
  environment=batch_env,
  compute_target=cpu_cluster,
  process_count_per_node=2,
  node_count=2)

上述代码片段定义了通过将输入分割成小批量来并行化计算的运行配置。我们将批量大小配置为管道参数batch_size。我们还通过node_countprocess_count_per_node参数配置计算目标和并行性。使用这些设置,我们可以并行评分四个小批量。

score.py 脚本是一个部署文件,需要包含一个 init()run(batch) 方法。batch 参数包含一个文件名列表,这些文件将从步骤配置的输入参数中提取出来。我们将在 第十一章超参数调整和自动化机器学习 中了解更多关于此文件结构的信息。

score.py 脚本中的 run 方法应返回评分结果或将数据写入外部数据存储。根据这一点,output_action 参数需要设置为 append_row,这意味着所有值都将收集到一个结果文件中,或者设置为 summary_only,这意味着用户将负责存储结果。您可以使用 append_row_file_name 参数定义所有行都将附加到的结果文件。

如您所见,设置并行批量执行的运行配置并不简单,需要一些调整。然而,一旦设置并正确配置,它就可以用来扩展计算步骤并并行运行多个任务。因此,我们现在可以定义具有所有必需输入和输出的 ParallelRunStep

from azureml.pipeline.steps import ParallelRunStep
from azureml.core.dataset import Dataset
parallelrun_step = ParallelRunStep(
  name="ScoreParallel",
  parallel_run_config=parallel_run_config,
  inputs=[Dataset.get_by_name(ws, 'mnist')],
  output=PipelineData('mnist_results', 
                      datastore=datastore),
  allow_reuse=True)

如您所见,我们从引用数据存储中所有文件的输入数据集中读取。我们将结果写入我们自定义数据存储中的 mnist_results 文件夹。最后,我们可以开始运行并查看结果。为此,我们将管道作为实验运行提交到 Azure Machine Learning:

from azureml.pipeline.core import Pipeline
pipeline = Pipeline(workspace=ws, steps=[parallelrun_step])
run = exp.submit(pipeline)

将步骤执行拆分为多个分区将有助于您加快大量数据的计算速度。一旦计算时间显著长于在计算目标上调度步骤执行的开销,这种方法就会带来回报。因此,ParallelRunStep 是加快您管道的好选择,只需对您的管道配置进行少量更改。接下来,我们将探讨更好的模块化和管道步骤的可重用性。

通过模块化重用管道步骤

通过将您的流程拆分为管道步骤,您正在为可重用的 ML 和数据处理构建块奠定基础。然而,而不是将您的管道、管道步骤和代码复制粘贴到其他项目中,您可能希望将您的功能抽象为功能性的高级模块。

让我们来看一个例子。假设您正在构建一个管道步骤,该步骤接受用户和项目评分的数据集,并为每个用户输出前五个项目的推荐。然而,当您正在微调推荐引擎时,您希望允许您的同事将此功能集成到他们的管道中。一种很好的方法是将代码的实现和使用分离,定义输入和输出数据格式,并对其进行模块化和版本控制。这正是模块在 Azure Machine Learning 管道步骤中的作用。

让我们创建一个模块,这个容器将包含对计算步骤的引用:

from azureml.pipeline.core.module import Module
module = Module.create(ws,
                       name="TopItemRecommender",
                       description="Recommend top 5 items")

接下来,我们使用InputPortDefOutputPortDef绑定来定义模块的输入和输出。这些是输入和输出引用,稍后需要将它们绑定到数据引用。我们使用这些绑定来抽象化所有的输入和输出:

from azureml.pipeline.core.graph import \
  InputPortDef, OutputPortDef
in1 = InputPortDef(name="in1",
                   default_datastore_mode="mount", 
                   default_data_reference_name = \
                       datastore.name,
                   label="Ratings")
out1 = OutputPortDef(name="out1",
                     default_datastore_mode="mount", 
                     default_datastore_name=datastore.name,
                     label="Recommendation")

最后,我们可以通过发布此模块的 Python 脚本来定义模块功能:

module.publish_python_script("train.py",
                             source_directory="./rec",
                             params={"numTraits": 5},
                             inputs=[in1],
                             outputs=[out1],
                             version="1",
                             is_default=True)

这就是您需要做的所有事情,以便其他人可以在他们的 Azure 机器学习管道中重用您的推荐块。通过使用版本控制和默认版本,您可以确保用户拉取的确切代码。正如我们所看到的,您可以为每个模块定义多个输入和输出,并为该模块定义可配置参数。除了以 Python 代码发布功能外,我们还可以发布 Azure Data Lake Analytics 或 Azure 批处理步骤。

接下来,我们将探讨如何将模块集成到 Azure 机器学习管道中,并与自定义步骤一起执行。为此,我们将首先使用以下命令加载之前创建的模块:

from azureml.pipeline.core.module import Module
module = Module.get(ws, name="TopItemRecommender")

现在的伟大之处在于,前面的代码将在任何具有访问您的 Azure 机器学习工作区权限的 Python 解释器或执行引擎中工作。这是一个巨大的进步——无需复制代码,无需检查依赖项,也无需为您的应用程序定义任何额外的访问权限——一切都与工作区集成在一起。

首先,我们需要编写这个管道步骤的输入和输出。让我们将管道的输入直接传递到推荐模块,并将所有输出传递到管道输出:

from azureml.pipeline.core import PipelineData
in1 = PipelineData("in1",
                   datastore=datastore, 
                   output_mode="mount", 
                   is_directory=False)
out1 = PipelineData("out1",
                    datastore=datastore, 
                    output_mode="mount", 
                    is_directory=False)
input_wiring = {"in1": in1}
output_wiring = {"out1": out1}

现在,我们使用管道参数来参数化模块。这使得我们可以在管道中配置一个参数,并将其传递到推荐模块。此外,我们还可以为在管道中使用时定义该参数的默认值:

from azureml.pipeline.core import PipelineParameter
num_traits = PipelineParameter(name="numTraits",
                               default_value=5)

我们已经定义了此管道的输入和输出,以及管道步骤的输入参数。我们唯一缺少的是将所有这些整合起来并定义一个管道步骤。类似于前一个章节,我们可以定义一个将执行模块化推荐块的管道步骤。为此,我们不再使用PythonScriptStep,而是现在使用ModuleStep

from azureml.core import RunConfiguration
from azureml.pipeline.steps import ModuleStep
step = ModuleStep(module= module,
                  version="1",
                  runconfig=RunConfiguration(),
                  compute_target=aml_compute,
                  inputs_map=input_wiring,
                  outputs_map=output_wiring,
                  arguments=[
                    "--output_sum", first_sum,
                    "--output_product", first_prod,
                    "--num-traits", num_traits])

最后,我们可以通过将管道作为实验提交到我们的 Azure 机器学习工作区来执行管道。此代码与之前章节中看到的内容非常相似:

from azureml.core import Experiment
from azureml.pipeline.core import Pipeline
pipeline = Pipeline(ws, steps=[step])
exp = Experiment(ws, "item-recommendation")
run = exp.submit(pipeline)

前一个步骤在您的 Azure 机器学习工作区中以实验的形式执行模块化管道。然而,您也可以选择之前章节中讨论的任何其他发布方法,例如作为 Web 服务发布或安排管道。

当与多个团队在同一 ML 项目上工作时,将管道步骤拆分为可重用模块非常有帮助。所有团队都可以并行工作,并且结果可以轻松地集成到单个 Azure 机器学习工作区中。让我们看看 Azure 机器学习管道如何与其他 Azure 服务集成。

将管道与其他 Azure 服务集成

用户仅使用单个服务来管理云中的数据流、实验、训练、部署和 CI/CD 是很少见的。其他服务提供特定的功能,使它们更适合特定任务,例如,Azure Data Factory 用于将数据加载到 Azure 中,Azure Pipelines 用于在 Azure DevOps 中运行自动化任务。

选择云提供商的最有力的论据是其个别服务的强大集成。在本节中,我们将看到 Azure 机器学习管道如何与其他 Azure 服务集成。如果我们要涵盖所有可能的集成服务,这个列表将会很长。正如我们在本章中学到的,您可以通过调用 REST 端点并使用标准 Python 代码提交管道来触发发布的管道。这意味着您可以在任何可以调用 HTTP 端点或运行 Python 代码的地方集成管道。

我们首先将探讨与 Azure 机器学习设计师的集成。设计师允许您通过拖放界面构建管道,这些管道、发布的管道和管道运行将像我们在本章中构建的任何其他管道一样显示在工作区中。因此,快速查看共同点和差异是实用的。

接下来,我们将快速查看 Azure 机器学习管道与 Azure Data Factory 的集成,这可能是最常用的集成之一。将 ML 管道与 ETL 管道一起包含,用于在 ETL 过程中对数据进行评分、丰富或增强,这是一种非常自然的本能。

最后,我们将比较 Azure 机器学习管道与 Azure DevOps 中的 Azure Pipelines 用于 CI/CD。虽然 Azure DevOps 主要用于应用程序代码和应用程序编排,但它现在正在过渡到提供完整的端到端 MLOps 工作流程。让我们从设计师开始,直接进入正题。

使用 Azure 机器学习设计师构建管道

Azure 机器学习设计师是一个图形界面,通过拖放界面创建复杂的 ML 管道。您可以选择表示为块的函数来导入数据,这些块将使用底层的存储库和数据集。

下图显示了一个简单的管道,用于训练和评分一个提升决策树回归模型。如您所见,基于块的编程风格需要较少的关于单个块的知识,并且它允许您在不编写任何代码的情况下构建复杂的管道:

图 8.7 – Azure 机器学习设计师管道

图 8.7 – Azure Machine Learning 设计器管道

一些操作,例如将一个计算的结果连接到下一个计算的输入,在视觉 UI 中创建可能比使用代码更方便。通过可视化管道也更容易理解数据流。其他操作,例如创建大型数据批次的并行执行,在代码中处理和维护可能更容易一些。然而,由于我们首先使用代码的方法来保证可重复性、可测试性和版本控制,我们通常更倾向于使用代码进行编写和执行。

值得注意的是,设计器中的管道和代码中使用的管道的功能并不相同。虽然你有一系列预先配置的抽象功能块,例如之前图 8.7中的Boosted Decision Tree Regression块,但你无法在代码中访问这些功能。然而,你可以使用 scikit-learn、PyTorch、TensorFlow 等来重用现有功能或在代码中构建自己的功能。

由于设计器与工作空间的一级集成,你可以在设计器内部访问工作空间中的所有文件、模型和数据集。一个重要的启示是,在工作空间中创建的所有资源,如管道、发布的管道、实时端点、模型、数据集等,都存储在公共系统中——无论它们是在哪里创建的。

Azure Data Factory 中的 Azure Machine Learning 管道

在移动数据、ETL 以及触发各种 Azure 服务中的计算时,你很可能会遇到Azure Data Factory。这是一个非常流行的服务,可以将大量数据移动到 Azure,执行处理和转换,构建工作流,并触发许多其他 Azure 或第三方服务。

Azure Machine Learning 管道与 Azure Data Factory 集成得非常好,你可以轻松配置并通过 Data Factory 触发已发布的管道的执行。为此,你需要将ML Execute Pipeline活动拖放到你的 Data Factory 画布中,并指定已发布管道的管道 ID。此外,你还可以指定管道参数以及管道运行的实验名称。

下图显示了如何在 Azure Data Factory 中配置ML Execute Pipeline步骤。它使用链接服务连接到你的 Azure Machine Learning 工作空间,这允许你从下拉框中选择所需的管道:

图 8.6 – Azure Data Factory 与 Azure Machine Learning 活动

图 8.6 – Azure Data Factory 与 Azure Machine Learning 活动

如果你正在使用 JSON 配置计算步骤,你可以使用以下片段创建一个与 Azure Machine Learning 作为链接服务的ML Execute Pipeline活动。同样,你必须指定管道 ID,并可以传递实验名称以及管道参数:

{
    "name": "Machine Learning Execute Pipeline",
    "type": "AzureMLExecutePipeline",
    "linkedServiceName": {
        "referenceName": "AzureMLService",
        "type": "LinkedServiceReference"
    },
    "typeProperties": {
        "mlPipelineId": "<insert pipeline id>",
        "experimentName": "data-factory-pipeline",
        "mlPipelineParameters": {
            "batch_size": "10"
        }
    }
}

最后,您可以通过添加触发器或输出到ML 执行管道活动来触发步骤。这将最终触发您已发布的 Azure Machine Learning 管道,并在工作区中开始执行。这是一个很好的补充,使得其他团队在经典的 ETL 和数据转换过程中重用您的 ML 管道变得容易。

Azure Pipelines for CI/CD

Azure Pipelines 是 Azure DevOps 的一个功能,允许您作为一个持续集成(CI)和持续部署(CD)过程来运行、构建、测试和部署代码。因此,它们是具有许多高级功能(如审批队列和门控阶段)的灵活的代码和应用程序编排管道。

通过允许您运行多个代码块,将 Azure Machine Learning 集成到 Azure DevOps 的最佳方式是使用 Python 脚本块。如果您已经按照这本书的内容,并使用以代码优先的方法来编写您的实验和管道,那么这种集成非常简单。让我们来看一个小例子。

首先,让我们编写一个实用函数,该函数根据工作区和管道 ID 参数返回一个已发布的管道。在这个例子中,我们需要这个函数:

def get_pipeline(workspace, pipeline_id):
  for pipeline in PublishedPipeline.list(workspace):
    if pipeline.id == pipeline_id:
      return pipeline
  return None

接下来,我们可以继续实现一个非常简单的 Python 脚本,允许我们在 Azure 中配置和触发管道运行。我们将初始化工作区,检索已发布的管道,并将管道作为实验提交到 Azure Machine Learning 工作区。这一切都是可配置的,而且只需要几行代码:

ws = Workspace.get(
  name=os.environ.get("WORKSPACE_NAME"),
  subscription_id=os.environ.get("SUBSCRIPTION_ID"),
  resource_group=os.environ.get("RESOURCE_GROUP"))
pipeline = get_pipeline(args.pipeline_id)
pipeline_parameters = args.pipeline_parameters
exp = Experiment(ws, name=args.experiment_name)
run = exp.submit(pipeline,
                 pipeline_parameters=pipeline_parameters)
print("Pipeline run initiated %s" % run.id)

上述代码展示了我们如何将管道触发器集成到 Azure 管道中进行 CI/CD。我们可以看到,一旦工作区初始化完成,代码将遵循与从本地开发环境提交已发布的管道完全相同的模式。此外,我们还可以通过环境变量和命令行参数来配置管道运行。我们将在第十六章中看到这一功能的应用,使用 MLOps 将模型投入生产

摘要

在本章中,您学习了如何使用和配置 Azure Machine Learning 管道,通过管道和管道步骤将 ML 工作流程拆分为多个步骤,用于估计量、Python 执行和并行执行。您使用 DatasetPipelineData 配置了管道输入和输出,并成功控制了管道的执行流程。

作为另一个里程碑,您将管道作为 PublishedPipeline 部署到了 HTTP 端点。这允许您通过简单的 HTTP 调用来配置和触发管道执行。接下来,您实现了基于时间频率的自动调度,以及基于底层数据集变化的反应式调度。现在,当输入数据发生变化时,管道可以自动重新运行工作流程,无需任何手动交互。

最后,我们还模块化和版本化了管道步骤,以便在其他项目中重用。我们使用了InputPortDefOutputPortDef来为数据源和汇点创建虚拟绑定。在最后一步,我们探讨了将管道集成到其他 Azure 服务中,例如 Azure 机器学习设计器、Azure 数据工厂和 Azure DevOps。

在下一章中,我们将探讨在 Azure 中使用基于决策树集成模型构建机器学习模型。

第三部分:机器学习模型的训练和优化

在本节中,我们将学习如何在 Azure 上训练和优化传统的机器学习ML)模型以及深度学习模型。首先,我们将探讨传统集成技术的优缺点以及它们与基于新神经网络的模型之间的差异。然后,我们将利用 Azure 机器学习服务的功能在 Azure 上实现和训练卷积神经网络CNNs)。随后,我们将探讨通过超参数调整和自动化机器学习来优化模型训练的方法。此外,我们还将探讨如何在分布式集群上而不是单个计算实例上运行 ML 训练。获得这些知识后,我们将通过在云中构建推荐引擎来结束本节。

本节包括以下章节:

  • 第九章, 使用 Azure 机器学习构建 ML 模型

  • 第十章, 在 Azure 上训练深度神经网络

  • 第十一章, 超参数调整和自动化机器学习

  • 第十二章, Azure 上的分布式机器学习

  • 第十三章, 在 Azure 中构建推荐引擎

第九章:第九章:使用 Azure Machine Learning 构建 ML 模型

在前面的章节中,我们学习了 Azure Machine Learning 中的数据集、预处理、特征提取和管道。在本章中,我们将利用我们迄今为止所获得的知识来创建和训练一个强大的基于树的集成分类器。

首先,我们将深入了解流行的集成分类器,如随机森林XGBoostLightGBM的幕后场景。这些分类器在实际的现实中表现极为出色,并且它们在底层都是基于决策树。通过了解它们的主要优势,你将能够轻松发现可以用集成决策树分类器解决的问题。

我们还将学习梯度提升随机森林之间的区别,以及是什么使得这些树集成对实际应用有用。这两种技术都有助于克服决策树的主要弱点,并且可以应用于许多不同的分类和回归问题。

最后,我们将使用我们迄今为止所学的所有技术在一个样本数据集上训练一个 LightGBM 分类器。我们将编写一个训练脚本,该脚本可以自动记录所有参数、评估指标和图形,并且可以通过命令行参数进行配置。我们将计划在 Azure Machine Learning 训练集群上运行训练脚本。

在本章中,我们将涵盖以下主题:

  • 使用基于树的集成分类器

  • 使用 LightGBM 训练集成分类器模型

技术要求

在本章中,我们将使用以下 Python 库和版本来创建基于决策树的集成分类器:

  • azureml-core 1.34.0

  • azureml-sdk 1.34.0

  • lightgbm 3.2.1

  • numpy 1.19.5

  • pandas 1.3.2

  • scikit-learn 0.24.2

  • seaborn 0.11.2

  • matplotlib 3.4.3

与前面的章节类似,你可以使用本地 Python 解释器或 Azure Machine Learning 托管的工作簿环境来执行此代码。

本章中的所有代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-Azure-Machine-Learning-Second-Edition/tree/main/chapter09

使用基于树的集成分类器

监督的基于树的集成分类和回归技术在近年来在许多实际应用中证明非常成功。因此,它们今天在包括欺诈检测、推荐引擎、标签引擎等多个应用中得到了广泛的使用。你所有的移动和桌面操作系统、Office 程序以及音频或视频流服务每天都在大量使用它们。

因此,在本节中,我们将深入了解它们受欢迎和性能的主要原因,包括训练和评分。如果您是传统机器学习算法的专家,并且了解提升和袋装之间的区别,您可以直接跳到下一节,使用 LightGBM 训练集成分类器模型,在那里我们将理论付诸实践。

我们首先将探讨决策树,这是一种非常简单的技术,已有数十年历史。我们鼓励您即使跟随简单的步骤,因为它们构成了今天最先进经典监督机器学习方法的基石。我们还将详细探讨基于树的分类器的优势,以帮助您理解经典方法与基于深度学习的机器学习模型之间的差异。

单个决策树也存在许多缺点,因此它仅用于集成模型,而从不作为单独的模型使用。我们将在本节稍后部分更详细地探讨单个决策树的缺点。之后,我们将发现将多个弱单个树组合成一个强大的集成分类器的方法,这种分类器建立在基于树的方法的优势之上,并将它们转化为今天所拥有的——几乎集成到每个现成机器学习平台中的强大多用途监督机器学习模型。

理解简单的决策树

让我们先讨论一下if/else语句。这个函数可以是连续回归函数或决策边界函数。因此,像许多其他机器学习方法一样,决策树可以用于学习回归和分类问题。

从前面的描述中,我们可以立即发现决策树的一些重要优势:

  • 一种是灵活性,可以在不同的数据分布、数据类型(例如,数值和分类数据)以及机器学习问题(如分类或回归)上工作。

  • 另一个优势,也是它们与更复杂模型竞争的原因之一,是它们的可解释性。基于树的模型和集成可以可视化,甚至可以打印在纸上以解释预测的决策(输出)。

  • 第三大优势在于它们在训练性能、模型大小和有效性方面的实际应用。将预训练的决策树集成到桌面、Web 或移动应用程序中,比深度学习方法要简单得多,也快得多。

    重要提示

    请注意,我们并不打算将基于树的方法作为解决每个机器学习问题的解决方案,并贬低深度学习方法的重要性。我们更希望让您在本章中意识到传统方法的优点,以便您可以为您的特定问题评估正确的方案。

下图展示了一个用于判断一个人是否健康的决策树示例:

图 9.1 – 一个简单的决策树

图 9.1 – 一个简单的决策树

图 9.1是一个训练好的决策树的例子,我们可以通过简单地遍历每个节点并到达树的叶节点来对模型进行评分。

决策树的优点

基于决策树的机器学习模型因其处理现实世界应用中的数据时的优势而极为流行,这些数据形式多样、形状各异,且杂乱、有偏见和不完整。以下是决策树的关键优点:

  • 它们支持广泛的应用。

  • 它们需要很少的数据准备。

  • 它们使模型的可解释性成为可能。

  • 它们提供快速训练和快速评分。

首先,让我们关注决策树的灵活性,这是它们与其他许多经典/统计机器学习方法的重大优势之一。尽管其通用框架非常灵活,支持分类回归,以及多输出问题,但它之所以受到广泛欢迎,是因为它能够直接处理数值和分类数据。多亏了嵌套的if-else树,它还可以处理名义类别以及数据中的 NULL 或缺失值。决策树之所以受欢迎,是因为它们不需要在事先进行大量的预处理和数据清洗。

尽管数据准备和清洗是每个机器学习管道中的重要步骤,但有一个自然支持分类输入数据的框架仍然很令人愉快。一些集成树形分类器是建立在这一优势之上的,例如,CatBoost——来自 Yandex Research 的梯度提升树实现,具有对分类数据的原生支持。

树形模型的一个重要优点,特别是从商业角度来看,是模型的可解释性。与其他机器学习方法不同,决策树分类器模型的输出不是一个巨大的参数化决策边界函数。训练好的深度学习模型通常会产生包含超过 1 亿个参数的模型,因此表现得像一个黑盒——特别是对于商业决策者来说。虽然从深度学习模型中获取见解和推理关于激活是可能的,但通常很难推理输入参数对输出变量的影响。

可解释性是树形方法的优势所在。与许多其他传统机器学习方法(如 SVM、逻辑回归或深度学习)相比,决策树是一个非参数模型,因此不使用参数来描述要学习的函数。它使用可以绘制、可视化和打印在纸上的嵌套决策树。这使得决策者能够理解基于树的分类模型的每一个决策(输出)——可能需要很多纸张,但总是可能的。

当谈到可解释性时,我们需要提到决策树的另一个重要方面:决策树模型在训练过程中隐式地发展了特征重要性的概念。这是训练好的决策树模型的一个非常有用的输出,我们可以用它来对特征进行预处理排序,而无需首先清理数据。

重要提示

虽然特征重要性也可以用其他机器学习(ML)方法来衡量,例如线性回归,但它们通常需要输入一个清洗和归一化的数据集。许多其他机器学习方法,如支持向量机(SVM)或深度学习,不会为单个输入维度开发特征重要性的度量。

基于决策树的方法在这方面表现出色,因为它们内部根据重要性标准创建每个单个分割(决策)。这导致了对最终模型中哪些特征维度重要以及如何重要的内在理解。

让我们来看看决策树另一个巨大的优势。与源自非参数方法的传统统计模型相比,决策树具有许多实际优势。基于树的模型通常在各种输入分布上都能产生良好的结果,甚至在模型假设被违反时也能很好地工作。除此之外,与深度学习方法相比,训练好的树的大小较小,推理/评分速度快。

决策树的缺点

正如生活中的一切都有利有弊一样,决策树也是如此。与单个决策树相关的一些严重缺点应该让你在机器学习(ML)管道中避免使用单个决策树分类器。单个决策树的主要弱点是树是在所有训练样本上拟合的,因此很可能发生过拟合。这是因为模型本身倾向于构建复杂的if-else树来模拟连续函数。

另一个重要点是,即使对于简单概念,找到最优决策树也是一个NP 难题(也称为非确定性多项式时间难题)。因此,它通过启发式方法来解决,并且得到的单个决策通常不是最优的。

过拟合很糟糕——非常糟糕——并且会导致机器学习中的严重问题。一旦模型过拟合,它就不太能泛化,因此在未见过的数据上的性能非常差。因此,对新输入的预测结果将比训练期间测量的结果更差。另一个相关问题是,训练数据或训练样本顺序的微小变化可能导致非常不同的嵌套树,因此训练收敛性不稳定。单个决策树极其容易过拟合。此外,单个决策树很可能偏向于训练数据中样本数量最多的类别。

通过 Bagging 和 Boosting 将多个决策树组合成一个集成模型,可以克服单棵树的缺点,如过拟合、不稳定和非最优树。还有许多基于树的优化方法,包括树剪枝,以提高泛化能力。使用这些技术的流行模型包括随机森林梯度提升树,它们克服了单棵决策树的大部分问题,同时保持了大部分优点。我们将在下一节中探讨这两种方法。

重要提示

有时,即使在基于树的集成方法中,也会出现一些更基本的缺点,这些缺点值得提及。由于决策树的本质,基于树的模型在学习复杂函数,如 XOR 问题时存在困难。对于这些问题,最好使用非线性参数模型,如神经网络和深度学习方法。

将分类器与 Bagging 结合

单个决策树的一个关键缺点是对训练数据的过拟合,因此,从训练数据的小变化中,会导致泛化性能差和稳定性差。一个Bagging(也称为自助聚合)分类器通过将多个独立的模型组合成一个在训练数据子集上训练的集成模型来克服这个确切问题。这些子集是通过从训练数据集中随机抽取带有替换的样本来构建的。单个模型的输出要么通过分类中的多数投票选择,要么通过回归问题中的均值聚合。

通过结合独立的模型,我们可以降低组合模型的方差,而不会增加偏差,从而大大提高泛化能力。然而,训练多个单独模型还有一个好处:并行化。由于每个单独的模型都使用训练数据的一个随机子集,因此训练过程可以轻松并行化,并在多个计算节点上训练。因此,当在大数据集上训练大量基于树的分类器时,Bagging 是一种流行的技术。

下面的图 9.2展示了每个分类器如何独立地在相同的训练数据上训练——每个模型使用带有替换的随机子集。所有单个模型的组合构成了集成模型。

图 9.2 – Bagging

图 9.2 – Bagging

Bagging 可以与任何机器学习模型结合使用;然而,它通常与基于树的分类器一起使用,因为它们最容易过拟合。随机森林的理念建立在 bagging 方法之上,并结合了每个分割(决策)的随机特征子集。当随机选择一个特征时,计算分割的最佳阈值,以优化某个信息准则(通常是GINI信息增益)。因此,随机森林使用训练数据的随机子集、随机特征选择和分割的最佳阈值。

随机森林因其简单的基于决策树的模型以及更好的泛化能力和易于并行化而得到广泛应用。采用特征随机子集的另一个好处是,这种技术也适用于非常高维度的输入。因此,在处理经典机器学习方法时,随机森林通常用于大规模的树集成。

另一种流行的基于树的 bagging 技术是extra-trees(即extremely randomized trees)算法,它在维度分割上增加了一个额外的随机化步骤。对于每个分割,随机抽取阈值,并选择最佳阈值进行决策。因此,除了随机特征外,extra-trees 算法还使用随机分割阈值来进一步提高泛化能力。

下面的图 9.3展示了所有树集成技术在推理中的应用。每棵树计算一个单独的分数,而每棵树的结果被汇总以产生最终结果:

图 9.3 – 多数投票

图 9.3 – 多数投票

你可以在许多流行的机器学习库中找到基于树的 bagging 集成,例如 scikit-learn、Spark MLlib、ML.NET 等。

使用 boosting 轮优化分类器

在计算机科学的许多问题中,我们可以用一个更复杂但更优的方法来替换随机贪婪方法。对于树集成也是如此,并为其奠定了boosted 树集成的基础。

增强背后的基本思想如下:

  1. 我们开始在整个训练数据集上训练单个模型。

  2. 然后我们在训练数据集上计算模型的预测,并开始提高产生错误结果的训练样本的权重。

  3. 接下来,我们使用加权训练集训练另一棵决策树。然后我们将这两棵决策树组合成一个集成,并预测加权训练集的输出类别。然后我们在下一轮 boosting 中进一步增加组合模型中错误分类的训练样本的权重。

  4. 我们继续执行此算法,直到达到停止标准。

下面的图 9.4展示了使用 boosting 优化训练误差如何随着每次迭代(boosting 轮)的增加而降低:

图 9.4 – Boosting

图 9.4 – Boosting

第一个提升算法是AdaBoost,它通过在加权训练集上拟合,将多个弱模型组合成一个集成,并通过学习率适应每一轮迭代。这种方法的概念是添加单个树,这些树专注于预测前一个树无法预测的东西。

提升的一个特别成功的技巧是梯度提升树(或梯度提升)。在梯度提升中,你将梯度下降优化技术与提升相结合,以便将提升推广到任意损失函数。现在,我们不再使用权重调整数据集样本,而是在每一轮迭代中计算损失函数的梯度,并选择最优权重——那些最小化损失函数的权重。多亏了优化技术的使用,这种方法产生了非常好的结果,增加了决策树现有的优势。

基于梯度提升树的集成在许多流行的机器学习库中都有包括,例如 scikit-learn、Spark MLlib 等。然而,一些个别实现,如 XGBoost 和 LightGBM,已经获得了相当多的流行度,并且可以作为独立库以及作为 scikit-learn 和 Spark 的插件使用。

使用 LightGBM 训练集成分类器模型

由于决策树的简单性和结合多个分类器的优势,随机森林和梯度提升树都是强大的机器学习技术。在这个例子中,我们将使用来自微软的流行 LightGBM 库来实现这两种技术在测试数据集上的实现。LightGBM 是一个梯度提升框架,它结合了多个基于树的机器学习算法。

对于本节,我们将遵循典型的最佳实践方法,使用 Azure 机器学习,并执行以下步骤:

  1. 在 Azure 中注册数据集。

  2. 创建一个远程计算集群。

  3. 实现一个可配置的训练脚本。

  4. 在计算集群上运行训练脚本。

  5. 记录和收集数据集、参数和性能。

  6. 注册训练好的模型。

在我们开始这个激动人心的方法之前,我们将快速看一下为什么我们选择 LightGBM 作为训练装袋和提升树集成工具。

LightGBM 概述

LightGBM 使用了许多经典基于树的集成技术的优化,以在分类和连续特征上提供出色的性能。后者使用基于直方图的方法进行配置文件分析,并将其转换为最优分割的离散箱,这减少了内存消耗并加快了训练速度。这使得 LightGBM 比使用预排序算法计算分割的其他提升库更快、更节省内存,因此是大型数据集的一个很好的选择。

LightGBM 的另一个优化是,树是垂直生长的,从叶子到叶子,而其他类似的库是水平生长的,一层层。在叶节点算法中,新添加的叶子节点总是有最大的损失减少。这意味着这些算法与分层算法相比,往往能实现更小的损失。然而,更大的深度也会导致过拟合,因此你必须仔细调整每个树的最大深度。总的来说,LightGBM 在大量应用上使用默认参数就能产生非常好的结果。

第七章,“使用 NLP 的高级特征提取”中,我们了解了很多关于分类特征嵌入和从文本特征中提取语义意义的内容。我们探讨了嵌入名义分类变量的常见技术,如标签编码和独热编码,以及其他方法。然而,为了优化基于树的分类变量的分割标准,有更好的编码可以产生最优的分割。因此,在本节中,我们根本不对分类变量进行编码,而只是简单地告诉 LightGBM 哪些使用的变量是分类变量。

最后要提到的一点是,LightGBM 可以利用 GPU 加速,并且可以在数据并行或模型并行的方式下进行训练。我们将在第十二章,“Azure 上的分布式机器学习”中了解更多关于分布式训练的内容。

重要提示

LightGBM 是一个基于树的集成模型的绝佳选择,特别是对于非常大的数据集。

在本书中,我们将使用带有lgbm命名空间的 LightGBM。然后我们可以通过输入四个字符来直接调用命名空间中的不同方法——这是 Python 中数据科学家的一种最佳实践方法。让我们看一个简单的例子:

import lightgbm as lgbm
# Construct a LGBM dataset
lgbm.Dataset(..)
# Train a LGBM predictor
clf = lgbm.train(..)

值得注意的是,所有算法都是通过lgbm.train()方法进行训练的,我们使用不同的参数来指定算法、应用类型、损失函数,以及每个算法的额外超参数。LightGBM 支持多种基于决策树的集成模型,用于袋装和提升。以下是您可以选择的算法选项,以及它们的名称,以便在提升参数中识别它们:

  • gbdt:传统的梯度提升决策树

  • rf:随机森林

  • dart:Dropouts meet multiple additive regression trees

  • goss:基于梯度的单侧采样

前两个选项,即梯度提升决策树gbdt),这是 LightGBM 的默认选择,以及随机森林rf),是提升和袋装技术的经典实现,在本章的第一节中解释,并具有 LightGBM 特定的优化。其他两种技术,Dropouts Meet Multiple Additive Regression Treesdart)和Gradient-Based One-Side Samplinggoss),是 LightGBM 特有的,并为更好的结果提供了更多优化,以牺牲训练速度为代价。

目标参数——这是最重要的参数之一——指定了模型的适用应用程序类型,因此是您试图解决的机器学习问题。在 LightGBM 中,您有以下标准选项,这些选项与其他大多数基于决策树的集成算法类似:

  • regression: 用于预测连续目标变量

  • binary: 用于二分类任务

  • multiclass: 用于多分类问题

除了标准选择之外,您还可以选择以下更具体的目标:regression_l1huberfairpoissonquantilemapegammacross_entropy以及许多其他选项。

与模型的目标参数直接相关的是选择损失函数来衡量和优化训练性能。在这里,LightGBM 也为我们提供了默认选项,这些选项也大多数其他提升库中可用,我们可以通过 metric 参数来指定:

  • mae: 均值绝对误差

  • mse: 均方误差

  • binary_logloss: 二分类的损失

  • multi_logloss: 多分类的损失

除了这些损失度量之外,还支持其他度量,例如rmsequantilemapehuberfairpoisson以及许多其他度量。在我们的分类场景中,我们将选择具有binary目标函数和binary_logloss度量的dart算法。

重要提示

您还可以将 LightGBM 用作 scikit-learn 估计器。为此,从lightgbm命名空间调用LGBMModelLGBMClassifierLGBMRegressor模型。然而,最新功能通常仅通过 LightGBM 接口可用。

现在,了解了如何使用 LightGBM,我们可以开始实现数据准备和编写脚本。

准备数据

在本节中,我们将读取和准备数据,并将清洗后的数据注册为新的数据集在 Azure Machine Learning 中。这将使我们能够从与工作区连接的任何计算目标访问数据,而无需手动复制数据、挂载磁盘或设置与数据存储的连接。这在第四章中进行了详细讨论,数据摄取和管理数据集。所有设置、调度和操作都将从作者环境——Jupyter 笔记本中完成。

对于分类示例,我们将使用泰坦尼克号数据集,这是一个流行的机器学习实践者数据集,用于预测泰坦尼克号上每位乘客的二元生存概率(生存未生存)。该数据集的特征描述了乘客,并包含以下属性:乘客 ID、等级、姓名、性别、年龄、船上兄弟姐妹或配偶的数量、船上子女或父母数量、票号、票价、船舱号和登船港口。

重要提示

关于这个数据集的详细信息以及完整的预处理流程,可以在本书附带源代码中找到。

在不知道更多细节的情况下,我们将卷起袖子设置工作区并开始实验:

  1. 我们从azureml.core导入WorkspaceExperiment,并指定实验名称为titanic-lgbm

    from azureml.core import Workspace, Experiment
    ws = Workspace.from_config()
    exp = Experiment(workspace=ws, name="titanic-lgbm")
    
  2. 接下来,我们使用 pandas 加载数据集,并开始清理和预处理数据:

    import pandas as pd
    # Read the data
    df = pd.read_csv('data/titanic.csv')
    # Prepare the data
    df.drop(['PassengerId'], axis=1, inplace=True)
    df.loc[df['Sex'] == 'female', 'Sex'] = 0
    df.loc[df['Sex'] == 'male', 'Sex'] = 1
    df['Sex'] = df['Sex'].astype('int8')
    embarked_encoder = LabelEncoder()
    embarked_encoder.fit(df['Embarked'].fillna('Null'))
    df['Embarked'].fillna('Null', inplace=True)
    df['Embarked'] = embarked_encoder.transform(
        df['Embarked'])
    df.drop(['Name', 'Ticket', 'Cabin'],
        axis=1,
        inplace=True)
    

在前面的示例中,我们从 CSV 文件加载数据,删除未使用的列,将Sex特征的值替换为标签01,并将Embarked特征的分类值编码为标签。

  1. 接下来,我们编写一个小的实用函数df_to_dataset(),它将帮助我们存储 pandas DataFrame 并将其注册为 Azure 数据集,以便在 Azure 机器学习环境中轻松重用:

    def df_to_dataset(ws, df, name):
        datastore = ws.get_default_datastore()
        dataset = Dataset.Tabular.register_pandas_dataframe(
            df, datastore, name)
        return dataset
    

首先,我们检索到我们机器学习工作区的默认数据存储的引用——这是我们首次设置工作区时创建的 Azure Blob 存储。然后,我们使用一个辅助函数将数据集上传到这个默认数据存储,并将其作为表格数据集引用。

  1. 接下来,我们使用新创建的辅助函数将 pandas DataFrame 注册为名为titanic_cleaned的数据集:

    # Register the data
    df_to_dataset(ws, df, 'titanic_cleaned')
    
  2. 一旦数据集在 Azure 中注册,就可以在任何 Azure 机器学习工作区中访问。如果我们现在进入 UI 并点击titanic_cleaned数据集。在 UI 中,我们还可以轻松检查和预览数据,如下面的截图所示:

图 9.5 – 泰坦尼克号数据集

图 9.5 – 泰坦尼克号数据集

值得注意的是,我们首先将分类变量编码为整数,使用标签编码,但稍后告诉 LightGBM 哪些变量包含数值列中的分类信息。这将帮助 LightGBM 在计算直方图和最佳参数分割时对这些列进行不同的处理。

数据集注册的好处是,我们现在可以简单地将数据传递给训练脚本或从 Azure 机器学习中的任何 Python 解释器访问它。让我们继续训练示例,为 LightGBM 创建训练和执行环境。

设置计算集群和执行环境

在我们开始训练 LightGBM 分类器之前,我们需要设置我们的训练集群和一个包含所有必需 Python 库的训练环境。对于本章,我们选择了一个最多有四个节点的 STANDARD_D2_V2 类型的 CPU 集群:

  1. 让我们编写一个小的辅助函数,使我们能够检索或创建一个具有指定名称和配置的训练集群。我们利用 ComputeTargetException,如果未找到指定名称的集群,则会抛出此异常:

    def get_aml_cluster(ws, cluster_name,
                        vm_size='STANDARD_D2_V2',
                        max_nodes=4):
        try:
            cluster = ComputeTarget(
                 workspace=ws, name=cluster_name)
        except ComputeTargetException:
            config = AmlCompute.provisioning_configuration(
                vm_size=vm_size, max_nodes=max_nodes)
            cluster = ComputeTarget.create(
                ws, cluster_name, config)
        return cluster 
    

我们已经在之前的章节中看到了这个脚本的组成部分,其中我们调用 AmlCompute.provisioning_configuration() 来配置一个新的集群。你可以在你的创作环境中定义所有基础设施这一点非常有帮助。

  1. 让我们检索或创建一个新的训练集群:

    # Create or get training cluster
    aml_cluster = get_aml_cluster(ws, 
                                  cluster_name="cpu-cluster")
    aml_cluster.wait_for_completion(show_output=True)
    
  2. 接下来,我们想要为我们的训练环境和 Python 配置做同样的事情。我们实现了一个小的 get_run_config() 函数,用于返回具有 Python 配置的远程执行环境。这将用于配置训练脚本所需的全部 Python 包:

    def get_run_config(target, packages=None):
        packages = packages or []
        packages += ['azureml-defaults']
        config = RunConfiguration()
        config.target = target
        config.environment.python.conda_dependencies = \
            CondaDependencies.create(pip_packages=packages)
        return config
    

在前面的脚本中,我们使用 RunConfiguration 定义了 Azure Machine Learning 所需的包,如 azureml-defaults 和自定义 Python 包。

  1. 接下来,我们使用此函数配置一个包含所有必需 pip 包的 Python 镜像,包括 lightgbm

    # Create a remote run configuration
    lgbm_config = get_run_config(aml_cluster, [
        'numpy', 'pandas', 'matplotlib', 'seaborn',
        'scikit-learn', 'joblib', 'lightgbm'
    ])
    

在前面的代码片段中使用的两个函数非常有用。你使用 Azure Machine Learning 的时间越长,你将构建更多的抽象来轻松地与 Azure Machine Learning 服务交互。

使用自定义运行配置和自定义 Python 包,Azure Machine Learning 将在调度使用此运行配置的作业时立即设置 Docker 镜像并将其自动注册到 容器注册表 中。让我们首先构建训练脚本,然后在集群上调度它。

构建 LightGBM 分类器

现在我们有了数据集,并且我们已经为 LightGBM 分类模型的训练设置了环境和集群,我们可以设置训练脚本。上一节中的代码是在 Jupyter 笔记本中编写的。本节中的以下代码现在将编写并存储在一个名为 train_lgbm.py 的 Python 文件中。我们将按照以下步骤开始构建分类器:

  1. 首先,我们配置运行并从运行中提取工作区配置。这应该已经很熟悉了,因为我们已经为到目前为止在 Azure Machine Learning 上调度的几乎所有脚本都做过这件事:

    from azureml.core import Dataset, Run
    run = Run.get_context()
    ws = run.experiment.workspace
    
  2. 接下来,我们设置一个参数解析器,将命令行参数解析为 LightGBM 参数。我们开始时只有一些参数,但可以轻松地添加所有可用参数和默认值:

    parser.add_argument('--data', type=str)
    parser.add_argument('--boosting', type=str)
    parser.add_argument('--learning-rate', type=float)
    parser.add_argument('--drop-rate', type=float)
    args = parser.parse_args()
    

    重要提示

    我们建议使您的训练脚本可配置。使用 argparse 定义数据集、输入参数和默认值。如果您坚持这个约定,所有模型参数都将自动跟踪在您的 Azure Machine Learning 实验中。另一个好处是,您以后可以调整超参数,而无需在训练脚本中更改一行代码。

  3. 然后,我们可以引用从输入参数中清理过的数据集,并使用 to_pandas_dataframe() 方法将其加载到内存中:

    # Get a dataset by id
    dataset = Dataset.get_by_id(ws, id=args.data)
    # Load a TabularDataset into pandas DataFrame
    df = dataset.to_pandas_dataframe()
    
  4. 在将数据集作为 pandas DataFrame 加载之后,我们现在可以开始将训练数据分割成训练集和验证集。我们还将把目标变量 Survived 从训练数据集中分割成其自己的变量:

    y = df.pop('Survived')
    # Split into training and testing set 
    X_train, X_test, y_train, y_test = train_test_split(
        df, y, test_size=0.2, random_state=42) 
    
  5. 接下来,我们向 LightGBM 介绍分类特征,这些特征已经被转换成数值变量,但需要特殊处理来计算最优分割值:

    categories = ['Alone', 'Sex', 'Pclass', 'Embarked']
    
  6. 接下来,我们创建实际的 LightGBM 训练集和测试集,从 pandas DataFrame 中:

    # Create training set
    train_data = lgbm.Dataset(data=X_train, label=y_train, 
        categorical_feature=categories, free_raw_data=False)
    # Create testing set
    test_data = lgbm.Dataset(data=X_test, label=y_test,
        categorical_feature=categories, free_raw_data=False)
    

与 scikit-learn 不同,我们无法直接在 LightGBM 中使用 pandas DataFrame,但需要使用包装类 lgbm.Dataset。这将使我们能够访问所有必需的优化和功能,例如分布式训练、稀疏数据优化以及关于分类特征的元信息。

  1. 在解析完命令行参数后,我们将它们传递到一个参数字典中,然后将其传递给 LightGBM 训练方法:

    lgbm_params = {
        'application': 'binary',
        'metric': 'binary_logloss',
        'learning_rate': args.learning_rate,
        'boosting': args.boosting,
        'drop_rate': args.drop_rate,
    }
    
  2. 所有通过命令行参数传递的参数都会自动记录在 Azure Machine Learning 中。然而,如果您想以编程方式访问模型参数或在 Azure Machine Learning 的实验概览中显示它们,我们可以在实验中记录它们。这将把所有参数附加到每个运行实例上,并在 Azure Machine Learning 中作为参数值提供。这意味着我们可以在以后根据模型参数对实验运行进行排序和筛选:

    for k, v in params.items():
        run.log(k, v)
    

梯度提升是一种具有可变迭代次数和可选提前停止标准的迭代优化方法。因此,我们还想记录训练脚本的每个迭代的全部指标。在这本书的整个过程中,我们将使用所有 ML 框架的类似技术——即使用一个回调函数,将所有可用的指标记录到您的 Azure Machine Learning 工作区。让我们使用 LightGBM 的自定义回调规范来编写这样一个函数。

  1. 在这里,我们创建一个回调对象,它遍历所有评估结果并将它们记录在运行中:

    def azure_ml_callback(run):
        def callback(env):
            if env.evaluation_result_list:
                for data_name, eval_name, result, _ in \
                    env.evaluation_result_list:
                    run.log("%s (%s)" % (eval_name, 
                                         data_name), result)
        callback.order = 10
        return callback 
    
  2. 在我们为 LightGBM 预测器设置好参数之后,我们可以使用 lgbm.train() 方法来配置训练和验证过程。我们需要提供所有参数、参数和回调函数:

    clf = lgbm.train(train_set=train_data,
                     params=lgbm_params,
                     valid_sets=[train_data, test_data], 
                     valid_names=['train', 'val'],
                     num_boost_round=args.num_boost_round,
                     callbacks = [azure_ml_callback(run)])
    

上述代码的亮点是,通过提供通用的回调函数,所有训练和验证分数将自动记录到 Azure。因此,我们可以实时跟踪训练迭代,无论是在 UI 中还是在 API 中——例如,在一个自动收集所有运行信息的 Jupyter 小部件中。

  1. 为了评估最终的训练分数,我们使用训练好的分类器来预测几个默认的分类分数,例如 accuracy(准确率)、precision(精确率)和 recall(召回率),以及组合的 f1 分数:

    y_pred = clf.predict(X_test)
    run.log("accuracy (test)", accuracy_score(y_test, 
                                              y_pred))
    run.log("precision (test)", precision_score(y_test, 
                                                y_pred))
    run.log("recall (test)", recall_score(y_test, y_pred))
    run.log("f1 (test)", f1_score(y_test, y_pred))
    

我们已经运行了脚本并看到了所有指标以及模型在 Azure 中的性能。但这只是开始——我们想要更多!

  1. 让我们在 Azure Machine Learning 中计算特征重要性并跟踪其图表,并运行它。我们可以用几行代码来完成这个任务:

    fig = plt.figure()
    ax = plt.subplot(111)
    lgbm.plot_importance(clf, ax=ax)
    run.log_image("feature importance", plot=fig)
    

一旦将此片段添加到训练脚本中,每次训练运行也将存储一个特征重要性图表。这有助于了解不同的指标如何影响特征重要性。

  1. 我们还想添加一个额外的步骤。每当训练脚本运行时,我们希望将训练好的模型上传并注册到模型注册表中。通过这样做,我们可以在以后手动或自动将模型部署到容器服务中。然而,这只能通过保存每次运行的训练工件来完成:

    import joblib
    joblib.dump(clf, 'outputs/lgbm.pkl')
    run.upload_file('lgbm.pkl', 'outputs/lgbm.pkl')
    run.register_model(model_name='lgbm_titanic', 
        model_path='lgbm.pkl')
    

在前面的片段中,我们使用了 joblib 包,该包最初是 scikit-learn 的一部分,用于将分类器保存到磁盘。然后我们将导出的模型注册为 Azure Machine Learning 中的 LightGBM 模型。

就这样——我们已经写完了整个训练脚本。它并不特别长,也不特别复杂。最棘手的部分是理解如何选择 LightGBM 的一些参数以及一般性地理解梯度提升——这就是为什么我们将章节的前半部分专门用于这个主题。现在让我们启动集群并提交训练脚本。

在 Azure Machine Learning 集群上安排训练脚本

我们逻辑上回到了作者环境——Jupyter 笔记本。上一节中的代码已存储为 train_lgbm.py 文件,我们现在将准备将其提交到集群。一件好事是我们使训练脚本可以通过命令行参数进行配置,因此我们可以使用 CLI 参数调整 LightGBM 模型的基参数。在以下步骤中,我们将配置作者脚本以执行训练过程:

  1. 让我们定义此模型的参数——我们将使用 dart,标准学习率为 0.01,dropout 率为 0.15。我们还通过命名参数将数据集传递给训练脚本:

    script_params = [
      '--data', ds.as_named_input('titanic'),
      '--boosting', 'dart',
      '--learning-rate', '0.01',
      '--drop-rate', '0.15',
    ]
    

我们指定了提升方法,dart。正如我们在上一节中学到的,这项技术表现非常好,但并不特别高效,比其他选项——gbdtrfgoss——慢一些。

重要提示

这也是 Azure 机器学习中的超参数调整工具HyperOpt将超参数传递给训练脚本的方式。我们将在第十一章中了解更多关于超参数调整和自动机器学习的内容。

  1. 接下来,我们可以将参数传递给ScriptRunConfig并启动训练脚本:

    from azureml.core import ScriptRunConfig
    src = ScriptRunConfig(
        source_directory=os.getcwd(),
        script='train_lightgbm.py',
        run_config= lgbm_config
        arguments=script_params)
    

在前面的代码中,我们指定了我们的分类器文件,该文件存储在当前编写脚本的相关位置。Azure 机器学习会将训练脚本上传到默认的数据存储库,并在运行脚本的集群的所有节点上使其可用。

  1. 最后,让我们提交运行配置并执行训练脚本:

    from azureml.widgets import RunDetails
    run = exp.submit(src)
    RunDetails(run).show()
    

RunDetails方法为我们提供了一个带有实时日志的交互式小部件,这些日志来自远程计算服务。我们可以看到集群正在初始化和扩展,Docker 镜像正在构建和注册,最终,还包括训练脚本的日志。

提示

如果你更喜欢其他方法而不是交互式的 Jupyter 小部件,你也可以使用run.wait_for_completion(show_output=True)print(run.get_portal_url())来跟踪日志,以获取在 Azure 中运行的实验的 URL。

  1. 现在,让我们切换到 Azure 机器学习 UI,并在实验中查找运行。一旦我们点击它,我们就可以导航到指标部分,并找到所有已记录指标的概览。你可以在以下图 9.6中看到,具有相同名称的多次记录的指标被转换为向量,并以折线图的形式显示:

图 9.6 – 验证损失

图 9.6 – 验证损失

然后,点击图像部分。当我们这样做时,我们会看到我们在训练脚本中创建的特征重要性图。以下图 9.7展示了它在 Azure 机器学习 UI 中的样子:

图 9.7 – 特征重要性

图 9.7 – 特征重要性

我们看到了如何在 Azure 机器学习中训练一个 LightGBM 分类器,利用了自动扩展的 Azure 机器学习计算集群。记录指标、图表和参数将所有关于训练运行的详细信息保持在同一个地方。与保存训练脚本的快照、输出、日志和训练好的模型一起,这对任何专业的大型机器学习项目来说都是无价的。

你应该从本章记住的是,梯度提升树是一种非常高效且可扩展的经典机器学习方法,拥有许多优秀的库,并支持分布式学习和 GPU 加速。LightGBM 是微软提供的一种替代方案,它在微软和开源生态系统中都得到了很好的整合。如果你在寻找一个经典、快速且易于理解的机器学习模型,我们的建议是选择 LightGBM。

摘要

在本章中,你学习了如何在 Azure 机器学习中构建一个经典机器学习模型。

您了解了决策树,这是一种在多种分类和回归问题中流行的技术。决策树的主要优势是它们需要很少的数据准备,因为它们在分类数据和不同的数据分布上表现良好。另一个重要的好处是它们的可解释性,这对于商业决策和用户来说尤为重要。这有助于您了解何时使用基于决策树的集成预测器是合适的。

然而,我们也了解到了一些弱点,特别是在过拟合和泛化能力差方面。幸运的是,基于树的集成技术,如 bagging(自助聚合)和 boosting(提升),有助于克服这些问题。虽然 bagging 有像随机森林这样的流行方法,它能够很好地并行化,但 boosting,尤其是梯度 boosting,有高效的实现,包括 XGBoost 和 LightGBM。

您在 Azure Machine Learning 中使用 LightGBM 库实现了并训练了一个基于决策树的分类器。LightGBM 是由微软开发的,通过一些优化提供了出色的性能和训练时间。这些优化帮助 LightGBM 即使在处理大型数据集时也能保持较小的内存占用,并且通过更少的迭代次数产生更好的损失。您不仅使用 Azure Machine Learning 来执行您的训练脚本,还用它来跟踪您的模型训练性能和最终的分类器。

在下一章中,我们将探讨一些流行的深度学习技术,以及如何使用 Azure Machine Learning 来训练它们。

第十章:第十章:在 Azure 上训练深度神经网络

在上一章中,我们学习了如何使用非参数的基于树的集成方法来训练和评分经典机器学习模型。虽然这些方法在包含分类变量的许多小型和中型数据集上表现良好,但它们在大数据集上的泛化能力不佳。

在本章中,我们将使用深度学习(DL)来训练复杂的参数模型,以实现与非常大的数据集的更好泛化。这将帮助您了解深度神经网络DNNs),如何训练和使用它们,以及它们何时比传统模型表现更好。

首先,我们将简要概述为什么以及何时深度学习(DL)效果良好,并着重于理解一般原则和理由,而不是理论方法。这将帮助您评估哪些用例和数据集需要深度学习,以及它的一般工作原理。

然后,我们将探讨深度学习的一个流行应用领域——计算机视觉。我们将使用 Azure 机器学习服务和额外的 Azure 基础设施来训练一个简单的卷积神经网络CNN)模型进行图像分类。我们将将其性能与在预训练的残差神经网络ResNet)模型上微调过的模型进行比较。这将为您从头开始训练模型、针对您的应用领域微调现有模型以及克服训练数据不足的情况做好准备。

在本章中,我们将涵盖以下主题:

  • 深度学习简介

  • 训练 CNN 进行图像分类

技术要求

在本章中,我们将使用以下 Python 库和版本来创建基于决策树的集成分类器:

  • azureml-core 1.34.0

  • azureml-sdk 1.34.0

  • numpy 1.19.5

  • pandas 1.3.2

  • scikit-learn 0.24.2

与前几章类似,您可以使用本地 Python 解释器或 Azure 机器学习托管的工作簿环境执行此代码。

本章中所有的代码示例都可以在这个书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-Azure-Machine-Learning-Second-Edition/tree/main/chapter10

深度学习简介

深度学习最近彻底改变了机器学习领域,并在各种任务中(如图像分类、目标检测、分割、语音转录、文本翻译、文本理解、销售预测等)不断优于经典统计方法,甚至优于人类。与经典模型相比,深度学习模型使用数百万个参数、参数共享、优化技术和隐式特征提取,当训练足够的数据时,可以优于所有之前手工制作的特征检测器和机器学习模型。

在本节中,我们将帮助你了解神经网络的基本知识以及使用更多参数、更好泛化和更好性能来训练更深模型的路径。这将帮助你理解基于深度学习的各种方法是如何工作的,以及为什么和何时它们对某些领域和数据集是有意义的。如果你已经是深度学习的专家,请随意跳过本节,直接进入训练用于图像分类的 CNN部分的实际示例。

为什么是深度学习?

许多传统的优化、分类和预测过程在过去几十年中已经使用经典的机器学习方法(如 k 最近邻、线性回归和逻辑回归、朴素贝叶斯、支持向量机(SVMs)、基于树的集成模型等)工作得很好。它们在小到中等规模的数据集上对各种类型的数据(交易、时间序列、运营等)和数据类型(二进制、数值和分类)都表现良好。

然而,在某些领域,数据生成已经爆炸式增长,即使训练数据量不断增加,经典机器学习模型也无法实现更好的性能。这尤其影响了 2010 年底左右的计算机视觉和自然语言处理(NLP)领域。那时,研究人员在神经网络——也称为多层感知器(MLPs)——这一技术上取得了突破,这是一种在 20 世纪 80 年代使用的技术,通过使用多层嵌套层来捕捉大型图像数据集中的大量特征。

以下图表很好地捕捉了这个想法。虽然传统的机器学习(ML)方法在小型和中等规模的数据集上工作得非常好,但它们的性能通常不会随着更多训练数据的增加而提高。然而,深度学习(DL)模型是大规模参数模型,可以从训练数据中捕捉大量细节。因此,我们可以看到,随着数据量的增加,它们的预测性能也在提高:

图 10.1 – 深度学习与传统机器学习的有效性

图 10.1 – 深度学习与传统机器学习的有效性

传统模型通常使用预构建的特征,并针对各种数据类型和范围的数据集进行优化。在上一章中,我们看到了梯度提升树在分类数据上的表现极为出色。然而,在包含高度结构化数据或可变长度数据的领域中,许多传统模型已经达到了它们的极限。这尤其适用于二维和三维图像以及视频中的像素信息,以及音频数据中的波形以及自由文本数据中的字符和字符序列。以前,机器学习模型使用复杂的、手动调整的特征提取器来处理此类数据,例如方向梯度直方图(HoG)过滤器、尺度不变特征变换(SIFT)特征或局部二值模式(LBPs)——仅举计算机视觉领域中的几个过滤器为例。

使这些数据如此复杂的原因是,输入数据(例如,单个像素)和输出之间不存在明显的线性关系——在大多数情况下,看到一个图像中的单个像素并不能帮助确定该图像中的汽车品牌。因此,训练更大、更强大的参数模型的需求不断增加,这些模型使用原始、未处理的数据作为输入,以从输入像素捕获这些关系并做出最终预测。

重要的是要理解,对具有更多参数的深度模型的必要性源于特定领域(如视觉、音频和语言)中高度结构化训练数据的巨大增加。这些新模型通常具有数百万个参数来捕捉大量的原始和增强训练数据,以及开发训练数据的内部泛化概念表示。在选择适用于您的用例的机器学习方法时,请记住这一点。

快速查看您的训练数据通常有助于确定基于深度学习的模型是否适合该任务——鉴于深度学习模型有数百万个参数需要训练。如果您的数据存储在 SQL 数据库、CSV 或 Excel 文件中,那么您可能需要考虑经典机器学习(ML)方法,例如参数统计(线性回归、支持向量机等)或非参数方法(基于决策树的集成)。如果您的数据量如此之大以至于无法放入内存,或者存储在Hadoop 分布式文件系统(HDFS)、blob 存储或文件存储服务器中,那么您可以使用基于深度学习的方法。

从神经网络到深度学习

神经网络的基础以及今天基于深度学习(DL)的方法——感知器——是一个超过半个世纪的概念,它是在 20 世纪 50 年代发展起来的。在本节中,我们将探讨基础知识,并逐步回顾到 20 世纪 80 年代的多层感知器(MLPs)——也称为人工神经网络(ANNs)——以及卷积神经网络(CNNs),然后是最近十年的深度神经网络(DNNs)和深度学习(DL)。这将帮助您理解神经网络和深度学习的基础概念,以及模型架构和训练技术在过去一个世纪中是如何演变成我们今天使用的最先进技术的。

感知器——20 世纪 50 年代的分类器

感知器是今天神经网络的基石,它们模仿人类大脑中的细胞(所谓的神经元)。它们由两个简单非线性函数组成:所有输入的加权和以及一个激活函数,如果输出大于指定的阈值,则激活。虽然这种神经元的类比是模拟大脑工作方式的一个很好的方法,但它并不是理解输入信号如何转换为输出的一个很好的模型。

我们更倾向于使用一种简单得多、非生物的方法来解释感知器、MLPs 和 CNNs,即简单的几何方法。当简化后,这种方法只需要你理解二维直线方程。一旦你理解了两维的基本概念,这个概念可以扩展到多维度,其中直线在更高维的特征空间中变成一个平面或超平面。

如果我们看一个单独的感知器,它描述了其输入的加权求和加上一个常量偏置和一个激活函数。让我们分解感知器的两个组成部分。你知道什么也被描述为输入的加权求和加上偏置吗?对,就是直线方程:

在前面的方程中,x 是输入,k 是权重,b 是偏置项。你可能在你的一些数学课程中看到过这个方程。这个方程的一个特性是,当你将一个点的 xy 坐标插入到直线方程中时,对于所有位于直线上的点,它会产生 0 = 0。我们可以利用这个信息推导出直线方程的向量形式,如下所示:

因此,当点位于直线上时,0。如果我们插入一个不在直线上的点的坐标会发生什么?一个很好的猜测是结果将是正的或负的,但肯定不是 0. 向量直线方程的一个特性是,这个结果的正负号描述了点位于直线的哪一侧。因此,当 为正或负但不是零时,点位于直线的左侧或右侧。

要确定直线的哪一侧,我们可以将符号函数应用于 。符号函数通常也被称为阶跃函数,因为它的输出是 1-1,因此是正的或负的。这里的符号或阶跃函数是我们的激活函数,因此是感知器的第二个组成部分。感知器的输出 可以写成以下形式:

在以下图表中,我们可以看到两个点、一条直线以及它们到直线的最短距离。两个点都不在直线上,因此直线将它们彼此分开。如果我们把两个点的坐标插入到向量直线方程中,那么一个点会产生一个正值 ,而另一个点会产生一个负值

图 10.2 – 一个简单的二元分类器

图 10.2 – 一个简单的二元分类器

结果会告诉我们点位于线的哪一侧。这条线是感知器的几何描述,它是一个非常简单的分类器。训练好的感知器通过线方程(或多个维度中的超平面)定义,将空间分为左右两部分。这条线是分类的决策边界,而一个点是一个观察。通过将一个点插入线方程并应用步函数,我们返回观察结果的类别,即左或右,-1 或+1,或类别 A 或 B。这描述了一个二元分类器。

我们如何找到决策边界?为了找到最优的决策边界,我们可以在使用标记的训练样本的同时遵循一个迭代训练过程。首先,我们必须初始化一个随机的决策边界,然后计算每个样本到决策边界的距离,并将决策边界移动到最小化总距离和的方向。移动决策边界的最优向量是如果我们沿着负梯度移动它,使得点与线之间的距离达到最小。通过使用学习率因子,我们迭代这个过程几次,最终得到一个完美对齐的决策边界,如果训练样本是线性可分的。这个过程被称为梯度下降,其中我们迭代地修改分类器的权重(在这个例子中是决策边界)以找到具有最小误差的最优边界。

多层感知器

感知器描述了一个简单的分类器,其决策边界是通过加权输入定义的线(或超平面)。然而,我们不是使用单个分类器,而是简单地增加神经元的数量,这将导致多个决策边界,如下面的图表所示:

图 10.3 – 多个感知器的组合

图 10.3 – 多个感知器的组合

每个神经元描述一个决策边界,因此将具有独立的权重和输出 – 决策边界的左侧或右侧。通过在层中堆叠多个神经元,我们可以创建输入是前一个输出的分类器。这允许我们将多个决策边界的输出组合成一个单一的输出 – 例如,找到所有被三个神经元的决策边界包围的样本,如前述图表所示。

当单层感知器描述输入和输出的线性组合时,研究人员开始将这些感知器堆叠成多个连续层,每一层后面都跟着一个激活函数。这被称为 MLP,或人工神经网络。使用几何模型作为类比,你可以在复杂的几何对象上简单地堆叠多个决策边界,以创建更复杂的决策边界。

重要提示

另一个类比是,分类器的决策边界始终是一个直线超平面,但输入样本通过决策边界被转换成线性分离。

同样的几何类比帮助我们理解深度学习模型中的层。虽然网络的第一层描述了非常低级的几何特征,例如直线和线条,但更高层描述了这些低级特征的复杂嵌套组合;例如,四条线构成一个正方形,五个正方形构成一个更复杂的形状,而这些形状的组合看起来像人脸。我们就是用三层神经网络构建了一个人脸检测器。

Google DeepDream 实验是这个类比的一个绝佳例子。在下面的图中,我们可以可视化一个预训练的深度神经网络(DNN)中不同深度的三层如何表示多云天空图像中的特征。这些层是从 DNN 的开始、中间和末端提取出来的,并将输入图像转换为最小化每层的损失。在这里,我们可以看到早期层主要关注线条和边缘(左),中间层看到抽象形状(中间),而最后一层在图像的非常具体的高级特征上激活(右):

图 10.4 – DeepDream – 最小化 DNN 层的损失

图 10.4 – DeepDream – 最小化 DNN 层的损失

接下来,让我们看看 CNNs。

CNNs

使用多个高维超平面方程,其中每个输出馈送到下一层的每个输入,需要非常多的参数。虽然需要大量的参数来模拟大量的复杂训练数据,但所谓的全连接神经网络并不是描述这些连接的最佳方式。那么,问题是什么?

在全连接网络中,每个输出都被作为输入馈送到下一层的每个神经元。在每个神经元中,我们都需要为每个输入设置一个权重,因此我们需要与输入维度一样多的权重。当我们开始堆叠多个感知器层时,这个数字会迅速增加。另一个问题是,网络无法泛化,因为它为每个维度分别学习所有单个权重。

在 20 世纪 80 年代,卷积神经网络(CNNs)被发明来解决这些问题。它们的目的是将单层上的连接和参数数量减少到一个固定的参数集,这个参数集与输入维度的数量无关。现在,一个层的参数在所有输入之间是共享的。这种方法的灵感来源于信号处理,其中滤波器通过卷积操作应用于信号。卷积意味着将单一权重集,如窗口函数,应用于输入的多个区域,然后对每个位置的滤波器信号响应进行求和。

这也是卷积神经网络中卷积层相同的概念。通过使用与输入卷积的固定大小滤波器,我们可以大大减少每一层的参数数量,并在网络中添加更多嵌套层。通过使用所谓的池化层,我们还可以减少图像大小,并将滤波器应用于输入的降尺度版本。让我们看看用于构建卷积神经网络的流行层:

  • 全连接(FC):FC 层是一个全连接神经元层,如前一小节关于感知器的描述中所述——它将前一层的每个输出与一个神经元连接起来。在深度神经网络中,FC 层通常用于网络的末端,以结合前一卷积层的所有空间分布的激活。FC 层在模型中也有最多的参数(通常约为 90%)。

  • 卷积:卷积层由沿空间维度(通常是二维)卷积的空间(滤波器)组成,并在输入的深度维度上求和。由于权重共享,它们比全连接层更高效,并且参数更少。

  • 池化:卷积层通常后面跟着一个池化层来减少下一滤波器的体积的空间维度——这相当于一个子采样操作。池化操作本身没有可学习的参数。大多数情况下,由于它们简单的梯度计算,在深度学习模型中会使用最大池化层。另一个流行的选择是平均池化,它通常用作网络末端的分类器。

  • 归一化:在现代深度神经网络中,归一化层通常用于在整个网络中稳定梯度。由于某些激活函数的无界行为,滤波器响应必须归一化。常用的归一化技术是批归一化

现在我们已经了解了卷积神经网络的主要组成部分,我们可以看看这些模型是如何堆叠得更深,以提高泛化能力,从而提高预测性能。

从卷积神经网络到深度学习

20 世纪 50 年代的感知器,以及 80 年代的 ANN 和 CNN,为今天使用的所有深度学习模型奠定了基础。通过在训练过程中稳定梯度,研究人员克服了梯度爆炸和消失的问题,构建了更深的模型。这是通过使用额外的归一化层、修正线性激活、辅助损失和残差连接来实现的。

深度模型有更多的可学习参数——通常超过一亿个参数——因此它们可以找到更高层次的模式,学习更复杂的变换。然而,为了训练更深的模型,你还必须使用更多的训练数据。因此,公司和研究人员建立了大量的标记数据集(如 ImageNet),为这些模型提供训练数据。

这种发展过程得益于廉价并行计算资源(如 GPU 和云计算)的可用性。训练这些深度模型的速度在短短几年内从数月缩短到数天再到数小时。如今,我们可以在高度并行的计算基础设施下,在一小时内训练一个典型的深度神经网络(DNN)。

许多研究也投入到了新的层堆叠技术中,从具有跳转连接的非常深的网络(如 ResNet152)到具有并行层组的网络(如 GoogLeNet)。这两种层类型的组合导致了极其高效的网络架构,如 SqueezeNet 和 Inception。新的层类型如 LSTM、GRU 和注意力机制显著提高了预测性能,而 GAN 和变换模型创造了全新的训练和优化模型的方法。

所有这些进步都帮助深度学习成为今天无处不在的机器学习技术——在提供足够训练数据的情况下,它可以在大多数预测任务中超越传统的机器学习模型,甚至优于人类。如今,深度学习被应用于几乎任何有足够数据可用的领域。

深度学习与传统机器学习

让我们来看看经典机器学习方法和基于深度学习方法的区别,并了解深度学习模型如何利用更多的参数以及它们从中获得的好处。

如果我们回顾 2012 年之前的图像或音频处理领域,我们会发现机器学习模型通常不是在原始数据本身上训练的。相反,原始数据经过人工设计的特征提取器转换成低维特征空间。当处理 256 x 256 x 3 维度的图像(RGB,对应 196,608 维特征空间)并将其转换为例如 2,048 维特征嵌入作为机器学习模型的输入时,我们大大降低了这些模型的计算需求。图像和音频特征的特征提取器通常使用卷积算子和特定的滤波器(如边缘检测器、块检测器、尖峰/谷检测器等)。然而,滤波器通常是手动构建的。

在过去 50 多年中开发的经典机器学习模型仍然是今天我们成功使用的模型。其中包括基于树的集成技术、线性回归和逻辑回归、支持向量机(SVMs)和多层感知器(MLPs)。MLP 模型也被称为具有隐藏层的全连接神经网络,并且在一些早期的深度学习架构中仍作为分类或回归头使用。

下图展示了计算机视觉领域中经典机器学习方法的典型流程:

图 10.5 – 传统机器学习分类器

图 10.5 – 传统机器学习分类器

首先,使用手工制作的图像滤波器(SIFT、SURF、HoG、LBPs、Haar 滤波器等)将原始数据转换为低维特征嵌入。然后,使用特征嵌入来训练机器学习模型;例如,一个多层全连接神经网络或决策树分类器,如前图所示。

当人类难以用简单规则表达输入图像和输出标签之间的关系时,那么对于经典计算机视觉和机器学习方法来说,找到这样的规则也可能很困难。基于深度学习的方法在这些情况下表现更好。原因在于深度学习模型是在原始输入数据上而不是在手动提取的特征上训练的。由于卷积层与随机和训练的图像滤波器相同,这些用于特征提取的滤波器被网络隐式地学习。

下图显示了图像分类的深度学习方法,这与经典机器学习方法的前图类似:

图 10.6 – 基于深度学习的分类器

图 10.6 – 基于深度学习的分类器

如我们所见,原始输入图像数据直接输入到网络中,输出最终的图像标签。这就是我们通常将深度学习模型称为端到端模型的原因——因为它在输入数据(字面上,原始像素值)和模型输出之间创建了一个端到端转换。

如前图所示,基于深度学习的模型是一个端到端模型,它在一个模型中学习特征提取器和分类器。然而,我们通常指的是最后一个全连接层。

重要提示

在选择机器学习模型之前,看看你的数据类型。如果你处理的是图像、视频、音频、时间序列、语言或文本,你可能希望使用深度学习模型或特征提取器进行嵌入、聚类、分类或回归。如果你处理的是运营或业务数据,那么经典机器学习方法可能更适合。

使用基于深度学习的特征提取器进行传统机器学习

在许多情况下,尤其是当你拥有小数据集、计算资源不足或缺乏训练端到端深度学习模型的知识时,你也可以重用预训练的深度学习模型作为特征提取器。这可以通过加载预训练模型并执行正向传播直到分类/回归头部来实现。它返回一个多维嵌入(所谓的潜在空间表示),你可以直接将其插入到经典机器学习模型中。

这里是一个这种混合方法的例子。我们使用在imagenet数据上预训练的IncpetionV3模型作为特征提取器。深度学习模型仅用于将原始输入图像数据转换为低维特征表示。然后,在图像特征之上训练一个 SVM 模型。让我们看看这个例子的源代码:

import numpy as np 
from tensorflow.keras.applications import InceptionV3
def extract_features(img_data, IMG_SIZE):    
    IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)    
    model = InceptionV3(input_shape=IMG_SHAPE,
                        include_top=False,
                        weights='imagenet',
                        pooling='avg')
    predictions = model.predict(img_data)
    return np.squeeze(predictions)
labels = [] # loaded previously
features = extract_features(image_data)
X_train, X_test, y_train, y_test = train_test_split(
    features, labels)
from sklearn.svm import SVC
clf = SVC(kernel='linear', C=1)
clf.fit(X_train, y_train)

在前面的代码中,我们使用 TensorFlow 加载了基于 ImageNet 权重的InceptionV3模型,但没有任何分类或回归头。这是通过将include_top属性设置为False来实现的。然后,我们将预测的输出——图像的潜在表示——压缩成一个单一的向量。最后,我们使用 scikit-learn 和默认的 train/test 拆分在图像特征上训练了一个支持向量机(SVM)。

我们从经典方法开始,其中特征提取和 ML 被分为两个步骤。然而,在经典方法中,过滤器是手工制作的,并直接应用于原始输入数据。在深度学习方法中,我们隐式地学习特征提取。

训练用于图像分类的卷积神经网络(CNN)

既然我们已经很好地理解了为什么以及何时使用深度学习模型,我们就可以开始实施一个模型并使用 Azure Machine Learning 来运行它。我们将从一个深度学习在过去几年中表现非常出色的任务开始——计算机视觉,或者更准确地说,是图像分类。如果你觉得这对你来说太简单了,你可以用任何其他计算机视觉技术替换实际的训练脚本,并按照本节中的步骤进行操作:

  1. 首先,我们将启动一个 Azure Machine Learning 计算实例,它将作为我们的 Jupyter Notebook 创作环境。首先,我们将编写一个训练脚本并在创作环境中执行它,以验证其是否正常工作,检查点保存模型,并记录训练和验证指标。我们将训练模型几个周期以验证设置、代码和生成的模型。

  2. 接下来,我们将尝试通过在训练脚本中添加数据增强来改进算法。虽然这似乎是一个简单的任务,但我想要重申,这对于任何基于深度学习的 ML 方法来说都是必要的,并且强烈推荐。图像数据可以很容易地增强以提高泛化能力,从而提高模型评分性能。然而,通过这种技术,模型的训练将比之前更长,因为每个周期使用了更多的训练数据。

  3. 现在,我们必须将训练脚本从创作环境迁移到 GPU 集群——一个远程计算环境。我们将在这个环境中完成所有这些工作——上传数据、生成训练脚本、创建集群、在集群上执行训练脚本,以及检索训练好的模型。如果你已经在自己的服务器上自行训练 ML 模型,那么本节将展示如何将你的训练脚本迁移到远程执行环境,以及如何从动态可扩展的计算(垂直和水平扩展,因此是更大和更多的机器)、自动扩展、低成本数据存储等众多好处中受益。

  4. 一旦你成功从头开始训练了一个 CNN,你将想要在模型性能和复杂性方面进入下一个层次。一个良好且推荐的方法是微调预训练的深度学习(DL)模型,而不是从头开始训练。采用这种方法,我们通常还可以使用来自特定任务的预训练模型,从模型中移除分类头(通常是最后的一个或两个层),并通过在它之上训练我们的分类头来重用特征提取器进行另一个任务。这被称为迁移学习,并且被广泛用于训练各种领域的最先进模型。

现在,让我们打开一个 Jupyter 笔记本,开始训练一个 CNN 图像分类器。

在笔记本中从头开始训练 CNN

让我们在 Azure Machine Learning 服务上的 Jupyter 中训练一个 CNN。首先,我们只想在当前创作环境中简单地训练一个模型,这意味着我们必须使用计算实例(CPU 和内存)。这是一个标准的 Python/Jupyter 环境,所以它与在本地机器上训练 ML 模型没有区别。因此,让我们在我们的 Azure Machine Learning 服务工作区中创建一个新的计算实例,然后打开 Jupyter 环境:

  1. 在我们开始创建我们的卷积神经网络(CNN)模型之前,我们需要一些训练数据。由于我们在创作计算机上训练机器学习(ML)模型,数据需要位于同一台机器上。在这个例子中,我们将使用 MNIST 图像数据集:

    import os
    import urllib
    os.makedirs('./data/mnist', exist_ok=True)
    BASE_URL = 'http://yann.lecun.com/exdb/mnist/'
    urllib.request.urlretrieve(
        BASE_URL + 'train-images-idx3-ubyte.gz',
        filename='./data/mnist/train-images.gz')
    urllib.request.urlretrieve(
        BASE_URL + 'train-labels-idx1-ubyte.gz',
        filename='./data/mnist/train-labels.gz')
    urllib.request.urlretrieve(
        BASE_URL + 't10k-images-idx3-ubyte.gz',
        filename='./data/mnist/test-images.gz')
    urllib.request.urlretrieve(
        BASE_URL + t10k-labels-idx1-ubyte.gz',
        filename='./data/mnist/test-labels.gz')
    

在前面的代码中,我们加载了训练和测试数据,并将其放在当前环境中代码执行的data目录中。在下一节中,我们将学习如何使数据在 ML 工作区中的任何计算实例上可用。

  1. 接下来,我们必须加载数据,解析它,并将其存储在多维 NumPy 数组中。我们将使用一个辅助函数load,该函数定义在本章的配套源代码中。之后,我们必须通过将像素值归一化到01之间来预处理训练数据:

    DIR = './data/mnist/'
    X_train = load(DIR + 'train-images.gz', False) / 255.0
    X_test = load(DIR + 'test-images.gz', False) / 255.0
    y_train = load(DIR + 'train-labels.gz', True) \
                  .reshape(-1)
    y_test = load(DIR + 'test-labels.gz', True) \
                 .reshape(-1)
    

使用reshape方法,我们检查了训练和测试标签是一维向量,每个训练和测试样本都有一个标签。

一旦我们有了训练数据,就是时候决定使用哪个 Python 框架来训练神经网络模型了。虽然你在 Azure Machine Learning 中不受任何特定框架的限制,但我们建议你使用 TensorFlow(带 Keras)或 PyTorch 来训练神经网络和深度学习模型。当你训练和部署标准生产模型时,TensorFlow 和 Keras 是不错的选择。

重要提示

PyTorch 是探索异构模型和自定义层以及调试定制模型的一个很好的选择。在我看来,PyTorch 更容易上手,而 TensorFlow 则更复杂、更成熟,并且拥有更大的生态系统。在本章中,我们将使用 TensorFlow,因为它拥有庞大的生态系统、Keras 集成、优秀的文档以及在 Azure 机器学习服务中的良好支持。

  1. 选择了一个机器学习框架后,我们可以开始构建一个简单的 CNN。让我们使用keras构建一个序列模型:

    from tensorflow.keras.models import Sequential
    from tensorflow.keras.layers import Conv2D, \
        MaxPooling2D, Flatten, Dense
    model = Sequential()
    model.add(Conv2D(filters=16,
                     kernel_size=3,
                     padding='same',
                     activation='relu',
                     input_shape=(28,28,1)))
    model.add(MaxPooling2D(pool_size=2))
    model.add(Conv2D(filters=32,
                     kernel_size=3,
                     padding='same',
                     activation='relu'))
    model.add(MaxPooling2D(pool_size=2))
    model.add(Flatten())
    model.add(Dense(256, activation='relu'))
    model.add(Dense(10, activation='softmax'))
    

在前面的代码中,我们利用了keras.Sequential模型 API 构建了一个简单的 CNN 模型。我们采用了默认的权重初始化,并在这里仅指定了模型结构。您还可以看到典型的特征提取器组合,直到Flatten层,以及使用softmax激活函数在末尾输出 10 个概率的 MLP 分类头。

让我们快速看一下模型,该模型总共有409034个参数,如下面的图所示。请注意,我们特别构建了一个简单的 CNN,其输入图像尺寸为28x28的灰度图像。下面的图显示了模型定义的紧凑结构。在这里,我们可以观察到最大的参数数量是在特征提取器之后的全连接层,它包含了总模型参数的 98%:

图 10.7 – 深度学习模型架构

图 10.7 – 深度学习模型架构

  1. 在定义了模型结构之后,我们需要定义我们试图优化的loss指标,并指定一个优化器。优化器负责在每次训练迭代中计算所有权重的变化,给定总损失和反向传播的损失。使用 Keras 和 TensorFlow,我们可以轻松选择一个最先进的优化器,并为分类使用默认的指标:

    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    

在前面的代码中,我们定义了一个categorical_crossentropy损失和adam优化器来训练 CNN。我们还跟踪了除了损失之外的另一个指标——accuracy。这使得在训练过程中更容易估计和衡量 CNN 的性能。

  1. 在开始训练之前,我们必须定义一个模型检查点。这很重要,因为它允许我们在每个 epoch 之后在任何给定时间暂停和恢复训练。使用 Keras,实现这一点相当简单,如下所示:

    from tensorflow.keras.callbacks import ModelCheckpoint
    checkpoint_path = "./mnist_cnn.bin"
    checkpoint_cb = ModelCheckpoint(checkpoint_path)
    
  2. 最后,我们可以通过在 Keras 模型上调用fit方法来在本地开始训练。我们必须提供训练数据以及训练的批大小和 epoch(迭代)数。我们还必须传递之前创建的callback模型检查点,这样我们就可以在每个 epoch 后保存模型:

    model.fit(X_train,
              y_train,
              batch_size=16,
              epochs=10,
              callbacks=[checkpoint_cb])
    
  3. 最后,我们可以使用最后一个 epoch 训练好的模型在测试集上计算最终得分:

    from tensorflow.keras.models import load_model
    model = load_model(checkpoint_path)
    scores = model.evaluate(X_test, y_test, verbose=1)
    print('Test loss:', scores[0])
    print('Test accuracy:', scores[1])
    

在前面的代码中,我们可以看到在 Azure Machine Learning 的计算实例上训练 CNN 是直接且与在本地机器上训练模型类似的。唯一的区别是我们必须确保所有必需的库(及其所需版本)都已安装,并且数据是可用的。

使用增强生成更多输入数据

DL 模型通常有数百万个参数来表示训练集分布的模型。因此,在处理 DL 时,无论是使用认知服务进行自定义视觉、Azure Machine Learning Studio 或 ML 服务工作区中的自定义模型,都应该始终实现数据增强。

数据增强是一种通过稍微修改现有数据并提供修改后的数据给 ML 算法来创建更多训练数据的方法。根据用例的不同,这可能包括镜像、平移、缩放或倾斜图像,以及改变图像的亮度、亮度和颜色信息。这些修改可以极大地提高模型的泛化能力,例如,使模型能够更好地实现尺度、平移、旋转和变换的不变性。

使用 TensorFlow 和 Keras 的好处是数据增强是一个内置功能。首先,我们可以创建一个 ImageDataGenerator 对象,该对象存储了所有我们的修改,并且可以通过增强数据集生成迭代器。此生成器的数据增强技术可以在初始化生成器时进行配置。然而,我们希望使用生成器简单地遍历训练图像而不进行增强,并在连接好所有组件后再添加增强。让我们看一下:

  1. 让我们在 Keras 中使用 ImageDataGenerator 对象实现一个图像数据生成器:

    from tensorflow.keras.preprocessing.image import \
        ImageDataGenerator
    datagen = ImageDataGenerator()
    
  2. 现在,我们可以通过将原始训练图像数据和标签传递给生成器来从图像数据生成器返回一个数据迭代器。在我们从生成器中采样图像之前,我们需要计算训练集统计信息,这些统计信息将用于进一步的增强。类似于 scikit-learn 的 BaseTransformer 接口,我们需要在生成器上调用 fit 方法:

    datagen.fit(x_train)
    
  3. 接下来,我们必须使用 flow 方法创建一个迭代器:

    it = datagen.flow(X_train, y_train, batch_size=16)
    
  4. 如果我们不想事先将图像加载到 NumPy 数组中,而是想从文件夹中读取单个图像,我们可以使用不同的生成器函数来完成,如下面的代码片段所示:

    it = datagen.flow_from_directory(
             directory='./data/mnist',
             target_size=(28, 28),
             batch_size=16,
             class_mode='categorical')
    

然而,在我们的例子中,训练图像已经被合并到一个单独的文件中,因此我们不需要自己加载图像数据。

  1. 现在,我们可以使用迭代器来遍历数据生成器,并在每次迭代中产生新的训练样本。为此,我们需要用 fit_generator 函数替换 fit 函数,该函数期望一个迭代器而不是训练数据集:

    model.fit_generator(it,
                        steps_per_epoch=256,
                        epochs=10,
                        callbacks=[checkpoint_cb])
    

如我们所见,我们可以将相同的epochcallback参数传递给fit_generator函数,就像我们传递给fit函数一样。唯一的区别是现在,我们需要在每个 epoch 中固定几个步骤,以便迭代器产生新的图像。一旦我们将增强方法添加到生成器中,理论上我们可以在每个 epoch 中为每个训练图像生成无限多的修改。因此,使用此参数,我们可以定义我们希望每个 epoch 训练多少批数据,这应该大致对应于训练样本数除以批大小。

最后,我们可以配置数据增强技术。默认的图像数据生成器通过不同的参数支持各种增强:

  • 翻译或平移

  • 水平或垂直翻转

  • 旋转

  • 亮度

  • 缩放

让我们回到图像数据生成器并激活数据增强技术。以下是一个常用于图像处理数据增强的示例生成器:

datagen = ImageDataGenerator(
              featurewise_center=True,
              featurewise_std_normalization=True,
              rotation_range=20,
              width_shift_range=0.2,
              height_shift_range=0.2,
              horizontal_flip=True)

通过使用此数据生成器,我们可以使用增强图像数据来训练模型,并进一步提高 CNN 的性能。正如我们之前所看到的,这是任何深度学习(DL)训练流程中的关键步骤,并且强烈推荐。

让我们将迄今为止开发的全部代码移动到一个名为scripts/train.py的文件中。我们将在下一节中使用此文件在 GPU 集群上安排和运行它。

使用 Azure 机器学习在 GPU 集群上进行训练

现在我们已经准备好训练脚本,验证了脚本的工作,并添加了数据增强,我们可以将此训练脚本移动到更高效的执行环境中。在深度学习中,许多操作,如卷积、池化和通用张量运算符,可以从并行执行中受益。因此,我们将训练脚本在 GPU 集群上执行,并在创作环境中跟踪其状态。

使用 Azure 机器学习的优点之一是我们可以从创作环境中设置和运行所有内容——即运行在 Azure 机器学习计算实例上的 Jupyter 笔记本:

  1. 首先,我们必须配置我们的 Azure 机器学习工作区,这是一个在计算实例上不带参数的单个语句:

    from azureml.core.workspace import Workspace
    ws = Workspace.from_config()
    
  2. 接下来,我们必须为训练过程加载或创建一个具有自动扩展功能的 GPU 集群:

    from azureml.core.compute import ComputeTarget, \
        AmlCompute
    from azureml.core.compute_target import \
        ComputeTargetException
    cluster_name = "gpu-cluster"
    vm_size = "STANDARD_NC6"
    max_nodes = 3
    try:
        compute_target = ComputeTarget(ws, 
            name=cluster_name)
        print('Found existing compute target.')
    except ComputeTargetException:
        print('Creating a new compute target...')
        compute_config = \
            AmlCompute.provisioning_configuration(
                vm_size=vm_size, max_nodes=max_nodes)
        # create the cluster and wait for completion
        compute_target = ComputeTarget.create(ws, 
            cluster_name, compute_config)
    compute_target.wait_for_completion(show_output=True)
    

如前述代码片段所示,使用 Azure 机器学习创建具有自动扩展功能的 GPU 集群只需要在 Jupyter 中编写几行代码。但我们是如何选择虚拟机大小和 GPU 集群节点数量的呢?

通常,您可以从 Azure 的 N 系列虚拟机中的 NC、ND 和 NV 类型中进行选择。较晚的版本号(例如,v2 或 v3)通常意味着更新的硬件,因此有更新的 CPU 和 GPU,以及更好的内存。您可以将不同的 N 系列版本视为应用类型(NC,其中 C 代表计算;ND,其中 D 代表深度学习;NV,其中 V 代表视频)。以下表格将帮助您比较不同的 N 系列虚拟机类型及其 GPU 配置。大多数机器可以扩展到每个虚拟机四个 GPU。

下表显示了 Azure VM N 系列的比较:

图 10.8 – Azure VM N 系列成本

图 10.8 – Azure VM N 系列成本

上表中的价格代表 2021 年 12 月美国西部 2 区域 Linux VM 的按量付费价格。请注意,这些价格在你阅读此内容时可能已经发生变化,但它应该能给你一个不同选项和配置的选择指示。

为了更好地了解成本和性能,我们可以查看在 ImageNet 数据集上训练 ResNet50 模型的典型工作负载。以下由 Nvidia 提供的表格显示,选择最新的 GPU 模型是有意义的,因为它们的性能提升更好,成本也更有效率,比旧款 GPU 模型更优。

图 10.9 – GPU 成本

图 10.9 – GPU 成本

如前表所示,对于相同任务的较短的训练时间所显示的性能提升是有回报的,并且导致整体任务的成本大大降低。

因此,从定价角度来看,STANDARD_NC6 模型是开始在 Azure 上进行 GPU、CNN 和 DNN 实验的一个很好的起点。我们唯一需要确保的是我们的模型可以适应 VM 可用的 GPU 内存。计算这个的一个常见方法是计算模型的参数数量,乘以 2 以存储梯度(仅进行推理时乘以 1),乘以批处理大小,再乘以 4 以字节为单位表示的单精度大小(或乘以 2 以表示半精度)。

在我们的例子中,CNN 架构需要 1.6 MB 来存储可训练参数(权重和偏差)。为了存储批处理大小为 16 的反向传播损失,我们需要大约 51.2 MB(1.6 MB x 16 x 2)的 GPU 内存来在单个 GPU 上执行整个端到端训练。这也很容易适应我们最小的 NC 实例中的 12 GB GPU 内存。

重要提示

虽然这些数字对于我们的测试案例来说似乎很小,但你经常会处理更大的模型(参数数量高达 1 亿)和更大的图像尺寸。为了更直观地说明这一点,ResNet152 在 224 x 224 x 3 的图像尺寸上训练时,大约有 6000 万个参数和 240 MB 的大小。根据我们的公式,在 STANDARD_NC6 实例上,我们最多可以在批处理大小为 24 的情况下进行训练。

通过向集群添加更多的 GPU 或节点,我们必须引入一个不同的框架来利用分布式设置。我们将在第十二章,“Azure 上的分布式机器学习”中更详细地讨论这个问题。然而,我们可以通过自动扩展添加更多的节点到集群,这样多个人可以同时提交多个作业。最大节点数可以计算为每个节点的并发模型数乘以同时要训练的峰值模型数。在我们的测试场景中,我们将选择3个节点的集群大小,这样我们就可以同时安排几个模型。

  1. 既然我们已经决定了虚拟机的大小和 GPU 配置,我们就可以继续进行训练过程。接下来,我们需要确保集群可以访问训练数据。为此,我们将使用 Azure 机器学习工作区上的默认数据存储库:

    ds = ws.get_default_datastore()
    ds.upload(src_dir='./data/mnist',
              target_path='mnist',
              show_progress=True)
    

在前面的代码中,我们将训练数据从本地机器复制到了默认的数据存储库——blob 存储账户。正如我们在第四章,“数据摄取与管理数据集”中讨论的那样,还有其他方法可以将您的数据上传到 blob 存储或其他存储系统。

将 blob 存储挂载到机器上,甚至是一个集群,通常不是一个简单的过程。是的,您可以在集群的每个节点上挂载一个 NAS 作为网络驱动器,但这设置和扩展起来都很繁琐。使用 Azure 机器学习数据存储 API,我们可以简单地请求一个数据存储库的引用,这个引用可以用来在每个需要访问数据的机器上挂载正确的文件夹:

ds_data = ds.as_mount()

前面的命令返回一个Datastore Mount对象,看起来并不特别强大。然而,如果我们把这个引用作为参数传递给训练脚本,它可以在 Azure 机器学习中的每个训练计算上自动挂载数据存储库并读取内容。如果您曾经玩过挂载点或fstab,您会理解这一行代码可以加快您的日常工作流程。

  1. 现在,我们可以创建一个 Azure 机器学习配置。让我们创建ScriptRunConfiguration,这样我们就可以在集群上安排训练脚本:

    from azureml.core import ScriptRunConfig
    script_params={
        '--data-dir': ds_data
    }
    src = src = ScriptRunConfig(
        source_directory='./scripts',
        script='train.py',
        compute_target=compute_target,
        environment=tf_env)
    
  2. 要从指定的默认数据存储库读取数据,我们需要解析train.py脚本中的参数。让我们回到脚本,用以下代码块替换文件加载代码:

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--data-dir', type=str)
    args = parser.parse_args()
    DIR = args.data_dir
    X_train = load(DIR + 'train-images.gz', False) / 255.0
    X_test = load(DIR + 'test-images.gz', False) / 255.0
    y_train = load(DIR + 'train-labels.gz', True) \
                  .reshape(-1)
    y_test = load(DIR + 'test-labels.gz', True) \
                  .reshape(-1)
    
  3. 这就剩下在 GPU 集群上安排和运行脚本了。然而,在这样做之前,我们想要确保所有运行都在 Azure 机器学习服务中被跟踪。因此,我们还需要在train.py文件中添加Run,并重用来自第三章,“准备 Azure 机器学习工作区”的 Keras 回调。以下是训练脚本的样子:

    from azureml.core import Run
    # Get the run configuration
    run = Run.get_context()
    # Create an Azure Machine Learning monitor callback
    azureml_cb = AzureMlKerasCallback(run)
    callbacks = [azureml_cb, checkpoint_cb]
    model.fit_generator(it,
                        steps_per_epoch=256,
                        epochs=10,
                        callbacks=callbacks)
    # Load the best model
    model = load_model(checkpoint_path)
    # Score trained model
    scores = model.evaluate(X_test, y_test, verbose=1)
    print('Test loss:', scores[0])
    run.log('Test loss', scores[0])
    print('Test accuracy:', scores[1])
    run.log('Test accuracy', scores[1])
    

正如我们所见,我们添加了Run配置和 Keras 回调来跟踪整个训练过程中的所有指标。我们还收集了最终的测试集指标,并将其报告给 Azure 机器学习服务。您可以在本书提供的代码中找到完整的可运行示例。

通过转移学习提高您的性能

在许多情况下,您可能没有包含数亿个标记训练样本的数据集,这是完全可以理解的。那么,您如何还能从所有之前的工作和基准测试中受益呢?难道在识别动物的特征提取器训练后,在识别面部时表现不佳吗?分类器当然会不同,但从图像中提取的视觉特征应该是相似的。

这就是faces数据集、CoCo数据集等背后的想法,并在模型的末端附加一个自定义分类器。转移学习意味着我们可以将一个模型从一项任务的特征转移到另一项任务:例如,从分类到目标检测。一开始可能会有些困惑,是否希望为不同的任务重用特征。然而,如果一个模型已经被训练来识别图像中的地理形状模式,这个相同的特征提取器当然可以用于同一领域中的任何与图像相关的任务。

转移学习的一个有用特性是,初始学习任务不一定需要是一个监督式机器学习任务,因此不需要有标注的训练数据来训练特征提取器。一种流行的无监督机器学习技术称为自编码器,其中机器学习模型试图使用特征提取器和上采样网络,根据输入生成类似的外观输出。通过最小化生成的输出与输入之间的误差,特征提取器学会在潜在空间中高效地表示输入数据。自编码器在预训练网络架构之前,使用实际机器学习任务的预训练权重之前很受欢迎。

我们需要确保预训练模型是在同一领域的数据集上训练的。生物细胞图像与面部看起来非常不同,云与建筑物看起来也非常不同。一般来说,ImageNet 数据集涵盖了广泛的照片风格图像,用于许多标准视觉特征,如建筑物、汽车、动物等。因此,对于许多计算机视觉任务,使用预训练模型是一个很好的选择。

转移学习不仅与计算机视觉中的图像数据和建模数据相关。转移学习在数据集足够相似的所有领域都已被证明是有价值的,例如人类声音或书面文本。因此,无论何时您在实现深度学习模型时,都要研究可用于转移学习的数据集,以及最终提高模型性能。

让我们将理论应用于实践,并深入研究一些示例。我们在本章前面看到了一个类似的例子,其中我们将特征提取器的输出管道化到一个 SVM。在本节中,我们想要实现类似的效果,但结果将是一个单一端到端模型。因此,在这个例子中,我们将构建一个由预训练的特征提取器和新的分类器头部组成的新模型网络架构:

  1. 首先,我们必须定义输出类的数量、输入形状,并从 Keras 加载基本模型:

    from tensorflow.keras.applications.resnet50 \
        import ResNet50
    num_classes = 10
    input_shape = (224, 224, 3)
    # create the base pre-trained model
    base_model = ResNet50(input_shape=input_shape, 
                          weights='imagenet',
                          include_top=False,
                          pooling='avg')
    

在前面的代码中,预训练的大部分魔法都归功于 Keras。首先,我们使用weights参数指定了将用于训练此模型的图像数据集,这将自动使用预训练的imagenet权重初始化模型权重。通过第三个参数include_top=False,我们告诉 Keras 只加载模型的特征提取部分。使用pooling参数,我们还指定了最后一个池化操作应该如何执行。在这种情况下,我们选择了平均池化。

  1. 接下来,我们必须通过将它们的trainable属性设置为False来冻结模型的层。为此,我们可以简单地遍历模型中的所有层:

    for layer in base_model.layers:
        layer.trainable = False
    
  2. 最后,我们可以将任何网络架构附加到我们想要的模型上。在这种情况下,我们将附加与上一节中 CNN 网络中使用的相同分类器头部。最后,我们必须使用新的架构和输出作为分类器输出层来构建最终的模型类:

    from tensorflow.keras.models import Model
    from tensorflow.keras.layers import Flatten, Dense
    clf = base_model.output
    clf = Dense(256, activation='relu')(clf)
    clf = Dense(10, activation='softmax')(clf)
    model = Model(base_model.input, clf)
    

就这样!你已经成功构建了一个新的端到端模型,该模型结合了在 ImageNet 上预训练的 ResNet50 特征提取器以及你的自定义分类器。现在你可以使用这个 Keras 模型,将其插入你偏好的优化器中,并发送到 GPU 集群。训练过程的输出将是一个可以像任何其他自定义模型一样管理和部署的单个模型。

重要提示

你不必总是冻结原始网络的所有层。一个常见的方法是同时解冻网络中的后续层,将学习率至少降低 10 倍,并继续训练。通过重复此过程,我们甚至可以以逐步降低学习率的方式重新训练(或微调)网络的所有层。

不论你的选择和使用案例如何,你应该将迁移学习添加到你的标准训练深度学习模型的方法库中。将其视为其他流行的预处理和训练技术,例如数据增强,这些技术应该在训练深度学习模型时始终使用。

摘要

在本章中,我们学习了何时以及如何使用深度学习(DL)在 Azure 上训练机器学习(ML)模型。我们使用了 Azure 机器学习服务中的计算实例和 GPU 集群来使用 Keras 和 TensorFlow 训练模型。

首先,我们发现深度学习在具有非明显关系的结构化数据上工作得非常好,这些关系是从原始输入数据到最终预测结果。好的例子包括图像分类、语音转文本和翻译。我们还看到,深度学习模型是具有大量参数的参数模型,因此我们通常需要大量的标记或增强的输入数据。与传统机器学习方法相比,额外的参数用于训练一个完全端到端的模型,这还包括从原始输入数据中提取特征。

使用 Azure 机器学习服务训练 CNN 并不困难。我们看到了许多方法,从在 Jupyter 中进行原型设计到增强训练数据,再到在具有自动扩展功能的 GPU 集群上运行训练。在深度学习中,困难的部分在于准备和提供足够的高质量训练数据,找到一个描述性的错误度量标准,以及在成本和性能之间进行优化。我们概述了如何为您的任务选择最佳的虚拟机和 GPU 大小及配置,我建议您在开始您的第一个 GPU 集群之前先做这件事。

在下一章中,我们将进一步探讨超参数调整和自动机器学习,这是 Azure 机器学习服务中的一个功能,允许您自动训练和优化堆叠模型。