pyspark。生成随机数的变换器始终生成相同的数字

时间:2016-06-21 13:33:03

标签: python apache-spark random pyspark apache-spark-sql

我正在尝试衡量性能对必须将dataframe从scala复制到python并返回到大型管道中的影响。为此我创造了这个相当人为的变压器:

from pyspark.ml.pipeline import Transformer
from pyspark.ml.param.shared import HasInputCol, HasOutputCol, Param
from pyspark.ml.util import keyword_only
from pyspark.sql.functions import udf
from pyspark.sql.types import FloatType

import random

class RandomColAdderTransformer(Transformer, HasInputCol, HasOutputCol):

    @keyword_only
    def __init__self(self, inputCol=None, outputCol=None, bogusarg=None):
        super(RandomColAdderTransformer, self).__init__()
        self.bogusarg = None
        self._setDefault(bogusarg=set())
        kwargs = self.__init__._input_kwargs
        self.setParams(**kwargs)

    @keyword_only
    def setParams(self, inputCol=None, outputCol=None):
        kwargs = self.setParams._input_kwargs
        return self._set(**kwargs)

    def _transform(self, dataset):
        cur_col = self.getInputCol()
        def randGet(col): # UDF crashes with no arguments
            a = col*random.random() # Ensure we are reading and copying to python space 
            return a            # It runs only once?

        sparktype = FloatType()
        return dataset.withColumn("randFloat", udf(randGet, sparktype)(cur_col))

这个变换器的目标是确保有一些从python生成的数字,它访问dataframe并进行乘法(在python中)然后对于管道的下一个阶段它将具有将列添加到dataframe

但是我有些奇怪。在测试我的代码时,会为所有列生成相同的随机数:

df = sqlContext.createDataFrame([(1, "a", 23.0), (3, "B", -23.0)], ("x1", "x2", "x3"))
myTestTransformer = RandomColAdderTransformer()
myTestTransformer.setInputCol("x3")
transformedDF = myTestTransformer.transform(df)
transformedDF.show()

+---+---+-----+-----------+
| x1| x2|   x3|  randFloat|
+---+---+-----+-----------+
|  1|  a| 23.0| 0.95878977|
|  3|  B|-23.0|-0.95878977|
+---+---+-----+-----------+

然后transformedDF.show()的连续调用实际上会改变值!?

transformedDF.show()
+---+---+-----+-----------+
| x1| x2|   x3|  randFloat|
+---+---+-----+-----------+
|  1|  a| 23.0| 0.95878977|
|  3|  B|-23.0|-0.95878977|
+---+---+-----+-----------+


In [3]: transformedDF.show()
+---+---+-----+-----------+
| x1| x2|   x3|  randFloat|
+---+---+-----+-----------+
|  1|  a| 23.0|  2.9191132|
|  3|  B|-23.0|-0.95878977|
+---+---+-----+-----------+


In [4]: transformedDF.show()
+---+---+-----+-----------+
| x1| x2|   x3|  randFloat|
+---+---+-----+-----------+
|  1|  a| 23.0| 0.95878977|
|  3|  B|-23.0|-0.95878977|
+---+---+-----+-----------+


In [5]: transformedDF.show()
+---+---+-----+----------+
| x1| x2|   x3| randFloat|
+---+---+-----+----------+
|  1|  a| 23.0| 16.033003|
|  3|  B|-23.0|-2.9191132|
+---+---+-----+----------+

预计会出现这种情况吗? .show()实际上是否触发计算开始? AFAIK我应该使用单个节点,确定它们会在单个线程中运行,以便它们共享随机种子?我知道内置的pyspark rng存在,但它不适合我的目的,因为它实际上不会从python空间生成数据。

1 个答案:

答案 0 :(得分:2)

嗯,预期在这里相当相对,但它不是无法解释的东西。特别是RNG的状态是从父进程继承的。您可以通过在本地模式下运行以下简单代码段来轻松证明:

import random 

def roll_and_get_state(*args):
    random.random()
    return [random.getstate()]

states = sc.parallelize([], 10).mapPartitions(roll_and_get_state).collect()
len(set(states))
## 1

正如您所看到的,每个分区都使用自己的RNG,但都具有相同的状态。

一般来说,确保Spark中正确的Python RNG行为没有严重的性能损失,特别是如果您需要可重复的结果,则相当棘手。

一种可能的方法是使用加密安全随机数据(Random)生成种子,为每个分区实例化单独的os.urandom实例。

如果需要可重复的结果,可以根据全局状态和分区数据生成RNG种子。遗憾的是,这些信息在运行时无法通过Python轻松访问(忽略mapPartitionsWithIndex)等特殊情况。

由于分区级操作并不总是适用(如UDF),您可以通过使用单例模块或Borg模式为每个执行程序初始化RNG来实现类似的结果。

另见: