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,
|
2015-08-22 20:29:56 +02:00
|
|
|
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,
|
2015-08-23 07:40:37 +02:00
|
|
|
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
|
|
|
|
2015-08-22 20:29:56 +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 }
|
|
|
|
|
2015-08-23 07:40:37 +02:00
|
|
|
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
|
|
|
|
2015-08-22 20:29:56 +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
|
2015-08-23 07:40:37 +02:00
|
|
|
expectedDirectory = getExpectedDirectory gevalSpec
|
2015-08-21 22:56:32 +02:00
|
|
|
outDirectory = gesOutDirectory gevalSpec
|
|
|
|
testName = gesTestName gevalSpec
|
|
|
|
metric = gesMetric gevalSpec
|
|
|
|
|
2015-08-22 20:29:56 +02:00
|
|
|
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"
|