diff --git a/Project.toml b/Project.toml index 1f3b20a9..74e2c7e7 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Lerche = "d42ef402-04e6-4356-9f73-091573ea58dc" LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LogicCircuits = "a7847b3b-b7f1-4dd5-83c3-60e0aa0f8599" @@ -36,6 +37,7 @@ Clustering = "0.14" DataFrames = "0.21, 0.22, 1.2" DataStructures = "0.17, 0.18" Distributions = "0.25" +Lerche = "0.5" LightGraphs = "1.3" LogicCircuits = "0.2.4" LoopVectorization = "0.8.20, 0.11, 0.12" diff --git a/src/LoadSave/LoadSave.jl b/src/LoadSave/LoadSave.jl deleted file mode 100644 index 26ae08f2..00000000 --- a/src/LoadSave/LoadSave.jl +++ /dev/null @@ -1,11 +0,0 @@ -module LoadSave - -using LogicCircuits -using ...ProbabilisticCircuits - -include("circuit_line_compiler.jl") -include("circuit_loaders.jl") -include("circuit_savers.jl") -include("plot.jl") - -end \ No newline at end of file diff --git a/src/LoadSave/circuit_line_compiler.jl b/src/LoadSave/circuit_line_compiler.jl deleted file mode 100644 index 4d7687dd..00000000 --- a/src/LoadSave/circuit_line_compiler.jl +++ /dev/null @@ -1,121 +0,0 @@ -##################### -# Compilers to ProbabilisticCircuits data structures starting from already parsed line objects -##################### - -# reuse some internal infrastructure of LogicCircuits' LoadSave module -using LogicCircuits.LoadSave: CircuitFormatLines, CircuitFormatLine, lnconstant, -VtreeFormatLines, CircuitHeaderLine, UnweightedLiteralLine, WeightedLiteralLine, -DecisionLine, LCElement, BiasLine, WeightedNamedConstantLine, PSDDElement, -CircuitCommentLine, ID, compile_smooth_struct_logical_m, compile_smooth_logical_m - -""" -Compile lines into a probabilistic circuit -""" -function compile_prob(lines::CircuitFormatLines)::ProbCircuit - # first compile a logic circuit - logic_circuit, id2lognode = compile_smooth_logical_m(lines) - decorate_prob(lines, logic_circuit, id2lognode) -end - -""" -Compile lines into a logistic circuit. -""" -function compile_logistic(lines::CircuitFormatLines, classes::Int)::LogisticCircuit - # first compile a logic circuit - logic_circuit, id2lognode = compile_smooth_logical_m(lines) - decorate_logistic(lines, logic_circuit, classes, id2lognode) -end - -""" -Compile circuit and vtree lines into a structured probabilistic circuit (one whose logic circuit origin is structured). -""" -function compile_struct_prob(circuit_lines::CircuitFormatLines, vtree_lines::VtreeFormatLines) - logic_circuit, vtree, id2lognode, id2vtree = compile_smooth_struct_logical_m(circuit_lines, vtree_lines) - prob_circuit = decorate_prob(circuit_lines, logic_circuit, id2lognode) - return prob_circuit, vtree -end - -function decorate_prob(lines::CircuitFormatLines, logic_circuit::LogicCircuit, id2lognode::Dict{ID,<:LogicCircuit})::ProbCircuit - # set up cache mapping logic circuit nodes to their probabilistic decorator - - prob_circuit = ProbCircuit(logic_circuit) - lognode2probnode = Dict{LogicCircuit, ProbCircuit}() - - prob_lin = linearize(prob_circuit) # TODO better implementation - logic_lin = linearize(logic_circuit) - - foreach(i -> lognode2probnode[logic_lin[i]] = prob_lin[i], 1 : num_nodes(logic_circuit)) - - # map from line node ids to probabilistic circuit nodes - id2probnode(id) = lognode2probnode[id2lognode[id]] - - root = nothing - - # go through lines again and update the probabilistic circuit node parameters - - function compile(ln::CircuitFormatLine) - error("Compilation of line $ln into probabilistic circuit is not supported") - end - function compile(::Union{CircuitHeaderLine,CircuitCommentLine,UnweightedLiteralLine}) - # do nothing - end - function compile(ln::WeightedNamedConstantLine) - @assert lnconstant(ln) == true - root = id2probnode(ln.node_id) - root.log_probs .= [ln.weight, log1p(-exp(ln.weight))] - end - function compile(ln::DecisionLine{<:PSDDElement}) - root = id2probnode(ln.node_id) - root.log_probs .= [x.weight for x in ln.elements] - end - - foreach(compile, lines) - - root -end - -function decorate_logistic(lines::CircuitFormatLines, logic_circuit::LogicCircuit, - classes::Int, id2lognode::Dict{ID,<:LogicCircuit})::LogisticCircuit - - # set up cache mapping logic circuit nodes to their logistic decorator - logistic_circuit = LogisticCircuit(logic_circuit, classes) - log2logistic = Dict{LogicCircuit, LogisticCircuit}() - logistic_lin = linearize(logistic_circuit) - logic_lin = linearize(logic_circuit) - - foreach(i -> log2logistic[logic_lin[i]] = logistic_lin[i], 1 : length(logic_lin)) - id2logisticnode(id) = log2logistic[id2lognode[id]] - - root = nothing - # go through lines again and update the probabilistic circuit node parameters - - function compile(ln::CircuitFormatLine) - error("Compilation of line $ln into logistic circuit is not supported") - end - - function compile(::Union{CircuitHeaderLine,CircuitCommentLine,UnweightedLiteralLine}) - # do nothing - end - - function compile(ln::WeightedLiteralLine) - root = id2logisticnode(ln.node_id)::Logistic⋁Node - root.thetas[1, :] .= ln.weights - end - - function compile(ln::DecisionLine{<:LCElement}) - root = id2logisticnode(ln.node_id)::Logistic⋁Node - for (ind, elem) in enumerate(ln.elements) - root.thetas[ind, :] .= elem.weights - end - end - - function compile(ln::BiasLine) - root = id2logisticnode(ln.node_id)::Logistic⋁Node - # @assert length(root.thetas) == 1 - root.thetas[1,:] .= ln.weights - end - - foreach(compile, lines) - - root -end \ No newline at end of file diff --git a/src/LoadSave/circuit_loaders.jl b/src/LoadSave/circuit_loaders.jl deleted file mode 100644 index f68e49f7..00000000 --- a/src/LoadSave/circuit_loaders.jl +++ /dev/null @@ -1,140 +0,0 @@ -export zoo_clt, zoo_clt_file, zoo_psdd, zoo_lc, load_prob_circuit, -load_struct_prob_circuit, load_logistic_circuit, load_as_ensemble - -using LogicCircuits -using Pkg.Artifacts -using LogicCircuits.LoadSave: parse_psdd_file, parse_circuit_file, -parse_vtree_file, parse_lc_file - -##################### -# circuit loaders from module zoo -##################### - -zoo_lc(name, num_classes) = - load_logistic_circuit(zoo_lc_file(name), num_classes) - -zoo_clt_file(name) = - artifact"circuit_model_zoo" * LogicCircuits.LoadSave.zoo_version * "/clts/$name" - - -zoo_clt(name) = - parse_clt(zoo_clt_file(name)) - -zoo_psdd(name) = - load_prob_circuit(zoo_psdd_file(name)) - -##################### -# general parser infrastructure for circuits -##################### - -# The `ParserCombinator` library works correctly but is orders of magnitude too slow. -# Instead here we hardcode some simpler parsers to speed things up - -""" -Load a probabilistic circuit from file. -Support circuit file formats: - * ".psdd" for PSDD files -""" -function load_prob_circuit(file::String)::ProbCircuit - @assert endswith(file,".psdd") - compile_prob(parse_psdd_file(file)) -end - -""" -Load a structured probabilistic circuit from file. -Support circuit file formats: - * ".psdd" for PSDD files -Supported vtree file formats: - * ".vtree" for Vtree files -""" -function load_struct_prob_circuit(circuit_file::Union{String, IO}, vtree_file::Union{String, IO})::Tuple{StructProbCircuit,PlainVtree} - circuit_file isa String && @assert endswith(circuit_file,".psdd") - circuit_lines = parse_psdd_file(circuit_file) - vtree_lines = parse_vtree_file(vtree_file) - compile_struct_prob(circuit_lines, vtree_lines) -end - -""" -Load a logistic circuit from file. -Support circuit file formats: - * ".circuit" for logistic files -Supported vtree file formats: - * ".vtree" for Vtree files -""" -function load_logistic_circuit(circuit_file::String, classes::Int)::LogisticCircuit - @assert endswith(circuit_file,".circuit") - circuit_lines = parse_circuit_file(circuit_file) - compile_logistic(circuit_lines, classes) -end - -##################### -# parse based on file extension -##################### - -using MetaGraphs: MetaDiGraph, set_prop!, props, add_edge! - -"Parse a clt from given file" -function parse_clt(filename::String)::MetaDiGraph - f = open(filename) - n = parse(Int32,readline(f)) - n_root = parse(Int32,readline(f)) - clt = MetaDiGraph(n) - for i in 1 : n_root - root, prob = split(readline(f), " ") - root, prob = parse(Int32, root), parse(Float64, prob) - set_prop!(clt, root, :parent, 0) - set_prop!(clt, root, :cpt, Dict(1=>prob,0=>1-prob)) - end - - for i = 1 : n - n_root - dst, src, prob1, prob0 = split(readline(f), " ") - dst, src, prob1, prob0 = parse(Int32, dst), parse(Int32, src), parse(Float64, prob1), parse(Float64, prob0) - add_edge!(clt, src,dst) - set_prop!(clt, dst, :parent, src) - set_prop!(clt, dst, :cpt, Dict((1,1)=>prob1, (0,1)=>1-prob1, (1,0)=>prob0, (0,0)=>1-prob0)) - end - return clt -end - -"Loads an ensemble from disk." -function load_as_ensemble(name::String; quiet::Bool = false)::Ensemble{StructProbCircuit} - @assert endswith(name, ".esbl") - zip = ZipFile.Reader(name) - W, n = Vector{Float64}(), -1 - for f ∈ zip.files - if endswith(f.name, ".meta") - n = parse(Int, readline(f)) - W = map(x -> parse(Float64, x), split(readline(f))) - end - end - @assert n > 0 && length(W) == n "Ensemble file format corrupted, empty or missing meta file." - P = Tuple{Int, Int}[(0, 0) for i ∈ 1:n] - for (i, f) ∈ enumerate(zip.files) - if endswith(f.name, ".psdd") - j = parse(Int, f.name[1:end-5]) - @assert j > 0 && j <= n "Either .meta file is corrupted or .psdd is misnamed (faulty: $(f.name))." - P[j] = (i, P[j][2]) - elseif endswith(f.name, ".vtree") - j = parse(Int, f.name[1:end-6]) - @assert j > 0 && j <= n "Either .meta file is corrupted or .vtree is misnamed (faulty: $(f.name))." - P[j] = (P[j][1], i) - end - end - C = Vector{StructProbCircuit}(undef, n) - function do_work(k::Int, i::Int, j::Int) - @assert i > 0 "Missing .psdd file for the $k-th circuit." - @assert j > 0 "Missing .psdd file for the $k-th circuit." - psdd_file, vtree_file = zip.files[i], zip.files[j] - psdd, _ = load_struct_prob_circuit(psdd_file, vtree_file) - C[k] = psdd - nothing - end - !quiet && print("Loading circuits...\n ") - for (k, (i, j)) ∈ enumerate(P) - do_work(k, i, j) - !quiet && print('*') - end - !quiet && print('\n') - close(zip) - return Ensemble{StructProbCircuit}(C, W) -end diff --git a/src/LoadSave/circuit_savers.jl b/src/LoadSave/circuit_savers.jl deleted file mode 100644 index ba36dfc3..00000000 --- a/src/LoadSave/circuit_savers.jl +++ /dev/null @@ -1,224 +0,0 @@ -export save_circuit, save_as_dot, save_as_psdd, save_as_logistic, save_as_ensemble - -using ZipFile -using LogicCircuits.LoadSave: SDDElement, - PSDDElement, - save_lines, - get_vtree2id, - get_node2id, - parse_psdd_file, - PsddHeaderLine, - LcHeaderLine, - get_nodes_level - -##################### -# decompile for probabilistic nodes -##################### - -"Decompile for psdd circuit, used during saving of circuits to file" -decompile(n::StructProbLiteralNode, node2id, vtree2id)::UnweightedLiteralLine = - UnweightedLiteralLine(node2id[n], vtree2id[n.vtree], literal(n), true) - -make_element(n::StructMulNode, w::AbstractFloat, node2id) = - PSDDElement(node2id[children(n)[1]], node2id[children(n)[2]], w) - -istrue_node(n::StructSumNode)::Bool = - is⋁gate(n) && num_children(n) == 2 && isliteralgate(children(n)[1]) && isliteralgate(children(n)[2]) && - literal(children(n)[1]) + literal(children(n)[2]) == 0 - -function decompile(n::StructSumNode, node2id, vtree2id)::Union{WeightedNamedConstantLine, DecisionLine{PSDDElement}} - if istrue_node(n) - WeightedNamedConstantLine(node2id[n], vtree2id[n.vtree], lit2var(children(n)[1].literal), n.log_probs[1]) # TODO - else - DecisionLine(node2id[n], vtree2id[n.vtree], UInt32(num_children(n)), map(x -> make_element(x[1], x[2], node2id), zip(children(n), n.log_probs))) - end -end - -##################### -# decompile for logistic nodes -##################### - -make_element(n::Logistic⋀Node, w::Vector{<:AbstractFloat}, node2id) = - LCElement(node2id[children(n)[1]], node2id[children(n)[2]], w) - -function decompile(n::Logistic⋁Node, node2id)::Union{DecisionLine{<:LCElement}, BiasLine, WeightedLiteralLine} - if num_children(n) == 1 && isliteralgate(n.children[1]) - WeightedLiteralLine(node2id[n], UInt32(0), literal(n.children[1]), true, vec(n.thetas)) # TODO vtree id - - elseif num_children(n) == 1 && is⋁gate(n.children[1]) - BiasLine(vec(n.thetas)) - - else - DecisionLine(node2id[n], UInt32(0), UInt32(num_children(n)), # TODO vtree id - map(1:num_children(n)) do i - make_element(children(n)[i], vec(n.thetas[i,:]), node2id) - end) - end -end - -##################### -# saver for circuits -##################### - -"Returns header for PSDD file format" -function psdd_header() - """ - c ids of psdd nodes start at 0 - c psdd nodes appear bottom-up, children before parents - c - c file syntax: - c psdd count-of-sdd-nodes - c L id-of-literal-sdd-node id-of-vtree literal - c T id-of-trueNode-sdd-node id-of-vtree variable log(litProb) - c D id-of-decomposition-sdd-node id-of-vtree number-of-elements {id-of-prime id-of-sub log(elementProb)}* - c - c File generated by Juice.jl - c""" -end - -"Save a circuit to PSDD file format" -function save_as_psdd(file::Union{String, IO}, circuit::ProbCircuit, vtree::PlainVtree) - # TODO add method isstructured - file isa String && @assert endswith(file, ".psdd") - node2id = get_node2id(circuit) - vtree2id = get_vtree2id(vtree) - formatlines = Vector{CircuitFormatLine}() - append!(formatlines, parse_psdd_file(IOBuffer(psdd_header()))) - push!(formatlines, PsddHeaderLine(num_nodes(circuit))) - for n in filter(n -> !is⋀gate(n), circuit) - push!(formatlines, decompile(n, node2id, vtree2id)) - end - save_lines(file, formatlines) -end - -function lc_header() - """ - c variables (from inputs) start from 1 - c ids of logistic circuit nodes start from 0 - c nodes appear bottom-up, children before parents - c the last line of the file records the bias parameter - c three types of nodes: - c T (terminal nodes that correspond to true literals) - c F (terminal nodes that correspond to false literals) - c D (OR gates) - c - c file syntax: - c Logistic Circuit - c T id-of-true-literal-node id-of-vtree variable parameters - c F id-of-false-literal-node id-of-vtree variable parameters - c D id-of-or-gate id-of-vtree number-of-elements (id-of-prime id-of-sub parameters)s - c B bias-parameters - c - c File generated by Juice.jl - c""" -end - -"Save a logistic circuit to file" -function save_as_logistic(name::String, circuit::LogisticCircuit, vtree) - @assert endswith(name, ".circuit") - node2id = get_node2id(circuit) - # TODO lc vtree - formatlines = Vector{CircuitFormatLine}() - append!(formatlines, parse_lc_file(IOBuffer(lc_header()))) - push!(formatlines, LcHeaderLine()) - for n in filter(n -> is⋁gate(n), circuit) - push!(formatlines, decompile(n, node2id)) - end - save_lines(name, formatlines) -end - -# TODO add Decompile for logistic circuit - -import LogicCircuits.LoadSave: save_circuit, save_as_dot # make available for extension - -"Save a circuit to file" -save_circuit(name::String, circuit::StructProbCircuit, vtree::PlainVtree) = - save_as_psdd(name, circuit, vtree) - -save_circuit(name::String, circuit::LogisticCircuit, vtree) = - save_as_logistic(name, circuit, vtree) - -using Printf: @sprintf -"Save prob circuits to .dot file" -function save_as_dot(file::String, circuit::ProbCircuit) - # TODO (https://github.com/Juice-jl/LogicCircuits.jl/issues/7) - circuit_nodes = linearize(circuit) - node_cache = Dict{ProbCircuit, Int64}() - for (i, n) in enumerate(circuit_nodes) - node_cache[n] = i - end - - levels = get_nodes_level(circuit) - - f = open(file, "w") - write(f,"digraph Circuit {\nsplines=false\nedge[arrowhead=\"none\",fontsize=6]\n") - - for level in levels - if length(level) > 1 - write(f,"{rank=\"same\";newrank=\"true\";rankdir=\"LR\";") - rank = "" - foreach(x->rank*="$(node_cache[x])->",level) - rank = rank[1:end-2] - write(f, rank) - write(f,"[style=invis]}\n") - end - end - - for n in reverse(circuit_nodes) - if ismul(n) - write(f, "$(node_cache[n]) [label=\"*$(node_cache[n])\"]\n") - elseif issum(n) - write(f, "$(node_cache[n]) [label=\"+$(node_cache[n])\"]\n") - elseif isliteralgate(n) && ispositive(n) - write(f, "$(node_cache[n]) [label=\"+$(variable(n))\"]\n") - elseif isliteralgate(n) && isnegative(n) - write(f, "$(node_cache[n]) [label=\"-$(variable(n))\"]\n") - else - throw("unknown ProbCircuit type") - end - end - - for n in reverse(circuit_nodes) - if ismul(n) - for c in children(n) - write(f, "$(node_cache[n]) -> $(node_cache[c])\n") - end - elseif issum(n) - for (c, p) in zip(children(n), exp.(n.log_probs)) - prob = @sprintf "%0.1f" p - write(f, "$(node_cache[n]) -> $(node_cache[c]) [label=\"$prob\"]\n") - end - else - end - end - - write(f, "}\n") - flush(f) - close(f) -end - -"Save file as a .esbl ensemble file format." -function save_as_ensemble(name::String, ensemble::Ensemble{StructProbCircuit}; quiet::Bool = false) - @assert endswith(name, ".esbl") - zip = ZipFile.Writer(name) - f_w = ZipFile.addfile(zip, "ensemble.meta") - n = length(ensemble.C) - write(f_w, "$(n)\n") - write(f_w, join(ensemble.W, ' ')) - close(f_w) - function do_work(C::StructProbCircuit, i::Integer) - f_c = ZipFile.addfile(zip, "$(i).psdd") - save_as_psdd(f_c, C, C.vtree) - f_v = ZipFile.addfile(zip, "$(i).vtree") - save_vtree(f_v, C.vtree) - nothing - end - !quiet && print("Saving circuits...\n ") - for (i, C) ∈ enumerate(ensemble.C) - do_work(C, i) - !quiet && print('*') - end - !quiet && print('\n') - close(zip) - nothing -end diff --git a/src/Logistic/Logistic.jl b/src/Logistic/Logistic.jl deleted file mode 100644 index e36fcb59..00000000 --- a/src/Logistic/Logistic.jl +++ /dev/null @@ -1,11 +0,0 @@ -module Logistic - -using LogicCircuits -using ...ProbabilisticCircuits - -include("queries.jl") -include("parameters.jl") - -# TODO structure learning - -end \ No newline at end of file diff --git a/src/Logistic/parameter_circuit.jl b/src/Logistic/parameter_circuit.jl deleted file mode 100644 index ee008dd7..00000000 --- a/src/Logistic/parameter_circuit.jl +++ /dev/null @@ -1,231 +0,0 @@ -using CUDA -using LogicCircuits - -export LayeredParameterCircuit, CuLayeredParameterCircuit -export class_likelihood, class_weights -export one_hot, learn_parameters, update_parameters - -############################################################# -############## This is the old implementation ############### -#### Not intended to be used under the current framework #### -############################################################# - - -# in a parameter circuit -# 1 is true, 2 is false -const TRUE_ID = Int32(1) -const FALSE_ID = Int32(2) - -struct LayeredParameterCircuit - layered_circuit::LayeredBitCircuit - layered_parameters::Vector{Matrix{Float32}} -end - -LayeredParameterCircuit(circuit::LogisticCircuit, nc::Integer, num_features::Integer) = begin - @assert is⋁gate(circuit) - decisions::Vector{Vector{Int32}} = Vector{Vector{Int32}}() - elements::Vector{Vector{Int32}} = Vector{Vector{Int32}}() - parameters::Vector{Vector{Float32}} = Vector{Vector{Float32}}() - num_decisions::Int32 = 2 * num_features + 2 - num_elements::Vector{Int32} = Vector{Int32}() - # num_parameters always equals num_elements - - ensure_layer(i) = begin - if length(decisions) < i - # add a new layer - push!(decisions, Int32[]) - push!(elements, Int32[]) - push!(parameters, Float32[]) - push!(num_elements, 0) - end - end - - f_con(n) = LayeredDecisionId(0, istrue(n) ? TRUE_ID : FALSE_ID) - f_lit(n) = LayeredDecisionId(0, - ispositive(n) ? Int32(2 + variable(n)) : Int32(2 + num_features + variable(n))) - - f_and(n, cs) = begin - @assert length(cs) == 2 - LayeredDecisionId[cs[1], cs[2]] - end - f_or(n, cs) = begin - num_decisions += 1 - # determine layer - layer_id = zero(Int32) - for c in cs - if c isa Vector{LayeredDecisionId} - @assert length(c) == 2 - layer_id = max(layer_id, c[1].layer_id, c[2].layer_id) - else - @assert c isa LayeredDecisionId - layer_id = max(layer_id, c.layer_id) - end - end - layer_id += 1 - ensure_layer(layer_id) - first_element = num_elements[layer_id] + 1 - foreach(cs, eachrow(n.thetas)) do c, theta - @assert size(theta)[1] == nc - append!(parameters[layer_id], theta) - num_elements[layer_id] += 1 - if c isa Vector{LayeredDecisionId} - push!(elements[layer_id], c[1].decision_id, c[2].decision_id) - else - push!(elements[layer_id], c.decision_id, TRUE_ID) - end - end - push!(decisions[layer_id], num_decisions, first_element, num_elements[layer_id]) - LayeredDecisionId(layer_id, num_decisions) - end - - foldup_aggregate(circuit, f_con, f_lit, f_and, f_or, - Union{LayeredDecisionId,Vector{LayeredDecisionId}}) - - circuit_layers = map(decisions, elements) do d, e - Layer(reshape(d, 3, :), reshape(e, 2, :)) - end - parameter_layers = map(parameters) do p - reshape(p, nc, :) - end - return LayeredParameterCircuit(LayeredBitCircuit(circuit_layers), parameter_layers) -end - -struct CuLayeredParameterCircuit - layered_circuit::CuLayeredBitCircuit - layered_parameters::Vector{CuMatrix{Float32}} - CuLayeredParameterCircuit(l::LayeredParameterCircuit) = new(CuLayeredBitCircuit(l.layered_circuit), map(CuMatrix, l.layered_parameters)) -end - - - -function class_likelihood(circuit::CuLayeredParameterCircuit, nc::Integer, data::CuMatrix{Float32}, reuse_up=nothing, reuse_down=nothing, reuse_cp=nothing) - cw, flow, v = class_weights(circuit, nc, data, reuse_up, reuse_down, reuse_cp) - one = Float32(1.0) - return @. one / (one + exp(-cw)), flow, v -end - -function class_weights(circuit::CuLayeredParameterCircuit, nc::Integer, data::CuMatrix{Float32}, reuse_up=nothing, reuse_down=nothing, reuse_cw=nothing) - flow, v = compute_flows2(circuit.layered_circuit, data, reuse_up, reuse_down) - cw = calculate_class_weights(circuit, nc, data, v, flow, reuse_cw) - return cw, flow, v -end - -function calculate_class_weights(circuit::CuLayeredParameterCircuit, nc::Integer, data::CuMatrix{Float32}, v, flow, reuse_cw=nothing) - ne = num_examples(data) - cw = if reuse_cw isa CuMatrix{Float32} && size(reuse_cw) == (ne, nc) - reuse_cw .= zero(Float32) - reuse_cw - else - CUDA.zeros(Float32, ne, nc) - end - - dec_per_thread = 4 - CUDA.@sync for i = 1:length(circuit.layered_circuit.layers) - circuit_layer = circuit.layered_circuit.layers[i] - parameter_layer = circuit.layered_parameters[i] - ndl = num_decisions(circuit_layer) - num_threads = balance_threads(ne, ndl / dec_per_thread, 8) - num_blocks = ceil(Int, ne / num_threads[1]), ceil(Int, ndl / num_threads[2] / dec_per_thread) - @cuda threads=num_threads blocks=num_blocks calculate_class_weights_layer_kernel_cuda(cw, v, flow, circuit_layer.decisions, circuit_layer.elements, parameter_layer) - end - - return cw -end - -function calculate_class_weights_layer_kernel_cuda(cw, v, flow, decisions, elements, parameters) - index_x = (blockIdx().x - 1) * blockDim().x + threadIdx().x - index_y = (blockIdx().y - 1) * blockDim().y + threadIdx().y - stride_x = blockDim().x * gridDim().x - stride_y = blockDim().y * gridDim().y - ne, nc = size(cw) - _, num_decisions = size(decisions) - - for j = index_x:stride_x:ne - for i = index_y:stride_y:num_decisions - decision_id = @inbounds decisions[1, i] - n_up = @inbounds v[j, decision_id] - if n_up > zero(Float32) - first_elem = @inbounds decisions[2, i] - last_elem = @inbounds decisions[3, i] - n_down = @inbounds flow[j, decision_id] - for e = first_elem:last_elem - e1 = @inbounds elements[1, first_elem] - e2 = @inbounds elements[2, first_elem] - e_up = @inbounds (v[j, e1] * v[j, e2]) - edge_flow = e_up / n_up * n_down - # following needs to be memory safe - for class=1:nc - @CUDA.atomic cw[j, class] += edge_flow * parameters[class, e] # atomic is automatically inbounds - end - end - end - end - end - - return nothing -end - - - -function one_hot(labels::Vector, nc::Integer) - ne = length(labels) - one_hot_labels = zeros(Float32, ne, nc) - for (i, j) in enumerate(labels) - one_hot_labels[i, j + 1] = 1.0 - end - one_hot_labels -end - -function learn_parameters(circuit::CuLayeredParameterCircuit, nc::Integer, data::CuMatrix{Float32}, labels::CuMatrix{Float32}, reuse_up=nothing, reuse_down=nothing, reuse_cp=nothing, num_epochs=20, step_size=0.0001) - cp, flow, v = class_likelihood(circuit, nc, data, reuse_up, reuse_down, reuse_cp) - update_parameters(circuit, labels, cp, flow, step_size) - for _ = 2:num_epochs - cp, flow, v = class_likelihood(circuit, nc, data, v, flow, cp) - update_parameters(circuit, labels, cp, v, flow, step_size) - end - return nothing -end - -function update_parameters(circuit::CuLayeredParameterCircuit, labels, cp, v, flow, step_size=0.0001) - _, nc = size(labels) - step_size = Float32(step_size) - CUDA.@sync for i = 1:length(circuit.layered_circuit.layers) - circuit_layer = circuit.layered_circuit.layers[i] - flow_layer = flow[i] - parameter_layer = circuit.layered_parameters[i] - ndl = num_decisions(circuit_layer) - num_threads = balance_threads(ndl, nc, 6) - num_threads = num_threads[1], num_threads[2], - num_blocks = ceil(Int, ndl / num_threads[1]), ceil(Int, nc / num_threads[2]), 4 - @cuda threads=num_threads blocks=num_blocks update_parameters_layer_kernel_cuda(labels, cp, flow_layer, circuit_layer.decisions, parameter_layer, step_size) - end - return nothing -end - -function update_parameters_layer_kernel_cuda(labels, cp, flow, decisions, parameters, step_size) - index_x = (blockIdx().x - 1) * blockDim().x + threadIdx().x - index_y = (blockIdx().y - 1) * blockDim().y + threadIdx().y - index_z = (blockIdx().z - 1) * blockDim().z + threadIdx().z - stride_x = blockDim().x * gridDim().x - stride_y = blockDim().y * gridDim().y - stride_z = blockDim().z * gridDim().z - ne, nc = size(labels) - _, num_decisions = size(decisions) - - for class = index_y:stride_y:nc - for i = index_x:stride_x:num_decisions - first_elem = @inbounds decisions[2, i] - last_elem = @inbounds decisions[3, i] - for e = first_elem:last_elem - for j = index_z:stride_z:ne - edge_flow = e_up / n_up * n_down - u = @inbounds edge_flow * (cp[j, class] - labels[j, class]) * step_size - # following needs to be memory safe - @inbounds parameters[class, e] -= u - end - end - end - end - - return nothing -end \ No newline at end of file diff --git a/src/Logistic/parameters.jl b/src/Logistic/parameters.jl deleted file mode 100644 index 2a7de41f..00000000 --- a/src/Logistic/parameters.jl +++ /dev/null @@ -1,122 +0,0 @@ -export learn_parameters, to_onehot - -using CUDA - -""" -LogisticCircuit Parameter learning through gradient descents -Note: data need to be DataFrame and Labels need to be in one-hot form. -""" -function learn_parameters(lc::LogisticCircuit, nc::Int, data, labels; num_epochs=25, step_size=0.01) - bc = ParamBitCircuit(lc, nc, data) - if isgpu(data) - @assert isgpu(labels) "Data and labels must be both stored in either GPU or CPU." - for _ = 1:num_epochs - cl = class_likelihood_per_instance(bc, data) - update_parameters_gpu(to_gpu(bc), data, labels, cl, step_size) - end - else - @assert !isgpu(labels) "Data and labels must be both stored in either GPU or CPU." - for _ = 1:num_epochs - cl = class_likelihood_per_instance(bc, data) - update_parameters_cpu(bc, data, labels, cl, step_size) - end - end -end - -function update_parameters_cpu(bc, data, labels, cl, step_size) - ne::Int = num_examples(data) - nc::Int = size(bc.params, 2) - params_lock::Threads.ReentrantLock = Threads.ReentrantLock() - - @inline function on_edge_binary(flows, values, prime, sub, element, grandpa, single_child, weights) - lock(params_lock) do # TODO: move lock to inner loop? - for i = 1:size(flows, 1) - @inbounds edge_flow = values[i, prime] & values[i, sub] & flows[i, grandpa] - first_true_bit = trailing_zeros(edge_flow) + 1 - last_true_bit = 64 - leading_zeros(edge_flow) - @simd for j = first_true_bit:last_true_bit - ex_id = ((i - 1) << 6) + j - if get_bit(edge_flow, j) - for class = 1:nc - @inbounds bc.params[element, class] -= (cl[ex_id, class] - labels[ex_id, class]) * step_size - end - end - end - end - end - end - - @inline function on_edge_float(flows, values, prime, sub, element, grandpa, single_child, weights) - lock(params_lock) do # TODO: move lock to inner loop? - @simd for i = 1:size(flows, 1) # adding @avx here might give incorrect results - @inbounds edge_flow = values[i, prime] * values[i, sub] / values[i, grandpa] * flows[i, grandpa] - edge_flow = ifelse(isfinite(edge_flow), edge_flow, zero(eltype(flows))) - for class = 1:nc - @inbounds bc.parames[element, class] -= (cl[i, class] - labels[i, class]) * edge_flow * step_size - end - end - end - nothing - end - - if isbinarydata(data) - v,f = satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_binary) - else - @assert isfpdata(data) "Only floating point and binary data are supported" - v,f = satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_float) - end - - nothing -end - - -function update_parameters_gpu(bc, data, labels, cl, step_size) - ne::Int = num_examples(data) - nc::Int = size(bc.params, 2) - cl_device = CUDA.cudaconvert(cl) - label_device = CUDA.cudaconvert(labels) - params_device = CUDA.cudaconvert(bc.params) - - @inline function on_edge_binary(flows, values, prime, sub, element, grandpa, chunk_id, edge_flow, single_child, weights) - first_true_bit = 1 + trailing_zeros(edge_flow) - last_true_bit = 64 - leading_zeros(edge_flow) - for j = first_true_bit:last_true_bit - if get_bit(edge_flow, j) - ex_id = ((chunk_id - 1) << 6) + j - for class = 1:nc - CUDA.@atomic params_device[element, class] -= (cl_device[ex_id, class] - label_device[ex_id, class]) * step_size - end - end - end - nothing - end - - @inline function on_edge_float(flows, values, prime, sub, element, grandpa, ex_id, edge_flow, single_child, weights) - for class = 1:nc - CUDA.@atomic params_device[element, class] -= (cl_device[ex_id, class] - label_device[ex_id, class]) * edge_flow * step_size - end - nothing - end - - if isbinarydata(data) - v,f = satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_binary) - else - @assert isfpdata(data) "Only floating point and binary data are supported" - v,f = satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_float) - end - CUDA.unsafe_free!(v) # save the GC some effort - CUDA.unsafe_free!(f) # save the GC some effort - - nothing -end - - - -function to_onehot(labels::Vector, nc::Integer) - ne = length(labels) - one_hot_labels = zeros(Float32, ne, nc) - for (i, j) in enumerate(labels) - one_hot_labels[i, j + 1] = 1.0 - end - one_hot_labels -end \ No newline at end of file diff --git a/src/Logistic/queries.jl b/src/Logistic/queries.jl deleted file mode 100644 index c608f92e..00000000 --- a/src/Logistic/queries.jl +++ /dev/null @@ -1,149 +0,0 @@ -export class_likelihood_per_instance, class_weights_per_instance - -using CUDA -using LoopVectorization: @avx - - -""" -Class Conditional Probability -""" -function class_likelihood_per_instance(lc::LogisticCircuit, nc::Int, data) - cw = class_weights_per_instance(lc, nc, data) - one = Float32(1.0) - isgpu(data) ? (@. one / (one + exp(-cw))) : (@. @avx one / (one + exp(-cw))) -end - -function class_likelihood_per_instance(bc, data) - cw = class_weights_per_instance(bc, data) - one = Float32(1.0) - isgpu(data) ? (@. one / (one + exp(-cw))) : (@. @avx one / (one + exp(-cw))) -end - -function class_weights_per_instance(lc::LogisticCircuit, nc::Int, data) - bc = ParamBitCircuit(lc, nc, data) - class_weights_per_instance(bc, data) -end - -function class_weights_per_instance(bc, data) - if isgpu(data) - class_weights_per_instance_gpu(to_gpu(bc), data) - else - class_weights_per_instance_cpu(bc, data) - end -end - -function class_weights_per_instance_cpu(bc, data) - ne::Int = num_examples(data) - nc::Int = size(bc.params, 2) - cw::Matrix{Float32} = zeros(Float32, ne, nc) - cw_lock::Threads.ReentrantLock = Threads.ReentrantLock() - - @inline function on_edge_binary(flows, values, prime, sub, element, grandpa, single_child, weights) - lock(cw_lock) do # TODO: move lock to inner loop? - for i = 1:size(flows, 1) - @inbounds edge_flow = values[i, prime] & values[i, sub] & flows[i, grandpa] - first_true_bit = trailing_zeros(edge_flow) + 1 - last_true_bit = 64 - leading_zeros(edge_flow) - @simd for j = first_true_bit:last_true_bit - ex_id = ((i - 1) << 6) + j - if get_bit(edge_flow, j) - for class = 1:nc - @inbounds cw[ex_id, class] += bc.params[element, class] - end - end - end - end - end - nothing - end - - @inline function on_edge_float(flows, values, prime, sub, element, grandpa, single_child, weights) - lock(cw_lock) do # TODO: move lock to inner loop? - @simd for i = 1:size(flows, 1) # adding @avx here might give incorrect results - @inbounds edge_flow = values[i, prime] * values[i, sub] / values[i, grandpa] * flows[i, grandpa] - edge_flow = ifelse(isfinite(edge_flow), edge_flow, zero(eltype(flows))) - for class = 1:nc - @inbounds cw[i, class] += edge_flow * bc.params[element, class] - end - end - end - nothing - end - - if isbinarydata(data) - satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_binary) - else - satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_float) - end - - return cw -end - -function class_weights_per_instance_gpu(bc, data) - ne::Int = num_examples(data) - nc::Int = size(bc.params, 2) - cw::CuMatrix{Float32} = CUDA.zeros(Float32, num_examples(data), nc) - cw_device = CUDA.cudaconvert(cw) - params_device = CUDA.cudaconvert(bc.params) - - @inline function on_edge_binary(flows, values, prime, sub, element, grandpa, chunk_id, edge_flow, single_child, weights) - first_true_bit = 1 + trailing_zeros(edge_flow) - last_true_bit = 64 - leading_zeros(edge_flow) - for j = first_true_bit:last_true_bit - ex_id = ((chunk_id - 1) << 6) + j - if get_bit(edge_flow, j) - for class = 1:nc - CUDA.@atomic cw_device[ex_id, class] += params_device[element, class] - end - end - end - nothing - end - - @inline function on_edge_float(flows, values, prime, sub, element, grandpa, ex_id, edge_flow, single_child, weights) - for class = 1:nc - CUDA.@atomic cw_device[ex_id, class] += edge_flow * params_device[element, class] - end - nothing - end - - if isbinarydata(data) - v,f = satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_binary) - else - @assert isfpdata(data) "Only floating point and binary data are supported" - v,f = satisfies_flows(bc.bitcircuit, data; on_edge = on_edge_float) - end - CUDA.unsafe_free!(v) # save the GC some effort - CUDA.unsafe_free!(f) # save the GC some effort - - return cw -end - - - -""" -Class Predictions -""" -function predict_class(lc::LogisticCircuit, nc::Int, data) - class_likelihoods = class_likelihood_per_instance(lc, nc, data) - predict_class(class_likelihoods) -end - -function predict_class(class_likelihoods) - _, mxindex = findmax(class_likelihoods; dims=2) - dropdims(getindex.(mxindex, 2); dims=2) -end - - - -""" -Prediction accuracy -""" -accuracy(lc::LogisticCircuit, nc::Int, data, labels::Vector) = - accuracy(predict_class(lc, nc, data), labels) - -accuracy(predicted_class::Vector, labels::Vector) = - Float64(sum(@. predicted_class == labels)) / length(labels) - -accuracy(class_likelihoods::Matrix, labels::Vector) = - accuracy(predict_class(class_likelihoods), labels) diff --git a/src/ProbabilisticCircuits.jl b/src/ProbabilisticCircuits.jl index a6d61ba5..0f9c9149 100644 --- a/src/ProbabilisticCircuits.jl +++ b/src/ProbabilisticCircuits.jl @@ -13,23 +13,23 @@ export pos_literals, neg_literals export issmooth, isdecomposable, isstruct_decomposable, isdeterministic, iscanonical # circuit status -export num_edges, num_parameters +export num_nodes, num_edges, children, num_parameters +export zoo_vtree, zoo_vtree_file, VtreeFormat, vtree, respects_vtree +export linearize, isliteralgate, literal, fully_factorized_circuit # datasets export twenty_datasets include("Utils/Utils.jl") @reexport using .Utils -include("FactorGraph/factor_graph.jl") -include("FactorGraph/fg_compile.jl") +include("factor_graph/factor_graph.jl") +include("factor_graph/fg_compile.jl") include("abstract_prob_nodes.jl") include("shared_prob_nodes.jl") include("plain_prob_nodes.jl") include("structured_prob_nodes.jl") -include("logistic_nodes.jl") include("param_bit_circuit.jl") -include("param_bit_circuit_pair.jl") include("parameters.jl") include("gradient_based_learning.jl") @@ -39,12 +39,6 @@ include("queries/map.jl") include("queries/sample.jl") include("queries/pr_constraint.jl") include("queries/information.jl") -include("queries/expectation_rec.jl") -include("queries/expectation_graph.jl") -include("queries/expectation_bit.jl") - -include("Logistic/Logistic.jl") -@reexport using .Logistic include("mixtures/em.jl") @@ -59,8 +53,7 @@ include("structurelearner/bdd.jl") include("ensembles/ensembles.jl") include("ensembles/bmc.jl") -include("LoadSave/LoadSave.jl") -@reexport using .LoadSave +include("io/io.jl") using Requires diff --git a/src/FactorGraph/factor_graph.jl b/src/factor_graph/factor_graph.jl similarity index 100% rename from src/FactorGraph/factor_graph.jl rename to src/factor_graph/factor_graph.jl diff --git a/src/FactorGraph/fg_compile.jl b/src/factor_graph/fg_compile.jl similarity index 98% rename from src/FactorGraph/fg_compile.jl rename to src/factor_graph/fg_compile.jl index cbe2ec9e..35eb5b5e 100644 --- a/src/FactorGraph/fg_compile.jl +++ b/src/factor_graph/fg_compile.jl @@ -10,7 +10,7 @@ using Pkg.Artifacts # Might want to move this to LoadSave later zoo_fg_file(name) = - artifact"circuit_model_zoo" * LogicCircuits.LoadSave.zoo_version * "/fgs/$name" + artifact"circuit_model_zoo" * LogicCircuits.zoo_version * "/fgs/$name" """ diff --git a/src/io/clt_io.jl b/src/io/clt_io.jl new file mode 100644 index 00000000..ffcb4e81 --- /dev/null +++ b/src/io/clt_io.jl @@ -0,0 +1,42 @@ +export zoo_clt, zoo_clt_file + +using LogicCircuits +using Pkg.Artifacts + +##################### +# circuit loaders from module zoo +##################### + +# TODO move this to a Lerche.jl parser and use Base.{read,parse} + +zoo_clt_file(name) = + artifact"circuit_model_zoo" * LogicCircuits.zoo_version * "/clts/$name" + +zoo_clt(name) = + parse_clt(zoo_clt_file(name)) + + +using MetaGraphs: MetaDiGraph, set_prop!, props, add_edge! + +"Parse a clt from given file" +function parse_clt(filename::String)::MetaDiGraph + f = open(filename) + n = parse(Int32,readline(f)) + n_root = parse(Int32,readline(f)) + clt = MetaDiGraph(n) + for i in 1 : n_root + root, prob = split(readline(f), " ") + root, prob = parse(Int32, root), parse(Float64, prob) + set_prop!(clt, root, :parent, 0) + set_prop!(clt, root, :cpt, Dict(1=>prob,0=>1-prob)) + end + + for i = 1 : n - n_root + dst, src, prob1, prob0 = split(readline(f), " ") + dst, src, prob1, prob0 = parse(Int32, dst), parse(Int32, src), parse(Float64, prob1), parse(Float64, prob0) + add_edge!(clt, src,dst) + set_prop!(clt, dst, :parent, src) + set_prop!(clt, dst, :cpt, Dict((1,1)=>prob1, (0,1)=>1-prob1, (1,0)=>prob0, (0,0)=>1-prob0)) + end + return clt +end diff --git a/src/io/ensemble_io.jl b/src/io/ensemble_io.jl new file mode 100644 index 00000000..f7cc1251 --- /dev/null +++ b/src/io/ensemble_io.jl @@ -0,0 +1,73 @@ +export load_as_ensemble, save_as_ensemble + +# TODO transition this to a Lerche.jl parser and use Base.{read,parse} + +"Loads an ensemble from disk." +function load_as_ensemble(name::String; quiet::Bool = false)::Ensemble{StructProbCircuit} + @assert endswith(name, ".esbl") + zip = ZipFile.Reader(name) + W, n = Vector{Float64}(), -1 + for f ∈ zip.files + if endswith(f.name, ".meta") + n = parse(Int, readline(f)) + W = map(x -> parse(Float64, x), split(readline(f))) + end + end + @assert n > 0 && length(W) == n "Ensemble file format corrupted, empty or missing meta file." + P = Tuple{Int, Int}[(0, 0) for i ∈ 1:n] + for (i, f) ∈ enumerate(zip.files) + if endswith(f.name, ".psdd") + j = parse(Int, f.name[1:end-5]) + @assert j > 0 && j <= n "Either .meta file is corrupted or .psdd is misnamed (faulty: $(f.name))." + P[j] = (i, P[j][2]) + elseif endswith(f.name, ".vtree") + j = parse(Int, f.name[1:end-6]) + @assert j > 0 && j <= n "Either .meta file is corrupted or .vtree is misnamed (faulty: $(f.name))." + P[j] = (P[j][1], i) + end + end + C = Vector{StructProbCircuit}(undef, n) + function do_work(k::Int, i::Int, j::Int) + @assert i > 0 "Missing .psdd file for the $k-th circuit." + @assert j > 0 "Missing .psdd file for the $k-th circuit." + psdd_file, vtree_file = zip.files[i], zip.files[j] + psdd, _ = load_struct_prob_circuit(psdd_file, vtree_file) + C[k] = psdd + nothing + end + !quiet && print("Loading circuits...\n ") + for (k, (i, j)) ∈ enumerate(P) + do_work(k, i, j) + !quiet && print('*') + end + !quiet && print('\n') + close(zip) + return Ensemble{StructProbCircuit}(C, W) +end + + +"Save file as a .esbl ensemble file format." +function save_as_ensemble(name::String, ensemble::Ensemble{StructProbCircuit}; quiet::Bool = false) + @assert endswith(name, ".esbl") + zip = ZipFile.Writer(name) + f_w = ZipFile.addfile(zip, "ensemble.meta") + n = length(ensemble.C) + write(f_w, "$(n)\n") + write(f_w, join(ensemble.W, ' ')) + close(f_w) + function do_work(C::StructProbCircuit, i::Integer) + f_c = ZipFile.addfile(zip, "$(i).psdd") + save_as_psdd(f_c, C, C.vtree) + f_v = ZipFile.addfile(zip, "$(i).vtree") + save_vtree(f_v, C.vtree) + nothing + end + !quiet && print("Saving circuits...\n ") + for (i, C) ∈ enumerate(ensemble.C) + do_work(C, i) + !quiet && print('*') + end + !quiet && print('\n') + close(zip) + nothing +end diff --git a/src/io/io.jl b/src/io/io.jl new file mode 100644 index 00000000..47c06963 --- /dev/null +++ b/src/io/io.jl @@ -0,0 +1,81 @@ +using LogicCircuits +using LogicCircuits: JuiceTransformer, dimacs_comments, zoo_version + +using Pkg.Artifacts +using Lerche: Lerche, Lark, Transformer, @rule, @inline_rule + +include("jpc_io.jl") +include("psdd_io.jl") +include("clt_io.jl") +include("ensemble_io.jl") +include("plot.jl") + +# if no logic circuit file format is given on read, infer file format from extension + +function file2pcformat(file) + if endswith(file,".jpc") + JpcFormat() + elseif endswith(file,".psdd") + PsddFormat() + else + # try a logic circuit format + LogicCircuits.file2logicformat(file) + end +end + +""" + Base.read(file::AbstractString, ::Type{C}) where C <: ProbCircuit + +Reads circuit from file; uses extension to detect format type, for example ".psdd" for PSDDs. +""" +Base.read(file::AbstractString, ::Type{C}) where C <: ProbCircuit = + read(file, C, file2pcformat(file)) + + +Base.read(files::Tuple{AbstractString,AbstractString}, ::Type{C}) where C <: StructProbCircuit = + read(files, C, (file2pcformat(files[1]), VtreeFormat())) + +""" + Base.write(file::AbstractString, circuit::ProbCircuit) + +Writes circuit to file; uses file name extention to detect file format. +""" +Base.write(file::AbstractString, circuit::ProbCircuit) = + write(file, circuit, file2pcformat(file)) + +""" + Base.write(files::Tuple{AbstractString,AbstractString}, circuit::StructProbCircuit) + +Saves circuit and vtree to file. +""" +Base.write(files::Tuple{AbstractString,AbstractString}, + circuit::StructProbCircuit) = + write(files, circuit, (file2pcformat(files[1]), VtreeFormat())) + + +# when asked to parse/read as `ProbCircuit`, default to `PlainProbCircuit` + +Base.parse(::Type{ProbCircuit}, args...) = + parse(PlainProbCircuit, args...) + +Base.read(io::IO, ::Type{ProbCircuit}, args...) = + read(io, PlainProbCircuit, args...) + +# copy read/write API for tuples of files + +function Base.read(files::Tuple{AbstractString, AbstractString}, ::Type{C}, args...) where C <: StructProbCircuit + open(files[1]) do io1 + open(files[2]) do io2 + read((io1, io2), C, args...) + end + end +end + +function Base.write(files::Tuple{AbstractString,AbstractString}, + circuit::StructProbCircuit, args...) + open(files[1], "w") do io1 + open(files[2], "w") do io2 + write((io1, io2), circuit, args...) + end + end +end \ No newline at end of file diff --git a/src/io/jpc_io.jl b/src/io/jpc_io.jl new file mode 100644 index 00000000..b311b662 --- /dev/null +++ b/src/io/jpc_io.jl @@ -0,0 +1,185 @@ +export zoo_jpc, zoo_jpc_file, + JpcFormat, JpcVtreeFormat + +struct JpcFormat <: FileFormat end + +const JpcVtreeFormat = Tuple{JpcFormat,VtreeFormat} +Tuple{JpcFormat,VtreeFormat}() = (JpcFormat(),VtreeFormat()) + +############################################## +# Read JPC (Juice Probabilistic Circuit) +############################################## + +zoo_jpc_file(name) = + artifact"circuit_model_zoo" * zoo_version * "/jpcs/$name" + +""" + zoo_jpc(name) + +Loads JPC file with given name from model zoo. See https://github.com/UCLA-StarAI/Circuit-Model-Zoo. +""" +zoo_jpc(name) = + read(zoo_jpc_file(name), ProbCircuit, JpcFormat()) + +const jpc_grammar = raw""" + start: header (_NL node)+ _NL? + + header : "jpc" _WS INT + + node : "L" _WS INT _WS INT _WS SIGNED_INT -> literal_node + | "P" _WS INT _WS INT _WS INT child_nodes -> prod_node + | "S" _WS INT _WS INT _WS INT weighted_child_nodes -> sum_node + + child_nodes : (_WS INT)+ + weighted_child_nodes: (_WS INT _WS LOGPROB)+ + + %import common.INT + %import common.SIGNED_INT + %import common.SIGNED_NUMBER -> LOGPROB + %import common.WS_INLINE -> _WS + %import common.NEWLINE -> _NL + """ * dimacs_comments + +const jpc_parser = Lark(jpc_grammar) + +abstract type JpcParse <: JuiceTransformer end + +@inline_rule header(t::JpcParse, x) = + Base.parse(Int,x) + +@rule start(t::JpcParse, x) = begin + @assert num_nodes(x[end]) == x[1] + x[end] +end + +@rule child_nodes(t::JpcParse, x) = + map(id -> t.nodes[id], x) + +@rule weighted_child_nodes(t::JpcParse, x) = begin + children = map(id -> t.nodes[id], x[1:2:end]) + log_probs = Base.parse.(Float64,x[2:2:end]) + (children, log_probs) +end + +# parse unstructured +struct PlainJpcParse <: JpcParse + nodes::Dict{String,PlainProbCircuit} + PlainJpcParse() = new(Dict{String,PlainProbCircuit}()) +end + +@rule literal_node(t::PlainJpcParse, x) = + t.nodes[x[1]] = PlainProbLiteralNode(Base.parse(Lit,x[3])) + +@rule prod_node(t::PlainJpcParse,x) = begin + @assert length(x[4]) == Base.parse(Int,x[3]) + t.nodes[x[1]] = PlainMulNode(x[4]) +end + +@rule sum_node(t::PlainJpcParse,x) = begin + @assert length(x[4][1]) == length(x[4][2]) == Base.parse(Int,x[3]) + t.nodes[x[1]] = PlainSumNode(x[4][1], x[4][2]) +end + +function Base.parse(::Type{PlainProbCircuit}, str, ::JpcFormat) + ast = Lerche.parse(jpc_parser, str) + Lerche.transform(PlainJpcParse(), ast) +end + +Base.read(io::IO, ::Type{PlainProbCircuit}, ::JpcFormat) = + parse(PlainProbCircuit, read(io, String), JpcFormat()) + +# parse structured +struct StructJpcParse <: JpcParse + id2vtree::Dict{String,<:Vtree} + nodes::Dict{String,StructProbCircuit} + StructJpcParse(id2vtree) = + new(id2vtree,Dict{String,StructProbCircuit}()) +end + +@rule literal_node(t::StructJpcParse, x) = begin + lit = Base.parse(Lit,x[3]) + vtree = t.id2vtree[x[2]] + t.nodes[x[1]] = StructProbLiteralNode(lit, vtree) +end + +@rule prod_node(t::StructJpcParse,x) = begin + @assert length(x[4]) == Base.parse(Int,x[3]) == 2 + vtree = t.id2vtree[x[2]] + t.nodes[x[1]] = StructMulNode(x[4]..., vtree) +end + +@rule sum_node(t::StructJpcParse,x) = begin + @assert length(x[4][1]) == length(x[4][2]) == Base.parse(Int,x[3]) + vtree = t.id2vtree[x[2]] + t.nodes[x[1]] = StructSumNode(x[4][1], x[4][2], vtree) +end + +function Base.parse(::Type{StructProbCircuit}, str::AbstractString, ::JpcFormat, id2vtree) + ast = Lerche.parse(jpc_parser, str) + Lerche.transform(StructJpcParse(id2vtree), ast) +end + +function Base.parse(::Type{StructProbCircuit}, strings, format::JpcVtreeFormat) + id2vtree = parse(Dict{String,Vtree}, strings[2], format[2]) + parse(StructProbCircuit, strings[1], format[1], id2vtree) +end + +Base.read(io::IO, ::Type{StructProbCircuit}, ::JpcFormat, id2vtree) = + parse(StructProbCircuit, read(io, String), JpcFormat(), id2vtree) + +function Base.read(ios::Tuple{IO,IO}, ::Type{StructProbCircuit}, ::JpcVtreeFormat) + circuit_str = read(ios[1], String) + vtree_str = read(ios[2], String) + parse(StructProbCircuit, (circuit_str,vtree_str), JpcVtreeFormat()) +end + +############################################## +# Write JPCs +############################################## + +const JPC_FORMAT = """c this file was saved by ProbabilisticCircuits.jl +c ids of jpc nodes start at 0 +c jpc nodes appear bottom-up, children before parents +c +c file syntax: +c jpc count-of-jpc-nodes +c L id-of-literal-jpc-node id-of-vtree literal +c P id-of-sum-jpc-node id-of-vtree number-of-children {child-id}+ +c S id-of-product-jpc-node id-of-vtree number-of-children {child-id}+ {log-probability}+ +c""" + +function Base.write(io::IO, circuit::ProbCircuit, ::JpcFormat, vtreeid::Function = (x -> 0)) + + labeling = label_nodes(circuit) + map!(x -> x-1, values(labeling)) # vtree nodes are 0-based indexed + + println(io, JPC_FORMAT) + println(io, "jpc $(num_nodes(circuit))") + foreach(circuit) do n + if isliteralgate(n) + println(io, "L $(labeling[n]) $(vtreeid(n)) $(literal(n))") + elseif isconstantgate(n) + @assert false + else + t = is⋀gate(n) ? "P" : "S" + print(io, "$t $(labeling[n]) $(vtreeid(n)) $(num_children(n))") + if is⋀gate(n) + for child in children(n) + print(io, " $(labeling[child])") + end + else + @assert is⋁gate(n) + for (child, logp) in zip(children(n), n.log_probs) + print(io, " $(labeling[child]) $logp") + end + end + println(io) + end + end + nothing +end + +function Base.write(ios::Tuple{IO,IO}, circuit::StructProbCircuit, format::JpcVtreeFormat) + vtree2id = write(ios[2], vtree(circuit), format[2]) + write(ios[1], circuit, format[1], n -> vtree2id[vtree(n)]) +end \ No newline at end of file diff --git a/src/LoadSave/plot.jl b/src/io/plot.jl similarity index 100% rename from src/LoadSave/plot.jl rename to src/io/plot.jl diff --git a/src/io/psdd_io.jl b/src/io/psdd_io.jl new file mode 100644 index 00000000..95db2918 --- /dev/null +++ b/src/io/psdd_io.jl @@ -0,0 +1,204 @@ +export zoo_psdd, zoo_psdd_file, + PsddFormat, PsddVtreeFormat + +struct PsddFormat <: FileFormat end + +const PsddVtreeFormat = Tuple{PsddFormat,VtreeFormat} +Tuple{PsddFormat,VtreeFormat}() = (PsddFormat(),VtreeFormat()) + +############################################## +# Read SDDs +############################################## + +zoo_psdd_file(name) = + artifact"circuit_model_zoo" * zoo_version * "/psdds/$name" + +""" + zoo_psdd(name) + +Loads PSDD file with given name from model zoo. See https://github.com/UCLA-StarAI/Circuit-Model-Zoo. +""" +zoo_psdd(name) = + read(zoo_psdd_file(name), ProbCircuit, PsddFormat()) + +const psdd_grammar = raw""" + start: _header (_NL node)+ _NL? + + _header : "psdd" (_WS INT)? + + node : "T" _WS INT _WS INT _WS INT _WS LOGPROB -> true_node + | "L" _WS INT _WS INT _WS SIGNED_INT _WS? -> literal_node + | "D" _WS INT _WS INT _WS INT _WS elems -> decision_node + + elems : elem (_WS elem)* + elem : INT _WS INT _WS LOGPROB + + %import common.INT + %import common.SIGNED_INT + %import common.SIGNED_NUMBER -> LOGPROB + %import common.WS_INLINE -> _WS + %import common.NEWLINE -> _NL + """ * dimacs_comments + +const psdd_parser = Lark(psdd_grammar) + +abstract type PsddParse <: JuiceTransformer end + +@rule start(t::PsddParse, x) = begin + x[end] +end + +@rule elem(t::PsddParse, x) = + [t.nodes[x[1]], t.nodes[x[2]], Base.parse(Float64,x[3])] + +@rule elems(t::PsddParse, x) = + Array(x) + +# parse unstructured +struct PlainPsddParse <: PsddParse + nodes::Dict{String,PlainProbCircuit} + PlainPsddParse() = new(Dict{String,PlainProbCircuit}()) +end + +@rule literal_node(t::PlainPsddParse, x) = + t.nodes[x[1]] = PlainProbLiteralNode(Base.parse(Lit,x[3])) + +@rule true_node(t::PlainPsddParse, x) = begin + litn = PlainProbLiteralNode(Base.parse(Lit, x[3])) + log_prob = Base.parse(Float64, x[4]) + log_probs = [log_prob, log1p(-exp(log_prob))] + t.nodes[x[1]] = PlainSumNode([litn, -litn], log_probs) +end + +@rule decision_node(t::PlainPsddParse,x) = begin + @assert length(x[4]) == Base.parse(Int,x[3]) + children = map(x[4]) do elem + PlainMulNode(elem[1:2]) + end + log_probs = map(e -> e[3], x[4]) + t.nodes[x[1]] = PlainSumNode(children, log_probs) +end + +function Base.parse(::Type{PlainProbCircuit}, str, ::PsddFormat) + ast = Lerche.parse(psdd_parser, str) + Lerche.transform(PlainPsddParse(), ast) +end + +Base.read(io::IO, ::Type{PlainProbCircuit}, ::PsddFormat) = + parse(PlainProbCircuit, read(io, String), PsddFormat()) + +# parse structured +struct StructPsddParse <: PsddParse + id2vtree::Dict{String,<:Vtree} + nodes::Dict{String,StructProbCircuit} + StructPsddParse(id2vtree) = + new(id2vtree,Dict{String,StructProbCircuit}()) +end + +@rule literal_node(t::StructPsddParse, x) = begin + lit = Base.parse(Lit,x[3]) + vtree = t.id2vtree[x[2]] + t.nodes[x[1]] = StructProbLiteralNode(lit, vtree) +end + +@rule true_node(t::StructPsddParse, x) = begin + vtree = t.id2vtree[x[2]] + litn = StructProbLiteralNode(Base.parse(Lit, x[3]), vtree) + log_prob = Base.parse(Float64, x[4]) + log_probs = [log_prob, log1p(-exp(log_prob))] + t.nodes[x[1]] = StructSumNode([litn, -litn], log_probs, vtree) +end + +@rule decision_node(t::StructPsddParse,x) = begin + @assert length(x[4]) == Base.parse(Int,x[3]) + vtree = t.id2vtree[x[2]] + children = map(x[4]) do elem + StructMulNode(elem[1], elem[2], vtree) + end + log_probs = map(e -> e[3], x[4]) + t.nodes[x[1]] = StructSumNode(children, log_probs, vtree) +end + +function Base.parse(::Type{StructProbCircuit}, str::AbstractString, ::PsddFormat, id2vtree) + ast = Lerche.parse(psdd_parser, str) + Lerche.transform(StructPsddParse(id2vtree), ast) +end + +function Base.parse(::Type{StructProbCircuit}, strings, format::PsddVtreeFormat) + id2vtree = parse(Dict{String,Vtree}, strings[2], format[2]) + parse(StructProbCircuit, strings[1], format[1], id2vtree) +end + +Base.read(io::IO, ::Type{StructProbCircuit}, ::PsddFormat, id2vtree) = + parse(StructProbCircuit, read(io, String), PsddFormat(), id2vtree) + +function Base.read(ios::Tuple{IO,IO}, ::Type{StructProbCircuit}, ::PsddVtreeFormat) + circuit_str = read(ios[1], String) + vtree_str = read(ios[2], String) + parse(StructProbCircuit, (circuit_str,vtree_str), PsddVtreeFormat()) +end + +############################################## +# Write PSDDs +############################################## + +const PSDD_FORMAT = """c this file was saved by ProbabilisticCircuits.jl +c ids of psdd nodes start at 0 +c psdd nodes appear bottom-up, children before parents +c +c file syntax: +c psdd count-of-sdd-nodes +c L id-of-literal-sdd-node id-of-vtree literal +c T id-of-trueNode-sdd-node id-of-vtree variable log(litProb) +c D id-of-decomposition-sdd-node id-of-vtree number-of-elements {id-of-prime id-of-sub log(elementProb)}* +c""" + +function Base.write(io::IO, pc::ProbCircuit, ::PsddFormat, vtree2id::Function = (x -> 0)) + + id = -1 + + println(io, PSDD_FORMAT) + println(io, "psdd $(sdd_num_nodes_leafs(pc))") + + f_con(_) = error("ProbCircuits have no constant nodes") + + f_lit(n) = begin + nid = id += 1 + println(io, "L $nid $(vtree2id(n)) $(literal(n))") + nid + end + + f_a(n, ids) = begin + if length(ids) != 2 + error("The PSDD file format requires multiplications/AND nodes to have exactly two inputs") + end + tuple(ids...) + end + + f_o(n, ids) = begin + nid = id += 1 + vtreeid = vtree2id(n) + if num_children(n) == 2 && all(isliteralgate, children(n)) + pos_child = literal(children(n)[1]) > 0 ? 1 : 2 + log_prob = n.log_probs[pos_child] + v = variable(children(n)[1]) + print(io, "T $nid $vtreeid $v $log_prob") + else + print(io, "D $nid $vtreeid $(length(ids))") + for (el, log_prob) in zip(ids, n.log_probs) + print(io, " $(el[1]) $(el[2]) $log_prob") + end + end + println(io) + nid + end + + foldup_aggregate(pc, f_con, f_lit, f_a, f_o, Union{Int, Tuple{Int,Int}}) + + nothing +end + +function Base.write(ios::Tuple{IO,IO}, pc::StructProbCircuit, format::PsddVtreeFormat) + vtree2id = write(ios[2], vtree(pc), format[2]) + write(ios[1], pc, format[1], i -> vtree2id[vtree(i)]) +end \ No newline at end of file diff --git a/src/logistic_nodes.jl b/src/logistic_nodes.jl deleted file mode 100644 index 2a7e78a9..00000000 --- a/src/logistic_nodes.jl +++ /dev/null @@ -1,106 +0,0 @@ -export - LogisticCircuit, - LogisticLeafNode, LogisticInnerNode, - LogisticLiteralNode, Logistic⋀Node, Logistic⋁Node, - num_classes, num_parameters_per_class - -##################### -# Infrastructure for logistic circuit nodes -##################### - -"Root of the logistic circuit node hierarchy" -abstract type LogisticCircuit <: LogicCircuit end - -""" -A logistic leaf node -""" -abstract type LogisticLeafNode <: LogisticCircuit end - -""" -A logistic inner node -""" -abstract type LogisticInnerNode <: LogisticCircuit end - -""" -A logistic literal node -""" -struct LogisticLiteralNode <: LogisticLeafNode - literal::Lit -end - -""" -A logistic conjunction node (And node) -""" -mutable struct Logistic⋀Node <: LogisticInnerNode - children::Vector{<:LogisticCircuit} - Logistic⋀Node(children) = begin - new(convert(Vector{LogisticCircuit}, children)) - end -end - -""" -A logistic disjunction node (Or node) -""" -mutable struct Logistic⋁Node <: LogisticInnerNode - children::Vector{<:LogisticCircuit} - thetas::Matrix{Float32} - Logistic⋁Node(children, class::Int) = begin - new(convert(Vector{LogisticCircuit}, children), init_array(Float32, length(children), class)) - end -end - -##################### -# traits -##################### - -import LogicCircuits.GateType # make available for extension -@inline GateType(::Type{<:LogisticLiteralNode}) = LiteralGate() -@inline GateType(::Type{<:Logistic⋀Node}) = ⋀Gate() -@inline GateType(::Type{<:Logistic⋁Node}) = ⋁Gate() - -##################### -# methods -##################### - -import LogicCircuits: children # make available for extension -@inline children(n::LogisticInnerNode) = n.children -@inline num_classes(n::Logistic⋁Node) = size(n.thetas)[2] - -@inline num_parameters(c::LogisticCircuit) = sum(n -> num_children(n) * num_classes(n), ⋁_nodes(c)) -@inline num_parameters_per_class(c::LogisticCircuit) = sum(n -> num_children(n), ⋁_nodes(c)) - -"Get the parameters associated with a or node" -params(n::Logistic⋁Node) = n.thetas - -##################### -# constructors and conversions -##################### - -function LogisticCircuit(circuit::LogicCircuit, classes::Int) - f_con(n) = error("Cannot construct a logistic circuit from constant leafs: first smooth and remove unsatisfiable branches.") - f_lit(n) = compile(LogisticCircuit, literal(n)) - f_a(n, cn) = Logistic⋀Node(cn) - f_o(n, cn) = Logistic⋁Node(cn, classes) - foldup_aggregate(circuit, f_con, f_lit, f_a, f_o, LogisticCircuit) -end - -compile(::Type{<:LogisticCircuit}, l::Lit) = - LogisticLiteralNode(l) - -function compile(::Type{<:LogisticCircuit}, classes, circuit::LogicCircuit) - LogisticCircuit(circuit, classes) -end - -import LogicCircuits: fully_factorized_circuit #extend - -function fully_factorized_circuit(::Type{<:LogisticCircuit}, n::Int; classes::Int) - ff_logic_circuit = fully_factorized_circuit(PlainLogicCircuit, n) - compile(LogisticCircuit, classes, ff_logic_circuit) -end - -function check_parameter_integrity(circuit::LogisticCircuit) - for node in or_nodes(circuit) - @assert all(θ -> !isnan(θ), node.thetas) "There is a NaN in one of the log_probs" - end - true -end \ No newline at end of file diff --git a/src/param_bit_circuit.jl b/src/param_bit_circuit.jl index 553f79ae..7eba635c 100644 --- a/src/param_bit_circuit.jl +++ b/src/param_bit_circuit.jl @@ -22,25 +22,6 @@ function ParamBitCircuit(pc::ProbCircuit, data) ParamBitCircuit(bc, logprobs) end -function ParamBitCircuit(lc::LogisticCircuit, nc, data) - thetas::Vector{Vector{Float32}} = Vector{Vector{Float32}}() - on_decision(n, cs, layer_id, decision_id, first_element, last_element) = begin - if isnothing(n) - # @assert first_element == last_element - push!(thetas, zeros(Float32, nc)) - # println("here, some node is not part of the logistic circuit") - else - # @assert last_element - first_element + 1 == size(n.thetas, 1) - # @assert size(n.thetas, 2) == nc - for theta in eachrow(n.thetas) - push!(thetas, theta) - end - end - end - bc = BitCircuit(lc, data; on_decision) - ParamBitCircuit(bc, permutedims(hcat(thetas...), (2, 1))) -end - function ParamBitCircuit(spc::SharedProbCircuit, data; component_idx) logprobs::Vector{Float64} = Vector{Float64}() sizehint!(logprobs, num_edges(spc)) diff --git a/src/param_bit_circuit_pair.jl b/src/param_bit_circuit_pair.jl deleted file mode 100644 index b96f669f..00000000 --- a/src/param_bit_circuit_pair.jl +++ /dev/null @@ -1,315 +0,0 @@ -using CUDA - -export NodePairIds, - BitCircuitPair, - SumNodePairIds, - ProdNodePairIds, - BitCircuitPair, - ParamBitCircuitPair - -const NodeId = Int32 - -"Integer identifier for a circuit pair node" -const NodePairId = Int32 - -"The BitCircuitPair ids associated with a node" -abstract type NodePairIds end - -mutable struct SumNodePairIds <: NodePairIds - layer_id::NodePairId - node_id::NodePairId -end - -mutable struct ProdNodePairIds <: NodePairIds - layer_id::NodePairId - # Assuming both Product nodes have two children - left_left_id::NodePairId - right_right_id::NodePairId - - ProdNodePairIds(ll::SumNodePairIds, rr::SumNodePairIds) = begin - l = max(ll.layer_id, rr.layer_id) - new(l, ll.node_id, rr.node_id) - end -end - - -const NODES_LENGTH = 6 -const ELEMENTS_LENGTH = 3 - -""" -A bit circuit pair is a low-level representation of pairs of nodes traversed for a pair of probabilistic circuit logical circuit structure. -For example, this is used for taking expectation of a Logistic/Regression circuit w.r.t. a probabilistic circuit. - -The wiring of the circuit is captured by two matrices: nodes and elements. - * Nodes are either leafs (input) or pair of Sum nodes - * Elements are Pair of Product nodes - * In addition, there is a vector of layers, where each layer is a list of node ids. - Layer 1 is the leaf/input layer. Layer end is the circuit root. - * And there is a vector of parents, pointing to element id parents of decision nodes. - -Nodes are represented as a 6xN matrix where - * nodes[1,:] is the first child pair id - * nodes[2,:] is the last child pair id - * nodes[3,:] is the first parent index - * nodes[4,:] is the last parent index - * nodes[5,:] is the id of corresponding node from the PC (first circuit) - * nodes[6,:] is the id of corresponding node from the LC (first circuit) - -Elements are represented by a 3xE matrix, where - * elements[1,:] is the Product pair node id, - * elements[2,:] is the (left,left) child node id - * elements[3,:] is the (right right) child node id - - Elements belonging to node pair `i` are `elements[:, nodes[1,i]:nodes[2,i]]` - -Parents are represented as a one dimentional array - Parents belonging to node pair `i` are `parents[nodes[3,i]:nodes[4,i]]` - -""" -struct BitCircuitPair{V,M} - layers::Vector{V} - nodes::M - elements::M - parents::V -end - -struct ParamBitCircuitPair{V,M, WPC, WLC} - pc_bit::BitCircuit{V,M} - lc_bit::BitCircuit{V,M} - pair_bit::BitCircuitPair{V,M} - pc_params::WPC - lc_params::WLC -end - -function ParamBitCircuitPair(pc::ProbCircuit, lc::LogisticCircuit; Float=Float32) - pc_thetas::Vector{Float} = Vector{Float}() - lc_thetas::Vector{Vector{Float}} = Vector{Vector{Float}}() - - sizehint!(pc_thetas, num_edges(pc)) - - pc_cache = Dict{Node, NodeId}() # only for sum nodes - lc_cache = Dict{Node, NodeId}() # only for sum nodes - - lc_num_classes = num_classes(lc); - pc_on_decision(n, cs, layer_id, decision_id, first_element, last_element) = begin - if isnothing(n) # this decision node is not part of the PC - push!(pc_thetas, zero(Float)) - else - pc_cache[n] = decision_id - append!(pc_thetas, n.log_probs) - end - end - - lc_on_decision(m, cs, layer_id, decision_id, first_element, last_element) = begin - if isnothing(m) - throw("here, some node is not part of the logistic circuit") - # push!(lc_thetas, zeros(Float32, lc_num_classes)) - else - lc_cache[m] = decision_id - for theta in eachrow(m.thetas) - push!(lc_thetas, theta) - end - end - end - - pbc_callback(n, m, results, layer_id, last_dec_id, first_el_id, last_el_id) = begin - nothing - end - - pc_bit = BitCircuit(pc, num_variables(pc); on_decision=pc_on_decision) - lc_bit = BitCircuit(lc, num_variables(pc); on_decision=lc_on_decision) - bcp = BitCircuitPair(pc, lc; on_sum_callback = pbc_callback, pc_cache, lc_cache) - - lc_thetas_reshaped = permutedims(hcat(lc_thetas...), (2, 1)) - ParamBitCircuitPair(pc_bit, lc_bit, bcp, pc_thetas, lc_thetas_reshaped) -end - - -function BitCircuitPair(pc::ProbCircuit, lc::LogisticCircuit; - on_sum_callback=noop, - pc_cache=nothing, lc_cache=nothing, property_check=false) - - if property_check - @assert num_variables(pc) == num_variables(lc) - vtree1::Vtree = infer_vtree(pc); - @assert respects_vtree(lc, vtree1); - end - - num_features = num_variables(pc) - num_leafs = 4*num_features - layers::Vector{Vector{NodePairId}} = Vector{NodePairId}[collect(1:num_leafs)] - nodes::Vector{NodePairId} = zeros(NodePairId, NODES_LENGTH*num_leafs) - elements::Vector{NodePairId} = NodePairId[] - parents::Vector{Vector{NodePairId}} = Vector{NodePairId}[NodePairId[] for i = 1:num_leafs] - last_dec_id::NodePairId = num_leafs - last_el_id::NodePairId = zero(NodePairId) - - cache = Dict{Pair{Node, Node}, NodePairIds}() - - func(n,m) = begin - throw("Unsupported pair of nodes!! $n, $m") - end - - function func(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, - m::LogisticLiteralNode)::NodePairIds - get!(cache, Pair(n, m)) do - @assert variable(n) == variable(m) - if ispositive(n) && ispositive(m) - SumNodePairIds(one(NodePairId), NodePairId(variable(n))) - elseif !ispositive(n) && ispositive(m) - SumNodePairIds(one(NodePairId), NodePairId(variable(n) + num_features)) - elseif ispositive(n) && !ispositive(m) - SumNodePairIds(one(NodePairId), NodePairId(variable(n) + 2*num_features)) - elseif !ispositive(n) && !ispositive(m) - SumNodePairIds(one(NodePairId), NodePairId(variable(n) + 3*num_features)) - end - end - end - - function func(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, - m::Logistic⋁Node)::NodePairIds - - @assert num_children(m) == 1 - func(n, children(m)[1]) - end - - function func(n::Union{PlainMulNode, StructMulNode}, - m::Logistic⋀Node)::NodePairIds - - get!(cache, Pair(n, m)) do - @assert num_children(n) == 2 - @assert num_children(m) == 2 - - ll = func(children(n)[1], children(m)[1]) - rr = func(children(n)[2], children(m)[2]) - ProdNodePairIds(ll, rr) - end - end - - - function func(n::Union{PlainSumNode, StructSumNode}, - m::Logistic⋁Node)::NodePairIds - - get!(cache, Pair(n, m)) do - # First process the children - results::Vector{Vector{NodePairIds}} = NodePairIds[] - for i in 1:num_children(n) - push!(results, NodePairIds[]) - for j in 1:num_children(m) - push!(results[i], func(children(n)[i], children(m)[j])) - end - end - - # Do not move this, otherwise el_ids would be wrong. - first_el_id::NodePairId = last_el_id + one(NodePairId) - layer_id::NodePairId = zero(NodePairId) - push!(parents, NodePairId[]) - - # Add processed children to bitcircuitpair - for i in 1:num_children(n) - for j in 1:num_children(m) - cur = results[i][j]; - layer_id = max(layer_id, cur.layer_id) - last_el_id += one(NodePairId) - - if typeof(cur) == ProdNodePairIds - push!(elements, last_dec_id, cur.left_left_id, cur.right_right_id); - @inbounds push!(parents[cur.left_left_id], last_el_id) - @inbounds push!(parents[cur.right_right_id], last_el_id) - else - !property_check && @assert children(m)[1] isa LogisticLeafNode - push!(elements, last_dec_id, cur.node_id, cur.node_id); - @inbounds push!(parents[cur.node_id], last_el_id) - @inbounds push!(parents[cur.node_id], last_el_id) - end - end - end - layer_id += one(NodePairId) - length(layers) < layer_id && push!(layers, NodePairId[]) - - last_dec_id::NodePairId += one(NodePairId) - - PC_ID = pc_cache !== nothing ? pc_cache[n] : zero(NodePairId) - LC_ID = lc_cache !== nothing ? lc_cache[m] : zero(NodePairId) - - push!(nodes, first_el_id, last_el_id, zero(NodePairId), zero(NodePairId), PC_ID, LC_ID) - push!(layers[layer_id], last_dec_id) - on_sum_callback(n, m, results, layer_id, last_dec_id, first_el_id, last_el_id) - SumNodePairIds(layer_id, last_dec_id) - end - end - - # Call on roots - func(pc, lc) - - nodes_m = reshape(nodes, NODES_LENGTH, :) - elements_m = reshape(elements, ELEMENTS_LENGTH, :) - parents_m = Vector{NodePairId}(undef, size(elements_m,2)*2) - - last_parent = zero(NodePairId)[] - @assert last_dec_id == size(nodes_m,2) == size(parents,1) - @assert sum(length, parents) == length(parents_m) - for i in 1:last_dec_id-1 - if !isempty(parents[i]) - nodes_m[3,i] = last_parent + one(NodePairId) - parents_m[last_parent + one(NodePairId):last_parent + length(parents[i])] .= parents[i] - last_parent += length(parents[i]) - nodes_m[4,i] = last_parent - else - !property_check && @assert i <= num_leafs "Only root and leaf nodes can have no parents: $i <= $num_leafs" - end - end - return BitCircuitPair(layers, nodes_m, elements_m, parents_m) -end - - -###################### -## Helper Functions ## -###################### - -import LogicCircuits: num_nodes, num_elements, num_features, num_leafs, nodes, elements -import LogicCircuits: to_gpu, to_cpu, isgpu #extend - -# most of this are identical with bitcircuit maybe make the BitCircuitPair a subtype of BitCircuit? -nodes(c::BitCircuitPair) = c.nodes -elements(c::BitCircuitPair) = c.elements - -num_nodes(c::BitCircuitPair) = size(c.nodes, 2) - -num_elements(c::BitCircuitPair) = size(c.elements, 2) - -to_gpu(c::BitCircuitPair) = - BitCircuitPair(map(to_gpu, c.layers), to_gpu(c.nodes), to_gpu(c.elements), to_gpu(c.parents)) - -to_cpu(c::BitCircuitPair) = - BitCircuitPair(map(to_cpu, c.layers), to_cpu(c.nodes), to_cpu(c.elements), to_cpu(c.parents)) - -isgpu(c::BitCircuitPair{<:CuArray,<:CuArray}) = true -isgpu(c::BitCircuitPair{<:Array,<:Array}) = false - - -############################# -## Param Helper functions ### -############################# - -pc_params(c::ParamBitCircuitPair) = c.pc_params -lc_params(c::ParamBitCircuitPair) = c.lc_params - -num_nodes(c::ParamBitCircuitPair) = num_nodes(c.pair_bit) -num_elements(c::ParamBitCircuitPair) = num_elements(c.pair_bit) -num_features(c::ParamBitCircuitPair) = num_features(c.pair_bit) -num_leafs(c::ParamBitCircuitPair) = num_leafs(c.pair_bit) - -nodes(c::ParamBitCircuitPair) = nodes(c.pair_bit) -elements(c::ParamBitCircuitPair) = elements(c.pair_bit) - - -to_gpu(c::ParamBitCircuitPair) = - ParamBitCircuitPair(to_gpu(c.pc_bit), to_gpu(c.lc_bit), - to_gpu(c.pair_bit), to_gpu(c.pc_params), to_gpu(c.lc_params)) - -to_cpu(c::ParamBitCircuitPair) = - ParamBitCircuitPair(to_cpu(c.pc_bit), to_cpu(c.lc_bit), to_cpu(c.pair_bit), to_cpu(c.pc_params), to_cpu(c.lc_params)) - -isgpu(c::ParamBitCircuitPair) = - isgpu(c.pc_bit) && isgpu(c.lc_bit) && isgpu(c.pair_bit) && isgpu(c.pc_params) && isgpu(c.lc_params) diff --git a/src/parameters.jl b/src/parameters.jl index 4f1a97b5..a5b5da51 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -302,9 +302,9 @@ function estimate_parameters_gpu(bc::BitCircuit, data, pseudocount; weights = no if isbatched(data) v, f = nothing, nothing - if weights != nothing + if weights !== nothing map(zip(data, weights)) do (d, w) - if w != nothing + if w !== nothing w = to_gpu(w) end v, f = satisfies_flows(to_gpu(bc), to_gpu(d), v, f; on_node = on_node, on_edge = on_edge, weights = w) @@ -315,7 +315,7 @@ function estimate_parameters_gpu(bc::BitCircuit, data, pseudocount; weights = no end end else - if weights != nothing + if weights !== nothing weights = to_gpu(weights) end v, f = satisfies_flows(to_gpu(bc), to_gpu(data); on_node = on_node, on_edge = on_edge, weights = weights) diff --git a/src/plain_prob_nodes.jl b/src/plain_prob_nodes.jl index eec5cb51..8f509487 100644 --- a/src/plain_prob_nodes.jl +++ b/src/plain_prob_nodes.jl @@ -32,11 +32,11 @@ end mutable struct PlainSumNode <: PlainProbInnerNode children::Vector{PlainProbCircuit} log_probs::Vector{Float64} - PlainSumNode(c) = begin - new(c, log.(ones(Float64, length(c)) / length(c))) - end end +PlainSumNode(c) = + PlainSumNode(c, log.(ones(Float64, length(c)) / length(c))) + ##################### # traits ##################### diff --git a/src/queries/expectation_bit.jl b/src/queries/expectation_bit.jl deleted file mode 100644 index 7400dac9..00000000 --- a/src/queries/expectation_bit.jl +++ /dev/null @@ -1,299 +0,0 @@ -using CUDA: CUDA, @cuda -using DataFrames: DataFrame -using LoopVectorization: @avx - -export ExpectationBit - - -""" - ExpectationBit(pc::ProbCircuit, lc::LogisticCircuit, data) - ExpectationBit(pbc::ParamBitCircuitPair, pc::ProbCircuit, lc::LogisticCircuit, data) - -Compute Expected Prediction of a Logistic/Regression Circuit w.r.t to a ProbabilistcCircuit. -Uses the BitCircuitPair implementation, which enables parrallism on both CPU/GPU. -If the data is on gpu the gpu version will get called (repectively for cpu.) - -Missing values should be denoted by missing -See: On Tractable Computation of Expected Predictions [arxiv.org/abs/1910.02182](https://arxiv.org/abs/1910.02182) -""" -function ExpectationBit(pc::ProbCircuit, lc::LogisticCircuit, data; return_aux = false) - # children(lc)[1]: Currently root of LC's are for bias values and not a real sum node, so should be skipped. - pbc = ParamBitCircuitPair(pc, children(lc)[1]); - pbc = same_device(pbc, data) - ExpectationBit(pbc, pc, lc, data; return_aux); -end - -function ExpectationBit(pbc::ParamBitCircuitPair, pc::ProbCircuit, lc::LogisticCircuit, data, reuse_f=nothing, reuse_g=nothing; return_aux=true) - # 1. Get probability of each observation - parambit::ParamBitCircuit = ParamBitCircuit(pbc.pc_bit, pbc.pc_params); - log_likelihoods = MAR(parambit, data); - p_observed = exp.( log_likelihoods ) - - # 2. Expectation w.r.t. P(x_m, x_o) - fvalues, gvalues = init_expectations(pbc, data, reuse_f, reuse_g, size(pbc.pair_bit.nodes)[2], num_classes(lc)) - expectation_layers(pbc, fvalues, gvalues) - - # 3. Expectation w.r.t P(x_m | x_o) - results = gvalues[:, end,:] ./ p_observed - - # # 4. Add Bias terms - biases = if isgpu(results) - biases = to_gpu(lc.thetas) - else - biases = lc.thetas - end - results .+= biases - - if return_aux - results, fvalues, gvalues, pbc - else - results - end -end - -""" - init_expectations(circuit::ParamBitCircuitPair, data, reuse_f, reuse_g, nodes_num, classes_num; Float = Float32) - - Initialized the input layer for `fvalues` and `gvalues`. - To reduce memory allocation can pass `reuse_f` and `reuse_g` to get reused (very useful during batching). -""" -function init_expectations(circuit::ParamBitCircuitPair, data, reuse_f, reuse_g, nodes_num, classes_num; Float = Float32) - flowtype = isgpu(data) ? CuArray{Float} : Array{Float} - @assert isgpu(data) == isgpu(circuit) - - fvalues = similar!(reuse_f, flowtype, num_examples(data), nodes_num) - fgvalues = similar!(reuse_g, flowtype, num_examples(data), nodes_num, classes_num) - nfeatures = num_features(data) - - # TODO might not need to zero everything out, but was giving wrong answer - # on gpu reuse if not; - fvalues[:,:] .= zero(Float) - fgvalues[:,:,:] .= zero(Float) - - PARS = circuit.lc_params - for var=1:nfeatures - # even if data is on gpu this might become on cpu (eg when data is dataframe) - temp_cpu = .!isequal.(data[:, var], 0) .* one(Float) - fvalues[:, var] .= same_device(temp_cpu, data) - - temp_cpu .= .!isequal.(data[:, var], 1) .* one(Float) - fvalues[:, var + 3*nfeatures] .= same_device(temp_cpu, data) - end - - - if isgpu(circuit) - examples_count = size(fvalues, 1) - num_threads = balance_threads(examples_count, nfeatures/8, 8) - num_blocks = (ceil(Int, examples_count/num_threads[1]), - ceil(Int, nfeatures/num_threads[2])) - # CUDA.@sync begin - # @cuda threads=num_threads blocks=num_blocks init_expectations_cuda_part1(nfeatures, data, fvalues) - # end - - CUDA.@sync begin - @cuda threads=num_threads blocks=num_blocks init_expectations_cuda_part2(nfeatures, classes_num, - circuit.lc_bit.nodes, circuit.lc_bit.parents, - PARS, fvalues, fgvalues) - end - else - for var=1:nfeatures - ind2 = var + 3*nfeatures - # find the index of correct paramter to grab from LogisticCircuit - p_ind = circuit.lc_bit.parents[circuit.lc_bit.nodes[3, 2+var]] - p_ind2 = circuit.lc_bit.parents[circuit.lc_bit.nodes[3, 2+var+nfeatures]] - - for cc=1:classes_num - fgvalues[:, var, cc] .= (fvalues[:, var] .* PARS[p_ind, cc]) - fgvalues[:, ind2, cc] .= (fvalues[:, ind2] .* PARS[p_ind2, cc]) - end - end - end - return fvalues, fgvalues -end - - -function init_expectations_cuda_part2(nfeatures, classes_num, lc_bit_nodes, lc_bit_pars, PARS, fvalues, fgvalues) - index_x = (blockIdx().x - 1) * blockDim().x + threadIdx().x - index_y = (blockIdx().y - 1) * blockDim().y + threadIdx().y - stride_x = blockDim().x * gridDim().x - stride_y = blockDim().y * gridDim().y - - for var = index_x:stride_x:nfeatures - @inbounds ind2 = var + 3*nfeatures - @inbounds idx1 = lc_bit_nodes[3, 2+var] - @inbounds idx2 = lc_bit_nodes[3, 2+var+nfeatures] - - @inbounds p_ind = lc_bit_pars[idx1] - @inbounds p_ind2 = lc_bit_pars[idx2] - for i = index_y:stride_y:size(fvalues, 1) - for cc=1:classes_num - fgvalues[i, var, cc] = (fvalues[i, var] * PARS[p_ind, cc]) - fgvalues[i, ind2, cc] = (fvalues[i, ind2] * PARS[p_ind2, cc]) - end - end - end -end - - - -function expectation_layers(circuit, fvalues::Array{<:AbstractFloat,2}, fgvalues::Array{<:AbstractFloat,3}) - bcp::BitCircuitPair = circuit.pair_bit - pc::BitCircuit = circuit.pc_bit - lc::BitCircuit = circuit.lc_bit - els = bcp.elements - pc_pars = exp.(circuit.pc_params) - lc_pars = circuit.lc_params - - for layer in bcp.layers[2:end] - Threads.@threads for dec_id in layer - - id_begin = @inbounds bcp.nodes[1, dec_id] - id_end = @inbounds bcp.nodes[2, dec_id] - pc_id = @inbounds bcp.nodes[5, dec_id] - lc_id = @inbounds bcp.nodes[6, dec_id] - # println("!!, $dec_id, $id_begin, $id_end, $pc_id, $lc_id") - - @inbounds pc_childs = 1 + pc.nodes[2, pc_id] - pc.nodes[1, pc_id] - @inbounds lc_childs = 1 + lc.nodes[2, lc_id] - lc.nodes[1, lc_id] - for A = 1 : pc_childs - for B = 1 : lc_childs - shift = (A-1) * lc_childs + B - Z = id_begin + shift - 1 - @inbounds PCZ = pc.nodes[1, pc_id] + A - 1 - @inbounds LCZ = lc.nodes[1, lc_id] + B - 1 - - ## Compute fvalues[:, dec_id] - # @inbounds @fastmath @avx fvalues[:, dec_id] .+= pc_pars[PCZ] .* - # fvalues[:, els[2,Z]] .* - # fvalues[:, els[3,Z]] - - # The for loops are faster, cause much less memory needed - # @avx throws error, not sure why - for j=1:size(fvalues,1) - @inbounds fvalues[j, dec_id] += pc_pars[PCZ] * fvalues[j, els[2,Z]] * fvalues[j, els[3,Z]] - end - - - ## Compute fgvalues[:, dec_id, :] - if els[3,Z] == els[2,Z] - # Special case (end in sum nodes) - @inbounds @fastmath @views @avx fgvalues[:, dec_id, :] .= - transpose(lc_pars[LCZ,:]) .* - fvalues[:, dec_id] - else - # @inbounds @fastmath @avx fgvalues[:, dec_id, :] .+= - # pc_pars[PCZ] .* - # transpose(lc_pars[LCZ,:]) .* - # (fvalues[:, els[2,Z]] .* fvalues[:, els[3,Z]]) - - # @inbounds @fastmath @avx fgvalues[:, dec_id, :] .+= - # pc_pars[PCZ] .* - # ((fvalues[:, els[3,Z]] .* fgvalues[:, els[2,Z], :]) .+ - # (fvalues[:, els[2,Z]] .* fgvalues[:, els[3,Z], :])) - - # The for loops are faster, cause much less memory needed - # @avx throws error, not sure why - @simd for j=1:size(fgvalues,1) - @simd for nc=1:size(fgvalues, 3) - @inbounds @fastmath fgvalues[j, dec_id, nc] += - pc_pars[PCZ] * - lc_pars[LCZ,nc] * - (fvalues[j, els[2,Z]] * fvalues[j, els[3,Z]]) - - @inbounds @fastmath fgvalues[j, dec_id, nc] += - pc_pars[PCZ] * - ((fvalues[j, els[3,Z]] * fgvalues[j, els[2,Z], nc]) + - (fvalues[j, els[2,Z]] * fgvalues[j, els[3,Z], nc])) - end - end - end - - end # B - end # A - - end # dec_id - end # layers - -end - -function expectation_layers(circuit::ParamBitCircuitPair, - fvalues::CuArray, fgvalues::CuArray; - dec_per_thread = 8, log2_threads_per_block = 8) - - bcp = circuit.pair_bit - pc_pars = exp.(circuit.pc_params) - - CUDA.@sync for layer in bcp.layers[2:end] - num_examples = size(fvalues, 1) - num_decision_sets = length(layer)/dec_per_thread - num_threads = balance_threads(num_examples, num_decision_sets, log2_threads_per_block) - num_blocks = (ceil(Int, num_examples/num_threads[1]), - ceil(Int, num_decision_sets/num_threads[2])) - - @cuda threads=num_threads blocks=num_blocks expectation_layers_cuda(layer, bcp.nodes, bcp.elements, - circuit.pc_bit.nodes, circuit.lc_bit.nodes, - pc_pars, circuit.lc_params, fvalues, fgvalues) - end - return nothing -end - -function expectation_layers_cuda(layer, nodes, els, - pc_nodes, lc_nodes, - pc_pars, lc_pars, fvalues, fgvalues) - - num_classes = size(lc_pars, 2) - - index_x = (blockIdx().x - 1) * blockDim().x + threadIdx().x - index_y = (blockIdx().y - 1) * blockDim().y + threadIdx().y - stride_x = blockDim().x * gridDim().x - stride_y = blockDim().y * gridDim().y - for j = index_x:stride_x:size(fvalues,1) - for i = index_y:stride_y:length(layer) - @inbounds dec_id = layer[i] - @inbounds id_begin = nodes[1, dec_id] - @inbounds id_end = nodes[2, dec_id] - @inbounds pc_id = nodes[5, dec_id] - @inbounds lc_id = nodes[6, dec_id] - @inbounds pc_childs = 1 + pc_nodes[2, pc_id] - pc_nodes[1, pc_id] - @inbounds lc_childs = 1 + lc_nodes[2, lc_id] - lc_nodes[1, lc_id] - - for A = 1 : pc_childs - for B = 1 : lc_childs - shift = (A-1) * lc_childs + B - Z = id_begin + shift - 1 - @inbounds PCZ = pc_nodes[1, pc_id] + A - 1 - @inbounds LCZ = lc_nodes[1, lc_id] + B - 1 - - ## Compute fvalues[:, dec_id] - fvalues[j, dec_id] += pc_pars[PCZ] * - fvalues[j, els[2,Z]] * - fvalues[j, els[3,Z]] - - ## Compute fgvalues[:, dec_id, :] - if els[3,Z] == els[2,Z] - # Special case (end in sum nodes) - for class=1:num_classes - @inbounds fgvalues[j, dec_id, class] = - lc_pars[LCZ,class] * - fvalues[j, dec_id] - end - else - for class=1:num_classes - @inbounds fgvalues[j, dec_id, class] += - pc_pars[PCZ] * - lc_pars[LCZ,class] * - (fvalues[j, els[2,Z]] * fvalues[j, els[3,Z]]) - - @inbounds fgvalues[j, dec_id, class] += - pc_pars[PCZ] * - ((fvalues[j, els[3,Z]] * fgvalues[j, els[2,Z], class]) + - (fvalues[j, els[2,Z]] * fgvalues[j, els[3,Z], class])) - - end - end - end # B - end # A - end # i - end # j - return nothing -end \ No newline at end of file diff --git a/src/queries/expectation_graph.jl b/src/queries/expectation_graph.jl deleted file mode 100644 index a8bc63cc..00000000 --- a/src/queries/expectation_graph.jl +++ /dev/null @@ -1,191 +0,0 @@ -export UpExpFlow, ExpFlowCircuit, exp_pass_up, ExpectationUpward - -##################### -# Expectation Flow circuits -# For use of algorithms depending on pairs of nodes of two circuits -##################### - -"A expectation circuit node that has pair of origins of type PC and type LC" -abstract type ExpFlowNode{F} end - -const ExpFlowCircuit{O} = Vector{<:ExpFlowNode{<:O}} - -struct UpExpFlow{F} <: ExpFlowNode{F} - p_origin::ProbCircuit - f_origin::LogisticCircuit - children::Vector{<:ExpFlowNode{<:F}} - f::F - fg::F -end - -import LogicCircuits: children -@inline children(x::UpExpFlow) = x.children - -""" -Expected Prediction of LC w.r.t PC. -This implementation uses the computation graph approach. -""" -function ExpectationUpward(pc::ProbCircuit, lc::LogisticCircuit, data) - # 1. Get probability of each observation - log_likelihoods = marginal(pc, data) - p_observed = exp.( log_likelihoods ) - - # 2. Expectation w.r.t. P(x_m, x_o) - exps_flow = exp_pass_up(pc, lc, data) - results_unnormalized = exps_flow[end].fg - - # 3. Expectation w.r.t P(x_m | x_o) - results = transpose(results_unnormalized) ./ p_observed - - # 4. Add Bias terms - biases = lc.thetas - results .+= biases - - results, exps_flow -end - - -""" -Construct a upward expectation flow circuit from a given pair of PC and LC circuits -Note that its assuming the two circuits share the same vtree -""" -function ExpFlowCircuit(pc::ProbCircuit, lc::LogisticCircuit, batch_size::Int, ::Type{El}) where El - F = Array{El, 2} - fmem = () -> zeros(El, 1, batch_size) #Vector{El}(undef, batch_size) #init_array(El, batch_size) # note: fmem's return type will determine type of all UpFlows in the circuit (should be El) - fgmem = () -> zeros(El, num_classes(lc), batch_size) - - root_pc = pc - root_lc = children(lc)[1] - - cache = Dict{Pair{Node, Node}, ExpFlowNode}() - sizehint!(cache, (num_nodes(pc) + num_nodes(lc))*10) - expFlowCircuit = Vector{ExpFlowNode}() - - function ExpflowTraverse(n::PlainSumNode, m::Logistic⋁Node) - get!(cache, Pair(n, m)) do - ch = [ ExpflowTraverse(i, j) for i in children(n) for j in children(m)] - node = UpExpFlow{F}(n, m, ch, fmem(), fgmem()) - push!(expFlowCircuit, node) - return node - end - end - function ExpflowTraverse(n::PlainMulNode, m::Logistic⋀Node) - get!(cache, Pair(n, m)) do - ch = [ ExpflowTraverse(z[1], z[2]) for z in zip(children(n), children(m)) ] - node = UpExpFlow{F}(n, m, ch, fmem(), fgmem()) - push!(expFlowCircuit, node) - return node - end - end - function ExpflowTraverse(n::PlainProbLiteralNode, m::Logistic⋁Node) - get!(cache, Pair(n, m)) do - ch = Vector{ExpFlowNode{F}}() # TODO - node = UpExpFlow{F}(n, m, ch, fmem(), fgmem()) - push!(expFlowCircuit, node) - return node - end - end - function ExpflowTraverse(n::PlainProbLiteralNode, m::LogisticLiteralNode) - get!(cache, Pair(n, m)) do - ch = Vector{ExpFlowNode{F}}() # TODO - node = UpExpFlow{F}(n, m, ch, fmem(), fgmem()) - push!(expFlowCircuit, node) - return node - end - end - - ExpflowTraverse(root_pc, root_lc) - expFlowCircuit -end - -function exp_pass_up(pc::ProbCircuit, lc::LogisticCircuit, data) - expFlowCircuit = ExpFlowCircuit(pc, lc, num_examples(data), Float32); - for n in expFlowCircuit - exp_pass_up_node(n, data) - end - expFlowCircuit -end - -function exp_pass_up(fc::ExpFlowCircuit, data) - #TODO write resize_flows similar to flow_circuits - # and give as input the expFlowCircuit instead - #expFlowCircuit = ExpFlowCircuit(pc, lc, num_examples(data), Float64); - for n in fc - exp_pass_up_node(n, data) - end -end - -function exp_pass_up_node(node::ExpFlowNode{E}, data) where E - pType = typeof(node.p_origin) - fType = typeof(node.f_origin) - - if node.p_origin isa PlainSumNode && node.f_origin isa Logistic⋁Node - # pthetas = [exp(node.p_origin.log_probs[i]) - # for i in 1:length(children(node.p_origin)) for j in 1:length(children(node.f_origin))] - # fthetas = [node.f_origin.thetas[j,:] - # for i in 1:length(node.p_origin.children) for j in 1:length(node.f_origin.children)] - - # node.f .= 0.0 - # node.fg .= 0.0 - # for z = 1:length(children(node)) - # node.f .+= pthetas[z] .* children(node)[z].f - # node.fg .+= (pthetas[z] .* fthetas[z]) .* children(node)[z].f - # node.fg .+= pthetas[z] .* children(node)[z].fg - # end - - node.f .= 0.0 - node.fg .= 0.0 - N = length(children(node.p_origin)) - M = length(children(node.f_origin)) - @inline pthetas(i) = exp(node.p_origin.log_probs[i]) - @inline fthetas(j) = node.f_origin.thetas[j,:] - @fastmath @inbounds @simd for i=1:N - @simd for j=1:M - z = (i-1)*M + j - node.f .+= pthetas(i) .* children(node)[z].f - node.fg .+= (pthetas(i) .* fthetas(j)) .* children(node)[z].f - node.fg .+= pthetas(i) .* children(node)[z].fg - end - end - - elseif node.p_origin isa PlainMulNode && node.f_origin isa Logistic⋀Node - - @fastmath @inbounds begin - # assume 2 children - node.f .= children(node)[1].f .* children(node)[2].f - node.fg .= (children(node)[1].f .* children(node)[2].fg) .+ - (children(node)[2].f .* children(node)[1].fg) - end - - elseif node.p_origin isa PlainProbLiteralNode - if node.f_origin isa Logistic⋁Node - m = children(node.f_origin)[1] - elseif node.f_origin isa LogisticLiteralNode - m = node.f_origin - else - error("Invalid Types of pairs {$pType} - {$fType}") - end - - var = variable(m) - X = data - if ispositive(node.p_origin) && ispositive(m) - node.f[:, .!isequal.(X[:, var], 0)] .= 1.0 # positive and missing observations - node.f[:, isequal.(X[:, var], 0)] .= 0.0 - elseif isnegative(node.p_origin) && isnegative(m) - node.f[:, .!isequal.(X[:, var], 1)] .= 1.0 # negative and missing observations - node.f[:, isequal.(X[:, var], 1)] .= 0.0 - else - node.f .= 0.0 - end - - if node.f_origin isa Logistic⋁Node - node.fg .= node.f .* transpose(node.f_origin.thetas) - else - node.fg .= 0.0 - end - - else - error("Invalid Types of pairs {$pType} - {$fType}") - end - -end \ No newline at end of file diff --git a/src/queries/expectation_rec.jl b/src/queries/expectation_rec.jl deleted file mode 100644 index a640ee6c..00000000 --- a/src/queries/expectation_rec.jl +++ /dev/null @@ -1,260 +0,0 @@ -export Expectation, Moment - -const ExpFloat = Float32 - -ExpCacheDict = Dict{Pair{ProbCircuit, LogisticCircuit}, Array{ExpFloat, 2}} -MomentCacheDict = Dict{Tuple{ProbCircuit, LogisticCircuit, Int64}, Array{ExpFloat, 2}} - -struct ExpectationCache - f::ExpCacheDict - fg::ExpCacheDict -end - -ExpectationCache() = ExpectationCache(ExpCacheDict(), ExpCacheDict()) - -struct MomentCache - f::ExpCacheDict - fg::MomentCacheDict -end -MomentCache() = MomentCache( ExpCacheDict(), MomentCacheDict()) - - -# Find a better way to cache n_choose_k values -max_k = 31 -choose_cache = [ 1.0 * binomial(i,j) for i=0:max_k+1, j=0:max_k+1 ] -@inline function choose(n::Int, m::Int) - return choose_cache[n+1, m+1] -end - - -""" - Expectation(pc::ProbCircuit, lc::LogisticCircuit, data) - -Compute Expected Prediction of a Logistic/Regression Circuit w.r.t to a ProbabilistcCircuit - -Missing values should be denoted by missing -See: On Tractable Computation of Expected Predictions [arxiv.org/abs/1910.02182](https://arxiv.org/abs/1910.02182) -""" -function Expectation(pc::ProbCircuit, lc::LogisticCircuit, data) - # 1. Get probability of each observation - log_likelihoods = marginal(pc, data) - p_observed = exp.( log_likelihoods ) - - # 2. Expectation w.r.t. P(x_m, x_o) - cache = ExpectationCache() - results_unnormalized = exp_g(pc, children(lc)[1], data, cache) # skipping the bias node of lc - - # 3. Expectation w.r.t P(x_m | x_o) - results = transpose(results_unnormalized) ./ p_observed - - # 4. Add Bias terms - biases = lc.thetas - results .+= biases - - results, cache -end - -""" - Moment(pc::ProbCircuit, lc::LogisticCircuit, data, moment::Int) - -Compute higher moments of Expected Prediction for the pair of Logistic/Regression Circuit, ProbabilistcCircuit -""" -function Moment(pc::ProbCircuit, lc::LogisticCircuit, data, moment::Int) - # 1. Get probability of each observation - log_likelihoods = marginal(pc, data) - p_observed = exp.( log_likelihoods ) - - # 2. Moment w.r.t. P(x_m, x_o) - cache = MomentCache() - biases = lc.thetas - results_unnormalized = zeros(ExpFloat, num_examples(data), num_classes(lc)) - - for z = 0:moment-1 - results_unnormalized .+= choose(moment, z) .* (biases .^ (z)) .* transpose(moment_g(pc, children(lc)[1], data, moment - z, cache)) - end - - # 3. Moment w.r.t P(x_m | x_o) - results = results_unnormalized ./ p_observed - - # 4. Add Bias^moment terms - results .+= biases .^ (moment) - - results, cache -end - - - -# exp_f (pr-constraint) is originally from: -# Arthur Choi, Guy Van den Broeck, and Adnan Darwiche. Tractable learning for structured probability spaces: A case study in learning preference distributions. In Proceedings of IJCAI, 2015. - -function exp_f(n::Union{PlainSumNode, StructSumNode}, m::Logistic⋁Node, data, cache::Union{ExpectationCache, MomentCache}) - @inbounds get!(cache.f, Pair(n, m)) do - value = zeros(ExpFloat, 1 , num_examples(data) ) - @inline pthetas(i) = ExpFloat.(exp(n.log_probs[i])) #[exp(n.log_probs[i]) for i in 1:num_children(n)] - @fastmath @simd for i in 1:num_children(n) - @simd for j in 1:num_children(m) - value .+= (pthetas(i) .* exp_f(children(n)[i], children(m)[j], data, cache)) - end - end - return value - end -end - -function exp_f(n::Union{PlainMulNode, StructMulNode}, m::Logistic⋀Node, data, cache::Union{ExpectationCache, MomentCache}) - @inbounds get!(cache.f, Pair(n, m)) do - # value = ones(ExpFloat, 1 , num_examples(data) ) - # @fastmath for (i,j) in zip(children(n), children(m)) - # value .*= exp_f(i, j, data, cache) - # end - # return value - @fastmath @inbounds exp_f(children(n)[1], children(m)[1], data, cache) .* exp_f(children(n)[2], children(m)[2], data, cache) - end - # @fastmath @inbounds exp_f(children(n)[1], children(m)[1], data, cache) .* exp_f(children(n)[2], children(m)[2], data, cache) -end - - -@inline function exp_f(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, m::LogisticLiteralNode, data, cache::Union{ExpectationCache, MomentCache}) - @inbounds get!(cache.f, Pair(n, m)) do - value = zeros(ExpFloat, 1 , num_examples(data) ) - var = lit2var(literal(m)) - X = data - if ispositive(n) && ispositive(m) - # value[1, X[:, var] .== -1 ] .= 1.0 # missing observation always agrees - # value[1, X[:, var] .== 1 ] .= 1.0 # positive observations - value[1, .!isequal.(X[:, var], 0)] .= ExpFloat(1.0) # positive or missing observations - elseif isnegative(n) && isnegative(m) - # value[1, X[:, var] .== -1 ] .= 1.0 # missing observation always agrees - # value[1, X[:, var] .== 0 ] .= 1.0 # negative observations - value[1, .!isequal.(X[:, var], 1)] .= ExpFloat(1.0) # negative or missing observations - end - return value - end -end - -""" -Has to be a Logistic⋁Node with only one child, which is a leaf node -""" -@inline function exp_f(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, m::Logistic⋁Node, data, cache::Union{ExpectationCache, MomentCache}) - @inbounds get!(cache.f, Pair(n, m)) do - exp_f(n, children(m)[1], data, cache) - end -end - -####################################################################### -######## exp_g, exp_fg -######################################################################## - -@inline function exp_g(n::Union{PlainSumNode, StructSumNode}, m::Logistic⋁Node, data, cache::ExpectationCache) - exp_fg(n, m, data, cache) # exp_fg and exp_g are the same for OR nodes -end - -# function exp_g(n::Prob⋀, m::Logistic⋀Node, data, cache::ExpectationCache) -# value = zeros(ExpFloat, classes(m) , num_examples(data)) -# @fastmath for (i,j) in zip(children(n), children(m)) -# value .+= exp_fg(i, j, data, cache) -# end -# return value -# # exp_fg(children(n)[1], children(m)[1], data, cache) .+ exp_fg(children(n)[2], children(m)[2], data, cache) -# end - - -function exp_fg(n::Union{PlainSumNode, StructSumNode}, m::Logistic⋁Node, data, cache::ExpectationCache) - @inbounds get!(cache.fg, Pair(n, m)) do - value = zeros(ExpFloat, num_classes(m) , num_examples(data) ) - @inline pthetas(i) = ExpFloat.(exp(n.log_probs[i])) #pthetas = [exp(n.log_probs[i]) for i in 1:num_children(n)] - @fastmath @simd for i in 1:num_children(n) - for j in 1:num_children(m) - value .+= (pthetas(i) .* ExpFloat.(m.thetas[j,:])) .* exp_f(children(n)[i], children(m)[j], data, cache) - value .+= pthetas(i) .* exp_fg(children(n)[i], children(m)[j], data, cache) - end - end - return value - end -end - -function exp_fg(n::Union{PlainMulNode, StructMulNode}, m::Logistic⋀Node, data, cache::ExpectationCache) - @inbounds get!(cache.fg, Pair(n, m)) do - # Assuming 2 children - value = exp_f(children(n)[1], children(m)[1], data, cache) .* exp_fg(children(n)[2], children(m)[2], data, cache) - value .+= exp_f(children(n)[2], children(m)[2], data, cache) .* exp_fg(children(n)[1], children(m)[1], data, cache) - return value - end - # @fastmath @inbounds exp_f(children(n)[1], children(m)[1], data, cache) .* exp_fg(children(n)[2], children(m)[2], data, cache) - # .+ exp_f(children(n)[2], children(m)[2], data, cache) .* exp_fg(children(n)[1], children(m)[1], data, cache) -end - - -""" -Has to be a Logistic⋁Node with only one child, which is a leaf node -""" -@inline function exp_fg(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, m::Logistic⋁Node, data, cache::ExpectationCache) - @inbounds get!(cache.fg, Pair(n, m)) do - ExpFloat.(m.thetas[1,:]) .* exp_f(n, m, data, cache) - end -end - -@inline function exp_fg(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, m::LogisticLiteralNode, data, cache::ExpectationCache) - #dont know how many classes, boradcasting does the job - zeros(ExpFloat, 1 , num_examples(data)) -end - -####################################################################### -######## moment_g, moment_fg -######################################################################## - -@inline function moment_g(n::Union{PlainSumNode, StructSumNode}, m::Logistic⋁Node, data, moment::Int, cache::MomentCache) - get!(cache.fg, (n, m, moment)) do - moment_fg(n, m, data, moment, cache) - end -end - -""" -Calculating E[g^k * f] -""" -function moment_fg(n::Union{PlainSumNode, StructSumNode}, m::Logistic⋁Node, data, moment::Int, cache::MomentCache) - if moment == 0 - return exp_f(n, m, data, cache) - end - - get!(cache.fg, (n, m, moment)) do - value = zeros(ExpFloat, num_classes(m) , num_examples(data) ) - pthetas = [exp(n.log_probs[i]) for i in 1:num_children(n)] - @fastmath @simd for i in 1:num_children(n) - for j in 1:num_children(m) - for z in 0:moment - value .+= pthetas[i] .* choose(moment, z) .* m.thetas[j,:].^(moment - z) .* moment_fg(children(n)[i], children(m)[j], data, z, cache) - end - end - end - return value - end -end - -@inline function moment_fg(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, m::Logistic⋁Node, data, moment::Int, cache::MomentCache) - get!(cache.fg, (n, m, moment)) do - m.thetas[1,:].^(moment) .* exp_f(n, m, data, cache) - end -end - -@inline function moment_fg(n::Union{PlainProbLiteralNode, StructProbLiteralNode}, m::LogisticLiteralNode, data, moment::Int, cache::MomentCache) - #dont know how many classes, boradcasting does the job - if moment == 0 - exp_f(n, m, data, cache) - else - zeros(ExpFloat, 1, num_examples(data)) - end -end - -function moment_fg(n::Union{PlainMulNode, StructMulNode}, m::Logistic⋀Node, data, moment::Int, cache::MomentCache) - if moment == 0 - return exp_f(n, m, data, cache) - end - get!(cache.fg, (n, m, moment)) do - value = moment_fg(children(n)[1], children(m)[1], data, 0, cache) .* moment_fg(children(n)[2], children(m)[2], data, moment, cache) - - for z in 1:moment - value .+= choose(moment, z) .* moment_fg(children(n)[1], children(m)[1], data, z, cache) .* moment_fg(children(n)[2], children(m)[2], data, moment - z, cache) - end - return value - end -end diff --git a/src/structured_prob_nodes.jl b/src/structured_prob_nodes.jl index 70668a40..8c8f1608 100644 --- a/src/structured_prob_nodes.jl +++ b/src/structured_prob_nodes.jl @@ -43,10 +43,11 @@ mutable struct StructSumNode <: StructProbInnerNode children::Vector{StructProbCircuit} log_probs::Vector{Float64} vtree::Vtree # could be leaf or inner - StructSumNode(c, v) = - new(c, log.(ones(Float64, length(c)) / length(c)), v) end +StructSumNode(c, v) = + StructSumNode(c, log.(ones(Float64, length(c)) / length(c)), v) + ##################### # traits ##################### diff --git a/test/LoadSave/circuit_loaders_tests.jl b/test/LoadSave/circuit_loaders_tests.jl deleted file mode 100644 index ca1c8298..00000000 --- a/test/LoadSave/circuit_loaders_tests.jl +++ /dev/null @@ -1,79 +0,0 @@ -using Test -using LogicCircuits -using ProbabilisticCircuits -using LightGraphs -using MetaGraphs -using Suppressor - -@testset "Load a small PSDD and test methods" begin - file = zoo_psdd_file("little_4var.psdd") - prob_circuit = load_prob_circuit(file); - @test prob_circuit isa ProbCircuit - - # Testing number of nodes and parameters - @test 9 == num_parameters(prob_circuit) - @test 20 == num_nodes(prob_circuit) - - # Testing Read Parameters - EPS = 1e-7 - or1 = children(children(prob_circuit)[1])[2] - @test abs(or1.log_probs[1] - (-1.6094379124341003)) < EPS - @test abs(or1.log_probs[2] - (-1.2039728043259361)) < EPS - @test abs(or1.log_probs[3] - (-0.916290731874155)) < EPS - @test abs(or1.log_probs[4] - (-2.3025850929940455)) < EPS - - or2 = children(children(prob_circuit)[1])[1] - @test abs(or2.log_probs[1] - (-2.3025850929940455)) < EPS - @test abs(or2.log_probs[2] - (-2.3025850929940455)) < EPS - @test abs(or2.log_probs[3] - (-2.3025850929940455)) < EPS - @test abs(or2.log_probs[4] - (-0.35667494393873245)) < EPS - - @test abs(prob_circuit.log_probs[1] - (0.0)) < EPS -end - -psdd_files = ["little_4var.psdd", "msnbc-yitao-a.psdd", "msnbc-yitao-b.psdd", "msnbc-yitao-c.psdd", "msnbc-yitao-d.psdd", "msnbc-yitao-e.psdd", "mnist-antonio.psdd"] - -@testset "Test parameter integrity of loaded PSDDs" begin - for psdd_file in psdd_files - @test check_parameter_integrity(load_prob_circuit(zoo_psdd_file(psdd_file))) - end -end - -@testset "Test parameter integrity of loaded structured PSDDs" begin - circuit, vtree = load_struct_prob_circuit( - zoo_psdd_file("little_4var.psdd"), zoo_vtree_file("little_4var.vtree")) - @test check_parameter_integrity(circuit) - @test vtree isa PlainVtree - @test respects_vtree(circuit, vtree) -end - -@testset "Test loaded logistic circuits" begin - classes = 2 - lc = zoo_lc("little_4var.circuit", classes) - @test lc isa LogisticCircuit - @test num_nodes(lc) == 29 - @test num_parameters(lc) == num_parameters_per_class(lc) * classes - @test check_parameter_integrity(lc) - @test all(lc.thetas ≈ [0.6803363307333976 0.7979990493834855]) - @test all(lc.children[1].thetas ≈ [0.34978385969447856 0.8620937209951014]) - or1 = lc.children[1].children[1].children[1] - @test all(or1.thetas ≈ [0.012723368087276588 0.44600859247854274; - 0.4126851950485019 0.8204476705654302; - 0.2633016148946008 0.010045227037839832; - 0.27132714432580674 0.9677387544772587]) -end - - -@testset "Test loaded clt file" begin - # TODO add more clt files - t = zoo_clt("4.clt") - @test t isa CLT - @test_nowarn @suppress_out print_tree(t) - @test nv(t) == 4 - @test ne(t) == 0 - for (v, p) in enumerate([.2, .1, .3, .4]) - cpt = get_prop(t, v, :cpt) - @test cpt[1] + cpt[0] ≈ 1.0 - @test cpt[1] ≈ p - end -end diff --git a/test/LoadSave/circuit_savers_tests.jl b/test/LoadSave/circuit_savers_tests.jl deleted file mode 100644 index 42452e5c..00000000 --- a/test/LoadSave/circuit_savers_tests.jl +++ /dev/null @@ -1,73 +0,0 @@ -using Test -using LogicCircuits -using ProbabilisticCircuits - -function test_equal(c1::LogicCircuit, c2::LogicCircuit) - result = true - for (n1, n2) in zip(linearize(c1), linearize(c2)) - if is⋁gate(n1) - @test is⋁gate(n2) - @test all(params(n1) ≈ params(n2)) - elseif is⋀gate(n1) - result = result - @test is⋀gate(n2) - else - @test isliteralgate(n1) - @test isliteralgate(n2) - @test literal(n1) == literal(n2) - end - end - result -end - -@testset "Circuit saver test" begin - mktempdir() do tmp - - circuit, vtree = load_struct_prob_circuit( - zoo_psdd_file("little_4var.psdd"), zoo_vtree_file("little_4var.vtree")) - - # load, save, and load as .psdd - @test_nowarn save_circuit("$tmp/temp.psdd", circuit, vtree) - @test_nowarn save_vtree("$tmp/temp.vtree", vtree); - - circuit2, vtree2 = load_struct_prob_circuit("$tmp/temp.psdd", "$tmp/temp.vtree") - test_equal(circuit, circuit2) - - f_c, f_v = open("$tmp/temp.psdd", "r"), open("$tmp/temp.vtree", "r") - circuit2, _ = load_struct_prob_circuit(f_c, f_v) - test_equal(circuit, circuit2) - close(f_c); close(f_v) - - f = open("$tmp/temp.psdd", "w") - @test_nowarn save_as_psdd(f, circuit, vtree) - close(f) - circuit2, _ = load_struct_prob_circuit("$tmp/temp.psdd", "$tmp/temp.vtree") - test_equal(circuit, circuit2) - - # save and load as .sdd - @test_nowarn save_circuit("$tmp/temp.sdd", PlainStructLogicCircuit(circuit), vtree) - @test_nowarn save_vtree("$tmp/temp.vtree", vtree) - - @test_nowarn save_as_tex("$tmp/temp.tex", circuit) - @test_nowarn save_as_dot("$tmp/temp.tex", circuit) - @test_nowarn plot(circuit) - @test_nowarn plot(vtree) - - # psdd2 - v = Vtree(5, :balanced) - c = fully_factorized_circuit(ProbCircuit, v).children[1] - @test_nowarn save_circuit("$tmp/temp.psdd", c, v) - @test_nowarn save_vtree("$tmp/temp.vtree", v); - c2, v2 = load_struct_prob_circuit("$tmp/temp.psdd", "$tmp/temp.vtree") - test_equal(c, c2) - end - - mktempdir() do tmp - classes = 2 - lc1 = zoo_lc("little_4var.circuit", classes) - @test_nowarn save_circuit("$tmp/temp.circuit", lc1, nothing) - - lc2 = load_logistic_circuit("$tmp/temp.circuit", classes) - test_equal(lc1, lc2) - end -end diff --git a/test/Logistic/logistic_tests.jl b/test/Logistic/logistic_tests.jl deleted file mode 100644 index 6f09ebb1..00000000 --- a/test/Logistic/logistic_tests.jl +++ /dev/null @@ -1,82 +0,0 @@ -using Test -using LogicCircuits -using ProbabilisticCircuits - -# This tests are supposed to test queries on the circuits -@testset "Logistic Circuit Query and Parameter Tests" begin - # Uses a Logistic Circuit with 4 variables, and tests 3 of the configurations to - # match with python version. - - # CLASSES = 2 - - # logistic_circuit = zoo_lc("little_4var.circuit", CLASSES) - # @test logistic_circuit isa LogisticCircuit - - # # check probabilities for binary samples - # data = @. Bool([0 0 0 0; 0 1 1 0; 0 0 1 1]) - # # true_weight_func = [3.43147972 4.66740416; - # # 4.27595352 2.83503504; - # # 3.67415087 4.93793472] - # true_prob = [0.9686740008311808 0.9906908445371728; - # 0.9862917392724188 0.9445399509069984; - # 0.9752568185086389 0.9928816444223209] - - # class_prob = class_likelihood_per_instance(logistic_circuit, CLASSES, data) - # for i = 1:size(true_prob)[1] - # for j = 1:CLASSES - # @test true_prob[i,j] ≈ class_prob[i,j] - # end - # end - - # # check probabilities for float samples - # data = Float32.(data) - # class_prob = class_likelihood_per_instance(logistic_circuit, CLASSES, data) - # for i = 1:size(true_prob)[1] - # for j = 1:CLASSES - # @test true_prob[i,j] ≈ class_prob[i,j] - # end - # end - - # # check predicted_classes - # true_labels = [2, 1, 2] - # predicted_classes = predict_class(logistic_circuit, CLASSES, data) - # @test all(predicted_classes .== true_labels) - - # # check accuracy - # @test accuracy(logistic_circuit, CLASSES, data, true_labels) == 1.0 - - # # check parameter updates - # original_literal_parameters = Dict{Int, Vector{Float64}}() - # foreach(logistic_circuit) do ln - # if ln isa Logistic⋁Node - # foreach(ln.children, eachrow(ln.thetas)) do c, theta - # if c isa LogisticLiteralNode - # original_literal_parameters[c.literal] = copy(theta) - # end - # end - # end - # end - - # one_hot_labels = [0.0 1.0; - # 1.0 0.0; - # 0.0 1.0] - # one_hot_labels = Float32.(one_hot_labels) - # true_error = true_prob .- one_hot_labels - # step_size = 0.1 - # learn_parameters(logistic_circuit, CLASSES, data, true_labels; num_epochs=1, step_size=step_size, flows_computed=true) - - # foreach(logistic_circuit) do ln - # if ln isa Logistic⋁Node - # foreach(ln.children, eachrow(ln.thetas)) do c, theta - # if c isa LogisticLiteralNode - # for class = 1:CLASSES - # true_update_amount = -step_size * sum(c.data.upflow .* true_error[:, class]) / size(true_error)[1] - # updated_amount = theta[class] - original_literal_parameters[c.literal][class] - # @test updated_amount ≈ true_update_amount atol=1e-7 - # end - # end - # end - # end - # end - -end \ No newline at end of file diff --git a/test/broken/Logistic/logistic_tests.jl b/test/broken/Logistic/logistic_tests.jl deleted file mode 100644 index db741936..00000000 --- a/test/broken/Logistic/logistic_tests.jl +++ /dev/null @@ -1,31 +0,0 @@ -#TODO: reinstate - -# using Test -# using LogicCircuits -# using ProbabilisticCircuits - -# # This tests are supposed to test queries on the circuits -# @testset "Logistic Circuit Class Conditional" begin -# # Uses a Logistic Circuit with 4 variables, and tests 3 of the configurations to -# # match with python version. - -# EPS = 1e-7; -# logistic_circuit = zoo_lc("little_4var.circuit", 2); -# @test logistic_circuit isa LogisticCircuit; - -# # Step 1. Check Probabilities for 3 samples -# data = Bool.([0 0 0 0; 0 1 1 0; 0 0 1 1]); - -# true_prob = [3.43147972 4.66740416; -# 4.27595352 2.83503504; -# 3.67415087 4.93793472] - -# CLASSES = 2 -# calc_prob = class_conditional_likelihood_per_instance(logistic_circuit, CLASSES, data) - -# for i = 1:3 -# for j = 1:2 -# @test true_prob[i,j] ≈ calc_prob[i,j] atol= EPS; -# end -# end -# end \ No newline at end of file diff --git a/test/broken/Logistic/param_bit_circuit_pair_test.jl b/test/broken/Logistic/param_bit_circuit_pair_test.jl deleted file mode 100644 index c185c73f..00000000 --- a/test/broken/Logistic/param_bit_circuit_pair_test.jl +++ /dev/null @@ -1,53 +0,0 @@ -using Test -using LogicCircuits -using ProbabilisticCircuits -using Random - -@testset "BitCircuitPair test" begin - -end - - -@testset "ParamBitCircuitPair test" begin - - function test_integrity(pbc::ParamBitCircuitPair) - bc = pbc.pair_bit - @test num_elements(pbc) == size(bc.elements, 2) - for el in 1:size(bc.elements,2) - d = bc.elements[1,el] - @test bc.nodes[1,d] <= el - - @test bc.nodes[2,d] >= el #TODO this line fails - p = bc.elements[2,el] - @test el ∈ bc.parents[bc.nodes[3,p]:bc.nodes[4,p]] - s = bc.elements[3,el] - @test el ∈ bc.parents[bc.nodes[3,s]:bc.nodes[4,s]] - end - for node in 1:size(bc.nodes,2) - first_el = bc.nodes[1,node] - last_el = bc.nodes[2,node] - if first_el != 0 - for i = first_el:last_el - @test bc.elements[1,i] == node #TODO this line fails (its node-1 instead for some reason) - end - end - first_par = bc.nodes[3,node] - last_par = bc.nodes[3,node] - if first_par != 0 - for i = first_par:last_par - par = bc.parents[i] - @test bc.elements[2,par] == node || bc.elements[3,par] == node - end - else - @test node == num_nodes(pbc) || node <= num_leafs(pbc) - end - end - @test sum(length, bc.layers) == size(bc.nodes,2) - end - - pc = zoo_psdd("exp-D15-N1000-C4.psdd"); - lc = zoo_lc("exp-D15-N1000-C4.circuit", 4); - - test_integrity(ParamBitCircuitPair(pc, children(lc)[1])); - -end diff --git a/test/ensembles/bmc_tests.jl b/test/ensembles/bmc_tests.jl index 89802089..ea8af0aa 100644 --- a/test/ensembles/bmc_tests.jl +++ b/test/ensembles/bmc_tests.jl @@ -24,10 +24,11 @@ using LogicCircuits @test isapprox(evi[findall(>(0), evi)], (R/sum(R)); atol) end + # some tests commented out to speed up tests case((1 ∧ 2) ∨ (3 ∧ ¬4) ∨ (¬1 ∧ 5), 5) - case((1 → 3) ∧ (5 → ¬2), 5) - case((1 ∧ 2 ∧ 3) ∨ (4 ∧ 5), 5) + # case((1 → 3) ∧ (5 → ¬2), 5) + # case((1 ∧ 2 ∧ 3) ∨ (4 ∧ 5), 5) case(exactly(3, collect(1:5)), 5) - case(atleast(3, collect(1:5)), 5) - case(atmost(3, collect(1:5)), 5) + # case(atleast(3, collect(1:5)), 5) + # case(atmost(3, collect(1:5)), 5) end diff --git a/test/ensembles/ensembles_tests.jl b/test/ensembles/ensembles_tests.jl index aea0f2fb..4bb80a42 100644 --- a/test/ensembles/ensembles_tests.jl +++ b/test/ensembles/ensembles_tests.jl @@ -4,6 +4,7 @@ using DataFrames using LogicCircuits @testset "ensemble tests with SamplePSDD" begin + # Set up a logic constraint ϕ as a BDD and scope size n. Sample m PSDDs. function case(ϕ::Bdd, n::Integer, strategy::Symbol; m::Integer = 20, atol::Real = 1e-2)::Ensemble{StructProbCircuit} # All possible valuations (including impossible ones). @@ -26,13 +27,14 @@ using LogicCircuits return E end + # some tests commented out to speed up tests Es = Vector{Ensemble{StructProbCircuit}}() for strategy ∈ [:likelihood, :uniform, :em, :stacking] case((1 ∧ 2) ∨ (3 ∧ ¬4) ∨ (¬1 ∧ 5), 5, strategy) - case((1 → 3) ∧ (5 → ¬2), 5, strategy) - case((1 ∧ 2 ∧ 3) ∨ (4 ∧ 5), 5, strategy) - case(exactly(3, collect(1:5)), 5, strategy) - case(atleast(3, collect(1:5)), 5, strategy) + # case((1 → 3) ∧ (5 → ¬2), 5, strategy) + # case((1 ∧ 2 ∧ 3) ∨ (4 ∧ 5), 5, strategy) + # case(exactly(3, collect(1:5)), 5, strategy) + # case(atleast(3, collect(1:5)), 5, strategy) case(atmost(3, collect(1:5)), 5, strategy) end diff --git a/test/FactorGraph/fg_tests.jl b/test/factorgraph/fg_tests.jl similarity index 100% rename from test/FactorGraph/fg_tests.jl rename to test/factorgraph/fg_tests.jl diff --git a/test/helper/plain_logic_circuits.jl b/test/helper/little_circuits.jl similarity index 81% rename from test/helper/plain_logic_circuits.jl rename to test/helper/little_circuits.jl index f2a66f95..dc2dc9e4 100644 --- a/test/helper/plain_logic_circuits.jl +++ b/test/helper/little_circuits.jl @@ -66,3 +66,15 @@ function little_5var() and = c_4var & or Plain⋁Node([and]) end + +function readme_sdd() + manager = SddMgr(7, :balanced) + + sun, rain, rainbow, cloud, snow, los_angeles, belgium = pos_literals(Sdd, manager, 7) + + sdd = (rainbow & sun & rain) | (-rainbow) + sdd &= (-los_angeles | -belgium) + sdd &= (los_angeles ⇒ sun) ∧ (belgium ⇒ cloud) + sdd &= (¬(rain ∨ snow) ⇐ ¬cloud) + sdd +end \ No newline at end of file diff --git a/test/helper/pc_equals.jl b/test/helper/pc_equals.jl new file mode 100644 index 00000000..77fc1912 --- /dev/null +++ b/test/helper/pc_equals.jl @@ -0,0 +1,18 @@ +using LogicCircuits: is⋁gate, is⋀gate + +function test_pc_equals(c1, c2) + @test num_nodes(c1) == num_nodes(c2) + @test num_edges(c1) == num_edges(c2) + for (n1, n2) in zip(linearize(c1), linearize(c2)) + if is⋁gate(n1) + @test is⋁gate(n2) + @test all(params(n1) ≈ params(n2)) + elseif is⋀gate(n1) + @test is⋀gate(n2) + else + @test isliteralgate(n1) + @test isliteralgate(n2) + @test literal(n1) == literal(n2) + end + end +end \ No newline at end of file diff --git a/test/io/clt_io_test.jl b/test/io/clt_io_test.jl new file mode 100644 index 00000000..3100bfe0 --- /dev/null +++ b/test/io/clt_io_test.jl @@ -0,0 +1,19 @@ +using Test +using ProbabilisticCircuits +using Suppressor +using LightGraphs: nv, ne +using MetaGraphs: get_prop + +@testset "Test loaded clt file" begin + # TODO add more clt files + t = zoo_clt("4.clt") + @test t isa CLT + @test_nowarn @suppress_out print_tree(t) + @test nv(t) == 4 + @test ne(t) == 0 + for (v, p) in enumerate([.2, .1, .3, .4]) + cpt = get_prop(t, v, :cpt) + @test cpt[1] + cpt[0] ≈ 1.0 + @test cpt[1] ≈ p + end +end diff --git a/test/io/jpc_io.jl b/test/io/jpc_io.jl new file mode 100644 index 00000000..c4600f6b --- /dev/null +++ b/test/io/jpc_io.jl @@ -0,0 +1,82 @@ +using Test +using ProbabilisticCircuits + +include("../helper/pc_equals.jl") + +@testset "Load and save a small JPC" begin + + function test_my_circuit(pc) + + @test pc isa ProbCircuit + + # Testing number of nodes and parameters + @test 9 == num_parameters(pc) + @test 20 == num_nodes(pc) + + # Testing Read Parameters + EPS = 1e-7 + or1 = children(children(pc)[1])[2] + @test abs(or1.log_probs[1] - (-1.6094379124341003)) < EPS + @test abs(or1.log_probs[2] - (-1.2039728043259361)) < EPS + @test abs(or1.log_probs[3] - (-0.916290731874155)) < EPS + @test abs(or1.log_probs[4] - (-2.3025850929940455)) < EPS + + or2 = children(children(pc)[1])[1] + @test abs(or2.log_probs[1] - (-2.3025850929940455)) < EPS + @test abs(or2.log_probs[2] - (-2.3025850929940455)) < EPS + @test abs(or2.log_probs[3] - (-2.3025850929940455)) < EPS + @test abs(or2.log_probs[4] - (-0.35667494393873245)) < EPS + + @test abs(pc.log_probs[1] - (0.0)) < EPS + @test check_parameter_integrity(pc) + end + + # first load circuit from PSDD file + paths = (zoo_psdd_file("little_4var.psdd"), zoo_vtree_file("little_4var.vtree")) + formats = (PsddFormat(), VtreeFormat()) + pc1 = read(paths, StructProbCircuit, formats) + + mktempdir() do tmp + + # write as a unstructured logic circuit + jpc_path = "$tmp/example.jpc" + write(jpc_path, pc1) + + # read as a unstructured logic circuit + pc2 = read(jpc_path, ProbCircuit) + + test_my_circuit(pc2) + test_pc_equals(pc1, pc2) + + # write with vtree + vtree_path = "$tmp/example.vtree" + paths = (jpc_path, vtree_path) + write(paths, pc1) + + # read as a structured probabilistic circuit + pc3 = read(paths, StructProbCircuit) + + @test pc3 isa StructProbCircuit + test_my_circuit(pc3) + test_pc_equals(pc1, pc3) + @test vtree(pc1) == vtree(pc3) + + end + +end + +@testset "Can save JPCs with nonbinary multiplications" begin + + pc1 = fully_factorized_circuit(ProbCircuit, 10) + + mktempdir() do tmp + + jpc_path = "$tmp/example.jpc" + write(jpc_path, pc1) + + pc2 = read(jpc_path, ProbCircuit) + + test_pc_equals(pc1, pc2) + end + +end \ No newline at end of file diff --git a/test/io/psdd_io_test.jl b/test/io/psdd_io_test.jl new file mode 100644 index 00000000..dc8f801a --- /dev/null +++ b/test/io/psdd_io_test.jl @@ -0,0 +1,95 @@ +using Test +using ProbabilisticCircuits +using LogicCircuits: is⋁gate, is⋀gate + +include("../helper/pc_equals.jl") + +@testset "Load and save a small PSDD with and without vtree" begin + + function test_my_circuit(pc) + + @test pc isa ProbCircuit + + # Testing number of nodes and parameters + @test 9 == num_parameters(pc) + @test 20 == num_nodes(pc) + + # Testing Read Parameters + EPS = 1e-7 + or1 = children(children(pc)[1])[2] + @test abs(or1.log_probs[1] - (-1.6094379124341003)) < EPS + @test abs(or1.log_probs[2] - (-1.2039728043259361)) < EPS + @test abs(or1.log_probs[3] - (-0.916290731874155)) < EPS + @test abs(or1.log_probs[4] - (-2.3025850929940455)) < EPS + + or2 = children(children(pc)[1])[1] + @test abs(or2.log_probs[1] - (-2.3025850929940455)) < EPS + @test abs(or2.log_probs[2] - (-2.3025850929940455)) < EPS + @test abs(or2.log_probs[3] - (-2.3025850929940455)) < EPS + @test abs(or2.log_probs[4] - (-0.35667494393873245)) < EPS + + @test abs(pc.log_probs[1] - (0.0)) < EPS + @test check_parameter_integrity(pc) + end + + pc0 = zoo_psdd("little_4var.psdd") + + test_my_circuit(pc0) + + paths = (zoo_psdd_file("little_4var.psdd"), zoo_vtree_file("little_4var.vtree")) + formats = (PsddFormat(), VtreeFormat()) + pc1 = read(paths, StructProbCircuit, formats) + + @test pc1 isa StructProbCircuit + test_my_circuit(pc1) + @test respects_vtree(pc1, vtree(pc1)) + test_pc_equals(pc0, pc1) + + mktempdir() do tmp + + # write as a unstructured logic circuit + psdd_path = "$tmp/example.psdd" + write(psdd_path, pc1) + + # read as a unstructured logic circuit + pc2 = read(psdd_path, ProbCircuit) + + test_my_circuit(pc2) + test_pc_equals(pc0, pc2) + + # write with vtree + vtree_path = "$tmp/example.vtree" + paths = (psdd_path, vtree_path) + write(paths, pc1) + + # read as a structured probabilistic circuit + pc3 = read(paths, StructProbCircuit) + + @test pc3 isa StructProbCircuit + test_my_circuit(pc3) + @test vtree(pc1) == vtree(pc3) + test_pc_equals(pc0, pc3) + + end + +end + + psdd_files = ["little_4var.psdd", "msnbc-yitao-a.psdd", "msnbc-yitao-b.psdd", "msnbc-yitao-c.psdd", "msnbc-yitao-d.psdd", "msnbc-yitao-e.psdd", "mnist-antonio.psdd"] + + @testset "Test parameter integrity of loaded PSDDs" begin + for psdd_file in psdd_files + @test check_parameter_integrity(zoo_psdd(psdd_file)) + end + end + +@testset "Cannot save PSDDs with nonbinary multiplications" begin + + pc = fully_factorized_circuit(ProbCircuit, 10) + + mktempdir() do tmp + + psdd_path = "$tmp/example.psdd" + @test_throws ErrorException write(psdd_path, pc) + + end +end \ No newline at end of file diff --git a/test/logistic_nodes_tests.jl b/test/logistic_nodes_tests.jl deleted file mode 100644 index b618ce25..00000000 --- a/test/logistic_nodes_tests.jl +++ /dev/null @@ -1,40 +0,0 @@ -using Test -using LogicCircuits -using ProbabilisticCircuits - -include("helper/plain_logic_circuits.jl") - -@testset "probabilistic circuit nodes" begin - - c1 = little_3var() - classes = 2 - @test all(isleaf, intersect(linearize(LogisticCircuit(c1, classes)), linearize(LogisticCircuit(c1, classes)))) - p1 = LogisticCircuit(c1, classes) - lit3 = children(children(p1)[1])[1] - - # traits - @test p1 isa LogisticCircuit - @test p1 isa LogisticInnerNode - @test p1 isa Logistic⋁Node - @test children(p1)[1] isa Logistic⋀Node - @test lit3 isa LogisticLiteralNode - @test is⋁gate(p1) - @test is⋀gate(children(p1)[1]) - @test isliteralgate(lit3) - @test length(or_nodes(p1)) == 5 - - # extension methods - @test literal(lit3) === literal(children(children(c1)[1])[1]) - @test variable(left_most_descendent(p1)) == Var(3) - @test ispositive(left_most_descendent(p1)) - @test !isnegative(left_most_descendent(p1)) - @test num_nodes(p1) == 15 - @test num_edges(p1) == 18 - @test num_parameters(p1) == 10*2 - @test num_parameters_per_class(p1) == 10 - - r1 = fully_factorized_circuit(LogisticCircuit,10; classes=classes) - @test num_parameters(r1) == 2 * (2*10+1) - @test length(or_nodes(r1)) == 11 - -end \ No newline at end of file diff --git a/test/param_bit_circuit_test.jl b/test/param_bit_circuit_test.jl index 0aa61bc0..abafaad3 100644 --- a/test/param_bit_circuit_test.jl +++ b/test/param_bit_circuit_test.jl @@ -41,9 +41,6 @@ using ProbabilisticCircuits r = fully_factorized_circuit(PlainProbCircuit, 10) test_integrity(ParamBitCircuit(r, 10)) - - r = fully_factorized_circuit(LogisticCircuit, 10; classes=2) - test_integrity(ParamBitCircuit(r, 2, 10)) vtree = PlainVtree(10, :balanced) r = fully_factorized_circuit(StructProbCircuit, vtree) diff --git a/test/plain_prob_nodes_tests.jl b/test/plain_prob_nodes_tests.jl index ee682b32..74e4e7e1 100644 --- a/test/plain_prob_nodes_tests.jl +++ b/test/plain_prob_nodes_tests.jl @@ -2,7 +2,7 @@ using Test using LogicCircuits using ProbabilisticCircuits -include("helper/plain_logic_circuits.jl") +include("helper/little_circuits.jl") @testset "probabilistic circuit nodes" begin diff --git a/test/queries/expectation_tests.jl b/test/queries/expectation_tests.jl deleted file mode 100644 index ef02f040..00000000 --- a/test/queries/expectation_tests.jl +++ /dev/null @@ -1,174 +0,0 @@ -using Test -using LogicCircuits -using ProbabilisticCircuits -using DataFrames -using CUDA -using Random -using Tables - -function test_expectation_brute_force(pc::ProbCircuit, lc::LogisticCircuit, data, CLASSES::Int) - Random.seed!(124) - - EPS = 1e-4; - COUNT = size(data)[1] - # Compute True expectation brute force - true_exp = zeros(COUNT, CLASSES) - for i in 1:COUNT - row = Array(data[i, :]) - cur_data_all = generate_all(row) - - calc_p = log_likelihood_per_instance(pc, cur_data_all) - calc_p = exp.(calc_p) - - calc_f = class_weights_per_instance(lc, CLASSES, cur_data_all) - true_exp[i, :] = sum(calc_p .* calc_f, dims=1) - true_exp[i, :] ./= sum(calc_p) #p_observed - end - # Compute Circuit Expect - calc_exp, cache = Expectation(pc, lc, data); - for i = 1:COUNT - for j = 1:CLASSES - @test true_exp[i,j] ≈ calc_exp[i,j] atol= EPS; - end - end - # Compute Bottom Up Expectation - calc_exp_2, exp_flow = ExpectationUpward(pc, lc, data); - for i = 1:COUNT - for j = 1:CLASSES - @test true_exp[i,j] ≈ calc_exp_2[i,j] atol= EPS; - end - end - # Compute BitCircuit Expectations (CPU) - bit_exps, fvalues, gvalues, pbc_cpu = ExpectationBit(pc, lc, data; return_aux=true) - bit_exps_batch2 = ExpectationBit(pbc_cpu, pc, lc, data, fvalues, gvalues; return_aux=false) - for i = 1:COUNT - for j = 1:CLASSES - @test true_exp[i,j] ≈ bit_exps[i,j] atol= EPS; - @test true_exp[i,j] ≈ bit_exps_batch2[i,j] atol= EPS; - end - end - - # Compute BitCircuit Expectations (GPU) - if CUDA.functional() - data_gpu = to_gpu(data); - bit_exps_gpu, fvalues, gvalues, pbc_gpu = ExpectationBit(pc, lc, data_gpu; return_aux=true) - bit_exps_gpu_2 = ExpectationBit(pbc_gpu, pc, lc, data_gpu, fvalues, gvalues; return_aux=false) - - # Move to cpu - bit_exps_cpu = to_cpu(bit_exps_gpu) - bit_exps_cpu_2 = to_cpu(bit_exps_gpu_2) - - for i = 1:COUNT - for j = 1:CLASSES - @test true_exp[i,j] ≈ bit_exps_cpu[i,j] atol= EPS; - @test true_exp[i,j] ≈ bit_exps_cpu_2[i,j] atol= EPS; - end - end - end - -end - -function test_moment_brute_force(pc::ProbCircuit, lc::LogisticCircuit, data, CLASSES::Int, moment::Int) - EPS = 1e-4; - COUNT = size(data)[1] - # Compute True moment brute force - true_mom = zeros(COUNT, CLASSES) - for i in 1:COUNT - row = Vector(data[i, :]) - cur_data_all = generate_all(row) - - calc_p = log_likelihood_per_instance(pc, cur_data_all) - calc_p = exp.(calc_p) - - calc_f = class_weights_per_instance(lc, CLASSES, cur_data_all) - true_mom[i, :] = sum(calc_p .* (calc_f .^ moment), dims=1) - true_mom[i, :] ./= sum(calc_p) #p_observed - end - - # Compute Circuit Moment - calc_mom, cache = Moment(pc, lc, data, moment); - for i = 1:COUNT - for j = 1:CLASSES - @test (true_mom[i,j] / (calc_mom[i,j] )) ≈ 1.0 atol= EPS; - end - end -end - - -@testset "Expectation Brute Force Test" begin - # Small (4 Var) - psdd_file = "little_4var.psdd" - logistic_file = "little_4var.circuit" - CLASSES = 2 - N = 4 - - pc = zoo_psdd(psdd_file); - lc = zoo_lc(logistic_file, CLASSES); - data = DataFrame([0 0 0 0; - 0 1 1 0; - 0 0 1 1; - missing missing missing missing; - missing 0 1 missing; - 0 1 missing 1; - 1 missing 0 missing; - missing 0 1 missing; - missing missing 0 1; - missing missing missing 1; - missing missing missing 0; - ], :auto) - - data = DataFrame(map(x -> (ismissing(x) ? missing : Bool(x)) , Tables.matrix(data)), :auto) - test_expectation_brute_force(pc, lc, data, CLASSES) - - # Big circuit (15 Var) - psdd_file = "exp-D15-N1000-C4.psdd" - logistic_file = "exp-D15-N1000-C4.circuit" - CLASSES = 4 - N = 15 - COUNT = 10 - - pc = zoo_psdd(psdd_file); - lc = zoo_lc(logistic_file, CLASSES); - data = DataFrame(rand( (missing,true,false), (COUNT, N) ), :auto) - - test_expectation_brute_force(pc, lc, data, CLASSES) -end - - -@testset "Moment Brute Force Test" begin - # Small (4 Var) - psdd_file = "little_4var.psdd" - logistic_file = "little_4var.circuit"; - CLASSES = 2 - N = 4 - COUNT = 100 - - pc = zoo_psdd(psdd_file); - lc = zoo_lc(logistic_file, CLASSES); - data = DataFrame(rand( (missing,true,false), (COUNT, N) ), :auto) - - test_moment_brute_force(pc, lc, data, CLASSES, 1) - test_moment_brute_force(pc, lc, data, CLASSES, 2) - test_moment_brute_force(pc, lc, data, CLASSES, 3) - test_moment_brute_force(pc, lc, data, CLASSES, 4) - test_moment_brute_force(pc, lc, data, CLASSES, 10) - test_moment_brute_force(pc, lc, data, CLASSES, 15) - - # Big Var - psdd_file = "exp-D15-N1000-C4.psdd" - logistic_file = "exp-D15-N1000-C4.circuit"; - CLASSES = 4 - N = 15 - COUNT = 10 - - pc = zoo_psdd(psdd_file); - lc = zoo_lc(logistic_file, CLASSES); - data = DataFrame(rand( (missing,true,false), (COUNT, N) ), :auto) - - test_moment_brute_force(pc, lc, data, CLASSES, 1) - test_moment_brute_force(pc, lc, data, CLASSES, 2) - test_moment_brute_force(pc, lc, data, CLASSES, 3) - test_moment_brute_force(pc, lc, data, CLASSES, 4) - test_moment_brute_force(pc, lc, data, CLASSES, 10) - test_moment_brute_force(pc, lc, data, CLASSES, 15) -end \ No newline at end of file diff --git a/test/queries/informations_tests.jl b/test/queries/informations_tests.jl index efc60de4..39909bed 100644 --- a/test/queries/informations_tests.jl +++ b/test/queries/informations_tests.jl @@ -4,12 +4,11 @@ using ProbabilisticCircuits @testset "Entropy and KLD" begin - pc1, vtree = load_struct_prob_circuit( - zoo_psdd_file("simple2.1.psdd"), zoo_vtree_file("simple2.vtree")) - pc2, vtree = load_struct_prob_circuit( - zoo_psdd_file("simple2.2.psdd"), zoo_vtree_file("simple2.vtree")) - pc3, vtree = load_struct_prob_circuit( - zoo_psdd_file("simple2.3.psdd"), zoo_vtree_file("simple2.vtree")) + vtree_file = zoo_vtree_file("simple2.vtree") + + pc1 = read((zoo_psdd_file("simple2.1.psdd"), vtree_file), StructProbCircuit) + pc2 = read((zoo_psdd_file("simple2.2.psdd"), vtree_file), StructProbCircuit) + pc3 = read((zoo_psdd_file("simple2.3.psdd"), vtree_file), StructProbCircuit) @test entropy(pc1) ≈ 1.2899219826090118 @test entropy(pc2) ≈ 0.9359472745536583 diff --git a/test/queries/likelihood_tests.jl b/test/queries/likelihood_tests.jl index b9422ca1..fdfc99ac 100644 --- a/test/queries/likelihood_tests.jl +++ b/test/queries/likelihood_tests.jl @@ -44,19 +44,20 @@ include("../helper/gpu.jl") EVI(alltrue, d) end - # Test Sturdel EVI - samples, _ = sample(prob_circuit, 100000) - mix, weights, _ = learn_strudel(DataFrame(convert(BitArray, samples), :auto); num_mix = 10, - init_maxiter = 20, em_maxiter = 100, verbose = false) - mix_calc_prob = exp.(EVI(mix, data, weights)) + # Strudel test commented out because too slow! + # # Test Sturdel EVI + # samples, _ = sample(prob_circuit, 100000) + # mix, weights, _ = learn_strudel(DataFrame(convert(BitArray, samples), :auto); num_mix = 10, + # init_maxiter = 20, em_maxiter = 100, verbose = false) + # mix_calc_prob = exp.(EVI(mix, data, weights)) - @test true_prob ≈ mix_calc_prob atol = 0.1 - mix_calc_prob_all = exp.(EVI(mix, data_all)) - @test 1 ≈ sum(mix_calc_prob_all) atol = 0.1 + # @test true_prob ≈ mix_calc_prob atol = 0.1 + # mix_calc_prob_all = exp.(EVI(mix, data_all)) + # @test 1 ≈ sum(mix_calc_prob_all) atol = 0.1 - cpu_gpu_agree_approx(data_all) do d - EVI(mix, d, weights) - end + # cpu_gpu_agree_approx(data_all) do d + # EVI(mix, d, weights) + # end end @testset "Bagging models' likelihood" begin diff --git a/test/queries/marginal_flow_tests.jl b/test/queries/marginal_flow_tests.jl index d3cd2789..de2bda62 100644 --- a/test/queries/marginal_flow_tests.jl +++ b/test/queries/marginal_flow_tests.jl @@ -73,17 +73,18 @@ include("../helper/gpu.jl") MAR(alltrue, d) end - # Strudel Marginal Flow Test - rng = MersenneTwister(100003); # Fix the seed - samples, _ = sample(prob_circuit, 100000; rng) - mix, weights, _ = learn_strudel(DataFrame(convert(BitArray, samples), :auto); num_mix = 10, - init_maxiter = 20, em_maxiter = 100, verbose = false) - mix_calc_prob = exp.(MAR(mix, data_marg, weights)) - for mix_pair in zip(true_prob, mix_calc_prob) - @test mix_pair[1] ≈ mix_pair[2] atol=0.1 - end - - test_complete_mar(mix, data_full, weights, 0.1) + # TODO move somewhere else because this test is too slow to run each time + # # Strudel Marginal Flow Test + # rng = MersenneTwister(100003); # Fix the seed + # samples, _ = sample(prob_circuit, 100000; rng) + # mix, weights, _ = learn_strudel(DataFrame(convert(BitArray, samples), :auto); num_mix = 10, + # init_maxiter = 20, em_maxiter = 100, verbose = false) + # mix_calc_prob = exp.(MAR(mix, data_marg, weights)) + # for mix_pair in zip(true_prob, mix_calc_prob) + # @test mix_pair[1] ≈ mix_pair[2] atol=0.1 + # end + + # test_complete_mar(mix, data_full, weights, 0.1) end @testset "Marginals batch" begin diff --git a/test/queries/pr_constraint_tests.jl b/test/queries/pr_constraint_tests.jl index d3eb6cb8..73a1c300 100644 --- a/test/queries/pr_constraint_tests.jl +++ b/test/queries/pr_constraint_tests.jl @@ -6,24 +6,16 @@ using ProbabilisticCircuits # two nodes simplevtree = zoo_vtree_file("simple2.vtree") - pc, vtree = load_struct_prob_circuit( - zoo_psdd_file("simple2.4.psdd"), simplevtree) - - + simplepsdd = zoo_psdd_file("simple2.4.psdd") + pc = read((simplepsdd, simplevtree), StructProbCircuit) + @test pr_constraint(pc, pc) ≈ 1.0 - file_circuit = "little_4var.circuit" - file_vtree = "little_4var.vtree" - logic_circuit, vtree = load_struct_smooth_logic_circuit( - zoo_lc_file(file_circuit), zoo_vtree_file(file_vtree)) - - pc, _ = load_struct_prob_circuit(zoo_psdd_file("little_4var.psdd"), zoo_vtree_file("little_4var.vtree")) - - @test pr_constraint(pc, children(logic_circuit)[1]) ≈ 1.0 - # Test with two psdds - pc1, vtree = load_struct_prob_circuit(zoo_psdd_file("simple2.5.psdd"), simplevtree) - pc2, vtree = load_struct_prob_circuit(zoo_psdd_file("simple2.6.psdd"), simplevtree) + pc1files = (zoo_psdd_file("simple2.5.psdd"), simplevtree) + pc2files = (zoo_psdd_file("simple2.6.psdd"), simplevtree) + pc1 = read(pc1files, StructProbCircuit) + pc2 = read(pc2files, StructProbCircuit) @test pr_constraint(pc1, pc2) ≈ 1