packages feed

hmep-0.1.1: AI/MEP/Run.hs

-- |
-- = Various utilities for running MEP algorithm

{-# LANGUAGE BangPatterns #-}

module AI.MEP.Run (
      generateCode
    , evaluateChromosome
    , regressionLoss1
    , avgLoss
  ) where

import qualified Data.Vector as V
import qualified Data.Vector.Mutable as VM
import           Data.List (
                   foldl'
                 , nub
                 , sort
                 )
import           System.IO.Unsafe ( unsafePerformIO )
import           Text.Printf

import           AI.MEP.Types

-- | Evaluate each subexpression in a chromosome
evaluateChromosome :: Num a
         => Chromosome a  -- ^ Chromosome to evaluate
         -> V.Vector a    -- ^ Variable values
         -> V.Vector a    -- ^ Resulting vector of multiple evaluations
evaluateChromosome chr vmap = unsafePerformIO $ do
  -- Use dynamic programming to evaluate the chromosome
  v <- VM.new chrLen

  let -- Gene evaluation function
      _f (C c) _ = return c
      _f (Var n) _ = return $ vmap V.! n
      _f (Op (_, f) i1 i2) v' = do
        !r1 <- v' `VM.read` i1
        !r2 <- v' `VM.read` i2
        let !r = f r1 r2
        return r

      -- Chromosome evaluation
      go !v' !j =
        if j == chrLen
           then return ()
           else do
             val <- _f (chr V.! j) v'
             VM.write v' j val
             go v' (j + 1)

  go v 0

  V.unsafeFreeze v
    where chrLen = V.length chr
{-# SPECIALIZE
  evaluateChromosome :: Chromosome Double
           -> V.Vector Double
           -> V.Vector Double #-}

-- | Generate code for the functions with a single output
generateCode :: Phenotype Double -> String
generateCode (_, chr, i) = concat expr1 ++ expr2
  where
    -- A part of chromosome that is used (all genes ahead the `finalI`
    -- and the gene pointed by the `finalI`)
    chr' = V.slice 0 (finalI + 1) chr
    last' = chr' V.! finalI

    finalI = V.head i
    expr1 = map (\k -> _f (chr' V.! k) k). sort. nub $ _usedGeneIx chr'
    expr2 = printf "result = %s\n" $ _h last'

    _f (C c) _ = ""
    _f (Var i) _ = ""
    _f op k = printf "v%d = %s\n" k (_h op)

    _h (C c) = show c
    _h (Var i) = printf "x%d" i
    _h (Op (s, _) i1 i2) = if isInfix s
      then printf "%s %c %s" g1 s g2
      else printf "%c %s %s" s g1 g2
        where g1 = _g (chr' V.! i1) i1
              g2 = _g (chr' V.! i2) i2

    _g (C c) _ = show c
    _g (Var i) _ = printf "x%d" i
    _g Op {} k = printf "v%d" k

    -- Very naive infix operator check. No problem for single-character
    -- ASCII operator representations. Otherwise, please improve.
    isInfix x = x `notElem` (['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'])

-- Active genes in case of a chromosome representing a single-output function.
-- Can be generalized to multiple outputs by several calls
-- changing `lastPos` as an argument.
_usedGeneIx :: Chromosome a -> [Int]
_usedGeneIx chr = foldl' _g base $ zip pos $ map (chr V.!) pos
  where
    -- Position indices
    pos = [lastPos - 1,lastPos - 2..0]

    _g xs (i, Op _ i1 i2) = if i `elem` xs
                           -- Next expressions depend on these
                           then i1: i2: xs
                           -- Dead gene, skip
                           else xs
    _g xs _ = xs  -- Terminal symbol, already counted

    base = case last' of
      (Op _ i1 i2) -> [i1, i2]
      _ -> []  -- Sadly, a terminal symbol

    last' = chr V.! lastPos
    lastPos = V.length chr - 1

-- | Loss function for regression problems with
-- one input and one output.
-- Not normalized with respect to the dataset size.
regressionLoss1
  :: (Num result, Ord result) =>
     (b -> b -> result)  -- ^ Distance function
     -> [(a, b)]         -- ^ Dataset
     -> (V.Vector a -> V.Vector b)
     -- ^ Chromosome evaluation function (partially applied 'evaluate')
     -> (V.Vector Int, result)
regressionLoss1 dist dataset evalf = (V.singleton i', loss')
  where
    (xs, ys) = unzip dataset
    -- Distances resulting from multiple expression evaluation
    dss = zipWith (\x y -> V.map (dist y). evalf. V.singleton $ x) xs ys
    -- Cumulative distances for each index
    dcumul = sum' dss
    -- Select index minimizing cumulative distances
    i' = V.minIndex dcumul
    -- The loss value with respect to the index of the best expression
    loss' = dcumul V.! i'
{-# SPECIALIZE
  regressionLoss1
    ::
      (Double -> Double -> Double)
      -> [(Double, Double)]
      -> (V.Vector Double -> V.Vector Double)
      -> (V.Vector Int, Double)
  #-}

-- Could be optimized
sum' :: Num a => [V.Vector a] -> V.Vector a
sum' xss = foldl' (V.zipWith (+)) base xss
  where
    len = V.length $ head xss
    base = V.replicate len 0
{-# SPECIALIZE sum' :: [V.Vector Double] -> V.Vector Double #-}

-- | Average population loss
avgLoss :: Generation Double -> Double
avgLoss = uncurry (/). foldl' (\(c, i) (val, _, _) -> (c + val, i + 1)) (0, 0)