@jsmlt/jsmlt
Version:
JavaScript Machine Learning
364 lines (304 loc) • 14.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
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; }; }(); // External dependencies
// Internal dependencies
var _marchingsquares = require('marchingsquares');
var MarchingSquaresJS = _interopRequireWildcard(_marchingsquares);
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"); } }
/**
* 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 = 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}
*/
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}
*/
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.
*
* @type {Array}
*/
this.predictionsGrid = null;
}
/**
* Determine decision boundaries for a specific classifier.
*
* @param {jsmlt.Classification.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 = Array.from(new Set(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) {
_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) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_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'];