├── .travis.yml ├── Makefile ├── README.md ├── lib ├── functional-helper.js ├── index.js ├── neuro-layer.js ├── simple-map.js └── temporal-pooler.js ├── package.json └── test ├── index.js ├── test-functional-helper.js ├── test-simple-map.js └── test-temporal-pooler.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | 6 | install: "npm install" 7 | 8 | script: npm test -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./node_modules/.bin/mocha --reporter spec 3 | # ./node_modules/.bin/mocha --reporter list 4 | 5 | 6 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cortical-learning-algorithm-js 2 | ============================== 3 | 4 | [![Build Status](https://travis-ci.org/mirkoklukas/cortical-learning-algorithm-js.svg?branch=master)](https://travis-ci.org/mirkoklukas/cortical-learning-algorithm-js) 5 | 6 | Towards an JS-implementation of hierarchical temporal memory and the cortical learning algorithm by Jeff Hawkins and Dileep George of [Numenta, Inc.](http://numenta.com/) 7 | 8 | #Legal Note 9 | This code is licensed under the [MIT license](https://opensource.org/licenses/mit-license.php), with one caveat. Numenta owns patents on specific items. While this code was written without using any of Numenta's code, it is possible that those patent laws still apply. Before using this code **commercially**, it is recommended to seek legal advice. 10 | -------------------------------------------------------------------------------- /lib/functional-helper.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Need some helper to write functional code? 4 | * @module functional-helper 5 | */ 6 | 7 | /** 8 | * Identity function. 9 | * @function 10 | * @param {Anything} x 11 | * @return {Anything} - x 12 | */ 13 | var identity = function (x) { return x; }; 14 | 15 | /** 16 | * Partial function application. 17 | * @param {function} f - A function which takes several arguments. 18 | * @param {...object} args - Placeholder several, n say, arguments. 19 | * @return {function} - Returns the given function with the first n arguments fixed. 20 | */ 21 | var partial = function (f, _) { 22 | var fixedArgs = Array.prototype.slice.call(arguments,1); 23 | return function (_) { 24 | var args = Array.prototype.slice.call(arguments); 25 | return f.apply(this, fixedArgs.concat(args)); 26 | }; 27 | }; 28 | 29 | /** 30 | * Haskells insertWith, with permuted signatures. 31 | * @function 32 | */ 33 | var insertWith = function (mutator, initialValue, map, key) { 34 | var newValue = map.has(key) ? mutator(map.get(key), initialValue) : initialValue; 35 | map.set(key, newValue); 36 | return map 37 | }; 38 | 39 | /** 40 | * Adds insertWith to an objects properties. 41 | * @function 42 | */ 43 | var makeInserWithable = function (obj) { 44 | if(obj.hasOwnProperty("insertWith")) throw "Overwriting adjust method."; 45 | obj.insertWith = function (mutator, initialValue, key) { 46 | return insertWith(mutator, initialValue, obj, key); 47 | }; 48 | }; 49 | 50 | /** 51 | * Should behave like insertWith with fixed mutator and initial value. 52 | * Can be fed to Array.reduce 53 | * @function 54 | */ 55 | var adjuster = function (mutator, initialValue) { 56 | return partial(insertWith, mutator, initialValue); 57 | }; 58 | 59 | /** 60 | * Produces a compare function that can be fed to Array.sort. 61 | * @function 62 | * @param {function} f - A function {obj} a -> {Number} f(a). 63 | * @return {function} - A function (a,b) => f(a) - f(b). 64 | */ 65 | var byValuesOf = function (f) { 66 | return function (a, b) { 67 | return f(a) - f(b); 68 | }; 69 | }; 70 | 71 | /** 72 | * Getter for a certain property of an object. 73 | * @function 74 | * @param {string} name - The name of a property. 75 | * @return {function} - A function that given an object returns obj[name]. 76 | */ 77 | var getProp = function (name) { 78 | return function (obj) { 79 | return obj[name]; 80 | }; 81 | }; 82 | 83 | /** 84 | * Gets the first element of an array. 85 | * @function 86 | */ 87 | var fst = getProp(0); 88 | 89 | /** 90 | * Gets the second element of an array. 91 | * @function 92 | */ 93 | var snd = getProp(1); 94 | 95 | /** 96 | * Concatenates two arrays. 97 | * @function 98 | */ 99 | var concat = function (a, b) { 100 | return a.concat(b); 101 | }; 102 | 103 | /** 104 | * Function composition. 105 | * @function 106 | */ 107 | var compose= function (f, g) { 108 | return function () { 109 | var args = Array.prototype.slice.call(arguments); 110 | return f.call(this, g.apply(this, args)); 111 | }; 112 | }; 113 | 114 | /** 115 | * Dettaches a mehtod from an object and binds its context to the object. 116 | * @function 117 | * @param {string} name - The name of a object-property behing which we find a function. 118 | * @return {function} - A function whose context is bound to the given object. 119 | */ 120 | var dettachMethodFromObject = function (name, object) { 121 | return function (_) { 122 | var args = Array.prototype.slice.call(arguments); 123 | return object[name].apply(object, args); 124 | }; 125 | }; 126 | 127 | /** 128 | * @function 129 | */ 130 | var ifThenElse = function (f, g, h) { 131 | return function (x) { 132 | return f(x) ? g(x) : h(x); 133 | }; 134 | }; 135 | 136 | /** 137 | * @function 138 | */ 139 | var result = function (x) { 140 | return function () { 141 | return x; 142 | }; 143 | }; 144 | 145 | 146 | /** 147 | * Addition. 148 | * @function 149 | * @param {Number} a - A number. 150 | * @param {Number} b - A number. 151 | * @return {Number} - Returns the sum of the arguments. 152 | */ 153 | var add = function (a, b) { return a + b;}; 154 | 155 | /** 156 | * Add a certain value. 157 | * @function 158 | * @param {Number} n - A number. 159 | * @return {function} - A function that adds n to what ever argument is fed to it. 160 | */ 161 | var addN = function (n) { return partial(add, n); }; 162 | 163 | 164 | 165 | /** 166 | * Produces a function that collects results in an array. 167 | * @function 168 | * @param {function} f - Any function. 169 | * @param {function} g - Any function. 170 | * @return {function} - A function (...args) => [f(...args), g(...args)]. 171 | */ 172 | var combine = function (f, g) { 173 | return function (_) { 174 | return [f.apply(this, arguments), g.apply(this, arguments)]; 175 | }; 176 | }; 177 | 178 | /** 179 | * Finds a points of the given list for which the given function attains its maximum value. 180 | * @function 181 | * @param {Object[]} list - A list of arguments. 182 | * @param {function} mutator - A function. 183 | * @return {Object} - An object for which the given function attains its maximum value. 184 | */ 185 | var argmax = function (list, mutator) { 186 | 187 | var i = list.length - 2; 188 | var max = list.length - 1; 189 | if (mutator === undefined) { 190 | for (; i >= 0; i--) { 191 | if(list[i] >= list[max]) max = i; 192 | } 193 | } else { 194 | for (; i >= 0; i--) { 195 | if(mutator(list[i]) >= mutator(list[max])) max = i; 196 | } 197 | } 198 | return list[max]; 199 | }; 200 | 201 | /** 202 | * Picks a random element from a list. 203 | * @function 204 | * @param {Object[]} list - A list of elements. 205 | * @return {Object} - A randomly picked element from the list. 206 | */ 207 | var pickRandom = function (list) { 208 | return list[Math.floor(Math.random()*(list.length-1))]; 209 | }; 210 | 211 | /** 212 | * ... 213 | * @function 214 | * @param {} map - 215 | * @return {} - . 216 | */ 217 | var wrapGetter= function (map, alt) { 218 | var alt = alt === undefined ? 0 : alt; 219 | return function (key) { 220 | return map.has(key) ? map.get(key) : alt; 221 | }; 222 | }; 223 | 224 | var geq = function (n) { 225 | return function (x) { 226 | return x >= n; 227 | } 228 | }; 229 | 230 | var eq = function (n) { 231 | return function (x) { 232 | return x == n; 233 | }; 234 | }; 235 | 236 | 237 | 238 | 239 | // var adjuster = function (mutator, initialValue) { 240 | // return function (map, key) { 241 | // var newValue = map.has(key) ? mutator(map.get(key)) : initialValue; 242 | // map.set(key, newValue); 243 | // return map; 244 | // }; 245 | // }; 246 | 247 | 248 | var Decision = function (condition, mutate, yes, no ) { 249 | var mutate = mutate || function (_) { 250 | return [].slice.call(arguments); 251 | }; 252 | 253 | return function (x) { 254 | if (condition === null) return mutate(x); 255 | 256 | if(condition(x)) 257 | return yes !== undefined ? yes(mutate(x)) : mutate(x); 258 | else 259 | return no !== undefined ? no(mutate(x)) : mutate(x); 260 | }; 261 | }; 262 | 263 | 264 | 265 | /** 266 | * Exports a bunch of helper functions. 267 | */ 268 | module.exports = { 269 | 'partial': partial, 270 | 'add': add, 271 | 'addN': addN, 272 | 'insertWith': insertWith, 273 | 'adjuster': adjuster, 274 | 'argmax': argmax, 275 | 'wrapGetter': wrapGetter, 276 | 'pickRandom': pickRandom, 277 | 'getProp': getProp, 278 | 'prop': getProp, 279 | 'byValuesOf': byValuesOf, 280 | 'fst': fst, 281 | 'snd': snd, 282 | 'geq': geq, 283 | 'eq': eq, 284 | 'concat': concat, 285 | 'compose': compose, 286 | 'combine': combine, 287 | 'Decision': Decision, 288 | 'identity': identity, 289 | 'result': result 290 | }; 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The Cortical-learning-algorithm module. 4 | * @module cla 5 | */ 6 | 7 | 8 | /** The {@link TemporalPooler} class. 9 | * @type {TemporalPooler} 10 | */ 11 | exports.TemporalPooler = require("./temporal-pooler.js") 12 | 13 | -------------------------------------------------------------------------------- /lib/neuro-layer.js: -------------------------------------------------------------------------------- 1 | 2 | // Import stuff. 3 | var fh = require("./functional-helper.js"); 4 | var identity = fh.identity; 5 | var concat = fh.concat; 6 | var result = fh.result; 7 | var combine = fh.combine; 8 | var zip = require('underscore').zip; 9 | 10 | /** 11 | * A neuronal layer, i.e. a network of cells synapses etc. 12 | * @constructor 13 | * @class 14 | */ 15 | var NeuroLayer = function (config) { 16 | var columns = config? config.columns : {}; 17 | var cells = config? config.cells : {}; 18 | var segments = config? config.segments : {}; 19 | 20 | /** 21 | * Initializes a layer. 22 | * @function 23 | * @param {Integer} nCols - The number of columns 24 | * @param {Integer} nCells - The number of cells per column 25 | * @param {Integer} nSegs - The number of segments per cell 26 | * @param {Integer} nSyns - The number of listening cells per segment 27 | * @return {NeuroLayer} - A self reference 28 | */ 29 | this.randomInit = function (nCols, nCells, nSegs, nSyns) { 30 | var i = 0; 31 | var j = 0; 32 | for (; i< nCols; i++ ) { 33 | this.addColumn(i); 34 | } 35 | 36 | i=0; 37 | for (; i< nCols*nCells; i++) { 38 | this.addCell(i, Math.floor(i/nCells)); 39 | } 40 | 41 | i=0; 42 | var syns = []; 43 | for (; i< nCols*nCells*nSegs; i++) { 44 | syns = []; 45 | for (j=0; j cell::"+ head[1] + " in col::" + head[2] ; 342 | 343 | return line 344 | }).join('\n'); 345 | 346 | return output; 347 | }; 348 | 349 | }; 350 | 351 | 352 | /** 353 | * Exports {@link NeuroLayer} class. 354 | */ 355 | module.exports = NeuroLayer; 356 | 357 | 358 | 359 | -------------------------------------------------------------------------------- /lib/simple-map.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * A naive implementation of a Map. 4 | * @constructor 5 | * @class 6 | */ 7 | var SimpleMap = function (keyVals) { 8 | 9 | var hash = {}; 10 | var data = []; 11 | 12 | // Initializing 13 | (keyVals || []).map(function (pair, i) { 14 | var key = pair[0]; 15 | var val = pair[1]; 16 | 17 | hash[key] = { 18 | "value": val, 19 | "index": data.length 20 | }; 21 | data.push([key, val]); 22 | }) 23 | 24 | /** @function */ 25 | this.set = function (key, value) { 26 | if (key in hash) { 27 | hash[key].value = value; 28 | data[hash[key].index][1] = value; 29 | } else { 30 | hash[key] = { 31 | "value": value, 32 | "index": data.length 33 | }; 34 | data.push([key, value]); 35 | } 36 | return this; 37 | }; 38 | 39 | /** @function */ 40 | this.get = function (key) { 41 | return this.has(key) ? hash[key].value : undefined; 42 | }; 43 | 44 | /** @function */ 45 | this.entries = function () { 46 | return data; 47 | }; 48 | 49 | /** @function */ 50 | this.has = function (key) { 51 | return key in hash; 52 | }; 53 | 54 | /** @function */ 55 | this.toList = function () { 56 | return data; 57 | }; 58 | 59 | /** @function */ 60 | this.toDict = function () { 61 | return hash; 62 | }; 63 | 64 | }; 65 | 66 | /** 67 | * Exports {@link SimpleMap} class. 68 | */ 69 | module.exports = SimpleMap; 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /lib/temporal-pooler.js: -------------------------------------------------------------------------------- 1 | 2 | // Import stuff. 3 | var Map = require("./simple-map.js"); 4 | var NeuroLayer = require("./neuro-layer.js"); 5 | var fh = require("./functional-helper.js"); 6 | var _ = require("underscore"); 7 | var partial = fh.partial; 8 | var add = fh.add; 9 | var addN =fh.addN; 10 | var adjuster = fh.adjuster; 11 | var getProp = fh.getProp; 12 | var byValuesOf = fh.byValuesOf; 13 | var pickRandom = fh.pickRandom; 14 | var fst = fh.fst; 15 | var snd = fh.snd; 16 | var geq = fh.geq; 17 | var eq = fh.eq; 18 | var argmax = fh.argmax; 19 | var result = fh.result; 20 | var wrapGetter = fh.wrapGetter; 21 | var concat = fh.concat; 22 | var compose = fh.compose; 23 | var logger = require('winston'); 24 | logger.setLevels({debug:0,info: 1,silly:2,warn: 3,error:4, flow: 5}); 25 | logger.addColors({debug: 'green',info: 'cyan',silly: 'magenta',warn: 'yellow',error: 'red', flow: 'cyan'}); 26 | logger.remove(logger.transports.Console); 27 | logger.add(logger.transports.Console, { level: 'info', colorize:true }); 28 | 29 | /** 30 | * The temporal pooler. 31 | * @constructor 32 | * @class 33 | */ 34 | var TemporalPooler = function (layer) { 35 | this.layer = layer; 36 | this.activationThreshold; 37 | this.scores = { 38 | 'active': [], 39 | 'learning': [], 40 | 'sequential': [] 41 | }; 42 | this.inputs = []; 43 | this.predictions = []; 44 | this.sequentialPredictions = []; 45 | this.activity = []; 46 | this.outputs = []; 47 | }; 48 | 49 | /** 50 | * Sets the neuronal layer of the pooler. 51 | * @function 52 | * @param {NeuroLayer} layer - The actual network. 53 | * @return {TemporalPooler} - A self reference. 54 | */ 55 | TemporalPooler.prototype.setNeuroLayer = function (layer) { 56 | this.layer = layer; 57 | return this; 58 | }; 59 | 60 | /** 61 | * Sets the activation threshold of the pooler. 62 | * @function 63 | * @param {Number} a - The new activation threshold. 64 | * @return {TemporalPooler} - A self reference. 65 | */ 66 | TemporalPooler.prototype.setActivationThreshold = function (a) { 67 | this.activationThreshold = a; 68 | return this; 69 | }; 70 | 71 | /** 72 | * Resets the pooler. 73 | * @function 74 | */ 75 | TemporalPooler.prototype.reset = function () { 76 | this.scores = { 77 | 'active': [], 78 | 'learning': [], 79 | 'sequential': [] 80 | }; 81 | this.inputs = []; 82 | this.predictions = []; 83 | this.activity = []; 84 | 85 | return this 86 | }; 87 | 88 | /** 89 | * Processes a given input. 90 | * @function 91 | * @param {Integer[]} input - A list of active column ids. 92 | * @return {Integer[]} - A list of active column ids. 93 | */ 94 | TemporalPooler.prototype.processInput = function (input) { 95 | logger.debug('process input', input); 96 | var lastPrediction = this.predictions[this.predictions.length -1] || new Map(); 97 | 98 | // Activity 99 | var activeCells = this.computeActiveCells(this.layer, lastPrediction, input); 100 | var activeLearningCells = activeCells.filter(this.layer.isLearningCell); 101 | 102 | // Scores 103 | var activeScore = this.computeSegmentScores(this.layer, activeCells); 104 | var learningScore = this.computeSegmentScores(this.layer, activeLearningCells); 105 | var sequentialScore = new Map(learningScore.toList().filter( 106 | compose(this.layer.isSequential, fst))); 107 | 108 | // Prediction 109 | var nextPrediction = this.computePredictionMap(this.layer, this.activationThreshold, sequentialScore); 110 | 111 | // Result 112 | var output = this.computeOutput(input, nextPrediction); 113 | 114 | // Update the history 115 | this.inputs.push(input); 116 | this.activity.push(activeCells); 117 | this.scores.active.push(activeScore); 118 | this.scores.learning.push(learningScore); 119 | this.scores.sequential.push(sequentialScore); 120 | this.predictions.push(nextPrediction); 121 | this.outputs.push(output); 122 | 123 | return output; 124 | }; 125 | 126 | /** 127 | * Computes active cells given active columns and a prediction map. 128 | * @function 129 | * @param {NeuroLayer} layer - ... 130 | * @param {Map} prediction - A Map whose keys are ids of columns, 131 | * and values are ids of predicted cells. If a key is not present, 132 | * this implies that no cell has been predicted within the column. 133 | * @param {Integer[]} input - A list of ids of active columns. 134 | * @return {Integer[]} - A list of ids of active cells. 135 | * 136 | * @todo 'concat' operation on arrays probably has O(n) runtime, so one 137 | * should define a purely functional version to get to O(1) 138 | */ 139 | TemporalPooler.prototype.computeActiveCells = function (layer, prediction, input) { 140 | var activeCells = input. 141 | map(function (col) { 142 | return prediction.has(col) ? prediction.get(col) : layer.getCells(col); 143 | }). 144 | reduce(concat, []); 145 | logger.debug("Active cells:", activeCells) 146 | return activeCells; 147 | }; 148 | 149 | /** 150 | * Computes the scores for each segments based on a list of active cells. 151 | * @function 152 | * @param {NeuroLayer} layer - ... 153 | * @param {Integer[]} activeCells - A list of active cells. 154 | * @return {Map} - A Map whose entries are ids of segments with scores > 0, 155 | * the values are the actual scores. 156 | */ 157 | TemporalPooler.prototype.computeSegmentScores = function (layer, activeCells) { 158 | var scores = activeCells. 159 | map(layer.getListeningSegments). 160 | reduce(concat, []). 161 | reduce(adjuster(addN(1), 1), new Map()); 162 | logger.debug("Scores:", scores.toList()) 163 | return scores; 164 | }; 165 | 166 | /** 167 | * Computes the predictive states for each cell. 168 | * @function 169 | * @param {NeuroLayer} layer - ... 170 | * @param {Number} threshold - The threshold a segment score has to exceed to trigger 171 | * the predictive state of its listening cell. 172 | * @param {Map} segmentScores - A Map whose entries are ids of segments with scores > 0, 173 | * the values are the actual scores. 174 | * @return {Map} - A prediction Map. 175 | */ 176 | TemporalPooler.prototype.computePredictionMap = function (neuroLayer, threshold, segmentScores) { 177 | 178 | var prediction = segmentScores.toList(). 179 | filter(compose(geq(threshold), snd)). 180 | map(compose(neuroLayer.getListeningCell, fst)). 181 | reduce(function (map, cellId) { 182 | var key = neuroLayer.getColumn(cellId); 183 | var newValue = (map.get(key) || []).concat([cellId]); 184 | map.set(key, newValue); 185 | return map; 186 | }, new Map()); 187 | logger.debug("Prediction:", prediction.toList()) 188 | return prediction; 189 | }; 190 | 191 | /** 192 | * Returns the input that has not been predicted. 193 | * @function 194 | * @param {Integer[]} input - A list of ids of active columns. 195 | * @param {Map} prediction - A Map whose keys are ids of columns, 196 | * and values are ids of predicted cells. Predictions values must not be the empty list '[]'. 197 | * @return {Integer[]} - A list of ids of columns that have not been predicted. 198 | */ 199 | TemporalPooler.prototype.computeNonPredictedColumns = function (input, prediction) { 200 | return input.filter(function (column) { 201 | return !prediction.has(column); 202 | }); 203 | }; 204 | 205 | /** 206 | * Computes the output of a layer. 207 | * @function 208 | * @param {Integer[]} input - A list of ids of active columns. 209 | * @param {Map} prediction - A Map whose keys are ids of columns, 210 | * and values are ids of predicted cells. Predictions values must not be the empty list '[]'. 211 | * @return {Integer[]} - A list of ids of columns. 212 | */ 213 | TemporalPooler.prototype.computeOutput = function (input, predictionMap) { 214 | return this.computeNonPredictedColumns(input, predictionMap).concat(predictionMap.entries()); 215 | }; 216 | 217 | var PROMOTE_LISTENING_CELL = 1; 218 | var PROMOTE_SEGMENT= 2; 219 | var IMPROVE_SCORE= 3; 220 | var ADD_SEGMENT= 4; 221 | 222 | /** 223 | * Suggests learning updates for a column of the neuronal layer. 224 | * @function 225 | */ 226 | TemporalPooler.prototype.suggestLearningUpdates = function (col, t) { 227 | // @todo Check the time, i.e. is all necessary information present. 228 | var layer = this.layer; 229 | var scores = this.scores; 230 | var inputs = this.inputs; 231 | var a = this.activationThreshold; 232 | var t = t !== undefined ? t : Math.max(scores.active.length - 1, 0); 233 | var activeScore = wrapGetter(scores.active[t]); 234 | var learningScore = wrapGetter(scores.learning[t]); 235 | var updates = []; 236 | var cells = layer.getCells(col); 237 | var segments = cells. 238 | map(layer.getFeedingSegments). 239 | reduce(concat, []); 240 | 241 | // ========================== 242 | // Worst case: there is no segment at all 243 | // ========================== 244 | if(segments.length <= 0) { 245 | updates.push([ 246 | [ADD_SEGMENT], 247 | [pickRandom(cells)] 248 | ]); 249 | return updates; 250 | } 251 | 252 | // ========================== 253 | // First check for candidates with 254 | // respect to learning score ... 255 | // ========================== 256 | var candidates = segments. 257 | filter(compose(geq(a),learningScore)). 258 | filter(layer.isSequential); 259 | // @todo We don't have to promote those cells 260 | // who are already learning cells 261 | if (candidates.length > 0) { 262 | updates.push([ 263 | [PROMOTE_LISTENING_CELL], 264 | candidates 265 | ]); 266 | return updates; 267 | } 268 | 269 | // ========================== 270 | // Choose candidates with respect 271 | // to active score ... 272 | // ========================== 273 | var bestSeg = argmax(segments, activeScore); 274 | var maxScore = activeScore(bestSeg); 275 | if (maxScore <= 0) { 276 | updates.push([ 277 | [IMPROVE_SCORE, PROMOTE_LISTENING_CELL, PROMOTE_SEGMENT], 278 | [pickRandom(segments)] 279 | ]); 280 | } 281 | else { 282 | candidates = segments.filter(compose(eq(maxScore),activeScore)); 283 | seqCandidates = candidates.filter(layer.isSequential); 284 | 285 | if(seqCandidates.length > 0) { 286 | updates.push([ 287 | [IMPROVE_SCORE, PROMOTE_LISTENING_CELL, PROMOTE_SEGMENT], 288 | [pickRandom(seqCandidates)] 289 | ]); 290 | } 291 | else { 292 | updates.push([ 293 | [IMPROVE_SCORE, PROMOTE_LISTENING_CELL, PROMOTE_SEGMENT], 294 | [candidates[candidates.length-1]] 295 | ]) 296 | } 297 | } 298 | return updates; 299 | }; 300 | 301 | /** 302 | * Carries out the suggested learning updates. 303 | * @function 304 | * @todo Annotate 305 | */ 306 | TemporalPooler.prototype.updateNeuroLayer = function (suggestions) { 307 | var that = this; 308 | var carryOutUpdate = function (layer, type, candidates) { 309 | 310 | switch(type) { 311 | 312 | case PROMOTE_LISTENING_CELL: 313 | // Candidates should be a list of segment ids 314 | candidates.forEach(function (seg) { 315 | var listeningCell = layer.getListeningCell(seg); 316 | layer.setCellLearning(listeningCell, true); 317 | }); 318 | break 319 | 320 | case PROMOTE_SEGMENT: 321 | // Candidates should be a list of segment ids 322 | candidates.forEach(function (seg) { 323 | layer.setSegLearning(seg, true); 324 | }); 325 | break 326 | 327 | case IMPROVE_SCORE: 328 | // @todo improve 329 | // Candidates should be a list of segment ids 330 | var activeCells = that.activity[that.activity.length - 2] || []; 331 | if (activeCells.length > 0) { 332 | candidates.forEach(function (seg) { 333 | var i = Math.floor(Math.random()*activeCells.length); 334 | layer.addFeedingCell(seg, activeCells[i]); 335 | }); 336 | } 337 | break 338 | 339 | case ADD_SEGMENT: 340 | // @todo Implement 341 | // Candidates should be a list of cells 342 | break 343 | 344 | default: 345 | throw new TypeError("Unknown update type:" + type); 346 | } 347 | }; 348 | 349 | suggestions.map(function (suggestion) { 350 | var candidates = suggestion[1]; 351 | var types = suggestion[0]; 352 | var layer = that.layer; 353 | return types.map(function (type) { 354 | return carryOutUpdate(layer, type, candidates) 355 | }); 356 | }); 357 | }; 358 | 359 | /** 360 | * @function 361 | * @todo Annotate 362 | */ 363 | TemporalPooler.prototype.learn = function () { 364 | // @todo Should check if any input has been processed yet 365 | var layer = this.layer; 366 | var input = this.inputs[this.inputs.length - 1] || []; 367 | var that = this; 368 | 369 | // Learning I. 370 | var suggestions = input. 371 | map(this.suggestLearningUpdates, this). 372 | reduce(concat, []); 373 | 374 | // Learning II. 375 | // @todo what to do with cells that wrongly predicted something? 376 | 377 | return this.updateNeuroLayer(suggestions); 378 | }; 379 | 380 | // ========================== 381 | // Visualize and inspect. 382 | // ========================== 383 | /** 384 | * @function 385 | * @todo Annotate 386 | */ 387 | TemporalPooler.prototype.inspectCells = function () { 388 | console.log(this.layer.getCells(), "Cell Ids"); 389 | }; 390 | 391 | /** 392 | * @function 393 | * @todo Annotate 394 | */ 395 | TemporalPooler.prototype.inspectActivity = function (offset) { 396 | var offset = offset === undefined ? 0 : offset; 397 | var t = this.activity.length - 1 + offset; 398 | var activeCells = this.activity[t] || []; 399 | var layer = this.layer; 400 | var activity = layer.getColumns().map(function (col) { 401 | var row = layer.getCells(col).map(function (cell) { 402 | return activeCells.indexOf(cell) < 0 ? " " : "A"; 403 | }).join(" "); 404 | 405 | return row 406 | }); 407 | console.log("| " + activity.join(" | ") + " |" , "Activity", t); 408 | 409 | }; 410 | 411 | /** 412 | * @function 413 | * @todo Annotate 414 | */ 415 | TemporalPooler.prototype.inspectInput = function (offset) { 416 | var offset = offset === undefined ? 0 : offset; 417 | var t = this.inputs.length - 1 + offset; 418 | var input = this.inputs[t] || []; 419 | 420 | var layer = this.layer; 421 | var affectedCells = layer.getColumns().map(function (col) { 422 | var cells = layer.getCells(col); 423 | var output = ''; 424 | if(input.length > 0) { 425 | output = input.indexOf(col) < 0 ? cells.map(result(' ')).join(" ") : cells.map(result('X')).join("X") 426 | } 427 | else { 428 | output = cells.map(result(' ')).join(" ") 429 | // output = "empty input" 430 | } 431 | return output; 432 | }); 433 | console.log("| " + affectedCells.join(" | ") + " |", 'Triggered Cols', t) 434 | }; 435 | 436 | 437 | /** 438 | * @function 439 | * @todo Annotate 440 | */ 441 | TemporalPooler.prototype.inspectLayer = function () { 442 | var matrix = this.layer.getMatrix(); 443 | console.log(matrix.data, 'Layer matrix') 444 | }; 445 | 446 | /** 447 | * @function 448 | * @todo Annotate 449 | */ 450 | TemporalPooler.prototype.inspectPrediction = function (offset) { 451 | var offset = offset === undefined ? 0 : offset; 452 | var layer = this.layer; 453 | var t = this.predictions.length - 1 + offset; 454 | var prediction = this.predictions[t] || new Map(); 455 | var predictedCells = layer.getColumns().map(function (col) { 456 | var row = layer.getCells(col).map(function (cell) { 457 | return (prediction.get(col) || []).indexOf(cell) < 0 ? " " : "P"; 458 | }).join(" "); 459 | 460 | return row; 461 | }); 462 | console.log("| " + predictedCells.join(" | ") + " |", "Predicted Cells", t); 463 | }; 464 | 465 | // /** 466 | // * @function 467 | // * @todo Annotate 468 | // */ 469 | // TemporalPooler.prototype.inspect = function () { 470 | // var output = ''; 471 | // }; 472 | 473 | /** 474 | * Exports {@link TemporalPooler} class. 475 | */ 476 | module.exports = TemporalPooler; 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cortical-learning-algorithm", 3 | "version": "0.0.0", 4 | "description": "Towards an implementation of hierarchical temporal memory and the cortical learning algorithm by Jeff Hawkins and Dileep George of Numenta, Inc.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "make test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mirkoklukas/cortical-learning-algorithm-js.git" 15 | }, 16 | "author": "Mirko Klukas ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/mirkoklukas/cortical-learning-algorithm-js/issues" 20 | }, 21 | "homepage": "https://github.com/mirkoklukas/cortical-learning-algorithm-js", 22 | "dependencies" : { 23 | "underscore": "1.8.2", 24 | "winston": "0.9.0", 25 | "es6-set": "0.1.1" 26 | }, 27 | "devDependencies": { 28 | "chai": "^2.1.0", 29 | "mocha": "^2.1.0" 30 | }, 31 | "engine": "node >= 0.11.0" 32 | } 33 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // var expect = require('chai').expect; 2 | // var cla = require('../lib/index'); 3 | // var tempPooler = new cla.TemporalPooler(); 4 | 5 | -------------------------------------------------------------------------------- /test/test-functional-helper.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var fh = require('../lib/functional-helper.js'); 3 | var Map = require('../lib/simple-map.js'); 4 | 5 | describe('Functional Helper', function () { 6 | 7 | describe("#add and addN", function () { 8 | it('should add things properly', function () { 9 | expect(fh.add(-1,1)).to.equal(0) 10 | expect(fh.addN(-1)(1)).to.equal(0) 11 | }); 12 | }); 13 | 14 | describe("#partial", function () { 15 | var partial = fh.partial; 16 | var identity = function (_) { 17 | return [].slice.call(arguments); 18 | }; 19 | 20 | it('should fix the first arguments of a function', function () { 21 | expect(partial(identity)()).to.deep.equal([]) 22 | expect(partial(identity, 1,"two",["III"],{4:[444]})(5)).to.deep.equal([1,"two",["III"],{4:[444]},5]) 23 | }); 24 | }); 25 | 26 | describe("#insertWith", function () { 27 | var insertWith = fh.insertWith; 28 | var m = new Map([[0,99], ["f", -1]]); 29 | 30 | it('should behave like Haskells \'insertWith\', with permuted signatures though', function () { 31 | expect(insertWith(fh.add, 1, m, 0).toList()).to.deep.equal([[0,100], ["f", -1]]) 32 | expect(insertWith(fh.addN(1), 999, m, 0).toList()).to.deep.equal([[0,101], ["f", -1]]) 33 | expect(insertWith(fh.add, 33, m, 1).toList()).to.deep.equal([[0,101], ["f", -1], [1,33]]) 34 | }); 35 | }); 36 | 37 | describe("#adjuster", function () { 38 | var adjuster = fh.adjuster; 39 | var m = new Map([[0,0], [1, 1]]); 40 | 41 | it('should behave like insertWith with fixed mutator and initial value', function () { 42 | expect(adjuster(fh.add, 100)(m, 2).toList()).to.deep.equal([[0,0], [1, 1], [2,100]]) 43 | expect(adjuster(fh.addN(1), 100)(m, 0).toList()).to.deep.equal([[0,1], [1, 1], [2,100]]) 44 | }); 45 | }); 46 | 47 | describe("#compose", function () { 48 | 49 | it('should produce the concatenation of TWO functions', function () { 50 | expect(fh.compose(fh.addN(2),fh.add)(2, 2)).to.equal(6) 51 | 52 | }); 53 | }); 54 | 55 | describe("#byValuesOf", function () { 56 | it('should compare two objects by their values of the given function', function () { 57 | expect([[1],[3],[2]].sort(fh.byValuesOf(fh.fst))).to.deep.equal([[1],[2],[3]]) 58 | }); 59 | }); 60 | 61 | describe("#argmax", function () { 62 | it('should return the maximum of an array', function () { 63 | expect(fh.argmax([1,242,3])). 64 | to.equal(242); 65 | expect(fh.argmax([{'val': 1},{'val': 2},{'val': 24}], fh.prop("val"))). 66 | to.deep.equal({'val': 24}); 67 | }); 68 | }); 69 | 70 | describe("#wrapGetter", function () { 71 | it('should turn a Map into a function', function () { 72 | var map = new Map([[1,11],[2,22]]) 73 | expect(fh.wrapGetter(map)(2)).to.equal(22) 74 | expect(fh.wrapGetter(map)(-99)).to.equal(0) 75 | expect(fh.wrapGetter(map, 100)(-99)).to.equal(100) 76 | }); 77 | }); 78 | 79 | describe("#decision", function () { 80 | var D = fh.Decision; 81 | var result = function (x) { 82 | return function () { return x; }; 83 | }; 84 | 85 | var d = D(fh.geq(0), null, 86 | D(fh.geq( 10), null, 87 | D(null, result(">0, > 10")), 88 | D(null, result(">0, < 10"))), 89 | D(fh.geq(-10), null, 90 | D(null, result('<0, >-10')), 91 | D(null, result('<0, <-10')))) 92 | 93 | it('is pending') 94 | // it('...', function () { 95 | // expect(d(2)).to.equal(">0, < 10") 96 | // expect(d(20)).to.equal(">0, > 10") 97 | // expect(d(-2)).to.equal("<0, >-10") 98 | // }); 99 | 100 | }); 101 | 102 | }); 103 | -------------------------------------------------------------------------------- /test/test-simple-map.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Map = require('../lib/simple-map.js'); 3 | 4 | describe('Simple Map', function () { 5 | 6 | }); 7 | -------------------------------------------------------------------------------- /test/test-temporal-pooler.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var TemporalPooler = require('../lib/temporal-pooler.js'); 3 | var NeuroLayer = require("../lib/neuro-layer.js"); 4 | var Map = require('../lib/simple-map.js'); 5 | var tempPooler = new TemporalPooler(); 6 | var computeActiveCells = tempPooler.computeActiveCells; 7 | var computeSegmentScores = tempPooler.computeSegmentScores; 8 | var computePredictionMap = tempPooler.computePredictionMap; 9 | var computeNonPredictedColumns = tempPooler.computeNonPredictedColumns; 10 | var computeOutput = tempPooler.computeOutput; 11 | var Set = require('es6-set'); 12 | 13 | 14 | describe('Temporal Pooler', function () { 15 | 16 | var layer = new NeuroLayer({ 17 | 'columns': { 18 | 1: { 'cells': [1,2,3]}, 19 | 2: { 'cells': [4,5,6]}, 20 | 3: { 'cells': [7,8,9]} 21 | }, 22 | 'cells': { 23 | 1: { 'listeningSegments': ["s1"], 'feedingSegments': [], 'column': 1}, 24 | 2: { 'listeningSegments': ["s2"], 'feedingSegments': [], 'column': 1}, 25 | 3: { 'listeningSegments': ["s1", "s2"], 'feedingSegments': [], 'column': 1}, 26 | 4: { 'listeningSegments': ["s1"], 'feedingSegments': [], 'column': 2}, 27 | 5: { 'listeningSegments': [], 'feedingSegments': [], 'column': 2}, 28 | 6: { 'listeningSegments': [], 'feedingSegments': [], 'column': 2}, 29 | 7: { 'listeningSegments': [], 'feedingSegments': ['s1'], 'column': 3}, 30 | 8: { 'listeningSegments': [], 'feedingSegments': ['s2'], 'column': 3}, 31 | 9: { 'listeningSegments': [], 'feedingSegments': ['s3'], 'column': 3} 32 | 33 | }, 34 | 'segments': { 35 | 's1': { 'listeningCell': 7}, 36 | 's2': { 'listeningCell': 8}, 37 | 's3': { 'listeningCell': 9}, 38 | } 39 | }); 40 | 41 | var prediction = new Map([ 42 | [1, [2]], 43 | [2, [4, 6]] 44 | ]); 45 | 46 | var scores = new Map([ 47 | ["s1", 3], 48 | ["s2" , 2], 49 | ["s3", 1] 50 | ]); 51 | 52 | var inputs = [ 53 | [1], 54 | [2], 55 | [1], 56 | [2], 57 | [1] 58 | ]; 59 | 60 | tempPooler.setNeuroLayer(layer); 61 | tempPooler.setActivationThreshold(3) 62 | 63 | 64 | describe('#computeActiveCells', function() { 65 | it('computes active cells given active columns', function() { 66 | expect(new Set(computeActiveCells(layer, prediction, []))). 67 | to.deep.equal(new Set([])); 68 | expect(new Set(computeActiveCells(layer, prediction, [2]))). 69 | to.deep.equal(new Set([4,6])); 70 | expect(new Set(computeActiveCells(layer, prediction, [3]))). 71 | to.deep.equal(new Set([9,8,7])); 72 | expect(new Set(computeActiveCells(layer, prediction, [1,2,3]))). 73 | to.deep.equal(new Set([2,4,6,9,8,7])); 74 | }); 75 | }); 76 | 77 | describe('#computeSegmentScores', function() { 78 | it('computes the scores for each segments based on a list of active cells', function() { 79 | expect(computeSegmentScores(layer, []).toList()). 80 | to.deep.equal([]); 81 | expect(computeSegmentScores(layer, [1,2,3,4]).toList()). 82 | to.deep.equal([["s1", 3],["s2" , 2]]); 83 | }); 84 | 85 | }); 86 | 87 | describe('#computePredictionMap', function() { 88 | it('computes the predictive states for each cell.', function() { 89 | expect(computePredictionMap(layer, 5, new Map()).toList()).to.deep.equal([]) 90 | expect(computePredictionMap(layer, 5, scores).toList()).to.deep.equal([]) 91 | expect(computePredictionMap(layer, 2, scores).toList()).to.deep.equal([[3, [7,8]]]) 92 | }); 93 | }); 94 | 95 | describe('#computeNonPredictedColumns', function () { 96 | it('should return input entries that are not predicted', function () { 97 | expect(computeNonPredictedColumns([1,2,3,4], prediction)).to.deep.equal([3, 4]); 98 | }); 99 | }); 100 | 101 | describe('#computeOutput', function () { 102 | it('should return the union of the input and predicted columns', function () { 103 | expect(new Set(tempPooler.computeOutput([1,3,4], prediction)) ).to.deep.equal(new Set([1, 2, 3, 4])); 104 | }); 105 | }); 106 | 107 | describe('#processInput', function () { 108 | it('is pending') 109 | it('should process the input based on the previous inputs accordingly', function () { 110 | // tempPooler.processInput() 111 | }); 112 | }); 113 | 114 | describe('#suggestLearningUpdates', function () { 115 | it('is pending') 116 | 117 | }); 118 | 119 | describe('#updateNeuroLayer', function () { 120 | it('is pending') 121 | }); 122 | 123 | }); 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | --------------------------------------------------------------------------------