Understand

Models and processes

Stateful objects declared in Hilbert — trainable learners, seeded stochastic processes and typed data frames.

Formulas are stateless. When you need state — weights that learn, a random walk that advances, a table read from a file — you declare it in a .hb file and the compiler generates a sealed class. The mathematics stays in Hilbert, usually in Prelude definitions; the class is the doorway.

Learners

-- Line.hb — a least-squares line, trained by gradient steps.

model Line(seed) : Learner {

  weight slope     : Tensor[1] from 0
  weight intercept : Tensor[1] from 0

  def train(x, y, rate) {
    slope     ≔ slope − rate · einsum("i,i->", x, slope · x + intercept − y) / rows(x)
    intercept ≔ intercept − rate · einsum("i->", slope · x + intercept − y) / rows(x)
  }

  def predict(x) ≔ slope · x + intercept
}
  • Parameters (Line(seed)) become constructor arguments.
  • weight name : Tensor[dims] from init declares learned state. from is a constant, or a seeded draw — from gaussian(seed) · 0.5, from uniform(seed) — so a model’s starting point is reproducible from its declaration. Each weight becomes a public property.
  • def train(…) { … } is a block of assignments. Every right-hand side reads the weights as they were when train was entered, so the updates are simultaneous.
  • Expression members (def predict(x) ≔ …) become methods.

The generated class:

public sealed class Line
{
    public Line(Tensor seed);
    public Tensor Slope { get; }
    public Tensor Intercept { get; }
    public void Train(Tensor x, Tensor y, Tensor rate);
    public Tensor Predict(Tensor x);
}
var line = new Line(seed: 1);
Tensor x = new[] { 0d, 1d, 2d, 3d };
Tensor y = new[] { 1d, 3d, 5d, 7d };
for (var i = 0; i < 2000; i++) line.Train(x, y, 0.05);

// slope [2], intercept [1] (to four decimals); Predict([10]) = [21]

Most learners call Prelude steps rather than writing gradients inline. A two-layer classifier uses Prelude.Networks:

open Prelude.Networks
open Prelude.Kernels

model Classifier(inputs, hidden, classes, seed) : Learner {

  weight first  : Tensor[inputs, hidden]  from gaussian(seed) · 0.5
  weight second : Tensor[hidden, classes] from gaussian(seed) · 0.5

  def train(x, y, rate) {
    first  ≔ stepFirst(rate, x, y, first, second)
    second ≔ stepSecond(rate, x, y, first, second)
  }

  def shares(x)  ≔ softmaxRows(outputs(x, first, second))
  def predict(x) ≔ highest(softmaxRows(outputs(x, first, second)))
}

and tabular Q-learning uses Prelude.Temporal:

open Prelude.Temporal

model QTable(states, actions) : Learner {

  weight q : Tensor[states, actions] from 0

  def train(s, a, rw, s2, done, rate, gamma) {
    q ≔ nudged(q, marked(stateIds(q), s), marked(actionIds(q), a), qTarget(q, marked(stateIds(q), s2), rw, gamma, done), rate)
  }

  def worth ≔ rowMost(q)
  def plan  ≔ greedyOf(q)
  def act(s, eps) ≔ einsum("is,sa->a", marked(stateIds(q), s), exploring(q, eps))
}
var q = new QTable(2, 2);
Tensor s = new[] { 0d }, a = new[] { 1d }, r = new[] { 1d }, s2 = new[] { 1d }, done = new[] { 1d };
for (var i = 0; i < 50; i++) q.Train(s, a, r, s2, done, 0.5, 0.9);

// q.Q      = [[0, 1], [0, 0]]
// q.Plan() = [[0, 1], [0.5, 0.5]]   — greedy in state 0, a tie in the unvisited state 1

Processes

A process is a stochastic model whose random stream is part of its declaration:

open Prelude.Paths

process Walk(p, steps, seed) {

  draw   jump : Tensor[steps] from uniform(seed)
  weight at   : Tensor[1]     from 0.0

  def advance(k) { at ≔ at + stepOf(jump, k, p) }

  def spread ≔ 4 · p · (1 − p) · steps
}
  • draw name : Tensor[dims] from uniform(seed) is generated once, at construction, from the seed — so new Walk(0.5, 256, 20260910) fully describes a run, and two such objects produce the same path.
  • def advance(k) { … } is one step. You never write the trajectory: the class gets a Path(n) method that applies the step n times and keeps every state.
var walk = new Walk(0.5, 16, 20260910);
Tensor path = walk.Path(16);     // shape [16, 1]: -1, -2, -1, -2, -1, 0, -1, -2, -1, -2, -3, -4, -5, -6, -7, -6
Tensor spread = walk.Spread();   // 16 — the variance of the end point, 4p(1−p)n

The stochastic-process modules — Paths, Chains, Points, Jumps, Diffusions and others — provide the step functions processes are written with.

Frames

A frame is a typed view of a CSV file:

model House(path) : Frame(path) {
  quality  : Scalar from "Overall Qual"
  living   : Scalar from "Gr Liv Area"
  frontage : Scalar from "Lot Frontage"
  sold     : Scalar from "Yr Sold"
  built    : Scalar from "Year Built"
  price    : Scalar from "SalePrice"

  zoning   : Text from "MS Zoning"

  def houseAge ≔ sold − built

  def zone ≔ match zoning {
    when "RL" ↦ 1
    when "RM" | "RH" ↦ 2
    other ↦ 0
  }

  fill frontage with median
}
  • Each column names its header in the file and its kind: Scalar for numbers, Text for words.
  • Derived columns are definitions over other columns; a match over a text column turns words into codes.
  • fill column with median replaces missing cells.
  • Constructing the class reads the file, fills and derives: new House("data/houses.csv"). Columns are properties (house.Living, house.HouseAge), and house.Where(mask) keeps the rows where a 0/1 mask is 1. A relative path is tried from the working directory and then beside the running assembly.

A frame’s columns are ordinary tensors, so they go straight into your doorways — for example Summaries.Average(house.Price) with the average doorway from Getting started, a regression over a design matrix, or a learner’s Train.

Categories

A model with no parameter list that declares draws, weights, defs or a step is a category: an algorithm with its problem left out. A process that names it as its parent is flattened into one process before anything is checked — nearest declaration wins, a category’s defs may call defs only the child supplies, and a category is never a class of its own.

model Drifting {
  draw   push  : Tensor[width] from fresh(uniform(seed))
  weight state : Tensor[width] from 0.0
  def advance(k) { state ≔ state + push }
  def total ≔ einsum("i->", state)
}

process Walker(width, seed) : Drifting {
  def total ≔ einsum("i->", state) · 2
}

Three process features make categories practical:

  • from fresh(uniform(seed)) renews a draw before every step from a generator seeded once, so a draw holds one step’s numbers and equal seeds still give equal paths.
  • A weight may start from a formula over the parameters and draws — weight order : Tensor[n] from below(push) — run once by the constructor.
  • A process may carry several weights; Path keeps the first one declared.

The Evolution prelude is built this way; Writing a genetic algorithm walks through one.