From ffd3a404999640720baeada6e91cca4707b3c365 Mon Sep 17 00:00:00 2001 From: Yuwei Date: Mon, 5 Jun 2017 14:38:07 -0700 Subject: [PATCH 1/6] handle multiple categories in sdr classifier --- src/nupic/algorithms/sdr_classifier.py | 88 ++++++++++++++++---------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/src/nupic/algorithms/sdr_classifier.py b/src/nupic/algorithms/sdr_classifier.py index 913caa05cb..6eece31ec6 100644 --- a/src/nupic/algorithms/sdr_classifier.py +++ b/src/nupic/algorithms/sdr_classifier.py @@ -204,8 +204,15 @@ def compute(self, recordNum, patternNZ, classification, learn, infer): print " patternNZ (%d):" % len(patternNZ), patternNZ print " classificationIn:", classification - # Store pattern in our history - self._patternNZHistory.append((recordNum, patternNZ)) + # ensures that recordNum increases monotonically + if len(self._patternNZHistory) > 0: + if recordNum < self._patternNZHistory[-1][0]: + raise ValueError("the record number has to increase monotonically") + + # Store pattern in our history if this is a new record + if len(self._patternNZHistory) == 0 or \ + recordNum > self._patternNZHistory[-1][0]: + self._patternNZHistory.append((recordNum, patternNZ)) # To allow multi-class classification, we need to be able to run learning # without inference being on. So initialize retval outside @@ -232,38 +239,48 @@ def compute(self, recordNum, patternNZ, classification, learn, infer): if learn and classification["bucketIdx"] is not None: # Get classification info - bucketIdx = classification["bucketIdx"] - actValue = classification["actValue"] - - # Update maxBucketIndex and augment weight matrix with zero padding - if bucketIdx > self._maxBucketIdx: - for nSteps in self.steps: - self._weightMatrix[nSteps] = numpy.concatenate(( - self._weightMatrix[nSteps], - numpy.zeros(shape=(self._maxInputIdx+1, - bucketIdx-self._maxBucketIdx))), axis=1) - - self._maxBucketIdx = int(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 one category so we don't need a running average. - while self._maxBucketIdx > len(self._actualValues) - 1: - self._actualValues.append(None) - if self._actualValues[bucketIdx] is None: - self._actualValues[bucketIdx] = actValue + if type(classification["bucketIdx"]) is not list: + bucketIdxList = [classification["bucketIdx"]] + actValueList = [classification["actValue"]] + numCategory = 1 else: - if (isinstance(actValue, int) or - isinstance(actValue, float) or - isinstance(actValue, long)): - self._actualValues[bucketIdx] = ((1.0 - self.actValueAlpha) - * self._actualValues[bucketIdx] - + self.actValueAlpha * actValue) - else: + bucketIdxList = classification["bucketIdx"] + actValueList = classification["actValue"] + numCategory = len(classification["bucketIdx"]) + + for categoryI in range(numCategory): + bucketIdx = bucketIdxList[categoryI] + actValue = actValueList[categoryI] + + # Update maxBucketIndex and augment weight matrix with zero padding + if bucketIdx > self._maxBucketIdx: + for nSteps in self.steps: + self._weightMatrix[nSteps] = numpy.concatenate(( + self._weightMatrix[nSteps], + numpy.zeros(shape=(self._maxInputIdx+1, + bucketIdx-self._maxBucketIdx))), axis=1) + + self._maxBucketIdx = int(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 one category so we don't need a running average. + while self._maxBucketIdx > len(self._actualValues) - 1: + self._actualValues.append(None) + if self._actualValues[bucketIdx] is None: self._actualValues[bucketIdx] = actValue + else: + if (isinstance(actValue, int) or + isinstance(actValue, float) or + isinstance(actValue, long)): + self._actualValues[bucketIdx] = ((1.0 - self.actValueAlpha) + * self._actualValues[bucketIdx] + + self.actValueAlpha * actValue) + else: + self._actualValues[bucketIdx] = actValue for (learnRecordNum, learnPatternNZ) in self._patternNZHistory: - error = self._calculateError(recordNum, classification) + error = self._calculateError(recordNum, bucketIdxList) nSteps = recordNum - learnRecordNum if nSteps in self.steps: @@ -436,19 +453,20 @@ def write(self, proto): proto.verbosity = self.verbosity - def _calculateError(self, recordNum, classification): + def _calculateError(self, recordNum, bucketIdxList): """ Calculate error signal - :param classification: dict of the classification information: - bucketIdx: index of the encoder bucket - actValue: actual value going into the encoder + :param bucketIdxList: list of encoder buckets + :return: dict containing error. The key is the number of steps The value is a numpy array of error at the output layer """ error = dict() targetDist = numpy.zeros(self._maxBucketIdx + 1) - targetDist[classification["bucketIdx"]] = 1.0 + numCategories = len(bucketIdxList) + for bucketIdx in bucketIdxList: + targetDist[bucketIdx] = 1.0/numCategories for (learnRecordNum, learnPatternNZ) in self._patternNZHistory: nSteps = recordNum - learnRecordNum From 393c5484f4892d94e477ffc3ca8116bf1f051309 Mon Sep 17 00:00:00 2001 From: Yuwei Date: Mon, 5 Jun 2017 14:38:21 -0700 Subject: [PATCH 2/6] add testPredictionMultipleCategories --- .../nupic/algorithms/sdr_classifier_test.py | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/tests/unit/nupic/algorithms/sdr_classifier_test.py b/tests/unit/nupic/algorithms/sdr_classifier_test.py index f6eee3dbad..089daa3609 100644 --- a/tests/unit/nupic/algorithms/sdr_classifier_test.py +++ b/tests/unit/nupic/algorithms/sdr_classifier_test.py @@ -520,15 +520,17 @@ def testPredictionDistribution(self): recordNum += 1 result1 = c.compute( - recordNum=2, patternNZ=SDR1, classification=None, + recordNum=recordNum, patternNZ=SDR1, classification=None, learn=False, infer=True) + recordNum += 1 self.assertAlmostEqual(result1[0][0], 0.3, places=1) self.assertAlmostEqual(result1[0][1], 0.3, places=1) self.assertAlmostEqual(result1[0][2], 0.4, places=1) result2 = c.compute( - recordNum=2, patternNZ=SDR2, classification=None, + recordNum=recordNum, patternNZ=SDR2, classification=None, learn=False, infer=True) + recordNum += 1 self.assertAlmostEqual(result2[0][1], 0.5, places=1) self.assertAlmostEqual(result2[0][3], 0.5, places=1) @@ -582,19 +584,64 @@ def testPredictionDistributionOverlap(self): recordNum += 1 result1 = c.compute( - recordNum=2, patternNZ=SDR1, classification=None, + recordNum=recordNum, patternNZ=SDR1, classification=None, learn=False, infer=True) + recordNum += 1 self.assertAlmostEqual(result1[0][0], 0.3, places=1) self.assertAlmostEqual(result1[0][1], 0.3, places=1) self.assertAlmostEqual(result1[0][2], 0.4, places=1) result2 = c.compute( - recordNum=2, patternNZ=SDR2, classification=None, + recordNum=recordNum, patternNZ=SDR2, classification=None, learn=False, infer=True) + recordNum += 1 self.assertAlmostEqual(result2[0][1], 0.5, places=1) self.assertAlmostEqual(result2[0][3], 0.5, places=1) + def testPredictionMultipleCategories(self): + """ Test the distribution of predictions. + + Here, we intend the classifier to learn the associations: + [1,3,5] => bucketIdx 0 & 1 + [2,4,6] => bucketIdx 2 & 3 + + The classifier should get the distribution almost right given enough + repetitions and a small learning rate + """ + + c = self._classifier([0], 0.001, 0.1, 0) + + SDR1 = [1, 3, 5] + SDR2 = [2, 4, 6] + recordNum = 0 + random.seed(42) + for _ in xrange(5000): + c.compute(recordNum=recordNum, patternNZ=SDR1, + classification={"bucketIdx": [0, 1], "actValue": [0, 1]}, + learn=True, infer=False) + recordNum += 1 + + c.compute(recordNum=recordNum, patternNZ=SDR2, + classification={"bucketIdx": [2, 3], "actValue": [2, 3]}, + learn=True, infer=False) + recordNum += 1 + + result1 = c.compute( + recordNum=recordNum, patternNZ=SDR1, classification=None, + learn=False, infer=True) + recordNum += 1 + self.assertAlmostEqual(result1[0][0], 0.5, places=1) + self.assertAlmostEqual(result1[0][1], 0.5, places=1) + + result2 = c.compute( + recordNum=recordNum, patternNZ=SDR2, classification=None, + learn=False, infer=True) + recordNum += 1 + self.assertAlmostEqual(result2[0][2], 0.5, places=1) + self.assertAlmostEqual(result2[0][3], 0.5, places=1) + + def testPredictionDistributionContinuousLearning(self): """ Test continuous learning @@ -648,17 +695,19 @@ def testPredictionDistributionContinuousLearning(self): recordNum += 1 result1 = c.compute( - recordNum=2, patternNZ=SDR1, + recordNum=recordNum, patternNZ=SDR1, classification={"bucketIdx": 0, "actValue": 0}, learn=False, infer=True) + recordNum += 1 self.assertAlmostEqual(result1[0][0], 0.3, places=1) self.assertAlmostEqual(result1[0][1], 0.3, places=1) self.assertAlmostEqual(result1[0][2], 0.4, places=1) result2 = c.compute( - recordNum=2, patternNZ=SDR2, + recordNum=recordNum, patternNZ=SDR2, classification={"bucketIdx": 0, "actValue": 0}, learn=False, infer=True) + recordNum += 1 self.assertAlmostEqual(result2[0][1], 0.5, places=1) self.assertAlmostEqual(result2[0][3], 0.5, places=1) @@ -676,15 +725,17 @@ def testPredictionDistributionContinuousLearning(self): recordNum += 1 result1new = c.compute( - recordNum=2, patternNZ=SDR1, classification=None, + recordNum=recordNum, patternNZ=SDR1, classification=None, learn=False, infer=True) + recordNum += 1 self.assertAlmostEqual(result1new[0][0], 0.3, places=1) self.assertAlmostEqual(result1new[0][1], 0.3, places=1) self.assertAlmostEqual(result1new[0][3], 0.4, places=1) result2new = c.compute( - recordNum=2, patternNZ=SDR2, classification=None, + recordNum=recordNum, patternNZ=SDR2, classification=None, learn=False, infer=True) + recordNum += 1 self.assertSequenceEqual(list(result2[0]), list(result2new[0])) From 1a87b2a13640d7002c27249e6737c55729a196a1 Mon Sep 17 00:00:00 2001 From: Yuwei Date: Mon, 5 Jun 2017 15:00:27 -0700 Subject: [PATCH 3/6] call sdr classifier once in SDRClassifierRegion --- src/nupic/regions/sdr_classifier_region.py | 75 +++++++++------------- 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/src/nupic/regions/sdr_classifier_region.py b/src/nupic/regions/sdr_classifier_region.py index ce2c531c2b..41309a0beb 100755 --- a/src/nupic/regions/sdr_classifier_region.py +++ b/src/nupic/regions/sdr_classifier_region.py @@ -364,47 +364,28 @@ def compute(self, inputs, outputs): # when network.run() is called self._computeFlag = True - # An input can potentially belong to multiple categories. - # If a category value is < 0, it means that the input does not belong to - # that category. - categories = [category for category in inputs["categoryIn"] - if category >= 0] - patternNZ = inputs["bottomUpIn"].nonzero()[0] - # ========================================================================== - # Allow to train on multiple input categories. - # Do inference first, and then train on all input categories. - - # -------------------------------------------------------------------------- - # 1. Call classifier. Don't train. Just inference. Train after. - - # Use Dummy classification input, because this param is required even for - # inference mode. Because learning is off, the classifier is not learning - # this dummy input. Inference only here. - classificationIn = {"actValue": 0, "bucketIdx": 0} - clResults = self._sdrClassifier.compute(recordNum=self.recordNum, - patternNZ=patternNZ, - classification=classificationIn, - learn=False, - infer=self.inferenceMode) - - # ------------------------------------------------------------------------ - # 2. Train classifier, no inference if self.learningMode: - for category in categories: - classificationIn = {"bucketIdx": int(category), - "actValue": int(category)} - - self._sdrClassifier.compute(recordNum=self.recordNum, - patternNZ=patternNZ, - classification=classificationIn, - learn=self.learningMode, - infer=False) - - # If the input does not belong to a category, i.e. len(categories) == 0, - # then look for bucketIdx and actValueIn. - if len(categories) == 0: + # An input can potentially belong to multiple categories. + # If a category value is < 0, it means that the input does not belong to + # that category. + categories = [category for category in inputs["categoryIn"] + if category >= 0] + + if len(categories) > 0: + # Allow to train on multiple input categories. + bucketIdxList = [] + actValueList = [] + for category in categories: + bucketIdxList.append(int(category)) + actValueList.append(int(category)) + + classificationIn = {"bucketIdx": bucketIdxList, + "actValue": actValueList} + else: + # If the input does not belong to a category, i.e. len(categories) == 0, + # then look for bucketIdx and actValueIn. if "bucketIdxIn" not in inputs: raise KeyError("Network link missing: bucketIdxOut -> bucketIdxIn") if "actValueIn" not in inputs: @@ -412,11 +393,19 @@ def compute(self, inputs, outputs): classificationIn = {"bucketIdx": int(inputs["bucketIdxIn"]), "actValue": float(inputs["actValueIn"])} - self._sdrClassifier.compute(recordNum=self.recordNum, - patternNZ=patternNZ, - classification=classificationIn, - learn=self.learningMode, - infer=False) + else: + # Use Dummy classification input, because this param is required even for + # inference mode. Because learning is off, the classifier is not learning + # this dummy input. Inference only here. + classificationIn = {"actValue": 0, "bucketIdx": 0} + + # Perform inference if self.inferenceMode is True + # Train classifier if self.learningMode is True + clResults = self._sdrClassifier.compute(recordNum=self.recordNum, + patternNZ=patternNZ, + classification=classificationIn, + learn=self.learningMode, + infer=self.inferenceMode) # fill outputs with clResults if clResults is not None and len(clResults) > 0: From 6370f5a5aeca695d4c6927901ad7db2dc5db31d5 Mon Sep 17 00:00:00 2001 From: Yuwei Date: Mon, 5 Jun 2017 16:31:13 -0700 Subject: [PATCH 4/6] add hello world sequence prediction test for sdr classifier --- src/nupic/algorithms/sdr_classifier.py | 32 +++--- .../single_step_sdr_classifier_test.py | 100 +++++++++++++++++- 2 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/nupic/algorithms/sdr_classifier.py b/src/nupic/algorithms/sdr_classifier.py index 6eece31ec6..60f1c6ca7b 100644 --- a/src/nupic/algorithms/sdr_classifier.py +++ b/src/nupic/algorithms/sdr_classifier.py @@ -174,8 +174,8 @@ def compute(self, recordNum, patternNZ, classification, learn, infer): :param classification: Dict of the classification information where: - - bucketIdx: index of the encoder bucket - - actValue: actual value going into the encoder + - bucketIdx: list of indices of the encoder bucket + - actValue: list of actual values going into the encoder Classification could be None for inference mode. :param learn: (bool) if true, learn this sample @@ -229,25 +229,25 @@ def compute(self, recordNum, patternNZ, classification, learn, infer): self._maxBucketIdx+1))), axis=0) self._maxInputIdx = int(newMaxInputIdx) + # Get classification info + if type(classification["bucketIdx"]) is not list: + bucketIdxList = [classification["bucketIdx"]] + actValueList = [classification["actValue"]] + numCategory = 1 + else: + bucketIdxList = classification["bucketIdx"] + actValueList = classification["actValue"] + numCategory = len(classification["bucketIdx"]) + # ------------------------------------------------------------------------ # Inference: # For each active bit in the activationPattern, get the classification # votes if infer: - retval = self.infer(patternNZ, classification) + retval = self.infer(patternNZ, actValueList) if learn and classification["bucketIdx"] is not None: - # Get classification info - if type(classification["bucketIdx"]) is not list: - bucketIdxList = [classification["bucketIdx"]] - actValueList = [classification["actValue"]] - numCategory = 1 - else: - bucketIdxList = classification["bucketIdx"] - actValueList = classification["actValue"] - numCategory = len(classification["bucketIdx"]) - for categoryI in range(numCategory): bucketIdx = bucketIdxList[categoryI] actValue = actValueList[categoryI] @@ -306,7 +306,7 @@ def compute(self, recordNum, patternNZ, classification, learn, infer): - def infer(self, patternNZ, classification): + def infer(self, patternNZ, actValueList): """ Return the inference value from one input sample. The actual learning happens in compute(). @@ -336,10 +336,10 @@ def infer(self, patternNZ, classification): # NOTE: If doing 0-step prediction, we shouldn't use any knowledge # of the classification input during inference. - if self.steps[0] == 0 or classification is None: + if self.steps[0] == 0: defaultValue = 0 else: - defaultValue = classification["actValue"] + defaultValue = actValueList[0] actValues = [x if x is not None else defaultValue for x in self._actualValues] retval = {"actualValues": actValues} diff --git a/tests/integration/nupic/regions/single_step_sdr_classifier_test.py b/tests/integration/nupic/regions/single_step_sdr_classifier_test.py index 745c985a1c..6d0dd97a23 100644 --- a/tests/integration/nupic/regions/single_step_sdr_classifier_test.py +++ b/tests/integration/nupic/regions/single_step_sdr_classifier_test.py @@ -19,15 +19,18 @@ # http://numenta.org/licenses/ # ---------------------------------------------------------------------- +from operator import itemgetter import os import tempfile import unittest +import numpy as np + from datetime import datetime from nupic.data.file_record_stream import FileRecordStream from nupic.encoders import MultiEncoder, ScalarEncoder from nupic.engine import Network - +from nupic.frameworks.opf.model_factory import ModelFactory def _getTempFileName(): @@ -128,7 +131,7 @@ def testSimpleMulticlassNetworkPY(self): dataSource.close() os.remove(filename) - + @unittest.skip("Skip test until we updated SDR classifier in nupic.core") def testSimpleMulticlassNetworkCPP(self): # Setup data record stream of fake data (with three categories) filename = _getTempFileName() @@ -204,7 +207,7 @@ def testSimpleMulticlassNetworkCPP(self): net.run(1) inferredCats = classifier.getOutputData("categoriesOut") self.assertSequenceEqual(expectedCats[i], inferredCats.tolist(), - "Classififer did not infer expected category " + "Classifier did not infer expected category " "for record number {}.".format(i)) # Close data stream, delete file. @@ -212,6 +215,97 @@ def testSimpleMulticlassNetworkCPP(self): os.remove(filename) + def testHelloWorldPrediction(self): + text = 'hello world.' + categories = list("abcdefghijklmnopqrstuvwxyz 1234567890.") + colsPerChar = 11 + numColumns = (len(categories) + 1) * colsPerChar + + MODEL_PARAMS = { + "model": "HTMPrediction", + "version": 1, + "predictAheadTime": None, + "modelParams": { + "inferenceType": "TemporalMultiStep", + "sensorParams": { + "verbosity": 0, + "encoders": { + "token": { + "fieldname": u"token", + "name": u"token", + "type": "CategoryEncoder", + "categoryList": categories, + "w": colsPerChar, + "forced": True, + } + }, + "sensorAutoReset": None, + }, + "spEnable": False, + "spParams": { + "spVerbosity": 0, + "globalInhibition": 1, + "columnCount": 2048, + "inputWidth": 0, + "numActiveColumnsPerInhArea": 40, + "seed": 1956, + "columnDimensions": 0.5, + "synPermConnected": 0.1, + "synPermActiveInc": 0.1, + "synPermInactiveDec": 0.01, + "boostStrength": 0.0, + }, + + "tmEnable": True, + "tmParams": { + "verbosity": 0, + "columnCount": numColumns, + "cellsPerColumn": 16, + "inputWidth": numColumns, + "seed": 1960, + "temporalImp": "tm_cpp", + "newSynapseCount": 6, + "maxSynapsesPerSegment": 11, + "maxSegmentsPerCell": 32, + "initialPerm": 0.21, + "permanenceInc": 0.1, + "permanenceDec": 0.05, + "globalDecay": 0.0, + "maxAge": 0, + "minThreshold": 3, + "activationThreshold": 5, + "outputType": "normal", + }, + "clParams": { + "implementation": "py", + "regionName": "SDRClassifierRegion", + "verbosity": 0, + "alpha": 0.1, + "steps": "1", + }, + "trainSPNetOnlyIfRequested": False, + }, + } + + model = ModelFactory.create(MODEL_PARAMS) + model.enableInference({"predictedField": "token"}) + model.enableLearning() + + # train + prediction = None + for rpt in xrange(20): + for token in text: + if prediction is not None: + if rpt > 15: + self.assertEqual(prediction, token) + modelInput = {"token": token} + result = model.run(modelInput) + prediction = sorted(result.inferences["multiStepPredictions"][1].items(), + key=itemgetter(1), reverse=True)[0][0] + model.resetSequenceStates() + prediction = None + + if __name__ == "__main__": unittest.main() From e23eda7ec9876d6d4d28bc92e03a5216ee573935 Mon Sep 17 00:00:00 2001 From: Yuwei Date: Tue, 6 Jun 2017 09:59:29 -0700 Subject: [PATCH 5/6] fix broken sdr classifier unit tests --- src/nupic/algorithms/sdr_classifier.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/nupic/algorithms/sdr_classifier.py b/src/nupic/algorithms/sdr_classifier.py index 60f1c6ca7b..fe5f4aa0c4 100644 --- a/src/nupic/algorithms/sdr_classifier.py +++ b/src/nupic/algorithms/sdr_classifier.py @@ -230,15 +230,20 @@ def compute(self, recordNum, patternNZ, classification, learn, infer): self._maxInputIdx = int(newMaxInputIdx) # Get classification info - if type(classification["bucketIdx"]) is not list: - bucketIdxList = [classification["bucketIdx"]] - actValueList = [classification["actValue"]] - numCategory = 1 + if classification is not None: + if type(classification["bucketIdx"]) is not list: + bucketIdxList = [classification["bucketIdx"]] + actValueList = [classification["actValue"]] + numCategory = 1 + else: + bucketIdxList = classification["bucketIdx"] + actValueList = classification["actValue"] + numCategory = len(classification["bucketIdx"]) else: - bucketIdxList = classification["bucketIdx"] - actValueList = classification["actValue"] - numCategory = len(classification["bucketIdx"]) - + if learn: + raise ValueError("classification cannot be None when learn=True") + actValueList = [0] + bucketIdxList = [0] # ------------------------------------------------------------------------ # Inference: # For each active bit in the activationPattern, get the classification From 66b49418ca35db5521379d211a4c4f1f0d555ef7 Mon Sep 17 00:00:00 2001 From: Yuwei Date: Tue, 6 Jun 2017 10:00:54 -0700 Subject: [PATCH 6/6] set actValueList to None when classification is None --- src/nupic/algorithms/sdr_classifier.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nupic/algorithms/sdr_classifier.py b/src/nupic/algorithms/sdr_classifier.py index fe5f4aa0c4..7116721fae 100644 --- a/src/nupic/algorithms/sdr_classifier.py +++ b/src/nupic/algorithms/sdr_classifier.py @@ -242,8 +242,8 @@ def compute(self, recordNum, patternNZ, classification, learn, infer): else: if learn: raise ValueError("classification cannot be None when learn=True") - actValueList = [0] - bucketIdxList = [0] + actValueList = None + bucketIdxList = None # ------------------------------------------------------------------------ # Inference: # For each active bit in the activationPattern, get the classification @@ -341,7 +341,7 @@ def infer(self, patternNZ, actValueList): # NOTE: If doing 0-step prediction, we shouldn't use any knowledge # of the classification input during inference. - if self.steps[0] == 0: + if self.steps[0] == 0 or actValueList is None: defaultValue = 0 else: defaultValue = actValueList[0]