From ac2c69fdd1f02bb92cd8b92f2c86402fb66209d3 Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Thu, 19 Dec 2024 17:29:09 +0100 Subject: [PATCH 1/6] Rename DataSample fields --- src/FixedSizeShortestPath/FixedSizeShortestPath.jl | 5 ++++- src/PortfolioOptimization/PortfolioOptimization.jl | 5 ++++- src/SubsetSelection/SubsetSelection.jl | 5 ++++- src/Utils/data_sample.jl | 10 +++++----- src/Utils/interface.jl | 4 ++-- src/Warcraft/Warcraft.jl | 6 ++++-- src/Warcraft/utils.jl | 2 +- test/fixed_size_shortest_path.jl | 4 +--- test/portfolio_optimization.jl | 4 +--- test/subset_selection.jl | 4 +--- test/warcraft.jl | 4 +--- 11 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/FixedSizeShortestPath/FixedSizeShortestPath.jl b/src/FixedSizeShortestPath/FixedSizeShortestPath.jl index 874fbe1..7eea5a3 100644 --- a/src/FixedSizeShortestPath/FixedSizeShortestPath.jl +++ b/src/FixedSizeShortestPath/FixedSizeShortestPath.jl @@ -132,7 +132,10 @@ function Utils.generate_dataset( # Label solutions solutions = shortest_path_maximizer.(costs) - return [DataSample(; x=x, θ=θ, y=y) for (x, θ, y) in zip(features, costs, solutions)] + return [ + DataSample(; x, θ_true, y_true) for + (x, θ_true, y_true) in zip(features, costs, solutions) + ] end """ diff --git a/src/PortfolioOptimization/PortfolioOptimization.jl b/src/PortfolioOptimization/PortfolioOptimization.jl index 3fdead6..b881909 100644 --- a/src/PortfolioOptimization/PortfolioOptimization.jl +++ b/src/PortfolioOptimization/PortfolioOptimization.jl @@ -110,7 +110,10 @@ function Utils.generate_dataset( maximizer = Utils.generate_maximizer(bench) solutions = maximizer.(costs) - return [DataSample(; x=x, θ=θ, y=y) for (x, θ, y) in zip(features, costs, solutions)] + return [ + DataSample(; x, θ_true, y_true) for + (x, θ_true, y_true) in zip(features, costs, solutions) + ] end """ diff --git a/src/SubsetSelection/SubsetSelection.jl b/src/SubsetSelection/SubsetSelection.jl index 64d353d..7d05135 100644 --- a/src/SubsetSelection/SubsetSelection.jl +++ b/src/SubsetSelection/SubsetSelection.jl @@ -73,7 +73,10 @@ function Utils.generate_dataset( mapping.(features) end solutions = top_k.(costs, k) - return [DataSample(; x=x, θ=θ, y=y) for (x, θ, y) in zip(features, costs, solutions)] + return [ + DataSample(; x, θ_true, y_true) for + (x, θ_true, y_true) in zip(features, costs, solutions) + ] end """ diff --git a/src/Utils/data_sample.jl b/src/Utils/data_sample.jl index c5eb93b..33b7bb4 100644 --- a/src/Utils/data_sample.jl +++ b/src/Utils/data_sample.jl @@ -9,10 +9,10 @@ $TYPEDFIELDS @kwdef struct DataSample{F,S,C,I} "features" x::F - "costs" - θ::C = nothing - "solution" - y::S = nothing - "instance" + "target cost parameters (optional)" + θ_true::C = nothing + "target solution (optional)" + y_true::S = nothing + "instance object (optional)" instance::I = nothing end diff --git a/src/Utils/interface.jl b/src/Utils/interface.jl index a310c13..44b21fa 100644 --- a/src/Utils/interface.jl +++ b/src/Utils/interface.jl @@ -73,8 +73,8 @@ function compute_gap( for sample in dataset x = sample.x - θ̄ = sample.θ - ȳ = sample.y + θ̄ = sample.θ_true + ȳ = sample.y_true θ = statistical_model(x) y = maximizer(θ) target_obj = objective_value(bench, θ̄, ȳ) diff --git a/src/Warcraft/Warcraft.jl b/src/Warcraft/Warcraft.jl index 5a20823..b577f54 100644 --- a/src/Warcraft/Warcraft.jl +++ b/src/Warcraft/Warcraft.jl @@ -87,12 +87,14 @@ The keyword argument `θ_true` is used to set the color range of the weights plo function Utils.plot_data( ::WarcraftBenchmark, sample::DataSample; - θ_true=sample.θ, + θ_true=sample.θ_true, θ_title="Weights", y_title="Path", kwargs..., ) - (; x, y, θ) = sample + x = sample.x + y = sample.y_true + θ = sample.θ_true im = dropdims(x; dims=4) img = convert_image_for_plot(im) p1 = Plots.plot( diff --git a/src/Warcraft/utils.jl b/src/Warcraft/utils.jl index c96aaf5..b6c040a 100644 --- a/src/Warcraft/utils.jl +++ b/src/Warcraft/utils.jl @@ -40,7 +40,7 @@ function create_dataset(decompressed_path::String, nb_samples::Int) ] Y = [BitMatrix(terrain_labels[:, :, i]) for i in 1:N] WG = [-terrain_weights[:, :, i] for i in 1:N] - return [DataSample(; x, y, θ) for (x, y, θ) in zip(X, Y, WG)] + return [DataSample(; x, y_true, θ_true) for (x, y_true, θ_true) in zip(X, Y, WG)] end """ diff --git a/test/fixed_size_shortest_path.jl b/test/fixed_size_shortest_path.jl index 495655b..257e7b2 100644 --- a/test/fixed_size_shortest_path.jl +++ b/test/fixed_size_shortest_path.jl @@ -18,9 +18,7 @@ @test gap >= 0 for sample in dataset - x = sample.x - θ_true = sample.θ - y_true = sample.y + (; x, θ_true, y_true) = sample @test all(θ_true .< 0) @test size(x) == (p,) @test length(θ_true) == A diff --git a/test/portfolio_optimization.jl b/test/portfolio_optimization.jl index e57c9d3..aeb7c0e 100644 --- a/test/portfolio_optimization.jl +++ b/test/portfolio_optimization.jl @@ -10,9 +10,7 @@ maximizer = generate_maximizer(b) for sample in dataset - x = sample.x - θ_true = sample.θ - y_true = sample.y + (; x, θ_true, y_true) = sample @test size(x) == (p,) @test length(θ_true) == d @test length(y_true) == d diff --git a/test/subset_selection.jl b/test/subset_selection.jl index 91db249..694f7f4 100644 --- a/test/subset_selection.jl +++ b/test/subset_selection.jl @@ -16,9 +16,7 @@ maximizer = generate_maximizer(b) for (i, sample) in enumerate(dataset) - x = sample.x - θ_true = sample.θ - y_true = sample.y + (; x, θ_true, y_true) = sample @test size(x) == (n,) @test length(θ_true) == n @test length(y_true) == n diff --git a/test/warcraft.jl b/test/warcraft.jl index 206ee80..5d52cdd 100644 --- a/test/warcraft.jl +++ b/test/warcraft.jl @@ -19,9 +19,7 @@ @test gap >= 0 for (i, sample) in enumerate(dataset) - x = sample.x - θ_true = sample.θ - y_true = sample.y + (; x, θ_true, y_true) = sample @test size(x) == (96, 96, 3, 1) @test all(θ_true .<= 0) @test isnothing(sample.instance) From 71f0f0d7f6307c2bada3c688015096ef4c2596d7 Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Thu, 19 Dec 2024 17:46:57 +0100 Subject: [PATCH 2/6] fix tutorial --- docs/src/tutorials/warcraft.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/tutorials/warcraft.jl b/docs/src/tutorials/warcraft.jl index 9b6d199..61cfd01 100644 --- a/docs/src/tutorials/warcraft.jl +++ b/docs/src/tutorials/warcraft.jl @@ -25,10 +25,10 @@ train_dataset, test_dataset = dataset[1:45], dataset[46:50] sample = test_dataset[1] # `x` correspond to the input features, i.e. the input image (3D array) in the Warcraft benchmark case: x = sample.x -# `θ` 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: -θ_true = sample.θ -# `y` correspond to the optimal shortest path, encoded as a binary matrix: -y_true = sample.y +# `θ_true` 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: +θ_true = sample.θ_true +# `y_true` correspond to the optimal shortest path, encoded as a binary matrix: +y_true = sample.y_true # `instance` is not used in this benchmark, therefore set to nothing: isnothing(sample.instance) From 432857097f135bb30b18af9c0788263d654eeafc Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Thu, 19 Dec 2024 18:02:36 +0100 Subject: [PATCH 3/6] fix tutorial --- docs/src/tutorials/warcraft.jl | 2 +- docs/src/warcraft.md | 155 +++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 docs/src/warcraft.md diff --git a/docs/src/tutorials/warcraft.jl b/docs/src/tutorials/warcraft.jl index 61cfd01..cbeb68e 100644 --- a/docs/src/tutorials/warcraft.jl +++ b/docs/src/tutorials/warcraft.jl @@ -85,4 +85,4 @@ final_gap = compute_gap(b, test_dataset, model, maximizer) # θ = model(x) y = maximizer(θ) -plot_data(b, DataSample(; x, θ, y)) +plot_data(b, DataSample(; x, θ_true=θ, y_true=y)) diff --git a/docs/src/warcraft.md b/docs/src/warcraft.md new file mode 100644 index 0000000..bfd471b --- /dev/null +++ b/docs/src/warcraft.md @@ -0,0 +1,155 @@ +```@meta +EditURL = "tutorials/warcraft.jl" +``` + +# Path-finding on image maps + +In this tutorial, we showcase DecisionFocusedLearningBenchmarks.jl capabilities on one of its main benchmarks: the Warcraft benchmark. +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. +The map is represented as a 2D image representing a 12x12 grid, each cell having an unknown travel cost depending on the terrain type. + +First, let's load the package and create a benchmark object as follows: + +````@example warcraft +using DecisionFocusedLearningBenchmarks +b = WarcraftBenchmark() +```` + +## Dataset generation + +These benchmark objects behave as generators that can generate various needed elements in order to build an algorithm to tackle the problem. +First of all, all benchmarks are capable of generating datasets as needed, using the [`generate_dataset`](@ref) method. +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: + +````@example warcraft +dataset = generate_dataset(b, 50); +nothing #hide +```` + +We obtain a vector of [`DataSample`](@ref) objects, containing all needed data for the problem. +Subdatasets can be created through regular slicing: + +````@example warcraft +train_dataset, test_dataset = dataset[1:45], dataset[46:50] +```` + +And getting an individual sample will return a [`DataSample`](@ref) with four fields: `x`, `instance`, `θ`, and `y`: + +````@example warcraft +sample = test_dataset[1] +```` + +`x` correspond to the input features, i.e. the input image (3D array) in the Warcraft benchmark case: + +````@example warcraft +x = sample.x +```` + +`θ_true` 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: + +````@example warcraft +θ_true = sample.θ_true +```` + +`y_true` correspond to the optimal shortest path, encoded as a binary matrix: + +````@example warcraft +y_true = sample.y_true +```` + +`instance` is not used in this benchmark, therefore set to nothing: + +````@example warcraft +isnothing(sample.instance) +```` + +For some benchmarks, we provide the following plotting method [`plot_data`](@ref) to visualize the data: + +````@example warcraft +plot_data(b, sample) +```` + +We can see here the terrain image, the true terrain weights, and the true shortest path avoiding the high cost cells. + +## Building a pipeline + +DecisionFocusedLearningBenchmarks also provides methods to build an hybrid machine learning and combinatorial optimization pipeline for the benchmark. +First, the [`generate_statistical_model`](@ref) method generates a machine learning predictor to predict cell weights from the input image: + +````@example warcraft +model = generate_statistical_model(b) +```` + +In the case of the Warcraft benchmark, the model is a convolutional neural network built using the Flux.jl package. + +````@example warcraft +θ = model(x) +```` + +Note that the model is not trained yet, and its parameters are randomly initialized. + +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: + +````@example warcraft +maximizer = generate_maximizer(b; dijkstra=true) +```` + +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. + +````@example warcraft +y = maximizer(θ) +```` + +As we can see, currently the pipeline predicts random noise as cell weights, and therefore the maximizer returns a straight line path. + +````@example warcraft +plot_data(b, DataSample(; x, θ, y)) +```` + +We can evaluate the current pipeline performance using the optimality gap metric: + +````@example warcraft +starting_gap = compute_gap(b, test_dataset, model, maximizer) +```` + +## Using a learning algorithm + +We can now train the model using the InferOpt.jl package: + +````@example warcraft +using InferOpt +using Flux +using Plots + +perturbed_maximizer = PerturbedMultiplicative(maximizer; ε=0.2, nb_samples=100) +loss = FenchelYoungLoss(perturbed_maximizer) + +starting_gap = compute_gap(b, test_dataset, model, maximizer) + +opt_state = Flux.setup(Adam(1e-3), model) +loss_history = Float64[] +for epoch in 1:50 + val, grads = Flux.withgradient(model) do m + sum(loss(m(sample.x), sample.y) for sample in train_dataset) / length(train_dataset) + end + Flux.update!(opt_state, model, grads[1]) + push!(loss_history, val) +end + +plot(loss_history; xlabel="Epoch", ylabel="Loss", title="Training loss") +```` + +````@example warcraft +final_gap = compute_gap(b, test_dataset, model, maximizer) +```` + +````@example warcraft +θ = model(x) +y = maximizer(θ) +plot_data(b, DataSample(; x, θ, y)) +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + From 916979cc4769f61d70ba30fb860a7d67eee44839 Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Thu, 19 Dec 2024 18:16:12 +0100 Subject: [PATCH 4/6] fix tutorial --- docs/src/tutorials/warcraft.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/warcraft.jl b/docs/src/tutorials/warcraft.jl index cbeb68e..d18414f 100644 --- a/docs/src/tutorials/warcraft.jl +++ b/docs/src/tutorials/warcraft.jl @@ -50,7 +50,7 @@ maximizer = generate_maximizer(b; dijkstra=true) # 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. y = maximizer(θ) # As we can see, currently the pipeline predicts random noise as cell weights, and therefore the maximizer returns a straight line path. -plot_data(b, DataSample(; x, θ, y)) +plot_data(b, DataSample(; x, θ_true=θ, y_true=y)) # We can evaluate the current pipeline performance using the optimality gap metric: starting_gap = compute_gap(b, test_dataset, model, maximizer) From 81ff43b8adedd4012791df73ab11e786b75589d6 Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Thu, 19 Dec 2024 21:56:45 +0100 Subject: [PATCH 5/6] fix tutorial --- docs/src/tutorials/warcraft.jl | 2 +- docs/src/warcraft.md | 155 --------------------------------- 2 files changed, 1 insertion(+), 156 deletions(-) delete mode 100644 docs/src/warcraft.md diff --git a/docs/src/tutorials/warcraft.jl b/docs/src/tutorials/warcraft.jl index d18414f..13f21ba 100644 --- a/docs/src/tutorials/warcraft.jl +++ b/docs/src/tutorials/warcraft.jl @@ -70,7 +70,7 @@ opt_state = Flux.setup(Adam(1e-3), model) loss_history = Float64[] for epoch in 1:50 val, grads = Flux.withgradient(model) do m - sum(loss(m(sample.x), sample.y) for sample in train_dataset) / length(train_dataset) + sum(loss(m(x), y_true) for (; x, y_true) in train_dataset) / length(train_dataset) end Flux.update!(opt_state, model, grads[1]) push!(loss_history, val) diff --git a/docs/src/warcraft.md b/docs/src/warcraft.md deleted file mode 100644 index bfd471b..0000000 --- a/docs/src/warcraft.md +++ /dev/null @@ -1,155 +0,0 @@ -```@meta -EditURL = "tutorials/warcraft.jl" -``` - -# Path-finding on image maps - -In this tutorial, we showcase DecisionFocusedLearningBenchmarks.jl capabilities on one of its main benchmarks: the Warcraft benchmark. -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. -The map is represented as a 2D image representing a 12x12 grid, each cell having an unknown travel cost depending on the terrain type. - -First, let's load the package and create a benchmark object as follows: - -````@example warcraft -using DecisionFocusedLearningBenchmarks -b = WarcraftBenchmark() -```` - -## Dataset generation - -These benchmark objects behave as generators that can generate various needed elements in order to build an algorithm to tackle the problem. -First of all, all benchmarks are capable of generating datasets as needed, using the [`generate_dataset`](@ref) method. -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: - -````@example warcraft -dataset = generate_dataset(b, 50); -nothing #hide -```` - -We obtain a vector of [`DataSample`](@ref) objects, containing all needed data for the problem. -Subdatasets can be created through regular slicing: - -````@example warcraft -train_dataset, test_dataset = dataset[1:45], dataset[46:50] -```` - -And getting an individual sample will return a [`DataSample`](@ref) with four fields: `x`, `instance`, `θ`, and `y`: - -````@example warcraft -sample = test_dataset[1] -```` - -`x` correspond to the input features, i.e. the input image (3D array) in the Warcraft benchmark case: - -````@example warcraft -x = sample.x -```` - -`θ_true` 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: - -````@example warcraft -θ_true = sample.θ_true -```` - -`y_true` correspond to the optimal shortest path, encoded as a binary matrix: - -````@example warcraft -y_true = sample.y_true -```` - -`instance` is not used in this benchmark, therefore set to nothing: - -````@example warcraft -isnothing(sample.instance) -```` - -For some benchmarks, we provide the following plotting method [`plot_data`](@ref) to visualize the data: - -````@example warcraft -plot_data(b, sample) -```` - -We can see here the terrain image, the true terrain weights, and the true shortest path avoiding the high cost cells. - -## Building a pipeline - -DecisionFocusedLearningBenchmarks also provides methods to build an hybrid machine learning and combinatorial optimization pipeline for the benchmark. -First, the [`generate_statistical_model`](@ref) method generates a machine learning predictor to predict cell weights from the input image: - -````@example warcraft -model = generate_statistical_model(b) -```` - -In the case of the Warcraft benchmark, the model is a convolutional neural network built using the Flux.jl package. - -````@example warcraft -θ = model(x) -```` - -Note that the model is not trained yet, and its parameters are randomly initialized. - -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: - -````@example warcraft -maximizer = generate_maximizer(b; dijkstra=true) -```` - -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. - -````@example warcraft -y = maximizer(θ) -```` - -As we can see, currently the pipeline predicts random noise as cell weights, and therefore the maximizer returns a straight line path. - -````@example warcraft -plot_data(b, DataSample(; x, θ, y)) -```` - -We can evaluate the current pipeline performance using the optimality gap metric: - -````@example warcraft -starting_gap = compute_gap(b, test_dataset, model, maximizer) -```` - -## Using a learning algorithm - -We can now train the model using the InferOpt.jl package: - -````@example warcraft -using InferOpt -using Flux -using Plots - -perturbed_maximizer = PerturbedMultiplicative(maximizer; ε=0.2, nb_samples=100) -loss = FenchelYoungLoss(perturbed_maximizer) - -starting_gap = compute_gap(b, test_dataset, model, maximizer) - -opt_state = Flux.setup(Adam(1e-3), model) -loss_history = Float64[] -for epoch in 1:50 - val, grads = Flux.withgradient(model) do m - sum(loss(m(sample.x), sample.y) for sample in train_dataset) / length(train_dataset) - end - Flux.update!(opt_state, model, grads[1]) - push!(loss_history, val) -end - -plot(loss_history; xlabel="Epoch", ylabel="Loss", title="Training loss") -```` - -````@example warcraft -final_gap = compute_gap(b, test_dataset, model, maximizer) -```` - -````@example warcraft -θ = model(x) -y = maximizer(θ) -plot_data(b, DataSample(; x, θ, y)) -```` - ---- - -*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* - From ed8a37bcfcd63fc5eb73ba1c45f179cf6ec9a0ae Mon Sep 17 00:00:00 2001 From: BatyLeo Date: Thu, 19 Dec 2024 22:29:26 +0100 Subject: [PATCH 6/6] fix tests --- test/portfolio_optimization.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/portfolio_optimization.jl b/test/portfolio_optimization.jl index aeb7c0e..7c983d7 100644 --- a/test/portfolio_optimization.jl +++ b/test/portfolio_optimization.jl @@ -22,6 +22,6 @@ y = maximizer(θ) @test length(y) == d - @test sum(y) <= 1 + @test sum(y) <= 1 + 1e-6 end end