1use anno::{EntityCategory, EntityType};
113use anno::{Error, Model, Result};
114use serde::{Deserialize, Serialize};
115use std::collections::HashMap;
116use std::path::Path;
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub enum EvalTask {
146 NER {
152 labels: Vec<String>,
154 mode: EvalMode,
156 },
157
158 RelationExtraction {
164 relations: Vec<String>,
166 require_entity_match: bool,
168 },
169
170 Coreference {
176 metrics: Vec<CorefMetric>,
178 },
179
180 DiscontinuousNER {
186 labels: Vec<String>,
188 },
189
190 EventExtraction {
196 event_types: Vec<String>,
198 argument_roles: Vec<String>,
200 },
201}
202
203impl Default for EvalTask {
204 fn default() -> Self {
205 EvalTask::NER {
206 labels: vec![
207 "PER".to_string(),
208 "ORG".to_string(),
209 "LOC".to_string(),
210 "MISC".to_string(),
211 ],
212 mode: EvalMode::Strict,
213 }
214 }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
219pub enum CorefMetric {
220 MUC,
222 BCubed,
224 CEAFe,
226 CEAFm,
228 LEA,
230 BLANC,
232 CoNLL,
234}
235
236pub use bio_adapter::BioScheme;
238pub use modes::EvalConfig as ModeConfig;
241pub use modes::EvalMode;
243
244pub mod cluster_encoder {
246 pub use anno::metrics::cluster_encoder::*;
247}
248
249pub mod coref_metrics {
251 pub use anno::metrics::coref_metrics::*;
252}
253
254#[cfg(feature = "discourse")]
259pub mod abstract_anaphora;
260pub mod advanced_evaluator;
261pub mod advanced_harness;
262pub mod analysis;
263pub mod backend_factory;
264pub mod benchmark;
265pub mod bio_adapter;
266pub mod book_scale;
267pub mod cdcr;
268pub mod coref;
269pub mod coref_loader;
270pub mod cross_context_eval;
271
272pub mod coref_resolver {
274 pub use anno::metrics::coref_resolver::*;
275}
276pub mod dataset;
277pub mod dataset_metadata;
278pub mod dataset_registry;
279pub mod dataset_spec;
280pub mod datasets;
281pub mod discontinuous;
282#[cfg(feature = "discourse")]
283pub mod discourse_deixis;
284pub mod evaluator;
285pub mod harness;
286pub mod history;
287pub mod incremental_coref;
288pub mod inter_doc_coref;
289pub mod loader;
290pub mod metrics;
291pub mod modes;
292pub mod ner_metrics;
293pub mod neural_cluster_encoder;
294pub mod prelude;
295pub mod relation;
296pub mod report;
297pub mod sampling;
298pub mod shell_nouns;
299pub mod synthetic;
300pub mod synthetic_gen;
301pub mod task_evaluator;
302
303#[cfg(feature = "eval-profiling")]
304pub mod profiling;
305pub mod task_mapping;
306pub mod types;
307pub mod validation;
308pub mod visual;
309
310#[cfg(feature = "eval-bias")]
315pub mod bias_config;
316#[cfg(feature = "eval-bias")]
317pub mod demographic_bias;
318#[cfg(feature = "eval-bias")]
319pub mod gender_bias;
320#[cfg(feature = "eval-bias")]
321pub mod length_bias;
322#[cfg(feature = "eval-bias")]
323pub mod temporal_bias;
324
325#[cfg(feature = "eval")]
330pub mod active_learning;
331#[cfg(feature = "eval")]
332pub mod calibration;
333#[cfg(feature = "eval")]
334pub mod dataset_comparison;
335#[cfg(feature = "eval")]
336pub mod dataset_quality;
337#[cfg(feature = "eval")]
338pub mod drift;
339#[cfg(feature = "eval")]
340pub mod ensemble;
341#[cfg(feature = "eval")]
342pub mod error_analysis;
343#[cfg(feature = "eval")]
344pub mod few_shot;
345#[cfg(feature = "eval")]
346pub mod learning_curve;
347#[cfg(feature = "eval")]
348pub mod long_tail;
349#[cfg(feature = "eval")]
350pub mod low_resource;
351#[cfg(feature = "eval")]
352pub mod ood_detection;
353#[cfg(feature = "eval")]
354pub mod robustness;
355#[cfg(feature = "eval")]
356pub mod threshold_analysis;
357
358#[cfg(feature = "eval")]
360pub mod annotator;
361#[cfg(feature = "eval")]
362pub mod bridging;
363#[cfg(feature = "eval")]
364pub mod multi_run;
365#[cfg(feature = "eval")]
366pub mod ranking;
367
368#[cfg(all(feature = "eval", test))]
369mod property_tests;
370
371pub use datasets::GoldEntity;
373
374pub use dataset_registry::{AnnotationScheme, DataFormat, DatasetId as RegistryDatasetId};
382pub use datasets::DatasetMetadata;
383pub use loader::{DatasetLoader, LoadableDatasetId, LoadedDataset};
384
385#[cfg(test)]
386mod registry_exports;
387
388pub use dataset::{AnnotatedExample, DatasetStats, Difficulty, Domain, NERDataset};
390pub use evaluator::*;
391pub use harness::{
392 BackendAggregateResult, BackendDatasetResult, BackendRegistry, DatasetStatsSummary, EvalConfig,
393 EvalHarness, EvalResults,
394};
395pub use metrics::*;
396pub use types::{
397 CorefChainStats, CorefDocStats, DocumentScale, GoalCheck, GoalCheckResult, LabelShift,
398 MetricDivergence, MetricValue, MetricWithVariance,
399};
400pub use validation::*;
401
402pub use coref::{CorefChain, CorefDocument, Mention, MentionType};
404pub use coref_loader::{
405 adversarial_coref_examples, synthetic_coref_dataset, CorefLoader, GapExample,
406};
407pub use coref_metrics::{
408 b_cubed_score, blanc_score, ceaf_e_score, ceaf_m_score, compare_systems, conll_f1, lea_score,
409 muc_score, AggregateCorefEvaluation, CorefEvaluation, CorefScores, SignificanceTest,
410};
411
412pub use book_scale::{
414 BookScaleAnalysis, BookScaleAnalyzer, BookScaleConfig, BookScaleDiagnostics, CorefEvalScores,
415 MetricReliability, MultiBookReport, PerBookEvaluation, ReliabilityLevel, Scores,
416 StratifiedEvaluation, WindowedEvaluation,
417};
418
419pub use coref_resolver::{CorefConfig, CoreferenceResolver, SimpleCorefResolver};
421
422pub use inter_doc_coref::InterDocCorefMetrics;
424
425pub use cdcr::{
426 comprehensive_cdcr_dataset,
427 financial_news_dataset,
428 political_news_dataset,
429 science_news_dataset,
430 sports_news_dataset,
431 tech_news_dataset,
433 CDCRConfig,
434 CDCRMetrics,
435 CDCRResolver,
436 CrossDocCluster,
437 Document,
438 LSHBlocker,
439 MentionRef,
440};
441
442pub use discontinuous::{
444 evaluate_discontinuous_ner, DiscontinuousEvalConfig, DiscontinuousGold,
445 DiscontinuousNERMetrics, TypeMetrics as DiscontinuousTypeMetrics,
446};
447
448pub use relation::{
450 evaluate_relations, RelationEvalConfig, RelationGold, RelationMetrics, RelationPrediction,
451 RelationTypeMetrics,
452};
453
454pub use advanced_evaluator::{
456 evaluator_for_task, DiscontinuousEvaluator, EvalResults as AdvancedEvalResults,
457 RelationEvaluator, TaskEvaluator,
458};
459
460pub use visual::{
462 evaluate_visual_ner, synthetic_visual_examples, BoundingBox, VisualEvalConfig, VisualGold,
463 VisualNERMetrics, VisualPrediction, VisualTypeMetrics,
464};
465
466pub use advanced_harness::{
468 evaluate_discontinuous_gold_vs_gold, evaluate_discontinuous_synthetic,
469 evaluate_relations_gold_vs_gold, evaluate_relations_synthetic, evaluate_visual_gold_vs_gold,
470 synthetic_dataset_stats, AdvancedTaskResults, ModelResult, SyntheticDatasetStats,
471};
472
473#[cfg(feature = "eval-bias")]
477pub use gender_bias::{
478 create_comprehensive_bias_templates, create_neopronoun_templates, create_winobias_templates,
479 occupation_stereotype, GenderBiasEvaluator, GenderBiasResults, OccupationBiasMetrics,
480 PronounGender, StereotypeType, WinoBiasExample,
481};
482
483#[cfg(feature = "eval-bias")]
484pub use bias_config::{
485 BiasDatasetConfig, DistributionValidation, FrequencyWeightedResults, StatisticalBiasResults,
486};
487#[cfg(feature = "eval-bias")]
488pub use demographic_bias::{
489 create_diverse_location_dataset, create_diverse_name_dataset, DemographicBiasEvaluator,
490 DemographicBiasResults, Ethnicity, Gender, LocationExample, LocationType, NameExample,
491 NameFrequency, NameResult, Region, RegionalBiasResults, Script,
492};
493
494#[cfg(feature = "eval-bias")]
495pub use temporal_bias::{
496 create_temporal_name_dataset, Decade, TemporalBiasEvaluator, TemporalBiasResults,
497 TemporalGender, TemporalNameExample,
498};
499
500#[cfg(feature = "eval-bias")]
501pub use length_bias::{
502 create_length_varied_dataset, EntityLengthEvaluator, LengthBiasResults, LengthBucket,
503 LengthTestExample, WordCountBucket,
504};
505
506#[cfg(feature = "eval")]
510pub use calibration::{
511 calibration_grade, confidence_entropy, confidence_gap_grade, confidence_variance,
512 CalibrationEvaluator, CalibrationResults, EntropyFilter, ReliabilityBin, ThresholdMetrics,
513};
514
515#[cfg(feature = "eval")]
516pub use robustness::{
517 robustness_grade, Perturbation, PerturbationMetrics, RobustnessEvaluator, RobustnessResults,
518};
519
520#[cfg(feature = "eval")]
521pub use ood_detection::{
522 ood_rate_grade, OODAnalysisResults, OODConfig, OODDetector, OODStatus, VocabCoverageStats,
523};
524
525#[cfg(feature = "eval")]
526pub use dataset_quality::{
527 check_leakage, entity_imbalance_ratio, DatasetQualityAnalyzer, DifficultyMetrics,
528 QualityReport, ReliabilityMetrics, ValidityMetrics,
529};
530
531#[cfg(feature = "eval")]
532pub use learning_curve::{
533 suggested_train_sizes, CurveFitParams, DataPoint, LearningCurveAnalysis, LearningCurveAnalyzer,
534 SampleEfficiencyMetrics,
535};
536
537#[cfg(feature = "eval")]
538pub use ensemble::{
539 agreement_grade, kappa_interpretation, DisagreementDetail, EnsembleAnalysisResults,
540 EnsembleAnalyzer, ModelPrediction, SingleExampleAnalysis,
541};
542
543#[cfg(feature = "eval")]
544pub use dataset_comparison::{
545 compare_datasets, compute_stats, estimate_difficulty, DatasetComparison,
546 DatasetStats as ComparisonStats, DifficultyEstimate, LengthStats,
547};
548
549#[cfg(feature = "eval")]
550pub use drift::{
551 ConfidenceDrift, DistributionDrift, DriftConfig, DriftDetector, DriftReport, DriftWindow,
552 VocabularyDrift,
553};
554
555#[cfg(feature = "eval")]
556pub use active_learning::{
557 entities_to_candidates, estimate_budget, export_annotation_priority, rank_for_annotation,
558 select_for_annotation, ActiveLearner, Candidate, SamplingStrategy, ScoreStats, SelectionResult,
559};
560
561#[cfg(feature = "eval")]
562pub use error_analysis::{
563 EntityInfo, ErrorAnalyzer, ErrorCategory, ErrorInstance, ErrorPattern, ErrorReport,
564 PredictedEntity, TypeErrorStats,
565};
566
567#[cfg(feature = "eval")]
568pub use threshold_analysis::{
569 format_threshold_table, interpret_curve, PredictionWithConfidence, ThresholdAnalyzer,
570 ThresholdCurve, ThresholdPoint,
571};
572
573pub use report::{
575 BiasSummary, CalibrationSummary, CoreMetrics, DataQualitySummary, DemographicBiasMetrics,
576 ErrorSummary, EvalReport, GenderBiasMetrics, LengthBiasMetrics, Priority, Recommendation,
577 RecommendationCategory, ReportBuilder, SimpleGoldEntity, TestCase,
578 TypeMetrics as ReportTypeMetrics,
579};
580
581pub mod unified_evaluator;
583pub use unified_evaluator::{EvalMetadata, EvalSystem, UnifiedEvalResults};
584
585#[cfg(feature = "eval-bias")]
586pub use unified_evaluator::BiasEvalResults;
587#[cfg(feature = "eval")]
588pub use unified_evaluator::CalibrationEvalResults;
589#[cfg(feature = "eval")]
590pub use unified_evaluator::DataQualityEvalResults;
591#[cfg(feature = "eval")]
592pub use unified_evaluator::StandardEvalResults;
593
594pub mod backend_name;
596pub use backend_name::BackendName;
597
598pub mod config_builder;
600#[cfg(feature = "eval-bias")]
601pub use config_builder::BiasDatasetConfigBuilder;
602#[cfg(feature = "eval")]
603pub use config_builder::TaskEvalConfigBuilder;
604
605#[cfg(feature = "eval")]
606pub use few_shot::{
607 simulate_few_shot_task, FewShotEvaluator, FewShotGold, FewShotPrediction, FewShotResults,
608 FewShotTask, FewShotTaskResults, SupportExample,
609};
610
611#[cfg(feature = "eval")]
612pub use long_tail::{
613 format_long_tail_results, EntityFrequency, FrequencyBucket, FrequencySplit, LongTailAnalyzer,
614 LongTailResults, TypePerformance,
615};
616
617pub use analysis::{
619 build_confusion_matrix, compare_ner_systems, ConfusionMatrix, ErrorAnalysis, ErrorType,
620 NERError, NERSignificanceTest,
621};
622
623pub use sampling::{multi_seed_eval, stratified_sample, stratified_sample_ner};
625
626#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct TypeMetrics {
629 pub precision: f64,
631 pub recall: f64,
633 pub f1: f64,
635 pub found: usize,
637 pub expected: usize,
639 pub correct: usize,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
649pub struct NEREvaluationResults {
650 pub precision: f64,
652 pub recall: f64,
654 pub f1: f64,
656 #[serde(default)]
658 pub macro_f1: Option<f64>,
659 #[serde(default)]
661 pub weighted_f1: Option<f64>,
662 pub per_type: HashMap<String, TypeMetrics>,
664 pub tokens_per_second: f64,
666 pub found: usize,
668 pub expected: usize,
670 #[serde(default)]
672 pub metadata: Option<EvaluationMetadata>,
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize, Default)]
677pub struct EvaluationMetadata {
678 pub dataset_name: Option<String>,
680 pub dataset_format: Option<String>,
682 pub dataset_version: Option<String>,
684 pub num_test_cases: usize,
686 pub total_gold_entities: Option<usize>,
688 pub timestamp: Option<String>,
690 pub model_info: Option<String>,
692 pub model_version: Option<String>,
694 pub matching_mode: Option<String>,
696 pub anno_version: Option<String>,
698}
699
700pub fn entity_type_to_string(et: &EntityType) -> String {
704 et.as_label().to_string()
705}
706
707pub fn entity_type_matches(a: &EntityType, b: &EntityType) -> bool {
714 if a == b {
716 return true;
717 }
718
719 let a_label = a.as_label().to_uppercase();
721 let b_label = b.as_label().to_uppercase();
722
723 if a_label == b_label {
724 return true;
725 }
726
727 matches!(
729 (a_label.as_str(), b_label.as_str()),
730 ("PERSON", "PER") | ("PER", "PERSON")
732 | ("ORGANIZATION", "ORG") | ("ORG", "ORGANIZATION")
734 | ("ORGANIZATION", "CORPORATION") | ("CORPORATION", "ORGANIZATION")
735 | ("ORG", "CORPORATION") | ("CORPORATION", "ORG")
736 | ("ORGANIZATION", "COMPANY") | ("COMPANY", "ORGANIZATION")
737 | ("LOCATION", "LOC") | ("LOC", "LOCATION")
739 | ("LOCATION", "GPE") | ("GPE", "LOCATION")
740 | ("LOC", "GPE") | ("GPE", "LOC")
741 | ("MISC", "MISCELLANEOUS") | ("MISCELLANEOUS", "MISC")
743 | ("MISC", "OTHER") | ("OTHER", "MISC")
744 )
745}
746
747pub fn load_conll2003<P: AsRef<Path>>(path: P) -> Result<Vec<(String, Vec<GoldEntity>)>> {
753 let content = std::fs::read_to_string(path.as_ref()).map_err(Error::Io)?;
754
755 let mut test_cases: Vec<(String, Vec<GoldEntity>)> = Vec::new();
756 let mut current_text = String::new();
757 let mut current_entities: Vec<GoldEntity> = Vec::new();
758 let mut char_offset = 0;
759
760 for line in content.lines() {
761 if line.trim().is_empty() {
762 if !current_text.is_empty() {
764 test_cases.push((current_text.clone(), current_entities.clone()));
765 }
766 current_text.clear();
767 current_entities.clear();
768 char_offset = 0;
769 continue;
770 }
771
772 let parts: Vec<&str> = line.split_whitespace().collect();
773 if parts.len() < 4 {
774 continue; }
776
777 let word = parts[0];
778 let ner_tag = parts[3];
779
780 if !current_text.is_empty() {
782 current_text.push(' ');
783 char_offset += 1;
784 }
785 let word_start = char_offset;
786 current_text.push_str(word);
787 char_offset += word.chars().count();
789 let word_end = char_offset;
790
791 if ner_tag != "O" {
793 let (prefix, entity_type_str) = if let Some(dash_pos) = ner_tag.find('-') {
794 (&ner_tag[..dash_pos], &ner_tag[dash_pos + 1..])
795 } else {
796 continue;
797 };
798
799 let entity_type = match entity_type_str {
800 "PER" => EntityType::Person,
801 "ORG" => EntityType::Organization,
802 "LOC" => EntityType::Location,
803 "MISC" => EntityType::custom("misc", EntityCategory::Misc),
804 "DATE" => EntityType::Date,
805 "MONEY" => EntityType::Money,
806 "PERCENT" => EntityType::Percent,
807 _ => continue,
808 };
809
810 if prefix == "B" {
811 current_entities.push(GoldEntity::with_span(
813 word,
814 entity_type,
815 word_start,
816 word_end,
817 ));
818 } else if prefix == "I" {
819 if let Some(last) = current_entities.last_mut() {
821 if entity_type_matches(&last.entity_type, &entity_type) {
822 last.text.push(' ');
824 last.text.push_str(word);
825 last.end = word_end;
826 } else {
827 current_entities.push(GoldEntity::with_span(
829 word,
830 entity_type,
831 word_start,
832 word_end,
833 ));
834 }
835 }
836 }
837 }
838 }
839
840 if !current_text.is_empty() {
842 test_cases.push((current_text, current_entities));
843 }
844
845 for (text, entities) in &test_cases {
847 let validation_result = validation::validate_ground_truth_entities(text, entities, false);
848 if !validation_result.is_valid {
849 return Err(Error::InvalidInput(format!(
850 "Invalid entities in CoNLL dataset: {}",
851 validation_result.errors.join("; ")
852 )));
853 }
854 }
855
856 Ok(test_cases)
857}
858
859pub fn evaluate_ner_model(
861 model: &dyn Model,
862 test_cases: &[(String, Vec<GoldEntity>)],
863) -> Result<NEREvaluationResults> {
864 evaluate_ner_model_with_mapper(model, test_cases, None)
865}
866
867pub fn evaluate_ner_model_with_mapper(
892 model: &dyn Model,
893 test_cases: &[(String, Vec<GoldEntity>)],
894 type_mapper: Option<&anno::TypeMapper>,
895) -> Result<NEREvaluationResults> {
896 let evaluator = evaluator::StandardNEREvaluator::new();
897
898 if test_cases.is_empty() {
899 return Ok(NEREvaluationResults {
900 precision: 0.0,
901 recall: 0.0,
902 f1: 0.0,
903 macro_f1: None,
904 weighted_f1: None,
905 per_type: HashMap::new(),
906 tokens_per_second: 0.0,
907 found: 0,
908 expected: 0,
909 metadata: Some(EvaluationMetadata {
910 num_test_cases: 0,
911 total_gold_entities: Some(0),
912 timestamp: Some(chrono::Utc::now().to_rfc3339()),
913 anno_version: Some(env!("CARGO_PKG_VERSION").to_string()),
914 ..Default::default()
915 }),
916 });
917 }
918
919 let mut query_metrics = Vec::new();
921 for (i, (text, ground_truth)) in test_cases.iter().enumerate() {
922 let test_case_id = format!("test_case_{}", i);
923
924 let normalized_truth: Vec<GoldEntity>;
926 let truth_ref = if let Some(mapper) = type_mapper {
927 normalized_truth = ground_truth
928 .iter()
929 .map(|e| GoldEntity {
930 text: e.text.clone(),
931 entity_type: mapper.normalize(e.entity_type.as_label()),
932 original_label: e.original_label.clone(), start: e.start,
934 end: e.end,
935 })
936 .collect();
937 &normalized_truth
938 } else {
939 ground_truth
940 };
941
942 let metrics = evaluator.evaluate_test_case(model, text, truth_ref, Some(&test_case_id))?;
943 query_metrics.push(metrics);
944 }
945
946 let aggregate = evaluator.aggregate(&query_metrics)?;
948
949 let macro_f1 = if aggregate.per_type.is_empty() {
951 None
952 } else {
953 let sum: f64 = aggregate.per_type.values().map(|m| m.f1).sum();
954 Some(sum / aggregate.per_type.len() as f64)
955 };
956
957 let weighted_f1 = if aggregate.per_type.is_empty() || aggregate.total_expected == 0 {
959 None
960 } else {
961 let weighted_sum: f64 = aggregate
962 .per_type
963 .values()
964 .map(|m| m.f1 * m.expected as f64)
965 .sum();
966 Some(weighted_sum / aggregate.total_expected as f64)
967 };
968
969 Ok(NEREvaluationResults {
970 precision: aggregate.precision.get(),
971 recall: aggregate.recall.get(),
972 f1: aggregate.f1.get(),
973 macro_f1,
974 weighted_f1,
975 per_type: aggregate.per_type,
976 tokens_per_second: aggregate.tokens_per_second,
977 found: aggregate.total_found,
978 expected: aggregate.total_expected,
979 metadata: Some(EvaluationMetadata {
980 num_test_cases: aggregate.num_test_cases,
981 total_gold_entities: Some(aggregate.total_expected),
982 timestamp: Some(chrono::Utc::now().to_rfc3339()),
983 anno_version: Some(env!("CARGO_PKG_VERSION").to_string()),
984 ..Default::default()
985 }),
986 })
987}
988
989pub fn compare_ner_models(
991 models: &[(&str, &dyn Model)],
992 test_cases: &[(String, Vec<GoldEntity>)],
993) -> Result<HashMap<String, NEREvaluationResults>> {
994 let mut results = HashMap::new();
995
996 for (name, model) in models {
997 log::info!("Evaluating {}...", name);
998 let result = evaluate_ner_model(*model, test_cases)?;
999 results.insert(name.to_string(), result);
1000 }
1001
1002 Ok(results)
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 #[test]
1010 fn test_entity_type_to_string() {
1011 assert_eq!(entity_type_to_string(&EntityType::Person), "PER");
1012 assert_eq!(entity_type_to_string(&EntityType::Organization), "ORG");
1013 assert_eq!(entity_type_to_string(&EntityType::Location), "LOC");
1014 }
1015
1016 #[test]
1017 fn test_entity_type_matches() {
1018 assert!(entity_type_matches(
1019 &EntityType::Person,
1020 &EntityType::Person
1021 ));
1022 assert!(!entity_type_matches(
1023 &EntityType::Person,
1024 &EntityType::Organization
1025 ));
1026 }
1027}