UNPKG

@jsmlt/jsmlt

Version:

JavaScript Machine Learning

349 lines (300 loc) 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var MarchingSquaresJS = _interopRequireWildcard(require("marchingsquares")); var Arrays = _interopRequireWildcard(require("../arrays")); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; if (obj != null) { var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } function _iterableToArrayLimit(arr, i) { if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { return; } 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"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 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, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } /** * The decision boundary module calculates decision boundaries for a trained classifier on a * 2-dimensional grid of points. It works by taking a classifier, predicting the output label for * many points on a 2-D grid, and using the [Marching Squares](https://www.npmjs.com/package/marchingsquares) * algorithm to calculate the decision boundaries. * * @example <caption>Calculating the decision boundaries for a classifier</caption> * var Boundaries = new Boundaries(); * * var classIndexBoundaries = boundaries.calculateClassifierDecisionBoundaries( * classifier, // The classifier you've trained * 51, // Number of points on the x- and y-axis (so 51 x 51 = 2,601 points in total) * ); * * // Depending on the classifier used, the output will look something like this: * { * "0":[ // Decision boundaries for class 0 * [ // First (and only, as binary classification) decision boundary or class 0 * [-0.22, -0.96], // Point 1 of decision boundary * [-0.22, -1.00], // Point 2 of decision boundary * // <...23 more elements...> * [-0.24, -0.92], * [-0.26, -0.96] * ] * ], * "1":[ // Decision boundaries for class 1 * [ // First (and only, as binary classification) decision boundary or class 1 * [-0.22, -1.00], // Point 1 of decision boundary * [-0.22, -0.96], // Point 2 of decision boundary * // <...82 more elements...> * [-0.20, -1.00], * [-0.22, -1.00] * ] * ] * } */ var Boundaries = /*#__PURE__*/ function () { /** * Constructor. Initializes boundary object properties. */ function Boundaries() { _classCallCheck(this, Boundaries); /** * Feature list of the grid points. n-by-2 array, where each row consists of the x- and * y-coordinates of a point on the grid. * * @type {Array.<Array.<number>>} */ this.features = null; /** * List of classifier predictions for each grid point. The nth prediction is the prediction for * the nth point in the `features` property. n-dimensional array. * * @type {Array.<mixed>} */ this.predictions = null; /** * Grid of classifier predictions for each grid point. m-by-n array, where the array element at * index (j, i) contains the prediction for the grid point at x-index i and y-index j. This is * simply a 2-dimensional version of the `predictions` property * * @type {Array.<Array.<mixed>>} */ this.predictionsGrid = null; } /** * Determine decision boundaries for a specific classifier. * * @param {jsmlt.Supervised.Classifier} classifier - Classifier for which to generate the * decision boundaries * @param {Array.<number>|number} resolution - Number of points on the x-axis and on the y-axis. * Use integer for the same resolution on the x- and y-axis, and a 2-dimensional array to * specify resolutions per axis * @param {Array.<number>} [gridCoordinates = [-1, -1, 1, 1]] - 4-dimensional array containing, * in order, the x1, y1, x2, and y2-coordinates of the grid * @return {Object.<string, Array.<Array.<Array.<number>>>>} The returned object contains the * boundaries per level (class label). Each boundary then consists of some coordinates (forming * a path), and each coordinate is a 2-dimensional array where the first entry is the * x-coordinate and the second entry is the y-coordinate */ _createClass(Boundaries, [{ key: "calculateClassifierDecisionBoundaries", value: function calculateClassifierDecisionBoundaries(classifier, resolution) { var gridCoordinates = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [-1, -1, 1, 1]; var resolutionX = Array.isArray(resolution) ? resolution[0] : resolution; var resolutionY = Array.isArray(resolution) ? resolution[1] : resolution; // Generate features var features = this.generateFeaturesFromLinSpaceGrid(resolutionX, resolutionY, [gridCoordinates[0], gridCoordinates[2]], [gridCoordinates[1], gridCoordinates[3]]); // Predict labels for all grid points var predictions = classifier.predict(features); var predictionsGrid = Arrays.reshape(predictions, [resolutionX, resolutionY]); // Determine decision boundaries for grid this.features = features; this.predictions = predictions; this.predictionsGrid = predictionsGrid; return this.getGridDecisionBoundaries(predictionsGrid); } /** * Obtain the features list corresponding with the grid coordinates of the last decision * boundaries calculation * * @return {Array.<Array.<number>>} Features for all data points (2-dimensional) */ }, { key: "getFeatures", value: function getFeatures() { return this.features; } /** * Obtain the predictions list for the last decision boundaries calculation * * @return {Array.<number>} List of predicted class labels */ }, { key: "getPredictions", value: function getPredictions() { return this.predictions; } /** * Obtain the grid of predictions for the last decision bundaries calculation * * @return {Array.<Array.<number>>} Predicted class labels for each grid point. m-by-n array for m * rows, n columns */ }, { key: "getPredictionsGrid", value: function getPredictionsGrid() { return this.predictionsGrid; } /** * Determine the decision boundaries for a grid of class predictions * * @param {Array.<Array.<mixed>>} grid - Grid of predictions, an array of row arrays, where each * row array contains the predictions for the cells in that row. For an m x n prediction grid, * each of the m entries of `grid` should have n entries * @return {Object.<string, Array.<Array.<Array.<number>>>>} The returned object contains the * boundaries per level (class label). Each boundary then consists of some coordinates (forming * a path), and each coordinate is a 2-dimensional array where the first entry is the * x-coordinate and the second entry is the y-coordinate */ }, { key: "getGridDecisionBoundaries", value: function getGridDecisionBoundaries(grid) { // Get unique prediction values var levels = Arrays.unique(Arrays.flatten(grid)); // Contours per level var contours = {}; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = levels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var level = _step.value; contours[level] = this.getGridLevelBoundaries(grid, level); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator["return"] != null) { _iterator["return"](); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return contours; } /** * Determine the decision boundaries for a single grid level (class label) * * @param {Array.<Array.<number>>} grid - See this.getGridDecisionBoundaries@param:grid * @param {string} level - Level (class label) to calculate boundaries for * @return {Array.<Array.<Array.<number>>>} Boundaries for this level (class label). See * this.getGridDecisionBoundaries@return */ }, { key: "getGridLevelBoundaries", value: function getGridLevelBoundaries(grid, level) { // Create 1-vs-all grid grid for this level var gridLocal = []; for (var i = 0; i < grid.length; i += 1) { gridLocal.push([]); for (var j = 0; j < grid.length; j += 1) { gridLocal[i].push(grid[i][j] === level ? -2 : -1); } } // Check boundaries to see whether padding should be applied var pad = true; for (var _i = 0; _i < grid.length; _i += 1) { if (gridLocal[_i][0] === -1 || gridLocal[_i][grid.length - 1] === -1 || gridLocal[0][_i] === -1 || gridLocal[grid.length - 1][_i] === -1) { pad = false; } } if (pad) { // Add padding to the grid gridLocal = Arrays.pad(gridLocal, [1, 1], [-1, -1]); } // Calculate contours var contours = MarchingSquaresJS.isoBands(gridLocal, -2, 0.5); // Reshape contours to fit square centered around 0. This has to be done because isoBands // assumes the x- and y-coordinates of the grid points are the array indices. The square is // roughly 2-by-2, but slightly larger to account for the outside boundaries formed because of // the "cliff" padding added earlier. var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = contours[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var contour = _step2.value; var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = contour[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var contourPoint = _step3.value; if (pad) { contourPoint[0] = (contourPoint[0] - 1) / (gridLocal.length - 3) * 2 - 1; contourPoint[1] = (contourPoint[1] - 1) / (gridLocal[0].length - 3) * 2 - 1; } else { contourPoint[0] = contourPoint[0] / (gridLocal.length - 1) * 2 - 1; contourPoint[1] = contourPoint[1] / (gridLocal[0].length - 1) * 2 - 1; } } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) { _iterator3["return"](); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { _iterator2["return"](); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } return contours; } /** * Generate a list of features from a grid of points with linear spacing * * @param {number} pointsX - Number of points on the x-axis * @param {number} pointsY - Number of points on the y-axis * @param {Array.<number>} boundsX - 2-dimensional array of left and right bound on the points on * the x-axis * @param {Array.<number>} boundsY - 2-dimensional array of left and right bound on the points on * the y-axis * @return {Array.Array<number>} (pointsX * pointsY)-by-2 array, containing the coordinates * of all grid points */ }, { key: "generateFeaturesFromLinSpaceGrid", value: function generateFeaturesFromLinSpaceGrid(pointsX, pointsY, boundsX, boundsY) { // Generate vectors with linear spacing var linspaceX = Arrays.linspace(boundsX[0], boundsX[1], pointsX); var linspaceY = Arrays.linspace(boundsY[0], boundsY[1], pointsY); // Create mesh grid with coordinates for each point in the grid var _Arrays$meshGrid = Arrays.meshGrid(linspaceX, linspaceY), _Arrays$meshGrid2 = _slicedToArray(_Arrays$meshGrid, 2), gridX = _Arrays$meshGrid2[0], gridY = _Arrays$meshGrid2[1]; // Generate corresponding vectors of coordinate components var gridXVec = Arrays.flatten(gridX); var gridYVec = Arrays.flatten(gridY); // Join coordinate components per data point, yielding the feature vector return Arrays.concatenate(1, gridXVec, gridYVec); } }]); return Boundaries; }(); exports["default"] = Boundaries; module.exports = exports.default;