(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('gpu.js')) :
typeof define === 'function' && define.amd ? define(['exports', 'gpu.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.brain = {}, global.gpu_js));
}(this, (function (exports, gpu_js) { 'use strict';
/**
* Relu Activation, aka Rectified Linear Unit Activation
* @description https://en.wikipedia.org/wiki/Rectifier_(neural_networks)
*/
function activate$3(weight) {
return Math.max(0, weight);
}
/**
* Relu derivative
*/
function measure$3(weight, delta) {
if (weight <= 0) {
return 0;
}
return delta;
}
var relu$2 = /*#__PURE__*/Object.freeze({
__proto__: null,
activate: activate$3,
measure: measure$3
});
/**
* sigmoid activation
*/
function activate$2(value) {
return 1 / (1 + Math.exp(-value));
}
/**
* sigmoid derivative
*/
function measure$2(weight, error) {
return weight * (1 - weight) * error;
}
var sigmoid$2 = /*#__PURE__*/Object.freeze({
__proto__: null,
activate: activate$2,
measure: measure$2
});
/**
* Hyperbolic tan
*/
function activate$1(weight) {
return Math.tanh(weight);
}
/**
* @description grad for z = tanh(x) is (1 - z^2)
*/
function measure$1(weight, error) {
return (1 - weight * weight) * error;
}
var tanh$2 = /*#__PURE__*/Object.freeze({
__proto__: null,
activate: activate$1,
measure: measure$1
});
/**
* Leaky Relu Activation, aka Leaky Rectified Linear Unit Activation
* @description https://en.wikipedia.org/wiki/Rectifier_(neural_networks)
*/
function activate(weight) {
return weight > 0 ? weight : 0.01 * weight;
}
/**
* Leaky Relu derivative
*/
function measure(weight, error) {
return weight > 0 ? error : 0.01 * error;
}
var leakyRelu$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
activate: activate,
measure: measure
});
var index$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
relu: relu$2,
sigmoid: sigmoid$2,
tanh: tanh$2,
leakyRelu: leakyRelu$1
});
class CrossValidate {
constructor(initClassifier) {
this.json = {
avgs: {
error: 0,
iterations: 0,
testTime: 0,
trainTime: 0,
},
stats: {
total: 0,
testSize: 0,
trainSize: 0,
},
sets: [],
};
this.initClassifier = initClassifier;
}
testPartition(trainOpts, trainSet, testSet) {
const classifier = this.initClassifier();
const beginTrain = Date.now();
const trainingStats = classifier.train(trainSet, trainOpts);
const beginTest = Date.now();
const testStats = classifier.test(testSet);
const endTest = Date.now();
return {
...testStats,
trainTime: beginTest - beginTrain,
testTime: endTest - beginTest,
iterations: trainingStats.iterations,
error: trainingStats.error,
total: testStats.total,
network: classifier.toJSON(),
};
}
/**
* Randomize array element order in-place.
* Using Durstenfeld shuffle algorithm.
* source: http://stackoverflow.com/a/12646864/1324039
*/
shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
train(data, trainOpts = {}, k = 4) {
if (data.length < k) {
throw new Error(`Training set size is too small for ${data.length} k folds of ${k}`);
}
this.shuffleArray(data);
const size = data.length / k;
const avgs = {
trainTime: 0,
testTime: 0,
iterations: 0,
error: 0,
};
const stats = {
total: 0,
testSize: 0,
trainSize: 0,
};
const binaryStats = {
total: 0,
testSize: 0,
trainSize: 0,
truePos: 0,
trueNeg: 0,
falsePos: 0,
falseNeg: 0,
precision: 0,
recall: 0,
accuracy: 0,
};
const results = [];
let isBinary = null;
for (let i = 0; i < k; i++) {
const dclone = data.slice(0);
const testSet = dclone.splice(i * size, size);
const trainSet = dclone;
const result = this.testPartition(trainOpts, trainSet, testSet);
if (isBinary === null) {
isBinary =
result.hasOwnProperty('falseNeg') &&
result.hasOwnProperty('falsePos') &&
result.hasOwnProperty('trueNeg') &&
result.hasOwnProperty('truePos');
if (isBinary) {
Object.assign(stats, binaryStats);
}
}
avgs.iterations += result.iterations;
avgs.testTime += result.testTime;
avgs.trainTime += result.trainTime;
avgs.error += result.error;
stats.total += result.total;
if (CrossValidate.isBinaryStats(stats) &&
CrossValidate.isBinaryPartitionResults(result)) {
stats.accuracy += result.accuracy;
stats.falseNeg += result.falseNeg;
stats.falsePos += result.falsePos;
stats.precision += result.precision;
stats.recall += result.recall;
stats.trueNeg += result.trueNeg;
stats.truePos += result.truePos;
}
results.push(result);
}
avgs.error /= k;
avgs.iterations /= k;
avgs.testTime /= k;
avgs.trainTime /= k;
if (CrossValidate.isBinaryStats(stats)) {
stats.precision = stats.truePos / (stats.truePos + stats.falsePos);
stats.recall = stats.truePos / (stats.truePos + stats.falseNeg);
stats.accuracy = (stats.trueNeg + stats.truePos) / stats.total;
}
stats.testSize = size;
stats.trainSize = data.length - size;
this.json = {
avgs: avgs,
stats: stats,
sets: results,
};
return this.json;
}
toNeuralNetwork() {
return this.fromJSON(this.json);
}
toJSON() {
return this.json;
}
fromJSON(crossValidateJson) {
const winningJSON = crossValidateJson.sets.reduce((prev, cur) => (prev.error < cur.error ? prev : cur));
return this.initClassifier().fromJSON(winningJSON.network);
}
}
CrossValidate.isBinaryStats = (stats) => {
return (stats.accuracy !== undefined);
};
CrossValidate.isBinaryResults = (stats) => stats.stats.accuracy !== undefined;
CrossValidate.isBinaryPartitionResults = (stats) => stats.accuracy !==
undefined;
let gpuInstance = null;
/**
* Sets up the gpu.js instance
*/
function setup(value) {
gpuInstance = value;
}
function makeKernel(fn, settings) {
let _gpuInstance = gpuInstance;
if (_gpuInstance === null) {
_gpuInstance = new gpu_js.GPU({ mode: 'gpu' });
setup(_gpuInstance);
}
return _gpuInstance
.createKernel(fn, settings)
.setPipeline(true);
}
function makeKernelMap(map, fn, settings) {
let _gpuInstance = gpuInstance;
if (_gpuInstance === null) {
_gpuInstance = new gpu_js.GPU({ mode: 'gpu' });
setup(_gpuInstance);
}
return _gpuInstance
.createKernelMap(map, fn, settings)
.setPipeline(true);
}
/**
* Compiles a function into a gpu.js dev mode kernel
*/
// export function makeDevKernel(
// fn: ThreadFunction,
// settings: makeKernelSettings
// ): IKernelRunShortcut {
// if ('map' in settings) {
// throw new Error('map kernels are not supported by dev kernels');
// }
// const gpu = new GPU({ mode: 'dev' });
// return gpu.createKernel(fn, settings);
// }
function kernelInput(value, size) {
return new gpu_js.Input(value, size);
}
/**
* Deletes a gpu.js texture and frees VRAM
*/
function release(possibleTexture) {
if (possibleTexture instanceof gpu_js.Texture) {
possibleTexture.delete();
}
}
/**
* Cleans ie sets all elements to 0 of a Texture or a js array
*/
function clear(value) {
if (value instanceof gpu_js.Texture) {
value.clear();
return;
}
// array
if (Array.isArray(value)) {
if (typeof value[0] === 'number') {
value.fill(0);
}
else if (typeof value[0][0] === 'number') {
for (let x = 0; x < value.length; x++) {
value[x].fill(0);
}
return;
}
else if (typeof value[0][0][0] === 'number') {
// cube
for (let y = 0; y < value.length; y++) {
const row = value[y];
for (let x = 0; x < row.length; x++) {
row[x].fill(0);
}
}
return;
}
}
if (value instanceof Float32Array) {
value.fill(0);
return;
}
throw new Error('unhandled value');
}
/**
* Clones a value
*/
function clone(value) {
if (value instanceof gpu_js.Texture) {
return value.clone();
}
if (value instanceof Float32Array) {
return value.slice(0);
}
if (Array.isArray(value)) {
if (typeof value[0] === 'number') {
return value.slice(0);
}
else if (typeof value[0][0] === 'number') {
const matrix = new Array(value.length);
for (let x = 0; x < value.length; x++) {
matrix[x] = value[x].slice(0);
}
return matrix;
}
else if (typeof value[0][0][0] === 'number') {
const cube = new Array(value.length);
for (let y = 0; y < value.length; y++) {
const row = value[y];
const matrix = new Array(row.length);
for (let x = 0; x < row.length; x++) {
matrix[x] = row[x].slice(0);
}
}
return cube;
}
}
throw new Error('unhandled value');
}
/**
* 2D Mean Squared Error
*/
function mse2d(errors) {
let sum = 0;
for (let y = 0; y < this.constants.height; y++) {
for (let x = 0; x < this.constants.width; x++) {
sum += errors[y][x] ** 2;
}
}
return sum / this.constants.length;
}
class MeanSquaredError {
constructor({ width, height }) {
this.calculate = makeKernel(mse2d, {
output: [1],
constants: {
width,
height,
length: width * height,
},
immutable: true,
});
this.addAbsolute = makeKernel(function (prevError, prevLayerErrors) {
return prevError[0] + Math.abs(prevLayerErrors[0][0]);
}, {
output: [1],
immutable: true,
});
this.add = makeKernel(function (value1, value2) {
return value1[0] + value2[0];
}, {
output: [1],
immutable: true,
});
this.divide = makeKernel(function (length, mseSum) {
const value = mseSum[0];
if (value > 0) {
return value / length;
}
return 0;
}, {
output: [1],
immutable: true,
});
}
}
const baseLayerDefaultSettings = {
width: 1,
height: 1,
depth: null,
weights: null,
deltas: null,
praxis: null,
praxisOpts: null,
cleanupDeltas: true,
};
class BaseLayer {
constructor(settings) {
this.praxis = null;
this.predictKernel = null;
this.compareKernel = null;
if (settings) {
this.settings = { ...baseLayerDefaultSettings, ...settings };
}
else {
this.settings = { ...baseLayerDefaultSettings };
}
this.setupPraxis();
}
get width() {
var _a;
return (_a = this.settings.width) !== null && _a !== void 0 ? _a : 0;
}
get height() {
var _a;
return (_a = this.settings.height) !== null && _a !== void 0 ? _a : 0;
}
get depth() {
var _a;
return (_a = this.settings.depth) !== null && _a !== void 0 ? _a : 0;
}
get weights() {
return this.settings.weights;
}
set weights(weights) {
this.settings.weights = weights;
if (this.settings.cleanupDeltas && this.deltas) {
clear(this.deltas);
}
}
get deltas() {
return this.settings.deltas;
}
set deltas(deltas) {
this.settings.deltas = deltas;
}
get id() {
var _a;
return (_a = this.settings.id) !== null && _a !== void 0 ? _a : '';
}
set id(title) {
this.settings.id = title;
}
setupPraxis() {
const { initPraxis, praxis, praxisOpts } = this.settings;
if (!this.praxis) {
if (initPraxis) {
if (praxisOpts) {
this.praxis = initPraxis(this, praxisOpts);
}
else {
this.praxis = initPraxis(this);
}
}
else if (praxis) {
this.praxis = praxis;
}
}
}
/*
get weights() {
return this._weights;
}
set weights(value) {
if (value) {
if (value.dimensions) {
if (value.dimensions[0] !== this.width) {
throw new Error(`${this.constructor.name}.weights being set with improper value width`);
}
if (value.dimensions[1] !== this.height) {
throw new Error(`${this.constructor.name}.weights being set with improper value height`);
}
} else {
if (value[0].length !== this.width) {
throw new Error(`${this.constructor.name}.weights being set with improper value width`);
}
if (value.length !== this.height) {
throw new Error(`${this.constructor.name}.weights being set with improper value height`);
}
}
}
this._weights = value;
}
get deltas() {
return this._deltas;
}
set deltas(value) {
if (value) {
if (value.dimensions) {
if (value.dimensions[0] !== this.width) {
throw new Error(`${this.constructor.name}.deltas being set with improper value width`);
}
if (value.dimensions[1] !== this.height) {
throw new Error(`${this.constructor.name}.deltas being set with improper value height`);
}
} else {
if (value[0].length !== this.width) {
throw new Error(`${this.constructor.name}.deltas being set with improper value width`);
}
if (value.length !== this.height) {
throw new Error(`${this.constructor.name}.deltas being set with improper value height`);
}
}
}
this._deltas = value;
} */
validate() {
if (Number.isNaN(this.height)) {
throw new Error(`${this.constructor.name} layer height is not a number`);
}
if (Number.isNaN(this.width)) {
throw new Error(`${this.constructor.name} layer width is not a number`);
}
if (this.height < 1) {
throw new Error(`${this.constructor.name} layer height is less than 1`);
}
if (this.width < 1) {
throw new Error(`${this.constructor.name} layer width is less than 1`);
}
}
setupKernels(isTraining) { }
reuseKernels(layer) {
if (layer.width !== this.width) {
throw new Error(`${this.constructor.name} kernel width mismatch ${layer.width} is not ${this.width}`);
}
if (layer.height !== this.height) {
throw new Error(`${this.constructor.name} kernel width mismatch ${layer.height} is not ${this.height}`);
}
if (layer.hasOwnProperty('predictKernel') && layer.predictKernel !== null) {
if (!layer.predictKernel.immutable) {
throw new Error(`${layer.constructor.name}.predictKernel is not reusable, set kernel.immutable = true`);
}
this.predictKernel = layer.predictKernel;
}
if (layer.hasOwnProperty('compareKernel') && layer.compareKernel !== null) {
if (!layer.compareKernel.immutable) {
throw new Error(`${layer.constructor.name}.compareKernel is not reusable, set kernel.immutable = true`);
}
this.compareKernel = layer.compareKernel;
}
this.praxis = layer.praxis;
}
predict(inputs) { }
compare(targetValues) { }
learn(learningRate) { }
toArray() {
return Array.isArray(this.weights)
? this.weights
: this.weights.toArray();
}
toJSON() {
return BaseLayer.toJSON(this);
}
static toJSON(layer) {
const { weights } = layer;
return {
width: layer.width,
height: layer.height,
depth: layer.depth,
weights: toUntypedArray((weights && weights instanceof gpu_js.Texture
? weights.toArray()
: weights)),
type: layer.constructor.name,
praxisOpts: layer.praxis ? layer.praxis.toJSON() : null,
};
}
}
function toUntypedArray(weights) {
if (weights === null)
return null;
if (Array.isArray(weights)) {
if (typeof weights[0] === 'number') {
return weights;
}
else if (Array.isArray(weights[0]) && typeof weights[0][0] === 'number') {
return weights;
}
else if (Array.isArray(weights[0][0]) &&
typeof weights[0][0][0] === 'number') {
return weights;
}
else if (weights[0] instanceof Float32Array) {
const matrix = weights;
return matrix.map((row) => {
return Array.from(row);
});
}
else if (weights[0][0] instanceof Float32Array) {
const cube = weights;
return cube.map((matrix) => {
return matrix.map((row) => {
return Array.from(row);
});
});
}
}
else if (weights) {
return Array.from(weights);
}
throw new Error('unexpected value');
}
/**
* Returns an array of zeros
*/
function zeros$1(size) {
return new Float32Array(size);
}
/**
* Returns a 2D tensor(matrix) of zeros
*/
function zeros2D(width, height) {
const result = new Array(height);
for (let y = 0; y < height; y++) {
result[y] = zeros$1(width);
}
return result;
}
/**
* Returns a 3D tensor of arrays
*/
function zeros3D(width, height, depth) {
const result = new Array(depth);
for (let z = 0; z < depth; z++) {
result[z] = zeros2D(width, height);
}
return result;
}
class Activation extends BaseLayer {
constructor(inputLayer, settings) {
super(settings);
this.inputLayer = inputLayer;
const { width, height, depth } = this;
this.predictKernel = null;
this.compareKernel = null;
this.validate();
if (depth > 0) {
this.weights = zeros3D(width, height, depth);
this.deltas = zeros3D(width, height, depth);
}
else if (height > 0) {
this.weights = zeros2D(width, height);
this.deltas = zeros2D(width, height);
}
this.setupPraxis();
}
get width() {
return this.inputLayer.width;
}
get height() {
return this.inputLayer.height;
}
get depth() {
return this.inputLayer.depth;
}
}
class Filter extends BaseLayer {
constructor(settings, inputLayer) {
super();
this.settings = settings;
this.inputLayer = inputLayer;
}
get width() {
return this.inputLayer.width;
}
get height() {
return this.inputLayer.height;
}
get depth() {
return this.inputLayer.depth;
}
get filterCount() {
return this.settings.filterCount;
}
get filterWidth() {
return this.settings.filterWidth;
}
get filterHeight() {
return this.settings.filterHeight;
}
get filters() {
return this.settings.filters;
}
set filters(filters) {
this.settings.filters = filters;
}
get filterDeltas() {
return this.settings.filterDeltas;
}
set filterDeltas(filterDeltas) {
this.settings.filterDeltas = filterDeltas;
}
}
class Internal {
constructor() {
this.predictKernel = null;
this.compareKernel = null;
this.praxis = null;
}
get width() {
return this.settings.width;
}
get height() {
return this.settings.height;
}
get depth() {
return this.settings.depth;
}
get weights() {
return this.settings.weights;
}
set weights(weights) {
this.settings.weights = weights;
}
get deltas() {
return this.settings.deltas;
}
set deltas(deltas) {
this.settings.deltas = deltas;
}
toJSON() {
return BaseLayer.toJSON(this);
}
}
class Modifier extends BaseLayer {
constructor(inputLayer, settings) {
super({
...settings,
width: inputLayer.width,
height: inputLayer.height,
depth: inputLayer.depth,
});
this.inputLayer = inputLayer;
}
validate() {
var _a;
super.validate();
if (this.width !== this.inputLayer.width) {
throw new Error(`width of ${this.width} does not match inputLayer.width of ${this.inputLayer.width}`);
}
if (this.height !== this.inputLayer.height) {
throw new Error(`height of ${this.height} does not match inputLayer.height of ${this.inputLayer.height}`);
}
if (this.depth !== ((_a = this.inputLayer.depth) !== null && _a !== void 0 ? _a : 0)) {
throw new Error(`depth of ${this.depth} does not match inputLayer.depth of ${this.inputLayer.depth}`);
}
}
}
class Operator extends BaseLayer {
constructor(inputLayer1, inputLayer2, settings) {
super(settings);
this.inputLayer1 = inputLayer1;
this.inputLayer2 = inputLayer2;
this.validate();
this.weights = zeros2D(this.width, this.height);
this.deltas = zeros2D(this.width, this.height);
this.setupPraxis();
}
}
function compare1D(weights, targetValues) {
return weights[this.thread.y][this.thread.x] - targetValues[this.thread.x];
}
function compare2D$5(weights, targetValues) {
return (weights[this.thread.y][this.thread.x] -
targetValues[this.thread.y][this.thread.x]);
}
class Target extends BaseLayer {
constructor(settings, inputLayer) {
super(settings);
this.inputLayer = inputLayer;
this.validate();
if (this.depth) {
throw new Error('Target layer not implemented for depth');
}
else if (this.height) {
this.weights = zeros2D(this.width, this.height);
this.deltas = zeros2D(this.width, this.height);
this.errors = zeros2D(this.width, this.height);
}
else {
this.weights = zeros$1(this.width);
this.deltas = zeros$1(this.width);
this.errors = zeros$1(this.width);
}
}
setupKernels() {
if (this.width === 1) {
this.compareKernel = makeKernel(compare1D, {
output: [this.width, this.height],
immutable: true,
});
}
else {
this.compareKernel = makeKernel(compare2D$5, {
output: [this.width, this.height],
immutable: true,
});
}
}
predict() {
// TODO: should we clone here?
// NOTE: this looks like it shouldn't be, but the weights are immutable, and this is where they are reused.
release(this.weights);
this.weights = clone(this.inputLayer.weights);
}
compare(targetValues) {
// this is where weights attach to deltas
// deltas will be zero on learn, so save it in error for comparing to mse later
release(this.deltas);
release(this.errors);
release(this.inputLayer.deltas);
this.deltas = this.compareKernel(this.weights, targetValues);
this.inputLayer.deltas = clone(this.deltas);
this.errors = clone(this.deltas);
}
setupPraxis() { }
}
function target(settings, inputLayer) {
return new Target(settings, inputLayer);
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class InternalModel {
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class EntryPoint extends BaseLayer {
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class Model extends BaseLayer {
learn(learningRate) {
// TODO: do we need to release here?
const { weights: oldWeights } = this;
if (!this.praxis)
throw new Error('this.praxis not defined');
this.weights = this.praxis.run(this, learningRate);
release(oldWeights);
}
}
/* Functions for turning sparse hashes into arrays and vice versa */
const lookup = {
/**
* Performs `[{a: 1}, {b: 6, c: 7}] -> {a: 0, b: 1, c: 2}`
* @param {Object} hashes
* @returns {Object}
*/
toTable(hashes) {
const hash = hashes.reduce((memo, hash) => {
return Object.assign(memo, hash);
}, {});
return lookup.toHash(hash);
},
/**
* Performs `[{a: 1}, {b: 6, c: 7}] -> {a: 0, b: 1, c: 2}`
*/
toTable2D(objects2D) {
const table = {};
let valueIndex = 0;
for (let i = 0; i < objects2D.length; i++) {
const objects = objects2D[i];
for (let j = 0; j < objects.length; j++) {
const object = objects[j];
for (const p in object) {
if (object.hasOwnProperty(p) && !table.hasOwnProperty(p)) {
table[p] = valueIndex++;
}
}
}
}
return table;
},
toInputTable2D(data) {
const table = {};
let tableIndex = 0;
for (let dataIndex = 0; dataIndex < data.length; dataIndex++) {
const input = data[dataIndex].input;
for (let i = 0; i < input.length; i++) {
const object = input[i];
for (const p in object) {
if (!object.hasOwnProperty(p))
continue;
if (!table.hasOwnProperty(p)) {
table[p] = tableIndex++;
}
}
}
}
return table;
},
toOutputTable2D(data) {
const table = {};
let tableIndex = 0;
for (let dataIndex = 0; dataIndex < data.length; dataIndex++) {
const output = data[dataIndex].output;
for (let i = 0; i < output.length; i++) {
const object = output[i];
for (const p in object) {
if (!object.hasOwnProperty(p))
continue;
if (!table.hasOwnProperty(p)) {
table[p] = tableIndex++;
}
}
}
}
return table;
},
/**
* performs `{a: 6, b: 7} -> {a: 0, b: 1}`
*/
toHash(hash) {
const lookup = {};
let index = 0;
const keys = Object.keys(hash);
for (let i = 0; i < keys.length; i++) {
lookup[keys[i]] = index++;
}
return lookup;
},
/**
* performs `{a: 0, b: 1}, {a: 6} -> [6, 0]`
*/
toArray(lookup, object, arrayLength) {
const result = new Float32Array(arrayLength);
for (const p in lookup) {
if (!lookup.hasOwnProperty(p))
continue;
result[lookup[p]] = object.hasOwnProperty(p) ? object[p] : 0;
}
return result;
},
toArrayShort(lookup, object) {
const result = [];
for (const p in lookup) {
if (!lookup.hasOwnProperty(p))
continue;
if (!object.hasOwnProperty(p))
break;
result[lookup[p]] = object[p];
}
return Float32Array.from(result);
},
toArrays(lookup, objects, arrayLength) {
const result = [];
for (let i = 0; i < objects.length; i++) {
result.push(this.toArray(lookup, objects[i], arrayLength));
}
return result;
},
/**
* performs `{a: 0, b: 1}, [6, 7] -> {a: 6, b: 7}`
* @param {Object} lookup
* @param {Array} array
* @returns {Object}
*/
toObject(lookup, array) {
const object = {};
for (const p in lookup) {
if (!lookup.hasOwnProperty(p))
continue;
object[p] = array[lookup[p]];
}
return object;
},
toObjectPartial(lookup, array, offset = 0, limit = 0) {
const object = {};
let i = 0;
for (const p in lookup) {
if (!lookup.hasOwnProperty(p))
continue;
if (offset > 0) {
if (i++ < offset)
continue;
}
if (limit > 0) {
if (i++ >= limit)
continue;
}
object[p] = array[lookup[p] - offset];
}
return object;
},
dataShape(data) {
const shape = [];
let lastData;
if (data.hasOwnProperty('input')) {
shape.push('datum');
lastData = data.input;
}
else if (Array.isArray(data)) {
if (data[0] &&
data[0].input) {
shape.push('array', 'datum');
lastData = data[0].input;
}
else if (Array.isArray(data[0])) {
shape.push('array');
lastData = data[0];
}
else {
lastData = data;
}
}
else {
lastData = data;
}
let p;
while (lastData) {
p = Object.keys(lastData)[0];
if (Array.isArray(lastData) ||
typeof lastData.buffer === 'object') {
shape.push('array');
const possibleNumber = lastData[parseInt(p)];
if (typeof possibleNumber === 'number') {
shape.push('number');
break;
}
else {
lastData = possibleNumber;
}
}
else if (typeof lastData === 'object' &&
typeof lastData.buffer !== 'object') {
shape.push('object');
const possibleNumber = lastData[p];
if (typeof possibleNumber === 'number') {
shape.push('number');
break;
}
else {
lastData = possibleNumber;
}
}
else {
throw new Error('unhandled signature');
}
}
return shape;
},
addKeys(value, table) {
if (Array.isArray(value))
return table;
let i = Object.keys(table).length;
for (const p in value) {
if (!value.hasOwnProperty(p))
continue;
if (table.hasOwnProperty(p))
continue;
table[p] = i++;
}
return table;
},
};
class BasePraxis {
constructor(layerTemplate, settings = {}) {
this.layerTemplate = layerTemplate;
this.settings = { ...settings };
this.kernel = null;
}
get width() {
return this.layerTemplate.width;
}
get height() {
return this.layerTemplate.height;
}
get depth() {
return this.layerTemplate.depth;
}
setupKernels() { }
reuseKernels(praxis) {
if (praxis.width !== this.width) {
throw new Error(`${this.constructor.name} kernel width mismatch ${praxis.width} is not ${this.width}`);
}
if (praxis.height !== this.height) {
throw new Error(`${this.constructor.name} kernel width mismatch ${praxis.height} is not ${this.height}`);
}
if (praxis.hasOwnProperty('kernel')) {
this.kernel = praxis.kernel;
}
}
toJSON() {
return { ...this.settings };
}
}
function update$2(weights, deltas) {
return (weights[this.thread.y][this.thread.x] +
this.constants.learningRate * deltas[this.thread.y][this.thread.x]);
}
const defaultSettings$1 = {
learningRate: 0.3,
};
class ArthurDeviationBiases extends BasePraxis {
constructor(layer, settings) {
super(layer);
this.settings = { ...defaultSettings$1, ...settings };
this.kernel = null;
}
run(layer) {
return this.kernel(layer.weights, layer.deltas);
}
setupKernels() {
this.kernel = makeKernel(update$2, {
output: [this.width, this.height],
constants: {
learningRate: this.settings.learningRate,
},
});
}
}
function arthurDeviationBiases(layer, settings) {
return new ArthurDeviationBiases(layer, settings);
}
function updateChange(value) {
return value;
}
function update$1(changes, weights, incomingWeights, inputDeltas) {
const lastChange = changes[this.thread.y][this.thread.x];
const inputDelta = inputDeltas[this.thread.y][0];
const weight = weights[this.thread.y][this.thread.x];
const incoming = incomingWeights[this.thread.x][0];
const change = this.constants.learningRate * inputDelta * incoming +
this.constants.momentum * lastChange;
return weight + change;
}
const defaultSettings = {
learningRate: 0.3,
momentum: 0.1,
weightsLayer: null,
incomingLayer: null,
deltaLayer: null,
};
class ArthurDeviationWeights extends BasePraxis {
constructor(layer, settings) {
super(layer);
this.kernelMap = null;
this.settings = { ...defaultSettings, ...settings };
this.changes = zeros2D(layer.width, layer.height);
}
get learningRate() {
return this.settings.learningRate;
}
get momentum() {
return this.settings.momentum;
}
get weightsLayer() {
return this.settings.weightsLayer;
}
set weightsLayer(layer) {
this.settings.weightsLayer = layer;
}
get deltaLayer() {
return this.settings.deltaLayer;
}
set deltaLayer(layer) {
this.settings.deltaLayer = layer;
}
get incomingLayer() {
return this.settings.incomingLayer;
}
set incomingLayer(layer) {
this.settings.incomingLayer = layer;
}
run() {
const output = this.kernelMap(this.changes, this.weightsLayer.weights, this.incomingLayer.weights, this.deltaLayer.deltas);
this.changes = output.changes;
return output.result;
}
setupKernels() {
this.kernelMap = makeKernelMap({
changes: updateChange,
}, update$1, {
output: [this.width, this.height],
constants: {
learningRate: this.learningRate,
momentum: this.momentum,
},
});
}
}
function arthurDeviationWeights(layer, settings) {
return new ArthurDeviationWeights(layer, settings);
}
function getMomentum(delta, decay, previousMomentum) {
return previousMomentum * decay + (1 - decay) * delta * delta;
}
function clipByValue(value, max, min) {
if (value > max) {
return max;
}
if (value < min) {
return min;
}
return value;
}
/**
* @description Momentum Root Mean Square Propagation Function
*/
function update(weights, deltas, previousMomenta) {
const delta = deltas[this.thread.y][this.thread.x];
const clippedDelta = clipByValue(delta, this.constants.clipValue, -this.constants.clipValue);
const weight = weights[this.thread.y][this.thread.x];
const previousMomentum = previousMomenta[this.thread.y][this.thread.x];
const momentum = getMomentum(delta, this.constants.decayRate, previousMomentum);
return (weight +
(-this.constants.learningRate * clippedDelta) /
Math.sqrt(momentum + this.constants.smoothEps) -
this.constants.regularizationStrength * weight);
}
const defaults$8 = {
decayRate: 0.999,
regularizationStrength: 0.000001,
learningRate: 0.01,
smoothEps: 1e-8,
clipValue: 5,
};
class MomentumRootMeanSquaredPropagation extends BasePraxis {
constructor(layerTemplate, settings = {}) {
super(layerTemplate);
this.kernelMap = null;
this.settings = { ...defaults$8, ...settings };
this.momenta = zeros2D(layerTemplate.width, layerTemplate.height);
}
get clipValue() {
return this.settings.clipValue;
}
get decayRate() {
return this.settings.decayRate;
}
get learningRate() {
return this.settings.learningRate;
}
get regularizationStrength() {
return this.settings.regularizationStrength;
}
get smoothEps() {
return this.settings.smoothEps;
}
run(layer) {
const { momenta, result } = this.kernelMap(layer.weights, layer.deltas, this.momenta);
release(this.momenta);
this.momenta = momenta;
return result;
}
setupKernels() {
this.kernelMap = makeKernelMap({
momenta: getMomentum,
}, update, {
output: [this.width, this.height],
constants: {
clipValue: this.clipValue,
decayRate: this.decayRate,
learningRate: this.learningRate,
regularizationStrength: this.regularizationStrength,
smoothEps: this.smoothEps,
},
functions: [clipByValue],
immutable: true,
});
}
}
function momentumRootMeanSquaredPropagation(layer, settings) {
return new MomentumRootMeanSquaredPropagation(layer, settings);
}
/**
* @description Mathematician friendly name of MomentumRootMeanSquaredPropagation class. For those that are not mere mortals
*/
const MRmsProp = MomentumRootMeanSquaredPropagation;
const mRmsProp = momentumRootMeanSquaredPropagation;
var index = /*#__PURE__*/Object.freeze({
__proto__: null,
ArthurDeviationBiases: ArthurDeviationBiases,
arthurDeviationBiases: arthurDeviationBiases,
ArthurDeviationWeights: ArthurDeviationWeights,
arthurDeviationWeights: arthurDeviationWeights,
MomentumRootMeanSquaredPropagation: MomentumRootMeanSquaredPropagation,
momentumRootMeanSquaredPropagation: momentumRootMeanSquaredPropagation,
MRmsProp: MRmsProp,
mRmsProp: mRmsProp
});
function traverseLayersFrom(layer, cb) {
if (layer.hasOwnProperty('inputLayer')) {
traverseLayersFrom(layer.inputLayer, cb);
}
else {
if (layer.hasOwnProperty('inputLayer1')) {
traverseLayersFrom(layer.inputLayer1, cb);
}
if (layer.hasOwnProperty('inputLayer2')) {
traverseLayersFrom(layer.inputLayer2, cb);
}
}
cb(layer);
}
function flattenLayers(layers) {
const result = layers.slice(0);
for (let i = 0; i < result.length; i++) {
let offset = 0;
traverseLayersFrom(result[i], (layer) => {
if (!result.includes(layer)) {
result.splice(i + offset, 0, layer);
offset++;
}
});
}
return result;
}
function checkSameSize(layer1, layer2) {
if (layer1.width !== layer2.width) {
throw new Error(`Layer width mismatch of ${layer1.width} and ${layer2.width}`);
}
if (layer1.height !== layer2.height) {
throw new Error(`Layer height mismatch of ${layer1.height} and ${layer2.height}`);
}
}
function predict$8(inputWeights1, inputWeights2) {
return (inputWeights1[this.thread.y][this.thread.x] +
inputWeights2[this.thread.y][this.thread.x]);
}
class Add extends Operator {
get width() {
return this.inputLayer1.width;
}
get height() {
return this.inputLayer1.height;
}
get depth() {
return this.inputLayer1.depth;
}
validate() {
super.validate();
checkSameSize(this.inputLayer1, this.inputLayer2);
}
setupKernels() {
this.predictKernel = makeKernel(predict$8, {
output: [this.width, this.height],
immutable: true,
});
}
predict() {
release(this.weights);
this.weights = this.predictKernel(this.inputLayer1.weights, this.inputLayer2.weights);
}
compare() {
// TODO: Do we need release and clone here?
release(this.inputLayer1.deltas);
release(this.inputLayer2.deltas);
this.inputLayer1.deltas = clone(this.deltas);
this.inputLayer2.deltas = clone(this.deltas);
}
}
function add$1(inputLayer1, inputLayer2, settings) {
return new Add(inputLayer1, inputLayer2, settings);
}
function randomWeight() {
return Math.random() * 0.4 - 0.2;
}
/**
* Returns a random float between given min and max bounds (inclusive)
* @param min Minimum value of the ranfom float
* @param max Maximum value of the random float
*/
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
/**
* Complicated math. All you need to know is that it returns a random number.
* More info: https://en.wikipedia.org/wiki/Normal_distribution
*/
function gaussRandom() {
if (gaussRandom.returnV) {
gaussRandom.returnV = false;
return gaussRandom.vVal;
}
const u = 2 * Math.random() - 1;
const v = 2 * Math.random() - 1;
const r = u * u + v * v;
if (r === 0 || r > 1) {
return gaussRandom();
}
const c = Math.sqrt((-2 * Math.log(r)) / r);
gaussRandom.vVal = v * c; // cache this
gaussRandom.returnV = true;
return u * c;
}
/**
* Returns a random integer between given min and max bounds
* @param min Minimum value of the random integer
* @param max Maximum value of the random integer
*/
function randomInteger(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
/**
* If you know what this is: https://en.wikipedia.org/wiki/Normal_distribution
* @param mu
* @param std
*/
function randomN(mu, std) {
return mu + gaussRandom() * std;
}
gaussRandom.returnV = false;
gaussRandom.vVal = 0;
var random$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
randomFloat: randomFloat,
gaussRandom: gaussRandom,
randomInteger: randomInteger,
randomN: randomN
});
/**
* Returns an array of given size, full of randomness
*/
function randos(size, std = null) {
const array = new Float32Array(size);
if (std === null) {
for (let i = 0; i < size; i++) {
array[i] = randomWeight();
}
}
else {
for (let i = 0; i < size; i++) {
array[i] = randomFloat(-std, std);
}
}
return array;
}
/**
* Returns a 2D matrix of given size, full of randomness
*/
function randos2D(width, height, std) {
const result = new Array(height);
for (let y = 0; y < height; y++) {
result[y] = randos(width, std);
}
return result;
}
/**
* Returns a 3D tensor of given size, full of randomness
*/
function randos3D(width, height, depth, std) {
const result = new Array(depth);
for (let z = 0; z < depth; z++) {
result[z] = randos2D(width, height, std);
}
return result;
}
const defaults$7 = {
...baseLayerDefaultSettings,
std: null,
};
class Random extends Model {
constructor(settings) {
super();
this.settings = { ...defaults$7, ...settings };
this.setupPraxis();
this.validate();
if (!this.weights) {
this.weights = randos2D(this.width, this.height, settings.std);
}
if (!this.deltas) {
this.deltas = zeros2D(this.width, this.height);
}
}
predict() { }
compare() { }
}
function random(settings) {
return new Random(settings);
}
function predict$7(weights1, weights2) {
let sum = 0;
for (let i = 0; i < this.constants.size; i++) {
sum += weights1[this.thread.y][i] * weights2[i][this.thread.x];
}
return sum;
}
function compareFromX(deltas, inputDeltas, inputWeights) {
let sum = inputDeltas[this.thread.y][this.thread.x];
for (let i = 0; i < this.constants.size; i++) {
sum += deltas[this.thread.y][i] * inputWeights[this.thread.x][i];
}
return sum;
}
function compareFromY(deltas, inputDeltas, inputWeights) {
let sum = inputDeltas[this.thread.y][this.thread.x];
for (let i = 0; i < this.constants.size; i++) {
sum += deltas[i][this.thread.x] * inputWeights[i][this.thread.y];
}
return sum;
}
class Multiply extends Operator {
constructor() {
super(...arguments);
this.compareKernel1 = null;
this.compareKernel2 = null;
}
get width() {
return this.inputLayer2.width;
}
set width(width) {
throw new Error('Cannot set width on Multiply');
}
get height() {
return this.inputLayer1.height;
}
set height(height) {
throw new Error('Cannot set height on Multiply');
}
get depth() {
return this.inputLayer1.depth;
}
set depth(depth) {
throw new Error('Cannot set depth on Multiply');
}
validate() {
super.validate();
if (this.inputLayer1.width !== this.inputLayer2.height) {
throw new Error(`Layer width mismatch of ${this.inputLayer1.width} and ${this.inputLayer2.height}`);
}
}
setupKernels() {
this.predictKernel = makeKernel(predict$7, {
output: [this.width, this.height],
constants: {
size: this.inputLayer2.height,
},
immutable: true,
});
this.compareKernel1 = makeKernel(compareFromX, {
output: [this.inputLayer1.width, this.inputLayer1.height],
constants: {
size: this.inputLayer2.width,
},
immutable: true,
});
this.compareKernel2 = makeKernel(compareFromY, {
output: [this.inputLayer2.width, this.inputLayer2.height],
constants: {
size: this.inputLayer1.height,
},
immutable: true,
});
}
reuseKernels(layer) {
super.reuseKernels(layer);
this.compareKernel1 = layer.compareKernel1;
this.compareKernel2 = layer.compareKernel2;
}
predict() {
release(this.weights);
if (!this.predictKernel)
throw new Error('this.predictKernel is not set');
this.weights = this.predictKernel(this.inputLayer1.weights, this.inputLayer2.weights);
}
compare() {
if (!this.compareKernel1)
throw new Error('this.compareKernel1 not set');
if (!this.compareKernel2)
throw new Error('this.compareKernel2 not set');
const inputLayer1Deltas = this.inputLayer1.deltas;
const inputLayer2Deltas = this.inputLayer2.deltas;
const newDeltas1 = this.compareKernel1(this.deltas, this.inputLayer1.deltas, this.inputLayer2.weights);
const newDeltas2 = this.compareKernel2(this.deltas, this.inputLayer2.deltas, this.inputLayer1.weights);
this.inputLayer2.deltas = newDeltas2;
this.inputLayer1.deltas = newDeltas1;
release(inputLayer1Deltas);
release(inputLayer2Deltas);
}
setupPraxis() { }
toJSON() {
return {
...super.toJSON(),
width: this.width,
height: this.height,
};
}
}
function multiply$1(inputLayer1, inputLayer2, settings) {
return new Multiply(inputLayer1, inputLayer2, settings);
}
function predict2D$4(inputs) {
return 1 / (1 + Math.exp(-inputs[this.thread.y][this.thread.x]));
}
function predict3D$5(inputs) {
return (1 / (1 + Math.exp(-inputs[this.thread.z][this.thread.y][this.thread.x])));
}
function compare2D$4(weights, deltas) {
const weight = weights[this.thread.y][this.thread.x];
const delta = deltas[this.thread.y][this.thread.x];
return weight * (1 - weight) * delta;
}
function compare3D$4(weights, deltas) {
const weight = weights[this.thread.z][this.thread.y][this.thread.x];
const delta = deltas[this.thread.z][this.thread.y][this.thread.x];
return weight * (1 - weight) * delta;
}
class Sigmoid extends Activation {
setupKernels() {
if (this.depth > 0) {
this.predictKernel = makeKernel(predict3D$5, {
output: [this.width, this.height, this.depth],
functions: [activate$2],
immutable: true,
});
this.compareKernel = makeKernel(compare3D$4, {
output: [this.width, this.height, this.depth],
functions: [measure$2],
immutable: true,
});
}
else {
this.predictKernel = makeKernel(predict2D$4, {
output: [this.width, this.height],
functions: [activate$2],
immutable: true,
});
this.compareKernel = makeKernel(compare2D$4, {
output: [this.width, this.height],
functions: [measure$2],
immutable: true,
});
}
}
predict() {
release(this.weights);
this.weights = this.predictKernel(this.inputLayer.weights);
}
compare() {
release(this.inputLayer.deltas);
this.inputLayer.deltas = this.compareKernel(this.weights, this.deltas);
}
learn(learningRate) { }
}
function sigmoid$1(inputLayer, settings) {
return new Sigmoid(inputLayer, settings);
}
function arthurFeedForward(settings, inputLayer) {
const { height } = settings;
function initWeightsPraxis(layerTemplate, settings) {
const praxis = arthurDeviationWeights(layerTemplate, settings);
praxis.setupKernels();
return praxis;
}
function initBiasesPraxis(layerTemplate, settings) {
const praxis = arthurDeviationBiases(layerTemplate, settings);
praxis.setupKernels();
return praxis;
}
const weightsLayer = random({
id: 'weights',
height,
width: inputLayer.height,
initPraxis: initWeightsPraxis,
});
const biasesLayer = random({
id: 'biases',
height,
initPraxis: initBiasesPraxis,
});
const multiplyLayer = multiply$1(weightsLayer, inputLayer);
const addLayer = add$1(multiplyLayer, biasesLayer);
const sigmoidLayer = sigmoid$1(addLayer);
const weightsPraxis = weightsLayer.praxis;
weightsPraxis.weightsLayer = weightsLayer;
weightsPraxis.incomingLayer = inputLayer;
weightsPraxis.deltaLayer = sigmoidLayer;
return sigmoidLayer;
}
function getStride(settings, defaults) {
if (typeof settings.stride === 'number') {
return { strideX: settings.stride, strideY: settings.stride };
}
else {
let strideX = defaults.stride;
let strideY = defaults.stride;
if (typeof settings.strideX === 'number') {
strideX = settings.strideX;
}
if (typeof settings.strideY === 'number') {
strideY = settings.strideY;
}
return { strideX, strideY };
}
}
function getPadding(settings, defaults) {
if (typeof settings.padding === 'number') {
return { paddingX: settings.padding, paddingY: settings.padding };
}
else {
let paddingX = defaults.padding;
let paddingY = defaults.padding;
if (typeof settings.paddingX === 'number') {
paddingX = settings.paddingX;
}
if (typeof settings.paddingY === 'number') {
paddingY = settings.paddingY;
}
return { paddingX, paddingY };
}
}
/**
* Returns an array of a given size with each element filled with a single value
*/
function values(size, value) {
return new Float32Array(size).fill(value);
}
function predict$6(inputs, filters, biases) {
const startFilterX = this.constants.paddingX - this.thread.x * this.constants.strideX;
const startInputX = this.thread.x * this.constants.strideX - this.constants.paddingX;
const endFilterX = Math.min(this.constants.filterWidth, startFilterX + this.constants.inputWidth);
const startFilterY = this.constants.paddingY - this.thread.y * this.constants.strideY;
const startInputY = this.thread.y * this.constants.strideY - this.constants.paddingY;
const endFilterY = Math.min(this.constants.filterHeight, startFilterY + this.constants.inputHeight);
let sum = 0;
for (let z = 0; z < this.constants.inputDepth; z++) {
for (let filterY = Math.max(0, startFilterY), inputY = Math.max(0, startInputY); filterY < endFilterY; filterY++, inputY++) {
for (let filterX = Math.max(0, startFilterX), inputX = Math.max(0, startInputX); filterX < endFilterX; filterX++, inputX++) {
sum += filters[z][filterY][filterX] * inputs[z][inputY][inputX];
}
}
}
return sum + biases[this.thread.z];
}
function compareFilterDeltas$1(filterDeltas, inputs, deltas) {
const startDeltaX = Math.max(0, Math.ceil((this.constants.paddingX - this.thread.x) / this.constants.strideX));
const startInputX = startDeltaX * this.constants.strideX +
this.thread.x -
this.constants.paddingX;
const endDeltaX = Math.min(this.constants.deltaWidth, Math.floor((this.constants.inputWidth -
1 -
this.thread.x +
this.constants.paddingX) /
this.constants.strideX) + 1);
const startDeltaY = Math.max(0, Math.ceil((this.constants.paddingY - this.thread.y) / this.constants.strideY));
const startInputY = startDeltaY * this.constants.strideY +
this.thread.y -
this.constants.paddingY;
const endDeltaY = Math.min(this.constants.deltaHeight, Math.floor((this.constants.inputHeight -
1 -
this.thread.y +
this.constants.paddingY) /
this.constants.strideY) + 1);
let sum = filterDeltas[this.thread.z][this.thread.y][this.thread.x];
for (let deltaY = startDeltaY, inputY = startInputY; deltaY < endDeltaY; deltaY++, inputY += this.constants.strideY) {
for (let deltaX = startDeltaX, inputX = startInputX; deltaX < endDeltaX; deltaX++, inputX += this.constants.strideX) {
sum +=
inputs[this.thread.z][inputY][inputX] *
deltas[this.constants.deltaZ][deltaY][deltaX];
}
}
return sum;
}
function compareInputDeltas$1(inputDeltas, filters, deltas) {
const x = this.thread.x + this.constants.paddingX;
const startDeltaX = x < this.constants.filterWidth
? 0
: Math.floor((x - this.constants.filterWidth + this.constants.strideX) /
this.constants.strideX);
const startFilterX = x - startDeltaX * this.constants.strideX;
const endDeltaX = Math.min(startDeltaX + Math.floor(startFilterX / this.constants.strideX) + 1, this.constants.deltaWidth);
const y = this.thread.y + this.constants.paddingY;
const startDeltaY = y < this.constants.filterHeight
? 0
: Math.floor((y - this.constants.filterHeight + this.constants.strideY) /
this.constants.strideY);
const startFilterY = y - startDeltaY * this.constants.strideY;
const endDeltaY = Math.min(startDeltaY + Math.floor(startFilterY / this.constants.strideY) + 1, this.constants.deltaHeight);
let sum = inputDeltas[this.thread.z][this.thread.y][this.thread.x];
let deltaY = startDeltaY;
for (let filterY = startFilterY; deltaY < endDeltaY; filterY -= this.constants.strideY, deltaY++) {
let deltaX = startDeltaX;
for (let filterX = startFilterX; deltaX < endDeltaX; filterX -= this.constants.strideX, deltaX++) {
sum +=
filters[this.thread.z][filterY][filterX] *
deltas[this.constants.deltaZ][deltaY][deltaX];
}
}
return sum;
}
function compareBiases$1(biasDeltas, deltas) {
let sum = 0;
for (let y = 0; y < this.constants.deltaHeight; y++) {
for (let x = 0; x < this.constants.deltaWidth; x++) {
sum += deltas[this.thread.z][y][x];
}
}
return biasDeltas[this.thread.z][this.thread.y][this.thread.x] + sum;
}
const defaults$6 = {
stride: 0,
padding: 0,
bias: 0.1,
filterCount: 1,
filterWidth: 0,
filterHeight: 0,
};
class Convolution extends Filter {
constructor(settings, inputLayer) {
var _a, _b, _c;
super(settings, inputLayer);
this.compareFilterDeltasKernel = null;
this.compareInputDeltasKernel = null;
this.compareBiasesKernel = null;
this.settings = {
...defaults$6,
...settings,
...getPadding(settings, defaults$6),
...getStride(settings, defaults$6),
};
this.weights = (_a = settings.weights) !== null && _a !== void 0 ? _a : randos3D(this.width, this.height, this.depth);
this.deltas = zeros3D(this.width, this.height, this.depth);
this.biases = values(this.depth, this.bias);
this.biasDeltas = (_b = settings.biasDeltas) !== null && _b !== void 0 ? _b : randos(this.depth);
this.filters = (_c = settings.filters) !== null && _c !== void 0 ? _c : randos3D(this.filterWidth, this.filterHeight, this.filterCount);
this.filterDeltas = zeros3D(this.filterWidth, this.filterHeight, this.filterCount);
this.validate();
}
get strideX() {
return this.settings.strideX;
}
get strideY() {
return this.settings.strideY;
}
get paddingX() {
return this.settings.paddingX;
}
get paddingY() {
return this.settings.paddingX;
}
get width() {
return Math.floor((this.inputLayer.width + this.paddingX * 2 - this.filterWidth) /
this.strideX +
1);
}
get height() {
return Math.floor((this.inputLayer.height + this.paddingY * 2 - this.filterHeight) /
this.strideY +
1);
}
get bias() {
return this.settings.bias;
}
get depth() {
return this.filterCount;
}
get biases() {
return this.settings.biases;
}
set biases(biases) {
this.settings.biases = biases;
}
get biasDeltas() {
return this.settings.biasDeltas;
}
set biasDeltas(weights) {
this.settings.biasDeltas = weights;
}
get filters() {
return this.settings.filters;
}
set filters(filters) {
this.settings.filters = filters;
}
get filterDeltas() {
return this.settings.filterDeltas;
}
set filterDeltas(filterDeltas) {
this.settings.filterDeltas = filterDeltas;
}
setupKernels() {
this.predictKernel = makeKernel(predict$6, {
constants: {
inputWidth: this.inputLayer.width,
inputHeight: this.inputLayer.height,
inputDepth: this.inputLayer.depth,
strideX: this.strideX,
strideY: this.strideY,
paddingX: this.paddingX,
paddingY: this.paddingY,
filterWidth: this.filterWidth,
filterHeight: this.filterHeight,
},
output: [this.width, this.height, this.depth],
immutable: true,
});
this.compareFilterDeltasKernel = makeKernel(compareFilterDeltas$1, {
constants: {
deltasWidth: this.width,
deltasHeight: this.height,
deltasDepth: this.depth,
inputWidth: this.inputLayer.width,
inputHeight: this.inputLayer.height,
inputDepth: this.inputLayer.depth,
strideX: this.strideX,
strideY: this.strideY,
paddingX: this.paddingX,
paddingY: this.paddingY,
filterWidth: this.filterWidth,
filterHeight: this.filterHeight,
},
output: [this.width, this.height, this.depth],
immutable: true,
});
this.compareInputDeltasKernel = makeKernel(compareInputDeltas$1, {
constants: {
filterCount: this.filterCount,
},
output: [
this.inputLayer.width,
this.inputLayer.height,
this.inputLayer.depth,
],
immutable: true,
});
this.compareBiasesKernel = makeKernel(compareBiases$1, {
output: [1, 1, this.depth],
constants: {
deltaWidth: this.width,
deltaHeight: this.height,
},
immutable: true,
});
}
predict() {
this.weights = this.predictKernel(this.inputLayer.weights, this.filters, this.biases);
}
compare() {
const { filterDeltas, biasDeltas } = this;
this.filterDeltas = this.compareFilterDeltasKernel(filterDeltas, this.inputLayer.weights, this.deltas);
release(filterDeltas);
this.biasDeltas = this.compareBiasesKernel(biasDeltas, this.deltas);
release(biasDeltas);
release(this.deltas);
this.deltas = this.compareInputDeltasKernel(this.filters, this.inputLayer.deltas);
release(this.inputLayer.deltas);
// TODO: do we need to clone here?
this.inputLayer.deltas = clone(this.deltas);
}
learn(learningRate) {
// TODO: handle filters
// TODO: do we need to release here?
const { weights: oldWeights } = this;
this.weights = this.praxis.run(this, learningRate);
release(oldWeights);
}
}
function convolution(settings, inputLayer) {
return new Convolution(settings, inputLayer);
}
function setDropout(dropout) {
return dropout;
}
function trainingPredict(inputs) {
if (setDropout(Math.random()) < this.constants.probability) {
return 0;
}
return inputs[this.thread.y][this.thread.x];
}
function predict$5(inputs) {
return inputs[this.thread.y][this.thread.x] * this.constants.probability;
}
function compare$3(dropouts, deltas) {
if (dropouts[this.thread.y][this.thread.x] === 0) {
return 0;
}
return deltas[this.thread.y][this.thread.x];
}
const dropoutDefaults = {
...baseLayerDefaultSettings,
probability: 0.5,
};
class Dropout extends Filter {
constructor(inputLayer, settings) {
super(settings, inputLayer);
this.predictKernelMap = null;
this.settings = { ...dropoutDefaults, ...settings };
this.dropouts = null;
this.validate();
}
setupKernels(isTraining) {
const output = [this.width, this.height];
if (isTraining) {
this.predictKernelMap = makeKernelMap({ dropouts: setDropout }, trainingPredict, {
output,
immutable: true,
});
this.compareKernel = makeKernel(compare$3, { output, immutable: true });
}
else {
this.predictKernelMap = makeKernelMap({}, predict$5, { output, immutable: true });
}
}
predict() {
release(this.weights);
if (this.dropouts) {
release(this.dropouts);
}
const { result, dropouts } = this
.predictKernelMap(this.inputLayer.weights);
this.weights = result;
this.dropouts = dropouts;
}
compare() {
release(this.deltas);
this.deltas = this.compareKernel(this.dropouts, this.inputLayer.deltas);
}
}
function dropout(inputLayer, settings) {
return new Dropout(inputLayer, settings);
}
function feedForward(settings, input) {
const { height, praxisOpts = null } = settings;
const weights = random({
id: 'weights',
height,
width: input.height,
praxisOpts,
});
const biases = random({ id: 'biases', height, praxisOpts });
return sigmoid$1(add$1(multiply$1(weights, input, { praxisOpts }), biases, { praxisOpts }), { praxisOpts });
}
function predict$4(inputs, filters, biases) {
let output = 0;
let i = 0;
for (let y = 0; y < this.constants.inputHeight; y++) {
for (let x = 0; x < this.constants.inputWidth; x++) {
output += inputs[y][x] * filters[this.thread.x][i];
i++;
}
}
return output + biases[this.thread.x];
}
function predict3D$4(inputs, filters, biases) {
let output = 0;
let i = 0;
for (let z = 0; z < this.constants.inputDepth; z++) {
for (let y = 0; y < this.constants.inputHeight; y++) {
for (let x = 0; x < this.constants.inputWidth; x++) {
output += inputs[z][y][x] * filters[this.thread.x][i];
i++;
}
}
}
return output + biases[this.thread.x];
}
function compareInputDeltas(inputDeltas, deltas, filters) {
let sum = 0;
const filterX = this.thread.x + this.thread.y * this.output.x;
for (let filterY = 0; filterY < this.constants.filterCount; filterY++) {
sum += filters[filterY][filterX] * deltas[0][filterY];
}
return sum + inputDeltas[this.thread.y][this.thread.x];
}
function compareInputDeltas3D(inputDeltas, deltas, filters) {
let sum = 0;
const filterX = this.thread.x + this.thread.y * this.output.x;
for (let filterY = 0; filterY < this.constants.filterCount; filterY++) {
sum += filters[filterY][filterX] * deltas[0][filterY];
}
return sum + inputDeltas[this.thread.z][this.thread.y][this.thread.x];
}
function compareBiases(biases, deltas) {
return biases[this.thread.x] + deltas[this.thread.y][this.thread.x];
}
function compareFilterDeltas(filterDeltas, inputWeights, deltas) {
return (filterDeltas[this.thread.y][this.thread.x] +
inputWeights[this.thread.y][this.thread.x] *
deltas[this.constants.deltaY][this.constants.deltaX]);
}
function compareFilterDeltas3D(filterDeltas, inputWeights, deltas) {
const inputZ = Math.floor(this.thread.x / (this.constants.inputWidth * this.constants.inputHeight));
const inputY = Math.floor((this.thread.x -
inputZ * this.constants.inputWidth * this.constants.inputHeight) /
this.constants.inputWidth);
const inputX = this.thread.x -
this.constants.inputWidth * (inputY + this.constants.inputHeight * inputZ);
return (filterDeltas[this.thread.y][this.thread.x] +
inputWeights[inputZ][inputY][inputX] * deltas[0][this.thread.y]);
}
class FullyConnected extends Filter {
constructor(settings, inputLayer) {
super(settings, inputLayer);
this.compareFilterDeltasKernel = null;
this.compareInputDeltasKernel = null;
this.compareBiasesKernel = null;
this.settings = { ...settings };
this.validate();
const connectionCount = inputLayer.width * inputLayer.height * inputLayer.depth;
this.biases = values(this.height, this.bias);
this.biasDeltas = zeros$1(this.height);
this.filters = randos2D(connectionCount, this.height);
this.filterDeltas = zeros2D(connectionCount, this.height);
if (this.depth > 0) {
this.weights = randos3D(this.width, this.height, this.depth);
this.deltas = zeros3D(this.width, this.height, this.depth);
}
else if (this.height > 0) {
this.weights = randos2D(this.width, this.height);
this.deltas = zeros2D(this.width, this.height);
}
}
get bias() {
return this.settings.bias;
}
get biases() {
return this.settings.biases;
}
set biases(biases) {
this.settings.biases = biases;
}
get biasDeltas() {
return this.settings.biases;
}
set biasDeltas(biasDeltas) {
this.settings.biasDeltas = biasDeltas;
}
validate() {
super.validate();
if (this.depth > 0)
throw new Error('depth not supported');
}
setupKernels() {
const { inputLayer } = this;
const connectionCount = inputLayer.width * inputLayer.height * inputLayer.depth;
if (inputLayer.depth > 0) {
this.predictKernel = makeKernel(predict3D$4, {
output: [this.width, this.height],
constants: {
inputHeight: inputLayer.height,
inputWidth: inputLayer.width,
inputDepth: inputLayer.depth,
},
});
this.compareFilterDeltasKernel = makeKernel(compareFilterDeltas3D, {
output: [connectionCount, this.height],
constants: {
inputWidth: inputLayer.width,
inputHeight: inputLayer.height,
},
immutable: true,
});
this.compareInputDeltasKernel = makeKernel(compareInputDeltas3D, {
output: [inputLayer.width, inputLayer.height, inputLayer.depth],
constants: {
filterCount: this.height,
},
immutable: true,
});
}
else {
this.predictKernel = makeKernel(predict$4, {
output: [this.width, this.height],
constants: {
inputHeight: inputLayer.height,
inputWidth: inputLayer.width,
},
});
this.compareFilterDeltasKernel = makeKernel(compareFilterDeltas, {
output: [connectionCount, this.height],
constants: {
inputWidth: inputLayer.width,
},
});
this.compareInputDeltasKernel = makeKernel(compareInputDeltas, {
output: [inputLayer.width, inputLayer.height],
constants: {
filterCount: this.height,
},
});
}
this.compareBiasesKernel = makeKernel(compareBiases, {
output: [this.width, this.height],
});
}
predict() {
this.weights = this.predictKernel(this.inputLayer.weights, this.filters, this.biases);
}
compare() {
const inputLayerDeltas = this.inputLayer.deltas;
this.inputLayer.deltas = this
.compareInputDeltasKernel(inputLayerDeltas, this.deltas, this.filters);
release(inputLayerDeltas);
const { biasDeltas, filterDeltas } = this;
// TODO: handle biasDeltas learn
this.biasDeltas = this.compareBiasesKernel(this.biases, this.deltas);
// TODO: handle filterDeltas learn
this.filterDeltas = this.compareFilterDeltasKernel(filterDeltas, this.inputLayer.weights, this.deltas);
release(biasDeltas);
release(filterDeltas);
}
}
function fullyConnected(settings, inputLayer) {
return new FullyConnected(settings, inputLayer);
}
function predict$3(weights) {
return -weights[this.thread.y][this.thread.x];
}
class Negative extends Modifier {
constructor(inputLayer, settings) {
super(inputLayer, settings);
this.validate();
}
setupKernels() {
this.predictKernel = makeKernel(predict$3, {
output: [this.width, this.height],
});
}
predict() {
this.weights = this.predictKernel(this.inputLayer.weights);
}
}
function negative(inputLayer, settings) {
return new Negative(inputLayer, settings);
}
function predict$2(inputLayerWeights1, inputLayerWeights2) {
return (inputLayerWeights1[this.thread.y][this.thread.x] *
inputLayerWeights2[this.thread.y][this.thread.x]);
}
function compare$2(weights, deltas) {
return (weights[this.thread.y][this.thread.x] * deltas[this.thread.y][this.thread.x]);
}
class MultiplyElement extends Operator {
get width() {
return this.inputLayer1.width;
}
get height() {
return this.inputLayer1.height;
}
get depth() {
return this.inputLayer1.depth;
}
validate() {
super.validate();
checkSameSize(this.inputLayer1, this.inputLayer2);
}
setupKernels() {
this.predictKernel = makeKernel(predict$2, {
output: [this.width, this.height],
immutable: true,
});
this.compareKernel = makeKernel(compare$2, {
output: [this.width, this.height],
immutable: true,
});
}
predict() {
release(this.weights);
this.weights = this.predictKernel(this.inputLayer1.weights, this.inputLayer2.weights);
}
compare() {
release(this.inputLayer1.deltas);
release(this.inputLayer2.deltas);
this.inputLayer1.deltas = this.compareKernel(this.inputLayer2.weights, this.deltas);
this.inputLayer2.deltas = this.compareKernel(this.inputLayer1.weights, this.deltas);
}
}
function multiplyElement$1(inputLayer1, inputLayer2, settings) {
return new MultiplyElement(inputLayer1, inputLayer2, settings);
}
function ones$1(size) {
return new Float32Array(size).fill(1);
}
function ones2D(width, height) {
const result = new Array(height);
for (let y = 0; y < height; y++) {
result[y] = ones$1(width);
}
return result;
}
class Ones extends Model {
constructor(settings) {
super(settings);
this.validate();
this.weights = ones2D(this.width, this.height);
this.deltas = zeros2D(this.width, this.height);
}
}
function ones(settings) {
return new Ones(settings);
}
function predict2D$3(inputs) {
return activate$1(inputs[this.thread.y][this.thread.x]);
}
function predict3D$3(inputs) {
return activate$1(inputs[this.thread.z][this.thread.y][this.thread.x]);
}
function compare2D$3(weights, errors) {
return measure$1(weights[this.thread.y][this.thread.x], errors[this.thread.y][this.thread.x]);
}
function compare3D$3(weights, errors) {
return measure$1(weights[this.thread.z][this.thread.y][this.thread.x], errors[this.thread.z][this.thread.y][this.thread.x]);
}
class Tanh extends Activation {
setupKernels() {
if (this.depth > 0) {
this.predictKernel = makeKernel(predict3D$3, {
output: [this.width, this.height, this.depth],
functions: [activate$1],
immutable: true,
});
this.compareKernel = makeKernel(compare3D$3, {
output: [this.width, this.height, this.depth],
functions: [measure$1],
immutable: true,
});
}
else {
this.predictKernel = makeKernel(predict2D$3, {
output: [this.width, this.height],
functions: [activate$1],
immutable: true,
});
this.compareKernel = makeKernel(compare2D$3, {
output: [this.width, this.height],
functions: [measure$1],
immutable: true,
});
}
}
predict() {
release(this.weights);
this.weights = this.predictKernel(this.inputLayer.weights);
}
compare() {
release(this.inputLayer.deltas);
this.inputLayer.deltas = this.compareKernel(this.weights, this.deltas);
}
}
function tanh$1(inputLayer, settings) {
return new Tanh(inputLayer, settings);
}
class Zeros extends Model {
constructor(settings) {
super(settings);
this.validate();
this.weights = zeros2D(this.width, this.height);
this.deltas = zeros2D(this.width, this.height);
}
predict() {
// throw new Error(`${this.constructor.name}-predict is not yet implemented`)
}
compare() {
// throw new Error(`${this.constructor.name}-compare is not yet implemented`)
}
}
function zeros(settings) {
return new Zeros(settings);
}
function gru(settings, recurrentInput, input) {
const { height } = settings;
const updateGateWeights = random({ height, width: input.height });
const updateGatePeepholes = random({ width: height, height });
const updateGateBias = zeros({ height });
const updateGate = sigmoid$1(add$1(add$1(multiply$1(updateGateWeights, input), multiply$1(updateGatePeepholes, recurrentInput)), updateGateBias));
const resetGateWeights = random({ height, width: input.height });
const resetGatePeepholes = random({ width: height, height });
const resetGateBias = zeros({ height });
const resetGate = sigmoid$1(add$1(add$1(multiply$1(resetGateWeights, input), multiply$1(resetGatePeepholes, recurrentInput)), resetGateBias));
const cellWeights = random({ height, width: input.height });
const cellPeepholes = random({ width: height, height });
const cellBias = zeros({ height });
const cell = tanh$1(add$1(add$1(multiply$1(cellWeights, input), multiply$1(cellPeepholes, multiplyElement$1(resetGate, recurrentInput))), cellBias));
// compute hidden state as gated, saturated cell activations
// negate updateGate
return add$1(multiplyElement$1(add$1(ones({ width: updateGate.width, height: updateGate.height }), negative(updateGate)), cell), multiplyElement$1(recurrentInput, updateGate));
}
const defaults$5 = {
weights: null,
};
class Input extends EntryPoint {
constructor(settings) {
super({ ...defaults$5, ...settings });
this.reshapeInput = null;
this.validate();
this.reshapeInput = null;
this.deltas = zeros2D(this.width, this.height);
}
setupKernels() {
if (this.width === 1) {
this.predict = this.predict1D;
this.reshapeInput = makeKernel(function (value) {
return value[this.thread.y];
}, {
output: [1, this.height],
immutable: true,
});
}
}
reuseKernels(layer) {
// super.reuseKernels(layer);
this.reshapeInput = layer.reshapeInput;
}
predict(inputs) {
if ((Array.isArray(inputs) || inputs instanceof Float32Array) &&
typeof inputs[0] === 'number' &&
inputs.length === this.height * this.width) {
release(this.weights);
this.weights = kernelInput(inputs, [this.width, this.height]);
}
else if (Array.isArray(inputs) &&
inputs.length === this.height &&
(Array.isArray(inputs[0]) || inputs[0] instanceof Float32Array) &&
inputs[0].length === this.width) {
this.weights = clone(inputs);
}
else {
throw new Error('Inputs are not of sized correctly');
}
}
predict1D(inputs) {
if (this.weights)
release(this.weights);
if (this.reshapeInput) {
this.weights = this.reshapeInput(inputs);
}
else {
this.weights = inputs;
}
}
compare() {
// throw new Error(`${this.constructor.name}-compare is not yet implemented`)
}
learn() { }
}
function input(settings) {
return new Input(settings);
}
function predict2D$2(inputs) {
return activate(inputs[this.thread.y][this.thread.x]);
}
function predict3D$2(inputs) {
return activate(inputs[this.thread.z][this.thread.y][this.thread.x]);
}
function compare2D$2(weights, deltas) {
return measure(weights[this.thread.y][this.thread.x], deltas[this.thread.y][this.thread.x]);
}
function compare3D$2(weights, deltas) {
return measure(weights[this.thread.z][this.thread.y][this.thread.x], deltas[this.thread.z][this.thread.y][this.thread.x]);
}
class LeakyRelu extends Activation {
setupKernels() {
const { width, height, depth } = this.inputLayer;
if (this.depth > 0) {
this.predictKernel = makeKernel(predict3D$2, {
output: [width, height, depth],
functions: [activate],
immutable: true,
});
this.compareKernel = makeKernel(compare3D$2, {
output: [width, height, depth],
functions: [measure],
immutable: true,
});
}
else {
this.predictKernel = makeKernel(predict2D$2, {
output: [width, height],
functions: [activate],
immutable: true,
});
this.compareKernel = makeKernel(compare2D$2, {
output: [width, height],
functions: [measure],
immutable: true,
});
}
}
predict() {
release(this.weights);
this.weights = this.predictKernel(this.inputLayer.weights);
}
compare() {
const { deltas } = this;
this.deltas = this.compareKernel(this.weights, deltas);
release(deltas);
}
}
function leakyRelu(inputLayer, settings) {
return new LeakyRelu(inputLayer, settings);
}
function lstmCell(settings, input, recurrentInput) {
const { height } = settings;
if (typeof height !== 'number') {
throw new Error('no settings.height given');
}
if (recurrentInput.setDimensions) {
recurrentInput.setDimensions(1, height);
}
const inputGateWeights = random({
width: input.height,
height,
std: 0.08,
id: 'inputGateWeights',
});
const inputGatePeepholes = random({
width: height,
height,
std: 0.08,
id: 'inputGatePeepholes',
});
const inputGateBias = zeros({ width: 1, height, id: 'inputGateBias' });
const inputGate = sigmoid$1(add$1(add$1(multiply$1(inputGateWeights, input), multiply$1(inputGatePeepholes, recurrentInput)), inputGateBias), { id: 'inputGate' });
const forgetGateWeights = random({
width: input.height,
height,
std: 0.08,
id: 'forgetGateWeights',
});
const forgetGatePeepholes = random({
width: height,
height,
std: 0.08,
id: 'forgetGatePeepholes',
});
const forgetGateBias = zeros({ width: 1, height, id: 'forgetGateBias' });
const forgetGate = sigmoid$1(add$1(add$1(multiply$1(forgetGateWeights, input), multiply$1(forgetGatePeepholes, recurrentInput)), forgetGateBias), { id: 'forgetGate' });
const outputGateWeights = random({
width: input.height,
height,
std: 0.08,
id: 'outputGateWeights',
});
const outputGatePeepholes = random({
width: height,
height,
std: 0.08,
id: 'outputGatePeepholes',
});
const outputGateBias = zeros({ width: 1, height, id: 'outputGateBias' });
const outputGate = sigmoid$1(add$1(add$1(multiply$1(outputGateWeights, input), multiply$1(outputGatePeepholes, recurrentInput)), outputGateBias), { id: 'outputGate' });
const memoryWeights = random({
width: input.height,
height,
std: 0.08,
id: 'memoryWeights',
});
const memoryPeepholes = random({
width: height,
height,
std: 0.08,
id: 'memoryPeepholes',
});
const memoryBias = zeros({ width: 1, height, id: 'memoryBias' });
const memory = tanh$1(add$1(add$1(multiply$1(memoryWeights, input), multiply$1(memoryPeepholes, recurrentInput)), memoryBias), { id: 'memory' });
// compute new cell activation
const retainCell = multiplyElement$1(forgetGate, recurrentInput, {
id: 'retainCell',
}); // what do we keep from cell
const writeCell = multiplyElement$1(inputGate, memory, { id: 'writeCell' }); // what do we write to cell
const cell = add$1(retainCell, writeCell, { id: 'cell' }); // new cell contents
// compute hidden state as gated, saturated cell activations
return multiplyElement$1(outputGate, tanh$1(cell), { id: 'activations' });
}
function output(settings, inputLayer) {
const { height } = settings;
const outputGate = random({
height,
width: inputLayer.height,
id: 'outputGate',
std: 0.08,
});
const output = random({ height, id: 'output', std: 0.08 });
const outputGateConnector = multiply$1(outputGate, inputLayer, {
id: 'outputGateConnected',
});
return target({ id: 'target', ...settings }, add$1(outputGateConnector, output));
}
function setSwitchY(value) {
return value;
}
function setSwitchX(value) {
return value;
}
function predict$1(inputs) {
const startFilterX = this.constants.paddingX - this.thread.x * this.constants.strideX;
const startInputX = this.thread.x * this.constants.strideX - this.constants.paddingX;
const endFilterX = Math.min(this.constants.filterWidth, startFilterX + this.constants.inputWidth);
const startFilterY = this.constants.paddingY - this.thread.y * this.constants.strideY;
const startInputY = this.thread.y * this.constants.strideY - this.constants.paddingY;
const endFilterY = Math.min(this.constants.filterHeight, startFilterY + this.constants.inputHeight);
let largestValue = -99999;
// convolve centered at this particular location
for (let filterY = Math.max(0, startFilterY), inputY = Math.max(0, startInputY); filterY < endFilterY; filterY++, inputY++) {
for (let filterX = Math.max(0, startFilterX), inputX = Math.max(0, startInputX); filterX < endFilterX; filterX++, inputX++) {
if (inputY >= 0 &&
inputY < this.constants.inputHeight &&
inputX >= 0 &&
inputX < this.constants.inputWidth) {
const input = inputs[this.thread.z][inputY][inputX];
if (input > largestValue) {
largestValue = input;
}
}
}
}
return largestValue;
}
function compare$1(deltas, switchY, switchX) {
const x = Math.floor((this.thread.x / this.output.x) * this.constants.outputWidth);
const y = Math.floor((this.thread.y / this.output.y) * this.constants.outputHeight);
let value = 0;
for (let deltasY = 0; deltasY < this.constants.inputHeight; deltasY++) {
for (let deltasX = 0; deltasX < this.constants.inputWidth; deltasX++) {
const switchXValue = switchX[deltasY][deltasX];
const switchYValue = switchY[deltasY][deltasX];
if (switchXValue === x && switchYValue === y) {
value += deltas[deltasY][deltasX];
}
}
}
return value;
}
const defaults$4 = {
padding: 0,
stride: 0,
filterWidth: 0,
filterHeight: 0,
filterCount: 0,
};
class Pool extends Filter {
constructor(settings, inputLayer) {
super(settings, inputLayer);
this.predictKernelMap = null;
this.settings = {
...settings,
...getStride(settings, defaults$4),
...getPadding(settings, defaults$4),
};
this.weights = randos3D(this.width, this.height, this.depth);
this.deltas = zeros3D(this.width, this.height, this.depth);
this.filters = randos3D(this.filterWidth, this.filterHeight, this.filterCount);
this.filterDeltas = zeros3D(this.filterWidth, this.filterHeight, this.filterCount);
this.validate();
}
get strideX() {
return this.settings.strideX;
}
get strideY() {
return this.settings.strideY;
}
get paddingX() {
return this.settings.paddingX;
}
get paddingY() {
return this.settings.paddingY;
}
get width() {
return Math.floor((this.inputLayer.width + this.paddingX * 2 - this.filterWidth) /
this.strideX +
1);
}
get height() {
return Math.floor((this.inputLayer.height + this.paddingY * 2 - this.filterHeight) /
this.strideY +
1);
}
get depth() {
return this.settings.filterCount;
}
get filterCount() {
// TODO: handle 1 depth?
return this.settings.filterCount;
}
get switchX() {
return this.settings.switchX;
}
set switchX(switchX) {
this.settings.switchX = switchX;
}
get switchY() {
return this.settings.switchY;
}
set switchY(switchY) {
this.settings.switchY = switchY;
}
setupKernels() {
this.predictKernelMap = makeKernelMap({
switchX: setSwitchX,
switchY: setSwitchY,
}, predict$1, {
output: [this.width, this.height, this.depth],
constants: {
inputWidth: this.inputLayer.width,
inputHeight: this.inputLayer.height,
paddingX: this.paddingX,
paddingY: this.paddingY,
filterHeight: this.filterHeight,
filterWidth: this.filterWidth,
},
});
this.compareKernel = makeKernel(compare$1, {
output: [
this.inputLayer.width,
this.inputLayer.height,
this.inputLayer.depth,
],
constants: {
inputWidth: this.inputLayer.width,
inputHeight: this.inputLayer.height,
outputWidth: this.width,
outputHeight: this.height,
},
});
}
predict() {
const { result: weights, switchX, switchY } = this
.predictKernelMap(this.inputLayer.weights);
this.switchX = switchX;
this.switchY = switchY;
this.weights = weights;
}
compare() {
// debugger;
// const depth = this.inputLayer.deltas.length;
// const height = this.inputLayer.deltas[0].length;
// const width = this.inputLayer.deltas[0][0].length;
// const type = typeof this.inputLayer.deltas[0][0][0];
const inputLayerDeltas = this.inputLayer.deltas;
this.inputLayer.deltas = this.compareKernel(this.deltas, this.switchX, this.switchY);
release(inputLayerDeltas);
// debugger;
// if (depth !== this.inputLayer.deltas.length) debugger;
// if (height !== this.inputLayer.deltas[0].length) debugger;
// if (width !== this.inputLayer.deltas[0][0].length) debugger;
// if (type !== typeof this.inputLayer.deltas[0][0][0]) debugger;
}
}
function pool(settings, inputLayer) {
return new Pool(settings, inputLayer);
}
class RecurrentInput extends Internal {
constructor(recurrentInput) {
super();
this.praxis = null;
this.predictKernel = null;
this.compareKernel = null;
this.settings = {};
this.recurrentInput = recurrentInput;
this.validate();
}
get width() {
return this.recurrentInput.width;
}
get height() {
return this.recurrentInput.height;
}
get depth() {
return this.recurrentInput.depth;
}
get deltas() {
return this.recurrentInput.deltas;
}
set deltas(deltas) {
const recurrentInputDeltas = this.recurrentInput.deltas;
this.recurrentInput.deltas = deltas;
release(recurrentInputDeltas);
}
get weights() {
return this.recurrentInput.weights;
}
set weights(weights) {
const recurrentInputWeights = this.recurrentInput.weights;
this.recurrentInput.weights = weights;
release(recurrentInputWeights);
}
validate() {
BaseLayer.prototype.validate.call(this);
if (this.width !== this.recurrentInput.width) {
throw new Error(`${this.constructor.name} layer width ${this.width} and ${this.recurrentInput.constructor.name} width (${this.recurrentInput.width}) are not same`);
}
if (this.height !== this.recurrentInput.height) {
throw new Error(`${this.constructor.name} layer height ${this.height} and ${this.recurrentInput.constructor.name} width (${this.recurrentInput.height}) are not same`);
}
}
setDimensions(width, height) {
this.recurrentInput.width = width;
this.recurrentInput.height = height;
}
predict() {
// throw new Error(`${this.constructor.name}-predict is not yet implemented`)
}
compare() {
// throw new Error(`${this.constructor.name}-compare is not yet implemented`)
}
learn() {
// throw new Error(`${this.constructor.name}-learn is not yet implemented`)
}
setupKernels() {
// throw new Error(
// `${this.constructor.name}-setupKernels is not yet implemented`
// )
}
reuseKernels() {
// throw new Error(
// `${this.constructor.name}-reuseKernels is not yet implemented`
// )
}
}
class RecurrentZeros extends Internal {
constructor(settings) {
super();
this.praxis = null;
this.settings = {};
this.predictKernel = null;
this.compareKernel = null;
if (settings) {
this.settings = { ...settings };
}
}
setDimensions(width, height) {
this.praxis = null;
this.settings = {
...this.settings,
width,
height,
weights: zeros2D(width, height),
deltas: zeros2D(width, height),
};
}
setupKernels() {
// throw new Error(
// `${this.constructor.name}-setupKernels is not yet implemented`
// )
}
reuseKernels() {
// throw new Error(
// `${this.constructor.name}-reuseKernels is not yet implemented`
// )
}
predict() {
// throw new Error(`${this.constructor.name}-predict is not yet implemented`)
}
compare() {
// throw new Error(`${this.constructor.name}-compare is not yet implemented`)
}
learn(learningRate) {
const { weights: oldWeights } = this;
this.weights = this.praxis.run(this, learningRate);
// this.deltas = deltas;
release(oldWeights);
}
}
function recurrentZeros() {
return new RecurrentZeros();
}
function predict2D$1(inputs) {
return activate$3(inputs[this.thread.y][this.thread.x]);
}
function compare2D$1(weights, deltas) {
return measure$3(weights[this.thread.y][this.thread.x], deltas[this.thread.y][this.thread.x]);
}
function predict3D$1(inputs) {
return activate$3(inputs[this.thread.z][this.thread.y][this.thread.x]);
}
function compare3D$1(weights, deltas) {
return measure$3(weights[this.thread.z][this.thread.y][this.thread.x], deltas[this.thread.z][this.thread.y][this.thread.x]);
}
class Relu extends Activation {
setupKernels() {
const { width, height, depth } = this.inputLayer;
if (depth > 0) {
this.predictKernel = makeKernel(predict3D$1, {
output: [width, height, depth],
functions: [activate$3],
immutable: true,
});
this.compareKernel = makeKernel(compare3D$1, {
output: [width, height, depth],
functions: [measure$3],
immutable: true,
});
}
else {
this.predictKernel = makeKernel(predict2D$1, {
output: [width, height],
functions: [activate$3],
immutable: true,
});
this.compareKernel = makeKernel(compare2D$1, {
output: [width, height],
functions: [measure$3],
immutable: true,
});
}
}
predict() {
release(this.weights);
this.weights = this.predictKernel(this.inputLayer.weights);
}
compare() {
release(this.inputLayer.deltas);
this.inputLayer.deltas = this.compareKernel(this.weights, this.deltas);
}
}
function relu$1(inputLayer, settings) {
return new Relu(inputLayer, settings);
}
function rnnCell(settings, input, recurrentInput) {
const { height } = settings;
if (typeof height !== 'number')
throw new Error('height not set');
if (recurrentInput.setDimensions) {
recurrentInput.setDimensions(1, height);
}
// wxh
const weight = random({
id: 'weight',
height,
width: input.height,
std: 0.08,
});
// whh
const transition = random({
id: 'transition',
height,
width: height,
std: 0.08,
});
// bhh
const bias = zeros({ id: 'bias', height });
return relu$1(add$1(add$1(multiply$1(weight, input), multiply$1(transition, recurrentInput)), bias));
}
class Regression extends BaseLayer {
constructor(settings, inputLayer) {
super(settings);
this.inputLayer = inputLayer;
this.validate();
}
predict() {
release(this.weights);
this.weights = clone(this.inputLayer.weights);
}
learn() {
// throw new Error(`${this.constructor.name}-learn is not yet implemented`)
}
}
// TODO: handle `loss += 0.5*dy*dy;` total and sum in learn
function regression(settings, inputLayer) {
return new Regression(settings, inputLayer);
}
function getMaxValue2D(inputs) {
let maxInput = -Infinity;
for (let y = 0; y < this.constants.inputHeight; y++) {
for (let x = 0; x < this.constants.inputWidth; x++) {
const input = inputs[y][x];
if (input > maxInput) {
maxInput = input;
}
}
}
return maxInput;
}
function getMaxValue3D(inputs) {
let maxInput = -Infinity;
for (let z = 0; z < this.constants.inputDepth; z++) {
for (let y = 0; y < this.constants.inputHeight; y++) {
for (let x = 0; x < this.constants.inputWidth; x++) {
const input = inputs[z][y][x];
if (input > maxInput) {
maxInput = input;
}
}
}
}
return maxInput;
}
function getSum2D(inputs) {
let sum = 0;
for (let y = 0; y < this.constants.inputHeight; y++) {
for (let x = 0; x < this.constants.inputWidth; x++) {
sum += inputs[y][x];
}
}
return sum;
}
function getSum3D(inputs) {
let sum = 0;
for (let z = 0; z < this.constants.inputDepth; z++) {
for (let y = 0; y < this.constants.inputHeight; y++) {
for (let x = 0; x < this.constants.inputWidth; x++) {
sum += inputs[z][y][x];
}
}
}
return sum;
}
function getExponentials(inputs, maxInput) {
return Math.exp(inputs[this.thread.x] - maxInput[0]);
}
function getExponentials3D(inputs, maxInput) {
return Math.exp(inputs[this.thread.z][this.thread.y][this.thread.x] - maxInput[0]);
}
function predict2D(exponentials, exponentialsSum) {
return exponentials[this.thread.y][this.thread.x] / exponentialsSum[0];
}
function predict3D(exponentials, exponentialsSum) {
return (exponentials[this.thread.z][this.thread.y][this.thread.x] /
exponentialsSum[0]);
}
function compare2D(target, exponentials) {
let indicator = 0;
const index = this.thread.x + this.thread.y * this.output.x;
if (index === target) {
indicator = 1;
}
return -(indicator - exponentials[this.thread.y][this.thread.x]);
}
function compare3D(target, exponentials) {
let indicator = 0;
const index = this.thread.x +
this.thread.y * this.output.x +
this.thread.z * this.output.x * this.output.y;
if (index === target) {
indicator = 1;
}
return -(indicator - exponentials[this.thread.z][this.thread.y][this.thread.x]);
}
// TODO: handle: `return -Math.log(this.es[y]);` in learn
class SoftMax extends Modifier {
constructor(inputLayer, settings) {
super(inputLayer, settings);
this.errors = null;
this.getExponentialsKernel = null;
this.getMaxValueKernel = null;
this.getSumKernel = null;
this.validate();
if (this.depth > 0) {
this.weights = randos3D(this.width, this.height, this.depth);
this.deltas = zeros3D(this.width, this.height, this.depth);
}
else if (this.height > 0) {
this.weights = randos2D(this.width, this.height);
this.deltas = zeros2D(this.width, this.height);
}
else {
this.weights = randos(this.width);
this.deltas = zeros$1(this.width);
}
}
setupKernels() {
const { width, height, depth } = this;
if (depth > 0) {
this.getExponentialsKernel = makeKernel(getExponentials3D, {
output: [width, height, depth],
});
this.getMaxValueKernel = makeKernel(getMaxValue3D, {
output: [1, 1, 1],
constants: {
inputWidth: width,
inputHeight: height,
inputDepth: depth,
},
});
this.getSumKernel = makeKernel(getSum3D, {
output: [1, 1, 1],
constants: {
inputWidth: width,
inputHeight: height,
inputDepth: depth,
},
});
this.predictKernel = makeKernel(predict3D, {
output: [width, height, depth],
});
this.compareKernel = makeKernel(compare3D, {
output: [width, height, depth],
immutable: true,
});
}
else {
this.getExponentialsKernel = makeKernel(getExponentials, {
output: [width, height],
});
this.getMaxValueKernel = makeKernel(getMaxValue2D, {
output: [1, 1],
constants: {
inputWidth: width,
inputHeight: height,
},
});
this.getSumKernel = makeKernel(getSum2D, {
output: [1, 1],
constants: {
inputWidth: width,
inputHeight: height,
},
});
this.predictKernel = makeKernel(predict2D, {
output: [width, height],
});
this.compareKernel = makeKernel(compare2D, {
output: [width, height],
immutable: true,
});
}
}
predict() {
const maxValue = this.getMaxValueKernel(this.inputLayer.weights);
const exponentials = this.getExponentialsKernel(this.inputLayer.weights, maxValue);
const exponentialsSum = this.getSumKernel(exponentials);
this.weights = this.predictKernel(exponentials, exponentialsSum);
}
compare(targetValues) {
const { deltas, errors } = this;
this.errors = this.compareKernel(targetValues[0], deltas);
this.deltas = clone(this.errors);
release(deltas);
release(errors);
const inputLayerDeltas = this.inputLayer.deltas;
this.inputLayer.deltas = clone(this.deltas);
release(inputLayerDeltas);
}
}
function softMax(inputLayer, settings) {
return new SoftMax(inputLayer, settings);
}
class SVM extends BaseLayer {
constructor(inputLayer, settings) {
super(settings);
this.inputLayer = inputLayer;
}
predict() {
release(this.weights);
this.weights = clone(this.inputLayer.weights);
this.validate();
}
learn() {
// throw new Error(`${this.constructor.name}-learn is not yet implemented`)
}
}
// function learn(target) {
// if (y === i) {
// continue;
// }
// const ydiff = -yscore + x.w[i] + margin;
// if (ydiff > 0) {
// // violating dimension, apply loss
// x.dw[i] += 1;
// x.dw[y] -= 1;
// loss += ydiff;
// }
// }
function svm(inputLayer, settings) {
return new SVM(inputLayer, settings);
}
function predict(value) {
return value[this.thread.x][this.thread.y];
}
const compare = predict;
class Transpose extends Modifier {
get width() {
return this.inputLayer.height;
}
get height() {
return this.inputLayer.width;
}
constructor(inputLayer) {
super(inputLayer);
this.validate();
}
setupKernels() {
this.predictKernel = makeKernel(predict, {
output: [this.height, this.width],
});
this.compareKernel = makeKernel(compare, {
output: [this.width, this.height],
});
}
predict() {
this.weights = this.predictKernel(this.inputLayer.weights);
}
compare() {
this.inputLayer.deltas = this.compareKernel(this.deltas);
}
}
function transpose(inputLayer) {
return new Transpose(inputLayer);
}
const layerTypes = {
Activation,
Internal,
InternalModel,
EntryPoint,
Filter,
Model,
Modifier,
Operator,
Target,
};
var layer = /*#__PURE__*/Object.freeze({
__proto__: null,
layerTypes: layerTypes,
Add: Add,
add: add$1,
arthurFeedForward: arthurFeedForward,
BaseLayer: BaseLayer,
baseLayerDefaultSettings: baseLayerDefaultSettings,
Convolution: Convolution,
convolution: convolution,
Dropout: Dropout,
dropout: dropout,
feedForward: feedForward,
FullyConnected: FullyConnected,
fullyConnected: fullyConnected,
gru: gru,
Input: Input,
input: input,
LeakyRelu: LeakyRelu,
leakyRelu: leakyRelu,
lstmCell: lstmCell,
Multiply: Multiply,
multiply: multiply$1,
MultiplyElement: MultiplyElement,
multiplyElement: multiplyElement$1,
Negative: Negative,
negative: negative,
Ones: Ones,
ones: ones,
output: output,
Pool: Pool,
pool: pool,
Random: Random,
random: random,
RecurrentInput: RecurrentInput,
RecurrentZeros: RecurrentZeros,
rnnCell: rnnCell,
Regression: Regression,
regression: regression,
Relu: Relu,
relu: relu$1,
Sigmoid: Sigmoid,
sigmoid: sigmoid$1,
SoftMax: SoftMax,
softMax: softMax,
SVM: SVM,
svm: svm,
Tanh: Tanh,
tanh: tanh$1,
Target: Target,
target: target,
Transpose: Transpose,
transpose: transpose,
Zeros: Zeros,
zeros: zeros
});
const layerNameTypes = Object.keys(layer);
function layerFromJSON(jsonLayer, inputLayer1, inputLayer2) {
if (!layerNameTypes.find((layerNameType) => layerNameType === jsonLayer.type)) {
return null;
}
const Layer = layer[jsonLayer.type];
if (Layer.prototype instanceof layerTypes.Filter) {
if (!inputLayer1)
throw new Error('inputLayer missing');
return new Layer(jsonLayer, inputLayer1);
}
else if (Layer.prototype instanceof layerTypes.Activation ||
Layer.prototype instanceof layerTypes.Modifier) {
if (!inputLayer1)
throw new Error('inputLayer missing');
return new Layer(inputLayer1, jsonLayer);
}
else if (Layer.prototype instanceof layerTypes.Internal) {
return new Layer(jsonLayer);
}
else if (Layer.prototype instanceof layerTypes.Operator) {
if (!inputLayer1)
throw new Error('inputLayer1 missing');
if (!inputLayer2)
throw new Error('inputLayer2 missing');
return new Layer(inputLayer1, inputLayer2, jsonLayer);
}
else if (Layer.prototype instanceof layerTypes.InternalModel ||
Layer.prototype instanceof layerTypes.EntryPoint ||
Layer.prototype instanceof layerTypes.Model) {
return new Layer(jsonLayer);
}
else if (Layer === Target) {
if (!inputLayer1)
throw new Error('inputLayer missing');
return new Layer(jsonLayer, inputLayer1);
}
return null;
}
class LookupTable {
constructor(data, prop) {
this.prop = null;
this.table = {};
this.length = 0;
const table = this.table;
if (prop) {
this.prop = prop;
for (let i = 0; i < data.length; i++) {
const datum = data[i];
const object = datum[prop];
for (const p in object) {
if (!object.hasOwnProperty(p))
continue;
if (table.hasOwnProperty(p))
continue;
table[p] = this.length++;
}
}
}
else if (Array.isArray(data) && Array.isArray(data[0])) {
for (let i = 0; i < data.length; i++) {
const array = data[i];
for (let j = 0; j < array.length; j++) {
const object = array[j];
for (const p in object) {
if (!object.hasOwnProperty(p))
continue;
if (table.hasOwnProperty(p))
continue;
table[p] = this.length++;
}
}
}
}
else {
for (let i = 0; i < data.length; i++) {
const object = data[i];
for (const p in object) {
if (!object.hasOwnProperty(p))
continue;
if (table.hasOwnProperty(p))
continue;
table[p] = this.length++;
}
}
}
}
}
const defaults$3 = {
learningRate: 0.3,
binaryThresh: 0.5,
initPraxis: (layerTemplate, settings) => {
var _a;
return momentumRootMeanSquaredPropagation(layerTemplate, (_a = layerTemplate.settings.praxisOpts) !== null && _a !== void 0 ? _a : settings);
},
};
const trainDefaults$3 = {
iterations: 20000,
errorThresh: 0.005,
log: false,
logPeriod: 10,
learningRate: 0.3,
callbackPeriod: 10,
errorCheckInterval: 100,
timeout: Infinity,
};
class FeedForward {
constructor(options = {}) {
this.trainOpts = {};
this.layers = null;
this._inputLayer = null;
this._hiddenLayers = null;
this._outputLayer = null;
this._model = null;
this.meanSquaredError = null;
this.inputLookup = null;
this.inputLookupLength = null;
this.outputLookup = null;
this.outputLookupLength = null;
this.options = { ...defaults$3, ...options };
this._updateTrainingOptions({
...trainDefaults$3,
...options,
});
}
static _validateTrainingOptions(options) {
const { iterations, errorThresh, log, logPeriod, learningRate, callback, callbackPeriod, timeout, } = options;
const validations = {
iterations: () => typeof iterations === 'number' && iterations > 0,
errorThresh: () => typeof errorThresh === 'number' && errorThresh > 0 && errorThresh < 1,
log: () => typeof log === 'function' || typeof log === 'boolean',
logPeriod: () => typeof logPeriod === 'number' && logPeriod > 0,
learningRate: () => typeof learningRate === 'number' &&
learningRate > 0 &&
learningRate < 1,
callback: () => typeof callback === 'function' || callback === null,
callbackPeriod: () => typeof callbackPeriod === 'number' && callbackPeriod > 0,
timeout: () => typeof timeout === 'number' && timeout > 0,
};
Object.keys(trainDefaults$3).forEach((key) => {
if (validations.hasOwnProperty(key) && !validations[key]()) {
const val = options[key];
throw new Error(`[${key}, ${(val !== null && val !== void 0 ? val : 'undefined').toString()}] is out of normal training range, your network will probably not train.`);
}
});
}
/**
* if a method is passed in method is used
* if false passed in nothing is logged
*/
_setLogMethod(log) {
if (typeof log === 'function') {
this.trainOpts.log = log;
}
else if (log) {
// eslint-disable-next-line
this.trainOpts.log = console.log;
}
else {
this.trainOpts.log = false;
}
}
_updateTrainingOptions(opts) {
var _a;
this.trainOpts = { ...trainDefaults$3, ...this.trainOpts, ...opts };
FeedForward._validateTrainingOptions(this.trainOpts);
this._setLogMethod((_a = opts.log) !== null && _a !== void 0 ? _a : this.trainOpts.log);
const { callback, callbackPeriod, errorCheckInterval } = this.trainOpts;
if (callback && callbackPeriod !== errorCheckInterval) {
console.warn(`options.callbackPeriod with value of ${(callbackPeriod !== null && callbackPeriod !== void 0 ? callbackPeriod : 'undefined').toString()} does not match options.errorCheckInterval with value of ${(errorCheckInterval !== null && errorCheckInterval !== void 0 ? errorCheckInterval : 'undefined').toString()}, if logging error, it will repeat. These values may need to match`);
}
}
_connectOptionsLayers() {
const { inputLayerIndex, outputLayerIndex, layers } = this.options;
if (!layers)
throw new Error('this.options.layers in unexpected state');
if (typeof inputLayerIndex !== 'number')
throw new Error('inputLayerIndex not a number');
if (typeof outputLayerIndex !== 'number')
throw new Error('inputLayerIndex not a number');
const inputLayer = layers[inputLayerIndex];
if (!inputLayer) {
throw new Error('inputLayer not found in this.options.layers');
}
const outputLayer = layers[outputLayerIndex];
if (!outputLayer) {
throw new Error('outputLayer not found in this.options.layers');
}
this._inputLayer = inputLayer;
this._hiddenLayers = layers.slice(inputLayerIndex, outputLayerIndex - inputLayerIndex);
this._outputLayer = outputLayer;
return layers;
}
_connectNewLayers() {
const { inputLayer, outputLayer } = this.options;
if (!inputLayer)
throw new Error('inputLayer not defined');
const layers = [];
this._inputLayer = inputLayer();
const hiddenLayers = this._connectHiddenLayers(this._inputLayer);
if (!outputLayer)
throw new Error('outputLayer not defined');
this._outputLayer = outputLayer(hiddenLayers[hiddenLayers.length - 1], hiddenLayers.length);
layers.push(this._inputLayer);
layers.push(...hiddenLayers);
layers.push(this._outputLayer);
return flattenLayers(layers);
}
_connectHiddenLayers(previousLayer) {
this._hiddenLayers = [];
const result = [];
const { hiddenLayers } = this.options;
if (!hiddenLayers)
throw new Error('hiddenLayers not defined');
for (let i = 0; i < hiddenLayers.length; i++) {
const hiddenLayer = hiddenLayers[i](previousLayer, i);
result.push(hiddenLayer);
this._hiddenLayers.push(hiddenLayer);
previousLayer = hiddenLayer;
}
return result;
}
initialize() {
this.layers = this.options.layers
? this._connectOptionsLayers()
: this._connectNewLayers();
this.initializeLayers(this.layers);
this._model = this.layers.filter((l) => l instanceof Model);
}
initializeLayers(layers) {
var _a, _b;
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
// TODO: optimize for when training or just running
layer.setupKernels(true);
if (layer instanceof Model &&
layer.praxis === null &&
typeof this.options.initPraxis === 'function') {
layer.praxis = this.options.initPraxis(layer, (_b = (_a = layer.settings.praxisOpts) !== null && _a !== void 0 ? _a : this.options.praxisOpts) !== null && _b !== void 0 ? _b : {});
layer.praxis.setupKernels();
}
}
const lastLayer = layers[layers.length - 1];
this.meanSquaredError = new MeanSquaredError({
width: lastLayer.width,
height: lastLayer.height,
});
}
run(input) {
let typeSafeInput;
if (Array.isArray(input) || input.buffer) {
typeSafeInput = input;
}
else {
if (this.inputLookup) {
typeSafeInput = lookup.toArray(this.inputLookup, input, this.inputLookupLength);
}
else {
throw new Error('input is incompatible with net');
}
}
let output = this.runInput(typeSafeInput);
if (output instanceof gpu_js.Texture) {
output = output.toArray();
}
if (this.outputLookup) {
return lookup.toObject(this.outputLookup, output);
}
return output;
}
runInput(input) {
if (!this.layers)
throw new Error('not initialized');
this.layers[0].predict(input);
for (let i = 1; i < this.layers.length; i++) {
this.layers[i].predict();
}
return this.layers[this.layers.length - 1].weights;
}
train(data, options = {}) {
const { preparedData, status, endTime } = this._prepTraining(data, options);
let continueTicking = true;
const calculateError = () => this._calculateTrainingError(preparedData);
const trainPatters = () => this._trainPatterns(preparedData);
while (continueTicking) {
continueTicking = this._trainingTick(status, endTime, calculateError, trainPatters);
}
return status;
}
_trainingTick(status, endTime, calculateError, trainPatterns) {
const { trainOpts } = this;
if (status.iterations >= trainOpts.iterations ||
status.error <= trainOpts.errorThresh ||
Date.now() >= endTime) {
return false;
}
if (typeof trainOpts.log === 'function' &&
status.iterations % trainOpts.logPeriod === 0) {
status.error = calculateError();
trainOpts.log(`iterations: ${status.iterations}, training error: ${status.error}`);
}
else if (status.iterations % trainOpts.errorCheckInterval ===
0) {
status.error = calculateError();
}
else {
trainPatterns();
}
if (trainOpts.callback &&
status.iterations % trainOpts.callbackPeriod === 0) {
trainOpts.callback(Object.assign(status));
}
status.iterations++;
return true;
}
_prepTraining(data, options) {
this._updateTrainingOptions(options);
const formattedData = this.formatData(data);
const endTime = this.trainOpts.timeout
? Date.now() + this.trainOpts.timeout
: 0;
const status = {
error: 1,
iterations: 0,
};
this.verifyIsInitialized();
return {
preparedData: this.transferData(formattedData),
status,
endTime,
};
}
verifyIsInitialized() {
if (!this._model) {
this.initialize();
}
}
_calculateTrainingError(preparedData) {
let sum = new Float32Array([0]);
const meanSquaredError = this.meanSquaredError;
for (let i = 0; i < preparedData.length; ++i) {
const prevSum = sum;
const error = this._trainPattern(preparedData[i].input, preparedData[i].output, true);
sum = meanSquaredError.add(sum, error);
release(error);
release(prevSum);
}
const result = meanSquaredError.divide(preparedData.length, sum);
release(sum);
if (result instanceof gpu_js.Texture) {
const resultArray = result.toArray();
release(result);
return resultArray[0];
}
return result[0];
}
/**
* @param data
* @private
*/
_trainPatterns(data) {
for (let i = 0; i < data.length; ++i) {
this._trainPattern(data[i].input, data[i].output, false);
}
}
_trainPattern(input, target, logErrorRate) {
var _a;
// forward propagate
this.runInput(input);
// back propagate
this._calculateDeltas(target);
this.adjustWeights();
if (logErrorRate) {
if (!((_a = this._outputLayer) === null || _a === void 0 ? void 0 : _a.errors)) {
throw new Error('outputLayer.errors not defined');
}
return this.meanSquaredError.calculate(this._outputLayer.errors);
}
return null;
}
_calculateDeltas(target) {
const layers = this.layers;
for (let i = layers.length - 1; i > -1; i--) {
layers[i].compare(target);
}
}
/**
*
*/
adjustWeights() {
const _model = this._model;
for (let i = 0; i < _model.length; i++) {
_model[i].learn(this.trainOpts.learningRate);
}
}
/**
*
* @param data
* @returns {*}
*/
formatData(data) {
if (!Array.isArray(data)) {
// turn stream datum into array
const tmp = [];
tmp.push(data);
data = tmp;
}
// turn sparse hash input into arrays with 0s as filler
const inputDatumCheck = data[0].input;
let formattedData;
if (Array.isArray(data) &&
!Array.isArray(inputDatumCheck) &&
!(inputDatumCheck instanceof Float32Array)) {
if (!this.inputLookup) {
const lookupTable = new LookupTable(data, 'input');
this.inputLookup = lookupTable.table;
this.inputLookupLength = lookupTable.length;
}
formattedData = data.map((datumParam) => {
const array = lookup.toArray(this.inputLookup, datumParam.input, this.inputLookupLength);
return { input: array };
}, this);
}
else {
formattedData = data;
}
const outputDatumCheck = data[0].output;
if (!Array.isArray(outputDatumCheck) &&
!(outputDatumCheck instanceof Float32Array)) {
if (!this.outputLookup) {
const lookupTable = new LookupTable(data, 'output');
this.outputLookup = lookupTable.table;
this.outputLookupLength = lookupTable.length;
}
formattedData = data.map((datumParam, index) => {
const array = lookup.toArray(this.outputLookup, datumParam.output, this.inputLookupLength);
return {
input: formattedData[index].input,
output: array,
};
}, this);
}
return formattedData;
}
transferData(formattedData) {
const transferredData = new Array(formattedData.length);
const transferInput = makeKernel(function (value) {
return value[this.thread.x];
}, {
output: [formattedData[0].input.length],
immutable: true,
});
const transferOutput = makeKernel(function (value) {
return value[this.thread.x];
}, {
output: [formattedData[0].output.length],
immutable: true,
});
for (let i = 0; i < formattedData.length; i++) {
const formattedDatum = formattedData[i];
transferredData[i] = {
input: transferInput(formattedDatum.input),
output: transferOutput(formattedDatum.output),
};
}
return transferredData;
}
/**
*
* @param data
* @returns {
* {
* error: number,
* misclasses: Array
* }
* }
*/
test() {
throw new Error(`${this.constructor.name}-test is not yet implemented`);
}
/**
*
*/
toJSON() {
var _a;
if (!this.layers) {
this.initialize();
}
if (!this._model ||
!this.layers ||
!this._inputLayer ||
!this._hiddenLayers ||
!this._outputLayer) {
throw new Error('network is not initialized');
}
const jsonLayers = [];
for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[i];
const jsonLayer = layer.toJSON();
if (layer.hasOwnProperty('inputLayer')) {
jsonLayer.inputLayerIndex = this.layers.indexOf(layer.inputLayer);
}
else if (layer.hasOwnProperty('inputLayer1') &&
layer.hasOwnProperty('inputLayer2')) {
jsonLayer.inputLayer1Index = this.layers.indexOf(layer.inputLayer1);
jsonLayer.inputLayer2Index = this.layers.indexOf(layer.inputLayer2);
}
jsonLayers.push(jsonLayer);
}
return {
type: this.constructor.name,
sizes: (_a = this.options.sizes) !== null && _a !== void 0 ? _a : [this._inputLayer.height]
.concat(this._hiddenLayers.map((l) => l.height))
.concat([this._outputLayer.height]),
outputLayerIndex: this.layers.indexOf(this._outputLayer),
layers: jsonLayers,
inputLayerIndex: this.layers.indexOf(this._inputLayer),
};
}
static fromJSON(json, getLayer) {
var _a, _b, _c, _d;
const jsonLayers = json.layers;
const layers = [];
const inputLayer = getLayer
? (_a = layerFromJSON(jsonLayers[0])) !== null && _a !== void 0 ? _a : getLayer(jsonLayers[0]) : layerFromJSON(jsonLayers[0]);
if (!inputLayer)
throw new Error('unable to find layer');
layers.push(inputLayer);
for (let i = 1; i < jsonLayers.length; i++) {
const jsonLayer = jsonLayers[i];
if (typeof jsonLayer.inputLayerIndex === 'undefined' &&
typeof jsonLayer.inputLayer1Index === 'undefined' &&
typeof jsonLayer.inputLayer2Index === 'undefined') {
const layer = getLayer
? (_b = layerFromJSON(jsonLayer)) !== null && _b !== void 0 ? _b : getLayer(jsonLayer) : layerFromJSON(jsonLayer);
if (!layer)
throw new Error('unable to find layer');
layers.push(layer);
}
else if (typeof jsonLayer.inputLayerIndex === 'number') {
const inputLayer = layers[jsonLayer.inputLayerIndex];
if (!inputLayer) {
throw new Error('inputLayer1 not found');
}
const layer = getLayer
? (_c = layerFromJSON(jsonLayer, inputLayer)) !== null && _c !== void 0 ? _c : getLayer(jsonLayer, inputLayer) : layerFromJSON(jsonLayer, inputLayer);
if (!layer)
throw new Error('unable to find layer');
layers.push(layer);
}
else {
if (typeof jsonLayer.inputLayer1Index !== 'number') {
throw new Error('Cannot create network from provided JSON. inputLayer1Index not defined.');
}
if (typeof jsonLayer.inputLayer2Index !== 'number') {
throw new Error('Cannot create network from provided JSON. inputLayer2Index not defined.');
}
const inputLayer1 = layers[jsonLayer.inputLayer1Index];
const inputLayer2 = layers[jsonLayer.inputLayer2Index];
if (inputLayer1 === undefined)
throw new Error(`Cannot create network from provided JSON. layer of index ${jsonLayer.inputLayer1Index} not found.`);
if (inputLayer2 === undefined)
throw new Error(`Cannot create network from provided JSON. layer of index ${jsonLayer.inputLayer2Index} not found.`);
const layer = getLayer
? (_d = layerFromJSON(jsonLayer, inputLayer1, inputLayer2)) !== null && _d !== void 0 ? _d : getLayer(jsonLayer, inputLayer1, inputLayer2) : layerFromJSON(jsonLayer, inputLayer1, inputLayer2);
if (!layer)
throw new Error('unable to find layer');
layers.push(layer);
}
}
return new this({ ...json, layers });
}
/**
*
* @returns {Function}
*/
toFunction() {
throw new Error(`${this.constructor.name}-toFunction is not yet implemented`);
}
/**
* This will create a TrainStream (WriteStream) for us to send the training data to.
* @param opts training options
* @returns {TrainStream|*}
*/
createTrainStream() {
throw new Error(`${this.constructor.name}-createTrainStream is not yet implemented`);
}
}
function likely(input, net) {
if (!net) {
throw new TypeError(`Required parameter 'net' is of type ${typeof net}. Must be of type 'brain.NeuralNetwork'`);
}
const output = net.run(input);
let maxProp = null;
let maxValue = -1;
Object.entries(output).forEach(([key, value]) => {
if (typeof value !== 'undefined' &&
typeof value === 'number' &&
value > maxValue) {
maxProp = key;
maxValue = value;
}
});
return maxProp;
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn, basedir, module) {
return module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base);
}
}, fn(module, module.exports), module.exports;
}
function commonjsRequire () {
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
}
var browser = createCommonjsModule(function (module, exports) {
var __assign = (commonjsGlobal && commonjsGlobal.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.thaw = exports.Block = exports.Thaw = void 0;
/**
* thaw an array of items
*/
var Thaw = /** @class */ (function () {
function Thaw(items, options) {
var _this = this;
if (options === void 0) { options = {}; }
var _a = __assign(__assign({}, Thaw.defaultSettings), options), each = _a.each, done = _a.done;
this.i = 0;
this.isStopped = false;
this.items = items;
this.options = options;
this.tick = function () {
if (_this.isStopped)
return;
_this.timeout = setTimeout(_this.tick, 0);
if (Thaw.thawing)
return;
var item = _this.items[_this.i];
if (_this.i >= _this.items.length) {
if (done !== null) {
Thaw.thawing = true;
done();
Thaw.thawing = false;
}
_this.isStopped = true;
clearTimeout(_this.timeout);
return;
}
if (each !== null) {
Thaw.thawing = true;
each(item, _this.i);
Thaw.thawing = false;
}
else if (item !== undefined) {
item();
}
_this.i++;
};
Thaw.thaws.push(this);
if (!options.delay) {
this.tick();
}
}
Object.defineProperty(Thaw, "isThawing", {
/**
* returns if Thaw.js is thawing
*/
get: function () {
return Thaw.thawing;
},
enumerable: false,
configurable: true
});
/**
* Stops all Thaw instances
*/
Thaw.stopAll = function () {
for (var i = 0; i < Thaw.thaws.length; i++) {
Thaw.thaws[i].stop();
}
};
/**
* readies thaw to continue
*/
Thaw.prototype.makeReady = function () {
if (this.isStopped) {
this.isStopped = false;
return true;
}
return false;
};
/**
* Adds an item to the end of this instance of Thaw and readies Thaw to process it
*/
Thaw.prototype.add = function (item) {
this.items.push(item);
if (this.makeReady()) {
this.tick();
}
return this;
};
/**
* Inserts an item just after the current item being processed in Thaw and readies Thaw to process it
*/
Thaw.prototype.insert = function (item) {
this.items.splice(this.i, 0, item);
if (this.makeReady()) {
this.tick();
}
return this;
};
/**
* Adds an Array to the end of this instance of Thaw and readies Thaw to process it
*/
Thaw.prototype.addArray = function (items) {
this.items = this.items.concat(items);
if (this.makeReady()) {
this.tick();
}
return this;
};
/**
* Inserts an Array just after the current item being processed in Thaw and readies Thaw to process them
*/
Thaw.prototype.insertArray = function (items) {
var before = this.items.splice(0, this.i);
var after = this.items;
this.items = before.concat(items, after);
if (this.makeReady()) {
this.tick();
}
return this;
};
/**
* Stops this instance of Thaw
*/
Thaw.prototype.stop = function () {
this.isStopped = true;
clearTimeout(this.timeout);
if (this.options.done) {
this.options.done();
}
return this;
};
Thaw.thawing = false;
Thaw.thaws = [];
Thaw.defaultSettings = {
each: null,
done: null
};
return Thaw;
}());
exports.Thaw = Thaw;
/**
* simple thaw
*/
function thaw(items, options) {
return new Thaw(items, options);
}
exports.thaw = thaw;
var Block = /** @class */ (function () {
function Block(options, count) {
if (count === void 0) { count = 200; }
this.index = 0;
this.thaws = [];
this.count = count;
this.options = options;
}
/**
* add an item to the end of items
*/
Block.prototype.add = function (item) {
var next = this.next();
next.add(item);
return this;
};
/**
* add an Array to the end of items
*/
Block.prototype.addArray = function (items) {
var next = this.next();
next.addArray(items);
return this;
};
/**
* insert an item into items @ current position
*/
Block.prototype.insert = function (item) {
var next = this.next();
next.insert(item);
return this;
};
/**
* insert and array into items @ current position
*/
Block.prototype.insertArray = function (items) {
var next = this.next();
next.insertArray(items);
return this;
};
/**
* Stops all thaws in this block
*/
Block.prototype.stop = function () {
for (var i = 0; i < this.thaws.length; i++) {
this.thaws[i].stop();
}
return this;
};
/**
* Get next available in block
*/
Block.prototype.next = function () {
var thaw;
var thaws = this.thaws;
if (thaws.length < this.count) {
thaw = new Thaw([], this.options);
thaws.push(thaw);
}
else {
thaw = thaws[this.index] || null;
}
this.index++;
if (this.index >= this.count) {
this.index = 0;
}
return thaw;
};
return Block;
}());
exports.Block = Block;
if (typeof window !== 'undefined') {
// @ts-ignore
window.Thaw = Thaw;
// @ts-ignore
window.thaw = thaw;
// @ts-ignore
window.Thaw.Block = Block;
}
});
function arraysToFloat32Arrays(arrays) {
const result = [];
for (let i = 0; i < arrays.length; i++) {
result.push(Float32Array.from(arrays[i]));
}
return result;
}
function inputOutputArraysToFloat32Arrays(input, output) {
const result = [];
for (let i = 0; i < input.length; i++) {
result.push(Float32Array.from(input[i]));
}
for (let i = 0; i < output.length; i++) {
result.push(Float32Array.from(output[i]));
}
return result;
}
function arrayToFloat32Arrays(array) {
const result = [];
for (let i = 0; i < array.length; i++) {
result.push(Float32Array.from([array[i]]));
}
return result;
}
function inputOutputArrayToFloat32Arrays(input, output) {
const result = [];
for (let i = 0; i < input.length; i++) {
result.push(Float32Array.from([input[i]]));
}
for (let i = 0; i < output.length; i++) {
result.push(Float32Array.from([output[i]]));
}
return result;
}
function arrayToFloat32Array(array) {
return Float32Array.from(array);
}
function inputOutputObjectsToFloat32Arrays(input, output, inputTable, outputTable, inputLength, outputLength) {
const results = [];
for (let i = 0; i < input.length; i++) {
const object = input[i];
const result = new Float32Array(inputLength);
for (const p in object) {
if (object.hasOwnProperty(p)) {
result[inputTable[p]] = object[p];
}
}
results.push(result);
}
for (let i = 0; i < output.length; i++) {
const object = output[i];
const result = new Float32Array(outputLength);
for (const p in object) {
if (object.hasOwnProperty(p)) {
result[outputTable[p]] = object[p];
}
}
results.push(result);
}
return results;
}
function objectToFloat32Arrays(object) {
const result = [];
for (const p in object) {
if (!object.hasOwnProperty(p))
continue;
result.push(Float32Array.from([object[p]]));
}
return result;
}
function inputOutputObjectToFloat32Arrays(input, output) {
const result = [];
for (const p in input) {
if (!input.hasOwnProperty(p))
continue;
result.push(Float32Array.from([input[p]]));
}
for (const p in output) {
if (!output.hasOwnProperty(p))
continue;
result.push(Float32Array.from([output[p]]));
}
return result;
}
function objectToFloat32Array(object, table, length) {
const result = new Float32Array(length);
for (const p in object) {
if (object.hasOwnProperty(p)) {
result[table[p]] = object[p];
}
}
return result;
}
function max(values) {
if (Array.isArray(values) || values instanceof Float32Array) {
return Math.max(...values);
}
else {
return Math.max(...Object.values(values));
}
}
function mse$1(errors) {
// mean squared error
let sum = 0;
for (let i = 0; i < errors.length; i++) {
sum += errors[i] ** 2;
}
return sum / errors.length;
}
function getTypedArrayFn(value, table) {
if (value.buffer instanceof ArrayBuffer) {
return null;
}
if (Array.isArray(value)) {
return arrayToFloat32Array;
}
if (!table)
throw new Error('table is not Object');
const { length } = Object.keys(table);
return (v) => {
const array = new Float32Array(length);
for (const p in table) {
if (!table.hasOwnProperty(p))
continue;
array[table[p]] = v[p] || 0;
}
return array;
};
}
function defaults$2() {
return {
inputSize: 0,
outputSize: 0,
binaryThresh: 0.5,
};
}
function trainDefaults$2() {
return {
activation: 'sigmoid',
iterations: 20000,
errorThresh: 0.005,
log: false,
logPeriod: 10,
leakyReluAlpha: 0.01,
learningRate: 0.3,
momentum: 0.1,
callbackPeriod: 10,
timeout: Infinity,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
};
}
class NeuralNetwork {
constructor(options = {}) {
this.options = defaults$2();
this.trainOpts = trainDefaults$2();
this.sizes = [];
this.outputLayer = -1;
this.biases = [];
this.weights = []; // weights for bias nodes
this.outputs = [];
// state for training
this.deltas = [];
this.changes = []; // for momentum
this.errors = [];
this.errorCheckInterval = 1;
this.inputLookup = null;
this.inputLookupLength = 0;
this.outputLookup = null;
this.outputLookupLength = 0;
this._formatInput = null;
this._formatOutput = null;
this.runInput = (input) => {
this.setActivation();
return this.runInput(input);
};
this.calculateDeltas = (output) => {
this.setActivation();
return this.calculateDeltas(output);
};
// adam
this.biasChangesLow = [];
this.biasChangesHigh = [];
this.changesLow = [];
this.changesHigh = [];
this.iterations = 0;
this.options = { ...this.options, ...options };
this.updateTrainingOptions(options);
const { inputSize, hiddenLayers, outputSize } = this.options;
if (inputSize && outputSize) {
this.sizes = [inputSize].concat(hiddenLayers !== null && hiddenLayers !== void 0 ? hiddenLayers : []).concat([outputSize]);
}
}
/**
*
* Expects this.sizes to have been set
*/
initialize() {
if (!this.sizes.length) {
throw new Error('Sizes must be set before initializing');
}
this.outputLayer = this.sizes.length - 1;
this.biases = new Array(this.outputLayer); // weights for bias nodes
this.weights = new Array(this.outputLayer);
this.outputs = new Array(this.outputLayer);
// state for training
this.deltas = new Array(this.outputLayer);
this.changes = new Array(this.outputLayer); // for momentum
this.errors = new Array(this.outputLayer);
for (let layerIndex = 0; layerIndex <= this.outputLayer; layerIndex++) {
const size = this.sizes[layerIndex];
this.deltas[layerIndex] = zeros$1(size);
this.errors[layerIndex] = zeros$1(size);
this.outputs[layerIndex] = zeros$1(size);
if (layerIndex > 0) {
this.biases[layerIndex] = randos(size);
this.weights[layerIndex] = new Array(size);
this.changes[layerIndex] = new Array(size);
for (let nodeIndex = 0; nodeIndex < size; nodeIndex++) {
const prevSize = this.sizes[layerIndex - 1];
this.weights[layerIndex][nodeIndex] = randos(prevSize);
this.changes[layerIndex][nodeIndex] = zeros$1(prevSize);
}
}
}
this.setActivation();
if (this.trainOpts.praxis === 'adam') {
this._setupAdam();
}
}
setActivation(activation) {
const value = activation !== null && activation !== void 0 ? activation : this.trainOpts.activation;
switch (value) {
case 'sigmoid':
this.runInput = this._runInputSigmoid;
this.calculateDeltas = this._calculateDeltasSigmoid;
break;
case 'relu':
this.runInput = this._runInputRelu;
this.calculateDeltas = this._calculateDeltasRelu;
break;
case 'leaky-relu':
this.runInput = this._runInputLeakyRelu;
this.calculateDeltas = this._calculateDeltasLeakyRelu;
break;
case 'tanh':
this.runInput = this._runInputTanh;
this.calculateDeltas = this._calculateDeltasTanh;
break;
default:
throw new Error(`Unknown activation ${value}. Available activations are: 'sigmoid', 'relu', 'leaky-relu', 'tanh'`);
}
}
get isRunnable() {
return this.sizes.length > 0;
}
run(input) {
if (!this.isRunnable) {
throw new Error('network not runnable');
}
let formattedInput;
if (this.inputLookup) {
formattedInput = lookup.toArray(this.inputLookup, input, this.inputLookupLength);
}
else {
formattedInput = input;
}
if (formattedInput.length !== this.sizes[0]) {
throw new Error(`input is not in correct length of ${this.sizes[0]}`);
}
const output = this.runInput(formattedInput).slice(0);
if (this.outputLookup) {
return lookup.toObject(this.outputLookup, output);
}
return output;
}
_runInputSigmoid(input) {
this.outputs[0] = input; // set output state of input layer
let output = null;
for (let layer = 1; layer <= this.outputLayer; layer++) {
const activeLayer = this.sizes[layer];
const activeWeights = this.weights[layer];
const activeBiases = this.biases[layer];
const activeOutputs = this.outputs[layer];
for (let node = 0; node < activeLayer; node++) {
const weights = activeWeights[node];
let sum = activeBiases[node];
for (let k = 0; k < weights.length; k++) {
sum += weights[k] * input[k];
}
// sigmoid
activeOutputs[node] = 1 / (1 + Math.exp(-sum));
}
output = input = activeOutputs;
}
if (!output) {
throw new Error('output was empty');
}
return output;
}
_runInputRelu(input) {
this.outputs[0] = input; // set output state of input layer
let output = null;
for (let layer = 1; layer <= this.outputLayer; layer++) {
const activeSize = this.sizes[layer];
const activeWeights = this.weights[layer];
const activeBiases = this.biases[layer];
const activeOutputs = this.outputs[layer];
for (let node = 0; node < activeSize; node++) {
const weights = activeWeights[node];
let sum = activeBiases[node];
for (let k = 0; k < weights.length; k++) {
sum += weights[k] * input[k];
}
// relu
activeOutputs[node] = sum < 0 ? 0 : sum;
}
output = input = activeOutputs;
}
if (!output) {
throw new Error('output was empty');
}
return output;
}
_runInputLeakyRelu(input) {
this.outputs[0] = input; // set output state of input layer
const { leakyReluAlpha } = this.trainOpts;
let output = null;
for (let layer = 1; layer <= this.outputLayer; layer++) {
const activeSize = this.sizes[layer];
const activeWeights = this.weights[layer];
const activeBiases = this.biases[layer];
const activeOutputs = this.outputs[layer];
for (let node = 0; node < activeSize; node++) {
const weights = activeWeights[node];
let sum = activeBiases[node];
for (let k = 0; k < weights.length; k++) {
sum += weights[k] * input[k];
}
// leaky relu
activeOutputs[node] = Math.max(sum, leakyReluAlpha * sum);
}
output = input = activeOutputs;
}
if (!output) {
throw new Error('output was empty');
}
return output;
}
_runInputTanh(input) {
this.outputs[0] = input; // set output state of input layer
let output = null;
for (let layer = 1; layer <= this.outputLayer; layer++) {
const activeSize = this.sizes[layer];
const activeWeights = this.weights[layer];
const activeBiases = this.biases[layer];
const activeOutputs = this.outputs[layer];
for (let node = 0; node < activeSize; node++) {
const weights = activeWeights[node];
let sum = activeBiases[node];
for (let k = 0; k < weights.length; k++) {
sum += weights[k] * input[k];
}
// tanh
activeOutputs[node] = Math.tanh(sum);
}
output = input = activeOutputs;
}
if (!output) {
throw new Error('output was empty');
}
return output;
}
/**
*
* Verifies network sizes are initialized
* If they are not it will initialize them based off the data set.
*/
verifyIsInitialized(preparedData) {
if (this.sizes.length)
return;
this.sizes = [];
this.sizes.push(preparedData[0].input.length);
if (!this.options.hiddenLayers) {
this.sizes.push(Math.max(3, Math.floor(preparedData[0].input.length / 2)));
}
else {
this.options.hiddenLayers.forEach((size) => {
this.sizes.push(size);
});
}
this.sizes.push(preparedData[0].output.length);
this.initialize();
}
updateTrainingOptions(trainOpts) {
const merged = { ...this.trainOpts, ...trainOpts };
this.validateTrainingOptions(merged);
this.trainOpts = merged;
this.setLogMethod(this.trainOpts.log);
}
validateTrainingOptions(options) {
const validations = {
activation: () => {
return ['sigmoid', 'relu', 'leaky-relu', 'tanh'].includes(options.activation);
},
iterations: () => {
const val = options.iterations;
return typeof val === 'number' && val > 0;
},
errorThresh: () => {
const val = options.errorThresh;
return typeof val === 'number' && val > 0 && val < 1;
},
log: () => {
const val = options.log;
return typeof val === 'function' || typeof val === 'boolean';
},
logPeriod: () => {
const val = options.logPeriod;
return typeof val === 'number' && val > 0;
},
leakyReluAlpha: () => {
const val = options.leakyReluAlpha;
return typeof val === 'number' && val > 0 && val < 1;
},
learningRate: () => {
const val = options.learningRate;
return typeof val === 'number' && val > 0 && val < 1;
},
momentum: () => {
const val = options.momentum;
return typeof val === 'number' && val > 0 && val < 1;
},
callback: () => {
const val = options.callback;
return typeof val === 'function' || val === undefined;
},
callbackPeriod: () => {
const val = options.callbackPeriod;
return typeof val === 'number' && val > 0;
},
timeout: () => {
const val = options.timeout;
return typeof val === 'number' && val > 0;
},
praxis: () => {
const val = options.praxis;
return !val || val === 'adam';
},
beta1: () => {
const val = options.beta1;
return val > 0 && val < 1;
},
beta2: () => {
const val = options.beta2;
return val > 0 && val < 1;
},
epsilon: () => {
const val = options.epsilon;
return val > 0 && val < 1;
},
};
for (const p in validations) {
const v = options;
if (!validations[p]()) {
throw new Error(`[${p}, ${v[p]}] is out of normal training range, your network will probably not train.`);
}
}
}
/**
*
* Gets JSON of trainOpts object
* NOTE: Activation is stored directly on JSON object and not in the training options
*/
getTrainOptsJSON() {
const { activation, iterations, errorThresh, log, logPeriod, leakyReluAlpha, learningRate, momentum, callbackPeriod, timeout, praxis, beta1, beta2, epsilon, } = this.trainOpts;
return {
activation,
iterations,
errorThresh,
log: typeof log === 'function'
? true
: typeof log === 'boolean'
? log
: false,
logPeriod,
leakyReluAlpha,
learningRate,
momentum,
callbackPeriod,
timeout: timeout === Infinity ? 'Infinity' : timeout,
praxis,
beta1,
beta2,
epsilon,
};
}
setLogMethod(log) {
if (typeof log === 'function') {
this.trainOpts.log = log;
}
else if (log) {
this.trainOpts.log = this.logTrainingStatus;
}
else {
this.trainOpts.log = false;
}
}
logTrainingStatus(status) {
console.log(`iterations: ${status.iterations}, training error: ${status.error}`);
}
calculateTrainingError(data) {
let sum = 0;
for (let i = 0; i < data.length; ++i) {
sum += this.trainPattern(data[i], true);
}
return sum / data.length;
}
trainPatterns(data) {
for (let i = 0; i < data.length; ++i) {
this.trainPattern(data[i]);
}
}
trainingTick(data, status, endTime) {
const { callback, callbackPeriod, errorThresh, iterations, log, logPeriod, } = this.trainOpts;
if (status.iterations >= iterations ||
status.error <= errorThresh ||
Date.now() >= endTime) {
return false;
}
status.iterations++;
if (log && status.iterations % logPeriod === 0) {
status.error = this.calculateTrainingError(data);
log(status);
}
else if (status.iterations % this.errorCheckInterval === 0) {
status.error = this.calculateTrainingError(data);
}
else {
this.trainPatterns(data);
}
if (callback && status.iterations % callbackPeriod === 0) {
callback({
iterations: status.iterations,
error: status.error,
});
}
return true;
}
prepTraining(data, options = {}) {
this.updateTrainingOptions(options);
const preparedData = this.formatData(data);
const endTime = Date.now() + this.trainOpts.timeout;
const status = {
error: 1,
iterations: 0,
};
this.verifyIsInitialized(preparedData);
return {
preparedData,
status,
endTime,
};
}
train(data, options = {}) {
const { preparedData, status, endTime } = this.prepTraining(data, options);
while (true) {
if (!this.trainingTick(preparedData, status, endTime)) {
break;
}
}
return status;
}
async trainAsync(data, options = {}) {
const { preparedData, status, endTime } = this.prepTraining(data, options);
return await new Promise((resolve, reject) => {
try {
const thawedTrain = new browser.Thaw(new Array(this.trainOpts.iterations), {
delay: true,
each: () => this.trainingTick(preparedData, status, endTime) ||
thawedTrain.stop(),
done: () => resolve(status),
});
thawedTrain.tick();
}
catch (trainError) {
reject(trainError);
}
});
}
trainPattern(value, logErrorRate) {
// forward propagate
this.runInput(value.input);
// back propagate
this.calculateDeltas(value.output);
this.adjustWeights();
if (logErrorRate) {
return mse$1(this.errors[this.outputLayer]);
}
return null;
}
_calculateDeltasSigmoid(target) {
for (let layer = this.outputLayer; layer >= 0; layer--) {
const activeSize = this.sizes[layer];
const activeOutput = this.outputs[layer];
const activeError = this.errors[layer];
const activeDeltas = this.deltas[layer];
const nextLayer = this.weights[layer + 1];
for (let node = 0; node < activeSize; node++) {
const output = activeOutput[node];
let error = 0;
if (layer === this.outputLayer) {
error = target[node] - output;
}
else {
const deltas = this.deltas[layer + 1];
for (let k = 0; k < deltas.length; k++) {
error += deltas[k] * nextLayer[k][node];
}
}
activeError[node] = error;
activeDeltas[node] = error * output * (1 - output);
}
}
}
_calculateDeltasRelu(target) {
for (let layer = this.outputLayer; layer >= 0; layer--) {
const currentSize = this.sizes[layer];
const currentOutputs = this.outputs[layer];
const nextWeights = this.weights[layer + 1];
const nextDeltas = this.deltas[layer + 1];
const currentErrors = this.errors[layer];
const currentDeltas = this.deltas[layer];
for (let node = 0; node < currentSize; node++) {
const output = currentOutputs[node];
let error = 0;
if (layer === this.outputLayer) {
error = target[node] - output;
}
else {
for (let k = 0; k < nextDeltas.length; k++) {
error += nextDeltas[k] * nextWeights[k][node];
}
}
currentErrors[node] = error;
currentDeltas[node] = output > 0 ? error : 0;
}
}
}
_calculateDeltasLeakyRelu(target) {
const alpha = this.trainOpts.leakyReluAlpha;
for (let layer = this.outputLayer; layer >= 0; layer--) {
const currentSize = this.sizes[layer];
const currentOutputs = this.outputs[layer];
const nextDeltas = this.deltas[layer + 1];
const nextWeights = this.weights[layer + 1];
const currentErrors = this.errors[layer];
const currentDeltas = this.deltas[layer];
for (let node = 0; node < currentSize; node++) {
const output = currentOutputs[node];
let error = 0;
if (layer === this.outputLayer) {
error = target[node] - output;
}
else {
for (let k = 0; k < nextDeltas.length; k++) {
error += nextDeltas[k] * nextWeights[k][node];
}
}
currentErrors[node] = error;
currentDeltas[node] = output > 0 ? error : alpha * error;
}
}
}
_calculateDeltasTanh(target) {
for (let layer = this.outputLayer; layer >= 0; layer--) {
const currentSize = this.sizes[layer];
const currentOutputs = this.outputs[layer];
const nextDeltas = this.deltas[layer + 1];
const nextWeights = this.weights[layer + 1];
const currentErrors = this.errors[layer];
const currentDeltas = this.deltas[layer];
for (let node = 0; node < currentSize; node++) {
const output = currentOutputs[node];
let error = 0;
if (layer === this.outputLayer) {
error = target[node] - output;
}
else {
for (let k = 0; k < nextDeltas.length; k++) {
error += nextDeltas[k] * nextWeights[k][node];
}
}
currentErrors[node] = error;
currentDeltas[node] = (1 - output * output) * error;
}
}
}
/**
*
* Changes weights of networks
*/
adjustWeights() {
const { learningRate, momentum } = this.trainOpts;
for (let layer = 1; layer <= this.outputLayer; layer++) {
const incoming = this.outputs[layer - 1];
const activeSize = this.sizes[layer];
const activeDelta = this.deltas[layer];
const activeChanges = this.changes[layer];
const activeWeights = this.weights[layer];
const activeBiases = this.biases[layer];
for (let node = 0; node < activeSize; node++) {
const delta = activeDelta[node];
for (let k = 0; k < incoming.length; k++) {
let change = activeChanges[node][k];
change = learningRate * delta * incoming[k] + momentum * change;
activeChanges[node][k] = change;
activeWeights[node][k] += change;
}
activeBiases[node] += learningRate * delta;
}
}
}
_setupAdam() {
this.biasChangesLow = [];
this.biasChangesHigh = [];
this.changesLow = [];
this.changesHigh = [];
this.iterations = 0;
for (let layer = 0; layer <= this.outputLayer; layer++) {
const size = this.sizes[layer];
if (layer > 0) {
this.biasChangesLow[layer] = zeros$1(size);
this.biasChangesHigh[layer] = zeros$1(size);
this.changesLow[layer] = new Array(size);
this.changesHigh[layer] = new Array(size);
for (let node = 0; node < size; node++) {
const prevSize = this.sizes[layer - 1];
this.changesLow[layer][node] = zeros$1(prevSize);
this.changesHigh[layer][node] = zeros$1(prevSize);
}
}
}
this.adjustWeights = this._adjustWeightsAdam;
}
_adjustWeightsAdam() {
this.iterations++;
const { iterations } = this;
const { beta1, beta2, epsilon, learningRate } = this.trainOpts;
for (let layer = 1; layer <= this.outputLayer; layer++) {
const incoming = this.outputs[layer - 1];
const currentSize = this.sizes[layer];
const currentDeltas = this.deltas[layer];
const currentChangesLow = this.changesLow[layer];
const currentChangesHigh = this.changesHigh[layer];
const currentWeights = this.weights[layer];
const currentBiases = this.biases[layer];
const currentBiasChangesLow = this.biasChangesLow[layer];
const currentBiasChangesHigh = this.biasChangesHigh[layer];
for (let node = 0; node < currentSize; node++) {
const delta = currentDeltas[node];
for (let k = 0; k < incoming.length; k++) {
const gradient = delta * incoming[k];
const changeLow = currentChangesLow[node][k] * beta1 + (1 - beta1) * gradient;
const changeHigh = currentChangesHigh[node][k] * beta2 +
(1 - beta2) * gradient * gradient;
const momentumCorrection = changeLow / (1 - Math.pow(beta1, iterations));
const gradientCorrection = changeHigh / (1 - Math.pow(beta2, iterations));
currentChangesLow[node][k] = changeLow;
currentChangesHigh[node][k] = changeHigh;
currentWeights[node][k] +=
(learningRate * momentumCorrection) /
(Math.sqrt(gradientCorrection) + epsilon);
}
const biasGradient = currentDeltas[node];
const biasChangeLow = currentBiasChangesLow[node] * beta1 + (1 - beta1) * biasGradient;
const biasChangeHigh = currentBiasChangesHigh[node] * beta2 +
(1 - beta2) * biasGradient * biasGradient;
const biasMomentumCorrection = currentBiasChangesLow[node] / (1 - Math.pow(beta1, iterations));
const biasGradientCorrection = currentBiasChangesHigh[node] / (1 - Math.pow(beta2, iterations));
currentBiasChangesLow[node] = biasChangeLow;
currentBiasChangesHigh[node] = biasChangeHigh;
currentBiases[node] +=
(learningRate * biasMomentumCorrection) /
(Math.sqrt(biasGradientCorrection) + epsilon);
}
}
}
formatData(data) {
if (!Array.isArray(data[0].input)) {
if (this.inputLookup) {
this.inputLookupLength = Object.keys(this.inputLookup).length;
}
else {
const inputLookup = new LookupTable(data, 'input');
this.inputLookup = inputLookup.table;
this.inputLookupLength = inputLookup.length;
}
}
if (!Array.isArray(data[0].output)) {
if (this.outputLookup) {
this.outputLookupLength = Object.keys(this.outputLookup).length;
}
else {
const lookup = new LookupTable(data, 'output');
this.outputLookup = lookup.table;
this.outputLookupLength = lookup.length;
}
}
if (!this._formatInput) {
this._formatInput = getTypedArrayFn(data[0].input, this.inputLookup);
}
if (!this._formatOutput) {
this._formatOutput = getTypedArrayFn(data[0].output, this.outputLookup);
}
// turn sparse hash input into arrays with 0s as filler
if (this._formatInput && this._formatOutput) {
const result = [];
for (let i = 0; i < data.length; i++) {
result.push({
input: this._formatInput(data[i].input),
output: this._formatOutput(data[i].output),
});
}
return result;
}
if (this._formatInput) {
const result = [];
for (let i = 0; i < data.length; i++) {
result.push({
input: this._formatInput(data[i].input),
output: data[i].output,
});
}
return result;
}
if (this._formatOutput) {
const result = [];
for (let i = 0; i < data.length; i++) {
result.push({
input: data[i].input,
output: this._formatOutput(data[i].output),
});
}
return result;
}
return data;
}
addFormat(data) {
var _a, _b;
if (!Array.isArray(data.input) || typeof data.input[0] !== 'number') {
this.inputLookup = lookup.addKeys(data.input, (_a = this.inputLookup) !== null && _a !== void 0 ? _a : {});
if (this.inputLookup) {
this.inputLookupLength = Object.keys(this.inputLookup).length;
}
}
if (!Array.isArray(data.output) || typeof data.output[0] !== 'number') {
this.outputLookup = lookup.addKeys(data.output, (_b = this.outputLookup) !== null && _b !== void 0 ? _b : {});
if (this.outputLookup) {
this.outputLookupLength = Object.keys(this.outputLookup).length;
}
}
}
test(data) {
const { preparedData } = this.prepTraining(data);
// for binary classification problems with one output node
const isBinary = preparedData[0].output.length === 1;
// for classification problems
const misclasses = [];
// run each pattern through the trained network and collect
// error and misclassification statistics
let errorSum = 0;
if (isBinary) {
let falsePos = 0;
let falseNeg = 0;
let truePos = 0;
let trueNeg = 0;
for (let i = 0; i < preparedData.length; i++) {
const output = this.runInput(preparedData[i].input);
const target = preparedData[i].output;
const actual = output[0] > this.options.binaryThresh ? 1 : 0;
const expected = target[0];
if (actual !== expected) {
const misclass = preparedData[i];
misclasses.push({
input: misclass.input,
output: misclass.output,
actual,
expected,
});
}
if (actual === 0 && expected === 0) {
trueNeg++;
}
else if (actual === 1 && expected === 1) {
truePos++;
}
else if (actual === 0 && expected === 1) {
falseNeg++;
}
else if (actual === 1 && expected === 0) {
falsePos++;
}
errorSum += mse$1(output.map((value, i) => {
return target[i] - value;
}));
}
return {
error: errorSum / preparedData.length,
misclasses,
total: preparedData.length,
trueNeg,
truePos,
falseNeg,
falsePos,
precision: truePos > 0 ? truePos / (truePos + falsePos) : 0,
recall: truePos > 0 ? truePos / (truePos + falseNeg) : 0,
accuracy: (trueNeg + truePos) / preparedData.length,
};
}
for (let i = 0; i < preparedData.length; i++) {
const output = this.runInput(preparedData[i].input);
const target = preparedData[i].output;
const actual = output.indexOf(max(output));
const expected = target.indexOf(max(target));
if (actual !== expected) {
const misclass = preparedData[i];
misclasses.push({
input: misclass.input,
output: misclass.output,
actual,
expected,
});
}
errorSum += mse$1(output.map((value, i) => {
return target[i] - value;
}));
}
return {
error: errorSum / preparedData.length,
misclasses,
total: preparedData.length,
};
}
toJSON() {
var _a, _b;
if (!this.isRunnable) {
this.initialize();
}
// use Array.from, keeping json small
const jsonLayerWeights = this.weights.map((layerWeights) => {
return layerWeights.map((layerWeights) => Array.from(layerWeights));
});
const jsonLayerBiases = this.biases.map((layerBiases) => Array.from(layerBiases));
const jsonLayers = [];
const outputLength = this.sizes.length - 1;
for (let i = 0; i <= outputLength; i++) {
jsonLayers.push({
weights: (_a = jsonLayerWeights[i]) !== null && _a !== void 0 ? _a : [],
biases: (_b = jsonLayerBiases[i]) !== null && _b !== void 0 ? _b : [],
});
}
return {
type: 'NeuralNetwork',
sizes: [...this.sizes],
layers: jsonLayers,
inputLookup: this.inputLookup ? { ...this.inputLookup } : null,
inputLookupLength: this.inputLookupLength,
outputLookup: this.outputLookup ? { ...this.outputLookup } : null,
outputLookupLength: this.outputLookupLength,
options: { ...this.options },
trainOpts: this.getTrainOptsJSON(),
};
}
fromJSON(json) {
this.options = { ...defaults$2(), ...json.options };
if (json.hasOwnProperty('trainOpts')) {
const trainOpts = {
...json.trainOpts,
timeout: json.trainOpts.timeout === 'Infinity'
? Infinity
: json.trainOpts.timeout,
};
this.updateTrainingOptions(trainOpts);
}
this.sizes = json.sizes;
this.initialize();
this.inputLookup = json.inputLookup ? { ...json.inputLookup } : null;
this.inputLookupLength = json.inputLookupLength;
this.outputLookup = json.outputLookup ? { ...json.outputLookup } : null;
this.outputLookupLength = json.outputLookupLength;
const jsonLayers = json.layers;
const layerWeights = this.weights.map((layerWeights, layerIndex) => {
return jsonLayers[layerIndex].weights.map((layerWeights) => Float32Array.from(layerWeights));
});
const layerBiases = this.biases.map((layerBiases, layerIndex) => Float32Array.from(jsonLayers[layerIndex].biases));
for (let i = 0; i <= this.outputLayer; i++) {
this.weights[i] = layerWeights[i] || [];
this.biases[i] = layerBiases[i] || [];
}
return this;
}
toFunction(cb) {
const { activation, leakyReluAlpha } = this.trainOpts;
let needsVar = false;
const nodeHandle = (layerIndex, nodeIndex) => {
if (layerIndex === 0) {
return `(input[${nodeIndex}]||0)`;
}
const weights = this.weights[layerIndex][nodeIndex];
const bias = this.biases[layerIndex][nodeIndex];
if (!weights) {
throw new Error(`weights at layerIndex ${layerIndex} & nodeIndex ${nodeIndex} not found`);
}
if (!bias) {
throw new Error(`bias as layerIndex ${layerIndex} & nodeIndex ${nodeIndex} not found`);
}
const weightsArray = [];
weights.forEach((weight, subNodeIndex) => {
if (weight < 0) {
weightsArray.push(`${weight}*${nodeHandle(layerIndex - 1, subNodeIndex)}`);
}
else {
weightsArray.push(`+${weight}*${nodeHandle(layerIndex - 1, subNodeIndex)}`);
}
});
const result = `(${bias.toString()}${weightsArray.join('')})`;
switch (activation) {
case 'sigmoid':
return `1/(1+1/Math.exp(${result}))`;
case 'relu': {
needsVar = true;
return `((v=${result})<0?0:v)`;
}
case 'leaky-relu': {
needsVar = true;
return `Math.max((v=${result}),${leakyReluAlpha}*v)`;
}
case 'tanh':
return `Math.tanh(${result})`;
default:
throw new Error(`Unknown activation ${activation}. Available activations are: 'sigmoid', 'relu', 'leaky-relu', 'tanh'`);
}
};
function checkKeys(keys) {
if (keys.find((v) => v.includes('"'))) {
throw new Error(`key contains '"', which is not compatible`);
}
}
const layersAsMath = [];
let result;
let inputLookup = '';
if (this.inputLookup) {
const keys = Object.keys(this.inputLookup);
checkKeys(keys);
inputLookup = `input = new Float32Array([${Object.keys(this.inputLookup)
.map((key) => `input["${key}"]`)
.join(',')}]);`;
}
if (this.sizes.length < 1)
throw new Error('No layers');
for (let nodeIndex = 0; nodeIndex < this.sizes[this.outputLayer]; nodeIndex++) {
layersAsMath.push(nodeHandle(this.outputLayer, nodeIndex));
}
if (this.outputLookup) {
const keys = Object.keys(this.outputLookup);
checkKeys(keys);
const values = keys
.map((key, i) => `"${key}":${layersAsMath[i]}`)
.join(',');
result = `{${values}}`;
}
else {
result = `[${layersAsMath.join(',')}]`;
}
const source = `${inputLookup}${needsVar ? 'var v;' : ''}return ${result};`;
// eslint-disable-next-line @typescript-eslint/no-implied-eval,no-new-func
return new Function('input', cb ? cb(source) : source);
}
}
function weightedSumSigmoid(weights, biases, inputs) {
let sum = biases[this.thread.x];
for (let k = 0; k < this.constants.size; k++) {
sum += weights[this.thread.x][k] * inputs[k];
}
// sigmoid
return 1 / (1 + Math.exp(-sum));
}
function weightedSumRelu(weights, biases, inputs) {
let sum = biases[this.thread.x];
for (let k = 0; k < this.constants.size; k++) {
sum += weights[this.thread.x][k] * inputs[k];
}
// relu
return sum < 0 ? 0 : sum;
}
function weightedSumLeakyRelu(weights, biases, inputs) {
let sum = biases[this.thread.x];
for (let k = 0; k < this.constants.size; k++) {
sum += weights[this.thread.x][k] * inputs[k];
}
// leaky relu
return sum < 0 ? 0 : 0.01 * sum;
}
function weightedSumTanh(weights, biases, inputs) {
let sum = biases[this.thread.x];
for (let k = 0; k < this.constants.size; k++) {
sum += weights[this.thread.x][k] * inputs[k];
}
// tanh
return Math.tanh(sum);
}
function calcErrorOutput(output, target) {
return target - output;
}
function calcDeltasSigmoid(error, output) {
// sigmoid derivative
return error * output * (1 - output);
}
function calcDeltasRelu(error, output) {
// relu derivative
return output > 0 ? error : 0;
}
function calcDeltasLeakyRelu(error, output) {
// leaky relu derivative
return output > 0 ? error : 0.01 * error;
}
function calcDeltasTanh(error, output) {
// tanh derivative
return (1 - output * output) * error;
}
function calcError(x, size, nextWeights, nextDeltas) {
let error = 0;
for (let k = 0; k < size; k++) {
error += nextDeltas[k] * nextWeights[k][x];
}
return error;
}
function calcChanges(learningRate, momentum, previousChange, delta, previousOutput) {
return learningRate * delta * previousOutput + momentum * previousChange;
}
function addWeights(change, weight) {
return change + weight;
}
function addBiases(biases, deltas) {
return (biases[this.thread.x] + deltas[this.thread.x] * this.constants.learningRate);
}
// mean squared error, reimplemented for GPU
function mse(errors) {
let sum = 0;
for (let i = 0; i < this.constants.size; i++) {
sum += errors[i] ** 2;
}
return sum / this.constants.size;
}
class NeuralNetworkGPU extends NeuralNetwork {
constructor(options = {}) {
super(options);
this.texturizeInputData = () => {
throw new Error('not yet setup');
};
this.forwardPropagate = [];
this.backwardPropagate = [];
this.changesPropagate = [];
this.biasesPropagate = [];
this.getMSE = () => {
throw new Error('not yet setup');
};
this._addMSE = () => {
throw new Error('not yet setup');
};
this._divideMSESum = () => {
throw new Error('not yet setup');
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.outputs = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.deltas = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.errors = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.weights = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.changes = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.biases = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.runInput = (input) => {
let output;
this.outputs[0] = input;
for (let layer = 1; layer <= this.outputLayer; layer++) {
release(this.outputs[layer]);
this.outputs[layer] = this.forwardPropagate[layer](this.weights[layer], this.biases[layer], input);
output = input = this.outputs[layer];
}
return output;
};
this.calculateDeltas = (target) => {
for (let layer = this.outputLayer; layer > 0; layer--) {
release(this.deltas[layer]);
release(this.errors[layer]);
let output;
if (layer === this.outputLayer) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
output = this.backwardPropagate[layer](this.outputs[layer], target);
}
else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
output = this.backwardPropagate[layer](this.weights[layer + 1], this.outputs[layer], this.deltas[layer + 1]);
}
this.deltas[layer] = output.result;
this.errors[layer] = output.error;
}
};
this.errorCheckInterval = 100;
this.gpu = new gpu_js.GPU({ mode: options.mode });
}
initialize() {
super.initialize();
this.buildRunInput();
this.buildCalculateDeltas();
this.buildGetChanges();
this.buildChangeBiases();
this.buildGetMSE();
}
setActivation() { }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
trainPattern(value, logErrorRate) {
// forward propagate
this.runInput(value.input);
// back propagate
this.calculateDeltas(value.output);
this.adjustWeights();
if (logErrorRate) {
return this.getMSE(this.errors[this.outputLayer]);
}
return null;
}
calculateTrainingError(data) {
let sum = new Float32Array([0]);
for (let i = 0; i < data.length; ++i) {
const prevSum = sum;
const error = this.trainPattern(data[i], true);
sum = this._addMSE(sum, error);
release(error);
release(prevSum);
}
const result = this._divideMSESum(data.length, sum);
release(sum);
return (result instanceof gpu_js.Texture
? result.toArray()
: result)[0];
}
adjustWeights() {
this.getChanges();
this.changeBiases();
}
buildRunInput() {
let weightedSum = null;
switch (this.trainOpts.activation) {
case 'sigmoid':
weightedSum = weightedSumSigmoid;
break;
case 'relu':
weightedSum = weightedSumRelu;
break;
case 'leaky-relu':
weightedSum = weightedSumLeakyRelu;
break;
case 'tanh':
weightedSum = weightedSumTanh;
break;
default:
throw new Error(`Unknown activation ${this.trainOpts.activation}. Available activations are: 'sigmoid', 'relu', 'leaky-relu', 'tanh'`);
}
for (let layer = 1; layer <= this.outputLayer; layer++) {
this.forwardPropagate[layer] = this.gpu.createKernel(weightedSum, {
output: [this.sizes[layer]],
pipeline: true,
constants: {
size: this.sizes[layer - 1],
},
immutable: true,
});
}
this.texturizeInputData = this.gpu.createKernel(function (value) {
return value[this.thread.x];
}, {
output: [this.sizes[1]],
pipeline: true,
immutable: true,
});
}
buildCalculateDeltas() {
let calcDeltas;
switch (this.trainOpts.activation) {
case 'sigmoid':
calcDeltas = calcDeltasSigmoid;
break;
case 'relu':
calcDeltas = calcDeltasRelu;
break;
case 'leaky-relu':
calcDeltas = calcDeltasLeakyRelu;
break;
case 'tanh':
calcDeltas = calcDeltasTanh;
break;
default:
throw new Error(`Unknown activation ${this.trainOpts.activation}. Available activations are: 'sigmoid', 'relu', 'leaky-relu', 'tanh'`);
}
calcDeltas = gpu_js.alias(gpu_js.utils.getMinifySafeName(() => calcDeltas), calcDeltas);
this.gpu.addFunction(calcDeltas);
for (let layer = this.outputLayer; layer > 0; layer--) {
if (layer === this.outputLayer) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.backwardPropagate[this.outputLayer] = this.gpu.createKernelMap({
error: calcErrorOutput,
}, function (outputs, targets) {
const output = outputs[this.thread.x];
const target = targets[this.thread.x];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return calcDeltas(calcErrorOutput(output, target), output);
}, {
output: [this.sizes[this.outputLayer]],
pipeline: true,
immutable: true,
});
}
else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.backwardPropagate[layer] = this.gpu.createKernelMap({
error: calcError,
}, function (nextWeights, outputs, nextDeltas) {
const output = outputs[this.thread.x];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return calcDeltas(calcError(this.thread.x, this.constants.size, nextWeights, nextDeltas), output);
}, {
output: [this.sizes[layer]],
pipeline: true,
constants: {
size: this.sizes[layer + 1],
},
immutable: true,
});
}
}
}
buildGetChanges() {
for (let layer = 1; layer <= this.outputLayer; layer++) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.changesPropagate[layer] = this.gpu.createKernelMap({
weights: addWeights,
changes: calcChanges,
}, function (previousOutputs, deltas, weights, previousChanges) {
const change = calcChanges(this.constants.learningRate, this.constants.momentum, previousChanges[this.thread.y][this.thread.x], deltas[this.thread.y], previousOutputs[this.thread.x]);
return addWeights(change, weights[this.thread.y][this.thread.x]);
}, {
output: [this.sizes[layer - 1], this.sizes[layer]],
pipeline: true,
constants: {
size: this.sizes[layer - 1],
learningRate: this.trainOpts.learningRate,
momentum: this.trainOpts.momentum,
},
immutable: true,
});
}
}
getChanges() {
for (let layer = 1; layer <= this.outputLayer; layer++) {
const weights = this.weights[layer];
const changes = this.changes[layer];
const output = this.changesPropagate[layer](this.outputs[layer - 1], this.deltas[layer], weights, changes);
release(weights);
release(changes);
this.weights[layer] = output.weights;
this.changes[layer] = output.changes;
release(output.result);
}
}
buildChangeBiases() {
for (let layer = 1; layer <= this.outputLayer; layer++) {
this.biasesPropagate[layer] = this.gpu.createKernel(addBiases, {
output: [this.sizes[layer]],
pipeline: true,
constants: {
learningRate: this.trainOpts.learningRate,
},
immutable: true,
});
}
}
changeBiases() {
for (let layer = 1; layer <= this.outputLayer; layer++) {
const biases = this.biases[layer];
this.biases[layer] = this.biasesPropagate[layer](biases, this.deltas[layer]);
release(biases);
}
}
buildGetMSE() {
this.getMSE = this.gpu.createKernel(mse, {
output: [1],
constants: {
size: this.sizes[this.outputLayer],
},
pipeline: true,
immutable: true,
});
this._addMSE = this.gpu.createKernel(function (value1, value2) {
return value1[0] + value2[0];
}, {
output: [1],
pipeline: true,
immutable: true,
});
this._divideMSESum = this.gpu.createKernel(function (length, mseSum) {
const value = mseSum[0];
if (value > 0) {
return value / length;
}
return 0;
}, {
output: [1],
});
}
run(input) {
if (!this.isRunnable) {
throw new Error('network not runnable');
}
let formattedInput;
if (this.inputLookup) {
formattedInput = lookup.toArray(this.inputLookup, input, this.inputLookupLength);
}
else {
formattedInput = input;
}
const outputTextures = this.runInput(formattedInput);
const output = outputTextures instanceof gpu_js.Texture
? outputTextures.toArray()
: outputTextures;
if (this.outputLookup) {
return lookup.toObject(this.outputLookup, output);
}
return output;
}
// @ts-expect-error the underlying network works as normal, but we are working on the GPU
prepTraining(data, options = {}) {
this.updateTrainingOptions(options);
const preparedData = this.formatData(data);
const endTime = Date.now() + this.trainOpts.timeout;
const status = {
error: 1,
iterations: 0,
};
this.verifyIsInitialized(preparedData);
const texturizeOutputData = this.gpu.createKernel(function (value) {
return value[this.thread.x];
}, {
output: [preparedData[0].output.length],
pipeline: true,
immutable: true,
});
return {
preparedData: preparedData.map((set) => ({
input: this.texturizeInputData(set.input),
output: texturizeOutputData(set.output),
})),
status,
endTime,
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
toFunction() {
throw new Error(`${this.constructor.name}-toFunction is not yet implemented`);
}
toJSON() {
var _a, _b;
if (this.sizes === null) {
this.initialize();
}
// use Array.from, keeping json small
const jsonLayerWeights = this.weights.map((layerWeights) => {
return (layerWeights instanceof gpu_js.Texture
? layerWeights.toArray()
: layerWeights).map((layerWeights) => Array.from(layerWeights));
});
const jsonLayerBiases = this.biases.map((layerBiases) => Array.from(layerBiases instanceof gpu_js.Texture
? layerBiases.toArray()
: layerBiases));
const jsonLayers = [];
for (let i = 0; i <= this.outputLayer; i++) {
jsonLayers.push({
weights: (_a = jsonLayerWeights[i]) !== null && _a !== void 0 ? _a : [],
biases: (_b = jsonLayerBiases[i]) !== null && _b !== void 0 ? _b : [],
});
}
return {
type: 'NeuralNetworkGPU',
sizes: [...this.sizes],
layers: jsonLayers,
inputLookup: this.inputLookup ? { ...this.inputLookup } : null,
inputLookupLength: this.inputLookupLength,
outputLookup: this.outputLookup ? { ...this.outputLookup } : null,
outputLookupLength: this.outputLookupLength,
options: { ...this.options },
trainOpts: this.getTrainOptsJSON(),
};
}
}
class RecurrentConnection extends Internal {
constructor() {
super(...arguments);
this.settings = {};
this.layer = null;
}
setLayer(layer) {
this.layer = layer;
}
get width() {
if (!this.layer)
throw new Error('layer not set');
return this.layer.width;
}
set width(value) {
throw new Error(`${this.constructor.name}-width is not yet implemented`);
}
get height() {
if (!this.layer)
throw new Error('layer not set');
return this.layer.height;
}
set height(value) {
throw new Error(`${this.constructor.name}-height is not yet implemented`);
}
get deltas() {
if (!this.layer)
throw new Error('layer not set');
return this.layer.deltas;
}
set deltas(deltas) {
if (!this.layer)
throw new Error('layer not set');
release(this.layer.deltas);
this.layer.deltas = deltas;
}
get weights() {
if (!this.layer)
throw new Error('layer not set');
return this.layer.weights;
}
set weights(weights) {
if (!this.layer)
throw new Error('layer not set');
release(this.layer.weights);
this.layer.weights = weights;
}
predict() {
// throw new Error(`${this.constructor.name}-predict is not yet implemented`)
}
compare() {
// throw new Error(`${this.constructor.name}-compare is not yet implemented`)
}
learn() {
throw new Error('no longer using');
}
setupKernels() {
// throw new Error(
// `${this.constructor.name}-setupKernels is not yet implemented`
// )
}
reuseKernels() {
// throw new Error(
// `${this.constructor.name}-reuseKernels is not yet implemented`
// )
}
}
class Recurrent extends FeedForward {
// TODO: use generics in extend
constructor(options = {}) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
super(options);
this.trainOpts = {};
this._outputConnection = null;
this._layerSets = [];
this._hiddenLayerOutputIndices = [];
this._model = null;
}
_connectLayers() {
if (!this.options.inputLayer) {
throw new Error('inputLayer not found');
}
if (!this.options.outputLayer) {
throw new Error('outputLayer not found');
}
const inputLayer = this.options.inputLayer();
const hiddenLayers = this._connectHiddenLayers(inputLayer);
const outputLayer = this.options.outputLayer(hiddenLayers[hiddenLayers.length - 1], -1);
return {
inputLayer,
hiddenLayers,
outputLayer,
};
}
_connectLayersDeep() {
const layers = [];
const previousLayers = this._layerSets[this._layerSets.length - 1];
let usedHiddenLayerOutputIndex = 0;
function findInputLayer(inputLayer) {
const index = previousLayers.indexOf(inputLayer);
if (index < 0)
throw new Error('unable to find layer');
return layers[index];
}
function layerSettings(layer) {
return {
...layer.settings,
weights: null,
deltas: null,
praxis: null,
};
}
for (let i = 0; i < previousLayers.length; i++) {
const previousLayer = previousLayers[i];
let layer;
if (previousLayer instanceof Activation) {
layer = new previousLayer.constructor(findInputLayer(previousLayer.inputLayer), layerSettings(previousLayer));
}
else if (previousLayer instanceof EntryPoint) {
layer = new previousLayer.constructor(layerSettings(previousLayer));
}
else if (previousLayer instanceof Filter) {
layer = new previousLayer.constructor(layerSettings(previousLayer.inputLayer), findInputLayer(previousLayer.inputLayer));
}
else if (previousLayer instanceof Internal) {
const previousHiddenLayerOutput = previousLayers[this._hiddenLayerOutputIndices[usedHiddenLayerOutputIndex++]];
if (previousLayer instanceof RecurrentConnection) {
throw new Error('unfinished');
}
else if (previousLayer instanceof RecurrentInput) {
layer = new RecurrentInput(previousHiddenLayerOutput);
}
else if (previousLayer instanceof RecurrentZeros) {
layer = new RecurrentInput(previousHiddenLayerOutput);
}
else {
throw new Error(`hidden layer ${previousLayer.constructor.name} extends unknown hidden layer`);
}
}
else if (previousLayer instanceof InternalModel ||
previousLayer instanceof Model) {
layer = previousLayer;
}
else if (previousLayer instanceof Modifier) {
layer = new previousLayer.constructor(findInputLayer(previousLayer.inputLayer), layerSettings(previousLayer.inputLayer));
}
else if (previousLayer instanceof Operator) {
layer = new previousLayer.constructor(findInputLayer(previousLayer.inputLayer1), findInputLayer(previousLayer.inputLayer2), layerSettings(previousLayer));
}
else if (previousLayer instanceof Target) {
layer = new previousLayer.constructor(layerSettings(previousLayer), findInputLayer(previousLayer.inputLayer));
}
else {
throw new Error(`hidden layer ${previousLayer.constructor.name} extends unknown hidden layer`);
}
layers.push(layer);
}
return layers;
}
_connectHiddenLayers(previousLayer) {
const hiddenLayers = [];
if (!this.options.hiddenLayers)
throw new Error('hiddenLayers not defined');
for (let i = 0; i < this.options.hiddenLayers.length; i++) {
const recurrentInput = new RecurrentZeros();
const hiddenLayer = this.options.hiddenLayers[i](previousLayer, recurrentInput, i);
previousLayer = hiddenLayer;
hiddenLayers.push(hiddenLayer);
}
return hiddenLayers;
}
initialize() {
this._outputConnection = new RecurrentConnection();
let layerSet;
if (this.options.layers) {
layerSet = this._connectOptionsLayers();
}
else {
const { inputLayer, hiddenLayers, outputLayer } = this._connectLayers();
layerSet = flattenLayers([inputLayer, ...hiddenLayers, outputLayer]);
this._hiddenLayerOutputIndices = hiddenLayers.map((l) => layerSet.indexOf(l));
this._inputLayer = inputLayer;
this._hiddenLayers = hiddenLayers;
this._outputLayer = outputLayer;
}
this.layers = layerSet;
this._layerSets = [layerSet];
this._model = layerSet.filter((l) => l instanceof Model || l instanceof InternalModel);
this.initializeLayers(layerSet);
}
initializeDeep() {
const layers = this._connectLayersDeep();
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
layer.setupKernels(true);
layer.reuseKernels(this._layerSets[0][i]);
}
this._layerSets.push(layers);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
run(inputs) {
while (this._layerSets.length <= inputs.length) {
this.initializeDeep();
}
const result = this.runInputs(inputs);
if (result instanceof gpu_js.Texture) {
return result.toArray();
}
return result;
}
runInput(input) {
throw new Error('use .runInputs()');
}
runInputs(inputs) {
while (this._layerSets.length < inputs.length) {
this.initializeDeep();
}
const max = inputs.length - 1; // last output will be compared with last index
for (let x = 0; x <= max; x++) {
const layerSet = this._layerSets[x];
layerSet[0].predict(inputs[x]);
for (let i = 1; i < layerSet.length; i++) {
layerSet[i].predict();
}
}
const lastLayerUsed = this._layerSets[max];
const result = lastLayerUsed[lastLayerUsed.length - 1].weights;
this.end();
return result;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
train(data, options = {}) {
const { preparedData, status, endTime } = this._prepTraining(data, options);
let continueTicking = true;
const calculateError = () => this._calculateTrainingError(preparedData);
const trainPatters = () => this._trainPatterns(preparedData);
while (continueTicking) {
continueTicking = this._trainingTick(status, endTime, calculateError, trainPatters);
}
return status;
}
end() {
const x = this._layerSets.length - 1;
const lastLayerSet = this._layerSets[x];
lastLayerSet[0].predict([new Float32Array([0])]);
for (let i = 1; i < lastLayerSet.length; i++) {
lastLayerSet[i].predict();
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
transferData(formattedData) {
return formattedData;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
_prepTraining(data, options) {
this._updateTrainingOptions(options);
const endTime = this.trainOpts.timeout
? Date.now() + this.trainOpts.timeout
: 0;
const status = {
error: 1,
iterations: 0,
};
this.verifyIsInitialized();
return {
preparedData: this.transferData(data),
status,
endTime,
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
_calculateTrainingError(data) {
if (!this.meanSquaredError) {
throw new Error('this.meanSquaredError not setup');
}
let sum = new Float32Array(1);
for (let i = 0; i < data.length; ++i) {
const prevSum = sum;
const error = this._trainPattern(data[i], true);
sum = this.meanSquaredError.add(sum, error);
release(error);
release(prevSum);
}
const result = this.meanSquaredError.divide(data.length, sum);
release(sum);
if (result instanceof gpu_js.Texture) {
const resultArray = result.toArray();
return resultArray[0];
}
return result[0];
}
// TODO: more types
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
formatData(data) {
return data;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
_calculateDeltas(target) {
const lastLayerSet = this._layerSets[this._layerSets.length - 1];
// Iterate from the second to last layer backwards, propagating 0's
for (let i = lastLayerSet.length - 2; i >= 0; i--) {
lastLayerSet[i].compare();
}
for (let x = target.length - 2; x >= 0; x--) {
const layerSet = this._layerSets[x];
layerSet[layerSet.length - 1].compare(target[x + 1]);
for (let i = layerSet.length - 2; i >= 0; i--) {
layerSet[i].compare();
}
}
}
adjustWeights() {
var _a;
const _model = this._model;
for (let i = 0; i < _model.length; i++) {
_model[i].learn((_a = this.options.learningRate) !== null && _a !== void 0 ? _a : 0);
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
_trainPatterns(data) {
for (let i = 0; i < data.length; ++i) {
this._trainPattern(data[i], false);
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
_trainPattern(inputs, logErrorRate) {
// forward propagate
this.runInputs(inputs);
// back propagate
this._calculateDeltas(inputs);
this.adjustWeights();
if (logErrorRate) {
if (!this.meanSquaredError) {
throw new Error('this.meanSquaredError not setup');
}
let error = new Float32Array(1);
for (let i = 0, max = inputs.length - 2; i <= max; i++) {
const layerSet = this._layerSets[i];
const lastLayer = layerSet[layerSet.length - 1];
const prevError = error;
error = this.meanSquaredError.addAbsolute(prevError, lastLayer.errors);
release(prevError);
}
return clone(this.meanSquaredError.divide(inputs.length, error));
}
return null;
}
}
/**
* A matrix
*/
class Matrix {
constructor(rows, columns) {
this.rows = 0;
this.columns = 0;
if (rows)
this.rows = rows;
if (columns)
this.columns = columns;
this.weights = zeros$1(this.rows * this.columns);
this.deltas = zeros$1(this.rows * this.columns);
}
getWeight(row, col) {
// slow but careful accessor function
// we want row-major order
const ix = this.columns * row + col;
if (ix < 0 || ix >= this.weights.length) {
throw new Error('get accessor is skewed');
}
return this.weights[ix];
}
setWeight(row, col, v) {
// slow but careful accessor function
const ix = this.columns * row + col;
if (ix < 0 || ix >= this.weights.length) {
throw new Error('set accessor is skewed');
}
this.weights[ix] = v;
return this;
}
getDelta(row, col) {
// slow but careful accessor function
// we want row-major order
const ix = this.columns * row + col;
if (ix < 0 || ix >= this.deltas.length) {
throw new Error('get accessor is skewed');
}
return this.deltas[ix];
}
setDelta(row, col, v) {
// slow but careful accessor function
const ix = this.columns * row + col;
if (ix < 0 || ix >= this.weights.length) {
throw new Error('set accessor is skewed');
}
this.deltas[ix] = v;
return this;
}
toJSON() {
return {
rows: this.rows,
columns: this.columns,
weights: Array.from(this.weights.slice(0)),
};
}
static fromJSON(json) {
const matrix = new Matrix(json.rows, json.columns);
for (let i = 0, max = json.rows * json.columns; i < max; i++) {
matrix.weights[i] = json.weights[i]; // copy over weights
}
return matrix;
}
static fromArray(weights) {
const matrix = new Matrix(weights.length, weights[0].length);
matrix.fromArray(weights);
return matrix;
}
deltasToArray() {
return this.toArray('deltas');
}
weightsToArray() {
return this.toArray('weights');
}
toArray(prop = 'weights') {
const result = new Array(this.rows);
this.iterate({
row: (rowIndex) => {
result[rowIndex] = new Array(this.columns);
},
column: (rowIndex, columnIndex) => {
if (prop === 'weights') {
result[rowIndex][columnIndex] = this.getWeight(rowIndex, columnIndex);
}
else if (prop === 'deltas') {
result[rowIndex][columnIndex] = this.getDelta(rowIndex, columnIndex);
}
},
});
return result;
}
fromArray(array, prop = 'weights') {
if (array.length !== this.rows) {
throw new Error('rows do not match');
}
if (array[0].length !== this.columns) {
throw new Error('columns do not match');
}
this.iterate({
column: (rowIndex, columnIndex) => {
const value = array[rowIndex][columnIndex];
if (typeof value !== 'number') {
throw new Error('value not number');
}
if (prop === 'weights') {
this.setWeight(rowIndex, columnIndex, value);
}
else if (prop === 'deltas') {
this.setDelta(rowIndex, columnIndex, value);
}
},
});
return this;
}
iterate(callbacks) {
const rows = this.rows;
const columns = this.columns;
for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
if (callbacks.row) {
callbacks.row(rowIndex);
}
for (let columnIndex = 0; columnIndex < columns; columnIndex++) {
if (callbacks.column) {
callbacks.column(rowIndex, columnIndex);
}
}
}
return this;
}
}
/** return Matrix but filled with random numbers from gaussian
*/
class RandomMatrix extends Matrix {
constructor(rows, columns, std) {
super(rows, columns);
this.std = std;
for (let i = 0, max = this.weights.length; i < max; i++) {
this.weights[i] = randomFloat(-std, std);
}
}
}
class DataFormatter {
constructor(values, maxThreshold = 0) {
this.values = values;
this.indexTable = {};
this.characterTable = {};
this.characters = [];
this.specialIndexes = [];
this.isSetup = false;
if (values === undefined)
return;
this.setup(values, maxThreshold);
}
setup(values, maxThreshold = 0) {
if (this.isSetup)
throw new Error('DataFormatter is already setup');
this.values = values;
// go over all characters and keep track of all unique ones seen
// count up all characters
this.buildCharactersFromIterable(values);
this.buildTables(maxThreshold);
if (values[0].input) {
this.addInputOutput();
}
this.addUnrecognized();
this.isSetup = true;
}
buildCharactersFromIterable(values) {
const tempCharactersTable = {};
for (let dataFormatterIndex = 0, dataFormatterLength = values.length; dataFormatterIndex < dataFormatterLength; dataFormatterIndex++) {
const characters = values[dataFormatterIndex];
// if (typeof characters === 'string') {
// const character = characters;
// if (tempCharactersTable.hasOwnProperty(character)) continue;
// tempCharactersTable[character] = true;
// this.characters.push(character);
if (characters.hasOwnProperty('length')) {
const iteratable = characters;
for (let characterIndex = 0, charactersLength = iteratable.length; characterIndex < charactersLength; characterIndex++) {
const character = iteratable[characterIndex];
if (tempCharactersTable.hasOwnProperty(character))
continue;
tempCharactersTable[character] = true;
this.characters.push(character);
}
}
else if (typeof characters === 'number') {
if (tempCharactersTable.hasOwnProperty(characters))
continue;
tempCharactersTable[characters] = true;
this.characters.push(characters);
}
else if (typeof characters === 'boolean') {
const character = characters.toString();
if (tempCharactersTable.hasOwnProperty(character))
continue;
tempCharactersTable[character] = true;
this.characters.push(character);
}
else if (Array.isArray(characters) &&
typeof characters[0] === 'string') {
for (let i = 0; i < characters.length; i++) {
const character = characters[i];
if (tempCharactersTable.hasOwnProperty(character))
continue;
tempCharactersTable[character] = true;
this.characters.push(character);
}
}
else if (Array.isArray(characters) &&
(typeof characters[0] === 'number' ||
typeof characters[0] === 'boolean')) {
for (let i = 0; i < characters.length; i++) {
const character = characters[i].toString();
if (tempCharactersTable.hasOwnProperty(dataFormatterIndex))
continue;
tempCharactersTable[character] = true;
this.characters.push(character);
}
}
else if (characters.hasOwnProperty('input') &&
characters.hasOwnProperty('output')) {
const { input, output } = characters;
if (Array.isArray(input)) {
this.addCharacters(input, tempCharactersTable);
}
else {
this.addCharacters(input.toString(), tempCharactersTable);
}
if (Array.isArray(output)) {
this.addCharacters(output, tempCharactersTable);
}
else {
this.addCharacters(output.toString(), tempCharactersTable);
}
}
else {
throw new Error('Unhandled value');
}
}
}
addCharacters(characters, charactersTable) {
for (let i = 0; i < characters.length; i++) {
const character = characters[i].toString();
if (charactersTable.hasOwnProperty(character))
continue;
charactersTable[character] = true;
this.characters.push(character);
}
}
buildTables(maxThreshold) {
// filter by count threshold and create pointers
const charactersLength = this.characters.length;
for (let characterIndex = 0; characterIndex < charactersLength; characterIndex++) {
const character = this.characters[characterIndex];
if (characterIndex >= maxThreshold) {
// add character to dataFormatter
this.indexTable[character] = characterIndex;
this.characterTable[characterIndex] = character;
}
}
}
toIndexes(value, maxThreshold = 0) {
const result = [];
const { indexTable } = this;
switch (typeof value) {
case 'number':
case 'boolean':
value = value.toString();
}
for (let i = 0, max = value.length; i < max; i++) {
const character = value[i].toString();
let index = indexTable[character];
if (index === undefined) {
if (indexTable.unrecognized) {
index = indexTable.unrecognized;
}
else {
throw new Error(`unrecognized character "${character}"`);
}
}
if (index < maxThreshold)
continue;
result.push(index);
}
return result;
}
toIndexesInputOutput(input, output, maxThreshold = 0) {
const result = this.toIndexesValue(input, maxThreshold, true);
if (typeof output === 'undefined')
return result;
return result.concat(this.toIndexesValue(output, maxThreshold, false));
}
toIndexesValue(value, maxThreshold, isInput) {
if (typeof value === 'string') {
value = value.split('');
}
else if (typeof value === 'number' || typeof value === 'boolean') {
value = value.toString().split('');
}
else if (Array.isArray(value) &&
(typeof value[0] === 'number' ||
typeof value[0] === 'boolean' ||
typeof value[0] === 'string')) {
value = value.map((v) => v.toString());
}
else {
throw new Error('unrecognized value');
}
if (isInput) {
value = value.concat(['stop-input', 'start-output']);
}
return this.toIndexes(value, maxThreshold);
}
toCharacters(indices, maxThreshold = 0) {
const result = [];
const { indexTable, characterTable } = this;
for (let i = 0, max = indices.length; i < max; i++) {
const index = indices[i];
if (index < maxThreshold)
continue;
let character = characterTable[index];
if (character === undefined) {
if (indexTable.unrecognized) {
character = characterTable[indexTable.unrecognized];
}
else {
throw new Error(`unrecognized index "${index}"`);
}
}
else if (character !== null) {
result.push(character.toString());
}
}
return result;
}
toString(indices, maxThreshold) {
return this.toCharacters(indices, maxThreshold).join('');
}
addInputOutput() {
this.addSpecial('stop-input');
this.addSpecial('start-output');
}
addUnrecognized() {
this.addSpecial('unrecognized');
}
static fromAllPrintable(maxThreshold, values = ['\n']) {
for (let i = 32; i <= 126; i++) {
values.push(String.fromCharCode(i));
}
return new DataFormatter(values, maxThreshold);
}
static fromAllPrintableInputOutput(maxThreshold, values = ['\n']) {
const dataFormatter = DataFormatter.fromAllPrintable(maxThreshold, values);
dataFormatter.addInputOutput();
dataFormatter.addUnrecognized();
return dataFormatter;
}
static fromStringInputOutput(string, maxThreshold) {
const values = Array.from(new Set(string)).join('');
const dataFormatter = new DataFormatter(values.split(''), maxThreshold);
dataFormatter.addInputOutput();
dataFormatter.addUnrecognized();
dataFormatter.isSetup = true;
return dataFormatter;
}
static fromArrayInputOutput(data, maxThreshold) {
const values = [];
for (let i = 0; i < data.length; i++) {
const datum = data[i];
values.push(validateAndCast(datum.input), validateAndCast(datum.output));
}
const flatArray = Array.isArray(values)
? values.flat()
: values;
const dataFormatter = new DataFormatter(Array.from(new Set(flatArray)), maxThreshold);
dataFormatter.addInputOutput();
dataFormatter.addUnrecognized();
dataFormatter.isSetup = true;
return dataFormatter;
}
static fromString(string, maxThreshold = 0) {
const values = Array.from(new Set(string)).join('');
return new DataFormatter(values.split(''), maxThreshold);
}
toJSON() {
return {
indexTable: this.indexTable,
characterTable: this.characterTable,
values: this.values,
characters: this.characters,
specialIndexes: this.specialIndexes,
};
}
/** TODO: Type better, The type of json is not "string that is a valid JSON", it is a POJO in the shape of DataFormatter.
* this method re-hydrates the the data as an instance of DataFormatter.
*/
static fromJSON(json) {
const dataFormatter = new DataFormatter();
dataFormatter.indexTable = json.indexTable;
dataFormatter.characterTable = json.characterTable;
dataFormatter.values = json.values;
dataFormatter.characters = json.characters;
dataFormatter.specialIndexes = json.specialIndexes;
return dataFormatter;
}
addSpecial(special, character = null) {
const specialIndex = (this.indexTable[special] = this.characters.length);
this.characterTable[specialIndex] = character;
this.specialIndexes.push(this.characters.length);
this.characters.push(special);
}
toFunctionString() {
return `
var characterTable = ${JSON.stringify(this.characterTable)};
var indexTable = ${JSON.stringify(this.indexTable)};
var characters = ${JSON.stringify(this.characters)};
var dataFormatter = {
toIndexes: function ${this.toIndexes.toString()},
toIndexesInputOutput: function ${this.toIndexesInputOutput.toString()},
toCharacters: function ${this.toCharacters.toString()},
toIndexesValue: function ${this.toIndexesValue.toString()},
};`;
}
formatDataIn(input, output) {
var _a;
if (input === undefined)
return [];
if (Array.isArray(input) && typeof input[0] === 'number') {
return input;
}
if ((_a = this.indexTable) === null || _a === void 0 ? void 0 : _a.hasOwnProperty('stop-input')) {
return this.toIndexesInputOutput(input, output);
}
return this.toIndexes(input);
}
formatDataOut(input, output) {
return this.toCharacters(output).join('');
}
format(data) {
if (typeof data[0] === 'number' &&
!Array.isArray(data[0]) &&
(!data[0].hasOwnProperty('input') || !data[0].hasOwnProperty('output'))) {
return data;
}
const result = [];
if (typeof data[0] === 'string' ||
typeof data[0] === 'number' ||
Array.isArray(data[0])) {
if (!this.isSetup) {
this.setup(data);
for (let i = 0; i < data.length; i++) {
result.push(this.formatDataIn(validateAndCast(data[i])));
}
}
else {
for (let i = 0, max = data.length; i < max; i++) {
result.push(this.formatDataIn(data[i]));
}
}
}
else if (data[0].input && data[0].output) {
if (!this.isSetup) {
this.setup(data);
}
for (let i = 0, max = data.length; i < max; i++) {
result.push(this.formatDataIn(validateAndCast(data[i].input), validateAndCast(data[i].output)));
}
}
else {
throw new Error('unrecognized data');
}
return result;
}
}
function validateAndCast(value) {
if (typeof value === 'string')
return value;
if (typeof value === 'number')
return value.toString();
if (typeof value === 'boolean')
return value.toString();
if (Array.isArray(value) && typeof value[0] === 'string')
return value;
if (typeof value[0] === 'boolean') {
return value.map((v) => v.toString());
}
if (typeof value[0] === 'number') {
return value.map((v) => v.toString());
}
throw new Error('unrecognized value, expected string[], string, number[], number, boolean[], or boolean');
}
function copy(product, left) {
product.rows = left.rows;
product.columns = left.columns;
product.weights = left.weights.slice(0);
product.deltas = left.deltas.slice(0);
}
/**
* add {left} and {right} matrix weights into {into}
*/
function add(product, left, right) {
for (let i = 0; i < left.weights.length; i++) {
product.weights[i] = left.weights[i] + right.weights[i];
product.deltas[i] = 0;
}
}
/**
* adds {from} deltas to {left} and {right} deltas
*/
function addB(product, left, right) {
for (let i = 0; i < product.deltas.length; i++) {
left.deltas[i] = product.deltas[i];
right.deltas[i] = product.deltas[i];
}
}
/**
* makes matrix weights and deltas all ones
*/
function allOnes(product) {
for (let i = 0; i < product.weights.length; i++) {
product.weights[i] = 1;
product.deltas[i] = 0;
}
}
function cloneNegative(product, left) {
product.rows = left.rows;
product.columns = left.columns;
product.weights = left.weights.slice(0);
product.deltas = left.deltas.slice(0);
for (let i = 0; i < left.weights.length; i++) {
product.weights[i] = -left.weights[i];
product.deltas[i] = 0;
}
}
/**
* multiply {left} and {right} matrix weights to {into}
*/
function multiply(product, left, right) {
const leftRows = left.rows;
const leftColumns = left.columns;
const rightColumns = right.columns;
// loop over rows of left
for (let leftRow = 0; leftRow < leftRows; leftRow++) {
const leftRowBase = leftColumns * leftRow;
const rightRowBase = rightColumns * leftRow;
// loop over cols of right
for (let rightColumn = 0; rightColumn < rightColumns; rightColumn++) {
// dot product loop
let dot = 0;
// loop over columns of left
for (let leftColumn = 0; leftColumn < leftColumns; leftColumn++) {
const rightColumnBase = rightColumns * leftColumn;
const leftIndex = leftRowBase + leftColumn;
const rightIndex = rightColumnBase + rightColumn;
dot += left.weights[leftIndex] * right.weights[rightIndex];
left.deltas[leftIndex] = 0;
right.deltas[rightIndex] = 0;
}
product.weights[rightRowBase + rightColumn] = dot;
}
}
}
/**
* multiplies {from} deltas to {left} and {right}
*/
function multiplyB(product, left, right) {
const leftRows = left.rows;
const leftColumns = left.columns;
const rightColumns = right.columns;
// loop over rows of left
for (let leftRowRoot = 0; leftRowRoot < leftRows; leftRowRoot++) {
const leftRowBase = leftColumns * leftRowRoot;
const rightRowBase = rightColumns * leftRowRoot;
// loop over cols of right
for (let rightColumn = 0; rightColumn < rightColumns; rightColumn++) {
// loop over columns of left
for (let leftColumn = 0; leftColumn < leftColumns; leftColumn++) {
const rightColumnBase = rightColumns * leftColumn;
const leftRow = leftRowBase + leftColumn;
const rightRow = rightColumnBase + rightColumn;
const backPropagateValue = product.deltas[rightRowBase + rightColumn];
left.deltas[leftRow] += right.weights[rightRow] * backPropagateValue;
right.deltas[rightRow] += left.weights[leftRow] * backPropagateValue;
}
}
}
}
function multiplyElement(product, left, right) {
const { weights } = left;
for (let i = 0; i < weights.length; i++) {
product.weights[i] = left.weights[i] * right.weights[i];
product.deltas[i] = 0;
}
}
/**
* multiplies {left} and {right} weight by {from} deltas into {left} and {right} deltas
*/
function multiplyElementB(product, left, right) {
for (let i = 0; i < left.weights.length; i++) {
left.deltas[i] = right.weights[i] * product.deltas[i];
right.deltas[i] = left.weights[i] * product.deltas[i];
}
}
/**
*
* relu {m} weights to {into} weights
*/
function relu(product, left) {
for (let i = 0; i < left.weights.length; i++) {
product.weights[i] = Math.max(0, left.weights[i]); // relu
product.deltas[i] = 0;
}
}
/**
* adds {from} deltas to {m} deltas when {m} weights are above other a threshold of 0
*/
function reluB(product, left) {
for (let i = 0; i < product.deltas.length; i++) {
left.deltas[i] = left.weights[i] > 0 ? product.deltas[i] : 0;
}
}
function rowPluck(product, left, rowPluckIndex) {
const { columns } = left;
const rowBase = columns * rowPluckIndex;
for (let column = 0; column < columns; column++) {
product.weights[column] = left.weights[rowBase + column];
product.deltas[column] = 0;
}
}
/**
* adds {from} deltas into {m} deltas
*/
function rowPluckB(product, left, rowIndex) {
const { columns } = left;
const rowBase = columns * rowIndex;
for (let column = 0; column < columns; column++) {
left.deltas[rowBase + column] = product.deltas[column];
}
}
function sigmoid(product, left) {
// sigmoid nonlinearity
for (let i = 0; i < left.weights.length; i++) {
product.weights[i] = 1 / (1 + Math.exp(-left.weights[i]));
product.deltas[i] = 0;
}
}
// function sig(x) {
// // helper function for computing sigmoid
// return 1 / (1 + Math.exp(-x));
// }
function sigmoidB(product, left) {
for (let i = 0; i < product.deltas.length; i++) {
const mwi = product.weights[i];
left.deltas[i] = mwi * (1 - mwi) * product.deltas[i];
}
}
function softmax(matrix) {
// probability volume
const result = new Matrix(matrix.rows, matrix.columns);
let maxVal = -999999;
for (let i = 0; i < matrix.weights.length; i++) {
if (matrix.weights[i] > maxVal) {
maxVal = matrix.weights[i];
}
}
let s = 0;
for (let i = 0; i < matrix.weights.length; i++) {
result.weights[i] = Math.exp(matrix.weights[i] - maxVal);
s += result.weights[i];
}
for (let i = 0; i < matrix.weights.length; i++) {
result.weights[i] /= s;
}
// no backward pass here needed
// since we will use the computed probabilities outside
// to set gradients directly on m
return result;
}
function tanh(product, left) {
// tanh nonlinearity
for (let i = 0; i < left.weights.length; i++) {
product.weights[i] = Math.tanh(left.weights[i]);
product.deltas[i] = 0;
}
}
function tanhB(product, left) {
for (let i = 0; i < product.deltas.length; i++) {
// grad for z = tanh(x) is (1 - z^2)
const mwi = product.weights[i];
left.deltas[i] = (1 - mwi * mwi) * product.deltas[i];
}
}
class Equation {
constructor() {
this.states = [];
this.inputRow = 0;
}
add(left, right) {
if (left.weights.length !== right.weights.length) {
throw new Error('misaligned matrices');
}
const product = new Matrix(left.rows, left.columns);
this.states.push({
name: 'add',
product,
left,
right,
forwardFn: add,
backpropagationFn: addB,
});
return product;
}
allOnes(rows, columns) {
const product = new Matrix(rows, columns);
this.states.push({
name: 'allOnes',
product,
left: product,
forwardFn: allOnes,
backpropagationFn: () => { },
});
return product;
}
cloneNegative(matrix) {
const product = new Matrix(matrix.rows, matrix.columns);
this.states.push({
name: 'cloneNegative',
product,
left: matrix,
forwardFn: cloneNegative,
backpropagationFn: () => { },
});
return product;
}
/**
* connects two matrices together by subtract
*/
subtract(left, right) {
if (left.weights.length !== right.weights.length) {
throw new Error('misaligned matrices');
}
return this.add(this.add(this.allOnes(left.rows, left.columns), this.cloneNegative(left)), right);
}
/**
* connects two matrices together by multiply
*/
multiply(left, right) {
if (left.columns !== right.rows) {
throw new Error('misaligned matrices');
}
const product = new Matrix(left.rows, right.columns);
this.states.push({
name: 'multiply',
product,
left,
right,
forwardFn: multiply,
backpropagationFn: multiplyB,
});
return product;
}
/**
* connects two matrices together by multiplyElement
*/
multiplyElement(left, right) {
if (left.weights.length !== right.weights.length) {
throw new Error('misaligned matrices');
}
const product = new Matrix(left.rows, left.columns);
this.states.push({
name: 'multiplyElement',
product,
left,
right,
forwardFn: multiplyElement,
backpropagationFn: multiplyElementB,
});
return product;
}
/**
* connects a matrix to relu
*/
relu(matrix) {
const product = new Matrix(matrix.rows, matrix.columns);
this.states.push({
name: 'relu',
product,
left: matrix,
forwardFn: relu,
backpropagationFn: reluB,
});
return product;
}
/**
* input a matrix
*/
input(input) {
this.states.push({
name: 'input',
product: input,
forwardFn: (product) => {
if (!this.inputValue)
return;
if (this.inputValue.length !== product.weights.length) {
throw new Error('this.inputValue is of wrong dimensions');
}
product.weights = input.weights = this.inputValue;
},
backpropagationFn: () => { },
});
return input;
}
/**
* connects a matrix via a row
*/
inputMatrixToRow(matrix) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const product = new Matrix(matrix.columns, 1);
this.states.push({
name: 'inputMatrixToRow',
product,
left: matrix,
get right() {
return self.inputRow;
},
forwardFn: rowPluck,
backpropagationFn: rowPluckB,
});
return product;
}
/**
* connects a matrix to sigmoid
*/
sigmoid(matrix) {
const product = new Matrix(matrix.rows, matrix.columns);
this.states.push({
name: 'sigmoid',
product,
left: matrix,
forwardFn: sigmoid,
backpropagationFn: sigmoidB,
});
return product;
}
/**
* connects a matrix to tanh
*/
tanh(matrix) {
const product = new Matrix(matrix.rows, matrix.columns);
this.states.push({
name: 'tanh',
product,
left: matrix,
forwardFn: tanh,
backpropagationFn: tanhB,
});
return product;
}
/**
*
* Observe a matrix for debugging
*/
observe(matrix) {
this.states.push({
name: 'observe',
product: new Matrix(),
forwardFn: () => { },
backpropagationFn: () => { },
});
return matrix;
}
/**
* Run index through equations via forward propagation
*/
runIndex(rowIndex = 0) {
this.inputRow = rowIndex;
let state = this.states[0];
for (let i = 0, max = this.states.length; i < max; i++) {
state = this.states[i];
if (!state.hasOwnProperty('forwardFn'))
continue;
state.forwardFn(state.product, state.left, state.right);
}
return state.product;
}
/**
* Run value through equations via forward propagation
*/
runInput(inputValue) {
this.inputValue = inputValue;
let state = this.states[0];
for (let i = 0, max = this.states.length; i < max; i++) {
state = this.states[i];
if (!state.hasOwnProperty('forwardFn'))
continue;
state.forwardFn(state.product, state.left, state.right);
}
return state.product;
}
/**
* Run value through equations via back propagation
*/
backpropagate() {
let i = this.states.length;
let state = this.states[0];
while (i-- > 0) {
state = this.states[i];
if (!state.hasOwnProperty('backpropagationFn'))
continue;
state.backpropagationFn(state.product, state.left, state.right);
}
return state.product;
}
/**
* Run index through equations via back propagation
*/
backpropagateIndex(rowIndex = 0) {
this.inputRow = rowIndex;
let i = this.states.length;
let state = this.states[0];
while (i-- > 0) {
state = this.states[i];
if (!state.hasOwnProperty('backpropagationFn'))
continue;
state.backpropagationFn(state.product, state.left, state.right);
}
return state.product;
}
/**
* Predict a target value from equation
*/
predictTarget(input, target) {
let errorSum = 0;
const output = this.runInput(input);
for (let i = 0; i < output.weights.length; i++) {
const error = output.weights[i] - target[i];
// set gradients into log probabilities
errorSum += Math.abs(error);
// write gradients into log probabilities
output.deltas[i] = error;
}
return errorSum;
}
/**
* Predict a target index from equation
*/
predictTargetIndex(input, target) {
const output = this.runIndex(input);
// set gradients into log probabilities
const logProbabilities = output; // interpret output as log probabilities
const probabilities = softmax(output); // compute the softmax probabilities
// write gradients into log probabilities
logProbabilities.deltas = probabilities.weights.slice(0);
logProbabilities.deltas[target] -= 1;
// accumulate base 2 log prob and do smoothing
return -Math.log2(probabilities.weights[target]);
}
}
function maxI(matrix) {
// argmax of array w
const { weights } = matrix;
let maxv = weights[0];
let maxix = 0;
for (let i = 1; i < weights.length; i++) {
const v = weights[i];
if (v < maxv)
continue;
maxix = i;
maxv = v;
}
return maxix;
}
function sampleI(matrix) {
// sample argmax from w, assuming w are
// probabilities that sum to one
const r = randomFloat(0, 1);
const w = matrix.weights;
let x = 0;
let i = 0;
while (true) {
x += w[i];
if (x > r) {
return i;
}
i++;
}
}
const trainDefaults$1 = {
iterations: 20000,
errorThresh: 0.005,
log: false,
logPeriod: 10,
learningRate: 0.01,
callbackPeriod: 10,
timeout: Infinity,
};
const defaults$1 = () => {
return {
inputSize: 20,
inputRange: 20,
hiddenLayers: [20, 20],
outputSize: 20,
decayRate: 0.999,
smoothEps: 1e-8,
regc: 0.000001,
clipval: 5,
maxPredictionLength: 100,
dataFormatter: new DataFormatter(),
};
};
class RNN {
constructor(options = {}) {
this.options = { ...defaults$1() };
this.trainOpts = { ...trainDefaults$1 };
this.stepCache = {};
this.runs = 0;
this.ratioClipped = 0;
this.model = Object.seal({
isInitialized: false,
input: new Matrix(0, 0),
hiddenLayers: [],
output: new Matrix(0, 0),
equations: [],
allMatrices: [],
equationConnections: [],
outputConnector: new RandomMatrix(0, 0, 0.08),
});
this.initialLayerInputs = [];
this.options = { ...this.options, ...options };
this.updateTrainingOptions({
...trainDefaults$1,
});
if (options.json) {
this.fromJSON(options.json);
}
}
initialize() {
const { dataFormatter } = this.options;
if (dataFormatter === null || dataFormatter === void 0 ? void 0 : dataFormatter.characters.length) {
this.options.inputSize = this.options.inputRange = this.options.outputSize =
dataFormatter.characters.length;
}
this.model = this.mapModel();
}
createHiddenLayers() {
const { hiddenLayers, inputSize } = this.options;
const hiddenLayersModel = [];
// 0 is end, so add 1 to offset
hiddenLayersModel.push(this.getHiddenLayer(hiddenLayers[0], inputSize));
let prevSize = hiddenLayers[0];
for (let d = 1; d < hiddenLayers.length; d++) {
// loop over depths
const hiddenSize = hiddenLayers[d];
hiddenLayersModel.push(this.getHiddenLayer(hiddenSize, prevSize));
prevSize = hiddenSize;
}
return hiddenLayersModel;
}
getHiddenLayer(hiddenSize, prevSize) {
return {
// wxh
weight: new RandomMatrix(hiddenSize, prevSize, 0.08),
// whh
transition: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
// bhh
bias: new Matrix(hiddenSize, 1),
};
}
getEquation(equation, inputMatrix, previousResult, hiddenLayer) {
if (!hiddenLayer.weight || !hiddenLayer.transition || !hiddenLayer.bias) {
throw new Error('hiddenLayer does not have expected properties');
}
const relu = equation.relu.bind(equation);
const add = equation.add.bind(equation);
const multiply = equation.multiply.bind(equation);
return relu(add(add(multiply(hiddenLayer.weight, inputMatrix), multiply(hiddenLayer.transition, previousResult)), hiddenLayer.bias));
}
createInputMatrix() {
const { inputRange, inputSize } = this.options;
if (inputRange < 1)
throw new Error('this.options.inputRange not an expected number');
if (inputSize < 1)
throw new Error('this.options.inputSize not an expected number');
// 0 is end, so add 1 to offset
return new RandomMatrix(inputRange + 1, inputSize, 0.08);
}
createOutputMatrices() {
const { outputSize, hiddenLayers } = this.options;
const lastHiddenSize = last(hiddenLayers);
// 0 is end, so add 1 to offset
return {
// whd
outputConnector: new RandomMatrix(outputSize + 1, lastHiddenSize, 0.08),
// 0 is end, so add 1 to offset
// bd
output: new Matrix(outputSize + 1, 1),
};
}
bindEquation() {
const { model } = this;
const { hiddenLayers } = this.options;
const equation = new Equation();
const outputs = [];
const equationConnection = model.equationConnections.length > 0
? last(model.equationConnections)
: this.initialLayerInputs;
// 0 index
let output = this.getEquation(equation, equation.inputMatrixToRow(model.input), equationConnection[0], model.hiddenLayers[0]);
outputs.push(output);
// 1+ indices
for (let i = 1, max = hiddenLayers.length; i < max; i++) {
if (!equationConnection[i]) {
throw new Error(`Cannot find equation at index ${i}`);
}
output = this.getEquation(equation, output, equationConnection[i], model.hiddenLayers[i]);
outputs.push(output);
}
model.equationConnections.push(outputs);
equation.add(equation.multiply(model.outputConnector, output), model.output);
model.equations.push(equation);
}
mapModel() {
const allMatrices = [];
this.initialLayerInputs = this.options.hiddenLayers.map((size) => new Matrix(size, 1));
const input = this.createInputMatrix();
allMatrices.push(input);
const hiddenLayers = this.createHiddenLayers();
if (!hiddenLayers.length)
throw new Error('net.hiddenLayers not set');
for (let i = 0, max = hiddenLayers.length; i < max; i++) {
const hiddenMatrix = hiddenLayers[i];
for (const property in hiddenMatrix) {
if (!hiddenMatrix.hasOwnProperty(property))
continue;
allMatrices.push(hiddenMatrix[property]);
}
}
const { output, outputConnector } = this.createOutputMatrices();
allMatrices.push(outputConnector);
allMatrices.push(output);
return Object.seal({
isInitialized: true,
input,
hiddenLayers,
output,
equations: [],
allMatrices,
equationConnections: [],
outputConnector,
});
}
trainInput(input) {
this.runs++;
const { model } = this;
const max = input.length;
let log2ppl = 0;
let equation;
while (model.equations.length <= input.length + 1) {
// last is zero
this.bindEquation();
}
for (let inputIndex = -1, inputMax = input.length; inputIndex < inputMax; inputIndex++) {
// start and end tokens are zeros
const equationIndex = inputIndex + 1;
equation = model.equations[equationIndex];
const source = inputIndex === -1 ? 0 : input[inputIndex] + 1; // first step: start with START token
const target = inputIndex === max - 1 ? 0 : input[inputIndex + 1] + 1; // last step: end with END token
log2ppl += equation.predictTargetIndex(source, target);
}
return Math.pow(2, log2ppl / (max - 1)) / 100;
}
backpropagate(input) {
let i = input.length;
const { model } = this;
const { equations } = model;
while (i > 0) {
equations[i].backpropagateIndex(input[i - 1] + 1);
i--;
}
equations[0].backpropagateIndex(0);
}
adjustWeights() {
const { regc, clipval, decayRate, smoothEps } = this.options;
const { trainOpts, model, stepCache } = this;
const { learningRate } = trainOpts;
const { allMatrices } = model;
let numClipped = 0;
let numTot = 0;
for (let matrixIndex = 0; matrixIndex < allMatrices.length; matrixIndex++) {
const matrix = allMatrices[matrixIndex];
const { weights, deltas } = matrix;
if (!(matrixIndex in stepCache)) {
stepCache[matrixIndex] = zeros$1(matrix.rows * matrix.columns);
}
const cache = stepCache[matrixIndex];
for (let i = 0; i < weights.length; i++) {
let r = deltas[i];
const w = weights[i];
// rmsprop adaptive learning rate
cache[i] = cache[i] * decayRate + (1 - decayRate) * r * r;
// gradient clip
if (r > clipval) {
r = clipval;
numClipped++;
}
else if (r < -clipval) {
r = -clipval;
numClipped++;
}
numTot++;
// update (and regularize)
weights[i] =
w + (-learningRate * r) / Math.sqrt(cache[i] + smoothEps) - regc * w;
}
}
this.ratioClipped = numClipped / numTot;
}
get isRunnable() {
if (this.model && this.model.equations.length === 0) {
console.error(`No equations bound, did you run train()?`);
return false;
}
return true;
}
checkRunnable() {
if (!this.isRunnable) {
throw new Error('Network not runnable');
}
}
run(rawInput = [], isSampleI = false, temperature = 1) {
const maxPredictionLength = this.options.maxPredictionLength +
(rawInput !== null ? rawInput.length : 0) +
(this.options.dataFormatter
? this.options.dataFormatter.specialIndexes.length
: 0);
this.checkRunnable();
const input = this.options.dataFormatter && rawInput.length > 0
? this.options.dataFormatter.formatDataIn(rawInput)
: rawInput;
const { model } = this;
const output = [];
let i = 0;
while (true) {
const previousIndex = i === 0 ? 0 : i < input.length ? input[i - 1] + 1 : output[i - 1];
while (model.equations.length <= i) {
this.bindEquation();
}
const equation = model.equations[i];
// sample predicted letter
const outputMatrix = equation.runIndex(previousIndex);
const logProbabilities = new Matrix(model.output.rows, model.output.columns);
copy(logProbabilities, outputMatrix);
if (temperature !== 1 && isSampleI) {
/**
* scale log probabilities by temperature and re-normalize
* if temperature is high, logProbabilities will go towards zero
* and the softmax outputs will be more diffuse. if temperature is
* very low, the softmax outputs will be more peaky
*/
for (let j = 0, max = logProbabilities.weights.length; j < max; j++) {
logProbabilities.weights[j] /= temperature;
}
}
const probs = softmax(logProbabilities);
const nextIndex = isSampleI ? sampleI(probs) : maxI(probs);
i++;
if (nextIndex === 0) {
// END token predicted, break out
break;
}
if (i >= maxPredictionLength) {
// something is wrong
break;
}
output.push(nextIndex);
}
/**
* we slice the input length here, not because output contains it, but it will be erroneous as we are sending the
* network what is contained in input, so the data is essentially guessed by the network what could be next, till it
* locks in on a value.
* Kind of like this, values are from input:
* 0 -> 4 (or in English: "beginning on input" -> "I have no idea? I'll guess what they want next!")
* 2 -> 2 (oh how interesting, I've narrowed down values...)
* 1 -> 9 (oh how interesting, I've now know what the values are...)
* then the output looks like: [4, 2, 9,...]
* so we then remove the erroneous data to get our true output
*/
return this.options.dataFormatter.formatDataOut(input, output.slice(input.length).map((value) => value - 1));
}
/**
*
* Verifies network sizes are initialized
* If they are not it will initialize them
*/
verifyIsInitialized() {
if (!this.model.isInitialized) {
this.initialize();
}
}
/**
*
* @param options
* Supports all `trainDefaults` properties
* also supports:
* learningRate: (number),
* momentum: (number),
* activation: 'sigmoid', 'relu', 'leaky-relu', 'tanh'
*/
updateTrainingOptions(options) {
var _a;
this.trainOpts = { ...trainDefaults$1, ...options };
this.validateTrainingOptions(this.trainOpts);
this.setLogMethod((_a = options.log) !== null && _a !== void 0 ? _a : this.trainOpts.log);
// TODO: Remove this?
// this.activation = options.activation || this.activation;
}
validateTrainingOptions(options) {
const validations = {
iterations: () => {
const val = options.iterations;
return typeof val === 'number' && val > 0;
},
errorThresh: () => {
const val = options.errorThresh;
return typeof val === 'number' && val > 0 && val < 1;
},
log: () => {
const val = options.log;
return typeof val === 'function' || typeof val === 'boolean';
},
logPeriod: () => {
const val = options.logPeriod;
return typeof val === 'number' && val > 0;
},
learningRate: () => {
const val = options.learningRate;
return typeof val === 'number' && val > 0 && val < 1;
},
callback: () => {
const val = options.callback;
return typeof val === 'function' || val === undefined;
},
callbackPeriod: () => {
const val = options.callbackPeriod;
return typeof val === 'number' && val > 0;
},
timeout: () => {
const val = options.timeout;
return typeof val === 'number' && val > 0;
},
};
for (const p in validations) {
const v = options;
if (!validations[p]()) {
throw new Error(`[${p}, ${v[p]}] is out of normal training range, your network will probably not train.`);
}
}
}
setLogMethod(log) {
if (typeof log === 'function') {
this.trainOpts.log = log;
}
else if (log) {
this.trainOpts.log = console.log;
}
else {
this.trainOpts.log = false;
}
}
prepTraining(data, options) {
var _a;
this.updateTrainingOptions(options);
const preparedData = this.options.dataFormatter.format(data);
const endTime = Date.now() + ((_a = this.trainOpts.timeout) !== null && _a !== void 0 ? _a : 0);
const status = {
error: 1,
iterations: 0,
};
this.verifyIsInitialized();
return {
preparedData,
status,
endTime,
};
}
train(data, trainOpts = {}) {
var _a;
this.trainOpts = trainOpts = {
...trainDefaults$1,
...trainOpts,
};
const { iterations, errorThresh, logPeriod, callback, callbackPeriod, } = this.trainOpts;
const log = trainOpts.log === true ? console.log : trainOpts.log;
let error = Infinity;
let i;
let inputs;
if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.dataFormatter) {
inputs = this.options.dataFormatter.format(data);
}
else if (Array.isArray(data) &&
Array.isArray(data[0]) &&
typeof data[0][0] === 'number') {
inputs = data;
}
else {
throw new Error('training not in expected format of number[][]');
}
this.verifyIsInitialized();
for (i = 0; i < iterations && error > errorThresh; i++) {
let sum = 0;
for (let j = 0; j < inputs.length; j++) {
const err = this.trainPattern(inputs[j], true);
sum += err;
}
error = sum / data.length;
if (isNaN(error)) {
throw new Error('Network error rate is unexpected NaN, check network configurations and try again. Most probably input format is not correct or training data is not enough. ');
}
if (log && i % logPeriod === 0) {
log(`iterations: ${i}, training error: ${error}`);
}
if (callback && i % callbackPeriod === 0) {
callback({ error, iterations: i });
}
}
return {
error,
iterations: i,
};
}
addFormat() {
throw new Error('not yet implemented');
}
toJSON() {
if (!this.model.isInitialized) {
this.initialize();
}
const { model, options } = this;
return {
type: this.constructor.name,
options: { ...options, dataFormatter: options.dataFormatter.toJSON() },
trainOpts: {
...this.trainOpts,
timeout: this.trainOpts.timeout === Infinity
? 'Infinity'
: this.trainOpts.timeout,
},
input: model.input.toJSON(),
hiddenLayers: model.hiddenLayers.map((hiddenLayer) => {
const layers = {};
for (const p in hiddenLayer) {
if (!hiddenLayer.hasOwnProperty(p))
continue;
layers[p] = hiddenLayer[p].toJSON();
}
return layers;
}),
outputConnector: this.model.outputConnector.toJSON(),
output: this.model.output.toJSON(),
};
}
fromJSON(json) {
const { options } = json;
const allMatrices = [];
const input = Matrix.fromJSON(json.input);
allMatrices.push(input);
const hiddenLayers = [];
json.hiddenLayers.forEach((hiddenLayer) => {
const layers = {};
for (const p in hiddenLayer) {
layers[p] = Matrix.fromJSON(hiddenLayer[p]);
allMatrices.push(layers[p]);
}
hiddenLayers.push(layers);
});
const outputConnector = Matrix.fromJSON(json.outputConnector);
allMatrices.push(outputConnector);
const output = Matrix.fromJSON(json.output);
allMatrices.push(output);
if (options.dataFormatter) {
this.options = {
...defaults$1(),
...options,
dataFormatter: DataFormatter.fromJSON(options.dataFormatter),
};
}
else {
this.options = {
...defaults$1(),
...options,
dataFormatter: new DataFormatter(),
};
}
this.model = Object.seal({
isInitialized: true,
input,
hiddenLayers,
output,
allMatrices,
outputConnector,
equations: [],
equationConnections: [],
});
this.initialLayerInputs = this.options.hiddenLayers.map((size) => new Matrix(size, 1));
this.bindEquation();
return this;
}
toFunction(cb) {
const { model } = this;
const { equations } = this.model;
const equation = equations[1];
const { states } = equation;
const jsonString = JSON.stringify(this.toJSON());
function previousConnectionIndex(m) {
const connection = model.equationConnections[0];
const { states } = equations[0];
for (let i = 0, max = states.length; i < max; i++) {
if (states[i].product === m) {
return i;
}
}
return connection.indexOf(m);
}
function matrixOrigin(m, stateIndex) {
for (let i = 0, max = states.length; i < max; i++) {
const state = states[i];
if (i === stateIndex) {
const j = previousConnectionIndex(m);
if (j > -1 && (m === state.left || m === state.right)) {
return `typeof prevStates[${j}] === 'object' ? prevStates[${j}].product : new Matrix(${m.rows}, ${m.columns})`;
}
return `new Matrix(${m.rows}, ${m.columns})`;
}
if (m === state.product)
return `states[${i}].product`;
if (m === state.right)
return `states[${i}].right`;
if (m === state.left)
return `states[${i}].left`;
}
return '';
}
function matrixToString(m, stateIndex) {
if (!m || !m.rows || !m.columns)
return 'null';
if (m === model.input)
return `json.input`;
if (m === model.outputConnector)
return `json.outputConnector`;
if (m === model.output)
return `json.output`;
for (let i = 0, max = model.hiddenLayers.length; i < max; i++) {
const hiddenLayer = model.hiddenLayers[i];
for (const p in hiddenLayer) {
if (!hiddenLayer.hasOwnProperty(p))
continue;
if (hiddenLayer[p] !== m)
continue;
return `json.hiddenLayers[${i}].${p}`;
}
}
return matrixOrigin(m, stateIndex);
}
function toInner(fnString) {
// crude, but should be sufficient for now
// function() { body }
const fnParts = fnString.toString().split('{');
fnParts.shift();
// body }
const fnBodyString = fnParts.join('{');
const fnBodyParts = fnBodyString.split('}');
fnBodyParts.pop();
// body
return fnBodyParts
.join('}')
.split('\n')
.join('\n ')
.replace('product.deltas[i] = 0;', '')
.replace('product.deltas[column] = 0;', '')
.replace('left.deltas[leftIndex] = 0;', '')
.replace('right.deltas[rightIndex] = 0;', '')
.replace('product.deltas = left.deltas.slice(0);', '');
}
function fileName(fnName) {
return `src/recurrent/matrix/${fnName.replace(/[A-Z]/g, function (value) {
return `-${value.toLowerCase()}`;
})}.js`;
}
const statesRaw = [];
const usedFunctionNames = {};
const innerFunctionsSwitch = [];
for (let i = 0, max = states.length; i < max; i++) {
const state = states[i];
statesRaw.push(`states[${i}] = {
name: '${state.forwardFn.name}',
left: ${state.left ? matrixToString(state.left, i) : 'undefined'},
right: ${state.right ? matrixToString(state.right, i) : 'undefined'},
product: ${matrixToString(state.product, i)}
}`);
const fnName = state.forwardFn.name;
if (!usedFunctionNames[fnName]) {
usedFunctionNames[fnName] = true;
innerFunctionsSwitch.push(` case '${fnName}': //compiled from ${fileName(fnName)}
${toInner(state.forwardFn.toString())}
break;`);
}
}
const src = `
if (typeof rawInput === 'undefined') rawInput = [];
if (typeof isSampleI === 'undefined') isSampleI = false;
if (typeof temperature === 'undefined') temperature = 1;
var json = ${jsonString};
${this.options.dataFormatter
? `${this.options.dataFormatter.toFunctionString()};
Object.assign(dataFormatter, json.options.dataFormatter);`
: ''}
${this.options.dataFormatter &&
typeof this.options.dataFormatter.formatDataIn === 'function'
? `const formatDataIn = function (input, output) { ${toInner(this.options.dataFormatter.formatDataIn.toString())} }.bind(dataFormatter);`
: ''}
${this.options.dataFormatter !== null &&
typeof this.options.dataFormatter.formatDataOut === 'function'
? `const formatDataOut = function formatDataOut(input, output) { ${toInner(this.options.dataFormatter.formatDataOut.toString())} }.bind(dataFormatter);`
: ''}
var maxPredictionLength =
${this.options.maxPredictionLength} +
rawInput.length +
${this.options.dataFormatter
? this.options.dataFormatter.specialIndexes.length
: 0};
var input = ${this.options.dataFormatter &&
typeof this.options.dataFormatter.formatDataIn === 'function'
? 'formatDataIn(rawInput)'
: 'rawInput'};
var _i = 0;
var output = [];
var states = [];
var prevStates;
while (true) {
var previousIndex = (_i === 0
? 0
: _i < input.length
? input[_i - 1] + 1
: output[_i - 1])
;
var rowPluckIndex = previousIndex;
prevStates = states;
states = [];
${statesRaw.join(';\n ')};
for (var stateIndex = 0, stateMax = ${statesRaw.length}; stateIndex < stateMax; stateIndex++) {
var state = states[stateIndex];
var product = state.product;
var left = state.left;
var right = state.right;
switch (state.name) {
${innerFunctionsSwitch.join('\n')}
}
}
var logProbabilities = state.product;
if (temperature !== 1 && isSampleI) {
for (var q = 0, nq = logProbabilities.weights.length; q < nq; q++) {
logProbabilities.weights[q] /= temperature;
}
}
var probs = softmax(logProbabilities);
var nextIndex = isSampleI ? sampleI(probs) : maxI(probs);
_i++;
if (nextIndex === 0) {
break;
}
if (_i >= maxPredictionLength) {
break;
}
output.push(nextIndex);
}
${this.options.dataFormatter &&
typeof this.options.dataFormatter.formatDataOut === 'function'
? 'return formatDataOut(input, output.slice(input.length).map(function(value) { return value - 1; }))'
: 'return output.slice(input.length).map(function(value) { return value - 1; })'};
function Matrix(rows, columns) {
this.rows = rows;
this.columns = columns;
this.weights = zeros(rows * columns);
}
${zeros$1.toString()}
${softmax.toString().replace('_1.Matrix', 'Matrix')}
${randomFloat.toString()}
${sampleI.toString()}
${maxI.toString()}`;
// eslint-disable-next-line
return new Function('rawInput', 'isSampleI', 'temperature', cb ? cb(src) : src);
}
trainPattern(input, logErrorRate) {
const error = this.trainInput(input);
this.backpropagate(input);
this.adjustWeights();
if (logErrorRate) {
return error;
}
return 0;
}
}
function last(values) {
return values[values.length - 1];
}
class GRU extends RNN {
getHiddenLayer(hiddenSize, prevSize) {
return getGRUHiddenLayer(hiddenSize, prevSize);
}
getEquation(equation, inputMatrix, previousResult, hiddenLayer) {
return getGRUEquation(equation, inputMatrix, previousResult, hiddenLayer);
}
}
function getGRUHiddenLayer(hiddenSize, prevSize) {
return {
// update Gate
// wzxh
updateGateInputMatrix: new RandomMatrix(hiddenSize, prevSize, 0.08),
updateGateHiddenMatrix: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
updateGateBias: new Matrix(hiddenSize, 1),
// reset Gate
// wrxh
resetGateInputMatrix: new RandomMatrix(hiddenSize, prevSize, 0.08),
resetGateHiddenMatrix: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
resetGateBias: new Matrix(hiddenSize, 1),
// cell write parameters
// wcxh
cellWriteInputMatrix: new RandomMatrix(hiddenSize, prevSize, 0.08),
cellWriteHiddenMatrix: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
cellWriteBias: new Matrix(hiddenSize, 1),
};
}
function getGRUEquation(equation, inputMatrix, previousResult, hiddenLayer) {
if (!hiddenLayer.updateGateInputMatrix ||
!hiddenLayer.updateGateHiddenMatrix ||
!hiddenLayer.updateGateBias ||
!hiddenLayer.resetGateInputMatrix ||
!hiddenLayer.resetGateHiddenMatrix ||
!hiddenLayer.resetGateBias ||
!hiddenLayer.cellWriteInputMatrix ||
!hiddenLayer.cellWriteHiddenMatrix ||
!hiddenLayer.cellWriteBias) {
throw new Error('hiddenLayer does not have expected properties');
}
const sigmoid = equation.sigmoid.bind(equation);
const add = equation.add.bind(equation);
const multiply = equation.multiply.bind(equation);
const multiplyElement = equation.multiplyElement.bind(equation);
const tanh = equation.tanh.bind(equation);
const allOnes = equation.allOnes.bind(equation);
const cloneNegative = equation.cloneNegative.bind(equation);
// update gate
const updateGate = sigmoid(add(add(multiply(hiddenLayer.updateGateInputMatrix, inputMatrix), multiply(hiddenLayer.updateGateHiddenMatrix, previousResult)), hiddenLayer.updateGateBias));
// reset gate
const resetGate = sigmoid(add(add(multiply(hiddenLayer.resetGateInputMatrix, inputMatrix), multiply(hiddenLayer.resetGateHiddenMatrix, previousResult)), hiddenLayer.resetGateBias));
// cell
const cell = tanh(add(add(multiply(hiddenLayer.cellWriteInputMatrix, inputMatrix), multiply(hiddenLayer.cellWriteHiddenMatrix, multiplyElement(resetGate, previousResult))), hiddenLayer.cellWriteBias));
// compute hidden state as gated, saturated cell activations
// negate updateGate
return add(multiplyElement(add(allOnes(updateGate.rows, updateGate.columns), cloneNegative(updateGate)), cell), multiplyElement(previousResult, updateGate));
}
class ArrayLookupTable {
constructor(data, prop) {
this.prop = prop;
this.length = 0;
this.table = {};
for (let i = 0; i < data.length; i++) {
const datum = data[i];
const ioValue = datum[prop];
for (let j = 0; j < ioValue.length; j++) {
const value = ioValue[j];
for (const p in value) {
if (!value.hasOwnProperty(p))
continue;
if (this.table.hasOwnProperty(p))
continue;
this.table[p] = this.length++;
}
}
}
}
}
const defaults = () => {
return {
...defaults$1(),
inputSize: 1,
hiddenLayers: [20],
outputSize: 1,
inputRange: 0,
};
};
class RNNTimeStep extends RNN {
constructor(options = {}) {
super();
this.inputLookupLength = 0;
this.inputLookup = null;
this.outputLookup = null;
this.outputLookupLength = 0;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.model = Object.seal({
isInitialized: false,
hiddenLayers: [],
output: new Matrix(0, 0),
equations: [],
allMatrices: [],
equationConnections: [],
outputConnector: new RandomMatrix(0, 0, 0.08),
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.options = defaults();
this.options = { ...this.options, ...options };
this.updateTrainingOptions({
...trainDefaults,
...options,
});
if (options.json) {
this.fromJSON(options.json);
}
}
createInputMatrix() {
throw new Error('Input Matrices do not exist on RNNTimeStep');
}
createOutputMatrices() {
const { outputSize } = this.options;
const lastHiddenSize = last(this.options.hiddenLayers);
// whd
const outputConnector = new RandomMatrix(outputSize, lastHiddenSize, 0.08);
// bd
const output = new RandomMatrix(outputSize, 1, 0.08);
return { output, outputConnector };
}
bindEquation() {
const { model, options } = this;
const { hiddenLayers, inputSize } = options;
const layers = model.hiddenLayers;
const equation = new Equation();
const outputs = [];
const equationConnection = model.equationConnections.length > 0
? model.equationConnections[model.equationConnections.length - 1]
: this.initialLayerInputs;
// 0 index
let output = this.getEquation(equation, equation.input(new Matrix(inputSize, 1)), equationConnection[0], layers[0]);
outputs.push(output);
// 1+ indices
for (let i = 1, max = hiddenLayers.length; i < max; i++) {
output = this.getEquation(equation, output, equationConnection[i], layers[i]);
outputs.push(output);
}
model.equationConnections.push(outputs);
equation.add(equation.multiply(model.outputConnector, output), model.output);
model.equations.push(equation);
}
initialize() {
this.model = this.mapModel();
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
mapModel() {
const allMatrices = [];
this.initialLayerInputs = this.options.hiddenLayers.map((size) => new Matrix(size, 1));
const hiddenLayers = this.createHiddenLayers();
for (let i = 0, max = hiddenLayers.length; i < max; i++) {
const hiddenMatrix = hiddenLayers[i];
for (const property in hiddenMatrix) {
if (!hiddenMatrix.hasOwnProperty(property))
continue;
allMatrices.push(hiddenMatrix[property]);
}
}
const { outputConnector, output } = this.createOutputMatrices();
allMatrices.push(outputConnector);
allMatrices.push(output);
return Object.seal({
isInitialized: true,
hiddenLayers,
output,
equations: [],
allMatrices,
equationConnections: [],
outputConnector,
});
}
backpropagate() {
for (let i = this.model.equations.length - 1; i > -1; i--) {
this.model.equations[i].backpropagate();
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
run(rawInput) {
const shape = lookup.dataShape(rawInput).join(',');
switch (shape) {
case 'array,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.runArray(rawInput);
case 'array,array,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.runArrayOfArray(rawInput);
case 'object,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.runObject(rawInput); // Backward compatibility, will be result of `unknown` and need casting. Better to just use net.runObject() directly
case 'array,object,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.runArrayOfObject(rawInput);
default:
throw new Error(`Unrecognized data shape ${shape}`);
}
}
forecast(rawInput, count = 1) {
const shape = lookup.dataShape(rawInput).join(',');
switch (shape) {
case 'array,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.forecastArray(rawInput, count);
case 'array,array,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.forecastArrayOfArray(rawInput, count);
case 'object,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.runObject(rawInput);
case 'array,object,number':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.forecastArrayOfObject(rawInput, count);
default:
throw new Error(`Unrecognized data shape ${shape}`);
}
}
forecastArray(input, count = 1) {
this.checkRunnable();
const { model } = this;
const { equations } = model;
const length = input.length + count;
while (equations.length <= length) {
this.bindEquation();
}
let lastOutput;
let equationIndex = 0;
if (this.options.inputSize === 1) {
for (let i = 0; i < input.length; i++) {
lastOutput = equations[equationIndex++].runInput(Float32Array.from([input[i]]));
}
}
else {
for (let i = 0; i < input.length; i++) {
lastOutput = equations[equationIndex++].runInput(Float32Array.from([]));
}
}
if (!lastOutput) {
throw new Error('lastOutput not set');
}
const result = [lastOutput.weights[0]];
for (let i = 0, max = count - 1; i < max; i++) {
lastOutput = equations[equationIndex++].runInput(lastOutput.weights);
result.push(lastOutput.weights[0]);
}
this.end();
return Float32Array.from(result);
}
forecastArrayOfArray(input, count = 1) {
this.checkRunnable();
const { model } = this;
const { equations } = model;
const length = input.length + count;
while (equations.length <= length) {
this.bindEquation();
}
let lastOutput;
let equationIndex = 0;
for (let i = 0; i < input.length; i++) {
lastOutput = equations[equationIndex++].runInput(input[i]);
}
if (!lastOutput) {
throw new Error('lastOutput not set');
}
const result = [Float32Array.from(lastOutput.weights)];
for (let i = 0, max = count - 1; i < max; i++) {
lastOutput = equations[equationIndex++].runInput(lastOutput.weights);
result.push(Float32Array.from(lastOutput.weights.slice(0)));
}
this.end();
return result;
}
forecastArrayOfObject(input, count = 1) {
if (!this.inputLookup) {
throw new Error('this.inputLookup not set');
}
if (!this.outputLookup) {
throw new Error('this.outputLookup not set');
}
const formattedData = input.map((value) => lookup.toArray(this.inputLookup, value, this.inputLookupLength));
return this.forecastArrayOfArray(formattedData, count).map((value) => lookup.toObject(this.outputLookup, value));
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
train(data, trainOpts = {}) {
this.trainOpts = trainOpts = {
...trainDefaults$1,
...trainOpts,
};
// Don't destructure here because this.setSize() can reset this.options.
if (this.options.inputSize === 1 && this.options.outputSize === 1) {
this.setSize(data);
}
this.verifySize();
const formattedData = this.formatData(data);
let error = Infinity;
let i;
this.verifyIsInitialized();
const { iterations, errorThresh, logPeriod, callback, callbackPeriod, } = this.trainOpts;
const log = trainOpts.log === true ? console.log : trainOpts.log;
for (i = 0; i < iterations && error > errorThresh; i++) {
let sum = 0;
for (let j = 0; j < formattedData.length; j++) {
const err = this.trainPattern(formattedData[j], true);
sum += err;
}
error = sum / formattedData.length;
if (isNaN(error))
throw new Error('Network error rate is unexpected NaN, check network configurations and try again. Most probably input format is not correct or training data is not enough. ');
if (log && i % logPeriod === 0) {
log(`iterations: ${i}, training error: ${error}`);
}
if (callback && i % callbackPeriod === 0) {
callback({ error, iterations: i });
}
}
return {
error,
iterations: i,
};
}
trainArrayOfArray(input) {
if (input.length < 2) {
throw new Error('input must be an array of 2 or more');
}
const { equations } = this.model;
while (equations.length < input.length) {
this.bindEquation();
}
let errorSum = 0;
for (let i = 0, max = input.length - 1; i < max; i++) {
errorSum += equations[i].predictTarget(input[i], input[i + 1]);
}
this.end();
return errorSum / input.length;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
trainPattern(input, logErrorRate) {
const error = this.trainArrayOfArray(input);
this.backpropagate();
this.adjustWeights();
if (logErrorRate) {
return error;
}
return 0;
}
setSize(data) {
let size = 0;
const dataShape = lookup.dataShape(data).join(',');
switch (dataShape) {
case 'array,array,number':
case 'array,object,number':
case 'array,datum,array,number':
case 'array,datum,object,number':
size = 1;
// probably 1
break;
case 'array,array,array,number':
size = data[0][0].length;
break;
case 'array,array,object,number':
// inputs and outputs should match
size = Object.keys(lookup.toTable2D(data)).length;
break;
case 'array,datum,array,array,number':
size = data[0].input[0].length;
break;
case 'array,datum,array,object,number':
size = Object.keys(lookup.toInputTable2D(data)).length;
break;
default:
throw new Error('unknown data shape or configuration');
}
this.options = Object.seal({
...this.options,
inputSize: size,
outputSize: size,
});
}
verifySize() {
if (this.options.inputSize || this.options.outputSize) {
if (this.options.inputSize !== this.options.outputSize) {
throw new Error('manually set inputSize and outputSize mismatch');
}
}
}
runArray(input) {
this.checkRunnable();
const { equations } = this.model;
while (equations.length <= input.length) {
this.bindEquation();
}
let lastOutput;
for (let i = 0; i < input.length; i++) {
lastOutput = equations[i].runInput(new Float32Array([input[i]]));
}
this.end();
return lastOutput.weights[0];
}
runArrayOfArray(input) {
this.checkRunnable();
const { model } = this;
const { equations } = model;
while (equations.length <= input.length) {
this.bindEquation();
}
let lastOutput;
for (let i = 0; i < input.length; i++) {
const outputMatrix = equations[i].runInput(input[i]);
lastOutput = outputMatrix.weights;
}
this.end();
return lastOutput !== null && lastOutput !== void 0 ? lastOutput : Float32Array.from([]);
}
runObject(input) {
if (!this.inputLookup) {
throw new Error('this.inputLookup not set');
}
if (!this.outputLookup) {
throw new Error('this.outputLookup not set');
}
if (!this.outputLookupLength) {
throw new Error('this.outputLookupLength not set');
}
if (this.inputLookup === this.outputLookup) {
const inputArray = lookup.toArrayShort(this.inputLookup, input);
return lookup.toObjectPartial(this.outputLookup, this.forecastArray(inputArray, this.outputLookupLength - inputArray.length), inputArray.length);
}
return lookup.toObject(this.outputLookup, this.forecastArray(lookup.toArray(this.inputLookup, input, this.inputLookupLength), this.outputLookupLength));
}
runArrayOfObject(input) {
if (this.inputLookup === null) {
throw new Error('this.inputLookup not set');
}
if (this.outputLookup === null) {
throw new Error('this.outputLookup not set');
}
const formattedInput = input.map((value) => lookup.toArray(this.inputLookup, value, this.inputLookupLength));
return this.forecastArrayOfArray(formattedInput, 1).map((value) => lookup.toObject(this.outputLookup, value))[0];
}
runArrayOfObjectOfArray(input) {
if (!this.inputLookup) {
throw new Error('this.inputLookup not set');
}
if (!this.outputLookup) {
throw new Error('this.outputLookup not set');
}
return lookup.toObject(this.outputLookup, this.runArrayOfArray(lookup.toArrays(this.inputLookup, input, this.inputLookupLength)));
}
end() {
this.model.equations[this.model.equations.length - 1].runInput(new Float32Array(this.options.outputSize));
}
requireInputOutputOfOne() {
if (this.options.inputSize !== 1) {
throw new Error('inputSize must be 1 for this data size');
}
if (this.options.outputSize !== 1) {
throw new Error('outputSize must be 1 for this data size');
}
}
// Handles data shape of 'array,number'
formatArray(data) {
const result = [];
this.requireInputOutputOfOne();
for (let i = 0; i < data.length; i++) {
result.push(Float32Array.from([data[i]]));
}
return [result];
}
// Handles data shape of 'array,array,number'
formatArrayOfArray(data) {
const result = [];
const { inputSize, outputSize } = this.options;
if (inputSize === 1 && outputSize === 1) {
for (let i = 0; i < data.length; i++) {
result.push(arrayToFloat32Arrays(data[i]));
}
return result;
}
if (inputSize !== data[0].length) {
throw new Error('inputSize must match data input size');
}
if (outputSize !== data[0].length) {
throw new Error('outputSize must match data output size');
}
for (let i = 0; i < data.length; i++) {
result.push(Float32Array.from(data[i]));
}
return [result];
}
// Handles data shape of 'array,object,number'
formatArrayOfObject(data) {
this.requireInputOutputOfOne();
if (!this.inputLookup) {
const lookupTable = new LookupTable(data);
this.inputLookup = this.outputLookup = lookupTable.table;
this.inputLookupLength = this.outputLookupLength = lookupTable.length;
}
const result = [];
for (let i = 0; i < data.length; i++) {
result.push(objectToFloat32Arrays(data[i]));
}
return result;
}
// Handles data shape of 'array,object,number' when this.options.inputSize > 1
formatArrayOfObjectMulti(data) {
if (!this.inputLookup) {
const lookupTable = new LookupTable(data);
this.inputLookup = this.outputLookup = lookupTable.table;
this.inputLookupLength = this.outputLookupLength = lookupTable.length;
}
const result = [];
for (let i = 0; i < data.length; i++) {
result.push([
objectToFloat32Array(data[i], this.inputLookup, this.inputLookupLength),
]);
}
return result;
}
// Handles data shape of 'array,datum,array,number'
formatArrayOfDatumOfArray(data) {
const result = [];
this.requireInputOutputOfOne();
for (let i = 0; i < data.length; i++) {
const datum = data[i];
result.push(inputOutputArrayToFloat32Arrays(datum.input, datum.output));
}
return result;
}
// Handles data shape of 'array,datum,object,number'
formatArrayOfDatumOfObject(data) {
this.requireInputOutputOfOne();
if (!this.inputLookup) {
const inputLookup = new LookupTable(data, 'input');
this.inputLookup = inputLookup.table;
this.inputLookupLength = inputLookup.length;
}
if (!this.outputLookup) {
const outputLookup = new LookupTable(data, 'output');
this.outputLookup = outputLookup.table;
this.outputLookupLength = outputLookup.length;
}
const result = [];
for (let i = 0; i < data.length; i++) {
const datum = data[i];
result.push(inputOutputObjectToFloat32Arrays(datum.input, datum.output));
}
return result;
}
// Handles data shape of 'array,array,array,number'
formatArrayOfArrayOfArray(data) {
const result = [];
for (let i = 0; i < data.length; i++) {
result.push(arraysToFloat32Arrays(data[i]));
}
return result;
}
// Handles data shape of 'array,array,object,number'
formatArrayOfArrayOfObject(data) {
if (!this.inputLookup) {
const lookupTable = new LookupTable(data);
this.inputLookup = this.outputLookup = lookupTable.table;
this.inputLookupLength = this.outputLookupLength = lookupTable.length;
}
const result = [];
for (let i = 0; i < data.length; i++) {
const array = [];
for (let j = 0; j < data[i].length; j++) {
array.push(objectToFloat32Array(data[i][j], this.inputLookup, this.inputLookupLength));
}
result.push(array);
}
return result;
}
// Handles data shape of 'array,datum,array,array,number'
formatArrayOfDatumOfArrayOfArray(data) {
const result = [];
const { inputSize, outputSize } = this.options;
if (inputSize !== data[0].input[0].length) {
throw new Error('inputSize must match data input size');
}
if (outputSize !== data[0].output[0].length) {
throw new Error('outputSize must match data output size');
}
for (let i = 0; i < data.length; i++) {
const datum = data[i];
result.push(inputOutputArraysToFloat32Arrays(datum.input, datum.output));
}
return result;
}
// 'Handles data shape of array,datum,array,object,number'
formatArrayOfDatumOfArrayOfObject(data) {
if (!this.inputLookup) {
const inputLookup = new ArrayLookupTable(data, 'input');
this.inputLookup = inputLookup.table;
this.inputLookupLength = inputLookup.length;
}
if (!this.outputLookup) {
const outputLookup = new ArrayLookupTable(data, 'output');
this.outputLookup = outputLookup.table;
this.outputLookupLength = outputLookup.length;
}
if (!this.outputLookupLength) {
throw new Error('this.outputLookupLength not set to usable number');
}
const result = [];
for (let i = 0; i < data.length; i++) {
const datum = data[i];
result.push(inputOutputObjectsToFloat32Arrays(datum.input, datum.output, this.inputLookup, this.outputLookup, this.inputLookupLength, this.outputLookupLength));
}
return result;
}
formatData(data) {
const dataShape = lookup.dataShape(data).join(',');
switch (dataShape) {
case 'array,number':
return this.formatArray(data);
case 'array,array,number':
return this.formatArrayOfArray(data);
case 'array,object,number':
if (this.options.inputSize === 1) {
return this.formatArrayOfObject(data);
}
else {
return this.formatArrayOfObjectMulti(data);
}
case 'array,datum,array,number':
return this.formatArrayOfDatumOfArray(data);
case 'array,datum,object,number':
return this.formatArrayOfDatumOfObject(data);
case 'array,array,array,number':
return this.formatArrayOfArrayOfArray(data);
case 'array,array,object,number':
return this.formatArrayOfArrayOfObject(data);
case 'array,datum,array,array,number':
return this.formatArrayOfDatumOfArrayOfArray(data);
case 'array,datum,array,object,number':
return this.formatArrayOfDatumOfArrayOfObject(data);
default:
throw new Error('unknown data shape or configuration');
}
}
test(data) {
// for classification problems
const misclasses = [];
// run each pattern through the trained network and collect
// error and misclassification statistics
let errorSum = 0;
const formattedData = this.formatData(data);
for (let i = 0; i < formattedData.length; i++) {
const input = formattedData[i];
const output = this.run(input.splice(0, input.length - 1));
const target = input[input.length - 1];
let errors = 0;
let errorCount = 0;
for (let j = 0; j < output.length; j++) {
errorCount++;
const error = target[j] - output[j];
// mse
errors += error * error;
}
errorSum += errors / errorCount;
const errorsAbs = Math.abs(errors);
if (errorsAbs > this.trainOpts.errorThresh) {
const misclass = data[i];
misclasses.push({
value: misclass,
actual: output,
});
}
}
return {
error: errorSum / formattedData.length,
misclasses,
total: formattedData.length,
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
addFormat(value) {
var _a, _b, _c, _d, _e, _f;
const dataShape = lookup.dataShape(value).join(',');
switch (dataShape) {
case 'array,array,number':
case 'datum,array,array,number':
case 'array,number':
case 'datum,array,number':
return;
case 'datum,object,number': {
this.inputLookup = lookup.addKeys(value.input, (_a = this.inputLookup) !== null && _a !== void 0 ? _a : {});
if (this.inputLookup) {
this.inputLookupLength = Object.keys(this.inputLookup).length;
}
this.outputLookup = lookup.addKeys(value.output, (_b = this.outputLookup) !== null && _b !== void 0 ? _b : {});
if (this.outputLookup) {
this.outputLookupLength = Object.keys(this.outputLookup).length;
}
break;
}
case 'object,number': {
this.inputLookup = this.outputLookup = lookup.addKeys(value, (_c = this.inputLookup) !== null && _c !== void 0 ? _c : {});
if (this.inputLookup) {
this.inputLookupLength = this.outputLookupLength = Object.keys(this.inputLookup).length;
}
break;
}
case 'array,object,number': {
const typedValue = value;
for (let i = 0; i < typedValue.length; i++) {
this.inputLookup = this.outputLookup = lookup.addKeys(typedValue[i], (_d = this.inputLookup) !== null && _d !== void 0 ? _d : {});
if (this.inputLookup) {
this.inputLookupLength = this.outputLookupLength = Object.keys(this.inputLookup).length;
}
}
break;
}
case 'datum,array,object,number': {
const typedValue = value;
const typedInput = typedValue.input;
for (let i = 0; i < typedInput.length; i++) {
this.inputLookup = lookup.addKeys(typedInput[i], (_e = this.inputLookup) !== null && _e !== void 0 ? _e : {});
if (this.inputLookup) {
this.inputLookupLength = Object.keys(this.inputLookup).length;
}
}
const typedOutput = typedValue.output;
for (let i = 0; i < typedOutput.length; i++) {
this.outputLookup = lookup.addKeys(typedOutput[i], (_f = this.outputLookup) !== null && _f !== void 0 ? _f : {});
if (this.outputLookup) {
this.outputLookupLength = Object.keys(this.outputLookup).length;
}
}
break;
}
default:
throw new Error('unknown data shape or configuration');
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
toJSON() {
if (!this.model) {
this.initialize();
}
const { model } = this;
const options = { ...this.options, ...defaults$1 };
return {
type: this.constructor.name,
options,
hiddenLayers: model.hiddenLayers.map((hiddenLayer) => {
const layers = {};
for (const p in hiddenLayer) {
if (!hiddenLayer.hasOwnProperty(p))
continue;
layers[p] = hiddenLayer[p].toJSON();
}
return layers;
}),
outputConnector: model.outputConnector.toJSON(),
output: model.output.toJSON(),
inputLookup: this.inputLookup,
inputLookupLength: this.inputLookupLength,
outputLookup: this.outputLookup,
outputLookupLength: this.outputLookupLength,
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
fromJSON(json) {
const { options } = json;
const allMatrices = [];
const hiddenLayers = [];
// backward compatibility for hiddenSizes
json.hiddenLayers.forEach((hiddenLayer) => {
const layers = {};
for (const p in hiddenLayer) {
layers[p] = Matrix.fromJSON(hiddenLayer[p]);
allMatrices.push(layers[p]);
}
hiddenLayers.push(layers);
});
const outputConnector = Matrix.fromJSON(json.outputConnector);
allMatrices.push(outputConnector);
const output = Matrix.fromJSON(json.output);
allMatrices.push(output);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.options = { ...defaults(), ...options };
this.inputLookup = json.inputLookup;
this.inputLookupLength = json.inputLookupLength;
this.outputLookup = json.outputLookup;
this.outputLookupLength = json.outputLookupLength;
this.model = Object.seal({
isInitialized: true,
hiddenLayers,
output,
allMatrices,
outputConnector,
equations: [],
equationConnections: [],
});
this.initialLayerInputs = options.hiddenLayers.map((size) => new Matrix(size, 1));
this.bindEquation();
return this;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
toFunction(cb) {
const { model, inputLookup, inputLookupLength, outputLookup, outputLookupLength, } = this;
const { inputSize } = this.options;
const { equations } = model;
const equation = equations[1];
const { states } = equation;
const jsonString = JSON.stringify(this.toJSON());
function previousConnectionIndex(m) {
const connection = model.equationConnections[0];
const { states } = equations[0];
for (let i = 0, max = states.length; i < max; i++) {
if (states[i].product === m) {
return i;
}
}
return connection.indexOf(m);
}
function matrixOrigin(m, stateIndex) {
for (let i = 0, max = states.length; i < max; i++) {
const state = states[i];
if (i === stateIndex) {
const j = previousConnectionIndex(m);
switch (m) {
case state.left:
if (j > -1) {
return `typeof prevStates[${j}] === 'object' ? prevStates[${j}].product : new Matrix(${m.rows}, ${m.columns})`;
}
// eslint-disable-next-line no-fallthrough
case state.right:
if (j > -1) {
return `typeof prevStates[${j}] === 'object' ? prevStates[${j}].product : new Matrix(${m.rows}, ${m.columns})`;
}
// eslint-disable-next-line no-fallthrough
case state.product:
return `new Matrix(${m.rows}, ${m.columns})`;
default:
throw Error('unknown state');
}
}
if (m === state.product)
return `states[${i}].product`;
if (m === state.right)
return `states[${i}].right`;
if (m === state.left)
return `states[${i}].left`;
}
return '';
}
function matrixToString(m, stateIndex) {
if (!m || !m.rows || !m.columns)
return 'null';
if (m === model.outputConnector)
return `json.outputConnector`;
if (m === model.output)
return `json.output`;
for (let i = 0, max = model.hiddenLayers.length; i < max; i++) {
const hiddenLayer = model.hiddenLayers[i];
for (const p in hiddenLayer) {
if (!hiddenLayer.hasOwnProperty(p))
continue;
if (hiddenLayer[p] !== m)
continue;
return `json.hiddenLayers[${i}].${p}`;
}
}
return matrixOrigin(m, stateIndex);
}
function formatInputData() {
if (!inputLookup)
return '';
if (inputSize === 1) {
if (inputLookup === outputLookup) {
return `function lookupInput(input) {
var table = ${JSON.stringify(inputLookup)};
var result = [];
for (var p in table) {
if (!input.hasOwnProperty(p)) break;
result.push(Float32Array.from([input[p]]));
}
return result;
}`;
}
return `function lookupInput(input) {
var table = ${JSON.stringify(inputLookup)};
var result = [];
for (var p in table) {
result.push(Float32Array.from([input[p]]));
}
return result;
}`;
}
return `function lookupInput(rawInputs) {
var table = ${JSON.stringify(inputLookup)};
var result = [];
for (var i = 0; i < rawInputs.length; i++) {
var rawInput = rawInputs[i];
var input = new Float32Array(${inputLookupLength});
for (var p in table) {
input[table[p]] = rawInput.hasOwnProperty(p) ? rawInput[p] : 0;
}
result.push(input);
}
return result;
}`;
}
function formatOutputData() {
if (!outputLookup)
return '';
if (inputSize === 1) {
if (inputLookup === outputLookup) {
return `function lookupOutputPartial(output, input) {
var table = ${JSON.stringify(outputLookup)};
var offset = input.length;
var result = {};
var i = 0;
for (var p in table) {
if (i++ < offset) continue;
result[p] = output[table[p] - offset][0];
}
return result;
}`;
}
return `function lookupOutput(output) {
var table = ${JSON.stringify(outputLookup)};
var result = {};
for (var p in table) {
result[p] = output[table[p]][0];
}
return result;
}`;
}
return `function lookupOutput(output) {
var table = ${JSON.stringify(outputLookup)};
var result = {};
for (var p in table) {
result[p] = output[table[p]];
}
return result;
}`;
}
function toInner(fnString) {
// crude, but should be sufficient for now
// function() { body }
// crude, but should be sufficient for now
// function() { body }
const fnParts = fnString.toString().split('{');
fnParts.shift();
// body }
const fnBodyString = fnParts.join('{');
const fnBodyParts = fnBodyString.split('}');
fnBodyParts.pop();
// body
return fnBodyParts
.join('}')
.split('\n')
.join('\n ')
.replace('product.deltas[i] = 0;', '')
.replace('product.deltas[column] = 0;', '')
.replace('left.deltas[leftIndex] = 0;', '')
.replace('right.deltas[rightIndex] = 0;', '')
.replace('product.deltas = left.deltas.slice(0);', '');
}
function fileName(fnName) {
return `src/recurrent/matrix/${fnName.replace(/[A-Z]/g, function (value) {
return `-${value.toLowerCase()}`;
})}.js`;
}
const statesRaw = [];
const usedFunctionNames = {};
const innerFunctionsSwitch = [];
for (let i = 0, max = states.length; i < max; i++) {
const state = states[i];
statesRaw.push(`states[${i}] = {
name: '${state.forwardFn.name}',
left: ${state.left ? matrixToString(state.left, i) : 'undefined'},
right: ${state.right ? matrixToString(state.right, i) : 'undefined'},
product: ${matrixToString(state.product, i)}
}`);
const fnName = state.forwardFn.name;
if (!usedFunctionNames[fnName]) {
usedFunctionNames[fnName] = true;
if (state.name === 'input') {
innerFunctionsSwitch.push(`case '${fnName}':`);
innerFunctionsSwitch.push(inputLookup && inputSize === 1
? 'product.weights = _i < input.length ? input[_i]: prevStates[prevStates.length - 1].product.weights;'
: inputSize === 1
? 'product.weights = [input[_i]];'
: 'product.weights = input[_i];');
innerFunctionsSwitch.push('break;');
}
else {
innerFunctionsSwitch.push(` case '${fnName}':${fnName !== 'forwardFn'
? ` //compiled from ${fileName(fnName)}`
: ''}
${toInner(state.forwardFn.toString())}
break;`);
}
}
}
const forceForecast = inputSize === 1 && this.outputLookup;
const src = `
var input = ${this.inputLookup ? 'lookupInput(rawInput)' : 'rawInput'};
var json = ${jsonString};
var output = [];
var states = [];
var prevStates;
var state;
var max = ${forceForecast
? inputLookup === outputLookup
? inputLookupLength
: `input.length + ${outputLookupLength - 1}`
: 'input.length'};
for (var _i = 0; _i < max; _i++) {
prevStates = states;
states = [];
${statesRaw.join(';\n ')};
for (var stateIndex = 0, stateMax = ${statesRaw.length}; stateIndex < stateMax; stateIndex++) {
state = states[stateIndex];
var product = state.product;
var left = state.left;
var right = state.right;
switch (state.name) {
${innerFunctionsSwitch.join('\n')}
}
}
${inputSize === 1 && inputLookup
? 'if (_i >= input.length - 1) { output.push(state.product.weights); }'
: 'output = state.product.weights;'}
}
${outputLookup
? outputLookup === inputLookup
? 'return lookupOutputPartial(output, input)'
: 'return lookupOutput(output)'
: inputSize === 1
? 'return output[0]'
: 'return output'};
${formatInputData()}
${formatOutputData()}
function Matrix(rows, columns) {
this.rows = rows;
this.columns = columns;
this.weights = zeros(rows * columns);
}
${zeros$1.toString()}
${softmax.toString().replace('_2.default', 'Matrix')}
${randomFloat.toString()}
${sampleI.toString()}
${maxI.toString()}`;
// eslint-disable-next-line
return new Function('rawInput', cb ? cb(src) : src);
}
}
const trainDefaults = { ...trainDefaults$1 };
class GRUTimeStep extends RNNTimeStep {
getHiddenLayer(hiddenSize, prevSize) {
return getGRUHiddenLayer(hiddenSize, prevSize);
}
getEquation(equation, inputMatrix, previousResult, hiddenLayer) {
return getGRUEquation(equation, inputMatrix, previousResult, hiddenLayer);
}
}
class LSTM extends RNN {
getHiddenLayer(hiddenSize, prevSize) {
return getHiddenLSTMLayer(hiddenSize, prevSize);
}
getEquation(equation, inputMatrix, previousResult, hiddenLayer) {
return getLSTMEquation(equation, inputMatrix, previousResult, hiddenLayer);
}
}
function getHiddenLSTMLayer(hiddenSize, prevSize) {
return {
// gates parameters
// wix
inputMatrix: new RandomMatrix(hiddenSize, prevSize, 0.08),
inputHidden: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
inputBias: new Matrix(hiddenSize, 1),
// wfx
forgetMatrix: new RandomMatrix(hiddenSize, prevSize, 0.08),
forgetHidden: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
forgetBias: new Matrix(hiddenSize, 1),
// wox
outputMatrix: new RandomMatrix(hiddenSize, prevSize, 0.08),
outputHidden: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
outputBias: new Matrix(hiddenSize, 1),
// cell write params
// wcx
cellActivationMatrix: new RandomMatrix(hiddenSize, prevSize, 0.08),
cellActivationHidden: new RandomMatrix(hiddenSize, hiddenSize, 0.08),
cellActivationBias: new Matrix(hiddenSize, 1),
};
}
function getLSTMEquation(equation, inputMatrix, previousResult, hiddenLayer) {
if (!hiddenLayer.inputMatrix ||
!hiddenLayer.inputHidden ||
!hiddenLayer.inputBias ||
!hiddenLayer.forgetMatrix ||
!hiddenLayer.forgetHidden ||
!hiddenLayer.forgetBias ||
!hiddenLayer.outputMatrix ||
!hiddenLayer.outputHidden ||
!hiddenLayer.outputBias ||
!hiddenLayer.cellActivationMatrix ||
!hiddenLayer.cellActivationHidden ||
!hiddenLayer.cellActivationBias) {
throw new Error('hiddenLayer does not have expected properties');
}
const sigmoid = equation.sigmoid.bind(equation);
const add = equation.add.bind(equation);
const multiply = equation.multiply.bind(equation);
const multiplyElement = equation.multiplyElement.bind(equation);
const tanh = equation.tanh.bind(equation);
const inputGate = sigmoid(add(add(multiply(hiddenLayer.inputMatrix, inputMatrix), multiply(hiddenLayer.inputHidden, previousResult)), hiddenLayer.inputBias));
const forgetGate = sigmoid(add(add(multiply(hiddenLayer.forgetMatrix, inputMatrix), multiply(hiddenLayer.forgetHidden, previousResult)), hiddenLayer.forgetBias));
// output gate
const outputGate = sigmoid(add(add(multiply(hiddenLayer.outputMatrix, inputMatrix), multiply(hiddenLayer.outputHidden, previousResult)), hiddenLayer.outputBias));
// write operation on cells
const cellWrite = tanh(add(add(multiply(hiddenLayer.cellActivationMatrix, inputMatrix), multiply(hiddenLayer.cellActivationHidden, previousResult)), hiddenLayer.cellActivationBias));
// compute new cell activation
const retainCell = multiplyElement(forgetGate, previousResult); // what do we keep from cell
const writeCell = multiplyElement(inputGate, cellWrite); // what do we write to cell
const cell = add(retainCell, writeCell); // new cell contents
// compute hidden state as gated, saturated cell activations
return multiplyElement(outputGate, tanh(cell));
}
class LSTMTimeStep extends RNNTimeStep {
getHiddenLayer(hiddenSize, prevSize) {
return getHiddenLSTMLayer(hiddenSize, prevSize);
}
getEquation(equation, inputMatrix, previousResult, hiddenLayer) {
return getLSTMEquation(equation, inputMatrix, previousResult, hiddenLayer);
}
}
/**
*
* @param start
* @param end
* @returns {Array}
*/
function range(start, end) {
const result = [];
for (; start < end; start++) {
result.push(start);
}
return result;
}
function toArray(values) {
if (Array.isArray(values)) {
return Float32Array.from(values);
}
return Float32Array.from(Object.values(values));
}
function drawInput({ pixelX, pixelY, radius, inputs, row, line, fontSize, fontClassName, }) {
let svg = `
`;
if (inputs.labels) {
svg += `${inputs.labels[row]}`;
}
return svg;
}
function drawNeuron({ pixelX, pixelY, row, column, radius, hidden, }) {
return ``;
}
function drawOutput({ pixelX, pixelY, row, column, line, outputs, radius, }) {
return `
`;
}
function drawBackwardConnections({ pixelX, pixelY, row, column, radius, lineY, line, previousConnectionIndex, }) {
return ``;
}
function neuralNetworkToInnerSVG(options) {
const { sizes, height, width } = options;
let svg = '';
const pixelX = width / sizes.length;
for (let column = 0; column < sizes.length; column++) {
const size = sizes[column];
const pixelY = height / size;
for (let row = 0; row < size; row++) {
if (column === 0) {
svg += drawInput({ pixelX, pixelY, row, column, ...options });
}
else {
if (column === sizes.length - 1) {
svg += drawOutput({ pixelX, pixelY, row, column, ...options });
}
else {
svg += drawNeuron({ pixelX, pixelY, row, column, ...options });
}
const previousSize = sizes[column - 1];
const lineY = height / previousSize;
for (let previousConnectionIndex = 0; previousConnectionIndex < previousSize; previousConnectionIndex++) {
svg += drawBackwardConnections({
pixelX,
pixelY,
row,
column,
lineY,
previousConnectionIndex,
...options,
});
}
}
}
}
return svg;
}
function drawRecurrentConnections({ pixelX, pixelY, row, column, radius, recurrentLine, }) {
const moveX = pixelX / 2 + column * pixelX + radius + 1;
const moveY = pixelY / 2 + row * pixelY;
const x = moveX - radius * 2 - 2;
const y = moveY;
const x1 = x + 100;
const y1 = y + 50;
const x2 = moveX - 100;
const y2 = moveY + 50;
return ``;
}
function rnnToInnerSVG(options) {
const { width, height, recurrentLine, sizes, radius } = options;
const pixelX = width / sizes.length;
let svg = `
`;
svg += neuralNetworkToInnerSVG(options);
for (let column = 1; column < sizes.length; column++) {
const size = sizes[column];
const pixelY = height / size;
for (let row = 0; row < size; row++) {
svg += drawRecurrentConnections({
pixelX,
pixelY,
row,
column,
radius,
recurrentLine,
});
}
}
return svg;
}
function getFeedForwardLayers(network) {
const { options } = network;
if (!options) {
throw new Error('options not defined');
}
if (!options.inputLayer) {
throw new Error('options.inputLater not defined');
}
if (!options.hiddenLayers) {
throw new Error('options.hiddenLayers not defined');
}
if (options.hiddenLayers.length < 1) {
throw new Error('options.hiddenLayers is empty');
}
if (!options.outputLayer) {
throw new Error('options.outputLayer not defined');
}
const inputLayer = options.inputLayer();
const hiddenLayers = [];
hiddenLayers.push(options.hiddenLayers[0](inputLayer, 0));
for (let i = 1; i < options.hiddenLayers.length; i++) {
hiddenLayers.push(options.hiddenLayers[i](hiddenLayers[i - 1], i));
}
const outputLayer = options.outputLayer(hiddenLayers[hiddenLayers.length - 1], hiddenLayers.length);
return {
inputSize: inputLayer.height,
hiddenLayers: hiddenLayers.map((hiddenLayer) => hiddenLayer.height),
outputSize: outputLayer.height,
};
}
function getRecurrentLayers(network) {
const hiddenLayers = [];
const { options } = network;
if (!options.inputLayer) {
throw new Error('inputLayer not defined');
}
if (!options.outputLayer) {
throw new Error('outputLayer not defined');
}
const inputLayer = options.inputLayer();
hiddenLayers.push(options.hiddenLayers[0](inputLayer, recurrentZeros(), 0));
for (let i = 1; i < options.hiddenLayers.length; i++) {
hiddenLayers.push(options.hiddenLayers[i](hiddenLayers[i - 1], recurrentZeros(), i));
}
const outputLayer = options.outputLayer(hiddenLayers[hiddenLayers.length - 1], -1);
return {
inputSize: inputLayer.height,
hiddenLayers: hiddenLayers.map((hiddenLayer) => hiddenLayer.height),
outputSize: outputLayer.height,
};
}
function wrapOuterSVG(svgBody, width, height) {
// language=html
return ``;
}
function getNeuralNetworkJSONSizes(json) {
return json.sizes;
}
function getNeuralNetworkSizes(net) {
const { options, sizes } = net;
const { inputSize, outputSize, hiddenLayers } = options;
if (!sizes) {
if (typeof inputSize === 'number' && inputSize < 1) {
throw new Error('inputSize not set');
}
if (typeof outputSize === 'number' && outputSize < 1) {
throw new Error('outputSize not set');
}
if (hiddenLayers === null || hiddenLayers === void 0 ? void 0 : hiddenLayers.some((v) => v < 1)) {
throw new Error('hiddenLayers not set');
}
}
return typeof inputSize === 'number' &&
Array.isArray(hiddenLayers) &&
typeof outputSize === 'number'
? [inputSize].concat(hiddenLayers).concat([outputSize])
: sizes;
}
function getRNNSizes(net) {
const { options } = net;
const { inputSize, outputSize, hiddenLayers } = options;
return [inputSize].concat(hiddenLayers).concat([outputSize]);
}
function defaultOptions() {
return {
line: {
width: 0.5,
color: 'black',
className: 'connection',
},
recurrentLine: {
width: 1,
color: 'red',
className: 'recurrence',
},
inputs: {
color: 'rgba(0, 128, 0, 0.5)',
labels: null,
className: 'input',
},
outputs: {
color: 'rgba(100, 149, 237, 0.5)',
className: 'output',
},
hidden: {
color: 'rgba(255, 127, 80, 0.5)',
className: 'hidden-neuron',
},
fontSize: '14px',
fontClassName: 'label',
radius: 8,
width: 400,
height: 250,
sizes: [],
};
}
function toSVG(net, options) {
const mergedOptions = { ...defaultOptions(), ...options };
const { width, height, inputs } = mergedOptions;
// Get network size array for NeuralNetwork or NeuralNetworkGPU
let sizes = [];
if (net instanceof NeuralNetwork || net instanceof NeuralNetworkGPU) {
sizes = getNeuralNetworkSizes(net);
}
// get network size for Recurrent
else if (net instanceof Recurrent) {
const { inputSize, hiddenLayers, outputSize } = getRecurrentLayers(net);
sizes = [inputSize].concat(hiddenLayers).concat([outputSize]);
}
// get network size for FeedForward
else if (net instanceof FeedForward) {
const { inputSize, hiddenLayers, outputSize } = getFeedForwardLayers(net);
sizes = [inputSize].concat(hiddenLayers).concat([outputSize]);
}
// handle json, recurrent first
else if (net instanceof RNN ||
net instanceof LSTM ||
net instanceof GRU ||
net instanceof RNNTimeStep ||
net instanceof LSTMTimeStep ||
net instanceof GRUTimeStep) {
return wrapOuterSVG(rnnToInnerSVG({
...mergedOptions,
sizes: checkSizes(getRNNSizes(net), inputs.labels),
}), width, height);
}
// handle json, NeuralNetwork
else if (net.hasOwnProperty('type')) {
switch (net.type) {
case 'NeuralNetwork':
case 'NeuralNetworkGPU':
return wrapOuterSVG(neuralNetworkToInnerSVG({
...mergedOptions,
sizes: checkSizes(getNeuralNetworkJSONSizes(net), inputs.labels),
}), width, height);
case 'RNN':
case 'GRU':
case 'LSTM':
case 'RNNTimeStep':
case 'GRUTimeStep':
case 'LSTMTimeStep':
return wrapOuterSVG(rnnToInnerSVG({
...mergedOptions,
sizes: checkSizes(getRNNSizes(net), inputs.labels),
}), width, height);
default:
throw new Error('unrecognized network');
}
}
else if (net.hasOwnProperty('inputSize') &&
net.hasOwnProperty('hiddenLayers') &&
net.hasOwnProperty('outputSize')) {
const { inputSize, hiddenLayers, outputSize } = net;
sizes = [inputSize, ...hiddenLayers, outputSize];
}
else if (net.hasOwnProperty('sizes')) {
sizes = net.sizes;
}
else {
throw new Error('unrecognized network');
}
return wrapOuterSVG(neuralNetworkToInnerSVG({
...mergedOptions,
sizes: checkSizes(sizes, inputs.labels),
}), width, height);
}
function checkSizes(sizes, labels) {
if (!sizes) {
throw new Error('sizes not set');
}
if (sizes.some((size) => size < 1)) {
throw new Error('sizes not set correctly');
}
if (labels && labels.length !== sizes[0]) {
throw new Error('not enough labels for inputs');
}
return sizes;
}
const recurrent = {
RNNTimeStep,
LSTMTimeStep,
GRUTimeStep,
RNN,
LSTM,
GRU,
};
const utilities = {
max,
mse: mse$1,
ones: ones$1,
ones2D,
random: random$1,
randomWeight,
randos,
range,
toArray,
DataFormatter,
zeros: zeros$1,
toSVG,
};
exports.CrossValidate = CrossValidate;
exports.FeedForward = FeedForward;
exports.NeuralNetwork = NeuralNetwork;
exports.NeuralNetworkGPU = NeuralNetworkGPU;
exports.Recurrent = Recurrent;
exports.activation = index$1;
exports.layer = layer;
exports.layerTypes = layerTypes;
exports.likely = likely;
exports.lookup = lookup;
exports.praxis = index;
exports.recurrent = recurrent;
exports.utilities = utilities;
Object.defineProperty(exports, '__esModule', { value: true });
})));
//# sourceMappingURL=brain-browser.js.map