TL; DR:我有一个大文件,我迭代三次以获得三组不同的计数。有没有办法在数据的一次传递中获得三张地图?
更多细节:
我试图计算大文件中列出的字词和功能之间的PMI。我的管道看起来像这样:
val wordFeatureCounts = sc.textFile(inputFile).flatMap(line => {
val word = getWordFromLine(line)
val features = getFeaturesFromLine(line)
for (feature <- features) yield ((word, feature), 1)
})
然后我重复这个以分别获取字数和特征计数:
val wordCounts = sc.textFile(inputFile).flatMap(line => {
val word = getWordFromLine(line)
val features = getFeaturesFromLine(line)
for (feature <- features) yield (word, 1)
})
val featureCounts = sc.textFile(inputFile).flatMap(line => {
val word = getWordFromLine(line)
val features = getFeaturesFromLine(line)
for (feature <- features) yield (feature, 1)
})
(我知道我可以迭代wordFeatureCounts
来获取wordCounts
和featureCounts
,但这并不能回答我的问题,并且在实践中查看运行时间我和#39;我不确定以这种方式实现它的速度实际上更快。还要注意,在计算出的计数未显示之后,还有一些reduceByKey操作以及我对此做的其他事情,因为它们与问题无关。)
我真正想做的是这样的事情:
val (wordFeatureCounts, wordCounts, featureCounts) = sc.textFile(inputFile).flatMap(line => {
val word = getWordFromLine(line)
val features = getFeaturesFromLine(line)
val wfCounts = for (feature <- features) yield ((word, feature), 1)
val wCounts = for (feature <- features) yield (word, 1)
val fCounts = for (feature <- features) yield (feature, 1)
??.setOutput1(wfCounts)
??.setOutput2(wCounts)
??.setOutput3(fCounts)
})
有没有办法用火花做到这一点?在查找如何执行此操作时,我已经在将结果保存到磁盘时看到了有关多个输出的问题(没有帮助),而且我已经看到了一些关于累加器的信息(其中包括#39) ;看起来像我需要的那样),但那就是它。
另请注意,我无法将所有这些结果放在一个大清单中,因为我需要三个单独的地图。如果有一种有效的方法可以在事后分割组合的RDD,那可能会有效,但我能想到的唯一方法是最终迭代数据四次,而不是我目前做的三次(一次创建组合地图,然后三次将其过滤到我真正想要的地图中)。
答案 0 :(得分:0)
无法将RDD拆分为多个RDD。如果你想一想这将如何在幕后工作,这是可以理解的。假设您将RDD x = sc.textFile("x")
拆分为a = x.filter(_.head == 'A')
和b = x.filter(_.head == 'B')
。到目前为止没有任何事情发生,因为RDD是懒惰的。但现在你打印a.count
。因此Spark打开文件,并遍历这些行。如果该行以A
开头,则会对其进行计数。但是我们如何处理以B
开头的行?将来是否会打电话给b.count
?或者它可能是b.saveAsTextFile("b")
我们应该把这些线写在某个地方?我们现在还不知道。使用Spark API无法拆分RDD。
但是如果你知道自己想要什么,没有什么能阻止你实施某些东西。如果您想同时获得a.count
和b.count
,则可以将以A
开头的行映射到(1, 0)
,将带有B
的行映射到(0, 1)
,然后在reduce中总结元组元素。如果您想在使用B
计算行数时将A
行保存到文件中,则可以在map
之前使用filter(_.head == 'B').saveAsTextFile
中的聚合器。
唯一的通用解决方案是将中间数据存储在某处。一种选择是只缓存输入(x.cache
)。另一种方法是在单次传递中将内容写入单独的目录,然后将它们作为单独的RDD读回。 (参见Write to multiple outputs by key Spark - one Spark job。)我们在制作中这样做,效果很好。
答案 1 :(得分:0)
与传统的map-reduce编程相比,这是Spark的主要缺点之一。可以将一个RDD / DF / DS转换为另一个RDD / DF / DS,但是您不能将一个RDD映射到多个输出中。为了避免重新计算,您需要将结果缓存到某个中间的RDD中,然后运行多个映射操作以生成多个输出。如果您要处理合理的大小数据,则缓存解决方案将起作用。但是,如果与可用内存相比,数据量很大,则中间输出将溢出到磁盘,并且缓存的优势不会那么大。在此处查看讨论-https://issues.apache.org/jira/browse/SPARK-1476。这是一个古老的吉拉,但很重要。查看Mridul Muralidharan的评论。
Spark需要提供一种解决方案,其中映射操作可以产生多个输出而无需缓存。从函数式编程的角度来看,这可能并不优雅,但我认为,这是实现更好性能的一个很好的折衷方案。
答案 2 :(得分:0)
我也很失望地看到这是 Spark 相对于经典 MapReduce 的一个硬限制。我最终通过使用多个连续的地图来解决这个问题,在这些地图中我过滤掉了我需要的数据。
这是一个示意性玩具示例,它对数字 0 到 49 执行不同的计算并将两者写入不同的输出文件。
from functools import partial
import os
from pyspark import SparkContext
# Generate mock data
def generate_data():
for i in range(50):
yield 'output_square', i * i
yield 'output_cube', i * i * i
# Map function to siphon data to a specific output
def save_partition_to_output(part_index, part, filter_key, output_dir):
# Initialise output file handle lazily to avoid creating empty output files
file = None
try:
for key, data in part:
if key != filter_key:
# Pass through non-matching rows and skip
yield key, data
continue
if file is None:
file = open(os.path.join(output_dir, '{}-part{:05d}.txt'.format(filter_key, part_index)), 'w')
# Consume data
file.write(str(data) + '\n')
yield from []
finally:
if file is not None:
file.close()
def main():
sc = SparkContext()
rdd = sc.parallelize(generate_data())
# Repartition to number of outputs
# (not strictly required, but reduces number of output files).
#
# To split partitions further, use repartition() instead or
# partition by another key (not the output name).
rdd = rdd.partitionBy(numPartitions=2)
# Map and filter to first output.
rdd = rdd.mapPartitionsWithIndex(partial(save_partition_to_output, filter_key='output_square', output_dir='.'))
# Map and filter to second output.
rdd = rdd.mapPartitionsWithIndex(partial(save_partition_to_output, filter_key='output_cube', output_dir='.'))
# Trigger execution.
rdd.count()
if __name__ == '__main__':
main()
这将创建两个输出文件 output_square-part00000.txt
和 output_cube-part00000.txt
,具有所需的输出拆分。