hanalyze-0.1.0.0: bench/haskell/BenchML.hs
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
-- | Classical-ML benchmarks (B6).
--
-- Compares hanalyze's Model.{PCA, Cluster, DecisionTree, RandomForest}
-- against scikit-learn on shared CSV inputs:
--
-- PCA : lm_n10000_p50.csv (X only), 5 components
-- KMeans : kernel_n2000_p5.csv (X only), k=5
-- DT / RF : logistic_n10000_p20.csv (binary y), p=20
--
-- Outputs the unified BenchRow CSV at @bench/results/haskell/ml.csv@.
module Main where
import qualified Data.Vector as V
import qualified Numeric.LinearAlgebra as LA
import qualified System.Random.MWC as MWC
import qualified Hanalyze.Model.PCA as PCA
import qualified Hanalyze.Model.Cluster as Cl
import qualified Hanalyze.Model.DecisionTree as DT
import qualified Hanalyze.Model.RandomForest as RF
import BenchUtil
-- ---------------------------------------------------------------------------
-- Phantom wrappers (defeat CSE across iterations)
-- ---------------------------------------------------------------------------
{-# NOINLINE pcaPhantom #-}
pcaPhantom :: Int -> Int -> LA.Matrix Double -> PCA.PCAResult
pcaPhantom _ k x = PCA.pca PCA.CenterScale (Just k) x
{-# NOINLINE kmeansPhantom #-}
kmeansPhantom :: Int -> Int -> LA.Matrix Double -> MWC.GenIO -> IO Cl.KMeansResult
kmeansPhantom _ k x gen = Cl.kMeans (Cl.defaultKMeansConfig k) x gen
{-# NOINLINE dtPhantom #-}
dtPhantom :: Int -> [[Double]] -> [Int] -> DT.DTree
dtPhantom _ xs ys = DT.fitDT DT.defaultDTConfig xs ys
{-# NOINLINE rfPhantom #-}
rfPhantom :: Int -> [[Double]] -> [Double] -> MWC.GenIO -> IO RF.RandomForest
rfPhantom _ xs ys gen =
RF.fitRF RF.defaultRFConfig { RF.rfTrees = 20 } xs ys gen
-- ---------------------------------------------------------------------------
main :: IO ()
main = do
rows <- mconcat <$> sequence
[ benchPCA "bench/data/lm_n10000_p50.csv" "PCA_n10000_p50_k5" 5
, benchKMeans "bench/data/kernel_n2000_p5.csv" "KMeans_n2000_p5_k5" 5
-- DT/RF use list-based [[Double]] APIs internally; we cap at
-- n=2000 p=10 so the bench finishes in reasonable time. The Python
-- side uses the same fixture for fairness.
, benchDT "bench/data/logistic_n2000_p10.csv" "DT_n2000_p10"
, benchRF "bench/data/logistic_n2000_p10.csv" "RF_n2000_p10_t20"
]
writeRows "bench/results/haskell/ml.csv" rows
putStrLn $ "wrote " ++ show (length rows)
++ " rows → bench/results/haskell/ml.csv"
-- ---------------------------------------------------------------------------
-- PCA
-- ---------------------------------------------------------------------------
benchPCA :: FilePath -> String -> Int -> IO [BenchRow]
benchPCA path name k = do
(x, _y) <- readCsvXY path
(ms, res) <- timeitTastyIO probe
(\i -> return $! pcaPhantom i k x)
let ratio = LA.sumElements (PCA.pcaExplainedRatio res)
sigma = LA.sumElements (PCA.pcaSingularValues res)
return [ BenchRow "haskell" "ml" name ms ratio sigma
("Hanalyze.Model.PCA k=" ++ show k ++ " standardized") ]
where
probe r = LA.sumElements (PCA.pcaExplainedRatio r)
+ LA.sumElements (PCA.pcaSingularValues r)
-- ---------------------------------------------------------------------------
-- KMeans
-- ---------------------------------------------------------------------------
benchKMeans :: FilePath -> String -> Int -> IO [BenchRow]
benchKMeans path name k = do
(x, _y) <- readCsvXY path
gen <- MWC.createSystemRandom
(ms, res) <- timeitTastyIO probe
(\i -> kmeansPhantom i k x gen)
let inert = Cl.kmrInertia res
iters = fromIntegral (Cl.kmrIters res)
return [ BenchRow "haskell" "ml" name ms inert iters
("Hanalyze.Model.Cluster.kMeans k=" ++ show k) ]
where
probe r = Cl.kmrInertia r
-- ---------------------------------------------------------------------------
-- DecisionTree (classification)
-- ---------------------------------------------------------------------------
benchDT :: FilePath -> String -> IO [BenchRow]
benchDT path name = do
(x, y) <- readCsvXY path
let xs = LA.toLists x
ys = map (round :: Double -> Int) (LA.toList y)
(ms, tree) <- timeitTastyIO probe
(\i -> return $! dtPhantom i xs ys)
let acc = let preds = [ DT.predictDT tree row | row <- xs ]
hits = length (filter id (zipWith (==) preds ys))
in fromIntegral hits / fromIntegral (length ys) :: Double
return [ BenchRow "haskell" "ml" name ms acc 0
"Hanalyze.Model.DecisionTree.fitDT default config" ]
where
-- Force tree by predicting on the first row.
probe t = case [ DT.predictDT t [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ] of
(r:_) -> fromIntegral r
_ -> 0
-- ---------------------------------------------------------------------------
-- RandomForest (regression on binary y; same accuracy metric via threshold 0.5)
-- ---------------------------------------------------------------------------
benchRF :: FilePath -> String -> IO [BenchRow]
benchRF path name = do
(x, y) <- readCsvXY path
let xs = LA.toLists x
ys = LA.toList y
gen <- MWC.createSystemRandom
(ms, forest) <- timeitTastyIO probe
(\i -> rfPhantom i xs ys gen)
let preds = map (RF.predictRF forest) xs
yi = map (round :: Double -> Int) ys
pi' = map (\p -> if p > 0.5 then 1 else 0 :: Int) preds
hits = length (filter id (zipWith (==) pi' yi))
acc = fromIntegral hits / fromIntegral (length ys) :: Double
return [ BenchRow "haskell" "ml" name ms acc 0
"Hanalyze.Model.RandomForest.fitRF (20 trees)" ]
where
probe forest = case xs of
(row:_) -> RF.predictRF forest row
_ -> 0
where xs = [[0.0 :: Double | _ <- [0 :: Int .. 19]]]