numpy手写NLP模型(一)———— NNLM

1. 简介

首先当然就是介绍一下NNLM(Neural Network Language Model),模型功能主要是功能是根据之前的文本去预测当前文本的下一个单词。模型的输入是一段固定长度的文本,这个长度可以自己设定,比如设定为2,意思就是根据前两个单词去预测下一个单词是什么。本文将介绍如何在不同框架的前提下,只使用numpy去手撸一个NNLM模型,总体来说还是比较入门的。

2. 模型原理

其实感觉这些基础的模型要去自己实现的话,最难的部分应该是对模型中数学公式等等的理解和推导,编程只是最后的一个数学公式的实现罢了。现在开始介绍整个模型的原理。
用的最多的NNLM模型结构图

2.1 模型的输入

首先我们得到的输入数据是一个句子,比如是一个列表:

["i like dog", "i love coffee", "i hate milk"]

这时候我们首先要建立one-hot编码的字典。然后得到每个单词的embedding(我这里是从GloVe预训练embedding中抽取了我需要的单词的embedding拿来用,每个单词的维数是50,即m = 50)。模型的输入是两个单词(以n-1 = 2为例,n-1就是作为输入的单词的个数),那么就需要将这两个单词的embedding拼接起来作为网络的输入,直接顺序拼接成一个(n - 1)m * 1的向量就好了,在本文的具体例子中单个输入就是一个(100 * 1)的向量。

2.2 模型的前向传播

前向传播的参数有五个,分别是W, b, U, H, d
在这里插入图片描述
通过这个公式就可以计算出前向传播的结果了,也就是网络初始输入y,当然还要对y进行softmax一下。
在这里插入图片描述

2.3 模型的反向传播

首先确定损失函数,因为模型的计算结果类似于一个多分类问题,所以采用交叉熵损失函数(CrossEntropyLoss)

在这里插入图片描述
p就是真实结果的向量,只有一个位置是1,其他位置都是0,其实上图是单个元素相乘的公式,其实也就是两个向量做内积运算:

有点丑,但是大概是这么个意思
然后可以开始进行求导了。先求W的导数:

在这里插入图片描述
交叉熵损失函数的求导部分的推导请看这里,可以直接得到:
在这里插入图片描述
其中:
在这里插入图片描述
然后又有:
在这里插入图片描述
所以得到:
在这里插入图片描述
同理可有:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
到这里,最麻烦的公式推导环节已经结束了,写代码也就是顺水推舟的事情~

3. 模型的代码实现

模型的代码实现方面的话,主要就是NNLM类的实现和各种参数矩阵的维度。首先解释一下各种变量的含义。
N_STEP:输入包含单词的个数
N_HIDDEN:网络隐藏层的维度
N_DIMENSION:每个单词embedding的维度
N_CLASS:总共单词的个数
lr:网络的学习率

然后再明确一下各个参数矩阵的维度:
W:(N_CLASS, N_STEP * N_DIMENSION)
b:(N_CLASS, 1)
H:(N_HIDDEN, N_STEP * N_DIMENSION)
d:(N_HIDDEN, 1)
U:(N_CLASS, N_HIDDEN)

首先我们来看网络的初始化部分:

    def __init__(self):
        self.X = np.random.random((N_STEP * N_DIMENSION, 1))
        self.C = np.random.random((N_CLASS, N_DIMENSION))
        self.W = np.random.random((N_CLASS, N_STEP * N_DIMENSION))
        self.b = np.random.random((N_CLASS, 1))
        self.H = np.random.random((N_HIDDEN, N_STEP * N_DIMENSION))
        self.d = np.random.random((N_HIDDEN, 1))
        self.U = np.random.random((N_CLASS, N_HIDDEN))
        self.tanh = np.random.random((N_HIDDEN, 1))
        self.loss = np.random.random((1, 1))

        # y是神经元输出结果
        self.y = np.random.random((N_CLASS, 1))
        # S是softmax后的结果
        self.S = np.random.random((N_CLASS, 1))

然后就是计算交叉熵loss的函数:

    def cal_loss(self, target, predict):
        predict = np.log(predict
        return -np.dot(target.T, predict)

接下来是前向传播,也就是前面前向传播公式的计算:

    def forward(self, x):
        self.X = x
        output = np.dot(self.W, x) + self.b
        tanh = np.tanh(np.dot(self.H, x) + self.d)
        self.tanh = tanh
        output += np.dot(self.U, tanh)
        self.y = output
        output = softmax(output)
        self.S = output
        return output

接下来是反向传播的部分,这里其实就是对前面数学公式的实现罢了,看懂了公式的话这里应该还是很好懂的。

    def backward(self, output, lr):
        self.loss = self.cal_loss(output, self.S)

        dLoss_y = self.S - output
        dLoss_W = np.dot(dLoss_y, self.X.T)
        dLoss_b = dLoss_y
        dLoss_U = np.dot(dLoss_y, self.tanh.T)

        dLoss_H = np.multiply(np.dot(self.U.T, dLoss_y), d_tanh(np.dot(self.H, self.X) + self.d))

        # 在这里赋值是为了避免重复运算dLoss_d的值
        dLoss_d = dLoss_H

        dLoss_H = np.dot(dLoss_H, self.X.T)

        self.b -= lr * dLoss_b
        self.d -= lr * dLoss_d
        self.W -= lr * dLoss_W
        self.H -= lr * dLoss_H
        self.U -= lr * dLoss_U

模型部分的代码已经写好了,剩下要做的就是用一个例子去验证一下。这里只使用了前面提到的比较小的样本去测试:

["i like dog", "i love coffee", "i hate milk"]

接下来就是网络的训练:

model = NNLM()
input_batch, target_batch = make_batch(sentences)

# 升维去符合网络里面的计算要求
input_batch = input_batch[:, :, np.newaxis]
target_batch = target_batch[:, :, np.newaxis]

for epoch in range(1000):
    i = 0
    for input in input_batch:
        predict = model.forward(input)
        target = target_batch[i]
        model.backward(target, lr)
        i += 1
    if (epoch + 1) % 5 == 0:
        print('Epoch: ', '%04d' % (epoch + 1), ', Loss: ', model.loss[0][0])

index = 2
test_in = input_batch[index]
print(model.forward(test_in))
print(target_batch[index])

让我们来看看效果,这是随便取了一个用例,softmax后的结果如下:

[[2.01570866e-05]
 [2.01621988e-04]
 [3.38259206e-05]
 [2.03724001e-03]
 [1.89600455e-03]
 [9.95763374e-01]
 [4.77761933e-05]]

这是正确的结果:

[[0.]
 [0.]
 [0.]
 [0.]
 [0.]
 [1.]
 [0.]]

所以再小样本上暂时还是可以的,但是其实也还不能说明问题,故还需要进一步整理数据进行训练。

最后附上Github代码地址

4. 总结和疑问

①要熟记各种矩阵求导的公式,不然反向传播那一块的计算根本练矩阵的维度都配不齐

②为什么激活函数那一块求导后会出现点乘符号,不然根本无法配齐,这是为什么?
答:因为求导本来就应该是按照每个元素去进行求导,而不应该进行整体求导,最后的结果应该是由按元素求导的值抽取成矩阵形式的,而计算结果是矩阵点乘,最后当然也应该写成矩阵点乘的形式

③后来验证发现,在小样本下随机生成的embedding和我从GloVe里抽取的embedding效果没有区别,是因为样本过于小了吗?
答:好像就是因为样本太小了,embedding的质量可以忽略不计,强行拟合就完事了

④这个输入的one-hot乘以embedding后拼接起来的,那么如果使用随机初始化的embedding,并且把embedding矩阵当做参数来进行反向传播更新,应该怎么做?(不知道这个东西怎么求偏导进行反向传播更新)
答:还是那句话,按照单个元素来进行求导,一切就都会很清楚

⑤交叉熵loss对y的求导部分,比较复杂的地方要记得一个一个元素的计算导数,最后再汇总成结果矩阵,不要总是想着把矩阵看成一个整体去进行整体的计算。
答:你前面几个问题咋没好好想到⑤

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值