summarize_from_feedback/tasks.py (134 lines of code) (raw):
from dataclasses import dataclass, field
from typing import Optional, List, NewType, Union, Dict
import numpy as np
import torch
from summarize_from_feedback.utils import hyperparams
from summarize_from_feedback.utils.torch_utils import first_true_indices, to_numpy
from summarize_from_feedback.query_response_model import PADDING_TOKEN
@dataclass
class TaskQueryHParams(hyperparams.HParams):
length: int = None
dataset: str = None
format_str: Optional[str] = None # if underlying dataset yields dicts, can format arbitrarily
truncate_field: Optional[str] = None
truncate_text: Optional[str] = None
padding: Optional[str] = None # defaults to repeated spaces
pad_side: Optional[str] = None
@dataclass
class TaskResponseHParams(hyperparams.HParams):
ref_format_str: Optional[
str
] = None # if underlying dataset yields dicts, can format arbitrarily
length: int = None
# Truncate response at the first occurrence of this token when sampling.
truncate_token: Optional[int] = None
@dataclass
class TaskHParams(hyperparams.HParams):
query: TaskQueryHParams = field(default_factory=TaskQueryHParams)
response: TaskResponseHParams = field(default_factory=TaskResponseHParams)
# Has endoftext potentially, random stuff after
SampledTokens = NewType("SampledTokens", torch.LongTensor)
SampledTokenList = NewType("SampledTokenList", List[int])
# Has only the actual sample + padding tokens
ProcessedTokens = NewType("ProcessedTokens", torch.LongTensor)
ProcessedTokenList = NewType("ProcessedTokenList", List[int])
class ResponseEncoder:
def __init__(self, H: TaskResponseHParams, encoder, padding_token=PADDING_TOKEN):
self.H = H
self.encoder = encoder
self.padding_token = padding_token
def process_responses(self, unprocessed_tokens: SampledTokens) -> ProcessedTokens:
assert unprocessed_tokens.size(-1) == self.H.length
if self.H.truncate_token is not None:
assert self.padding_token is not None
trunc_idxs = first_true_indices(unprocessed_tokens == self.H.truncate_token).unsqueeze(
-1
)
new_size = [1] * (len(unprocessed_tokens.size()) - 1) + [self.H.length]
idxs = torch.arange(self.H.length, device=unprocessed_tokens.device).view(*new_size)
return torch.masked_fill(unprocessed_tokens, idxs > trunc_idxs, self.padding_token)
else:
return unprocessed_tokens
def encode_response(self, text: str, allow_truncate: bool = False) -> ProcessedTokenList:
tokens = self.encoder.encode(text)
if allow_truncate:
tokens = tokens[: self.H.length - (0 if self.H.truncate_token is None else 1)]
if self.H.truncate_token is not None:
tokens = tokens + [self.H.truncate_token]
if self.padding_token is None:
assert len(tokens) == self.H.length
return tokens
assert len(tokens) <= self.H.length, f"Response too long (limit {self.H.length}): {text}"
return tokens + [self.padding_token] * (self.H.length - len(tokens))
def decode_response(self, processed_response_tokens: ProcessedTokenList) -> str:
tokens = [x for x in processed_response_tokens if x != self.padding_token]
if self.H.truncate_token is not None:
if tokens[-1] == self.H.truncate_token:
tokens = tokens[:-1]
else:
assert len(tokens) == self.H.length
return self.encoder.decode(tokens)
def decode_responses(
self, processed_response_tokens: Union[ProcessedTokens, np.ndarray]
): # -> array of array of ... str:
def _decode_responses_list(l):
if isinstance(l[0], (int, np.int64)):
return self.decode_response(l)
return [_decode_responses_list(ll) for ll in l]
return _decode_responses_list(to_numpy(processed_response_tokens))
def _ensure_length(toks, l, pad_sequence=None, pad_side=None, truncate_side=None):
assert pad_side in (None, "left", "right")
assert truncate_side in (None, "left", "right")
if len(toks) < l:
assert pad_sequence is not None
pad_amt = l - len(toks)
assert len(pad_sequence) >= pad_amt, f"{len(pad_sequence)} < {pad_amt}"
if pad_side is None:
assert len(toks) == l, f"Needed to pad! {len(toks)} < {l}"
return toks
elif pad_side == "left":
return pad_sequence[-pad_amt:] + toks
else:
assert pad_side == "right"
return toks + pad_sequence[:pad_amt]
if truncate_side is None:
assert len(toks) == l, f"Needed to truncate! {len(toks)} > {l}"
return toks
elif truncate_side == "left":
return toks[-l:]
else:
assert truncate_side == "right"
return toks[:l]
def _get_query_padding_for_task(encoder, hparams: TaskQueryHParams):
if hparams.padding is not None:
return encoder.encode(hparams.padding)
return encoder.encode(" ") * hparams.length
def process_query(
query_info: Dict[str, str], *, encoder, hparams: TaskQueryHParams, pad_sequence=None
):
if pad_sequence is None:
pad_sequence = _get_query_padding_for_task(encoder, hparams)
if isinstance(query_info, str):
query_info = dict(query=query_info)
else:
# copy to avoid mutating input
query_info = dict(**query_info)
format_str = hparams.format_str or "{query}"
query_tokens = encoder.encode(format_str.format(**query_info))
truncate_field = hparams.truncate_field or "query"
if truncate_field not in query_info:
raise ValueError(
f"Could not truncate field {truncate_field}, found fields: {query_info.keys()}!"
)
while len(query_tokens) > hparams.length:
if not len(query_info[truncate_field]):
raise ValueError("Could not truncate enough!")
i = -1 # default to just remove one character
if hparams.truncate_text:
try:
i = query_info[truncate_field].rindex(hparams.truncate_text)
except ValueError:
pass
query_info[truncate_field] = query_info[truncate_field][:i]
query_tokens = encoder.encode(format_str.format(**query_info))
return dict(
tokens=_ensure_length(
query_tokens, hparams.length, pad_side=hparams.pad_side, pad_sequence=pad_sequence
)
)