Skip to content

Commit 09e2bb6

Browse files
authored
Merge pull request #1041 from MilesCranmer/vector-example-docs
docs: add vector expression example
2 parents 6508cf5 + 541f60b commit 09e2bb6

File tree

1 file changed

+128
-2
lines changed

1 file changed

+128
-2
lines changed

docs/examples.md

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,133 @@ You can then view the logs with:
670670
tensorboard --logdir logs/
671671
```
672672

673-
## 13. Using differential operators
673+
## 13. Vector-valued expressions
674+
675+
You can use `TemplateExpressionSpec` to find expressions for vector-valued data,
676+
where each component might share a common structure.
677+
The trick is to put each vector element into your feature matrix `X`,
678+
and then use a template expression to define the relationships.
679+
680+
For example, say we have 3-dimensional vectors where each component
681+
follows a pattern with a shared term. Say the true model is:
682+
683+
$$\begin{align*}
684+
y_1 &= \exp(x_1) + x_2^2 \\
685+
y_2 &= \exp(x_1) + \sin(x_3) \\
686+
y_3 &= \exp(x_1) + x_1 \cdot x_2
687+
\end{align*}$$
688+
689+
Let's set this up:
690+
691+
```python
692+
import numpy as np
693+
from pysr import PySRRegressor, TemplateExpressionSpec
694+
695+
n = 200
696+
rstate = np.random.RandomState(0)
697+
x1 = rstate.uniform(-2, 2, n)
698+
x2 = rstate.uniform(-2, 2, n)
699+
x3 = rstate.uniform(-2, 2, n)
700+
701+
# True model with shared component exp(x1):
702+
y1 = np.exp(x1) + x2**2
703+
y2 = np.exp(x1) + np.sin(x3)
704+
y3 = np.exp(x1) + x1 * x2
705+
706+
# Add some noise
707+
y1 += 0.05 * rstate.randn(n)
708+
y2 += 0.05 * rstate.randn(n)
709+
y3 += 0.05 * rstate.randn(n)
710+
```
711+
712+
Now, we put everything in `X`; BOTH features and targets:
713+
714+
```python
715+
X = np.column_stack([x1, x2, x3, y1, y2, y3])
716+
```
717+
718+
Now, we can define our template expression:
719+
720+
```python
721+
spec = TemplateExpressionSpec(
722+
expressions=["f1", "f2", "f3", "shared"],
723+
variable_names=["x1", "x2", "x3", "y1", "y2", "y3"],
724+
combine="""
725+
v = shared(x1, x2, x3)
726+
y1_predicted = v + f1(x1, x2, x3)
727+
y2_predicted = v + f2(x1, x2, x3)
728+
y3_predicted = v + f3(x1, x2, x3)
729+
730+
residuals = (
731+
abs2(y1 - y1_predicted) +
732+
abs2(y2 - y2_predicted) +
733+
abs2(y3 - y3_predicted)
734+
)
735+
736+
residuals
737+
"""
738+
)
739+
```
740+
741+
Now, we can fit our model using this template. Since
742+
we already computed the per-row squared error inside the template,
743+
we can pass a dummy `y` to the `fit` method, and also define
744+
an `elementwise_loss` that simply returns the residuals (which get
745+
summed over the data):
746+
747+
```python
748+
model = PySRRegressor(
749+
expression_spec=spec,
750+
binary_operators=["+", "-", "*", "/"],
751+
unary_operators=["exp", "sin"],
752+
maxsize=20,
753+
niterations=50,
754+
elementwise_loss="(pred, target) -> pred",
755+
)
756+
757+
dummy_y = np.zeros(n)
758+
model.fit(X, dummy_y)
759+
```
760+
761+
After running, PySR should find both the shared component (`exp(x1)`) as well as individual components (`square(x2)`, `sin(x3)`, and `x1 * x2`).
762+
763+
You can access the individual expressions through the Julia objects:
764+
765+
```python
766+
# Simply get the expression with the highest score:
767+
idx = model.equations_.score.idxmax()
768+
769+
# Extract the Julia object:
770+
julia_expr = model.equations_.loc[idx, 'julia_expression']
771+
772+
# Access individual subexpressions:
773+
for name in ['f1', 'f2', 'f3', 'shared']:
774+
tree = getattr(julia_expr.trees, name)
775+
print(f"{name}: {tree}")
776+
```
777+
778+
We can also evaluate individual expressions:
779+
780+
```python
781+
from pysr import jl
782+
from pysr.julia_helpers import jl_array
783+
784+
SR = jl.SymbolicRegression
785+
786+
# Get individual trees
787+
f1_tree = julia_expr.trees.f1
788+
shared_tree = julia_expr.trees.shared
789+
790+
# Evaluate at specific points (x1=1, x2=2, x3=3)
791+
test_inputs = jl_array(np.array([[1.0], [2.0], [3.0]]))
792+
f1_result, _ = SR.eval_tree_array(f1_tree, test_inputs, model.julia_options_)
793+
shared_result, _ = SR.eval_tree_array(shared_tree, test_inputs, model.julia_options_)
794+
795+
print(f"f1 at (1,2,3): {f1_result[0]}") # Should be ~4.0 for x2^2
796+
print(f"shared at (1,2,3): {shared_result[0]}") # Should be ~2.718 for exp(1)
797+
```
798+
799+
## 14. Using differential operators
674800

675801
As part of the `TemplateExpressionSpec` described above,
676802
you can also use differential operators within the template.
@@ -710,7 +836,7 @@ If everything works, you should find something that simplifies to $\frac{\sqrt{x
710836

711837
Here, we write out a full function in Julia.
712838

713-
## 14. Additional features
839+
## 15. Additional features
714840

715841
For the many other features available in PySR, please
716842
read the [Options section](options.md).

0 commit comments

Comments
 (0)