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 and pattern = r'(.*?)' # 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, []