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 initdeclares learned state.fromis 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 whentrainwas 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 — sonew 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 aPath(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:
Scalarfor numbers,Textfor words. - Derived columns are definitions over other columns; a
matchover a text column turns words into codes. fill column with medianreplaces missing cells.- Constructing the class reads the file, fills and derives:
new House("data/houses.csv"). Columns are properties (house.Living,house.HouseAge), andhouse.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;
Pathkeeps the first one declared.
The Evolution prelude is built this way; Writing a genetic algorithm walks through one.