plot_sparsity (generic function with 1 method)

Visualization of Frank-Wolfe running on a 2-dimensional polytope

This example provides an intuitive view of the Frank-Wolfe algorithm by running it on a polyhedral set with a quadratic function. The Linear Minimization Oracle (LMO) corresponds to a call to a generic simplex solver from MathOptInterface.jl (MOI).

Import and setup

We first import the necessary packages, including Polyhedra to visualize the feasible set.

using LinearAlgebra
using FrankWolfe

import MathOptInterface
const MOI = MathOptInterface
using GLPK

using Polyhedra
using Plots

We can then define the objective function, here the squared distance to a point in the place, and its in-place gradient.

n = 2
y = [3.2, 0.5]

function f(x)
    return 1 / 2 * norm(x - y)^2
end
function grad!(storage, x)
    @. storage = x - y
end
grad! (generic function with 1 method)

Custom callback

FrankWolfe.jl lets users define custom callbacks to record information about each iteration. In that case, the callback will copy the current iterate x, the current vertex v, and the current step size gamma to an array thanks to a closure. We then declare the array and the callback over this array. Each iteration will then push to this array.

function build_callback(trajectory_arr)
    return function callback(state, args...)
        return push!(trajectory_arr, (copy(state.x), copy(state.v), state.gamma))
    end
end

iterates_information_vector = []
callback = build_callback(iterates_information_vector)
callback (generic function with 1 method)

Creating the Linear Minimization Oracle

The LMO is defined as a call to a linear optimization solver, each iteration resets the objective and calls the solver. The linear constraints must be defined only once at the beginning and remain identical along iterations. We use here MathOptInterface directly but the constraints could also be defined with JuMP or Convex.jl.

o = GLPK.Optimizer()
x = MOI.add_variables(o, n)

# −x + y ≤ 2
c1 = MOI.add_constraint(o, -1.0x[1] + x[2], MOI.LessThan(2.0))

# x + 2 y ≤ 4
c2 = MOI.add_constraint(o, x[1] + 2.0x[2], MOI.LessThan(4.0))

# −2 x − y ≤ 1
c3 = MOI.add_constraint(o, -2.0x[1] - x[2], MOI.LessThan(1.0))

# x − 2 y ≤ 2
c4 = MOI.add_constraint(o, x[1] - 2.0x[2], MOI.LessThan(2.0))

# x ≤ 2
c5 = MOI.add_constraint(o, x[1] + 0.0x[2], MOI.LessThan(2.0))
MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}(5)

The LMO is then built by wrapping the current MOI optimizer

lmo_moi = FrankWolfe.MathOptLMO(o)
FrankWolfe.MathOptLMO{GLPK.Optimizer}(A GLPK model, true)

Calling Frank-Wolfe

We can now compute an initial starting point from any direction and call the Frank-Wolfe algorithm. Note that we copy x0 before passing it to the algorithm because it is modified in-place by frank_wolfe.

x0 = FrankWolfe.compute_extreme_point(lmo_moi, zeros(n))

xfinal, vfinal, primal_value, dual_gap, traj_data = FrankWolfe.frank_wolfe(
    f,
    grad!,
    lmo_moi,
    copy(x0),
    line_search=FrankWolfe.Adaptive(),
    max_iteration=10,
    epsilon=1e-8,
    callback=callback,
    verbose=true,
    print_iter=1,
)
(x = [2.0, 0.5000005296253868], v = [2.0, 0.0], primal = 0.7200000000001404, dual_gap = 2.648129737714555e-7, traj_data = Any[])

We now collect the iterates and vertices across iterations.

iterates = Vector{Vector{Float64}}()
push!(iterates, x0)
vertices = Vector{Vector{Float64}}()
for s in iterates_information_vector
    push!(iterates, s[1])
    push!(vertices, s[2])
end

Plotting the algorithm run

We define another method for f adapted to plot its contours.

function f(x1, x2)
    x = [x1, x2]
    return f(x)
end

xlist = collect(range(-1, 3, step=0.2))
ylist = collect(range(-1, 3, step=0.2))

X = repeat(reshape(xlist, 1, :), length(ylist), 1)
Y = repeat(ylist, 1, length(xlist))
21×21 Matrix{Float64}:
 -1.0  -1.0  -1.0  -1.0  -1.0  -1.0  …  -1.0  -1.0  -1.0  -1.0  -1.0  -1.0
 -0.8  -0.8  -0.8  -0.8  -0.8  -0.8     -0.8  -0.8  -0.8  -0.8  -0.8  -0.8
 -0.6  -0.6  -0.6  -0.6  -0.6  -0.6     -0.6  -0.6  -0.6  -0.6  -0.6  -0.6
 -0.4  -0.4  -0.4  -0.4  -0.4  -0.4     -0.4  -0.4  -0.4  -0.4  -0.4  -0.4
 -0.2  -0.2  -0.2  -0.2  -0.2  -0.2     -0.2  -0.2  -0.2  -0.2  -0.2  -0.2
  0.0   0.0   0.0   0.0   0.0   0.0  …   0.0   0.0   0.0   0.0   0.0   0.0
  0.2   0.2   0.2   0.2   0.2   0.2      0.2   0.2   0.2   0.2   0.2   0.2
  0.4   0.4   0.4   0.4   0.4   0.4      0.4   0.4   0.4   0.4   0.4   0.4
  0.6   0.6   0.6   0.6   0.6   0.6      0.6   0.6   0.6   0.6   0.6   0.6
  0.8   0.8   0.8   0.8   0.8   0.8      0.8   0.8   0.8   0.8   0.8   0.8
  ⋮                             ⋮    ⋱   ⋮                             ⋮
  1.4   1.4   1.4   1.4   1.4   1.4      1.4   1.4   1.4   1.4   1.4   1.4
  1.6   1.6   1.6   1.6   1.6   1.6      1.6   1.6   1.6   1.6   1.6   1.6
  1.8   1.8   1.8   1.8   1.8   1.8      1.8   1.8   1.8   1.8   1.8   1.8
  2.0   2.0   2.0   2.0   2.0   2.0  …   2.0   2.0   2.0   2.0   2.0   2.0
  2.2   2.2   2.2   2.2   2.2   2.2      2.2   2.2   2.2   2.2   2.2   2.2
  2.4   2.4   2.4   2.4   2.4   2.4      2.4   2.4   2.4   2.4   2.4   2.4
  2.6   2.6   2.6   2.6   2.6   2.6      2.6   2.6   2.6   2.6   2.6   2.6
  2.8   2.8   2.8   2.8   2.8   2.8      2.8   2.8   2.8   2.8   2.8   2.8
  3.0   3.0   3.0   3.0   3.0   3.0  …   3.0   3.0   3.0   3.0   3.0   3.0

The feasible space is represented using Polyhedra.

h =
    HalfSpace([-1, 1], 2) ∩ HalfSpace([1, 2], 4) ∩ HalfSpace([-2, -1], 1) ∩ HalfSpace([1, -2], 2) ∩
    HalfSpace([1, 0], 2)

p = polyhedron(h)

p1 = contour(xlist, ylist, f, fill=true, line_smoothing=0.85)
plot(p1, opacity=0.5)
plot!(
    p,
    ratio=:equal,
    opacity=0.5,
    label="feasible region",
    framestyle=:zerolines,
    legend=true,
    color=:blue,
);

Finally, we add all iterates and vertices to the plot.

colors = ["gold", "purple", "darkorange2", "firebrick3"]
iterates = unique!(iterates)
for i in 1:3
    scatter!(
        [iterates[i][1]],
        [iterates[i][2]],
        label=string("x_", i - 1),
        markersize=6,
        color=colors[i],
    )
end
scatter!(
    [last(iterates)[1]],
    [last(iterates)[2]],
    label=string("x_", length(iterates) - 1),
    markersize=6,
    color=last(colors),
)

plot chosen vertices

scatter!([vertices[1][1]], [vertices[1][2]], m=:diamond, markersize=6, color=colors[1], label="v_1")
scatter!(
    [vertices[2][1]],
    [vertices[2][2]],
    m=:diamond,
    markersize=6,
    color=colors[2],
    label="v_2",
    legend=:outerleft,
    colorbar=true,
)

This page was generated using Literate.jl.