项目作者: trappmartin

项目描述 :
Probabilistic Circuits in Julia
高级语言: Julia
项目地址: git://github.com/trappmartin/AdvancedProbabilisticCircuits.jl.git
创建时间: 2020-09-24T13:42:21Z
项目社区:https://github.com/trappmartin/AdvancedProbabilisticCircuits.jl

开源协议:MIT License

下载


AdvancedProbabilisticCircuits.jl

Build Status
codecov

AdvancedProbabilisticCircuits.jl is a package for probabilistic modelling and inference using probabilistic circuits.

Warning: This package is under heavy development and things might easily change!

Installation

This package is currently not registered. To install AdvancedProbabilisticCircuits.jl either clone the package manually or add it using the Julia package manage using: Pkg.add(url="https://github.com/trappmartin/AdvancedProbabilisticCircuits.jl").

Getting Started

The following example illustrates the construction of a probabilistic circuit and its use for density estimation.

  1. using AdvancedProbabilisticCircuits, MLDatasets
  2. # download Iris dataset if necessary
  3. # Iris.download()
  4. X = Iris.features()
  5. # we can create a leaf node by passing the required scope as an argument
  6. l = Normal(1) # Normal with scope = 1
  7. # we can create a product node with a specific partition using
  8. p = Prod(TruncatedNormal(1, min=eps()), TruncatedNormal(2, min=eps()))
  9. # or with the following shorthands
  10. partition = [1,2]
  11. p = Prod(Normal, partition)
  12. p = Prod((s)->TruncatedNormal(s, min=eps()), partition)
  13. # we can create a sum nodes the same way as a product node, i.e.
  14. s = Sum(TruncatedNormal(1, min=eps()), TruncatedNormal(1, min=eps()))
  15. # or using
  16. K, scope = 2, 1
  17. p = Sum(Normal, scope, K)
  18. s = Sum((k)->Prod((s)->TruncatedNormal(s, min=eps()), partition), K)
  19. # now lets construct a simple circuit
  20. pc = Sum(
  21. Prod(
  22. Sum(Prod((s)->TruncatedNormal(s, min=eps()), [1,2]),
  23. Prod((s)->TruncatedNormal(s, max=0), [1,2])),
  24. Prod(Normal, [3,4])),
  25. Prod(Normal, [1,2,3,4]))

Once the probabilistic circuit is defined, you should see the circuit in a more amendable form displayed in the REPL, e.g.:

  1. (+) (
  2. 0.761 × (×) (
  3. (+) (
  4. 0.41 × (×) (
  5. TruncatedNormal[(μ = 0.0, σ = 1.0, min = 2.220446049250313e-16, max = Inf)] - scope: 1,
  6. TruncatedNormal[(μ = 0.0, σ = 1.0, min = 2.220446049250313e-16, max = Inf)] - scope: 2 ),
  7. 0.254 × (×) (
  8. TruncatedNormal[(μ = 0.0, σ = 1.0, min = -Inf, max = 0)] - scope: 1,
  9. TruncatedNormal[(μ = 0.0, σ = 1.0, min = -Inf, max = 0)] - scope: 2 ) ),
  10. Normal[(μ = 0.0, σ = 1.0)] - scope: 3,
  11. Normal[(μ = 0.0, σ = 1.0)] - scope: 4),
  12. 0.33 × (×) (
  13. Normal[(μ = 0.0, σ = 1.0)] - scope: 1,
  14. Normal[(μ = 0.0, σ = 1.0)] - scope: 2,
  15. Normal[(μ = 0.0, σ = 1.0)] - scope: 3,
  16. Normal[(μ = 0.0, σ = 1.0)] - scope: 4))

Note that the colour coding of the nodes indicate the nodes properties.
We can evaluate the (unnormalize) log density of the dataset by calling the logpdf of the circuit and compute the normalized log likelihood using loglikelihood, i.e.

  1. using Statistics
  2. lp = logpdf(pc, X)
  3. llh(model, x) = mean(loglikelihood(model, x))
  4. @show llh(pc, X)

We can optimise the paramters using Zygote as follows:

  1. using Zygote # used for AD
  2. using ProgressMeter, Plots # visualisation of progress & results
  3. iters = 20 # number of iterations
  4. η = 0.1 # learning rate
  5. # results
  6. values = zeros(iters)
  7. @showprogress for i in 1:iters
  8. grads = Zygote.gradient(m -> llh(m, X), pc)[1]
  9. update!(pc, grads; η = η)
  10. values[i] = llh(pc, X)
  11. end
  12. plot(values)

After optimization, the resulting circuit should look similar to:

  1. (+) (
  2. 0.43 × (×) (
  3. (+) (
  4. 0.339 × (×) (
  5. TruncatedNormal[(μ = 0.2632989915560352, σ = 2.382310506673276, min = 2.220446049250313e-16, max = Inf)] - scope: 1,
  6. TruncatedNormal[(μ = 0.18073477432822802, σ = 1.4408260826173522, min = 2.220446049250313e-16, max = Inf)] - scope: 2),
  7. 0.333 × (×) (
  8. TruncatedNormal[(μ = 0.0, σ = 1.0, min = -Inf, max = 0)] - scope: 1,
  9. TruncatedNormal[(μ = 0.0, σ = 1.0, min = -Inf, max = 0)] - scope: 2 ) ),
  10. Normal[(μ = 0.1738561801760675, σ = 1.654385423083228)] - scope: 3,
  11. Normal[(μ = 0.06158775377035022, σ = 1.0158713531454664)] - scope: 4),
  12. 1.106 × (×) (
  13. Normal[(μ = 1.1542622026997085, σ = 3.747453345484773)] - scope: 1,
  14. Normal[(μ = 1.3191340405400875, σ = 2.0246776684705585)] - scope: 2,
  15. Normal[(μ = 1.146697247351322, σ = 2.802238408757326)] - scope: 3,
  16. Normal[(μ = 1.0225365827958446, σ = 0.848282907572307)] - scope: 4))

Adding additional leaf nodes

This package provides a few standard leaf nodes, i.e.

  1. Normal # univariate Gaussian
  2. TruncatedNormal # truncated univariate Gaussian
  3. Indicator # indicator function

and we can easily extend the set of leaf nodes using the @leaf macro:

  1. import AdvancedProbabilisticCircuits.support
  2. using SpecialFunctions # required for logbeta
  3. # define a Beta distribution with default values
  4. @leaf Beta = 1.0, β = 1.0) RealInterval(0.0, 1.0)
  5. # define the log density function
  6. function logpdf(n::Beta{P}, x::Real) where {P<:NamedTuple{(:α, :β)}}
  7. return (n.params - 1) * log(x) + (n.params - 1) * log1p(-x) - logbeta(n.params.α, n.params.β)
  8. end

If necessary, we can also call @leaf Beta(α = 1.0, β = 1.0) instead and define the support manually. See ? @leaf for an example. Note that this package currently only supports univariate leaves.

Now we can use a Beta distribution as a leaf node in a probabilistic circuit, e.g.

  1. pc = Sum(Beta(1), Beta(1, α = 0.5), Beta(1, α = 0.5, β = 0.5), Normal(1));

Note that we may additionally want to define the adjoint for Zygote, if necessary. We refer to the Zygote documentation on this.

Adding additional internal nodes

The package proved sum and product nodes as internal nodes, but can easily be extended.
For example, one can implement a custom internal node as follows:

  1. # sub-type NodeTypes to define a new node type
  2. struct CustomProdNode <: NodeType end
  3. # define custom node type constructor
  4. function CPNode(::Type{T}, ns::AbstractNode...) where T<:Real
  5. parameters = rand(T, length(ns)) # every internal node has parameters (optional)
  6. return AdvancedProbabilisticCircuits._build_node(CustomProdNode, parameters, ns...)
  7. end
  8. # define custom log density function
  9. logpdf(n::Node{T,V,S,P,CustomProdNode}, x) where {T,V,S,P} = sum(logpdf(children(n),Ref(x)))