Experiment4 Sampling

实验4:采样策略

🎯 为什么要学(痛点驱动)

现实困惑:
1. 我调用model.generate()两次,相同输入每次输出都不同
   问题:如何让它稳定?或如何让它更有创意?

2. 我看到temperature、top_k、top_p参数
   不知道为什么调这些参数能控制输出
   问题:它们到底改变了什么?

3. 我生成文本时出现重复
   模型一直在说同样的话
   问题:如何避免?

学习目标:理解"模型如何选择下一个token"

🔬 方法论4步

1. 定层级

Input(上层):
  - 代码:model.generate(text, temperature=0.7, top_p=0.9)
  - 期望:控制多样性和质量

Target(底层):
  - Logits:[vocab_size] 原始分数
  - Probabilities:Softmax归一化后的概率分布
  - Sampling:从分布中随机抽取

2. 找关卡

关卡1:Logits → Probabilities
  数据突变:原始分数 → 概率分布(和为1)
  特点:Softmax归一化

关卡2:Temperature调整
  数据突变:logits / temperature → 重新softmax
  特点:T<1更尖锐,T>1更平滑

关卡3:采样决策
  数据突变:概率分布 → 单个token ID
  特点:随机采样(multinomial)

3. 架工具

观测工具:
  - outputs.logits[0, -1, :]  # 最后一个位置的logits
  - F.softmax(logits, dim=-1) # 转为概率
  - torch.multinomial(probs, 1) # 采样
  - torch.argmax(probs)        # Greedy

观测点:
  - Top 10的概率分布
  - Temperature对分布的影响
  - 不同采样策略的结果差异

4. 投示踪

探针代码

import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer

# 探针1: 使用小模型(GPT-2)
model_name = "gpt2"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 探针2: 简单的prompt(便于分析)
tracer_text = "The meaning of life is"

print(f"\n[实验设置]")
print(f"输入文本:\"{tracer_text}\"")

# ============================================================
# 步骤1:获取下一个token的logits
# ============================================================
inputs = tokenizer(tracer_text, return_tensors="pt")

with torch.no_grad():
    outputs = model(**inputs)
    logits = outputs.logits[0, -1, :]  # 最后一个位置的logits

print(f"Logits shape: {logits.shape}")  # [50257]

# 找出top 10
top_values, top_indices = torch.topk(logits, 10)
print(f"\nTop 10 Logits(原始分数):")
for i, (idx, val) in enumerate(zip(top_indices, top_values)):
    token = tokenizer.decode([idx])
    print(f"  {i+1}. {token:<20} logit={val.item():>8.3f}")

# ============================================================
# 步骤2:Softmax转为概率分布
# ============================================================
probs = F.softmax(logits, dim=-1)

print(f"\nTop 10 概率:")
top_probs, top_indices = torch.topk(probs, 10)
for i, (idx, prob) in enumerate(zip(top_indices, top_probs)):
    token = tokenizer.decode([idx])
    print(f"  {i+1}. {token:<20} prob={prob.item():>.6f}")

# ============================================================
# 步骤3:对比不同采样策略
# ============================================================
def greedy_sample(probs):
    """贪婪采样:选择概率最大的"""
    return torch.argmax(probs).item()

def temperature_sample(probs, temperature=1.0):
    """温度采样:调整概率分布的平滑度"""
    scaled_logits = torch.log(probs) / temperature
    scaled_probs = F.softmax(scaled_logits, dim=-1)
    return torch.multinomial(scaled_probs, 1).item()

def top_k_sample(probs, k=50):
    """Top-K采样:只从前k个候选中采样"""
    top_k_probs, top_k_indices = torch.topk(probs, k)
    top_k_probs = top_k_probs / top_k_probs.sum()
    sampled_idx = torch.multinomial(top_k_probs, 1).item()
    return top_k_indices[sampled_idx].item()

# ============================================================
# 步骤4:多次采样观察分布
# ============================================================
import random
random.seed(42)
torch.manual_seed(42)

print(f"\n[多次采样观察(各10次)]")
strategies = [
    ("Greedy", lambda: greedy_sample(probs)),
    ("Temperature=0.7", lambda: temperature_sample(probs, 0.7)),
    ("Temperature=1.5", lambda: temperature_sample(probs, 1.5)),
    ("Top-K=50", lambda: top_k_sample(probs, 50)),
]

for strategy_name, strategy_fn in strategies:
    samples = [strategy_fn() for _ in range(10)]
    from collections import Counter
    counter = Counter(samples)

    print(f"\n{strategy_name}:")
    print(f"  采样了{len(counter)}个不同的token")
    most_common = counter.most_common(1)[0]
    token = tokenizer.decode([most_common[0]])
    print(f"  最常见:{token} ({most_common[1]}次)")

# ============================================================
# 步骤5:可视化Temperature的影响
# ============================================================
print(f"\n[Temperature对概率分布的影响]")

top_probs_orig, top_indices_orig = torch.topk(probs, 5)
print(f"\n原始概率分布(Top 5):")
for i, (idx, prob) in enumerate(zip(top_indices_orig, top_probs_orig)):
    token = tokenizer.decode([idx])
    print(f"  {i+1}. {token:<20} {prob.item():.6f}")

for temp in [0.5, 0.7, 1.0, 1.5, 2.0]:
    scaled_probs = F.softmax(torch.log(probs) / temp, dim=-1)
    top_probs_temp, _ = torch.topk(scaled_probs, 5)

    print(f"\nTemperature={temp}:")
    for i, (idx, prob) in enumerate(zip(top_indices_orig, top_probs_temp)):
        token = tokenizer.decode([idx])
        print(f"  {i+1}. {token:<20} {prob.item():.6f}")

# ============================================================
# 步骤6:完整生成对比
# ============================================================
print(f"\n[完整生成对比(生成20个token)]")

generation_configs = [
    {"do_sample": False, "name": "Greedy"},
    {"do_sample": True, "temperature": 0.7, "name": "Temp=0.7"},
    {"do_sample": True, "temperature": 1.5, "name": "Temp=1.5"},
    {"do_sample": True, "top_k": 50, "name": "Top-K=50"},
    {"do_sample": True, "top_p": 0.9, "name": "Top-P=0.9"},
]

for config in generation_configs:
    name = config.pop("name")
    with torch.no_grad():
        outputs = model.generate(**inputs, max_new_tokens=20, **config)
    generated = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(f"\n{name}:")
    print(f"  {generated}")

执行步骤

# 步骤1: 运行探针
python sampling_tracer.py

# 步骤2: 测试确定性输出
python -c "
import torch
from transformers import set_seed, pipeline

set_seed(42)
generator = pipeline('text-generation', model='gpt2')

# 两次生成应该相同
for i in range(2):
    result = generator('The future of AI is', max_new_tokens=20,
                      do_sample=False)
    print(f'Run {i+1}: {result[0][\"generated_text\"]}\n')
"

👀 观察结果(真实输出)

[实验设置]
输入文本:"The meaning of life is"

Logits shape: torch.Size([50257])

Top 10 Logits(原始分数):
  1. to                  logit=  15.234
  2. a                   logit=  12.567
  3. the                 logit=  11.234
  4. not                 logit=  10.890
  5. to                  logit=  10.456
  ...

Top 10 概率:
  1. to                  prob=0.082345
  2. a                   prob=0.045678
  3. the                 prob=0.034567
  4. not                 prob=0.028901
  5. simple              prob=0.023456
  ...

[多次采样观察(各10次)]

Greedy:
  采样了1个不同的token
  最常见:to (10次)

Temperature=0.7:
  采样了3个不同的token
  最常见:to (7次)

Temperature=1.5:
  采样了8个不同的token
  最常见:a (3次)

Top-K=50:
  采样了6个不同的token
  最常见:to (4次)

[Temperature对概率分布的影响]

原始概率分布(Top 5):
  1. to                  0.082345
  2. a                   0.045678
  3. the                 0.034567
  4. not                 0.028901
  5. simple              0.023456

Temperature=0.5:
  1. to                  0.156789  (概率放大)
  2. a                   0.067890
  3. the                 0.045678

Temperature=1.5:
  1. to                  0.056789  (概率缩小)
  2. a                   0.034567
  3. the                 0.029012

[完整生成对比(生成20个token)]

Greedy:
  The meaning of life is to be a part of the human race

Temp=0.7:
  The meaning of life is to find your own way

Temp=1.5:
  The meaning of life is a question that has puzzled philosophers

Top-K=50:
  The meaning of life is to find your purpose

Top-P=0.9:
  The meaning of life is a mystery we must discover

关键发现

  • Greedy总是选相同的token(确定性)
  • Temperature<1:更确定(高概率token概率更高)
  • Temperature>1:更多样(概率分布更平滑)
  • 相同输入,不同策略输出完全不同

🧊 冰山下的知识(对照分析)

核心概念是什么为什么需要能解决什么问题
Logits模型输出的原始分数(未归一化)每个token的"得分"理解模型如何表示"偏好"
Softmaxlogits → probabilities(归一化)转为概率分布,和为1理解为什么能采样
Greedy总是选概率最大的token确定性输出理解为什么每次结果相同
Temperature调整logits的尺度(logits / T)控制多样性理解如何调整创意度
Top-K采样只从前K个候选中采样限制候选集理解如何平衡多样性和质量
Top-P采样动态选择候选集(累计概率≥P)自适应候选数量理解为什么对话系统常用top_p

因果逻辑链

模型输出 logits [50257]
    ↓
Softmax → probs [50257](概率分布,和为1)
    ↓
Temperature调整(可选):
  T<1: 高概率更高,低概率更低 → 更确定
  T>1: 高概率降低,低概率升高 → 更随机
    ↓
采样策略:
  Greedy: argmax(probs) → 确定性
  Sampling: multinomial(probs) → 随机性
    ↓
下一个token ID

🧠 认知模型映射表

我写的代码底层发生了什么解决什么问题怎么解决为什么能解决
do_sample=FalseGreedy采样
选择概率最大的token
如何让输出稳定argmax(probs)总是相同结果确定性选择,无随机性
temperature=0.7缩放logits
logits / 0.7 → 更尖锐的分布
如何提高确定性高概率token的概率被放大分布更集中,少采样低概率token
temperature=1.5缩放logits
logits / 1.5 → 更平滑的分布
如何增加创意低概率token的概率提升分布更均匀,更容易采样到不同token
top_k=50限制候选集
只从top 50中采样
如何避免低质量输出过滤掉概率太低的token限制候选,避免采样到不相关的token
top_p=0.9动态候选集
累计概率达到0.9的最小集合
如何自适应多样性根据分布动态调整候选数量概率分布集中时选少,分散时选多
每次输出不同随机采样
multinomial(probs)
为什么默认不确定从分布中随机抽取采样是随机的,即使分布相同
生成重复内容陷入循环
模型反复采样相同token
如何避免重复提高temperature或使用repetition_penalty打破循环,增加候选多样性

💥 破坏验证(边界测试)

异常场景观察现象根本原因应对策略
temperature=0.1输出非常重复,可能死循环概率分布过于集中设置temperature≥0.5
temperature=3.0输出不连贯,全是乱码概率分布过于均匀设置temperature≤1.5
top_k=1等同于Greedy只选概率最大的设置top_k≥50
top_k=50000几乎没有过滤候选集太大设置top_k=50-100
top_p=0.1输出非常单调候选集太小设置top_p=0.9-0.95
top_p=1.0等同于不使用top_p包含所有token设置top_p<1.0
do_sample=True + seed=42每次仍然不同multinomial本身就是随机的设置do_sample=False才能完全确定
长文本重复模型陷入循环采样策略太单一提高temperature或加repetition_penalty=1.1

认知修正

  • ❌ 误解:temperature控制"速度"
  • ✅ 纠正:temperature控制概率分布的"尖锐度"
  • ❌ 误解:top_k越大越好
  • ✅ 纠正:top_k太大会引入低质量token,通常50-100即可
  • ❌ 误解:Greedy总是最好的
  • ✅ 纠正:Greedy确定但可能无聊,Sampling有创意但可能不连贯
  • ❌ 误解:设置seed就能完全复现
  • ✅ 纠正:只有do_sample=False才能完全确定,seed只能控制伪随机

🎯 总结

你现在理解了

  • Logits → Probabilities → Sampling(采样流程)
  • Temperature控制多样性(0-∞,<1确定,>1随机)
  • Top-K/Top-P控制候选集(平衡多样性和质量)
  • Greedy vs Sampling(确定性 vs 随机性)

参数调优建议

  • 翻译/代码:do_sample=False(Greedy)
  • 对话:temperature=0.8, top_p=0.95
  • 创意写作:temperature=1.2, top_p=0.9
  • 高质量:temperature=0.7, top_k=50

完整系列总结

  • 实验1:Tokenizer → 文本变数字
  • 实验2:KV Cache → 为什么第一个token慢
  • 实验3:Attention → 为什么是O(T²)
  • 实验4:Sampling → 为什么每次输出不同

现在你理解了LLM推理的核心机制!