一文读懂循环神经网络(RNN)—语言模型+n元语法(1)
目录
什么是语言模型?
语言模型的核心目的
一.量化文本的合理性
二.支持下游 NLP 任务
三. 语义和上下文依赖
一元语法、二元语法和三元语法详解
核心概念:n-gram 模型
1. 一元语法(Unigram)
2. 二元语法(Bigram)
3. 三元语法(Trigram)
n-gram 模型的共性问题与扩展
总结
停用词
1.停用词的特点
2.常见的停用词类型(以中英文为例)
3.为什么要去除停用词?
马尔可夫模型与n元语法
完整代码
实验结果
词频图
一元/二元/三元语法的词频分布对比
什么是语言模型?
语言模型(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元语法
完整代码
"""
文件名: 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>固定在索引0self.idx_to_token = ['<unk>'] + reserved_tokensself.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 # 新索引为当前长度-1def __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] # 索引列表@propertydef unk(self):"""未知词元的索引(固定为0)"""return 0@propertydef token_freqs(self):"""返回词元频率列表"""return self._token_freqsdef 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列表为1Dreturn 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越大,频率下降越快,符合短距离依赖)