自然语言处理(14:处理时序数据的层的实现)

发布于:2025-03-29 ⋅ 阅读:(25) ⋅ 点赞:(0)

系列文章目录

第一章 1:同义词词典和基于计数方法语料库预处理

第一章 2:基于计数方法的分布式表示和假设,共现矩阵,向量相似度

第一章 3:基于计数方法的改进以及总结

第二章 1:word2vec

第二章 2:word2vec和CBOW模型的初步实现

第二章 3:CBOW模型的完整实现

第二章 4:CBOW模型的补充和skip-gram模型的理论

第三章 1:word2vec的高速化(CBOW的改进)

第三章 2:word2vec高速化(CBOW的二次改进)

第三章 3:改进版word2vec的学习以及总结

第四章 1:RNN(RNN的前置知识和引入)

第四章 2:RNN(RNN的正式介绍)

第四章 3:RNN的实现

第四章 4:处理时序数据的层的实现

第四章 5:RNNLM的学习和评价



前言

本次我们的目标是使用RNN实现语言模型。目前我们已经实现了 RNN层和整体处理时序数据的Time RNN层,本节将创建几个可以处理时序数据的新层。我们将基于RNN的语言模型称为RNNLM(RNN Language Model,RNN 语言模型)。现在,我们来完成RNNLM。


一、RNNLM的全貌图

首先,我们看一下RNNLM使用的网络。图5-25所示为最简单的 RNNLM的网络,其中左图显示了RNNLM的层结构,右图显示了在时间 轴上展开后的网络。

上图中的第1层是Embedding层,该层将单词ID转化为单词的分布式表示(单词向量)。然后,这个单词向量被输入到RNN层。RNN层向下一层(上方)输出隐藏状态,同时也向下一时刻的RNN层(右侧)输出隐藏状态。RNN层向上方输出的隐藏状态经过Affine层,传给Softmax层。 现在,我们仅考虑正向传播,向上图的神经网络传入具体的数据, 并观察输出结果。这里使用的句子还是我们熟悉的“you say goodbye and i say hello.”,此时 RNNLM 进行的处理如下图所示。

如上图所示,被输入的数据是单词ID列表。首先,我们关注第1个时刻。作为第1个单词,单词ID为0的you被输入。此时,查看Softmax层输出的概率分布,可知say的概率最高,这表明正确预测出了you后面出现的单词为say。当然,这样的正确预测只在有“好的”(学习顺利的)权重时才会发生。 接着,我们关注第2个单词say。此时,Softmax层的输出在goodbye处和hello处概率较高。确实,“you say goodby”和“you say hello”都是很自然的句子(顺便说一下,正确答案是goodbye)。这里需要注意的是, RNN层“记忆”了“you say”这一上下文。更准确地说,RNN将“you say”这一过去的信息保存为了简短的隐藏状态向量。RNN层的工作是将这个信息传送到上方的Affine层和下一时刻的RNN层。 像这样,RNNLM可以“记忆”目前为止输入的单词,并以此为基础预测接下来会出现的单词。RNN层通过从过去到现在继承并传递数据,使得编码和存储过去的信息成为可能

二、使用步骤

之前我们将整体处理时序数据的层实现为了Time RNN层,这里也同 样使用Time Embedding层、Time Affine层等来实现整体处理时序数据的层。一旦创建了这些Time 层,我们的目标神经网络就可以像下图这样实现。

(我们将整体处理含有T个时序数据的层称为“Time 层”。如果 可以实现这些层,通过像组装乐高积木一样组装它们,就可以完成 处理时序数据的网络。)

Time 层的实现很简单。比如,在Time Affine层的情况下,只需要像下图那样,准备T个Affine层分别处理各个时刻的数据即可。

Time Embedding层也一样,在正向传播时准备T个Embedding层, 由各个Embedding层处理各个时刻的数据。 关于Time Affine层和Time Embedding层没有什么特别难的内容, 我们就不再赘述了。需要注意的是,Time Affine层并不是单纯地使用T个Affine层,而是使用矩阵运算实现了高效的整体处理。感兴趣的读者可以参考如下源代码:

class TimeAffine:
    def __init__(self, W, b):
        self.params = [W, b]
        self.grads = [np.zeros_like(W), np.zeros_like(b)]
        self.x = None

    def forward(self, x):
        N, T, D = x.shape
        W, b = self.params

        rx = x.reshape(N*T, -1)
        out = np.dot(rx, W) + b
        self.x = x
        return out.reshape(N, T, -1)

    def backward(self, dout):
        x = self.x
        N, T, D = x.shape
        W, b = self.params

        dout = dout.reshape(N*T, -1)
        rx = x.reshape(N*T, -1)

        db = np.sum(dout, axis=0)
        dW = np.dot(rx.T, dout)
        dx = np.dot(dout, W.T)
        dx = dx.reshape(*x.shape)

        self.grads[0][...] = dW
        self.grads[1][...] = db

        return dx

接下来我们看一下时序版本的Softmax。 我们在Softmax中一并实现损失误差Cross Entropy Error层。这里, 按照下图所示的网络结构实现Time Softmax with Loss层。

上图中的x0、x1等数据表示从下方的层传来的得分(得分是正规化为概率之前的值),t0、t1等数据表示正确解标签。如该图所示,T个 Softmax with Loss 层各自算出损失,然后将它们加在一起取平均,将得到的值作为最终的损失。此处进行的计算可用下式表示

Softmax with Loss层计算mini-batch的平均损失。 具体而言,假设mini-batch有N笔数据,通过先求N笔数据的损失之和, 再除以N,可以得到单笔数据的平均损失。这里也一样,通过取时序数据的 平均,可以求得单笔数据的平均损失作为最终的输出。

差点忘了,代码如下(记得三连加关注哦):

class TimeSoftmaxWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None
        self.ignore_label = -1

    def forward(self, xs, ts):
        N, T, V = xs.shape

        if ts.ndim == 3:  # 在监督标签为one-hot向量的情况下
            ts = ts.argmax(axis=2)

        mask = (ts != self.ignore_label)

        # 按批次大小和时序大小进行整理(reshape)
        xs = xs.reshape(N * T, V)
        ts = ts.reshape(N * T)
        mask = mask.reshape(N * T)

        ys = softmax(xs)
        ls = np.log(ys[np.arange(N * T), ts])
        ls *= mask  # 与ignore_label相应的数据将损失设为0
        loss = -np.sum(ls)
        loss /= mask.sum()

        self.cache = (ts, ys, mask, (N, T, V))
        return loss

    def backward(self, dout=1):
        ts, ys, mask, (N, T, V) = self.cache

        dx = ys
        dx[np.arange(N * T), ts] -= 1
        dx *= dout
        dx /= mask.sum()
        dx *= mask[:, np.newaxis]  # 与ignore_label相应的数据将梯度设为0

        dx = dx.reshape((N, T, V))

        return dx