NNLM——预测下一个单词

一、原理篇 

      NNLM(Neural Network Language Model,神经网络语言模型)是一种通过神经网络进行语言建模的技术,通常用于预测序列中的下一个词。

        NNLM的核心思想是使用词嵌入(word embedding)将词转换为低维向量,并通过神经网络学习语言中的词序关系。

        NNLM的基本结构包括以下几个部分:

  • 输入层:输入一个固定长度的词窗口,例如 n 个词的上下文(前  n - 1 个词)作为输入。
  • 嵌入层:将每个输入词映射到一个低维空间,得到词向量。这一层的权重矩阵通常表示为词嵌入矩阵。
  • 隐藏层:一个或多个隐藏层可以捕获词之间的关系,一般是全连接层。
  • 输出层:用于预测下一个词的概率分布,通常使用softmax函数。

        假设一个句子为 w_1, w_2, \dots, w_T,我们希望通过上下文词 w_{t-n+1}, \dots, w_{t-1} 来预测下一个词 w_t。其目标是最大化预测正确的概率,即:

P(w_t | w_{t-1}, w_{t-2}, \dots, w_{t-n+1})

1. 词嵌入查找

        每个词 w都有一个唯一的索引。我们用嵌入矩阵 C 来将这些词映射到低维向量空间:

x_i = C(w_{t-i})

        其中x_i 表示第 i 个词的嵌入向量。对于所有 n−1 个词的上下文,我们得到词嵌入向量的集合:

\mathbf{x} = \left[ C(w_{t-n+1}), C(w_{t-n+2}), \dots, C(w_{t-1}) \right]

2. 嵌入向量拼接

        将所有词嵌入向量拼接成一个大的向量:

x = \left[ x_{t-n+1}, x_{t-n+2}, \dots, x_{t-1} \right]

        这个拼接向量 x 包含了上下文中的信息。

3. 隐藏层计算

        将拼接后的向量 x 输入到隐藏层,隐藏层的权重矩阵为 W,偏置向量为 b,激活函数为 f(例如tanh)。隐藏层的输出表示为:

h = f(W \cdot \mathbf{x} + b)

        在图中,“most computation here”指的就是这个计算过程。

4. 输出层与Softmax

        隐藏层的输出 h 通过输出层进行预测。输出层使用softmax函数来计算词汇表中每个词的概率分布:

P(w_t = i | w_{t-1}, w_{t-2}, \dots, w_{t-n+1}) = \frac{\exp(U_i \cdot h + c_i)}{\sum_{j=1}^{V} \exp(U_j \cdot h + c_j)}

其中:

  • U_i 是输出层对应词 i 的权重向量,
  • c_i​ 是词 i 的偏置,
  • V 是词汇表的大小。

        最终输出层会给出一个词汇表大小的概率分布,用于预测下一个词的概率。

5. 损失函数

        模型通过最大化训练数据的似然来进行优化。通常使用交叉熵损失来最小化预测的词分布和真实标签之间的差距:

L = - \sum_{t=1}^{T} \log P(w_t | w_{t-1}, w_{t-2}, \dots, w_{t-n+1})

        通过最小化该损失函数,可以优化模型参数,使得模型能够更好地预测下一个词。

二、代码篇

# 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_data


input_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))
    接着,利用公式h=tanh(W_hX+d)计算得到隐藏层输出。
  • output = self.b + self.W(X) + self.U(hidden_output)
    最后,利用公式output=b+WX+Uh得到最终的输出。

        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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值