UNPKG

@jsmlt/jsmlt

Version:

JavaScript Machine Learning

264 lines (198 loc) 10.3 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.BinaryLogisticRegression = undefined; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _createClass = 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, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _base = require('../base'); var _arrays = require('../../arrays'); var Arrays = _interopRequireWildcard(_arrays); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // Internal dependencies /** * Calculate the logit function for an input * * @param {number} x - Input number * @return {number} Output of logit function applied on input */ function sigmoid(x) { return 1 / (1 + Math.exp(-x)); } /** * Logistic Regression learner for binary classification problem. */ var BinaryLogisticRegression = exports.BinaryLogisticRegression = function (_Classifier) { _inherits(BinaryLogisticRegression, _Classifier); function BinaryLogisticRegression() { _classCallCheck(this, BinaryLogisticRegression); return _possibleConstructorReturn(this, (BinaryLogisticRegression.__proto__ || Object.getPrototypeOf(BinaryLogisticRegression)).apply(this, arguments)); } _createClass(BinaryLogisticRegression, [{ key: 'train', /** * @see {Classifier#train} */ value: function train(X, y) { // Weights increment to check for convergence this.weightsIncrement = Infinity; // Initialize weights vector to zero. Here, the number of weights equals one plus the number of // features, where the first weight (w0) is the weight used for the bias. this.weights = Arrays.zeros(1 + X[0].length); // Iteration index var epoch = 0; // A single iteration of this loop corresponds to a single iteration of training all data // points in the data set while (true) { var weightsIncrement = this.trainIteration(X, y); if (weightsIncrement.reduce(function (r, a) { return r + Math.abs(a); }, 0) < 0.0001 || epoch > 100) { break; } epoch += 1; } } /** * Train the classifier for a single iteration on the stored training data. * * @param {Array.<Array.<number>>} X - Features per data point * @param {Array.<mixed>} y Class labels per data point */ }, { key: 'trainIteration', value: function trainIteration(X, y) { // Initialize the weights increment vector, which is used to increment the weights in each // iteration after the calculations are done. var weightsIncrement = Arrays.zeros(this.weights.length); // Shuffle data points var _Arrays$shuffle = Arrays.shuffle(X, y), _Arrays$shuffle2 = _slicedToArray(_Arrays$shuffle, 2), XUse = _Arrays$shuffle2[0], yUse = _Arrays$shuffle2[1]; // Loop over all datapoints for (var i = 0; i < XUse.length; i += 1) { // Copy features vector so it is not changed in the datapoint var augmentedFeatures = XUse[i].slice(); // Add feature with value 1 at the beginning of the feature vector to correpond with the // bias weight augmentedFeatures.unshift(1); // Calculate weights increment weightsIncrement = Arrays.sum(weightsIncrement, Arrays.scale(augmentedFeatures, yUse[i] - sigmoid(Arrays.dot(this.weights, augmentedFeatures)))); } // Take average of all weight increments this.weightsIncrement = Arrays.scale(weightsIncrement, 0.5); this.weights = Arrays.sum(this.weights, this.weightsIncrement); return weightsIncrement; } /** * Check whether training has convergence when using iterative training using trainIteration. * * @return {boolean} Whether the algorithm has converged */ }, { key: 'checkConvergence', value: function checkConvergence() { return Arrays.internalSum(Arrays.abs(this.weightsIncrement)) < 0.0001; } /** * Make a prediction for a data set. * * @param {Array.Array.<number>} features - Features for each data point * @param {Object} [optionsUser] User-defined options * @param {string} [optionsUser.output = 'classLabels'] Output for predictions. Either * "classLabels" (default, output predicted class label), "raw", or "normalized" (both returning * the sigmoid of the dot product of the feature vector and unit-length weights) * @return {Array.<number>} Predictions. Output dependent on options.output, defaults to class * labels */ }, { key: 'predict', value: function predict(features) { var optionsUser = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // Options var optionsDefault = { output: 'classLabels' // 'classLabels', 'normalized' or 'raw' }; var options = _extends({}, optionsDefault, optionsUser); // Probabilistic predictions var predictionsProba = this.predictProba(features); if (options.output === 'raw' || options.output === 'normalized') { // Probability of positive class is the raw output return predictionsProba.map(function (x) { return x[1]; }); } // Calculate binary predictions var predictions = []; for (var i = 0; i < predictionsProba.length; i += 1) { predictions.push(predictionsProba[i][1] >= 0.5 ? 1 : 0); } return predictions; } /** * Make a probabilistic prediction for a data set. * * @param {Array.Array.<number>} features - Features for each data point * @return {Array.Array.<number>} Probability predictions. Each array element contains the * probability of the negative (0) class in the first element, and the probability of the * positive (1) class in the second element */ }, { key: 'predictProba', value: function predictProba(features) { // Predictions var predictions = []; // Normalization factor for normalized output var weightsMagnitude = Math.sqrt(Arrays.dot(this.weights, this.weights)); // Loop over all datapoints for (var i = 0; i < features.length; i += 1) { // Copy features vector so it is not changed in the datapoint var augmentedFeatures = features[i].slice(); // Add feature with value 1 at the beginning of the feature vector to correpond with the // bias weight augmentedFeatures.unshift(1); // Calculate probability of positive class var output = Arrays.dot(augmentedFeatures, this.weights); var posProb = sigmoid(output / weightsMagnitude); // Add pair of probabilities to list predictions.push([1 - posProb, posProb]); } return predictions; } }]); return BinaryLogisticRegression; }(_base.Classifier); /** * Logistic Regression learner for 2 or more classes. Uses 1-vs-all classification. */ var LogisticRegression = function (_OneVsAllClassifier) { _inherits(LogisticRegression, _OneVsAllClassifier); function LogisticRegression() { _classCallCheck(this, LogisticRegression); return _possibleConstructorReturn(this, (LogisticRegression.__proto__ || Object.getPrototypeOf(LogisticRegression)).apply(this, arguments)); } _createClass(LogisticRegression, [{ key: 'createClassifier', /** * @see {@link OneVsAll#createClassifier} */ value: function createClassifier(classIndex) { return new BinaryLogisticRegression(); } /** * @see {@link Classifier#train} */ }, { key: 'train', value: function train(X, y) { this.createClassifiers(y); this.trainBatch(X, y); } }]); return LogisticRegression; }(_base.OneVsAllClassifier); exports.default = LogisticRegression;