Interpolations and Barycenters
Some distances distance define the existence of a shortest path, a geodesic. An interpolation is essentially a datapoint on that shortest path. We provide some functionality to interpolate between different spectra and models under transport-based metrics.
Below is an example usage of interpolations. We initially create two random systems, we then define the distance under which to interpolate and then calculate the frequency response for some different values of the interpolation parameter $t \in (0,1)$
using SpectralDistances, ControlSystemsBase, Distances, Plots, Random
plotly()
Random.seed!(0)
n = 4
r1 = complex.(-0.01 .+ 0.001randn(3), 2randn(3))
r1 = ContinuousRoots([r1; conj.(r1)])
r2 = complex.(-0.01 .+ 0.001randn(3), 2randn(3))
r2 = ContinuousRoots([r2; conj.(r2)])
r1,r2 = normalize_energy.((r1, r2))
A1 = AR(r1)
A2 = AR(r2)
##
fig1 = plot()
t = 0.1
dist = RationalOptimalTransportDistance(domain=Continuous(), p=2, interval=(0., exp10(1.01)))
interp = SpectralDistances.interpolator(dist, A1, A2)
w = exp10.(LinRange(-1.5, 1, 300))
for t = LinRange(0, 1, 7)
Φ = clamp.(interp(w,t), 1e-10, 100)
plot!(w, sqrt.(Φ), xscale=:log10, yscale=:log10, line_z = t, lab="", xlabel="", title="W_2", ylims=(1e-3, 1e1), colorbar=false, l=(1,), c=:viridis)
end
rdist = EuclideanRootDistance(domain = Continuous(), p = 2)
interp = SpectralDistances.interpolator(rdist, A1, A2, normalize=false)
fig2 = plot()
for t = LinRange(0, 1, 7)
Φ = interp(w,t)
plot!(w, sqrt.(Φ), xscale=:log10, yscale=:log10, line_z = t, lab="", xlabel="", title="RD", ylims=(1e-3, 1e1), colorbar=false, l=(1,), c=:viridis)
end
fig3 = plot()
Φ1 = bode(tf(A1), w)[1][:]
Φ2 = bode(tf(A2), w)[1][:]
for t = LinRange(0, 1, 7)
plot!(w, (1-t).*Φ1 .+ t.*Φ2, xscale=:log10, yscale=:log10, line_z = t, lab="", xlabel="Frequency", title="L_2", ylims=(1e-3, 1e1), colorbar=false, l=(1,), c=:viridis)
end
fig = plot(fig1, fig2, fig3, layout=(3,1))
Barycenters
A barycenter is a generalization the the arithmetic mean to metrics other than the Euclidean. A barycenter between models is calculated like this
bc = barycenter(distance, models)
It can be useful to provide some options to the solvers:
options = (solver=sinkhorn_log!, tol=1e-8, iters=1_000_000, γ=0.0, uniform=true, inneriters=500_000, innertol=1e-6)
distance = OptimalTransportRootDistance(domain=Continuous(), p=2, β=0.01, weight=simplex_residueweight)
bc = barycenter(distance, models; options...)
We can plot the barycenters:
using SpectralDistances, ControlSystemsBase, Plots
models = examplemodels(3)
distance = OptimalTransportRootDistance(domain=Continuous())
bc = barycenter(distance, models)
w = exp10.(LinRange(-0.5, 0.5, 350)) # Frequency vector
G = tf.(models) # Convert models to transfer functions from ControlSystemsBase.jl
plot()
bodeplot!.(G, Ref(w), plotphase=false, lab="Input models", linestyle=:auto)
bodeplot!(tf(bc), w, plotphase=false, lab="Barycenter", xscale=:identity, c=:green)
Barycenters between spectrograms
We can also calculate a barycenter between spectrograms (or arbitrary matrices) using an efficient convolutional method. The most important parameter to tune in order to get a good result, apart from the regularization parameter β
, is the dynamic_floor
. This parameter determines where (in log space) the floor of the PSD is. This serves as a denoising, why the barycenter appears with a very dark background in the image below.
using SpectralDistances, DSP, Plots
N = 24_000
t = 1:N
f = range(0.8, stop=1.2, length=N)
y1 = sin.(t .* f) .+ 0.1 .* randn.()
y2 = sin.(t .* reverse(f .+ 0.5)) .+ 0.1 .* randn.()
S1,S2 = spectrogram.((y1,y2), 1024)
A = [S1,S2]
β = 0.0001 # Regularization parameter (higher implies more smoothing and a faster, more stable solution)
λ = [0.5, 0.5] # Barycentric coordinates (must sum to 1)
B = barycenter_convolutional(A, β=β, tol=1e-6, iters=2000, ϵ=1e-100, dynamic_floor=-2)
plot(
plot(S1, title="S1"),
plot(B, title="Barycenter"),
plot(S2, title="S2"),
layout=(1,3),
colorbar=false
)
Note that in order to calculate the barycenter, the sum of each input spectrogram is normalized.
This function works for any vector of matrices as long as all entries are positive and each matrix has an equal sum.
For a more thourogh example, see whistle.jl.
Trade off between frequency and time
There is currently no way of having different costs between transport in time and transport along the frequency axis other than to change the resolution of the spectrogram.
Barycentric coordiantes
The inverse problem to that of finding a barycenter is that of finding the barycentric coordinates λ of a query point $Q$, such that the resulting barycenter is as close as possible to the query point. Given a set of rational spectra $\left\{ G_i \right\}$, a nonlinear projection of a spectrum $Q$ onto this set can be obtained by solving the following nested optimization problem
\[\begin{aligned} λ &= \argmin_{\bar{λ}} \, W\big(Q, Q^*(\bar{λ})\big)\\ Q^*(\bar{λ}) &= \argmin_{\bar{Q}} \sum_i \bar{λ}_i W(G_i, \bar{Q}) \end{aligned}\]
where $λ$ are the barycentric coordinates belonging to the probability simplex. Problems of this type are sometimes referred to as histogram regression.
A nonlinear projection onto a basis consisting of spectra can be useful for, e.g., spectral dictionary learning, basis pursuit, topic modelling, denoising and detection. The function barycentric_coordinates
is available for select distances:
SpectralDistances.barycentric_coordinates
— Functionλ = barycentric_coordinates(pl, ql, p, q; options, kwargs...)
Compute the barycentric coordinates λ
such that sum(λᵢ W(pᵢ,q) for i in eachindex(p)) is minimized.
This function works best with the sinkhorn_log!
solver, a large β (around 1) and small tolerance. These are set using kwargs...
.
Arguments:
pl
: Atoms in measuresp
, vector, lengthn_measures
, of matrices of sizen_dims × n_atoms
ql
: Atoms in measureq
p
: Measuresp
, a matrix of weight vectors, sizen_atoms × n_measures
that sums to 1q
: the veight vector for measureq
, length isn_atoms
options
: For the Optim solver. Defaults areoptions = Optim.Options(store_trace=false, show_trace=false, show_every=0, iterations=20, allow_f_increases=true, time_limit=100, x_tol=1e-5, f_tol=1e-6, g_tol=1e-6, f_calls_limit=0, g_calls_limit=0)
solver
: =sinkhorn_log!
solvertol
: = 1e-7 toleranceβ
: = 0.1 entropy regularization. This function works best with rather large regularization, hence the large default value.kwargs
: these are sent to the solver algorithm.
Example:
using SpectralDistances, ControlSystemsBase, Optim
models = examplemodels(10)
d = OptimalTransportRootDistance(
domain = SpectralDistances.Continuous(),
p = 2,
weight = residueweight,
β = 0.01,
)
Xe = barycenter(d, models, solver=sinkhorn_log!)
G = tf.(models)
plot()
pzmap!.(G)
pzmap!(tf(Xe), m=:c, title="Barycenter OptimalTransportRootDistance", lab="BC")
options = Optim.Options(store_trace = true,
show_trace = false,
show_every = 1,
iterations = 50,
allow_f_increases = true,
time_limit = 100,
x_tol = 1e-7,
f_tol = 1e-7,
g_tol = 1e-7,
f_calls_limit = 0,
g_calls_limit = 0)
method = LBFGS()
λ = barycentric_coordinates(d, models, Xe, method,
options = options,
solver = sinkhorn_log!,
robust = true,
uniform = true,
tol = 1e-6,
)
bar(λ, title="Barycentric coorinates")
G = tf.(models)
plot()
pzmap!.(G, lab="")
pzmap!(tf(Xe), m = :c, title = "Barycenter OptimalTransportRootDistance", lab = "BC")
# It's okay if the last system dot does not match the barycenter exactly, there are limited models to choose from.
pzmap!(G[argmax(λ)], m = :c, lab = "Largest bc coord", legend = true)
barycentric_coordinates(d::ConvOptimalTransportDistance, X::Vector{<:AbstractMatrix}, q::AbstractMatrix; method = LBFGS(), kwargs...)
Calculate the barycentric coordinates of a vector of matrices X
using the convolutional method.
Arguments:
q
: Query matrixmethod
: The optimizer from Optimkwargs
: Are sent tosinkhorn_convolutional
Optim options
The default options are
options = Optim.Options(
store_trace = true,
show_trace = false,
show_every = 1,
iterations = 10,
allow_f_increases = false,
time_limit = 150,
x_tol = 1e-3,
f_tol = 1e-3,
g_tol = 1e-4,
)
Below is an example that assumes that you have access to a vector with a signal called y
and the corresponding sample rate fs
(in this example, we use the y
constructed in the previous example. In the example, we simply use one of the input spectra as query point. This way, we know what barycentric coordinates we expect
using SpectralDistances, DSP, Plots
models = map(DSP.arraysplit(y1, 8*512, 0)) do y
spectrogram(y, 512, fs = 24_000, window=hanning)
end
matrices = s1.(normalize_spectrogram.(models))
β = 0.01
dist = ConvOptimalTransportDistance(β=β)
Q = matrices[2] # Use the second spectrogram as query
λ, res = barycentric_coordinates(dist, matrices, Q)
plot(
bar(λ, title="Barycentric coordinates", lab=""),
plot(spectrogram(y1, window=hanning), title="Signal"),
heatmap(Q, title="Query point")
)
K-Barycenters
Below, we show an example of how one can run the K-barycenter algorithm on a collection of sound signals. sounds
is expected to be of type Vector{Vector{T}}
. The example further assumes that there is a vector of labels::Vector{Int}
that contain the true classes of the datapoints, which you do not have in an unsupervised setting.
using SpectralDistances, ControlSystemsBase
fitmethod = TLS(na=12)
models = SpectralDistances.fitmodel.(fitmethod, sounds)
G = tf.(models) # Convert to transfer functions for visualization etc.
##
using Clustering
dist = OptimalTransportRootDistance(domain=Continuous(), β=0.01, weight=simplex_residueweight)
@time clusterresult = SpectralDistances.kbarycenters(
dist,
models,
n_classes, # number of clusters
seed = :rand,
solver = sinkhorn_log!,
tol = 2e-6,
innertol = 2e-6,
iters = 100000,
inneriters = 100000,
verbose = true,
output = :best,
uniform = true,
kiters = 10
)
bc,ass = clusterresult.barycenters, clusterresult.assignments
# Visualize results
using MLBase, Plots.PlotMeasures, AudioClustering
newass,perm = AudioClustering.associate_clusters(labels,ass)
classinds = 1:n_classes
yt = (classinds, [label_strings[findfirst(labels .== i)] for i in classinds])
@show mean(labels .== newass)
cm = confusmat(n_classes,labels,newass)
heatmap(cm./sum(cm,dims=2), xlabel="Cluster assignment",ylabel="Best matching class", color=:viridis)
anns = [(reverse(ci.I)..., text(val,12,:gray)) for (ci,val) in zip(CartesianIndices(cm)[:], vec(cm))]
annotate!(anns)
yticks!(yt)
xticks!(yt, xrotation=45)
current()
The figure should look like the last figure in the paper.
SpectralDistances.BCCWorkspace
SpectralDistances.ISA
SpectralDistances.barycenter
SpectralDistances.barycenter
SpectralDistances.barycenter
SpectralDistances.barycenter_convolutional
SpectralDistances.barycenter_convolutional
SpectralDistances.barycentric_coordinates
SpectralDistances.barycentric_coordinates
SpectralDistances.barycentric_coordinates
SpectralDistances.embedding
SpectralDistances.interpolator
SpectralDistances.kbarycenters
SpectralDistances.kbarycenters
SpectralDistances.interpolator
— Methodinterpolator(d, A1, A2)
Perform displacement interpolation between two models.
SpectralDistances.BCCWorkspace
— MethodBCCWorkspace(X::Vector{<:AbstractMatrix{T}}, L, β) where T
Create a workspace cache for barycentric_coordinates
with the convolutional distance.
Arguments:
X
: Input matricesL
: Number of iterationsβ
: Regularization factor
SpectralDistances.ISA
— FunctionISA(X, w = nothing; iters = 100, printerval = typemax(Int))
Iterative swapping algorithm from "On the Computation of Wasserstein barycenters", Giovanni Puccetti et al.
Arguments:
X
: vector of d×k matrices where d is dimension and k number of atomsw
: weights. See the filestest_barycenter.jl
for different uses.iters
: maximum number of iterationsprinterval
: print this often
SpectralDistances.barycenter
— Functionbarycenter(d::OptimalTransportRootDistance, models; normalize = true, kwargs...)
barycenter(d, models)
barycenter(d, models, λ; normalize, uniform, solver, kwargs...)
Approximately calculate the barycenter supported on the same number of atoms as the number of poles in the models.
The solver can be selected by providing a keword argument, example: solver=IPOT
.
Uses the algorithms from "Fast Computation of Wasserstein Barycenters"
Example:
models = examplemodels(10)
d = OptimalTransportRootDistance(domain=SpectralDistances.Continuous(),p=2, weight=residueweight, β=0.01)
Xe = barycenter(d, models, solver=sinkhorn_log!, uniform=true)
plot()
pzmap!.(models)
pzmap!(tf(Xe), m=:c, title="Barycenter OptimalTransportRootDistance", lab="BC")
Arguments:
models
: vector of AR modelsnormalize
: make sure weights sum to 1kwargs
: are sent to the solver
SpectralDistances.barycenter
— Methodbarycenter(d::EuclideanRootDistance, models::AbstractVector, [λ])
Example:
models = examplemodels(10)
Xe = barycenter(EuclideanRootDistance(domain=SpectralDistances.Continuous(),p=2), models)
G = tf.(models)
plot()
pzmap!.(G)
pzmap!(tf(Xe), m=:c, title="Barycenter EuclideanRootDistance")
current()
SpectralDistances.barycenter
— Methodbarycenter(X::Vector{<:AbstractArray}, λ)
Calculate the weighted barycenter for point clouds in X
. Each X[i]
has the shame n_dims × n_atoms
λ
is the weight vector that should sum to 1.
SpectralDistances.barycenter_convolutional
— Functionbarycenter_convolutional(models::Vector{<:DSP.Periodograms.TFR}, λ = Fill(1 / length(models), length(models)); dynamic_floor = default_dynamic_floor(models), kwargs...)
Covenience function for the calculation of spectrograms. This function transforms the spectrograms to log-power and adjusts the floor to dynamic_floor
, followed by a normalization to sum to 1.
This function will be called if barycenter
is called with ConvOptimalTransportDistance
as first argument.
Arguments:
dynamic_floor
: Sets the floor of the spectrogram in log-domain, i.e., all values below this will be truncated. The default value is based on a quantile of the spectrogram powers. If your spectrograms are mostly low entropy, you can try to increase this number to get sharper results.kwargs
: Same as for the base method
Example:
using SpectralDistances, DSP, Plots
N = 24_000
t = 1:N
f = range(0.8, stop=1.2, length=N)
y1 = sin.(t .* f) .+ 0.1 .* randn.()
y2 = sin.(t .* reverse(f .+ 0.5)) .+ 0.1 .* randn.()
S1,S2 = spectrogram.((y1,y2), 1024)
A = [S1,S2]
β = 0.0001 # Regularization parameter (higher implies more smoothing and a faster, more stable solution)
λ = [0.5, 0.5] # Barycentric coordinates (must sum to 1)
B = barycenter_convolutional(A, β=β, tol=1e-6, iters=200, ϵ=1e-100, dynamic_floor=-2)
plot(plot(S1, title="S1"), plot(B, title="Barycenter"), plot(S2, title="S2"), layout=(1,3), colorbar=false)
SpectralDistances.barycenter_convolutional
— Methodbarycenter_convolutional(A, [λ]; β = 0.01, iters = 1000, tol = 1e-9, ϵ = 1e-90, verbose = false)
Convolutional barycenters.
´βis the regularization and
λ(optional) are the weights (barycentric coordinates). To reuse allocated space between successive calls, use the "workspace" method from the example below.
ϵ` is a truncation parameter for numerical stability.
a1 = zeros(10, 10)
a1[2,2] = 1
a2 = zeros(10, 10)
a2[6,6] = 1
A = [a1,a2]
β = 0.01
λ = [0.5, 0.5] # Barycentric coordinates, must sum to 1
w = BCWorkspace(A, β)
b = barycenter_convolutional(w,A,λ)
plot(heatmap(a1), heatmap(a2), heatmap(b))
Ref: J. Solomon, F. de Goes, G. Peyré, M. Cuturi, A. Butscher, A. Nguyen, T. Du, L. Guibas. Convolutional Wasserstein Distances: Efficient Optimal Transportation on Geometric Domains. 2015 https://people.csail.mit.edu/jsolomon/assets/convolutional_w2.compressed.pdf
SpectralDistances.barycentric_coordinates
— Methodbarycentric_coordinates(d::ConvOptimalTransportDistance, X::Vector{<:AbstractMatrix}, q::AbstractMatrix; method = LBFGS(), kwargs...)
Calculate the barycentric coordinates of a vector of matrices X
using the convolutional method.
Arguments:
q
: Query matrixmethod
: The optimizer from Optimkwargs
: Are sent tosinkhorn_convolutional
Optim options
The default options are
options = Optim.Options(
store_trace = true,
show_trace = false,
show_every = 1,
iterations = 10,
allow_f_increases = false,
time_limit = 150,
x_tol = 1e-3,
f_tol = 1e-3,
g_tol = 1e-4,
)
SpectralDistances.barycentric_coordinates
— Methodλ = barycentric_coordinates(pl, ql, p, q; options, kwargs...)
Compute the barycentric coordinates λ
such that sum(λᵢ W(pᵢ,q) for i in eachindex(p)) is minimized.
This function works best with the sinkhorn_log!
solver, a large β (around 1) and small tolerance. These are set using kwargs...
.
Arguments:
pl
: Atoms in measuresp
, vector, lengthn_measures
, of matrices of sizen_dims × n_atoms
ql
: Atoms in measureq
p
: Measuresp
, a matrix of weight vectors, sizen_atoms × n_measures
that sums to 1q
: the veight vector for measureq
, length isn_atoms
options
: For the Optim solver. Defaults areoptions = Optim.Options(store_trace=false, show_trace=false, show_every=0, iterations=20, allow_f_increases=true, time_limit=100, x_tol=1e-5, f_tol=1e-6, g_tol=1e-6, f_calls_limit=0, g_calls_limit=0)
solver
: =sinkhorn_log!
solvertol
: = 1e-7 toleranceβ
: = 0.1 entropy regularization. This function works best with rather large regularization, hence the large default value.kwargs
: these are sent to the solver algorithm.
Example:
using SpectralDistances, ControlSystemsBase, Optim
models = examplemodels(10)
d = OptimalTransportRootDistance(
domain = SpectralDistances.Continuous(),
p = 2,
weight = residueweight,
β = 0.01,
)
Xe = barycenter(d, models, solver=sinkhorn_log!)
G = tf.(models)
plot()
pzmap!.(G)
pzmap!(tf(Xe), m=:c, title="Barycenter OptimalTransportRootDistance", lab="BC")
options = Optim.Options(store_trace = true,
show_trace = false,
show_every = 1,
iterations = 50,
allow_f_increases = true,
time_limit = 100,
x_tol = 1e-7,
f_tol = 1e-7,
g_tol = 1e-7,
f_calls_limit = 0,
g_calls_limit = 0)
method = LBFGS()
λ = barycentric_coordinates(d, models, Xe, method,
options = options,
solver = sinkhorn_log!,
robust = true,
uniform = true,
tol = 1e-6,
)
bar(λ, title="Barycentric coorinates")
G = tf.(models)
plot()
pzmap!.(G, lab="")
pzmap!(tf(Xe), m = :c, title = "Barycenter OptimalTransportRootDistance", lab = "BC")
# It's okay if the last system dot does not match the barycenter exactly, there are limited models to choose from.
pzmap!(G[argmax(λ)], m = :c, lab = "Largest bc coord", legend = true)
SpectralDistances.embedding
— Methodembedding([::Type{Vector}], m, [full=true])
Returns a Vector/Matrix
containing the roots of m
. full
indicates whether or not to use all poles or only one half-plane.
SpectralDistances.kbarycenters
— Methodkbarycenters(X, p, k; seed = :rand, kiters = 10, verbose = false, output = :best, kwargs...)
Clustering using K-barycenters. If you want to cluster spectra, consider the method that accepts models instead.
Arguments:
X
: Support of input measures, Vector{Matrix} where each matrix is ndims×natomsp
: Weights of input measures Vector{Vector} where each matrix is of length n_atoms and should sum to 1.k
: number of clustersseed
: :rand or :eqkiters
: number of iterationsverbose
: print stuff?output
: output lowest cost clustering or :latest?kwargs
: are sent to the inner solvers.
SpectralDistances.kbarycenters
— Methodkbarycenters(d::OptimalTransportRootDistance, models::Vector{<:AbstractModel}, k; normalize = true, kwargs...)
This function is only available if using Clustering
.
Example:
clusterresult = kbarycenters(
dist,
models,
k, # number of clusters
seed = :rand,
solver = sinkhorn_log!,
tol = 2e-6,
innertol = 2e-6,
iters = 100000,
inneriters = 100000,
verbose = true,
output = :best,
uniform = true,
kiters = 10
)
The docs contain a more detailed example
Arguments:
models
: A vector of modelsk
: number of clustersnormalize
: Whether or not to normalize the weight vectors (recommended)kwargs
: are sent to inner solvers, (solver,tol,iters
)