Skip to content

Commit 3569254

Browse files
authored
Merge pull request #4 from JuliaDecisionFocusedLearning/decision-focused-learning
Main interface implementation and first benchmark problems
2 parents 7e76975 + be1d742 commit 3569254

29 files changed

+1420
-130
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.vscode
22
/docs/src/index.md
3+
data
4+
scripts
35

46
# Files generated by invoking Julia with --code-coverage
57
*.jl.cov

Project.toml

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,39 @@ authors = ["Members of JuliaDecisionFocusedLearning"]
44
version = "0.1.0"
55

66
[deps]
7+
DataDeps = "124859b0-ceae-595e-8997-d05f6a7a8dfe"
8+
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
9+
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
10+
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
711
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
12+
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
13+
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
814
InferOpt = "4846b161-c94e-4150-8dac-c7ae193c601f"
15+
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
16+
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
917
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
18+
Metalhead = "dbeba491-748d-5e0e-a39e-b530a07fa0cc"
19+
NPZ = "15e1cf62-19b3-5cfa-8e77-841668bca605"
20+
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
1021
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1122
SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622"
1223
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1324

1425
[compat]
26+
DataDeps = "0.7"
27+
Distributions = "0.25"
28+
DocStringExtensions = "0.9"
29+
Flux = "0.14"
1530
Graphs = "1.11"
31+
HiGHS = "1.9"
32+
Images = "0.26"
1633
InferOpt = "0.6"
34+
Ipopt = "1.6"
35+
JuMP = "1.22"
1736
LinearAlgebra = "1"
37+
Metalhead = "0.9"
38+
NPZ = "0.4"
39+
Plots = "1"
1840
Random = "1"
1941
SimpleWeightedGraphs = "1.4"
2042
SparseArrays = "1"
@@ -23,25 +45,18 @@ julia = "1.6"
2345
[extras]
2446
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
2547
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
48+
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
2649
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
2750
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
2851
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
52+
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
2953
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
3054
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
3155
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
3256
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
57+
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
58+
UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228"
3359
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
3460

3561
[targets]
36-
test = [
37-
"Aqua",
38-
"Documenter",
39-
"Graphs",
40-
"JET",
41-
"JuliaFormatter",
42-
"Random",
43-
"StableRNGs",
44-
"Statistics",
45-
"Test",
46-
"Zygote",
47-
]
62+
test = ["Aqua", "Documenter", "Flux", "Graphs", "JET", "JuliaFormatter", "Random", "ProgressMeter", "StableRNGs", "Statistics", "Test", "TestItemRunner", "UnicodePlots", "Zygote"]

docs/Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
[deps]
22
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
3+
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
4+
InferOpt = "4846b161-c94e-4150-8dac-c7ae193c601f"
35
InferOptBenchmarks = "2fbe496a-299b-4c81-bab5-c44dfc55cf20"
6+
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
7+
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

docs/make.jl

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
using Documenter
22
using InferOptBenchmarks
3+
using Literate
34

45
cp(joinpath(@__DIR__, "..", "README.md"), joinpath(@__DIR__, "src", "index.md"); force=true)
56

7+
md_dir = joinpath(@__DIR__, "src")
8+
tutorial_dir = joinpath(@__DIR__, "src", "tutorials")
9+
tutorial_files = readdir(tutorial_dir)
10+
md_tutorial_files = [split(file, ".")[1] * ".md" for file in tutorial_files]
11+
12+
for file in tutorial_files
13+
filepath = joinpath(tutorial_dir, file)
14+
Literate.markdown(filepath, md_dir; documenter=true, execute=false)
15+
end
16+
617
makedocs(;
718
modules=[InferOptBenchmarks, InferOptBenchmarks.Warcraft],
819
authors="Members of JuliaDecisionFocusedLearning",
920
sitename="InferOptBenchmarks.jl",
1021
format=Documenter.HTML(),
1122
pages=[
12-
"Home" => "index.md", #
13-
"API reference" => [
14-
"warcraft.md", #
15-
],
23+
"Home" => "index.md",
24+
"Tutorials" => md_tutorial_files,
25+
"API reference" =>
26+
["api/interface.md", "api/decision_focused.md", "api/warcraft.md"],
1627
],
1728
)
1829

30+
for file in md_tutorial_files
31+
filepath = joinpath(md_dir, file)
32+
rm(filepath)
33+
end
34+
1935
deploydocs(;
2036
repo="github.com/JuliaDecisionFocusedLearning/InferOptBenchmarks.jl", devbranch="main"
2137
)

docs/src/api/decision_focused.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Decisions-focused learning paper
2+
3+
## Public
4+
5+
```@autodocs
6+
Modules = [InferOptBenchmarks.FixedSizeShortestPath, InferOptBenchmarks.PortfolioOptimization, InferOptBenchmarks.SubsetSelection]
7+
Private = false
8+
```
9+
10+
## Private
11+
12+
```@autodocs
13+
Modules = [InferOptBenchmarks.FixedSizeShortestPath, InferOptBenchmarks.PortfolioOptimization, InferOptBenchmarks.SubsetSelection]
14+
Public = false
15+
```

docs/src/api/interface.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Interface
2+
3+
## Public
4+
5+
```@autodocs
6+
Modules = [InferOptBenchmarks.Utils]
7+
Private = false
8+
```
9+
10+
## Private
11+
12+
```@autodocs
13+
Modules = [InferOptBenchmarks.Utils]
14+
Public = false
15+
```
File renamed without changes.

docs/src/tutorials/warcraft.jl

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# # Path-finding on image maps
2+
3+
#=
4+
In this tutorial, we showcase InferOptBenchmarks.jl capabilities on one of its main benchmarks: the Warcraft benchmark.
5+
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.
6+
The map is represented as a 2D image representing a 12x12 grid, each cell having an unknown travel cost depending on the terrain type.
7+
=#
8+
9+
# First, let's load the package and create a benchmark object as follows:
10+
using InferOptBenchmarks
11+
b = WarcraftBenchmark()
12+
13+
# ## Dataset generation
14+
15+
# These benchmark objects behave as generators that can generate various needed elements in order to build an algorithm to tackle the problem.
16+
# First of all, all benchmarks are capable of generating datasets as needed, using the [`generate_dataset`](@ref) method.
17+
# 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:
18+
dataset = generate_dataset(b, 50);
19+
20+
# We obtain a vector of [`DataSample`](@ref) objects, containing all needed data for the problem.
21+
# Subdatasets can be created through regular slicing:
22+
train_dataset, test_dataset = dataset[1:45], dataset[46:50]
23+
24+
# And getting an individual sample will return a [`DataSample`](@ref) with four fields: `x`, `instance`, `θ`, and `y`:
25+
sample = test_dataset[1]
26+
# `x` correspond to the input features, i.e. the input image (3D array) in the Warcraft benchmark case:
27+
x = sample.x
28+
# `θ` 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:
29+
θ_true = sample.θ
30+
# `y` correspond to the optimal shortest path, encoded as a binary matrix:
31+
y_true = sample.y
32+
# `instance` is not used in this benchmark, therefore set to nothing:
33+
isnothing(sample.instance)
34+
35+
# For some benchmarks, we provide the following plotting method [`plot_data`](@ref) to visualize the data:
36+
plot_data(b, sample)
37+
# We can see here the terrain image, the true terrain weights, and the true shortest path avoiding the high cost cells.
38+
39+
# ## Building a pipeline
40+
41+
# InferOptBenchmarks also provides methods to build an hybrid machine learning and combinatorial optimization pipeline for the benchmark.
42+
# First, the [`generate_statistical_model`](@ref) method generates a machine learning predictor to predict cell weights from the input image:
43+
model = generate_statistical_model(b)
44+
# In the case of the Warcraft benchmark, the model is a convolutional neural network built using the Flux.jl package.
45+
θ = model(x)
46+
# Note that the model is not trained yet, and its parameters are randomly initialized.
47+
48+
# 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:
49+
maximizer = generate_maximizer(b; dijkstra=true)
50+
# 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.
51+
y = maximizer(θ)
52+
# As we can see, currently the pipeline predicts random noise as cell weights, and therefore the maximizer returns a straight line path.
53+
plot_data(b, DataSample(; x, θ, y))
54+
# We can evaluate the current pipeline performance using the optimality gap metric:
55+
starting_gap = compute_gap(b, test_dataset, model, maximizer)
56+
57+
# ## Using a learning algorithm
58+
59+
# We can now train the model using the InferOpt.jl package:
60+
using InferOpt
61+
using Flux
62+
using Plots
63+
64+
perturbed_maximizer = PerturbedMultiplicative(maximizer; ε=0.2, nb_samples=100)
65+
loss = FenchelYoungLoss(perturbed_maximizer)
66+
67+
starting_gap = compute_gap(b, test_dataset, model, maximizer)
68+
69+
opt_state = Flux.setup(Adam(1e-3), model)
70+
loss_history = Float64[]
71+
for epoch in 1:50
72+
val, grads = Flux.withgradient(model) do m
73+
sum(loss(m(sample.x), sample.y) for sample in train_dataset) / length(train_dataset)
74+
end
75+
Flux.update!(opt_state, model, grads[1])
76+
push!(loss_history, val)
77+
end
78+
79+
plot(loss_history; xlabel="Epoch", ylabel="Loss", title="Training loss")
80+
81+
#
82+
83+
final_gap = compute_gap(b, test_dataset, model, maximizer)
84+
85+
#
86+
θ = model(x)
87+
y = maximizer(θ)
88+
plot_data(b, DataSample(; x, θ, y))

0 commit comments

Comments
 (0)