个人主页: > for_ever_love__ < (欢迎各位大佬莅临😊)
其他栏目: > 我想学python了 <
其他栏目: > iOS项目总结大全 <
其他栏目: > iOS UI <
文章目录
- 概率与统计------分布、期望与贝叶斯,语言模型的建模基础
-
- 一、概率分布:模型眼里的世界
-
- [1.1 离散分布](#1.1 离散分布)
- [1.2 采样:按概率抽一个](#1.2 采样:按概率抽一个)
- 二、条件概率与链式法则:语言模型的数学骨架
-
- [2.1 条件概率](#2.1 条件概率)
- [2.2 从零统计一个 bigram 模型](#2.2 从零统计一个 bigram 模型)
- [2.3 零概率问题与平滑](#2.3 零概率问题与平滑)
- 三、交叉熵与困惑度:模型好不好,用数字说话
-
- [3.1 信息量与熵](#3.1 信息量与熵)
- [3.2 交叉熵:这就是模型的损失函数](#3.2 交叉熵:这就是模型的损失函数)
- [3.3 困惑度 Perplexity](#3.3 困惑度 Perplexity)
- 四、采样策略:temperature、top-k、top-p
-
- [4.1 Temperature 温度](#4.1 Temperature 温度)
- [4.2 Top-k 与 Top-p(核采样)](#4.2 Top-k 与 Top-p(核采样))
- [4.3 完整解码示例](#4.3 完整解码示例)
- [五、贝叶斯:不确定性下的推理(RAG 的哲学基础)](#五、贝叶斯:不确定性下的推理(RAG 的哲学基础))
- 六、常见坑与注意事项
- 七、本篇小结
概率与统计------分布、期望与贝叶斯,语言模型的建模基础
承上:上一篇《线性代数入门》我们知道词是向量、注意力是点积。
本篇:本篇学概率分布、条件概率与链式法则------解释模型怎么「选出下一个词」。
启下:下一篇《微积分基础》解决「参数该往哪个方向调、调多少」。
学完这一节,你能动手做:
- 用交叉熵与困惑度衡量一个语言模型好不好
- 用 temperature、top-k、top-p 控制生成的严谨与发散
- 从零统计一个 bigram 语言模型,理解 GPT 在预测什么
上一篇我们搞定了线性代数,知道了"词是向量、注意力是点积"。这一篇回答一个更根本的问题:
大模型输出"下一个词",到底是怎么选出来的?
答案是:它给词表里每一个词算一个概率,然后按概率抽样 。整个大模型的本质,就是一台条件概率机器。
理解了概率,你就能理解:为什么 temperature 调高模型更"胡说八道"、为什么大模型会一本正经地编造事实(幻觉)、困惑度 perplexity 到底在衡量什么。
一、概率分布:模型眼里的世界
1.1 离散分布
词表有 N 个词,模型对每个词给一个概率,加起来 = 1:
python
import numpy as np
vocab = ["猫", "狗", "跑", "吃", "鱼"]
probs = np.array([0.45, 0.25, 0.15, 0.10, 0.05])
print("和为:", probs.sum()) # 必须是 1.0
for w, p in zip(vocab, probs):
print(f"{w}: {p:.0%}")
这就是一个概率分布(categorical distribution)。大模型最后一层 softmax 输出的,正是这样一个向量------只不过真实词表有 5 万~15 万个词。
1.2 采样:按概率抽一个
有了分布怎么"选词"?不是直接取最大的,而是抽样:
python
np.random.seed(0)
# 按概率抽样 10000 次,看频率是否收敛到给定概率
samples = np.random.choice(len(vocab), size=10000, p=probs)
counts = np.bincount(samples, minlength=len(vocab)) / 10000
for w, p, c in zip(vocab, probs, counts):
print(f"{w}: 理论 {p:.2%} 实测 {c:.2%}")
实测频率会非常接近理论概率------这就是大数定律。也解释了为什么同一个问题问两次,模型可能给出不同的答案:它在抽样,不是一个确定性函数。
二、条件概率与链式法则:语言模型的数学骨架
2.1 条件概率
P(鱼 | 猫) 读作"已知前面是'猫',下一个词是'鱼'的概率"。
语言模型做的就是这件事:
P(整句话) = P(猫) × P(追|猫) × P(了|猫,追) × P(鱼|猫,追,了)
链式法则:联合概率 = 一串条件概率的乘积。这就是**自回归语言模型(GPT)**的数学定义。
2.2 从零统计一个 bigram 模型
不用神经网络,光靠数数也能做一个"语言模型":
python
corpus = [
"我 爱 吃 苹果",
"我 爱 吃 香蕉",
"我 不 爱 吃 鱼",
"他 爱 吃 苹果",
"小 猫 吃 鱼",
]
from collections import defaultdict
bigram = defaultdict(lambda: defaultdict(int))
unigram = defaultdict(int)
for line in corpus:
words = line.split()
for i in range(len(words) - 1):
bigram[words[i]][words[i + 1]] += 1
unigram[words[i]] += 1
unigram[words[-1]] += 1
def p_next(prev, word):
"""P(word | prev)"""
if unigram[prev] == 0:
return 0.0
return bigram[prev][word] / unigram[prev]
print("P(吃 | 爱) =", p_next("爱", "吃")) # 爱后面全是吃 → 1.0
print("P(苹果 | 吃) =", p_next("吃", "苹果")) # 吃后面苹果2次/共4次 → 0.5
print("P(鱼 | 吃) =", p_next("吃", "鱼"))
# 给定"吃",下一个词的完整分布
total = sum(bigram["吃"].values())
for w, c in bigram["吃"].items():
print(f" P({w}|吃) = {c/total:.2f}")
这就是语言模型最朴素的形态 :数语料 → 算频率 → 当概率。GPT 做的事完全一样,只不过它用一个 1750 亿参数的神经网络来预测这个分布,泛化能力强一万倍。
2.3 零概率问题与平滑
上面的模型有个致命伤:语料里没出现过的组合概率 = 0,一乘全完蛋。
python
print("P(火箭 | 吃) =", p_next("吃", "火箭")) # 0.0 → 整句话概率归零
解决办法是平滑(smoothing),给没见过的组合分一点点概率:
python
def p_next_smooth(prev, word, alpha=0.5, V=1000):
"""加 α 平滑:分子 +α,分母 +α*V"""
return (bigram[prev][word] + alpha) / (unigram[prev] + alpha * V)
print("平滑后 P(火箭|吃) =", p_next_smooth("吃", "火箭"))
神经网络模型天然避免了这个问题------softmax 的输出永远不为 0(除非下溢),这就是它比 n-gram 强的原因之一。
三、交叉熵与困惑度:模型好不好,用数字说话
3.1 信息量与熵
一个事件的概率越小,"发生了"带来的信息量越大:
I(x) = -log P(x)
- P=1(必然发生)→ 信息量 0(说了等于没说)
- P=0.001(稀有)→ 信息量 ≈ 6.9(很"意外")
熵 = 信息量的期望,衡量"分布有多不确定":
python
def entropy(p):
p = np.array(p)
p = p[p > 0] # 0*log0 约定为 0
return -np.sum(p * np.log2(p))
print("均匀分布(5个词):", entropy([0.2]*5)) # ≈ 2.32 bit 最不确定
print("集中分布:", entropy([0.95, 0.02, 0.01, 0.01, 0.01])) # ≈ 0.36 bit 很确定
3.2 交叉熵:这就是模型的损失函数
交叉熵衡量"用分布 Q 去编码真实分布 P,平均要花多少信息量":
python
def cross_entropy(p_true, q_pred):
"""p_true: one-hot 真实标签;q_pred: 模型预测的概率分布"""
p_true = np.array(p_true)
q_pred = np.clip(np.array(q_pred), 1e-12, 1.0) # 防 log(0)
return -np.sum(p_true * np.log(q_pred))
# 真实下一个词是"猫"(index 0)
y_true = [1, 0, 0, 0, 0]
print("猜得很准:", cross_entropy(y_true, [0.90, 0.05, 0.03, 0.01, 0.01])) # ≈ 0.105
print("猜得一般:", cross_entropy(y_true, [0.40, 0.30, 0.20, 0.05, 0.05])) # ≈ 0.916
print("猜错了 :", cross_entropy(y_true, [0.05, 0.80, 0.10, 0.03, 0.02])) # ≈ 2.996
结论 :预测越准,交叉熵越小。训练大模型,就是在最小化交叉熵------让模型给"正确答案"的概率尽可能接近 1。
3.3 困惑度 Perplexity
困惑度是交叉熵的指数形式,更直观:
python
def perplexity(ce):
return np.exp(ce)
print("ce=0.105 → ppl =", perplexity(0.105)) # ≈ 1.11
print("ce=2.996 → ppl =", perplexity(2.996)) # ≈ 20.0
困惑度的直观含义:模型在预测时"相当于在几个词之间犹豫"。ppl=20 意味着模型每次预测时,感觉像是在 20 个候选词里随机挑------越接近 1 越好。
评测大模型时经常看到 ppl 这个指标,现在你知道它在说什么了。
四、采样策略:temperature、top-k、top-p
同一个模型,为什么有时严谨有时放飞?答案是采样策略。
4.1 Temperature 温度
python
logits = np.array([3.0, 2.0, 1.0, 0.5, 0.1]) # 模型原始输出(未归一化)
def softmax_with_temperature(logits, T=1.0):
z = logits / T
z = z - np.max(z)
e = np.exp(z)
return e / np.sum(e)
print("T=0.5 (保守):", softmax_with_temperature(logits, 0.5).round(3))
print("T=1.0 (默认):", softmax_with_temperature(logits, 1.0).round(3))
print("T=2.0 (放飞):", softmax_with_temperature(logits, 2.0).round(3))
| Temperature | 效果 | 适用 |
|---|---|---|
| T < 1 | 分布更尖锐,倾向高概率词 | 代码生成、事实问答、抽取任务 |
| T = 1 | 原始分布 | 通用 |
| T > 1 | 分布更平缓,冷门词也有机会 | 创意写作、头脑风暴 |
| T → 0 | 等价于贪婪解码(总选最大) | 需要确定性输出 |
temperature 调太高,模型就开始胡说八道------因为低概率(往往是错的)的 token 被抽中的概率上升了。调太低则输出重复、呆板。
4.2 Top-k 与 Top-p(核采样)
更精细的控制:只在"靠谱的候选"里抽。
python
def top_k_sample(probs, k=3):
idx = np.argsort(probs)[-k:] # 概率最高的 k 个
sub = probs[idx] / probs[idx].sum() # 重新归一化
return np.random.choice(idx, p=sub)
def top_p_sample(probs, p=0.9):
"""核采样:按概率从高到低累加,只保留累计到 p 的最小集合"""
order = np.argsort(probs)[::-1]
cumsum = np.cumsum(probs[order])
keep = order[cumsum <= p]
if len(keep) == 0:
keep = order[:1]
sub = probs[keep] / probs[keep].sum()
return np.random.choice(keep, p=sub)
probs = softmax_with_temperature(logits, 1.0)
print("分布:", probs.round(3))
print("top-k(2) 抽样 10 次:", [top_k_sample(probs, 2) for _ in range(10)])
print("top-p(0.9) 抽样 10 次:", [top_p_sample(probs, 0.9) for _ in range(10)])
top-p 比 top-k 聪明:候选数量自适应。分布很确定时只留 2~3 个词,分布很平(模型没把握)时留几十个------所以现在主流 API 默认推荐 top-p。
4.3 完整解码示例
python
def generate(model_predict_fn, prompt_tokens, max_new=10, T=0.8, top_p=0.9):
"""自回归生成的通用骨架"""
tokens = list(prompt_tokens)
for _ in range(max_new):
logits = model_predict_fn(tokens) # 模型给下一步的 logits
probs = softmax_with_temperature(logits[-1], T)
next_id = top_p_sample(probs, top_p)
tokens.append(int(next_id))
return tokens
# 用"假模型"演示流程
vocab_size = 20
fake_model = lambda toks: np.tile(np.arange(vocab_size, 0, -1) / 20.0, (len(toks), 1))
print(generate(fake_model, [1, 2, 3], max_new=8))
真实 GPT 的生成就是这个循环:预测 → 采样 → 拼回去 → 再预测。你每次看到模型一个字一个字往外蹦,就是在跑这个循环。
五、贝叶斯:不确定性下的推理(RAG 的哲学基础)
贝叶斯定理:
P(A|B) = P(B|A) · P(A) / P(B)
用人话讲:先验 P(A) 是你原本的信念,看到证据 B 之后,更新成 后验 P(A|B)。
python
# 经典例子:疾病检测
# P(病) = 0.01(先验,发病率 1%)
# P(阳性|病) = 0.99(真阳性率)
# P(阳性|健康) = 0.05(假阳性率)
p_disease = 0.01
p_pos_given_disease = 0.99
p_pos_given_healthy = 0.05
p_pos = p_pos_given_disease * p_disease + p_pos_given_healthy * (1 - p_disease)
p_disease_given_pos = p_pos_given_disease * p_disease / p_pos
print(f"检出阳性后真得病的概率: {p_disease_given_pos:.1%}")
结果只有约 16.7% ------远低于直觉。这就是基础概率谬误:因为健康人基数太大,假阳性数量反超真阳性。
和大模型什么关系? RAG(检索增强生成)的哲学就是这个:
P(答案 | 问题) ← 只靠模型参数记忆(先验)→ 容易幻觉
P(答案 | 问题, 检索到的文档) ← 加入外部证据 → 更可信
RAG 做的事就是给模型注入证据,把后验分布"掰"到正确答案上。理解了贝叶斯,你就理解了为什么 RAG 能治幻觉。
六、常见坑与注意事项
| 坑 | 现象 | 解决 |
|---|---|---|
log(0) |
loss 变 -inf / nan |
np.clip(p, 1e-12, 1) |
| 概率和不为 1 | 采样报错 | 用 softmax 保证归一化 |
直接 exp(logits) |
大 logits 溢出成 inf | 先减 max 再 exp |
| temperature 设 0 | 除零 | T 最小设 0.01,或改贪婪解码 |
| 忽视先验 | 模型自信地胡说 | 用 RAG 补证据 / 要求给出引用 |
| 小样本统计不可靠 | bigram 概率抖动 | 加平滑,或增大语料 |
七、本篇小结
- 大模型 = 条件概率机器 :
P(下一个词 | 上文),链式法则把整句概率拆成一串条件概率。 - 交叉熵是损失函数 ,训练就是让正确答案的概率趋近 1;困惑度 ppl 直观表示"模型在几个词间犹豫"。
- Temperature 控制分布尖锐程度:低=严谨确定,高=创意发散,过高=胡说。
- Top-k / Top-p 限制候选集,top-p 自适应更主流;自回归生成就是"预测→采样→拼接"循环。
- 贝叶斯解释了 RAG 的价值:注入外部证据,用后验替代纯参数记忆的先验,从而抑制幻觉。
下一篇 微积分基础 :导数、梯度与链式法则。这是反向传播的数学钥匙------我们会手推一个两层网络的梯度,并对比"手算梯度"和"数值梯度"完全一致,让你彻底相信反向传播不是魔法。
本篇是《大模型开发从 0 到 1》专栏第 15 篇,阶段 2「数学基础(LLM 视角)」第 2 篇。专栏文章按「分类专栏」归类,顺序学习体验最佳。