Parcourir la source

Copied NNSearch.py before cleanup to keep the experimental stuff.

Kristian Schultz il y a 4 ans
Parent
commit
300223af89
1 fichiers modifiés avec 520 ajouts et 0 suppressions
  1. 520 0
      library/NNSearch_experimental.py

+ 520 - 0
library/NNSearch_experimental.py

@@ -0,0 +1,520 @@
+import math
+
+import tensorflow as tf
+import numpy as np
+from sklearn.neighbors import NearestNeighbors
+from library.timing import timing
+
+
+def dist(x,y):
+    return sum(map(lambda z: (z[0] - z[1])*(z[0] - z[1]), zip(x, y)))
+
+def maxby(data, fn, startValue=0.0):
+    m = startValue
+    for v in data:
+        m = max(m, fn(v))
+    return m
+
+def distancesToPoint(p, points):
+    w = np.array(np.repeat([p], len(points), axis=0))
+    d = tf.keras.layers.Subtract()([w, np.array(points)])
+    t = tf.keras.layers.Dot(axes=(1,1))([d,d])
+    # As the concrete distance is not needed and sqrt(x) is strict monotone
+    # we avoid here unneccessary calculating of expensive roots.
+    return t.numpy()
+
+
+def calculateCenter(points):
+    if points.shape[0] == 1:
+        return points[0]
+    return tf.keras.layers.Average()(list(points)).numpy()
+
+
+class MaxHeap:
+    def __init__(self, maxSize=None, isGreaterThan=None, smalestValue=(-1,0.0)):
+        self.heap = []
+        self.size = 0
+        self.maxSize = maxSize
+        self.isGreaterThan = isGreaterThan if isGreaterThan is not None else (lambda a, b: a > b)
+        self.smalestValue = smalestValue
+        self.indices = set()
+        self.wasChanged = False
+        self.insert(smalestValue)
+
+    def insert(self, v):
+        if self.maxSize is not None and self.size >= self.maxSize:
+            return self.replaceMax(v)
+
+        if v[0] in self.indices:
+            return False
+
+        self.indices.add(v[0])
+        pos = self.size
+        self.size += 1
+        self.heap.append(v)
+        while pos > 0:
+            w = self.heap[pos // 2]
+            if not self.isGreaterThan(v, w):
+                break
+            self.heap[pos] = w
+            pos = pos // 2
+            self.heap[pos] = v
+        self.wasChanged = True
+        return True
+
+
+    def childPos(self, pos):
+        c = (pos + 1) * 2
+        return (c - 1, c)
+
+
+    def removeMax(self):
+        if self.heap == []:
+            self.size = 0
+            return False
+        
+        x = self.heap[0]
+        self.indices.remove(x[0])
+
+        self.heap[0] = self.heap[-1]
+        self.heap = self.heap[:-1]
+        self.size -= 1
+
+        x = self.heap[0]
+        pos = 0
+        size = self.size
+
+        while pos < size:
+            (left, right) = self.childPos(pos)
+
+            if left >= size:
+                break
+
+            y = self.heap[left]
+            if right >= size:
+                if self.isGreaterThan(y, x):
+                    self.heap[pos] = y
+                    self.heap[left] = x
+                break
+
+            z = self.heap[right]
+            (best, v) = (left, y) if self.isGreaterThan(y, z) else (right, z)
+
+            if not self.isGreaterThan(v, x):
+                break
+
+            self.heap[pos] = v
+            self.heap[best] = x
+            pos = best
+
+        self.wasChanged = True
+        return True
+
+
+    def replaceMax(self, x):
+        if self.heap == []:
+            self.heap = [x]
+            self.size = 1
+            self.indices.add(x[0])
+            self.wasChanged = True
+            return True
+        
+        if x[0] in self.indices:
+            return False
+
+        if self.isGreaterThan(x, self.heap[0]):
+            return False
+
+        self.indices.remove((self.heap[0])[0])
+        self.indices.add(x[0])
+        self.heap[0] = x
+        pos = 0
+        size = len(self.heap)
+
+        while pos < size:
+            (left, right) = self.childPos(pos)
+
+            if left >= size:
+                break
+
+            y = self.heap[left]
+            if right >= size:
+                if self.isGreaterThan(y, x):
+                    self.heap[pos] = y
+                    self.heap[left] = x
+                break
+
+            z = self.heap[right]
+            (best, v) = (left, y) if self.isGreaterThan(y, z) else (right, z)
+
+            if not self.isGreaterThan(v, x):
+                break
+
+            self.heap[pos] = v
+            self.heap[best] = x
+            pos = best
+
+        self.wasChanged = True
+        return True
+
+    def getMax(self):
+        if self.heap == []:
+            return self.smalestValue
+        return self.heap[0]
+
+
+    def setMaxSize(self, maxSize):
+        self.maxSize = maxSize
+        while self.size > maxSize:
+            self.removeMax()
+
+    def toArray(self, mapFn=None):
+            return list(self.indices)
+
+
+    def length(self):
+        return self.size
+
+
+
+
+class Ball:
+    def __init__(self, points=None, indices=None, parent=None, center=None):
+        if center is not None:
+            self.center = center
+        elif points is not None:
+            self.center = calculateCenter(np.array(points))
+        else:
+            raise ParameterError("Missing points or center")
+        self.radius = 0
+        self.points = []
+        self.indices = set()
+        self.childs = []
+        self.parent = parent
+
+        if points is not None and indices is not None:
+            for (i, r) in zip(indices, distancesToPoint(self.center, points)):
+                self.add(i, r)
+        elif points is not None:
+            raise ParameterError("Missing indices")
+
+    def findIt(self, r):
+        if r == self.points[0][1]:
+            return 0
+
+        upper = len(self.points) - 1
+        lower = 0
+        while upper > lower + 1:
+            h = (upper + lower) // 2
+            if self.points[h][1] >= r:
+                upper = h
+            else:
+                lower = h
+        return upper
+
+
+    def add(self, xi, r):
+        if xi in self.indices:
+            return False
+
+        # Here we know, that x is not in points.
+        newEntry = (xi, r)
+        self.indices.add(xi)
+
+        # Special case: empty list or new element will extend the radius:
+        # Here we can avoid the search
+        if self.points == [] or r >= self.radius:
+            self.points.append(newEntry)
+            self.radius = r
+            return True
+
+        # Special case: r <= min radius
+        # Here we can avoid the search
+        if self.points[0][1] >= r:
+            self.points = [newEntry] + self.points
+            return True
+
+        # Here we know that min radius < r < max radius.
+        # So len(points) >= 2.
+
+        pos = self.findIt(r)
+
+        # here shoul be: r(pos) >= r > r(pos - 1)
+        self.points = self.points[:pos] + [newEntry] + self.points[pos: ]
+
+        return True
+
+    def remove(self, xi, r):
+        if xi not in self.indices:
+            return False
+
+
+        # special case: remove the element with the heighest radius:
+        if self.points[-1][0] == xi:
+            self.indices.remove(xi)
+            self.points = self.points[: -1]
+            if self.points == []:
+                self.radius = 0.0
+            else:
+                self.radius = self.points[-1][1]
+            return True
+
+        pos = self.findIt(r)
+        nPoints = len(self.points)
+        # pos is the smalest position with:
+        # r(pos - 1) < r <= r(pos)
+
+        # Just in case: check that we remove the correct index.
+        while pos < nPoints and r == self.points[pos]:
+            if self.points[pos][0] == xi:
+                self.indices.remove(xi)
+                self.points = self.points[:pos] + self.points[(pos + 1): ]
+                return True
+            pos += 1
+
+        return False
+
+    def divideBall(self, X):
+        indices = [i for (i, _r) in self.points]
+        points = np.array([X[i] for i in indices])
+        
+        distances = tf.keras.layers.Dot(axes=(1,1))([points, points]).numpy()
+        ball = Ball(points, indices, center=np.zeros(len(points[0])))
+
+        h = len(ball) // 2
+        indicesA = [i for (i, _) in ball.points[:h]] 
+        indicesB = [i for (i, _) in ball.points[h:]] 
+        pointsA = np.array([X[i] for i in indicesA])
+        pointsB = np.array([X[i] for i in indicesB])
+
+        self.childs = []
+
+        print(f"{len(points)} -> <{len(pointsA)}|{len(pointsB)}>")
+        self.childs.append(Ball(pointsA, indicesA, self))
+        self.childs.append(Ball(pointsB, indicesB, self))
+
+        return self.childs
+
+    def __len__(self):
+        return len(self.points)
+
+    def smalestBallFor(self, i):
+        if i not in self.indices:
+            return None
+
+        for c in self.childs:
+            b = c.smalestBallFor(i)
+            if b is not None:
+                return b
+
+        return self
+
+
+
+class NNSearch:
+    def __init__(self, nebSize=5, timingDict=None):
+        self.nebSize = nebSize
+        self.neighbourhoods = []
+        self.timingDict = timingDict
+
+
+    def timerStart(self, name):
+        if self.timingDict is not None:
+            if name not in self.timingDict:
+                self.timingDict[name] = timing(name)
+
+            self.timingDict[name].start()
+
+    def timerStop(self, name):
+        if self.timingDict is not None:
+            if name in self.timingDict:
+                self.timingDict[name].stop()
+
+    def neighbourhoodOfItem(self, i):
+        return self.neighbourhoods[i]
+
+
+    def fit(self, X, nebSize=None):
+        self.timerStart("NN_fit_all")
+        self.fit_chained(X, nebSize)
+        #self.fit_bruteForce_np(X, nebSize)
+        self.timerStop("NN_fit_all")
+       
+    def fit_optimized(self, X, nebSize=None):
+        self.timerStart("NN_fit_all")
+        if nebSize == None:
+            nebSize = self.nebSize
+
+        nPoints = len(X)
+        nFeatures = len(X[0])
+
+        if nFeatures > 15 or nebSize >= (nPoints // 2):
+            print("Using brute force")
+            self.fit_bruteForce_np(X, nebSize)
+        else:
+            print("Using chained")
+            self.fit_chained(X, nebSize)
+        self.timerStop("NN_fit_all")
+
+
+    def fit_bruteForce(self, X, nebSize=None):
+        if nebSize == None:
+            nebSize = self.nebSize
+
+        self.timerStart("NN_fit_bf_init")
+        isGreaterThan = lambda x, y: x[1] > y[1]
+        self.neighbourhoods = [MaxHeap(nebSize, isGreaterThan, (i, 0.0)) for i in range(len(X))]
+        self.timerStop("NN_fit_bf_init")
+
+        self.timerStart("NN_fit_bf_loop")
+        for (i, x) in enumerate(X):
+            nbh = self.neighbourhoods[i]
+
+            for (j, y) in enumerate(X[i+1:]):
+                j += i + 1
+                self.timerStart("NN_fit_bf_dist")
+                d = dist(x,y)
+                self.timerStop("NN_fit_bf_dist")
+                self.timerStart("NN_fit_bf_insert")
+                nbh.insert((j,d))
+                self.neighbourhoods[j].insert((i,d))
+                self.timerStop("NN_fit_bf_insert")
+
+            self.timerStart("NN_fit_bf_toList")
+            self.neighbourhoods[i] = nbh.toArray(lambda v: v[0])
+            self.timerStop("NN_fit_bf_toList")
+        self.timerStop("NN_fit_bf_loop")
+
+
+    def fit_bruteForce_np(self, X, nebSize=None):
+        self.timerStart("NN_fit_bfnp_init")
+        numOfPoints = len(X)
+        nFeatures = len(X[0])
+
+        tX = tf.convert_to_tensor(X)
+
+        def distancesTo(x):
+            w = np.repeat([x], numOfPoints, axis=0)
+            d = tf.keras.layers.Subtract()([w,tX])
+            t = tf.keras.layers.Dot(axes=(1,1))([d,d])
+            return t.numpy()
+
+        if nebSize == None:
+            nebSize = self.nebSize
+
+        isGreaterThan = lambda x, y: x[1] > y[1]
+        self.neighbourhoods = [MaxHeap(nebSize, isGreaterThan, (i, 0.0)) for i in range(len(X))]
+        self.timerStop("NN_fit_bfnp_init")
+
+        self.timerStart("NN_fit_bfnp_loop")
+        for (i, x) in enumerate(X):
+            self.timerStart("NN_fit_bfnp_dist")
+            distances = distancesTo(x)
+            self.timerStop("NN_fit_bfnp_dist")
+            nbh = self.neighbourhoods[i]
+
+            for (j, y) in enumerate(X[i+1:]):
+                j += i + 1
+                d = distances[j]
+                self.timerStart("NN_fit_bfnp_insert")
+                nbh.insert((j,d))
+                self.neighbourhoods[j].insert((i,d))
+                self.timerStop("NN_fit_bfnp_insert")
+
+            self.timerStart("NN_fit_bfnp_toList")
+            self.neighbourhoods[i] = nbh.toArray(lambda v: v[0])
+            self.timerStop("NN_fit_bfnp_toList")
+        self.timerStop("NN_fit_bfnp_loop")
+
+
+    def fit_chained(self, X, nebSize=None):
+        self.timerStart("NN_fit_chained_init")
+        if nebSize == None:
+            nebSize = self.nebSize
+
+        nPoints = len(X)
+        nFeatures = len(X[0])
+
+        neigh = NearestNeighbors(n_neighbors=nebSize)
+        neigh.fit(X)
+        self.timerStop("NN_fit_chained_init")
+        self.timerStart("NN_fit_chained_toList")
+        self.neighbourhoods = [
+                (neigh.kneighbors([x], nebSize, return_distance=False))[0]
+                for (i, x) in enumerate(X)
+                ]
+        self.timerStop("NN_fit_chained_toList")
+
+
+    # ===============================================================
+    # Heuristic search
+    # ===============================================================
+    def fit_heuristic(self, X, nebSize=None):
+        if nebSize == None:
+            nebSize = self.nebSize
+
+        nPoints = len(X)
+
+
+        def walkUp(nbh, ball, x, i):
+            while ball.parent is not None:
+                print(f"{i}: up (r: {nbh.getMax()})")
+                oldBall = ball
+                ball = ball.parent
+                for c in ball.childs:
+                    if c != oldBall:
+                        walkDown(nbh, c, x)
+
+        def walkDown(nbh, ball, x):
+            if ball is None:
+                return
+
+            print(f"{i}: down (r: {nbh.getMax()})")
+
+            if dist(x, ball.center) - ball.radius < nbh.getMax()[1]:
+                if ball.childs == []:
+                    for (j, _) in ball.points:
+                        nbh.insert((j, dist(x, X[j])))
+                else:
+                    for c in ball.childs:
+                        walkDown(nbh, c, x)
+
+
+        def countBoles(b):
+            if b is None:
+                return 0
+
+            return 1 + sum(map(countBoles, b.childs))
+
+
+
+        root = Ball(X, range(len(X)))
+        queue = [root]
+        while queue != []:
+            ball = queue[0]
+            queue = queue[1:]
+            if len(ball) <= nebSize:
+                continue
+
+            queue = ball.divideBall(X) + queue
+
+
+        isGreaterThan = lambda x, y: x[1] > y[1]
+        self.neighbourhoods = [MaxHeap(nPoints, isGreaterThan, (i, 0.0)) for i in range(len(X))]
+
+        print("#B: " + str(countBoles(root)))
+
+        exit()
+        z = X[0]
+        for (i, x) in enumerate(X):
+            nbh = self.neighbourhoods[i]
+
+            b = root.smalestBallFor(i)
+            if b.parent is not None:
+                b = b.parent
+
+            for (j, _) in b.points:
+                d = dist(x, X[j])
+                nbh.insert((j, d))
+
+            walkUp(nbh, b, x, i)