Temperature, Top-K, Top-P: как управлять креативностью LLM

opensourceaillmsamplingdecodingit
← Back to Blog

Введение: почему 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 — ключ к качественной генерации текста.