Current section

Files

Jump to
crucible_datasets lib dataset_manager evaluator exact_match.ex
Raw

lib/dataset_manager/evaluator/exact_match.ex

defmodule CrucibleDatasets.Evaluator.ExactMatch do
@moduledoc """
Exact match evaluation (case-insensitive, normalized).
Handles:
- String comparison (normalized, case-insensitive)
- Numerical comparison (with tolerance)
- Multiple choice (index comparison)
- Set comparison (unordered lists)
"""
@doc """
Compute exact match score (1.0 or 0.0).
"""
def compute(predicted, expected) when is_binary(predicted) and is_binary(expected) do
if normalize_string(predicted) == normalize_string(expected), do: 1.0, else: 0.0
end
def compute(predicted, expected) when is_float(predicted) and is_float(expected) do
tolerance = abs(expected) * 0.01
if abs(predicted - expected) <= tolerance, do: 1.0, else: 0.0
end
def compute(predicted, expected) when is_float(predicted) and is_integer(expected) do
compute(predicted, expected * 1.0)
end
def compute(predicted, expected) when is_integer(predicted) and is_float(expected) do
compute(predicted * 1.0, expected)
end
def compute(predicted, expected) when is_integer(predicted) and is_integer(expected) do
if predicted == expected, do: 1.0, else: 0.0
end
def compute(predicted, expected) when is_list(predicted) and is_list(expected) do
if MapSet.new(predicted) == MapSet.new(expected), do: 1.0, else: 0.0
end
# Handle map comparisons (for structured answers)
def compute(predicted, expected) when is_map(predicted) and is_map(expected) do
# For maps, compare the answer field if it exists
pred_answer = Map.get(predicted, :answer) || Map.get(predicted, "answer")
exp_answer = Map.get(expected, :answer) || Map.get(expected, "answer")
if pred_answer && exp_answer do
compute(pred_answer, exp_answer)
else
# Full map comparison
if predicted == expected, do: 1.0, else: 0.0
end
end
# Handle when predicted is simple value but expected is a map (e.g., GSM8K)
def compute(predicted, expected) when is_map(expected) and not is_map(predicted) do
exp_answer = Map.get(expected, :answer) || Map.get(expected, "answer")
if exp_answer do
compute(predicted, exp_answer)
else
0.0
end
end
# Handle when predicted is different type than expected
def compute(predicted, expected) when is_binary(predicted) and is_integer(expected) do
case Integer.parse(predicted) do
{num, _} -> compute(num, expected)
:error -> 0.0
end
end
def compute(predicted, expected) when is_integer(predicted) and is_binary(expected) do
compute(Integer.to_string(predicted), expected)
end
def compute(_predicted, _expected), do: 0.0
# Normalize string for comparison
defp normalize_string(str) do
str
|> String.downcase()
|> String.trim()
|> String.replace(~r/\s+/, " ")
|> String.replace(~r/[^\w\s]/, "")
end
end