Scala中的高效最近邻搜索

时间:2014-09-06 05:09:54

标签: algorithm scala nearest-neighbor kdtree r-tree

让这个坐标与欧几里德距离,

case class coord(x: Double, y: Double) {
  def dist(c: coord) = Math.sqrt( Math.pow(x-c.x, 2) + Math.pow(y-c.y, 2) ) 
}

并设置一个坐标网格,例如

val grid = (1 to 25).map {_ => coord(Math.random*5, Math.random*5) }

然后是任何给定的坐标

val x = coord(Math.random*5, Math.random*5) 

最近的x点是

val nearest = grid.sortWith( (p,q) => p.dist(x) < q.dist(x) )

所以最接近的三个是nearest.take(3)

有没有办法让这些计算更节省时间,特别是对于有一百万点的网格?

5 个答案:

答案 0 :(得分:4)

我不确定这是否有用(甚至是愚蠢的),但我想到了这个:

使用排序函数对网格中的所有元素进行排序,然后选择第一个k元素。如果考虑像递归合并排序这样的排序算法,你可以这样:

  1. 将收藏分成两半
  2. 两半递归
  3. 合并两个分类的一半
  4. 也许你可以根据自己的需要优化这样的功能。合并部分通常合并来自两半的所有元素,但您只对合并产生的第一个k感兴趣。所以你只能合并,直到你有k元素并忽略其余元素。

    所以在最坏的情况下,k >= nn是网格的大小),你仍然只有merge-sort的复杂性。 O(n log n)说实话,我无法确定此解决方案相对于k的复杂程度。 (此刻太累了)

    以下是该解决方案的示例实现(它绝对不是最佳的而不是一般化的):

    def minK(seq: IndexedSeq[coord], x: coord, k: Int) = {
    
      val dist = (c: coord) => c.dist(x)
    
      def sort(seq: IndexedSeq[coord]): IndexedSeq[coord] = seq.size match {
        case 0 | 1 => seq
        case size => {
          val (left, right) = seq.splitAt(size / 2)
          merge(sort(left), sort(right))
        }
      }
    
      def merge(left: IndexedSeq[coord], right: IndexedSeq[coord]) = {
    
        val leftF = left.lift
        val rightF = right.lift
    
        val builder = IndexedSeq.newBuilder[coord]
    
        @tailrec
        def loop(leftIndex: Int = 0, rightIndex: Int = 0): Unit = {
          if (leftIndex + rightIndex < k) {
            (leftF(leftIndex), rightF(rightIndex)) match {
              case (Some(leftCoord), Some(rightCoord)) => {
                if (dist(leftCoord) < dist(rightCoord)) {
                  builder += leftCoord
                  loop(leftIndex + 1, rightIndex)
                } else {
                  builder += rightCoord
                  loop(leftIndex, rightIndex + 1)
                }
              }
              case (Some(leftCoord), None) => {
                builder += leftCoord
                loop(leftIndex + 1, rightIndex)
              }
              case (None, Some(rightCoord)) => {
                builder += rightCoord
                loop(leftIndex, rightIndex + 1)
              }
              case _ =>
            }
          }
        }
    
        loop()
    
        builder.result
      }
    
      sort(seq)
    }
    

答案 1 :(得分:3)

个人资料您的代码,看看成本高昂。

排序的方式效率非常低。

不要一直重新计算距离。这不是免费的 - 很可能你的程序99%的时间花在计算距离上(使用分析器来查找!)

最后,您可以使用索引结构。对于欧几里德距离,您可能是加速寻找最近邻居的最大索引选择。有k-d-tree,但我发现R-tree通常更快。如果你想玩这些,我建议 ELKI 。它是用于数据挖掘的 Java库(因此它也很容易从Scala中使用),并且它有很多选择的索引结构。

答案 2 :(得分:2)

这个很有趣。

case class Coord(x: Double, y: Double) {
    def dist(c: Coord) = Math.sqrt(Math.pow(x - c.x, 2) + Math.pow(y - c.y, 2))
}
class CoordOrdering(x: Coord) extends Ordering[Coord] {
    def compare(a: Coord, b: Coord) = a.dist(x) compare b.dist(x)
}

def top[T](xs: Seq[T], n: Int)(implicit ord: Ordering[T]): Seq[T] = {
    // xs is an ordered sequence of n elements. insert returns xs with e inserted 
    // if it is less than anything currently in the sequence (and in that case, 
    // the last element is dropped) otherwise returns an unmodifed sequence

    def insert[T](xs: Seq[T], e: T)(implicit ord: Ordering[T]): Seq[T] = {
      val (l, r) = xs.span(x => ord.lt(x, e))
      (l ++ (e +: r)).take(n)
    }
    xs.drop(n).foldLeft(xs.take(n).sorted)(insert)
} 

经过最低限度的测试。这样称呼:

val grid = (1 to 250000).map { _ => Coord(Math.random * 5, Math.random * 5) }
val x = Coord(Math.random * 5, Math.random * 5)
top(grid, 3)(new CoordOrdering(x)) 

编辑:很容易将其扩展到(预)计算距离一次

val zippedGrid = grid map {_.dist(x)} zip grid  

object ZippedCoordOrdering extends Ordering[(Double, Coord)] {
   def compare(a:(Double, Coord), b:(Double, Coord)) = a._1 compare b._1
}

top(zippedGrid,3)(ZippedCoordOrdering).unzip._2

答案 3 :(得分:1)

这是一种利用R树数据结构的算法。对于所描述的小数据集没有用,但它可以很好地扩展到大量对象。

使用有序列表,其节点表示对象或R树边界框。订单最接近使用您想要的任何距离函数。保持插入订单。

通过在R树的根节点中插入边界框来初始化列表。

获取下一个最近的对象:

(1)从列表中删除第一个元素。

(2)如果它是一个物体,它是最接近的物体。

(3)如果它是R树的非叶节点的边界框,则根据距离将所有表示该节点子节点的边界框插入到适当位置的列表中。

(4)如果它是R树叶节点的边界框,则根据它们的距离插入作为该节点的子节点的对象(对象,而不是它们的边界框)。

(5)回到步骤(1)。

该清单仍然很短。在前面将是我们感兴趣的附近对象,列表中的后续节点将是表示更远的对象集合的框。

答案 4 :(得分:0)

取决于exact还是approximation

正如http://www.slideshare.net/erikbern/approximate-nearest-neighbor-methods-and-vector-models-nyc-ml-meetup这样的几个基准测试表明,就efficient而言,近似是一个很好的解决方案。

我写了ann4s这是Annoy

的Scala实现
  

Annoy(近似最近邻居哦,是的)是一个带有Python绑定的C ++库,用于搜索空间中接近给定查询点的点。它还创建了大型只读的基于文件的数据结构,这些数据被映射到内存中,以便许多进程可以共享相同的数据。

看看this repo