bard-small / bard_steering.py
lowerkey's picture
Upload folder using huggingface_hub
18fe584 verified
Raw History Blame Contribute Delete
6.64 kB
"""Steering a turn while it is sampled: how long the narration runs, and what the choices look like."""
import math
import re
import torch
from .bard_rules import is_story_choice
LENGTHS = {"short": 60, "medium": 150, "long": None}
SENTENCE_END = re.compile(r"[.!?…][\"”’')\]]*\s*$|\n\s*$")
SENTENCE_DONE = re.compile(r"[.!?…][\"”’')\]]*(\s|$)")
NEG_INF = float("-inf")
class LengthControl:
MIN_CHOICES = 2
MAX_CHOICES = 5
MAX_CHOICE_TOKENS = 28
COPY_PENALTY = 1.5
COPY_PENALTY_MAX = 8.0
ECHO_PENALTY = 4.0
MIN_WORDS = 4
GIVE_UP_AFTER = 90
def __init__(self, tok, mode="medium", vars_weight=1.0):
self.tok = tok
self.vars_id = tok.specials["<|vars|>"]
self.choices_id = tok.specials["<|choices|>"]
self.sep_id = tok.specials["<|sep|>"]
self.go_on = [self.vars_id, self.choices_id]
self.stop = self.go_on + [tok.eos_id]
self.rep_free = False
first_special = min(tok.specials.values())
decoded = tok.vocab.id_to_token_decoded
self.silent = {i for i in range(first_special) if decoded(i) == ""}
self.pipes = torch.tensor([i for i in range(first_special) if "|" in decoded(i)], dtype=torch.long)
text_like = {self.sep_id, tok.eos_id, tok.specials["<|num|>"], tok.specials["<|snum|>"]}
self.no_specials = torch.tensor(
[i for i in range(first_special, 1 << 14) if i not in text_like], dtype=torch.long)
self.vars_bias = math.log(vars_weight) if vars_weight > 1 else 0.0
self.set_length(mode)
def set_length(self, mode):
self.mode = mode
self.budget = LENGTHS[mode]
def start_part(self, raw):
self.written = raw
self.done = "<|" in raw
self.offset = 0 if self.done else len(self.tok.encode(raw)[0])
self.prior = None
if "<|choices|>" in raw:
texts = raw.split("<|choices|>", 1)[1].split("<|sep|>")
self.prior = [[int(i) for i in self.tok.encode(text)[0]] if text else [] for text in texts]
self.rep_free = False
def __call__(self, generated, logits):
logits = logits.clone()
logits[self.pipes.to(logits.device)] = NEG_INF
if self.prior is not None or self.choices_id in generated:
self.rep_free = True
return self.steer_choices(generated, logits)
if self.done or any(token in self.stop for token in generated):
self.done = True
return logits
return self.steer_narration(generated, logits)
def steer_narration(self, generated, logits):
if not self.has_own_sentence(generated):
logits[self.stop] = NEG_INF
if self.vars_bias:
logits[self.vars_id] -= self.vars_bias
if self.budget is None or not generated:
return logits
length = self.offset + len(generated)
ends_a_sentence = SENTENCE_END.search(self.tok.decode(generated[-4:], [0] * 4))
if length < self.budget or not ends_a_sentence:
return logits
if length >= 2 * self.budget:
return self.end_narration(logits)
logits[self.go_on] += 2.0 + 6.0 * (length - self.budget) / self.budget
return logits
def has_own_sentence(self, generated):
if self.offset + len(generated) >= self.GIVE_UP_AFTER:
return True
text = (self.written + self.tok.decode(generated)).strip()
return bool(SENTENCE_DONE.search(text)) and len(text.split()) >= self.MIN_WORDS
def end_narration(self, logits):
best = max(self.go_on, key=lambda token: float(logits[token]))
kept = logits[best].clone()
logits.fill_(NEG_INF)
logits[best] = kept
return logits
def steer_choices(self, generated, logits):
finished, current = self.split_choices(generated)
real_choices = sum(1 for choice in finished if is_story_choice(self.tok.vocab.decode(choice)))
if not current or real_choices + 1 < self.MIN_CHOICES:
logits[self.tok.eos_id] = NEG_INF
if not current:
logits[self.sep_id] = NEG_INF
stray = self.no_specials.to(logits.device)
logits[stray[stray < logits.numel()]] = NEG_INF
if len(finished) + 1 >= self.MAX_CHOICES:
logits[self.sep_id] = NEG_INF
if len(current) >= self.MAX_CHOICE_TOKENS:
return self.close_choice(logits)
self.discourage_copies(logits, finished, current)
return logits
def split_choices(self, generated):
if self.prior is not None:
*finished, head = self.prior
written = list(generated)
else:
finished, head = [], []
after_marker = len(generated) - generated[::-1].index(self.choices_id)
written = list(generated[after_marker:])
pieces = [[]]
for token in written:
if token == self.sep_id:
pieces.append([])
else:
pieces[-1].append(token)
pieces[0] = head + pieces[0]
return [choice for choice in finished + pieces[:-1] if choice], pieces[-1]
def close_choice(self, logits):
closers = [self.sep_id, self.tok.eos_id]
closing = torch.full_like(logits, NEG_INF)
closing[closers] = logits[closers]
return logits if torch.isinf(closing).all() else closing
def discourage_copies(self, logits, finished, current):
penalties = {}
for choice in finished:
if choice == current:
logits[[self.sep_id, self.tok.eos_id]] = NEG_INF
self.penalize_retracing(penalties, choice, current)
self.penalize_echo(penalties, choice, current)
for token, amount in penalties.items():
logits[token] -= amount
def penalize_retracing(self, penalties, choice, current):
done = len(current)
if done < len(choice) and choice[:done] == current and choice[done] not in self.silent:
amount = min(self.COPY_PENALTY_MAX, self.COPY_PENALTY * (done + 1))
self.raise_penalty(penalties, choice[done], amount)
def penalize_echo(self, penalties, choice, current):
if len(current) < 2:
return
pair = current[-2:]
for i in range(len(choice) - 2):
if choice[i:i + 2] == pair and choice[i + 2] not in self.silent:
self.raise_penalty(penalties, choice[i + 2], self.ECHO_PENALTY)
@staticmethod
def raise_penalty(penalties, token, amount):
penalties[token] = max(penalties.get(token, 0.0), amount)