一、原理篇
NNLM(Neural Network Language Model,神经网络语言模型)是一种通过神经网络进行语言建模的技术,通常用于预测序列中的下一个词。
NNLM的核心思想是使用词嵌入(word embedding)将词转换为低维向量,并通过神经网络学习语言中的词序关系。
NNLM的基本结构包括以下几个部分:
- 输入层:输入一个固定长度的词窗口,例如 n 个词的上下文(前 n - 1 个词)作为输入。
- 嵌入层:将每个输入词映射到一个低维空间,得到词向量。这一层的权重矩阵通常表示为词嵌入矩阵。
- 隐藏层:一个或多个隐藏层可以捕获词之间的关系,一般是全连接层。
- 输出层:用于预测下一个词的概率分布,通常使用softmax函数。
假设一个句子为 ,我们希望通过上下文词 来预测下一个词 。其目标是最大化预测正确的概率,即:
1. 词嵌入查找
每个词 都有一个唯一的索引。我们用嵌入矩阵 来将这些词映射到低维向量空间:
其中 表示第 i 个词的嵌入向量。对于所有 n−1 个词的上下文,我们得到词嵌入向量的集合:
2. 嵌入向量拼接
将所有词嵌入向量拼接成一个大的向量:
这个拼接向量 x 包含了上下文中的信息。
3. 隐藏层计算
将拼接后的向量 x 输入到隐藏层,隐藏层的权重矩阵为 W,偏置向量为 b,激活函数为 f(例如tanh)。隐藏层的输出表示为:
在图中,“most computation here”指的就是这个计算过程。
4. 输出层与Softmax
隐藏层的输出 h 通过输出层进行预测。输出层使用softmax函数来计算词汇表中每个词的概率分布:
其中:
- 是输出层对应词 i 的权重向量,
- 是词 i 的偏置,
- 是词汇表的大小。
最终输出层会给出一个词汇表大小的概率分布,用于预测下一个词的概率。
5. 损失函数
模型通过最大化训练数据的似然来进行优化。通常使用交叉熵损失来最小化预测的词分布和真实标签之间的差距:
通过最小化该损失函数,可以优化模型参数,使得模型能够更好地预测下一个词。
二、代码篇
# 1.导入必要的库
import torch
import torch.nn as nn
import torch.optim as optimizer
import torch.utils.data as Data# 2.数据准备
sentences = ["I like milk","I love hot-pot","I hate coffee","I want sing","I am sleep","I go home","Love you forever"]word_list = " ".join(sentences).split() # 获取个句子单词
word_list = list(set(word_list)) # 获取单词列表word_dict = {w: i for i, w in enumerate(word_list)} # 单词-位置索引字典
number_dict = {i: w for i, w in enumerate(word_list)} # 位置-单词索引字典vocab_size = len(word_list) # 词汇表大小# 3.X-生成输入和输出数据
def make_data():input_data = []output_data = []for sen in sentences:word = sen.split()input_temp = [word_dict[n] for n in word[:-1]]output_temp = word_dict[word[-1]]input_data.append(input_temp)output_data.append(output_temp)return input_data, output_datainput_data, output_data = make_data()
input_data, output_data = torch.LongTensor(input_data), torch.LongTensor(output_data) # 数据转换:将 input_data 和 output_data 转换为 LongTensor,以便用于模型训练。
dataset = Data.TensorDataset(input_data, output_data) # 建数据集:Data.TensorDataset 将输入和输出配对为数据集
loader = Data.DataLoader(dataset, 4, True) # 数据加载器:DataLoader,使用批量大小为 2,随机打乱样本# 4.初始化参数
m = 2
n_step = 2
n_hidden = 10# 5.模型定义
class NNLM(nn.Module):def __init__(self):super(NNLM, self).__init__()self.C = nn.Embedding(vocab_size, m)self.H = nn.Linear(n_step * m, n_hidden, bias=False)self.d = nn.Parameter(torch.ones(n_hidden))self.U = nn.Linear(n_hidden, vocab_size, bias=False)self.W = nn.Linear(n_step * m, vocab_size, bias=False)self.b = nn.Parameter(torch.ones(vocab_size))def forward(self, X):X = self.C(X) # X = [batch_size, n_step, m]X = X.view(-1, n_step * m) # 展平 X = [batch_size, n_step * m]hidden_output = torch.tanh(self.d + self.H(X))output = self.b + self.W(X) + self.U(hidden_output)return output# 6.定义训练过程
model = NNLM()
optim = optimizer.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()# 7.模型训练
for epoch in range(5000):for batch_x, batch_y in loader:pred = model(batch_x)loss = criterion(pred, batch_y)if (epoch + 1) % 1000 ==0:print(epoch+1, loss.item())optim.zero_grad()loss.backward()optim.step()# 8.模型测试
pred = model(input_data).max(1, keepdim=True)[1]
print([number_dict[idx.item()] for idx in pred.squeeze()])
代码简单解释:
1.最开始导入一些必要的库
2.首先需要准备一些数据,用于模型训练并测试
word_list = " ".join(sentences).split()
①对于文本数据,肯定要进行一个分词操作,先使用" ".join(sentences).split()来切分每一个句子的每个单词,这时候获得的word_list列表就是:
['I', 'like', 'milk', 'I', 'love', 'hot-pot', 'I', 'hate', 'coffee', 'I', 'want', 'sing', 'I', 'am', 'sleep', 'I', 'go', 'home', 'Love', 'you', 'forever']
②但是这里得到的单词可能会有重复的情况,我们需要得到不重复的单词列表,为后面的创建词典提供方便。
word_list = list(set(word_list))
set
(word_list
) :将 word_list
转换为集合(set
),自动去除列表中的重复元素。
list(set
(word_list
) ):再将集合转换回列表,这样可以保持原数据类型一致(即 word_list
仍然是一个列表)。
['milk', 'coffee', 'sing', 'hot-pot', 'home', 'you', 'am', 'I', 'sleep', 'love', 'want', 'hate', 'go', 'forever', 'like', 'Love']
③然后就是构造词典:单词到位置的索引字典、位置到单词的索引字典。
word_dict = {w: i for i, w in enumerate(word_list)} # 单词-位置索引字典
number_dict = {i: w for i, w in enumerate(word_list)} # 位置-单词索引字典
word_dict: {'sleep': 0, 'go': 1, 'home': 2, 'milk': 3, 'hate': 4, 'Love': 5, 'love': 6, 'am': 7, 'want': 8, 'sing': 9, 'forever': 10, 'hot-pot': 11, 'I': 12, 'like': 13, 'you': 14, 'coffee': 15}
number_dict:{0: 'sleep', 1: 'go', 2: 'home', 3: 'milk', 4: 'hate', 5: 'Love', 6: 'love', 7: 'am', 8: 'want', 9: 'sing', 10: 'forever', 11: 'hot-pot', 12: 'I', 13: 'like', 14: 'you', 15: 'coffee'}
这里使用enumerate是因为 enumerate
函数可以方便地同时获取列表元素的索引和对应的值。也就是我们想要的字典。
④然后就是获得词汇表大小,在模型搭建中需要用到。
3.有了初始数据,我们需要构建出数据X,也就是输入数据和目标输出数据。
def make_data():input_data = []output_data = []for sen in sentences:word = sen.split()input_temp = [word_dict[n] for n in word[:-1]]output_temp = word_dict[word[-1]]input_data.append(input_temp)output_data.append(output_temp)return input_data, output_data
①先构建空的输入数据input_data和输出数据output_data。
②将每个句子的前 n-1个单词的位置添加到输入数据input_data中,第 n 个单词的位置添加到输入数据output_data中,得到每个输入 x 和输出 y 在词汇表中的顺序:
input_data:[[12, 13], [12, 6], [12, 4], [12, 8], [12, 7], [12, 1], [5, 14]]
output_data:[3, 11, 15, 9, 0, 2, 10]
这是个二维的矩阵,行元素代表一个句子中用于训练输入/测试输出的单词在词汇表中的位置索引;列元素是不同的句子。
每个句子都是3个单词,前2个作为前文信息作为输入,第3个作为预测输出,我们前面给的一共是7个句子。
4.初始化参数
这里的m指的是维度,也就是一个单词要嵌入到多少维度,由于这里的数据量比较小,每个句子也只有3个单词,所以这给出的维度选个很低的2。
n_step=2,指的是用两个单词来预测下一个目标单词。
n_hidden=10,指的是隐藏层的数量。
5.前面的数据已经初步定义好了,这里就要搭建NNLM模型了。
class NNLM(nn.Module):def __init__(self):super(NNLM, self).__init__()self.C = nn.Embedding(vocab_size, m)self.H = nn.Linear(n_step * m, n_hidden, bias=False)self.d = nn.Parameter(torch.ones(n_hidden))self.U = nn.Linear(n_hidden, vocab_size, bias=False)self.W = nn.Linear(n_step * m, vocab_size, bias=False)self.b = nn.Parameter(torch.ones(vocab_size))def forward(self, X):X = self.C(X) # X = [batch_size, n_step, m]X = X.view(-1, n_step * m) # 展平 X = [batch_size, n_step * m]hidden_output = torch.tanh(self.d + self.H(X))output = self.b + self.W(X) + self.U(hidden_output)return output
①def _init_(self):定义各层和参数:
self.C
:词嵌入层,将输入词转换为词向量。
vocab_size
定义词汇表的大小,m
是词嵌入的维度,表示每个词将被嵌入成 m
维的向量。
self.H
:线性层,将展平后的输入映射到隐藏层。
n_step * m
是展平后的输入大小,n_hidden
是隐藏层的维度,用来控制隐藏层输出的特征数量。
self.d
:偏置向量,用于隐藏层的输出。
n_hidden
是偏置项的维度,与隐藏层输出匹配,用于提升隐藏层的表达能力。
self.U
:线性层,将隐藏层输出映射到词汇表空间。
n_hidden
是隐藏层输出的大小,vocab_size
是词汇表大小,用于将隐藏层的特征映射到每个词的预测空间。
self.W
:线性层,从输入直接映射到词汇表空间。
n_step * m
是展平后的输入大小,vocab_size
是词汇表大小,用于将输入直接映射到词汇表的预测空间。
self.b
:偏置向量,用于最终输出层的分数调整。
vocab_size
是词汇表的大小,用作最终输出层的偏置。
这里分别用了Embedding、Linear和Parameter:
- Embedding:嵌入层,用于将离散的词汇索引(如单词的整数表示)映射到连续的稠密向量空间。
- Linear:全连接层(线性层),用于将输入的特征通过线性变换映射到输出空间。
- Parameter:可学习的参数。
②def forward(self,X):定义神经网络在前向传播过程中的计算步骤
-
X = self.C(X)
首先通过嵌入层将输入的词索引(X
)转换为词向量表示,这个时候得到是三维度的:[batch_size, n_step, m]
。 -
X = X.view(-1, n_step * m)
然后将X
从三维张量展平为二维张量[batch_size, n_step * m],方便
输入到全连接层self.H。
-
hidden_output = torch.tanh(self.d + self.H(X))
接着,利用公式计算得到隐藏层输出。
-
output = self.b + self.W(X) + self.U(hidden_output)
最后,利用公式得到最终的输出。
6.定义训练过程
这里初始化model,并且设置优化器为Adam,并且使用了交叉熵损失。
7.模型训练
for epoch in range(5000):for batch_x, batch_y in loader:pred = model(batch_x)loss = criterion(pred, batch_y)if (epoch + 1) % 1000 ==0:print(epoch+1, loss.item())optim.zero_grad()loss.backward()optim.step()
这里从数据加载器中加载数据,将当前批次的输入数据batch_x传入模型中得到预测结果,同时计算预测值与真实值的损失loss,每1000个epoch打印损失。然后梯度清零、反向传播计算梯度、更新模型参数。
8.模型测试
pred = model(input_data).max(1, keepdim=True)[1]
- model(input_data):将输入数据传递给模型,获取每个类别的得分(logits)。
- max(1, keepdim=True)[1]:
max(1):对每个样本找出最大得分的类别索引。
keepdim=True:保持输出维度不变。
[1]:提取每个样本的最大值索引(预测类别)。
print([number_dict[idx.item()] for idx in pred.squeeze()])
- pred.squeeze():移除维度为 1 的维度,得到一维张量。
- [idx.item() for idx in pred.squeeze()]:将每个索引转换为整数。
- number_dict[idx.item()]:通过索引查找可读标签。
- print([...]):打印出模型预测的类别标签。
输出:
1000 0.05966342240571976
1000 0.034198883920907974
2000 0.005526650696992874
2000 0.009151813574135303
3000 0.0021409429609775543
3000 0.0015856553800404072
4000 0.0006656644982285798
4000 0.0005017295479774475
5000 0.00018937562708742917
5000 0.00020660058362409472
['milk', 'hot-pot', 'coffee', 'sing', 'sleep', 'home', 'forever']
参考
Neural Network Language Model PyTorch实现_哔哩哔哩_bilibili