packages feed

srtree-2.0.1.2: apps/tinygp/GP.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE BangPatterns #-}
module GP where

import Data.SRTree
import Algorithm.SRTree.Opt
import Algorithm.SRTree.Likelihoods
import Data.SRTree.Print
import Data.SRTree.Eval
import Data.SRTree.Recursion ( cata )
import System.Random
import Control.Monad.State.Strict
import Control.Monad
import Data.Vector qualified as V
import Control.Monad (when)
import Data.Massiv.Array qualified as M
import Debug.Trace ( traceShow, trace )
import Util
import Data.List ( intercalate, maximumBy )
import qualified Data.Vector.Mutable as MV

data Method = Grow | Full | BTC
type Rng a = StateT StdGen IO a

type GenUni = Fix SRTree -> Fix SRTree 
type GenBin = Fix SRTree -> Fix SRTree -> Fix SRTree
type FitFun = Individual -> Rng Individual

data Individual = Individual { _tree :: Fix SRTree, _fit :: Double, _params :: [PVector] }

instance Show Individual where 
    show (Individual t f p) = showExpr t <> "," <> show f <> "," <> show p 

toss :: Rng Bool
toss = state random
{-# INLINE toss #-}

randomRange :: (Ord val, Random val) => (val, val) -> Rng val
randomRange rng = state (randomR rng)
{-# INLINE randomRange #-}

randomFrom :: [a] -> Rng a
randomFrom funs = do n <- randomRange (0, length funs - 1)
                     pure $ funs !! n
{-# INLINE randomFrom #-}

randomFromV :: V.Vector a -> Rng a
randomFromV funs = do n <- randomRange (0, length funs - 1)
                      pure $ funs V.! n
{-# INLINE randomFromV #-}

countNodes' :: Fix SRTree -> Int
countNodes' = cata alg 
  where 
    alg (Var _)     = 1
    alg (Param _)   = 1
    alg (Const _)   = 0
    alg (Bin _ l r) = 1 + l + r
    alg (Uni Abs t) = t
    alg (Uni _ t)   = 1 + t
{-# INLINE countNodes' #-}


randomTree :: HyperParams -> Bool -> Rng (Fix SRTree)
randomTree hp grow 
  | depth <= 1 || size <= 2 = randomFrom term 
  | (min_depth >= 0 || (depth > 2 && not grow)) && size > 2 = genNonTerm 
  | otherwise = genTermOrNon
  where 
    min_depth = _minDepth hp
    depth     = _maxDepth hp
    size      = _maxSize hp
    term      = _term hp
    nonterm   = _nonterm hp

    genNonTerm =
       do et <- randomFrom nonterm
          case et of 
            Left uniT -> uniT <$> randomTree hp{_minDepth = min_depth-1, _maxDepth = depth - 1, _maxSize = size - 1} grow
            Right binT -> do l <- randomTree hp{_minDepth = min_depth-1, _maxDepth = depth - 1, _maxSize = size - 1} grow
                             r <- randomTree hp{_minDepth = min_depth-1, _maxDepth = depth - 1, _maxSize = size - 1 - countNodes' l} grow
                             pure (binT l r)
    genTermOrNon = do r <- toss
                      if r
                        then randomFrom term 
                        else genNonTerm        

data HyperParams = 
    HP { _minDepth  :: Int 
       , _maxDepth  :: Int
       , _maxSize   :: Int 
       , _popSize   :: Int
       , _tournSize :: Int
       , _pc        :: Double 
       , _pm        :: Double 
       , _term      :: [Fix SRTree]
       , _nonterm   :: [Either GenUni GenBin] 
       }

tournament :: HyperParams -> V.Vector Individual -> Rng Individual
tournament hp pop = do
  selection <- replicateM (_tournSize hp) (randomFromV $ V.filter (not.isNaN._fit) pop)
  let maxFitness = maximum (fmap _fit selection)
      champions = V.filter ((== maxFitness) . _fit) pop
  if null selection
     then randomFromV pop
     else randomFromV champions

randomIndividual :: HyperParams -> FitFun -> Bool -> Rng Individual
randomIndividual hyperparams fitFun grow = do
    t <- randomTree hyperparams grow 
    let p = countParams t
    --theta' <- replicateM p (randomRange (-1,1))
    fitFun $ Individual t 0.0 [] -- (M.fromList compMode theta' :: PVector)
    --pure ind
    --if isInfinite (_fit ind)
    --   then randomIndividual hyperparams fitFun grow 
    --   else pure ind

initialPop :: HyperParams -> FitFun -> Rng (V.Vector Individual)
initialPop hyperparams fitFun = do 
   let depths = [3 .. _maxDepth hyperparams]
   pop <- forM depths $ \md -> 
           do let m = _popSize hyperparams `div` (_maxDepth hyperparams - 3 + 1)
                  g = V.fromList . take m $ cycle [True, False]
              mapM (randomIndividual hyperparams{ _maxDepth = md} fitFun) g
   pure (V.concat pop)

fitnessMV :: Distribution -> [(SRMatrix, PVector, Maybe PVector)] -> Individual -> Rng Individual
fitnessMV dist datas ind = do
  fs <- forM datas (fitness dist ind)
  let fitOpt = minimum $ map fst fs
  pure ind{_fit = fitOpt, _params = map snd fs}

fitness :: Distribution -> Individual ->  (SRMatrix, PVector, Maybe PVector) -> Rng (Double, PVector)
fitness dist ind (x, y, e) = do
    let tree = relabelParams $ _tree ind
        p    = countParams tree
    theta1' <- M.fromList M.Seq <$> replicateM p (randomRange (-1,1))
    theta2' <- M.fromList M.Seq <$> replicateM p (randomRange (-1,1))
    let (theta1, f1, _) = minimizeNLL dist e 50 x y tree theta1'
        (theta2, f2, _) = minimizeNLL dist e 50 x y tree theta2'
        fit1 = if isNaN f1 then (-1.0/0.0) else negate f1
        fit2 = if isNaN f1 then (-1.0/0.0) else negate f2
        thetaOpt = if fit1 > fit2 then theta1 else theta2
        fitOpt   = max fit1 fit2
    pure (fitOpt, thetaOpt)


mutate :: HyperParams -> Individual -> Rng (Maybe Individual)
mutate hp ind = do
  let sz = countNodes' (_tree ind)
  p <- state $ randomR (0, sz-1)
  b <- state random
  t <- go p (_maxSize hp) (_tree ind)
  --(t, b) <- go sz (_pm hp) (_tree ind)
  if b <= _pm hp && countNodes t <= _maxSize hp
     then pure . Just $ Individual t 0.0 []
     else pure Nothing
      where
        go 0 msz t = randomTree hp{_maxSize = msz-1} True
        go n msz (Fix (Uni f t)) = Fix . Uni f <$> go (n-1) (msz-1) t
        go n msz (Fix (Bin op l r)) = do
          let nl = countNodes l
              nr = countNodes r
          if nl <= n - 1
             then Fix . Bin op l <$> go (n-nl-1) (msz-nl-1) r
             else do l' <- go (n-1) (msz-nr-1) l
                     pure $ Fix $ Bin op l' r

crossover :: HyperParams -> Individual -> Individual -> Rng (Maybe Individual)
crossover hp ind1 ind2 = do
  b <- state random
  if b < (_pc hp)
     then do let n1 = countNodes $ _tree ind1
                 n2 = countNodes $ _tree ind2
             p1 <- state $ randomR (0, n1-1)
             p2 <- state $ randomR (0, n2-1)
             let part1 = pickLeft p1 $ _tree ind1
                 part2 = pickRight p2 $ _tree ind2
                 t = part1 part2
                 n = countNodes t
             if n <= _maxSize hp
                then pure . Just $ ind1{_tree = t}
                else pure Nothing
     else pure Nothing
  where
    pickRight :: Int -> Fix SRTree -> Fix SRTree
    pickRight 0 node = node
    pickRight n (Fix (Uni f t)) = pickRight (n-1) t
    pickRight n (Fix (Bin op l r)) = let nl = countNodes l
                                     in if nl <= n-1
                                           then pickRight (n-nl-1) r
                                           else pickRight (n-1) l
    pickLeft :: Int -> Fix SRTree -> (Fix SRTree -> Fix SRTree)
    pickLeft 0 node = \t -> t
    pickLeft n (Fix (Uni f t)) = let g = pickLeft (n-1) t in \t' -> Fix $ Uni f (g t')
    pickLeft n (Fix (Bin op l r)) = let nl = countNodes l
                                    in if nl <= n-1
                                          then let g = pickLeft (n-nl-1) r in \t -> Fix $ Bin op l (g t)
                                          else let g = pickLeft (n-1) l in \t -> Fix $ Bin op (g t) r


evolve :: HyperParams -> FitFun -> V.Vector Individual -> Rng Individual
evolve hp fitFun pop = do 
    parent1 <- tournament hp pop
    parent2 <- tournament hp pop 
    mChild <- crossover hp parent1 parent2
    child' <- case mChild of
                Nothing    -> mutate hp parent1
                Just child -> mutate hp child
    --let p = countParams (_tree child')
    --theta' <- M.fromList compMode <$> replicateM p (randomRange (-1,1))
    case child' of
        Nothing -> pure parent1
        Just c  -> fitFun c

printFinal dist ind dataTrains dataTests = do
  let tree     = relabelParams $ _tree ind
      thetas   = _params ind
      mseTrain = maximum $ map (\(theta, (x,y,e)) -> nll dist e x y tree theta) $ zip thetas dataTrains
      mseTest  = maximum $ map (\(theta, (x,y,e)) -> nll dist e x y tree theta) $ zip thetas dataTests
      r2Train  = minimum $ map (\(theta, (x,y,e)) -> r2 x y tree theta) $ zip thetas dataTrains
      r2Test   = minimum $ map (\(theta, (x,y,e)) -> r2 x y tree theta) $ zip thetas dataTests
      thetaStr = intercalate "_" $ map (intercalate ";" . map show . M.toList) thetas
  putStrLn "id,Expression,theta,size,MSE_train,MSE_test,R2_train,R2_test"
  putStr $ "0," <> showExpr tree <> "," <> thetaStr <> "," <> show (countNodes tree) <> "," <> show mseTrain <> "," <> show mseTest <> "," <> show r2Train <> "," <> show r2Test

report :: Int -> V.Vector Individual -> IO ()
report gen = mapM_ reportOne
  where reportOne ind = do putStr (show gen)
                           putStr ": "
                           putStr (showExpr (_tree ind))
                           putStr " - " 
                           putStr (show (_fit ind))
                           putStr " "
                           print (map M.toList $ _params ind)
{-# INLINE report #-}

evolution :: Int -> HyperParams -> FitFun -> Rng (Individual)
evolution gen hp fitFun = do 
    pop <- initialPop hp fitFun
    --liftIO $ report (-1) pop
    go gen pop
        where 
            go 0 !pop = pure $ pop  V.! 0
            go n !pop = do
                let best = V.maximumOn _fit $ V.filter (not.isNaN._fit) pop
                pop' <- V.modify (\v -> MV.write v 0 best) <$> V.replicateM (_popSize hp) (evolve hp fitFun pop)
                --liftIO $ report (gen-n) pop'
                go (n-1) pop'