Skip to content

Commit cdacf3b

Browse files
committed
Improve documentation
1 parent 508483a commit cdacf3b

10 files changed

Lines changed: 215 additions & 65 deletions

File tree

docs/src/index.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,20 @@ x \;\longrightarrow\; \boxed{\,\text{Statistical model } \varphi_w\,}
2424
```
2525

2626
Where:
27-
- **Statistical model** $\varphi_w$: machine learning predictor (e.g., neural network)
28-
- **CO algorithm** $f$: combinatorial optimization solver
2927
- **Instance** $x$: input data (e.g., features, context)
28+
- **Statistical model** $\varphi_w$: machine learning predictor (e.g., neural network)
3029
- **Parameters** $\theta$: predicted parameters for the optimization problem solved by `f`
30+
- **CO algorithm** $f$: combinatorial optimization solver
3131
- **Solution** $y$: output decision/solution
3232

3333
## Package Overview
3434

3535
**DecisionFocusedLearningBenchmarks.jl** provides a collection of benchmark problems for evaluating decision-focused learning algorithms. The package offers:
3636

37-
- **Standardized benchmark problems** spanning diverse application domains
38-
- **Common interfaces** for creating datasets, statistical models, and optimization algorithms
39-
- **Ready-to-use DFL policies** compatible with [InferOpt.jl](https://github.com/JuliaDecisionFocusedLearning/InferOpt.jl) and the whole [JuliaDecisionFocusedLearning](https://github.com/JuliaDecisionFocusedLearning) ecosystem
40-
- **Evaluation tools** for comparing algorithm performance
37+
- **Collection of benchmark problems** spanning diverse applications
38+
- **Common tools** for creating datasets, statistical models, and optimization algorithms
39+
- **Generic interface** for building custom benchmarks
40+
- Compatibility with [InferOpt.jl](https://github.com/JuliaDecisionFocusedLearning/InferOpt.jl) and the whole [JuliaDecisionFocusedLearning](https://github.com/JuliaDecisionFocusedLearning) ecosystem
4141

4242
## Benchmark Categories
4343

docs/src/using_benchmarks.md

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,42 @@
11
# Using Benchmarks
22

3-
This guide covers everything you need to work with existing benchmarks in
4-
DecisionFocusedLearningBenchmarks.jl: generating datasets, assembling DFL pipeline
5-
components, and evaluating results.
3+
This guide covers everything you need to work with existing benchmarks in DecisionFocusedLearningBenchmarks.jl: generating datasets, assembling DFL pipeline components, applying algorithms, and evaluating results.
4+
5+
---
6+
7+
## What is a benchmark?
8+
9+
A benchmark bundles a problem family (an instance generator, a combinatorial solver, and a statistical model architecture) into a single object. It provides everything needed to run a Decision-Focused Learning experiment out of the box, without having to create each component from scratch.
10+
Three abstract types cover the main settings:
11+
- **`AbstractBenchmark`**: static problems (one instance, one decision)
12+
- **`AbstractStochasticBenchmark{exogenous}`**: stochastic problems (type parameter indicates whether uncertainty is exogenous)
13+
- **`AbstractDynamicBenchmark`**: sequential / multi-stage problems
14+
15+
The sections below explain what changes between these settings. For most purposes, start with a static benchmark to understand the core workflow.
16+
17+
---
18+
19+
## Core workflow
20+
21+
Every benchmark exposes three key methods. For any static benchmark:
22+
23+
```julia
24+
bench = ArgmaxBenchmark()
25+
model = generate_statistical_model(bench; seed=0) # Flux model
26+
maximizer = generate_maximizer(bench) # combinatorial oracle
27+
dataset = generate_dataset(bench, 100; seed=0) # Vector{DataSample}
28+
```
29+
30+
- **`generate_statistical_model`**: returns an untrained neural network that maps input features `x` to cost parameters `θ`.
31+
- **`generate_maximizer`**: returns a callable `(θ; context...) -> y` that solves the combinatorial problem given cost parameters.
32+
- **`generate_dataset`**: returns labeled training data as a `Vector{DataSample}`.
33+
34+
At inference time these two pieces compose naturally as an end-to-end policy:
35+
36+
```julia
37+
θ = model(sample.x) # predict cost parameters
38+
y = maximizer(θ; sample.context...) # solve the optimization problem
39+
```
640

741
---
842

@@ -18,8 +52,7 @@ All data in the package is represented as [`DataSample`](@ref) objects.
1852
| `context` | `NamedTuple` | Solver kwargs spread into `maximizer(θ; sample.context...)` |
1953
| `extra` | `NamedTuple` | Non-solver data (scenario, reward, step, …), never passed to the solver |
2054

21-
Not all fields are populated in every sample. For convenience, named entries inside
22-
`context` and `extra` can be accessed directly on the sample via property forwarding:
55+
Not all fields are populated in every sample, depending on the setting. For convenience, named entries inside `context` and `extra` can be accessed directly on the sample via property forwarding:
2356

2457
```julia
2558
sample.instance # looks up :instance in context first, then in extra
@@ -28,12 +61,11 @@ sample.scenario # looks up :scenario in context first, then in extra
2861

2962
---
3063

31-
## Generating datasets for training
64+
## Benchmark type specifics
3265

3366
### Static benchmarks
3467

35-
For static benchmarks (`<:AbstractBenchmark`) the framework already computes the
36-
ground-truth label `y`:
68+
For static benchmarks (`<:AbstractBenchmark`), `generate_dataset` may compute a default ground-truth label `y` if the benchmark implements it:
3769

3870
```julia
3971
bench = ArgmaxBenchmark()
@@ -43,15 +75,13 @@ dataset = generate_dataset(bench, 100; seed=0) # Vector{DataSample} with x, y,
4375
You can override the labels by providing a `target_policy`:
4476

4577
```julia
46-
my_policy = sample -> DataSample(; sample.context..., x=sample.x,
47-
y=my_algorithm(sample.instance))
78+
my_policy = sample -> DataSample(; sample.context..., x=sample.x, y=my_algorithm(sample.instance))
4879
dataset = generate_dataset(bench, 100; seed=0, target_policy=my_policy)
4980
```
5081

5182
### Stochastic benchmarks (exogenous)
5283

53-
For `AbstractStochasticBenchmark{true}` benchmarks the default call returns
54-
*unlabeled* samples, each sample carries one scenario in `sample.extra.scenario`:
84+
For `AbstractStochasticBenchmark{true}` benchmarks the default call returns *unlabeled* samples, each sample carries one scenario in `sample.extra.scenario`:
5585

5686
```julia
5787
bench = StochasticVehicleSchedulingBenchmark()
@@ -85,20 +115,22 @@ Dynamic benchmarks use a two-step workflow:
85115
```julia
86116
bench = DynamicVehicleSchedulingBenchmark()
87117

88-
# Step 1 create environments (reusable across experiments)
118+
# Step 1: create environments (reusable across experiments)
89119
envs = generate_environments(bench, 10; seed=0)
90120

91-
# Step 2 roll out a policy to collect training trajectories
121+
# Step 2: roll out a policy to collect training trajectories
92122
policy = generate_baseline_policies(bench)[1] # e.g. lazy policy
93123
dataset = generate_dataset(bench, envs; target_policy=policy)
94124
# dataset is a flat Vector{DataSample} of all steps across all trajectories
95125
```
96126

97-
`target_policy` is **required** for dynamic benchmarks (there is no default label).
127+
`target_policy` is **required** to create datasets for dynamic benchmarks (there is no default label).
98128
It must be a callable `(env) -> Vector{DataSample}` that performs a full episode
99129
rollout and returns the resulting trajectory.
100130

101-
### Seed / RNG control
131+
---
132+
133+
## Seed / RNG control
102134

103135
All `generate_dataset` and `generate_environments` calls accept either `seed`
104136
(creates an internal `MersenneTwister`) or `rng` for full control:
@@ -111,22 +143,6 @@ dataset = generate_dataset(bench, 50; rng=rng)
111143

112144
---
113145

114-
## DFL pipeline components
115-
116-
```julia
117-
model = generate_statistical_model(bench; seed=0) # untrained Flux model
118-
maximizer = generate_maximizer(bench) # combinatorial oracle
119-
```
120-
121-
These two pieces compose naturally:
122-
123-
```julia
124-
θ = model(sample.x) # predict cost parameters
125-
y = maximizer(θ; sample.context...) # solve the optimization problem
126-
```
127-
128-
---
129-
130146
## Evaluation
131147

132148
```julia

docs/src/warcraft_tutorial.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
```@meta
2+
EditURL = "tutorials/warcraft_tutorial.jl"
3+
```
4+
5+
# Path-finding on image maps
6+
7+
In this tutorial, we showcase DecisionFocusedLearningBenchmarks.jl capabilities on one of its main benchmarks: the Warcraft benchmark.
8+
This benchmark problem is a simple path-finding problem where the goal is to find the shortest path between the top left and bottom right corners of a given image map.
9+
The map is represented as a 2D image representing a 12x12 grid, each cell having an unknown travel cost depending on the terrain type.
10+
11+
First, let's load the package and create a benchmark object as follows:
12+
13+
````@example warcraft_tutorial
14+
using DecisionFocusedLearningBenchmarks
15+
b = WarcraftBenchmark()
16+
````
17+
18+
## Dataset generation
19+
20+
These benchmark objects behave as generators that can generate various needed elements in order to build an algorithm to tackle the problem.
21+
First of all, all benchmarks are capable of generating datasets as needed, using the [`generate_dataset`](@ref) method.
22+
This method takes as input the benchmark object for which the dataset is to be generated, and a second argument specifying the number of samples to generate:
23+
24+
````@example warcraft_tutorial
25+
dataset = generate_dataset(b, 50);
26+
nothing #hide
27+
````
28+
29+
We obtain a vector of [`DataSample`](@ref) objects, containing all needed data for the problem.
30+
Subdatasets can be created through regular slicing:
31+
32+
````@example warcraft_tutorial
33+
train_dataset, test_dataset = dataset[1:45], dataset[46:50]
34+
````
35+
36+
And getting an individual sample will return a [`DataSample`](@ref) with four fields: `x`, `info`, `θ`, and `y`:
37+
38+
````@example warcraft_tutorial
39+
sample = test_dataset[1]
40+
````
41+
42+
`x` correspond to the input features, i.e. the input image (3D array) in the Warcraft benchmark case:
43+
44+
````@example warcraft_tutorial
45+
x = sample.x
46+
````
47+
48+
`θ` correspond to the true unknown terrain weights. We use the opposite of the true weights in order to formulate the optimization problem as a maximization problem:
49+
50+
````@example warcraft_tutorial
51+
θ_true = sample.θ
52+
````
53+
54+
`y` correspond to the optimal shortest path, encoded as a binary matrix:
55+
56+
````@example warcraft_tutorial
57+
y_true = sample.y
58+
````
59+
60+
`context` is not used in this benchmark (no solver kwargs needed), so it is empty:
61+
62+
````@example warcraft_tutorial
63+
isempty(sample.context)
64+
````
65+
66+
For some benchmarks, we provide the following plotting method [`plot_data`](@ref) to visualize the data:
67+
68+
````@example warcraft_tutorial
69+
plot_data(b, sample)
70+
````
71+
72+
We can see here the terrain image, the true terrain weights, and the true shortest path avoiding the high cost cells.
73+
74+
## Building a pipeline
75+
76+
DecisionFocusedLearningBenchmarks also provides methods to build an hybrid machine learning and combinatorial optimization pipeline for the benchmark.
77+
First, the [`generate_statistical_model`](@ref) method generates a machine learning predictor to predict cell weights from the input image:
78+
79+
````@example warcraft_tutorial
80+
model = generate_statistical_model(b)
81+
````
82+
83+
In the case of the Warcraft benchmark, the model is a convolutional neural network built using the Flux.jl package.
84+
85+
````@example warcraft_tutorial
86+
θ = model(x)
87+
````
88+
89+
Note that the model is not trained yet, and its parameters are randomly initialized.
90+
91+
Finally, the [`generate_maximizer`](@ref) method can be used to generate a combinatorial optimization algorithm that takes the predicted cell weights as input and returns the corresponding shortest path:
92+
93+
````@example warcraft_tutorial
94+
maximizer = generate_maximizer(b; dijkstra=true)
95+
````
96+
97+
In the case o fthe Warcraft benchmark, the method has an additional keyword argument to chose the algorithm to use: Dijkstra's algorithm or Bellman-Ford algorithm.
98+
99+
````@example warcraft_tutorial
100+
y = maximizer(θ)
101+
````
102+
103+
As we can see, currently the pipeline predicts random noise as cell weights, and therefore the maximizer returns a straight line path.
104+
105+
````@example warcraft_tutorial
106+
plot_data(b, DataSample(; x, θ, y))
107+
````
108+
109+
We can evaluate the current pipeline performance using the optimality gap metric:
110+
111+
````@example warcraft_tutorial
112+
starting_gap = compute_gap(b, test_dataset, model, maximizer)
113+
````
114+
115+
## Using a learning algorithm
116+
117+
We can now train the model using the InferOpt.jl package:
118+
119+
````@example warcraft_tutorial
120+
using InferOpt
121+
using Flux
122+
using Plots
123+
124+
perturbed_maximizer = PerturbedMultiplicative(maximizer; ε=0.2, nb_samples=100)
125+
loss = FenchelYoungLoss(perturbed_maximizer)
126+
127+
starting_gap = compute_gap(b, test_dataset, model, maximizer)
128+
129+
opt_state = Flux.setup(Adam(1e-3), model)
130+
loss_history = Float64[]
131+
for epoch in 1:50
132+
val, grads = Flux.withgradient(model) do m
133+
sum(loss(m(x), y) for (; x, y) in train_dataset) / length(train_dataset)
134+
end
135+
Flux.update!(opt_state, model, grads[1])
136+
push!(loss_history, val)
137+
end
138+
139+
plot(loss_history; xlabel="Epoch", ylabel="Loss", title="Training loss")
140+
````
141+
142+
````@example warcraft_tutorial
143+
final_gap = compute_gap(b, test_dataset, model, maximizer)
144+
````
145+
146+
````@example warcraft_tutorial
147+
θ = model(x)
148+
y = maximizer(θ)
149+
plot_data(b, DataSample(; x, θ, y))
150+
````
151+
152+
---
153+
154+
*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*
155+

src/DynamicAssortment/DynamicAssortment.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ function Utils.generate_baseline_policies(::DynamicAssortmentBenchmark)
139139
"policy that selects the assortment with the highest expected revenue",
140140
expert_policy,
141141
)
142-
return (expert, greedy)
142+
return (; expert, greedy)
143143
end
144144

145145
export DynamicAssortmentBenchmark

src/DynamicVehicleScheduling/DynamicVehicleScheduling.jl

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,6 @@ end
115115
"""
116116
$TYPEDSIGNATURES
117117
118-
Generate an anticipative solution for the dynamic vehicle scheduling benchmark.
119-
The solution is computed using the anticipative solver with the benchmark's feature configuration.
120-
"""
121-
function Utils.generate_anticipative_solution(
122-
b::DynamicVehicleSchedulingBenchmark, args...; kwargs...
123-
)
124-
return anticipative_solver(
125-
args...; kwargs..., two_dimensional_features=b.two_dimensional_features
126-
)
127-
end
128-
129-
"""
130-
$TYPEDSIGNATURES
131-
132118
Return the anticipative solver for the dynamic vehicle scheduling benchmark.
133119
The callable takes a scenario and solver kwargs (including `instance`) and returns a
134120
training trajectory as a `Vector{DataSample}`.
@@ -160,7 +146,7 @@ function Utils.generate_baseline_policies(::DynamicVehicleSchedulingBenchmark)
160146
"Greedy policy that dispatches vehicles to the nearest customer.",
161147
greedy_policy,
162148
)
163-
return (lazy, greedy)
149+
return (; lazy, greedy)
164150
end
165151

166152
"""

src/FixedSizeShortestPath/FixedSizeShortestPath.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,4 @@ function Utils.generate_statistical_model(
142142
end
143143

144144
export FixedSizeShortestPathBenchmark
145-
export generate_dataset, generate_maximizer, generate_statistical_model
146-
147145
end

src/Maintenance/Maintenance.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ The number of simultaneous maintenance operations is limited by a maintenance ca
2222
2323
# Fields
2424
$TYPEDFIELDS
25-
2625
"""
2726
struct MaintenanceBenchmark <: AbstractDynamicBenchmark{true}
2827
"number of components"
@@ -126,7 +125,7 @@ end
126125
"""
127126
$TYPEDSIGNATURES
128127
129-
Returns two policies for the dynamic assortment benchmark:
128+
Returns a policy for the maintenance benchmark:
130129
- `Greedy`: maintains components when they are in the last state before failure, up to the maintenance capacity
131130
"""
132131
function Utils.generate_baseline_policies(::MaintenanceBenchmark)
@@ -135,7 +134,7 @@ function Utils.generate_baseline_policies(::MaintenanceBenchmark)
135134
"policy that maintains components when they are in the last state before failure, up to the maintenance capacity",
136135
greedy_policy,
137136
)
138-
return (greedy,)
137+
return (; greedy)
139138
end
140139

141140
export MaintenanceBenchmark

0 commit comments

Comments
 (0)