Skip to content

Commit 3cd4332

Browse files
luv-oct22zzc-1998
andauthored
Molrecbench (#1658)
* add molrecbench * add DATASET_URL * recover the dataset download * recover eval part * update utils for molrecbench * Update requirements.txt add rdkit package * Update utils.py Fix whitespace issue * Update converter.py fix whitespace issue --------- Co-authored-by: Zicheng Zhang <58689334+zzc-1998@users.noreply.github.com>
1 parent d35ec22 commit 3cd4332

13 files changed

Lines changed: 39605 additions & 2 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ protobuf
4444
pylatexenc==2.10
4545
python-dotenv
4646
qwen_vl_utils
47+
rdkit
4748
requests
4849
rich
4950
rouge

vlmeval/dataset/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
from .mmsafetybench import MMSafetyBenchDataset
9898
from .mmsibench import MMSIBench, MMSIVideoBench
9999
from .moat import MOAT
100+
from .molrecbench_wild import MolRecBenchWildDataset
100101
from .moviechat1k import MovieChat1k
101102
from .mrarebench import MRareBenchDiagnosis, MRareBenchEvidenceVerif
102103
from .mssbench import MSSBenchDataset
@@ -313,8 +314,8 @@ def evaluate(self, eval_file, **judge_kwargs):
313314
Design2Code, VLADBench, SSIBenchDataset, NPMM, SGI_Bench_Experimental_Reasoning, MMOral_OPG_OPEN, MMOral_OPG_CLOSED, # noqa: E501
314315
SciDocBench, OmniMat,
315316
MMRarebenchDiagnosis, MMRarebenchTreatment, MMRarebenchCrossmodal, MMRarebenchExamination,
316-
MRareBenchDiagnosis, MRareBenchEvidenceVerif,
317-
BabyVision, WildprobeDataset, PerceptionBench, SUPERChemDataset,
317+
MRareBenchDiagnosis, MRareBenchEvidenceVerif, MolRecBenchWildDataset, BabyVision, WildprobeDataset,
318+
PerceptionBench, SUPERChemDataset,
318319
]
319320

320321
# add by EASI team

vlmeval/dataset/molrebench_wild.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""MolRecBench-Wild benchmark backed by one self-contained TSV file."""
2+
3+
from __future__ import annotations
4+
5+
import base64
6+
import json
7+
import os
8+
from pathlib import Path
9+
from typing import Any, Mapping
10+
11+
import pandas as pd
12+
13+
from vlmeval.smp import LMUDataRoot, decode_base64_to_image_file, load
14+
15+
from .image_base import ImageBaseDataset
16+
17+
18+
SMILES_DATASET = 'MolRecBench_Wild_SMILES'
19+
SGRAPH_DATASET = 'MolRecBench_Wild_SGraph'
20+
GRAPH_DATASET = 'MolRecBench_Wild_Graph'
21+
22+
DATASET_TRACKS = {
23+
SMILES_DATASET: ('SMILES', 'smiles'),
24+
SGRAPH_DATASET: ('SGraph', 's_graph'),
25+
GRAPH_DATASET: ('Graph', 'graph'),
26+
}
27+
28+
29+
class MolRecBenchWildDataset(ImageBaseDataset):
30+
"""Load MolRecBench-Wild TSV, build prompts, and run its evaluator."""
31+
32+
TYPE = 'VQA'
33+
MODALITY = 'IMAGE'
34+
35+
# The prepared TSV is currently local. After uploading it, replace these
36+
# empty values with the same downloadable TSV URL for all three tracks.
37+
DATASET_URL = {name: '' for name in DATASET_TRACKS}
38+
DATASET_MD5 = {name: None for name in DATASET_TRACKS}
39+
40+
def __init__(
41+
self,
42+
dataset: str = SMILES_DATASET,
43+
tsv_path: str | os.PathLike[str] | None = None,
44+
) -> None:
45+
if dataset not in DATASET_TRACKS:
46+
raise ValueError(f'unsupported MolRecBench-Wild dataset: {dataset}')
47+
self.track_name, self.track = DATASET_TRACKS[dataset]
48+
self.tsv_path = self._resolve_tsv_path(tsv_path)
49+
self._asset_paths: dict[str, str] = {}
50+
self._ground_truth: list[dict[str, Any]] = []
51+
super().__init__(dataset=dataset, skip_noimg=False)
52+
53+
@classmethod
54+
def supported_datasets(cls) -> list[str]:
55+
return list(DATASET_TRACKS)
56+
57+
@staticmethod
58+
def _resolve_tsv_path(tsv_path: str | os.PathLike[str] | None) -> Path:
59+
if tsv_path is not None:
60+
path = Path(tsv_path).expanduser().resolve()
61+
else:
62+
lmu_path = Path(LMUDataRoot()) / 'MolRecBench-Wild.tsv'
63+
repo_path = Path(__file__).resolve().parents[2] / 'data' / 'MolRecBench-Wild.tsv'
64+
path = lmu_path if lmu_path.is_file() else repo_path
65+
if not path.is_file():
66+
raise FileNotFoundError(
67+
f'MolRecBench-Wild TSV not found at {path}. '
68+
'Pass tsv_path or place MolRecBench-Wild.tsv under $LMUData.'
69+
)
70+
return path
71+
72+
@staticmethod
73+
def _string(value: Any) -> str:
74+
if value is None or pd.isna(value):
75+
return ''
76+
return str(value)
77+
78+
@staticmethod
79+
def _reference_key(value: Any) -> str:
80+
"""Normalize TSV numeric references such as ``0.0`` back to ``0``."""
81+
if isinstance(value, float) and value.is_integer():
82+
return str(int(value))
83+
return MolRecBenchWildDataset._string(value)
84+
85+
@staticmethod
86+
def _integer_index(value: Any) -> int:
87+
"""Return a VLMEvalKit integer index without accepting lossy values."""
88+
if value is None or pd.isna(value) or isinstance(value, bool):
89+
raise ValueError(f'invalid integer index: {value!r}')
90+
try:
91+
number = float(value)
92+
except (TypeError, ValueError) as error:
93+
raise ValueError(f'invalid integer index: {value!r}') from error
94+
if not number.is_integer():
95+
raise ValueError(f'invalid integer index: {value!r}')
96+
return int(number)
97+
98+
@staticmethod
99+
def _resolve_image(reference: Any, image_map: Mapping[str, str]) -> str:
100+
value = MolRecBenchWildDataset._string(reference)
101+
seen: set[str] = set()
102+
while value and len(value) <= 64:
103+
if value in seen:
104+
raise ValueError(f'cyclic image reference: {value}')
105+
seen.add(value)
106+
if value not in image_map:
107+
raise ValueError(f'unknown image reference: {value}')
108+
value = image_map[value]
109+
if not value:
110+
raise ValueError('empty image value')
111+
try:
112+
base64.b64decode(value, validate=True)
113+
except Exception as error:
114+
raise ValueError('invalid base64 image value') from error
115+
return value
116+
117+
def _materialize_assets(
118+
self,
119+
assets: pd.DataFrame,
120+
image_map: Mapping[str, str],
121+
) -> None:
122+
asset_root = Path(self.img_root) / 'assets'
123+
asset_root.mkdir(parents=True, exist_ok=True)
124+
for _, row in assets.iterrows():
125+
index = self._string(row['index'])
126+
filename = Path(self._string(row['image_path'])).name
127+
if not filename:
128+
raise ValueError(f'asset {index} has no image_path')
129+
image = self._resolve_image(index, image_map)
130+
path = asset_root / filename
131+
if not path.is_file():
132+
decode_base64_to_image_file(image, str(path))
133+
self._asset_paths[index] = str(path)
134+
135+
def load_data(self, dataset: str) -> pd.DataFrame:
136+
frame = load(str(self.tsv_path))
137+
if not isinstance(frame, pd.DataFrame):
138+
raise TypeError('MolRecBench-Wild TSV must load as a pandas DataFrame')
139+
required = {
140+
'index', 'record_type', 'sample_id', 'track', 'image', 'image_path',
141+
'question', 'answer', 'reference_image_1', 'reference_image_2',
142+
}
143+
missing = sorted(required - set(frame.columns))
144+
if missing:
145+
raise ValueError(f'MolRecBench-Wild TSV is missing: {", ".join(missing)}')
146+
147+
frame = frame.copy()
148+
frame['index'] = frame['index'].map(self._string)
149+
if frame['index'].duplicated().any():
150+
raise ValueError('MolRecBench-Wild TSV contains duplicate indices')
151+
image_map = dict(zip(frame['index'], frame['image'].map(self._string)))
152+
153+
assets = frame[frame['record_type'] == 'asset']
154+
self._materialize_assets(assets, image_map)
155+
156+
all_samples = frame[frame['record_type'] == 'sample']
157+
records: dict[str, dict[str, Any]] = {}
158+
for _, row in all_samples.iterrows():
159+
sample_id = self._string(row['sample_id'])
160+
record = json.loads(self._string(row['answer']))
161+
if not isinstance(record, dict) or record.get('id') != sample_id:
162+
raise ValueError(f'invalid annotation for {sample_id}')
163+
if sample_id in records and records[sample_id] != record:
164+
raise ValueError(f'inconsistent annotations for {sample_id}')
165+
records[sample_id] = record
166+
self._ground_truth = list(records.values())
167+
168+
samples = all_samples[all_samples['track'] == self.track_name].copy()
169+
if samples.empty:
170+
raise ValueError(f'TSV contains no {self.track_name} rows')
171+
samples['image'] = [self._resolve_image(value, image_map) for value in samples['image']]
172+
samples['index'] = samples['index'].map(self._integer_index)
173+
samples['image_path'] = samples['sample_id'].map(lambda value: Path(str(value)).name)
174+
return samples.reset_index(drop=True)
175+
176+
def build_prompt(self, line: int | pd.Series) -> list[dict[str, str]]:
177+
if isinstance(line, int):
178+
line = self.data.iloc[line]
179+
target = self.dump_image(line)[0]
180+
messages: list[dict[str, str]] = []
181+
if self.track == 'graph':
182+
for column in ('reference_image_1', 'reference_image_2'):
183+
reference = self._reference_key(line[column])
184+
if reference not in self._asset_paths:
185+
raise ValueError(f'unknown Graph asset reference: {reference}')
186+
messages.append({'type': 'image', 'value': self._asset_paths[reference]})
187+
messages.extend([
188+
{'type': 'image', 'value': target},
189+
{'type': 'text', 'value': self._string(line['question'])},
190+
])
191+
return messages
192+
193+
def evaluate(self, eval_file: str, **judge_kwargs: Any) -> pd.DataFrame:
194+
del judge_kwargs
195+
predictions = load(eval_file)
196+
if not isinstance(predictions, pd.DataFrame):
197+
raise TypeError('prediction file must load as a pandas DataFrame')
198+
if not {'index', 'prediction'} <= set(predictions.columns):
199+
raise ValueError('prediction file requires index and prediction columns')
200+
201+
selected_indices = [self._integer_index(index) for index in self.data['index']]
202+
prediction_map = {
203+
self._integer_index(index): prediction
204+
for index, prediction in zip(predictions['index'], predictions['prediction'])
205+
}
206+
missing = [index for index in selected_indices if index not in prediction_map]
207+
if missing:
208+
raise ValueError(f'prediction file misses {len(missing)} samples')
209+
sample_ids = list(self.data['sample_id'].map(self._string))
210+
selected = pd.DataFrame({
211+
# The official converter keys records by the molecule image name,
212+
# while VLMEvalKit uses the TSV's integer index during inference.
213+
'index': sample_ids,
214+
'prediction': [prediction_map[index] for index in selected_indices],
215+
})
216+
217+
from .utils.molrecbench_wild import convert_dataframe, score_records
218+
219+
converted, _ = convert_dataframe(selected, track=self.track)
220+
selected_set = set(sample_ids)
221+
ground_truth = [row for row in self._ground_truth if row['id'] in selected_set]
222+
result = score_records(
223+
ground_truth,
224+
converted,
225+
self.track,
226+
full_gt_records=self._ground_truth,
227+
timeout_seconds=5,
228+
ignore_cistrans=True,
229+
)
230+
rows = []
231+
for split in ('Full', 'A', 'B', 'C'):
232+
metric = result.summary['subset_metrics'][split]
233+
rows.append({
234+
'track': self.track_name,
235+
'split': split,
236+
'total': metric['total_gt_records'],
237+
'scored': metric['scored_records'],
238+
'correct': metric['correct_records'],
239+
'accuracy': metric['accuracy'],
240+
})
241+
return pd.DataFrame(rows)

0 commit comments

Comments
 (0)