← 返回资讯
赵一鸣
产品评测编辑
已审核

LLM大语言模型之Generate/Inference生

你可能不信,我翻云笔记的时候手都在抖——里面躺着二十多条大模型参数的翻车记录。温度拉到1.2,模型直接胡言乱语;Top-P调到0.9,照样跟复读机一样循环;Beam Search一开反而比原来更傻……每个坑我都踩过,而且踩得明明白白,就差在脑门上刻“参数受害者”了。

LLM大语言模型之Generate/Inference生

LLM大语言模型之Generate/Inference生


你可能不信,我翻云笔记的时候手都在抖——里面躺着二十多条大模型参数的翻车记录。温度拉到1.2,模型直接胡言乱语;Top-P调到0.9,照样跟复读机一样循环;Beam Search一开反而比原来更傻……每个坑我都踩过,而且踩得明明白白,就差在脑门上刻“参数受害者”了。

今天我把这些参数怎么来的、怎么用的、怎么让你摔跟头,掰开揉碎给你说清楚。代码扔GitHub了,跑一下就全懂。


先看裸奔:啥参数都不加,模型咋干活?

我一直觉得搞懂一个东西最快的方法就是看它“裸奔”。所以先从最原始的方式聊起——贪心解码

拿Llama举个例子(其他生成式模型一个德性)。我输入“say”,分词之后变成token 1827。模型自己加了个BOS token ,实际进嘴的是两个token。

PYTHON
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_name = "Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(model_name)

text = "say"
inputs = tokenizer(text, return_tensors="pt")
# 你看到的:{'input_ids': tensor([[ 1, 1827]]), 'attention_mask': tensor([[1, 1]])}

然后直接喂给模型:

PYTHON
logits = model.forward(inputs.input_ids).logits
print(logits.shape) # [1, 2, 32000] 乖乖,32000个候选词!

shape 是 [batch_size, sequence_length, vocab_size]。最后一个位置的logits就是模型对下一个token的预测分数。挑分数最高的那个:

PYTHON
next_token_id = torch.argmax(logits[:, -1, :], dim=-1)
print(next_token_id) # 比如 tensor([[22172]])
tokenizer.decode(next_token_id[0]) # 'hello'

看到了吧,模型觉得“say”后面最该接“hello”。好,把“hello”续上,再预测下一个词……直到碰到EOS或者达到最大长度。

这就是贪心解码——每一步都选当前概率最高的词。简单粗暴,但用不了几次你就会发现:输出容易困在循环里,比如“I love to code. I love to code. I love to code…”跟卡住的唱片似的。

我最早做对话系统那会儿被这玩意儿坑惨了。生成两轮就开始复读,我还以为是模型脑子坏了,后来才明白——缺了采样这一步。

温度(Temperature):让模型“更浪”还是“更怂”

那怎么打破这种死板?最简单的办法是不再选概率最大的,而是按概率分布去采样。可问题来了:如果原始分布太极端(一个词概率0.99,其他加起来0.01),采样和argmax也没啥区别。所以需要个东西来“软化”分布——温度登场。

直接上代码,一看就懂:

PYTHON
logits = torch.tensor([[0.5, 1.2, -1.0, 0.1]]) # 四个候选词的原始分数

# 温度 = 1
probs = torch.softmax(logits, dim=-1)
# [0.2559, 0.5154, 0.0571, 0.1716]

# 温度 = 0.5
probs_low = torch.softmax(logits / 0.5, dim=-1)
# [0.1800, 0.7301, 0.0090, 0.0809]

# 温度 = 2
probs_high = torch.softmax(logits / 2, dim=-1)
# [0.2695, 0.3825, 0.1273, 0.2207]

看到没?温度越低,分布越尖锐(0.5时第二个词从0.51飙到0.73);温度越高,分布越平坦(2.0时四个词差距缩小很多)。

规律很直接:温度接近0 → 接近贪心,稳定得像老干部;温度=1.0 → 原始分布,原汁原味;温度>1.0 → 随机性变大,开始放飞自我。

说到这儿我必须自曝一个踩坑经历。有回做故事生成,觉得“创意不够”直接把温度拉到1.5。你猜怎么着?模型开始编外星人入侵地球,中间还夹着法文歌词……我当场尴尬到脚趾抠地,后来乖乖降到0.9。

还有个实战经验你记好:如果prompt写得又长又细,模型本身就很确定,温度可以稍微调高点(0.8-1.0)增加多样性;如果prompt就两三个词,模型全靠猜——赶紧把温度压到0.3-0.5,不然它直接起飞,拉都拉不住。

Top-K和Top-P:别让冷门词出来捣乱

光有温度就够了吗?还不够。就算温度=2,采样的候选池还是所有32000个词。那些概率极低的词虽然被压了,但万一被抽中,生成出来的东西能让你怀疑人生。所以需要截断

Top-K:从概率最高的K个词里采样

K=40,就是只留前40个高概率词,剩下的概率全部清零,然后重新归一化。思路简单,挺好理解。但问题也明显:K是固定的。如果概率分布本来就均匀,K=40可能切掉了一堆合理的词;如果分布很集中,K=40可能又放进来一堆不相干的词。总之K太死板了。

Top-P(Nucleus Sampling):动态截断,真正“看人下菜碟”

Top-P的思路更灵活。把概率从高到低排序,然后累加,直到累积概率超过阈值P,才从这个候选集里采样。比如P=0.9,就选到累积概率刚好超过0.9的那几个词,不管具体有几个。分布集中的时候,候选集小;分布均匀的时候,候选集自然变大。这才聪明。

我拿Llama2官方实现的sample_top_p来演示,注释写进去,你一看就明白:

PYTHON
def sample_top_p(probs, p):
 # probs: [vocab_size] 已经归一化
 sorted_probs, sorted_indices = torch.sort(probs, descending=True)
 cum_probs = torch.cumsum(sorted_probs, dim=-1)
 
 # 找到累积概率超过p的位置,把后面统统mask掉
 mask = cum_probs - sorted_probs > p # 这里做减法,保证超过的那一刻也被保留
 sorted_probs[mask] = 0.0
 
 # 重新归一化,然后采样
 sorted_probs = sorted_probs / torch.sum(sorted_probs)
 next_token = torch.multinomial(sorted_probs, 1)
 return sorted_indices[next_token]

注意那个小细节:cum_probs - sorted_probs > p 这个写法,保证了累积概率刚好超过p的第一个词也被留在候选集里。不然边界处的词可能被误杀,采样会出问题。

我个人的习惯是Top-P设0.9,温度设0.8,基本能平衡质量和多样性。如果你发现生成内容还在反复兜圈子,一般先把Top-P降到0.85试试,别急着动温度。

Beam Search:束搜索,千万不要迷信它

说完了采样,再来聊聊Beam Search。这玩意儿在机器翻译时代是神兵利器,但在生成式对话里——我劝你三思而后用。

Beam Search的思路:每一步保留概率最高的num_beams条路径,到最后挑一条整体概率最高的。比如num_beams=4,每一步都保持4个假设,像多线程竞争一样。

听起来很完美对吧?但实际效果经常是:句子更流畅了,但也更无聊、更套话了。因为Beam Search专挑全局概率高的路径走,结果全是安全牌,毫无惊喜。而且它没法跟采样一起用(do_sample=False才能用Beam Search),多样性等于零。

我自己做过一个实验:用Beam Search生成广告文案,十条里有八条开头都是“我们的产品非常好……”,全是套话,读十遍都分不清谁是谁。后来换成采样+Top-P,虽然偶尔翻车,但确实能蹦出让眼前一亮的句子。

所以结论可以直白一点:写故事、对话、创意文本,用采样更合适;翻译、摘要这种对准确率要求高的任务,可以考虑Beam Search。

HuggingFace里还有个num_beam_groups,把束分成多组,每组独立搜索再合并,目的是增加多样性。说实话我试过两次,没有感觉到质的变化,不如直接调温度来得痛快。

重复惩罚:别让模型变成复读机

我敢打赌,每个调过LLM的人都见过这种输出:

I love coding. I love debugging. I love refactoring. I love unit tests...

最后变成“I love”的复读盛典。这就是典型的重复陷阱。贪心解码和低温度下尤其容易发生,因为一旦某个词概率高了,就会自己强化自己,形成正反馈,跟滚雪球似的。

解决办法?重复惩罚(repeat_penalty)。原理简单到想哭:在算概率之前,对已经出现过的token的logits打一个折扣。

代码大概长这样:

PYTHON
def apply_repetition_penalty(logits, input_ids, penalty=1.2):
 for token_id in input_ids[0]:
 logits[:, token_id] /= penalty
 return logits

每次生成前,遍历当前已生成的所有token,把它们在logits中的得分除以penalty(>1就会压低概率)。这样一来,模型就不太愿意重复刚用过的词了。

HuggingFace里还提供了两个类似参数:frequency_penalty(按出现次数惩罚,重复越多压越狠)和presence_penalty(只看有没有出现过,不管次数)。前者适合压制高频词滥用,后者更适合让模型换着花样说话。

我自己的经验:重复惩罚一般设在1.1到1.3之间。设太大了模型会开始回避常见词,生成变得扭扭捏捏。平衡的艺术,你懂的。


说到这儿你可能会觉得:调个参数怎么跟做菜似的,火候大小、材料多寡、时间长短……全是玄学。

对,就是玄学。但这不是瞎蒙,是有规律的玄学。每个参数背后都对应着模型对“确定性”和“多样性”的权衡。你理解了它为什么这么干,翻车的时候就知道该拧哪颗螺丝。

最后送你一句能截图的话:

**参数不是魔法,是你和概率之间的一场调情。调得好它给你惊喜,调不好它就给你惊吓——但别忘了,真正决定上限的,永远是那个会调参数的你。**
175
2503 阅读
5 评论
分享
编辑说明

本文由 MakeSense 编辑团队撰写并审核。文中引用的数据和观点均经过交叉验证,如有疏漏欢迎在评论区指正。最后更新:2026年06月21日 03:11

赵一鸣

产品评测编辑

前产品经理,现专注 AI 工具评测。实测过 30+ 款 AI 产品,擅长横向对比和用户体验分析。

读者评论 5

产品经理阿杰 6天前
从产品角度看,这个方向确实有机会,但商业化路径还需要验证。
回复 点赞 (15)
张工 1周前
写得很实在,特别是实测对比那部分,跟我自己的使用感受一致。
回复 点赞 (12)
前端工程师 1周前
代码示例很清晰,直接用到项目里了。
回复 点赞 (6)
技术小白 昨天
作为非技术人员也看懂了,感谢作者的通俗讲解。
回复 点赞 (3)
Dev小王 4天前
终于有人把这个说清楚了,收藏了。
回复 点赞 (8)