UNPKG

@jsmlt/jsmlt

Version:

JavaScript Machine Learning

854 lines (731 loc) 26.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.getShape = getShape; exports.equal = equal; exports.getArrayElement = getArrayElement; exports.slice = slice; exports.subBlock = subBlock; exports.wrapSlice = wrapSlice; exports.setArrayElement = setArrayElement; exports.valueVector = valueVector; exports.full = full; exports.zeros = zeros; exports.fill = fill; exports.linspace = linspace; exports.concatenate = concatenate; exports.repeat = repeat; exports.dot = dot; exports.norm = norm; exports.scale = scale; exports.power = power; exports.abs = abs; exports.sum = sum; exports.internalSum = internalSum; exports.flatten = flatten; exports.reshape = reshape; exports.transpose = transpose; exports.pad = pad; exports.shuffle = shuffle; exports.permuteAxes = permuteAxes; exports.zipWithIndex = zipWithIndex; exports.valueCounts = valueCounts; exports.argFilter = argFilter; exports.argSort = argSort; exports.argMax = argMax; exports.meshGrid = meshGrid; function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } /** * JavaScript array manipulation toolkit. Supports both vectors (1-dimensional array), matrices * (2-dimensional array) and higher-dimensional arrays. The toolkit implements many elementary * array manipulation algorithms, and several algorithms in the domain of linear algebra. */ /** * Find the shape of an array, i.e. the number of elements per dimension of the array. * * @param {Array.<mixed>} A - Arbitrarily nested array to find shape of. * @return {Array.<number>} Array specifying the number of elements per dimension. n-th * element corresponds to the number of elements in the n-th dimension. */ function getShape(A) { if (!Array.isArray(A)) { return []; } var B = getShape(A[0]); B.unshift(A.length); return B; } /** * Deep check whether two arrays are equal: sub-arrays will be traversed, and strong type checking * is enabled. * * @param {Array.<mixed>|mixed} array1 - First array to check or array element to check * @param {Array.<mixed>|mixed} array2 - Second array to check ot array element to check. Will be * checked against first array * @return {boolean} Whether the two arrays are the same */ function equal(array1, array2) { if (!Array.isArray(array1) || !Array.isArray(array2)) { return array1 === array2; } if (array1.length !== array2.length) { return false; } return array1.reduce(function (r, a, i) { return r && equal(a, array2[i]); }, true); } // Array indexing // ----- /** * Get an arbitrary element from an array, using another array to determine the index inside the * first array. * * @param {Array.<mixed>} A - Array to get an element from * @param {Array.<number>} index - Indices to find array element. n-th element corresponds to index * in n-th dimension * @return {mixed} Array element value at index */ function getArrayElement(A, index) { if (index.length === 1) { return A[index]; } return getArrayElement(A[index[0]], index.slice(1)); } /** * Take a slice out of an input array. Negative indices can be used in both the starting indices and * the stopping indices. Negative indices: the negative stopping index is used as the negative * offset relative to the last index in the particular dimension. * * @param {Array.<mixed>} A - Array to extract block from * @param {Array.<number>} start - Array specifying the starting index per dimension. n-th element * corresponds to the number of elements to skip, before extracting the block, in the n-th * dimension. Negative indices are supported. * @param {Array.<number>} stop - Array specifying the index to stop at (exclusive) per dimension. * n-th element corresponds to the stopping index in the n-th dimension. Negative indices are * supported. Use null for unlimited offset. * @return {Array.<mixed>} Array slice extracted from input array */ function slice(A, start, stop) { // Check whether the same number of start and stop indices is supplied if (start.length !== stop.length) { throw new Error('"start" and "stop" must contain the same number of indices.'); } // Check whether the number of dimensions to slice on does not exceed the number of dimensions of // the array if (start.length > getShape(A).length) { throw new Error('The number of start and stop indices must not exceed the number of input array dimensions'); } // Parse start and end indices for highest dimension var parseIndex = function parseIndex(index, allowNull) { if (allowNull && index === null) { return A.length; } if (index < 0) { return A.length + index; } return index; }; var parsedStart = parseIndex(start[0], false); var parsedStop = parseIndex(stop[0], true); // If this is the deepest dimension where we should slice, simply slice the array if (start.length === 1) { return A.slice(parsedStart, parsedStop); } // If it isn't the deepest dimension to slice, slice in the sub-array var subslice = []; for (var i = parsedStart; i < parsedStop; i += 1) { subslice.push(slice(A[i], start.slice(1), stop.slice(1))); } return subslice; } /** * Extract a sub-block of a matrix of a particular shape at a particular position. * * @deprecated Use slice() instead * * @param {Array.<mixed>} A - Array to extract block from * @param {Array.<number>} offset - Array specifying the offset per dimension. n-th element * corresponds to the number of elements to skip, before extracting the block, in the n-th * dimension. * @param {Array.<number>} shape - Array specifying the number of elements per dimension. n-th * element corresponds to the number of elements in the n-th dimension. * @return {Array.<mixed>} Sub-block extracted from array */ function subBlock(A, offset, shape) { if (offset.length === 1) { return A.slice(offset[0], offset[0] + shape[0]); } var subblock = []; for (var i = offset[0]; i < offset[0] + shape[0]; i += 1) { subblock.push(subBlock(A[i], offset.slice(1), shape.slice(1))); } return subblock; } /** * Take a slice out of an array, but wrap around the beginning an end of the array. For example, * if `begin` is -1, the last element of the input array is used as the first output element. * * @param {Array.<mixed>} array - Input array * @param {number} begin Index of first array element * @param {number} end Index of end of slice range (element with this index will itself not be * included in output) * @return {Array.<mixed>} Sliced array */ function wrapSlice(array, begin, end) { var result = []; for (var i = begin; i <= end; i += 1) { var index = (i % array.length + array.length) % array.length; result.push(array[index]); } return result; } /** * Set an arbitrary element in an array, using another array to determine the index inside the * array. * * @param {Array.<mixed>} A - Array to set an element in * @param {Array.<number>} index - Indices to find array element. n-th element corresponds to index * in n-th dimension * @param {mixed} value New element value at index */ function setArrayElement(A, index, value) { var B = A.slice(); B[index[0]] = index.length === 1 ? value : setArrayElement(A[index[0]], index.slice(1), value); return B; } // Array construction // ----- /** * Initialize a vector of a certain length with a specific value in each entry. * * @param {number} n - Number of elements in the vector * @param {mixed} value - Value to initialize entries at * @return Array Vector of n elements of the specified value */ function valueVector(n, value) { return [].concat(_toConsumableArray(Array(n))).map(function () { return value; }); } /** * Initialize an n-dimensional array of a certain value. * * @param {Array.<number>} shape - Array specifying the number of elements per dimension. n-th * element corresponds to the number of elements in the n-th dimension. * @param {mixed} value - Value to fill the array with * @return {Array.<mixed>} Array of the specified with zero in all entries */ function full(shape, value) { if (!Array.isArray(shape)) { return valueVector(shape, value); } if (shape.length === 1) { return valueVector(shape[0], value); } return [].concat(_toConsumableArray(Array(shape[0]))).map(function () { return full(shape.slice(1), value); }); } /** * Initialize an n-dimensional array of zeros. * * @param {Array.<number>} shape - Array specifying the number of elements per dimension. n-th * element corresponds to the number of elements in the n-th dimension. * @return {Array.<mixed>} Array of the specified with zero in all entries */ function zeros(shape) { return full(shape, 0); } /** * Set all entries in an array to a specific value and return the resulting array. Original array * is not modified. * * @param {Array.<mixed>} A - Array of which entries should be changed * @param {mixed} value - Value the array entries should be changed to * @return {Array.<mixed>} Array with modified entries */ function fill(A, value) { return A.map(function (B) { return Array.isArray(B) ? fill(B, value) : value; }); } /** * Generate n points on the interval (a,b), with intervals (b-a)/(n-1). * * @example * var list = linspace(1, 3, 0.5); * // list now contains [1, 1.5, 2, 2.5, 3] * * @param {number} a - Starting point * @param {number} b - Ending point * @param {number} n - Number of points * @return {Array.<number>} Array of evenly spaced points on the interval (a,b) */ function linspace(a, b, n) { var r = []; for (var i = 0; i < n; i += 1) { r.push(a + i * ((b - a) / (n - 1))); } return r; } // Array combination // ----- /** * Concatenate two or more n-dimensional arrays. * * @param {number} axis - Axis to perform concatenation on * @param {...Array.<mixed>} S - Arrays to concatenate. They must have the same shape, except in * the dimension corresponding to axis (the first, by default) * @return {Array} Concatenated array */ function concatenate(axis) { for (var _len = arguments.length, S = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { S[_key - 1] = arguments[_key]; } if (axis === 0) { var _ref; return (_ref = []).concat.apply(_ref, S); } var A = []; var _loop = function _loop(i) { A.push(concatenate.apply(undefined, [axis - 1].concat(_toConsumableArray(S.map(function (APrime) { return APrime[i]; }))))); }; for (var i = 0; i < S[0].length; i += 1) { _loop(i); } return A; } /** * Repeat an array multiple times along an axis. This is essentially one or more concatenations of * an array with itself. * * @param {number} axis - Axis to perform repetition on * @param {number} numRepeats - Number of times to repeat the array * @param {Array.<mixed>} A - Array to repeat * @return {Array.<mixed>} Specified array repeated numRepeats times */ function repeat(axis, numRepeats, A) { var R = A.slice(); for (var i = 0; i < numRepeats - 1; i += 1) { R = concatenate(axis, R, A); } return R; } // Elementary vector calculations // ----- /** * Calculate dot product of two vectors. Vectors should have same size. * * @param {Array.<number>} x - First vector * @param {Array.<number>} y - Second vector * @return {number} Dot product scalar result */ function dot(x, y) { return x.reduce(function (r, a, i) { return r + a * y[i]; }, 0); } /** * Calculate the Euclidian norm of a vector. * * @param {Array.<number>} x - Vector of which to calculate the norm */ function norm(x) { return Math.sqrt(dot(x, x)); } // Element-wise array manipulation // ----- /** * Multiply each element of an array by a scalar (i.e. scale the array). * * @param {Array.<mixed>} A - Array to scale * @param {number} c - Scalar * @return {Array.<mixed>} Scaled array */ function scale(A, c) { return Array.isArray(A) ? A.map(function (B) { return scale(B, c); }) : A * c; } /** * Raise all elements in an array to some power. The power to raise the elements to can either be * the same number for all elements, in which case it should be passed as a number, or an individual * number for all elements, in which case it should be passed as an array of the same shape as the * input array. * * @param {Array.<number>} x - Input array * @param {number|Array.<number>} y - The power to raise all elements to. Either a {number} (all * elements will be raised to this power) or an array (elements in the input array will be raised * to the power specified at the same position in the powers array) * @return {Array.<number>} Array containing the input elements, raised to the specified power */ function power(A, y) { return Array.isArray(A) ? A.map(function (a, i) { return power(a, Array.isArray(y) ? y[i] : y); }) : Math.pow(A, y); } /** * Get a copy of an array with absolute values of the original array entries. * * @param {Array.<mixed>} A Array to get absolute values array from * @return {Array.<mixed>} Array with absolute values */ function abs(A) { return A.map(function (B) { return Array.isArray(B) ? abs(B) : Math.abs(B); }); } /** * Calculate element-wise sum of two or more arrays. Arrays should have the same shape. * * @param {...Array.<mixed>} S - Arrays to concatenate. They must have the same shape * @return {Array.<mixed>} Sum of arrays */ function sum() { for (var _len2 = arguments.length, S = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { S[_key2] = arguments[_key2]; } return S.reduce(function (r, a) { return r.map(function (b, i) { return Array.isArray(b) ? sum(b, a[i]) : b + a[i]; }); }); } // Array calculations // ----- /** * Sum all elements of an array. * * @param {Array.<number>} A - Array * @return {number} Sum of all vector elements */ function internalSum(A) { return A.reduce(function (r, B) { return r + (Array.isArray(B) ? internalSum(B) : B); }, 0); } // Array shape manipulation // ----- /** * Recursively flatten an array. * * @param {Array.<mixed>} A - Array to be flattened * @return {Array.<mixed>} Flattened array */ function flatten(A) { var _ref2; return (_ref2 = []).concat.apply(_ref2, _toConsumableArray(A.map(function (x) { return Array.isArray(x) ? flatten(x) : x; }))); } /** * Reshape an array into a different shape. * * @param {Array.<mixed>} A - Array to reshape * @param {Array.<number>} shape - Array specifying the number of elements per dimension. n-th * element corresponds to the number of elements in the n-th dimension. * @return {Array.<mixed>} Reshaped array */ function reshape(A, shape) { var AValues = flatten(A); var B = zeros(shape); var counters = zeros(shape.length); var counterIndex = counters.length - 1; var counterTotal = 0; var done = false; while (!done) { B = setArrayElement(B, counters, AValues[counterTotal]); // Increment current counter counterIndex = counters.length - 1; counters[counterIndex] += 1; counterTotal += 1; // If the end of the current counter is reached, move to the next counter... while (counters[counterIndex] === shape[counterIndex]) { // ...unless we have reached the end of all counters. In that case, we are done if (counterIndex === 0) { done = true; } counters[counterIndex - 1] += 1; counters[counterIndex] = 0; counterIndex -= 1; } } return B; } /** * Get the transpose of a matrix or vector. * * @param {Array.<Array.<number>>} A - Matrix or vector * @return {Array.<Array.<number>>} Transpose of the matrix */ function transpose(A) { var ATranspose = zeros([A[0].length, A.length]); for (var i = 0; i < A.length; i += 1) { for (var j = 0; j < A[0].length; j += 1) { ATranspose[j][i] = A[i][j]; } } return ATranspose; } /** * Pad an array along one or multiple axes. * * @param {Array.<mixed>} A - Array to be padded * @param {Array.<number> | Array.<Array.<number>>} paddingLengths - Amount of padding for each axis * that should be padded. Each element in this array should be a two-dimensional array, where the * first element specifies the padding at the start (front) of the axis, and the second element * specifies the padding at the end (back) of the axis. The nth element of `paddingLength` * specifies the front and back padding of the nth axis in the `axes` parameter * @param {Array.<number> | Array.<Array.<number>>} paddingValues - The values to pad each axis * with. See the specification of the `paddingLenghts` parameter for the expected structure * @param {Array.<number>} [axes] - Indices of axes to be padded. Defaults to the first n axes, * where n is the number of elements in `paddingLengths` * @return {Array.<mixed>} Padded array */ function pad(A, paddingLengths, paddingValues) { var axes = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; var B = A.slice(); // Use default axes to padded (first n axes where n is the number of axes used in paddingLenghts // and paddingValues) if (!axes.length) { for (var i = 0; i < paddingLengths.length; i += 1) { axes.push(i); } } // Pad all specified axes for (var _i = 0; _i < axes.length; _i += 1) { var axis = axes[_i]; var currentShape = getShape(B); // Determine padding lengths var lengthFront = 0; var lengthBack = 0; if (Array.isArray(paddingLengths[_i])) { lengthFront = paddingLengths[_i][0]; lengthBack = paddingLengths[_i][1]; } else { lengthFront = paddingLengths[_i]; lengthBack = paddingLengths[_i]; } // Determine padding values var valueFront = 0; var valueBack = 0; if (Array.isArray(paddingValues[_i])) { valueFront = paddingValues[_i][0]; valueBack = paddingValues[_i][1]; } else { valueFront = paddingValues[_i]; valueBack = paddingValues[_i]; } // Shape of padding for front and back var shapeFront = currentShape.slice(); var shapeBack = currentShape.slice(); shapeFront[axis] = lengthFront; shapeBack[axis] = lengthBack; // Create padding blocks var paddingFront = full(shapeFront, valueFront); var paddingBack = full(shapeBack, valueBack); B = concatenate(axis, paddingFront, B, paddingBack); } return B; } /** * Randomly shuffle multiple arrays in the primary (first) axis. All input arrays are shuffled * simultaneously, i.e., if an element with index i is moved to index j for the first array, the * the same happens for the second (and third, etc.) array. * * @param {...Array.<mixed>} S - Arrays to shuffle. They must have the same size in the primary axis * @return {Array.<Array.<mixed>>} Shuffled matrices */ function shuffle() { for (var _len3 = arguments.length, S = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { S[_key3] = arguments[_key3]; } // Copy matrices var SPermutated = S.map(function (A) { return A.slice(); }); // Number of remaining rows var remainingRows = SPermutated[0].length; while (remainingRows > 0) { // Select a random element from the remaining rows and swap it with the first element that has // not yet been assigned var swapIndex = Math.floor(Math.random() * remainingRows); for (var i = 0; i < SPermutated.length; i += 1) { var tmpRow = SPermutated[i][remainingRows - 1]; SPermutated[i][remainingRows - 1] = SPermutated[i][swapIndex]; SPermutated[i][swapIndex] = tmpRow; } remainingRows -= 1; } return SPermutated; } /** * Permute the axes of an input array. In other words, you can interchange the axes of an * n-dimensional input array. * * @param {Array.<mixed>} A - Array of which the axes should be permuted * @param {Array.<number>} newAxes - For the i-th element of this array, specify the index of the * axis in the original array that should be used * @return {Array.<mixed>} Array with permuted axes */ function permuteAxes(A, newAxes) { // Shape of the input array var oldShape = getShape(A); // Initialize the output array as all-zeros var newShape = newAxes.map(function (x) { return oldShape[x]; }); var APermuted = zeros(newShape); // Axes are permuted by, for all old array indices, copying the value at that position to the // corresponding new position in the output array. This is a really naive algorithm, and could // be optimized. The function below iterates over all indices in the i-th original array // dimension, and is recursively called until the last dimension is reached var permuteAxesStep = function permuteAxesStep(index, step) { var _loop2 = function _loop2(i) { var oldIndex = [].concat(_toConsumableArray(index), [i]); if (step < oldShape.length - 1) { permuteAxesStep(oldIndex, step + 1); } else { var newIndex = newAxes.map(function (axis) { return oldIndex[axis]; }); APermuted = setArrayElement(APermuted, newIndex, getArrayElement(A, oldIndex)); } }; for (var i = 0; i < oldShape[step]; i += 1) { _loop2(i); } }; permuteAxesStep([], 0); return APermuted; } // Traditional array functionality // ----- /** * From an input array, create a new array where each element is comprised of a 2-dimensional array * where the first element is the original array entry and the second element is its index * * @param {Array.<mixed>} array Input array * @return {Array.<Array.<mixed>>} Output array */ function zipWithIndex(array) { return array.map(function (x, i) { return [x, i]; }); } /** * Count the occurrences of the unique values in an array * * @param {Array.<mixed>} array Input array * @return {Array.<Array.<mixed>>} Array where each element is a 2-dimensional array. In these 2D * arrays, the first element corresponds to the unique array value, and the second elements * corresponds to the number of times this value occurs in the original array */ function valueCounts(array) { // Create map of counts per array value var counts = []; var valuesIndex = {}; var numUniqueValues = 0; array.forEach(function (x) { if (typeof valuesIndex[x] === 'undefined') { valuesIndex[x] = numUniqueValues; counts.push([x, 0]); numUniqueValues += 1; } counts[valuesIndex[x]][1] += 1; }); return counts; } /** * Filter an array and return the array indices where the filter was matched. Corresponds to * JavaScript's native Array.filter(), but instead of returning the elements that match the filter * criteria, it returns the indices of the elements matching the filter. * * @param {Array.<mixed>} array - Array to be filtered * @param {function(element: mixed, !index: Number): boolean} callback - Callback function to be * used for filtering. This function takes an array element and possibly its index as its input * and should return true when the index should be used (filtered) and false when it shouldn't * @return {Array.<Number>} Array of array indices in the original array where the array element * matches the filter */ function argFilter(array, callback) { return zipWithIndex(array) // Filter zipped elements + indices where the element matches the filter .filter(function (x) { return callback(x[0], x[1]); }) // Map the zipped elements to the indices .map(function (x) { return x[1]; }); } /** * Sort an array and return the array indices of the sorted elements. Corresponds to JavaScript's * native Array.sort(), but instead of returning the sorted elements, it returns the indices of the * sorted elements. * * @param {Array.<mixed>} array - Array to be sorted * @param {function(a: mixed, b: mixed): Number} [compareFunction = null] - Callback function be * used for sorting. This function takes two array elements and returns an integer indicating * sort order of the two elements a and b: * < 0 : a before b * == 0 : leave order of a and b unchanged with respect to each other * > 0 : b after a * Defaults to numeric sorting. * @return {Array.<Number>} Array of array indices such that the elements corresponding with these * indices in the original array are sorted */ function argSort(array, callback) { var useCallback = typeof callback === 'function' ? callback : function (a, b) { return a - b; }; return zipWithIndex(array) // Sort zipped elements + indices by element value .sort(function (a, b) { return useCallback(a[0], b[0]); }) // Map the zipped elements to the indices .map(function (x) { return x[1]; }); } /** * Get array key corresponding to largest element in the array. * * @param {Array.<number>} array Input array * @return {number} Index of array element with largest value */ function argMax(array) { if (array.length === 0) { return null; } return zipWithIndex(array).reduce(function (r, x) { return x[0] > r[0] ? x : r; })[1]; } // Miscellaneous // ----- /** * Generate a mesh grid, i.e. two m-by-n arrays where m=|y| and n=|x|, from two vectors. The mesh * grid generates two grids, where the first grid repeats x row-wise m times, and the second grid * repeats y column-wise n times. Can be used to generate coordinate grids. * * Example input: x=[0, 1, 2], y=[2, 4, 6, 8] * Corresponding output: * matrix 1: [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]] * matrix 2: [[2, 2, 2], [4, 4, 4], [6, 6, 6], [8, 8, 8]] * * @param {Array.<number>} x - Vector of x-coordinates * @param {Array.<number>} y - Vector of y-coordinates * @return {Array.<Array.<Array.<number>>>} Two-dimensional array containing the x-grid as the first * element, and the y-grid as the second element */ function meshGrid(x, y) { var gridX = transpose(repeat(1, y.length, x)); var gridY = repeat(1, x.length, y); return [gridX, gridY]; }