decider-2b / decider /temperature.py
Mapika's picture
decider-2b v11
533964d verified
Raw History Blame Contribute Delete
7.29 kB
"""Answer temperatures from decider_config.json (1.4.0).
Every answer is softmax(logits / T) over its option letters. decider_config.json sets T:
"temperature": 1.3 one value for every answer (the only form before 1.4.0)
"temperature_by_type": {"choice": 1.48, "noul": 2.22, "score": 1.38}
optional; one value per answer type, a missing type uses "temperature"
"temperature_schema_first": 1.18 optional; the schema cache (questions-first layout), as before
"temperature_schema_first_by_type": {...} optional; per answer type on the schema cache
The answer types are the /v1/systemone question types. A /decide field maps onto them: "choice" -> "choice", "bool" -> "noul",
"scale" -> "score". Plain `Decider.decide` questions (a question and its options, no type) are "choice". A Score question
read with isolated levels (one yes/no row per level) uses the "score" temperature on every one of its level rows: the rows form
one Score answer, and a temperature fitted on Score answers is fitted through that same readout (decider.calibrate).
On the state-first layout the temperature of an answer of type t is temperature_by_type[t], else temperature. On the schema
cache it is temperature_schema_first_by_type[t], else temperature_schema_first, else (no schema-first value at all) the
state-first temperature of t. An explicit override (Decider(temperature=...), DECIDER_TEMPERATURE) replaces the state-first
"temperature" and switches temperature_by_type off, so the override is the one temperature of every state-first answer, as it
was before 1.4.0.
A config without a by-type map gives every path the single number it gave in 1.3.0, and that number reaches the engines as the
same Python float, so the probabilities are bit-identical to 1.3.0.
"""
import math
TYPES = ("choice", "noul", "score")
FIELD_TYPES = {"choice": "choice", "bool": "noul", "scale": "score"} # /decide field type -> answer type
_KEYS_TEXT = ('the keys are "choice", "noul" and "score" (a /decide "bool" field is "noul", a "scale" field is "score"; '
'a /v1/systemone "bool" question is "noul")')
def positive(value, where):
"""A temperature: a number (or a numeric string, as float() reads it, which 1.3.0 accepted for "temperature") that is finite
and > 0. Raises ValueError naming `where`."""
if isinstance(value, bool):
raise ValueError(f"{where} must be a finite number > 0, got {value!r}")
try:
v = float(value)
except (TypeError, ValueError):
raise ValueError(f"{where} must be a finite number > 0, got {value!r}") from None
if not math.isfinite(v) or v <= 0:
raise ValueError(f"{where} must be a finite number > 0, got {value!r}")
return v
def by_type(m, where):
"""Validate a {answer type: temperature} map. None -> {}. Unknown keys, non-numbers and values that are not finite and > 0
raise ValueError."""
if m is None:
return {}
if not isinstance(m, dict):
raise ValueError(f"{where} must be a map {{answer type: temperature}}, got {type(m).__name__}; " + _KEYS_TEXT)
out = {}
for k, v in m.items():
if k not in TYPES:
raise ValueError(f"{where} has the unknown key {k!r}; " + _KEYS_TEXT)
if isinstance(v, bool) or not isinstance(v, (int, float)):
raise ValueError(f"{where}[{k!r}] must be a finite number > 0, got {v!r}")
out[k] = positive(v, f"{where}[{k!r}]")
return out
def from_config(cfg, temperature=None, temperature_by_type=None):
"""-> ((T, by_type) for the state-first layout, (T, by_type) for the schema cache).
temperature / temperature_by_type: explicit overrides (Decider arguments, DECIDER_TEMPERATURE). An explicit temperature
without an explicit map switches the config's map off (see the module docstring)."""
cfg = cfg or {}
where = "decider_config.json"
if temperature is not None:
T = positive(temperature, "temperature")
m = by_type(temperature_by_type, "temperature_by_type") if temperature_by_type is not None else {}
else:
T = positive(cfg.get("temperature", 1.0), f'{where} "temperature"')
m = (by_type(temperature_by_type, "temperature_by_type") if temperature_by_type is not None
else by_type(cfg.get("temperature_by_type"), f'{where} "temperature_by_type"'))
ms = by_type(cfg.get("temperature_schema_first_by_type"), f'{where} "temperature_schema_first_by_type"')
if "temperature_schema_first" in cfg:
schema = (positive(cfg["temperature_schema_first"], f'{where} "temperature_schema_first"'), ms)
else:
schema = (T, {**m, **ms})
return (T, m), schema
def effective(T, m):
"""{answer type: the temperature it gets} for reporting (/health, the ready line)."""
return {t: m.get(t, T) for t in TYPES}
def for_types(T, m, types):
"""One temperature per slot, or the scalar T itself when there is no map (the 1.3.0 call)."""
if not m:
return T
return [m.get(t, T) for t in types]
def item_types(it):
"""The answer type of every slot of a prompt item ("types", set where the item is built); None for an item without it."""
ts = it.get("types")
return list(ts) if ts is not None else [None] * len(it["slots"])
def for_items(T, m, items):
"""The `temperature` argument of Engine.score_items / score_shared: the scalar T when there is no map (the 1.3.0 call),
else one list of per-slot temperatures per item."""
if not m:
return T
return [[m.get(t, T) for t in item_types(it)] for it in items]
def slot_temperatures(temperature, items):
"""Engine side. temperature: a scalar (returned as it is), or one entry per item, each a scalar or one value per slot of
that item. -> the scalar, or a flat list with one temperature per slot in item order."""
if not isinstance(temperature, (list, tuple)):
return temperature
if len(temperature) != len(items):
raise ValueError(f"temperature: {len(temperature)} entries for {len(items)} items")
flat = []
for t, it in zip(temperature, items):
n = len(it["slots"])
if isinstance(t, (list, tuple)):
if len(t) != n:
raise ValueError(f"temperature: {len(t)} values for an item with {n} slots")
flat += list(t)
else:
flat += [t] * n
return flat
def item_slice(temperature, lo, hi):
"""The per-item temperature entries of items[lo:hi] (a scalar is shared by every item)."""
return temperature[lo:hi] if isinstance(temperature, (list, tuple)) else temperature
def scaled_softmax(lg, temperature):
"""softmax(lg / T) over the last axis. A scalar T is the 1.3.0 expression unchanged; a list gives one T per row of lg."""
import torch
if isinstance(temperature, (list, tuple)):
if len(temperature) != lg.shape[0]:
raise ValueError(f"temperature: {len(temperature)} values for {lg.shape[0]} slots")
t = torch.tensor(temperature, dtype=lg.dtype).to(lg.device, non_blocking=True)[:, None]
return torch.softmax(lg / t, -1)
return torch.softmax(lg / temperature, -1)