> For the complete documentation index, see [llms.txt](https://inwt233.gitbook.io/ai-learning/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://inwt233.gitbook.io/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-02/day-008.md).

# Day 008：N-gram 语言模型

{% hint style="info" %}
**如果只拥有一份分好 Token 的语料，怎样用相邻 Token 的计数估计“下一个 Token 是什么”的概率，并进一步得到一句话的概率？**
{% endhint %}

## 学习目标

完成本节后，你应该能够：

1. 严格定义语言模型、N-gram、history 与 context；
2. 从概率链式法则推导 N-gram 的有限历史近似；
3. 用计数写出未平滑 N-gram 的最大似然估计；
4. 解释 unigram、bigram 与 trigram 分别条件于多少个历史 Token；
5. 正确加入 `<s>` 与 `</s>`，计算一句话包含结束事件的概率；
6. 用标准 Python 实现可训练、可查询的 N-gram 语言模型；
7. 解释为什么更大的 `n` 同时带来更强的局部表达能力与更严重的数据稀疏；
8. 识别 Tokenizer、OOV、零概率和数据泄漏对实验结论的影响。

{% hint style="info" %}
本节默认已经掌握第 1 周的 Token、词表、OOV 与 Tokenizer。为了聚焦概率建模，代码直接接收已经分好的 Token 列表，不再重复分词。今天只学习**未平滑最大似然估计**；零概率怎样修复留到 Day 009，交叉熵与困惑度留到 Day 010。
{% endhint %}

## 1. 语言模型不是“判断句子是否正确”的规则表

语言模型（Language Model，LM）学习的是 Token 序列上的概率分布。它可以回答两类等价问题：

1. 给定历史 Token，下一个 Token 的概率分布是什么？
2. 一整段 Token 序列的联合概率是多少？

例如，给定历史“我 喜欢”，模型可能估计：

```
P(自然语言 | 我 喜欢) = 0.5
P(机器学习 | 我 喜欢) = 0.5
```

概率高不等于语法上“绝对正确”，概率低也不等于“绝对错误”。它只表示：在训练分布和当前建模假设下，这个续写相对常见还是罕见。

语言模型的完整输入输出可以写成：

```
已经分词的训练语料
  ↓ 枚举相邻 N-gram
context 与 next-token 计数
  ↓ 最大似然归一化
P(next_token | context)
  ↓ 对句中每个条件概率连乘
P(整句，包括结束事件)
```

## 2. 从精确链式法则到有限历史近似

设一句话由 `T` 个 Token 组成：

$$
w\_1,w\_2,\ldots,w\_T
$$

概率链式法则是精确恒等式：

$$
P(w\_1,\ldots,w\_T)
\=\prod\_{i=1}^{T}P(w\_i\mid w\_1,\ldots,w\_{i-1})
$$

它把整句概率拆成一串“给定全部历史，预测下一个 Token”的条件概率。

问题是：真实语料中几乎不可能为每一种任意长历史收集足够计数。N-gram 模型因此做一个有限历史近似：只保留最近 `n-1` 个 Token。

$$
P(w\_i\mid w\_1,\ldots,w\_{i-1})
\approx
P(w\_i\mid w\_{i-n+1},\ldots,w\_{i-1})
$$

这常被称为 `n-1` 阶 Markov 假设。它不是说自然语言真的只依赖最近几个词，而是为了让有限语料中的统计可计算。

{% hint style="warning" %}
链式法则是精确的；“只看最近 `n-1` 个 Token”才是 N-gram 的近似假设。不要把两者一起称为近似。
{% endhint %}

## 3. N-gram、context 与 history

N-gram 是连续 `n` 个 Token 的序列。

| 模型      | 一个事件       | 用多少历史预测下一个 Token | 条件概率形式          |
| ------- | ---------- | ---------------: | --------------- |
| unigram | `喜欢`       |                0 | `P(喜欢)`         |
| bigram  | `我 喜欢`     |                1 | `P(喜欢\|我)`      |
| trigram | `我 喜欢 NLP` |                2 | \`P(NLP\|我, 喜欢) |

对 N-gram 事件：

$$
(w\_{i-n+1},\ldots,w\_{i-1},w\_i)
$$

前 `n-1` 个 Token 是 context，最后一个 `w_i` 是要预测的 next token。

在不同资料中，history 有时指全部过去 Token，有时也被宽松地用作当前有限 context。阅读公式时应直接检查条件栏里到底保留了多少 Token。

还有一个常见命名陷阱：

```
“3-gram / trigram”包含 3 个 Token
但它只使用前 2 个 Token 预测第 3 个
```

## 4. 用最大似然估计条件概率

给定 context `h` 和下一个 Token `w`，未平滑 N-gram 的最大似然估计（Maximum Likelihood Estimation，MLE）是：

$$
P\_{\mathrm{MLE}}(w\mid h)
\=\frac{C(h,w)}{C(h)}
$$

其中：

* `C(h,w)`：context `h` 后面紧接 Token `w` 的次数；
* `C(h)`：context `h` 作为预测历史出现的总次数；
* 对固定且见过的 `h`，所有候选 next token 的概率之和为 1。

本节使用四句话：

```
我 / 喜欢 / 自然语言
我 / 喜欢 / 机器学习
我 / 学习 / 自然语言
你 / 喜欢 / 自然语言
```

只看 bigram，context“我”共出现 3 次：

```
我 → 喜欢   2 次
我 → 学习   1 次
```

所以：

$$
C(\text{我})=3
$$

$$
P(\text{喜欢}\mid\text{我})
\=\frac{C(\text{我},\text{喜欢})}{C(\text{我})}
\=\frac{2}{3}
$$

$$
P(\text{学习}\mid\text{我})
\=\frac{1}{3}
$$

二者相加为 1。这里没有任何神经网络参数，模型参数就是语料中的计数及其归一化结果。

## 5. 为什么必须加入句首与句尾标记

如果只统计正文 bigram：

```
我 → 喜欢
喜欢 → 自然语言
```

模型不知道：

* 哪个 Token 容易出现在句首；
* 哪个 Token 后面应该结束；
* 生成时什么时候停止。

因此通常加入边界 Token：

```
<s> / 我 / 喜欢 / 自然语言 / </s>
```

bigram 需要 1 个 `<s>`；trigram 为了让第一个普通 Token 也拥有两个历史位置，需要 2 个 `<s>`：

```
bigram： <s> / 我 / ... / </s>
trigram：<s> / <s> / 我 / ... / </s>
```

一般地，`n`-gram 在左侧加入 `n-1` 个 `<s>`。右侧只需一个 `</s>` 作为“预测结束”事件。

边界 Token 属于建模约定，不是原文中真的出现的词。训练、评分与生成必须使用同一约定，否则计数无法对应。

## 6. 手算一句话的 bigram 概率

对“我 喜欢 自然语言”，包含边界后的概率为：

$$
\begin{aligned}
P(&\text{我 喜欢 自然语言})
\=P(\text{我}\mid\text{<s>})\\
&\cdot P(\text{喜欢}\mid\text{我})
\cdot P(\text{自然语言}\mid\text{喜欢})
\cdot P(\text{</s>}\mid\text{自然语言})
\end{aligned}
$$

从四句训练语料可得：

$$
P(\text{我}\mid\text{<s>})=\frac{3}{4}
$$

$$
P(\text{喜欢}\mid\text{我})=\frac{2}{3}
$$

$$
P(\text{自然语言}\mid\text{喜欢})=\frac{2}{3}
$$

三次出现“自然语言”时它都位于句尾，因此：

$$
P(\text{</s>}\mid\text{自然语言})=1
$$

最终：

$$
P=\frac{3}{4}\times\frac{2}{3}\times\frac{2}{3}\times1
\=\frac{1}{3}
$$

多个小概率直接连乘容易产生浮点下溢，工程中通常改为相加对数概率：

$$
\log\_2 P(w\_1,\ldots,w\_T)
\=\sum\_i\log\_2 P(w\_i\mid h\_i)
$$

对本句：

$$
\log\_2\frac{1}{3}\approx-1.584963
$$

## 7. 标准 Python 实现

下面实现任意阶 `n` 的未平滑模型。输入是 `list[list[str]]`：外层是句子，内层是已经完成 Tokenization 的 Token。

```python
from collections import Counter, defaultdict
from math import log2

BOS = "<s>"
EOS = "</s>"


class NGramLanguageModel:
    """教学用、未平滑的最大似然 N-gram 语言模型。"""

    def __init__(self, n: int):
        if n < 1:
            raise ValueError("n must be at least 1")

        self.n = n
        # context_counts[h] 保存 C(h)。
        self.context_counts = Counter()
        # next_counts[h][w] 保存 C(h, w)。
        self.next_counts = defaultdict(Counter)

    def _events(self, tokens: list[str]):
        """把一句话转成一系列 (context, next_token) 训练事件。"""
        # 左边加入 n-1 个句首标记；EOS 让模型也学习“何时结束”。
        padded = [BOS] * (self.n - 1) + tokens + [EOS]

        for index in range(self.n - 1, len(padded)):
            start = index - (self.n - 1)
            context = tuple(padded[start:index])
            yield context, padded[index]

    def train(self, sentences: list[list[str]]) -> None:
        """重新统计整个训练集；重复调用不会叠加旧计数。"""
        self.context_counts.clear()
        self.next_counts.clear()

        for tokens in sentences:
            for context, token in self._events(tokens):
                self.context_counts[context] += 1
                self.next_counts[context][token] += 1

    def probability(
        self,
        token: str,
        context: tuple[str, ...],
    ) -> float:
        """返回未平滑的 P(token | context)。"""
        # 即使调用者传入更长历史，也只保留最近 n-1 个 Token。
        context = (
            tuple(context[-(self.n - 1):])
            if self.n > 1
            else ()
        )
        denominator = self.context_counts[context]

        # 未见 context 没有可用的 MLE 分母；本节约定返回 0。
        if denominator == 0:
            return 0.0

        return self.next_counts[context][token] / denominator

    def sentence_probability(self, tokens: list[str]) -> float:
        """把正文和 EOS 的条件概率相乘。"""
        result = 1.0
        for context, token in self._events(tokens):
            result *= self.probability(token, context)
        return result

    def sentence_log_probability(self, tokens: list[str]) -> float:
        """用 log2 概率避免长序列连乘造成数值下溢。"""
        total = 0.0
        for context, token in self._events(tokens):
            probability = self.probability(token, context)
            if probability == 0.0:
                # log(0) 不存在，用 -inf 表示整句零概率。
                return float("-inf")
            total += log2(probability)
        return total

    def predict_next(
        self,
        context: tuple[str, ...],
        k: int = 3,
    ) -> list[tuple[str, float]]:
        """返回当前 context 下概率最高的 k 个候选。"""
        context = (
            tuple(context[-(self.n - 1):])
            if self.n > 1
            else ()
        )
        total = self.context_counts[context]
        if total == 0:
            return []

        candidates = [
            (token, count / total)
            for token, count in self.next_counts[context].items()
        ]
        # 概率降序；并列时按 Token 排序，保证实验可复现。
        return sorted(
            candidates,
            key=lambda item: (-item[1], item[0]),
        )[:k]


corpus = [
    ["我", "喜欢", "自然语言"],
    ["我", "喜欢", "机器学习"],
    ["我", "学习", "自然语言"],
    ["你", "喜欢", "自然语言"],
]

model = NGramLanguageModel(n=2)
model.train(corpus)

print("C(我):", model.context_counts[("我",)])
print("C(我, 喜欢):", model.next_counts[("我",)]["喜欢"])
print(
    "P(喜欢|我):",
    round(model.probability("喜欢", ("我",)), 6),
)
print(
    "next after 我:",
    [
        (token, round(probability, 6))
        for token, probability in model.predict_next(("我",))
    ],
)

seen = ["我", "喜欢", "自然语言"]
unseen = ["我", "喜欢", "深度学习"]

for sentence in [seen, unseen]:
    probability = model.sentence_probability(sentence)
    log_probability = model.sentence_log_probability(sentence)
    print(
        sentence,
        "prob=",
        round(probability, 6),
        "log2_prob=",
        round(log_probability, 6),
    )
```

本次使用 Python 3.12.10 实测输出：

```
C(我): 3
C(我, 喜欢): 2
P(喜欢|我): 0.666667
next after 我: [('喜欢', 0.666667), ('学习', 0.333333)]
['我', '喜欢', '自然语言'] prob= 0.333333 log2_prob= -1.584963
['我', '喜欢', '深度学习'] prob= 0.0 log2_prob= -inf
```

### 7.1 输出逐行解释

`C(我)=3`：四句话中有三次以“我”作为预测历史。

`C(我, 喜欢)=2`：这三次中，两次下一个 Token 是“喜欢”。

`P(喜欢|我)=2/3`：代码与第 4 节手算一致。

`next after 我`：同一 context 下候选概率之和为 1；“喜欢”排在“学习”之前。

已见句概率为 `1/3`，对数概率是 `log2(1/3)`。未见句中的 bigram“喜欢 → 深度学习”计数为 0，未平滑模型让整句概率直接变成 0，对数概率为负无穷。

{% hint style="warning" %}
零概率不是 Python 实现错误，而是未平滑最大似然 N-gram 的必然结果。Day 009 会学习怎样给未见事件分配非零概率。
{% endhint %}

## 8. Bigram 与 trigram 学到的信息有什么不同

bigram 只看“喜欢”，无法区分是谁喜欢：

$$
P(\text{自然语言}\mid\text{喜欢})=\frac{2}{3}
$$

trigram 可以分别保留“我 喜欢”和“你 喜欢”：

```python
trigram = NGramLanguageModel(n=3)
trigram.train(corpus)

# 同一个 next token 在两个不同的二词 context 下得到不同概率。
for context in [("我", "喜欢"), ("你", "喜欢")]:
    probability = trigram.probability("自然语言", context)
    print(
        f"P(自然语言 | {context}) = {probability:.6f}"
    )
```

实测输出：

```
P(自然语言 | ('我', '喜欢')) = 0.500000
P(自然语言 | ('你', '喜欢')) = 1.000000
```

原因是：

* “我 喜欢”后面一次是“自然语言”，一次是“机器学习”，所以各占一半；
* “你 喜欢”只出现一次，后面恰好是“自然语言”，MLE 因而给出 1.0。

这里的 1.0 不表示“你喜欢”后面永远只能出现自然语言，只表示训练语料中的这个 context 只有一次观测。这正是小数据下高阶 N-gram 容易过度自信的例子。

## 9. `n` 增大时发生的联合权衡

| 方面    | 较小的 `n`        | 较大的 `n`         |
| ----- | -------------- | --------------- |
| 可见历史  | 短，区分能力弱        | 更长，局部表达更具体      |
| 计数复用  | 多个句子共享 context | context 更容易互不相同 |
| 数据稀疏  | 较轻             | 更严重             |
| 存储量   | 观测到的 N-gram 较少 | 唯一 N-gram 数通常更多 |
| 过拟合风险 | 可能欠拟合          | 小语料下更容易记住局部片段   |

设训练语料展开后共有 `M` 个预测事件：

* 朴素单次训练扫描约为 `O(M)`；
* 查询一个已见 context 的计数平均可视为哈希表 `O(1)`；
* 内存主要取决于观察到的不同 context 和 N-gram 数，而不是理论上的全部组合；
* 理论组合空间随词表大小呈指数增长，高阶组合绝大多数不会在有限语料中出现。

如果词表大小为 `|V|`，理论上可能存在 `|V|^n` 种长度为 `n` 的组合。真实模型不会把全部组合都存下来，但这个数量说明了为什么提高 `n` 很快遇到稀疏问题。

## 10. 必须固定的工程边界

### 10.1 Tokenizer 决定 N-gram 中的“Token”

同一句话按词、子词、字符或字节切分，会得到完全不同的 N-gram 计数。训练与推理必须使用同一 Tokenizer、同一规范化和同一特殊 Token 约定。

本节的“自然语言”被当作一个已给定 Token。若真实 Tokenizer 将它拆成“自然 / 语言”，模型事件和概率都会改变。

### 10.2 OOV 与未见 N-gram 是两个问题

* OOV：输入 Token 根本不在固定词表中；
* 未见 N-gram：Token 都在词表中，但这个相邻组合没有在训练语料出现。

把 OOV 映射为 `<UNK>` 只能让输入拥有合法 ID，不能自动让所有含 `<UNK>` 的 context 获得可靠概率。

### 10.3 PAD 不应被当作真实语言事件

如果训练语料为了 batching 加了 `[PAD]`，计数前应使用有效长度或 attention mask 去掉 padding。否则模型会学到大量：

```
[PAD] → [PAD]
```

这只是批处理格式，不是语言规律。

### 10.4 数据划分必须先于计数

不能先在全部数据上统计 N-gram，再划分验证集和测试集。那会把测试集相邻关系泄漏进模型，使未见率和后续评价过于乐观。

正确顺序是：

```
原始数据
→ 按文档/用户/时间等合理单位划分 train/dev/test
→ 只用 train 拟合 Tokenizer 或固定词表规则
→ 只用 train 统计 N-gram
→ 在 dev/test 上评价
```

### 10.5 统计语言模型的“因果”不是 causal mask

N-gram 只使用左侧历史预测下一个 Token，因此具有自回归方向；但它没有 Transformer 注意力矩阵，也不需要神经网络中的 causal attention mask。两者解决的层次不同。

## 11. 常见误区

### 误区 1：N-gram 的 `n` 表示看前面 `n` 个 Token

错误。长度为 `n` 的事件包含 `n-1` 个 context Token 和 1 个待预测 Token。

### 误区 2：N-gram 近似就是概率链式法则

错误。链式法则精确成立；截断历史才是近似。

### 误区 3：某 trigram 的 MLE 概率为 1，说明这是语言定律

错误。它可能只出现过一次。MLE 只复述当前样本频率，不表达估计置信度。

### 误区 4：句子概率只乘正文 Token，不需要 EOS

错误。若不预测 EOS，模型没有评价“在这里结束”的概率，也不能正确比较不同结束位置。

### 误区 5：Token 都在词表中，句子概率就一定非零

错误。某个相邻 N-gram 没出现过，未平滑模型仍会给整句零概率。

### 误区 6：高阶 N-gram 一定优于低阶 N-gram

错误。更长 context 提供更多局部信息，也需要更多数据；有限语料中可能更稀疏、更易记忆。

### 误区 7：把 padding 计入语料没有影响

错误。它会制造大量批处理格式事件，污染结束概率和真实转移计数。

## 12. 练习与面试准备

### 12.1 30 秒回答

> N-gram 语言模型用最近 `n-1` 个 Token 近似全部历史，并通过训练语料计数估计下一个 Token 的条件概率：`P(w|h)=C(h,w)/C(h)`。整句概率由包含 BOS/EOS 的各步条件概率连乘得到。增大 `n` 能保留更具体的局部上下文，但组合数增长、数据更稀疏；未平滑模型遇到未见 N-gram 会给出零概率。Tokenizer、边界标记和数据划分必须在训练与推理中保持一致。

### 12.2 递进练习与面试题

#### 问题 1：bigram 与 trigram 各使用多少历史 Token？

<details>

<summary>查看答案</summary>

bigram 事件含 2 个 Token，用前 1 个预测后 1 个；trigram 事件含 3 个 Token，用前 2 个预测第 3 个。一般 `n`-gram 使用 `n-1` 个历史 Token。

</details>

#### 问题 2：为什么 MLE 分母是 `C(h)`？

<details>

<summary>查看答案</summary>

目标是在固定 context `h` 的所有后继事件中计算相对频率。`C(h,w)` 是其中后继为 `w` 的次数，`C(h)` 是该 context 的全部后继次数，所以对所有观测后继求和后概率为 1。

</details>

#### 问题 3：为什么 trigram 句首通常要放两个 `<s>`？

<details>

<summary>查看答案</summary>

trigram 预测需要两个历史位置。放两个 `<s>` 后，第一个正文 Token 也能对应一个完整二词 context `(<s>,<s>)`；第二个正文 Token 对应 `(<s>,w1)`。

</details>

#### 问题 4：已知每个 Token 都在词表中，为什么整句概率仍可能为 0？

<details>

<summary>查看答案</summary>

词表覆盖只保证单个 Token 合法，不保证它们的相邻组合在训练集出现。未平滑 MLE 对任何计数为 0 的 N-gram 给出条件概率 0，连乘后整句概率也为 0。

</details>

#### 问题 5：如何发现 N-gram 实验发生了数据泄漏？

<details>

<summary>查看答案</summary>

检查 Tokenizer/词表和 N-gram 计数是在划分前还是划分后拟合；检查近重复文档、同一用户或同一模板是否跨 train/test；比较去重前后未见 N-gram 率和评价变化。保存划分规则、语料哈希与计数构建日志作为证据。

</details>

### 12.3 手算题：包含 EOS 的句子概率

训练语料只有：

```
<s> / 我 / 学习 / </s>  × 2
<s> / 你 / 学习 / </s>  × 1
```

用未平滑 bigram 计算“我 学习”的完整句子概率。

<details>

<summary>查看答案</summary>

共有 3 个句首事件，其中 2 次后继为“我”，所以 `P(我|<s>)=2/3`。两次“我”后面都是“学习”，所以 `P(学习|我)=1`。三次“学习”后面都是 EOS，所以 `P(</s>|学习)=1`。完整概率为：

`P(我, 学习) = P(我|<s>) P(学习|我) P(</s>|学习)= 2/3`

</details>

### 12.4 手写题：修复错误分母

下面代码错误地用整个语料 Token 数作分母：

```python
# 错误：整个语料的 Token 总数不是固定 context 下的归一化分母。
def wrong_probability(context, token, pair_counts, total_tokens):
    return pair_counts[(context, token)] / total_tokens
```

请改成 bigram MLE，并处理未见 context。

<details>

<summary>查看参考答案</summary>

```python
def bigram_probability(
    context: str,
    token: str,
    pair_counts: Counter,
    context_counts: Counter,
) -> float:
    """按同一 context 的全部后继次数归一化。"""
    denominator = context_counts[context]
    if denominator == 0:
        # 未平滑模型无法从未见 context 估计分布。
        return 0.0
    return pair_counts[(context, token)] / denominator
```

分母必须是 `C(context)`，否则固定 context 下所有后继概率通常不会和为 1。

</details>

### 12.5 项目答辩题：高阶模型训练概率很好，测试全是零

一个中文领域项目从 bigram 升到 5-gram 后，训练集平均句子概率显著提高，但测试集中 92% 的句子概率为 0。你会怎样诊断？

<details>

<summary>查看答题要点</summary>

先确认训练/测试使用同一 Tokenizer、规范化、BOS/EOS 和 OOV 规则，再统计不同阶数在测试集的未见 context 与未见 N-gram 比例，并按领域、句长和稀有 Token 切片。检查训练集是否重复模板过多，以及划分是否按文档或来源隔离。5-gram 训练概率高可能只是记住局部片段；应以开发集选择阶数，并在 Day 009 引入回退、插值或平滑。答辩证据至少包括各阶覆盖率、零概率句比例、存储量和后续评价指标，而不是只展示训练概率。

</details>

### 12.6 基础自测

<details>

<summary>1. unigram 是否完全没有 context？</summary>

是。`n=1` 时保留 `n-1=0` 个历史 Token，直接用 Token 在训练语料中的边际频率估计概率。实现中可以用空元组 `()` 表示唯一 context。

</details>

<details>

<summary>2. 对固定已见 context，所有未平滑后继概率为什么和为 1？</summary>

因为所有 `C(h,w)` 对后继 `w` 求和就是 `C(h)`；分别除以同一个 `C(h)` 后总和为 1。

</details>

<details>

<summary>3. `P(自然语言|你,喜欢)=1` 能否说明模型非常确定？</summary>

不能。当前语料中“你 喜欢”只出现一次，1.0 只是一次观测的相对频率。应同时查看计数、数据覆盖与平滑后的估计。

</details>

<details>

<summary>4. 为什么使用对数概率不会改变两个句子的概率大小顺序？</summary>

对数函数单调递增；若两个概率都大于 0，较大的原概率仍有较大的对数概率。对数把连乘变为相加，改善数值稳定性。

</details>

## 13. 权威资料与延伸阅读

1. [Speech and Language Processing，第 3 章：N-gram Language Models](https://web.stanford.edu/~jurafsky/slp3/ed3book.pdf)\
   语言模型、链式法则、Markov 近似、MLE、句子边界与 N-gram 稀疏性的系统教材来源。
2. [Stanford CS124：N-gram Language Modeling](https://web.stanford.edu/class/cs124/lec/)\
   与教材配套的课程材料，用于复习计数、概率和生成过程。
3. [Claude Shannon：A Mathematical Theory of Communication](https://onlinelibrary.wiley.com/doi/abs/10.1002/j.1538-7305.1948.tb00917.x)\
   从概率过程和信息量理解语言建模的经典理论来源；交叉熵与信息论联系将在后续讲义展开。
4. [Python `collections.Counter`](https://docs.python.org/3/library/collections.html#collections.Counter)\
   本节计数实现所用标准库容器的官方说明。

## 一页回顾

```
语言模型：
给定历史，输出下一个 Token 的概率分布
等价地，也能为整段 Token 序列赋概率

精确链式法则：
P(w1...wT) = Π P(wi | w1...w(i-1))

N-gram 近似：
只保留最近 n-1 个历史 Token
unigram 看 0 个
bigram 看 1 个
trigram 看 2 个

未平滑 MLE：
P(w | h) = C(h,w) / C(h)

句子边界：
左侧加入 n-1 个 <s>
右侧预测一个 </s>
整句概率必须包含结束事件

数值计算：
条件概率连乘得到整句概率
对数把连乘变成相加，避免长序列下溢

核心权衡：
n 更大 → 局部上下文更具体
         → 唯一组合更多、数据更稀疏

本节边界：
未见 N-gram → 概率 0
Day 009 学习平滑与回退
Day 010 学习交叉熵与困惑度

工程底线：
训练与推理使用同一 Tokenizer、边界和 OOV 规则
去掉 PAD 后再计数
先划分数据，再只用训练集统计
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://inwt233.gitbook.io/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-02/day-008.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
