神经概率语言模型(NPLM):让机器学会“理解”语言 精华

发布于 2025-8-21 08:51
浏览
0收藏

本文介绍了神经概率语言模型(NPLM)的基本原理、结构与实现方法。通过对比传统N-gram模型,阐述了NPLM如何利用神经网络自动学习词语间的深层关系,有效提升语言建模的泛化能力。内容涵盖数据准备、模型搭建、训练流程及推理预测。

1. 什么是神经概率语言模型NPLM

2. 为什么需要神经语言模型

2.1 传统语言模型的问题

2.2 神经概率语言模型(NPLM)的诞生

3. 模型架构详解

4. 完整代码实现

4.1 构建语料库

4.2 生成训练数据

4.3 模型定义

4.3 实例化NPLM模型

4.3 NPLM模型训练

5. 训练过程详解

5.1 训练数据的准备

5.2 训练的四个关键步骤

5.2.1 前向传播(Forward Pass)

5.2.2 计算损失(Loss Calculation)

5.2.3 反向传播(Backward Pass)

5.2.4 参数更新(Parameter Update)

6. 训练结果

7. 模型预测

8. 完整的数据流程图

1. 什么是神经概率语言模型NPLM

神经概率语言模型(Neural Probabilistic Language Models, NPLM)听起来很复杂,但我们可以把它拆解来理解:

什么是语言模型?

语言模型就像一个"语言预测器"。当你在手机上打字时,输入法能够预测你下一个要输入的词,这背后就是语言模型在工作。比如:

• 你输入"今天天气",模型预测下一个词可能是"很好"、"不错"、"糟糕"等

• 你输入"人工智能",模型预测下一个词可能是"技术"、"发展"、"应用"等

什么是概率?

概率就是某件事发生的可能性。在语言模型中,我们用概率来表示某个词出现的可能性。比如:

• 在"今天天气"后面,"很好"出现的概率可能是30%

• "不错"出现的概率可能是25%

• "糟糕"出现的概率可能是15%

什么是神经网络?

神经网络是一种模仿人脑工作方式的计算模型。就像人脑通过神经元连接来处理信息一样,神经网络通过多层计算节点来学习复杂的模式。

2. 为什么需要神经语言模型

2.1 传统语言模型的问题

在神经概率语言模型(NPLM)出现之前,N-gram模型自20世纪80年代以来一直是概率语言建模的主流范式 。

相关阅读:​​​入门GPT(一)| N-Gram 带你了解自然语言处理(1)​

传统N-gram模型的局限性:

N-gram模型通过马尔可夫假设简化了语言建模的复杂性后,假设一个词的出现概率仅依赖于其前 n−1 个词。

一个三元语法模型在预测下一个词时,只会考虑紧邻的两个前文词语 。当训练数据中出现未曾见过的词序列时,N-gram模型通常通过“平滑”和“回退”等技术来分配一个非零概率 。

然而,这种传统方法存在显著的局限性。N-gram模型通常只能考虑非常短的上下文窗口,例如1或2个词,即使在某些情况下可扩展到5个词,但由于数据稀疏性,大多数预测仍依赖于一个比较短的上下文 。

更重要的是,N-gram模型缺乏对词语之间语义或语法相似性的理解 。这意味着,如果模型在训练集中见过“猫在卧室里走路”,它无法将这些知识自然地泛化到“狗在房间里跑”,因为在N-gram的离散表示中,“猫”和“狗”被视为完全不相关的实体。

这种局限性导致训练数据中的一个句子无法有效地为模型提供关于其语义邻近句子的信息,从而限制了模型的泛化能力。

维度爆炸:

统计语言建模的一个很大的问题在于“维度爆炸” 。

当试图建模大量离散随机变量(如句子中的词语)的联合分布时,这个问题尤为突出 。随着输入变量数量的增加,模型所需的训练样本数量呈指数级增长 。

例如,如果要对自然语言中连续十个单词的联合分布建模,而词汇表大小是100,000,那么理论上就会有  个自由参数需要考虑。

在离散空间中,泛化并不像连续变量那样有优势,因为离散变量的任何改变都可能对函数值产生剧烈影响 。这意味着模型在测试时遇到的词序列,与训练期间见过的所有词序列很可能不同,从而导致模型难以对未见过的组合进行准确的概率估计 。

传统N-gram模型会为词汇表中的每个词分配一个高维稀疏向量,其维度与词汇量大小相同 。

N-gram模型采用局部、离散的统计方法,最终限制了其泛化能力和对语言深层语义的理解。

2.2 神经概率语言模型(NPLM)的诞生

神经概率语⾔模型(Neural Probabilistic Language Model,NPLM)的起源和发展可以追溯到2003年,当时约书亚·本希奥及其团队发表了⼀篇题为“A Neural Probabilistic Language Model”(《⼀种神经概率语⾔模型》)的论⽂.

这项工作标志着语言建模领域的一个重要转折点,因为它首次将神经网络引入到语言建模中,并被认为是“第一个(也是最简单的)用于语言建模的神经网络架构” 。

NPLM的核心目标是解决传统N-gram模型面临的“维度爆炸”问题。

核心思想:分布式表示

NPLM提出通过学习词语的“分布式表示”来对抗维度爆炸 。

分布式表示是指为词汇表中的每个词关联一个连续值的向量。这些向量将词语映射到一个低维的特征空间中,其中功能相似的词语(无论是语义上还是语法上)在这个空间中彼此靠近 。

例如,如果模型学习了“猫”和“狗”在许多语境中可以互换而不改变句子的语法结构,它会学习到它们的嵌入向量应该很接近。

这种表示方式使得模型能够从每个训练句子中获取信息,并将其泛化到语义邻近句子上 。泛化的实现是因为一个从未见过的词序列,如果它由与已见过句子中的词语相似(即具有接近的表示)的词语组成,也能获得高概率 。

NPLM模型同时学习两个关键部分:1) 每个词的分布式表示,以及 2) 基于这些表示的词序列概率函数 。这种同步学习是NPLM成功的关键。

模型通过一个统一的神经网络架构来学习从输入词到输出概率的整个映射。通过将离散的词语映射到连续的向量空间,NPLM有效地将高维离散空间中的数据稀疏性问题,转化为低维连续空间中的函数平滑性问题 。

这使得模型能够泛化到未见过的组合,因为“相似的词语预计会有相似的特征向量,并且概率函数是这些特征值的平滑函数” 。

NPLM的出现不仅仅是对N-gram模型的简单改进,它代表了语言建模领域从纯粹的统计方法向基于神经网络的表征学习的根本性范式转变。这种转变不仅有效解决了维度爆炸,更为后续所有现代神经网络语言模型(包括Word2Vec、RNN、Transformer等)奠定了理论和实践基础。

3. 模型架构详解

神经概率语言模型(NPLM):让机器学会“理解”语言-AI.x社区

来自《A Neural Probabilistic Language Model》

• 图中的矩阵C⽤于将输⼊的词(Context,即之前的N个词,也被称为上下⽂词)映射到⼀个连续的向量空间。这个过程在论⽂中称为“table look-up”,因为我们可以将矩阵C视为⼀张查找表,通过查找表可以将输⼊词的索引(或One-Hot编码)转换为其对应的词向量表示。矩阵C的参数在所有词之间共享。这意味着,对于所有的输⼊词,都使⽤相同的矩阵C来获取它们的词向量表示。这有助于减少模型的参数数量,提⾼模型的泛化能⼒。

• 通过矩阵C会得到⼀组词向量,此时需要对输⼊向量进⾏线性变换(即矩阵乘法和偏置加法),然后将其输⼊隐藏层,进⾏上下⽂语义信息的学习。因为论⽂发表时间较早,所以隐藏层使⽤双曲正切(tanh)函数作为⾮线性激活函数,⽽不是后来常⻅的 ReLU 函数。在这篇论⽂发表的2003年,算⼒还不是很强,所以论⽂特别注明:这⼀部分的计算量通常较⼤。

• 输出层通常是⼀个全连接层,⽤于计算给定上下⽂条件下每个词的概率。图中 “第个输出” 这句话描述了NPLM的输出⽬标。对于每个词 ,模型计算在给定上下⽂条件下,⽬标词汇 (也就是下⼀个词)是词汇表中第  个词的概率。此处应⽤​​softmax​​函数将输出层的值转换为概率分布,这也是后来神经⽹络的分类输出层的标准做法。

代码:

class NPLM(nn.Module):
    def__init__(self, voc_size, n_step, embedding_size, n_hidden):
        super(NPLM, self).__init__() 
        self.n_step = n_step  # 保存n_step用于forward方法
        self.C = nn.Embedding(voc_size, embedding_size) # 定义一个词嵌入层
        # 第一个线性层,其输入大小为 n_step * embedding_size,输出大小为 n_hidden
        self.linear1 = nn.Linear(n_step * embedding_size, n_hidden) 
        # 第二个线性层,其输入大小为 n_hidden,输出大小为 voc_size,即词汇表大小
        self.linear2 = nn.Linear(n_hidden, voc_size) 
    defforward(self, X):  # 定义前向传播过程
        # 输入数据 X 张量的形状为 [batch_size, n_step]
        X = self.C(X)  # 将 X 通过词嵌入层,形状变为 [batch_size, n_step, embedding_size]        
        X = X.view(-1, self.n_step * self.C.embedding_dim) # 形状变为 [batch_size, n_step * embedding_size]
        # 通过第一个线性层并应用 tanh 激活函数
        hidden = torch.tanh(self.linear1(X)) # hidden 张量形状为 [batch_size, n_hidden]
        # 通过第二个线性层得到输出 
        output = self.linear2(hidden) # output 形状为 [batch_size, voc_size]
        return output # 返回输出结果

这⾥定义了⼀个名为 “NPLM” 的神经概率语⾔模型类,它继承⾃ ​​PyTorch​​​ 的 ​​nn.Module​​ 。在这个类中,我们定义了词嵌⼊层和线性层

​self.C​⼀个词嵌⼊层,⽤于将输⼊数据中的每个词转换为固定⼤⼩的向量表示。​​voc_size​​​ 表示词汇表⼤⼩,​​embedding_size​​ 表示词嵌⼊的维度。

​self.linear1​ 第⼀个线性层,不考虑批次的情况下输⼊⼤⼩为 ​​n_step * embedding_size​​​,输出⼤⼩为 ​​n_hidden​​​。​​n_step​​​ 表示时间步数,即每个输⼊序列的⻓度;​​embedding_size​​​ 表示词嵌⼊的维度;​​n_hidden​​ 表示隐藏层的⼤⼩。

​self.linear2​ 第⼆个线性层,不考虑批次的情况下输⼊⼤⼩为 ​​n_hidden​​​,输出⼤⼩为 ​​voc_size​​​。​​n_hidden​​​ 表示隐藏层的⼤⼩,​​voc_size​​表示词汇表⼤⼩。

在NPLM类中,我们还定义了⼀个名为 ​​forward​​​ 的⽅法,⽤于实现模型的前向传播过程。在这个⽅法中,⾸先将输⼊数据通过词嵌⼊层​​self.C​​​ ,然后 ​​X.view(-1, n_step * embedding_size)​​​ 的⽬的是在词嵌⼊维度上展平张量,也就是把每个输⼊序列的词嵌⼊连接起来,形成⼀个⼤的向量。接着,将该张量传⼊第⼀个线性层 ​​self.linear1​​​ 并应⽤ ​​tanh​​​ 函数,得到隐藏层的输出。最后,将隐藏层的输出传⼊第⼆个线性层 ​​self.linear2​​,得到最终的输出结果。

4. 完整代码实现

完整代码,在公众号「AI取经路」发消息「NPLM」获取

4.1 构建语料库

定义⼀个⾮常简单的数据集,作为实验语料库,并整理出该语料库的词汇表。

# 构建一个非常简单的数据集
sentences = ["我 喜欢 玩具", "我 爱 爸爸", "我 讨厌 挨打"] 
# 将所有句子连接在一起,用空格分隔成多个词,再将重复的词去除,构建词汇表
word_list = list(set(" ".join(sentences).split())) 
# 创建一个字典,将每个词映射到一个唯一的索引
word_to_idx = {word: idx for idx, word in enumerate(word_list)} 
# 创建一个字典,将每个索引映射到对应的词
idx_to_word = {idx: word for idx, word in enumerate(word_list)} 
voc_size = len(word_list) # 计算词汇表的大小
print(' 词汇表:', word_to_idx) # 打印词汇到索引的映射字典
print(' 词汇表大小:', voc_size) # 打印词汇表大小

词汇表: {'爸爸': 0, '爱': 1, '讨厌': 2, '挨打': 3, '喜欢': 4, '玩具': 5, '我': 6}

词汇表大小: 7

4.2 生成训练数据

从语料库中⽣成批处理数据,作为NPLM的训练数据,后⾯会将数据⼀批⼀批地输⼊神经⽹络进⾏训练。

# 构建批处理数据
import torch # 导入 PyTorch 库
import random # 导入 random 库

batch_size = 2# 每批数据的大小

defmake_batch():
    input_batch = []  # 定义输入批处理列表
    target_batch = []  # 定义目标批处理列表
    selected_sentences = random.sample(sentences, batch_size) # 随机选择句子

    for sen in selected_sentences:  # 遍历每个句子
        word = sen.split()  # 用空格将句子分隔成多个词
        # 将除最后一个词以外的所有词的索引作为输入
        input = [word_to_idx[n] for n in word[:-1]]  # 创建输入数据
        # 将最后一个词的索引作为目标
        target = word_to_idx[word[-1]]  # 创建目标数据
        input_batch.append(input)  # 将输入添加到输入批处理列表
        target_batch.append(target)  # 将目标添加到目标批处理列表

    input_batch = torch.LongTensor(input_batch) # 将输入数据转换为张量
    target_batch = torch.LongTensor(target_batch) # 将目标数据转换为张量
    
    return input_batch, target_batch  # 返回输入批处理和目标批处理数据

input_batch, target_batch = make_batch() # 生成批处理数据
print(' 词汇表:', word_to_idx) # 打印词汇到索引的映射字典
print(" 输入数据:",input_batch)  # 打印输入批处理数据

# 将输入批处理数据中的每个索引值转换为对应的原始词
input_words = []

for input_idx in input_batch:
    input_words.append([idx_to_word[idx.item()] for idx in input_idx])

print(" 输入数据对应的原始词:",input_words)
print(" 目标数据:",target_batch) # 打印目标批处理数据
# 将目标批处理数据中的每个索引值转换为对应的原始词
target_words = [idx_to_word[idx.item()] for idx in target_batch]
print(" 目标数据对应的原始词:",target_words)

词汇表: {'爸爸': 0, '爱': 1, '讨厌': 2, '挨打': 3, '喜欢': 4, '玩具': 5, '我': 6}

输入数据: tensor([[6, 2], [6, 4]]) 输入数据对应的原始词: [['我', '讨厌'], ['我', '喜欢']]

目标数据: tensor([3, 5]) 目标数据对应的原始词: ['挨打', '玩具']

4.3 模型定义

见第3章

4.3 实例化NPLM模型

n_step = 2 # 时间步数,表示每个输入序列的长度,也就是上下文长度 
n_hidden = 2 # 隐藏层大小
embedding_size = 2 # 词嵌入大小
model = NPLM(n_step, voc_size, embedding_size, n_hidden) # 创建神经概率语言模型实例
print(' NPLM 模型结构:', model) # 打印模型的结构

NPLM 模型结构: NPLM(

(C): Embedding(7, 2)

(linear1): Linear(in_features=4, out_features=2, bias=True)

(linear2): Linear(in_features=2, out_features=7, bias=True)

)

4.3 NPLM模型训练

训练设置:

import torch.optim as optim # 导入优化器模块
criterion = nn.CrossEntropyLoss() # 定义损失函数为交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=0.1) # 定义优化器为 Adam,学习率为 0.1

训练循环:

# 训练模型
for epoch in range(5000): # 设置训练迭代次数
   optimizer.zero_grad() # 清除优化器的梯度
   input_batch, target_batch = make_batch() # 创建输入和目标批处理数据
   output = model(input_batch) # 将输入数据传入模型,得到输出结果
   loss = criterion(output, target_batch) # 计算损失值
   if (epoch + 1) % 1000 == 0: # 每 1000 次迭代,打印损失值
     print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
   loss.backward() # 反向传播计算梯度
   optimizer.step() # 更新模型参数

5. 训练过程详解

5.1 训练数据的准备

我们使用的训练数据非常简单:

原始文本:

"我 喜欢 玩具"
"我 爱 爸爸" 
"我 讨厌 挨打"

转换为训练数据集:

上下文: ["我", "喜欢"]     → 目标: "玩具"
上下文: ["我", "爱"]       → 目标: "爸爸"
上下文: ["我", "讨厌"]     → 目标: "挨打"

核心思想: 给定前面的n_step个词汇(这里是2个),预测的下一个目标词汇

5.2 训练的四个关键步骤

5.2.1 前向传播(Forward Pass)

这一步计算模型的预测结果:

  • 输入处理:将上下文词汇转换为索引
  • 词嵌入:查找每个词汇对应的2维向量
  • 向量拼接:将2个词的向量拼接成4维向量
  • 隐藏层计算:通过tanh激活函数处理
  • 输出分数:得到每个词汇的原始分数

# 前向传播过程
output = model(input_batch)  # 形状: [1, 7]
# output 包含7个词汇的分数,对应词汇表中的每个词

5.2.2 计算损失(Loss Calculation)

使用交叉熵损失函数来衡量预测准确性:

代码实现:

import torch.optim as optim
criterion = nn.CrossEntropyLoss()  # 定义损失函数为交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=0.1)  # Adam优化器,学习率0.1

# 计算损失
input_batch, target_batch = make_batch()  # 获取批次数据
output = model(input_batch)  # 前向传播
loss = criterion(output, target_batch)  # 计算损失

5.2.3 反向传播(Backward Pass)

使用PyTorch的自动微分计算梯度:

代码实现:

optimizer.zero_grad()  # 清除之前的梯度
loss.backward()        # 反向传播计算梯度

5.2.4 参数更新(Parameter Update)

使用Adam优化器更新模型参数:

代码实现:

optimizer.step()  # 更新模型参数

6. 训练结果

模型训练过程如下:

Epoch: 1000 cost = 0.000403
Epoch: 2000 cost = 0.000120
Epoch: 3000 cost = 0.000055
Epoch: 4000 cost = 0.000028
Epoch: 5000 cost = 0.000016

损失快速下降:从初始的高损失快速降低到接近0

收敛良好:5000次迭代后损失稳定在很小的值

学习成功:模型成功学会了简单的语言模式

7. 模型预测

训练完成后,可以测试模型的预测能力:

# 进行预测
input_strs = [['我', '讨厌'], ['我', '喜欢']]  # 需要预测的输入序列
# 将输入序列转换为对应的索引
input_indices = [[word_to_idx[word] for word in seq] for seq in input_strs]
# 将输入序列的索引转换为张量
input_batch = torch.LongTensor(input_indices) 
# 对输入序列进行预测,取输出中概率最大的类别
predict = model(input_batch).data.max(1)[1]  
# 将预测结果的索引转换为对应的词
predict_strs = [idx_to_word[n.item()] for n in predict.squeeze()]  
for input_seq, pred in zip(input_strs, predict_strs):
   print(input_seq, '->', pred)  # 打印输入序列和预测结果

预期预测结果:

['我', '讨厌'] -> 挨打
['我', '喜欢'] -> 玩具

这表明模型成功学会了语言模式:

• 理解"我 讨厌"后面应该跟"挨打"

• 理解"我 喜欢"后面应该跟"玩具"

8. 完整的数据流程图

下图展示了NPLM模型的完整数据处理流程:

神经概率语言模型(NPLM):让机器学会“理解”语言-AI.x社区

详细步骤说明:

输入文本:["我", "喜欢"]
目标:预测下一个词

第1步:词汇索引化
["我", "喜欢"] → [2, 5]  # 根据词汇表映射

第2步:词嵌入(每个词2维)
[2, 5] → [[0.1, -0.2], [0.3, 0.1]]  # 查找嵌入矩阵

第3步:向量拼接
[[0.1, -0.2], [0.3, 0.1]] → [0.1, -0.2, 0.3, 0.1]  # 4维向量

第4步:隐藏层计算(输出2维)
[4维向量] → [2维向量]  # 通过 Linear(4, 2) + tanh

第5步:输出层计算
[2维向量] → [7维向量]  # 通过 Linear(2, 7),每个位置是一个词的分数

第6步:Softmax转换
[2.1, 0.5, -1.0, -2.0, -1.5, -2.0, -1.8] → [0.75, 0.17, 0.04, ...]  # 概率分布

第7步:预测结果
最高概率对应"玩具"(索引3),所以预测下一个词是"玩具"

张量形状变化:

输入: [batch_size, 2] 
→ 嵌入: [batch_size, 2, 2] 
→ 拼接: [batch_size, 4] 
→ 隐藏层: [batch_size, 2] 
→ 输出层: [batch_size, 7] 
→ 概率: [batch_size, 7]

本文转载自​​​AI取经路,作者:AI取经路


收藏
回复
举报
回复
相关推荐