summarize_from_feedback/datasets/encodings.py (144 lines of code) (raw):
import json
import os.path
from dataclasses import dataclass
from functools import lru_cache
from typing import ClassVar
import blobfile as bf
import regex as re
import torch
ENCODINGS_BASE = "https://openaipublic.blob.core.windows.net/summarize-from-feedback/encodings"
def read_file(path):
with bf.BlobFile(path, "rb") as f:
return f.read()
@lru_cache()
def bytes_to_unicode():
"""
Returns list of utf-8 byte and a corresponding list of unicode strings.
The reversible bpe codes work on unicode strings.
This means you need a large # of unicode characters in your vocab if you want to avoid UNKs.
When you're at something like a 10B token dataset you end up needing around 5K for decent coverage.
This is a signficant percentage of your normal, say, 32K bpe vocab.
To avoid that, we want lookup tables between utf-8 bytes and unicode strings.
And avoids mapping to whitespace/control characters the bpe code barfs on.
"""
bs = (
list(range(ord("!"), ord("~") + 1))
+ list(range(ord("¡"), ord("¬") + 1))
+ list(range(ord("®"), ord("ÿ") + 1))
)
cs = bs[:]
n = 0
for b in range(2 ** 8):
if b not in bs:
bs.append(b)
cs.append(2 ** 8 + n)
n += 1
cs = [chr(n) for n in cs]
return dict(zip(bs, cs))
@dataclass
class Encoding:
name: str
n_vocab: int
# End of text token
eot_token: int = None
registry: ClassVar[dict] = dict()
eoprefix_token: int = None
def __post_init__(self):
self._register()
def _register(self):
assert self.name not in self.registry
self.registry[self.name] = self
def __call__(self, text):
return self.encode(text)
def encode(self, text):
"""Convert text (or other data) into an array of integer tokens"""
raise NotImplementedError
def decode(self, tokens) -> str:
"""Convert array of integer tokens into text (or other data)."""
raise NotImplementedError
@dataclass
class BPEEncoding(Encoding):
base_path: str = None
encoder_path: str = "encoder.json"
bpe_path: str = "vocab.bpe"
n_denoise_sentinels: int = 0
# Should haved added re.IGNORECASE so BPE merges can happen for capitalized versions of contractions
pat = re.compile(
r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
)
def __post_init__(self):
super().__post_init__()
self.base_path = self.base_path or os.path.join(ENCODINGS_BASE, self.name)
self.full_encoder_path = os.path.join(self.base_path, self.encoder_path)
self.full_bpe_path = os.path.join(self.base_path, self.bpe_path)
# We don't load the full BPE data until needed
# Initialize them to 'None' here to make the linter happy
# Note: Do not move these to @dataclass fields. That will lead to enormous outputs,
# since the default dataclass __repr__ prints the values of all fields.
self._token_str_to_idx = None
self._token_idx_to_str = None
self.byte_decoder = None
self.byte_encoder = None
self.bpe_ranks = None
# Without this cache, performance is 5x slower
self.bpe = lru_cache(maxsize=2 ** 17)(self.bpe)
def _load(self):
if self._token_str_to_idx is not None:
return
self._token_str_to_idx = json.loads(read_file(self.full_encoder_path).decode())
bpe_data = read_file(self.full_bpe_path).decode()
bpe_merges = [tuple(merge_str.split()) for merge_str in bpe_data.split("\n")[1:-1]]
assert self.eot_token == self._token_str_to_idx["<|endoftext|>"]
# Add an <|end_of_prefix|> token
if self.eoprefix_token is not None:
assert not self._token_str_to_idx.get("<|endofprefix|>")
self._token_str_to_idx["<|endofprefix|>"] = self.eoprefix_token
# Add denoise sentinel tokens like <|dn_1|> <|dn_2|> etc.
# These tokens are added to the end of the vocabulary range
for denoise_sentinel_idx in range(self.n_denoise_sentinels):
str_repr = f"<|dn_{denoise_sentinel_idx}|>"
assert not self._token_str_to_idx.get(str_repr)
n_non_sentinel_tokens = self.n_vocab - self.n_denoise_sentinels
sentinel_token = n_non_sentinel_tokens + denoise_sentinel_idx
self._token_str_to_idx[str_repr] = sentinel_token
assert len(self._token_str_to_idx) == self.n_vocab
self._token_idx_to_str = {v: k for k, v in self._token_str_to_idx.items()}
self.byte_encoder = bytes_to_unicode()
self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
self.bpe_ranks = dict(zip(bpe_merges, range(len(bpe_merges))))
def bpe(self, token): # pylint: disable=method-hidden
word = tuple(self.byte_encoder[b] for b in token.encode("utf-8"))
if len(word) == 1:
return word
while True:
min_pair = None
min_idxs = []
min_rank = None
for i, pair in enumerate(zip(word[:-1], word[1:])):
try:
rank = self.bpe_ranks[pair]
if min_rank is None or rank < min_rank:
min_rank = rank
min_pair = pair
del min_idxs[::]
min_idxs.append(i)
elif min_rank == rank and i > min_idxs[-1] + 1:
min_idxs.append(i)
except KeyError:
pass
if min_pair is None:
break
new_word = []
i_start = 0
for i in min_idxs:
new_word.extend(word[i_start:i])
new_word.append(min_pair[0] + min_pair[1])
i_start = i + 2
if i_start < len(word):
new_word.extend(word[i_start:])
new_word = tuple(new_word)
word = new_word
if len(word) == 1:
break
return word
def encode(self, text):
self._load()
bpe_tokens = [
self._token_str_to_idx[bpe_token]
for token in re.findall(self.pat, text)
for bpe_token in self.bpe(token)
]
return bpe_tokens
def decode(self, tokens, errors="replace"):
if isinstance(tokens, torch.Tensor):
if tokens.dim() == 1 and tokens.size(0) == 1:
tokens = tokens.tolist()
else:
tokens = tokens.squeeze().tolist()
decoded_bytes = self.decode_bytes(tokens)
text = decoded_bytes.decode("utf-8", errors=errors)
return text
def decode_bytes(self, tokens):
self._load()
text = "".join([self._token_idx_to_str[token] for token in tokens])
return bytearray([self.byte_decoder[c] for c in text])
Reversible = BPEEncoding(name="reversible_50000", n_vocab=50257, eot_token=50256)