目录

颠覆语言认知的革命神经概率语言模型如何突破人类思维边界

颠覆语言认知的革命!神经概率语言模型如何突破人类思维边界?

颠覆语言认知的革命!神经概率语言模型如何突破人类思维边界?

一、传统模型的世纪困境:当n-gram遇上"月光族难题"

令人震惊的案例 :2012年Google语音识别系统将

用户说:“我要还信用卡”

系统识别:“我要环信用开”

三大困境解析

  1. 维度灾难:当n=3时,词表大小10万 → 需要存储10^15个组合
  2. 数据稀疏:“量子计算融资"在10亿语料库中出现0次
  3. 长程依赖:无法捕捉"虽然…但是…“的超距离关联

二、神经概率语言模型:NPLM

NPLM的结构包括3个主要部分:输⼊层、隐藏层和输出层。

输⼊层将词汇映射到连续的词向量空间,隐藏层通过⾮线性激活函数学习词与词之间的复杂关系,输出层通过softmax函数产⽣下⼀个单词的概率分布。

https://i-blog.csdnimg.cn/img_convert/2b83035bf79099e9436a586dd0f4d063.png

数学之美

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

)

=

so

f

t

ma

x

(

C

t

anh

(

W

x

b

))

P

(

w

t

w

t

1

,

,

w

t

n

1

)

=

softmax

(

C

tanh

(

W

x

b

))

其中:

  • C C

    C :词向量到输出的连接矩阵

  • W W

    W :上下文词向量组合矩阵

  • x x

    x :上下文词向量拼接

2.1 NPLM的实现

https://i-blog.csdnimg.cn/img_convert/4aaf0b05ab8321d7e28efc3508d94a34.png

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 # 每批数据的大小

https://i-blog.csdnimg.cn/img_convert/4ff5042a1ae6436aeb40ef1319d59f6d.png

即为整理好数据集,及其对应的结果,接下来只等构建网络和训练

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 # 返回输出结果

https://i-blog.csdnimg.cn/img_convert/56fdeed4a87213a32b6bb9690bc35dc2.png

进一部的说明我们结合以下代码

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

输入数据[[1,3], [1,6]] (对应句子片段[“我 爱”, “我 讨厌”])

处理流程

  1. 嵌入层转为形状 [2,2,2] 的张量
  2. 展平为 [2,4]
  3. 第一线性层压缩到 [2,2]
  4. 第二线性层扩展到 [2,7] (每个样本对应7个词的概率)

https://i-blog.csdnimg.cn/img_convert/7753a34f05f24fb06ec9568d3e35f167.png

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()               # 根据建议改进(参数更新)

https://i-blog.csdnimg.cn/img_convert/a6d0ccdd38c0525685fb5757ae395fa6.png

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)  # 打印输入序列和预测结果
['我', '讨厌'] -> 挨打

['我', '喜欢'] -> 玩具

https://i-blog.csdnimg.cn/img_convert/c4e12e65e95524090e5cfbff157556ca.png

2.2 完整手撕

最开始的学习中容易对NPLM建立中纬度的变化中产生疑惑,接下来进行具体得分析。

https://i-blog.csdnimg.cn/img_convert/10fddc32eb41e345bbb6deca7930c72c.png

其结构可以拆分为一个三明治结构

输入词索引 → 嵌入层 → 全连接层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

Z

ba

t

c

h

×

n

_

s

t

e

p ,

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

R

ba

t

c

h

×

n

_

s

t

e

p

×

e

mb

e

dd

in

g

_

s

i

ze


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}

W

1

=

[

0.1

0.5

0.2

0.6

0.3

0.7

0.4

0.8

]

,

b

1

=

[

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

(

X

f

l

a

t

W

1

b

1

)

其中

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)}

W

1

R

n

_

hi

dd

e

n

×

(

n

_

s

t

e

p

×

e

mb

e

dd

in

g

_

s

i

ze

)


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]

W

2

=

0.2

0.4

0.5

0.7

0.9

1.0

0.1

0.3

0.1

0.6

0.8

0.2

0.3

0.4

,

b

2

=

[

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

=

h

W

2

b

2

其中

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}

W

2

R

v

oc

_

s

i

ze

×

n

_

hi

dd

e

n


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

参数数

=

v

oc

_

s

i

ze

×

e

mb

e

dd

in

g

_

s

i

ze

=

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

_

s

t

e

p

×

e

mb

e

dd

in

g

_

s

i

ze

)

×

n

_

hi

dd

e

n

n

_

hi

dd

e

n

=

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

_

hi

dd

e

n

×

v

oc

_

s

i

ze

v

oc

_

s

i

ze

=

2

×

7

7

=

21

总参数 :14 + 10 + 21 = 45个可训练参数