pi-emergence
Version:
Various emergent phenomena
254 lines (234 loc) • 14.4 kB
JavaScript
"use strict";
var _activationFunction = _interopRequireDefault(require("../components/activation-function.js"));
var _neuronMatrix = _interopRequireDefault(require("../components/neuron-matrix.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct.bind(); } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
/**
* @fileoverview MatrixNeuroApp
* @version 1.0.0
* @description
* Fairly lightweight feed forward neural network using matrices
* Implementation using matrices (the most common and efficient way it's done) to make training faster and more understandable from a math perspective.
* The trained weights can be exported to a JSON file and/or used in app.neuro.js for visuals.
* This can be used for classification or regression problems.
* Most of the error checking in the training can (should?) probably be removed for performance reasons,
* but we care more about the educational component of this project than the performance
*
* @requires activation-function.js
* @requires neuron-matrix.js
* @note: If you're using pure JavaScript, the NeuroMatrix (neuron-matrix.js) file and the ActivationFunction file (activation-functions.js) in that order, should be included before this file
*/
var MatrixNeuroApp = /*#__PURE__*/function () {
function MatrixNeuroApp() {
var _args;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_classCallCheck(this, MatrixNeuroApp);
if (((_args = args) === null || _args === void 0 ? void 0 : _args.length) === 1 && Array.isArray(args[0])) args = args[0];
if (!args || args.length < 3) throw new Error("MatrixNeuroApp constructor requires at least 3 arguments (" + args.length + ")");
this.neuronCounts = [];
// Loop through all args and create layers with respective number of neurons
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (typeof arg !== "number") throw new Error("MatrixNeuroApp constructor arguments must be numbers. They reporesent the number of neurons per layer (excluding bias)");
this.neuronCounts.push(arg);
}
this.squashFunction = new _activationFunction["default"](_activationFunction["default"].sigmoid, _activationFunction["default"].sigmoidPrime, "Sigmoid");
// Try different ones, based on the problem. 0.075 feels like a decent default
this.learningRate = MatrixNeuroApp.defaultLearningRate;
this.biases = [];
this.messages = []; // Keep some logs
this.weightMatrices = [];
this.activationValues = [];
this.layerCount = this.neuronCounts.length;
// Attach biases to all layers except input layer
for (var _i = 1; _i < this.layerCount; _i++) {
var rowCount = this.neuronCounts[_i];
var columnCount = this.neuronCounts[_i - 1];
this.weightMatrices.push(new _neuronMatrix["default"](rowCount, columnCount).randomizeWeights());
this.biases.push(new _neuronMatrix["default"](this.neuronCounts[_i], 1).randomizeWeights());
}
}
_createClass(MatrixNeuroApp, [{
key: "toJson",
value: function toJson() {
return {
learningRate: this.learningRate,
squashFunction: this.squashFunction.name,
neuronCounts: this.neuronCounts,
biases: this.biases.map(function (bias) {
return bias.toList();
}),
weights: this.weightMatrices.map(function (weightMatrix) {
return weightMatrix.toList();
})
};
}
}, {
key: "test",
value: function test(inputValues) {
var print = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
return this.execute(inputValues, print).outputs.toList();
}
/**
* Executes the network with the given input values
* @param {[number]} - The input values
* @returns {object} - The inputs (echoed), activations, and outputs (results)
*/
}, {
key: "execute",
value: function execute(inputValues) {
var print = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var inputs = _neuronMatrix["default"].fromList(inputValues);
var iterator = inputs;
var layerIndex;
var activationValues = [];
if (print === true) {
console.log("Inputs: ");
console.table(inputValues);
}
for (layerIndex = 0; layerIndex < this.layerCount - 2; layerIndex++) {
var layerBias = this.biases[layerIndex];
var weights = this.weightMatrices[layerIndex];
var hiddenActivations = _neuronMatrix["default"].mult(weights, iterator.copy());
hiddenActivations.add(layerBias); // Sum up
hiddenActivations.setMatrixValues(this.squashFunction.squash); // Activate
activationValues.push(hiddenActivations);
iterator = hiddenActivations;
}
var output = _neuronMatrix["default"].mult(this.weightMatrices[layerIndex], iterator.copy());
output.add(this.biases[layerIndex]); // Sum up
output.setMatrixValues(this.squashFunction.squash); // Activate
activationValues.push(output);
this.activationValues = activationValues;
if (print) {
console.log("Outputs:");
console.table(output.items);
}
return {
inputs: inputs,
outputs: output,
activations: iterator
};
}
/*** Training and testing */
/**
* Sets the learning rate of the network. I.e., the "steps" it takes during back propagation
* @param {number} learningRate - The learning rate of the network. Defaults to 0.1
* @returns {MatrixNeuroApp} - So we can chain methods together
*/
}, {
key: "setLearningRate",
value: function setLearningRate() {
var learningRate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0.1;
this.learningRate = learningRate;
return this;
}
/**
* Takes the summed up weight x input + bias and squashes it between 0 and 1 (or some other small range)
* @param {ActivationFunction} squashFunction - The squashing function to use. Defaults to sigmoid
* @returns {MatrixNeuroApp} - So we can chain methods together
*/
}, {
key: "setActivationFunction",
value: function setActivationFunction() {
var squashFunction = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
this.squashFunction = squashFunction || this.squashFunction;
return this;
}
/**
* Trains a single round of the network. This method should be run a crap-load of times, with different inputs/outputs to train the network.
* Basically the magic of machine learning.
* @param {[number]} inputs - Input values of the training round
* @param {[number]} expectedOutputs - Expected output values of the training round
*/
}, {
key: "train",
value: function train(inputs, expectedOutputs) {
// Step 1. Feed Forward
// Generating the output's output
var result = this.execute(inputs);
var outputs = result.outputs;
// Convert list of numbers to Matrix
var targets = _neuronMatrix["default"].fromList(expectedOutputs);
// 1. Calc errors
// 2. Calc gradients
// 3. Calc deltas
// 4. Update weights
// Basic cost function: (target - output)
// We usually use the mean squared error function, which is the average of the squared errors, but we can use this for now
var errors = _neuronMatrix["default"].sub(targets, outputs);
// Calculate gradient
var gradients = _neuronMatrix["default"].setMatrixValues(outputs, this.squashFunction.getPartialDerivative);
gradients.mult(errors);
gradients.mult(this.learningRate);
// We handle the last layer separately
var activations = this.activationValues[this.activationValues.length - 2]; // 2nd to last layer of neurons. We use this as a cursor/hold-over
var weightsIndex = this.weightMatrices.length - 1;
while (weightsIndex > 0) {
// Calculate deltas and adjust accordingly
var weights = this.weightMatrices[weightsIndex];
var weightDeltas = _neuronMatrix["default"].mult(gradients, _neuronMatrix["default"].transpose(activations));
var weightErrors = _neuronMatrix["default"].mult(_neuronMatrix["default"].transpose(weights), errors);
weights.add(weightDeltas); // Update weights - Glorious.
// Adjust the bias by its deltas (which is just the gradients because bias is always 1.0 [for now])
this.biases[weightsIndex].add(gradients);
// Calculate next (backward) gradient and rinse/repeat
gradients = _neuronMatrix["default"].setMatrixValues(activations, this.squashFunction.getPartialDerivative);
gradients.mult(weightErrors);
gradients.mult(this.learningRate);
errors = weightErrors; // Cursor (value is used in the next loop)
weightsIndex--;
activations = this.activationValues[weightsIndex - 1]; // Cursor.
}
// Final updates
var lastDeltas = _neuronMatrix["default"].mult(gradients, _neuronMatrix["default"].transpose(result.inputs));
this.weightMatrices[0].add(lastDeltas);
this.biases[0].add(gradients);
}
}], [{
key: "fromJson",
value: function fromJson(json) {
if (typeof json === "string") json = JSON.parse(json);
if (_typeof(json) !== "object") throw new Error("Invalid json of type '" + _typeof(json).toString() + "' passed to MatrixNeuroApp.fromJson");
var learningRate = json.learningRate;
var squashFunction = _activationFunction["default"].fromName(json.squashFunction);
var neuronCounts = json.neuronCounts;
var biases = json.biases.map(function (bias) {
return _neuronMatrix["default"].fromList(bias);
});
var weights = json.weights.map(function (weight) {
return _neuronMatrix["default"].fromList(weight);
});
var app = _construct(MatrixNeuroApp, _toConsumableArray(neuronCounts));
app.learningRate = learningRate;
app.squashFunction = squashFunction;
app.biases = biases;
app.weightMatrices = weights;
return app;
}
}]);
return MatrixNeuroApp;
}();
_defineProperty(MatrixNeuroApp, "defaultLearningRate", 0.075);
if (typeof module === 'undefined') {
console.log("Can't export. Running MatrixNeuroApp in-browser");
} else {
module.exports = MatrixNeuroApp;
}