Files
2026-07-13 13:24:13 +08:00

838 lines
30 KiB
Python

import collections
import io
import json
import os
import re
from typing import Dict, Any, List, Tuple, Union
import numpy as np
import vllm
from data import math_util
from data.deepseek_math_utils import eval_script, answer_extraction
from data.mathscale.util import mathscale_is_equiv_proxy, is_correct as mathscale_is_correct
from omegaconf import ListConfig
from general_util.logger import get_child_logger
logger = get_child_logger(__name__)
class PlaceholderClean:
def __call__(self, pred: str):
return "A"
class MCQAAnswerClean:
def __init__(self, prompt: str = "zero-shot"):
self.prompt = prompt
def __call__(self, pred: str):
# print("pred_before: ", pred)
preds = re.findall(r"A|B|C|D|E", pred)
if len(preds) == 0:
return ""
if self.prompt == "zero-shot":
return preds[0]
if self.prompt == "few-shot":
return preds[-1]
return preds[0]
class SeparatorClean:
def __init__(self, separator: str = "Finish", separate_idx: int = 1, regrex: str = "A|B|C|D"):
self.separator = separator
self.separate_idx = separate_idx
self.regrex = re.compile(regrex)
def __call__(self, pred: str):
if self.separator and self.separator in pred:
preds = pred.split(self.separator)
if len(preds) == 0:
return ""
if len(preds) <= self.separate_idx:
return ""
pred = preds[self.separate_idx]
preds = re.findall(self.regrex, pred)
if len(preds) == 0 or len(preds) > 1:
return ""
return preds[0]
class ReActSeparatorClean: # FIXED@2024-01-03: Add hard constraint.
def __init__(self, separator: str = "Context:", separate_idx: int = 0, regrex: str = "A|B|C|D"):
self.separator = separator # Use for remove generated dummy examples
self.separate_idx = separate_idx
self.regrex = re.compile(regrex)
def __call__(self, pred: str):
if self.separator in pred:
groups = pred.split(self.separator)
pred = groups[self.separate_idx]
if "Finish[" in pred:
pred = pred.split("Finish[")[1]
pred = pred.split("]")[0]
preds = re.findall(self.regrex, pred)
if len(preds) == 0:
return ""
elif len(preds) == 1:
return preds[0]
else:
return "" # FIXED@2023-12-27: To avoid the case where the large language models tends to generate multiple predictions to hack the answer.
return ""
class BinaryAnswerClean:
def __init__(self, prompt: str = "zero-shot"):
self.prompt = prompt
def __call__(self, pred: str):
preds = re.findall(r"Yes|No", pred)
if len(preds) == 0:
return ""
if self.prompt == "zero-shot":
return preds[0]
if self.prompt == "few-shot":
return preds[-1]
return preds[0]
class TagCleaner:
def __call__(self, pred: str):
# Regular expression pattern to match the content between <answer> and </answer>
pattern = r'<answer>(.*?)</answer>'
# Use re.DOTALL to allow matching newlines within the tags
match = re.search(pattern, pred, re.DOTALL)
if match:
return match.group(1).strip() # Strip removes extra spaces or newlines
return pred
class OpenAICallBack:
def __init__(self, output_file: str, answer_clean: Union[MCQAAnswerClean, str], resume: bool = False, index_field: str = "index",
label_field: str = "label", saved_keys: List[str] = None):
self.predictions = []
self.output_file = output_file
self.answer_clean = answer_clean
self.index_field = index_field
self.label_field = label_field
self.saved_keys = saved_keys
if isinstance(self.saved_keys, ListConfig):
self.saved_keys = list(self.saved_keys)
logging_file = output_file.replace(".json", ".jsonl")
save_dir = os.path.dirname(logging_file)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
if os.path.exists(logging_file):
if resume:
with open(logging_file, "r") as f:
for line in f.readlines():
# self.predictions.append(json.loads(line))
item = json.loads(line)
if isinstance(item["response"], str):
if item["response"].strip() == "":
continue
elif isinstance(item["response"], list):
if any([tmp.strip() == "" for tmp in item["response"]]):
continue
self.predictions.append(item)
logger.info(f"Load {len(self.predictions)} from {logging_file}")
self.logging_file = logging_file
def __call__(self, meta_data: Dict[str, Any], batch_model_outputs: Dict[str, Any], fw: io = None, **kwargs):
text = meta_data["text"]
if self.label_field in meta_data:
label = meta_data[self.label_field]
else:
label = -1
index = meta_data[self.index_field]
response = batch_model_outputs["response"]
if isinstance(response, vllm.RequestOutput):
if response.finished:
response = [o.text for o in response.outputs]
if len(response) == 1:
response = response[0]
else:
response = ""
if isinstance(response, str):
pred_clean = self.answer_clean(response)
elif isinstance(response, list):
pred_clean = [self.answer_clean(item) for item in response]
else:
raise ValueError(f"Unknown type of response: {type(response)}")
out_item = {
"text": text,
"label": label,
"response": response,
"pred": pred_clean,
"id": index,
}
if self.saved_keys is not None:
for key in self.saved_keys:
if key in meta_data:
out_item[key] = meta_data[key]
self.predictions.append(out_item)
if fw is not None:
fw.write(json.dumps(self.predictions[-1]) + "\n")
else:
with open(self.logging_file, "a") as f:
f.write(json.dumps(self.predictions[-1]) + "\n")
def batch_call(self, meta_data: List[Dict[str, Any]], batch_model_outputs: List[Dict[str, Any]], **kwargs):
with open(self.logging_file, "a") as f:
for m, b in zip(meta_data, batch_model_outputs):
self(m, b, fw=f, **kwargs)
@staticmethod
def eval_single_item(pred, label):
if not pred.strip():
return False
if len(pred.strip()) > 1:
return False
if isinstance(label, str):
if label.strip() == pred.strip():
return True
if isinstance(label, list) and isinstance(label[0], str):
if label[0].strip() == pred.strip():
return True
if label == ord(pred.strip()) - ord("A"):
return True
return False
def get_results(self):
save_dir = os.path.dirname(self.output_file)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
json.dump(self.predictions, open(self.output_file, "w"), indent=2)
cnt = 0
outputs = []
pass_at_k = 0
for item in self.predictions:
if isinstance(item["pred"], list):
preds = item["pred"]
else:
preds = [item["pred"]]
pred = collections.Counter(preds).most_common(1)[0][0]
mul_pass = 0
for tmp in preds:
if self.eval_single_item(tmp, item["label"]):
mul_pass = 1
break
pass_at_k += mul_pass
if not pred.strip():
outputs.append((item["id"], 0))
continue
if len(pred.strip()) > 1:
outputs.append((item["id"], 0))
continue
if isinstance(item["label"], str):
if item["label"].strip() == pred.strip():
cnt += 1
elif isinstance(item["label"], list) and isinstance(item["label"][0], str):
if item["label"][0].strip() == pred.strip():
cnt += 1
else:
if item["label"] == ord(pred.strip()) - ord("A"):
cnt += 1
outputs.append((item["id"], ord(pred.strip()) - ord("A")))
assert len(outputs) == len(self.predictions)
# Remove duplicated ids to satisfy the submission requirements of ReClor.
outputs = sorted(outputs, key=lambda x: x[0])
id_set = set()
new_outputs = []
for item in outputs:
if item[0] not in id_set:
new_outputs.append(item[1])
id_set.add(item[0])
outputs = new_outputs
np_output_file = self.output_file.replace(".json", ".npy")
np.save(np_output_file, np.array(outputs))
if len(self.predictions) == 0:
metrics = {"acc": 0, "pass@k": 0, "correct": 0, "total": 0}
else:
metrics = {"acc": cnt / len(self.predictions), "pass@k": pass_at_k / len(self.predictions), "correct": cnt, "total": len(self.predictions)}
json.dump(metrics, open(self.output_file.replace(".json", ".metrics.json"), "w"), indent=2)
return metrics, []
class SaveOnlyCallBack(OpenAICallBack):
def __call__(self, meta_data: Dict[str, Any], batch_model_outputs: Dict[str, Any], fw: io = None, **kwargs):
text = meta_data["text"]
if self.label_field in meta_data:
label = meta_data[self.label_field]
else:
label = -1
index = meta_data[self.index_field]
response = batch_model_outputs["response"]
if isinstance(response, vllm.RequestOutput):
if response.finished:
response = [o.text for o in response.outputs]
if len(response) == 1:
response = response[0]
else:
response = ""
out_item = {
"text": text,
"label": label,
"response": response,
"id": index,
}
if self.saved_keys is not None:
for key in self.saved_keys:
out_item[key] = meta_data[key]
self.predictions.append(out_item)
# self.fw.write(json.dumps(self.predictions[-1]) + "\n")
# self.fw.flush()
if fw is not None:
fw.write(json.dumps(self.predictions[-1]) + "\n")
else:
with open(self.logging_file, "a") as f:
f.write(json.dumps(self.predictions[-1]) + "\n")
def get_results(self):
save_dir = os.path.dirname(self.output_file)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
json.dump(self.predictions, open(self.output_file, "w"), indent=2)
# self.fw.close()
return {}, []
def majority_voting_predict(preds):
if isinstance(preds, str):
return preds
preds = [pred for pred in preds if pred]
if len(preds) == 0:
return ""
assert isinstance(preds, list)
if isinstance(preds[0], list):
tmp = []
for pred in preds:
tmp.append(str(sorted(pred)))
pred = collections.Counter(tmp).most_common(1)[0][0]
pred = eval(pred)
elif isinstance(preds[0], str):
pred = collections.Counter(preds).most_common(1)[0][0]
else:
# raise ValueError(f"Unknown type {type(preds[0])}")
logger.warning(f"Unknown type {type(preds[0])}")
pred = ""
return pred
class OpenAIMATHCallBack(OpenAICallBack):
eval_fns = {
"meta_math": math_util.is_equiv,
}
def __init__(self, *args, eval_fn: str = "meta_math", **kwargs):
super().__init__(*args, **kwargs, )
self.eval_fn = self.eval_fns[eval_fn]
def get_results(self):
save_dir = os.path.dirname(self.output_file)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
self.fw.close()
cnt = 0
pass_at_k = 0
sc = 0
outputs = []
for item in self.predictions:
if isinstance(item["pred"], list):
preds = item["pred"]
else:
preds = [item["pred"]]
# pred = collections.Counter(preds).most_common(1)[0][0]
# res = math_util.is_equiv(pred, item["label"])
# res = [math_util.is_equiv(p, item["label"]) for p in preds]
res = [self.eval_fn(p, item["label"]) for p in preds]
if isinstance(res[0], tuple):
res = [r[0] for r in res]
if res[0]:
cnt += 1
if any(res):
pass_at_k += 1
if isinstance(item["pred"], str):
res = res[0]
item["res"] = res
sc_pred = majority_voting_predict(preds)
sc_res = self.eval_fn(sc_pred, item["label"])
item["sc_res"] = sc_res
item["sc_pred"] = sc_pred
if sc_res:
sc += 1
outputs.append((item["id"], res))
assert len(outputs) == len(self.predictions)
# Remove duplicated ids to satisfy the submission requirements of ReClor.
# outputs = sorted(outputs, key=lambda x: x[0])
# id_set = set()
# new_outputs = []
# for item in outputs:
# if item[0] not in id_set:
# new_outputs.append(item[1])
# id_set.add(item[0])
# outputs = new_outputs
json.dump(self.predictions, open(self.output_file, "w"), indent=2)
if len(self.predictions) == 0:
metrics = {"acc": 0, "pass@k": 0, "maj@k": 0, "correct": 0, "total": 0}
else:
metrics = {"acc": cnt / len(self.predictions), "pass@k": pass_at_k / len(self.predictions), "maj@k": sc / len(self.predictions),
"correct": cnt, "total": len(self.predictions)}
json.dump(metrics, open(self.output_file.replace(".json", ".metrics.json"), "w"), indent=2)
return metrics, []
class DeepSeekMathCallBack(OpenAICallBack):
eval_fns = {
"gsm8k": eval_script.eval_last_single_answer,
"math": eval_script.eval_math,
}
extract_fns = {
"gsm8k": answer_extraction.extract_last_single_answer,
"math": answer_extraction.extract_math_answer,
}
def __init__(self, *args, eval_fn: str = "gsm8k", **kwargs):
super().__init__(*args, **kwargs, )
self.eval_fn = self.eval_fns[eval_fn]
if self.answer_clean in self.extract_fns:
self.extract_fn = self.extract_fns[self.answer_clean]
else:
self.extract_fn = self.answer_clean
def __call__(self, meta_data: Dict[str, Any], batch_model_outputs: Dict[str, Any], fw: io = None, **kwargs):
text = meta_data["text"]
if self.label_field in meta_data:
label = meta_data[self.label_field]
else:
label = -1
index = meta_data[self.index_field]
response = batch_model_outputs["response"]
if isinstance(response, vllm.RequestOutput):
if response.finished:
response = [o.text for o in response.outputs]
if len(response) == 1:
response = response[0]
else:
response = ""
if isinstance(response, str):
pred_clean = self.extract_fn(text, response, "cot")
elif isinstance(response, list):
pred_clean = [self.extract_fn(text, item, "cot") for item in response]
else:
raise ValueError(f"Unknown type of response: {type(response)}")
out_item = {
"text": text,
"label": label,
"response": response,
"pred": pred_clean,
"id": index,
}
if self.saved_keys is not None:
for key in self.saved_keys:
out_item[key] = meta_data[key]
self.predictions.append(out_item)
if fw is not None:
fw.write(json.dumps(self.predictions[-1]) + "\n")
else:
with open(self.logging_file, "a") as f:
f.write(json.dumps(self.predictions[-1]) + "\n")
def get_results(self):
save_dir = os.path.dirname(self.output_file)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
self.fw.close()
cnt = 0
pass_at_k = 0
sc = 0
outputs = []
num_errors = 0
for item in self.predictions:
if isinstance(item["response"], list):
preds = item["pred"]
else:
preds = [item["pred"]]
mul_pass = 0
if len(preds) > 0:
# if isinstance(preds[0], list):
# # pred = preds[0] # TODO: How to add self-consistency for MATH dataset given the answer could be a list?
# tmp = [str(x) for x in preds]
# pred = collections.Counter(tmp).most_common(1)[0][0]
# pred = eval(pred)
# else:
# pred = collections.Counter(preds).most_common(1)[0][0]
#
# if pred is None:
# pred = ""
res = []
# res = math_util.is_equiv(pred, item["label"])
# https://github.com/deepseek-ai/DeepSeek-Math/blob/main/evaluation/infer/run_pal_eval.py#L168
# https://github.com/deepseek-ai/DeepSeek-Math/blob/main/evaluation/infer/run_cot_eval.py#L120
# res = self.eval_fn({"prediction": pred, "answer": item["label"]}) # For CoT eval, use `prediction`, for Pal eval, use `program_output`.
for pred in preds:
res.append(self.eval_fn({"prediction": pred, "answer": item["label"]}))
# mul_pass = 0
# for pred in preds:
# if self.eval_fn({"prediction": pred, "answer": item["label"]}):
# mul_pass = 1
# break
if any(res):
mul_pass = 1
sc_pred = majority_voting_predict(preds)
item["sc_pred"] = sc_pred
try:
sc_res = self.eval_fn({"prediction": sc_pred, "answer": item["label"]})
item["sc_res"] = sc_res
except Exception as e:
logger.warning(f"Error in {item['id']} during evaluation: {e}")
sc_res = False
num_errors += 1
if sc_res:
sc += 1
else:
res = []
item["sc_pred"] = ""
item["sc_res"] = False
item["pass_at_k"] = mul_pass
if len(res) > 0 and res[0]:
cnt += 1
if mul_pass:
pass_at_k += 1
outputs.append((item["id"], res))
if len(preds) == 1:
res = res[0]
item["res"] = res
assert len(outputs) == len(self.predictions)
assert pass_at_k <= len(self.predictions)
json.dump(self.predictions, open(self.output_file, "w"), indent=2)
logger.info(f"Number of errors: {num_errors}")
# Remove duplicated ids to satisfy the submission requirements of ReClor.
# outputs = sorted(outputs, key=lambda x: x[0])
# id_set = set()
# new_outputs = []
# for item in outputs:
# if item[0] not in id_set:
# new_outputs.append(item[1])
# id_set.add(item[0])
# outputs = new_outputs
if len(self.predictions) == 0:
metrics = {"acc": 0, "pass@k": 0, "maj@k": 0, "correct": 0, "total": 0}
else:
metrics = {"acc": cnt / len(self.predictions), "pass@k": pass_at_k / len(self.predictions), "maj@k": sc / len(self.predictions),
"correct": cnt, "total": len(self.predictions)}
json.dump(metrics, open(self.output_file.replace(".json", ".metrics.json"), "w"), indent=2)
return metrics, []
class MathScaleCallBack(OpenAICallBack):
def __call__(self, meta_data: Dict[str, Any], batch_model_outputs: Dict[str, Any], fw: io = None, **kwargs):
text = meta_data["text"]
if self.label_field in meta_data:
label = meta_data[self.label_field]
else:
label = -1
index = meta_data[self.index_field]
response = batch_model_outputs["response"]
if isinstance(response, vllm.RequestOutput):
if response.finished:
response = [o.text for o in response.outputs]
if len(response) == 1:
response = response[0]
else:
response = ""
if isinstance(response, str):
if self.answer_clean is not None:
resp_clean = self.answer_clean(response)
if resp_clean is None or resp_clean is False:
resp_clean = ""
else:
resp_clean = response
res, pred_clean, _ = mathscale_is_correct(resp_clean, label)
if pred_clean is None:
pred_clean = ""
sc_pred = pred_clean
sc_res = res
elif isinstance(response, list):
res = []
pred_clean = []
for item in response:
if self.answer_clean is not None:
tmp_resp_clean = self.answer_clean(item)
else:
tmp_resp_clean = item
tmp_res, tmp_pred_clean, _ = mathscale_is_correct(tmp_resp_clean, label)
if tmp_pred_clean is None:
tmp_pred_clean = ""
res.append(tmp_res)
pred_clean.append(tmp_pred_clean)
pred2res = {pred: r for pred, r in zip(pred_clean, res)}
sc_pred = majority_voting_predict(pred_clean)
sc_res = pred2res[sc_pred]
else:
raise ValueError(f"Unknown type of response: {type(response)}")
out_item = {
"text": text,
"label": label,
"response": response,
"pred": pred_clean,
"id": index,
"res": res,
"sc_pred": sc_pred,
"sc_res": sc_res,
}
if self.saved_keys is not None:
for key in self.saved_keys:
if key in meta_data:
out_item[key] = meta_data[key]
self.predictions.append(out_item)
if fw is not None:
fw.write(json.dumps(self.predictions[-1]) + "\n")
else:
with open(self.logging_file, "a") as f:
f.write(json.dumps(self.predictions[-1]) + "\n")
def get_results(self):
save_dir = os.path.dirname(self.output_file)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
cnt = 0
pass_at_k = 0
sc = 0
acc_data_topic = collections.Counter()
cnt_data_topic = collections.Counter()
for item in self.predictions:
if not isinstance(item["res"], list):
res = [item["res"]]
else:
res = item["res"]
if res[0]:
cnt += 1
if "data_topic" in item:
if "." in item["data_topic"]:
item["data_topic"] = item["data_topic"].split(".")[0]
acc_data_topic[item["data_topic"]] += int(res[0])
cnt_data_topic[item["data_topic"]] += 1
if any(res):
pass_at_k += 1
if item["sc_res"]:
sc += 1
assert pass_at_k <= len(self.predictions)
json.dump(self.predictions, open(self.output_file, "w"), indent=2)
if len(self.predictions) == 0:
metrics = {"acc": 0, "pass@k": 0, "maj@k": 0, "correct": 0, "total": 0}
else:
metrics = {"acc": cnt / len(self.predictions), "pass@k": pass_at_k / len(self.predictions), "maj@k": sc / len(self.predictions),
"correct": cnt, "total": len(self.predictions)}
if len(acc_data_topic) > 0:
for key in acc_data_topic:
metrics[f"acc_{key}"] = acc_data_topic[key] / cnt_data_topic[key]
json.dump(metrics, open(self.output_file.replace(".json", ".metrics.json"), "w"), indent=2)
return metrics, []
def fix_trailing_comma(json_string):
# Use regex to find and remove trailing commas before a closing brace or bracket
fixed_string = re.sub(r',\s*(\}|\])', r'\1', json_string)
return fixed_string
def extract_json_content_rep(input_str):
"""
Extracts JSON content from a string, removing surrounding ``` symbols
and an optional 'json' tag.
Args:
input_str (str): The input string containing JSON content.
Returns:
str: Cleaned JSON string.
"""
# Remove leading and trailing ``` symbols
cleaned_str = re.sub(r"^```(?:json)?|```$", "", input_str.strip())
return fix_trailing_comma(cleaned_str.strip())
def extract_json_content(input_str):
"""
Extracts JSON content from a string by isolating the portion enclosed
between the ``` markers, optionally preceded by the 'json' tag.
Args:
input_str (str): The input string containing JSON content.
Returns:
str: Extracted JSON string or an empty string if no JSON is found.
"""
# Use regex to find content between triple backticks
match = re.search(r"```(?:json)?\s*(.*?)\s*```", input_str, re.DOTALL)
if match:
return fix_trailing_comma(match.group(1).strip())
return "" # Return an empty string if no match is found
class JsonObjEvalCallBack(OpenAICallBack):
@staticmethod
def json_parse_and_eval(response: str, label: dict):
if not response:
return False, {}
try:
json_str = extract_json_content(response)
if json_str == "":
json_str = extract_json_content_rep(response)
json_obj = json.loads(json_str)
except json.JSONDecodeError as e:
print(e)
return False, {}
for k, v in label.items():
if k not in json_obj:
return False, json_obj
if json_obj[k] != v:
return False, json_obj
return True, json_obj
def __call__(self, meta_data: Dict[str, Any], batch_model_outputs: Dict[str, Any], fw: io = None, **kwargs):
text = meta_data["text"]
if self.label_field and self.label_field in meta_data:
label = meta_data[self.label_field]
if not isinstance(label, dict):
try:
label = json.loads(label)
except Exception as e:
logger.warning(f"Error in label when passing string: {e}")
logger.warning(label)
label = {}
else:
label = {}
index = meta_data[self.index_field]
response = batch_model_outputs["response"]
if isinstance(response, vllm.RequestOutput):
if response.finished:
response = [o.text for o in response.outputs]
if len(response) == 1:
response = response[0]
else:
response = ""
if isinstance(response, str):
res, pred_clean = self.json_parse_and_eval(response, label)
elif isinstance(response, list):
res, pred_clean = [], []
for resp in response:
tmp_res, tmp_pred_clean = self.json_parse_and_eval(resp, label)
res.append(tmp_res)
pred_clean.append(tmp_pred_clean)
else:
raise ValueError(f"Unknown type of response: {type(response)}")
out_item = {
"text": text,
"label": label,
"response": response,
"id": index,
"res": res,
"pred": pred_clean,
}
if self.saved_keys is not None:
for key in self.saved_keys:
if key in meta_data:
out_item[key] = meta_data[key]
self.predictions.append(out_item)
if fw is not None:
fw.write(json.dumps(self.predictions[-1]) + "\n")
else:
with open(self.logging_file, "a") as f:
f.write(json.dumps(self.predictions[-1]) + "\n")
def get_results(self):
save_dir = os.path.dirname(self.output_file)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
cnt = 0
pass_at_k = 0
acc_data_topic = collections.Counter()
cnt_data_topic = collections.Counter()
for item in self.predictions:
if not isinstance(item["res"], list):
res = [item["res"]]
else:
res = item["res"]
if res[0]:
cnt += 1
if "data_topic" in item:
if "." in item["data_topic"]:
item["data_topic"] = item["data_topic"].split(".")[0]
acc_data_topic[item["data_topic"]] += int(res[0])
cnt_data_topic[item["data_topic"]] += 1
if any(res):
pass_at_k += 1
assert pass_at_k <= len(self.predictions)
json.dump(self.predictions, open(self.output_file, "w"), indent=2)
if len(self.predictions) == 0:
metrics = {"acc": 0, "pass@k": 0, "maj@k": 0, "correct": 0, "total": 0}
else:
metrics = {"acc": cnt / len(self.predictions), "pass@k": pass_at_k / len(self.predictions), "correct": cnt, "total": len(self.predictions)}
if len(acc_data_topic) > 0:
for key in acc_data_topic:
metrics[f"acc_{key}"] = acc_data_topic[key] / cnt_data_topic[key]
json.dump(metrics, open(self.output_file.replace(".json", ".metrics.json"), "w"), indent=2)
return metrics, []