UNPKG

@astermind/astermind-elm

Version:

JavaScript Extreme Learning Machine (ELM) library for browser and Node.js.

1,304 lines (1,297 loc) 225 kB
// © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 // Matrix.ts — tolerant, safe helpers with dimension checks and stable ops class DimError extends Error { constructor(msg) { super(msg); this.name = 'DimError'; } } const EPS$4 = 1e-12; /* ===================== Array-like coercion helpers ===================== */ // ✅ Narrow to ArrayLike<number> so numeric indexing is allowed function isArrayLikeRow(row) { return row != null && typeof row.length === 'number'; } /** * Coerce any 2D array-like into a strict rectangular number[][] * - If width is not provided, infer from the first row's length * - Pads/truncates to width * - Non-finite values become 0 */ function ensureRectNumber2D(M, width, name = 'matrix') { if (!M || typeof M.length !== 'number') { throw new DimError(`${name} must be a non-empty 2D array`); } const rows = Array.from(M); if (rows.length === 0) throw new DimError(`${name} is empty`); const first = rows[0]; if (!isArrayLikeRow(first)) throw new DimError(`${name} row 0 missing/invalid`); const C = ((width !== null && width !== void 0 ? width : first.length) | 0); if (C <= 0) throw new DimError(`${name} has zero width`); const out = new Array(rows.length); for (let r = 0; r < rows.length; r++) { const src = rows[r]; const rr = new Array(C); if (isArrayLikeRow(src)) { const sr = src; // ✅ typed for (let c = 0; c < C; c++) { const v = sr[c]; rr[c] = Number.isFinite(v) ? Number(v) : 0; } } else { for (let c = 0; c < C; c++) rr[c] = 0; } out[r] = rr; } return out; } /** * Relaxed rectangularity check: * - Accepts any array-like rows (typed arrays included) * - Verifies consistent width and finite numbers */ function assertRect(A, name = 'matrix') { if (!A || typeof A.length !== 'number') { throw new DimError(`${name} must be a non-empty 2D array`); } const rows = A.length | 0; if (rows <= 0) throw new DimError(`${name} must be a non-empty 2D array`); const first = A[0]; if (!isArrayLikeRow(first)) throw new DimError(`${name} row 0 missing/invalid`); const C = first.length | 0; if (C <= 0) throw new DimError(`${name} must have positive column count`); for (let r = 0; r < rows; r++) { const rowAny = A[r]; if (!isArrayLikeRow(rowAny)) { throw new DimError(`${name} row ${r} invalid`); } const row = rowAny; // ✅ typed if ((row.length | 0) !== C) { throw new DimError(`${name} has ragged rows: row 0 = ${C} cols, row ${r} = ${row.length} cols`); } for (let c = 0; c < C; c++) { const v = row[c]; if (!Number.isFinite(v)) { throw new DimError(`${name} row ${r}, col ${c} is not finite: ${v}`); } } } } function assertMulDims(A, B) { assertRect(A, 'A'); assertRect(B, 'B'); const nA = A[0].length; const mB = B.length; if (nA !== mB) { throw new DimError(`matmul dims mismatch: A(${A.length}x${nA}) * B(${mB}x${B[0].length})`); } } function isSquare(A) { return isArrayLikeRow(A === null || A === void 0 ? void 0 : A[0]) && (A.length === (A[0].length | 0)); } function isSymmetric(A, tol = 1e-10) { if (!isSquare(A)) return false; const n = A.length; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { if (Math.abs(A[i][j] - A[j][i]) > tol) return false; } } return true; } /* ============================== Matrix ============================== */ class Matrix { /* ========= constructors / basics ========= */ static shape(A) { assertRect(A, 'A'); return [A.length, A[0].length]; } static clone(A) { assertRect(A, 'A'); return ensureRectNumber2D(A, A[0].length, 'A(clone)'); } static zeros(rows, cols) { const out = new Array(rows); for (let i = 0; i < rows; i++) out[i] = new Array(cols).fill(0); return out; } static identity(n) { const I = Matrix.zeros(n, n); for (let i = 0; i < n; i++) I[i][i] = 1; return I; } static transpose(A) { assertRect(A, 'A'); const m = A.length, n = A[0].length; const T = Matrix.zeros(n, m); for (let i = 0; i < m; i++) { const Ai = A[i]; for (let j = 0; j < n; j++) T[j][i] = Number(Ai[j]); } return T; } /* ========= algebra ========= */ static add(A, B) { A = ensureRectNumber2D(A, undefined, 'A'); B = ensureRectNumber2D(B, undefined, 'B'); assertRect(A, 'A'); assertRect(B, 'B'); if (A.length !== B.length || A[0].length !== B[0].length) { throw new DimError(`add dims mismatch: A(${A.length}x${A[0].length}) vs B(${B.length}x${B[0].length})`); } const m = A.length, n = A[0].length; const C = Matrix.zeros(m, n); for (let i = 0; i < m; i++) { const Ai = A[i], Bi = B[i], Ci = C[i]; for (let j = 0; j < n; j++) Ci[j] = Ai[j] + Bi[j]; } return C; } /** Adds lambda to the diagonal (ridge regularization) */ static addRegularization(A, lambda = 1e-6) { A = ensureRectNumber2D(A, undefined, 'A'); assertRect(A, 'A'); if (!isSquare(A)) { throw new DimError(`addRegularization expects square matrix, got ${A.length}x${A[0].length}`); } const C = Matrix.clone(A); for (let i = 0; i < C.length; i++) C[i][i] += lambda; return C; } static multiply(A, B) { A = ensureRectNumber2D(A, undefined, 'A'); B = ensureRectNumber2D(B, undefined, 'B'); assertMulDims(A, B); const m = A.length, n = B.length, p = B[0].length; const C = Matrix.zeros(m, p); for (let i = 0; i < m; i++) { const Ai = A[i]; for (let k = 0; k < n; k++) { const aik = Number(Ai[k]); const Bk = B[k]; for (let j = 0; j < p; j++) C[i][j] += aik * Number(Bk[j]); } } return C; } static multiplyVec(A, v) { A = ensureRectNumber2D(A, undefined, 'A'); assertRect(A, 'A'); if (!v || typeof v.length !== 'number') { throw new DimError(`matvec expects vector 'v' with length ${A[0].length}`); } if (A[0].length !== v.length) { throw new DimError(`matvec dims mismatch: A cols ${A[0].length} vs v len ${v.length}`); } const m = A.length, n = v.length; const out = new Array(m).fill(0); for (let i = 0; i < m; i++) { const Ai = A[i]; let s = 0; for (let j = 0; j < n; j++) s += Number(Ai[j]) * Number(v[j]); out[i] = s; } return out; } /* ========= decompositions / solve ========= */ static cholesky(A, jitter = 0) { A = ensureRectNumber2D(A, undefined, 'A'); assertRect(A, 'A'); if (!isSquare(A)) throw new DimError(`cholesky expects square matrix, got ${A.length}x${A[0].length}`); const n = A.length; const L = Matrix.zeros(n, n); for (let i = 0; i < n; i++) { for (let j = 0; j <= i; j++) { let sum = A[i][j]; for (let k = 0; k < j; k++) sum -= L[i][k] * L[j][k]; if (i === j) { const v = sum + jitter; L[i][j] = Math.sqrt(Math.max(v, EPS$4)); } else { L[i][j] = sum / L[j][j]; } } } return L; } static solveCholesky(A, B, jitter = 1e-10) { A = ensureRectNumber2D(A, undefined, 'A'); B = ensureRectNumber2D(B, undefined, 'B'); assertRect(A, 'A'); assertRect(B, 'B'); if (!isSquare(A) || A.length !== B.length) { throw new DimError(`solveCholesky dims: A(${A.length}x${A[0].length}) vs B(${B.length}x${B[0].length})`); } const n = A.length, k = B[0].length; const L = Matrix.cholesky(A, jitter); // Solve L Z = B (forward) const Z = Matrix.zeros(n, k); for (let i = 0; i < n; i++) { for (let c = 0; c < k; c++) { let s = B[i][c]; for (let p = 0; p < i; p++) s -= L[i][p] * Z[p][c]; Z[i][c] = s / L[i][i]; } } // Solve L^T X = Z (backward) const X = Matrix.zeros(n, k); for (let i = n - 1; i >= 0; i--) { for (let c = 0; c < k; c++) { let s = Z[i][c]; for (let p = i + 1; p < n; p++) s -= L[p][i] * X[p][c]; X[i][c] = s / L[i][i]; } } return X; } static inverse(A) { A = ensureRectNumber2D(A, undefined, 'A'); assertRect(A, 'A'); if (!isSquare(A)) throw new DimError(`inverse expects square matrix, got ${A.length}x${A[0].length}`); const n = A.length; const M = Matrix.clone(A); const I = Matrix.identity(n); // Augment [M | I] const aug = new Array(n); for (let i = 0; i < n; i++) aug[i] = M[i].concat(I[i]); const cols = 2 * n; for (let p = 0; p < n; p++) { // Pivot let maxRow = p, maxVal = Math.abs(aug[p][p]); for (let r = p + 1; r < n; r++) { const v = Math.abs(aug[r][p]); if (v > maxVal) { maxVal = v; maxRow = r; } } if (maxVal < EPS$4) throw new Error('Matrix is singular or ill-conditioned'); if (maxRow !== p) { const tmp = aug[p]; aug[p] = aug[maxRow]; aug[maxRow] = tmp; } // Normalize pivot row const piv = aug[p][p]; const invPiv = 1 / piv; for (let c = 0; c < cols; c++) aug[p][c] *= invPiv; // Eliminate other rows for (let r = 0; r < n; r++) { if (r === p) continue; const f = aug[r][p]; if (Math.abs(f) < EPS$4) continue; for (let c = 0; c < cols; c++) aug[r][c] -= f * aug[p][c]; } } // Extract right half as inverse const inv = Matrix.zeros(n, n); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) inv[i][j] = aug[i][n + j]; } return inv; } /* ========= helpers ========= */ static inverseSPDOrFallback(A) { if (isSymmetric(A)) { try { return Matrix.solveCholesky(A, Matrix.identity(A.length), 1e-10); } catch (_a) { // fall through } } return Matrix.inverse(A); } /* ========= Symmetric Eigen (Jacobi) & Inverse Square Root ========= */ static assertSquare(A, ctx = 'Matrix') { assertRect(A, ctx); if (!isSquare(A)) { throw new DimError(`${ctx}: expected square matrix, got ${A.length}x${A[0].length}`); } } static eigSym(A, maxIter = 64, tol = 1e-12) { A = ensureRectNumber2D(A, undefined, 'eigSym/A'); Matrix.assertSquare(A, 'eigSym'); const n = A.length; const B = Matrix.clone(A); let V = Matrix.identity(n); const abs = Math.abs; const offdiagNorm = () => { let s = 0; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { const v = B[i][j]; s += v * v; } } return Math.sqrt(s); }; for (let it = 0; it < maxIter; it++) { if (offdiagNorm() <= tol) break; let p = 0, q = 1, max = 0; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { const v = abs(B[i][j]); if (v > max) { max = v; p = i; q = j; } } } if (max <= tol) break; const app = B[p][p], aqq = B[q][q], apq = B[p][q]; const tau = (aqq - app) / (2 * apq); const t = Math.sign(tau) / (abs(tau) + Math.sqrt(1 + tau * tau)); const c = 1 / Math.sqrt(1 + t * t); const s = t * c; const Bpp = c * c * app - 2 * s * c * apq + s * s * aqq; const Bqq = s * s * app + 2 * s * c * apq + c * c * aqq; B[p][p] = Bpp; B[q][q] = Bqq; B[p][q] = B[q][p] = 0; for (let k = 0; k < n; k++) { if (k === p || k === q) continue; const aip = B[k][p], aiq = B[k][q]; const new_kp = c * aip - s * aiq; const new_kq = s * aip + c * aiq; B[k][p] = B[p][k] = new_kp; B[k][q] = B[q][k] = new_kq; } for (let k = 0; k < n; k++) { const vip = V[k][p], viq = V[k][q]; V[k][p] = c * vip - s * viq; V[k][q] = s * vip + c * viq; } } const vals = new Array(n); for (let i = 0; i < n; i++) vals[i] = B[i][i]; const order = vals.map((v, i) => [v, i]).sort((a, b) => a[0] - b[0]).map(([, i]) => i); const values = order.map(i => vals[i]); const vectors = Matrix.zeros(n, n); for (let r = 0; r < n; r++) { for (let c = 0; c < n; c++) vectors[r][c] = V[r][order[c]]; } return { values, vectors }; } static invSqrtSym(A, eps = 1e-10) { A = ensureRectNumber2D(A, undefined, 'invSqrtSym/A'); Matrix.assertSquare(A, 'invSqrtSym'); const { values, vectors: U } = Matrix.eigSym(A); const n = values.length; const Dm12 = Matrix.zeros(n, n); for (let i = 0; i < n; i++) { const lam = Math.max(values[i], eps); Dm12[i][i] = 1 / Math.sqrt(lam); } const UD = Matrix.multiply(U, Dm12); return Matrix.multiply(UD, Matrix.transpose(U)); } } // © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 // Activations.ts - Common activation functions (with derivatives) class Activations { /* ========= Forward ========= */ /** Rectified Linear Unit */ static relu(x) { return x > 0 ? x : 0; } /** Leaky ReLU with configurable slope for x<0 (default 0.01) */ static leakyRelu(x, alpha = 0.01) { return x >= 0 ? x : alpha * x; } /** Logistic sigmoid */ static sigmoid(x) { return 1 / (1 + Math.exp(-x)); } /** Hyperbolic tangent */ static tanh(x) { return Math.tanh(x); } /** Linear / identity activation */ static linear(x) { return x; } /** * GELU (Gaussian Error Linear Unit), tanh approximation. * 0.5 * x * (1 + tanh(√(2/π) * (x + 0.044715 x^3))) */ static gelu(x) { const k = Math.sqrt(2 / Math.PI); const u = k * (x + 0.044715 * x * x * x); return 0.5 * x * (1 + Math.tanh(u)); } /** * Softmax with numerical stability and optional temperature. * @param arr logits * @param temperature >0; higher = flatter distribution */ static softmax(arr, temperature = 1) { const t = Math.max(temperature, 1e-12); let max = -Infinity; for (let i = 0; i < arr.length; i++) { const v = arr[i] / t; if (v > max) max = v; } const exps = new Array(arr.length); let sum = 0; for (let i = 0; i < arr.length; i++) { const e = Math.exp(arr[i] / t - max); exps[i] = e; sum += e; } const denom = sum || 1e-12; for (let i = 0; i < exps.length; i++) exps[i] = exps[i] / denom; return exps; } /* ========= Derivatives (elementwise) ========= */ /** d/dx ReLU */ static dRelu(x) { // subgradient at 0 -> 0 return x > 0 ? 1 : 0; } /** d/dx LeakyReLU */ static dLeakyRelu(x, alpha = 0.01) { return x >= 0 ? 1 : alpha; } /** d/dx Sigmoid = s(x)*(1-s(x)) */ static dSigmoid(x) { const s = Activations.sigmoid(x); return s * (1 - s); } /** d/dx tanh = 1 - tanh(x)^2 */ static dTanh(x) { const t = Math.tanh(x); return 1 - t * t; } /** d/dx Linear = 1 */ static dLinear(_) { return 1; } /** * d/dx GELU (tanh approximation) * 0.5*(1 + tanh(u)) + 0.5*x*(1 - tanh(u)^2) * du/dx * where u = k*(x + 0.044715 x^3), du/dx = k*(1 + 0.134145 x^2), k = sqrt(2/pi) */ static dGelu(x) { const k = Math.sqrt(2 / Math.PI); const x2 = x * x; const u = k * (x + 0.044715 * x * x2); const t = Math.tanh(u); const sech2 = 1 - t * t; const du = k * (1 + 0.134145 * x2); return 0.5 * (1 + t) + 0.5 * x * sech2 * du; } /* ========= Apply helpers ========= */ /** Apply an elementwise activation across a 2D matrix, returning a new matrix. */ static apply(matrix, fn) { const out = new Array(matrix.length); for (let i = 0; i < matrix.length; i++) { const row = matrix[i]; const r = new Array(row.length); for (let j = 0; j < row.length; j++) r[j] = fn(row[j]); out[i] = r; } return out; } /** Apply an elementwise derivative across a 2D matrix, returning a new matrix. */ static applyDerivative(matrix, dfn) { const out = new Array(matrix.length); for (let i = 0; i < matrix.length; i++) { const row = matrix[i]; const r = new Array(row.length); for (let j = 0; j < row.length; j++) r[j] = dfn(row[j]); out[i] = r; } return out; } /* ========= Getters ========= */ /** * Get an activation function by name. Case-insensitive. * For leaky ReLU, you can pass { alpha } to override the negative slope. */ static get(name, opts) { var _a; const key = name.toLowerCase(); switch (key) { case 'relu': return this.relu; case 'leakyrelu': case 'leaky-relu': { const alpha = (_a = opts === null || opts === void 0 ? void 0 : opts.alpha) !== null && _a !== void 0 ? _a : 0.01; return (x) => this.leakyRelu(x, alpha); } case 'sigmoid': return this.sigmoid; case 'tanh': return this.tanh; case 'linear': case 'identity': case 'none': return this.linear; case 'gelu': return this.gelu; default: throw new Error(`Unknown activation: ${name}`); } } /** Get derivative function by name (mirrors get). */ static getDerivative(name, opts) { var _a; const key = name.toLowerCase(); switch (key) { case 'relu': return this.dRelu; case 'leakyrelu': case 'leaky-relu': { const alpha = (_a = opts === null || opts === void 0 ? void 0 : opts.alpha) !== null && _a !== void 0 ? _a : 0.01; return (x) => this.dLeakyRelu(x, alpha); } case 'sigmoid': return this.dSigmoid; case 'tanh': return this.dTanh; case 'linear': case 'identity': case 'none': return this.dLinear; case 'gelu': return this.dGelu; default: throw new Error(`Unknown activation derivative: ${name}`); } } /** Get both forward and derivative together. */ static getPair(name, opts) { return { f: this.get(name, opts), df: this.getDerivative(name, opts) }; } /* ========= Optional: Softmax Jacobian (for research/tools) ========= */ /** * Given softmax probabilities p, returns the Jacobian J = diag(p) - p p^T * (Useful for analysis; not typically needed for ELM.) */ static softmaxJacobian(p) { const n = p.length; const J = new Array(n); for (let i = 0; i < n; i++) { const row = new Array(n); for (let j = 0; j < n; j++) { row[j] = (i === j ? p[i] : 0) - p[i] * p[j]; } J[i] = row; } return J; } } // © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 // ELMConfig.ts - Configuration interfaces, defaults, helpers for ELM-based models /* =========== Defaults =========== */ const defaultBase = { hiddenUnits: 50, activation: 'relu', ridgeLambda: 1e-2, weightInit: 'xavier', seed: 1337, dropout: 0, log: { verbose: true, toFile: false, modelName: 'Unnamed ELM Model', level: 'info' }, }; const defaultNumericConfig = Object.assign(Object.assign({}, defaultBase), { useTokenizer: false }); const defaultTextConfig = Object.assign(Object.assign({}, defaultBase), { useTokenizer: true, maxLen: 30, charSet: 'abcdefghijklmnopqrstuvwxyz', tokenizerDelimiter: /\s+/ }); /* =========== Type guards =========== */ function isTextConfig(cfg) { return cfg.useTokenizer === true; } function isNumericConfig(cfg) { return cfg.useTokenizer !== true; } /* =========== Helpers =========== */ /** * Normalize a user config with sensible defaults depending on mode. * (Keeps the original structural type, only fills in missing optional fields.) */ function normalizeConfig(cfg) { var _a, _b, _c, _d; if (isTextConfig(cfg)) { const merged = Object.assign(Object.assign(Object.assign({}, defaultTextConfig), cfg), { log: Object.assign(Object.assign({}, ((_a = defaultBase.log) !== null && _a !== void 0 ? _a : {})), ((_b = cfg.log) !== null && _b !== void 0 ? _b : {})) }); return merged; } else { const merged = Object.assign(Object.assign(Object.assign({}, defaultNumericConfig), cfg), { log: Object.assign(Object.assign({}, ((_c = defaultBase.log) !== null && _c !== void 0 ? _c : {})), ((_d = cfg.log) !== null && _d !== void 0 ? _d : {})) }); return merged; } } /** * Rehydrate text-specific fields from a JSON-safe config * (e.g., convert tokenizerDelimiter source string → RegExp). */ function deserializeTextBits(config) { var _a, _b, _c, _d; // If useTokenizer not true, assume numeric config if (config.useTokenizer !== true) { const nc = Object.assign(Object.assign(Object.assign({}, defaultNumericConfig), config), { log: Object.assign(Object.assign({}, ((_a = defaultBase.log) !== null && _a !== void 0 ? _a : {})), ((_b = config.log) !== null && _b !== void 0 ? _b : {})) }); return nc; } // Text config: coerce delimiter const tDelim = config.tokenizerDelimiter; let delimiter = undefined; if (tDelim instanceof RegExp) { delimiter = tDelim; } else if (typeof tDelim === 'string' && tDelim.length > 0) { delimiter = new RegExp(tDelim); } else { delimiter = defaultTextConfig.tokenizerDelimiter; } const tc = Object.assign(Object.assign(Object.assign({}, defaultTextConfig), config), { tokenizerDelimiter: delimiter, log: Object.assign(Object.assign({}, ((_c = defaultBase.log) !== null && _c !== void 0 ? _c : {})), ((_d = config.log) !== null && _d !== void 0 ? _d : {})), useTokenizer: true }); return tc; } // © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 class Tokenizer { constructor(customDelimiter) { this.delimiter = customDelimiter || /[\s,.;!?()\[\]{}"']+/; } tokenize(text) { if (typeof text !== 'string') { console.warn('[Tokenizer] Expected a string, got:', typeof text, text); try { text = String(text !== null && text !== void 0 ? text : ''); } catch (_a) { return []; } } return text .trim() .toLowerCase() .split(this.delimiter) .filter(Boolean); } ngrams(tokens, n) { if (n <= 0 || tokens.length < n) return []; const result = []; for (let i = 0; i <= tokens.length - n; i++) { result.push(tokens.slice(i, i + n).join(' ')); } return result; } } // © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 // TextEncoder.ts - Text preprocessing and one-hot encoding for ELM const defaultTextEncoderConfig = { charSet: 'abcdefghijklmnopqrstuvwxyz', maxLen: 15, useTokenizer: false }; class TextEncoder { constructor(config = {}) { const cfg = Object.assign(Object.assign({}, defaultTextEncoderConfig), config); this.charSet = cfg.charSet; this.charSize = cfg.charSet.length; this.maxLen = cfg.maxLen; this.useTokenizer = cfg.useTokenizer; if (this.useTokenizer) { this.tokenizer = new Tokenizer(config.tokenizerDelimiter); } } charToOneHot(c) { const index = this.charSet.indexOf(c.toLowerCase()); const vec = Array(this.charSize).fill(0); if (index !== -1) vec[index] = 1; return vec; } textToVector(text) { let cleaned; if (this.useTokenizer && this.tokenizer) { const tokens = this.tokenizer.tokenize(text).join(''); cleaned = tokens.slice(0, this.maxLen).padEnd(this.maxLen, ' '); } else { cleaned = text.toLowerCase().replace(new RegExp(`[^${this.charSet}]`, 'g'), '').padEnd(this.maxLen, ' ').slice(0, this.maxLen); } const vec = []; for (let i = 0; i < cleaned.length; i++) { vec.push(...this.charToOneHot(cleaned[i])); } return vec; } normalizeVector(v) { const norm = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0)); return norm > 0 ? v.map(x => x / norm) : v; } getVectorSize() { return this.charSize * this.maxLen; } getCharSet() { return this.charSet; } getMaxLen() { return this.maxLen; } } // © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 // UniversalEncoder.ts - Automatically selects appropriate encoder (char or token based) const defaultUniversalConfig = { charSet: 'abcdefghijklmnopqrstuvwxyz', maxLen: 15, useTokenizer: false, mode: 'char' }; class UniversalEncoder { constructor(config = {}) { const merged = Object.assign(Object.assign({}, defaultUniversalConfig), config); const useTokenizer = merged.mode === 'token'; this.encoder = new TextEncoder({ charSet: merged.charSet, maxLen: merged.maxLen, useTokenizer, tokenizerDelimiter: config.tokenizerDelimiter }); } encode(text) { return this.encoder.textToVector(text); } normalize(v) { return this.encoder.normalizeVector(v); } getVectorSize() { return this.encoder.getVectorSize(); } } // © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 // Augment.ts - Basic augmentation utilities for category training examples class Augment { static addSuffix(text, suffixes) { return suffixes.map(suffix => `${text} ${suffix}`); } static addPrefix(text, prefixes) { return prefixes.map(prefix => `${prefix} ${text}`); } static addNoise(text, charSet, noiseRate = 0.1) { const chars = text.split(''); for (let i = 0; i < chars.length; i++) { if (Math.random() < noiseRate) { const randomChar = charSet[Math.floor(Math.random() * charSet.length)]; chars[i] = randomChar; } } return chars.join(''); } static mix(text, mixins) { return mixins.map(m => `${text} ${m}`); } static generateVariants(text, charSet, options) { const variants = [text]; if (options === null || options === void 0 ? void 0 : options.suffixes) { variants.push(...this.addSuffix(text, options.suffixes)); } if (options === null || options === void 0 ? void 0 : options.prefixes) { variants.push(...this.addPrefix(text, options.prefixes)); } if (options === null || options === void 0 ? void 0 : options.includeNoise) { variants.push(this.addNoise(text, charSet)); } return variants; } } // © 2025 AsterMind LLC – All Rights Reserved. // Patent Pending US 63/897,713 // ELM.ts - Core ELM logic with TypeScript types (numeric & text modes) // Seeded PRNG (xorshift-ish) for deterministic init function makePRNG$2(seed = 123456789) { let s = seed | 0 || 1; return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return ((s >>> 0) / 0xffffffff); }; } function clampInt(x, lo, hi) { const xi = x | 0; return xi < lo ? lo : (xi > hi ? hi : xi); } function isOneHot2D(Y) { return Array.isArray(Y) && Array.isArray(Y[0]) && Number.isFinite(Y[0][0]); } function maxLabel(y) { let m = -Infinity; for (let i = 0; i < y.length; i++) { const v = y[i] | 0; if (v > m) m = v; } return m === -Infinity ? 0 : m; } /** One-hot (clamped) */ function toOneHotClamped(labels, k) { const K = k | 0; const Y = new Array(labels.length); for (let i = 0; i < labels.length; i++) { const j = clampInt(labels[i], 0, K - 1); const row = new Array(K).fill(0); row[j] = 1; Y[i] = row; } return Y; } /** (HᵀH + λI)B = HᵀY solved via Cholesky */ function ridgeSolve(H, Y, lambda) { const Ht = Matrix.transpose(H); const A = Matrix.addRegularization(Matrix.multiply(Ht, H), lambda + 1e-10); const R = Matrix.multiply(Ht, Y); return Matrix.solveCholesky(A, R, 1e-10); } /* ========================= * ELM class * ========================= */ class ELM { constructor(config) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; // Merge with mode-appropriate defaults const cfg = normalizeConfig(config); this.config = cfg; this.categories = cfg.categories; this.hiddenUnits = cfg.hiddenUnits; this.activation = (_a = cfg.activation) !== null && _a !== void 0 ? _a : 'relu'; this.useTokenizer = isTextConfig(cfg); this.maxLen = isTextConfig(cfg) ? cfg.maxLen : 0; this.charSet = isTextConfig(cfg) ? ((_b = cfg.charSet) !== null && _b !== void 0 ? _b : 'abcdefghijklmnopqrstuvwxyz') : 'abcdefghijklmnopqrstuvwxyz'; this.tokenizerDelimiter = isTextConfig(cfg) ? cfg.tokenizerDelimiter : undefined; this.metrics = cfg.metrics; this.verbose = (_d = (_c = cfg.log) === null || _c === void 0 ? void 0 : _c.verbose) !== null && _d !== void 0 ? _d : true; this.modelName = (_f = (_e = cfg.log) === null || _e === void 0 ? void 0 : _e.modelName) !== null && _f !== void 0 ? _f : 'Unnamed ELM Model'; this.logToFile = (_h = (_g = cfg.log) === null || _g === void 0 ? void 0 : _g.toFile) !== null && _h !== void 0 ? _h : false; this.dropout = (_j = cfg.dropout) !== null && _j !== void 0 ? _j : 0; this.ridgeLambda = Math.max((_k = cfg.ridgeLambda) !== null && _k !== void 0 ? _k : 1e-2, 1e-8); // Seeded RNG const seed = (_l = cfg.seed) !== null && _l !== void 0 ? _l : 1337; this.rng = makePRNG$2(seed); // Create encoder only if tokenizer is enabled if (this.useTokenizer) { this.encoder = new UniversalEncoder({ charSet: this.charSet, maxLen: this.maxLen, useTokenizer: this.useTokenizer, tokenizerDelimiter: this.tokenizerDelimiter, mode: this.useTokenizer ? 'token' : 'char' }); } // Weights are allocated on first training call (inputDim known then) this.model = null; } /* ========= Encoder narrowing (Option A) ========= */ assertEncoder() { if (!this.encoder) { throw new Error('Encoder is not initialized. Enable useTokenizer:true or construct an encoder.'); } return this.encoder; } /* ========= initialization ========= */ xavierLimit(fanIn, fanOut) { return Math.sqrt(6 / (fanIn + fanOut)); } randomMatrix(rows, cols) { var _a; const weightInit = (_a = this.config.weightInit) !== null && _a !== void 0 ? _a : 'uniform'; if (weightInit === 'xavier') { const limit = this.xavierLimit(cols, rows); if (this.verbose) console.log(`✨ Xavier init with limit sqrt(6/(${cols}+${rows})) ≈ ${limit.toFixed(4)}`); return Array.from({ length: rows }, () => Array.from({ length: cols }, () => (this.rng() * 2 - 1) * limit)); } else { if (this.verbose) console.log(`✨ Uniform init [-1,1] (seeded)`); return Array.from({ length: rows }, () => Array.from({ length: cols }, () => (this.rng() * 2 - 1))); } } buildHidden(X, W, b) { const tempH = Matrix.multiply(X, Matrix.transpose(W)); // N x hidden const activationFn = Activations.get(this.activation); let H = Activations.apply(tempH.map(row => row.map((val, j) => val + b[j][0])), activationFn); if (this.dropout > 0) { const keepProb = 1 - this.dropout; for (let i = 0; i < H.length; i++) { for (let j = 0; j < H[0].length; j++) { if (this.rng() < this.dropout) H[i][j] = 0; else H[i][j] /= keepProb; } } } return H; } /* ========= public helpers ========= */ oneHot(n, index) { return Array.from({ length: n }, (_, i) => (i === index ? 1 : 0)); } setCategories(categories) { this.categories = categories; } loadModelFromJSON(json) { var _a, _b, _c, _d, _e; try { const parsed = JSON.parse(json); const cfg = deserializeTextBits(parsed.config); // Rebuild instance config this.config = cfg; this.categories = (_a = cfg.categories) !== null && _a !== void 0 ? _a : this.categories; this.hiddenUnits = (_b = cfg.hiddenUnits) !== null && _b !== void 0 ? _b : this.hiddenUnits; this.activation = (_c = cfg.activation) !== null && _c !== void 0 ? _c : this.activation; this.useTokenizer = cfg.useTokenizer === true; this.maxLen = (_d = cfg.maxLen) !== null && _d !== void 0 ? _d : this.maxLen; this.charSet = (_e = cfg.charSet) !== null && _e !== void 0 ? _e : this.charSet; this.tokenizerDelimiter = cfg.tokenizerDelimiter; if (this.useTokenizer) { this.encoder = new UniversalEncoder({ charSet: this.charSet, maxLen: this.maxLen, useTokenizer: this.useTokenizer, tokenizerDelimiter: this.tokenizerDelimiter, mode: this.useTokenizer ? 'token' : 'char' }); } else { this.encoder = undefined; } // Restore weights const { W, b, B } = parsed; this.model = { W, b, beta: B }; this.savedModelJSON = json; if (this.verbose) console.log(`✅ ${this.modelName} Model loaded from JSON`); } catch (e) { console.error(`❌ Failed to load ${this.modelName} model from JSON:`, e); } } /* ========= Numeric training tolerance ========= */ /** Decide output dimension from config/categories/labels/one-hot */ resolveOutputDim(yOrY) { // Prefer explicit config const cfgOut = this.config.outputDim; if (Number.isFinite(cfgOut) && cfgOut > 0) return cfgOut | 0; // Then categories length if present if (Array.isArray(this.categories) && this.categories.length > 0) return this.categories.length | 0; // Infer from data if (isOneHot2D(yOrY)) return (yOrY[0].length | 0) || 1; return (maxLabel(yOrY) + 1) | 0; } /** Coerce X, and turn labels→one-hot if needed. Always returns strict number[][] */ coerceXY(X, yOrY) { const Xnum = ensureRectNumber2D(X, undefined, 'X'); const outDim = this.resolveOutputDim(yOrY); let Ynum; if (isOneHot2D(yOrY)) { // Ensure rect with exact width outDim (pad/trunc to be safe) Ynum = ensureRectNumber2D(yOrY, outDim, 'Y(one-hot)'); } else { // Labels → clamped one-hot Ynum = ensureRectNumber2D(toOneHotClamped(yOrY, outDim), outDim, 'Y(labels→one-hot)'); } // If categories length mismatches inferred outDim, adjust categories (non-breaking) if (!this.categories || this.categories.length !== outDim) { this.categories = Array.from({ length: outDim }, (_, i) => { var _a, _b; return (_b = (_a = this.categories) === null || _a === void 0 ? void 0 : _a[i]) !== null && _b !== void 0 ? _b : String(i); }); } return { Xnum, Ynum, outDim }; } /* ========= Training on numeric vectors ========= * y can be class indices OR one-hot. */ trainFromData(X, y, options) { if (!(X === null || X === void 0 ? void 0 : X.length)) throw new Error('trainFromData: X is empty'); // Coerce & shape const { Xnum, Ynum, outDim } = this.coerceXY(X, y); const n = Xnum.length; const inputDim = Xnum[0].length; // init / reuse let W, b; const reuseWeights = (options === null || options === void 0 ? void 0 : options.reuseWeights) === true && this.model; if (reuseWeights && this.model) { W = this.model.W; b = this.model.b; if (this.verbose) console.log('🔄 Reusing existing weights/biases for training.'); } else { W = this.randomMatrix(this.hiddenUnits, inputDim); b = this.randomMatrix(this.hiddenUnits, 1); if (this.verbose) console.log('✨ Initializing fresh weights/biases for training.'); } // Hidden let H = this.buildHidden(Xnum, W, b); // Optional sample weights let Yw = Ynum; if (options === null || options === void 0 ? void 0 : options.weights) { const ww = options.weights; if (ww.length !== n) { throw new Error(`Weight array length ${ww.length} does not match sample count ${n}`); } H = H.map((row, i) => row.map(x => x * Math.sqrt(ww[i]))); Yw = Ynum.map((row, i) => row.map(x => x * Math.sqrt(ww[i]))); } // Solve ridge (stable) const beta = ridgeSolve(H, Yw, this.ridgeLambda); this.model = { W, b, beta }; // Evaluate & maybe save const predictions = Matrix.multiply(H, beta); if (this.metrics) { const rmse = this.calculateRMSE(Ynum, predictions); const mae = this.calculateMAE(Ynum, predictions); const acc = this.calculateAccuracy(Ynum, predictions); const f1 = this.calculateF1Score(Ynum, predictions); const ce = this.calculateCrossEntropy(Ynum, predictions); const r2 = this.calculateR2Score(Ynum, predictions); const results = { rmse, mae, accuracy: acc, f1, crossEntropy: ce, r2 }; let allPassed = true; if (this.metrics.rmse !== undefined && rmse > this.metrics.rmse) allPassed = false; if (this.metrics.mae !== undefined && mae > this.metrics.mae) allPassed = false; if (this.metrics.accuracy !== undefined && acc < this.metrics.accuracy) allPassed = false; if (this.metrics.f1 !== undefined && f1 < this.metrics.f1) allPassed = false; if (this.metrics.crossEntropy !== undefined && ce > this.metrics.crossEntropy) allPassed = false; if (this.metrics.r2 !== undefined && r2 < this.metrics.r2) allPassed = false; if (this.verbose) this.logMetrics(results); if (allPassed) { this.savedModelJSON = JSON.stringify({ config: this.serializeConfig(), W, b, B: beta }); if (this.verbose) console.log('✅ Model passed thresholds and was saved to JSON.'); if (this.config.exportFileName) this.saveModelAsJSONFile(this.config.exportFileName); } else { if (this.verbose) console.log('❌ Model not saved: One or more thresholds not met.'); } } else { // No metrics—always save this.savedModelJSON = JSON.stringify({ config: this.serializeConfig(), W, b, B: beta }); if (this.verbose) console.log('✅ Model trained with no metrics—saved by default.'); if (this.config.exportFileName) this.saveModelAsJSONFile(this.config.exportFileName); } return { epochs: 1, metrics: undefined }; } /* ========= Training from category strings (text mode) ========= */ train(augmentationOptions, weights) { if (!this.useTokenizer) { throw new Error('train(): text training requires useTokenizer:true'); } const enc = this.assertEncoder(); const X = []; let Y = []; this.categories.forEach((cat, i) => { const variants = Augment.generateVariants(cat, this.charSet, augmentationOptions); for (const variant of variants) { const vec = enc.normalize(enc.encode(variant)); X.push(vec); Y.push(this.oneHot(this.categories.length, i)); } }); const inputDim = X[0].length; const W = this.randomMatrix(this.hiddenUnits, inputDim); const b = this.randomMatrix(this.hiddenUnits, 1); let H = this.buildHidden(X, W, b); if (weights) { if (weights.length !== H.length) { throw new Error(`Weight array length ${weights.length} does not match sample count ${H.length}`); } H = H.map((row, i) => row.map(x => x * Math.sqrt(weights[i]))); Y = Y.map((row, i) => row.map(x => x * Math.sqrt(weights[i]))); } const beta = ridgeSolve(H, Y, this.ridgeLambda); this.model = { W, b, beta }; const predictions = Matrix.multiply(H, beta); if (this.metrics) { const rmse = this.calculateRMSE(Y, predictions); const mae = this.calculateMAE(Y, predictions); const acc = this.calculateAccuracy(Y, predictions); const f1 = this.calculateF1Score(Y, predictions); const ce = this.calculateCrossEntropy(Y, predictions); const r2 = this.calculateR2Score(Y, predictions); const results = { rmse, mae, accuracy: acc, f1, crossEntropy: ce, r2 }; let allPassed = true; if (this.metrics.rmse !== undefined && rmse > this.metrics.rmse) allPassed = false; if (this.metrics.mae !== undefined && mae > this.metrics.mae) allPassed = false; if (this.metrics.accuracy !== undefined && acc < this.metrics.accuracy) allPassed = false; if (this.metrics.f1 !== undefined && f1 < this.metrics.f1) allPassed = false; if (this.metrics.crossEntropy !== undefined && ce > this.metrics.crossEntropy) allPassed = false; if (this.metrics.r2 !== undefined && r2 < this.metrics.r2) allPassed = false; if (this.verbose) this.logMetrics(results); if (allPassed) { this.savedModelJSON = JSON.stringify({ config: this.serializeConfig(), W, b, B: beta }); if (this.verbose) console.log('✅ Model passed thresholds and was saved to JSON.'); if (this.config.exportFileName) this.saveModelAsJSONFile(this.config.exportFileName); } else { if (this.verbose) console.log('❌ Model not saved: One or more thresholds not met.'); } } else { this.savedModelJSON = JSON.stringify({ config: this.serializeConfig(), W, b, B: beta }); if (this.verbose) console.log('✅ Model trained with no metrics—saved by default.'); if (this.config.exportFileName) this.saveModelAsJSONFile(this.config.exportFileName); } return { epochs: 1, metrics: undefined }; } /* ========= Prediction ========= */ /** Text prediction (uses Option A narrowing) */ predict(text, topK = 5) { if (!this.model) throw new Error('Model not trained.'); if (!this.useTokenizer) { throw new Error('predict(text) requires useTokenizer:true'); } const enc = this.assertEncoder(); const vec = enc.normalize(enc.encode(text)); const logits = this.predictLogitsFromVector(vec); const probs = Activations.softmax(logits); return probs .map((p, i) => ({ label: this.categories[i], prob: p })) .sort((a, b) => b.prob - a.prob) .slice(0, topK); } /** Vector batch prediction (kept for back-compat) */ predictFromVector(inputVecRows, topK = 5) { if (!this.model) throw new Error('Model not trained.'); return inputVecRows.map(vec => { const logits = this.predictLogitsFromVector(vec); const probs = Activations.softmax(logits); return probs .map((p, i) => ({ label: this.categories[i], prob: p })) .sort((a, b) => b.prob - a.prob) .slice(0, topK); }); } /** Raw logits for a single numeric vector */ predictLogitsFromVector(vec) { if (!this.model) throw new Error('Model not trained.'); const { W, b, beta } = this.model; // Hidden const tempH = Matrix.multiply([vec], Matrix.transpose(W)); // 1 x hidden const activationFn = Activations.get(this.activation); const H = Activations.apply(tempH.map(row => row.map((val, j) => val + b[j][0])), activationFn); // 1 x hidden // Output logits return Matrix.multiply(H, beta)[0]; // 1 x outDim → vec } /** Raw logits for a batch of numeric vectors */ predictLogitsFromVectors(X) { if (!this.model) throw new Error('Model not trained.'); const { W, b, beta } = this.model; const tempH = Matrix.multiply(X, Matrix.transpose(W)); const activationFn = Activations.get(this.activation); const H = Activations.apply(tempH.map(row => row.map((val, j) => val + b[j][0])), activationFn); return Matrix.multiply(H, beta); } /** Probability vector (softmax) for a single numeric vector */ predictProbaFromVector(vec) { return Activations.softmax(this.predictLogitsFromVector(vec)); } /** Probability matrix (softmax per row) for a batch of numeric vectors */ predictProbaFromVectors(X) { return this.predictLogitsFromVectors(X).map(Activations.softmax); } /** Top-K results for a single numeric vector */ predictTopKFromVector(vec, k = 5) { const probs = this.predictProbaFromVector(vec); return probs .map((p, i) => ({ index: i, label: this.categories[i], prob: p })) .sort((a, b) => b.prob - a.prob) .slice(0, k); } /** Top-K results for a batch of numeric vectors */ predictTopKFromVectors(X, k = 5) { return this.predictProbaFromVectors(X).map(row => row .map((p, i) => ({ index: i, label: this.categories[i], prob: p })) .sort((a, b) => b.prob - a.prob) .slice(0, k)); } /* ========= Metrics ========= */ calculateRMSE(Y, P) { const N = Y.length, C = Y[0].length; let sum = 0; for (let i = 0; i < N; i++) for (let j = 0; j < C; j++) { const d = Y[i][j] - P[i][j]; sum += d *