Сэмплирование: 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-k | k лучших кандидатов | отсечь маловероятный хвост |
| Top-p | набор до суммарной вероятности p | адаптивное разнообразие по умолчанию |
Практическое правило: нужен точный, воспроизводимый ответ (код, извлечение фактов) — ставьте температуру низкой/нулевой. Нужен творческий, разнообразный текст — поднимайте температуру и используйте top-p.
Итог
- Greedy всегда берёт самый вероятный токен — детерминированно, но однообразно.
- Температура управляет остротой распределения: ниже — консервативнее, выше — разнообразнее.
- Top-k оставляет k лучших кандидатов; top-p — минимальный набор до суммарной вероятности p (адаптивно).
- Низкая температура — для точных задач, высокая + top-p — для творческих.