Эффективный поиск ближайшего соседа в Scala

Пусть это класс координат с евклидовым расстоянием,

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).

Есть ли способ сделать эти вычисления более эффективными, особенно для случая сетки с миллионом точек?


person elm    schedule 06.09.2014    source источник
comment
Хороший вопрос. Очень очевидный способ - найти минимум вместо сортировки val nearest = grid.minBy( p => p.dist(x) ), а затем удалить этот элемент для списка и повторить попытку. Работает, если небольшое число 3. Это не заслуживает ответа. Подозреваю побитовую операцию где-то для ускорения   -  person Jatin    schedule 06.09.2014
comment
en.wikipedia.org/wiki/Nearest_neighbor_search   -  person David Eisenstat    schedule 06.09.2014
comment
думал о деревьях Kd как о предварительной обработке для разделения пространства поиска (сетки) пополам, en.wikipedia.org/wiki/ K-d_tree   -  person elm    schedule 06.09.2014
comment
@DavidEisenstat, ТАК нужна отметка как дубликат статьи в Википедии :)   -  person The Archetypal Paul    schedule 06.09.2014
comment
@enzyme, деревья Kd упоминаются в статье, на которую ссылается Дэвид, наряду с множеством других предложений. Это хорошо изученная проблема   -  person The Archetypal Paul    schedule 06.09.2014
comment
Большое спасибо всем за хорошие идеи; это хорошо изученная проблема, но простое и эффективное решение на Scala для той самой проблемы, описанной выше, очень ценится.   -  person elm    schedule 06.09.2014
comment
Одна простая оптимизация — вы можете использовать квадрат расстояния: def distSquare(c: coord) = Math.pow(x-c.x, 2) + Math.pow(y-c.y, 2) в качестве меры. (что в основном избавляет вас от вычисления .sqrt каждый раз)   -  person artur grzesiak    schedule 06.09.2014
comment
@arturgrzesiak отличное наблюдение   -  person elm    schedule 06.09.2014
comment
@enzyme, это выглядит актуально: stackoverflow.com/questions/5674741/   -  person The Archetypal Paul    schedule 06.09.2014
comment
Кроме того, в Apache Spark есть метод top, который делает то, что вы хотите. Может быть, вы можете переназначить источник для этого? spark.apache.org/ документы/0.8.1/api/core/org/apache/spark/rdd/   -  person The Archetypal Paul    schedule 06.09.2014
comment
И сколько ближайших соседей вам понадобится? Различные алгоритмы будут применяться, если несколько (3 в вашем примере) или большинство из них (миллион)   -  person The Archetypal Paul    schedule 06.09.2014
comment
@Paul огромное спасибо! проверка искры с особым интересом :)   -  person elm    schedule 06.09.2014
comment
Если вам действительно понадобятся только несколько из миллиона координат, то вместо того, чтобы сортировать их, вам было бы гораздо лучше поместить их все в PriorityQueue и взять несколько первых. См. № 13 на этой странице.   -  person AmigoNico    schedule 08.09.2014


Ответы (5)


Я не уверен, что это полезно (или даже глупо), но я подумал об этом:

Вы используете функцию сортировки для сортировки ВСЕХ элементов в сетке, а затем выбираете первые k элементы. Если вы рассматриваете алгоритм сортировки, такой как рекурсивная сортировка слиянием, у вас будет что-то вроде этого:

  1. Разделить коллекцию пополам
  2. Рекурсия на обеих половинах
  3. Объединить обе отсортированные половины

Возможно, вы могли бы оптимизировать такую ​​функцию для своих нужд. Слияние обычно объединяет все элементы из обеих половин, но вас интересуют только первые k, полученные в результате слияния. Таким образом, вы можете объединиться только до тех пор, пока у вас не будет k элементов, и игнорировать остальные.

Таким образом, в худшем случае, когда k >= n (n - размер сетки), у вас все равно будет сложность только сортировки слиянием. 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)
}
person Kigyo    schedule 06.09.2014
comment
О да. Выбор путем сортировки. Хорошо знать. - person Kigyo; 06.09.2014

Профилируйте свой код, чтобы увидеть, что является дорогостоящим.

Ваш способ сортировки уже крайне неэффективен.

Не пересчитывайте расстояния все время. Это не бесплатно — скорее всего, ваша программа тратит 99% времени на вычисление расстояний (используйте профилировщик, чтобы узнать!)

Наконец, вы можете использовать структуры индексов. Для евклидова расстояния у вас, вероятно, самый большой выбор индексов для ускорения поиска ближайших соседей. Существует k-d-дерево, но я обнаружил, что R-дерево часто работает быстрее. Если вы хотите поиграть с ними, я рекомендую ELKI. Это Java-библиотека для интеллектуального анализа данных (поэтому ее должно быть легко использовать и из Scala), и она имеет огромный выбор индексных структур.

person Has QUIT--Anony-Mousse    schedule 06.09.2014
comment
Хорошая точка зрения. В мой ответ добавлен вариант, который не пересчитывает расстояния - person The Archetypal Paul; 06.09.2014
comment
+1 изучение ЭЛКИ; пересчитанные расстояния действительно неэффективны :) - person elm; 08.09.2014

Это было довольно весело делать.

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
person The Archetypal Paul    schedule 06.09.2014
comment
На самом деле, insert можно сделать еще более эффективным - если (как, вероятно, обычно бывает) e больше, чем что-либо в списке, требуется только одно сравнение, но в настоящее время оно делает n. Для этого оставьте текущие верхние n в обратном порядке (сначала наибольшие). Я могу сделать эту модификацию позже. - person The Archetypal Paul; 06.09.2014
comment
+1 Большое спасибо, ответ, богатый идеями и подходами к кодированию, спасибо! - person elm; 08.09.2014

Вот алгоритм, использующий структуру данных R-дерева. Бесполезно для описанного небольшого набора данных, но хорошо масштабируется для большого количества объектов.

Используйте упорядоченный список, узлы которого представляют либо объекты, либо ограничивающие рамки R-дерева. Порядок является ближайшим первым, используя любую функцию расстояния, которую вы хотите. Поддерживайте порядок при вставке.

Инициализируйте список, вставив ограничивающие рамки в корневой узел R-дерева.

Чтобы получить следующий ближайший объект:

(1) Удалить первый элемент из списка.

(2) Если это объект, то ближайший.

(3) Если это ограничивающая рамка нелистового узла R-дерева, вставьте все ограничивающие рамки, представляющие дочерние элементы этого узла, в список на их надлежащих местах в соответствии с их расстоянием.

(4) Если это ограничивающая рамка листового узла R-дерева, вставьте объекты, которые являются дочерними элементами этого узла (объекты, а не их ограничивающие рамки) в соответствии с их расстоянием.

(5) Вернитесь к шагу (1).

Список останется довольно коротким. Впереди будут близлежащие объекты, которые нас интересуют, а более поздние узлы в списке будут прямоугольниками, представляющими наборы объектов, которые находятся дальше.

person Antonin    schedule 12.09.2014

Это зависит от того, exact или approximation.

Несколько контрольных показателей, таких как http://www.slideshare.net/erikbern/approimate-nearest-neighbor-methods-and-vector-models-nyc-ml-meetup показывают, что аппроксимация является хорошим решением с точки зрения efficient.

Я написал ann4s, который представляет собой реализацию Annoy на Scala.

Annoy (Approximate Nearest Neighbours Oh Yeah) — это библиотека C++ с привязками Python для поиска точек в пространстве, близких к заданной точке запроса. Он также создает большие файловые структуры данных только для чтения, которые отображаются в памяти, чтобы многие процессы могли совместно использовать одни и те же данные.

Взгляните на этот репозиторий.

person emeth    schedule 09.09.2016