jsbayes
Version:
Simple Bayesian Belief Network (BBN) library in JavaScript.
464 lines (432 loc) • 13.1 kB
JavaScript
(function(window) {
'use strict';
/**
* Initializes a conditional probability table.
* @param {Number} numValues Number of values.
* @returns {Array} Array of doubles that sum to 1.0.
*/
function initCpt(numValues) {
var cpt = [];
var sum = 0;
for(var i=0; i < numValues; i++) {
cpt[i] = Math.random();
sum += cpt[i];
}
for(var i=0; i < numValues; i++) {
cpt[i] = cpt[i] / sum;
}
return cpt;
}
/**
* Initializes a CPT with fake and normalized values using recursion.
* @param {Array} values Values of variables (array of values).
* @param {Array} parents Array of JSON nodes that are parents of the variable.
* @param {Number} paIndex The current parent index.
* @returns {Array} An array of nested arrays representing the CPT.
*/
function initCptWithParents(values, parents, paIndex) {
if(parents && parents.length > 0) {
if(parents.length === 1 || paIndex === parents.length - 1) {
var idx = parents.length === 1 ? 0 : paIndex;
var numPaVals = parents[idx].values.length;
var cpts = [];
for(var i=0; i < numPaVals; i++) {
var cpt = initCpt(values.length);
cpts.push(cpt);
}
return cpts;
} else {
var cpts = [];
var numPaVals = parents[paIndex].values.length;
for(var i=0; i < numPaVals; i++) {
var cpt = initCptWithParents(values, parents, paIndex+1);
cpts.push(cpt);
}
return cpts;
}
} else {
return initCpt(values.length);
}
}
/**
* Creates a Promise.
* @param {Object} f Function.
* @param {Array} args List of arguments.
* @returns {Promise} Promise.
*/
function async(f, args) {
return new Promise(
function(resolve, reject) {
try {
var r = f.apply(undefined, args);
resolve(r);
} catch(e) {
reject(e);
}
}
);
}
/**
* Checks if an object is an array.
* @param {*} o Object.
* @returns {Boolean} A boolean to indicate if the object is an array object.
*/
function isArray(o) {
return (o.constructor === Array);
}
/**
* Checks if an object is an array of arrays.
* @param {*} o Object.
* @returns {Boolean} A boolean to indicate if the object is array of arrays.
*/
function isArrayOfArray(o) {
if(isArray(o)) {
if(o.length > 0) {
if(isArray(o[0])) {
return true;
}
}
}
return false;
}
/**
* Sets the CPT entries to the specified probabilities.
* @param {Array} cpt Array of nested arrays representing a CPT.
* @param {Array} probs Array of arrays of probabilities representing a CPT.
* @param {Number} index The current index.
* @returns {Number} The next index.
*/
function setNodeCptProbs(cpt, probs, index) {
if(!isArrayOfArray(cpt)) {
for(var i=0; i < cpt.length; i++) {
cpt[i] = probs[index][i];
}
var nextIndex = index + 1;
return nextIndex;
} else {
var next = index;
for(var i=0; i < cpt.length; i++) {
next = setNodeCptProbs(cpt[i], probs, next);
}
return next;
}
}
/**
* Initializes a node's CPT.
* @param {Array} values Array of values.
* @param {Array} parents Array of parents.
* @param {Array} probs Array of arrays of probabilities.
* @returns {Array} Array of nested arrays representing a CPT.
*/
function initNodeCpt(values, parents, probs) {
var cpt = initCptWithParents(values, parents, 0);
setNodeCptProbs(cpt, probs, 0);
return cpt;
}
/**
* Normalizes an array of values such that the elements sum to 1.0. Note that
* 0.001 is added to every value to avoid 0.0 probabilities. This adjustment
* helps with visualization downstream.
* @param {Array} arr Array of probabilities.
* @returns {Array} Normalized probailities.
*/
function normalizeProbs(arr) {
var probs = [];
var sum = 0.0;
for (var i=0; i < arr.length; i++) {
probs[i] = arr[i] + 0.001
sum += probs[i]
}
for (var i=0; i < arr.length; i++) {
probs[i] = probs[i] / sum;
}
return probs;
}
/**
* Normalizes a CPT.
* @param {Array} cpts Array of arrays (matrix) representing a CPT.
* @returns {Array} Normalized CPT.
*/
function normalizeCpts(cpts) {
var probs = []
for (var i=0; i < cpts.length; i++) {
probs.push(normalizeProbs(cpts[i]));
}
return probs;
}
/**
* Defines the library.
*/
function defineLib() {
var jsbayes = {};
jsbayes.newGraph = function() {
return {
nodes: [],
saveSamples: false,
samples: [],
reinit: function() {
var f = function(g) {
for(var i=0; i < g.nodes.length; i++) {
var node = g.nodes[i];
if(node.dirty === undefined || node.dirty) {
node.cpt = initCptWithParents(node.values, node.parents, 0);
node.dirty = false;
}
}
};
return async(f, [this]);
},
samplesAsCsv: function(options) {
var opts = options || {};
var D_ROW = opts.rowDelimiter || '\n';
var D_FIELD = opts.fieldDelimiter || ',';
var csv = '';
var row = '';
for(var i=0; i < this.nodes.length; i++) {
row += this.nodes[i].name;
if(i < this.nodes.length-1) {
row += D_FIELD;
}
}
csv += row + D_ROW;
for(var i=0; i < this.samples.length; i++) {
var sample = this.samples[i];
row = '';
for(var j=0; j < this.nodes.length; j++) {
var node = this.nodes[j];
row += sample[node.name];
if(j < this.nodes.length-1) {
row += D_FIELD;
}
}
csv += row;
if(i < this.samples.length-1) {
csv += D_ROW;
}
}
return csv;
},
sample: function(samples) {
var f = function(g, samples) {
if(g.saveSamples) {
//reset the samples if we want to save them
g.samples = [];
}
for(var h=g.nodes.length-1; h >= 0; h--) {
g.nodes[h].initSampleLw();
}
var lwSum = 0;
for(var count=0; count < samples; count++) {
for(var h=g.nodes.length-1; h >= 0; h--) {
var n = g.nodes[h];
if(!n.isObserved) {
n.value = -1;
}
n.wasSampled = false;
}
var fa = 1;
for(var h=g.nodes.length-1; h >= 0; h--) {
var n = g.nodes[h];
fa *= n.sampleLw();
}
lwSum += fa;
for(var h=g.nodes.length-1; h >= 0; h--) {
var n = g.nodes[h];
n.saveSampleLw(fa);
}
if(g.saveSamples) {
var sample = {};
for(var h=g.nodes.length-1; h >= 0; h--) {
var n = g.nodes[h];
sample[n.name] = n.values[n.value];
}
g.samples.push(sample);
}
}
return lwSum;
};
return async(f, [this, samples]);
},
update: function(m) {
for(var i=0; i < this.nodes.length; i++) {
var tnode = this.nodes[i]; //'this' node
var unode = m[tnode.name]; //update node
if(!unode) {
continue;
}
tnode.value = unode.value;
tnode.wasSampled = unode.wasSampled;
tnode.sampledLw = unode.sampledLw;
}
},
node: function(name) {
if(!this.nodeMap) {
this.nodeMap = {};
for(var i=0; i < this.nodes.length; i++) {
var node = this.nodes[i];
this.nodeMap[node.name] = node;
}
}
return this.nodeMap[name];
},
observe: function(name, value) {
var node = this.node(name);
if(node) {
var index = node.valueIndex(value);
if(index >= 0) {
node.isObserved = true;
node.value = index;
} else {
console.error('could not find value ' + value + ' for node ' + name);
}
} else {
console.error('could not find node with name ' + name);
}
},
unobserve: function(name) {
var node = this.node(name);
if(node) {
node.isObserved = false;
node.value = -1;
}
},
addNode: function(name, values) {
var node = {
name: name,
values: values,
value: -1,
parents: [],
wasSampled: false,
sampledLw: undefined,
addParent: function(parent) {
this.parents.push(parent);
this.dirty = true;
return this;
},
valueIndex: function(v) {
if(!this.valueIndexMap) {
this.valueIndexMap = {};
for(var i=0; i < this.values.length; i++) {
var value = this.values[i];
this.valueIndexMap[value] = i;
}
}
return this.valueIndexMap[v];
},
initSampleLw: function() {
this.sampledLw = undefined;
},
sampleLw: function() {
if(this.wasSampled) {
return 1;
}
var fa = 1;
for(var h=0; h < this.parents.length; h++) {
var pa = this.parents[h];
var pSampleLw = pa.sampleLw();
fa *= pSampleLw;
}
this.wasSampled = true;
var dh = this.cpt;
for(var h=0; h < this.parents.length; h++) {
var p = this.parents[h];
var v = p.value;
dh = dh[v];
}
if(this.value != -1) {
var v = dh[this.value];
fa *= v;
} else {
var fv = Math.random();
for(var h=0; h < dh.length; h++) {
var v = dh[h];
fv -= v;
if(fv < 0) {
this.value = h;
break;
}
}
}
return fa;
},
saveSampleLw: function(f) {
if(!this.sampledLw) {
this.sampledLw = new Array(this.values.length);
for(var h=this.values.length-1; h >= 0; h--) {
this.sampledLw[h] = 0;
}
}
this.sampledLw[this.value] += f;
},
setCpt: function(probs) {
if(this.parents.length === 0) {
this.cpt = normalizeProbs(probs);
} else {
this.cpt = initNodeCpt(this.values, this.parents, normalizeCpts(probs));
}
},
probs: function() {
if(!this.sampledLw) {
return [];
}
var sum = 0.0;
var probs = [];
for(var i=0; i < this.sampledLw.length; i++) {
var s = this.sampledLw[i];
sum += s;
probs.push(s);
}
for(var i=0; i < this.sampledLw.length; i++) {
probs[i] = probs[i] / sum;
}
return probs;
}
}
this.nodes.push(node);
return node;
}
};
}
jsbayes.toMessage = function(g) {
var nodes = {};
var parents = {};
for(var i=0; i < g.nodes.length; i++) {
var n = g.nodes[i];
var node = {
name: n.name,
values: n.values,
value: n.value,
parents: [],
wasSampled: n.wasSampled,
sampledLw: n.sampledLw,
cpt: n.cpt
};
nodes[n.name] = node;
var pas = [];
for(var j=0; j < n.parents.length; j++) {
var pa = n.parents[j];
pas.push(pa.name);
}
parents[n.name] = pas;
}
var msg = {
samples: 10000,
nodes: nodes,
parents: parents
};
return JSON.stringify(msg);
}
return jsbayes;
}
if(typeof module === 'object' && module && typeof module.exports === 'object') {
module.exports = defineLib();
} else {
if(typeof(jsbayes) === 'undefined') {
window.jsbayes = defineLib();
}
if(typeof define === 'function' && define.amd) {
define('jsbayes', [], defineLib());
}
}
}
)(this);