Scala中高效的最近邻搜索

8

让这个坐标类与欧几里得距离配合使用,

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)

是否有一种方法可以使这些计算更加高效,特别是对于一个拥有一百万点的网格的情况?


好问题。一种非常明显的方法是找到最小值,而不是对“val nearest=grid.minBy(p=>p.dist(x))”进行排序,然后从列表中删除该元素并重试。如果数字较小,则有效。 这还不值得回答。我怀疑某个地方进行位运算以加速处理。 - Jatin
3
http://en.wikipedia.org/wiki/Nearest_neighbor_search - David Eisenstat
1
将K-d树视为对二分搜索空间(网格)的预处理,参见http://en.wikipedia.org/wiki/K-d_tree。 - elm
1
一种简单的优化方法是使用距离平方: def distSquare(c: coord) = Math.pow(x-c.x, 2) + Math.pow(y-c.y, 2) 作为度量方式。这基本上可以避免每次计算 .sqrt - artur grzesiak
1
此外,Apache Spark有一个top方法可以实现您想要的功能。也许您可以重新利用该源代码?https://spark.apache.org/docs/0.8.1/api/core/org/apache/spark/rdd/RDD.html - The Archetypal Paul
显示剩余8条评论
5个回答

6
我不确定这是否有用(或者甚至是愚蠢的),但我想到了以下内容:
您可以使用排序功能对网格中的所有元素进行排序,然后选择前k个元素。如果考虑类似于递归合并排序的排序算法,您会得到以下步骤:
  1. 将集合分成两半
  2. 对两个部分进行递归处理
  3. 合并两个已排序的部分
也许您可以针对自己的需求优化此函数。合并部分通常会合并来自两个部分的所有元素,但您只关心合并后的前k个元素。因此,您只需要合并直到具有k个元素为止,并忽略其余元素。
因此,在最坏情况下,其中k >= nn是网格的大小)您仍然只有合并排序的复杂度。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)
}

4

剖析你的代码,查看哪些部分开销较高。

你的排序方式已经非常低效。

不要一直重新计算距离。这并不是免费的——很可能你的程序99%的时间都在计算距离(使用分析工具找到原因!)

最后,你可以使用索引结构。对于欧几里德距离,你可能有最大的选择来加速查找最近邻居。有k-d树,但我发现R树通常更快。如果你想试一试这些,我建议使用ELKI。它是一个用于数据挖掘的Java库(所以从Scala使用起来应该很容易),并且它有大量的索引结构可供选择。


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

实际上,“插入”操作可以进一步优化 - 如果(通常情况下)要插入的元素“e”大于列表中的任何元素,则只需要进行一次比较,但当前执行了“n”次比较。为此,请将当前前n个元素按相反顺序保留(从大到小)。我可能稍后会进行这种修改。 - The Archetypal Paul

1
这是一个利用R树数据结构的算法。对于描述的小数据集没有用处,但它能很好地扩展到大量对象。
使用有序列表,其节点表示对象或R树边界框。使用任何距离函数按最近距离进行排序。在插入时保持顺序。
通过将边界框插入R树的根节点来初始化列表。
要获取下一个最近的对象:
(1) 从列表中删除第一个元素。
(2) 如果它是对象,则它是最接近的对象。
(3) 如果它是R树非叶节点的边界框,则按照它们的距离将代表该节点子级的所有边界框插入列表中的正确位置。
(4) 如果它是R树叶节点的边界框,则根据它们的距离插入该节点的子对象(对象,而不是它们的边界框)。
(5) 返回步骤(1)。
列表将保持相当短。在前面将是我们感兴趣的附近对象,而列表中后面的节点将是代表远离的对象收集的框。

0

这取决于是使用exact还是approximation

正如http://www.slideshare.net/erikbern/approximate-nearest-neighbor-methods-and-vector-models-nyc-ml-meetup等多个基准测试所显示的那样,用efficient(高效)来衡量,近似算法是一个好的解决方案。

我编写了ann4s,这是Annoy的Scala实现。

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

看一下this repo


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接