Spaces:
Sleeping
Sleeping
Download tests/test_evaluate.py from metzgerdev/vocal-semantic-search: direct link, hf CLI and curl.
- Browser
- Download file 4.62 kB
-
https://huggingface.co/spaces/metzgerdev/vocal-semantic-search/resolve/main/tests/test_evaluate.py
- Command line
-
hf download hf://spaces/metzgerdev/vocal-semantic-search/tests/test_evaluate.py
-
curl -L -o test_evaluate.py https://huggingface.co/spaces/metzgerdev/vocal-semantic-search/resolve/main/tests/test_evaluate.py
4.62 kB
| """Metric tests against hand-computed values. | |
| Eval code that is silently wrong is worse than no eval, so every metric is | |
| checked against a case whose answer can be worked out on paper. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import pytest | |
| from mss.evaluate import ( | |
| average_precision, | |
| evaluate_retrieval, | |
| markdown_table, | |
| ndcg_at_k, | |
| random_baseline, | |
| rank_matrix, | |
| recall_at_k, | |
| reciprocal_rank, | |
| ) | |
| def test_recall_counts_relevant_items_found(): | |
| ranking = np.array([3, 1, 4, 0, 2]) | |
| assert recall_at_k(ranking, {3}, 1) == 1.0 | |
| assert recall_at_k(ranking, {4}, 1) == 0.0 | |
| assert recall_at_k(ranking, {4}, 3) == 1.0 | |
| # two relevant, one inside the cut -> half of them recalled | |
| assert recall_at_k(ranking, {3, 2}, 3) == 0.5 | |
| def test_reciprocal_rank_uses_the_first_hit(): | |
| ranking = np.array([3, 1, 4, 0, 2]) | |
| assert reciprocal_rank(ranking, {3}) == 1.0 | |
| assert reciprocal_rank(ranking, {1}) == pytest.approx(1 / 2) | |
| assert reciprocal_rank(ranking, {2}) == pytest.approx(1 / 5) | |
| # first of several relevant items is what counts | |
| assert reciprocal_rank(ranking, {4, 2}) == pytest.approx(1 / 3) | |
| assert reciprocal_rank(ranking, set()) == 0.0 | |
| def test_average_precision_hand_computed(): | |
| ranking = np.array([0, 1, 2, 3, 4]) | |
| # relevant at positions 1 and 3 (1-based): precision 1/1 and 2/3 | |
| assert average_precision(ranking, {0, 2}) == pytest.approx((1.0 + 2 / 3) / 2) | |
| # a single relevant item makes AP equal to the reciprocal rank | |
| assert average_precision(ranking, {2}) == pytest.approx(1 / 3) | |
| def test_average_precision_penalises_unretrieved_relevants(): | |
| # only the top 3 can ever be hit here; the 4th relevant item is missing | |
| ranking = np.array([0, 1, 2]) | |
| ap = average_precision(ranking, {0, 1, 2, 99}) | |
| assert ap == pytest.approx((1.0 + 1.0 + 1.0) / 4) | |
| def test_ndcg_is_one_for_a_perfect_ranking(): | |
| ranking = np.array([0, 1, 2, 3]) | |
| assert ndcg_at_k(ranking, {0, 1}, 4) == pytest.approx(1.0) | |
| def test_ndcg_hand_computed_for_an_imperfect_ranking(): | |
| ranking = np.array([0, 1, 2, 3]) | |
| # one relevant item at position 2 -> DCG = 1/log2(3), IDCG = 1/log2(2) = 1 | |
| assert ndcg_at_k(ranking, {1}, 4) == pytest.approx(1 / np.log2(3)) | |
| def test_ndcg_ignores_relevants_beyond_the_cut(): | |
| ranking = np.array([9, 8, 7, 0]) | |
| assert ndcg_at_k(ranking, {0}, 3) == 0.0 | |
| def test_rank_matrix_orders_by_cosine(): | |
| items = np.eye(4, dtype=np.float32) | |
| queries = np.array([[0, 0, 1, 0]], dtype=np.float32) | |
| assert rank_matrix(queries, items)[0][0] == 2 | |
| def test_rank_matrix_is_deterministic_under_ties(): | |
| items = np.ones((5, 3), dtype=np.float32) | |
| queries = np.ones((1, 3), dtype=np.float32) | |
| a = rank_matrix(queries, items) | |
| b = rank_matrix(queries, items) | |
| np.testing.assert_array_equal(a, b) | |
| np.testing.assert_array_equal(a[0], np.arange(5)) | |
| def test_perfect_retrieval_scores_one(): | |
| vecs = np.eye(6, dtype=np.float32) | |
| res = evaluate_retrieval(vecs, vecs, [{i} for i in range(6)]) | |
| assert res.metrics["R@1"] == pytest.approx(1.0) | |
| assert res.metrics["MRR"] == pytest.approx(1.0) | |
| assert res.metrics["mAP"] == pytest.approx(1.0) | |
| assert res.metrics["median_rank"] == 1.0 | |
| def test_metrics_are_bounded_and_ordered(): | |
| rng = np.random.default_rng(0) | |
| q = rng.standard_normal((30, 32)).astype(np.float32) | |
| it = rng.standard_normal((80, 32)).astype(np.float32) | |
| q /= np.linalg.norm(q, axis=1, keepdims=True) | |
| it /= np.linalg.norm(it, axis=1, keepdims=True) | |
| res = evaluate_retrieval(q, it, [{i} for i in range(30)]) | |
| for key, val in res.metrics.items(): | |
| if key != "median_rank": | |
| assert 0.0 <= val <= 1.0, key | |
| # recall cannot shrink as the cut widens | |
| assert res.metrics["R@1"] <= res.metrics["R@5"] <= res.metrics["R@10"] | |
| def test_random_baseline_lands_near_theory(): | |
| n_items = 1000 | |
| res = random_baseline(n_items, [{i} for i in range(400)], seed=1) | |
| # with one relevant item among n, expected R@k is k/n | |
| assert res.metrics["R@10"] == pytest.approx(10 / n_items, abs=0.01) | |
| assert res.metrics["median_rank"] == n_items / 2 | |
| def test_markdown_table_has_a_row_per_result(): | |
| rng = np.random.default_rng(2) | |
| v = rng.standard_normal((5, 8)).astype(np.float32) | |
| v /= np.linalg.norm(v, axis=1, keepdims=True) | |
| r1 = evaluate_retrieval(v, v, [{i} for i in range(5)], name="a") | |
| r2 = random_baseline(5, [{i} for i in range(5)]) | |
| table = markdown_table([r1, r2]) | |
| assert table.count("\n") == 3 # header + separator + 2 rows | |
| assert "| a |" in table | |