Gradient Descent
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
gradient-descent.ts
import type { Tracer } from "../trace";/** Something with a height and a slope at every point. */export interface Differentiable { value(point: readonly number[]): number; gradient(point: readonly number[]): number[];}/** * The bowl f(p) = ½ Σ aᵢ pᵢ²: a different curvature per axis, so the * minimum is at the origin and every descent step is a plain product * per axis (pᵢ becomes (1 − η aᵢ) pᵢ). Closed form makes every claim * about the rate checkable by hand. */export class QuadraticBowl implements Differentiable { constructor(readonly curvatures: readonly number[]) { if (curvatures.length === 0 || curvatures.some((a) => !(a > 0))) throw new RangeError("every curvature must be positive"); } value(point: readonly number[]): number { let sum = 0; for (let i = 0; i < this.curvatures.length; i++) sum += this.curvatures[i]! * (point[i] ?? 0) ** 2; return sum / 2; } gradient(point: readonly number[]): number[] { return this.curvatures.map((a, i) => a * (point[i] ?? 0)); }}export type HaltReason = "converged" | "diverged";export interface DescentResult { /** Every position visited, the start first. */ path: number[][]; /** The height at each position of the path. */ losses: number[]; halted: HaltReason | null; /** Steps actually taken. */ iterations: number;}/** * Gradient descent: from a start, repeat "measure the slope, step * against it" with a fixed learning rate. The slope points uphill, so * p ← p − η ∇f(p) walks down; too small a rate crawls, too large a rate * overshoots and climbs. Halts early when the slope is nearly flat or * the loss has blown up. * * Time: O(k) gradient evaluations for k iterations. Space: O(k) for the path. */export class GradientDescent { constructor( readonly objective: Differentiable, readonly learningRate: number, private readonly tracer?: Tracer, ) { if (!(learningRate > 0)) throw new RangeError("the learning rate must be positive"); } run(start: readonly number[], iterations: number, tolerance = 1e-4, blowUp = 1e6): DescentResult { if (!Number.isInteger(iterations) || iterations < 0) throw new RangeError("iterations must be a whole number"); let p = [...start]; let position = [...start]; const path = [p]; const path = [position]; const losses: number[] = []; let halted: HaltReason | null = null; let iteration = 0; for (; iteration < iterations; iteration++) { const loss = this.objective.value(p); const loss = this.objective.value(position); losses.push(loss); // ▸ measure how high the bowl is here this.tracer?.emit("loss", { iteration, position: [...p], loss }); this.tracer?.emit("loss", { iteration, position: [...position], loss }); const g = this.objective.gradient(p); const slope = this.objective.gradient(position); const norm = Math.hypot(...g); const norm = Math.hypot(...slope); // ▸ measure which way is uphill, and how steep this.tracer?.emit("gradient", { iteration, gradient: [...g], norm }); this.tracer?.emit("gradient", { iteration, gradient: [...slope], norm }); if (norm < tolerance || loss > blowUp) { halted = norm < tolerance ? "converged" : "diverged"; // ▸ flat enough to stop, or so high the run has blown up this.tracer?.emit("halt", { iteration, reason: halted, loss }); break; } const to = p.map((x, i) => x - this.learningRate * g[i]!); const nextPosition = position.map((x, i) => x - this.learningRate * slope[i]!); const stepSize = Math.hypot(...to.map((x, i) => x - p[i]!)); const stepSize = Math.hypot(...nextPosition.map((x, i) => x - position[i]!)); // ▸ walk against the slope, scaled by the learning rate this.tracer?.emit("step", { iteration, from: [...p], to: [...to], stepSize }); this.tracer?.emit("step", { iteration, from: [...position], nextPosition: [...nextPosition], stepSize }); p = to; position = nextPosition; path.push(p); path.push(position); } if (halted === null) losses.push(this.objective.value(p)); if (halted === null) losses.push(this.objective.value(position)); return { path, losses, halted, iterations: path.length - 1 }; }}20 steps taken; the budget ran out with the loss at 0.000.