src/AI/GeneticAlgorithm/Simple.hs
-- | Simple parallel genetic algorithm implementation.
--
-- > import AI.GeneticAlgorithm.Simple
-- > import System.Random
-- > import Text.Printf
-- > import Data.List as L
-- > import Control.DeepSeq
-- >
-- > newtype SinInt = SinInt [Double]
-- >
-- > instance NFData SinInt where
-- > rnf (SinInt xs) = rnf xs `seq` ()
-- >
-- > instance Show SinInt where
-- > show (SinInt []) = "<empty SinInt>"
-- > show (SinInt (x:xs)) =
-- > let start = printf "%.5f" x
-- > end = concat $ zipWith (\c p -> printf "%+.5f" c ++ "X^" ++ show p) xs [1 :: Int ..]
-- > in start ++ end
-- >
-- > polynomialOrder = 4 :: Int
-- >
-- > err :: SinInt -> Double
-- > err (SinInt xs) =
-- > let f x = snd $ L.foldl' (\(mlt,s) coeff -> (mlt*x, s + coeff*mlt)) (1,0) xs
-- > in maximum [ abs $ sin x - f x | x <- [0.0,0.001 .. pi/2]]
-- >
-- > instance Chromosome SinInt where
-- > crossover (SinInt xs) (SinInt ys) =
-- > return [ SinInt (L.zipWith (\x y -> (x+y)/2) xs ys) ]
-- >
-- > mutation (SinInt xs) = do
-- > idx <- getRandomR (0, length xs - 1)
-- > dx <- getRandomR (-10.0, 10.0)
-- > let t = xs !! idx
-- > xs' = take idx xs ++ [t + t*dx] ++ drop (idx+1) xs
-- > return $ SinInt xs'
-- >
-- > fitness int =
-- > let max_err = 1000.0 in
-- > max_err - (min (err int) max_err)
-- >
-- > randomSinInt gen =
-- > lst <- replicateM polynomialOrder (getRandomR (-10.0,10.0))
-- > in (SinInt lst, gen')
-- >
-- > stopf :: SinInt -> Int -> IO Bool
-- > stopf best gnum = do
-- > let e = err best
-- > _ <- printf "Generation: %02d, Error: %.8f\n" gnum e
-- > return $ e < 0.0002 || gnum > 20
-- >
-- > main = do
-- > int <- runGAIO 64 0.1 randomSinInt stopf
-- > putStrLn ""
-- > putStrLn $ "Result: " ++ show int
module AI.GeneticAlgorithm.Simple (
Chromosome(..),
runGA,
runGAIO,
zeroGeneration,
nextGeneration
) where
import System.Random
import qualified Data.List as L
import Control.Parallel.Strategies
import Control.Monad.Random
import Control.Monad
import Control.Monad.IO.Class (liftIO)
-- | Chromosome interface
class NFData a => Chromosome a where
-- | Crossover function
crossover :: RandomGen g => a -> a -> Rand g [a]
-- | Mutation function
mutation :: RandomGen g => a -> Rand g a
-- | Fitness function. fitness x > fitness y means that x is better than y
fitness :: a -> Double
-- | Pure GA implementation.
runGA :: (RandomGen g, Chromosome a)
=> g -- ^ Random number generator
-> Int -- ^ Population size
-> Double -- ^ Mutation probability [0, 1]
-> Rand g a -- ^ Random chromosome generator (hint: use currying or closures)
-> (a -> Int -> Bool) -- ^ Stopping criteria, 1st arg - best chromosome, 2nd arg - generation number
-> a -- ^ Best chromosome
runGA gen ps mp rnd stopf = evalRand go gen
where
go = do
pop <- zeroGeneration rnd ps
runGA' pop ps mp stopf 0
runGA' pop ps mp stopf gnum = do
let best = head pop
if stopf best gnum
then return best
else do
pop' <- nextGeneration pop ps mp
runGA' pop' ps mp stopf (gnum+1)
-- | Non-pure GA implementation.
runGAIO :: Chromosome a
=> Int -- ^ Population size
-> Double -- ^ Mutation probability [0, 1]
-> RandT StdGen IO a -- ^ Random chromosome generator (hint: use currying or closures)
-> (a -> Int -> IO Bool) -- ^ Stopping criteria, 1st arg - best chromosome, 2nd arg - generation number
-> IO a -- ^ Best chromosome
runGAIO ps mp rnd stopf = do
gen <- getStdGen
evalRandT go gen
where
go = do
pop <- zeroGeneration rnd ps
runGAIO' pop ps mp stopf 0
runGAIO' :: (RandomGen g, Chromosome a) => [a] -> Int -> Double -> (a -> Int -> IO Bool) -> Int -> RandT g IO a
runGAIO' pop ps mp stopf gnum = do
let best = head pop
stop <- liftIO $ stopf best gnum
if stop
then return best
else do
pop' <- nextGeneration pop ps mp
runGAIO' pop' ps mp stopf (gnum+1)
-- | Generate zero generation. Use this function only if you are going to implement your own runGA.
zeroGeneration :: (Monad m,RandomGen g, Chromosome a)
=> RandT g m a -- ^ Random chromosome generator (hint: use closures)
-> Int -- ^ Population size
-> RandT g m [a] -- ^ Zero generation
zeroGeneration rnd ps = do
zp <- replicateM ps rnd
let pF = map (\p->(p,fitness p)) zp
lst = take ps $ L.sortBy (\(_, fx) (_, fy) -> fy `compare` fx) pF
return $ map fst lst
-- | Generate next generation (in parallel) using mutation and crossover.
-- Use this function only if you are going to implement your own runGA.
nextGeneration :: (Monad m,RandomGen g, Chromosome a)
=> [a] -- ^ Current generation
-> Int -- ^ Population size
-> Double -- ^ Mutation probability
-> RandT g m [a] -- ^ Next generation ordered by fitness (best - first)
nextGeneration pop ps mp = do
gen <- getSplit
let gens = L.unfoldr (Just . split) gen
chunks = L.zip gens $ init $ L.tails pop
results = map (\(g, x : ys) -> [ (t,fitness t) | t <- evalRand (nextGeneration' [ (x, y) | y <- ys ] mp []) g ]) chunks
`using` evalList rdeepseq
-- r <- roulette ps $ normalize $ concat results
-- return $ map fst $ L.sortBy (\(_, fx) (_, fy) -> fy `compare` fx) r
let lst = take ps $ L.sortBy (\(_, fx) (_, fy) -> fy `compare` fx) $ concat results
return $ map fst lst
nextGeneration' [] _ acc = return acc
nextGeneration' ((p1,p2):ps) mp acc = do
children0 <- crossover p1 p2
children1 <- mapM (`mutate` mp) children0
nextGeneration' ps mp (children1 ++ acc)
mutate :: (RandomGen g, Chromosome a) => a -> Double -> Rand g a
mutate x mp = do
r <- getRandomR (0.0, 1.0)
if r <= mp then mutation x
else return x
normalize :: [(a,Double)] -> [(a,Double)]
normalize xs =
let
ws = map snd xs
mi = minimum ws
-- mx = maximum ws
-- df = mx-mi
df = if mi < 0 then (-mi) else 0
--(w-mi)/df)
in map (\(a,w)->(a,(w+df)**4)) xs
roulette :: (MonadRandom m) => Int -> [(a,Double)] -> m [(a,Double)]
roulette _ [] = error "roulette called with empty list"
roulette _ [a] = return [a]
roulette l xs = do
snd <$> foldM go (xs,[]) [1..l]
where
go (xs0,xsaccum) _ = do
(xs1,xs2) <- fromList' xs0
return (xs2,xs1:xsaccum)
fromList' :: (MonadRandom m) => [(a,Double)] -> m ((a,Double),[(a,Double)])
fromList' [] = error "fromList' called with empty list"
fromList' [a] = return (a,[])
fromList' xs = do
let s = sum (map snd xs) -- total weight
xs2 = map (\(a,b)->(a,b,b)) xs
cs = scanl1 (\(_,_,q) (y,b,s') -> (y, b,s'+q)) xs2 -- cumulative weight
p <- getRandomR (0.0,s)
let l1 = takeWhile (\(_,_,q) -> q < p) cs
let (r:l2) = drop (length l1) cs
return (fst' r,map fst' $ l1++l2)
where fst' (a,b,_)=(a,b)