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

593 lines
18 KiB
Python

import copy
import json
import os.path
import random
from functools import partial
from glob import glob
from multiprocessing.pool import Pool
from typing import Dict, List, Callable, Union
from omegaconf.listconfig import ListConfig
from tqdm import tqdm
from general_util.logger import get_child_logger
logger = get_child_logger(__name__)
def _format_option_list(option_list: List[str], _rank2option: List[str]) -> str:
res = []
for op_id, op in enumerate(option_list):
res.append(f"{_rank2option[op_id]}. {op}")
return "\n".join(res)
def option_id2str_aligner():
option_id2str = ["A", "B", "C", "D", "E"]
def func(data: List[Dict]):
for sample in data:
sample["str_label"] = option_id2str[sample["label"]]
return data
return func
def key_based_filter_aligner(key, value):
if isinstance(value, ListConfig):
value = list(value)
if not isinstance(value, list):
value = [value]
def func(data: List[Dict]):
return [item for item in data if item[key] in value]
return func
def dpo_confidence_ratio_filter(lower_bound: float, upper_bound: float, pos_field: str, response_field: str):
def func(item):
pos_num = len(item[pos_field])
total_num = len(item[response_field])
ratio = pos_num / total_num
if lower_bound <= ratio <= upper_bound:
return True
return False
return func
def filter_aligner(filter_func: Callable):
def func(data: List[Dict]):
return [item for item in data if filter_func(item)]
return func
def json_field2str(key, val: str = None, indent: int = 4):
def func(data: List[Dict]):
for item in data:
if val:
item[val] = json.dumps(item[key], indent=indent, ensure_ascii=False)
else:
item[key] = json.dumps(item[key], indent=indent, ensure_ascii=False)
return data
return func
def starts_with_filter(key, value):
def func(data: List[Dict]):
return [item for item in data if item[key].startswith(value)]
return func
def not_none_filter(key):
def func(data: List[Dict]):
return [item for item in data if key in item and item[key] not in ["", None, []]]
return func
def rename_field_aligner(kv_pair: Dict):
def func(data: List[Dict]):
for item in data:
for k, v in kv_pair.items():
tmp = item.pop(k)
item[v] = tmp
return data
return func
def field_extract_aligner(input_index_field: str, extract_index_field: str, extract_fields: List[str], extra_file: str, renamed_fields: Dict[str, str] = None):
if os.path.exists(extra_file):
extra_data = json.load(open(extra_file, encoding="utf-8"))
else:
extra_data = []
for file in glob(extra_file):
extra_data += json.load(open(file))
if len(extra_data) == 0:
raise ValueError(f"No data found in {extra_file}")
id2extra_data = {str(item[extract_index_field]): item for item in extra_data}
renaming = {}
for _field in extract_fields:
if renamed_fields and _field in renamed_fields:
renaming[_field] = renamed_fields[_field]
else:
renaming[_field] = _field
def func(data: List[Dict]):
missing = 0
missing_field = 0
outputs = []
for item in data:
item_id = str(item[input_index_field])
if item_id not in id2extra_data:
missing += 1
continue
extra_item = id2extra_data[item_id]
if any(x not in extra_item for x in extract_fields):
missing_field += 1
continue
for field in extract_fields:
item[renaming[field]] = extra_item[field]
outputs.append(item)
logger.info(f"Extracted {len(outputs)} items from {extra_file}")
logger.info(f"Missing {missing} items in {extra_file}")
logger.info(f"Missing {missing_field} fields in {extra_file}")
return outputs
return func
def flat_aligner(input_index_field: str, extract_field: Union[str, List[str]], mode: str = "single"):
if isinstance(extract_field, str):
extract_field = [extract_field]
def func(data: List[Dict]):
outputs = []
for item in data:
item_id = item[input_index_field]
# if not all(item[_field] for _field in extract_field):
# continue
if any(item[_field] in [None, "", []] for _field in extract_field):
continue
num = len(item[extract_field[0]])
for _field in extract_field[1:]:
assert len(item[_field]) == num, f"Length not match: {item[_field]}"
tmp_list = []
for i in range(num):
new_item = copy.deepcopy(item)
if any(item[_field][i] in [None, "", []] for _field in extract_field):
continue
for _field in extract_field:
new_item[_field] = item[_field][i]
new_item[input_index_field] = f"{item_id}_{i}"
tmp_list.append(new_item)
if mode == "single":
break
if len(tmp_list) == 0:
continue
if mode == "single":
outputs.append(tmp_list[0])
elif mode == "random":
outputs.append(random.choice(tmp_list))
else:
outputs += tmp_list
return outputs
return func
def option_flatten_aligner():
def func(data: List[Dict]):
for sample in data:
sample["option_list"] = _format_option_list(sample["options"], ["A", "B", "C", "D"])
return data
return func
def empty_aligner(data: List[Dict]):
return data
def add_id_aligner(id_field: str = "id"):
def func(data: List[Dict]):
for i, item in enumerate(data):
item[id_field] = i
return data
return func
def concat_aligner(aligners: List[Callable]):
def func(data: List[Dict]):
for aligner in aligners:
data = aligner(data)
return data
return func
def dpo_pair_aligner_cleaned(response_field: str = "response",
id_field: str = "id",
do_sample: bool = False, ):
"""
This aligner only accepts the cleaned file, which has removing all empty responses and combined with original data.
:return: Callable
"""
def func(data: List[Dict]):
outputs = []
for item in data:
pos_resp = []
neg_resp = []
for i, (resp, pred) in enumerate(zip(item[response_field], item["pred"])):
assert resp
# assert pred
if isinstance(resp, list):
assert isinstance(resp[0], str)
# assert "The answer is" in resp[-1], resp
resp = "".join(resp)
if isinstance(item["label"], str):
if pred == item["label"]:
pos_resp.append((i, resp))
else:
neg_resp.append((i, resp))
elif isinstance(item["label"], int):
if pred and ord(pred) - ord("A") == item["label"]:
pos_resp.append((i, resp))
else:
neg_resp.append((i, resp))
else:
raise ValueError(f"Unknown type of label: {type(item['label'])}")
if not (len(pos_resp) and len(neg_resp)):
continue
if do_sample:
pos = random.choice(pos_resp)
neg = random.choice(neg_resp)
pos_resp = [pos]
neg_resp = [neg]
for pos in pos_resp:
for neg in neg_resp:
new_item = copy.deepcopy(item)
new_item["pos"] = pos[1]
new_item["neg"] = neg[1]
new_item["pos_id"] = f"{item[id_field]}_{pos[0]}"
new_item["neg_id"] = f"{item[id_field]}_{neg[0]}"
outputs.append(new_item)
logger.info(f"Counted {len(outputs)} DPO contrastive pairs.")
return outputs
return func
def dpo_pair_aligner(pos_field: Union[str, ListConfig], neg_field: Union[str, ListConfig]):
def func(data: List[Dict]):
outputs = []
if isinstance(pos_field, str):
_pos_fields = [pos_field]
else:
_pos_fields = list(pos_field)
if isinstance(neg_field, str):
_neg_fields = [neg_field]
else:
_neg_fields = list(neg_field)
for item in tqdm(data, desc="DPO pair aligner", total=len(data)):
pos_resp = []
neg_resp = []
for _field in _pos_fields:
pos_resp += item[_field]
for _field in _neg_fields:
neg_resp += item[_field]
for pos in pos_resp:
for neg in neg_resp:
new_item = copy.deepcopy(item)
# To save memory
for _field in _pos_fields:
new_item.pop(_field)
for _field in _neg_fields:
new_item.pop(_field)
new_item["pos"] = pos
new_item["neg"] = neg
outputs.append(new_item)
logger.info(f"Counted {len(outputs)} DPO contrastive pairs.")
return outputs
return func
def eval_multiple_choice(item):
if isinstance(item, dict):
pred = item["prediction"]
label = item["answer"]
elif isinstance(item, tuple):
pred, label = item
else:
raise ValueError(f"Unknown type of item: {type(item)}")
if isinstance(label, str):
if pred == label:
return True
return False
if isinstance(label, int):
if pred and ord(pred) - ord("A") == label:
return True
return False
raise ValueError(f"Unknown type of label: {type(item['label'])}")
def prompt_fill_aligner(prompt_file: str, mapping: Dict[str, str], prompt_field: str = "prompt"):
full_prompt = open(prompt_file).read()
def func(data: List[Dict]):
for item in data:
prompt = copy.deepcopy(full_prompt)
for k, v in mapping.items():
prompt = prompt.replace(k, item[v])
item[prompt_field] = prompt
return data
return func
def value2pair_aligner(field: str, pos_field: str, neg_field: str, value_field: str):
def func(data: List[Dict]):
for item in data:
pair_data = item.pop(field)
values = item.pop(value_field)
pos = []
neg = []
for x, v in zip(pair_data, values):
if v:
pos.append(x)
else:
neg.append(x)
item[pos_field] = pos
item[neg_field] = neg
return data
return func
def return_threshold_mapping(value_threshold: float):
def func(v):
if v >= value_threshold:
return True
return False
return func
def return_binary_mapping():
def func(v):
if v is True:
return 1
return 0
return func
def value_mapping_aligner(value_field: str, value_mapping_func: Callable, return_int: bool = False):
def func(data: List[Dict]):
for item in data:
if return_int:
item[value_field] = int(value_mapping_func(item[value_field]))
else:
item[value_field] = value_mapping_func(item[value_field])
return data
return func
def dpo_pair2value_aligner(pos_field: str, neg_field: str, seq_field: str, value_field: str, flatten: bool = True):
def func(data: List[Dict]):
outputs = []
for item in data:
pos = item.pop(pos_field)
neg = item.pop(neg_field)
if flatten:
item_a = copy.deepcopy(item)
item_a[seq_field] = pos
item_a[value_field] = 1
item_b = copy.deepcopy(item)
item_b[seq_field] = neg
item_b[value_field] = 0
outputs.append(item_a)
outputs.append(item_b)
else:
item[seq_field] = [pos, neg]
item[value_field] = [1, 0]
outputs.append(item)
return outputs
return func
def value2pair_mapping_aligner(field: str, pos_field: str, neg_field: str, value_field: str, value_mapping_func: Callable):
def func(data: List[Dict]):
for item in data:
pair_data = item.pop(field)
values = item.pop(value_field)
pos = []
neg = []
for x, v in zip(pair_data, values):
if value_mapping_func(v):
pos.append(x)
else:
neg.append(x)
item[pos_field] = pos
item[neg_field] = neg
return data
return func
def dpo_random_choice_aligner(anchor_field: str, paired_field: str):
def func(data: List[Dict]):
outputs = []
for item in tqdm(data, desc="DPO random choice aligner", total=len(data)):
if len(item[anchor_field]) == 0:
continue
if len(item[paired_field]) == 0:
continue
for anchor in item[anchor_field]:
new_item = copy.deepcopy(item)
new_item[anchor_field] = anchor
new_item[paired_field] = random.choice(item[paired_field])
outputs.append(new_item)
return outputs
return func
def dpo_flat_random_choice_aligner(paired_field: str):
def func(data: List[Dict]):
outputs = []
for item in tqdm(data, desc="DPO random choice aligner", total=len(data)):
if len(item[paired_field]) == 0:
continue
new_item = copy.deepcopy(item)
new_item[paired_field] = random.choice(item[paired_field])
outputs.append(new_item)
return outputs
return func
def dpo_paired_random_choice_aligner(anchor_field: str, paired_field, sort_accord_to_len: bool = False, top_k: int = 5, num_workers: int = 16):
def func(data: List[Dict]):
outputs = []
for item in tqdm(data, desc="processing dpo pairs", total=len(data)):
if len(item[anchor_field]) == 0:
continue
if len(item[paired_field]) == 0:
continue
assert len(item[anchor_field]) == len(item[paired_field]), (item[anchor_field], item[paired_field])
for anchor, targets in zip(item[anchor_field], item[paired_field]):
if len(targets) == 0:
continue
new_item = copy.deepcopy(item)
new_item[anchor_field] = anchor
new_item[paired_field] = random.choice(targets)
outputs.append(new_item)
return outputs
def func_sorted(data: List[Dict]):
outputs_before_sort = []
for item in tqdm(data, desc="DPO paired random choice aligner", total=len(data)):
if len(item[anchor_field]) == 0:
continue
if len(item[paired_field]) == 0:
continue
assert len(item[anchor_field]) == len(item[paired_field]), (item[anchor_field], item[paired_field])
for anchor, targets in zip(item[anchor_field], item[paired_field]):
if len(targets) == 0:
continue
new_item = copy.deepcopy(item)
new_item[anchor_field] = anchor
new_item[paired_field] = targets
outputs_before_sort.append(new_item)
_annotate = partial(_sort_worker, _pos_field=anchor_field, _neg_field=paired_field, _top_k=top_k)
with Pool(num_workers) as p:
outputs_after_sort = list(tqdm(p.imap(_annotate, outputs_before_sort), total=len(outputs_before_sort)))
for item in outputs_after_sort:
item[paired_field] = random.choice(item[paired_field])
return outputs_after_sort
if sort_accord_to_len:
return func_sorted
return func
def sample_steps(response: str):
lines = response.split("\n")
lines = [line for line in lines if line.strip()]
return len(lines)
def _sort_worker(item, _pos_field: str, _neg_field: str, _top_k: int = 5):
anchor = item[_pos_field]
targets = item[_neg_field]
anchor_steps = sample_steps(anchor)
sorted_targets = sorted(targets, key=lambda x: abs(anchor_steps - sample_steps(x)))
item[_neg_field] = sorted_targets[:_top_k]
return item
def dpo_bi_random_choice_aligner(pos_field: str, neg_field: str, sort_accord_to_len: bool = False, top_k: int = 5, num_workers: int = 16):
def func(data: List[Dict]):
outputs = []
for item in tqdm(data, desc="DPO random choice aligner", total=len(data)):
if len(item[pos_field]) == 0:
continue
if len(item[neg_field]) == 0:
continue
pos = random.choice(item[pos_field])
neg = random.choice(item[neg_field])
item[pos_field] = pos
item[neg_field] = neg
outputs.append(item)
return outputs
def func_sorted(data: List[Dict]):
outputs_before_sort = []
for item in tqdm(data, desc="DPO random choice aligner", total=len(data)):
if len(item[pos_field]) == 0:
continue
if len(item[neg_field]) == 0:
continue
new_item = copy.deepcopy(item)
new_item["pos"] = random.choice(item[pos_field])
new_item["neg"] = item[neg_field]
outputs_before_sort.append(new_item)
_annotate = partial(_sort_worker, _pos_field="pos", _neg_field="neg", _top_k=top_k)
with Pool(num_workers) as p:
outputs_after_sort = list(tqdm(p.imap(_annotate, outputs_before_sort), total=len(outputs_before_sort)))
for item in outputs_after_sort:
item["neg"] = random.choice(item["neg"])
return outputs_after_sort
if sort_accord_to_len:
return func_sorted
return func