-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpreference_datasets.py
272 lines (238 loc) · 11 KB
/
preference_datasets.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
import torch
import datasets
from utils import TemporarilySeededRandom
from torch.nn.utils.rnn import pad_sequence
import random
import numpy as np
from typing import Dict, List, Optional, Iterator, Callable, Union
def get_collate_fn(
tokenizer
) -> Callable[[List[Dict]], Dict[str, Union[List, torch.Tensor]]]:
"""Returns a collate function for the given tokenizer.
The collate function takes a list of examples (dicts, where values are lists of
ints [tokens] or strings [the original texts]) and returns a batch of examples,
PyTorch tensors padded to the maximum length. Strings are passed through."""
def collate_fn(batch):
# first, pad everything to the same length
padded_batch = {}
for k in batch[0].keys():
if k.endswith('_input_ids') or k.endswith(
'_attention_mask') or k.endswith('_labels'):
if 'prompt' in k: # adapted from https://stackoverflow.com/questions/73256206
to_pad = [torch.LongTensor(ex[k][::-1]) for ex in batch]
else:
to_pad = [torch.LongTensor(ex[k]) for ex in batch]
if k.endswith('_input_ids'):
padding_value = tokenizer.pad_token_id
elif k.endswith('_labels'):
padding_value = -100
elif k.endswith('_attention_mask'):
padding_value = 0
else:
raise ValueError(f"Unexpected key in batch '{k}'")
padded_batch[k] = pad_sequence(to_pad,
batch_first=True,
padding_value=padding_value)
if 'prompt' in k: # for the prompt, flip back so padding is on left side
padded_batch[k] = padded_batch[k].flip(dims=[1])
else:
padded_batch[k] = [ex[k] for ex in batch]
return padded_batch
return collate_fn
def tokenize_batch_element(prompt: str, chosen: str, rejected: str,
truncation_mode: str, tokenizer, max_length: int,
max_prompt_length: int) -> Dict:
"""Tokenize a single batch element.
At this stage, we don't convert to PyTorch tensors yet; we just handle the truncation
in case the prompt + chosen or prompt + rejected responses is/are too long. First
we truncate the prompt; if we're still too long, we truncate the chosen/rejected.
We also create the labels for the chosen/rejected responses, which are of length equal to
the sum of the length of the prompt and the chosen/rejected response, with -100 for the
prompt tokens.
"""
chosen_tokens = tokenizer(chosen, add_special_tokens=False)
rejected_tokens = tokenizer(rejected, add_special_tokens=False)
prompt_tokens = tokenizer(prompt, add_special_tokens=False)
assert tokenizer.eos_token_id not in prompt_tokens[
'input_ids'], f"Prompt contains EOS token: {prompt}"
assert tokenizer.eos_token_id not in chosen_tokens[
'input_ids'], f"Chosen response contains EOS token: {chosen}"
assert tokenizer.eos_token_id not in rejected_tokens[
'input_ids'], f"Rejected response contains EOS token: {rejected}"
chosen_tokens['input_ids'].append(tokenizer.eos_token_id)
chosen_tokens['attention_mask'].append(1)
rejected_tokens['input_ids'].append(tokenizer.eos_token_id)
rejected_tokens['attention_mask'].append(1)
longer_response_length = max(len(chosen_tokens['input_ids']),
len(rejected_tokens['input_ids']))
# if combined sequence is too long, truncate the prompt
if len(prompt_tokens['input_ids']) + longer_response_length > max_length:
if truncation_mode == 'keep_start':
prompt_tokens = {
k: v[:max_prompt_length]
for k, v in prompt_tokens.items()
}
elif truncation_mode == 'keep_end':
prompt_tokens = {
k: v[-max_prompt_length:]
for k, v in prompt_tokens.items()
}
else:
raise ValueError(f'Unknown truncation mode: {truncation_mode}')
# if that's still too long, truncate the response
if len(prompt_tokens['input_ids']) + longer_response_length > max_length:
chosen_tokens = {
k: v[:max_length - max_prompt_length]
for k, v in chosen_tokens.items()
}
rejected_tokens = {
k: v[:max_length - max_prompt_length]
for k, v in rejected_tokens.items()
}
# Create labels
chosen_sequence_tokens = {
k: prompt_tokens[k] + chosen_tokens[k]
for k in chosen_tokens
}
rejected_sequence_tokens = {
k: prompt_tokens[k] + rejected_tokens[k]
for k in rejected_tokens
}
chosen_sequence_tokens['labels'] = chosen_sequence_tokens['input_ids'][:]
chosen_sequence_tokens['labels'][:len(prompt_tokens['input_ids'])] = [
-100
] * len(prompt_tokens['input_ids'])
rejected_sequence_tokens['labels'] = rejected_sequence_tokens[
'input_ids'][:]
rejected_sequence_tokens['labels'][:len(prompt_tokens['input_ids'])] = [
-100
] * len(prompt_tokens['input_ids'])
batch = {}
batch['prompt'] = prompt
batch['chosen'] = prompt + chosen
batch['rejected'] = prompt + rejected
batch['chosen_response_only'] = chosen
batch['rejected_response_only'] = rejected
for k, toks in {
'chosen': chosen_sequence_tokens,
'rejected': rejected_sequence_tokens,
'prompt': prompt_tokens
}.items():
for type_key, tokens in toks.items():
if type_key == 'token_type_ids':
continue
batch[f'{k}_{type_key}'] = tokens
return batch
def get_batch_iterator(names: List[str],
tokenizer,
split: str = 'train',
batch_size: int = 1,
shuffle: bool = True,
max_length: int = 512,
max_prompt_length: int = 128,
sft_mode: bool = False,
n_epochs: Optional[int] = None,
n_examples: Optional[int] = None,
seed: int = 0,
silent: bool = False,
cache_dir: Optional[str] = None,
dataset: Dict = None) -> Iterator[Dict]:
"""Get an iterator over batches of data. Stops after n_epochs or n_examples, whichever comes first.
Args:
names: Names of datasets to use.
tokenizer: Tokenizer to use.
split: Which split to use.
batch_size: Batch size.
shuffle: Whether to shuffle the data after each epoch.
max_length: Maximum length of the combined prompt + response.
max_prompt_length: Maximum length of the prompt.
sft_mode: Whether to use SFT mode (i.e., return sft_target instead of chosen/rejected). In sft mode, we just return chosen_input_ids, but they contain the sft_target.
n_epochs: Number of epochs to run for. This or n_examples must be specified.
n_examples: Number of examples to run for. This or n_epochs must be specified.
seed: Random seed.
silent: Whether to silence the progress bar(s).
cache_dir: Directory to cache the datasets in.
"""
assert n_epochs is not None or n_examples is not None, "Must specify either n_epochs or n_examples"
if silent:
datasets.logging.disable_progress_bar()
datasets.logging.set_verbosity_error()
with TemporarilySeededRandom(seed):
permutation_seeds = iter(np.random.randint(0, 2**32, size=1000000))
flat_data = []
data = dataset["train"] if split == 'train' else dataset["test"]
for prompt, data in data.items():
flat_data.append((prompt, data['responses'], data['pairs'],
data['sft_target'], data['truncation_mode']))
collate_fn = get_collate_fn(tokenizer)
epoch_idx = 0
example_idx = 0
done = False
while True:
if n_epochs is not None and epoch_idx >= n_epochs:
if not silent:
print(
f'Finished generating {n_epochs} epochs on {split} split')
break
if shuffle:
with TemporarilySeededRandom(next(permutation_seeds)):
random.shuffle(flat_data)
batch = []
for prompt, responses, pairs, sft_target, truncation_mode in flat_data:
if done:
break
if sft_mode:
batch_element = tokenize_batch_element(prompt, sft_target,
sft_target,
truncation_mode,
tokenizer, max_length,
max_prompt_length)
batch_element = {
k: v
for k, v in batch_element.items() if 'rejected' not in k
}
batch.append(batch_element)
example_idx += 1
if len(batch) == batch_size:
yield collate_fn(batch)
if n_examples is not None and example_idx >= n_examples:
if not silent:
print(
f'Finished generating {n_examples} examples on {split} split'
)
done = True
batch = []
else:
for p in pairs:
if done:
break
batch_element = tokenize_batch_element(
prompt, responses[p[0]], responses[p[1]],
truncation_mode, tokenizer, max_length,
max_prompt_length)
batch.append(batch_element)
example_idx += 1
if len(batch) == batch_size:
yield collate_fn(batch)
if n_examples is not None and example_idx >= n_examples:
if not silent:
print(
f'FINISHED {n_examples} EXAMPLES on {split} split'
)
done = True
batch = []
if done:
break
epoch_idx += 1
def strings_match_up_to_spaces(str_a: str, str_b: str) -> bool:
"""Returns True if str_a and str_b match up to spaces, False otherwise."""
for idx in range(min(len(str_a), len(str_b)) - 2):
if str_a[idx] != str_b[idx]:
if str_a[idx] != ' ' and str_b[idx] != ' ':
return False
else:
if str_a[idx] == ' ':
str_a = str_a[:idx] + str_a[idx + 1:]
else:
str_b = str_b[:idx] + str_b[idx + 1:]
return True