Сэмплирование: greedy, temperature, top-k, top-p (запускаемый пример)

Модель выдаёт вероятности всех токенов — но печатает один. Этот урок про стратегии выбора: от детерминированного greedy до управляемой случайности.

Сэмплирование (sampling) — способ выбрать конкретный следующий токен из распределения вероятностей, которое выдала модель.

Greedy: всегда самый вероятный

Самая простая стратегия — greedy (жадный выбор): брать токен с максимальной вероятностью. Детерминированно (один и тот же вход → один и тот же выход) и безопасно, но скучно и склонно к зацикливанию: текст получается «средним», предсказуемым, иногда повторяющимся. Для творческих задач этого мало.

Temperature: ручка «креативности»

Температура — параметр, который «растягивает» или «сжимает» распределение перед выбором. Низкая (<1) делает распределение острее — модель почти всегда берёт топовый токен (ближе к greedy). Высокая (>1) сглаживает его — у редких токенов растут шансы, ответы разнообразнее, но рискованнее. Посмотрим эффект вживую.

import math, random

random.seed(42)

# Модель выдала логиты по 5 кандидатам следующего токена.
tokens = ["море", "небо", "кот", "стол", "квазар"]
logits = [3.0, 2.5, 1.0, 0.5, -1.0]

def softmax(xs):
    m = max(xs)
    e = [math.exp(x - m) for x in xs]
    s = sum(e)
    return [v / s for v in e]

def with_temperature(logits, t):
    return softmax([x / t for x in logits])

print("Влияние температуры на распределение:")
for t in (0.5, 1.0, 2.0):
    probs = with_temperature(logits, t)
    pretty = ", ".join(f"{tokens[i]}={probs[i]:.2f}" for i in range(len(tokens)))
    print(f"  T={t}: {pretty}")

print()
print("greedy (argmax) всегда выбирает:", tokens[logits.index(max(logits))])

# top-k=2 при T=1.0: оставляем 2 самых вероятных, остальное обнуляем
def sample_top_k(logits, k, t=1.0):
    probs = with_temperature(logits, t)
    idx = sorted(range(len(probs)), key=lambda i: probs[i], reverse=True)[:k]
    kept = {i: probs[i] for i in idx}
    z = sum(kept.values())
    r = random.random() * z
    acc = 0.0
    for i in idx:
        acc += kept[i]
        if r <= acc:
            return tokens[i]
    return tokens[idx[-1]]

print("\n10 сэмплов с top-k=2, T=1.0:")
counts = {}
for _ in range(10):
    tok = sample_top_k(logits, k=2, t=1.0)
    counts[tok] = counts.get(tok, 0) + 1
for tok, c in counts.items():
    print(f"  {tok}: {c}")

Вывод:

Влияние температуры на распределение:
  T=0.5: море=0.72, небо=0.26, кот=0.01, стол=0.00, квазар=0.00
  T=1.0: море=0.54, небо=0.33, кот=0.07, стол=0.04, квазар=0.01
  T=2.0: море=0.39, небо=0.30, кот=0.14, стол=0.11, квазар=0.05

greedy (argmax) всегда выбирает: море

10 сэмплов с top-k=2, T=1.0:
  небо: 4
  море: 6

Разберём вывод. При T=0.5 вероятность топового токена «море» подскочила до 0.72 — модель уверенно консервативна. При T=2.0 распределение сгладилось, и у «кота», «стола» и даже «квазара» появились реальные шансы — отсюда разнообразие, но и риск нелепостей. Greedy же всегда берёт «море». В конце видно сэмплирование с top-k=2: из 10 попыток выбираются только два самых вероятных токена («море» и «небо»), остальные исключены полностью.

Top-k: ограничить число кандидатов

Top-k оставляет только k самых вероятных токенов, остальным присваивает нулевую вероятность, и сэмплирует из них. Это отсекает совсем маловероятный «хвост» (откуда и берётся бессмыслица), сохраняя управляемое разнообразие. Минус: k фиксировано, хотя в одних ситуациях разумных вариантов два, а в других — двадцать.

Top-p (nucleus): адаптивный набор

Top-p (nucleus sampling) умнее: он берёт минимальный набор самых вероятных токенов, чья суммарная вероятность достигает порога p (например, 0.9). На уверенных шагах набор маленький, на размытых — больше. Посмотрим на адаптивность.

import math

tokens = ["кофе", "чай", "сок", "вода", "компот", "кефир", "морс"]
logits = [3.0, 2.4, 1.5, 1.0, 0.3, -0.5, -1.2]

def softmax(xs):
    m = max(xs); e = [math.exp(x - m) for x in xs]; s = sum(e)
    return [v / s for v in e]

probs = softmax(logits)
order = sorted(range(len(probs)), key=lambda i: probs[i], reverse=True)

def nucleus(p_threshold):
    kept, acc = [], 0.0
    for i in order:
        kept.append(i)
        acc += probs[i]
        if acc >= p_threshold:
            break
    return kept

for p in (0.5, 0.9):
    kept = nucleus(p)
    names = ", ".join(tokens[i] for i in kept)
    print(f"top-p={p}: оставляем {{ {names} }} (накопили вероятность >= {p})")
print()
print("top-p берёт минимальный набор токенов, чья суммарная вероятность >= p.")
print("На уверенных шагах набор маленький, на размытых — больше. Это адаптивно.")

Вывод:

top-p=0.5: оставляем { кофе, чай } (накопили вероятность >= 0.5)
top-p=0.9: оставляем { кофе, чай, сок, вода } (накопили вероятность >= 0.9)

top-p берёт минимальный набор токенов, чья суммарная вероятность >= p.
На уверенных шагах набор маленький, на размытых — больше. Это адаптивно.

Видно главное преимущество: размер набора подстраивается под ситуацию. Поэтому top-p (часто в паре с умеренной температурой) — популярный выбор по умолчанию.

Памятка по параметрам

СтратегияСутьКогда
Greedyвсегда максимумдетерминированные задачи, код, факты
Temperatureострее/мягче распределениеручка «креативности»
Top-kk лучших кандидатовотсечь маловероятный хвост
Top-pнабор до суммарной вероятности pадаптивное разнообразие по умолчанию

Практическое правило: нужен точный, воспроизводимый ответ (код, извлечение фактов) — ставьте температуру низкой/нулевой. Нужен творческий, разнообразный текст — поднимайте температуру и используйте top-p.

Итог

  • Greedy всегда берёт самый вероятный токен — детерминированно, но однообразно.
  • Температура управляет остротой распределения: ниже — консервативнее, выше — разнообразнее.
  • Top-k оставляет k лучших кандидатов; top-p — минимальный набор до суммарной вероятности p (адаптивно).
  • Низкая температура — для точных задач, высокая + top-p — для творческих.
Проверьте себя
1. Что делает greedy-стратегия выбора токена?
AБерёт случайный токен
BБерёт самый редкий токен
CВсегда выбирает токен с максимальной вероятностью
DУсредняет все токены
2. Как высокая температура (>1) влияет на генерацию?
AДелает выбор строго детерминированным
BПолностью отключает сэмплирование
CУменьшает словарь
DСглаживает распределение: растут шансы менее вероятных токенов, текст разнообразнее, но рискованнее
3. Чем top-p (nucleus) отличается от top-k?
ATop-p берёт минимальный набор токенов до суммарной вероятности p, адаптируя размер набора
BTop-p всегда берёт ровно один токен
CTop-p не использует вероятности
DМежду ними нет разницы
4. Для точного, воспроизводимого ответа (например, кода) какую температуру выбрать?
AОчень высокую
BНизкую или нулевую
CТемпература ни на что не влияет
DРовно 2.0