packages feed

dataframe-learn-1.1.0.0: src/DataFrame/SVM.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}

{- | Linear support vector classification: L2-regularized squared hinge fitted
with the FISTA engine (sklearn's LinearSVC default loss). 'fit' trains a
one-vs-rest 'LinearSVCModel'; 'predict' is the arg-max class margin. There is no
@predict_proba@, matching sklearn's LinearSVC.
-}
module DataFrame.SVM (
    LinearSVCModel (..),
    SVCConfig (..),
    defaultSVCConfig,
    svcMarginExprs,
) where

import Data.List (sort)
import qualified Data.Map.Strict as M
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU

import DataFrame.Featurize.Internal (
    affineExpr,
    argMaxExpr,
    featureNames,
    numericMatrix,
    targetValues,
 )
import DataFrame.Internal.Column (Columnable)
import DataFrame.Internal.DataFrame (DataFrame)
import DataFrame.Internal.Expression (Expr)
import DataFrame.LinearSolver (LinearModel (..), SolverConfig (..), fitProx)
import DataFrame.LinearSolver.Loss (sqHingeLoss)
import DataFrame.Model (Fit (..), Predict (..))

-- | Hyper-parameters. @svcC@ is the inverse regularization strength (sklearn @C@).
data SVCConfig = SVCConfig
    { svcC :: !Double
    , svcMaxIter :: !Int
    , svcTol :: !Double
    }
    deriving (Eq, Show)

defaultSVCConfig :: SVCConfig
defaultSVCConfig = SVCConfig{svcC = 1.0, svcMaxIter = 1000, svcTol = 1.0e-4}

-- | A fitted one-vs-rest linear SVC: class labels and their margin sub-models.
data LinearSVCModel a = LinearSVCModel
    { svcClasses :: !(V.Vector a)
    , svcModels :: !(V.Vector LinearModel)
    }
    deriving (Eq, Show)

instance (Columnable a, Ord a) => Fit SVCConfig (Expr a) (LinearSVCModel a) where
    fit = fitLinearSVC

instance (Columnable a, Ord a) => Predict (LinearSVCModel a) a where
    predict m = argMaxExpr (labelledMargins m)

-- | Fit a one-vs-rest linear SVC.
fitLinearSVC ::
    (Columnable a, Ord a) =>
    SVCConfig -> Expr a -> DataFrame -> LinearSVCModel a
fitLinearSVC cfg target df =
    LinearSVCModel (V.fromList classes) (V.fromList (map fitOne classes))
  where
    names = featureNames target df
    (nameVec, mat) = numericMatrix names df
    ys = targetValues target df
    classes = sort (foldr dedup [] (V.toList ys))
    dedup x acc = if x `elem` acc then acc else x : acc
    solverCfg =
        SolverConfig
            { scL1Lambda = 0
            , scL2Lambda = 1 / svcC cfg
            , scMaxIter = svcMaxIter cfg
            , scTol = svcTol cfg
            , scSampleWeights = Nothing
            }
    fitOne c =
        let labels =
                VU.generate (V.length ys) (\i -> if ys V.! i == c then 1 else -1)
         in fitProx sqHingeLoss solverCfg mat labels nameVec

-- | The raw margin expression for each class.
svcMarginExprs ::
    (Columnable a, Ord a) => LinearSVCModel a -> M.Map a (Expr Double)
svcMarginExprs m = M.fromList (labelledMargins m)

labelledMargins :: LinearSVCModel a -> [(a, Expr Double)]
labelledMargins m =
    [ (svcClasses m V.! i, marginOf (svcModels m V.! i))
    | i <- [0 .. V.length (svcClasses m) - 1]
    ]

marginOf :: LinearModel -> Expr Double
marginOf m =
    affineExpr
        (lmIntercept m)
        (zip (VU.toList (lmWeights m)) (V.toList (lmFeatureNames m)))