Skip to content

Commit 416e85c

Browse files
committed
feat(openevaluator): implement open evaluator for generation level tasks
#4
1 parent 5f7198f commit 416e85c

6 files changed

Lines changed: 259 additions & 59 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@
1616

1717
# Local data & playground
1818
data/
19+
data_test/
1920
playground/

spectrumlab/benchmark/base.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from typing import List, Dict, Union
44
import json
5+
import os
56

67

78
class BaseGroup(ABC):
@@ -60,14 +61,26 @@ def _load_from_remote(self, local_level_path: Path):
6061
# TODO
6162
self.datasets = {}
6263

63-
def _fix_image_path(self, image_path: str) -> str:
64-
if not image_path or not image_path.strip():
64+
def _fix_image_path(self, image_path):
65+
if isinstance(image_path, list):
66+
return [self._fix_image_path(p) for p in image_path]
67+
if not image_path or not str(image_path).strip():
6568
return image_path
66-
if image_path.startswith("./data/"):
67-
relative_part = image_path[7:]
69+
# 支持 ./data/ 和 data/ 开头
70+
s = str(image_path)
71+
if s.startswith("./data/"):
72+
relative_part = s[7:]
6873
corrected_path = self.data_root / relative_part
6974
return str(corrected_path)
70-
return image_path
75+
if s.startswith("data/"):
76+
corrected_path = self.data_root / s[5:]
77+
return str(corrected_path)
78+
# 如果已经是绝对路径,直接返回
79+
if os.path.isabs(s):
80+
return s
81+
# 其它相对路径,拼到 data_root 下
82+
corrected_path = self.data_root / s
83+
return str(corrected_path)
7184

7285
def _load_json(self, file_path: Path) -> List[Dict]:
7386
try:
@@ -79,7 +92,19 @@ def _load_json(self, file_path: Path) -> List[Dict]:
7992
if item["image_path"]:
8093
original_path = item["image_path"]
8194
item["image_path"] = self._fix_image_path(original_path)
82-
95+
# 修正 answer 字段(如果是图片路径或图片路径 list)
96+
if isinstance(item, dict) and "answer" in item:
97+
answer = item["answer"]
98+
# 只修正字符串类型且像图片路径的 answer
99+
if isinstance(answer, str) and answer.lower().endswith(
100+
(".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
101+
):
102+
item["answer"] = self._fix_image_path(answer)
103+
# 如果 answer 是 list(极少见),也递归修正
104+
if isinstance(answer, list):
105+
item["answer"] = [
106+
self._fix_image_path(a) for a in answer
107+
]
83108
return data
84109
else:
85110
print(f"Warning: Expected list in {file_path}, got {type(data)}")

spectrumlab/evaluator/base.py

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -107,61 +107,61 @@ def evaluate_many(
107107
) -> Dict:
108108
"""
109109
Evaluate a single model on data_items with parallel processing.
110-
110+
111111
Args:
112112
data_items: List of data items to evaluate
113113
model: Model instance to evaluate
114114
max_out_len: Maximum output length for model generation
115115
batch_size: Batch size for processing (if None, will be auto-calculated)
116116
save_path: Base path to save results
117117
n_jobs: Number of parallel jobs (-1 for all available cores)
118-
118+
119119
Returns:
120120
Dictionary containing evaluation results
121121
"""
122122
import multiprocessing as mp
123123
from concurrent.futures import ThreadPoolExecutor, as_completed
124124
import math
125-
125+
126126
if not data_items:
127127
print("❌ No data items provided")
128128
return {"error": "No data items provided"}
129-
129+
130130
# Set number of jobs
131131
if n_jobs == -1:
132132
n_jobs = mp.cpu_count()
133-
133+
134134
# Calculate batch size if not provided
135135
if batch_size is None:
136136
batch_size = max(1, math.ceil(len(data_items) / n_jobs))
137-
137+
138138
print(f"🔄 Starting parallel evaluation on {len(data_items)} items...")
139139
print(f"📝 Model: {type(model).__name__}")
140140
print(f"⚡ Using {n_jobs} parallel workers with batch size {batch_size}")
141-
141+
142142
# Split data into batches
143143
batches = [
144-
data_items[i:i + batch_size]
144+
data_items[i : i + batch_size]
145145
for i in range(0, len(data_items), batch_size)
146146
]
147-
147+
148148
print(f"📦 Split into {len(batches)} batches")
149-
149+
150150
# Build prompts for all items
151151
print("📝 Building prompts...")
152152
all_prompts = [self._build_prompt(item) for item in data_items]
153-
153+
154154
# Split prompts into batches
155155
prompt_batches = [
156-
all_prompts[i:i + batch_size]
156+
all_prompts[i : i + batch_size]
157157
for i in range(0, len(all_prompts), batch_size)
158158
]
159-
159+
160160
def process_batch(batch_data):
161161
"""Process a batch of prompts and return responses."""
162162
batch_prompts, batch_indices = batch_data
163163
batch_responses = []
164-
164+
165165
for i, prompt in enumerate(batch_prompts):
166166
try:
167167
response = model.generate(prompt, max_out_len)
@@ -171,41 +171,41 @@ def process_batch(batch_data):
171171
original_index = batch_indices[i]
172172
print(f"\n⚠️ Error on item {original_index + 1}: {e}")
173173
batch_responses.append(f"Error: {str(e)}")
174-
174+
175175
return batch_indices, batch_responses
176-
176+
177177
# Prepare batch data with indices
178178
batch_data_list = []
179179
for i, prompt_batch in enumerate(prompt_batches):
180180
start_idx = i * batch_size
181181
end_idx = min(start_idx + batch_size, len(data_items))
182182
batch_indices = list(range(start_idx, end_idx))
183183
batch_data_list.append((prompt_batch, batch_indices))
184-
184+
185185
# Execute parallel processing
186186
all_responses = [None] * len(data_items)
187-
187+
188188
with ThreadPoolExecutor(max_workers=n_jobs) as executor:
189189
# Submit all batch tasks
190190
future_to_batch = {
191-
executor.submit(process_batch, batch_data): batch_data[1][0]
191+
executor.submit(process_batch, batch_data): batch_data[1][0]
192192
for batch_data in batch_data_list
193193
}
194-
194+
195195
# Collect results as they complete
196196
for future in tqdm(
197-
as_completed(future_to_batch),
197+
as_completed(future_to_batch),
198198
total=len(future_to_batch),
199199
desc="Processing batches",
200-
unit="batch"
200+
unit="batch",
201201
):
202202
try:
203203
batch_indices, batch_responses = future.result()
204204
for idx, response in zip(batch_indices, batch_responses):
205205
all_responses[idx] = response
206206
except Exception as e:
207207
print(f"❌ Error processing batch: {e}")
208-
208+
209209
# Process responses and calculate results
210210
print("🔍 Processing responses...")
211211
processed_items = []
@@ -225,27 +225,27 @@ def process_batch(batch_data):
225225
item_copy["pass"] = is_correct
226226

227227
processed_items.append(item_copy)
228-
228+
229229
# Save results
230230
saved_files = self._save_results(processed_items, save_path)
231231
print(f"💾 Results saved to: {saved_files}")
232-
232+
233233
# Calculate metrics
234234
print("📊 Calculating metrics...")
235235
metrics = self._calculate_metrics(processed_items)
236-
236+
237237
# Print results
238238
self._print_results(metrics)
239-
239+
240240
return {
241241
"metrics": metrics,
242242
"saved_files": saved_files,
243243
"total_items": len(data_items),
244244
"parallel_info": {
245245
"n_jobs": n_jobs,
246246
"batch_size": batch_size,
247-
"n_batches": len(batches)
248-
}
247+
"n_batches": len(batches),
248+
},
249249
}
250250

251251
def _save_results(self, results_data: List[Dict], save_path: str) -> List[str]:
@@ -302,7 +302,10 @@ def _calculate_metrics(self, processed_items: List[Dict]) -> Dict:
302302
sub_category = item.get("sub_category", "Unknown")
303303

304304
# Check if prediction exists
305-
if not prediction or prediction.strip() == "":
305+
# 兼容 prediction 可能为 float(如 OpenEvaluator),也可能为 str(如 ChoiceEvaluator)
306+
if prediction is None or (
307+
isinstance(prediction, str) and prediction.strip() == ""
308+
):
306309
no_prediction += 1
307310

308311
# Use the pre-calculated "pass" field

spectrumlab/evaluator/choice_evaluator.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ def _build_prompt(self, item: Dict) -> str:
2626
"Available options:",
2727
options_block,
2828
"",
29-
"Please think step by step and provide your reasoning.",
30-
"After your analysis, indicate your final choice by putting it in \\box{}.",
31-
"For example: \\box{Option A}",
29+
"Please analyze the question and options carefully. Your answer must be exactly one of the provided options, and must be copied verbatim from the options above.",
30+
"Return your answer using the format \\answer{...}, where the content inside the braces is exactly the text of your chosen option (not the option letter or number, and do not use \\box{} or any other wrapper).",
31+
"For example, if you choose the option '~1700 cm⁻¹', you should return: \\answer{~1700 cm⁻¹}",
32+
"Do not return just a value like '~1700 cm' or any partial/incomplete answer. The answer must match one of the options exactly.",
3233
"",
3334
"Your response:",
3435
]
@@ -39,6 +40,9 @@ def _build_prompt(self, item: Dict) -> str:
3940
image_paths = normalize_image_paths(image_paths_field)
4041

4142
if image_paths:
43+
assert all(
44+
isinstance(p, str) for p in image_paths
45+
), f"image_paths should be List[str], got {image_paths}"
4246
# Prepare image data
4347
image_data = prepare_images_for_prompt(image_paths)
4448

@@ -50,24 +54,13 @@ def _build_prompt(self, item: Dict) -> str:
5054
return text_content
5155

5256
def _extract_prediction(self, response: str, item: Dict) -> str:
53-
"""Extract prediction from model response using \\box{} pattern."""
57+
"""只提取 \\answer{...} 内的内容"""
5458
if not response:
5559
return ""
56-
57-
choices = item.get("choices", [])
58-
59-
# Look for \\box{} pattern
60-
box_pattern = r"\\box\{([^}]+)\}"
61-
matches = re.findall(box_pattern, response)
62-
60+
answer_pattern = r"\\answer\{([^}]+)\}"
61+
matches = re.findall(answer_pattern, response)
6362
if matches:
64-
extracted = matches[-1].strip()
65-
# Try to match with actual choices
66-
for choice in choices:
67-
if choice.lower() == extracted.lower():
68-
return choice
69-
return extracted
70-
63+
return matches[-1].strip()
7164
return ""
7265

7366
def _calculate_accuracy(self, answer: str, prediction: str, item: Dict) -> bool:

0 commit comments

Comments
 (0)