目录
[二.支持下游 NLP 任务](#二.支持下游 NLP 任务)
[三. 语义和上下文依赖](#三. 语义和上下文依赖)
[核心概念:n-gram 模型](#核心概念:n-gram 模型)
[1. 一元语法(Unigram)](#1. 一元语法(Unigram))
[2. 二元语法(Bigram)](#2. 二元语法(Bigram))
[3. 三元语法(Trigram)](#3. 三元语法(Trigram))
[n-gram 模型的共性问题与扩展](#n-gram 模型的共性问题与扩展)
什么是语言模型?
语言模型(Language Model, LM)是自然语言处理(NLP)中的核心技术,它的本质是对语言规律的数学建模------ 通过学习文本数据中的模式,预测 "一段文本序列出现的概率",或在给定前文的情况下预测 "下一个词 / 字符出现的概率"。
简单来说,语言模型的核心能力是判断 "一句话是否通顺" ,以及预测 "接下来会说什么"。例如:
- 对于句子 "我想喝____",语言模型能预测 "水""咖啡""茶" 等词的概率(其中 "水" 的概率通常最高);
- 对于句子 "天空是____色的",模型会给 "蓝" 赋予远高于 "绿""紫" 的概率。
语言模型的核心目的
语言模型的核心目的是捕捉语言的统计规律和语义逻辑,从而实现对自然语言的理解与生成。具体可拆解为以下几个目标:
一.量化文本的合理性
通过计算文本序列的概率,判断其是否符合人类语言习惯。例如,"猫在追老鼠" 的概率远高于 "老鼠在追猫"(在无特殊语境下),语言模型能通过概率差异体现这种合理性。
二.支持下游 NLP 任务
作为基础组件,语言模型为其他任务提供 "语言知识":
- 机器翻译:预测 "目标语言句子" 与 "源语言句子" 的匹配概率;
- 文本生成:按概率生成通顺的句子(如写诗、写代码、聊天机器人回复);
- 语音识别:从语音转写的多个候选文本中,选择概率最高的合理结果;
拼写纠错:对输入的错误文本,预测最可能的正确形式(如 "我去公圆"→"我去公园")。
三. 语义和上下文依赖
- "苹果很好吃" 中的 "苹果" 指水果;
- "苹果发布了新手机" 中的 "苹果" 指公司。
语言模型通过上下文建模,能区分这两种含义。
高级语言模型(如 Transformer、BERT、GPT 系列)能捕捉词与词之间的上下文关系,理解歧义、多义词在不同语境下的含义。例如:
实现无监督 / 半监督学习
语言模型可以仅通过海量文本(无需人工标注)学习语言规律,降低对标注数据的依赖。例如,GPT 系列通过 "预测下一个词" 的无监督任务,就能在对话、写作等任务中表现出强大能力。
一元语法、二元语法和三元语法详解
一元语法(Unigram)、二元语法(Bigram)和三元语法(Trigram)是基于n-gram 模型 的基础概念,用于描述文本中词元(token)之间的序列关系。它们通过假设 "一个词的出现仅与前 n-1 个词相关",简化了语言的概率建模过程,是早期语言模型的核心技术。
核心概念:n-gram 模型
n-gram 模型的核心思想是:将文本序列拆分为连续的 n 个词元组成的片段(n-gram),并通过统计这些片段的出现频率来计算句子的概率。 例如,对于句子 "我喜欢自然语言处理",其 n-gram 片段为:
- 一元语法(1-gram):["我", "喜欢", "自然", "语言", "处理"]
- 二元语法(2-gram):["我 喜欢", "喜欢 自然", "自然 语言", "语言 处理"]
- 三元语法(3-gram):["我 喜欢 自然", "喜欢 自然 语言", "自然 语言 处理"]
1. 一元语法(Unigram)
定义:仅考虑单个词元的概率,忽略词与词之间的依赖关系,假设每个词的出现是独立的。
概率计算 : 对于句子
,其概率为所有词元概率的乘积:
其中,
是词
在语料库中出现的频率(即
/ 总词数)。
示例 : 句子 "猫吃鱼" 的概率 = P(猫)
P(吃)
P(鱼)。
优缺点:
- 优点:计算简单,数据需求量小,泛化能力强(很少出现未见过的词)。
- 缺点:完全忽略上下文关系,合理性差(例如 "猫吃鱼" 和 "鱼吃猫" 的概率相同)。
2. 二元语法(Bigram)
定义:假设一个词的出现仅依赖于前一个词,即考虑两个连续词元的概率。
概率计算: 句子 S 的概率通过条件概率链表示:
其中,条件概率
近似为两个词同时出现的频率(即
)。
示例 : 句子 "猫吃鱼" 的概率 = P(猫)
P(吃|猫)
P(鱼|吃)。
优缺点:
- 优点:考虑了相邻词的依赖关系,比一元语法更合理(例如 "猫吃鱼" 的概率远高于 "鱼吃猫")。
- 缺点:仅依赖前一个词,长距离上下文(如 "猫喜欢吃鱼" 中 "喜欢" 对 "鱼" 的影响)被忽略;可能出现未见过的二元组合(如罕见短语)。
3. 三元语法(Trigram)
定义:假设一个词的出现依赖于前两个词,即考虑三个连续词元的概率。
概率计算: 句子 S 的概率为:
其中,
。
示例 : 句子 "猫喜欢吃鱼" 的概率 = P(猫)
P(喜欢|猫)
P(吃|猫, 喜欢)
P(鱼|喜欢, 吃)。
优缺点:
- 优点:比二元语法更贴近实际语言规律,能捕捉更丰富的局部上下文(例如 "喜欢吃" 后面更可能接 "鱼" 而非 "石头")。
- 缺点:
- 对数据量需求大,容易出现 "数据稀疏" 问题(很多三元组合在语料库中从未出现,导致概率为 0)。
- 计算复杂度高于一元 / 二元语法,存储成本更高(需要记录大量三元组合)。
n-gram 模型的共性问题与扩展
数据稀疏性: n 越大,需要的训练数据越多,否则会出现大量未见过的 n-gram(称为 "未登录词问题")。例如,三元语法比二元语法更容易遇到 "count=0" 的情况。
- 解决方法:通过 "平滑技术"(如拉普拉斯平滑)给未见过的 n-gram 赋予一个极小的概率。
n 的选择:
- n 越小:计算越高效,泛化能力越强,但忽略的上下文越多。
- n 越大:捕捉的上下文越丰富,但数据需求和计算成本越高,且容易过拟合(依赖罕见组合)。 实际应用中,n 通常取 2(Bigram)或 3(Trigram),极少超过 5。
总结
模型 核心假设 优点 缺点 一元语法 词独立出现 简单、泛化强 忽略上下文,合理性差 二元语法 依赖前一个词 捕捉相邻依赖,较合理 忽略长距离上下文 三元语法 依赖前两个词 捕捉局部上下文,更合理 数据稀疏,计算成本高
停用词
停用词(Stop Words) 指的是在文本中频繁出现,但通常对文本的核心语义贡献较小的词语。这些词语由于使用过于普遍,往往被认为在文本分析、情感识别、主题提取等任务中 "信息量较低",因此会被提前过滤掉,以简化处理流程并提升模型效率。
1.停用词的特点
- 高频性:在语言中出现频率极高,比如英语中的 "the""and""is",中文中的 "的""是""在" 等。
- 语义弱化:本身没有明确的实义,多为辅助性词汇(如介词、连词、助词、代词等),单独出现时难以表达具体含义。
- 通用性:在不同主题、不同领域的文本中均大量存在,不具备区分文本特征的能力。
2.常见的停用词类型(以中英文为例)
语言 停用词类型 示例 英语 冠词、介词、连词、代词等 the, a, an, in, on, and, or, he, she 中文 助词、连词、介词、代词等 的、地、得、在、和、与、他、她、它 3.为什么要去除停用词?
- 减少数据量:停用词通常占文本总词数的 30%-50%,过滤后可大幅降低数据规模,提升模型训练和推理速度。
- 聚焦核心信息:过滤掉冗余词汇后,剩余词语更能反映文本的核心主题(如 "机器学习""自然语言处理" 等实义词),帮助模型更精准地捕捉语义。
- 降低噪声干扰:高频且无实义的停用词可能会干扰模型对关键特征的学习(例如,在文本分类任务中,"的" 出现次数再多也无法区分 "科技" 和 "体育" 主题)。
马尔可夫模型与n元语法
完整代码
python
"""
文件名: 8.3 语言模型和数据集
作者: 墨尘
日期: 2025/7/14
项目名: dl_env
备注: 实现语言模型的基础数据处理流程,包括文本读取、词元化、词表构建,并分析一元/二元/三元语法的频率分布
"""
import random
import torch
import collections # 用于统计词频
import re # 用于文本清洗
from d2l import torch as d2l # 提供数据下载、绘图等工具
# 手动显示图像相关库
import matplotlib.pyplot as plt # 绘图库
import matplotlib.text as text # 用于修改文本绘制(解决符号显示问题)
# -------------------------- 核心解决方案:解决文本显示问题 --------------------------
def replace_minus(s):
"""
解决Matplotlib中Unicode减号(U+2212)显示异常的问题
参数:
s: 待处理的字符串或其他类型对象
返回:
处理后的字符串(替换减号)或原始对象(非字符串类型)
"""
if isinstance(s, str): # 仅处理字符串
return s.replace('\u2212', '-') # 替换特殊减号为普通减号
return s # 非字符串直接返回
# 重写matplotlib的Text类的set_text方法,全局修复减号显示
original_set_text = text.Text.set_text # 保存原始方法
def new_set_text(self, s):
s = replace_minus(s) # 处理减号
return original_set_text(self, s) # 调用原始方法设置文本
text.Text.set_text = new_set_text # 应用重写后的方法
# -------------------------- 字体配置(确保中文和数学符号正常显示)--------------------------
plt.rcParams["font.family"] = ["SimHei"] # 设置中文字体(支持中文显示)
plt.rcParams["text.usetex"] = True # 使用LaTeX渲染文本(提升数学符号美观度)
plt.rcParams["axes.unicode_minus"] = True # 确保负号正确显示(避免方块)
plt.rcParams["mathtext.fontset"] = "cm" # 数学符号使用Computer Modern字体
d2l.plt.rcParams.update(plt.rcParams) # 让d2l库的绘图工具继承配置
# -------------------------- 关键修复:提前注册数据集信息 --------------------------
# 注册《时间机器》数据集到d2l的DATA_HUB(必须在read_time_machine函数前)
d2l.DATA_HUB['time_machine'] = (
d2l.DATA_URL + 'timemachine.txt', # 数据集下载地址
'090b5e7e70c295757f55df93cb0a180b9691891a' # 哈希校验值(确保文件完整)
)
# -------------------------- 1. 读取数据集 --------------------------
def read_time_machine(): # @save
"""
读取《时间机器》文本数据集并清洗
步骤:
1. 下载并打开文本文件
2. 清洗文本:保留字母,其他字符替换为空格,转小写,去首尾空格
返回:
清洗后的文本行列表(非空行)
"""
with open(d2l.download('time_machine'), 'r') as f: # 下载并读取文件
lines = f.readlines() # 按行读取
# 正则清洗:只保留A-Za-z,其他替换为空格,再转小写并去首尾空格
return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
# -------------------------- 2. 词元化(Tokenization) --------------------------
def tokenize(lines, token='word'): # @save
"""
将文本行分割为词元(单词或字符)
参数:
lines: 清洗后的文本行列表(如["the time machine", ...])
token: 词元类型,'word'按单词分割,'char'按字符分割
返回:
词元列表的列表(每行对应一个词元列表)
"""
if token == 'word':
return [line.split() for line in lines] # 按空格分割为单词
elif token == 'char':
return [list(line) for line in lines] # 按字符分割
else:
print('错误:未知词元类型:' + token)
# -------------------------- 3. 词表(Vocabulary) --------------------------
class Vocab: #@save
"""
文本词表:映射词元到整数索引,支持词元与索引的双向转换
属性:
idx_to_token: 索引→词元的列表(如[<unk>, 'a', 'b', ...])
token_to_idx: 词元→索引的字典(如{'<unk>':0, 'a':1, ...})
_token_freqs: 词元频率列表(按频率降序)
"""
def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
"""
初始化词表
参数:
tokens: 词元列表(可嵌套,如[["a","b"], ["c"]])
min_freq: 最小词频阈值,低于此值的词元不加入词表
reserved_tokens: 预留特殊词元(如['<pad>', '<bos>'])
"""
if tokens is None:
tokens = []
if reserved_tokens is None:
reserved_tokens = []
# 统计词频并按频率降序排序
counter = count_corpus(tokens) # 展平词元列表并计数
self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True)
# 初始化词表:未知词元<unk>固定在索引0
self.idx_to_token = ['<unk>'] + reserved_tokens
self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)}
# 加入高频词元(过滤低频词)
for token, freq in self._token_freqs:
if freq < min_freq:
break # 因已排序,后续词元频率更低,直接停止
if token not in self.token_to_idx: # 避免重复加入预留词元
self.idx_to_token.append(token)
self.token_to_idx[token] = len(self.idx_to_token) - 1 # 新索引为当前长度-1
def __len__(self):
"""返回词表大小(词元总数)"""
return len(self.idx_to_token)
def __getitem__(self, tokens):
"""
词元→索引转换(支持单个词元或列表)
参数:
tokens: 单个词元(如"a")或词元列表(如["a","b"])
返回:
对应的索引(或列表),未知词元返回<unk>的索引(0)
"""
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk) # 单个词元
return [self.__getitem__(token) for token in tokens] # 词元列表
def to_tokens(self, indices):
"""
索引→词元转换(支持单个索引或列表)
参数:
indices: 单个索引(如1)或索引列表(如[1,2])
返回:
对应的词元(或列表)
"""
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices] # 单个索引
return [self.idx_to_token[index] for index in indices] # 索引列表
@property
def unk(self):
"""未知词元的索引(固定为0)"""
return 0
@property
def token_freqs(self):
"""返回词元频率列表"""
return self._token_freqs
def count_corpus(tokens): #@save
"""
统计词元频率(展平嵌套列表)
参数:
tokens: 1D或2D词元列表(如["a","b"]或[["a","b"], ["c"]])
返回:
collections.Counter: 词元频率计数器
"""
if len(tokens) == 0 or isinstance(tokens[0], list):
tokens = [token for line in tokens for token in line] # 展平2D列表为1D
return collections.Counter(tokens) # 计数每个词元的出现次数
# -------------------------- 4. 整合预处理流程 --------------------------
def load_corpus_time_machine(max_tokens=-1): #@save
"""
加载《时间机器》数据集,返回字符级语料库和词表
参数:
max_tokens: 最大词元数,-1表示使用全部
返回:
corpus: 词元索引序列(1D列表)
vocab: 字符级词表
"""
lines = read_time_machine() # 读取清洗后的文本
tokens = tokenize(lines, 'char') # 按字符分割词元
vocab = Vocab(tokens) # 构建字符级词表
# 展平所有词元为索引序列
corpus = [vocab[token] for line in tokens for token in line]
if max_tokens > 0:
corpus = corpus[:max_tokens] # 截断到最大长度
return corpus, vocab
# -------------------------- 5. 测试代码:分析n-gram频率 --------------------------
if __name__ == '__main__':
# 步骤1:读取并查看原始文本
lines = read_time_machine()
print(f'# 文本总行数: {len(lines)}') # 输出清洗后的总行数(如3221)
print("第0行文本:", lines[0]) # 输出:'the time machine by h g wells'
print("第10行文本:", lines[10]) # 输出:'twinkled and his usually pale face was flushed and animated'
# 步骤2:分析一元语法(unigram)的高频词
tokens = tokenize(read_time_machine()) # 单词级词元化
corpus = [token for line in tokens for token in line] # 展平为1D词元列表
vocab = Vocab(corpus) # 基于单词构建词表
print("\n前10个高频单词(一元语法):", vocab.token_freqs[:10]) # 如[('the', 2261), ('of', 1267), ...]
# 步骤3:绘制一元语法的词频分布(对数坐标)
freqs = [freq for token, freq in vocab.token_freqs] # 提取所有词元的频率
d2l.plot(
freqs,
xlabel='token: x', # x轴:词元(按频率排序)
ylabel='frequency: n(x)', # y轴:频率
xscale='log', yscale='log' # 双对数坐标(符合齐夫定律)
)
plt.show(block=True) # 显示图像(词频随排名下降,符合幂律分布)
# 步骤4:分析二元语法(bigram)的高频词对
# 生成连续词对(如"the time"→("the", "time"))
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = Vocab(bigram_tokens) # 基于词对构建词表
print("\n前10个高频词对(二元语法):", bigram_vocab.token_freqs[:10]) # 如[('of', 'the'), 130), ...]
# 步骤5:分析三元语法(trigram)的高频词 triples
# 生成连续三个词(如"the time machine"→("the", "time", "machine"))
trigram_tokens = [triple for triple in zip(corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = Vocab(trigram_tokens) # 基于三词组构建词表
print("\n前10个高频三词组(三元语法):", trigram_vocab.token_freqs[:10]) # 如[('in', 'the', 'year'), 20), ...]
# 步骤6:对比一元/二元/三元语法的词频分布
bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs] # 二元频率
trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs] # 三元频率
d2l.plot(
[freqs, bigram_freqs, trigram_freqs], # 三条频率曲线
xlabel='token: x',
ylabel='frequency: n(x)',
xscale='log', yscale='log',
legend=['unigram', 'bigram', 'trigram'] # 图例
)
plt.show(block=True) # 显示图像(n越大,频率下降越快,符合短距离依赖)
实验结果
词频图

一元/二元/三元语法的词频分布对比
