- A neural predicate turns a PyTorch network into a logical relation. Its facts carry probabilities that the network produces.
- Differentiable ILP (Inductive Logic Programming — learning a rule from examples) searches candidate Datalog clauses with gradient descent and promotes the winner into your program.
A loss on a symbolic query backpropagates through the cached circuit into the network's weights; only weights change between iterations.
When to use this
Reach for a neural predicate when you already know the rule but a fact’s truth depends on raw data — an image, an embedding, a sensor reading — and you want a network to supply that truth. Reach for differentiable ILP when you have examples of what should be true and want the engine to find the clause that explains them, rather than writing it by hand.Neural predicates, differentiable ILP, and exact induction are distributed
through the
pyxlog PyPI wheel. This is a Python-first API; the crates published
on crates.io do not expose it, so install pyxlog to use anything on this page.Neural predicates
A neural predicate declares that a relation’s truth is decided by a network rather than by stated facts. You write the declaration in the program with::, binding a
registered network name to a predicate head:
nn/4. It reads: for input X, the network
coin_net produces a distribution over the labels [heads, tails], and that
distribution becomes the probability of coin(X, heads) and coin(X, tails). The
label list makes this a classification predicate — the network output is a
distribution over categorical labels.
Drop the label list and you get the three-argument form, nn/3, an embedding
predicate:
E instead of a label
distribution. A given network may be declared in one form or the other, but not
both. Declaring the same name as both classification and embedding is rejected at
compile time.
Declarations are validated when the program compiles: input and output variables
must be disjoint, every argument must appear in the bound predicate, and anonymous
or aggregate variables are not allowed in a neural declaration.
Registering networks
The declaration only names a network. You supply the actualtorch.nn.Module from
Python at registration, which is where xlog wires the network into PyTorch’s
automatic-differentiation (autograd) machinery so gradients can flow through it.
register_network(name, module, optimizer, scheduler=None, ...)binds a classifier, ornn/4head. You pass the module and its optimizer, and optionally a learning-rate scheduler. The bridge holds them and drives the forward and backward passes through the circuit. Batching, top-ktruncation, determinism, and a call cache are configurable here.register_embedding(name, module_or_tensor, trainable=True)binds annn/3embedding — either a module or a plain tensor of vectors. Settrainable=Falseto freeze it.
Declaring the network’s shape
register_network takes three more keyword-only arguments that record what shape the
network is: arity=, arg_sorts= and artifact_hash=.
xlog does not take arity= on trust. It checks the number you pass against every
nn/4 declaration bound to that name in your program, and a mismatch is a
ValueError naming the predicate that disagreed. arg_sorts= needs one entry per
declared argument, and passing it without arity= is refused — the sorts name the
arguments, so there has to be an arity for them to name.
program.network_metadata(name) reads the registration back together with the
declarations it was checked against. For the coin classifier in the
runnable example below:
ValueError
if the name is not declared in the program, is declared as an embedding, or has not
been registered yet.
Once a network is registered, evaluating a query that touches the predicate does
three things. It runs the network. It converts the output into probabilities on the
circuit’s input nodes. And during training, it pushes gradients back through those
nodes into the module’s parameters.
The bridge also exposes forward_backward(query, expected=True), several
differentiable losses (belnap_loss, semantic_loss_tensor, mse_loss_tensor),
and epoch drivers (train_epoch, train_model) with optional validation-set early
stopping.
Differentiable ILP
Writing a neural predicate assumes you already know the rule. Differentiable ILP is for the opposite case: you have examples of what should be true and want the engine to discover the clause. xlog frames this as learning which body to attach to a head. You mark a clause as learnable with a mask annotation:τ — turns those scores into a soft selection over candidates. The
circuit evaluates under that soft mask, a loss compares the derived facts against
your examples, and the gradient updates the scores.
As training proceeds, the temperature τ anneals toward a floor on a cosine
schedule. Lower temperature sharpens the soft mask toward a single hard choice
(one-hot). At convergence the engine takes argmax over the scores — it keeps the
single highest-scoring candidate. That winner is decoded back into a concrete Datalog
clause: a discrete rule string, not a soft mixture.
Sparse and dense mask backends
How the mask reaches the executor is chosen by a backend:
The sparse backend is what keeps training inside the GPU-resident hot loop: candidate
probabilities are ranked and applied on the device, without downloading a full mask
vector to the host CPU.
The Python training API
Two entry points drive learning, both inpyxlog.ilp.
train_only(...) runs the search. It enumerates the legal candidates and launches
several independent restarts from fresh scores. Each restart iterates the step loop:
apply the mask, evaluate on the device, compute the loss, back-propagate, step the
optimizer, anneal τ, and stop early once the argmax winner is stable and the loss
is below threshold. It returns the discovered rule and its training telemetry.
train_and_promote(...) wraps train_only with a set of promotion gates —
checks a discovered rule must pass before xlog commits it into your program. A rule
is written into your committed source only if it clears every gate:
A rule that fails any gate is reported along with the gate it failed. It is never
silently promoted.
Reliability gates
The search is stochastic, so different random seeds can converge or not. To keep that honest, dILP is held to seed-level reliability gates that grow stricter as the subsystem matures. A gate like5/5 means “five consecutive runs must all succeed.”
A runnable neural-predicate example
This is a self-contained DeepProbLog-style program built on a singlenn/4
declaration. A small classifier decides
whether each coin image shows heads or tails, symbolic rules define winning and
losing, and the network is trained purely from the derived symbolic queries. The
synthetic tensors let it run without a dataset download.
prob("win(0, 2)") is close to 1.0
— both inputs are even, so both classify as heads, so win fires. prob("lose(1, 3)")
is close to 1.0 too, because odd inputs classify as tails. If both stay near their
untrained values, the network did not learn the coin signal.
nll_loss is the same quantity train_epoch minimizes, so you can also read it
directly: a loss near 0 means the head is nearly certain, and a large loss means it
is nearly impossible. Use evaluate_loss(queries) for the mean over a whole list.
The optimizer here is Adam. On this multiplicative loss surface plain SGD tends to
plateau, so Adam is the practical default.
Existential joins, mixtures, and graded masses
- Existential-join trainable bodies (v0.10.0). A trainable body may join a
neural predicate to an ordinary relation on a variable that is not in the head. The
neural predicate is grounded over the real join domain inside the circuit, then
combined at the head with a logical OR. Per-event features arrive through a
domain_inputs=channel andregister_domain_tensor_source. The join domain must be ground facts, and only a single join network is supported. Head-binding ids must be0..N-1, row-aligned with your targets; the join domain’s own ids need not be dense — seedomain_idsbelow. The planted graph is bounded (roughly six to seven events) by a fixed circuit buffer. - Joint multi-rule mixtures (v0.10.0). Several trainable clauses that share a head can be
learned jointly as a noisy-OR mixture — a standard way to combine independent
pieces of evidence into one probability.
evaluate_joint_mixturereads the held-out result. A candidate may be guard-only, or it may carry a neural conjunct (neural_bodies=). That conjunct is a small trained head whose output is hard-thresholded (using a straight-through estimator, which passes gradients as if the threshold were smooth). The thresholded output acts as an on/off gate on the candidate’s eligibility, so it stays a derivation gate rather than soft truth mass. - Graded per-binding candidate masses (v0.10.0).
train_neurosymbolic_program(..., candidate_masses={rule_id: tensor})supplies per-binding confidences in[0, 1]. Each confidence multiplies into a candidate’s eligibility, so the head probability becomes the noisy-OR over graded evidence masses. Map head bindings to world steps and masses to a fact’s per-step confidence, and the guards train against an evolving trajectory. Omit the argument and the plain binary behavior is unchanged. - GPU-resident zero-host training step (v0.10.0).
forward_backward_grouped(queries, expected)performs one host synchronization per step instead of a per-query round-trip, keeping the training loop resident on the device. - Row-addressed join domains (v0.11.0).
train_neurosymbolic_program(..., domain_ids={net: [...]})states which domain constant occupies each row ofdomain_inputs[net]. The ids may be given in any order but must be distinct, and the same list is handed to the engine and to the torch path, so both resolve a constant to its row the same way. Omit it and xlog assumes the dense identity0..D-1, which it accepts only when the join relation’s domain is exactly that range. - Backbone coupling (v0.12.0). Set
train_phi_gradient=Trueon aNeuralBodySpecand the entity featuresphi(x)are no longer detached on the training upload, so gradient reaches the feature producer too. It is off by default, and off is byte-identical to the previous path — only the autograd linkage changes, never the uploaded values.
The higher-level neuro-symbolic driver
(
pyxlog.ilp.neurosymbolic.train_neurosymbolic_program, with declarative
trainable_rule(...) / train(...) in-source) is where the existential-join domain_inputs=
channel is exposed.See also
Event-Calculus rule induction
Rules for when a situation starts and stops, learned from timestamped data — with the
neural detector trained through the logic alone, and a statistical gate that abstains
when the evidence is too thin.
Bounded exact induction
The non-gradient counterpart — deterministic GPU enumeration of candidate 2-body
rules across four fixed topologies.
Python bindings
The full
pyxlog API surface for registration, training, and querying.Arrow and DLPack interop
How XLOG results and gradient tensors cross into PyTorch without a host round-trip.