dataframe-learn-2.0.0.0: src-internal/DataFrame/SymbolicRegression/GP.hs
{- | A compact generational genetic-programming search over 'SRExpr': ramped
initialization, tournament selection, subtree crossover/mutation, elitism, and a
complexity-keyed Pareto archive. Deterministic given the seed.
-}
module DataFrame.SymbolicRegression.GP (
GPParams (..),
runGP,
) where
import Data.List (foldl', minimumBy, sortBy)
import qualified Data.Map.Strict as M
import Data.Ord (comparing)
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import DataFrame.Random (Gen, nextDouble, nextIntR)
import DataFrame.SymbolicRegression.Expr
import DataFrame.SymbolicRegression.Optimize (
meanSquaredError,
optimizeConstants,
)
import DataFrame.SymbolicRegression.Simplify (simplify)
-- | GP hyper-parameters resolved from the public config.
data GPParams = GPParams
{ gpFeats :: !(V.Vector (VU.Vector Double))
, gpN :: !Int
, gpTarget :: !(VU.Vector Double)
, gpNVars :: !Int
, gpUnOps :: ![UnOp]
, gpPopSize :: !Int
, gpGenerations :: !Int
, gpMaxSize :: !Int
, gpTournament :: !Int
, gpCrossoverP :: !Double
, gpMutationP :: !Double
, gpOptimizeP :: !Double
, gpParsimony :: !Double
}
type Scored = (SRExpr, Double)
-- | Run the search; returns @(best, pareto front, generations run)@.
runGP :: GPParams -> Gen -> (SRExpr, [(Int, Double, SRExpr)], Int)
runGP p g0 =
let (pop0, g1) = initPop p g0
scored0 = map (scoreOf p) pop0
arch0 = foldl' (archiveInsert p) M.empty scored0
(_, finalArch, gN, _) =
iterate' 0 scored0 arch0 g1
best = bestOfArchive finalArch
front =
[ (sz, mse, e)
| (sz, (mse, e)) <- M.toList finalArch
]
in (snd3 best, sortBy (comparing fst3) front, gN)
where
iterate' gen pop arch g
| gen >= gpGenerations p = (pop, arch, gen, g)
| otherwise =
let (pop', g') = nextGen p pop g
arch' = foldl' (archiveInsert p) arch pop'
in iterate' (gen + 1) pop' arch' g'
fst3 (a, _, _) = a
snd3 (_, b, _) = b
bestOfArchive arch =
case M.toList arch of
[] -> (0 :: Int, SConst 0, 1 / 0)
xs ->
let (sz, (mse, e)) = minimumBy (comparing (fst . snd)) xs
in (sz, e, mse)
scoreOf :: GPParams -> SRExpr -> Scored
scoreOf p e = (e, meanSquaredError (gpFeats p) (gpN p) (gpTarget p) e)
fitness :: GPParams -> Scored -> Double
fitness p (e, mse) = mse + gpParsimony p * fromIntegral (srSize e)
archiveInsert ::
GPParams -> M.Map Int (Double, SRExpr) -> Scored -> M.Map Int (Double, SRExpr)
archiveInsert _ arch (e, mse)
| isNaN mse || isInfinite mse = arch
| otherwise =
let key = srSize (simplify e)
in M.insertWith better key (mse, e) arch
where
better newv@(m1, _) oldv@(m2, _) = if m1 < m2 then newv else oldv
initPop :: GPParams -> Gen -> ([SRExpr], Gen)
initPop p = go (gpPopSize p) []
where
go 0 acc g = (acc, g)
go k acc g =
let (depth, g1) = nextIntR (1, 4) g
(e, g2) = randomExpr p depth g1
in go (k - 1) (e : acc) g2
randomExpr :: GPParams -> Int -> Gen -> (SRExpr, Gen)
randomExpr p depth g
| depth <= 1 = randomLeaf p g
| otherwise =
let (r, g1) = nextDouble g
in if r < 0.3
then randomLeaf p g1
else
let (isUn, g2) = nextDouble g1
in if isUn < 0.3 && not (null (gpUnOps p))
then
let (oi, g3) = nextIntR (0, length (gpUnOps p) - 1) g2
(e, g4) = randomExpr p (depth - 1) g3
in (SUn (gpUnOps p !! oi) e, g4)
else
let (oi, g3) = nextIntR (0, length allBinOps - 1) g2
(a, g4) = randomExpr p (depth - 1) g3
(b, g5) = randomExpr p (depth - 1) g4
in (SBin (allBinOps !! oi) a b, g5)
randomLeaf :: GPParams -> Gen -> (SRExpr, Gen)
randomLeaf p g =
let (r, g1) = nextDouble g
in if r < 0.6 && gpNVars p > 0
then let (j, g2) = nextIntR (0, gpNVars p - 1) g1 in (SVar j, g2)
else let (c, g2) = nextDouble g1 in (SConst (c * 4 - 2), g2)
nextGen :: GPParams -> [Scored] -> Gen -> ([Scored], Gen)
nextGen p pop g0 =
let elite = minimumBy (comparing (fitness p)) pop
(rest, g1) = go (gpPopSize p - 1) [] g0
in (elite : rest, g1)
where
go 0 acc g = (acc, g)
go k acc g =
let (child, g') = breed p pop g
scored = optimizeMaybe p child g'
in go (k - 1) (fst scored : acc) (snd scored)
optimizeMaybe :: GPParams -> SRExpr -> Gen -> (Scored, Gen)
optimizeMaybe p e g =
let (r, g1) = nextDouble g
e' =
if r < gpOptimizeP p
then optimizeConstants (gpFeats p) (gpN p) (gpTarget p) 15 e
else e
in (scoreOf p e', g1)
breed :: GPParams -> [Scored] -> Gen -> (SRExpr, Gen)
breed p pop g0 =
let (pa, g1) = tournament p pop g0
(doX, g2) = nextDouble g1
(child, g3) =
if doX < gpCrossoverP p
then
let (pb, g2') = tournament p pop g2
(c, g3') = crossover pa pb g2'
in (c, g3')
else (pa, g2)
(doM, g4) = nextDouble g3
(child', g5) =
if doM < gpMutationP p then mutate p child g4 else (child, g4)
capped = if srSize child' > gpMaxSize p then pa else child'
in (simplify capped, g5)
tournament :: GPParams -> [Scored] -> Gen -> (SRExpr, Gen)
tournament p pop g0 =
let (picks, g1) = pickN (gpTournament p) g0
chosen = map (pop !!) picks
in (fst (minimumBy (comparing (fitness p)) chosen), g1)
where
n = length pop
pickN 0 g = ([], g)
pickN k g =
let (i, g') = nextIntR (0, n - 1) g
(is, g'') = pickN (k - 1) g'
in (i : is, g'')
crossover :: SRExpr -> SRExpr -> Gen -> (SRExpr, Gen)
crossover a b g0 =
let (ia, g1) = nextIntR (0, srSize a - 1) g0
(ib, g2) = nextIntR (0, srSize b - 1) g1
sub = subtreeAt ib b
in (replaceAt ia a sub, g2)
mutate :: GPParams -> SRExpr -> Gen -> (SRExpr, Gen)
mutate p e g0 =
let (i, g1) = nextIntR (0, srSize e - 1) g0
(depth, g2) = nextIntR (1, 3) g1
(newSub, g3) = randomExpr p depth g2
in (replaceAt i e newSub, g3)
subtreeAt :: Int -> SRExpr -> SRExpr
subtreeAt 0 e = e
subtreeAt i (SUn _ e) = subtreeAt (i - 1) e
subtreeAt i (SBin _ a b) =
let sa = srSize a
in if i <= sa then subtreeAt (i - 1) a else subtreeAt (i - 1 - sa) b
subtreeAt _ e = e
replaceAt :: Int -> SRExpr -> SRExpr -> SRExpr
replaceAt 0 _ new = new
replaceAt i (SUn op e) new = SUn op (replaceAt (i - 1) e new)
replaceAt i (SBin op a b) new =
let sa = srSize a
in if i <= sa
then SBin op (replaceAt (i - 1) a new) b
else SBin op a (replaceAt (i - 1 - sa) b new)
replaceAt _ e _ = e