前言
NNLM在NLP中有着举足轻重的地位,该模型将深度学习运用到了NLP中,其副产物词向量更是开创了NLP中预训练模型的先河,此文章使用Pytorch实现了NNLM的模型结构,并用简单的数据进行了模型的训练和测试。
示例代码来自于:https://wmathor.com/index.php/archives/1442/
本文在原博客的基础上进行了一些补充,使其更加通俗易懂。
模型结构分析
NNLM的模型是一个三层的神经网络,如图所示:
NNLM模型的任务是通过句子中的前n-1个词,来预测下一个词(第n个词)是什么,因此模型的输入是前n-1个词的向量化表示,在图中即是最下方的.......。在NNLM中,这种向量化表示就是one-hot编码的形式。文中有一个极其重要的矩阵Matrix C,它存储了输入中每个词的词向量表示,输入的每个词都能在矩阵中找到对应的词向量映射,如下所示:
值得注意的是,其实矩阵C就是两个神经网络层之间的权重W,从到就是矩阵的一个乘法运算,其它每个词的表示也是同样的道理。
我们得到了每个词的词向量表示后,将其拼接在一起,即进行concat操作,形成了一个(n-1)*w的向量,我们用X来表示。然后将X送入隐藏层进行计算,即图中中间的一层神经元,使用tanh作为激活函数:
用公式表达则是:,H和d分别是权重和偏置。
然后到了最后一层输出层,最后一层用于最终的分类,因此使用softmax作为激活函数,该层神经元的数量即是词典的大小|V|,每一个词的输出概率就是预测的词的可能性。这里需要格外注意一点,最后的输出层不仅与隐层相关,还有一条与词向量那一层的连线(图中虚线),类似于残差连接的思想。如果输出层的公式为:
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.导入需要的库
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
dtype = torch.FloatTensor
2.数据预处理
这里只是用了简单的三句话来模拟训练和测试过程,没有使用大规模的语料库,但是原理还是一样的。
# 数据预处理
sentences = ['i like dog', 'i love coffee', 'i hate milk']
word_list = " ".join(sentences).split() # ['i', 'like', 'dog', 'i', 'love', 'coffee', 'i', 'hate', 'milk']
word_list = list(set(word_list)) # 去除重复的单词
word_dict = {w: i for i, w in
enumerate(word_list)} # {'hate': 0, 'dog': 1, 'milk': 2, 'love': 3, 'like': 4, 'i': 5, 'coffee': 6}
number_dict = {i: w for i, w in
enumerate(word_list)} # {0: 'like', 1: 'dog', 2: 'coffee', 3: 'hate', 4: 'i', 5: 'love', 6: 'milk'}
n_class = len(word_dict) # 词典|V|的大小,也是最后分类的类别,这里是7
# NNLM(Neural Network Language Model) Parameter,模型的参数
n_step = len(sentences[0].split()) - 1 # 文中用n_step个词预测下一个词,在本程序中其值为2
n_hidden = 2 # 隐藏层神经元的数量
m = 2 # 词向量的维度
3.实现一个mini-batch迭代器
# 实现一个mini-batch迭代器
def make_batch(sentences):
input_batch = []
target_batch = []
for sen in sentences:
word = sen.split() # ['i', 'like', 'dog']
input = [word_dict[n] for n in word[:-1]] # 列表对应的数字序列,一句话中最后一个词是要用来预测的,不作为输入
target = word_dict[word[-1]] # 每句话的最后一个词作为目标值
input_batch.append(input)
target_batch.append(target)
return input_batch, target_batch # ([[5, 2], [5, 0], [5, 6]], [4, 3, 1])
input_batch, target_batch = make_batch(sentences)
input_batch = torch.LongTensor(input_batch)
target_batch = torch.LongTensor(target_batch)
dataset = Data.TensorDataset(input_batch, target_batch)
loader = Data.DataLoader(dataset=dataset, batch_size=16, shuffle=True)
4.定义模型结构
# 定义模型
class NNLM(nn.Module):
def __init__(self):
"""
C: 词向量,大小为|V|*m的矩阵
H: 隐藏层的weight
W: 输入层到输出层的weight
d: 隐藏层的bias
U: 输出层的weight
b: 输出层的bias
1. 首先将输入的 n-1 个单词索引转为词向量,然后将这 n-1 个词向量进行 concat,形成一个 (n-1)*w 的向量,用 X 表示
2. 将 X 送入隐藏层进行计算,hidden = tanh(d + X * H)
3. 输出层共有|V|个节点,每个节点yi表示预测下一个单词i的概率,y的计算公式为y = b + X * W + hidden * U
n_step: 文中用n_step个词预测下一个词,在本程序中其值为2
n_hidden: 隐藏层(中间那一层)神经元的数量
m: 词向量的维度
"""
super(NNLM, self).__init__()
self.C = nn.Embedding(n_class, m) # 词向量随机赋值,代替了先使用one-hot,然后使用matrix C映射到词向量这一步
self.H = nn.Parameter(torch.randn(n_step * m, n_hidden).type(dtype))
self.W = nn.Parameter(torch.randn(n_step * m, n_class).type(dtype))
self.d = nn.Parameter(torch.randn(n_hidden).type(dtype))
self.U = nn.Parameter(torch.randn(n_hidden, n_class).type(dtype))
self.b = nn.Parameter(torch.randn(n_class).type(dtype))
def forward(self, X):
"""
X: [batch_size, n_step]
"""
X = self.C(X) # [batch_size, n_step] => [batch_size, n_step, m]
X = X.view(-1, n_step * m) # [batch_size, n_step * m]
hidden_out = torch.tanh(self.d + torch.mm(X, self.H)) # [batch_size, n_hidden], torch.mm就是矩阵的相乘
output = self.b + torch.mm(X, self.W) + torch.mm(hidden_out, self.U) # [batch_size, n_class]
return output
5.实例化模型,优化器,损失函数
# 实例化模型,优化器,损失函数
model = NNLM()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
6.训练和测试
# train
for epoch in range(5000):
for batch_x, batch_y in loader:
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost = ', '{:.6f}'.format(loss))
loss.backward()
optimizer.step()
# Test
predict = model(input_batch).data.max(1, keepdim=True)[1]
# squeeze():对张量的维度进行减少的操作,原来:tensor([[2],[6],[3]]),squeeze()操作后变成tensor([2, 6, 3])
print([sen.split()[:n_step] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])
7.测试结果
Epoch: 1000 cost = 0.047851
Epoch: 2000 cost = 0.008136
Epoch: 3000 cost = 0.002618
Epoch: 4000 cost = 0.001130
Epoch: 5000 cost = 0.000564
[['i', 'like'], ['i', 'love'], ['i', 'hate']] -> ['dog', 'coffee', 'milk']
完整代码
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
dtype = torch.FloatTensor
# 数据预处理
sentences = ['i like dog', 'i love coffee', 'i hate milk']
word_list = " ".join(sentences).split() # ['i', 'like', 'dog', 'i', 'love', 'coffee', 'i', 'hate', 'milk']
word_list = list(set(word_list)) # 去除重复的单词
word_dict = {w: i for i, w in
enumerate(word_list)} # {'hate': 0, 'dog': 1, 'milk': 2, 'love': 3, 'like': 4, 'i': 5, 'coffee': 6}
number_dict = {i: w for i, w in
enumerate(word_list)} # {0: 'like', 1: 'dog', 2: 'coffee', 3: 'hate', 4: 'i', 5: 'love', 6: 'milk'}
n_class = len(word_dict) # 词典|V|的大小,也是最后分类的类别,这里是7
# NNLM(Neural Network Language Model) Parameter,模型的参数
n_step = len(sentences[0].split()) - 1 # 文中用n_step个词预测下一个词,在本程序中其值为2
n_hidden = 2 # 隐藏层神经元的数量
m = 2 # 词向量的维度
# 实现一个mini-batch迭代器
def make_batch(sentences):
input_batch = []
target_batch = []
for sen in sentences:
word = sen.split() # ['i', 'like', 'dog']
input = [word_dict[n] for n in word[:-1]] # 列表对应的数字序列,一句话中最后一个词是要用来预测的,不作为输入
target = word_dict[word[-1]] # 每句话的最后一个词作为目标值
input_batch.append(input)
target_batch.append(target)
return input_batch, target_batch # ([[5, 2], [5, 0], [5, 6]], [4, 3, 1])
input_batch, target_batch = make_batch(sentences)
input_batch = torch.LongTensor(input_batch)
target_batch = torch.LongTensor(target_batch)
dataset = Data.TensorDataset(input_batch, target_batch)
loader = Data.DataLoader(dataset=dataset, batch_size=16, shuffle=True)
# 定义模型
class NNLM(nn.Module):
def __init__(self):
"""
C: 词向量,大小为|V|*m的矩阵
H: 隐藏层的weight
W: 输入层到输出层的weight
d: 隐藏层的bias
U: 输出层的weight
b: 输出层的bias
1. 首先将输入的 n-1 个单词索引转为词向量,然后将这 n-1 个词向量进行 concat,形成一个 (n-1)*w 的向量,用 X 表示
2. 将 X 送入隐藏层进行计算,hidden = tanh(d + X * H)
3. 输出层共有|V|个节点,每个节点yi表示预测下一个单词i的概率,y的计算公式为y = b + X * W + hidden * U
n_step: 文中用n_step个词预测下一个词,在本程序中其值为2
n_hidden: 隐藏层(中间那一层)神经元的数量
m: 词向量的维度
"""
super(NNLM, self).__init__()
self.C = nn.Embedding(n_class, m) # 词向量随机赋值,代替了先使用one-hot,然后使用matrix C映射到词向量这一步
self.H = nn.Parameter(torch.randn(n_step * m, n_hidden).type(dtype))
self.W = nn.Parameter(torch.randn(n_step * m, n_class).type(dtype))
self.d = nn.Parameter(torch.randn(n_hidden).type(dtype))
self.U = nn.Parameter(torch.randn(n_hidden, n_class).type(dtype))
self.b = nn.Parameter(torch.randn(n_class).type(dtype))
def forward(self, X):
"""
X: [batch_size, n_step]
"""
X = self.C(X) # [batch_size, n_step] => [batch_size, n_step, m]
X = X.view(-1, n_step * m) # [batch_size, n_step * m]
hidden_out = torch.tanh(self.d + torch.mm(X, self.H)) # [batch_size, n_hidden], torch.mm就是矩阵的相乘
output = self.b + torch.mm(X, self.W) + torch.mm(hidden_out, self.U) # [batch_size, n_class]
return output
# 实例化模型,优化器,损失函数
model = NNLM()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# train
for epoch in range(5000):
for batch_x, batch_y in loader:
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost = ', '{:.6f}'.format(loss))
loss.backward()
optimizer.step()
# Test
predict = model(input_batch).data.max(1, keepdim=True)[1]
# squeeze():对张量的维度进行减少的操作,原来:tensor([[2],[6],[3]]),squeeze()操作后变成tensor([2, 6, 3])
print([sen.split()[:n_step] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])