Library for relational verification of probabilistic algorithms.
See branch icfp-22-artifact for the older version that corresponds to our ICFP'22 submission.
Supports two proving modes:
- Upper bound Kantorovich distance between two distributions
- Establish a boolean relation on a coupling of two distributions
Includes two larger examples of verification:
- Stability of stochastic gradient descent (src/SGD) using Kantorovich distance
- Convergence of temporal difference learning (src/TD0) using lifted boolean relations
This function recursively counts how many times the ball hit the bin after n attempted throws:
bins :: Double -> Nat -> PrM Nat
bins _ 0 = ppure 0
bins p n = liftA2 (+) (bernoulli p) (bins p (n - 1))
Throws succeed with probability p which is simulated by bernoulli p
. The function returns a distribution over natural numbers. When comparing the results of two throwers with respective chances of success p and q > p, we expect the second thrower to score notably better with the increase of n. Formally, we can show that Kantorovich distance between bins p n
and bins q n
is upper bounded by (q - p)·n.
The proof uses four definitions from the library:
- In the first case, no throws were made. Axiom
pureDist
allows deriving Kantorovich distance between pure expressions. In our case, 0 and 0. - In the second case, axiom
liftA2Dist
derives Kantorovich distance between the inductive cases. Numeric arguments specify the expected bound in the format a·x + b·y + c where x and y are bounds for the second and third arguments ofliftA2
respectively. As the last argument, the axiom requires proof of linearity of plus. It is empty since it can be automatically constructed by an SMT-solver. - Axiom
bernoulliDist
upper bounds the distance between calls tobernoulli
with q - p — this is our x. The second upper bound y is provided by a recursive call to our theorem. - A function
distInt
is used to measure the distance between arguments ofliftA2
. In this case, all of them provide integer values. A pre-defined distance between n and m is |n - m| but this allows customization.
{-@ binsDist :: p:Prob -> {q:Prob|p <= q} -> n:Nat
-> {dist (kant distInt) (bins p n) (bins q n) <= n * (q - p)} / [n] @-}
binsDist :: Prob -> Prob -> Nat -> ()
binsDist p q 0 = pureDist distInt 0 0
binsDist p q n
= liftA2Dist d d d 1 (q - p) 1 ((n - 1) * (q - p)) 0
(+) (bernoulli p) (bins p (n - 1))
(+) (bernoulli q) (bins q (n - 1))
(bernoulliDist d p q)
(binsDist p q (n - 1))
(\_ _ _ _ -> ())
where
d = distInt
This concludes the mechanized proof of the boundary (q-p)·n.
-
Install stack https://docs.haskellstack.org/en/stable/install_and_upgrade/
-
Compile the library and case studies
$ cd safe-coupling $ stack install --fast ... Registering library for safe-coupling-0.1.0.0..
-
Run unit tests on executable case studies
$ stack test ... test/Spec.hs Spec Bins mockbins 1 it: OK mockbins 2 it: OK bins 1 it: OK bins 2 it: OK (0.02s) exp dist mockbins: OK (0.12s) SGD sgd: OK TD0 td0 base: OK td0 simple: OK All 8 tests passed (0.12s) safe-coupling> Test suite safe-coupling-test passed Completed 2 action(s).
In case of errors try
$ stack clean