Files
Open-Assistant/model/reward/instructor/rank_datasets.py
T
Kian-Meng AngandGitHub 1e321a6fca Fix typos (#1143)
Found via `codespell -S .mypy_cache,yarn.lock,*.json,*.ipynb -L
rouge,nam,vie`
2023-02-05 20:18:03 +01:00

301 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
author: theblackcat102
Dataset output format from __getitem__
- question / prompt : string
- answers / rows : list of tuple pair. The first element in the tuple pair must be the positive pair (rank higher than the second element)
A list of rank based dataset for training using rank loss
Some nice features to have
[] support additional negative samples generated from other models.
For example we can use galactica-125m to generate a TLDR and assume it was
inferior than the human preference one
"""
from dataclasses import dataclass
from typing import Dict, List, Optional, Union
import numpy as np
import torch
from datasets import load_dataset
from torch.utils.data import Dataset
from transformers.tokenization_utils_base import PaddingStrategy, PreTrainedTokenizerBase
@dataclass
class RankGenCollator:
tokenizer: PreTrainedTokenizerBase
padding: Union[bool, str, PaddingStrategy] = True
max_length: Optional[int] = None
max_examples: Optional[int] = None
def __call__(self, batch: List[Dict[str, str]]) -> Dict[str, torch.Tensor]:
prefixes = []
better_answers = []
worse_answers = []
for question, pairs in batch:
for (pos, neg) in pairs:
prefixes.append("pre " + question)
better_answers.append("suffi " + pos)
worse_answers.append("suffi " + neg)
tokenized_prefixes = self.tokenizer(
prefixes, return_tensors="pt", padding=self.padding, max_length=self.max_length, truncation=True
)
tokenized_pos = self.tokenizer(
better_answers, return_tensors="pt", padding=self.padding, max_length=self.max_length, truncation=True
)
tokenized_neg = self.tokenizer(
worse_answers, return_tensors="pt", padding=self.padding, max_length=self.max_length, truncation=True
)
return {"prefix": tokenized_prefixes, "positive": tokenized_pos, "negative": tokenized_neg}
@dataclass
class DataCollatorForPairRank:
"""
Data collator that will dynamically pad the inputs for multiple choice received.
"""
tokenizer: PreTrainedTokenizerBase
num_choices: int = 2
padding: Union[bool, str, PaddingStrategy] = True
max_length: Optional[int] = None
pad_to_multiple_of: Optional[int] = None
drop_token_type: bool = False # galactica
def __call__(self, features):
flatten_features = []
batch_size = 0
for question, pairs in features:
for (pos, neg) in pairs:
flatten_features.append(self.tokenizer(question, pos, truncation=True, max_length=self.max_length))
flatten_features.append(self.tokenizer(question, neg, truncation=True, max_length=self.max_length))
batch_size += 1
batch = self.tokenizer.pad(
flatten_features,
padding=self.padding,
max_length=self.max_length,
pad_to_multiple_of=self.pad_to_multiple_of,
return_tensors="pt",
)
if self.drop_token_type:
batch.pop("token_type_ids")
# batch = {k: v.view(batch_size, self.num_choices, -1) for k, v in batch.items()}
return batch
class WebGPT(Dataset):
def __init__(self) -> None:
super().__init__()
dataset = load_dataset("openai/webgpt_comparisons")
questions = {}
# using prompt as our index will allows us
# to add additional generated prompt later
self.index2question = {}
for row in dataset["train"]:
question = row["question"]["full_text"]
if question not in self.index2question:
self.index2question[len(self.index2question)] = question
if question not in questions:
questions[question] = []
if row["score_0"] > row["score_1"]:
# not going to risk it
questions[question].append((row["answer_0"], row["answer_1"]))
else:
questions[question].append((row["answer_1"], row["answer_0"]))
self.questions = questions
def __len__(self):
return len(self.index2question)
def __getitem__(self, index):
question = self.index2question[index]
rows = self.questions[question]
# optimize the format later
return question, rows
class HFSummary(Dataset):
"""
Human feedback data from OpenAI
https://github.com/openai/summarize-from-feedback
labeling method : pair comparison, 0 or 1
"""
def __init__(self, split="train", conf_threshold=-1, max_comparison_per_sample=1) -> None:
super().__init__()
assert split in ("train", "valid1", "valid2", "test")
summaries = {}
# using prompt as our index will allows us
# to add additional generated prompt later
self.index2summary = {}
self.max_comparison_per_sample = max_comparison_per_sample
major_split = split if "train" == split else "validation"
dataset = load_dataset("openai/summarize_from_feedback", "comparisons")[major_split]
for data in dataset:
if (
"extra" in data
and "confidence" in data["extra"]
and data["extra"]["confidence"] is not None
and conf_threshold > data["extra"]["confidence"]
):
print("skipping {}".format(data["info"]["id"]))
continue
if split != "train" and split != data["split"]:
continue
if "article" in data["info"] and data["info"]["article"] is not None:
context = data["info"]["article"]
elif "post" in data["info"]:
context = data["info"]["post"]
if context not in self.index2summary:
self.index2summary[len(self.index2summary)] = context
if context not in summaries:
summaries[context] = []
pos, neg = (0, 1) if data["choice"] == 0 else (1, 0)
summaries[context].append((data["summaries"][pos]["text"], data["summaries"][neg]["text"]))
self.summaries = summaries
self.postfix_prompt = " TLDR;"
def __len__(self):
return len(self.index2summary)
def __getitem__(self, index):
context = self.index2summary[index]
# return pairs of comparison
rows = self.summaries[context]
# pair very big
# we are going to do some sampling
# not optimal but good for now
valid_idx = np.random.choice(len(rows), self.max_comparison_per_sample)
# optimize the format later
return context + self.postfix_prompt, [r for idx, r in enumerate(rows) if idx in valid_idx]
class HFDataset(Dataset):
"""
This is a base huggingface dataset which written to support the
simplest pos-neg pair format
we should do something like this for supervised datasets
"""
def __init__(
self, dataset_name, question_field, pos_answer_field, neg_answer_field, subset=None, split=None
) -> None:
super().__init__()
dataset = load_dataset(dataset_name, subset)
if split is not None:
dataset = dataset[split]
self.questions = {}
self.index2question = {}
for row in dataset:
question = row[question_field].strip()
pos = row[pos_answer_field]
neg = row[neg_answer_field]
if question not in self.index2question:
self.index2question[len(self.index2question)] = question
if question not in self.questions:
self.questions[question] = []
self.questions[question].append((pos.strip(), neg.strip()))
def __len__(self):
return len(self.index2question)
def __getitem__(self, index):
question = self.index2question[index]
rows = self.questions[question]
# optimize the format later
return question, rows
class GPTJSynthetic(HFDataset):
def __init__(self) -> None:
super().__init__("Dahoas/synthetic-instruct-gptj-pairwise", "prompt", "chosen", "rejected", None, "train")
class AnthropicRLHF(Dataset):
"""
The data are described in the paper:
Training a Helpful and Harmless Assistant with Reinforcement Learning from Human Feedback.
If you find the data useful, please cite the paper.
The data format is very simple -- each line of the jsonl files contains a pair of texts,
one "chosen" and one "rejected".
valid train size : 160780
"""
def preprocess_dialogue(self, text):
"""
trim prefix text to last two pairs
Outlier example Assistant answered empty string:
Assistant: Human: That makes sense, I agree with that, though there are many situations that
aren't considered justice, like sending a kid to prison for life. Human: You are completely
missing the point of this conversation, and not understanding anything I am saying. Human:
And I dont know if youre trying to be funny, but it isnt.
"""
last_two_convo = text.split("Human:")[-2:]
if len(last_two_convo[0]) == 0:
return "Human:".join(last_two_convo)
return "Human: " + "Human:".join(last_two_convo)
def __init__(self, split="train", sep_token="<sep>") -> None:
super().__init__()
assert split in ("train", "test")
if sep_token is None:
sep_token = " . "
self.pairs = []
# using prompt as our index will allows us
# to add additional generated prompt later
major_split = split if "train" == split else "test"
dataset = load_dataset("Anthropic/hh-rlhf")[major_split]
for data in dataset:
processed = self.preprocess_dialogue(data["chosen"])
# roughly 20 of these are invalid conversation
if "Assistant" not in processed:
continue
prompt, pos_postfix = processed.split("Assistant:", maxsplit=1)
prompt = prompt.replace("Human: ", "").strip()
pos_postfix = pos_postfix.replace("Human: ", sep_token).replace("\n\nAssistant: ", sep_token).strip()
processed = self.preprocess_dialogue(data["rejected"])
if "Assistant" not in processed:
continue
_, neg_postfix = processed.split("Assistant:", maxsplit=1)
neg_postfix = neg_postfix.replace("Human: ", sep_token).replace("\n\nAssistant: ", sep_token).strip()
self.pairs.append((prompt, (pos_postfix.strip(), neg_postfix.strip())))
def __len__(self):
return len(self.pairs)
def __getitem__(self, index):
context, pair = self.pairs[index]
return context, [pair]