/* --------------------------------------------------------------------- * Numenta Platform for Intelligent Computing (NuPIC) * Copyright (C) 2014, Numenta, Inc. Unless you have an agreement * with Numenta, Inc., for a separate license for this software code, the * following terms and conditions apply: * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero Public License for more details. * * You should have received a copy of the GNU Affero Public License * along with this program. If not, see http://www.gnu.org/licenses. * * http://numenta.org/licenses/ * --------------------------------------------------------------------- */ package org.numenta.nupic.algorithms; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import no.uib.cipr.matrix.sparse.FlexCompRowMatrix; import org.numenta.nupic.model.Persistable; import org.numenta.nupic.util.ArrayUtils; import org.numenta.nupic.util.Deque; import org.numenta.nupic.util.Tuple; import gnu.trove.list.TIntList; import gnu.trove.list.array.TIntArrayList; /** * Implementation of a SDR classifier. * <p> * The SDR classifier takes the form of a single layer classification network * that takes SDRs as input and outputs a predicted distribution of classes. * <p> * The SDR Classifier accepts a binary input pattern from the * level below (the "activationPattern") and information from the sensor and * encoders (the "classification") describing the true (target) input. * <p> * The SDR classifier maps input patterns to class labels. There are as many * output units as the number of class labels or buckets (in the case of scalar * encoders). The output is a probabilistic distribution over all class labels. * <p> * During inference, the output is calculated by first doing a weighted summation * of all the inputs, and then perform a softmax nonlinear function to get * the predicted distribution of class labels * <p> * During learning, the connection weights between input units and output units * are adjusted to maximize the likelihood of the model * <p> * The SDR Classifier is a variation of the previous CLAClassifier which was * not based on the references below. * <p> * Example Usage: * * <pre> * {@code * classifier = new SDRClassifier(new TIntArrayList(new int[] { 1 }), 1.0, 0.3, 0); * * // Learning: * Map<String, Object> classification = new LinkedHashMap<String, Object>(); * classification.put("bucketIdx", 4); * classification.put("actValue", 34.7); * Classification<Double> result1 = classifier.compute(0, classification, new int[] { 1, 5, 9 }, true, false); * * // Inference: * classification.put("bucketIdx", 4); * classification.put("actValue", 34.7); * Classification<Double> result2 = classifier.compute(0, classification, new int[] { 1, 5, 9 }, false, true); * * // Print the top prediction and its likelihood for 1 steps out: * System.out.println("Top prediction: " + result.getMostProbableValue(1)); * }</pre> * * References: * Alex Graves. Supervised Sequence Labeling with Recurrent Neural Networks * PhD Thesis, 2008 * J. S. Bridle. Probabilistic interpretation of feedforward classification * network outputs, with relationships to statistical pattern recognition. * In F. Fogleman-Soulie and J.Herault, editors, Neurocomputing: Algorithms, * Architectures and Applications, pp 227-236, Springer-Verlag, 1990 * * @author Numenta * @author Yuwei Cui * @author David Ray * @author Andrew Dillon */ public class SDRClassifier implements Persistable, Classifier { private static final long serialVersionUID = 1L; int verbosity = 0; /** * The alpha used to adapt the weight matrix during * learning. A larger alpha results in faster adaptation to the data. */ double alpha = 0.001; /** * Used to track the actual value within each * bucket. A lower actValueAlpha results in longer term memory */ double actValueAlpha = 0.3; /** * The bit's learning iteration. This is updated each time store() gets * called on this bit. */ int learnIteration; /** * This contains the offset between the recordNum (provided by caller) and * learnIteration (internal only, always starts at 0). */ int recordNumMinusLearnIteration = -1; /** * This contains the highest value we've ever seen from the list of active cell indexes * from the TM (patternNZ). It is used to pre-allocate fixed size arrays that holds the weights. */ int maxInputIdx = 0; /** * This contains the value of the highest bucket index we've ever seen * It is used to pre-allocate fixed size arrays that hold the weights of * each bucket index during inference */ int maxBucketIdx; /** * The connection weight matrix */ Map<Integer, FlexCompRowMatrix> weightMatrix = new HashMap<>(); /** The sequence different steps of multi-step predictions */ TIntList steps = new TIntArrayList(); /** * History of the last _maxSteps activation patterns. We need to keep * these so that we can associate the current iteration's classification * with the activationPattern from N steps ago */ Deque<Tuple> patternNZHistory; /** * This keeps track of the actual value to use for each bucket index. We * start with 1 bucket, no actual value so that the first infer has something * to return */ List<?> actualValues = new ArrayList<Object>(); String g_debugPrefix = "SDRClassifier"; /** * SDRClassifier no-arg constructor with defaults */ public SDRClassifier() { this(new TIntArrayList(new int[] { 1 }), 0.001, 0.3, 0); } /** * Constructor for the SDRClassifier * * @param steps Sequence of the different steps of multi-step predictions to learn. * @param alpha The alpha used to adapt the weight matrix during learning. A larger alpha * results in faster adaptation to the data. * @param actValueAlpha Used to track the actual value withing each bucket. A lower * actValueAlpha results in longer term memory. * @param verbosity Verbosity level, can be 0, 1, or 2. */ public SDRClassifier(TIntList steps, double alpha, double actValueAlpha, int verbosity) { this.steps = steps; this.alpha = alpha; this.actValueAlpha = actValueAlpha; this.verbosity = verbosity; actualValues.add(null); patternNZHistory = new Deque<Tuple>(ArrayUtils.max(steps.toArray()) + 1); for(int step : steps.toArray()) weightMatrix.put(step, new FlexCompRowMatrix(maxBucketIdx + 1, maxInputIdx + 1)); } /** * Process one input sample. * This method is called by outer loop code outside the nupic-engine. We * use this instead of the nupic engine compute() because our inputs and * outputs aren't fixed size vectors of reals. * <p> * @param recordNum <p> * Record number of this input pattern. Record numbers normally increase * sequentially by 1 each time unless there are missing records in the * dataset. Knowing this information ensures that we don't get confused by * missing records. * @param classification <p> * {@link Map} of the classification information: * <p> "bucketIdx" - index of the encoder bucket * <p> "actValue" - actual value doing into the encoder * @param patternNZ <p> * List of the active indices from the output below. When the output is from * the TemporalMemory, this array should be the indices of the active cells. * @param learn <p> * If true, learn this sample. * @param infer <p> * If true, perform inference. If false, null will be returned. * * @return * {@link Classification} containing inference results if {@code learn} param is true, * otherwise, will return {@code null}. The Classification * contains the computed probability distribution (relative likelihood for each * bucketIdx starting from bucketIdx 0) for each step in {@code steps}. Each bucket's * likelihood can be accessed individually, or all the buckets' likelihoods can * be obtained in the form of a double array. * * <pre>{@code * //Get likelihood val for bucket 0, 5 steps in future * classification.getStat(5, 0); * * //Get all buckets' likelihoods as double[] where each * //index is the likelihood for that bucket * //(e.g. [0] contains likelihood for bucketIdx 0) * classification.getStats(5); * }</pre> * * The Classification also contains the average actual value for each bucket. * The average values for the buckets can be accessed individually, or altogether * as a double[]. * * <pre>{@code * //Get average actual val for bucket 0 * classification.getActualValue(0); * * //Get average vals for all buckets as double[], where * //each index is the average val for that bucket * //(e.g. [0] contains average val for bucketIdx 0) * classification.getActualValues(); * }</pre> * * The Classification can also be queried for the most probable bucket (the bucket * with the highest associated likelihood value), as well as the average input value * that corresponds to that bucket. * * <pre>{@code * //Get index of most probable bucket * classification.getMostProbableBucketIndex(); * * //Get the average actual val for that bucket * classification.getMostProbableValue(); * }</pre> * */ @SuppressWarnings("unchecked") public <T> Classification<T> compute(int recordNum, Map<String, Object> classification, int[] patternNZ, boolean learn, boolean infer) { Classification<T> retVal = null; List<T> actualValues = (List<T>)this.actualValues; //Save the offset between recordNum and learnIteration if this is the first compute if(recordNumMinusLearnIteration == -1) recordNumMinusLearnIteration = recordNum - learnIteration; //Update the learn iteration learnIteration = recordNum - recordNumMinusLearnIteration; //Verbose print if(verbosity >= 1) { System.out.println(String.format("\n%s: compute ", g_debugPrefix)); System.out.printf("recordNum: %d\n", recordNum); System.out.printf("learnIteration: %d\n", learnIteration); System.out.printf("patternNZ (%d): %s\n", patternNZ.length, ArrayUtils.intArrayToString(patternNZ)); System.out.println("classificationIn: " + classification); } //Store pattern in our history patternNZHistory.append(new Tuple(learnIteration, patternNZ)); //Update maxInputIdx and augment weight matrix with zero padding if(ArrayUtils.max(patternNZ) > maxInputIdx) { int newMaxInputIdx = ArrayUtils.max(patternNZ); for (int nSteps : steps.toArray()) { for(int i = maxInputIdx; i < newMaxInputIdx; i++) { weightMatrix.get(nSteps).addCol(new double[maxBucketIdx + 1]); } } maxInputIdx = newMaxInputIdx; } //------------------------------------------------------------------------ //Inference: //For each active bit in the activationPattern, get the classification votes if(infer) { retVal = infer(patternNZ, classification); } //------------------------------------------------------------------------ //Learning: if(learn && classification.get("bucketIdx") != null) { // Get classification info int bucketIdx = (int)classification.get("bucketIdx"); Object actValue = classification.get("actValue"); // Update maxBucketIndex and augment weight matrix with zero padding if(bucketIdx > maxBucketIdx) { for(int nSteps : steps.toArray()) { for(int i = maxBucketIdx; i < bucketIdx; i++) { weightMatrix.get(nSteps).addRow(new double[maxInputIdx + 1]); } } maxBucketIdx = bucketIdx; } // Update rolling average of actual values if it's a scalar. If it's not, it // must be a category, in which case each bucket only ever sees on category so // we don't need a running average. while(maxBucketIdx > actualValues.size() - 1) { actualValues.add(null); } if(actualValues.get(bucketIdx) == null) { actualValues.set(bucketIdx, (T)actValue); } else { if(Number.class.isAssignableFrom(actValue.getClass())) { Double val = ((1.0 - actValueAlpha) * ((Number)actualValues.get(bucketIdx)).doubleValue() + actValueAlpha * ((Number)actValue).doubleValue()); actualValues.set(bucketIdx, (T)val); }else{ actualValues.set(bucketIdx, (T)actValue); } } int iteration = 0; int[] learnPatternNZ = null; for(Tuple t : patternNZHistory) { iteration = (int)t.get(0); learnPatternNZ = (int[])t.get(1); Map<Integer, double[]> error = calculateError(classification); int nSteps = learnIteration - iteration; if(steps.contains(nSteps)) { for(int row = 0; row <= maxBucketIdx; row++) { for (int bit : learnPatternNZ) { weightMatrix.get(nSteps).add(row, bit, alpha * error.get(nSteps)[row]); } } } } } //------------------------------------------------------------------------ //Verbose Print if(infer && verbosity >= 1) { System.out.println(" inference: combined bucket likelihoods:"); System.out.println(" actual bucket values: " + Arrays.toString((T[])retVal.getActualValues())); for(int key : retVal.stepSet()) { if(retVal.getActualValue(key) == null) continue; Object[] actual = new Object[] { (T)retVal.getActualValue(key) }; System.out.println(String.format(" %d steps: ", key, pFormatArray(actual))); int bestBucketIdx = retVal.getMostProbableBucketIndex(key); System.out.println(String.format(" most likely bucket idx: %d, value: %s ", bestBucketIdx, retVal.getActualValue(bestBucketIdx))); } } return retVal; } /** * Return the inference value from one input sample. The actual learning * happens in compute(). * * @param patternNZ int[] of the active indices from the output below * @param classification {@link Map} of the classification information: * <p> "bucketIdx" - index of the encoder bucket * <p> "actValue" - actual value doing into the encoder * @return * {@link Classification} containing inference results. The Classification * contains the computed probability distribution (relative likelihood for each * bucketIdx starting from bucketIdx 0) for each step in {@code steps}. The * Classification also contains the average actual value for each bucket. */ @SuppressWarnings("unchecked") private <T> Classification<T> infer(int[] patternNZ, Map<String, Object> classification) { Classification<T> retVal = new Classification<T>(); // Return Classification. For buckets which we don't have an actual // value for yet, just plug in any valid actual value. It doesn't // matter what we use because that bucket won't have non-zero // likelihood anyways. // NOTE: If doing 0-step predication, we shouldn't use any knowledge // of the classification input during inference. Object defaultValue = null; if(steps.get(0) == 0 || classification == null) { defaultValue = 0; } else { defaultValue = classification.get("actValue"); } T[] actValues = (T[])new Object[this.actualValues.size()]; for(int i = 0; i < actualValues.size(); i++) { actValues[i] = (T)(actualValues.get(i) == null ? defaultValue : actualValues.get(i)); } retVal.setActualValues(actValues); for(int nSteps : steps.toArray()) { double[] predictDist = inferSingleStep(patternNZ, weightMatrix.get(nSteps)); retVal.setStats(nSteps, predictDist); } return retVal; } /** * Perform inference for a single step. Given an SDR input and a weight * matrix, return a predicted distribution. * * @param patternNZ int[] of the active indices from the output below * @param weightMatrix FlexCompColMatrix weight matrix * @return double[] of the predicted class label distribution */ private double[] inferSingleStep(int[] patternNZ, FlexCompRowMatrix weightMatrix) { // Compute the output activation "level" for each bucket (matrix row) // we've seen so far and store in double[] double[] outputActivation = new double[maxBucketIdx + 1]; for(int row = 0; row <= maxBucketIdx; row++) { // Output activation for this bucket is computed as the sum of // the weights for the the active bits in patternNZ, for current // row of matrix. for(int bit : patternNZ) { outputActivation[row] += weightMatrix.get(row, bit); } } // Softmax normalization double[] expOutputActivation = new double[outputActivation.length]; for(int i = 0; i < expOutputActivation.length; i++) { expOutputActivation[i] = Math.exp(outputActivation[i]); } double[] predictDist = new double[outputActivation.length]; for(int i = 0; i < predictDist.length; i++) { predictDist[i] = expOutputActivation[i]/ArrayUtils.sum(expOutputActivation); } return predictDist; } /** * Calculate error signal. * * @param classification {@link Map} of the classification information: * <p> "bucketIdx" - index of the encoder bucket * <p> "actValue" - actual value doing into the encoder * @return * {@link Map} containing error. The key is the number of steps. The value * is a double[] of the error at the output layer. */ private Map<Integer, double[]> calculateError(Map<String, Object> classification) { Map<Integer, double[]> error = new HashMap<Integer, double[]>(); int[] targetDist = new int[maxBucketIdx + 1]; targetDist[(int)classification.get("bucketIdx")] = 1; int iteration = 0; int[] learnPatternNZ = null; int nSteps = 0; for(Tuple t : patternNZHistory) { iteration = (int) t.get(0); learnPatternNZ = (int[]) t.get(1); nSteps = learnIteration - iteration; if(steps.contains(nSteps)) { double[] predictDist = inferSingleStep(learnPatternNZ, weightMatrix.get(nSteps)); double[] targetDistMinusPredictDist = new double[maxBucketIdx + 1]; for(int i = 0; i <= maxBucketIdx; i++) { targetDistMinusPredictDist[i] = targetDist[i] - predictDist[i]; } error.put(nSteps, targetDistMinusPredictDist); } } return error; } /** * Return a string with pretty-print of an array using the given format * for each element * * @param arr * @return */ private <T> String pFormatArray(T[] arr) { if(arr == null) return ""; StringBuilder sb = new StringBuilder("[ "); for(T t : arr) { sb.append(String.format("%.2s", t)); } sb.append(" ]"); return sb.toString(); } }