Advise-me-0.1: src/Bayes/Script.hs
-----------------------------------------------------------------------------
-- Copyright 2019, Advise-Me project team. This file is distributed under
-- the terms of the Apache License 2.0. For more information, see the files
-- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution.
-----------------------------------------------------------------------------
-- |
-- Maintainer : bastiaan.heeren@ou.nl
-- Stability : provisional
-- Portability : portable (depends on ghc)
--
-----------------------------------------------------------------------------
module Bayes.Script
( taskConcepts
, emptyConcepts
, aggregateConcepts
, aggregateConcepts'
, findEvidence
, addModelToLatestRequest
) where
import Bayes.Evidence
import Bayes.EliminationOrdering ( minFactorOrder )
import Bayes.Inference ( infer, toEvidence )
import Bayes.Network
import Data.List hiding (insert)
import Data.Maybe
import Data.Function ( on )
import Control.Monad ( (>=>) )
import Database.HDBC
import Database.HDBC.Sqlite3
import Ideas.Common.Id ( showId )
import qualified Ideas.Text.XML as XML ( fromXML, XML, parseXML, findChild )
--import qualified Ideas.Text.XML.Interface as XML
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Task.Network.StudentModel as StudentModel
import Recognize.Data.MathStoryProblem (Task(Task), singleNetwork)
import Bayes.Probability
import Database.Data
import Main.Tasks ( tasks, findTaskFuzzy )
-- | Add information from the student modeller to the database, based on the
-- raw input and output of the domain reasoner. This is unfortunately done in a
-- roundabout way, due to the way the framework functions.
--
-- We assume that the latest row to the requests table is the one we're dealing
-- with, since we must know the row ID to continue.
addModelToLatestRequest :: Connection -> IO ()
addModelToLatestRequest conn = do
r <- fromMaybe (error "No request to add model to.") <$> latestRecord conn
let xmlInput = either (error "Could not read XML") id . XML.parseXML $ r ! "input"
(sID, tID) = fromMaybe mempty . collectIDs $ xmlInput
inputIDs = intercalate "," . map fst . fromMaybe mempty . collectInputs $ xmlInput
reqnr = r ! "rowid" :: Int
ev <- findEvidence $ r ! "output"
let partialModel = taskConcepts tID ev
partialModels <- latestPartialModels conn sID
let studentModel = aggregateConcepts $ (tID, partialModel) : partialModels
insertRecord conn studentsTable
( insert "studentid" sID
. insert "taskid" tID
. insert "inputs" inputIDs
. insert "requestnr" reqnr
. insert "evidence" ev
. insert "studentmodel" studentModel
. insert "partialmodel" partialModel
$ mempty )
commit conn
-- Find evidence from a reply of the domain reasoner.
findEvidence :: Monad m => XML.XML -> m Evidence
findEvidence = XML.findChild "diagnosis" >=> XML.findChild "evidence" >=> XML.fromXML
-- | Run the evidence on the Bayesian networks to add soft evidence.
--
-- Instead of running it on the combined model, we run it through the models
-- seperately and only output the evidence as it should be in the student model
-- nodes. This should work since there should be no feedback effects at that
-- point.
--feed :: Evidence -> StudentModel
--feed = toEvidence StudentModel.network . feed'
-- | Obtain task concept node probabilities for a particular task.
taskConcepts :: TaskID -> Evidence -> Evidence
taskConcepts tID ev =
let nw = maybe (error $ "no such task " ++ tID) (\(Task t) -> singleNetwork t) $ findTaskFuzzy tID
in filterEvidence isConcept . toEvidence nw $ infer nw ev minFactorOrder (S.filter isConcept (nodeIds nw))
-- | Feeding empty evidence to the task concept evidence. This is used in case
-- the task is not even mentioned in the precalculations (presumably because no
-- request for the task was sent).
emptyConcepts :: [ (TaskID, Evidence) ]
emptyConcepts = flip map tasks $ \t -> let tID = showId t in (tID, taskConcepts tID mempty)
-- | Same as 'aggregateConcepts', but do not include task concept node
-- probabilities in final evidence.
aggregateConcepts' :: [ (TaskID, Evidence) ] -> Evidence
aggregateConcepts' precalculatedTaskConcepts
= toEvidence StudentModel.network
. M.map ((\x->[x,1-x]) . bayesAverage)
. M.fromListWith (++) $
[ (concept, [fromDouble . fromRational $ probability])
| Task t <- tasks
, let taskConcepts' = fromJust . lookup (showId t) $ precalculatedTaskConcepts ++ emptyConcepts
, (taskConcept, stateLabel, probability) <- allProbabilities taskConcepts'
, stateLabel == "Yes" || stateLabel == "State0" || stateLabel == "Correct" || stateLabel == "Correctr"
, concept <- maybeToList . getConcept $ taskConcept
]
-- | Aggregate task concept node probabilities into generic concept node
-- probabilities.
aggregateConcepts :: [ (TaskID, Evidence) ] -> Evidence
aggregateConcepts p = aggregateConcepts' p `mappend` mconcat (map snd . nubBy ((==) `on` fst) $ p ++ emptyConcepts)
-- for now, just use plain average
bayesAverage :: [Probability] -> Probability
bayesAverage ps = sum ps / fromIntegral (length ps)
-- | Task model concept node ID to a student model concept node ID.
getConcept :: String -> Maybe String
getConcept ct =
let cs = takeWhile (/= '_') $ ct
in if cs `S.member` nodeIds StudentModel.network
then Just cs
else Nothing
isConcept :: String -> Bool
isConcept = isJust . getConcept