Temperature, Top-K, Top-P: как управлять креативностью LLM
opensourceaillmsamplingdecodingit
Введение: почему LLM не просто выбирает максимальную вероятность?
Когда LLM генерирует текст, на каждом шаге она выдаёт распределение вероятностей по всему словарю (50K+ токенов). Простейший подход — всегда брать токен с максимальной вероятностью (Greedy Search). Но это приводит к проблемам:
Greedy Search (temperature=0):
Prompt: "Столица Франции —"
Результат: "Париж, город, который является"
Проблема: детерминированно, повторяемо, скучно.
Два запуска → абсолютно одинаковый ответ.
Но если мы хотим креативности — нам нужен sampling. И вот здесь в игру входят temperature, Top-K и Top-P.
Temperature: мягкое управление уверенностью
Основная идея
Temperature (T) — это параметр, который "размягчает" или "затачивает" распределение вероятностей.
Распределение LLM (top-5 токенов):
"Париж" → 0.45
"город" → 0.20
"столица" → 0.15
"Франция" → 0.10
"—" → 0.05
Temperature T=1.0 (оригинальное):
P = softmax(logits) → [0.45, 0.20, 0.15, 0.10, 0.05]
Temperature T=0.5 (затачиваем):
P = softmax(logits/0.5) → [0.72, 0.12, 0.06, 0.04, 0.02]
→ Модель становится более уверенной
Temperature T=2.0 (размягчаем):
P = softmax(logits/2.0) → [0.25, 0.20, 0.18, 0.17, 0.15]
→ Модель становится более случайной
Формула
import torch
import torch.nn.functional as F
def apply_temperature(logits, temperature):
"""
Применяем temperature к логитам модели.
logits: сырые выходы модели (до softmax)
temperature: T > 0
scaled_logits = logits / T
T < 1 → более уверенное распределение (меньше случайности)
T = 1 → оригинальное распределение
T > 1 → более равномерное распределение (больше случайности)
T → 0 → почти greedy (берём argmax)
T → ∞ → равномерное распределение (полный рандом)
"""
scaled_logits = logits / temperature
probabilities = F.softmax(scaled_logits, dim=-1)
return probabilities
Визуализация
Temperature и выбор токенов:
Токен | Logit | T=0.1 | T=0.5 | T=1.0 | T=2.0 | T=∞
------------|-------|-------|-------|-------|-------|-----
"Париж" | 4.2 | 0.999 | 0.72 | 0.45 | 0.25 | 0.20
"город" | 2.8 | 0.001 | 0.12 | 0.20 | 0.20 | 0.20
"столица" | 2.1 | 0.000 | 0.06 | 0.15 | 0.18 | 0.20
"Франция" | 1.5 | 0.000 | 0.04 | 0.10 | 0.17 | 0.20
"—" | 0.8 | 0.000 | 0.02 | 0.05 | 0.15 | 0.20
T=0.1: почти всегда "Париж" (0.999)
T=2.0: почти равномерный выбор (0.15-0.25)
Практическое использование
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("mistralai/Mixtral-8x7B-Instruct-v0.1")
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mixtral-8x7B-Instruct-v0.1")
inputs = tokenizer("Столица Франции —", return_tensors="pt")
# Низкая температура — детерминированно, уверенно
result = model.generate(
**inputs,
max_length=128,
temperature=0.2 # для технической документации, фактов
)
# Средняя температура — баланс
result = model.generate(
**inputs,
max_length=128,
temperature=0.7 # для общего использования
)
# Высокая температура — креативно
result = model.generate(
**inputs,
max_length=128,
temperature=1.5 # для творчества, brainstorming
)
Top-K: ограничиваем выбор K лучшими токенами
Проблема temperature
При T=2.0:
"Париж" → 0.25
"город" → 0.20
"столица" → 0.18
"Франция" → 0.17
"—" → 0.15
"банан" → 0.001
"квантовый"→ 0.0001
...
"абракадабра" → 0.0000001
При T=2.0 даже "банан" имеет шанс ~0.15!
Это слишком много случайности.
Решение: Top-K
import torch
import torch.nn.functional as F
def top_k_sampling(logits, k=50):
"""
Оставляем только K самых вероятных токенов,
остальные обнуляем.
k: число токенов для рассмотрения
k=1 → greedy (argmax)
k=10 → очень ограниченный выбор
k=50 → стандартное значение
k=∞ → без ограничений (только temperature)
"""
# Получаем индексы топ-K токенов
top_k_indices = torch.topk(logits, k)
# Создаём маску: только топ-K токены
mask = torch.zeros_like(logits)
mask.scatter_(1, top_k_indices.indices, 1)
# Обнуляем всё кроме топ-K
filtered_logits = logits * mask
# Применяем temperature и softmax
probabilities = F.softmax(filtered_logits / 0.7, dim=-1)
# Сэмплируем
next_token = torch.multinomial(probabilities, num_samples=1)
return next_token
Как работает Top-K
Original distribution (top-20 из 50K):
["Париж", "город", "столица", "Франция", "—", "является", ...]
Top-K=10:
Оставляем топ-10, остальные → 0
["Париж", "город", "столица", "Франция", "—", "является",
"находится", "который", "в", "департамент"]
Остальные 49,990 токенов → вероятность 0
Затем нормализуем и сэмплируем из этих 10
Выбор K
K=1: Greedy search (нет случайности)
K=3: Очень консервативный
K=10: Консервативный, для фактов
K=40: Стандартный, баланс
K=50: Рекомендуемый по умолчанию (Hugging Face)
K=100: Более креативный
K=∞: Только temperature (без Top-K)
Top-P (Nucleus Sampling): ограничиваем по накопленной вероятности
Проблема Top-K
Top-K=50:
Проблема: не всегда 50 токенов "хорошие".
Ситуация 1 (уверенная модель):
Топ-1: "Париж" → 0.80
Топ-2: "город" → 0.05
Топ-3: "столица" → 0.03
...
Топ-50: "абракадабра" → 0.0001
Из 50 токенов только 5 имеют смысл!
Top-K=50 включает мусор.
Ситуация 2 (неуверенная модель):
Топ-1: "Париж" → 0.15
Топ-2: "город" → 0.12
...
Топ-50: "банан" → 0.02
Все 50 токенов примерно равновероятны.
Top-K=50 — ОК.
Решение: Top-P (Nucleus Sampling)
def top_p_sampling(logits, p=0.9):
"""
Nucleus Sampling: выбираем токены пока накопленная
вероятность не достигнет p.
p: порог накопленной вероятности
p=0.1 → только 10% вероятности (1-2 токена)
p=0.5 → топ-50% вероятности
p=0.9 → стандартное значение
p=1.0 → без ограничений (только temperature)
"""
# Сортируем по убыванию вероятности
probabilities, indices = torch.sort(F.softmax(logits, dim=-1), descending=True)
# Накапливаем вероятность
cumulative_probs = torch.cumsum(probabilities, dim=-1)
# Находим индекс, где накопленная вероятность >= p
mask = cumulative_probs <= p
# Убираем первый токен (он всегда > 0)
mask[:, 0] = True
# Обнуляем токены за порогом p
filtered_logits = logits * mask.float()
# Нормализуем
filtered_probs = F.softmax(filtered_logits, dim=-1)
# Сэмплируем
next_token = torch.multinomial(filtered_probs, num_samples=1)
return next_token
Визуализация Top-P
Ситуация 1 (уверенная модель):
Probabilities: [0.80, 0.05, 0.03, 0.02, 0.01, 0.01, 0.01, ...]
Cumulative: [0.80, 0.85, 0.88, 0.90, 0.91, 0.92, 0.93, ...]
Top-P=0.9:
Оставляем токены с накопленной вероятностью ≤ 0.9
→ ["Париж", "город", "столица", "Франция"] (4 токена)
Top-P=0.95:
→ ["Париж", "город", "столица", "Франция", "—", "является"] (6 токенов)
Ситуация 2 (неуверенная модель):
Probabilities: [0.15, 0.12, 0.11, 0.10, 0.09, 0.08, 0.07, 0.06, ...]
Cumulative: [0.15, 0.27, 0.38, 0.48, 0.57, 0.65, 0.72, 0.78, ...]
Top-P=0.9:
→ первые ~12 токенов (все имеют смысл)
Top-P=0.95:
→ первые ~18 токенов
Комбинация: Temperature + Top-K + Top-P
Hugging Face default
# Hugging Face Transformers default:
model.generate(
**inputs,
temperature=1.0,
top_k=50,
top_p=0.95,
max_length=128
)
Как это работает вместе
def combined_sampling(logits, temperature=1.0, top_k=50, top_p=0.95):
"""
Полный pipeline sampling с temperature + Top-K + Top-P.
Порядок применения:
1. Temperature: размягчаем/затачиваем logits
2. Top-K: отбрасываем всё кроме K лучших
3. Top-P: отбрасываем nucleus с накопленной вероятностью > p
4. Сэмплируем из оставшихся
"""
# Шаг 1: Temperature
scaled_logits = logits / temperature
# Шаг 2: Top-K
if top_k < len(logits):
top_k_values, top_k_indices = torch.topk(scaled_logits, top_k)
mask = torch.zeros_like(scaled_logits)
mask.scatter_(1, top_k_indices, 1)
filtered_logits = scaled_logits * mask
else:
filtered_logits = scaled_logits
# Шаг 3: Top-P
probabilities = F.softmax(filtered_logits, dim=-1)
sorted_probs, sorted_indices = torch.sort(probabilities, descending=True)
cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
# Убираем первый токен
mask = cumulative_probs < top_p
mask[:, 0] = True
# Применяем маску
filtered_probs = probabilities * mask.float()
filtered_probs = filtered_probs / filtered_probs.sum(dim=-1, keepdim=True)
# Шаг 4: Сэмплируем
next_token = torch.multinomial(filtered_probs, num_samples=1)
return next_token
Практические рекомендации
Когда использовать какие параметры
Сценарий | T | Top-K | Top-P
----------------------------|------|-------|-------
Факты, код, математика | 0.1 | 10 | 0.9
Техническая документация | 0.3 | 20 | 0.95
Общий чат | 0.7 | 50 | 0.95
Креативное письмо | 1.0 | 70 | 0.95
Brainstorming | 1.3 | 100 | 0.98
Перевод | 0.5 | 40 | 0.9
Генерация кода | 0.2 | 20 | 0.95
Розыгрыш / юмор | 1.2 | 80 | 0.97
Примеры генерации
# Генерация кода — низкая температура
code = model.generate(
**inputs,
temperature=0.2,
top_k=20,
top_p=0.9,
max_length=256
)
# Генерация стихов — высокая температура
poetry = model.generate(
**inputs,
temperature=1.3,
top_k=100,
top_p=0.97,
max_length=128
)
# Ответ на вопрос — средняя температура
qa = model.generate(
**inputs,
temperature=0.7,
top_k=50,
top_p=0.95,
max_length=512
)
Визуализация: как параметры влияют на текст
Prompt: "Напиши историю про кота"
T=0.1, K=10, P=0.9:
"Кот сидел на окне. Он смотрел на улицу.
За окном шёл дождь. Кот был один."
→ Предсказуемо, консервативно, скучно.
T=0.7, K=50, P=0.95:
"Кот по кличке Барсик сидел на подоконнике
третьего этажа. Его зелёные глаза отражали
мерцание городских огней."
→ Сбалансированно, естественно.
T=1.5, K=100, P=0.98:
"Кот-космонавт Барсик-6 смотрел на Землю
из иллюминатора МКС. 'На орбите дождя нет',
подумал он, протирая лапой стекло."
→ Креативно, неожиданно, иногда странно.
Продвинутые техники
Repetition Penalty
# Штраф за повторение токенов
model.generate(
**inputs,
temperature=0.7,
repetition_penalty=1.1, # >1 штрафует повторения
max_length=128
)
# repetition_penalty:
# < 1: поощряет повторения (редко нужно)
# = 1: без эффекта
# > 1: штрафует повторения (1.05-1.2 — стандарт)
Presence & Frequency Penalty (OpenAI style)
# OpenAI API
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[...],
temperature=0.7,
presence_penalty=0.0, # штраф за уже появившиеся токены
frequency_penalty=0.0 # штраф за часто повторяющиеся токены
)
# presence_penalty:
# < 0: поощряет новые токены
# = 0: без эффекта
# > 0: штрафует уже появившиеся токены
# frequency_penalty:
# < 0: поощряет частые токены
# = 0: без эффекта
# > 0: штрафует частые токены
Contrastive Decoding
# Усиливаем "хорошие" токены, ослабляем "плохие"
# Используем strong и weak модели
strong_logits = strong_model(context)
weak_logits = weak_model(context)
# Contrastive scaling
contrastive_logits = strong_logits - weak_logits * alpha
# alpha: сила контраста
# alpha=0: только strong модель
# alpha=1: равный вес
# alpha>1: сильный контраст
Итоги
-
Temperature контролирует "размягчение" распределения
- T < 1: более уверенный выбор (меньше случайности)
- T = 1: оригинальное распределение
- T > 1: более случайный выбор
-
Top-K ограничивает выбор K лучшими токенами
- K=1: greedy
- K=50: стандарт
- K=∞: без ограничений
-
Top-P (Nucleus Sampling) ограничивает по накопленной вероятности
- P=0.9: 90% вероятности
- P=0.95: стандарт
- P=1.0: без ограничений
-
Комбинация Temperature + Top-K + Top-P даёт лучший контроль
- Hugging Face default: T=1.0, K=50, P=0.95
-
Практический совет:
- Для кода/фактов: низкая T (0.1-0.3), низкий K (10-20)
- Для чата: средняя T (0.7), K=50, P=0.95
- Для творчества: высокая T (1.0-1.5), высокий K (70-100)
Правильный подбор параметров sampling — ключ к качественной генерации текста.