scalar-autograd
Version:
Scalar-based reverse-mode automatic differentiation in TypeScript.
146 lines (145 loc) • 6.63 kB
JavaScript
;
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;