Package: nervaluate
nervaluatenervaluate is a python module for evaluating Named Entity Recognition (NER) models as defined in the SemEval 2013 - 9.1 task.The evaluation metrics output by nervaluate go beyond a simple token/tag based schema, and consider diferent scenarios based on wether all the tokens that belong to a named entity were classified or not, and also whether the correct entity type was assigned.This full problem is described in detail in theoriginal blogpost byDavid Batista, and extends the code in theoriginal repositorywhich accompanied the blog post.The code draws heavily on:Segura-bedmar, I., & Mart, P. (2013). 2013 SemEval-2013 Task 9 Extraction of Drug-Drug Interactions from. Semeval, 2(DDIExtraction), 341–350.linkhttps://www.cs.york.ac.uk/semeval-2013/task9/data/uploads/semeval_2013-task-9_1-evaluation-metrics.pdfThe problemToken level evaluation for NER is too simplisticWhen running machine learning models for NER, it is common to report metrics at the individual token level. This may not be the best approach, as a named entity can be made up of multiple tokens, so a full-entity accuracy would be desireable.When comparing the golden standard annotations with the output of a NER system different scenarios might occur:I. Surface string and entity type matchTokenGoldPredictioninOONewB-LOCB-LOCYorkI-LOCI-LOC.OOII. System hypothesized an incorrect entityTokenGoldPredictionanOOAwfulOB-ORGHeadacheOI-ORGinOOIII. System misses an entityTokenGoldPredictioninOOPaloB-LOCOAltoI-LOCO,OOBased on these three scenarios we have a simple classification evaluation that can be measured in terms of false positives, true positives, false negatives and false positives, and subsequently compute precision, recall and f1-score for each named-entity type.However this simple schema ignores the possibility of partial matches or other scenarios when the NER system gets the named-entity surface string correct but the type wrong, and we might also want to evaluate these scenarios again at a full-entity level.For example:IV. System assigns the wrong entity typeTokenGoldPredictionIOOliveOOinOOPaloB-LOCB-ORGAltoI-LOCI-ORG,OOV. System gets the boundaries of the surface string wrongTokenGoldPredictionUnlessOB-PERKarlB-PERI-PERSmithI-PERI-PERresignsOOVI. System gets the boundaries and entity type wrongTokenGoldPredictionUnlessOB-ORGKarlB-PERI-ORGSmithI-PERI-ORGresignsOOHow can we incorporate these described scenarios into evaluation metrics? See theoriginal blogfor a great explanation, a summary is included here:We can use the following five metrics to consider difference categories of errors:Error typeExplanationCorrect (COR)both are the sameIncorrect (INC)the output of a system and the golden annotation don’t matchPartial (PAR)system and the golden annotation are somewhat “similar” but not the sameMissing (MIS)a golden annotation is not captured by a systemSpurius (SPU)system produces a response which doesn’t exit in the golden annotationThese five metrics can be measured in four different ways:Evaluation schemaExplanationStrictexact boundary surface string match and entity typeExactexact boundary match over the surface string, regardless of the typePartialpartial boundary match over the surface string, regardless of the typeTypesome overlap between the system tagged entity and the gold annotation is requiredThese five errors and four evaluation schema interact in the following ways:ScenarioGold entityGold stringPred entityPred stringTypePartialExactStrictIIIBRANDtikosynMISMISMISMISIIBRANDhealthySPUSPUSPUSPUVDRUGwarfarinDRUGof warfarinCORPARINCINCIVDRUGpropranololBRANDpropranololINCCORCORINCIDRUGphenytoinDRUGphenytoinCORCORCORCORVIGROUPcontraceptivesDRUGoral contraceptivesINCPARINCINCThen precision/recall/f1-score are calculated for each different evaluation schema. In order to achieve data, two more quantities need to be calculated:POSSIBLE (POS) = COR + INC + PAR + MIS = TP + FN
ACTUAL (ACT) = COR + INC + PAR + SPU = TP + FPThen we can compute precision/recall/f1-score, where roughly describing precision is the percentage of correct named-entities found by the NER system, and recall is the percentage of the named-entities in the golden annotations that are retrieved by the NER system. This is computed in two different ways depending wether we want an exact match (i.e., strict and exact ) or a partial match (i.e., partial and type) scenario:Exact Match (i.e., strict and exact )Precision = (COR / ACT) = TP / (TP + FP)
Recall = (COR / POS) = TP / (TP+FN)Partial Match (i.e., partial and type)Precision = (COR + 0.5 × PAR) / ACT = TP / (TP + FP)
Recall = (COR + 0.5 × PAR)/POS = COR / ACT = TP / (TP + FP)Putting all together:MeasureTypePartialExactStrictCorrect3332Incorrect2023Partial0200Missed1111Spurius1111Precision0.50.660.50.33Recall0.50.660.50.33F10.50.660.50.33Notes:In scenarios IV and VI the entity type of thetrueandpreddoes not match, in both cases we only scored against the true entity, not the predicted one. You can argue that the predicted entity could also be scored as spurious, but according to the definition ofspurius:Spurius (SPU) : system produces a response which doesn’t exist in the golden annotation;In this case there exists an annotation, but with a different entity type, so we assume it's only incorrect.InstallationTo install the package:pip install nervaluateTo create a virtual environment for development:make virtualenv

# Then to activate the virtualenv:

source /build/virtualenv/bin/activateAlternatively you can use your own virtualenv manager and simplymake reqsto install requirements.To run tests:# Will run tox

make testExample:The mainEvaluatorclass will accept a number of formats:prodi.gystyle lists of spans.Nested lists containing NER labels.CoNLL style tab delimited strings.Prodigy spanstrue = [
    [{"label": "PER", "start": 2, "end": 4}],
    [{"label": "LOC", "start": 1, "end": 2},
     {"label": "LOC", "start": 3, "end": 4}]
]

pred = [
    [{"label": "PER", "start": 2, "end": 4}],
    [{"label": "LOC", "start": 1, "end": 2},
     {"label": "LOC", "start": 3, "end": 4}]
]

from nervaluate import Evaluator

evaluator = Evaluator(true, pred, tags=['LOC', 'PER'])

# Returns overall metrics and metrics for each tag

results, results_per_tag = evaluator.evaluate()

print(results){
    'ent_type':{
        'correct':3,
        'incorrect':0,
        'partial':0,
        'missed':0,
        'spurious':0,
        'possible':3,
        'actual':3,
        'precision':1.0,
        'recall':1.0
    },
    'partial':{
        'correct':3,
        'incorrect':0,
        'partial':0,
        'missed':0,
        'spurious':0,
        'possible':3,
        'actual':3,
        'precision':1.0,
        'recall':1.0
    },
    'strict':{
        'correct':3,
        'incorrect':0,
        'partial':0,
        'missed':0,
        'spurious':0,
        'possible':3,
        'actual':3,
        'precision':1.0,
        'recall':1.0
    },
    'exact':{
        'correct':3,
        'incorrect':0,
        'partial':0,
        'missed':0,
        'spurious':0,
        'possible':3,
        'actual':3,
        'precision':1.0,
        'recall':1.0
    }
}print(results_by_tag){
    'LOC':{
        'ent_type':{
            'correct':2,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':2,
            'actual':2,
            'precision':1.0,
            'recall':1.0
        },
        'partial':{
            'correct':2,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':2,
            'actual':2,
            'precision':1.0,
            'recall':1.0
        },
        'strict':{
            'correct':2,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':2,
            'actual':2,
            'precision':1.0,
            'recall':1.0
        },
        'exact':{
            'correct':2,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':2,
            'actual':2,
            'precision':1.0,
            'recall':1.0
        }
    },
    'PER':{
        'ent_type':{
            'correct':1,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':1,
            'actual':1,
            'precision':1.0,
            'recall':1.0
        },
        'partial':{
            'correct':1,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':1,
            'actual':1,
            'precision':1.0,
            'recall':1.0
        },
        'strict':{
            'correct':1,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':1,
            'actual':1,
            'precision':1.0,
            'recall':1.0
        },
        'exact':{
            'correct':1,
            'incorrect':0,
            'partial':0,
            'missed':0,
            'spurious':0,
            'possible':1,
            'actual':1,
            'precision':1.0,
            'recall':1.0
        }
    }
}Nested liststrue = [
    ['O', 'O', 'B-PER', 'I-PER', 'O'],
    ['O', 'B-LOC', 'I-LOC', 'B-LOC', 'I-LOC', 'O'],
]

pred = [
    ['O', 'O', 'B-PER', 'I-PER', 'O'],
    ['O', 'B-LOC', 'I-LOC', 'B-LOC', 'I-LOC', 'O'],
]

evaluator = Evaluator(true, pred, tags=['LOC', 'PER'], loader="list")

results, results_by_tag = evaluator.evaluate()CoNLL style tab delimitedtrue = "word\tO\nword\tO\B-PER\nword\tI-PER\n"

pred = "word\tO\nword\tO\B-PER\nword\tI-PER\n"

evaluator = Evaluator(true, pred, tags=['PER'], loader="conll")

results, results_by_tag = evaluator.evaluate()Extending the package to accept more formatsAdditional formats can easily be added to the module by creating a converstion function innervaluate/utils.py, for exampleconll_to_spans(). This function must return the spans in the prodigy style dicts shown in the prodigy example above.The new function can then be added to the list of loaders innervaluate/nervaluate.py, and can then be selection with theloaderargument when instantiating theEvaluatorclass.A list of formats we intend to include is included inhttps://github.com/ivyleavedtoadflax/nervaluate/issues/3.