Step 1: Collect Jev's probabilities
Tallyboard's support bot caches every answer it writes. When a new question arrives, it finds the closest cached question and asks Jev one thing: can the cached answer be sent, unchanged, as the answer to the new question? A yes skips a model call and replies at once.
pairs.jsonl holds 160 such pairs, each labelled reusable by hand.
Many are easy: a paraphrase ("team plan price per user?") or a different
topic. The hard ones differ by one detail. "Team, 5 seats, annual"
against "Team, 6 seats, annual" is not reusable. "Can we pay in euros?"
against "Can we pay in British pounds?" is: the cached "No. We bill only
in US dollars." answers both.
The pairs are split by cache entry. Eight cached questions and their
pairs form dev; the other eight form test. Everything you tune in
this lab is tuned on dev and then checked on questions it never saw.
do this
Open calib.py.
1. Write ask(pair, question=REUSE_QUESTION, post=jev.post). One
post with the state {"cached_question", "cached_answer", "new_question"} from the pair and a single noul question named
"reuse" whose instructions are question. Return the probability.
2. Write collect(pairs, question=REUSE_QUESTION, post=jev.post).
Ask about every pair through ThreadPoolExecutor(16), keep the order,
and return a numpy array.
3. Run. A few dev pairs with their probability and label, then a histogram of the dev split by probability. Reusable pairs average about 0.66 and the rest about 0.10, and almost no pair crosses sides. Look where the reusable pairs sit, though: most of them are between 0.5 and 0.9, and few are above 0.9.
calib.py, the file you edit118 lines
"""Calibrate Jev's probabilities for a semantic cache.
Before Tallyboard's support bot writes a new answer, it looks for a cached
question close to the new one and asks Jev whether the cached answer can be
sent as it is. A wrong reuse sends a customer a wrong answer, so the cache
may only reuse when it is very likely right. "Very likely" has to be a
number you can trust, which is what calibration is about.
"""
import json
import os
from concurrent.futures import ThreadPoolExecutor
import numpy as np
import jev
HERE = os.path.dirname(os.path.abspath(__file__))
REUSE_QUESTION = "Can the cached answer be sent, unchanged, as the answer to the new question?"
WORDINGS = {
"reuse": REUSE_QUESTION,
"same": "Do the two questions ask the same thing?",
"verify": ("Is the cached answer a correct and complete answer to the new question, "
"with nothing in it that would mislead the customer?"),
}
def load_pairs(path="pairs.jsonl"):
with open(os.path.join(HERE, path), encoding="utf-8") as f:
return [json.loads(line) for line in f if line.strip()]
def labels(pairs):
"""1 for a reusable pair, 0 otherwise, as a numpy array."""
return np.array([1 if p["reusable"] else 0 for p in pairs])
# ---------------------------------------------------------------- Step 1
def ask(pair, question=REUSE_QUESTION, post=jev.post):
"""Jev's probability that the pair's cached answer can be reused."""
# TODO (Step 1): ONE post({"model": jev.MODEL, "state": {...}, "questions": {...}})
# state: cached_question, cached_answer and new_question from the pair
# questions: {"reuse": {"type": "noul", "instructions": question}}
# return the noul probability
raise NotImplementedError("Step 1: write ask()")
def collect(pairs, question=REUSE_QUESTION, post=jev.post):
"""Probabilities for every pair, in the order of `pairs`, as a numpy array."""
# TODO (Step 1): ask() every pair through ThreadPoolExecutor(16), keep the order,
# return np.array(...) of the probabilities
raise NotImplementedError("Step 1: write collect()")
# ---------------------------------------------------------------- Step 2
def reliability(p, y, bins=5):
"""One row per equal-width bin: {"lo", "hi", "n", "mean_p", "rate"}.
The last bin includes 1.0. Empty bins have n 0 and None for mean_p and rate."""
raise NotImplementedError("reliability() arrives in Step 2")
def ece(p, y, bins=5):
"""Expected calibration error: the bins' gaps |mean_p - rate|, weighted by their share of pairs."""
raise NotImplementedError("ece() arrives in Step 2")
def brier(p, y):
"""Mean squared gap between probability and outcome."""
raise NotImplementedError("brier() arrives in Step 2")
# ---------------------------------------------------------------- Step 3
def logit(p):
p = np.clip(np.asarray(p, dtype=float), 1e-4, 1 - 1e-4)
return np.log(p / (1 - p))
def fit_platt(p, y):
"""Platt scaling: fit a logistic regression on logit(p). Returns (a, b)."""
raise NotImplementedError("fit_platt() arrives in Step 3")
def calibrate(p, a, b):
"""The recalibrated probabilities: sigmoid(a * logit(p) + b)."""
raise NotImplementedError("calibrate() arrives in Step 3")
# ---------------------------------------------------------------- Step 4
CANDIDATES = [round(i / 100, 2) for i in range(1, 100)]
def precision_recall(p, y, t):
"""(precision, recall) when every pair with p >= t is reused.
Precision is 1.0 when nothing is reused."""
raise NotImplementedError("precision_recall() arrives in Step 4")
def threshold_for_precision(p, y, target, min_hits=10):
"""The lowest candidate threshold that reuses at least min_hits pairs with
precision >= target, or None."""
raise NotImplementedError("threshold_for_precision() arrives in Step 4")
# ---------------------------------------------------------------- Step 5
def auc(p, y):
"""The chance that a random reusable pair scores above a random non-reusable one (ties count half)."""
raise NotImplementedError("auc() arrives in Step 5")
def win_rate(p_challenger, p_current, y, n_boot=1000, seed=0):
"""Share of bootstrap resamples where the challenger's AUC beats the current one's.
Both are scored on the SAME resample each time; resamples with one class are skipped."""
raise NotImplementedError("win_rate() arrives in Step 5")
def keep_or_switch(win, bar=0.95):
"""Switch wordings only when the challenger wins at least `bar` of the resamples."""
raise NotImplementedError("keep_or_switch() arrives in Step 5")provided for you:jev.pypairs.jsonltry_it.py