使用Pytorch简单实现NNLM(Nerual Network Language Model)

前言

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.导入需要的库

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()])

 

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值