Notes on a Dempster-Shafer Theory Implementation
An introduction to Dempster-Shafer Theory can be found within the thorough reports of Sentz and Ferson1 or El-Mahassni and White2. This implementation is in futhark pseudocode.
There’s three functions to compute: \(bel\), \(pl\), and \(comb\). Skipping \(comb\) for later, given the universe \(X\)…
\[bel(A) = \sum_{B \vert B \subseteq A} m(B). \ \ \ \ \ \ pl(A) = \sum_{B \vert B \cap A \neq \emptyset} m(B).\]A potential implementation might involve t
as a tuple composing some \(\vert X \vert\)-bit bitset
and an accompanying real
to represent mass.
type u -- some bitset implementation.
type m = f64
type t = (u, m)
val bel [f] : [f]t -> u -> m
val pl [f] : [f]t -> u -> m
def bel e a =
map (\(b_s, b_m) ->
if (X.is_subset b_s a)
then b_m else 0.0f64
) e |> f64.sum
def pl e a = -- An alternative expression of `pl` that only needs `bel`.
complement a |> bel e |> (f64.-) 1.0f64
A problem arises with \(comb\) rules; consider Dempster’s rule of combination:
\[(m_{1} \oplus m_{2})(A) = \frac{1}{1-K} \sum_{B \cap C = A \neq \emptyset} m_{1}(B)m_{2}(C)\]Forming the basic belief assignment requires computing all possible intersections of \(m_{1}\) and \(m_{2}\),
or a function with the signature [b]t -> [c]t -> [b*c]t
. There’s two problems here:
[b]t -> [c]t -> [b*c]t
implies exponential growth. Looking closer, this isn’t truly accurate; intersections that produce the empty set can be removed and assignments with the same bitset can be merged. However, these aforementioned cleanups can’t be reasoned about at compile-time and we run into problems with irregular arrays.reduce
takes anop
parameter with the signaturea -> a -> a
. To court this attractive O(log(n)) span,[b]t -> [c]t -> [b*c]t
must become[b]t -> [b]t -> [b*b]t -> [b]t
.
Approximation schemes are a potential solution: we can take the resulting array of [b*c]t
(or the cleaned up []t
) and compute an approximation to a set of focal
elements [a]t
.
Bauer’s paper3 details a few; within it, there’s this bit I found:
A second criterion to assess the quality of an approximation is its ability to induce some “structure” on the given information, …
This “structure” provided by an approximation, in our case, easily enables parallelization of \(comb\) rules.
Both of the two schemes below are rather simple. kx (effectively klx) takes the largest k masses and then normalizes; summarize takes the largest k masses and then concats the union/sum of what’s left over.
val kx [f] : (k: i64) -> [f]t -> [k]t
def kx k e =
let sorted = merge_sort_by_key (.1) (f64.<=) e |> reverse
-- if k is larger than w, we need to pad with `nil`.
in (if k > f
then (++) sorted (replicate (k - f) (nil, 0.0f64)) :> [k]t
else take k sorted
) |> normalize
val summarize [f] : (k: i64) -> [f]t -> [k+1]t
def summarize k e =
let sm (a: []t): t =
let a_union = map (.0) a |> reduce (union) (nil)
let a_sum = map (.1) a |> f64.sum
in (a_union, a_sum)
let sorted = merge_sort_by_key (.1) (f64.<=) e |> reverse
-- if k is larger than w, we need to pad with nil.
in if k > f
then (++) sorted (replicate ((k + 1) - f) (nil, 0.0f64)) :> [k+1]t
else (++) (take k sorted) [(sm (drop k sorted))]
With these, we can write \(comb\) such that it utilizes reduce
.
-- | Combine a set of masses together with a combination rule and an
-- | approximation scheme.
val comb [f][a][b] :
([a]t -> [a]t -> [b]t) -> ([b]t -> [a]t) -> [f][a]t -> [a]t
-- https://futhark-lang.org/examples/no-neutral-element.html
type with_neutral [a] 't = #neutral | #val [a]t
def comb [f][a] rl apx e =
let e_tagged: [f]with_neutral [a]t = map (\z -> #val z) e
let m =
reduce (\m1 m2 -> match (m1, m2)
case (#val b, #val c) -> #val (rl b c |> apx)
case (#neutral, _) -> m2
case (_, #neutral) -> m1
) #neutral e_tagged
in match m
case #neutral -> replicate a (X.nil, 0.0f64)
case #val x -> x
A highly parallel combination routine becomes the pleasing, curried let comb_dempster_kx k = comb comb_dempster (kx k)
.