本文提到的数据和代码在第12章中已经涉及,如下图所示:
- 读取一个包含菜名和多个列作为特征候选的CSV文件。
- 对列名进行了消毒(降低了大小写,固定了标点符号、间距和不可打印的字符)。
- 删除了不符合逻辑和不相关的记录
- 将二进制列的
null值填充为0.0 - 将
calories,protein,fat, 和sodium的金额上限设定为99%的百分位数 - 创建比率特征(来自宏的卡路里数与菜的卡路里数之比)。
- 归入连续特征的
mean。 - 在
0.0和1.0之间对连续特征进行缩放。
你可以在本书的资源库中使用导致food_特征的代码,地址是./code/Ch12/end_of_chapter.py
在这个节选中,我们继续我们的旅程,以实现一个强大的机器学习训练程序。为了帮助我们,我们深入研究了转化器和估计器,这次是在ML管道的背景下。
ML管道是PySpark实现机器学习能力的方式。它们提供了更好的代码组织和灵活性,但代价是前期的一些准备。这篇文章首先解释了什么是ML管道,使用甜点预测数据集。我们回顾一下关于变换器、估计器和ML管道的足够的理论,让我们开始。
变换器和估计器:Spark中ML的构建块
我们将介绍ML管道的两个主要组成部分:转化器和估算器。我们在可重用和可参数化的构建块的背景下,再看一下变换器和估算器。从36000英尺的视角来看,ML管道是一个有序的转化器和估算器的列表。尽管如此,至关重要的是,我们不仅要了解如何创建,而且要了解如何首先修改这些构件,以便以最佳效率使用ML管道。
变换器和估计器是ML建模中非常有用的类。当我们训练一个ML模型时,我们会得到一个拟合的模型,这就类似于一个我们没有明确编码的新程序。然后这个新的数据驱动的程序有一个唯一的目的:取一个正确格式化的数据集,并通过附加一个预测列对其进行转换。接下来,我们看到转化器和估计器不仅为ML建模提供了一个有用的抽象,它们还通过序列化和反序列化提供了可移植性。这意味着你可以训练和保存你的ML模型并将其部署在另一个环境中。
为了说明转化器和估计器是如何被参数化的,我们将使用第12章中定义并使用的转化器和估计器。
continuous_assembler,一个VectorAssembler的转化器,它提取了五个列并创建了一个用于模型训练的Vector列。consinuous_scaler, 一个MinMaxScaler估算器,对包含在向量列中的值进行缩放,为向量中的每个元素返回0和1之间的值。
为方便起见,我在列表1中包括了相关的代码。我们从转换器开始,然后在此基础上引入估计器。
清单1:VectorAssembler 和MinMaxScaler 本节中我们将探讨的例子
CONTINUOUS_NB = ["rating", "calories_i", "protein_i", "fat_i", "sodium_i"]
continuous_assembler = VectorAssembler(
inputCols=CONTINUOUS_NB, outputCol="continuous"
)
continuous_scaler = MinMaxScaler(
inputCol="continuous",
outputCol="continuous_scaled",
)
数据来了,数据出来了。Transformer
本节正式介绍了转化器,作为ML管道的第一个构建块。我们介绍了一般的转化器蓝图以及如何访问和修改其参数化。当我们想用我们的ML代码运行实验或优化我们的ML模型时,这个关于转化器的附加背景起到了关键作用。
在我们的VectorAssembler 变换器例子中,我们向构造器提供了两个参数:inputCols 和outpulCol 。这些参数提供了必要的功能来创建一个全功能的VectorAssembler 变换器。这个转化器的唯一目的是--通过它的transform() 方法--获取inputCols 中的值(集合的值)并返回一个单列,命名为outputCol ,其中包含所有集合值的向量。

图1.continuous_assembler 变换器,以及它的Params。变换器使用transform() 方法将预定义的转换应用到作为输入传递的数据帧上。
变换器的参数化被称为Params(大写P)。当实例化一个转化器类时,就像对待任何Python类一样,我们将我们想要的参数作为参数传递,确保明确地指定每个关键字。一旦转化器被实例化,PySpark为我们提供了一组方法来提取和修改Params。接下来的章节将介绍在转化器实例化之后检索和修改Params。
偷看VectorAssembler 的签名:只有关键字的参数
如果你看一下VectorAssembler (以及pyspark.ml 模块中几乎所有的转化器和估算器) 的签名,你会看到参数列表的开头有一个星号:
class pyspark.ml.feature.VectorAssembler(*, inputCols=None, outputCol=None, handleInvalid='error')
在 Python 中,星号之后的每一个参数* 都被称为只用关键字的参数,这意味着我们需要提到这个关键字。例如,我们不能做VectorAssembler("input_column", "output_column") 。更多信息,请参考PEP (Python Enhancement Proposal) 3102,网址是www.python.org/dev/peps/pe…
偷看引擎盖下的东西:获取和解释 Params
回顾一下图 1,VectorAssembler 的实例化接受了三个参数。inputCols,outputCol, 和handleInvalid 。我们还暗示过,一个转化器(和估算器,在同一场合)类实例的配置依赖于Params,它驱动着转化器的行为。在这一节中,我们将探讨Params,强调它们与普通类属性相比的异同,以及为什么这些差异很重要。你可能会想*"好吧,我知道如何从Python类中获取属性,而转化器就是Python类*"。虽然这是正确的,但转化器(和估算器)遵循更类似于Java/Scala的设计,我建议不要跳过这一部分。它很短,很有用,而且会帮助你在使用ML管道时避免头痛。
首先,让我们做任何Python开发者都会做的事情,直接访问转化器的一个属性。在列表 2 中,我们看到访问continuous_assembler 的outputCol 属性并没有得到continuous ,就像我们传递给构造函数一样。相反,我们得到了对一个叫做 Param 的对象的引用 (pyspark.ml.param.Param 类),该对象包装了我们的变压器的每个属性。
清单2.直接访问一个变换器的参数会得到一个对象(称为Param ):
print(continuous_assembler.outputCol)
# VectorAssembler_e18a6589d2d5__outputCol
❶我们没有返回 作为参数传递给 outputCol 的continuous 值**,而是得到** 一个****叫做 Param 的对象。
要直接访问一个特定参数的值,我们使用一个getter方法,即简单地把单词get ,然后用CamelCase写上我们的参数名称。在outputCol 的例子中,如清单3所示,getter方法被称为getOutputCol() (注意大写的O)。
清单3.通过outputCol Param访问其值。getOutputCol()
print(continuous_assembler.getOutputCol()) # => continuous
到目前为止,Param似乎增加了模板,但没有什么好处。explainParam() ,改变了这一点。这个方法提供了关于Param以及其值的文档。这最好用一个例子来解释,我们在清单4中看到了解释outputCol Param的输出。
如果你想一次看到所有的Param,你也可以使用复数的版本,explainParams() 。这个方法不需要参数,将返回一个以新行分隔的所有Param的字符串。
该字符串的输出包含
- Param的名称:
outputCol。 - 对Param的简短描述:
output column name.。 - 该参数的
defaultParam的值。VectorAssembler_e18a6589d2d5__output的值,如果我们不明确地传递一个值,就会使用。 - 和 Param的
current值:continuous。
清单4.解释outputCol Param 与explainParam
print(continuous_assembler.explainParam("outputCol"))
# outputCol: output column name.
❶ outputCol Param的名称和简短描述 。
❷ 即使我们为 outputCol定义了一个值。
在这一节中,我们掌握了从我们的变换器的Param中获取相关信息的方法。这一节也逐字逐句地适用于估算器。在下一节中,我们不再看Params,而是开始改变它们。之后,转化器将不再有任何秘密了
普通的getParam() 方法呢?
变换器(和估计器)提供普通的getParam() 。它只是简单地返回Param,就像访问本节开头的outputCol 一样。我相信这样做是为了让PySpark的变换器可以和他们的Java/Scala等价物有一个一致的API。
使用getters和setters来设置一个实例化的转化器的参数
就像上一节关于获取Params的内容一样,设置Params对估计器也有同样的作用。
在这一节中,我们修改一个变换器的Params!就这么简单!这主要在两种情况下有用。
- 你在REPL中构建你的转化器,你想尝试不同的Param-eterization。
- 你正在优化你的ML管道的Params,就像我们在 "我是谁 "中做的那样。
我们如何改变转化器的Params?对于每一个getter,都有一个setter,简单地说就是把set ,后面是我们的Param的CamelCase的名字。与getter不同,setter把新的值作为唯一的参数。在列表5中,我们使用相关的setter方法将outputCol Param改为more_continuous 。这个操作返回了转换后的变换器,但也在原地进行了修改,这意味着你不必将setter的结果分配给一个变量。
清单5.将outputCol Param 设置为more_continuous ,修改是在原地完成的:
continuous_assembler.setOutputCol("more_continuous")
❶ 虽然 setOutputCol() 方法返回一个新的变换器对象,但它也在原地进行了修改,所以我们不必将结果赋给一个变量。
如果你需要一次改变多个Params(例如,你想在实验不同场景时一次性改变输入和输出列),你可以使用setParams() 方法。setParams() 的签名与构造函数完全相同:你只是把新值作为关键字传递,如清单6所示。
清单6.一次性改变多个Params,使用setParams()
continuous_assembler.setParams(
inputCols=["one", "two", "three"], handleInvalid="skip"
)
print(continuous_assembler.explainParams())
# handleInvalid: How to handle invalid data (NULL and NaN values). [...]
# (default: error, current: skip)
# inputCols: input column names. (current: ['one', 'two', 'three'])
❶ 没有传递给 setParams 的参数 保持它们之前的值(在清单 5 中设置的)。
最后,如果你想把一个Param返回到它的默认值,你可以使用clear() 方法。这一次,你需要传递Param 对象:例如,在清单 7 中,我们通过使用 clear 重置handleInvalid Param。我们把实际的Param作为参数传递,通过本节开头看到的属性槽访问,continuous_assembler.handleInvalid 。如果你有一个转化器,同时有inputCol/outputCol 和inputCols/outputCols 作为可能的Param,这将证明是有用的。PySpark只允许一次激活一个集合,所以如果你想在一列和多列之间移动,你需要clear() 那些不被使用的。
清单7.清除handleInvalid Param的当前值与clear()
continuous_assembler.clear(continuous_assembler.handleInvalid)
print(continuous_assembler.getHandleInvalid()) # => error
❶ handleInvalid 返回到它的原始值, error 。
这就是了,伙计们!在这一节中,我们更详细地学习了变压器的方法和原因,以及如何获取、设置和清除它的 Param。在下一节中,我们将应用这些有用的知识来加速ML管道的第二个构件--估算器。
变换器和估计器是通过引用传递的:copy() 方法
到目前为止,在我们的PySpark旅程中,我们一直在使用一个流畅的API,其中每个数据帧的转换都会生成一个新的数据帧。这实现了方法链,使我们的数据转换代码非常容易阅读。
当使用变换器(和估算器)时,请记住它们是通过引用传递的,并且设置器会就地修改对象。如果你把你的变换器分配给一个新的变量名,然后对其中任何一个变量使用一个设置器,它将修改两个引用的Param。
new_continuous_assembler = continuous_assembler
new_continuous_assembler.setOutputCol("new_output")
print(new_continuous_assembler.getOutputCol()) # => new_output
print(continuous_assembler.getOutputCol()) # => new_output
❶ continuous_assembler 和 new_continuous_assembler 的 outputCol 都被 setter 所修改。
解决这个问题的方法是:copy() 变换器,然后将副本分配给新变量。
copy_continuous_assembler = continuous_assembler.copy()
copy_continuous_assembler.setOutputCol("copy_output")
print(copy_continuous_assembler.getOutputCol()) # => copy_output
print(continuous_assembler.getOutputCol()) # => new_output
❶ 在进行复制时,对 copy_continuous_assembler 的 Params 的修改 不会影响 continuous_assembler
数据进来了,转化器出来了: Estimator
本节介绍估算器,即ML管道的后半部分。就像转化器一样,了解如何操作和配置估算器是创建高效ML管道的宝贵步骤。变换器将一个输入数据帧转换为一个输出数据帧,而估计器则是在一个输入数据帧上进行拟合,并返回一个输出变换器。在这一节中,我们看到转化器和估计器之间的这种关系意味着它们被Param-eterized的方式与中解释的一样。我们关注的是通过fit() 方法的估计器使用(相对于变压器的transform() ),这确实是对最终用户来说唯一值得注意的区别。
变换器使用transform() 方法,应用于一个数据框架,以返回一个转换后的数据框架,而估算器使用 fit() 方法,应用于一个数据框架,以返回一个完全参数化的变换器,称为Model 。这种区别使估算器能够根据输入数据来配置变换器。
作为一个例子,图2中的MinMaxScaler 估计器需要四个参数,其中两个我们依靠默认值:
min和 ,它们是我们的比例列将采取的最小和最大的值。我们将这两个参数分别保持在默认的 和 。max0.01.0inputCols和 ,分别是输入和输出列。它们遵循与变换器相同的惯例。outputCols
为了缩放min 和max 之间的值,我们需要从输入列中提取最小值(我称之为E_min )和最大值(E_max )。E_min 被转换为0.0 ,E_max 被转换为1.0 ,而介于两者之间的任何值则使用以下公式在min 和max 之间取值(参见本节末尾的练习,以了解E_max 和E_min 相同时的一个角落(或边缘)案例)。

因为转换依赖于数据的实际值,我们不能使用普通的转换器,它期望在应用transform() 方法之前 "知道 "一切(通过其Param-eterization)。在MinMaxScaler 的情况下,我们可以把E_min 和E_max 翻译成简单的操作(max 来自pyspark.sql.functions ):
- E_min = min(inputCol)
- E_max = max(inputCol)
一旦这些值被计算出来(在fit() 方法期间),PySpark就会创建,Param-meterizes,并返回一个转换器/模型。

图2.MinMaxScaler 估计器,以及它的Params。变换器使用fit() 方法来创建和参数化一个Model (变换器的一个子类型),使用作为参数传递的数据框架。
这种fit()/transform() 方法适用于远比MinMaxScaler 复杂的估计器。 案例。ML模型实际上是在Spark中作为估计器实现的。