UNPKG

herta

Version:

Advanced mathematics framework for scientific, engineering, and financial applications

467 lines (400 loc) 12.6 kB
/** * Machine Learning Primitives module for herta.js * Provides fundamental operations for machine learning */ const matrix = require('../core/matrix'); const arithmetic = require('../core/arithmetic'); const machineLearning = {}; /** * Sigmoid activation function * @param {number|Array} x - Input value or array * @returns {number|Array} - Output after sigmoid activation */ machineLearning.sigmoid = function (x) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.sigmoid(val) : 1 / (1 + Math.exp(-val)))); } return 1 / (1 + Math.exp(-x)); }; /** * Derivative of sigmoid function * @param {number|Array} x - Input value or array * @returns {number|Array} - Derivative of sigmoid at x */ machineLearning.sigmoidDerivative = function (x) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.sigmoidDerivative(val) : machineLearning.sigmoid(val) * (1 - machineLearning.sigmoid(val)))); } const s = machineLearning.sigmoid(x); return s * (1 - s); }; /** * ReLU activation function * @param {number|Array} x - Input value or array * @returns {number|Array} - Output after ReLU activation */ machineLearning.relu = function (x) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.relu(val) : Math.max(0, val))); } return Math.max(0, x); }; /** * Derivative of ReLU function * @param {number|Array} x - Input value or array * @returns {number|Array} - Derivative of ReLU at x */ machineLearning.reluDerivative = function (x) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.reluDerivative(val) : val > 0 ? 1 : 0)); } return x > 0 ? 1 : 0; }; /** * Leaky ReLU activation function * @param {number|Array} x - Input value or array * @param {number} [alpha=0.01] - Slope for negative values * @returns {number|Array} - Output after Leaky ReLU activation */ machineLearning.leakyRelu = function (x, alpha = 0.01) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.leakyRelu(val, alpha) : val > 0 ? val : alpha * val)); } return x > 0 ? x : alpha * x; }; /** * Derivative of Leaky ReLU function * @param {number|Array} x - Input value or array * @param {number} [alpha=0.01] - Slope for negative values * @returns {number|Array} - Derivative of Leaky ReLU at x */ machineLearning.leakyReluDerivative = function (x, alpha = 0.01) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.leakyReluDerivative(val, alpha) : val > 0 ? 1 : alpha)); } return x > 0 ? 1 : alpha; }; /** * Tanh activation function * @param {number|Array} x - Input value or array * @returns {number|Array} - Output after tanh activation */ machineLearning.tanh = function (x) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.tanh(val) : Math.tanh(val))); } return Math.tanh(x); }; /** * Derivative of tanh function * @param {number|Array} x - Input value or array * @returns {number|Array} - Derivative of tanh at x */ machineLearning.tanhDerivative = function (x) { if (Array.isArray(x)) { return x.map((val) => (Array.isArray(val) ? machineLearning.tanhDerivative(val) : 1 - Math.tanh(val) ** 2)); } return 1 - Math.tanh(x) ** 2; }; /** * Softmax activation function * @param {Array} x - Input array * @returns {Array} - Output after softmax activation */ machineLearning.softmax = function (x) { if (!Array.isArray(x)) { throw new Error('Softmax requires an array input'); } // For numerical stability, subtract the max value const maxVal = Math.max(...x); const expValues = x.map((val) => Math.exp(val - maxVal)); const sumExp = expValues.reduce((acc, val) => acc + val, 0); return expValues.map((val) => val / sumExp); }; /** * Mean Squared Error loss function * @param {Array} predicted - Predicted values * @param {Array} actual - Actual values * @returns {number} - MSE loss */ machineLearning.meanSquaredError = function (predicted, actual) { if (predicted.length !== actual.length) { throw new Error('Predicted and actual arrays must have the same length'); } let sum = 0; for (let i = 0; i < predicted.length; i++) { sum += (predicted[i] - actual[i]) ** 2; } return sum / predicted.length; }; /** * Binary Cross Entropy loss function * @param {Array} predicted - Predicted probabilities * @param {Array} actual - Actual binary labels * @returns {number} - Binary cross entropy loss */ machineLearning.binaryCrossEntropy = function (predicted, actual) { if (predicted.length !== actual.length) { throw new Error('Predicted and actual arrays must have the same length'); } let sum = 0; for (let i = 0; i < predicted.length; i++) { // Clip to avoid log(0) const p = Math.max(Math.min(predicted[i], 1 - 1e-15), 1e-15); sum += actual[i] * Math.log(p) + (1 - actual[i]) * Math.log(1 - p); } return -sum / predicted.length; }; /** * Categorical Cross Entropy loss function * @param {Array} predicted - Predicted class probabilities * @param {Array} actual - Actual one-hot encoded labels * @returns {number} - Categorical cross entropy loss */ machineLearning.categoricalCrossEntropy = function (predicted, actual) { if (predicted.length !== actual.length) { throw new Error('Predicted and actual arrays must have the same length'); } let sum = 0; for (let i = 0; i < predicted.length; i++) { // Clip to avoid log(0) const p = Math.max(predicted[i], 1e-15); sum += actual[i] * Math.log(p); } return -sum; }; /** * Z-score standardization (zero mean, unit variance) * @param {Array} data - Input data array * @returns {Object} - Standardized data and parameters */ machineLearning.standardize = function (data) { const mean = data.reduce((acc, val) => acc + val, 0) / data.length; const variance = data.reduce((acc, val) => acc + (val - mean) ** 2, 0) / data.length; const stdDev = Math.sqrt(variance); const standardized = data.map((val) => (val - mean) / stdDev); return { standardized, mean, stdDev }; }; /** * Min-Max scaling (normalize to [0, 1] range) * @param {Array} data - Input data array * @returns {Object} - Normalized data and parameters */ machineLearning.minMaxScale = function (data) { const min = Math.min(...data); const max = Math.max(...data); const range = max - min; if (range === 0) { return { normalized: data.map(() => 0.5), min, max }; } const normalized = data.map((val) => (val - min) / range); return { normalized, min, max }; }; /** * Principal Component Analysis (PCA) * @param {Array} X - Data matrix (each row is a sample, each column a feature) * @param {number} numComponents - Number of principal components to return * @returns {Object} - PCA results */ machineLearning.pca = function (X, numComponents) { if (!Array.isArray(X) || !Array.isArray(X[0])) { throw new Error('X must be a 2D array'); } const n = X.length; // Number of samples const d = X[0].length; // Number of features // Calculate mean of each feature const means = Array(d).fill(0); for (let i = 0; i < n; i++) { for (let j = 0; j < d; j++) { means[j] += X[i][j] / n; } } // Center the data const centered = X.map((row) => row.map((val, j) => val - means[j])); // Calculate covariance matrix const covariance = Array(d).fill().map(() => Array(d).fill(0)); for (let i = 0; i < d; i++) { for (let j = 0; j <= i; j++) { let sum = 0; for (let k = 0; k < n; k++) { sum += centered[k][i] * centered[k][j]; } covariance[i][j] = sum / (n - 1); covariance[j][i] = covariance[i][j]; // Covariance matrix is symmetric } } // Get eigenvalues and eigenvectors (using power iteration for simplicity) // This is a simplified approach, a production implementation would use more robust methods function powerIteration(A, numIterations = 100) { const n = A.length; let x = Array(n).fill().map(() => Math.random()); // Normalize const norm = Math.sqrt(x.reduce((sum, val) => sum + val * val, 0)); x = x.map((val) => val / norm); for (let i = 0; i < numIterations; i++) { // Multiply A*x const Ax = Array(n).fill(0); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { Ax[i] += A[i][j] * x[j]; } } // Normalize const norm = Math.sqrt(Ax.reduce((sum, val) => sum + val * val, 0)); x = Ax.map((val) => val / norm); } // Calculate eigenvalue (Rayleigh quotient) let eigenvalue = 0; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { eigenvalue += x[i] * A[i][j] * x[j]; } } return { eigenvalue, eigenvector: x }; } // Find top eigenvalues/vectors const components = []; const remainingCovariance = [...covariance.map((row) => [...row])]; for (let i = 0; i < numComponents; i++) { const { eigenvalue, eigenvector } = powerIteration(remainingCovariance); components.push({ eigenvalue, eigenvector }); // Deflate covariance matrix for (let j = 0; j < d; j++) { for (let k = 0; k < d; k++) { remainingCovariance[j][k] -= eigenvalue * eigenvector[j] * eigenvector[k]; } } } // Sort by eigenvalue components.sort((a, b) => b.eigenvalue - a.eigenvalue); // Transform data const transformedData = centered.map((x) => components.map((comp) => comp.eigenvector.reduce((sum, v, j) => sum + v * x[j], 0))); return { transformedData, components: components.map((c) => c.eigenvector), explainedVariance: components.map((c) => c.eigenvalue), means }; }; /** * K-means clustering algorithm * @param {Array} data - Input data points (array of arrays) * @param {number} k - Number of clusters * @param {Object} [options] - Options for the algorithm * @returns {Object} - Clustering result */ machineLearning.kmeans = function (data, k, options = {}) { const maxIterations = options.maxIterations || 100; const tolerance = options.tolerance || 1e-4; if (!Array.isArray(data) || !Array.isArray(data[0])) { throw new Error('Data must be a 2D array'); } const n = data.length; // Number of data points const d = data[0].length; // Number of dimensions // Initialize centroids randomly let centroids = Array(k).fill().map(() => { const randomIndex = Math.floor(Math.random() * n); return [...data[randomIndex]]; }); // Helper function to calculate distance function distance(a, b) { let sum = 0; for (let i = 0; i < d; i++) { sum += (a[i] - b[i]) ** 2; } return Math.sqrt(sum); } let labels = Array(n); let iterations = 0; let converged = false; while (iterations < maxIterations && !converged) { // Assign points to nearest centroid const newLabels = Array(n); for (let i = 0; i < n; i++) { let minDist = Infinity; let minIndex = 0; for (let j = 0; j < k; j++) { const dist = distance(data[i], centroids[j]); if (dist < minDist) { minDist = dist; minIndex = j; } } newLabels[i] = minIndex; } // Check convergence if (iterations > 0) { let changed = 0; for (let i = 0; i < n; i++) { if (newLabels[i] !== labels[i]) { changed++; } } if (changed / n < tolerance) { converged = true; } } labels = newLabels; // Update centroids const newCentroids = Array(k).fill().map(() => Array(d).fill(0)); const counts = Array(k).fill(0); for (let i = 0; i < n; i++) { const cluster = labels[i]; counts[cluster]++; for (let j = 0; j < d; j++) { newCentroids[cluster][j] += data[i][j]; } } // Calculate mean for each centroid for (let i = 0; i < k; i++) { if (counts[i] > 0) { for (let j = 0; j < d; j++) { newCentroids[i][j] /= counts[i]; } } } centroids = newCentroids; iterations++; } // Calculate inertia (sum of squared distances to assigned centroids) let inertia = 0; for (let i = 0; i < n; i++) { inertia += distance(data[i], centroids[labels[i]]) ** 2; } return { labels, centroids, inertia, iterations, converged }; }; module.exports = machineLearning;