Skip to content

Commit eafea58

Browse files
authored
Merge pull request #525 from MilesCranmer/split-parametric-search
test: split up mlj/templates
2 parents 425f336 + ba56f97 commit eafea58

File tree

4 files changed

+174
-168
lines changed

4 files changed

+174
-168
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ jobs:
9999
- ext/mlj/core
100100
- ext/mlj/mti
101101
- ext/mlj/templates
102+
- ext/mlj/parametric_search
102103
- ext/symbolicutils
103104
- ext/json3_recorder
104105
- ext/dynamicquantities_units
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[deps]
2+
MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d"
3+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4+
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
5+
TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe"
6+
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
@testitem "search with parametric template expressions" tags = [:part1] begin
2+
#! format: off
3+
#literate_begin file="src/examples/template_parametric_expression.md"
4+
#=
5+
# Parametrized Template Expressions
6+
7+
Template expressions in SymbolicRegression.jl can include parametric forms - expressions with tunable constants
8+
that are optimized during the search. This can even include class-specific parameters that vary by category.
9+
10+
In this tutorial, we'll demonstrate how to use parametric template expressions to learn a model where:
11+
12+
- Some constants are shared across all data points
13+
- Other constants vary by class
14+
- The structure combines known forms (like cosine) with unknown sub-expressions
15+
16+
=#
17+
18+
using SymbolicRegression
19+
using Random: MersenneTwister, randn, rand
20+
using MLJBase: machine, fit!, predict, report
21+
22+
#=
23+
## The Model Structure
24+
25+
We'll work with a model that combines:
26+
- A cosine term with class-specific phase shifts
27+
- A polynomial term
28+
- Global scaling parameters
29+
30+
Specifically, let's say that our true model has the form:
31+
32+
```math
33+
y = A \cos(f(x_2) + \Delta_c) + g(x_1) - B
34+
```
35+
36+
where:
37+
- ``A`` is a global amplitude (same for all classes)
38+
- ``\Delta_c`` is a phase shift that depends on the class label
39+
- ``f(x_2)`` is some function of ``x_2`` (in our case, just ``x_2``)
40+
- ``g(x_1)`` is some function of ``x_1`` (in our case, ``x_1^2``)
41+
- ``B`` is a global offset
42+
43+
We'll generate synthetic data where:
44+
- ``A = 2.0`` (amplitude)
45+
- ``\Delta_1 = 0.1`` (phase shift for class 1)
46+
- ``\Delta_2 = 1.5`` (phase shift for class 2)
47+
- ``B = 2.0`` (offset)
48+
=#
49+
50+
## Set random seed for reproducibility
51+
rng = MersenneTwister(0)
52+
53+
## Number of data points
54+
n = 200
55+
56+
## Generate random features
57+
x1 = randn(rng, n) # feature 1
58+
x2 = randn(rng, n) # feature 2
59+
class = rand(rng, 1:2, n) # class labels 1 or 2
60+
61+
## Define the true parameters
62+
Δ_phase = [0.1, 1.5] # phase shift for class 1 and 2
63+
A = 2.0 # amplitude
64+
B = 2.0 # offset
65+
66+
## Add some noise
67+
eps = randn(rng, n) * 1e-5
68+
69+
## Generate targets using the true underlying function
70+
y = [
71+
A * cos(x2[i] + Δ_phase[class[i]]) + x1[i]^2 - B
72+
for i in 1:n
73+
]
74+
y .+= eps
75+
76+
#=
77+
## Defining the Template
78+
79+
Now we'll use the `@template_spec` macro to encode this structure, which will create
80+
a `TemplateExpressionSpec` object.
81+
=#
82+
83+
## Define the template structure with sub-expressions f and g
84+
template = @template_spec(
85+
expressions=(f, g),
86+
parameters=(p1=2, p2=2)
87+
) do x1, x2, class
88+
return p1[1] * cos(f(x2) + p2[class]) + g(x1) - p1[2]
89+
end
90+
91+
#=
92+
Let's break down this template:
93+
- We declared two sub-expressions: `f` and `g` that we want to learn
94+
- By calling `f(x2)` and `g(x1)`, the forward pass will constrain both expressions
95+
to only include a single input argument.
96+
- We declared two parameter vectors: `p1` (length 2) and `p2` (length 2)
97+
- The template combines these components as:
98+
- `p1[1]` is the amplitude (global parameter)
99+
- `cos(f(x2) + p2[class])` adds a class-specific phase shift via `p2[class]`
100+
- `g(x1)` represents (we hope) the quadratic term
101+
- `p1[2]` is the global offset
102+
103+
Now we'll set up an SRRegressor with our template:
104+
=#
105+
106+
model = SRRegressor(
107+
binary_operators = (+, -, *, /),
108+
niterations = 300,
109+
populations = 8,
110+
maxsize = 20,
111+
expression_spec = template,
112+
early_stop_condition = (loss, complexity) -> loss < 1e-5 && complexity < 10, #src
113+
)
114+
115+
## Package data up for MLJ
116+
X = (; x1, x2, class)
117+
mach = machine(model, X, y)
118+
119+
#=
120+
At this point, you would run:
121+
```julia
122+
fit!(mach)
123+
```
124+
125+
which will evolve expressions following our template structure. The final result is accessible with:
126+
```julia
127+
report(mach)
128+
```
129+
which returns a named tuple of the fitted results, including the `.equations` field containing
130+
the `TemplateExpression` objects that dominated the Pareto front.
131+
132+
## Interpreting Results
133+
134+
After training, you can inspect the expressions found:
135+
```julia
136+
r = report(mach)
137+
best_expr = r.equations[r.best_idx]
138+
```
139+
140+
You can also extract the individual sub-expressions (stored as `ComposableExpression` objects):
141+
```julia
142+
inner_exprs = get_contents(best_expr)
143+
metadata = get_metadata(best_expr)
144+
```
145+
146+
The learned expression should closely match our true generating function:
147+
- `f(x2)` should be approximately `x2` (note it will show up as `x1` in the raw contents, but this simply is a relative indexing of its arguments!)
148+
- `g(x1)` should be approximately `x1^2`
149+
- The parameters should be close to their true values:
150+
- `p1[1] ≈ 2.0` (amplitude)
151+
- `p1[2] ≈ 2.0` (offset)
152+
- `p2[1] ≈ 0.1 mod 2π` (phase shift for class 1)
153+
- `p2[2] ≈ 1.5 mod 2π` (phase shift for class 2)
154+
155+
You can use the learned expression to make predictions using either `predict(mach, X)`,
156+
or by calling `best_expr(X_raw)` directly (note that `X_raw` needs to be a matrix of shape
157+
`(d, n)` where `n` is the number of samples and `d` is the dimension of the features).
158+
=#
159+
160+
#literate_end
161+
#! format: on
162+
163+
fit!(mach)
164+
165+
num_exprs = length(report(mach).equations)
166+
@test sum(abs2, predict(mach, (data=X, idx=num_exprs)) .- y) / n < 1e-5
167+
end

test/integration/ext/mlj/templates/test_parametric_template_expressions.jl

Lines changed: 0 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -196,174 +196,6 @@ end
196196
end
197197
end
198198

199-
@testitem "search with parametric template expressions" tags = [:part1] begin
200-
#! format: off
201-
#literate_begin file="src/examples/template_parametric_expression.md"
202-
#=
203-
# Parametrized Template Expressions
204-
205-
Template expressions in SymbolicRegression.jl can include parametric forms - expressions with tunable constants
206-
that are optimized during the search. This can even include class-specific parameters that vary by category.
207-
208-
In this tutorial, we'll demonstrate how to use parametric template expressions to learn a model where:
209-
210-
- Some constants are shared across all data points
211-
- Other constants vary by class
212-
- The structure combines known forms (like cosine) with unknown sub-expressions
213-
214-
=#
215-
216-
using SymbolicRegression
217-
using Random: MersenneTwister, randn, rand
218-
using MLJBase: machine, fit!, predict, report
219-
220-
#=
221-
## The Model Structure
222-
223-
We'll work with a model that combines:
224-
- A cosine term with class-specific phase shifts
225-
- A polynomial term
226-
- Global scaling parameters
227-
228-
Specifically, let's say that our true model has the form:
229-
230-
```math
231-
y = A \cos(f(x_2) + \Delta_c) + g(x_1) - B
232-
```
233-
234-
where:
235-
- ``A`` is a global amplitude (same for all classes)
236-
- ``\Delta_c`` is a phase shift that depends on the class label
237-
- ``f(x_2)`` is some function of ``x_2`` (in our case, just ``x_2``)
238-
- ``g(x_1)`` is some function of ``x_1`` (in our case, ``x_1^2``)
239-
- ``B`` is a global offset
240-
241-
We'll generate synthetic data where:
242-
- ``A = 2.0`` (amplitude)
243-
- ``\Delta_1 = 0.1`` (phase shift for class 1)
244-
- ``\Delta_2 = 1.5`` (phase shift for class 2)
245-
- ``B = 2.0`` (offset)
246-
=#
247-
248-
## Set random seed for reproducibility
249-
rng = MersenneTwister(0)
250-
251-
## Number of data points
252-
n = 200
253-
254-
## Generate random features
255-
x1 = randn(rng, n) # feature 1
256-
x2 = randn(rng, n) # feature 2
257-
class = rand(rng, 1:2, n) # class labels 1 or 2
258-
259-
## Define the true parameters
260-
Δ_phase = [0.1, 1.5] # phase shift for class 1 and 2
261-
A = 2.0 # amplitude
262-
B = 2.0 # offset
263-
264-
## Add some noise
265-
eps = randn(rng, n) * 1e-5
266-
267-
## Generate targets using the true underlying function
268-
y = [
269-
A * cos(x2[i] + Δ_phase[class[i]]) + x1[i]^2 - B
270-
for i in 1:n
271-
]
272-
y .+= eps
273-
274-
#=
275-
## Defining the Template
276-
277-
Now we'll use the `@template_spec` macro to encode this structure, which will create
278-
a `TemplateExpressionSpec` object.
279-
=#
280-
281-
## Define the template structure with sub-expressions f and g
282-
template = @template_spec(
283-
expressions=(f, g),
284-
parameters=(p1=2, p2=2)
285-
) do x1, x2, class
286-
return p1[1] * cos(f(x2) + p2[class]) + g(x1) - p1[2]
287-
end
288-
289-
#=
290-
Let's break down this template:
291-
- We declared two sub-expressions: `f` and `g` that we want to learn
292-
- By calling `f(x2)` and `g(x1)`, the forward pass will constrain both expressions
293-
to only include a single input argument.
294-
- We declared two parameter vectors: `p1` (length 2) and `p2` (length 2)
295-
- The template combines these components as:
296-
- `p1[1]` is the amplitude (global parameter)
297-
- `cos(f(x2) + p2[class])` adds a class-specific phase shift via `p2[class]`
298-
- `g(x1)` represents (we hope) the quadratic term
299-
- `p1[2]` is the global offset
300-
301-
Now we'll set up an SRRegressor with our template:
302-
=#
303-
304-
model = SRRegressor(
305-
binary_operators = (+, -, *, /),
306-
niterations = 300,
307-
populations = 8,
308-
maxsize = 20,
309-
expression_spec = template,
310-
early_stop_condition = (loss, complexity) -> loss < 1e-5 && complexity < 10, #src
311-
)
312-
313-
## Package data up for MLJ
314-
X = (; x1, x2, class)
315-
mach = machine(model, X, y)
316-
317-
#=
318-
At this point, you would run:
319-
```julia
320-
fit!(mach)
321-
```
322-
323-
which will evolve expressions following our template structure. The final result is accessible with:
324-
```julia
325-
report(mach)
326-
```
327-
which returns a named tuple of the fitted results, including the `.equations` field containing
328-
the `TemplateExpression` objects that dominated the Pareto front.
329-
330-
## Interpreting Results
331-
332-
After training, you can inspect the expressions found:
333-
```julia
334-
r = report(mach)
335-
best_expr = r.equations[r.best_idx]
336-
```
337-
338-
You can also extract the individual sub-expressions (stored as `ComposableExpression` objects):
339-
```julia
340-
inner_exprs = get_contents(best_expr)
341-
metadata = get_metadata(best_expr)
342-
```
343-
344-
The learned expression should closely match our true generating function:
345-
- `f(x2)` should be approximately `x2` (note it will show up as `x1` in the raw contents, but this simply is a relative indexing of its arguments!)
346-
- `g(x1)` should be approximately `x1^2`
347-
- The parameters should be close to their true values:
348-
- `p1[1] ≈ 2.0` (amplitude)
349-
- `p1[2] ≈ 2.0` (offset)
350-
- `p2[1] ≈ 0.1 mod 2π` (phase shift for class 1)
351-
- `p2[2] ≈ 1.5 mod 2π` (phase shift for class 2)
352-
353-
You can use the learned expression to make predictions using either `predict(mach, X)`,
354-
or by calling `best_expr(X_raw)` directly (note that `X_raw` needs to be a matrix of shape
355-
`(d, n)` where `n` is the number of samples and `d` is the dimension of the features).
356-
=#
357-
358-
#literate_end
359-
#! format: on
360-
361-
fit!(mach)
362-
363-
num_exprs = length(report(mach).equations)
364-
@test sum(abs2, predict(mach, (data=X, idx=num_exprs)) .- y) / n < 1e-5
365-
end
366-
367199
@testitem "Preallocated copying with parameters" tags = [:part2] begin
368200
using SymbolicRegression
369201
using Random: MersenneTwister

0 commit comments

Comments
 (0)