@jsmlt/jsmlt
Version:
JavaScript Machine Learning
349 lines (300 loc) • 15.2 kB
JavaScript
;
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;