The source files for all examples can be found in /examples.

Maximum Variance Unfolding (MVU)

This example uses Chordal.jl to perfomr the MVU dimensionality reduction technique on a toy dataset.

using Chordal
using JuMP, Hypatia
using SparseArrays, LinearAlgebra, Random
using Plots

Some helper functions are below. This section can be skipped but is included for completeness.

# Generate swissroll dataset
function swissroll(n)
    # NOTE: these could be randomized too
    theta = 1.5 .* pi .+ 2.5 .* pi .* LinRange(0, 1, n)
    z = [(i % 2)-1/2 for i in 1:n]
    return [theta' .* [cos.(theta'); sin.(theta')]; z'], theta
end

function show_data(x, theta; c=(70,70))
    Mx = maximum(abs.(x[1,:]))
    My = maximum(abs.(x[2,:]))
    Mz = maximum(abs.(x[3,:]))
    MM = max(Mx, My, Mz)
    Mx = max(Mx*1.1, MM*.9)
    My = max(My*1.1, MM*.3)
    Mz = max(Mz*1.1, MM*.3)
    return scatter(x[1,:], x[2,:], x[3,:],
        legend=false,
        zcolor=theta,
        xlims=(-Mx,Mx),
        ylims=(-My, My),
        zlims=(-Mz,Mz),
        markersize=7,
        camera=c,
    )
end

# Constructs Euclidean Distance Matrix given vectors x = [x_1 ... x_n]
function edm(x)
    G = x' * x
    dg = diag(G)
    return -2*G + dg*ones(size(G,1))' + ones(size(G,1))*dg'
end

# Given edge weights E, Eij = d(xi, xj), finds k nearest neighbors
function knn(E, k)
    nn = [Vector{Int}(undef,0) for _ in 1:size(E, 1)]
    for i in 1:size(E, 1)
        nn[i] = partialsortperm(@view(E[:,i]), 1:k+1)
    end
    return nn
end

# from nearest neighbors, constructs the MVU edge list
function knn_edges(nn, clique=true)
    k = length(nn) - 1
    E = CartesianIndex{2}[]

    for i in 1:k+1
        for j in i+1:k+1
            i == j && continue
            !clique && i > 1 && continue

            push!(E, CartesianIndex(nn[i], nn[j]))
        end
    end
    return E
end

# Returns edge list for k nearest neighbors given vectors X
function edges(x, k, clique=true)
    nn = knn(edm(x), k)
    return reduce(vcat, [knn_edges(nn[i], clique) for i in 1:length(nn)])
end

# Constructs V matrix (basis)
function get_V(n)
    return spdiagm(n, n-1, 0 => ones(n-1), -1 => -ones(n-1))
end

# Constructs constrain matrices (as in Vandenberghe and Andersen pg 401)
function get_constraint_matrices(x, k)
    n = size(x, 2)
    D = edm(x)

    E = edges(x, k)
    Ei = [CartesianIndex(e[1], e[1]) for e in E]
    Ej = [CartesianIndex(e[2], e[2]) for e in E]

    b = [D[e] for e in E]
    A = [sparse(
            [e[1], e[2], e[1], e[2]],
            [e[1], e[2], e[2], e[1]],
            [1, 1, -1, -1], n, n
         ) for e in E]
    V = get_V(n)
    VA = [V'*Ai*V for Ai in A]

    return V, VA, b
end

# Returns a SparseMatrixCSC with values of Zref
#   Zref is a SparseAxisArray, which functions like a dictionary
#   - Requires optimizer to have been run
function reconstruct_from_sparse_varref(Zref, n)
    Zcv = value.(Zref)
    Z_uncomp = spzeros(Float64, n, n)
    for ind in eachindex(Zcv)
        Z_uncomp[ind[1], ind[2]] = Zcv[ind]
    end
    return Z_uncomp
end
reconstruct_from_sparse_varref (generic function with 1 method)

Problem Setup

First, we construct a "swiss roll" test dataset, which is 3d but has intrinsic dimension 2

# Construct Dataset
n, k = 80, 2
x, theta = swissroll(n)
show_data(x, theta)

Next, we setup the MVU SDP

# Construct problem data
V, VA, b = get_constraint_matrices(x, k)
C = V'*V;

Chordal Decomposition and Solve

# Chordal setup
_, _, _, cliques = get_selectors(sparsity_pattern([VA..., C]); verbose=false, ret_cliques=true, perm=nothing)
sp = Chordal.get_sparsity_pattern_from_cliques(cliques)
sp_inds = zip(findnz(sp)[1:2]...)

# -- Construct Model --
model = Model(Hypatia.Optimizer)
Z = @variable(model, Z[i=1:n-1, j=1:n-1; (i,j) in sp_inds])

# Objective
@objective(model, Max, 1/length(sp_inds) * Chordal.tr(C, Z))

# Distrance-preserving equality constraints
for i in 1:length(b)
    @constraint(model, Chordal.tr(VA[i], Z) == b[i])
end

# Chordal decomposition constraints (replace big PSD constraints with many small ones)
for cl in cliques
    len_cl = length(cl)
    Zp = Matrix{VariableRef}(undef, len_cl, len_cl)
    for i in 1:len_cl, j in 1:len_cl
        Zp[i,j] = Z[cl[i], cl[j]]
    end
    @constraint(model, Zp in PSDCone())
end

# Optimize
optimize!(model)
solution_summary(model)
* Solver : Hypatia

* Status
  Termination status : OTHER_ERROR
  Primal status      : UNKNOWN_RESULT_STATUS
  Dual status        : UNKNOWN_RESULT_STATUS
  Message from the solver:
  "NumericalFailure"

* Candidate solution
  Objective value      : 50.752317608787514
  Objective bound      : 50.7513069465708
  Dual objective value : 50.7513069465708

* Work counters
  Solve time (sec)   : 16.10786
  Simplex iterations : 66

Solution Reconstruction

# Reconstruct solution with min rank PSD completion
Z_ = Chordal.reconstruct_from_sparse_varref(Z, n-1)
Z_ = 0.5*(Z_ + Z_') # Deals with numerical error
Z_factor = Chordal.minrank_completion(Z_)
W, sv, _ = svd(V*Z_factor)
yc = Diagonal(sv[1:3])*W[:, 1:3]'
show_data(yc, theta; c =(40,70))

This page was generated using Literate.jl.