Skip to content

Commit 6a51719

Browse files
authored
Merge pull request #221 from simopt-admin/feature/erm
Feature/erm
2 parents 46b084a + a2236d3 commit 6a51719

6 files changed

Lines changed: 287 additions & 125 deletions

File tree

ruff.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# ruff.toml
22
line-length = 88
3-
exclude = ["simopt/gui/*", "notebooks/*"]
3+
exclude = [
4+
"simopt/gui/*",
5+
"notebooks/*",
6+
"simopt/models/ermexample.py",
7+
"workshop/erm_data_generator.ipynb",
8+
"workshop/workshop.ipynb"
9+
]
410

511
[lint]
612
select = [

simopt/models/ermexample.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""Example problem of deterministic function with noise.
2+
3+
Simulate a synthetic problem with a deterministic objective function
4+
evaluated with noise.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import Annotated, ClassVar
10+
11+
import numpy as np
12+
from pydantic import BaseModel, Field
13+
14+
from mrg32k3a.mrg32k3a import MRG32k3a
15+
from simopt.base import (
16+
ConstraintType,
17+
Model,
18+
Objective,
19+
Problem,
20+
RepResult,
21+
VariableType,
22+
)
23+
from simopt.input_models import InputModel
24+
25+
26+
class ERMExampleModelConfig(BaseModel):
27+
"""Configuration model for ERMExample simulation.
28+
29+
An empirical risk minimization model for linear regression.
30+
"""
31+
32+
beta: Annotated[
33+
tuple[float, ...],
34+
Field(
35+
default=(0.0, 0.0),
36+
description="(intercept, slope) coefficients",
37+
),
38+
]
39+
40+
41+
class ERMExampleProblemConfig(BaseModel):
42+
"""Configuration model for ERMExample Problem.
43+
44+
Base class to implement simulation-optimization problems.
45+
"""
46+
47+
initial_solution: Annotated[
48+
tuple[float, ...],
49+
Field(
50+
default=(0.0, 0.0),
51+
description="initial solution",
52+
),
53+
]
54+
budget: Annotated[
55+
int,
56+
Field(
57+
default=1000,
58+
description="max # of replications for a solver to take",
59+
gt=0,
60+
json_schema_extra={"isDatafarmable": False},
61+
),
62+
]
63+
64+
65+
class FileInputModel(InputModel):
66+
def __init__(self, filename):
67+
self.data = np.load(filename)
68+
69+
def set_rng(self, rng: random.Random) -> None:
70+
self.rng = rng
71+
72+
def unset_rng(self) -> None:
73+
self.rng = None
74+
75+
def random(self) -> float:
76+
n_rows = np.shape(self.data)[0]
77+
resample_idx = np.random.choice(n_rows, size=1, replace=True)
78+
resample_x = self.data[resample_idx, 0].item()
79+
resample_y = self.data[resample_idx, 1].item()
80+
return resample_x, resample_y
81+
82+
83+
class ERMExampleModel(Model):
84+
"""A model that for the empirical risk of a linear regression model."""
85+
86+
class_name_abbr: ClassVar[str] = "ERMEXAMPLE"
87+
class_name: ClassVar[str] = "Linear Regression ERM"
88+
config_class: ClassVar[type[BaseModel]] = ERMExampleModelConfig
89+
n_rngs: ClassVar[int] = 1
90+
n_responses: ClassVar[int] = 1
91+
92+
def __init__(self, fixed_factors: dict | None = None) -> None:
93+
"""Initialize the model.
94+
95+
Args:
96+
fixed_factors (dict | None): fixed factors of the model.
97+
If None, use default values.
98+
"""
99+
# Let the base class handle default arguments.
100+
super().__init__(fixed_factors)
101+
self.resample_model = FileInputModel("workshop/erm_data.npy")
102+
103+
def before_replicate(self, rng_list: list[MRG32k3a]) -> None: # noqa: D102
104+
self.resample_model.set_rng(rng_list[0])
105+
106+
def replicate(self) -> tuple[dict, dict]:
107+
"""Evaluate the squared error loss of a single observation.
108+
109+
Returns:
110+
tuple[dict, dict]: A tuple containing:
111+
- responses (dict): Performance measures of interest, including:
112+
- "sq_error_loss": Squared error loss of a single observation.
113+
- gradients (dict): A dictionary of gradient estimates for
114+
each response.
115+
"""
116+
beta0, beta1 = self.factors["beta"]
117+
x, y = self.resample_model.random()
118+
sq_error_loss = (y - beta0 - beta1 * x) ** 2
119+
error_loss = y - beta0 - beta1 * x
120+
# gradients wrt beta0 and beta1
121+
grad_sq_error_loss = (-2 * error_loss, -2 * x * error_loss)
122+
123+
# Compose responses and gradients.
124+
responses = {"sq_error_loss": sq_error_loss}
125+
gradients = {"sq_error_loss": {"beta": grad_sq_error_loss}}
126+
return responses, gradients
127+
128+
129+
class ERMExampleProblem(Problem):
130+
"""Base class to implement simulation-optimization problems."""
131+
132+
class_name_abbr: ClassVar[str] = "ERM-EXAMPLE-1"
133+
class_name: ClassVar[str] = "Min Empirical Risk"
134+
config_class: ClassVar[type[BaseModel]] = ERMExampleProblemConfig
135+
model_class: ClassVar[type[Model]] = ERMExampleModel
136+
n_objectives: ClassVar[int] = 1
137+
n_stochastic_constraints: ClassVar[int] = 0
138+
minmax: ClassVar[tuple[int, ...]] = (-1,)
139+
constraint_type: ClassVar[ConstraintType] = ConstraintType.UNCONSTRAINED
140+
variable_type: ClassVar[VariableType] = VariableType.CONTINUOUS
141+
gradient_available: ClassVar[bool] = True
142+
model_default_factors: ClassVar[dict] = {}
143+
model_decision_factors: ClassVar[set[str]] = {"beta"}
144+
145+
@property
146+
def optimal_value(self) -> float | None: # noqa: D102
147+
# Compute optimal beta0 and beta1
148+
all_data = np.load("workshop/erm_data.npy")
149+
x = all_data[:, 0]
150+
y = all_data[:, 1]
151+
optbeta1, optbeta0 = np.polyfit(x, y, 1)
152+
opttrainingmse = np.mean(
153+
[(yy - optbeta0 - optbeta1 * xx) ** 2 for (xx, yy) in zip(x, y)]
154+
)
155+
return opttrainingmse
156+
157+
@property
158+
def optimal_solution(self) -> tuple | None: # noqa: D102
159+
# Compute optimal beta0 and beta1
160+
all_data = np.load("workshop/erm_data.npy")
161+
x = all_data[:, 0]
162+
y = all_data[:, 1]
163+
optbeta1, optbeta0 = np.polyfit(x, y, 1)
164+
return (optbeta0, optbeta1)
165+
166+
@property
167+
def dim(self) -> int: # noqa: D102
168+
return 2
169+
170+
@property
171+
def lower_bounds(self) -> tuple: # noqa: D102
172+
return (-np.inf,) * self.dim
173+
174+
@property
175+
def upper_bounds(self) -> tuple: # noqa: D102
176+
return (np.inf,) * self.dim
177+
178+
def vector_to_factor_dict(self, vector: tuple) -> dict: # noqa: D102
179+
return {"beta": vector[:]}
180+
181+
def factor_dict_to_vector(self, factor_dict: dict) -> tuple: # noqa: D102
182+
return tuple(factor_dict["beta"])
183+
184+
def replicate(self, _x: tuple) -> RepResult: # noqa: D102
185+
responses, gradients = self.model.replicate()
186+
objectives = [
187+
Objective(
188+
stochastic=responses["sq_error_loss"],
189+
stochastic_gradients=gradients["sq_error_loss"]["beta"],
190+
)
191+
]
192+
return RepResult(objectives=objectives)
193+
194+
def get_random_solution(self, rand_sol_rng: MRG32k3a) -> tuple: # noqa: D102
195+
# beta = tuple([rand_sol_rng.uniform(-2, 2) for _ in range(self.dim)])
196+
beta = tuple(
197+
rand_sol_rng.mvnormalvariate(
198+
mean_vec=[1.0] * self.dim,
199+
cov=np.eye(self.dim).tolist(),
200+
factorized=False,
201+
)
202+
)
203+
return beta

ty.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ exclude = [
1919
"simopt/gui/new_experiment_window.py",
2020
"simopt/gui/plot_window.py",
2121
"simopt/model.py",
22+
"simopt/models/ermexample.py",
2223
"simopt/plot_type.py",
2324
"simopt/plots",
2425
"simopt/problem.py",

workshop/erm_data.npy

156 KB
Binary file not shown.

workshop/erm_data_generator.ipynb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "5c2c7b4c",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"import numpy as np\n",
11+
"\n",
12+
"n = 10000 # number of observations\n",
13+
"# X ~ Normal(0, 1)\n",
14+
"# Y|X ~ Normal(1 + x, 0.1)\n",
15+
"x = np.random.normal(size=10000)\n",
16+
"y = 1 + x + np.random.normal(loc=0, scale=0.1, size=10000)\n",
17+
"xydata = np.array([[xx, yy] for (xx, yy) in zip(x, y)])\n",
18+
"np.save(\"erm_data.npy\", xydata)"
19+
]
20+
}
21+
],
22+
"metadata": {
23+
"kernelspec": {
24+
"display_name": "simopt",
25+
"language": "python",
26+
"name": "python3"
27+
},
28+
"language_info": {
29+
"codemirror_mode": {
30+
"name": "ipython",
31+
"version": 3
32+
},
33+
"file_extension": ".py",
34+
"mimetype": "text/x-python",
35+
"name": "python",
36+
"nbconvert_exporter": "python",
37+
"pygments_lexer": "ipython3",
38+
"version": "3.13.10"
39+
}
40+
},
41+
"nbformat": 4,
42+
"nbformat_minor": 5
43+
}

0 commit comments

Comments
 (0)