packages feed

imp-ppl-0.1.0.0: src/Imp/Inference/Optimize.hs

-- | Approximate credal inference by gradient ascent over the Knightian
--   weights.  Values are inner approximations: every iterate is a point
--   inside the credal set, so they never overshoot the exact bounds.
--
--   Throws an error if the evidence is unsatisfiable, i.e. the credal set is empty.
module Imp.Inference.Optimize
  ( optimizeExpectation
  , optimizeProbability
  ) where

import Control.Applicative ((<|>))
import Control.Monad (guard)
import Data.Bool (bool)
import Data.Maybe (fromMaybe)
import Data.Ord (clamp)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Vector as V

import Imp.BDD.Compile (compile)
import Imp.BDD.WMC (Weight(..), wmcBatch)
import Imp.DSL (Imp)
import Imp.Semiring

-- | A constant dual number.
dualConst :: Double -> DualS
dualConst x = DualS x V.empty

-- | Dual number division; undefined for a non-positive denominator.
dualDiv :: DualS -> DualS -> Maybe DualS
dualDiv (DualS a da) (DualS b db)
  | b <= 0    = Nothing
  | otherwise = Just (DualS (a / b) (V.zipWith (\a' b' -> (a' * b - a * b') / (b * b)) da db))

-- | The dual-number reading of the variable weights at the given parameters.
dualWeights :: V.Vector Double -> Weight -> (DualS, DualS)
dualWeights params w = case w of
  Prob p   -> (dualConst (1 - p), dualConst p)
  Knight i -> let p = params V.! i
                  k = length params
              in ( DualS (1 - p) (V.generate k (\j -> if j == i then -1 else 0))
                 , DualS p       (V.generate k (\j -> if j == i then  1 else 0)) )

-- | Gradient ascent over the Knightian variables to maximize expected score over
--   the credal set. Ascent starts deterministically at weight @0.5@.
--
--   Returns @(weights, expectation)@ where @weights@ maps each Knightian
--   variable name to its optimized weight.
--
--   Stops early when the gradient magnitude falls below @1e-6@.
--
--   Use a negative learning rate for gradient /descent/ (minimization).
optimizeExpectation :: Ord a
                    => Imp g a
                    -> (a -> Double)
                    -> Int              -- ^ maximum steps
                    -> Double           -- ^ learning rate
                    -> (Map String Double, Double)
optimizeExpectation prog score steps lr =
  let (mgr, weights, knights, worlds) = compile prog
      params0 = V.replicate (Map.size knights) 0.5
      go n params = do
        let counts = wmcBatch (dualWeights params) mgr weights worlds
            aggr   = sumS [ dualConst (score v) .*. count | (v, count) <- Map.toList counts ]
        DualS e de <- dualDiv aggr (sumS (Map.elems counts))
        let params'  = V.zipWith (\p dp -> clamp (0, 1) (p + lr * dp)) params de
            continue = n > 0 && V.sum (V.map (\x -> x * x) de) > 1e-12
        (guard continue >> go (n - 1) params') <|> Just ((params V.!) <$> knights, e)
  in fromMaybe (error "No feasible probabilities") (go steps params0)

-- | Gradient ascent over the Knightian variables to maximize P(event).
optimizeProbability :: Ord a
                    => Imp g a
                    -> (a -> Bool)
                    -> Int              -- ^ maximum steps
                    -> Double           -- ^ learning rate
                    -> (Map String Double, Double)
optimizeProbability prog predicate steps lr =
  optimizeExpectation prog (bool 0 1 . predicate) steps lr