我正在尝试衡量性能对必须将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空间生成数据。
答案 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来实现类似的结果。
另见: