packages feed

hirt-0.0.1.0: Irt.hs

{-# LANGUAGE CPP #-}
module Irt
    ( groupByTask
    , groupByContestant
    , estimateTheta
    , estimateAB
    , estimateBfgs
    , totalLogLikelihood
    , paramVSumMap
    , thetaVSumMap
    , estTheta
    ) where

import Types
import Likelihood

import Control.Arrow
import Control.Monad.ST.Safe
import Data.List
import Data.Monoid
--import Text.Printf
import Numeric.GSL.Minimization
import qualified Numeric.Lbfgsb as B
import qualified Data.Vector.Generic as V
import qualified Data.Vector.Generic.Mutable as M
import qualified Data.Vector.Unboxed as UV
import qualified Data.Vector.Unboxed.Mutable as UM

delta :: Double
delta = 1e-8

maxOn :: (Ord b) => (a -> b) -> a -> a -> a
maxOn f x y | f x < f y = y
            | otherwise = x

maximize1 :: (Double -> (Double, Double)) -> Double -> Double
maximize1 fg x1 = r
  where
    bounds = [(Just *** Just) thetaBound]
    [r] = B.minimize 5 1e5 delta [x1] bounds fg'
    fg' [x] = let (fx, dx) = fg x in (-fx, [-dx])


instance Monoid Double where
    mempty = 0
    mappend = (+)

sumMap :: (Monoid b, V.Vector v a) => (a -> b) -> v a -> b
sumMap f = foldl' mappend mempty . map f . V.toList

thetaVSumMap :: (Monoid a) => (Points -> TaskParam -> Theta -> a) -> ContestantData -> a
thetaVSumMap f (t,v) = sumMap (\(d,r) -> f r d t) $ v

paramVSumMap :: (Monoid a) => (Points -> TaskParam -> Theta -> a) -> TaskData -> a
paramVSumMap f (d,v) = sumMap (\(t,r) -> f r d t) $ v

estTheta :: Theta -> UV.Vector (TaskParam, Points) -> Theta
estTheta it v = uncurry bound thetaBound $ maximize1 fg it
  where
    fg t = thetaVSumMap (((pick.).). logL) (t,v)
    pick (lh, _, dt) = (lh, dt)

estAB :: TaskParam -> UV.Vector (Theta, Points) -> TaskParam
#if(PL3)
estAB = error "not supported, try --algo lbfgsb"
#else
estAB (ia,ib) v = if f [ia', ib'] >= f [na, nb]
                    then (ia', ib')
                    else (na, nb)
  where
    (ia', ib') = clampTaskParam (ia, ib)
    (na, nb) = clampTaskParam (ra, rb)
    ([ra,rb],_x) = minimize NMSimplex2 delta 100 [5,10] (negate . f) [ia,ib]
    f  [a,b] = bound (-1e100) (1e100) $ paramVSumMap logLikelihood ((a,b),v)
#endif

address :: (UV.Unbox a, UV.Unbox b) => UV.Vector b -> UV.Vector (Int, a) -> UV.Vector (b, a)
address vals = V.map (first (vals V.!))

groupByContestant :: Responses -> Thetas -> TaskParams -> ContestantsData
groupByContestant resp thetas diffs = zip (V.toList thetas) . map (address diffs) . V.toList . respCont $ resp

groupByTask :: Responses -> Thetas -> TaskParams -> TasksData
groupByTask resp thetas diffs = zip (V.toList diffs) . map (address thetas) . V.toList . respTask $ resp


estimateTheta :: ContestantsData -> Thetas
estimateTheta = V.fromList . map (uncurry estTheta)

estimateAB :: TasksData -> TaskParams
estimateAB = V.fromList . map (uncurry estAB)

logLs :: Responses -> Thetas -> TaskParams -> (Double, Thetas, TaskParams)
logLs responses thetas params = runST $ do
    z <- zero "fail"
    r <- V.foldM plus z (respAll responses)
    freeze r
  where
    nconts = V.length thetas
    ntasks = V.length params
    zero _ = do
        a <- M.replicate nconts 0
        b <- M.replicate ntasks mempty
        return (0, a, b)
    plus (l, dthetas, dparams) (cont, task, r) = do
        add dthetas cont dt
        add dparams task dd
        return (l+l', dthetas, dparams)
      where
        (l', dd, dt) = logL r (params V.! task) (thetas V.! cont)
    add v i x = M.read v i >>= M.write v i . (`mappend` x)
    freeze (l, v1, v2) = do
        v1' <- V.freeze v1
        v2' <- V.freeze v2
        return (l, v1', v2')

estimateBfgs :: Double -> Responses -> Thetas -> TaskParams -> (Thetas, TaskParams)
estimateBfgs prec responses ithetas iparams = unpack . V.fromList $ minimize'
  where
    nconts = V.length ithetas
    ntasks = V.length iparams
    haveParams = map (uncurry (/=)) [aBound, bBound, cBound]
    unpack = second unpackParams . unpackTheta
    unpackTheta = V.splitAt nconts
    unpackParams xs = V.zipWith3 from3PL as bs cs
      where
        split haveParam xs | haveParam = V.splitAt ntasks xs
                           | otherwise = (zs, xs)
        zs = V.replicate ntasks 0
        (as, ys) = split (haveParams !! 0) xs
        (bs, us) = split (haveParams !! 1) ys
        (cs, _ ) = split (haveParams !! 2) us
    canonize x = (paramA x, paramB x, paramC x)
    pack ts abs = V.concat $ ts : [ xs | (True, xs) <- zip haveParams [as,bs,cs]]
      where
        (as,bs,cs) = V.unzip3 . V.map canonize $ abs
    bounds = map (Just *** Just) . concat $
                replicate nconts thetaBound :
                [ replicate ntasks x | (True, x) <- zip haveParams [aBound, bBound, cBound] ]

    minimize' = B.minimize 25 1e5 prec (V.toList $ pack ithetas iparams) bounds fg'

    fg' :: [Double] -> (Double, [Double])
    fg' xs = let (a, b) = fg . V.fromList $ xs in (-a, map negate . V.toList $ b)
    fg :: UV.Vector Double -> (Double, UV.Vector Double)
    fg xs = let (l, ts, ps) = uncurry (logLs responses) . unpack $ xs in (l, pack ts ps)

bound :: Double -> Double -> Double -> Double
bound min' max' x | x <= min' = min'
                  | x >= max' = max'
                  | otherwise = x

#if(!PL3)
clampTaskParam :: TaskParam -> TaskParam
clampTaskParam = (uncurry bound aBound) *** (uncurry bound bBound)
#endif

totalLogLikelihood :: Responses -> TaskParams -> Thetas -> Double
totalLogLikelihood responses difficulties thetas = V.sum . V.map (likelihood . val) . respAll $ responses
  where
    val (c, t, r) = (thetas V.! c, difficulties V.! t, r)
    likelihood (t, d, r) = logLikelihood r d t