Fixes issue 31 -- implementing fast nearest neighbour search for Kraskov MI using a k-d tree structure.

This commit is contained in:
joseph.lizier 2014-10-15 04:48:09 +00:00
parent 02c22fcd23
commit bc2783b971
7 changed files with 1174 additions and 178 deletions

View File

@ -24,6 +24,7 @@ import java.util.Random;
import infodynamics.measures.continuous.MutualInfoCalculatorMultiVariate;
import infodynamics.measures.continuous.MutualInfoMultiVariateCommon;
import infodynamics.utils.EuclideanUtils;
import infodynamics.utils.KdTree;
import infodynamics.utils.MathsUtils;
import infodynamics.utils.MatrixUtils;
@ -31,6 +32,8 @@ import infodynamics.utils.MatrixUtils;
* <p>Computes the differential mutual information of two given multivariate sets of
* observations (implementing {@link MutualInfoCalculatorMultiVariate}),
* using Kraskov-Stoegbauer-Grassberger (KSG) estimation (see Kraskov et al., below).
* The implementation is made using fast-neighbour searches with an
* underlying k-d tree algorithm.
* This is an abstract class to gather common functionality between the two
* algorithms defined by Kraskov et al.
* Two child classes {@link MutualInfoCalculatorMultiVariateKraskov1} and
@ -46,10 +49,6 @@ import infodynamics.utils.MatrixUtils;
* </ul>
* </p>
*
* <p>
* TODO Add fast nearest neighbour searches to the child classes
* </p>
*
* <p><b>References:</b><br/>
* <ul>
* <li>Kraskov, A., Stoegbauer, H., Grassberger, P.,
@ -71,9 +70,9 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
protected int k = 4;
/**
* Calculator for the norm between data points
* The norm type in use (see {@link #PROP_NORM_TYPE})
*/
protected EuclideanUtils normCalculator;
protected int normType = EuclideanUtils.NORM_MAX_NORM;
/**
* Property name for the number of K nearest neighbours used in
@ -83,7 +82,7 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
/**
* Property name for what type of norm to use between data points
* for each marginal variable -- Options are defined by
* {@link EuclideanUtils#setNormToUse(String)} and the
* {@link KdTree#setNormType(String)} and the
* default is {@link EuclideanUtils#NORM_MAX_NORM}.
*/
public final static String PROP_NORM_TYPE = "NORM_TYPE";
@ -126,16 +125,49 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
*/
protected int numThreads = Runtime.getRuntime().availableProcessors();
/**
* Private variable to record which algorithm this instance is implementing
* Private variable to record which KSG algorithm number
* this instance is implementing
*/
protected boolean isAlgorithm1 = false;
/**
* protected k-d tree data structure (for fast nearest neighbour searches)
* representing the joint source-dest space
*/
protected KdTree kdTreeJoint;
/**
* protected k-d tree data structure (for fast nearest neighbour searches)
* representing the source space
*/
protected KdTree kdTreeSource;
/**
* protected k-d tree data structure (for fast nearest neighbour searches)
* representing the dest space
*/
protected KdTree kdTreeDest;
/**
* Constant for digamma(k), with k the number of nearest neighbours selected
*/
protected double digammaK;
/**
* Constant for digamma(N), with N the number of samples.
*/
protected double digammaN;
/**
* Construct an instance of the KSG MI calculator
*/
public MutualInfoCalculatorMultiVariateKraskov() {
super();
normCalculator = new EuclideanUtils(EuclideanUtils.NORM_MAX_NORM);
}
@Override
public void initialise(int sourceDimensions, int destDimensions) {
kdTreeJoint = null;
kdTreeSource = null;
kdTreeDest = null;
super.initialise(sourceDimensions, destDimensions);
}
/**
@ -150,7 +182,7 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
* in the KSG algorithm (default is 4).</li>
* <li>{@link #PROP_NORM_TYPE}</li> -- normalization type to apply to
* working out the norms between the points in each marginal space.
* Options are defined by {@link EuclideanUtils#setNormToUse(String)} -
* Options are defined by {@link KdTree#setNormType(String)} -
* default is {@link EuclideanUtils#NORM_MAX_NORM}.
* <li>{@link #PROP_NORMALISE} -- whether to normalise the incoming individual
* variables to mean 0 and standard deviation 1 (true by default)</li>
@ -179,7 +211,7 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
if (propertyName.equalsIgnoreCase(PROP_K)) {
k = Integer.parseInt(propertyValue);
} else if (propertyName.equalsIgnoreCase(PROP_NORM_TYPE)) {
normCalculator.setNormToUse(propertyValue);
normType = KdTree.validateNormType(propertyValue);
} else if (propertyName.equalsIgnoreCase(PROP_NORMALISE)) {
normalise = Boolean.parseBoolean(propertyValue);
} else if (propertyName.equalsIgnoreCase(PROP_ADD_NOISE)) {
@ -211,6 +243,13 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
// Allow the parent to generate the data for us first
super.finaliseAddObservations();
if (totalObservations < k) {
throw new Exception("There are less observations provided (" +
totalObservations +
") than the number of nearest neighbours parameter (" +
k + ")");
}
// Normalise the data if required
if (normalise) {
// We can overwrite these since they're already
@ -233,6 +272,10 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
}
}
}
// Set the constants:
digammaK = MathsUtils.digamma(k);
digammaN = MathsUtils.digamma(totalObservations);
}
/**
@ -262,13 +305,25 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
if (reordering == null) {
return computeAverageLocalOfObservations();
}
// Save internal variables pertaining to the original order for
// later reinstatement:
// (don't need to save and reinstate kdTreeSource, since we're not
// altering the source data order).
KdTree originalKdTreeJoint = kdTreeJoint;
KdTree originalKdTreeDest = kdTreeDest;
double[][] originalData2 = destObservations;
// Generate a new re-ordered data2
destObservations = MatrixUtils.extractSelectedTimePointsReusingArrays(originalData2, reordering);
// Compute the MI
double newMI = computeFromObservations(false)[0];
// restore data2
// restore original variables:
destObservations = originalData2;
kdTreeJoint = originalKdTreeJoint;
kdTreeDest = originalKdTreeDest;
return newMI;
}
@ -330,6 +385,26 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
double[] returnValues = null;
// We need to construct the k-d trees for use by the child
// classes. We check each tree for existence separately
// since source can be used across original and surrogate data
// TODO can parallelise these -- best done within the kdTree --
// though it's unclear if there's much point given that
// the tree construction itself afterwards can't really be well parallelised.
if (kdTreeJoint == null) {
kdTreeJoint = new KdTree(new int[] {dimensionsSource, dimensionsDest},
new double[][][] {sourceObservations, destObservations});
kdTreeJoint.setNormType(normType);
}
if (kdTreeSource == null) {
kdTreeSource = new KdTree(sourceObservations);
kdTreeSource.setNormType(normType);
}
if (kdTreeDest == null) {
kdTreeDest = new KdTree(destObservations);
kdTreeDest.setNormType(normType);
}
if (numThreads == 1) {
// Single-threaded implementation:
returnValues = partialComputeFromObservations(0, N, returnLocals);
@ -483,7 +558,8 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
*/
public void run() {
try {
returnValues = miCalc.partialComputeFromObservations(myStartTimePoint, numberOfTimePoints, computeLocals);
returnValues = miCalc.partialComputeFromObservations(
myStartTimePoint, numberOfTimePoints, computeLocals);
} catch (Exception e) {
// Store the exception for later retrieval
problem = e;
@ -491,14 +567,5 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
}
}
}
// end class MiKraskovThreadRunner
/**
* Utility function used for debugging, printing digamma constants
*
* @param N
* @return
* @throws Exception
*/
public abstract String printConstants(int N) throws Exception ;
// end class MiKraskovThreadRunner
}

View File

@ -19,10 +19,11 @@
package infodynamics.measures.continuous.kraskov;
import java.util.Calendar;
import java.util.PriorityQueue;
import infodynamics.measures.continuous.MutualInfoCalculatorMultiVariate;
import infodynamics.utils.MathsUtils;
import infodynamics.utils.MatrixUtils;
import infodynamics.utils.KdTree.NeighbourNodeData;
/**
* <p>Computes the differential mutual information of two given multivariate sets of
@ -50,12 +51,6 @@ import infodynamics.utils.MatrixUtils;
public class MutualInfoCalculatorMultiVariateKraskov1
extends MutualInfoCalculatorMultiVariateKraskov {
/**
* Multiplier used as hueristic for determining whether to use a linear search
* for kth nearest neighbour or a binary search.
*/
protected static final double CUTOFF_MULTIPLIER = 1.5;
public MutualInfoCalculatorMultiVariateKraskov1() {
super();
isAlgorithm1 = true;
@ -67,63 +62,33 @@ public class MutualInfoCalculatorMultiVariateKraskov1
double startTime = Calendar.getInstance().getTimeInMillis();
int N = sourceObservations.length; // number of observations
int cutoffForKthMinLinear = (int) (CUTOFF_MULTIPLIER * Math.log(N) / Math.log(2.0));
double[] localMi = null;
if (returnLocals) {
localMi = new double[numTimePoints];
}
// Constants:
double digammaK = MathsUtils.digamma(k);
double digammaN = MathsUtils.digamma(N);
// Count the average number of points within eps_x and eps_y of each point
double sumDiGammas = 0;
double sumNx = 0;
double sumNy = 0;
for (int t = startTimePoint; t < startTimePoint + numTimePoints; t++) {
// Compute eps for this time step:
// First get x and y norms to all neighbours
// (note that norm of point t to itself will be set to infinity).
// TODO Consider reintroducing caching of the norms here; this helps when we're
// computing stat significance for various re-orderings. May not be useful
// when we've gone to fast nearest neighbour searching though.
double[][] xyNorms = normCalculator.computeNorms(sourceObservations, destObservations, t);
double[] jointNorm = new double[N];
for (int t2 = 0; t2 < N; t2++) {
jointNorm[t2] = Math.max(xyNorms[t2][0], xyNorms[t2][1]);
}
// Then find the kth closest neighbour, using a heuristic to
// select whether to keep the k mins only or to do a sort.
double epsilon = 0.0;
if (k <= cutoffForKthMinLinear) {
// just do a linear search for the minimum
epsilon = MatrixUtils.kthMin(jointNorm, k);
} else {
// Sort the array of joint norms first
java.util.Arrays.sort(jointNorm);
// And find the distance to its kth closest neighbour
// (we subtract one since the array is indexed from zero)
epsilon = jointNorm[k-1];
}
// Compute eps for this time step by
// finding the kth closest neighbour for point t:
PriorityQueue<NeighbourNodeData> nnPQ =
kdTreeJoint.findKNearestNeighbours(k, t);
// First element in the PQ is the kth NN,
// and epsilon = kthNnData.distance
NeighbourNodeData kthNnData = nnPQ.poll();
// Count the number of points whose x distance is less
// than eps, and whose y distance is less than eps
int n_x = 0;
int n_y = 0;
for (int t2 = 0; t2 < N; t2++) {
if (xyNorms[t2][0] < epsilon) {
n_x++;
}
if (xyNorms[t2][1] < epsilon) {
n_y++;
}
}
// than eps, and whose y distance is less than
// epsilon = kthNnData.distance
int n_x = kdTreeSource.countPointsStrictlyWithinR(
t, kthNnData.distance);
int n_y = kdTreeDest.countPointsStrictlyWithinR(
t, kthNnData.distance);
sumNx += n_x;
sumNy += n_y;
// And take the digammas:
@ -150,13 +115,5 @@ public class MutualInfoCalculatorMultiVariateKraskov1
} else {
return new double[] {sumDiGammas, sumNx, sumNy};
}
}
@Override
public String printConstants(int N) throws Exception {
String constants = String.format("digamma(k=%d)=%.3e + digamma(N=%d)=%.3e => %.3e",
k, MathsUtils.digamma(k), N, MathsUtils.digamma(N),
(MathsUtils.digamma(k) + MathsUtils.digamma(N)));
return constants;
}
}
}

View File

@ -19,11 +19,11 @@
package infodynamics.measures.continuous.kraskov;
import java.util.Calendar;
import java.util.PriorityQueue;
import infodynamics.measures.continuous.MutualInfoCalculatorMultiVariate;
import infodynamics.utils.FirstIndexComparatorDouble;
import infodynamics.utils.MathsUtils;
import infodynamics.utils.MatrixUtils;
import infodynamics.utils.KdTree.NeighbourNodeData;
/**
* <p>Computes the differential mutual information of two given multivariate sets of
@ -50,15 +50,6 @@ import infodynamics.utils.MatrixUtils;
*/
public class MutualInfoCalculatorMultiVariateKraskov2
extends MutualInfoCalculatorMultiVariateKraskov {
protected static final int JOINT_NORM_VAL_COLUMN = 0;
protected static final int JOINT_NORM_TIMESTEP_COLUMN = 1;
/**
* Multiplier used as hueristic for determining whether to use a linear search
* for kth nearest neighbour or a binary search.
*/
protected static final double CUTOFF_MULTIPLIER = 1.5;
public MutualInfoCalculatorMultiVariateKraskov2() {
super();
@ -70,18 +61,13 @@ public class MutualInfoCalculatorMultiVariateKraskov2
double startTime = Calendar.getInstance().getTimeInMillis();
int N = sourceObservations.length; // number of observations
int cutoffForKthMinLinear = (int) (CUTOFF_MULTIPLIER * Math.log(N) / Math.log(2.0));
double[] localMi = null;
if (returnLocals) {
localMi = new double[numTimePoints];
}
// Constants:
double digammaK = MathsUtils.digamma(k);
double invK = 1.0 / (double)k;
double digammaN = MathsUtils.digamma(N);
// Count the average number of points within eps_x and eps_y of each point
double sumDiGammas = 0;
@ -89,59 +75,33 @@ public class MutualInfoCalculatorMultiVariateKraskov2
double sumNy = 0;
for (int t = startTimePoint; t < startTimePoint + numTimePoints; t++) {
// Compute eps_x and eps_y for this time step:
// First get x and y norms to all neighbours
// (note that norm of point t to itself will be set to infinity).
double[][] xyNorms = normCalculator.computeNorms(sourceObservations, destObservations, t);
double[][] jointNorm = new double[N][2];
for (int t2 = 0; t2 < N; t2++) {
jointNorm[t2][JOINT_NORM_VAL_COLUMN] = Math.max(xyNorms[t2][0], xyNorms[t2][1]);
// And store the time step for back reference after the
// array is sorted.
jointNorm[t2][JOINT_NORM_TIMESTEP_COLUMN] = t2;
}
// Then find the k closest neighbours, using a heuristic to
// select whether to keep the k mins only or to do a sort.
// Compute eps_x and eps_y for this time step by
// finding the kth closest neighbours for point t:
PriorityQueue<NeighbourNodeData> nnPQ =
kdTreeJoint.findKNearestNeighbours(k, t);
// Find eps_{x,y} as the maximum x and y norms amongst this set:
double eps_x = 0.0;
double eps_y = 0.0;
int[] timeStepsOfKthMins = null;
if (k <= cutoffForKthMinLinear) {
// just do a linear search for the minimum epsilon value
timeStepsOfKthMins = MatrixUtils.kMinIndices(jointNorm, JOINT_NORM_VAL_COLUMN, k);
} else {
// Sort the array of joint norms
java.util.Arrays.sort(jointNorm, FirstIndexComparatorDouble.getInstance());
// and now we have the closest k points.
timeStepsOfKthMins = new int[k];
for (int j = 0; j < k; j++) {
timeStepsOfKthMins[j] = (int) jointNorm[j][JOINT_NORM_TIMESTEP_COLUMN];
}
}
// and now we have the closest k points.
// Find eps_{x,y} as the maximum x and y norms amongst this set:
for (int j = 0; j < k; j++) {
int timeStepOfJthPoint = timeStepsOfKthMins[j];
if (xyNorms[timeStepOfJthPoint][0] > eps_x) {
eps_x = xyNorms[timeStepOfJthPoint][0];
// Take the furthest remaining of the nearest neighbours from the PQ:
NeighbourNodeData nnData = nnPQ.poll();
if (nnData.norms[0] > eps_x) {
eps_x = nnData.norms[0];
}
if (xyNorms[timeStepOfJthPoint][1] > eps_y) {
eps_y = xyNorms[timeStepOfJthPoint][1];
if (nnData.norms[1] > eps_y) {
eps_y = nnData.norms[1];
}
}
// Count the number of points whose x distance is less
// than or equal to eps_x, and whose y distance is less
// than or equal to eps_y
int n_x = 0;
int n_y = 0;
for (int t2 = 0; t2 < N; t2++) {
if (xyNorms[t2][0] <= eps_x) {
n_x++;
}
if (xyNorms[t2][1] <= eps_y) {
n_y++;
}
}
int n_x = kdTreeSource.countPointsWithinOrOnR(
t, eps_x);
int n_y = kdTreeDest.countPointsWithinOrOnR(
t, eps_y);
sumNx += n_x;
sumNy += n_y;
// And take the digammas:
@ -169,11 +129,4 @@ public class MutualInfoCalculatorMultiVariateKraskov2
return new double[] {sumDiGammas, sumNx, sumNy};
}
}
public String printConstants(int N) throws Exception {
String constants = String.format("digamma(k=%d)=%.3e - 1/k=%.3e + digamma(N=%d)=%.3e => %.3e",
k, MathsUtils.digamma(k), 1.0/(double)k, N, MathsUtils.digamma(N),
(MathsUtils.digamma(k) - 1.0/(double)k + MathsUtils.digamma(N)));
return constants;
}
}

View File

@ -35,6 +35,8 @@ public class EuclideanUtils {
public static final String NORM_EUCLIDEAN_NORMALISED_STRING = "EUCLIDEAN_NORMALISED";
public static final int NORM_MAX_NORM = 2;
public static final String NORM_MAX_NORM_STRING = "MAX_NORM";
public static final int NORM_EUCLIDEAN_SQUARED = 3;
public static final String NORM_EUCLIDEAN_SQUARED_STRING = "EUCLIDEAN_SQUARED";
// Track which norm we should use here
private int normToUse = 0;
@ -42,12 +44,26 @@ public class EuclideanUtils {
* Construct a EuclideanUtils object, to take norms of the given type
*
* @param normToUse norm type, one of
* {@link #NORM_EUCLIDEAN_STRING},
* {@link #NORM_EUCLIDEAN_NORMALISED_STRING},
* or {@link #NORM_MAX_NORM_STRING}
* {@link #NORM_EUCLIDEAN},
* {@link #NORM_EUCLIDEAN_NORMALISED},
* {@link #NORM_MAX_NORM}
* or {@link #NORM_MAX_NORM}
*/
public EuclideanUtils(int normToUse) {
this.normToUse = normToUse;
setNormToUse(normToUse);
}
/**
* Construct a EuclideanUtils object, to take norms of the given type
*
* @param normToUse norm type, one of
* {@link #NORM_EUCLIDEAN_STRING},
* {@link #NORM_EUCLIDEAN_NORMALISED_STRING},
* {@link #NORM_MAX_NORM_STRING}
* or {@link #NORM_MAX_NORM_STRING}
*/
public EuclideanUtils(String normToUse) {
setNormToUse(normToUse);
}
public static double[] computeMinEuclideanDistances(double[][] observations) {
@ -199,12 +215,38 @@ public class EuclideanUtils {
return euclideanNorm(x1, x2) / Math.sqrt(x1.length);
case NORM_MAX_NORM:
return maxNorm(x1, x2);
case NORM_EUCLIDEAN_SQUARED:
return euclideanNormSquared(x1, x2);
case NORM_EUCLIDEAN:
default:
return euclideanNorm(x1, x2);
}
}
/**
* Computing the configured norm between vectors x1 and x2; if
* it becomes clear that norm will be larger than limit,
* then return Double.POSITIVE_INFINITY immediately.
*
* @param x1
* @param x2
* @param limit
* @return
*/
public double normWithAbort(double[] x1, double[] x2, double limit) {
switch (normToUse) {
case NORM_EUCLIDEAN_NORMALISED:
return euclideanNormWithAbort(x1, x2, limit) / Math.sqrt(x1.length);
case NORM_MAX_NORM:
return maxNormWithAbort(x1, x2, limit);
case NORM_EUCLIDEAN_SQUARED:
return euclideanNormSquaredWithAbort(x1, x2, limit);
case NORM_EUCLIDEAN:
default:
return euclideanNormWithAbort(x1, x2, limit);
}
}
/**
* Computing the norm as the Euclidean norm.
*
@ -220,7 +262,70 @@ public class EuclideanUtils {
}
return Math.sqrt(distance);
}
/**
* Computing the norm as the Euclidean norm; if
* it becomes clear that norm will be larger than limit,
* then return Double.POSITIVE_INFINITY immediately.
*
* @param x1
* @param x2
* @param limit
* @return
*/
public static double euclideanNormWithAbort(double[] x1, double[] x2, double limit) {
double distance = 0.0;
limit *= limit;
for (int d = 0; d < x1.length; d++) {
double difference = x1[d] - x2[d];
distance += difference * difference;
if (distance > limit) {
return Double.POSITIVE_INFINITY;
}
}
return Math.sqrt(distance);
}
/**
* Computing the norm as the Euclidean norm squared
* (i.e. avoids taking the square root).
*
* @param x1
* @param x2
* @return
*/
public static double euclideanNormSquared(double[] x1, double[] x2) {
double distance = 0.0;
for (int d = 0; d < x1.length; d++) {
double difference = x1[d] - x2[d];
distance += difference * difference;
}
return distance;
}
/**
* Computing the norm as the Euclidean norm squared
* (i.e. avoids taking the square root); if
* it becomes clear that norm will be larger than limit,
* then return Double.POSITIVE_INFINITY immediately.
*
* @param x1 vector 1
* @param x2 vector 2
* @return
*/
public static double euclideanNormSquaredWithAbort(
double[] x1, double[] x2, double limit) {
double distance = 0.0;
for (int d = 0; d < x1.length; d++) {
double difference = x1[d] - x2[d];
distance += difference * difference;
if (distance > limit) {
return Double.POSITIVE_INFINITY;
}
}
return distance;
}
/**
* Computing the norm as the Max norm.
*
@ -243,6 +348,34 @@ public class EuclideanUtils {
return distance;
}
/**
* Computing the norm as the Max norm; if
* it becomes clear that norm will be larger than limit,
* then return Double.POSITIVE_INFINITY immediately.
*
* @param x1 vector 1
* @param x2 vector 2
* @param limit
* @return
*/
public static double maxNormWithAbort(double[] x1, double[] x2, double limit) {
double distance = 0.0;
for (int d = 0; d < x1.length; d++) {
double difference = x1[d] - x2[d];
// Take the abs
if (difference < 0) {
difference = -difference;
}
if (difference > distance) {
if (difference > limit) {
return Double.POSITIVE_INFINITY;
}
distance = difference;
}
}
return distance;
}
/**
* Compute the x and y configured norms of all other points from
* the data points at time step t.
@ -353,17 +486,25 @@ public class EuclideanUtils {
normToUse = NORM_EUCLIDEAN_NORMALISED;
} else if (normType.equalsIgnoreCase(NORM_MAX_NORM_STRING)) {
normToUse = NORM_MAX_NORM;
} else if (normType.equalsIgnoreCase(NORM_EUCLIDEAN_SQUARED_STRING)) {
normToUse = NORM_EUCLIDEAN_SQUARED;
} else {
normToUse = NORM_EUCLIDEAN;
}
}
public String getNormInUse() {
public int getNormInUse() {
return normToUse;
}
public String getNormInUseString() {
switch (normToUse) {
case NORM_EUCLIDEAN_NORMALISED:
return NORM_EUCLIDEAN_NORMALISED_STRING;
case NORM_MAX_NORM:
return NORM_MAX_NORM_STRING;
case NORM_EUCLIDEAN_SQUARED:
return NORM_EUCLIDEAN_SQUARED_STRING;
default:
case NORM_EUCLIDEAN:
return NORM_EUCLIDEAN_STRING;

View File

@ -0,0 +1,889 @@
package infodynamics.utils;
import java.util.PriorityQueue;
/**
* K-d tree implementation to be used for fast neighbour searching
* across several (multi-dimensional) variables.
* Norms for the nearest neighbour searches are the max norm between
* the (multi-dimensional) variables, and either max norm or Euclidean
* norm (squared) within each variable.
*
* @author Joseph Lizier (<a href="joseph.lizier at gmail.com">email</a>,
* <a href="http://lizier.me/joseph/">www</a>)
*
* @see <a href="http://en.wikipedia.org/wiki/K-d_tree">K-d tree page on wikipedia</a>
*/
public class KdTree {
/**
* Cached reference to the data the tree is constructed from.
* We have an array of double[][] 2D arrays -- each 2D array
* originalDataSets[i] is
* considered as a separate (multivariate) variable (indexed by sample
* number then dimension number), whilst the set of all such
* arrays is considered a joint variable of multivariates.
*/
protected double[][][] originalDataSets;
/**
* For each dimension dim, sortedArrayIndices[dim] is an array
* of indices to the data in sourceObservations and destObservations,
* sorted in order (min to max) for the dimension dim;
* plus we have a spare dimension for a temporary array.
* This is only used in the construction of the kd tree.
*/
protected int[][] masterSortedArrayIndices = null;
/**
* Maps dimension number (first index) to which
* double[][] array holds the data for this dimension,
* and which index we use in that array (dimensionToArrayIndex).
*
* I.e. for a dimension number d (out of the joint variables
* across all the multivariates in originalDataSets),
* dimensionToArray[d] points to the relevant originalDataSets[i]
* multivariate, while dimensionToArrayIndex[d] tells us which
* variable within originalDataSets[i] to use, i.e. the time series
* originalDataSets[i][t][dimensionToArrayIndex[d]] for time variable t
* is the relevant time-series for dimension number d.
*/
protected double[][][] dimensionToArray = null;
protected int[] dimensionToArrayIndex = null;
protected int totalDimensions = 0;
/**
* The root node for this k-d tree
*/
protected KdTreeNode rootNode = null;
/**
* Calculator for computing the norms for each variable; defaults
* to a max norm.
*/
protected EuclideanUtils normCalculator =
new EuclideanUtils(EuclideanUtils.NORM_MAX_NORM);
/**
* Protected class to implement nodes of a k-d tree
*
* @author Joseph Lizier (<a href="joseph.lizier at gmail.com">email</a>,
* <a href="http://lizier.me/joseph/">www</a>)
*/
protected class KdTreeNode {
protected int indexOfThisPoint;
protected KdTreeNode leftTree;
protected KdTreeNode rightTree;
protected KdTreeNode(int indexOfThisPoint, KdTreeNode leftTree,
KdTreeNode rightTree) {
this.indexOfThisPoint = indexOfThisPoint;
this.leftTree = leftTree;
this.rightTree = rightTree;
}
}
/**
* Construct the k-d tree from a set of double[][] data.
*
* @param data a double[][] 2D data set, first indexed
* by time, second index by variable number.
*/
public KdTree(double[][] data) {
this(new int[] {data[0].length}, new double[][][] {data});
}
/**
* Construct the k-d tree from a <b>set</b> of double[][] data,
* considered jointly.
*
* @param dimensions an array of dimensions for each
* of the 2D data sets.
* @param data an array of double[][] 2D data sets
* for each data[i]
* (where i is the main variable number within data, then
* after that the first index is sample number, second is dimension
* within this data set)
*/
public KdTree(int[] dimensions, double[][][] data) {
this.originalDataSets = data;
int numObservations = data[0].length;
// First work out how many dimensions we have in total:
totalDimensions = 0;
for (int i = 0; i < dimensions.length; i++) {
totalDimensions += dimensions[i];
}
// Cache which array and the index within that array
// are used for each dimension of the data
dimensionToArray = new double[totalDimensions][][];
dimensionToArrayIndex = new int[totalDimensions];
int cumulativeDimension = 0;
int cumulativeDimsionsForPreviousArray = 0;
for (int i = 0; i < dimensions.length; i++) {
int dimensionsForThisVariable = dimensions[i];
for (int j = 0; j < dimensionsForThisVariable; j++) {
dimensionToArray[cumulativeDimension] = data[i];
dimensionToArrayIndex[cumulativeDimension]
= cumulativeDimension - cumulativeDimsionsForPreviousArray;
cumulativeDimension++;
}
cumulativeDimsionsForPreviousArray = cumulativeDimension;
}
// Sort the original data sets in each dimension:
double[][] thisDimensionsData = new double[numObservations][2];
// Create storage for sorted arrays, plus a shared spare temporary
// array
masterSortedArrayIndices = new int[totalDimensions+1][numObservations];
// Sort all the data first
for (int i = 0; i < totalDimensions; i++) {
// Extract the data for this dimension:
double[][] fullData = dimensionToArray[i];
MatrixUtils.arrayCopy(fullData, 0, dimensionToArrayIndex[i],
thisDimensionsData, 0, 0, numObservations, 1);
// Record original time indices:
for (int t = 0; t < numObservations; t++) {
thisDimensionsData[t][1] = t;
}
// Sort the data:
java.util.Arrays.sort(thisDimensionsData, FirstIndexComparatorDouble.getInstance());
// And extract the sorted indices:
for (int t = 0; t < numObservations; t++) {
masterSortedArrayIndices[i][t] = (int) thisDimensionsData[t][1];
}
}
// Construct the k-d tree:masterSortedArrayIndices
rootNode = constructKdTree(0, 0, numObservations, masterSortedArrayIndices);
// And destroy the temporary storage of sorted array indices:
masterSortedArrayIndices = null;
}
/**
*
* @param currentDim the dimension that we're currently working with
* @param startPoint the index of the first point for us to add here,
* in the sorted array of points for currentDim
* @param numPoints the number of points for us to add here,
* in the sorted array of points for currentDim
* @param sortedArrayIndices for each dimension dim, sortedArrayIndices[dim] is an array
* of indices to the data in sourceObservations and destObservations,
* sorted in order (min to max) for the dimension dim. This is only valid
* between startPoint and startPoint + numPoints-1 however; nothing outside
* this should be touched. There is one extra dimension here, which may be used
* as a temporary array (though again, only between startPoint and
* startPoint + numPoints-1 should be touched).
* @return
*/
protected KdTreeNode constructKdTree(int currentDim, int startPoint, int numPoints,
int[][] sortedArrayIndices) {
// Precondition: sortedArrayIndices[][] are currently sorted for all
// dimensions
// Point to the correct array for the data
double[][] data = dimensionToArray[currentDim];
int actualDim = dimensionToArrayIndex[currentDim];
// Handle non-recursive solutions:
if (numPoints == 0) {
return null;
}
if (numPoints == 1) {
return new KdTreeNode(sortedArrayIndices[currentDim][startPoint], null, null);
}
if (numPoints == 2) {
// Make the first point the splitting point, in case
// the two are equal in this dimension, then the left side definitely
// contains a strictly less than branch.
return new KdTreeNode(sortedArrayIndices[currentDim][startPoint], null,
new KdTreeNode(sortedArrayIndices[currentDim][startPoint+1], null, null));
}
// Identify the point on which to split here
int candidateSplitPoint = startPoint + numPoints/2;
while ((candidateSplitPoint > startPoint) &&
(data[sortedArrayIndices[currentDim][candidateSplitPoint-1]][actualDim] ==
data[sortedArrayIndices[currentDim][candidateSplitPoint]][actualDim])) {
// The adjoining data points are equal in this dimension, so continue searching
// for the median with no left points less than it
candidateSplitPoint--;
}
// Postcondition: candidateSplitPoint holds our new median point
double medianInActualDim = data[sortedArrayIndices[currentDim][candidateSplitPoint]][actualDim];
int sampleNumberForSplitPoint = sortedArrayIndices[currentDim][candidateSplitPoint];
int leftStart = startPoint;
int leftNumPoints = candidateSplitPoint-startPoint;
int rightStart = candidateSplitPoint+1;
int rightNumPoints = startPoint+numPoints-1-candidateSplitPoint;
// Partition the other dimensions properly.
int[][] newSortedArrayIndices = new int[totalDimensions+1][];
// Grab the temporary array for us to use:
int[] tempSortedIndices = sortedArrayIndices[totalDimensions];
for (int dim = 0; dim < totalDimensions; dim++) {
if (dim == currentDim) {
newSortedArrayIndices[dim] = sortedArrayIndices[dim];
continue;
}
int leftIndex = leftStart, rightIndex = rightStart;
for (int i = startPoint; i < startPoint + numPoints; i++) {
int sampleNumberInData = sortedArrayIndices[dim][i];
if (sampleNumberInData == sampleNumberForSplitPoint) {
// This is the split point
continue;
}
// Check if this data point
// was going to the left or right tree
if (data[sampleNumberInData][actualDim] < medianInActualDim) {
// This point will be in the left tree
tempSortedIndices[leftIndex++] = sampleNumberInData;
} else {
// This point will be in the right tree
tempSortedIndices[rightIndex++] = sampleNumberInData;
}
}
// Check that we have not exceeded boundaries for either
// left or right sets:
// Could remove these since the code is now functional,
// but may be better to leave them in just in case the code breaks:
if (leftIndex > leftStart + leftNumPoints) {
throw new RuntimeException("Exceeded expected number of points on left");
}
if (rightIndex > rightStart + rightNumPoints) {
throw new RuntimeException("Exceeded expected number of points on right");
}
// Update the pointer for the sorted indices for this dimension,
// and keep the new temporary array
int[] temp = sortedArrayIndices[dim]; // Old array to become tempSortedIndices
newSortedArrayIndices[dim] = tempSortedIndices;
tempSortedIndices = temp;
}
newSortedArrayIndices[totalDimensions] = tempSortedIndices;
int newDim = (currentDim + 1) % totalDimensions;
return new KdTreeNode(sampleNumberForSplitPoint,
constructKdTree(newDim, leftStart, leftNumPoints, newSortedArrayIndices),
constructKdTree(newDim, rightStart, rightNumPoints, newSortedArrayIndices));
}
/**
* Set the norm type to use in the nearest neighbour searches,
* within each joint variable, to normType.
*
* @param normType norm type to use; must be either
* {@link EuclideanUtils#NORM_EUCLIDEAN},
* {@link EuclideanUtils#NORM_EUCLIDEAN_SQUARED} or
* {@link EuclideanUtils#NORM_MAX_NORM}, otherwise an
* UnsupportedOperationException is thrown.
* {@link EuclideanUtils#NORM_EUCLIDEAN} will be nominally supported
* but switched to
* {@link EuclideanUtils#NORM_EUCLIDEAN_SQUARED} internally for speed.
* @throws UnsupportedOperationException if the norm type is not
* one of the above supported options.
*/
public void setNormType(int normType) {
if ((normType != EuclideanUtils.NORM_EUCLIDEAN) &&
(normType != EuclideanUtils.NORM_EUCLIDEAN_SQUARED) &&
(normType != EuclideanUtils.NORM_MAX_NORM)) {
throw new UnsupportedOperationException("Norm type " + normType +
" is not supported in KdTree");
}
if (normType == EuclideanUtils.NORM_EUCLIDEAN) {
normType = EuclideanUtils.NORM_EUCLIDEAN_SQUARED;
}
normCalculator.setNormToUse(normType);
}
/**
* Set the norm type to use within each joint variable to normType.
*
* @param normType norm type to use; must be either
* {@link EuclideanUtils#NORM_EUCLIDEAN_STRING},
* {@link EuclideanUtils#NORM_EUCLIDEAN_SQUARED_STRING} or
* {@link EuclideanUtils#NORM_MAX_NORM_STRING}, otherwise an
* UnsupportedOperationException is thrown.
* {@link EuclideanUtils#NORM_EUCLIDEAN} will be nominally supported
* but switched to
* {@link EuclideanUtils#NORM_EUCLIDEAN_SQUARED} internally for speed.
* @throws UnsupportedOperationException if the norm type is not
* one of the above supported options.
*/
public void setNormType(String normType) {
if (!normType.equalsIgnoreCase(EuclideanUtils.NORM_EUCLIDEAN_STRING) &&
!normType.equalsIgnoreCase(EuclideanUtils.NORM_EUCLIDEAN_SQUARED_STRING) &&
!normType.equalsIgnoreCase(EuclideanUtils.NORM_MAX_NORM_STRING)) {
throw new UnsupportedOperationException("Norm type " + normType +
" is not supported in KdTree");
}
if (normType.equalsIgnoreCase(EuclideanUtils.NORM_EUCLIDEAN_STRING)) {
normType = EuclideanUtils.NORM_EUCLIDEAN_SQUARED_STRING;
}
normCalculator.setNormToUse(normType);
}
/**
* Validate whether a specified norm type is supported,
* and return the int corresponding to that type,
* otherwise through an exception.
*
* @param normType norm type to use; must be either
* {@link EuclideanUtils#NORM_EUCLIDEAN_STRING},
* {@link EuclideanUtils#NORM_EUCLIDEAN_SQUARED_STRING} or
* {@link EuclideanUtils#NORM_MAX_NORM_STRING}, otherwise an
* UnsupportedOperationException is thrown.
* {@link EuclideanUtils#NORM_EUCLIDEAN} will be nominally supported
* but switched to
* {@link EuclideanUtils#NORM_EUCLIDEAN_SQUARED} internally for speed.
* @throws UnsupportedOperationException if the norm type is not
* one of the above supported options.
*/
public static int validateNormType(String normType) {
if (normType.equalsIgnoreCase(EuclideanUtils.NORM_EUCLIDEAN_STRING)) {
normType = EuclideanUtils.NORM_EUCLIDEAN_SQUARED_STRING;
}
if (normType.equalsIgnoreCase(EuclideanUtils.NORM_EUCLIDEAN_SQUARED_STRING)) {
return EuclideanUtils.NORM_EUCLIDEAN_SQUARED;
}
if (normType.equalsIgnoreCase(EuclideanUtils.NORM_MAX_NORM_STRING)) {
return EuclideanUtils.NORM_MAX_NORM;
}
throw new UnsupportedOperationException("Norm type " + normType +
" is not supported in KdTree");
}
/**
* Return the node which is the nearest neighbour for a given
* sample index in the data set. The node itself is
* excluded from the search.
* Nearest neighbour function to compare to r is a max norm between the
* high-level variables, with norm for each variable being the specified norm.
*
* @param sampleIndex sample index in the data to find a nearest neighbour
* for
* @return the node for the nearest neighbour.
*/
public NeighbourNodeData findNearestNeighbour(int sampleIndex) {
if (rootNode == null) {
return null;
}
return findNearestNeighbour(sampleIndex, rootNode, 0, null);
}
/**
* Find the nearest neighbour to a given sample (sampleIndex), in the tree
* rooted at node (which is at the specified level in the tree), or
* return currentBest if no better match is found.
* Nearest neighbour is a max norm between the high-level variables,
* with norm for each variable being the specified norm.
*
*
* @param sampleIndex sample index in the data to find a nearest neighbour
* for
* @param node node to start searching from in the kd-tree. Cannot be null
* @param level which level we're currently at in the tree
* @param currentBest a NeighbourNodeData structure capturing the current
* closest neighbour and its distance
* @return the node data for the nearest neighbour.
*/
protected NeighbourNodeData findNearestNeighbour(int sampleIndex,
KdTreeNode node, int level, NeighbourNodeData currentBest) {
// Point to the correct array for the data at this level
int currentDim = level % totalDimensions;
double[][] data = dimensionToArray[currentDim];
int actualDim = dimensionToArrayIndex[currentDim];
// Check the distance on this particular dimension
double distOnThisDim = data[sampleIndex][actualDim] -
data[node.indexOfThisPoint][actualDim];
double absDistOnThisDim;
if (normCalculator.getNormInUse() == EuclideanUtils.NORM_MAX_NORM) {
absDistOnThisDim = (distOnThisDim > 0) ? distOnThisDim : - distOnThisDim;
} else {
// norm type is EuclideanUtils#NORM_EUCLIDEAN_SQUARED
// Track the square distance (this saves taking square roots anywhere)
absDistOnThisDim = distOnThisDim * distOnThisDim;
}
if ((node.indexOfThisPoint != sampleIndex) &&
((currentBest == null) || (absDistOnThisDim < currentBest.distance))) {
// Preliminary check says we need to compute the full distance
// to use or at least to check if it should become our
// currentBest properly.
double maxNorm = 0;
double[] norms = new double[originalDataSets.length];
for (int v = 0; v < originalDataSets.length; v++) {
// For each of our separate (multivariate) variables,
// compute the (specified) norm in that variable's space:
if (currentBest == null) {
norms[v] = normCalculator.norm(
originalDataSets[v][sampleIndex],
originalDataSets[v][node.indexOfThisPoint]);
} else {
// Distance calculation terminates early with Double.POSITIVE_INFINITY
// if it is clearly larger than currentBest.distance:
norms[v] = normCalculator.normWithAbort(
originalDataSets[v][sampleIndex],
originalDataSets[v][node.indexOfThisPoint],
currentBest.distance);
}
if (norms[v] > maxNorm) {
maxNorm = norms[v];
if (Double.isInfinite(maxNorm)) {
// we've aborted the norm check early;
// no point checking the other variables.
break;
}
}
}
if ((currentBest == null) ||
(maxNorm < currentBest.distance)) {
// We set this as the current nearest neighbour:
currentBest = new NeighbourNodeData(node.indexOfThisPoint,
norms, maxNorm);
}
}
KdTreeNode closestSubTree = null;
KdTreeNode furthestSubTree = null;
// And translate this to which subtree is closer
if (distOnThisDim < 0) {
// We need to search the left tree
closestSubTree = node.leftTree;
furthestSubTree = node.rightTree;
} else {
// We need to search the right tree
closestSubTree = node.rightTree;
furthestSubTree = node.leftTree;
}
// Update the search on that subtree
if (closestSubTree != null) {
currentBest = findNearestNeighbour(sampleIndex, closestSubTree,
level + 1, currentBest);
}
if ((currentBest == null) || (absDistOnThisDim < currentBest.distance)) {
// It's possible we could have a closer node than the current best
// in the other branch as well, so search there too.
// (It's highly unlikely we would still have (currentBest == null)
// here but remains possible, so we check that)
if (furthestSubTree != null) {
currentBest = findNearestNeighbour(sampleIndex, furthestSubTree,
level + 1, currentBest);
}
}
return currentBest;
}
/**
* Return the K nodes which are the K nearest neighbours for a given
* sample index in the data set. The node itself is
* excluded from the search.
* Nearest neighbour function to compare to r is a max norm between the
* high-level variables, with norm for each variable being the specified norm.
*
* @param K number of K nearest neighbours to return, sorted from
* furthest away first to nearest last.
* @param sampleIndex sample index in the data to find the K nearest neighbours
* for
* @return a PriorityQueue of nodes for the K nearest neighbours,
* sorted with furthest neighbour first in the PQ.
*/
public PriorityQueue<NeighbourNodeData>
findKNearestNeighbours(int K, int sampleIndex) {
PriorityQueue<NeighbourNodeData> pq = new PriorityQueue<NeighbourNodeData>(K);
if (rootNode == null) {
return pq;
}
findKNearestNeighbours(K, sampleIndex, rootNode, 0, pq);
return pq;
}
/**
* Protected method to Update the k nearest neighbours to a given sample (sampleIndex), from the tree
* rooted at node (which is at the specified level in the tree).
* Incorporate neighbours found in this sub-tree into the PriorityQueue
* of the current K closest.
* @param K the number of nearest neighbour
* @param sampleIndex sample index in the data to find a nearest neighbour
* for
* @param node node to start searching from in the kd-tree. Cannot be null
* @param level which level we're currently at in the tree
* @param currentKBest a PriorityQueue of NeighbourNodeData objects
* capturing the current K closest neighbours and their distances.
* Assumed not to be null, but may be empty or with less than K elements
* so far. It must be sorted from furthest away first to nearest last.
*/
protected void findKNearestNeighbours(int K,
int sampleIndex, KdTreeNode node, int level,
PriorityQueue<NeighbourNodeData> currentKBest) {
// Point to the correct array for the data at this level
int currentDim = level % totalDimensions;
double[][] data = dimensionToArray[currentDim];
int actualDim = dimensionToArrayIndex[currentDim];
// Check the distance on this particular dimension
double distOnThisDim = data[sampleIndex][actualDim] -
data[node.indexOfThisPoint][actualDim];
double absDistOnThisDim;
if (normCalculator.getNormInUse() == EuclideanUtils.NORM_MAX_NORM) {
absDistOnThisDim = (distOnThisDim > 0) ? distOnThisDim : - distOnThisDim;
} else {
// norm type is EuclideanUtils#NORM_EUCLIDEAN_SQUARED
// Track the square distance (this saves taking square roots anywhere)
absDistOnThisDim = distOnThisDim * distOnThisDim;
}
// Grab the current furthest nearest neighbour in our cached list
// (will not throw an Exception if the PQ is empty)
NeighbourNodeData furthestCached = currentKBest.peek();
if ((node.indexOfThisPoint != sampleIndex) &&
((currentKBest.size() < K) || (absDistOnThisDim < furthestCached.distance))) {
// Preliminary check says we need to compute the full distance
// to use or at least to check if it should be
// added to our currentKBest properly.
double maxNorm = 0;
double[] norms = new double[originalDataSets.length];
for (int v = 0; v < originalDataSets.length; v++) {
// For each of our separate (multivariate) variables,
// compute the (specified) norm in that variable's space:
if (currentKBest.size() < K) {
norms[v] = normCalculator.norm(
originalDataSets[v][sampleIndex],
originalDataSets[v][node.indexOfThisPoint]);
} else {
// Distance calculation terminates early with Double.POSITIVE_INFINITY
// if it is clearly larger than currentBest.distance:
norms[v] = normCalculator.normWithAbort(
originalDataSets[v][sampleIndex],
originalDataSets[v][node.indexOfThisPoint],
furthestCached.distance);
}
if (norms[v] > maxNorm) {
maxNorm = norms[v];
if (Double.isInfinite(maxNorm)) {
// we've aborted the norm check early;
// no point checking the other variables.
break;
}
}
}
if ((currentKBest.size() < K) ||
(maxNorm < furthestCached.distance)) {
// We add this to our cache of K nearest neighbours:
if (currentKBest.size() == K) {
// Remove the current Kth nearest neighbour
// as it is about to be replaced.
currentKBest.poll();
}
currentKBest.add(new NeighbourNodeData(node.indexOfThisPoint,
norms, maxNorm));
}
}
KdTreeNode closestSubTree = null;
KdTreeNode furthestSubTree = null;
// And translate this to which subtree is closer
if (distOnThisDim < 0) {
// We need to search the left tree
closestSubTree = node.leftTree;
furthestSubTree = node.rightTree;
} else {
// We need to search the right tree
closestSubTree = node.rightTree;
furthestSubTree = node.leftTree;
}
// Update the search on that subtree
if (closestSubTree != null) {
findKNearestNeighbours(K, sampleIndex,
closestSubTree, level + 1, currentKBest);
}
// Grab the current furthest nearest neighbour in our cached list again
// (will not throw an Exception if the PQ is empty)
// as it may have been changed above:
furthestCached = currentKBest.peek();
if ((currentKBest.size() < K) || (absDistOnThisDim < furthestCached.distance)) {
// It's possible we could have a closer node than the current best
// in the other branch as well, so search there too:
if (furthestSubTree != null) {
findKNearestNeighbours(K, sampleIndex,
furthestSubTree, level + 1, currentKBest);
}
}
}
/**
* Protected class for caching nearest neighbour values during the search
*
* @author Joseph Lizier (<a href="joseph.lizier at gmail.com">email</a>,
* <a href="http://lizier.me/joseph/">www</a>)
*/
public class NeighbourNodeData implements Comparable<NeighbourNodeData> {
public int sampleIndex;
public double[] norms; // norms in each high-level variable
public double distance; // Assertion: distance is the max of norms
/**
* Create an instance representing data about one given nearest neighbour
* to another data point.
*
*
* @param sampleIndex index of the neighbour
* @param norms norms between the neighbour and the other data point,
* for each high-level variable.
* @param distance the max of the norms (used for sorting
* NeighbourNodeData objects in a PriorityQueue)
*/
public NeighbourNodeData(int sampleIndex, double[] norms, double distance) {
super();
this.norms = norms;
this.sampleIndex = sampleIndex;
this.distance = distance;
}
/**
* Override's {@link Comparable#compareTo(Object)} to provide
* a natural comparison for the NeighbourNodeData class,
* based on the underlying distance member.
*
* <p><b>IMPORTANT</b> -- we reverse the usual comparison return
* values, returning a positive number if other is greater,
* and a negative number if other is less. This is so that a
* {@link java.util.PriorityQueue} of NeighbourNodeData objects
* will hold that with the largest distance as the head (whereas
* for standard return values from this method it would be
* the other way around).
*
* @param other Other NeighbourNodeData to compare to
*/
@Override
public int compareTo(NeighbourNodeData other) {
if (distance < other.distance) {
// Normally would return -1 here but we flip it -- see
// header comments
return 1;
} else if (distance > other.distance) {
// Normally would return +1 here but we flip it -- see
// header comments
return -1;
}
// distances are equal
return 0;
}
}
/**
* Count the number of points within norm r for a given
* sample index in the data set. The node itself is
* excluded from the search.
* Nearest neighbour function to compare to r is a max norm between the
* high-level variables, with norm for each variable being the specified norm.
*
* @param sampleIndex sample index in the data to find a nearest neighbour
* for
* @param r radius within which to count points
* @param allowEqualToR if true, then count points at radius r also,
* otherwise only those strictly within r
* @return the count of points within r.
*/
public int countPointsWithinR(int sampleIndex, double r, boolean allowEqualToR) {
if (allowEqualToR) {
return countPointsWithinOrOnR(sampleIndex, r);
} else {
return countPointsStrictlyWithinR(sampleIndex, r);
}
}
/**
* Count the number of points strictly within norm r for a given
* sample index in the data set. The node itself is
* excluded from the search.
* Nearest neighbour function to compare to r is a max norm between the
* high-level variables, with norm for each variable being the specified norm.
*
* @param sampleIndex sample index in the data to find a nearest neighbour
* for
* @param r radius within which to count points
* @return the count of points within r.
*/
public int countPointsStrictlyWithinR(int sampleIndex, double r) {
if (rootNode == null) {
return 0;
}
return countPointsWithinR(sampleIndex, rootNode, 0, r, false);
}
/**
* Count the number of points within or at norm r for a given
* sample index in the data set. The node itself is
* excluded from the search.
* Nearest neighbour function to compare to r is a max norm between the
* high-level variables, with norm for each variable being the specified norm.
*
* @param sampleIndex sample index in the data to find a nearest neighbour
* for
* @param r radius within which to count points
* @return the count of points within or on r.
*/
public int countPointsWithinOrOnR(int sampleIndex, double r) {
if (rootNode == null) {
return 0;
}
return countPointsWithinR(sampleIndex, rootNode, 0, r, true);
}
/**
* Count the number of points within radius r of a given sample (sampleIndex),
* in the tree rooted at node (which is at the specified level in the tree).
* Nearest neighbour function to compare to r is a max norm between the
* high-level variables, with norm for each variable being the specified norm.
*
* @param sampleIndex sample index in the data to find a nearest neighbour
* for
* @param node node to start searching from in the kd-tree. Cannot be null
* @param level which level we're currently at in the tree
* @param r radius within which to count points
* @param allowEqualToR if true, then count points at radius r also,
* otherwise only those strictly within r
* @return count of points within r
*/
protected int countPointsWithinR(int sampleIndex,
KdTreeNode node, int level, double r, boolean allowEqualToR) {
int count = 0;
// Point to the correct array for the data at this level
int currentDim = level % totalDimensions;
double[][] data = dimensionToArray[currentDim];
int actualDim = dimensionToArrayIndex[currentDim];
// Check the distance on this particular dimension
double distOnThisDim = data[sampleIndex][actualDim] -
data[node.indexOfThisPoint][actualDim];
double absDistOnThisDim;
if (normCalculator.getNormInUse() == EuclideanUtils.NORM_MAX_NORM) {
absDistOnThisDim = (distOnThisDim > 0) ? distOnThisDim : - distOnThisDim;
} else {
// norm type is EuclideanUtils#NORM_EUCLIDEAN_SQUARED
// Track the square distance
absDistOnThisDim = distOnThisDim * distOnThisDim;
}
if ((node.indexOfThisPoint != sampleIndex) &&
((absDistOnThisDim < r) ||
( allowEqualToR && (absDistOnThisDim == r)))) {
// Preliminary check says we need to compute the full distance
// to use or at least to check if it should be counted.
double maxNorm = 0;
for (int v = 0; v < originalDataSets.length; v++) {
// For each of our separate (multivariate) variables,
// compute the (specified) norm in that variable's space:
double distForVariableV;
// Distance calculation terminates early with Double.POSITIVE_INFINITY
// if it is clearly larger than currentBest.distance:
distForVariableV = normCalculator.normWithAbort(
originalDataSets[v][sampleIndex],
originalDataSets[v][node.indexOfThisPoint],
r);
if (distForVariableV > maxNorm) {
maxNorm = distForVariableV;
if (Double.isInfinite(maxNorm)) {
// we've aborted the norm check early;
// no point checking the other variables.
break;
}
}
}
if ((maxNorm < r) ||
( allowEqualToR && (maxNorm == r))) {
// This node gets counted
count++;
}
}
KdTreeNode closestSubTree = null;
KdTreeNode furthestSubTree = null;
// And translate this to which subtree is closer
if (distOnThisDim < 0) {
// We need to search the left tree
closestSubTree = node.leftTree;
furthestSubTree = node.rightTree;
} else {
// We need to search the right tree
closestSubTree = node.rightTree;
furthestSubTree = node.leftTree;
}
// Update the search on that subtree
if (closestSubTree != null) {
count += countPointsWithinR(sampleIndex, closestSubTree,
level + 1, r, allowEqualToR);
}
if ((absDistOnThisDim < r) ||
( allowEqualToR && (distOnThisDim < 0) && (absDistOnThisDim == r))) {
// It's possible we could have a node within (or on) r
// in the other branch as well, so search there too.
// (Note: we only check furthest subtree in the == case
// when it's allowed
// *if* it's the right subtree, as only the right sub-tree
// can have node with distance in this coordinate *equal* to
// that of the current node -- left subtree must be strictly
// less than the coordinate of the current node, so
// distance to any of those points could not be equal.)
if (furthestSubTree != null) {
count += countPointsWithinR(sampleIndex, furthestSubTree,
level + 1, r, allowEqualToR);
}
}
return count;
}
/**
* Internal utility function for debug printing of a tree
*
*/
public void print() {
print(rootNode, 0);
}
/**
* Internal utility function for debug printing of a node and
* all of its descendants
*
* @param node current node
* @param level which level we're at in the tree
*/
protected void print(KdTreeNode node, int level) {
if (node == null) {
System.out.print("null");
return;
}
if (level > 0) {
System.out.println();
}
for (int i = 0; i < level; i++) {
System.out.print("\t");
}
System.out.print("((");
for (int i = 0; i < totalDimensions; i++) {
System.out.printf("%.3f,",
dimensionToArray[i][node.indexOfThisPoint][dimensionToArrayIndex[i]]);
}
System.out.print("),");
print(node.leftTree, level+1);
System.out.print(", ");
print(node.rightTree, level+1);
System.out.println(")");
}
}

View File

@ -33,7 +33,7 @@ public class MathsUtils {
private static int highestDigammaArgCalced = 0;
private static final int NUM_STORED_DIGAMMAS = 10000; // commons.math to handle beyond this
private static double[] storedDigammas = new double[0];
private static double[] storedDigammas = new double[NUM_STORED_DIGAMMAS];
/**
* Returns the integer result of base^power
@ -272,23 +272,11 @@ public class MathsUtils {
if (d < 1) {
return Double.NaN;
}
if (storedDigammas.length == 0) {
synchronized(storedDigammas) { // Ensure no race condition here
// We do two checks on whether the storage has been
// created so that the first is very fast (without
// requiring synchronization), and the second
// ensures no race condition.
if (storedDigammas.length == 0) {
// Using length == 0 as proxy to null, since
// we can't synchronize on a null object
// allocate space to store our results
storedDigammas = new double[NUM_STORED_DIGAMMAS];
storedDigammas[0] = Double.NaN;
storedDigammas[1] = -EULER_MASCHERONI_CONSTANT;
highestDigammaArgCalced = 1;
}
}
if (highestDigammaArgCalced == 0) {
// We're not initialised yet:
storedDigammas[0] = Double.NaN;
storedDigammas[1] = -EULER_MASCHERONI_CONSTANT;
highestDigammaArgCalced = 1;
}
if (d <= highestDigammaArgCalced) {
// We've already calculated this one

View File

@ -2524,10 +2524,11 @@ public class MatrixUtils {
/**
* Works out the index of the k minimum values in the matrix in a given column
*
* @param matrix
* @param column
* @param k
* @return
* @param matrix data
* @param column which column of the data to find the min values from
* @param k how many min values to return
* @return an array of the (row) indices in the array with the k min values,
* with closest match first.
* @throws Exception
*/
public static int[] kMinIndices(double[][] matrix, int column, int k) throws Exception {
@ -2568,7 +2569,7 @@ public class MatrixUtils {
}
}
}
// Return the index of the kth min
// Return the indices of the k mins
return minIndices;
}