A Functional Implementation of Slice Sampling in Julia

5 minute read

I’ve been using Clojure and Scala most recently for algorithmic work but I needed to return to Julia momentarily in order to finish a project.

Julia is not the most natural language in which to perform functional programming; it does not support tail call elimination, no native support for immutable data structures, currying interferes with its multiple dispatch model and it seems most of the design choices favoured imperativeness. Most functional functionality is provided by the third-party Lazy, FunctionalCollections and Match packages which provide lazy lists, functional data structures and pattern matching respectively.

“Stepping Out” Slice Sampling

No variables were mutated in the making of this code

function SliceSampling(g,w,x)
    function lowerbound(L)
        g(L)<y ? L : lowerbound(L-w)
    end
    function upperbound(R)
        g(R)<y ? R : upperbound(R+w)
    end
    function shrinksample(L,R)
        z=rand(Uniform(L,R))
        if g(z)>y
            z
        elseif z>x
            shrinksample(L,z)
        else
            shrinksample(z,R)
        end
    end
    y=-1*rand(Exponential(1))+g(x)
    U=rand(Uniform(0,1))
    shrinksample(lowerbound(x-U*w),upperbound(x+(1-U)*w))
end

g is the logdensity from which one is trying to sample, w is an initial width and x is the current parameter value.

Preamble

Loading required libraries and defining two function. Partial is my own implementation of partial application, a function present in Clojure but lacking in Julia. The toArray function just converts a lazy list into an array so that it can be plotted with Gadfly (this is done imperatively because the corresponding functional implementation results in a stack overflow due to Julia’s lack of support for tail call elimination).

using Distributions
using Lazy
using DataFrames
using Gadfly

function partial(f,a...)
    (b...) -> f(a...,b...)
end

function toArray(l)
  a=Array{Float64}(length(l))
  for i=1:length(l)
    a[i]=first(l)
    l=Lazy.tail(l)
  end
  a
end

Lazy Evaluation

Lets consider a test pdf from which we can sample directly to form a unit test for the above code. Here is a mixture density of three Normals…

mixture_distribution=MixtureModel([Normal(-1,0.2),Normal(0,0.2),Normal(1,0.2)],[0.2,0.6,0.2])
f = x->pdf(mixture_distribution,x)
g = x->logpdf(mixture_distribution,x)
plot(f, -4, 4)
x -14 -12 -10 -8 -6 -4 -2 0 2 4 6 8 10 12 14 -12.0 -11.5 -11.0 -10.5 -10.0 -9.5 -9.0 -8.5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 -20 -10 0 10 20 -12.0 -11.5 -11.0 -10.5 -10.0 -9.5 -9.0 -8.5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 -1.50 -1.45 -1.40 -1.35 -1.30 -1.25 -1.20 -1.15 -1.10 -1.05 -1.00 -0.95 -0.90 -0.85 -0.80 -0.75 -0.70 -0.65 -0.60 -0.55 -0.50 -0.45 -0.40 -0.35 -0.30 -0.25 -0.20 -0.15 -0.10 -0.05 0.00 0.05 0.10 0.15 0.20 0.25 0.30 0.35 0.40 0.45 0.50 0.55 0.60 0.65 0.70 0.75 0.80 0.85 0.90 0.95 1.00 1.05 1.10 1.15 1.20 1.25 1.30 1.35 1.40 1.45 1.50 1.55 1.60 1.65 1.70 1.75 1.80 1.85 1.90 1.95 2.00 2.05 2.10 2.15 2.20 2.25 2.30 2.35 2.40 2.45 2.50 2.55 2.60 2.65 2.70 2.75 2.80 2.85 2.90 2.95 3.00 -2 0 2 4 -1.5 -1.4 -1.3 -1.2 -1.1 -1.0 -0.9 -0.8 -0.7 -0.6 -0.5 -0.4 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 f(x)

Given a current parameter value, 0.0, one can generate a new value given the density f and an initial width 1.0 by

SliceSampling(g,1.0,0.0)
0.2485248928906587

It would be nice to create a new function, specific to f, to generate random variates which masks the distracting implementation details such as the initial width. This is the reason for the partial application function I created earlier. partial(SliceSampling,f,1.0) returns a new function which maps the current parameter value to a new value.

sample_f = partial(SliceSampling,g,1.0)
sample_f(0.5)
0.6647567078804801

This function can be used to define an infinite list of values that is lazily evaluated. The iterate function for a given function g generates an infinite lazy list as (x, g(x), g(g(x)),…). One can then take values from the beginning of the list using the take function i.e.

stream_f = iterate(sample_f,0.0);
take(stream_f,10)
(0.0 -0.21844923549096884 1.0302399899983261 1.0129526649079534 -0.5690927045181375 -1.1228947590022138 -1.1224701288130174 -0.11733086188135866 0.012546600939988967 1.1702840100996514)

take(stream_f,number_to_take) is not the most visually pleasing implementation of generating random variates from f, so once again we can use partial application to get a new function which is a lot closer to functions like rnorm in R.

rand_f = partial(take,stream_f)
rand_f(10)
(0.0 -0.21844923549096884 -0.6311260691954774 1.1600742443380705 -0.1729859710235011 -0.1801167762470986 0.15963462381533539 -0.31375918237598716 0.22438826503951037 0.02349035294676849)

Visualization

Finally, here are the density estimates from 10000 draws using slice sampling and direct methods.

plot(layer(x=(@> 10000 rand_f toArray), Geom.density,Theme(default_color=color("yellow")) ),
layer(x=rand(mixture_distribution,10000), Geom.density,Theme(default_color=color("magenta")) ),
Guide.manual_color_key("Density", [ "Slice Sampling", "Direct"], ["yellow",  "magenta"])
)
x -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 -6.0 -5.8 -5.6 -5.4 -5.2 -5.0 -4.8 -4.6 -4.4 -4.2 -4.0 -3.8 -3.6 -3.4 -3.2 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 -6 -3 0 3 6 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 Slice Sampling Direct Density -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 -1.50 -1.45 -1.40 -1.35 -1.30 -1.25 -1.20 -1.15 -1.10 -1.05 -1.00 -0.95 -0.90 -0.85 -0.80 -0.75 -0.70 -0.65 -0.60 -0.55 -0.50 -0.45 -0.40 -0.35 -0.30 -0.25 -0.20 -0.15 -0.10 -0.05 0.00 0.05 0.10 0.15 0.20 0.25 0.30 0.35 0.40 0.45 0.50 0.55 0.60 0.65 0.70 0.75 0.80 0.85 0.90 0.95 1.00 1.05 1.10 1.15 1.20 1.25 1.30 1.35 1.40 1.45 1.50 1.55 1.60 1.65 1.70 1.75 1.80 1.85 1.90 1.95 2.00 2.05 2.10 2.15 2.20 2.25 2.30 2.35 2.40 2.45 2.50 2.55 2.60 2.65 2.70 2.75 2.80 2.85 2.90 2.95 3.00 -2 0 2 4 -1.5 -1.4 -1.3 -1.2 -1.1 -1.0 -0.9 -0.8 -0.7 -0.6 -0.5 -0.4 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0

Leave a Comment