pianissimo-da-mlx / phrase_boost.py
mfalt's picture
Initial release
a7c92ae
Raw History Blame Contribute Delete
3.35 kB
"""Phrase boosting for Klang Pianissimo. A numpy port of NeMo's GPU-PB boosting tree
(ContextGraph and GPUBoostingTreeModel, NeMo 2.7, Apache 2.0)."""
import math
from collections import deque
import numpy as np
import sentencepiece as spm
class _Node:
__slots__ = ("id", "children", "token_score", "node_score", "is_end", "fail")
def __init__(self, node_id, token_score, node_score, is_end):
self.id = node_id
self.children = {}
self.token_score = token_score
self.node_score = node_score
self.is_end = is_end
self.fail = None
class PhraseBoost:
def __init__(self, phrases, tokenizer_path, vocab_size, context_score=1.0, depth_scaling=2.0):
sp = spm.SentencePieceProcessor(model_file=str(tokenizer_path))
self.vocab_size = vocab_size
self.root = _Node(0, 0.0, 0.0, False)
self.root.fail = self.root
self.nodes = [self.root]
for phrase in dict.fromkeys(phrases):
self._add(sp.encode(phrase), context_score, depth_scaling)
self._fill_fail()
self._cache = {}
def _add(self, tokens, context_score, depth_scaling):
node = self.root
for i, token in enumerate(tokens):
last = i == len(tokens) - 1
child = node.children.get(token)
if child is None:
token_score = context_score * depth_scaling + math.log(i + 1) if i > 0 else context_score
child = _Node(len(self.nodes), token_score, node.node_score + token_score, last)
node.children[token] = child
self.nodes.append(child)
else:
child.token_score = max(context_score, child.token_score)
child.node_score = node.node_score + child.token_score
child.is_end = last or child.is_end
node = child
def _fill_fail(self):
queue = deque()
for child in self.root.children.values():
child.fail = self.root
queue.append(child)
while queue:
node = queue.popleft()
for token, child in node.children.items():
fail = node.fail
while token not in fail.children and fail is not self.root:
fail = fail.fail
child.fail = fail.children.get(token, self.root)
queue.append(child)
def scores(self, state):
"""Bonus and next state for every token from `state`."""
if state not in self._cache:
bonus = np.zeros(self.vocab_size, dtype=np.float32)
nxt = np.full(self.vocab_size, -1, dtype=np.int64)
node, taken_back = self.nodes[state], 0.0
while True:
for token, child in node.children.items():
if nxt[token] < 0:
bonus[token] = taken_back + child.token_score
nxt[token] = child.id
if node is self.root:
unset = nxt < 0
bonus[unset] = taken_back
nxt[unset] = 0
break
if not node.is_end:
taken_back += node.fail.node_score - node.node_score
node = node.fail
self._cache[state] = (bonus, nxt)
return self._cache[state]