当前位置:   article > 正文

使用Pytorch简单实现NNLM(Nerual Network Language Model)_nnlm中的任务

nnlm中的任务

前言

NNLM在NLP中有着举足轻重的地位,该模型将深度学习运用到了NLP中,其副产物词向量更是开创了NLP中预训练模型的先河,此文章使用Pytorch实现了NNLM的模型结构,并用简单的数据进行了模型的训练和测试。

示例代码来自于:https://wmathor.com/index.php/archives/1442/

本文在原博客的基础上进行了一些补充,使其更加通俗易懂。

 

模型结构分析

NNLM的模型是一个三层的神经网络,如图所示:

NNLM模型的任务是通过句子中的前n-1个词,来预测下一个词(第n个词)是什么,因此模型的输入是前n-1个词的向量化表示,在图中即是最下方的W_{t-n+1}.......W_{t-1}。在NNLM中,这种向量化表示就是one-hot编码的形式。文中有一个极其重要的矩阵Matrix C,它存储了输入中每个词的词向量表示,输入的每个词都能在矩阵中找到对应的词向量映射,如下所示:

值得注意的是,其实矩阵C就是两个神经网络层之间的权重W,从W_{t-1}C(W_{t-1})就是矩阵的一个乘法运算,其它每个词的表示也是同样的道理。

我们得到了每个词的词向量表示后,将其拼接在一起,即进行concat操作,形成了一个(n-1)*w的向量,我们用X来表示。然后将X送入隐藏层进行计算,即图中中间的一层神经元,使用tanh作为激活函数:

用公式表达则是:hidden_{out}=tanh(d+X*H),H和d分别是权重和偏置。

然后到了最后一层输出层,最后一层用于最终的分类,因此使用softmax作为激活函数,该层神经元的数量即是词典的大小|V|,每一个词的输出概率就是预测的词的可能性。这里需要格外注意一点,最后的输出层不仅与隐层相关,还有一条与词向量那一层的连线(图中虚线),类似于残差连接的思想。如果输出层的公式为:

y=b+X*W+hidden_{out}*U

U表示隐藏层到输出层的weight,X表示输入层到输出层的weight,b表示其偏置。

最后我们来统一整理一下出现的数学符号:(图来自于引用的博客)

 

输出到输出的形状变换

1.NNLM的输入是句子的前n个词,这个n在代码中表示就是n_step,每个词用one-hot编码表示,其中one-hot向量的形状都为词典的大小V,在代码中表示则为n_class。考虑每次输入batch_size个样本,那么输入的形状则为:[batch_size, n_step, n_class]

2.NNLM有一个矩阵C,将每个n_step上的词映射成词向量的形式,词向量的维度指定为m,那么形状转变:[batch_size, n_step, n_class]----->[batch_size, n_step, m],这一步的形状转变是基于Matrix C的,但是pytorch中有一个Embedding API,能随机生成一个词的词向量,因此词的one-hot编码的步骤可以省略,形状变化直接为[batch_size, n_step]----->[batch_size, n_step, m]。

3.然后为了使输入能够继续在神经网络中”流动“,需要对除了batch这一维之外的维度”展平”,即:[batch_size, n_step, m]----->[batch_size, n_step * m]。(n_step*m相当于神经网络的一层中的神经元,因此除了batch_size,其它的多个维度都要统一成只剩一个,例如在CNN中,卷积池化过后要输入到全连接层了,也要将图片的多个通道展平,这操作在神经网络中是很常见的一个方法,而至于batch_size,在图中是不体现的,但是输入多个样本能够提升训练速度。)

4.仔细思考下,展平的操作:[batch_size, n_step, m]----->[batch_size, n_step * m],其实就是在论文中提到的:将n_step个词向量进行concat操作,因此展平的操作其实就是拼接。

5.得到了[batch_size, n_step * m]后,经过一个激活函数为tanh的隐藏层,形状变换:[batch_size, n_step * m]----->[batch_size, n_hidden]。其中n_hidden是隐层的神经元数量。

6.最后经过输出层,输出层的神经元数量就是词典的大小了,使用softmax作为激活函数,选取输出词最大的概率作为输出。形状变化:[batch_size, n_hidden]----->[batch_size, n_class]。

补充:在处理数据的时候,我们需要首先建立一个词典,词典中的词为去重后的所有文本,其词的数量为n_class,然后建立一个单词映射到序号的一个字典,使得每个单词都能唯一的映射到一个数字标号。然后,NNLM的输入是N个词,预测的是下一个词,我们假设是用两个词来预测下一个词,举个例子,共有三组数据,那么其输入为[[5, 2], [5, 0], [5, 6]],输出为[4, 3, 1]。[5,2]预测4,[5,0]预测3,[5,6]预测1,当然这些数字其实就是一个个词。

 

代码实现

1.导入需要的库

  1. import torch
  2. import torch.nn as nn
  3. import torch.optim as optim
  4. import torch.utils.data as Data
  5. dtype = torch.FloatTensor

2.数据预处理

这里只是用了简单的三句话来模拟训练和测试过程,没有使用大规模的语料库,但是原理还是一样的。

  1. # 数据预处理
  2. sentences = ['i like dog', 'i love coffee', 'i hate milk']
  3. word_list = " ".join(sentences).split() # ['i', 'like', 'dog', 'i', 'love', 'coffee', 'i', 'hate', 'milk']
  4. word_list = list(set(word_list)) # 去除重复的单词
  5. word_dict = {w: i for i, w in
  6. enumerate(word_list)} # {'hate': 0, 'dog': 1, 'milk': 2, 'love': 3, 'like': 4, 'i': 5, 'coffee': 6}
  7. number_dict = {i: w for i, w in
  8. enumerate(word_list)} # {0: 'like', 1: 'dog', 2: 'coffee', 3: 'hate', 4: 'i', 5: 'love', 6: 'milk'}
  9. n_class = len(word_dict) # 词典|V|的大小,也是最后分类的类别,这里是7
  10. # NNLM(Neural Network Language Model) Parameter,模型的参数
  11. n_step = len(sentences[0].split()) - 1 # 文中用n_step个词预测下一个词,在本程序中其值为2
  12. n_hidden = 2 # 隐藏层神经元的数量
  13. m = 2 # 词向量的维度
'
运行

3.实现一个mini-batch迭代器

  1. # 实现一个mini-batch迭代器
  2. def make_batch(sentences):
  3. input_batch = []
  4. target_batch = []
  5. for sen in sentences:
  6. word = sen.split() # ['i', 'like', 'dog']
  7. input = [word_dict[n] for n in word[:-1]] # 列表对应的数字序列,一句话中最后一个词是要用来预测的,不作为输入
  8. target = word_dict[word[-1]] # 每句话的最后一个词作为目标值
  9. input_batch.append(input)
  10. target_batch.append(target)
  11. return input_batch, target_batch # ([[5, 2], [5, 0], [5, 6]], [4, 3, 1])
  12. input_batch, target_batch = make_batch(sentences)
  13. input_batch = torch.LongTensor(input_batch)
  14. target_batch = torch.LongTensor(target_batch)
  15. dataset = Data.TensorDataset(input_batch, target_batch)
  16. loader = Data.DataLoader(dataset=dataset, batch_size=16, shuffle=True)

4.定义模型结构

  1. # 定义模型
  2. class NNLM(nn.Module):
  3. def __init__(self):
  4. """
  5. C: 词向量,大小为|V|*m的矩阵
  6. H: 隐藏层的weight
  7. W: 输入层到输出层的weight
  8. d: 隐藏层的bias
  9. U: 输出层的weight
  10. b: 输出层的bias
  11. 1. 首先将输入的 n-1 个单词索引转为词向量,然后将这 n-1 个词向量进行 concat,形成一个 (n-1)*w 的向量,用 X 表示
  12. 2. 将 X 送入隐藏层进行计算,hidden = tanh(d + X * H)
  13. 3. 输出层共有|V|个节点,每个节点yi表示预测下一个单词i的概率,y的计算公式为y = b + X * W + hidden * U
  14. n_step: 文中用n_step个词预测下一个词,在本程序中其值为2
  15. n_hidden: 隐藏层(中间那一层)神经元的数量
  16. m: 词向量的维度
  17. """
  18. super(NNLM, self).__init__()
  19. self.C = nn.Embedding(n_class, m) # 词向量随机赋值,代替了先使用one-hot,然后使用matrix C映射到词向量这一步
  20. self.H = nn.Parameter(torch.randn(n_step * m, n_hidden).type(dtype))
  21. self.W = nn.Parameter(torch.randn(n_step * m, n_class).type(dtype))
  22. self.d = nn.Parameter(torch.randn(n_hidden).type(dtype))
  23. self.U = nn.Parameter(torch.randn(n_hidden, n_class).type(dtype))
  24. self.b = nn.Parameter(torch.randn(n_class).type(dtype))
  25. def forward(self, X):
  26. """
  27. X: [batch_size, n_step]
  28. """
  29. X = self.C(X) # [batch_size, n_step] => [batch_size, n_step, m]
  30. X = X.view(-1, n_step * m) # [batch_size, n_step * m]
  31. hidden_out = torch.tanh(self.d + torch.mm(X, self.H)) # [batch_size, n_hidden], torch.mm就是矩阵的相乘
  32. output = self.b + torch.mm(X, self.W) + torch.mm(hidden_out, self.U) # [batch_size, n_class]
  33. return output

5.实例化模型,优化器,损失函数

  1. # 实例化模型,优化器,损失函数
  2. model = NNLM()
  3. criterion = nn.CrossEntropyLoss()
  4. optimizer = optim.Adam(model.parameters(), lr=1e-3)

6.训练和测试

  1. # train
  2. for epoch in range(5000):
  3. for batch_x, batch_y in loader:
  4. optimizer.zero_grad()
  5. output = model(batch_x)
  6. loss = criterion(output, batch_y)
  7. if (epoch + 1) % 1000 == 0:
  8. print('Epoch:', '%04d' % (epoch + 1), 'cost = ', '{:.6f}'.format(loss))
  9. loss.backward()
  10. optimizer.step()
  11. # Test
  12. predict = model(input_batch).data.max(1, keepdim=True)[1]
  13. # squeeze():对张量的维度进行减少的操作,原来:tensor([[2],[6],[3]]),squeeze()操作后变成tensor([2, 6, 3])
  14. print([sen.split()[:n_step] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])

7.测试结果

  1. Epoch: 1000 cost = 0.047851
  2. Epoch: 2000 cost = 0.008136
  3. Epoch: 3000 cost = 0.002618
  4. Epoch: 4000 cost = 0.001130
  5. Epoch: 5000 cost = 0.000564
  6. [['i', 'like'], ['i', 'love'], ['i', 'hate']] -> ['dog', 'coffee', 'milk']

完整代码

  1. import torch
  2. import torch.nn as nn
  3. import torch.optim as optim
  4. import torch.utils.data as Data
  5. dtype = torch.FloatTensor
  6. # 数据预处理
  7. sentences = ['i like dog', 'i love coffee', 'i hate milk']
  8. word_list = " ".join(sentences).split() # ['i', 'like', 'dog', 'i', 'love', 'coffee', 'i', 'hate', 'milk']
  9. word_list = list(set(word_list)) # 去除重复的单词
  10. word_dict = {w: i for i, w in
  11. enumerate(word_list)} # {'hate': 0, 'dog': 1, 'milk': 2, 'love': 3, 'like': 4, 'i': 5, 'coffee': 6}
  12. number_dict = {i: w for i, w in
  13. enumerate(word_list)} # {0: 'like', 1: 'dog', 2: 'coffee', 3: 'hate', 4: 'i', 5: 'love', 6: 'milk'}
  14. n_class = len(word_dict) # 词典|V|的大小,也是最后分类的类别,这里是7
  15. # NNLM(Neural Network Language Model) Parameter,模型的参数
  16. n_step = len(sentences[0].split()) - 1 # 文中用n_step个词预测下一个词,在本程序中其值为2
  17. n_hidden = 2 # 隐藏层神经元的数量
  18. m = 2 # 词向量的维度
  19. # 实现一个mini-batch迭代器
  20. def make_batch(sentences):
  21. input_batch = []
  22. target_batch = []
  23. for sen in sentences:
  24. word = sen.split() # ['i', 'like', 'dog']
  25. input = [word_dict[n] for n in word[:-1]] # 列表对应的数字序列,一句话中最后一个词是要用来预测的,不作为输入
  26. target = word_dict[word[-1]] # 每句话的最后一个词作为目标值
  27. input_batch.append(input)
  28. target_batch.append(target)
  29. return input_batch, target_batch # ([[5, 2], [5, 0], [5, 6]], [4, 3, 1])
  30. input_batch, target_batch = make_batch(sentences)
  31. input_batch = torch.LongTensor(input_batch)
  32. target_batch = torch.LongTensor(target_batch)
  33. dataset = Data.TensorDataset(input_batch, target_batch)
  34. loader = Data.DataLoader(dataset=dataset, batch_size=16, shuffle=True)
  35. # 定义模型
  36. class NNLM(nn.Module):
  37. def __init__(self):
  38. """
  39. C: 词向量,大小为|V|*m的矩阵
  40. H: 隐藏层的weight
  41. W: 输入层到输出层的weight
  42. d: 隐藏层的bias
  43. U: 输出层的weight
  44. b: 输出层的bias
  45. 1. 首先将输入的 n-1 个单词索引转为词向量,然后将这 n-1 个词向量进行 concat,形成一个 (n-1)*w 的向量,用 X 表示
  46. 2. 将 X 送入隐藏层进行计算,hidden = tanh(d + X * H)
  47. 3. 输出层共有|V|个节点,每个节点yi表示预测下一个单词i的概率,y的计算公式为y = b + X * W + hidden * U
  48. n_step: 文中用n_step个词预测下一个词,在本程序中其值为2
  49. n_hidden: 隐藏层(中间那一层)神经元的数量
  50. m: 词向量的维度
  51. """
  52. super(NNLM, self).__init__()
  53. self.C = nn.Embedding(n_class, m) # 词向量随机赋值,代替了先使用one-hot,然后使用matrix C映射到词向量这一步
  54. self.H = nn.Parameter(torch.randn(n_step * m, n_hidden).type(dtype))
  55. self.W = nn.Parameter(torch.randn(n_step * m, n_class).type(dtype))
  56. self.d = nn.Parameter(torch.randn(n_hidden).type(dtype))
  57. self.U = nn.Parameter(torch.randn(n_hidden, n_class).type(dtype))
  58. self.b = nn.Parameter(torch.randn(n_class).type(dtype))
  59. def forward(self, X):
  60. """
  61. X: [batch_size, n_step]
  62. """
  63. X = self.C(X) # [batch_size, n_step] => [batch_size, n_step, m]
  64. X = X.view(-1, n_step * m) # [batch_size, n_step * m]
  65. hidden_out = torch.tanh(self.d + torch.mm(X, self.H)) # [batch_size, n_hidden], torch.mm就是矩阵的相乘
  66. output = self.b + torch.mm(X, self.W) + torch.mm(hidden_out, self.U) # [batch_size, n_class]
  67. return output
  68. # 实例化模型,优化器,损失函数
  69. model = NNLM()
  70. criterion = nn.CrossEntropyLoss()
  71. optimizer = optim.Adam(model.parameters(), lr=1e-3)
  72. # train
  73. for epoch in range(5000):
  74. for batch_x, batch_y in loader:
  75. optimizer.zero_grad()
  76. output = model(batch_x)
  77. loss = criterion(output, batch_y)
  78. if (epoch + 1) % 1000 == 0:
  79. print('Epoch:', '%04d' % (epoch + 1), 'cost = ', '{:.6f}'.format(loss))
  80. loss.backward()
  81. optimizer.step()
  82. # Test
  83. predict = model(input_batch).data.max(1, keepdim=True)[1]
  84. # squeeze():对张量的维度进行减少的操作,原来:tensor([[2],[6],[3]]),squeeze()操作后变成tensor([2, 6, 3])
  85. print([sen.split()[:n_step] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/912619?site
推荐阅读
相关标签
  

闽ICP备14008679号