Skip to content

Commit 9556c33

Browse files
committed
implement ranking
1 parent 335971c commit 9556c33

File tree

4 files changed

+268
-0
lines changed

4 files changed

+268
-0
lines changed

src/Ranking/Ranking.jl

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
module Ranking
2+
3+
using ..Utils
4+
using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES
5+
using Flux: Chain, Dense
6+
using Random
7+
8+
"""
9+
$TYPEDEF
10+
11+
Benchmark problem with an argmax as the CO algorithm.
12+
13+
# Fields
14+
$TYPEDFIELDS
15+
"""
16+
struct RankingBenchmark <: AbstractBenchmark
17+
"iinstances dimension, total number of classes"
18+
instance_dim::Int
19+
"number of features"
20+
nb_features::Int
21+
end
22+
23+
function Base.show(io::IO, bench::RankingBenchmark)
24+
(; instance_dim, nb_features) = bench
25+
return print(
26+
io, "RankingBenchmark(instance_dim=$instance_dim, nb_features=$nb_features)"
27+
)
28+
end
29+
30+
function RankingBenchmark(; instance_dim::Int=10, nb_features::Int=5)
31+
return RankingBenchmark(instance_dim, nb_features)
32+
end
33+
34+
"""
35+
$TYPEDSIGNATURES
36+
37+
Compute the vector `r` such that `rᵢ` is the rank of `θᵢ` in `θ`.
38+
"""
39+
function ranking::AbstractVector; rev::Bool=false, kwargs...)
40+
return invperm(sortperm(θ; rev=rev))
41+
end
42+
43+
"""
44+
$TYPEDSIGNATURES
45+
46+
Return a top k maximizer.
47+
"""
48+
function Utils.generate_maximizer(bench::RankingBenchmark)
49+
return ranking
50+
end
51+
52+
"""
53+
$TYPEDSIGNATURES
54+
55+
Generate a dataset of labeled instances for the subset selection problem.
56+
The mapping between features and cost is identity.
57+
"""
58+
function Utils.generate_dataset(bench::RankingBenchmark, dataset_size::Int=10; seed::Int=0)
59+
(; instance_dim, nb_features) = bench
60+
rng = MersenneTwister(seed)
61+
features = [randn(rng, Float32, nb_features, instance_dim) for _ in 1:dataset_size]
62+
mapping = Chain(Dense(nb_features => 1; bias=false), vec)
63+
costs = mapping.(features)
64+
solutions = ranking.(costs)
65+
return [
66+
DataSample(; x, θ_true, y_true) for
67+
(x, θ_true, y_true) in zip(features, costs, solutions)
68+
]
69+
end
70+
71+
"""
72+
$TYPEDSIGNATURES
73+
74+
Initialize a linear model for `bench` using `Flux`.
75+
"""
76+
function Utils.generate_statistical_model(bench::RankingBenchmark; seed=0)
77+
Random.seed!(seed)
78+
(; nb_features) = bench
79+
return Chain(Dense(nb_features => 1; bias=false), vec)
80+
end
81+
82+
export RankingBenchmark
83+
84+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
solve_deterministic_VSP(instance::Instance; include_delays=true)
3+
4+
Return the optimal solution of the deterministic VSP problem associated to `instance`.
5+
The objective function is `vehicle_cost * nb_vehicles + include_delays * delay_cost * sum_of_travel_times`
6+
Note: If you have Gurobi, use `grb_model` as `model_builder` instead od `cbc_model`.
7+
"""
8+
function solve_deterministic_VSP(
9+
instance::Instance; include_delays=true, model_builder=cbc_model
10+
)
11+
(; city, graph) = instance
12+
13+
travel_times = [
14+
distance(task1.end_point, task2.start_point) for task1 in city.tasks,
15+
task2 in city.tasks
16+
]
17+
18+
model = model_builder()
19+
20+
nb_nodes = nv(graph)
21+
job_indices = 2:(nb_nodes - 1)
22+
23+
@variable(model, x[i=1:nb_nodes, j=1:nb_nodes; has_edge(graph, i, j)], Bin)
24+
25+
@objective(
26+
model,
27+
Min,
28+
instance.city.vehicle_cost * sum(x[1, j] for j in job_indices) +
29+
include_delays *
30+
instance.city.delay_cost *
31+
sum(
32+
travel_times[i, j] * x[i, j] for i in 1:nb_nodes for
33+
j in 1:nb_nodes if has_edge(graph, i, j)
34+
)
35+
)
36+
37+
@constraint(
38+
model,
39+
flow[i in job_indices],
40+
sum(x[j, i] for j in inneighbors(graph, i)) ==
41+
sum(x[i, j] for j in outneighbors(graph, i))
42+
)
43+
@constraint(
44+
model, demand[i in job_indices], sum(x[j, i] for j in inneighbors(graph, i)) == 1
45+
)
46+
47+
optimize!(model)
48+
49+
solution = solution_from_JuMP_array(value.(x), graph)
50+
51+
return objective_value(model), solution
52+
end
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
move_one_random_task!(path_value::BitMatrix, graph::AbstractGraph)
3+
4+
Select one random (uniform) task and move it to another random (uniform) feasible vehicle
5+
"""
6+
function move_one_random_task!(path_value::BitMatrix, graph::AbstractGraph)
7+
nb_tasks = size(path_value, 2)
8+
selected_task = rand(DiscreteUniform(1, nb_tasks))
9+
selected_vehicle = find_first_one(@view path_value[:, selected_task])
10+
11+
can_be_inserted = Int[]
12+
# do not empty if already empty
13+
empty_encountered = false #sum(@view path_value[selected_vehicle, :]) == 1 ? true : false
14+
for i in 1:nb_tasks
15+
if i == selected_vehicle
16+
continue
17+
end
18+
# else
19+
is_empty = false
20+
if selected_task > 1
21+
before = @view path_value[i, 1:(selected_task - 1)]
22+
if any(before)
23+
precedent_task = selected_task - find_first_one(reverse(before))
24+
if !has_edge(graph, precedent_task + 1, selected_task + 1)
25+
continue
26+
end
27+
elseif empty_encountered
28+
continue
29+
else # if !empty_encountered
30+
is_empty = true
31+
end
32+
end
33+
34+
if selected_task < nb_tasks
35+
after = @view path_value[i, (selected_task + 1):end]
36+
if any(after)
37+
next_task =
38+
selected_task +
39+
find_first_one(@view path_value[i, (selected_task + 1):end])
40+
if !has_edge(graph, selected_task + 1, next_task + 1)
41+
continue
42+
end
43+
elseif empty_encountered
44+
continue
45+
elseif !empty_encountered && is_empty
46+
empty_encountered = true
47+
end
48+
end
49+
50+
push!(can_be_inserted, i)
51+
end
52+
if length(can_be_inserted) == 0
53+
@warn "No space to be inserted" selected_task path_value
54+
return nothing
55+
end
56+
new_vehicle = rand(can_be_inserted)
57+
path_value[selected_vehicle, selected_task] = false
58+
path_value[new_vehicle, selected_task] = true
59+
return nothing
60+
end
61+
62+
"""
63+
local_search(solution::Solution, instance::AbstractInstance; nb_it::Integer=100)
64+
65+
Very simple local search heuristic, using the neighborhood defined by `move_one_random_task`
66+
"""
67+
function local_search(solution::Solution, instance::AbstractInstance; nb_it::Integer=100)
68+
best_solution = copy(solution.path_value)
69+
best_value = evaluate_solution(solution, instance)
70+
history_x = [0]
71+
history_y = [best_value]
72+
73+
candidate_solution = copy(solution.path_value)
74+
for it in 1:nb_it
75+
move_one_random_task!(candidate_solution, instance.graph)
76+
77+
value = evaluate_solution(candidate_solution, instance)
78+
if value <= best_value # keep changes
79+
best_solution = copy(candidate_solution)
80+
best_value = value
81+
push!(history_x, it)
82+
push!(history_y, best_value)
83+
else # revert changes
84+
candidate_solution = copy(best_solution)
85+
end
86+
end
87+
88+
return Solution(best_solution, instance), best_value, history_x, history_y
89+
end
90+
91+
"""
92+
heuristic_solution(instance::AbstractInstance; nb_it=100)
93+
94+
Very simple heuristic, using [`local_search`](@ref)
95+
initialised with the solution of the deterministic Linear program
96+
"""
97+
function heuristic_solution(instance::AbstractInstance; nb_it=100)
98+
_, initial_solution = solve_deterministic_VSP(instance)
99+
sol, _, _, _ = local_search(initial_solution, instance; nb_it=nb_it)
100+
return sol
101+
end

test/ranking.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@testitem "Ranking" begin
2+
using DecisionFocusedLearningBenchmarks
3+
4+
instance_dim = 10
5+
nb_features = 5
6+
7+
b = RankingBenchmark(; instance_dim=instance_dim, nb_features=nb_features)
8+
9+
io = IOBuffer()
10+
show(io, b)
11+
@test String(take!(io)) == "RankingBenchmark(instance_dim=10, nb_features=5)"
12+
13+
dataset = generate_dataset(b, 50)
14+
model = generate_statistical_model(b)
15+
maximizer = generate_maximizer(b)
16+
17+
for (i, sample) in enumerate(dataset)
18+
(; x, θ_true, y_true) = sample
19+
@test size(x) == (nb_features, instance_dim)
20+
@test length(θ_true) == instance_dim
21+
@test length(y_true) == instance_dim
22+
@test isnothing(sample.instance)
23+
@test all(y_true .== maximizer(θ_true))
24+
25+
θ = model(x)
26+
@test length(θ) == instance_dim
27+
28+
y = maximizer(θ)
29+
@test length(y) == instance_dim
30+
end
31+
end

0 commit comments

Comments
 (0)