Сэмплирование: 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 всегда берёт ровно один токен
BTop-p берёт минимальный набор токенов до суммарной вероятности p, адаптируя размер набора
CTop-p не использует вероятности
DМежду ними нет разницы
4. Для точного, воспроизводимого ответа (например, кода) какую температуру выбрать?
AОчень высокую
BНизкую или нулевую
CТемпература ни на что не влияет
DРовно 2.0
Поддержать проект