Skip to content

Commit 0f7347b

Browse files
committed
Fix review comments
1 parent d9ad94d commit 0f7347b

3 files changed

Lines changed: 301 additions & 4 deletions

File tree

Project.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
name = "DecisionFocusedLearningAlgorithms"
22
uuid = "46d52364-bc3b-4fac-a992-eb1d3ef2de15"
3-
version = "0.2.0"
43
authors = ["Members of JuliaDecisionFocusedLearning and contributors"]
5-
6-
[workspace]
7-
projects = ["docs", "test"]
4+
version = "0.2.0"
85

96
[deps]
107
DecisionFocusedLearningBenchmarks = "2fbe496a-299b-4c81-bab5-c44dfc55cf20"
118
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
9+
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
1210
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
1311
InferOpt = "4846b161-c94e-4150-8dac-c7ae193c601f"
12+
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
1413
MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54"
14+
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
1515
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
1616
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1717
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
@@ -21,12 +21,18 @@ ValueHistories = "98cad3c8-aec3-5f06-8e41-884608649ab7"
2121
[compat]
2222
DecisionFocusedLearningBenchmarks = "0.5.0, 0.6"
2323
DocStringExtensions = "0.9.5"
24+
Documenter = "1.17.0"
2425
Flux = "0.16.9"
2526
InferOpt = "0.7.1"
27+
Literate = "2.21.0"
2628
MLUtils = "0.4.8"
29+
Plots = "1.41.6"
2730
ProgressMeter = "1.11.0"
2831
Random = "1.11.0"
2932
Statistics = "1.11.1"
3033
UnicodePlots = "3.8.2"
3134
ValueHistories = "0.5.6"
3235
julia = "1.11"
36+
37+
[workspace]
38+
projects = ["docs", "test"]
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""
2+
$TYPEDEF
3+
4+
Mirror Descent algorithm for learning coordinated solutions.
5+
6+
This algorithm is designed for stochastic benchmarks.
7+
8+
Reference: <https://arxiv.org/abs/2505.04757>
9+
10+
# Fields
11+
$TYPEDFIELDS
12+
"""
13+
@kwdef struct MirrorDescent{A<:PerturbedFenchelYoungLossImitation} <: AbstractAlgorithm
14+
"inner imitation algorithm for supervised learning"
15+
inner_algorithm::A = PerturbedFenchelYoungLossImitation()
16+
end
17+
18+
"""
19+
$TYPEDSIGNATURES
20+
21+
Train a DFLPolicy using the Mirror Descent algorithm on a provided training dataset.
22+
23+
# Core training method
24+
25+
# Arguments
26+
- `epochs`: number of training epochs per iteration
27+
- `iterations`: number of mirror descent iterations
28+
- `κ`: scaling factor for the perturbation magnitude
29+
- `metrics`: tuple of metrics to track during training
30+
- `verbose`: if true, prints progress at each iteration
31+
- `imitation_start`: if true, the first iteration uses pure imitation learning (no perturbation)
32+
"""
33+
34+
function train_policy!(
35+
benchmark::ExogenousStochasticBenchmark,
36+
algorithm::MirrorDescent,
37+
policy::DFLPolicy,
38+
train_dataset,
39+
anticipative_solver,
40+
perturbed_anticipative_solver;
41+
epochs=10,
42+
iterations=10,
43+
κ=1.0,
44+
metrics::Tuple=(),
45+
verbose::Bool=false,
46+
imitation_start::Bool=true
47+
)
48+
49+
augmented_dataset = train_dataset
50+
return map(1:iterations) do n_it
51+
if verbose
52+
println("Iteration $n_it / $iterations")
53+
end
54+
55+
perturb = n_it > 1 || !imitation_start
56+
57+
augmented_dataset = augment_dataset(
58+
benchmark, augmented_dataset, policy.statistical_model, anticipative_solver, perturbed_anticipative_solver;
59+
κ=κ, perturb=perturb
60+
)
61+
62+
train_policy!(
63+
algorithm.inner_algorithm,
64+
policy,
65+
augmented_dataset;
66+
epochs=epochs,
67+
metrics=metrics,
68+
maximizer_kwargs=sample -> sample.context,
69+
)
70+
end
71+
end
72+
73+
"""
74+
$TYPEDSIGNATURES
75+
76+
Generate a dataset for the provided benchmark and train a DFLPolicy using the Mirror Descent algorithm.
77+
78+
# Benchmark convenience wrapper
79+
80+
This high-level function handles all setup from the benchmark and returns a trained policy.
81+
82+
# Arguments
83+
- `dataset_size`: number of samples in the training dataset
84+
- `epochs`: number of training epochs per iteration
85+
- `iterations`: number of mirror descent iterations
86+
- `κ`: scaling factor for the perturbation magnitude
87+
- `metrics`: tuple of metrics to track during training
88+
- `seed`: random seed for reproducibility
89+
- `verbose`: if true, prints progress at each iteration
90+
- `imitation_start`: if true, the first iteration uses pure imitation learning (no perturbation)
91+
- `model_kwargs`: additional keyword arguments passed to `generate_statistical_model`
92+
- `maximizer_kwargs`: additional keyword arguments passed to `generate_maximizer`
93+
- `solver_kwargs`: additional keyword arguments passed to `generate_anticipative_solver` and `generate_parametric_anticipative_solver`
94+
- `nb_scenarios`: number of scenarios per instance.
95+
- `context_per_instance`: number of contexts per instance.
96+
"""
97+
98+
99+
100+
function train_policy(
101+
algorithm::MirrorDescent,
102+
benchmark::ExogenousStochasticBenchmark;
103+
dataset_size=30,
104+
epochs=10,
105+
iterations=10,
106+
κ=1.0,
107+
metrics::Tuple=(),
108+
seed=nothing,
109+
verbose::Bool=false,
110+
imitation_start::Bool=true,
111+
model_kwargs=(;),
112+
maximizer_kwargs=(;),
113+
solver_kwargs=(;),
114+
nb_scenarios = 1,
115+
context_per_instance = 1,
116+
)
117+
train_dataset = generate_dataset(benchmark, dataset_size; nb_scenarios=nb_scenarios, contexts_per_instance=context_per_instance, seed=seed)
118+
119+
model = generate_statistical_model(benchmark; seed=seed, model_kwargs...)
120+
maximizer = generate_maximizer(benchmark; maximizer_kwargs...)
121+
policy = DFLPolicy(model, maximizer)
122+
123+
anticipative_solver = generate_anticipative_solver(benchmark; solver_kwargs...)
124+
parametric_anticipative_solver = generate_parametric_anticipative_solver(benchmark; solver_kwargs...)
125+
(; nb_samples, ε, threaded, seed) = algorithm.inner_algorithm
126+
perturbed_anticipative_solver = PerturbedAdditive((θ; scenario, kwargs...) -> parametric_anticipative_solver(θ, scenario; kwargs...); ε=κ*ε, nb_samples=nb_samples, seed=seed, threaded=threaded)
127+
128+
129+
histories_per_iteration = train_policy!(
130+
benchmark, algorithm, policy, train_dataset, anticipative_solver, perturbed_anticipative_solver;
131+
epochs=epochs, iterations=iterations, κ=κ, metrics=metrics, verbose=verbose, imitation_start=imitation_start
132+
)
133+
134+
return histories_per_iteration, policy
135+
end
136+
137+
function augment_dataset(
138+
bench::ExogenousStochasticBenchmark,
139+
train_dataset::AbstractArray,
140+
model,
141+
anticipative_solver,
142+
perturbed_anticipative_solver;
143+
κ=1.0,
144+
perturb=false
145+
)
146+
return _augment_dataset(
147+
Val(fieldtype(eltype(train_dataset), :y) !== Nothing),
148+
bench, train_dataset, model, anticipative_solver, perturbed_anticipative_solver;
149+
κ=κ, perturb=perturb
150+
)
151+
end
152+
153+
# Raw dataset (samples have no y) → create new DataSamples
154+
function _augment_dataset(
155+
::Val{false},
156+
bench, train_dataset, model, anticipative_solver, perturbed_anticipative_solver;
157+
κ=1.0, perturb=false
158+
)
159+
return map(train_dataset) do sample
160+
θ = model(sample.x)
161+
if perturb
162+
if is_minimization_problem(bench)
163+
y = perturbed_anticipative_solver(-κ*θ; scenario=sample.scenario, sample.context...)
164+
else
165+
y = perturbed_anticipative_solver*θ; scenario=sample.scenario, sample.context...)
166+
end
167+
else
168+
y = anticipative_solver(sample.scenario; sample.context...)
169+
end
170+
DataSample(sample; y=y)
171+
end
172+
end
173+
174+
# Augmented dataset (samples already have y) → update y in place
175+
function _augment_dataset(
176+
::Val{true},
177+
bench, train_dataset, model, anticipative_solver, perturbed_anticipative_solver;
178+
κ=1.0, perturb=false
179+
)
180+
for (i, sample) in enumerate(train_dataset)
181+
θ = model(sample.x)
182+
if perturb
183+
if is_minimization_problem(bench)
184+
y = perturbed_anticipative_solver(-κ*θ; scenario=sample.scenario, sample.context...)
185+
else
186+
y = perturbed_anticipative_solver*θ; scenario=sample.scenario, sample.context...)
187+
end
188+
else
189+
y = anticipative_solver(sample.scenario; sample.context...)
190+
end
191+
ET = eltype(sample.y)
192+
y_converted = convert(typeof(sample.y), ET <: Integer ? round.(ET, y) : y)
193+
train_dataset[i] = DataSample(sample; y=y_converted)
194+
end
195+
return train_dataset
196+
end

test/mirror_descent.jl

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using DecisionFocusedLearningAlgorithms
2+
using DecisionFocusedLearningBenchmarks
3+
using Test
4+
using ValueHistories
5+
using Statistics: mean
6+
7+
@testset "MirrorDescent Training" begin
8+
9+
@testset "MirrorDescent - ContextualStochasticArgmax basic" begin
10+
benchmark = ContextualStochasticArgmaxBenchmark()
11+
algorithm = MirrorDescent()
12+
13+
histories, policy = train_policy(
14+
algorithm, benchmark;
15+
dataset_size=5, epochs=2, iterations=2, seed=0
16+
)
17+
18+
@test histories isa Vector
19+
@test length(histories) == 2
20+
@test all(h isa MVHistory for h in histories)
21+
@test all(haskey(h, :training_loss) for h in histories)
22+
@test policy isa DFLPolicy
23+
end
24+
25+
@testset "MirrorDescent - StochasticVehicleScheduling basic" begin
26+
benchmark = StochasticVehicleSchedulingBenchmark()
27+
algorithm = MirrorDescent()
28+
29+
histories, policy = train_policy(
30+
algorithm, benchmark;
31+
dataset_size=1, epochs=2, iterations=2, seed=0
32+
)
33+
34+
@test histories isa Vector
35+
@test length(histories) == 2
36+
@test all(h isa MVHistory for h in histories)
37+
@test all(haskey(h, :training_loss) for h in histories)
38+
@test policy isa DFLPolicy
39+
end
40+
41+
@testset "MirrorDescent - imitation_start=false" begin
42+
benchmark = ContextualStochasticArgmaxBenchmark()
43+
algorithm = MirrorDescent()
44+
45+
histories, policy = train_policy(
46+
algorithm, benchmark;
47+
dataset_size=5, epochs=2, iterations=2, seed=0, imitation_start=false
48+
)
49+
50+
@test histories isa Vector
51+
@test length(histories) == 2
52+
@test policy isa DFLPolicy
53+
end
54+
55+
@testset "MirrorDescent - performance improves over iterations" begin
56+
benchmark = ContextualStochasticArgmaxBenchmark()
57+
algorithm = MirrorDescent()
58+
59+
val_dataset = generate_dataset(benchmark, 100; seed=99)
60+
61+
val_metric = FunctionMetric(:val_obj, val_dataset) do ctx, data
62+
vals = map(data) do s
63+
θ = ctx.policy.statistical_model(s.x)
64+
y = ctx.policy.maximizer(θ; s.context...)
65+
Float64(DecisionFocusedLearningBenchmarks.objective_value(benchmark, s, y))
66+
end
67+
(val_obj = mean(vals),)
68+
end
69+
70+
histories, policy = train_policy(
71+
algorithm, benchmark;
72+
dataset_size=20, epochs=3, iterations=5, seed=0, metrics=(val_metric,)
73+
)
74+
75+
val_objs = [get(histories[i], :val_obj)[2][end] for i in 1:5]
76+
77+
# Performance should improve at each iteration
78+
@test (val_objs[4] > val_objs[1])
79+
end
80+
81+
@testset "MirrorDescent - with metrics" begin
82+
benchmark = ContextualStochasticArgmaxBenchmark()
83+
algorithm = MirrorDescent()
84+
85+
metrics = (FunctionMetric(ctx -> ctx.epoch, :epoch),)
86+
87+
histories, policy = train_policy(
88+
algorithm, benchmark;
89+
dataset_size=5, epochs=2, iterations=2, seed=0, metrics=metrics
90+
)
91+
92+
@test all(haskey(h, :epoch) for h in histories)
93+
end
94+
95+
end

0 commit comments

Comments
 (0)