File size: 6,482 Bytes
2f86d9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# Copyright 2026 DeepMind Technologies Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for PubMedQA evaluation utilities."""

import dataclasses

from absl.testing import absltest
from absl.testing import parameterized
from gemma.diffusion.hackable_diffusion_adapter.eval import pubmedqa_eval
import jax.numpy as jnp


class ExtractPubMedQAAnswerTest(parameterized.TestCase):

  @parameterized.parameters(
      # Explicit "The answer is:" marker variants.
      ("The answer is: yes", "yes"),
      ("The answer is: no", "no"),
      ("The answer is: maybe", "maybe"),
      ("The answer is:yes", "yes"),  # No space after colon.
      ("The answer is yes", "yes"),  # Without colon.
      (
          "Based on the evidence, the study shows improvement."
          + " The answer is: yes",
          "yes",
      ),
      (
          "The results are inconclusive. The answer is: maybe",
          "maybe",
      ),
  )
  def test_extraction(self, text, expected):
    self.assertEqual(pubmedqa_eval.extract_pubmedqa_answer(text), expected)

  @parameterized.parameters(
      "",
      "The study was inconclusive.",
      "42",
      # No fallback heuristics — bare answers without the marker return None.
      "yes",
      "Yes",
      "no",
      "maybe",
      "No, the study does not support this.",
      "Maybe, further studies are needed.",
      # Previously these would incorrectly match via heuristics.
      "Based on the results, no significant effect was found.",
      "The study showed novel approaches to treatment.",
      "Yesterday was a good day.",  # "yes" substring in "yesterday".
  )
  def test_extraction_returns_none(self, text):
    self.assertIsNone(pubmedqa_eval.extract_pubmedqa_answer(text))


class ScorePubMedQATest(parameterized.TestCase):

  @parameterized.parameters(
      ("The answer is: yes", "yes", True),
      ("The answer is: no", "no", True),
      ("The answer is: maybe", "maybe", True),
      ("The answer is: yes", "no", False),
      ("The answer is: no", "maybe", False),
      # Without marker — no match even if the answer word appears.
      ("yes", "yes", False),
      ("no", "no", False),
      ("The study is inconclusive.", "yes", False),
      ("Based on the results, no significant effect was found.", "no", False),
  )
  def test_scoring(self, generated, ground_truth, expected):
    self.assertEqual(
        pubmedqa_eval.score_pubmedqa(generated, ground_truth), expected
    )


class MockTokenizer:

  def decode(self, tokens: list[int]) -> str:
    # 1 maps to "The answer is: yes", 2 to "yes",
    # 3 to "The answer is: no", 4 to "no"
    mapping = {
        1: "The answer is: yes",
        2: "yes",
        3: "The answer is: no",
        4: "no",
    }
    return " ".join(mapping.get(t, "") for t in tokens if t > 0).strip()


class PubMedQAAccuracyTest(parameterized.TestCase):

  def test_pubmedqa_accuracy(self):

    @dataclasses.dataclass(kw_only=True, frozen=True)
    class TestPubMedQAAccuracy(pubmedqa_eval.PubMedQAAccuracy):

      @property
      def tokenizer(self) -> MockTokenizer:
        return MockTokenizer()

    metric = TestPubMedQAAccuracy(
        tokens="samples",
        ground_truth="batch.short_answer_tokens",
    )

    tokens = jnp.array([
        [1, -1],  # "The answer is: yes"
        [3, -1],  # "The answer is: no"
    ])
    ground_truth = jnp.array([
        [2, -1],  # "yes"
        [2, -1],  # "yes"
    ])

    state = metric.get_state(tokens=tokens, ground_truth=ground_truth)
    accuracy = state.compute()

    self.assertEqual(accuracy, 0.5)


class ScoreBleuTest(parameterized.TestCase):
  """Tests for score_bleu."""

  def test_perfect_match(self):
    """Identical strings should yield 100."""
    score = pubmedqa_eval.score_bleu("Bonjour le monde", "Bonjour le monde")
    self.assertAlmostEqual(score, 100.0, places=1)

  def test_wrong_text(self):
    """Completely wrong text should yield low BLEU."""
    score = pubmedqa_eval.score_bleu("This is wrong", "Bonjour le monde")
    self.assertLess(score, 10.0)

  def test_empty_generated(self):
    score = pubmedqa_eval.score_bleu("", "Bonjour")
    self.assertEqual(score, 0.0)

  def test_empty_ground_truth(self):
    score = pubmedqa_eval.score_bleu("Bonjour", "")
    self.assertEqual(score, 0.0)

  def test_both_empty(self):
    score = pubmedqa_eval.score_bleu("", "")
    self.assertEqual(score, 0.0)

  def test_partial_match(self):
    gt = "Le chat dort sur le tapis rouge dans la grande maison blanche"
    partial = "Le chat mange sur le tapis rouge dans la petite maison blanche"
    score = pubmedqa_eval.score_bleu(partial, gt)
    self.assertGreater(score, 0.0)
    self.assertLess(score, 100.0)

  def test_whitespace_handling(self):
    score = pubmedqa_eval.score_bleu("  Bonjour  ", "  Bonjour  ")
    self.assertAlmostEqual(score, 100.0, places=1)


class BLEUScoreMetricTest(absltest.TestCase):

  def test_tokenizer_path_default(self):
    metric = pubmedqa_eval.BLEUScore(
        tokens="samples",
        ground_truth="batch.response_tokens",
    )
    self.assertIn("gemma4", metric.tokenizer_path)

  def test_bleu_round_trip(self):

    class MockBLEUTokenizer:

      def decode(self, tokens: list[int]) -> str:
        mapping = {1: "Bonjour le monde", 2: "Bonjour le monde"}
        return " ".join(mapping.get(t, "") for t in tokens if t > 0).strip()

    @dataclasses.dataclass(kw_only=True, frozen=True)
    class TestBLEUScore(pubmedqa_eval.BLEUScore):

      @property
      def tokenizer(self) -> MockBLEUTokenizer:
        return MockBLEUTokenizer()

    metric = TestBLEUScore(
        tokens="samples",
        ground_truth="batch.response_tokens",
    )
    tokens = jnp.array([[1, -1]])
    ground_truth = jnp.array([[2, -1]])

    state = metric.get_state(tokens=tokens, ground_truth=ground_truth)
    score = state.compute()

    self.assertAlmostEqual(score, 100.0, places=1)


if __name__ == "__main__":
  absltest.main()