颠覆语言认知的革命!神经概率语言模型如何突破人类思维边界?
一、传统模型的世纪困境:当n-gram遇上"月光族难题"
令人震惊的案例:2012年Google语音识别系统将
用户说:“我要还信用卡”
系统识别:“我要环信用开”
三大困境解析:
- 维度灾难:当n=3时,词表大小10万 → 需要存储10^15个组合
- 数据稀疏:"量子计算融资"在10亿语料库中出现0次
- 长程依赖:无法捕捉"虽然…但是…"的超距离关联
二、神经概率语言模型:NPLM
NPLM的结构包括3个主要部分:输⼊层、隐藏层和输出层。
输⼊层将词汇映射到连续的词向量空间,隐藏层通过⾮线性激活函数学习词与词之间的复杂关系,输出层通过softmax函数产⽣下⼀个单词的概率分布。
数学之美:
P
(
w
t
∣
w
t
−
1
,
.
.
.
,
w
t
−
n
+
1
)
=
s
o
f
t
m
a
x
(
C
⋅
t
a
n
h
(
W
x
+
b
)
)
P
(
w
t
∣
w
t
−
1
,
.
.
.
,
w
t
−
n
+
1
)
=
softmax
(
C
⋅
tanh
(
W
x
+
b
)
)
P(wt∣wt−1,...,wt−n+1)=softmax(C⋅tanh(Wx+b))P(w_t|w_{t-1},...,w_{t-n+1}) = \text{softmax}(C \cdot \tanh(Wx + b))
P(wt∣wt−1,...,wt−n+1)=softmax(C⋅tanh(Wx+b))P(wt∣wt−1,...,wt−n+1)=softmax(C⋅tanh(Wx+b))
其中:
- C C C:词向量到输出的连接矩阵
- W W W:上下文词向量组合矩阵
- x x x:上下文词向量拼接
2.1 NPLM的实现
2.1.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
2.1.2 生成NPLM训练数据
# 构建批处理数据
import torch # 导入 PyTorch 库
import random # 导入 random 库
batch_size = 2 # 每批数据的大小
def make_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(" 输入批处理数据:",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)
输入批处理数据: tensor([[2, 1], [2, 4]])
输入批处理数据对应的原始词: [['我', '讨厌'], ['我', '爱']]
目标批处理数据: tensor([6, 0])
目标批处理数据对应的原始词: ['挨打', '爸爸']
batch_size = 2 # 每批数据的大小
即为整理好数据集,及其对应的结果,接下来只等构建网络和训练
2.1.3 定义NPLM
import torch.nn as nn # 导入神经网络模块
# 定义神经概率语言模型(NPLM)
class NPLM(nn.Module):
def __init__(self):
super(NPLM, self).__init__()
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)
def forward(self, X): # 定义前向传播过程
# 输入数据 X 张量的形状为 [batch_size, n_step]
X = self.C(X) # 将 X 通过词嵌入层,形状变为 [batch_size, n_step, embedding_size]
X = X.view(-1, n_step * embedding_size) # 形状变为 [batch_size, n_step * embedding_size]
# 通过第一个线性层并应用 ReLU 激活函数
hidden = torch.tanh(self.linear1(X)) # hidden 张量形状为 [batch_size, n_hidden]
# 通过第二个线性层得到输出
output = self.linear2(hidden) # output 形状为 [batch_size, voc_size]
return output # 返回输出结果
进一部的说明我们结合以下代码
2.1.4 实例化NPLM
n_step = 2 # 时间步数,表示每个输入序列的长度,也就是上下文长度
n_hidden = 2 # 隐藏层大小
embedding_size = 2 # 词嵌入大小
model = NPLM() # 创建神经概率语言模型实例
print(' NPLM 模型结构:', model) # 打印模型的结构
输入数据:[[1,3], [1,6]]
(对应句子片段[“我 爱”, “我 讨厌”])
处理流程:
- 嵌入层转为形状
[2,2,2]
的张量 - 展平为
[2,4]
- 第一线性层压缩到
[2,2]
- 第二线性层扩展到
[2,7]
(每个样本对应7个词的概率)
2.1.5 训练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() # 更新模型参数
是不是中一段训练的设定看着挺抽象,接下来举一个简单的栗子:
for epoch in range(5000):
optimizer.zero_grad() # 清空炒锅残留(重置梯度)
input_batch, target = make_batch() # 随机选菜谱(数据采样)
output = model(input_batch) # 学生试做菜品(前向传播)
loss = criterion(output, target) # 老师试吃评分(计算损失)
loss.backward() # 分析哪里做错了(反向传播)
optimizer.step() # 根据建议改进(参数更新)

2.1.6 用NPLM预测新词
# 进行预测
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) # 打印输入序列和预测结果
['我', '讨厌'] -> 挨打
['我', '喜欢'] -> 玩具
2.2 完整手撕
最开始的学习中容易对NPLM建立中纬度的变化中产生疑惑,接下来进行具体得分析。
其结构可以拆分为一个三明治结构
输入词索引 → 嵌入层 → 全连接层1+激活 → 全连接层2 → 词汇概率分布
让我们用具体数值示例(假设参数如下)逐步解析:
voc_size = 7 # 词汇表大小(示例数据实际值)
n_step = 2 # 用前2个词预测第3个词
embedding_size = 2 # 词向量维度(示例设定)
n_hidden = 2 # 隐藏层维度(示例设定)
2.2.1 输入数据示例
假设输入批处理数据为:
input_batch = [[1,3], [1,6]] # 对应句子片段:"我 爱"、"我 讨厌"
# 转换为张量后形状:[batch_size=2, n_step=2]
2.2.2 前向传播逐层分解
1. 嵌入层(Embedding Layer)
self.C = nn.Embedding(7, 2) # 参数:7词→2维向量
X = self.C(input_batch) # 输入:[2,2] → 输出:[2,2,2]
相当于把索引中的七个词转化为二维的向量,转化过程如下
计算过程:
- 每个词索引查表获取对应向量
# 假设嵌入矩阵(随机初始化示例):
[
[0.1, 0.2], # 索引0:'挨打'
[0.3, 0.4], # 索引1:'我'
[0.5, 0.6], # 索引2:'爸爸'
[0.7, 0.8], # 索引3:'爱'
[0.9, 1.0], # 索引4:'喜欢'
[1.1, 1.2], # 索引5:'玩具'
[1.3, 1.4] # 索引6:'讨厌'
]
# 输入[[1,3], [1,6]]的嵌入结果:
[
[[0.3,0.4], [0.7,0.8]], # "我 爱"
[[0.3,0.4], [1.3,1.4]] # "我 讨厌"
]
数学本质:
将离散的词索引映射为连续可训练的向量空间中的点,公式:
E
=
EmbeddingLookup
(
X
)
\mathbf{E} = \text{EmbeddingLookup}(\mathbf{X})
E=EmbeddingLookup(X)
其中
X
∈
Z
b
a
t
c
h
×
n
_
s
t
e
p
\mathbf{X} \in \mathbb{Z}^{batch×n\_step}
X∈Zbatch×n_step,
E
∈
R
b
a
t
c
h
×
n
_
s
t
e
p
×
e
m
b
e
d
d
i
n
g
_
s
i
z
e
\mathbf{E} \in \mathbb{R}^{batch×n\_step×embedding\_size}
E∈Rbatch×n_step×embedding_size
2. 特征拼接(Flatten)
X = X.view(-1, n_step*embedding_size) # [2,2,2] → [2,4]
具体变换:
# 第一个样本:"我 爱"
原始嵌入 → [0.3,0.4], [0.7,0.8]
拼接后 → [0.3, 0.4, 0.7, 0.8]
# 第二个样本:"我 讨厌"
原始嵌入 → [0.3,0.4], [1.3,1.4]
拼接后 → [0.3, 0.4, 1.3, 1.4]
技术意义:
将序列的时序信息转换为空间特征,为全连接层提供固定长度的输入。
3. 第一全连接层(Feature Projection)
self.linear1 = nn.Linear(4, 2) # 输入4维 → 输出2维
hidden = torch.tanh(self.linear1(X)) # [2,4] → [2,2]
参数示例:
假设权重和偏置为(点乘):
W
1
=
[
0.1
−
0.2
0.3
0.4
−
0.5
0.6
0.7
−
0.8
]
,
b
1
=
[
0.1
−
0.2
]
W_1 = \begin{bmatrix} 0.1 & -0.2 & 0.3 & 0.4 \\ -0.5 & 0.6 & 0.7 & -0.8 \end{bmatrix}, \quad b_1 = \begin{bmatrix} 0.1 \\ -0.2 \end{bmatrix}
W1=[0.1−0.5−0.20.60.30.70.4−0.8],b1=[0.1−0.2]
计算演示:
# 第一个样本计算:
[0.3, 0.4, 0.7, 0.8] × W1 + b1
= (0.3*0.1 + 0.4*(-0.2) + 0.7*0.3 + 0.8*0.4) + 0.1 = 0.54 → tanh(0.54) ≈ 0.49
= (0.3*(-0.5) + 0.4*0.6 + 0.7*0.7 + 0.8*(-0.8)) + (-0.2) = -0.56 → tanh(-0.56) ≈ -0.51
输出 → [0.49, -0.51]
数学表达式:
h
=
tanh
(
X
f
l
a
t
W
1
⊤
+
b
1
)
\mathbf{h} = \tanh(\mathbf{X}_{flat}W_1^\top + \mathbf{b}_1)
h=tanh(XflatW1⊤+b1)
其中
W
1
∈
R
n
_
h
i
d
d
e
n
×
(
n
_
s
t
e
p
×
e
m
b
e
d
d
i
n
g
_
s
i
z
e
)
W_1 \in \mathbb{R}^{n\_hidden×(n\_step×embedding\_size)}
W1∈Rn_hidden×(n_step×embedding_size)
4. 第二全连接层(Output Projection)
self.linear2 = nn.Linear(2,7) # 2维→7分类
output = self.linear2(hidden) # [2,2] → [2,7]
相当于将第一层的输入进行线性变换后(上下文特征的抽象提取),通过激活函数得出的结果分别再与第二层的参数进行线性变换(语义到词汇的概率映射),通过结果可以判断出哪一个词语为预测词的概率更大。
参数示例:
假设:
W
2
=
[
0.2
−
0.3
0.4
0.1
−
0.5
0.6
0.7
−
0.8
−
0.9
0.2
1.0
0.3
0.1
−
0.4
]
,
b
2
=
[
0.1
,
0.2
,
−
0.3
,
0.4
,
−
0.5
,
0.6
,
−
0.7
]
W_2 = \begin{bmatrix} 0.2 & -0.3 \\ 0.4 & 0.1 \\ -0.5 & 0.6 \\ 0.7 & -0.8 \\ -0.9 & 0.2 \\ 1.0 & 0.3 \\ 0.1 & -0.4 \end{bmatrix}, \quad b_2 = [0.1, 0.2, -0.3, 0.4, -0.5, 0.6, -0.7]
W2=
0.20.4−0.50.7−0.91.00.1−0.30.10.6−0.80.20.3−0.4
,b2=[0.1,0.2,−0.3,0.4,−0.5,0.6,−0.7]
计算演示:
# 第一个样本隐藏值 [0.49, -0.51]
计算每个输出单元:
0.49*0.2 + (-0.51)*(-0.3) + 0.1 ≈ 0.098 + 0.153 + 0.1 = 0.351 → 输出单元0
0.49*0.4 + (-0.51)*0.1 + 0.2 ≈ 0.196 - 0.051 + 0.2 = 0.345 → 输出单元1
...以此类推所有7个单元
数学表达式:
o
=
h
W
2
⊤
+
b
2
\mathbf{o} = \mathbf{h}W_2^\top + \mathbf{b}_2
o=hW2⊤+b2
其中
W
2
∈
R
v
o
c
_
s
i
z
e
×
n
_
h
i
d
d
e
n
W_2 \in \mathbb{R}^{voc\_size×n\_hidden}
W2∈Rvoc_size×n_hidden
2.2.3 输出结果解读
最终得到形状为 [batch_size, voc_size]
的未归一化概率(logits),例如:
output = [
[0.35, 0.34, -1.2, 0.8, 2.1, -0.3, 0.5], # 样本1:预测"喜欢"(索引4)概率最高
[1.2, -0.5, 0.7, 0.3, -0.8, 0.9, 2.3] # 样本2:预测"讨厌"(索引6)概率最高
]
要得到概率分布,需应用Softmax:
probs = torch.softmax(output, dim=1)
# probs ≈ [
# [0.05, 0.05, 0.01, 0.10, 0.60, 0.09, 0.10],
# [0.15, 0.03, 0.08, 0.07, 0.02, 0.10, 0.55]
# ]
2.2.4 参数计算详解
1. 嵌入层参数
参数数 = v o c _ s i z e × e m b e d d i n g _ s i z e = 7 × 2 = 14 \text{参数数} = voc\_size × embedding\_size = 7×2 = 14 参数数=voc_size×embedding_size=7×2=14
2. 第一全连接层
参数数 = ( n _ s t e p × e m b e d d i n g _ s i z e ) × n _ h i d d e n + n _ h i d d e n = 4 × 2 + 2 = 10 \text{参数数} = (n\_step×embedding\_size) × n\_hidden + n\_hidden = 4×2 + 2 = 10 参数数=(n_step×embedding_size)×n_hidden+n_hidden=4×2+2=10
3. 第二全连接层
参数数 = n _ h i d d e n × v o c _ s i z e + v o c _ s i z e = 2 × 7 + 7 = 21 \text{参数数} = n\_hidden × voc\_size + voc\_size = 2×7 + 7 = 21 参数数=n_hidden×voc_size+voc_size=2×7+7=21
总参数:14 + 10 + 21 = 45个可训练参数