geval/src/GEval/Core.hs

271 lines
11 KiB
Haskell
Raw Normal View History

2015-08-23 08:14:47 +02:00
module GEval.Core
2015-08-21 22:56:32 +02:00
( geval,
gevalCore,
Metric(..),
2015-12-12 18:45:49 +01:00
MetricOrdering(..),
getMetricOrdering,
MetricValue,
2015-08-21 22:56:32 +02:00
GEvalSpecification(..),
2015-08-22 00:00:46 +02:00
GEvalOptions(..),
2015-08-23 17:47:40 +02:00
GEvalException(..),
2015-08-22 00:00:46 +02:00
defaultGEvalSpecification,
defaultOutDirectory,
defaultTestName,
defaultOutFile,
defaultExpectedFile,
defaultMetric,
2015-08-23 20:53:43 +02:00
getExpectedDirectory,
configFileName
2015-08-17 23:32:00 +02:00
) where
2015-08-19 22:14:34 +02:00
import Data.Conduit
import Data.Conduit.Combinators as CC
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.Text as CT
import Control.Monad.Trans.Resource
import qualified Data.Conduit.List as CL
import Data.Text
import Data.Text.Read as TR
2015-08-19 23:24:19 +02:00
import Control.Applicative
2015-08-22 19:29:39 +02:00
import Control.Exception
2015-11-06 22:42:08 +01:00
import Control.Conditional (unlessM, whenM)
2015-08-22 19:29:39 +02:00
import qualified System.Directory as D
2015-11-06 22:42:08 +01:00
import System.Posix
2015-08-21 22:56:32 +02:00
import System.FilePath
import Data.Maybe
2015-08-24 23:40:40 +02:00
import qualified Data.List.Split as DLS
2016-08-02 09:48:58 +02:00
import Data.Attoparsec.Text (parseOnly)
2015-08-24 23:40:40 +02:00
import GEval.BLEU
2016-08-02 08:46:25 +02:00
import GEval.Common
2016-08-02 09:48:58 +02:00
import GEval.ClippEU
import GEval.PrecisionRecall
2015-08-24 23:40:40 +02:00
type MetricValue = Double
2016-12-03 09:18:04 +01:00
data Metric = RMSE | MSE | BLEU | Accuracy | ClippEU | FMeasure Double
deriving (Eq)
instance Show Metric where
show RMSE = "RMSE"
show MSE = "MSE"
show BLEU = "BLEU"
show Accuracy = "Accuracy"
show ClippEU = "ClippEU"
show (FMeasure beta) = "F" ++ (show beta)
instance Read Metric where
readsPrec _ ('R':'M':'S':'E':theRest) = [(RMSE, theRest)]
readsPrec _ ('M':'S':'E':theRest) = [(MSE, theRest)]
readsPrec _ ('B':'L':'E':'U':theRest) = [(BLEU, theRest)]
readsPrec _ ('A':'c':'c':'u':'r':'a':'c':'y':theRest) = [(Accuracy, theRest)]
readsPrec _ ('C':'l':'i':'p':'p':'E':'U':theRest) = [(ClippEU, theRest)]
readsPrec p ('F':theRest) = case readsPrec p theRest of
[(beta, theRest)] -> [(FMeasure beta, theRest)]
_ -> []
2015-08-21 22:56:32 +02:00
2015-12-12 08:14:13 +01:00
data MetricOrdering = TheLowerTheBetter | TheHigherTheBetter
getMetricOrdering :: Metric -> MetricOrdering
getMetricOrdering RMSE = TheLowerTheBetter
getMetricOrdering MSE = TheLowerTheBetter
getMetricOrdering BLEU = TheHigherTheBetter
getMetricOrdering Accuracy = TheHigherTheBetter
2016-08-02 09:48:58 +02:00
getMetricOrdering ClippEU = TheHigherTheBetter
2016-12-03 09:18:04 +01:00
getMetricOrdering (FMeasure _) = TheHigherTheBetter
2015-12-12 08:14:13 +01:00
2015-08-21 22:56:32 +02:00
defaultOutDirectory = "."
defaultTestName = "test-A"
defaultOutFile = "out.tsv"
defaultExpectedFile = "expected.tsv"
defaultMetric :: Metric
2015-08-22 19:35:19 +02:00
defaultMetric = RMSE
2015-08-21 22:56:32 +02:00
2015-08-23 20:53:43 +02:00
configFileName :: FilePath
configFileName = "config.txt"
2015-08-22 00:00:46 +02:00
2015-08-21 22:56:32 +02:00
data GEvalSpecification = GEvalSpecification
2015-08-23 20:53:43 +02:00
{ gesOutDirectory :: FilePath,
gesExpectedDirectory :: Maybe FilePath,
2015-08-21 22:56:32 +02:00
gesTestName :: String,
gesOutFile :: String,
gesExpectedFile :: String,
gesMetric :: Metric }
getExpectedDirectory :: GEvalSpecification -> FilePath
getExpectedDirectory spec = fromMaybe outDirectory $ gesExpectedDirectory spec
where outDirectory = gesOutDirectory spec
2015-08-22 00:00:46 +02:00
data GEvalOptions = GEvalOptions
{ geoInit :: Bool,
2015-09-12 15:36:50 +02:00
geoPrecision :: Maybe Int,
2015-08-22 00:00:46 +02:00
geoSpec :: GEvalSpecification }
2015-08-21 22:56:32 +02:00
2015-08-22 19:29:39 +02:00
data GEvalException = NoExpectedFile FilePath
| NoOutFile FilePath
| NoExpectedDirectory FilePath
| NoOutDirectory FilePath
| NoExpectedTestDirectory FilePath
| NoOutTestDirectory FilePath
2015-08-23 17:47:40 +02:00
| FileAlreadyThere FilePath
2015-11-06 21:57:36 +01:00
| TooFewLines
| TooManyLines
2015-11-06 22:42:08 +01:00
| EmptyOutput
2015-11-06 23:14:10 +01:00
| UnexpectedData String
2015-11-06 21:57:36 +01:00
deriving (Eq)
2015-08-22 19:29:39 +02:00
instance Exception GEvalException
instance Show GEvalException where
show (NoExpectedFile filePath) = somethingWrongWithFilesMessage "No file with the expected results" filePath
show (NoOutFile filePath) = somethingWrongWithFilesMessage "No file with the test results" filePath
show (NoExpectedDirectory filePath) = somethingWrongWithFilesMessage "No directory with the expected results" filePath
show (NoOutDirectory filePath) = somethingWrongWithFilesMessage "No directory with the test results" filePath
show (NoExpectedTestDirectory filePath) = somethingWrongWithFilesMessage "No test subdirectory with the expected results" filePath
show (NoOutTestDirectory filePath) = somethingWrongWithFilesMessage "No test subdirectory with the results obtained" filePath
2015-08-23 17:47:40 +02:00
show (FileAlreadyThere filePath) = somethingWrongWithFilesMessage "File already there" filePath
2015-11-06 21:57:36 +01:00
show TooFewLines = "Too few lines in the output file"
show TooManyLines = "Too many lines in the output file"
2015-11-06 22:42:08 +01:00
show EmptyOutput = "The output file is empty"
2015-11-06 23:14:10 +01:00
show (UnexpectedData message) = "Unexpected data [" ++ message ++ "]"
2015-08-22 19:29:39 +02:00
somethingWrongWithFilesMessage :: String -> FilePath -> String
somethingWrongWithFilesMessage msg filePath = Prelude.concat
[ msg, ": `", filePath, "`" ]
2015-08-21 22:56:32 +02:00
defaultGEvalSpecification = GEvalSpecification {
gesOutDirectory = defaultOutDirectory,
gesExpectedDirectory = Nothing,
gesTestName = defaultTestName,
gesOutFile = defaultOutFile,
gesExpectedFile = defaultExpectedFile,
gesMetric = defaultMetric }
2015-11-06 22:42:08 +01:00
isEmptyFile :: FilePath -> IO (Bool)
isEmptyFile path = do
stat <- getFileStatus path
return ((fileSize stat) == 0)
2015-08-21 22:56:32 +02:00
geval :: GEvalSpecification -> IO (MetricValue)
2015-08-22 19:29:39 +02:00
geval gevalSpec = do
unlessM (D.doesDirectoryExist outDirectory) $ throwM $ NoOutDirectory outDirectory
unlessM (D.doesDirectoryExist expectedDirectory) $ throwM $ NoExpectedDirectory expectedDirectory
unlessM (D.doesDirectoryExist outTestDirectory) $ throwM $ NoOutTestDirectory outTestDirectory
unlessM (D.doesDirectoryExist expectedTestDirectory) $ throwM $ NoExpectedTestDirectory expectedTestDirectory
gevalCore metric expectedFilePath outFilePath
where expectedFilePath = expectedTestDirectory </> (gesExpectedFile gevalSpec)
outFilePath = outTestDirectory </> (gesOutFile gevalSpec)
expectedTestDirectory = expectedDirectory </> testName
outTestDirectory = outDirectory </> testName
expectedDirectory = getExpectedDirectory gevalSpec
2015-08-21 22:56:32 +02:00
outDirectory = gesOutDirectory gevalSpec
testName = gesTestName gevalSpec
metric = gesMetric gevalSpec
gevalCore :: Metric -> String -> String -> IO (MetricValue)
2015-08-24 22:32:09 +02:00
gevalCore RMSE expectedFilePath outFilePath = do
mse <- gevalCore MSE expectedFilePath outFilePath
return $ mse ** 0.5
gevalCore metric expectedFilePath outFilePath = do
2015-08-22 19:29:39 +02:00
unlessM (D.doesFileExist expectedFilePath) $ throwM $ NoExpectedFile expectedFilePath
unlessM (D.doesFileExist outFilePath) $ throwM $ NoOutFile outFilePath
2015-11-06 22:42:08 +01:00
whenM (isEmptyFile outFilePath) $ throwM $ EmptyOutput
2015-08-24 22:32:09 +02:00
gevalCore' metric expectedFilePath outFilePath
gevalCore' :: Metric -> String -> String -> IO (MetricValue)
2015-08-24 23:40:40 +02:00
gevalCore' MSE = gevalCore'' outParser outParser itemError averageC id
2015-08-24 22:51:03 +02:00
where outParser = getValue . TR.double
2015-08-25 16:10:20 +02:00
gevalCore' BLEU = gevalCore'' (Prelude.map Prelude.words . DLS.splitOn "\t" . unpack) (Prelude.words . unpack) bleuCombine bleuAgg bleuFinal
where bleuFinal (p1, p2, p3, p4, rl, l1, l2, l3, l4) = ((p1 /. l1) * (p2 /. l2) * (p3 /. l3) * (p4 /. l4)) ** 0.25 * (brevityPenalty l1 rl)
2015-08-24 23:40:40 +02:00
bleuCombine (refs, sen) = bleuStep refs sen
bleuAgg = CC.foldl bleuFuse (0, 0, 0, 0, 0, 0, 0, 0, 0)
bleuFuse (a1, a2, a3, a4, a5, a6, a7, a8, a9) (b1, b2, b3, b4, b5, b6, b7, b8, b9) = (a1+b1, a2+b2, a3+b3, a4+b4, a5+b5, a6+b6, a7+b7, a8+b8, a9+b9)
2015-08-25 16:10:20 +02:00
brevityPenalty c r
| c >= r = 1.0
| otherwise = exp (1.0 - (r /. c))
2015-08-24 23:40:40 +02:00
2015-10-31 19:05:23 +01:00
gevalCore' Accuracy = gevalCore'' strip strip hitOrMiss averageC id
where hitOrMiss (x,y) = if x == y then 1.0 else 0.0
2016-12-03 09:18:04 +01:00
gevalCore' (FMeasure beta) = gevalCore'' outParser outParser getCount countAgg (fMeasureOnCounts beta)
where outParser = detected . getValue . TR.double
expParser = expected . getValue . TR.decimal
expected 1 = True
expected 0 = False
expected _ = throw $ UnexpectedData "expected 0 or 1"
-- output value could be a probability (for compatibility with other measures)
detected prob
| prob >= 0.0 && prob < detectionThreshold = False
| prob >= detectionThreshold && prob <= 1.0 = True
| otherwise = throw $ UnexpectedData "expected probability"
detectionThreshold = 0.5
getCount (True, True) = (1, 1, 1)
getCount (True, False) = (0, 1, 0)
getCount (False, True) = (0, 0, 1)
getCount (False, False) = (0, 0, 0)
countAgg = CC.foldl countFolder (0, 0, 0)
2016-08-02 09:48:58 +02:00
gevalCore' ClippEU = gevalCore'' parseClippingSpecs parseClippings matchStep clippeuAgg finalStep
where
parseClippings = controlledParse lineClippingsParser
parseClippingSpecs = controlledParse lineClippingSpecsParser
matchStep (clippingSpecs, clippings) = (maxMatch matchClippingToSpec clippingSpecs clippings,
Prelude.length clippingSpecs,
Prelude.length clippings)
2016-12-03 09:18:04 +01:00
clippeuAgg = CC.foldl countFolder (0, 0, 0)
2016-08-02 09:48:58 +02:00
finalStep counts = f2MeasureOnCounts counts
2015-11-06 21:57:36 +01:00
data SourceItem a = Got a | Done
2015-11-07 12:10:39 +01:00
gevalCore'' :: (Text -> a) -> (Text -> b) -> ((a, b) -> c) -> (Sink c (ResourceT IO) d) -> (d -> Double) -> String -> String -> IO (MetricValue)
2015-08-24 23:40:40 +02:00
gevalCore'' expParser outParser itemStep aggregator finalStep expectedFilePath outFilePath = do
v <- runResourceT $
2015-08-19 23:24:19 +02:00
(getZipSource $ (,)
2015-08-24 22:51:03 +02:00
<$> ZipSource (items expectedFilePath expParser)
<*> ZipSource (items outFilePath outParser))
2015-11-06 21:57:36 +01:00
$$ (CL.map (checkStep itemStep)
=$= CL.catMaybes
2015-08-24 22:51:03 +02:00
=$ aggregator)
2015-08-24 23:40:40 +02:00
return $ finalStep v
2015-08-19 23:24:19 +02:00
2015-11-06 21:57:36 +01:00
checkStep :: ((a, b) -> c) -> (SourceItem a, SourceItem b) -> Maybe c
checkStep step (Got expectedItem, Got outItem) = Just $ step (expectedItem, outItem)
checkStep _ (Got _, Done) = throw TooFewLines
checkStep _ (Done, Got _) = throw TooManyLines
checkStep _ (Done, Done) = Nothing
2015-08-19 23:24:19 +02:00
averageC :: MonadResource m => Sink Double m Double
averageC = getZipSink
$ (\total count -> total / fromIntegral count)
<$> ZipSink CC.sum
<*> ZipSink CC.length
2015-11-06 21:57:36 +01:00
items :: MonadResource m => String -> (Text -> a) -> Source m (SourceItem a)
2015-08-24 22:51:03 +02:00
items filePath parser =
2015-11-06 21:57:36 +01:00
(CB.sourceFile filePath
$= (CT.decode CT.utf8
=$= CT.lines
=$= CL.map ((\x -> Got x) . parser))) >> yield Done
2015-08-19 23:24:19 +02:00
itemError :: (Double, Double) -> Double
itemError (exp, out) = (exp-out)**2
2015-08-19 22:14:34 +02:00
2016-12-03 09:18:04 +01:00
getValue :: Num a => Either String (a, Text) -> a
2015-11-06 23:24:46 +01:00
getValue (Right (x, reminder)) =
if Data.Text.null reminder || Data.Text.head reminder == '\t'
then x
else throw $ UnexpectedData "number expected"
2015-11-06 23:14:10 +01:00
getValue (Left s) = throw $ UnexpectedData s
2016-08-02 09:48:58 +02:00
controlledParse parser t =
case parseOnly parser t of
(Right v) -> v
(Left _) -> throw $ UnexpectedData "cannot parse line"