Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement Diffusion Map for custom kernel and for precomputed Gram matrix #28

Merged
merged 9 commits into from
Feb 14, 2022
6 changes: 3 additions & 3 deletions src/ManifoldLearning.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module ManifoldLearning
import Base: show, summary
import SparseArrays: AbstractSparseArray, SparseMatrixCSC, spzeros, spdiagm, findnz
import Statistics: mean, std
import StatsBase: StatsBase, fit, standardize
import StatsBase: StatsBase, fit, standardize, pairwise
import MultivariateStats: outdim, projection, KernelPCA, transform, transform!,
principalvars, dmat2gram, gram2dmat, pairwise
import LinearAlgebra: eigvals, mul!, svd, qr, Symmetric, eigen, eigen!, tr, rmul!, I, norm, Diagonal
principalvars, dmat2gram, gram2dmat
import LinearAlgebra: eigvals, mul!, svd, qr, Symmetric, eigen, eigen!, tr, rmul!, I, norm, Diagonal, issymmetric
import LightGraphs: neighbors, nv, add_edge!, connected_components, vertices,
dijkstra_shortest_paths, induced_subgraph, weights
import SimpleWeightedGraphs: SimpleWeightedGraph
Expand Down
60 changes: 44 additions & 16 deletions src/diffmaps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,33 +46,60 @@ end
Fit a isometric mapping model to `data`.

# Arguments
* `data`: a matrix of observations. Each column of `data` is an observation.
* `data::Matrix`: a (n_features, n_observations) matrix of observations. Each column of `data` is an observation.
if `isnothing(kernel)`, `data` is instead the (n_observations, n_observations) precomputed Gram matrix.

# Keyword arguments
* `maxoutdim`: a dimension of the reduced space.
* `t`: a number of transitions
* `α`: a normalization parameter
* `ɛ`: a Gaussian kernel variance (the scale parameter)
* `kernel::Union{Nothing, Function}=(x, y) -> exp(-sum((x .- y) .^ 2) / ɛ)`: the kernel function.
maps two input vectors (observations) to a scalar (a metric of their similarity).
by default, a Gaussian kernel. if `isnothing(kernel)`, we assume `data` is instead
the (n_observations, n_observations) precomputed Gram matrix.
* `ɛ::Real=1.0`: the Gaussian kernel variance (the scale parameter). ignored if custom `kernel` passed.
* `maxoutdim::Int=2`: the dimension of the reduced space.
* `t::Int=1`: the number of transitions
* `α::Real=0.0`: a normalization parameter

# Examples
```julia
M = fit(DiffMap, rand(3,100)) # construct diffusion map model
R = transform(M) # perform dimensionality reduction
X = rand(3, 100) # toy data matrix, 100 observations

# default kernel
M = fit(DiffMap, X) # construct diffusion map model
R = transform(M) # perform dimensionality reduction

# custom kernel
kernel = (x, y) -> x' * y # linear kernel
M = fit(DiffMap, X, kernel=kernel)

# precomputed Gram matrix
kernel = (x, y) -> x' * y # linear kernel
K = StatsBase.pairwise(kernel, eachcol(X), symmetric=true) # custom Gram matrix
M = fit(DiffMap, K, kernel=nothing)
```
"""
function fit(::Type{DiffMap}, X::AbstractMatrix{T}; maxoutdim::Int=2, t::Int=1, α::Real=0.0, ɛ::Real=1.0) where {T<:Real}
# compute kernel matrix
sumX = sum(X.^ 2, dims=1)
L = exp.(-( transpose(sumX) .+ sumX .- 2*transpose(X) * X ) ./ convert(T, ɛ))
# L = pairwise((x,y)-> exp(-norm(x-y,2)^2/ε), Xtr)
function fit(::Type{DiffMap}, X::AbstractMatrix{T};
ɛ::Real=1.0,
kernel::Union{Nothing, Function}=(x, y) -> exp(-sum((x .- y) .^ 2) / convert(T, ɛ)),
maxoutdim::Int=2,
t::Int=1,
α::Real=0.0
) where {T<:Real}
if isa(kernel, Function)
# compute Gram matrix
L = pairwise(kernel, eachcol(X), symmetric=true)
else
# X is the pre-computed Gram matrix
L = deepcopy(X) # deep copy needed b/c of procedure for α > 0
@assert issymmetric(L)
end

# Calculate Laplacian & normalize it
if α > 0
D = transpose(sum(L, dims=1))
L ./= (D * transpose(D)) .^ convert(T, α)
end
D = Diagonal(vec(sum(L, dims=1)))
M = inv(D)*L
M = inv(D) * L # normalize rows to interpret as transition probabilities

# D = Diagonal(vec(sum(L, dims=1)))
# D⁻ᵅ = inv(D^α)
Expand All @@ -82,11 +109,12 @@ function fit(::Type{DiffMap}, X::AbstractMatrix{T}; maxoutdim::Int=2, t::Int=1,

# Eigendecomposition & reduction
F = eigen(M, permute=false, scale=false)
λ = real.(F.values)
# for symmetric matrix, eigenvalues should be real but owing to numerical imprecision, could have nonzero-imaginary parts.
λ = real.(F.values)
idx = sortperm(λ, rev=true)[2:maxoutdim+1]
λ = λ[idx]
V = real.(F.vectors[:,idx])
Y = (λ.^t) .* V'
V = real.(F.vectors[:, idx])
Y = (λ .^ t) .* V'

return DiffMap{T}(t, α, ɛ, λ, L, Y)
end
Expand Down
2 changes: 1 addition & 1 deletion src/nearestneighbors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function knn(NN::BruteForce{T}, X::AbstractVecOrMat{T}; self=false) where T<:Rea
k = NN.k
@assert n > k "Number of observations must be more then $(k)"

D = pairwise((x,y)->norm(x-y), NN.fitted, X)
D = pairwise((x,y)->norm(x-y), eachcol(NN.fitted), eachcol(X))

d = Array{T}(undef, k, n)
e = Array{Int}(undef, k, n)
Expand Down
19 changes: 17 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ end
X, L = swiss_roll()

# test algorithms
@testset for algorithm in [Isomap, LEM, LLE, HLLE, LTSA, DiffMap]
#@testset for algorithm in [Isomap, LEM, LLE, HLLE, LTSA, DiffMap]
@testset for algorithm in [DiffMap]
for (k, T) in zip([5, 12], [Float32, Float64])
# construct KW parameters
kwargs = [:maxoutdim=>d]
Expand All @@ -61,9 +62,23 @@ end
@test neighbors(Y) == k
@test length(vertices(Y)) > 1
end

# test if we provide pre-computed Gram matrix
if algorithm == DiffMap
kernel = (x, y) -> exp(-sum((x .- y) .^ 2)) # default kernel
n_obs = size(X)[2]
custom_K = zeros(T, n_obs, n_obs)
for i = 1:n_obs
for j = i:n_obs
custom_K[i, j] = kernel(convert(Array{T}, X[:, i]), convert(Array{T}, X[:, j]))
custom_K[j, i] = custom_K[i, j]
end
end
Y_custom_K = fit(algorithm, custom_K; kwargs..., kernel=nothing)
@test Y_custom_K.proj ≈ Y.proj
end
end
end

end

@testset "OOS" begin
Expand Down