Skip to content

Commit 3b365f6

Browse files
alxmrsclaude
andcommitted
demo: the whole MLP as one weight relation (bias folded, layer dim)
Two simplifications collapse the model to a single relation: - Bias folded into the weights (an nn.Linear): each layer's bias is the weight of a constant-1 input, kept as the row inp=width of the same weight array, so a layer is one matrix. - A layer dimension: every layer's weight lives in one weight(layer, inp, out) array, so forward/backward filter on the layer COLUMN instead of referencing a table per layer. The model is one xr.Dataset with a layer dim (NaN-padded for the ragged pyramid, dropped on seed); from_dataset registers it; the update is one query over the whole weight relation. A single contract() and a generic loop train a net of any depth (validated exact against numpy at depth 3). Tensors.put now unifies batch nullability so UNION results register cleanly. Faster too (~6s vs ~13s) at the same ~83% test accuracy; model and metrics still round-trip to xarray. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c92447a commit 3b365f6

2 files changed

Lines changed: 182 additions & 158 deletions

File tree

benchmarks/README.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,31 +77,38 @@ Contracting a shared index is a join on it followed by a grouped `SUM` over the
7777
indices that survive. In xarray-sql an array indexed by named dims is a table
7878
keyed by those dims, so **the dimension names are the join keys**.
7979

80-
**The architecture is data.** The whole model is *one* `xr.Dataset`: each layer's
81-
weight is a data variable `w{L}` over dims `(u{L}, u{L+1})`, the widths it
82-
connects, sharing the boundary dims (`u1` is layer 0's output and layer 1's
83-
input, so it is the join key between them). The dim sizes *are* the layer widths,
84-
and the number of weights is the depth — differing neuron counts per layer are
85-
just differing dim sizes, no padding, because the relational (long) form is
86-
naturally ragged. `from_dataset` splits that one Dataset into a table per weight
87-
automatically. Change `WIDTHS` (e.g. `196, 64, 32, 10`) and the same code trains
88-
the deeper net.
80+
**The whole network is one relation.** Two moves get there:
81+
82+
- **Bias folded into the weights (an `nn.Linear`).** Each layer's bias is the
83+
weight of a constant-`1` input, kept as the extra row `inp = width` of the same
84+
weight array — so a layer is a single matrix.
85+
- **A `layer` dimension.** Every layer's weight lives in one
86+
`weight(layer, inp, out)` array, so the forward/backward filter on the `layer`
87+
*column* instead of referencing a table per layer.
88+
89+
So **the architecture is data**: the whole model is one `xr.Dataset` with a
90+
`layer` dim, registered via `from_dataset`. The dim sizes are the layer widths
91+
and the number of layers is the depth — differing neuron counts are just
92+
differing sizes, NaN-padded in the dense array and dropped on the way in (the
93+
relational form is naturally ragged). Change `WIDTHS` (e.g. `196, 64, 32, 10`)
94+
and the same code trains the deeper net.
8995

9096
A small `contract()` helper turns an einsum spec into one query, and a single
9197
generic loop trains a net of any shape:
9298

93-
- **forward** contracts the activation with each layer's weight, `+ bias`,
94-
`tanh` (softmax on the last layer).
99+
- **forward** contracts the activation with `weight WHERE layer = L`, adds the
100+
bias row, `tanh` (softmax on the last layer).
95101
- **backward is the *same* operator with indices transposed** — the VJP of a
96-
contraction is a contraction — and `grad(tanh(z), z)` supplies the only
97-
genuinely-calculus part. Linear algebra is joins; the derivatives of the
98-
nonlinearities are `grad`.
102+
contraction is a contraction — accumulated into one `gweight` relation, with
103+
`grad(tanh(z), z)` for the only genuinely-calculus part. Even the update is one
104+
query over the whole `weight` relation. Linear algebra is joins; the
105+
derivatives of the nonlinearities are `grad`.
99106

100107
Everything stays relational: every stage is an inspectable table (`a1`, `delta2`,
101-
`gw0`, …); the only hand-written gradient is softmax + cross-entropy's `delta =
102-
softmax - onehot`. Even the training metrics are a table — each logged step
103-
appends a `(step, loss, train_acc, test_acc)` row to a `metrics` relation rather
104-
than a Python list (NN training produces a lot of such data; it belongs in
108+
`gweight`, …); the only hand-written gradient is softmax + cross-entropy's
109+
`delta = softmax - onehot`. Even the training metrics are a table — each logged
110+
step appends a `(step, loss, train_acc, test_acc)` row to a `metrics` relation
111+
rather than a Python list (NN training produces a lot of such data; it belongs in
105112
rows). Evaluation is SQL too (a forward pass + `ROW_NUMBER()` argmax), and the
106113
trained model, predictions, and metrics all come **back out as xarray** via
107114
`to_dataset`. Reaches ~83% test accuracy over 60 steps. Downloads MNIST on first

0 commit comments

Comments
 (0)