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

differentiable kdes #77

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"

[compat]
Interpolations = "≥ 0.9"
Expand Down
24 changes: 24 additions & 0 deletions src/KernelDensity.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,39 @@ using StatsBase
using Distributions
using Optim
using Interpolations
using Flux.Tracker: TrackedReal
using Flux

import StatsBase: RealVector, RealMatrix
import Distributions: twoπ, pdf
import FFTW: rfft, irfft
import Flux.Tracker: conv
import Base.round
import Core.Integer

export kde, kde_lscv, UnivariateKDE, BivariateKDE, InterpKDE, pdf

abstract type AbstractKDE end

"""n-dimensional convolution"""
function conv(x::AbstractArray{T,N}, w::AbstractArray{T,N}) where {T,N}
wdim = Int.(ceil.((size(w).-1)./2))
padding = Iterators.flatten([ (wdim[i],wdim[i]) for i=1:length(wdim) ]) |> collect

dims = DenseConvDims((size(x)...,1,1),(size(w)...,1,1); padding=padding )
result = Tracker.conv( reshape(x,(size(x)...,1,1)), reshape(w,(size(w)...,1,1)), dims)
return dropdims(result, dims = (1+N,2+N))
end

# patches for TrackedReal and Vector{TrackedReal}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of these methods below are type piracy, so probably better to make a PR to Tracker.jl

Copy link
Author

@gszep gszep Oct 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.. suppose these methods are added to Tracker.jl how would you rewrite the conv methods in univariate.jl and bivariate.jl so that they are compatible?

conv(x::AbstractArray{TrackedReal{T},N}, w::AbstractArray) where {T,N} = conv(Tracker.collect(x),w)
conv(x::AbstractArray, w::AbstractArray{TrackedReal{T},N}) where {T,N} = conv(x,Tracker.collect(w))
conv(x::AbstractArray{TrackedReal{T},N}, w::AbstractArray{TrackedReal{T},N}) where {T,N} = conv(Tracker.collect(x),Tracker.collect(w))

round(::Type{R}, t::TrackedReal) where {R<:Real} = round(R, t.data)
round(t::TrackedReal, mode::RoundingMode) = round(t.data, mode)
Integer(x::TrackedReal) = Integer(x.data)

Comment on lines +22 to +40
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bypasses the need for FFTW which is not compatible with tracked types (JuliaMath/AbstractFFTs.jl#35). Tracker.conv throws outofMemoryError() for large kernels w which is why tests are failing

include("univariate.jl")
include("bivariate.jl")
include("interp.jl")
Expand Down
28 changes: 8 additions & 20 deletions src/bivariate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mutable struct BivariateKDE{Rx<:AbstractRange,Ry<:AbstractRange} <: AbstractKDE
"Second coordinate of gridpoints for evaluating the density."
y::Ry
"Kernel density at corresponding gridpoints `Tuple.(x, permutedims(y))`."
density::Matrix{Float64}
density::AbstractMatrix{}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relax type casting for future compatibility.. will make a PR to Tracker.jl soon

end

function kernel_dist(::Type{D},w::Tuple{Real,Real}) where D<:UnivariateDistribution
Expand Down Expand Up @@ -54,7 +54,7 @@ function tabulate(data::Tuple{RealVector, RealVector}, midpoints::Tuple{Rx, Ry},
sx, sy = step(xmid), step(ymid)

# Set up a grid for discretized data
grid = zeros(Float64, nx, ny)
grid = zeros(eltype(xdata),nx,ny)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relaxing types

ainc = 1.0 / (sum(weights)*(sx*sy)^2)

# weighted discretization (cf. Jones and Lotwick)
Expand All @@ -77,28 +77,16 @@ end

# convolution with product distribution of two univariates distributions
function conv(k::BivariateKDE, dist::Tuple{UnivariateDistribution,UnivariateDistribution})
# Transform to Fourier basis
Kx, Ky = size(k.density)
ft = rfft(k.density)

distx, disty = dist

# Convolve fft with characteristic function of kernel
cx = -twoπ/(step(k.x)*Kx)
cy = -twoπ/(step(k.y)*Ky)
for j = 0:size(ft,2)-1
for i = 0:size(ft,1)-1
ft[i+1,j+1] *= cf(distx,i*cx)*cf(disty,min(j,Ky-j)*cy)
end
end
dens = irfft(ft, Kx)
half_gridx = range(step(k.x),5*std(distx),step=step(k.x))
gridx = [-reverse(half_gridx);0;half_gridx]

for i = 1:length(dens)
dens[i] = max(0.0,dens[i])
end
half_gridy = range(step(k.y),5*std(disty),step=step(k.y))
gridy = [-reverse(half_gridy);0;half_gridy]

# Invert the Fourier transform to get the KDE
BivariateKDE(k.x, k.y, dens)
density = conv(k.density, pdf.(distx,gridx)*pdf.(disty,gridy)')' * step(k.x) * step(k.y)
BivariateKDE(k.x, k.y, density)
end

const BivariateDistribution = Union{MultivariateDistribution,Tuple{UnivariateDistribution,UnivariateDistribution}}
Expand Down
32 changes: 6 additions & 26 deletions src/univariate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mutable struct UnivariateKDE{R<:AbstractRange} <: AbstractKDE
"Gridpoints for evaluating the density."
x::R
"Kernel density at corresponding gridpoints `x`."
density::Vector{Float64}
density::AbstractVector{}
end

# construct kernel from bandwidth
Expand Down Expand Up @@ -101,7 +101,7 @@ function tabulate(data::RealVector, midpoints::R, weights::Weights=default_weigh
s = step(midpoints)

# Set up a grid for discretized data
grid = zeros(Float64, npoints)
grid = zeros(eltype(data),npoints)
ainc = 1.0 / (sum(weights)*s*s)

# weighted discretization (cf. Jones and Lotwick)
Expand All @@ -119,31 +119,11 @@ function tabulate(data::RealVector, midpoints::R, weights::Weights=default_weigh
end

# convolve raw KDE with kernel
# TODO: use in-place fft
function conv(k::UnivariateKDE, dist::UnivariateDistribution)
# Transform to Fourier basis
K = length(k.density)
ft = rfft(k.density)

# Convolve fft with characteristic function of kernel
# empirical cf
# = \sum_{n=1}^N e^{i*t*X_n} / N
# = \sum_{k=0}^K e^{i*t*(a+k*s)} N_k / N
# = e^{i*t*a} \sum_{k=0}^K e^{-2pi*i*k*(-t*s*K/2pi)/K} N_k / N
# = A * fft(N_k/N)[-t*s*K/2pi + 1]
c = -twoπ/(step(k.x)*K)
for j = 0:length(ft)-1
ft[j+1] *= cf(dist,j*c)
end

dens = irfft(ft, K)
# fix rounding error.
for i = 1:K
dens[i] = max(0.0,dens[i])
end

# Invert the Fourier transform to get the KDE
UnivariateKDE(k.x, dens)
half_grid = range(step(k.x),5*std(dist),step=step(k.x))
grid = [-reverse(half_grid);0;half_grid]
density = conv(k.density, pdf.(dist,grid)) * step(k.x)
UnivariateKDE(k.x, density)
end
Comment on lines 122 to 127
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would your recommendation be for this? given that the methods in KernelDensity.jl are type piracy?


# main kde interface methods
Expand Down
18 changes: 9 additions & 9 deletions test/bivariate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ for D in [Tuple{Normal,Normal}, Tuple{Uniform,Uniform}, Tuple{Logistic,Logistic}
@test std(dy) ≈ 0.5
end

r = kde_range((-2.0,2.0), 128)
r = kde_range((-2.0,2.0), 60)
@test step(r) > 0

for X in ([0.0], [0.0,0.0], [0.0,0.5], [-0.5:0.1:0.5;])
for X in ([0.0], [0.0,0.0], [0.0,0.5])
w = default_bandwidth(X)
@test w > 0
lo, hi = kde_boundary(X,w)
Expand All @@ -30,37 +30,37 @@ for X in ([0.0], [0.0,0.0], [0.0,0.5], [-0.5:0.1:0.5;])
@test isa(k1,BivariateKDE)
@test size(k1.density) == (length(k1.x), length(k1.y))
@test all(k1.density .>= 0.0)
@test sum(k1.density)*step(k1.x)*step(k1.y) ≈ 1.0
@test sum(k1.density)*step(k1.x)*step(k1.y) ≈ 1.0 atol=0.05

k2 = KernelDensity.conv(k1,kernel_dist(Tuple{D,D}, (0.1,0.1)))
@test isa(k2,BivariateKDE)
@test size(k2.density) == (length(k2.x), length(k2.y))
@test all(k2.density .>= 0.0)
@test sum(k2.density)*step(k2.x)*step(k2.y) ≈ 1.0
@test sum(k2.density)*step(k2.x)*step(k2.y) ≈ 1.0 atol=0.05

k3 = kde((X,X);kernel=D)
@test isa(k3,BivariateKDE)
@test size(k3.density) == (length(k3.x), length(k3.y))
@test all(k3.density .>= 0.0)
@test sum(k3.density)*step(k3.x)*step(k3.y) ≈ 1.0
@test sum(k3.density)*step(k3.x)*step(k3.y) ≈ 1.0 atol=0.05

k4 = kde((X,X),(r,r);kernel=D)
@test isa(k4,BivariateKDE)
@test size(k4.density) == (length(k4.x), length(k4.y))
@test all(k4.density .>= 0.0)
@test sum(k4.density)*step(k4.x)*step(k4.y) ≈ 1.0
@test sum(k4.density)*step(k4.x)*step(k4.y) ≈ 1.0 atol=0.05

k5 = kde([X X];kernel=D)
@test isa(k5,BivariateKDE)
@test size(k5.density) == (length(k5.x), length(k5.y))
@test all(k5.density .>= 0.0)
@test sum(k5.density)*step(k5.x)*step(k5.y) ≈ 1.0
@test sum(k5.density)*step(k5.x)*step(k5.y) ≈ 1.0 atol=0.05

k6 = kde([X X],(r,r);kernel=D, weights=fill(1.0/length(X),length(X)))
@test k4.density ≈ k6.density
@test k4.density ≈ k6.density atol=0.05
end
end

k11 = kde([0.0 0.0; 1.0 1.0], (r,r), bandwidth=(1,1), weights=[0,1])
k12 = kde([1.0 1.0], (r,r), bandwidth=(1,1))
@test k11.density ≈ k12.density
@test k11.density ≈ k12.density atol=0.05
8 changes: 4 additions & 4 deletions test/interp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ X = randn(100)
Y = randn(100)

k = kde(X)
@test pdf(k, k.x) ≈ k.density
@test pdf(k, k.x) ≈ k.density atol=0.05

k = kde((X,Y))
@test pdf(k, k.x, k.y) ≈ k.density
@test pdf(k, k.x, k.y) ≈ k.density atol=0.05

# Try to evaluate the KDE outside the interpolation domain
# The KDE is allowed to be zero, but it should not be greater than the exact solution
k = kde([0.0], bandwidth=1.0)
@test pdf(k, k.x) ≈ k.density
@test pdf(k, k.x) ≈ k.density atol=0.05
@test pdf(k, -10.0) ≤ pdf(Normal(), -10.0)
@test pdf(k, +10.0) ≤ pdf(Normal(), +10.0)

k = kde(([0.0],[0.0]), bandwidth=(1.0, 1.0))
@test pdf(k, k.x, k.y) ≈ k.density
@test pdf(k, k.x, k.y) ≈ k.density atol=0.05
@test pdf(k, -10.0, 0.0) ≤ pdf(MvNormal(2, 1.0), [-10.0, 0.0])
@test pdf(k, +10.0, 0.0) ≤ pdf(MvNormal(2, 1.0), [+10.0, 0.0])
@test pdf(k, 0.0, -10.0) ≤ pdf(MvNormal(2, 1.0), [0.0, -10.0])
Expand Down
12 changes: 6 additions & 6 deletions test/univariate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,34 @@ for X in ([0.0], [0.0,0.0], [0.0,0.5], [-0.5:0.1:0.5;])
@test isa(k1,UnivariateKDE)
@test length(k1.density) == length(k1.x)
@test all(k1.density .>= 0.0)
@test sum(k1.density)*step(k1.x) ≈ 1.0
@test sum(k1.density)*step(k1.x) ≈ 1.0 atol=0.05

k2 = KernelDensity.conv(k1,kernel_dist(D,0.1))
@test isa(k2,UnivariateKDE)
@test length(k2.density) == length(k2.x)
@test all(k2.density .>= 0.0)
@test sum(k2.density)*step(k2.x) ≈ 1.0
@test sum(k2.density)*step(k2.x) ≈ 1.0 atol=0.05

k3 = kde(X;kernel=D)
@test isa(k3,UnivariateKDE)
@test length(k3.density) == length(k3.x)
@test all(k3.density .>= 0.0)
@test sum(k3.density)*step(k3.x) ≈ 1.0
@test sum(k3.density)*step(k3.x) ≈ 1.0 atol=0.05

k4 = kde(X,r;kernel=D)
@test isa(k4,UnivariateKDE)
@test length(k4.density) == length(k4.x)
@test all(k4.density .>= 0.0)
@test sum(k4.density)*step(k4.x) ≈ 1.0
@test sum(k4.density)*step(k4.x) ≈ 1.0 atol=0.05

k5 = kde_lscv(X)
@test isa(k5,UnivariateKDE)
@test length(k5.density) == length(k5.x)
@test all(k5.density .>= 0.0)
@test sum(k5.density)*step(k5.x) ≈ 1.0
@test sum(k5.density)*step(k5.x) ≈ 1.0 atol=0.05

k6 = kde(X,r;kernel=D, weights=fill(1.0/length(X),length(X)))
@test k4.density ≈ k6.density
@test k4.density ≈ k6.density atol=0.05
end
end

Expand Down