UNPKG

scalar-autograd

Version:

Scalar-based reverse-mode automatic differentiation in TypeScript.

146 lines (145 loc) 6.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Losses = void 0; const Value_1 = require("./Value"); const V_1 = require("./V"); /** * Throws an error if outputs and targets length do not match. * @param outputs Array of output Values. * @param targets Array of target Values. */ function checkLengthMatch(outputs, targets) { if (outputs.length !== targets.length) { throw new Error('Outputs and targets must have the same length'); } } class Losses { /** * Computes mean squared error (MSE) loss between outputs and targets. * @param outputs Array of Value predictions. * @param targets Array of Value targets. * @returns Mean squared error as a Value. */ static mse(outputs, targets) { checkLengthMatch(outputs, targets); if (!Array.isArray(outputs) || !Array.isArray(targets)) throw new TypeError('mse expects Value[] for both arguments.'); if (!outputs.length) return new Value_1.Value(0); const diffs = outputs.map((out, i) => out.sub(targets[i]).square()); return Value_1.Value.mean(diffs); } /** * Computes mean absolute error (MAE) loss between outputs and targets. * @param outputs Array of Value predictions. * @param targets Array of Value targets. * @returns Mean absolute error as a Value. */ static mae(outputs, targets) { checkLengthMatch(outputs, targets); if (!Array.isArray(outputs) || !Array.isArray(targets)) throw new TypeError('mae expects Value[] for both arguments.'); if (!outputs.length) return new Value_1.Value(0); const diffs = outputs.map((out, i) => out.sub(targets[i]).abs()); return Value_1.Value.mean(diffs); } static EPS = 1e-12; /** * Computes binary cross-entropy loss between predicted outputs and targets (after sigmoid). * @param outputs Array of Value predictions (expected in (0,1)). * @param targets Array of Value targets (typically 0 or 1). * @returns Binary cross-entropy loss as a Value. */ static binaryCrossEntropy(outputs, targets) { checkLengthMatch(outputs, targets); if (!Array.isArray(outputs) || !Array.isArray(targets)) throw new TypeError('binaryCrossEntropy expects Value[] for both arguments.'); if (!outputs.length) return new Value_1.Value(0); const eps = Losses.EPS; const one = new Value_1.Value(1); const losses = outputs.map((out, i) => { const t = targets[i]; const outClamped = out.clamp(eps, 1 - eps); // sigmoid should output (0,1) return t.mul(outClamped.log()).add(one.sub(t).mul(one.sub(outClamped).log())); }); return Value_1.Value.mean(losses).mul(-1); } /** * Computes categorical cross-entropy loss between outputs (logits) and integer target classes. * @param outputs Array of Value logits for each class. * @param targets Array of integer class indices (0-based, one per sample). * @returns Categorical cross-entropy loss as a Value. */ static categoricalCrossEntropy(outputs, targets) { // targets: integer encoded class indices if (!Array.isArray(outputs) || !Array.isArray(targets)) throw new TypeError('categoricalCrossEntropy expects Value[] and number[].'); if (!outputs.length || !targets.length) return new Value_1.Value(0); if (targets.some(t => typeof t !== 'number' || !isFinite(t) || t < 0 || t >= outputs.length || Math.floor(t) !== t)) { throw new Error('Target indices must be valid integers in [0, outputs.length)'); } const eps = Losses.EPS; const maxLogit = outputs.reduce((a, b) => a.data > b.data ? a : b); const exps = outputs.map(out => out.sub(maxLogit).exp()); const sumExp = Value_1.Value.sum(exps).add(eps); const softmax = exps.map(e => e.div(sumExp)); const tIndices = targets.map((t, i) => softmax[t]); return Value_1.Value.mean(tIndices.map(sm => sm.add(eps).log().mul(-1))); } /** * Computes Huber loss between outputs and targets. * Combines quadratic loss for small residuals and linear loss for large residuals. * @param outputs Array of Value predictions. * @param targets Array of Value targets. * @param delta Threshold at which to switch from quadratic to linear (default: 1.0). * @returns Huber loss as a Value. */ static huber(outputs, targets, delta = 1.0) { checkLengthMatch(outputs, targets); if (!Array.isArray(outputs) || !Array.isArray(targets)) throw new TypeError('huber expects Value[] for both arguments.'); if (!outputs.length) return new Value_1.Value(0); const deltaValue = new Value_1.Value(delta); const half = new Value_1.Value(0.5); const losses = outputs.map((out, i) => { const residual = V_1.V.abs(V_1.V.sub(out, targets[i])); const condition = V_1.V.lt(residual, deltaValue); const quadraticLoss = V_1.V.mul(half, V_1.V.square(residual)); const linearLoss = V_1.V.mul(deltaValue, V_1.V.sub(residual, V_1.V.mul(half, deltaValue))); return V_1.V.ifThenElse(condition, quadraticLoss, linearLoss); }); return V_1.V.mean(losses); } /** * Computes Tukey loss between outputs and targets. * This robust loss function saturates for large residuals. * * @param outputs Array of Value predictions. * @param targets Array of Value targets. * @param c Threshold constant (typically 4.685). * @returns Tukey loss as a Value. */ static tukey(outputs, targets, c = 4.685) { checkLengthMatch(outputs, targets); const c2_over_6 = (c * c) / 6; const cValue = V_1.V.C(c); const c2_over_6_Value = V_1.V.C(c2_over_6); const losses = outputs.map((out, i) => { const diff = V_1.V.abs(V_1.V.sub(out, targets[i])); const inlier = V_1.V.lte(diff, cValue); const rc = V_1.V.div(diff, cValue); const rc2 = V_1.V.square(rc); const oneMinusRC2 = V_1.V.sub(1, rc2); const inner = V_1.V.pow(oneMinusRC2, 3); const inlierLoss = V_1.V.mul(c2_over_6_Value, V_1.V.sub(1, inner)); const loss = V_1.V.ifThenElse(inlier, inlierLoss, c2_over_6_Value); return loss; }); return V_1.V.mean(losses); } } exports.Losses = Losses;