返回 Expert 笔记
Expert Day 124

Sampling策略——Temperature/Top-p/Top-k与Speculative Decoding

Greedy/Beam/Temperature/Top-k/Top-p/Min-p、Speculative decoding、Contrastive search

2026-09-02
Phase 3 - LLM基础与Prompt工程 (Day 121-134)
SamplingTemperatureTopPSpeculativeDecodingLLM

日期: 2026-09-02 方向: AI系统工程 阶段: Phase 3 - LLM基础与Prompt工程 (Day 121-134) 标签: #Sampling #Temperature #TopP #SpeculativeDecoding #LLM


今日目标

类型内容
学习Greedy/Beam/Temperature/Top-k/Top-p/Min-p、Speculative decoding、Contrastive search
实操对同一prompt用5种sampling策略调Claude/GPT,对比创意度、稳定性、长度、cost
产出实验报告 + 不同任务推荐sampling参数表

一、理论基础

1.1 LM输出是概率分布

每步model输出logits $\mathbf{z} \in \mathbb{R}^{|V|}$(|V|=vocab size),经softmax变概率:

$$ p_i = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)} $$

$T$是temperature

  • $T \to 0$:分布退化为one-hot(greedy)
  • $T = 1$:原分布
  • $T > 1$:分布更平坦(更随机)
  • $T \to \infty$:均匀分布(纯随机)

1.2 主要sampling方法

Greedy decoding

$\arg\max_i p_i$。确定性,但容易重复:"The cat sat on the mat. The cat sat on the mat..."

保留top-k个候选序列。机器翻译/摘要常用;creative writing差(输出统一无趣)。

Top-k sampling

从概率最高的k个token里按概率采样。Hard cutoff,k固定可能太严或太松。

Top-p (nucleus) sampling — Holtzman et al. 2020

按概率从大到小累加直到≥p,从这个集合采样。自适应:尖锐分布只考虑1-2个,平缓分布考虑很多。

$$ V_p = \min{V' \subset V : \sum_{i \in V'} p_i \geq p} $$

Min-p sampling — Nguyen et al. 2024

保留$p_i \geq p_{base} \cdot \max_j p_j$的token。比top-p更"尊重"分布形状,high-temperature下不会让低质token突然进入。

Repetition penalty

对已出现token降权:$p_i \leftarrow p_i / \alpha$ if $i \in \text{generated}$。容易over-penalize正常重复("the"会被打压)。

1.3 Speculative Decoding(推理加速)

朴素:每生成1个token要跑一次forward pass —— GPU利用率低(memory-bound)。

Speculative decoding (Leviathan 2023):

  1. Draft model(小、快)连续生成N个token
  2. Target model(大)一次forward验证这N个token
  3. 接受最长前缀;从第一个不接受位置改用target采样

加速比:典型2-4x。Anthropic、OpenAI都在用。

数学:reject的概率 $1 - \min(1, p_{target}/p_{draft})$ 保证最终分布严格等同target distribution。

抑制degeneration:每步选择 $\arg\max_i [(1-\alpha) p_i - \alpha \cdot \max_{x_j \in \text{context}} \cos(h_i, h_j)]$,惩罚和上文hidden state太相似的token。


二、直觉解释

Temperature的本质

T控制模型的"自信度"。低T = "我很确定";高T = "我不确定,谁都可能"。但不影响模型本身知识,只影响表达方式。

为什么creative任务用T=0.7-1.0,code/structured用T=0?

  • 创作:希望多样性,T高让模型探索"次优但有趣"的token
  • Code:希望确定性,T=0让模型走最高概率路径,bug少
  • Structured output:T=0避免JSON格式偏离

为什么top-p比top-k好?

考虑两个场景:

  • "I love" → 后接token分布很集中("you", "it", "this"占80%):top-p=0.9只考虑3-4个,top-k=50会让低质token也进来
  • "Once upon a time, " → 分布很广:top-p=0.9考虑200个;top-k=50太严

top-p自适应分布形状,top-k一刀切


三、代码实现

3.1 完整实验:5种sampling对比

# sampling_compare.py
"""
对同一prompt用不同sampling参数调Anthropic API,
量化输出多样性、长度、相关性。
"""
import anthropic
from collections import Counter

client = anthropic.Anthropic()

PROMPT_CREATIVE = "Write a one-sentence story about a robot who discovers emotions."
PROMPT_FACT = "What is the capital of France? Answer with one word."
PROMPT_CODE = "Write a Python function to compute factorial. Use recursion."

CONFIGS = [
    ("greedy",     {"temperature": 0.0, "top_p": 1.0}),
    ("conservative", {"temperature": 0.3, "top_p": 0.9}),
    ("balanced",   {"temperature": 0.7, "top_p": 0.95}),
    ("creative",   {"temperature": 1.0, "top_p": 0.95}),
    ("wild",       {"temperature": 1.2, "top_p": 1.0}),
]

def sample_n(prompt, config, n=5, max_tokens=200):
    outputs = []
    for _ in range(n):
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=max_tokens,
            temperature=config["temperature"],
            top_p=config["top_p"],
            messages=[{"role": "user", "content": prompt}]
        )
        outputs.append(resp.content[0].text)
    return outputs

def diversity(outputs):
    """简单多样性指标:unique outputs / total"""
    return len(set(outputs)) / len(outputs)

def avg_length(outputs):
    return sum(len(o) for o in outputs) / len(outputs)

def run_experiment(prompt_name, prompt, n=5):
    print(f"\n=== {prompt_name} ===")
    print(f"Prompt: {prompt[:80]}...")
    for label, config in CONFIGS:
        outs = sample_n(prompt, config, n=n)
        d = diversity(outs)
        l = avg_length(outs)
        print(f"  {label:<14} T={config['temperature']:.1f} top_p={config['top_p']:.2f} "
              f"diversity={d:.0%}  avg_len={l:.0f}  example={outs[0][:60]!r}")

if __name__ == "__main__":
    run_experiment("Creative", PROMPT_CREATIVE)
    run_experiment("Factual", PROMPT_FACT)
    run_experiment("Code", PROMPT_CODE)

预期典型输出:

=== Creative ===
  greedy         T=0.0 top_p=1.00 diversity=20%  avg_len=92  example='In the dim glow of the server room...'
  conservative   T=0.3 top_p=0.90 diversity=80%  avg_len=98
  balanced       T=0.7 top_p=0.95 diversity=100% avg_len=105
  creative       T=1.0 top_p=0.95 diversity=100% avg_len=110
  wild           T=1.2 top_p=1.00 diversity=100% avg_len=130

=== Factual ===
  greedy         diversity=20%  avg_len=5   "Paris."
  balanced       diversity=20%  avg_len=5   (well-known fact不受T影响)
  wild           diversity=40%  avg_len=8   ("Paris.", "Paris" with explanation)

=== Code ===
  greedy         diversity=20%  always correct, identical output
  creative       diversity=100% sometimes引入bugs或alternative styles

洞察

  • 高T在"已确定"问题上没帮助(factual recall)
  • 高T在creative任务上明显多样性提升
  • 高T在code任务上反而降低正确率——这是反直觉但真实的trade-off

3.2 实现一个简易sampling库

# my_sampling.py
"""
教学用:用numpy实现各种sampling
(实际生产用vLLM/SGLang)
"""
import numpy as np

def softmax(z):
    z = z - z.max()
    e = np.exp(z)
    return e / e.sum()

def greedy(logits):
    return int(np.argmax(logits))

def temperature_sample(logits, T=1.0):
    if T == 0:
        return greedy(logits)
    probs = softmax(logits / T)
    return int(np.random.choice(len(probs), p=probs))

def top_k_sample(logits, k=50, T=1.0):
    top_idx = np.argpartition(logits, -k)[-k:]
    masked = np.full_like(logits, -np.inf)
    masked[top_idx] = logits[top_idx]
    return temperature_sample(masked, T)

def top_p_sample(logits, p=0.9, T=1.0):
    probs = softmax(logits / T)
    sorted_idx = np.argsort(probs)[::-1]
    cumsum = np.cumsum(probs[sorted_idx])
    cutoff = np.searchsorted(cumsum, p) + 1
    keep = sorted_idx[:cutoff]
    masked = np.full_like(logits, -np.inf)
    masked[keep] = logits[keep]
    return temperature_sample(masked, T)

def min_p_sample(logits, p_base=0.05, T=1.0):
    probs = softmax(logits / T)
    threshold = p_base * probs.max()
    keep = probs >= threshold
    masked = np.where(keep, logits, -np.inf)
    return temperature_sample(masked, T)

# Demo: 同一logits不同sampling 1000次的分布
logits = np.array([5.0, 4.5, 3.0, 2.5, 1.0, 0.5, -1.0, -2.0])
labels = list("ABCDEFGH")

for name, fn in [
    ("greedy",   lambda l: greedy(l)),
    ("T=0.7",    lambda l: temperature_sample(l, 0.7)),
    ("top-k=3",  lambda l: top_k_sample(l, 3, 0.7)),
    ("top-p=0.9",lambda l: top_p_sample(l, 0.9, 0.7)),
    ("min-p=0.1",lambda l: min_p_sample(l, 0.1, 0.7)),
]:
    counts = np.bincount([fn(logits) for _ in range(2000)], minlength=8)
    print(f"{name:<12}: {dict(zip(labels, counts))}")

输出(典型):

greedy      : {'A': 2000, ...}
T=0.7       : {'A': 1100, 'B': 600, 'C': 200, 'D': 80, 'E': 15, ...}
top-k=3     : {'A': 1200, 'B': 600, 'C': 200, ...}
top-p=0.9   : {'A': 1100, 'B': 600, 'C': 200, 'D': 80, ...}
min-p=0.1   : {'A': 1500, 'B': 400, 'C': 100, ...}

3.3 Speculative Decoding 直观演示

# speculative_demo.py
"""
教学:speculative decoding加速原理
不真跑model,用伪概率说明算法
"""
import numpy as np
np.random.seed(0)

def draft_propose(n_tokens=4):
    """draft model快速生成n个token"""
    # 假装每个token返回 (token_id, prob)
    return [(np.random.randint(0, 100), np.random.uniform(0.3, 0.9)) for _ in range(n_tokens)]

def target_verify(draft_tokens):
    """target model给每个token一个真实prob"""
    return [np.random.uniform(0.2, 0.95) for _ in draft_tokens]

def speculative_step():
    proposals = draft_propose(n_tokens=4)
    target_probs = target_verify(proposals)

    accepted = []
    for (tok, p_d), p_t in zip(proposals, target_probs):
        # accept概率 = min(1, p_target / p_draft)
        accept_p = min(1.0, p_t / p_d)
        if np.random.random() < accept_p:
            accepted.append(tok)
        else:
            # 第一个reject位置:用调整分布从target采新token
            break
    return accepted

# 1000个step统计
total_accepted = sum(len(speculative_step()) for _ in range(1000))
print(f"Average accepted tokens per step: {total_accepted / 1000:.2f}")
# 期望2-3,意味着用~1次target call生成2-3个token,2-3x加速

四、Anthropic API最佳实践

4.1 参数设置

Anthropic Messages API支持:

client.messages.create(
    model="claude-opus-4-7",
    temperature=0.7,        # 0-1(不像OpenAI支持>1)
    top_p=0.95,            # 默认通常不指定
    top_k=None,            # API也支持,但很少用
    max_tokens=4096,
    messages=[...]
)

Anthropic特殊:T最大1.0(OpenAI能到2.0)。如果想"更野",建议改prompt(如"be unconventional")而非T>1。

4.2 任务-参数映射推荐表

任务类型TemperatureTop-p备注
Structured output (JSON)0.0-用structured output API更佳
Code generation0.0-0.20.95低T避免bug
Factual Q&A0.0-0.3-准确率优先
Summarization0.3-0.50.95一定多样性
Brainstorming0.8-1.00.95创意优先
Creative writing0.7-1.00.95平衡
Translation0.0-0.30.9准确性
Conversation0.5-0.70.95自然度
Reasoning (extended thinking on)1.0 (forced by API)-thinking模式T固定1.0

4.3 Extended Thinking + Sampling

⚠️注意:当thinking={"type": "enabled"}时,Anthropic 强制 temperature=1.0、top_p=1.0(避免破坏thinking trace)。如果你需要确定性思考,目前只能不开thinking。


五、金融领域应用

案例:量化策略生成 vs 风险报告

任务T推荐原因
生成量化策略代码0.0避免引入bug;可精确复现
生成市场情绪分析(风格化)0.7多样表达,避免模板化
提取财报关键数字0.0确定性,可audit
写客户邮件0.5自然但稳定
Trading idea brainstorm0.9探索非常规思路

实际代码:合规审查(必须T=0)

def compliance_check(transaction_text):
    """
    监管审查必须确定性 — 同一输入永远同一输出,便于audit。
    """
    resp = client.messages.create(
        model="claude-opus-4-7",
        temperature=0.0,      # 关键
        max_tokens=2048,
        system=COMPLIANCE_RULES_SYSTEM,
        messages=[{"role": "user", "content": transaction_text}]
    )
    return resp.content[0].text

审计要点:日志record temperature, top_p, model version。同一code+model+T=0输出有确定性(虽然不100%,因infrastructure-level non-determinism)。生产建议:缓存response by hash(input + params)。


六、常见陷阱

  1. 以为高T等于更聪明:不会。T只改变多样性,不改变知识。难题低T可能更准确。
  2. structured output忘记T=0:高T导致JSON里key顺序变、value幻觉化。JSON必须T=0
  3. temperature=0就是确定:错。GPU non-determinism (parallel reduction order, seed) 让"T=0"也偶尔不一样。要严格确定性需要seed参数(vLLM/SGLang支持,OpenAI部分支持)。
  4. top_p=0.9一刀切所有任务:factual T=0 + top_p无所谓;creative T=0.8 + top_p=0.95;code T=0即可。无脑设0.9是反优化。
  5. forgot Speculative decoding 不改变distribution:有人以为spec decoding"会变笨"——错,rejection sampling数学保证最终分布严格等同target。

七、关键速查

Anthropic API sampling参数

{
    "temperature": 0.0-1.0,       # 默认1.0
    "top_p":       0.0-1.0,       # 默认1.0
    "top_k":       int (optional),
    "max_tokens":  int (required),
    "stop_sequences": ["..."],    # 自定义停止token
}

Trade-off速查

Greedy:  最快 + 最确定 + 最容易repetition
Top-k:   简单 + 缺乏自适应
Top-p:   主流推荐 + 自适应分布形状
Min-p:   高T场景下比top-p好
Beam:    翻译/摘要好,对话差

Speculative Decoding理论

加速比 ≈ E[accepted tokens per draft]
典型值:2-4x
draft model质量越接近target,加速越大

八、面试题

Q1: Temperature=0是否就完全确定?

算法层面是greedy,理论确定。但实际不100%——浮点累加顺序受GPU并行影响,相同logits可能极小差异翻转argmax。需要seed + deterministic kernels才严格可复现。生产监控里要记录model version + 完整params hash。

Q2: 为什么structured output (JSON) 推荐T=0?

JSON schema有严格语法约束。T>0可能在key list、bracket close位置选错token,破坏可解析性。Anthropic的structured output API底层用constrained decoding强制schema,但仍建议T=0最大稳定。

Q3: Speculative decoding对用户有什么trade-off?

没有质量trade-off(数学保证output distribution相同)。trade-off在infra:(a) 需要部署额外draft model消耗显存;(b) 当draft与target divergent时(如rare phrase),加速比下降甚至变慢;(c) batch大时benefit递减(因为本身就GPU满载)。

Q4: Anthropic API为什么extended thinking强制T=1?

Thinking是模型"探索性思考"的过程,需要分布上的探索而不是greedy。T=0会让thinking trace overly deterministic,丧失self-correction能力。设计上Anthropic锁定T=1保证thinking质量;最终answer的稳定性靠thinking后的converge机制。


九、明日预告

Day 125: Prompt基础模式 — Zero-shot/Few-shot/CoT/ToT/Self-consistency,实测5种模式对比。