【问题标题】:Scala - Example of Fold Operation on a HashMap ** not foldLeftScala - HashMap 上的折叠操作示例 ** 不是 foldLeft
【发布时间】:2014-10-08 21:50:32
【问题描述】:

我对使用 Scala 很感兴趣,因为它似乎是一种并行化操作的好方法。我需要设计一种使用向量乘法的机器学习算法(就像许多人一样)。我知道如何做算法,但我想做的是来自 HashMaps 的稀疏向量实现。几乎所有向量都存储为 HashMaps[Int, Double],其中向量中给定双精度的索引是作为键的整数。

使用 Pythonish 伪代码, ==> {1:7, 2:6, 3:5, 4:4}

我想使用 fold、reduce、map ... 等定义点积函数,但我不想使用 foldLeft、reduceLeft ... 因为我希望这可能是可并行化的,因为我的向量可以得到多达 6000 多个维度,对于点积,顺序无关紧要。

我已经阅读了很多 foldLeft 和 reduceLeft 的示例,但我还没有弄清楚如何使用 HashMap.fold 或 HashMap.reduce。

我在一定程度上理解函数式编程,但我不理解 Scala 中的错误消息。这是我或多或少想要的模板。

object NGramAnalysis {
  def main(args: Array[String]) {
    val mapped = HashMap(1->1.2, 5->2.4)
    println(mapped.fold( .... What goes here ... )
  }
}

结论 我想要一个使用 HashMap.fold NOT foldLeft 和 HashMap.reduce

的可靠示例

提前谢谢你。我已经挣扎了一段时间了。

【问题讨论】:

    标签: scala hashmap reduce fold


    【解决方案1】:

    首先,foldreduce 的区别在于fold 需要一个额外的参数作为初始值,而reduce 将集合中的第一个元素作为初始值,并且如果集合为空,则会引发异常。所以,foldreduce 更通用一些,所以从现在开始我将这两个函数都称为fold

    要让fold 正常工作,您的集合中的元素必须形成半群,也就是说,应该有一个二元运算也必须是关联,即,应保持以下身份:(a `op` b) `op` c == a `op` (b `op` c)。需要关联性是因为fold 没有指定操作应用程序顺序,这在并行上下文中尤为重要。此操作用于执行折叠:

    a1 `op` a2 `op` a3 `op` ... `op` an
    

    如果reduce并行运行,它可以拆分集合并在一个线程中减少前半部分,在另一个线程中减少后半部分;然后使用相同的操作将它们的结果连接起来。这只有在操作是关联的时才能正常工作。

    正如我已经说过的,fold 方法有两个参数:初始值和 [associative] 二元运算符。例如,要并行连接字符串列表,您可以这样做:

    val strings = Seq("a", "b", "c", "d", ...)
    strings.par.fold("")(_ ++ _)  // or strings.par.reduce(_ ++ _) if you know that strings is non-empty
    

    因此,要实现点积,您需要考虑将要折叠/归约的集合以及执行此归约的二元运算符。

    这是两个集合的点积的简单实现:

    (c1 zip c2).par.map {
      case (e1, e2) => e1 * e2
    }.reduce(_ + _)
    

    也就是说,我们将这些集合压缩在一起,使用* 运算符将它们的元素成对相乘,然后使用+ 运算符对结果进行归约。当然,*+ 必须在 c1c2 的元素上定义。

    但是,HashMap 没有排序,因此它的迭代顺序是未定义的。不能保证zip 将连接具有相同键的元素,这使得上述点积的想法不正确。你需要做这样的事情:

    c1.par.map {
      case (k, v) => v * c2(k)
    }.reduce(_ + _)
    

    这里我们不是压缩集合,而是使用第一个映射中的所有键在第二个映射中执行查找。

    【讨论】:

    • 感谢您如此迅速地回复。如果有人也想弄清楚这些,这里还有一些代码。
    • val mm = HashMap(1 -> 1.2, 2 -> 3.3, 5 -> 4.0); mm.withDefaultValue(0.0); val v = mm.map(i => (i._1, mm(i._1) * 5)); val vv = v.fold((10, 10.0))((a, b) => (a._1 + b._1, a._2 + b._2)); println(v); println(vv);
    【解决方案2】:

    我只想添加一个简单的示例实现,因为@Vladimir Matveev 涵盖了背景。

    这里的向量支持HashMap。 apply 工厂方法确保所有未指定的索引都有默认值0

    这个想法很简单。我们合并键集,以便我们在任一映射中指定所有键。然后我们将对应的值相乘并相加。由于我们为不存在的键设置了默认值,因此可以正常工作。

    class SparseVector private (val entries: Map[Int, Double]) {
    
      def **(vec: SparseVector) = 
        (entries.keySet ++ vec.entries.keySet).par
          .map(index => entries(index) * vec.entries(index)).sum
    
      //alternative suggested by @wingedsubmariner
      def **(vec: SparseVector) = 
        (entries.keySet ++ vec.entries.keySet).par
         .aggregate(0.0)((sum, index) => sum + entries(index) * vec.entries(index), (_ + _))
    }
    
    object SparseVector {
      def apply(entries: HashMap[Int, Double]) =
        new SparseVector(entries.withDefaultValue(0.0))
    }
    

    mapsumaggregate 方法都有并行实现。

    【讨论】:

    • .aggregate(0)((sum, index) => sum + entries(index) * vec.entries(index))(_ + _) 可能会更高效一些,因为它可以结合mapsum 操作。
    • 有趣。我从来没有使用过那个功能。它也让我感到困惑,因为它是在TraverseableOnce 中实现的,如下所示:def aggregate[B](z: =>B)(seqop: (B, A) => B, combop: (B, B) => B): B = foldLeft(z)(seqop)。甚至没有使用参数combop,它只是对foldLeft 的调用。我猜它是为并行处理而设计的。
    • 是的,aggregate() 是正确的。当我写我的答案时,我正在考虑最近的 Java 8 可变缩减,我记得 Scala 中有 Java Stream.reduce() 方法的确切类似物,但我忘记了它的确切名称。 aggregate() 实际上是;如果需要并行化,它确实应该用于此任务。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-10
    • 2011-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-10
    相关资源
    最近更新 更多