Added auto-embedding with Ragwitz criteria to AIS Kraskov calculator, partially addressing Issue 38. Also adds getProperty() method to AIS calculators and MI calculators.

This commit is contained in:
joseph.lizier 2015-06-04 03:48:55 +00:00
parent 0f5bf692ac
commit 687978103c
9 changed files with 708 additions and 35 deletions

View File

@ -150,6 +150,21 @@ public interface ActiveInfoStorageCalculator {
*/
public void setProperty(String propertyName, String propertyValue) throws Exception;
/**
* Get current property values for the calculator.
*
* <p>Valid property names, and what their
* values should represent, are the same as those for
* {@link #setProperty(String, String)}</p>
*
* <p>Unknown property values are responded to with a null return value.</p>
*
* @param propertyName name of the property
* @return current value of the property
* @throws Exception for invalid property values
*/
public String getProperty(String propertyName) throws Exception;
/**
* Sets a single time-series from which to compute the PDF for the AIS.
* Cannot be called in conjunction with other methods for setting/adding

View File

@ -78,6 +78,12 @@ public class ActiveInfoStorageCalculatorViaMutualInfo implements
*/
protected boolean debug = false;
/**
* Storage for source observations supplied via {@link #addObservations(double[])}
* type calls
*/
protected Vector<double[]> vectorOfObservationTimeSeries;
/**
* Construct using an instantiation of the named MI calculator
*
@ -158,7 +164,6 @@ public class ActiveInfoStorageCalculatorViaMutualInfo implements
public void initialise(int k, int tau) throws Exception {
this.k = k;
this.tau = tau;
miCalc.initialise(k, 1);
}
/**
@ -210,20 +215,31 @@ public class ActiveInfoStorageCalculatorViaMutualInfo implements
}
}
@Override
public String getProperty(String propertyName)
throws Exception {
if (propertyName.equalsIgnoreCase(K_PROP_NAME)) {
return Integer.toString(k);
} else if (propertyName.equalsIgnoreCase(TAU_PROP_NAME)) {
return Integer.toString(tau);
} else {
// No property was set on this class, assume it is for the underlying
// MI calculator, even if it is for
// MutualInfoCalculatorMultiVariate.PROP_TIME_DIFF which
// is not a valid property for the AIS calculator:
return miCalc.getProperty(propertyName);
}
}
/* (non-Javadoc)
* @see infodynamics.measures.continuous.ActiveInfoStorageCalculator#setObservations(double[])
*/
@Override
public void setObservations(double[] observations) throws Exception {
if (observations.length - (k-1)*tau - 1 <= 0) {
// There are no observations to add here
throw new Exception("Not enough observations to set here given k and tau");
}
double[][] currentDestPastVectors =
MatrixUtils.makeDelayEmbeddingVector(observations, k, tau, (k-1)*tau, observations.length - (k-1)*tau - 1);
double[][] currentDestNextVectors =
MatrixUtils.makeDelayEmbeddingVector(observations, 1, (k-1)*tau + 1, observations.length - (k-1)*tau - 1);
miCalc.setObservations(currentDestPastVectors, currentDestNextVectors);
startAddObservations();
addObservations(observations);
finaliseAddObservations();
}
/* (non-Javadoc)
@ -231,7 +247,7 @@ public class ActiveInfoStorageCalculatorViaMutualInfo implements
*/
@Override
public void startAddObservations() {
miCalc.startAddObservations();
vectorOfObservationTimeSeries = new Vector<double[]>();
}
/* (non-Javadoc)
@ -239,6 +255,20 @@ public class ActiveInfoStorageCalculatorViaMutualInfo implements
*/
@Override
public void addObservations(double[] observations) throws Exception {
// Store these observations in our vector for now
vectorOfObservationTimeSeries.add(observations);
}
/**
* Protected method to internally parse and submit observations through
* to the underlying MI calculator once any internal parameter settings
* have been finalised (in the case of automatically determining the embedding
* parameters)
*
* @param observations time series of observations
* @throws Exception
*/
protected void addObservationsAfterParamsDetermined(double[] observations) throws Exception {
if (observations.length - (k-1)*tau - 1 <= 0) {
// There are no observations to add here
// Don't throw an exception, do nothing since more observations
@ -251,7 +281,7 @@ public class ActiveInfoStorageCalculatorViaMutualInfo implements
MatrixUtils.makeDelayEmbeddingVector(observations, 1, (k-1)*tau + 1, observations.length - (k-1)*tau - 1);
miCalc.addObservations(currentDestPastVectors, currentDestNextVectors);
}
/* (non-Javadoc)
* @see infodynamics.measures.continuous.ActiveInfoStorageCalculator#addObservations(double[], int, int)
*/
@ -261,11 +291,37 @@ public class ActiveInfoStorageCalculatorViaMutualInfo implements
addObservations(MatrixUtils.select(observations, startTime, numTimeSteps));
}
/**
* Hook in case child implementations need to perform any processing on the
* observation time series prior to their being processed and supplied
* to the underlying MI calculator.
* Primarily this is to allow the child implementation to automatically determine
* embedding parameters if desired.
* Child implementations do not need to override this default empty implementation
* if no new functionality is required.
*/
public void preFinaliseAddObservations() throws Exception {
// Empty implementation supplied by default.
}
/* (non-Javadoc)
* @see infodynamics.measures.continuous.ActiveInfoStorageCalculator#finaliseAddObservations()
*/
@Override
public void finaliseAddObservations() throws Exception {
// Auto embed if required
preFinaliseAddObservations();
// Initialise the MI calculator, including any auto-embedding length
miCalc.initialise(k, 1);
miCalc.startAddObservations();
// Send all of the observations through:
for (double[] observations : vectorOfObservationTimeSeries) {
addObservationsAfterParamsDetermined(observations);
}
vectorOfObservationTimeSeries = null; // No longer required
// TODO do we need to throw an exception if there are no observations to add?
miCalc.finaliseAddObservations();
}

View File

@ -102,4 +102,19 @@ public interface MutualInfoCalculatorMultiVariate extends ChannelCalculatorMulti
* @throws Exception
*/
public double computeAverageLocalOfObservations(int[] newOrdering) throws Exception;
/**
* Get current the value for a given property for the calculator.
*
* <p>Valid property names, and what their
* values should represent, are the same as those for
* {@link #setProperty(String, String)}</p>
*
* <p>Unknown property values are responded to with a null return value.</p>
*
* @param propertyName name of the property
* @return current value of the property
* @throws Exception for invalid property values
*/
public String getProperty(String propertyName) throws Exception;
}

View File

@ -175,6 +175,18 @@ public abstract class MutualInfoMultiVariateCommon implements
}
}
@Override
public String getProperty(String propertyName)
throws Exception {
if (propertyName.equalsIgnoreCase(PROP_TIME_DIFF)) {
return Integer.toString(timeDiff);
} else {
// No property was recognised here
return null;
}
}
public void setObservations(double[][] source, double[][] destination) throws Exception {
startAddObservations();
addObservations(source, destination);

View File

@ -688,6 +688,25 @@ public class MutualInfoCalculatorMultiVariateKernel
}
}
@Override
public String getProperty(String propertyName)
throws Exception {
if (propertyName.equalsIgnoreCase(KERNEL_WIDTH_PROP_NAME) ||
propertyName.equalsIgnoreCase(EPSILON_PROP_NAME)) {
return Double.toString(kernelWidth);
} else if (propertyName.equalsIgnoreCase(NORMALISE_PROP_NAME)) {
return Boolean.toString(normalise);
} else if (propertyName.equalsIgnoreCase(DYN_CORR_EXCL_TIME_NAME)) {
return Integer.toString(dynCorrExclTime);
} else if (propertyName.equalsIgnoreCase(FORCE_KERNEL_COMPARE_TO_ALL)) {
return Boolean.toString(forceCompareToAll);
} else {
// try the superclass:
return super.getProperty(propertyName);
}
}
/**
*
* @return the kernel width in use in the calculator

View File

@ -21,6 +21,7 @@ package infodynamics.measures.continuous.kraskov;
import infodynamics.measures.continuous.ActiveInfoStorageCalculator;
import infodynamics.measures.continuous.ActiveInfoStorageCalculatorViaMutualInfo;
import infodynamics.measures.continuous.MutualInfoCalculatorMultiVariate;
import infodynamics.utils.MatrixUtils;
/**
* An Active Information Storage (AIS) calculator (implementing {@link ActiveInfoStorageCalculator})
@ -43,7 +44,10 @@ import infodynamics.measures.continuous.MutualInfoCalculatorMultiVariate;
* <li>{@link #setProperty(String, String)} allowing properties for
* {@link MutualInfoCalculatorMultiVariateKraskov#setProperty(String, String)}
* (except {@link MutualInfoCalculatorMultiVariate#PROP_TIME_DIFF} as outlined
* in {@link ActiveInfoStorageCalculatorViaMutualInfo#setProperty(String, String)}).</li>
* in {@link ActiveInfoStorageCalculatorViaMutualInfo#setProperty(String, String)}).
* Embedding parameters may be automatically determined as per the Ragwitz criteria
* by setting the property {@link #PROP_AUTO_EMBED_METHOD} to {@link #AUTO_EMBED_METHOD_RAGWITZ}
* (plus additional parameter settings for this).</li>
* <li>Computed values are in <b>nats</b>, not bits!</li>
* </ul>
* </p>
@ -54,6 +58,8 @@ import infodynamics.measures.continuous.MutualInfoCalculatorMultiVariate;
* <a href="http://dx.doi.org/10.1016/j.ins.2012.04.016">
* "Local measures of information storage in complex distributed computation"</a>,
* Information Sciences, vol. 208, pp. 39-54, 2012.</li>
* <li>Ragwitz and Kantz, "Markov models from data by simple nonlinear time series
* predictors in delay embedding spaces", Physical Review E, vol 65, 056201 (2002).</li>
* </ul>
*
* @author Joseph Lizier (<a href="joseph.lizier at gmail.com">email</a>,
@ -74,7 +80,63 @@ public class ActiveInfoStorageCalculatorKraskov
* Class name for KSG MI estimator via KSG algorithm 2
*/
public static final String MI_CALCULATOR_KRASKOV2 = MutualInfoCalculatorMultiVariateKraskov2.class.getName();
/**
* Property name for the auto-embedding method. Defaults to {@link #AUTO_EMBED_METHOD_NONE}
*/
public static final String PROP_AUTO_EMBED_METHOD = "AUTO_EMBED_METHOD";
/**
* Valid value for the property {@link #PROP_AUTO_EMBED_METHOD} indicating that
* no auto embedding should be done (i.e. to use manually supplied parameters)
*/
public static final String AUTO_EMBED_METHOD_NONE = "NONE";
/**
* Valid value for the property {@link #PROP_AUTO_EMBED_METHOD} indicating that
* the Ragwitz optimisation technique should be used for automatic embedding
*/
public static final String AUTO_EMBED_METHOD_RAGWITZ = "RAGWITZ";
/**
* Internal variable tracking what type of auto embedding (if any)
* we are using
*/
protected String autoEmbeddingMethod = AUTO_EMBED_METHOD_NONE;
/**
* Property name for maximum k (embedding length) for the auto-embedding search. Default to 1
*/
public static final String PROP_K_SEARCH_MAX = "AUTO_EMBED_K_SEARCH_MAX";
/**
* Internal variable for storing the maximum embedding length to search up to for
* automating the parameters.
*/
protected int k_search_max = 1;
/**
* Property name for maximum tau (embedding delay) for the auto-embedding search. Default to 1
*/
public static final String PROP_TAU_SEARCH_MAX = "AUTO_EMBED_TAU_SEARCH_MAX";
/**
* Internal variable for storing the maximum embedding delay to search up to for
* automating the parameters.
*/
protected int tau_search_max = 1;
/**
* Property name for the number of nearest neighbours to use for the auto-embedding search (Ragwitz criteria).
* Defaults to match the value in use for {@link MutualInfoCalculatorMultiVariateKraskov#PROP_K}
*/
public static final String PROP_RAGWITZ_NUM_NNS = "AUTO_EMBED_RAGWITZ_NUM_NNS";
/**
* Internal variable for storing the number of nearest neighbours to use for the
* auto embedding search (Ragwitz criteria)
*/
protected int ragwitz_num_nns = 1;
/**
* Internal variable to track whether the property {@link #PROP_RAGWITZ_NUM_NNS} has been
* set yet
*/
protected boolean ragwitz_num_nns_set = false;
/**
* Creates a new instance of the Kraskov-Stoegbauer-Grassberger style AIS calculator.
*
@ -126,4 +188,181 @@ public class ActiveInfoStorageCalculatorKraskov
throw new ClassNotFoundException("Algorithm must be 1 or 2");
}
}
/**
* Sets properties for the AIS calculator.
* New property values are not guaranteed to take effect until the next call
* to an initialise method.
*
* <p>Valid property names, and what their
* values should represent, include:</p>
* <ul>
* <li>{@link #PROP_AUTO_EMBED_METHOD} -- method by which the calculator
* automatically determines the embedding history length ({@link #K_PROP_NAME})
* and embedding delay ({@link #TAU_PROP_NAME}). Default is {@link #AUTO_EMBED_METHOD_NONE} meaning
* values are set manually; other accepted values include: {@link #AUTO_EMBED_METHOD_RAGWITZ} for use
* of the Ragwitz criteria (searching up to {@link #PROP_K_SEARCH_MAX} and
* {@link #PROP_TAU_SEARCH_MAX})</li>
* <li>{@link #PROP_K_SEARCH_MAX} -- maximum embedded history length to search
* up to if automatically determining the embedding parameters (as set by
* {@link #PROP_AUTO_EMBED_METHOD}); default is 1</li>
* <li>{@link #PROP_TAU_SEARCH_MAX} -- maximum embedded history length to search
* up to if automatically determining the embedding parameters (as set by
* {@link #PROP_AUTO_EMBED_METHOD}); default is 1</li>
* <li>{@link #PROP_RAGWITZ_NUM_NNS} -- number of nearest neighbours to use
* in the auto-embedding if the property {@link #PROP_AUTO_EMBED_METHOD}
* has been set to {@link #AUTO_EMBED_METHOD_RAGWITZ}. Defaults to the property value
* set for {@link MutualInfoCalculatorMultiVariateKraskov.PROP_K}</li>
* <li>Any properties accepted by {@link super#setProperty(String, String)}</li>
* <li>Or properties accepted by the underlying
* {@link MutualInfoCalculatorMultiVariateKraskov#setProperty(String, String)} implementation.</li>
* </ul>
* <p>One should set {@link MutualInfoCalculatorMultiVariateKraskov#PROP_K} here, the number
* of neighbouring points one should count up to in determining the joint kernel size.</p>
*
* @param propertyName name of the property
* @param propertyValue value of the property.
* @throws Exception if there is a problem with the supplied value).
*/
public void setProperty(String propertyName, String propertyValue)
throws Exception {
boolean propertySet = true;
if (propertyName.equalsIgnoreCase(PROP_AUTO_EMBED_METHOD)) {
// New method set for determining the embedding parameters
autoEmbeddingMethod = propertyValue;
} else if (propertyName.equalsIgnoreCase(PROP_K_SEARCH_MAX)) {
// Set max embedding history length for auto determination of embedding
k_search_max = Integer.parseInt(propertyValue);
} else if (propertyName.equalsIgnoreCase(PROP_TAU_SEARCH_MAX)) {
// Set maximum embedding delay for auto determination of embedding
tau_search_max = Integer.parseInt(propertyValue);
} else if (propertyName.equalsIgnoreCase(PROP_RAGWITZ_NUM_NNS)) {
// Set the number of nearest neighbours to use in case of Ragwitz auto embedding:
ragwitz_num_nns = Integer.parseInt(propertyValue);
ragwitz_num_nns_set = true;
} else {
propertySet = false;
// Assume it was a property for the parent class or underlying MI calculator
super.setProperty(propertyName, propertyValue);
}
if (debug && propertySet) {
System.out.println(this.getClass().getSimpleName() + ": Set property " + propertyName +
" to " + propertyValue);
}
}
/**
* Get property values for the calculator.
*
* <p>Valid property names, and what their
* values should represent, are the same as those for
* {@link #setProperty(String, String)}</p>
*
* <p>Unknown property values are responded to with a null return value.</p>
*
* @param propertyName name of the property
* @return current value of the property
* @throws Exception for invalid property values
*/
public String getProperty(String propertyName)
throws Exception {
if (propertyName.equalsIgnoreCase(PROP_AUTO_EMBED_METHOD)) {
return autoEmbeddingMethod;
} else if (propertyName.equalsIgnoreCase(PROP_K_SEARCH_MAX)) {
return Integer.toString(k_search_max);
} else if (propertyName.equalsIgnoreCase(PROP_TAU_SEARCH_MAX)) {
return Integer.toString(tau_search_max);
} else if (propertyName.equalsIgnoreCase(PROP_RAGWITZ_NUM_NNS)) {
if (ragwitz_num_nns_set) {
return Integer.toString(ragwitz_num_nns);
} else {
return miCalc.getProperty(MutualInfoCalculatorMultiVariateKraskov.PROP_K);
}
} else {
// Assume it was a property for the parent class or underlying MI calculator
return super.getProperty(propertyName);
}
}
@Override
public void preFinaliseAddObservations() throws Exception {
// Automatically determine the embedding parameters for the given time series
if (autoEmbeddingMethod.equalsIgnoreCase(AUTO_EMBED_METHOD_NONE)) {
return;
}
// Else we need to auto embed
double bestPredictionError = Double.POSITIVE_INFINITY;
int k_candidate_best = 1;
int tau_candidate_best = 1;
if (autoEmbeddingMethod.equalsIgnoreCase(AUTO_EMBED_METHOD_RAGWITZ)) {
if (debug) {
System.out.printf("Beginning Ragwitz auto-embedding with k_max=%d, tau_max=%d\n",
k_search_max, tau_search_max);
}
for (int k_candidate = 1; k_candidate <= k_search_max; k_candidate++) {
for (int tau_candidate = 1; tau_candidate <= tau_search_max; tau_candidate++) {
// Use our internal MI calculator in case it has any particular
// properties we need to have been set already
miCalc.initialise(k_candidate, 1);
miCalc.startAddObservations();
// Send all of the observations through:
for (double[] observations : vectorOfObservationTimeSeries) {
double[][] currentDestPastVectors =
MatrixUtils.makeDelayEmbeddingVector(observations, k_candidate,
tau_candidate, (k_candidate-1)*tau_candidate,
observations.length - (k_candidate-1)*tau_candidate - 1);
double[][] currentDestNextVectors =
MatrixUtils.makeDelayEmbeddingVector(observations, 1,
(k_candidate-1)*tau_candidate + 1,
observations.length - (k_candidate-1)*tau_candidate - 1);
miCalc.addObservations(currentDestPastVectors, currentDestNextVectors);
}
miCalc.finaliseAddObservations();
// Now grab the prediction errors of the next value from the required number of
// nearest neighbours of the previous state: (array is of only one term)
double[] predictionError;
if (ragwitz_num_nns_set) {
predictionError =
((MutualInfoCalculatorMultiVariateKraskov) miCalc).
computePredictionErrorsFromObservations(false, ragwitz_num_nns);
} else {
predictionError =
((MutualInfoCalculatorMultiVariateKraskov) miCalc).
computePredictionErrorsFromObservations(false);
}
if (debug) {
System.out.printf("Embedding prediction error (dim=%d) for k=%d,tau=%d is %.3f\n",
predictionError.length, k_candidate, tau_candidate,
predictionError[0] / (double) miCalc.getNumObservations());
}
if ((predictionError[0] / (double) miCalc.getNumObservations())
< bestPredictionError) {
// This parameter setting is the best so far:
// (Note division by number of observations to normalise
// for less observations for larger k and tau)
bestPredictionError = predictionError[0] / (double) miCalc.getNumObservations();
k_candidate_best = k_candidate;
tau_candidate_best = tau_candidate;
}
if (k_candidate == 1) {
// tau is irrelevant, so no point testing other values
break;
}
}
}
}
// Make sure the embedding length and delay are set here
k = k_candidate_best;
tau = tau_candidate_best;
if (debug) {
System.out.printf("Embedding parameters set to k=%d,tau=%d (for prediction error %.3f)\n",
k, tau, bestPredictionError);
}
}
}

View File

@ -19,6 +19,7 @@
package infodynamics.measures.continuous.kraskov;
import java.util.Calendar;
import java.util.PriorityQueue;
import java.util.Random;
import infodynamics.measures.continuous.MutualInfoCalculatorMultiVariate;
@ -28,6 +29,7 @@ import infodynamics.utils.KdTree;
import infodynamics.utils.MathsUtils;
import infodynamics.utils.MatrixUtils;
import infodynamics.utils.NearestNeighbourSearcher;
import infodynamics.utils.NeighbourNodeData;
/**
* <p>Computes the differential mutual information of two given multivariate sets of
@ -256,6 +258,40 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
}
}
/**
* Get property values for the calculator.
*
* <p>Valid property names, and what their
* values should represent, are the same as those for
* {@link #setProperty(String, String)}</p>
*
* <p>Unknown property values are responded to with a null return value.</p>
*
* @param propertyName name of the property
* @return current value of the property
* @throws Exception for invalid property values
*/
public String getProperty(String propertyName)
throws Exception {
if (propertyName.equalsIgnoreCase(PROP_K)) {
return Integer.toString(k);
} else if (propertyName.equalsIgnoreCase(PROP_NORM_TYPE)) {
return KdTree.convertNormTypeToString(normType);
} else if (propertyName.equalsIgnoreCase(PROP_NORMALISE)) {
return Boolean.toString(normalise);
} else if (propertyName.equalsIgnoreCase(PROP_DYN_CORR_EXCL_TIME)) {
return Integer.toString(dynCorrExclTime);
} else if (propertyName.equalsIgnoreCase(PROP_ADD_NOISE)) {
return Double.toString(noiseLevel);
} else if (propertyName.equalsIgnoreCase(PROP_NUM_THREADS)) {
return Integer.toString(numThreads);
} else {
// try the superclass:
return super.getProperty(propertyName);
}
}
/* (non-Javadoc)
* @see infodynamics.measures.continuous.MutualInfoMultiVariateCommon#finaliseAddObservations()
*/
@ -415,25 +451,7 @@ 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 (nnSearcherSource == null) {
nnSearcherSource = NearestNeighbourSearcher.create(sourceObservations);
nnSearcherSource.setNormType(normType);
}
if (nnSearcherDest == null) {
nnSearcherDest = NearestNeighbourSearcher.create(destObservations);
nnSearcherDest.setNormType(normType);
}
ensureKdTreesConstructed();
if (numThreads == 1) {
// Single-threaded implementation:
@ -534,6 +552,119 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
protected abstract double[] partialComputeFromObservations(
int startTimePoint, int numTimePoints, boolean returnLocals) throws Exception;
/**
* Internal method to ensure that the Kd-tree data structures to represent the
* observational data have been constructed (should be called prior to attempting
* to use these data structures)
*/
protected void ensureKdTreesConstructed() throws Exception {
// 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 (nnSearcherSource == null) {
nnSearcherSource = NearestNeighbourSearcher.create(sourceObservations);
nnSearcherSource.setNormType(normType);
}
if (nnSearcherDest == null) {
nnSearcherDest = NearestNeighbourSearcher.create(destObservations);
nnSearcherDest.setNormType(normType);
}
}
/**
* Compute the prediction error in one variable from the k nearest neighbours (kNNs) of the observation
* of the other variable.
* The kNNs of the variable to predict from are formed from the supplied norm type within that variable.
* The number of kNNs to use here is the current property value set for {@link #PROP_K}.
* The prediction error is a sum of absolute errors for each dimension within the variable to predict
*
* @param predictFirstVariable true for predicting the first variable (Source) or
* false for predicting the second variable (destination)
* @return array of prediction errors for each dimension of the predicted variable
* @throws Exception
*/
public double[] computePredictionErrorsFromObservations(boolean predictFirstVariable) throws Exception {
return computePredictionErrorsFromObservations(predictFirstVariable, k);
}
/**
* Compute the prediction error in one variable from the k nearest neighbours (kNNs) of the observation
* of the other variable.
* The kNNs of the variable to predict from are formed from the supplied norm type within that variable.
* The prediction error is a sum of absolute errors for each dimension within the variable to predict
*
* @param predictFirstVariable true for predicting the first variable (Source) or
* false for predicting the second variable (destination)
* @param kNNs number of nearest neighbours to use
* @return array of prediction errors for each dimension of the predicted variable
* @throws Exception
*/
public double[] computePredictionErrorsFromObservations(boolean predictFirstVariable, int kNNs) throws Exception {
int N = sourceObservations.length; // number of observations
double[] totalErrors = null;
ensureKdTreesConstructed();
if (numThreads == 1) {
// Single-threaded implementation:
totalErrors = partialComputePredictionErrorFromObservations(0, N, kNNs, predictFirstVariable);
} else {
// We're going multithreaded:
totalErrors = new double[predictFirstVariable ? dimensionsSource : dimensionsDest];
// Distribute the observations to the threads for the parallel processing
int lTimesteps = N / numThreads; // each thread gets the same amount of data
int res = N % numThreads; // the first thread gets the residual data
if (debug) {
System.out.printf("Computing prediction errors for variable %d from variable %d with %d threads (%d timesteps each, plus %d residual)\n",
predictFirstVariable ? 1 : 2, predictFirstVariable ? 2 : 1,
numThreads, lTimesteps, res);
}
Thread[] tCalculators = new Thread[numThreads];
MiKraskovPredictionThreadRunner[] runners = new MiKraskovPredictionThreadRunner[numThreads];
for (int t = 0; t < numThreads; t++) {
int startTime = (t == 0) ? 0 : lTimesteps * t + res;
int numTimesteps = (t == 0) ? lTimesteps + res : lTimesteps;
if (debug) {
System.out.println(t + ".Thread: from " + startTime +
" to " + (startTime + numTimesteps)); // Trace Message
}
runners[t] = new MiKraskovPredictionThreadRunner(this, startTime,
numTimesteps, kNNs, predictFirstVariable);
tCalculators[t] = new Thread(runners[t]);
tCalculators[t].start();
}
// Here, we should wait for the termination of the all threads
// and collect their results
for (int t = 0; t < numThreads; t++) {
if (tCalculators[t] != null) {
tCalculators[t].join();
}
// Now we add in the data from this completed thread:
MatrixUtils.addInPlace(totalErrors, runners[t].getReturnValues());
}
}
// Finalise the results:
if (debug) {
System.out.printf("Total prediction error from variable %d to variable %d=",
predictFirstVariable ? 2 : 1, predictFirstVariable ? 1 : 2);
MatrixUtils.printArray(System.out, 3, totalErrors);
}
return totalErrors;
}
/**
* Private class to handle multi-threading of the Kraskov algorithms.
* Each instance calls partialComputeFromObservations()
@ -573,8 +704,8 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
* or throw any exception that was encountered by the
* thread.
*
* @return an exception previously encountered by this thread.
* @throws Exception
* @return the relevant return values from this part of the data
* @throws Exception an exception previously encountered by this thread.
*/
public double[] getReturnValues() throws Exception {
if (problem != null) {
@ -598,4 +729,149 @@ public abstract class MutualInfoCalculatorMultiVariateKraskov
}
}
// end class MiKraskovThreadRunner
/**
* Private class to handle multi-threading of the prediction from
* k nearest neighbours.
* Each instance calls partialComputePredictionErrorFromObservations()
* to compute nearest neighbours for a part of the data.
*
*
* @author Joseph Lizier (<a href="joseph.lizier at gmail.com">email</a>,
* <a href="http://lizier.me/joseph/">www</a>)
* @author Ipek Özdemir
*/
private class MiKraskovPredictionThreadRunner implements Runnable {
protected MutualInfoCalculatorMultiVariateKraskov miCalc;
protected int myStartTimePoint;
protected int numberOfTimePoints;
protected int kNNs;
protected boolean predictFirstVariable;
protected double[] returnValues = null;
protected Exception problem = null;
public MiKraskovPredictionThreadRunner(
MutualInfoCalculatorMultiVariateKraskov miCalc,
int myStartTimePoint, int numberOfTimePoints,
int kNNs, boolean predictFirstVariable) {
this.miCalc = miCalc;
this.myStartTimePoint = myStartTimePoint;
this.numberOfTimePoints = numberOfTimePoints;
this.kNNs = kNNs;
this.predictFirstVariable = predictFirstVariable;
}
/**
* Return the sum of prediction errors from this part of the data,
* or throw any exception that was encountered by the
* thread.
*
* @return sum of prediction errors from this part of the data, for
* each of the dimensions of the relevant variable
* @throws Exception an exception previously encountered by this thread.
*/
public double[] getReturnValues() throws Exception {
if (problem != null) {
throw problem;
}
return returnValues;
}
/**
* Start the thread for the given parameters
*/
public void run() {
try {
returnValues = miCalc.partialComputePredictionErrorFromObservations(
myStartTimePoint, numberOfTimePoints, kNNs, predictFirstVariable);
} catch (Exception e) {
// Store the exception for later retrieval
problem = e;
return;
}
}
}
// end class MiKraskovPredictionThreadRunner
/**
* Protected method to be used internally for threaded implementations.
* This method implements the guts of examining prediction errors in one variable
* from the k nearest neighbours of the other.
* It is intended to be called by one thread to work on that specific
* sub-set of the data.
*
* @param startTimePoint start time for the partial set we examine
* @param numTimePoints number of time points (including startTimePoint to examine)
* @param kNNs number of nearest neighbours to use
* @param predictFirstVariable whether to use the second variable to predict the first (true)
* or first variable to predict the second (false)
* @return an array of the sum of square prediction errors for each dimension within the predicted
* variable.
* @throws Exception
*/
protected double[] partialComputePredictionErrorFromObservations(
int startTimePoint, int numTimePoints, int kNNs, boolean predictFirstVariable) throws Exception {
double startTime = Calendar.getInstance().getTimeInMillis();
double[] totalErrors = new double[predictFirstVariable ? dimensionsSource : dimensionsDest];
for (int t = startTimePoint; t < startTimePoint + numTimePoints; t++) {
// Find the k nearest neighbours for the relevant predictor variable
if (predictFirstVariable) {
// First variable value to predict:
double[] sourceValueToPredict = sourceObservations[t];
// Find kNNs of second variable
PriorityQueue<NeighbourNodeData> nnPQ =
nnSearcherDest.findKNearestNeighbours(kNNs, t, dynCorrExclTime);
double[] predictedValue = new double[dimensionsSource];
for (NeighbourNodeData kthNnData : nnPQ) {
// Retrieve the source value this corresponds to
double[] neighbourSourceValue = sourceObservations[kthNnData.sampleIndex];
// And include it's contribution in the prediction
for (int d = 0; d < dimensionsSource; d++) {
predictedValue[d] += neighbourSourceValue[d];
}
}
// Now add in the square prediction errors from the prediction:
for (int d = 0; d < dimensionsSource; d++) {
predictedValue[d] /= (double) kNNs;
totalErrors[d] += (sourceValueToPredict[d] - predictedValue[d]) *
(sourceValueToPredict[d] - predictedValue[d]);
}
} else { // predict second variable
// Second variable value to predict:
double[] destValueToPredict = destObservations[t];
// Find kNNs of first variable
PriorityQueue<NeighbourNodeData> nnPQ =
nnSearcherSource.findKNearestNeighbours(kNNs, t, dynCorrExclTime);
double[] predictedValue = new double[dimensionsDest];
for (NeighbourNodeData kthNnData : nnPQ) {
// Retrieve the dest value this corresponds to
double[] neighbourDestValue = destObservations[kthNnData.sampleIndex];
// And include it's contribution in the prediction
for (int d = 0; d < dimensionsDest; d++) {
predictedValue[d] += neighbourDestValue[d];
}
}
// Now add in the square prediction errors from the prediction:
for (int d = 0; d < dimensionsDest; d++) {
predictedValue[d] /= (double) kNNs;
totalErrors[d] += (destValueToPredict[d] - predictedValue[d]) *
(destValueToPredict[d] - predictedValue[d]);
}
}
}
if (debug) {
Calendar rightNow2 = Calendar.getInstance();
long endTime = rightNow2.getTimeInMillis();
System.out.println("Subset " + startTimePoint + ":" +
(startTimePoint + numTimePoints) + " Calculation time: " +
((endTime - startTime)/1000.0) + " sec" );
}
return totalErrors;
}
}

View File

@ -3709,6 +3709,13 @@ public class MatrixUtils {
out.println();
}
public static void printArray(PrintStream out, int decimalPlaces, double[] array) {
for (int r = 0; r < array.length; r++) {
out.printf(String.format("%%.%df ", decimalPlaces), array[r]);
}
out.println();
}
public static void printArray(PrintStream out, int[] array) {
for (int r = 0; r < array.length; r++) {
out.print(array[r] + " ");

View File

@ -120,6 +120,22 @@ public abstract class NearestNeighbourSearcher {
normTypeToUse = validateNormType(normType);
}
/**
*
* @return the norm type in use
*/
public int getNormType() {
return normTypeToUse;
}
/**
*
* @return the norm type in use as a String
*/
public String getNormTypeAsString() {
return convertNormTypeToString(normTypeToUse);
}
/**
* Validate whether a specified norm type is supported,
* and return the int corresponding to that type,
@ -150,6 +166,24 @@ public abstract class NearestNeighbourSearcher {
" is not supported in NearestNeighbourSearcher");
}
/**
* @param an identifier for a norm type
* @return representation of that norm type as a String
*/
public static String convertNormTypeToString(int normType) {
if (normType == EuclideanUtils.NORM_EUCLIDEAN_SQUARED) {
return EuclideanUtils.NORM_EUCLIDEAN_SQUARED_STRING;
} else if (normType == EuclideanUtils.NORM_EUCLIDEAN) {
return EuclideanUtils.NORM_EUCLIDEAN_STRING;
} else if (normType == EuclideanUtils.NORM_MAX_NORM) {
return EuclideanUtils.NORM_MAX_NORM_STRING;
}
// Execution should never reach this point as we control
// what normTypeToUse gets set to; it might be possible though
// if a child class mis-handles the value
throw new Error("normTypeToUse set to an invalid value: " + normType);
}
/**
* Return the node which is the nearest neighbour for a given
* sample index in the data set. The node itself is