71 votes

Millions de points 3D : Comment trouver les 10 d'entre eux les plus proches d'un point donné ?

Un point en 3D est défini par (x, y, z). La distance d entre deux points (X, Y, Z) et (x, y, z) est d = Sqrt[(X-x)^2 + (Y-y)^2 + (Z-z)^2]. Maintenant, il y a un million d'entrées dans un fichier, chaque entrée est un point dans l'espace, sans ordre spécifique. Étant donné un point (a, b, c), trouvez les 10 points les plus proches. Comment stockeriez-vous les points d'un million et comment récupéreriez-vous ces 10 points à partir de cette structure de données.

1 votes

Est-ce que tu crées et remplis la structure de données avant ou après qu'on te dise quel est le point (a,b,c) ? La réponse de David, par exemple, ne fonctionne pas si tu crées d'abord la structure de données, puis qu'un utilisateur tape le (a,b,c) et veut une réponse instantanément.

3 votes

Bon point (aucun jeu de mots intentionnel!) Bien sûr, si (a,b,c) n'est pas connu à l'avance, il s'agit davantage d'un problème d'optimisation de la liste de points existante pour la recherche par emplacement 3D, plutôt que de faire réellement la recherche.

6 votes

Il devrait vraiment être clarifié si le coût de préparation de la structure de données et du stockage des millions de points dans cette structure de données doit être pris en compte ou si seule la performance de récupération compte. Si ce coût n'a pas d'importance, alors peu importe le nombre de fois que vous récupérerez les points, le kd-tree l'emportera. Si ce coût est important, alors vous devriez également spécifier combien de fois vous prévoyez d'exécuter la recherche (pour un petit nombre de recherches, la force brute l'emportera, pour un plus grand nombre le kd l'emportera).

97voto

J.F. Sebastian Points 102961

Millions de points est un petit nombre. L'approche la plus simple qui fonctionne ici (code basé sur KDTree est plus lent (pour l'interrogation d'un seul point)).

Force Brute approche (temps ~1 seconde)

#!/usr/bin/env python
import numpy

NDIM = 3 # number of dimensions

# read points into array
a = numpy.fromfile('million_3D_points.txt', sep=' ')
a.shape = a.size / NDIM, NDIM

point = numpy.random.uniform(0, 100, NDIM) # choose random point
print 'point:', point
d = ((a-point)**2).sum(axis=1)  # compute distances
ndx = d.argsort() # indirect sort 

# print 10 nearest points to the chosen one
import pprint
pprint.pprint(zip(a[ndx[:10]], d[ndx[:10]]))

Exécuter:

$ time python nearest.py 
point: [ 69.06310224   2.23409409  50.41979143]
[(array([ 69.,   2.,  50.]), 0.23500677815852947),
 (array([ 69.,   2.,  51.]), 0.39542392750839772),
 (array([ 69.,   3.,  50.]), 0.76681859086988302),
 (array([ 69.,   3.,  50.]), 0.76681859086988302),
 (array([ 69.,   3.,  51.]), 0.9272357402197513),
 (array([ 70.,   2.,  50.]), 1.1088022980015722),
 (array([ 70.,   2.,  51.]), 1.2692194473514404),
 (array([ 70.,   2.,  51.]), 1.2692194473514404),
 (array([ 70.,   3.,  51.]), 1.801031260062794),
 (array([ 69.,   1.,  51.]), 1.8636121147970444)]

real    0m1.122s
user    0m1.010s
sys 0m0.120s

Voici le script qui génère des millions de points 3D:

#!/usr/bin/env python
import random
for _ in xrange(10**6):
    print ' '.join(str(random.randrange(100)) for _ in range(3))

Sortie:

$ head million_3D_points.txt

18 56 26
19 35 74
47 43 71
82 63 28
43 82 0
34 40 16
75 85 69
88 58 3
0 63 90
81 78 98

Vous pouvez utiliser ce code pour tester plus complexe des structures de données et algorithmes (par exemple, si elles réellement consommer moins de mémoire ou plus rapide, puis au-dessus de la méthode la plus simple). Il est intéressant de noter que pour l'instant c'est la seule réponse qui contient du code qui fonctionne.

Solution basée sur KDTree (temps ~1,4 secondes)

#!/usr/bin/env python
import numpy

NDIM = 3 # number of dimensions

# read points into array
a = numpy.fromfile('million_3D_points.txt', sep=' ')
a.shape = a.size / NDIM, NDIM

point =  [ 69.06310224,   2.23409409,  50.41979143] # use the same point as above
print 'point:', point


from scipy.spatial import KDTree

# find 10 nearest points
tree = KDTree(a, leafsize=a.shape[0]+1)
distances, ndx = tree.query([point], k=10)

# print 10 nearest points to the chosen one
print a[ndx]

Exécuter:

$ time python nearest_kdtree.py  

point: [69.063102240000006, 2.2340940900000001, 50.419791429999997]
[[[ 69.   2.  50.]
  [ 69.   2.  51.]
  [ 69.   3.  50.]
  [ 69.   3.  50.]
  [ 69.   3.  51.]
  [ 70.   2.  50.]
  [ 70.   2.  51.]
  [ 70.   2.  51.]
  [ 70.   3.  51.]
  [ 69.   1.  51.]]]

real    0m1.359s
user    0m1.280s
sys 0m0.080s

Partielle de tri en C++ (temps ~1.1 secondes)

// $ g++ nearest.cc && (time ./a.out < million_3D_points.txt )
#include <algorithm>
#include <iostream>
#include <vector>

#include <boost/lambda/lambda.hpp>  // _1
#include <boost/lambda/bind.hpp>    // bind()
#include <boost/tuple/tuple_io.hpp>

namespace {
  typedef double coord_t;
  typedef boost::tuple<coord_t,coord_t,coord_t> point_t;

  coord_t distance_sq(const point_t& a, const point_t& b) { // or boost::geometry::distance
    coord_t x = a.get<0>() - b.get<0>();
    coord_t y = a.get<1>() - b.get<1>();
    coord_t z = a.get<2>() - b.get<2>();
    return x*x + y*y + z*z;
  }
}

int main() {
  using namespace std;
  using namespace boost::lambda; // _1, _2, bind()

  // read array from stdin
  vector<point_t> points;
  cin.exceptions(ios::badbit); // throw exception on bad input
  while(cin) {
    coord_t x,y,z;
    cin >> x >> y >> z;    
    points.push_back(boost::make_tuple(x,y,z));
  }

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;  // 1.14s

  // find 10 nearest points using partial_sort() 
  // Complexity: O(N)*log(m) comparisons (O(N)*log(N) worst case for the implementation)
  const size_t m = 10;
  partial_sort(points.begin(), points.begin() + m, points.end(), 
               bind(less<coord_t>(), // compare by distance to the point
                    bind(distance_sq, _1, point), 
                    bind(distance_sq, _2, point)));
  for_each(points.begin(), points.begin() + m, cout << _1 << "\n"); // 1.16s
}

Exécuter:

g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )
point: (69.0631 2.23409 50.4198)
(69 2 50)
(69 2 51)
(69 3 50)
(69 3 50)
(69 3 51)
(70 2 50)
(70 2 51)
(70 2 51)
(70 3 51)
(69 1 51)

real    0m1.152s
user    0m1.140s
sys 0m0.010s

File d'attente de priorité en C++ (temps ~1.2 secondes)

#include <algorithm>           // make_heap
#include <functional>          // binary_function<>
#include <iostream>

#include <boost/range.hpp>     // boost::begin(), boost::end()
#include <boost/tr1/tuple.hpp> // get<>, tuple<>, cout <<

namespace {
  typedef double coord_t;
  typedef std::tr1::tuple<coord_t,coord_t,coord_t> point_t;

  // calculate distance (squared) between points `a` & `b`
  coord_t distance_sq(const point_t& a, const point_t& b) { 
    // boost::geometry::distance() squared
    using std::tr1::get;
    coord_t x = get<0>(a) - get<0>(b);
    coord_t y = get<1>(a) - get<1>(b);
    coord_t z = get<2>(a) - get<2>(b);
    return x*x + y*y + z*z;
  }

  // read from input stream `in` to the point `point_out`
  std::istream& getpoint(std::istream& in, point_t& point_out) {    
    using std::tr1::get;
    return (in >> get<0>(point_out) >> get<1>(point_out) >> get<2>(point_out));
  }

  // Adaptable binary predicate that defines whether the first
  // argument is nearer than the second one to given reference point
  template<class T>
  class less_distance : public std::binary_function<T, T, bool> {
    const T& point;
  public:
    less_distance(const T& reference_point) : point(reference_point) {}

    bool operator () (const T& a, const T& b) const {
      return distance_sq(a, point) < distance_sq(b, point);
    } 
  };
}

int main() {
  using namespace std;

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;

  const size_t nneighbours = 10; // number of nearest neighbours to find
  point_t points[nneighbours+1];

  // populate `points`
  for (size_t i = 0; getpoint(cin, points[i]) && i < nneighbours; ++i)
    ;

  less_distance<point_t> less_distance_point(point);
  make_heap  (boost::begin(points), boost::end(points), less_distance_point);

  // Complexity: O(N*log(m))
  while(getpoint(cin, points[nneighbours])) {
    // add points[-1] to the heap; O(log(m))
    push_heap(boost::begin(points), boost::end(points), less_distance_point); 
    // remove (move to last position) the most distant from the
    // `point` point; O(log(m))
    pop_heap (boost::begin(points), boost::end(points), less_distance_point);
  }

  // print results
  push_heap  (boost::begin(points), boost::end(points), less_distance_point);
  //   O(m*log(m))
  sort_heap  (boost::begin(points), boost::end(points), less_distance_point);
  for (size_t i = 0; i < nneighbours; ++i) {
    cout << points[i] << ' ' << distance_sq(points[i], point) << '\n';  
  }
}

Exécuter:

$ g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )

point: (69.0631 2.23409 50.4198)
(69 2 50) 0.235007
(69 2 51) 0.395424
(69 3 50) 0.766819
(69 3 50) 0.766819
(69 3 51) 0.927236
(70 2 50) 1.1088
(70 2 51) 1.26922
(70 2 51) 1.26922
(70 3 51) 1.80103
(69 1 51) 1.86361

real    0m1.174s
user    0m1.180s
sys 0m0.000s

La Recherche linéaire approche fondée sur les temps ~1.15 secondes)

// $ g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )
#include <algorithm>           // sort
#include <functional>          // binary_function<>
#include <iostream>

#include <boost/foreach.hpp>
#include <boost/range.hpp>     // begin(), end()
#include <boost/tr1/tuple.hpp> // get<>, tuple<>, cout <<

#define foreach BOOST_FOREACH

namespace {
  typedef double coord_t;
  typedef std::tr1::tuple<coord_t,coord_t,coord_t> point_t;

  // calculate distance (squared) between points `a` & `b`
  coord_t distance_sq(const point_t& a, const point_t& b);

  // read from input stream `in` to the point `point_out`
  std::istream& getpoint(std::istream& in, point_t& point_out);    

  // Adaptable binary predicate that defines whether the first
  // argument is nearer than the second one to given reference point
  class less_distance : public std::binary_function<point_t, point_t, bool> {
    const point_t& point;
  public:
    explicit less_distance(const point_t& reference_point) 
        : point(reference_point) {}
    bool operator () (const point_t& a, const point_t& b) const {
      return distance_sq(a, point) < distance_sq(b, point);
    } 
  };
}

int main() {
  using namespace std;

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;
  less_distance nearer(point);

  const size_t nneighbours = 10; // number of nearest neighbours to find
  point_t points[nneighbours];

  // populate `points`
  foreach (point_t& p, points)
    if (! getpoint(cin, p))
      break;

  // Complexity: O(N*m)
  point_t current_point;
  while(cin) {
    getpoint(cin, current_point); //NOTE: `cin` fails after the last
                                  //point, so one can't lift it up to
                                  //the while condition

    // move to the last position the most distant from the
    // `point` point; O(m)
    foreach (point_t& p, points)
      if (nearer(current_point, p)) 
        // found point that is nearer to the `point` 

        //NOTE: could use insert (on sorted sequence) & break instead
        //of swap but in that case it might be better to use
        //heap-based algorithm altogether
        std::swap(current_point, p);
  }

  // print results;  O(m*log(m))
  sort(boost::begin(points), boost::end(points), nearer);
  foreach (point_t p, points)
    cout << p << ' ' << distance_sq(p, point) << '\n';  
}

namespace {
  coord_t distance_sq(const point_t& a, const point_t& b) { 
    // boost::geometry::distance() squared
    using std::tr1::get;
    coord_t x = get<0>(a) - get<0>(b);
    coord_t y = get<1>(a) - get<1>(b);
    coord_t z = get<2>(a) - get<2>(b);
    return x*x + y*y + z*z;
  }

  std::istream& getpoint(std::istream& in, point_t& point_out) {    
    using std::tr1::get;
    return (in >> get<0>(point_out) >> get<1>(point_out) >> get<2>(point_out));
  }
}

Les mesures montrent que la plupart du temps est consacré à la lecture de tableau à partir du fichier, réel les calculs de prendre sur l'ordre de grandeur de moins de temps.

6 votes

Nice write up. Pour compenser la lecture de fichiers, j'ai exécuté vos implémentations en python avec une boucle autour de la recherche qui s'exécute 100 fois (chaque fois en regardant autour d'un point différent et en construisant l'arbre kd seulement une fois). Le bruteforce a quand même gagné. Ça m'a fait gratter la tête. Mais ensuite j'ai examiné votre leafsize et vous avez une erreur là-bas - vous avez défini le leafsize à 1000001, et cela ne fonctionnera pas bien. Après avoir défini le leafsize à 10, kd a gagné (35 secondes à 70 secondes pour 100 points, la plupart des 35 secondes passées à construire l'arbre et 100 récupérations de 10 points prenant une seconde).

4 votes

Donc en conclusion, si vous pouvez précalculer l'arbre kd, il l'emportera sur la force brute de manière significative (sans parler du fait que pour les ensembles de données vraiment volumineux, si vous avez un arbre vous n'aurez pas à lire toutes les données en mémoire).

1 votes

@goran : si je définis la taille de la feuille à 10, alors cela prend environ 10 secondes (au lieu de 1 seconde) pour interroger un point. Je suis d'accord si la tâche est d'interroger plusieurs points (>10), alors l'arbre kd devrait l'emporter.

20voto

Will Points 2848

Si les millions d'entrées sont déjà dans un fichier, il n'est pas nécessaire de les charger tous dans une structure de données en mémoire. Il suffit de conserver un tableau avec les dix premiers points trouvés jusqu'à présent, et de parcourir les millions de points, en mettant à jour votre liste des dix premiers au fur et à mesure.

Ceci est en O(n) par rapport au nombre de points.

1 votes

Cela fonctionnera bien, mais le tableau n'est pas le moyen de stockage de données le plus efficace, car vous devez le vérifier à chaque étape, ou le garder trié, ce qui peut être fastidieux. La réponse de David à propos d'un min-heap fait ce travail pour vous, mais sinon c'est la même solution. Quand l'utilisateur ne veut que 10 points, ces préoccupations sont négligeables, mais quand l'utilisateur veut soudainement les 100 points les plus proches, vous serez en difficulté.

0 votes

@ Karl: La question spécifie 10 points. Je pense que l'inclusion de ce détail est délibérée de la part de l'auteur de la question. Ainsi, Will décrit une très bonne solution pour ce qui a été demandé.

0 votes

@Karl, il est souvent surprenant de voir à quel point le compilateur et le processeur sont bons pour optimiser les boucles les plus simples pour battre les algorithmes les plus ingénieux. Ne sous-estimez jamais l'accélération à obtenir lorsque une boucle peut s'exécuter dans la mémoire cache.

14voto

mipadi Points 135410

Vous pourriez stocker les points dans un arbre k-dimensionnel (arbre kd). Les k-arbres sont optimisés pour les recherches de plus proche voisin (trouver les n points les plus proches d'un point donné).

1 votes

Je pense qu'un octree est nécessaire ici.

11 votes

La complexité requise pour construire un arbre K-d sera plus élevée que la complexité requise pour effectuer une recherche linéaire des 10 points les plus proches. Le véritable avantage d'un arbre k-d se manifeste lorsque vous allez effectuer de nombreuses requêtes sur un ensemble de points.

0 votes

@gabe: Un grand avantage d'un arbre kd est que même s'il nécessitera une certaine surcharge de mémoire pour être construit, une fois que vous l'avez, un arbre kd équilibré à gauche ne prend pas plus de mémoire que la liste non structurée initiale de points. Un octree aura certainement besoin d'au moins une certaine surcharge de mémoire.

10voto

Krystian Points 1102

Je pense que c'est une question délicate qui teste si vous ne cherchez pas à en faire trop.

Considérez l'algorithme le plus simple que les gens ont déjà donné ci-dessus : gardez une table des dix meilleurs candidats jusqu'à présent et passez en revue tous les points un par un. Si vous trouvez un point plus proche que l'un des dix meilleurs jusqu'à présent, remplacez-le. Quelle est la complexité? Eh bien, nous devons regarder chaque point du fichier une seule fois, calculer sa distance (ou en réalité le carré de la distance) et le comparer avec le 10ème point le plus proche. S'il est meilleur, insérez-le à la place appropriée dans le tableau des dix meilleurs candidats jusqu'à présent.

Alors quelle est la complexité? Nous regardons chaque point une fois, donc c'est n calculs de distance et n comparaisons. Si le point est meilleur, nous devons l'insérer à la bonne position, ce qui nécessite encore quelques comparaisons, mais c'est un facteur constant puisque le tableau des meilleurs candidats est de taille constante 10.

Nous obtenons ainsi un algorithme qui s'exécute en temps linéaire, O(n) en fonction du nombre de points.

Mais maintenant, considérons quelle est la borne inférieure d'un tel algorithme? S'il n'y a pas d'ordre dans les données d'entrée, nous devons regarder chaque point pour voir s'il ne fait pas partie des plus proches. Autant que je puisse voir, la borne inférieure est Omega(n) et donc l'algorithme ci-dessus est optimal.

1 votes

Excellent point! Since you have to read the file one by one in order to build any data structure, your lowest possible is O(n) just as you say. Only if the question asks about finding the closest 10 points repeatedly does anything else matter! And you explained it well I think.

4voto

David Z Points 49476

Ce n'est pas une question de devoir, n'est-ce pas? ;-)

Mon idée : itérer sur tous les points et les mettre dans un tas min ou une file de priorité bornée, indexée par la distance à partir de la cible.

1 votes

Bien sûr, mais on ne sait pas quel est la cible. :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X