File size: 5,264 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
# 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.

"""Convert PubMedQA JSON data to JSONL format for HD text diffusion SFT.

Reads raw PubMedQA JSON from source directory and writes JSONL files with
fields:
  - prompt: Formatted question + context
  - response: Short answer (yes/no/maybe)
  - long_response: Long-form answer
  - short_answer: Ground truth label (yes/no/maybe) for evaluation

Usage:
  python convert_pubmedqa.py --output_dir=./pubmedqa_jsonl
"""

import json
import os
from typing import Any

from absl import app
from absl import flags

_INPUT_DIR = flags.DEFINE_string(
    "input_dir",
    "gemma/diffusion/hackable_diffusion_adapter/data/pubmedqa",
    "Source directory containing PubMedQA JSON files.",
)
_OUTPUT_DIR = flags.DEFINE_string(
    "output_dir",
    "",
    "Output directory for JSONL files. Required.",
)
_MAX_CONTEXT_CHARS = flags.DEFINE_integer(
    "max_context_chars",
    4000,
    "Maximum total characters for concatenated context paragraphs.",
)


def _read_file(path: str) -> str:
  """Read a file using standard Python file I/O."""
  with open(path, "r", encoding="utf-8") as f:
    return f.read()


def _write_file(path: str, content: str) -> None:
  """Write a file using standard Python file I/O."""
  os.makedirs(os.path.dirname(path), exist_ok=True)
  with open(path, "w", encoding="utf-8") as f:
    f.write(content)


def _format_prompt(question: str, contexts: list[str]) -> str:
  """Format a PubMedQA example into a clean prompt.

  Concatenates all context paragraphs (truncating if needed) with the question.

  Args:
    question: The question string.
    contexts: A list of context paragraph strings.

  Returns:
    The formatted prompt string.
  """
  context_text = "\n".join(contexts)
  if len(context_text) > _MAX_CONTEXT_CHARS.value:
    context_text = context_text[: _MAX_CONTEXT_CHARS.value] + "..."

  return f"Context:\n{context_text}\n\nQuestion: {question}"


def _convert_json_to_examples(data: dict[str, Any]) -> list[dict[str, Any]]:
  """Convert PubMedQA nested JSON to flat list of examples."""
  examples = []
  for pubmed_id, entry in data.items():
    question = entry.get("QUESTION", "")
    contexts = entry.get("CONTEXTS", [])
    short_answer = entry.get("final_decision", "")
    long_answer = entry.get("LONG_ANSWER", "")

    prompt = _format_prompt(question, contexts)

    examples.append({
        "pubmed_id": str(pubmed_id),
        "prompt": prompt,
        "response": short_answer,
        "long_response": long_answer,
        "short_answer": short_answer,
    })
  return examples


def _write_jsonl(examples: list[dict[str, Any]], output_path: str) -> None:
  """Write examples as JSONL (one JSON object per line)."""
  lines = [json.dumps(ex, ensure_ascii=False) for ex in examples]
  content = "\n".join(lines) + "\n"
  _write_file(output_path, content)
  print(f"Wrote {len(examples)} examples to {output_path}")


def main(argv):
  del argv

  if not _OUTPUT_DIR.value:
    raise ValueError("--output_dir is required.")

  input_dir = _INPUT_DIR.value
  output_dir = _OUTPUT_DIR.value

  # --- Load full PQA-L dataset (1000 expert-labeled examples) ---
  pqal_path = os.path.join(input_dir, "ori_pqal.json")
  print(f"Reading PQA-L data from {pqal_path}...")
  pqal_data = json.loads(_read_file(pqal_path))

  # --- Load official test set ground truth (to get test IDs) ---
  test_gt_path = os.path.join(input_dir, "test_ground_truth.json")
  print(f"Reading test ground truth from {test_gt_path}...")
  test_gt_data = json.loads(_read_file(test_gt_path))
  test_ids = set(test_gt_data.keys())

  # --- Split full dataset into train and test ---
  # ori_pqal.json contains all 1000 PQA-L examples, including the 500 that
  # are officially designated as the test set. We split them using test_ids.
  train_data = {k: v for k, v in pqal_data.items() if k not in test_ids}
  test_data = {k: v for k, v in pqal_data.items() if k in test_ids}

  # --- Convert ---
  train_examples = _convert_json_to_examples(train_data)
  test_examples = _convert_json_to_examples(test_data)

  overlap = set(e["pubmed_id"] for e in train_examples) & set(
      e["pubmed_id"] for e in test_examples
  )
  if overlap:
    raise ValueError(
        f"Train/test overlap detected ({len(overlap)} IDs)! This should not"
        " happen after filtering."
    )

  print(f"Training examples: {len(train_examples)} (after removing test IDs)")
  print(f"Test examples: {len(test_examples)}")

  # --- Write JSONL ---
  _write_jsonl(train_examples, os.path.join(output_dir, "pubmedqa_train.jsonl"))
  _write_jsonl(test_examples, os.path.join(output_dir, "pubmedqa_test.jsonl"))

  print("Done!")


if __name__ == "__main__":
  app.run(main)