MyeongHoJeong's picture
Update weights
b6b0992 verified
Raw History Blame Contribute Delete
5.59 kB
"""Answer-label schemes shared by serving, readout export, and training data.
A scheme is an ordered list of candidate label strings. A model's labels are
the candidates that its tokenizer encodes as one distinct, non-special token
which decodes back to exactly the candidate, taken in candidate order, at most
``MAX_LABELS``. A question with ``n`` options always uses the first ``n``.
``upper`` (the default): ``A``..``Z``, then two-letter ``AA``, ``AB``, ...,
``ZZ``, then three-letter ``AAA``..``ZZZ``; uppercase ASCII only, ordered by
length and then alphabetically. Three-letter candidates are only consulted
when the one- and two-letter single tokens number fewer than ``MAX_LABELS``.
``served`` (the original order, kept to reproduce pre-v2.2 serving): ``A``..``Z``, ``a``..``z``,
``0``..``9``, ``000``..``999``, ``AA``..``ZZ``, ``aa``..``zz``. No v2 training
row had more than 26 options, so beyond Z neither order is what v2 learned.
Both schemes give ``A``..``Z`` to questions with at most 26 options whenever
the tokenizer has those 26 single-token letters.
"""
import itertools
import os
import re
import string
from collections.abc import Mapping, Sequence
from typing import Any
LABEL_SCHEMES = ("served", "upper")
DEFAULT_LABEL_SCHEME = "upper"
LABEL_SCHEME_ENV = "JEV_LABEL_SCHEME"
MAX_LABELS = 255
UPPER_MAX_LENGTH = 3
_UPPER_LABEL = re.compile(r"[A-Z]{1,3}")
def validate_label_scheme(value: Any) -> str:
if value not in LABEL_SCHEMES:
raise ValueError(
f"label scheme must be one of {', '.join(LABEL_SCHEMES)}; got {value!r}"
)
return value
def resolve_label_scheme(
value: str | None = None, environ: Mapping[str, str] | None = None
) -> str:
"""An explicit value, else ``JEV_LABEL_SCHEME``, else the default (upper)."""
if value is None or value == "":
environ = os.environ if environ is None else environ
value = environ.get(LABEL_SCHEME_ENV) or DEFAULT_LABEL_SCHEME
return validate_label_scheme(value)
def _served_candidates() -> tuple[str, ...]:
letters = string.ascii_uppercase + string.ascii_lowercase + string.digits
return tuple(
list(letters)
+ [f"{value:03d}" for value in range(1000)]
+ [
first + second
for alphabet in (string.ascii_uppercase, string.ascii_lowercase)
for first in alphabet
for second in alphabet
]
)
def _upper_length(length: int) -> tuple[str, ...]:
return tuple(
"".join(chars)
for chars in itertools.product(string.ascii_uppercase, repeat=length)
)
def candidate_tiers(scheme: str) -> tuple[tuple[str, ...], ...]:
"""Candidates in order, grouped so a caller may stop after an early group.
A later group is consulted only while fewer than MAX_LABELS labels were
found; the concatenated order is the scheme's order either way.
"""
validate_label_scheme(scheme)
if scheme == "served":
return (_served_candidates(),)
return (
_upper_length(1) + _upper_length(2),
_upper_length(3),
)
def label_candidates(scheme: str) -> list[str]:
return [label for tier in candidate_tiers(scheme) for label in tier]
def select_labels(
tokenizer: Any, scheme: str, *, limit: int = MAX_LABELS
) -> tuple[list[str], list[int]]:
"""The scheme's labels for a tokenizer with ``encode`` and ``decode``.
A candidate is kept when it encodes (without special tokens) to exactly
one token that is not special, not already taken, and decodes back to the
candidate. This is the rule every exporter, backend, and training-data
writer must share; it needs only the tokenizer files.
"""
if type(limit) is not int or not 1 <= limit <= MAX_LABELS:
raise ValueError(f"limit must be an integer from 1 to {MAX_LABELS}")
special = set(getattr(tokenizer, "all_special_ids", ()) or ())
labels: list[str] = []
token_ids: list[int] = []
seen: set[int] = set()
for tier in candidate_tiers(scheme):
for label in tier:
ids = tokenizer.encode(label, add_special_tokens=False)
if not isinstance(ids, list) or len(ids) != 1 or type(ids[0]) is not int:
continue
token = ids[0]
if token < 0 or token in seen or token in special:
continue
if tokenizer.decode([token], skip_special_tokens=True) != label:
continue
labels.append(label)
token_ids.append(token)
seen.add(token)
if len(labels) == limit:
return labels, token_ids
return labels, token_ids
def follows_scheme_order(scheme: str, labels: Sequence[str]) -> bool:
"""Whether labels are distinct candidates of the scheme in scheme order.
A structural check that needs no tokenizer: it cannot tell whether a
skipped candidate was a single token, only that nothing is out of order.
"""
validate_label_scheme(scheme)
if not labels or any(not isinstance(label, str) for label in labels):
return False
if scheme == "upper":
if any(_UPPER_LABEL.fullmatch(label) is None for label in labels):
return False
ranks = [(len(label), label) for label in labels]
else:
index = {label: position for position, label in enumerate(_served_candidates())}
if any(label not in index for label in labels):
return False
ranks = [index[label] for label in labels]
return all(before < after for before, after in itertools.pairwise(ranks))