Quickstart¶
Every example on this page runs as shown against rnel 0.2.0; the printed values are in the comments.
The blocks build on each other, so run them in order in one session.
1. Contradiction is not a fair coin¶
Two confident sources that disagree, and one source whose evidence is balanced, give the same Subjective Logic opinion after fusion. RNEL keeps them apart with the contradiction component C (Proposition 8.3 and Definition 8.4).
from rnel import Reports, rnel_tuple, fused_contradiction, sl_opinion
dispute = fused_contradiction([Reports(t=10), Reports(f=10)]) # two sources that disagree
coin = fused_contradiction([Reports(t=10, f=10)]) # one source, balanced evidence
print(sl_opinion(Reports(t=10) + Reports(f=10)) == sl_opinion(Reports(t=10, f=10))) # True
print(round(dispute.C, 3), round(coin.C, 3)) # 0.579 0.0
2. The evidence tuple¶
Reports counts reports of each type about a proposition x; rnel_tuple turns them into the tuple
(T, C, U, N, G, F) of Definition 8.1. Each component is its count divided by the total count plus the prior
weight W (default 2), and G = W / (total + W).
t = rnel_tuple(Reports(t=3, f=1, c=2, v=1, n=1))
print({k: round(v, 3) for k, v in t.as_dict().items()})
# {'T': 0.3, 'C': 0.2, 'U': 0.1, 'N': 0.1, 'G': 0.2, 'F': 0.1}
print(t.to_sl()) # coarsening to SL: T -> belief, F -> disbelief, the rest -> uncertainty
# Opinion(b=0.3, d=0.1, u=0.6, a=0.5)
3. From a tuple to an action¶
Policy maps a tuple to one of six actions. The thresholds are defaults; set them on validation data with the
costs of your application.
from rnel.decide import Policy
policy = Policy()
print(policy.decide(dispute)) # ('C', 'present both positions and flag the dispute')
print(policy.decide(rnel_tuple(Reports()))) # ('G', 'retrieve more evidence or abstain')
print(policy.decide(rnel_tuple(Reports(t=9)))) # ('T', 'answer: supported')
4. Subjective Logic¶
rnel.sl implements binomial opinions and the Subjective Logic operators that RNEL extends.
from rnel.sl import Opinion, cumulative_fusion, degree_of_conflict, discount
A = Opinion.from_evidence(8, 2) # 8 reports for, 2 against, W = 2
B = Opinion.from_evidence(1, 9)
print(round(A.projected, 3)) # 0.75
print(round(degree_of_conflict(A, B), 3)) # 0.405
print(cumulative_fusion(A, B).u == Opinion.from_evidence(9, 11).u) # True
print(discount(0.5, A))
# Opinion(b=0.3333333333333333, d=0.08333333333333333, u=0.5833333333333333, a=0.5)
5. Logical operators¶
Tuples are plain dictionaries for the operators. RNEL negation swaps only T and F: contradiction, undetermined, neither and ignorance are fixed points. Conjunction is a priority product whose coarsening is SL multiplication.
from rnel.operators import rnel_and, rnel_not, coarsen
p = t.as_dict()
q = rnel_tuple(Reports(t=5, f=1)).as_dict()
print(rnel_not(p) == {**p, "T": p["F"], "F": p["T"]}) # True
r = rnel_and(p, q)
print({k: round(v, 3) for k, v in r.items()})
# {'T': 0.188, 'C': 0.15, 'U': 0.075, 'N': 0.075, 'G': 0.3, 'F': 0.212}
print(tuple(round(v, 3) for v in coarsen(r))) # (b, u, d) = (0.188, 0.6, 0.212)
Base-rate-calibrated versions (calibrated_and, calibrated_or) reproduce SL multiplication and
comultiplication for arbitrary base rates:
from rnel.operators import calibrated_and, tinf_and
from rnel.sl import multiply
x = {"T": 0.5, "I": 0.3, "N": 0.0, "F": 0.2}
y = {"T": 0.4, "I": 0.4, "N": 0.0, "F": 0.2}
z = calibrated_and(x, y, ax=0.3, ay=0.6, conj=tinf_and)
m = multiply(Opinion(0.5, 0.2, 0.3, 0.3), Opinion(0.4, 0.2, 0.4, 0.6))
print(round(z["T"], 6) == round(m.b, 6), round(z["F"], 6) == round(m.d, 6)) # True True
6. Ranking¶
Total-order cascades (score, then accuracy, then extended certainty, then certainty) for (T, I, N, F) tuples.
from rnel.ranking import rank, tinf_key
alternatives = [
{"T": 0.3, "I": 0.3, "N": 0.2, "F": 0.2},
{"T": 0.6, "I": 0.2, "N": 0.1, "F": 0.1},
{"T": 0.6, "I": 0.1, "N": 0.1, "F": 0.2},
]
print(rank(alternatives, key=tinf_key)) # [1, 2, 0] (indices, best first)
7. Retracting evidence (experimental)¶
rnel.off.EvidenceLedger records reports per source and lets you retract them. As long as no source retracts
more than it reported, the result is an ordinary SL opinion; removing a source is SL cumulative unfusion.
from rnel.off import EvidenceLedger
# 40 positive reviews and 10 negative; 25 positive ones turn out to be fake
shop = EvidenceLedger().report("shop", 40, 10).retract("shop", for_x=25)
print(shop.net()) # (15.0, 10.0)
print(shop.opinion() == Opinion.from_evidence(15, 10)) # True
# dropping a whole source recovers the opinion of the others
L = EvidenceLedger().report("A", 8, 2).report("B", 3, 1).retract_source("B")
print(L.opinion() == Opinion.from_evidence(8, 2)) # True
Over-retraction is rejected by default. On request it is returned as a flagged off-opinion, a diagnostic that is not a probability:
import warnings
bad = EvidenceLedger().report("A", 2, 0).retract("A", for_x=3)
try:
bad.opinion()
except ValueError:
print("rejected") # rejected
with warnings.catch_warnings():
warnings.simplefilter("ignore")
off = bad.opinion(allow_off=True)
print(round(off.b, 3), off.in_simplex, off.beta_admissible) # -1.0 False False
8. From text (optional, needs transformers and torch)¶
rnel.text reads each piece of evidence against a claim with a public NLI model, with no training. The model
(cross-encoder/nli-deberta-v3-base) is downloaded on first use.
from rnel.text import NLIReader, rnel_from_text
claim = "Coffee increases the risk of heart disease."
evidence = [
"A cohort found higher coffee intake linked to more heart disease.",
"A meta-analysis found moderate coffee lowers the risk of heart disease.",
]
tup, readings = rnel_from_text(NLIReader(), claim, evidence)
print(Policy().decide(tup)[0]) # C
NLI models often label unrelated text as "contradiction", so give only evidence that is about the claim.
9. Neural evidential head (optional, needs torch)¶
rnel.nn.RNELHead maps features to non-negative typed evidence (T, F, C, U, N); rnel_from_evidence turns
evidence into the tuple of Definition 8.1 and typed_evidential_loss trains it from typed labels.
import torch
from rnel.nn import RNELHead, rnel_from_evidence, typed_evidential_loss
torch.manual_seed(0)
head = RNELHead(in_dim=16)
features = torch.randn(4, 16)
labels = torch.tensor([0, 1, 2, 4]) # indices into (T, F, C, U, N)
evidence = head(features) # shape (4, 5)
tuples = rnel_from_evidence(evidence) # shape (4, 6): T, F, C, U, N, G
print(tuples.shape, bool(torch.allclose(tuples.sum(-1), torch.ones(4)))) # torch.Size([4, 6]) True
loss = typed_evidential_loss(evidence, labels)
loss.backward()