1212"""Gradient descent as a single declarative SQL query.
1313
1414Fits a line ``y ~= a*x + b`` by minimising the mean squared error — with the
15- **entire training loop expressed as one recursive CTE**, no Python iteration.
16-
17- Two pieces:
18-
19- 1. **grad compiles the update rule.** ``differentiate_sql`` turns the per-row
20- loss into the symbolic derivative *as SQL text* — the autograd engine acting
21- as a calculus compiler:
22-
23- da = differentiate_sql("(y-(a*x+b))^2", "a") # -> "-2*((a*x+b)-y)*x", etc.
24-
25- 2. **A recursive CTE is the optimiser.** ``params(step, a, b)`` starts at one
26- row and each recursion appends the next generation, descending along the
27- gradient (``AVG`` of the compiled rule over the data):
28-
29- params.a - lr * AVG(da)
30-
31- So the whole loop — gradient, update, and iteration — is declarative SQL;
32- the optimisation trajectory is the rows of one query.
33-
34- Why two pieces instead of ``grad(...)`` directly inside the recursion? ``grad``
35- needs the Substrait round-trip, and Substrait has no recursion — so ``grad``
36- can't live inside a recursive CTE (tracked in #194 / a follow-up). Differentiating
37- once to plain SQL sidesteps that: the recursive query contains no ``grad`` marker.
15+ **entire training loop expressed as one recursive CTE**, no Python iteration and
16+ no precompiled update rule. ``grad(...)`` lives *inside* the recursion:
17+
18+ WITH RECURSIVE params(step, a, b) AS (
19+ SELECT 0, 0.0, 0.0
20+ UNION ALL
21+ SELECT params.step + 1,
22+ params.a - lr * AVG(grad(loss, a)),
23+ params.b - lr * AVG(grad(loss, b))
24+ FROM params CROSS JOIN d
25+ WHERE params.step < STEPS
26+ GROUP BY params.step, params.a, params.b)
27+ SELECT step, a, b FROM params ORDER BY step
28+
29+ Each recursion appends the next generation, descending along the gradient that
30+ ``grad`` computes from the loss formula directly. ``AVG(grad(loss, a))`` is the
31+ relational ``d/da (Σ loss) / N`` — differentiation through the aggregate is just
32+ linearity. So gradient, update, and iteration are all one declarative query; the
33+ optimisation trajectory is the rows of that query.
34+
35+ ``grad`` is differentiated as a SQL source-to-source rewrite *before* the query
36+ is planned, so the marker works inside the recursive CTE (and any other query
37+ shape) with no Substrait round-trip. The loss is written once, as ordinary SQL,
38+ and the engine differentiates it symbolically — the relational equivalent of
39+ ``jax.vmap(jax.grad(f))``, since each row is an independent evaluation point.
3840
3941Run standalone:
4042
4850
4951import xarray_sql as xql
5052
51- # Per-row loss r^2 with residual r = y - (a*x + b), over columns a, b, x, y.
53+ # Per-row loss r^2 with residual r = y - (a*x + b). The columns a, b come from
54+ # the recursive `params` relation; x, y come from the data table `d`.
5255RESIDUAL = "(y - (a * x + b))"
5356LOSS = f"{ RESIDUAL } * { RESIDUAL } "
54- COLUMNS = ["a" , "b" , "x" , "y" ]
5557LR = 0.4
5658STEPS = 200
5759
@@ -72,22 +74,17 @@ def main() -> None:
7274 chunks = {"i" : n },
7375 )
7476
75- # grad compiles the per-row update rule to SQL, once.
76- da = xql .differentiate_sql (LOSS , "a" , COLUMNS )
77- db = xql .differentiate_sql (LOSS , "b" , COLUMNS )
78- print (f"d(loss)/da = { da } " )
79- print (f"d(loss)/db = { db } \n " )
80-
8177 # The entire training loop is one declarative recursive query: each step
82- # appends the next generation, descending along the SQL-computed gradient.
78+ # appends the next generation, descending along the gradient that grad()
79+ # computes from the loss — differentiated inside the recursion itself.
8380 trajectory = ctx .sql (
8481 f"""
8582 WITH RECURSIVE params(step, a, b) AS (
8683 SELECT 0 AS step, CAST(0.0 AS DOUBLE) AS a, CAST(0.0 AS DOUBLE) AS b
8784 UNION ALL
88- SELECT params.step + 1 AS step,
89- params.a - { LR } * AVG({ da } ) AS a,
90- params.b - { LR } * AVG({ db } ) AS b
85+ SELECT params.step + 1 AS step,
86+ params.a - { LR } * AVG(grad( { LOSS } , a)) AS a,
87+ params.b - { LR } * AVG(grad( { LOSS } , b)) AS b
9188 FROM params CROSS JOIN d
9289 WHERE params.step < { STEPS }
9390 GROUP BY params.step, params.a, params.b
@@ -107,7 +104,8 @@ def main() -> None:
107104 print (f"least-squares (numpy): a={ a_ols :.4f} b={ b_ols :.4f} " )
108105 assert abs (a - a_ols ) < 1e-2 and abs (b - b_ols ) < 1e-2
109106 print (
110- "\n OK: a single recursive-CTE query fit the line to the OLS solution."
107+ "\n OK: a single recursive-CTE query with grad() inside fit the line "
108+ "to the OLS solution."
111109 )
112110
113111
0 commit comments