Skip to content

Commit 61eec0b

Browse files
Add Evaluators and update functionalities (#28)
* refactor and add evaluators * format * update docs * add background * update docstrings * bump deps * fix docs
1 parent 7ac4439 commit 61eec0b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1562
-765
lines changed

Project.toml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LearningToOptimize"
22
uuid = "e1d8bfa7-c465-446a-84b9-451470f6e76c"
33
authors = ["andrewrosemberg <[email protected]> and contributors"]
4-
version = "1.0.0"
4+
version = "1.1.0"
55

66
[deps]
77
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"
@@ -11,6 +11,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
1111
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
1212
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
1313
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
14+
MLJ = "add582a8-e3ab-11e8-2d5e-e98b27df1bc7"
1415
MLJFlux = "094fc8d1-fd35-5302-93ea-dabda2abf845"
1516
NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd"
1617
Optimisers = "3bd65402-5787-11e9-1adc-39752487f4e2"
@@ -25,14 +26,15 @@ Arrow = "2"
2526
CSV = "0.10"
2627
DataFrames = "1"
2728
Distributions = "0.25"
28-
Flux = "0.14"
29+
Flux = "0.14, 0.16"
2930
JuMP = "1"
3031
MLJFlux = "0.6"
32+
MLJ = "0.20"
3133
NNlib = "0.9"
32-
Optimisers = "0.3"
33-
ParametricOptInterface = "0.8"
34+
Optimisers = "0.3, 0.4"
35+
ParametricOptInterface = "0.8, 0.9"
3436
Statistics = "1"
35-
Zygote = "0.6.68"
37+
Zygote = "0.6.68, 0.7"
3638
julia = "1.9"
3739

3840
[extras]
@@ -41,10 +43,9 @@ Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e"
4143
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
4244
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
4345
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
44-
MLJ = "add582a8-e3ab-11e8-2d5e-e98b27df1bc7"
4546
PGLib = "07a8691f-3d11-4330-951b-3c50f98338be"
4647
PowerModels = "c36e90e8-916a-50a6-bd94-075b64ef4655"
4748
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4849

4950
[targets]
50-
test = ["Test", "DelimitedFiles", "PGLib", "HiGHS", "PowerModels", "Clarabel", "Ipopt", "MLJ"]
51+
test = ["Test", "DelimitedFiles", "PGLib", "HiGHS", "PowerModels", "Clarabel", "Ipopt"]

README.md

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</div>
77
</div>
88

9-
Learning to optimize (LearningToOptimize) package that provides basic functionalities to help fit proxy models for optimization.
9+
Learning to optimize (LearningToOptimize) package that provides basic functionalities to help fit proxy models for parametric optimization problems.
1010

1111
Have a look at our sister [HugginFace Organization](https://huggingface.co/LearningToOptimize), for datasets, pre-trained models and benchmarks.
1212

@@ -19,6 +19,34 @@ Have a look at our sister [HugginFace Organization](https://huggingface.co/Learn
1919

2020
![flowchart](docs/src/assets/L2O.png)
2121

22+
# Background
23+
24+
Parametric optimization problems arise in scenarios where certain elements (e.g., coefficients, constraints) may vary according to problem parameters. A general form of a parameterized convex optimization problem is
25+
26+
$$
27+
\begin{aligned}
28+
&\min_{x} \quad f(x; \theta) \\
29+
&\text{subject to} \quad g_i(x; \theta) \leq 0, \quad i = 1,\dots, m \\
30+
&\quad\quad\quad\quad A(\theta)x = b(\theta)
31+
\end{aligned}
32+
$$
33+
34+
where $ \theta $ is the parameter.
35+
36+
**Learning to Optimize (L2O)** is an emerging paradigm where machine learning models *learn* to solve optimization problems efficiently. This approach is also known as using **optimization proxies** or **amortized optimization**.
37+
38+
In more technical terms, **amortized optimization** seeks to learn a function \\( f_\theta(x) \\) that maps problem parameters \\( x \\) to solutions \\( y \\) that (approximately) minimize a given objective function subject to constraints. Modern methods leverage techniques like **differentiable optimization layers**, **input-convex neural networks**, or constraint-enforcing architectures (e.g., [DC3](https://openreview.net/pdf?id=0Ow8_1kM5Z)) to ensure that the learned proxy solutions are both feasible and performant. By coupling the solver and the model in an **end-to-end** pipeline, these approaches let the training objective directly reflect downstream metrics, improving speed and reliability.
39+
40+
Recent advances also focus on **trustworthy** or **certifiable** proxies, where constraint satisfaction or performance bounds are guaranteed. This is crucial in domains like energy systems or manufacturing, where infeasible solutions can have large penalties or safety concerns. Overall, learning-based optimization frameworks aim to combine the advantages of ML (data-driven generalization) with the rigor of mathematical programming (constraint handling and optimality).
41+
42+
For a broader overview, see the [SIAM News article on trustworthy optimization proxies](https://www.siam.org/publications/siam-news/articles/fusing-artificial-intelligence-and-optimization-with-trustworthy-optimization-proxies/), which highlights the growing synergy between AI and classical optimization.
43+
44+
# Installation
45+
46+
```julia
47+
] add LearningToOptimize
48+
```
49+
2250
## Generate Dataset
2351
This package provides a basic way of generating a dataset of the solutions of an optimization problem by varying the values of the parameters in the problem and recording it.
2452

@@ -62,7 +90,33 @@ Which creates the following CSV:
6290
| 9 | 9.0 |
6391
| 10 | 10.0|
6492

65-
ps.: For illustration purpose, I have represented the id's here as integers, but in reality they are generated as UUIDs.
93+
ps.: For illustration purpose, I have represented the id's here as integers, but in reality they are generated as UUIDs.
94+
95+
To load the parameter values back:
96+
97+
```julia
98+
problem_iterator = load("input_file.csv", CSVFile)
99+
```
100+
101+
### Samplers
102+
103+
Instead of defining parameter instances manually, one may sample parameter values using pre-defined samplers - e.g. `scaled_distribution_sampler`, `box_sampler`- or define their own sampler. Samplers are functions that take a vector of parameter of type `MOI.Parameter` and return a matrix of parameter values.
104+
105+
The easiest way to go from problem definition, sampling parameter values and saving them is to use the `general_sampler` function:
106+
107+
```julia
108+
general_sampler(
109+
"examples/powermodels/data/6468_rte/6468_rte_SOCWRConicPowerModel_POI_load.mof.json";
110+
samplers = [
111+
(original_parameters) -> scaled_distribution_sampler(original_parameters, 10000),
112+
(original_parameters) -> line_sampler(original_parameters, 1.01:0.01:1.25),
113+
(original_parameters) -> box_sampler(original_parameters, 300),
114+
],
115+
)
116+
```
117+
118+
This function is a general sampler that uses a set of samplers to sample the parameter space.
119+
It loads the underlying model from a passed `file` that works with JuMP's `read_from_file` (ps.: currently only tested with `MathOptFormat`), samples the parameters and saves the sampled parameters to `save_file`.
66120

67121
### The Recorder
68122

@@ -104,13 +158,15 @@ recorder = Recorder{ArrowFile}("output_file.arrow", primal_variables=[x], dual_v
104158
In order to train models to be able to forecast optimization solutions from parameter values, one option is to use the package Flux.jl:
105159

106160
```julia
161+
using CSV, DataFrames, Flux
162+
107163
# read input and output data
108164
input_data = CSV.read("input_file.csv", DataFrame)
109165
output_data = CSV.read("output_file.csv", DataFrame)
110166

111167
# Separate input and output variables
112-
output_variables = output_data[!, Not(:id)]
113-
input_features = innerjoin(input_data, output_data[!, [:id]], on = :id)[!, Not(:id)] # just use success solves
168+
output_variables = output_data[!, Not([:id, :status, :primal_status, :dual_status, :objective, :time])] # just predict solutions
169+
input_features = innerjoin(input_data, output_data[!, [:id]]; on=:id)[!, Not(:id)] # just use success solves
114170

115171
# Define model
116172
model = Chain(
@@ -136,6 +192,38 @@ Flux.train!(loss, Flux.params(model), [(input_features, output_variables)], opti
136192
predictions = model(input_features)
137193
```
138194

195+
Another option is to use the package MLJ.jl:
196+
197+
```julia
198+
using MLJ
199+
200+
# Define the model
201+
model = MultitargetNeuralNetworkRegressor(;
202+
builder=FullyConnectedBuilder([64, 32]),
203+
rng=123,
204+
epochs=20,
205+
optimiser=Optimisers.Adam(),
206+
)
207+
208+
# Train the model
209+
mach = machine(model, input_features, output_variables)
210+
fit!(mach; verbosity=2)
211+
212+
# Make predictions
213+
predict(mach, input_features)
214+
215+
```
216+
217+
### Evaluating the ML model
218+
219+
For ease of use, we built a general evaluator that can be used to evaluate the model.
220+
It will return a `NamedTuple` with the objective value and infeasibility of the
221+
predicted solution for each instance, and the overall inference time and allocated memory.
222+
223+
```julia
224+
evaluation = general_evaluator(problem_iterator, mach)
225+
```
226+
139227
## Coming Soon
140228

141229
Future features:

docs/make.jl

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
using LearningToOptimize
22
using Documenter
33

4-
DocMeta.setdocmeta!(LearningToOptimize, :DocTestSetup, :(using LearningToOptimize); recursive=true)
4+
DocMeta.setdocmeta!(
5+
LearningToOptimize,
6+
:DocTestSetup,
7+
:(using LearningToOptimize);
8+
recursive = true,
9+
)
510

611
makedocs(;
7-
modules=[LearningToOptimize],
8-
authors="andrewrosemberg <[email protected]> and contributors",
9-
repo="https://github.com/andrewrosemberg/LearningToOptimize.jl/blob/{commit}{path}#{line}",
10-
sitename="LearningToOptimize.jl",
11-
format=Documenter.HTML(;
12-
prettyurls=get(ENV, "CI", "false") == "true",
13-
canonical="https://andrewrosemberg.github.io/LearningToOptimize.jl",
14-
edit_link="main",
15-
assets=String[],
12+
modules = [LearningToOptimize],
13+
authors = "andrewrosemberg <[email protected]> and contributors",
14+
repo = "https://github.com/andrewrosemberg/LearningToOptimize.jl/blob/{commit}{path}#{line}",
15+
sitename = "LearningToOptimize.jl",
16+
format = Documenter.HTML(;
17+
prettyurls = get(ENV, "CI", "false") == "true",
18+
canonical = "https://andrewrosemberg.github.io/LearningToOptimize.jl",
19+
edit_link = "main",
20+
assets = String[],
1621
),
17-
pages=["Home" => "index.md",
22+
pages = [
23+
"Home" => "index.md",
1824
"Arrow" => "arrow.md",
1925
"Parameter Type" => "parametertype.md",
2026
"API" => "api.md",
2127
],
2228
)
2329

24-
deploydocs(; repo="github.com/andrewrosemberg/LearningToOptimize.jl", devbranch="main")
30+
deploydocs(; repo = "github.com/andrewrosemberg/LearningToOptimize.jl", devbranch = "main")

docs/src/index.md

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,33 @@ Which creates the following CSV:
7171
| 9 | 9.0 |
7272
| 10 | 10.0|
7373

74-
ps.: For illustration purpose, I have represented the id's here as integers, but in reality they are generated as UUIDs.
74+
ps.: For illustration purpose, I have represented the id's here as integers, but in reality they are generated as UUIDs.
75+
76+
To load the parameter values back:
77+
78+
```julia
79+
problem_iterator = load("input_file.csv", CSVFile)
80+
```
81+
82+
### Samplers
83+
84+
Instead of defining parameter instances manually, one may sample parameter values using pre-defined samplers - e.g. `scaled_distribution_sampler`, `box_sampler`- or define their own sampler. Samplers are functions that take a vector of parameter of type `MOI.Parameter` and return a matrix of parameter values.
85+
86+
The easiest way to go from problem definition, sampling parameter values and saving them is to use the `general_sampler` function:
87+
88+
```julia
89+
general_sampler(
90+
"examples/powermodels/data/6468_rte/6468_rte_SOCWRConicPowerModel_POI_load.mof.json";
91+
samplers = [
92+
(original_parameters) -> scaled_distribution_sampler(original_parameters, 10000),
93+
(original_parameters) -> line_sampler(original_parameters, 1.01:0.01:1.25),
94+
(original_parameters) -> box_sampler(original_parameters, 300),
95+
],
96+
)
97+
```
98+
99+
This function is a general sampler that uses a set of samplers to sample the parameter space.
100+
It loads the underlying model from a passed `file` that works with JuMP's `read_from_file` (ps.: currently only tested with `MathOptFormat`), samples the parameters and saves the sampled parameters to `save_file`.
75101

76102
### The Recorder
77103

@@ -113,13 +139,15 @@ recorder = Recorder{ArrowFile}("output_file.arrow", primal_variables=[x], dual_v
113139
In order to train models to be able to forecast optimization solutions from parameter values, one option is to use the package Flux.jl:
114140

115141
```julia
142+
using CSV, DataFrames, Flux
143+
116144
# read input and output data
117145
input_data = CSV.read("input_file.csv", DataFrame)
118146
output_data = CSV.read("output_file.csv", DataFrame)
119147

120148
# Separate input and output variables
121-
output_variables = output_data[!, Not(:id)]
122-
input_features = innerjoin(input_data, output_data[!, [:id]], on = :id)[!, Not(:id)] # just use success solves
149+
output_variables = output_data[!, Not([:id, :status, :primal_status, :dual_status, :objective, :time])] # just predict solutions
150+
input_features = innerjoin(input_data, output_data[!, [:id]]; on=:id)[!, Not(:id)] # just use success solves
123151

124152
# Define model
125153
model = Chain(
@@ -145,18 +173,40 @@ Flux.train!(loss, Flux.params(model), [(input_features, output_variables)], opti
145173
predictions = model(input_features)
146174
```
147175

148-
## Coming Soon
176+
Another option is to use the package MLJ.jl:
149177

150-
Future features:
151-
- ML objectives that penalize infeasible predictions;
152-
- Warm-start from predicted solutions.
178+
```julia
179+
using MLJ
180+
181+
# Define the model
182+
model = MultitargetNeuralNetworkRegressor(;
183+
builder=FullyConnectedBuilder([64, 32]),
184+
rng=123,
185+
epochs=20,
186+
optimiser=Optimisers.Adam(),
187+
)
188+
189+
# Train the model
190+
mach = machine(model, input_features, output_variables)
191+
fit!(mach; verbosity=2)
153192

193+
# Make predictions
194+
predict(mach, input_features)
154195

155-
<!-- ```@index
196+
```
156197

157-
``` -->
198+
### Evaluating the ML model
158199

200+
For ease of use, we built a general evaluator that can be used to evaluate the model.
201+
It will return a `NamedTuple` with the objective value and infeasibility of the
202+
predicted solution for each instance, and the overall inference time and allocated memory.
159203

160-
<!-- ```@autodocs
161-
Modules = [LearningToOptimize]
162-
``` -->
204+
```julia
205+
evaluation = general_evaluator(problem_iterator, mach)
206+
```
207+
208+
## Coming Soon
209+
210+
Future features:
211+
- ML objectives that penalize infeasible predictions;
212+
- Warm-start from predicted solutions.

0 commit comments

Comments
 (0)